Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/cext/cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,4 @@ prefix = "Qk"
"CDelayUnit" = "DelayUnit"
"CNeighbors" = "Neighbors"
"COperationKind" = "OperationKind"
CDagNeighbors = "DagNeighbors"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CDagNeighbors = "DagNeighbors"
"CDagNeighbors" = "DagNeighbors"

technically completely fine as it was before, the suggestion is just for consistency with the others

140 changes: 140 additions & 0 deletions crates/cext/src/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,146 @@ pub unsafe extern "C" fn qk_dag_op_node_kind(dag: *const DAGCircuit, node: u32)
}
}

/// A struct for storing successors and predecessors information
/// retrieved from `qk_dag_successors` and `qk_dag_predecessors`, respectively.
#[repr(C)]
pub struct CDagNeighbors {
/// Array of size `num_neighbors` of node indices.
neighbors: *const u32,
/// The length of the `neighbors` array.
num_neighbors: usize,
}
Comment on lines +711 to +717
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the design discussions that Kevin had been leading was about whether we wanted to do this sort of thing with a generic QkBuf struct that the user allocated.

For this PR, I'm fine having the manual struct like this - we can always add successors_buf variants later if we choose.

Comment on lines +709 to +717
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be good to comment both here and in the built documentation of the struct that it's undefined behaviour to write to either struct field.


/// @ingroup QkDag
/// Retrieve the successors of the specified node.
///
/// The successors array and its length are stored in a `QkDagNeighbors` struct through
/// the `successors` output parameter. Each element in the array is a DAG node index.
/// You must call the `qk_dag_neighbors_clear`
/// function when done to free the memory allocated for the struct.
///
/// @param dag A pointer to the DAG.
/// @param node The node to get the successors of.
/// @param successors A pointer to a `QkDagNeighbors` instance where the successors information will be written.
///
/// # Example
/// ```c
/// QkDag *dag = qk_dag_new();
/// QkQuantumRegister *qr = qk_quantum_register_new(2, "qr");
/// qk_dag_add_quantum_register(dag, qr);
///
/// uint32_t node_cx = qk_dag_apply_gate(dag, QkGate_CX, (uint32_t[]){0, 1}, NULL, false);
///
/// QkDagNeighbors successors;
/// qk_dag_successors(dag, node_cx, &successors);
///
/// qk_dag_neighbors_clear(&successors);
/// qk_dag_free(dag);
Comment on lines +733 to +743
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this example doesn't free the qr register (add_quantum_register and the CircuitData counterpart unfortunately have "clone" semantics, not "move" semantics).

/// ```
///
/// # Safety
///
/// Behavior is undefined if ``dag`` is not a valid, non-null pointer to a ``QkDag``.
#[unsafe(no_mangle)]
#[cfg(feature = "cbinding")]
pub unsafe extern "C" fn qk_dag_successors(
dag: *const DAGCircuit,
node: u32,
successors: *mut CDagNeighbors,
Comment on lines +748 to +754
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we continue to use an out param, then there are safety requirements on CDagNeighbors too, and we should document that all the data in it is overwritten without being freed. (Though personally I'd swap to using a return value.)

) {
// SAFETY: Per documentation, the pointers are to valid data.
let dag = unsafe { const_ptr_as_ref(dag) };
let dag_neighbors = unsafe { mut_ptr_as_ref(successors) };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dag_neighbors reference is actually UB because the intended call pattern

struct QkDagNeighbors neighbors;
qk_dag_successors(dag, node, &neighbors);

passes neighbors with uninitialised data within. It's UB in Rust to hold uninitialised data in a value or referred to like this.

If we keep the out param, the correct way to do this is not to produce a reference at all, but instead to use directly pointer access to write into the fields, like

// SAFETY: per documentation, `successors` points to an aligned and writeable `CDagNeighbors`.
unsafe {
    (&raw mut (*successors).nodes).write(...);
    (&raw mut (*successors).num_neighbors).write(...);
}


let successors: Vec<u32> = dag
.successors(NodeIndex::new(node as usize))
.map(|node| node.index() as u32)
.collect();
dag_neighbors.num_neighbors = successors.len();
dag_neighbors.neighbors = Box::into_raw(successors.into_boxed_slice()) as *const u32;
Comment on lines +760 to +765
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can collect directly into a Box<[u32]> if you prefer.

}
Comment on lines +749 to +766
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The primary reason for accepting a buffer-like object as an out parameter is to allow the caller to re-use the same allocation space across multiple calls. The actual function call, though, always overwrites the allocation (and we don't store enough information to avoid that), which requires the user to manually call qk_dag_neighbors_clear after every call already.

Given that, would just returning the struct directly from the function be an easier API than having an out param? It reduces the necessary boilerplate of the call site by a line - the current API is

struct QkDagNeighbors neighbors;
qk_dag_successors(dag, node, &neighbors);
qk_dag_neighbors_clear(&neighbors);

whereas if we don't use an out param (and this function doesn't and needn't use the return value already), then we can just have

struct QkDagNeighbors neighbors = qk_dag_successors(dag, node);
qk_dag_neighbors_clear(&neighbors);


/// @ingroup QkDag
/// Retrieve the predecessors of the specified node.
///
/// The predecessors array and its length are returned in a `QkDagNeighbors` struct through
/// the `predecessors` output parameter. Each element in the array is a DAG node index.
/// You must call the `qk_dag_neighbors_clear`
/// function when done to free the memory allocated for the struct.
///
/// @param dag A pointer to the DAG.
/// @param node The node to get the predecessors of.
/// @param predecessors A pointer to a `QkDagNeighbors` instance where the predecessors information will be written.
///
/// # Example
/// ```c
/// QkDag *dag = qk_dag_new();
/// QkQuantumRegister *qr = qk_quantum_register_new(2, "qr");
/// qk_dag_add_quantum_register(dag, qr);
///
/// uint32_t node_cx = qk_dag_apply_gate(dag, QkGate_CX, (uint32_t[]){0, 1}, NULL, false);
///
/// QkDagNeighbors predecessors;
/// qk_dag_predecessors(dag, node_cx, &predecessors);
///
/// qk_dag_neighbors_clear(&predecessors);
/// qk_dag_free(dag);
/// ```
///
/// # Safety
///
/// Behavior is undefined if ``dag`` is not a valid, non-null pointer to a ``QkDag``.
#[unsafe(no_mangle)]
#[cfg(feature = "cbinding")]
pub unsafe extern "C" fn qk_dag_predecessors(
dag: *const DAGCircuit,
node: u32,
predecessors: *mut CDagNeighbors,
) {
// SAFETY: Per documentation, the pointers are to valid data.
let dag = unsafe { const_ptr_as_ref(dag) };
let dag_neighbors = unsafe { mut_ptr_as_ref(predecessors) };

let predecessors: Vec<u32> = dag
.predecessors(NodeIndex::new(node as usize))
.map(|node| node.index() as u32)
.collect();
dag_neighbors.num_neighbors = predecessors.len();
dag_neighbors.neighbors = Box::into_raw(predecessors.into_boxed_slice()) as *const u32;
}

/// @ingroup QkDag
/// Clear the fields of the input `QkDagNeighbors` struct.
///
/// The function deallocates the memory pointed to by the `neighbors` field and sets it to NULL.
/// It also sets the `num_neighbors` field to 0.
///
/// @param neighbors A pointer to a `QkDagNeighbors` object.
///
/// # Safety
///
/// Behavior is undefined if ``neighbors`` is not a valid, non-null pointer to a QkDagNeighbors
/// object populated with either ``qk_dag_successors`` or ``qk_dag_predecessors``.
#[unsafe(no_mangle)]
#[cfg(feature = "cbinding")]
pub unsafe extern "C" fn qk_dag_neighbors_clear(neighbors: *mut CDagNeighbors) {
// SAFETY: Per documentation, the pointer is to a valid data.
let neighbors = unsafe { mut_ptr_as_ref(neighbors) };

if neighbors.num_neighbors > 0 {
let slice = std::ptr::slice_from_raw_parts_mut(
neighbors.neighbors as *mut u32,
neighbors.num_neighbors,
);
unsafe {
let _ = Box::from_raw(slice);
}
}

neighbors.num_neighbors = 0;
neighbors.neighbors = std::ptr::null();
}

/// @ingroup QkDag
/// Free the DAG.
///
Expand Down
24 changes: 16 additions & 8 deletions crates/circuit/src/dag_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3404,11 +3404,10 @@ impl DAGCircuit {

/// Returns iterator of the successors of a node as :class:`.DAGOpNode`\ s and
/// :class:`.DAGOutNode`\ s.
fn successors(&self, py: Python, node: &DAGNode) -> PyResult<Py<PyIterator>> {
#[pyo3(name = "successors")]
fn py_successors(&self, py: Python, node: &DAGNode) -> PyResult<Py<PyIterator>> {
let successors: PyResult<Vec<_>> = self
.dag
.neighbors_directed(node.node.unwrap(), Outgoing)
.unique()
.successors(node.node.unwrap())
.map(|i| self.get_node(py, i))
.collect();
Ok(PyTuple::new(py, successors?)?
Expand All @@ -3420,11 +3419,10 @@ impl DAGCircuit {

/// Returns iterator of the predecessors of a node as :class:`.DAGOpNode`\ s and
/// :class:`.DAGInNode`\ s.
fn predecessors(&self, py: Python, node: &DAGNode) -> PyResult<Py<PyIterator>> {
#[pyo3(name = "predecessors")]
fn py_predecessors(&self, py: Python, node: &DAGNode) -> PyResult<Py<PyIterator>> {
let predecessors: PyResult<Vec<_>> = self
.dag
.neighbors_directed(node.node.unwrap(), Incoming)
.unique()
.predecessors(node.node.unwrap())
.map(|i| self.get_node(py, i))
.collect();
Ok(PyTuple::new(py, predecessors?)?
Expand Down Expand Up @@ -7769,6 +7767,16 @@ impl DAGCircuit {
pub fn get_metadata(&self) -> Option<&Py<PyAny>> {
self.metadata.as_ref()
}

// Returns an iterator over the unique successors of the given node
pub fn successors(&self, node: NodeIndex) -> impl Iterator<Item = NodeIndex> {
self.dag.neighbors_directed(node, Outgoing).unique()
}

// Returns an iterator over the unique predecessors of the given node
Comment on lines +7771 to +7776
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Returns an iterator over the unique successors of the given node
pub fn successors(&self, node: NodeIndex) -> impl Iterator<Item = NodeIndex> {
self.dag.neighbors_directed(node, Outgoing).unique()
}
// Returns an iterator over the unique predecessors of the given node
/// Returns an iterator over the unique successors of the given node
pub fn successors(&self, node: NodeIndex) -> impl Iterator<Item = NodeIndex> {
self.dag.neighbors_directed(node, Outgoing).unique()
}
/// Returns an iterator over the unique predecessors of the given node

pub fn predecessors(&self, node: NodeIndex) -> impl Iterator<Item = NodeIndex> {
self.dag.neighbors_directed(node, Incoming).unique()
}
}

pub struct DAGCircuitBuilder {
Expand Down
3 changes: 3 additions & 0 deletions docs/cdoc/qk-dag.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Data Types

.. doxygenenum:: QkOperationKind

.. doxygenstruct:: QkDagNeighbors
:members:

Functions
=========

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
features_c:
- |
Added the :c:func:`qk_dag_successors` and :c:func:`qk_dag_predecessors` functions to retrieve
the successors and predecessors of a DAG node, respectively. The neighbors information is returned
by these functions through an output parameter of the newly added :c:struct:`QkDagNeighbors` struct.
The :c:func:`qk_dag_neighbors_clear` function was also added to free the memory and reset the fields
of :c:struct:`QkDagNeighbors`.
Comment on lines +5 to +8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we swap away from an out param, we'd need to update this.

72 changes: 72 additions & 0 deletions test/c/test_dag.c
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,77 @@ static int test_dag_endpoint_node_value(void) {
return result;
}

/*
* Test qk_dag_successors and qk_dag_predecessors
*/
static int test_dag_node_neighbors(void) {
int result = Ok;
QkDag *dag = qk_dag_new();
QkQuantumRegister *qr = qk_quantum_register_new(3, "qr");
qk_dag_add_quantum_register(dag, qr);
qk_quantum_register_free(qr);

uint32_t node_h = qk_dag_apply_gate(dag, QkGate_H, (uint32_t[]){0}, NULL, false);
uint32_t node_ccx = qk_dag_apply_gate(dag, QkGate_CCX, (uint32_t[]){0, 1, 2}, NULL, false);
uint32_t node_cx = qk_dag_apply_gate(dag, QkGate_CX, (uint32_t[]){1, 2}, NULL, false);
Comment on lines +325 to +327
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I didn't realise that the (uint32_t[]){1, 2} form compiled on Windows. That's very good information to know.


QkDagNeighbors successors, predecessors;

// H node
qk_dag_successors(dag, node_h, &successors);
qk_dag_predecessors(dag, node_h, &predecessors);
if (successors.num_neighbors != 1 || successors.neighbors[0] != node_ccx ||
predecessors.num_neighbors != 1 ||
qk_dag_node_type(dag, predecessors.neighbors[0]) != QkDagNodeType_QubitIn) {
printf("Incorrect neighbors information for the H node!\n");
result = EqualityError;
goto cleanup;
}
qk_dag_neighbors_clear(&successors);
qk_dag_neighbors_clear(&predecessors);

if (successors.neighbors != NULL || successors.num_neighbors != 0) {
printf("qk_dag_neighbors_clear didn't work!\n");
result = RuntimeError;
goto cleanup;
}

// CCX node
qk_dag_successors(dag, node_ccx, &successors);
qk_dag_predecessors(dag, node_ccx, &predecessors);
if (successors.num_neighbors != 2 || // CX is counted as a unique successor
successors.neighbors[0] != node_cx ||
qk_dag_node_type(dag, successors.neighbors[1]) != QkDagNodeType_QubitOut ||
predecessors.num_neighbors != 3 ||
qk_dag_node_type(dag, predecessors.neighbors[0]) != QkDagNodeType_QubitIn ||
qk_dag_node_type(dag, predecessors.neighbors[1]) != QkDagNodeType_QubitIn ||
predecessors.neighbors[2] != node_h) {
printf("Incorrect neighbors information for the CCX node!\n");
result = EqualityError;
goto cleanup;
}
qk_dag_neighbors_clear(&successors);
qk_dag_neighbors_clear(&predecessors);

// CX node
qk_dag_successors(dag, node_cx, &successors);
qk_dag_predecessors(dag, node_cx, &predecessors);
if (successors.num_neighbors != 2 ||
qk_dag_node_type(dag, successors.neighbors[0]) != QkDagNodeType_QubitOut ||
qk_dag_node_type(dag, successors.neighbors[1]) != QkDagNodeType_QubitOut ||
predecessors.num_neighbors != 1 || // CCX is counted as a unique predecessor
predecessors.neighbors[0] != node_ccx) {
printf("Incorrect neighbors information for the CX node!\n");
result = EqualityError;
}

cleanup:
qk_dag_neighbors_clear(&successors);
qk_dag_neighbors_clear(&predecessors);
qk_dag_free(dag);
return result;
}

int test_dag(void) {
int num_failed = 0;
num_failed += RUN_TEST(test_empty);
Expand All @@ -321,6 +392,7 @@ int test_dag(void) {
num_failed += RUN_TEST(test_dag_node_type);
num_failed += RUN_TEST(test_dag_endpoint_node_value);
num_failed += RUN_TEST(test_op_node_bits_explicit);
num_failed += RUN_TEST(test_dag_node_neighbors);

fflush(stderr);
fprintf(stderr, "=== Number of failed subtests: %i\n", num_failed);
Expand Down