Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ rand_pcg = "0.9"
rand_distr = "0.5"
num-traits = "0.2"
uuid = { version = "1.18", features = ["v4", "fast-rng"], default-features = false }
anyhow = "1.0"

# Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an
# actual C extension (the feature disables linking in `libpython`, which is forbidden in Python
Expand Down
1 change: 1 addition & 0 deletions crates/cext/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ nalgebra.workspace = true
hashbrown.workspace = true
rand.workspace = true
rand_pcg.workspace = true
anyhow.workspace = true

[build-dependencies]
cbindgen = "0.29"
Expand Down
96 changes: 96 additions & 0 deletions crates/cext/src/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use anyhow::Error;
use num_complex::Complex64;
use smallvec::smallvec;
use std::ffi::{CString, c_char};

use qiskit_circuit::Qubit;
use qiskit_circuit::bit::{ClassicalRegister, QuantumRegister};
Expand All @@ -23,6 +25,7 @@ use qiskit_circuit::operations::{
};

use crate::circuit::unitary_from_pointer;
use crate::exit_codes::ExitCode;
use crate::pointers::{const_ptr_as_ref, mut_ptr_as_ref};

/// @ingroup QkDag
Expand Down Expand Up @@ -968,3 +971,96 @@ pub unsafe extern "C" fn qk_dag_topological_op_nodes(dag: *const DAGCircuit, out
unsafe { out_order.add(i).write(node.index() as u32) }
}
}

/// @ingroup QkDag
/// Replace a node in a `QkDag` with a subcircuit specfied by another `QkDag`
///
/// @param dag A pointer to the DAG.
/// @param node The node index of the operation to replace with the other `QkDag`. This
/// must be the node index for an operation node in ``dag`` and the qargs and cargs
/// count must match the number of qubits and clbits in `replacement`.
/// @param replacement The other `QkDag` to replace `node` with. This dag must have
/// the same number of qubits as the operation for ``node``. The node
/// bit ordering will be ordering will be handled in order, so `qargs[0]` for
/// `node` will be mapped to `qubits[0]` in `replacement`, `qargs[1]` to
/// `qubits[0]`, etc. The same pattern applies to classical bits too.
Comment on lines +1381 to +1385
Copy link
Member

Choose a reason for hiding this comment

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

I didn't think about it in the meeting, but the reason that the map needs to exist is for nodes that have "extra" wires, like control-flow or classical expressions - those don't appear in qargs or cargs, so they don't have a native order that the user can be determined, and so they can't just build replacement in order.

Also, if there are any Vars in either circuit, then substitute_node_with_dag panics if you pass None in that argument (??).

If we merge this PR without it, we'll likely have to break the API later to add it back in. That's not necessarily a deal-breaker, but it is more than we thought of at the time.

Copy link
Member Author

Choose a reason for hiding this comment

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

I can expose the mapping interfaces as an array of uint32_t with NULL for no mapping (we don't even need a length because it has to match the qargs length). That should work for Vars too (although for vars it's a bit weird to expose that and not have it in C yet) right?

Copy link
Member Author

Choose a reason for hiding this comment

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

As discussed offline we'll look at adding a new variant function like substitute_node_with_dag_reordering that takes the mapping for the control flow use case when we add that to the C API. But we will keep this function as the simpler entrypoint for the common case.

/// @param error A pointer to a pointer with an nul terminated string with an
/// error description. If the function fails a pointer to the string with
/// the error description will be written to this pointer. That pointer
/// needs to be freed with `qk_str_free`. This can be a null pointer in
/// which case the error will not be written out.
///
/// @returns The return code for the operation, ``QkExitCode_Success`` means success and all
/// other values indicate an error.
///
/// # Example
///
/// ```c
/// QkDag *dag = qk_dag_new();
/// QkQuantumRegister *qr = qk_quantum_register_new(1, "my_register");
/// qk_dag_add_quantum_register(dag, qr);
///
/// uint32_t qubit[1] = {0};
/// uint32_t node_to_replace = qk_dag_apply_gate(dag, QkGate_H, qubit, NULL, false);
/// qk_dag_apply_gate(dag, QkGate_S, qubit, NULL, false);
///
/// // Build replacement dag for H
/// QkDag *replacement = qk_dag_new();
/// QkQuantumRegister *replacement_qr = qk_quantum_register_new(1, "other");
/// qk_dag_add_quantum_register(replacement, replacement_qr);
/// double pi_param [1] = {3.14159,};
/// qk_dag_apply_gate(replacement, QkGate_RZ, qubit, pi_param, false);
/// qk_dag_apply_gate(replacement, QkGate_SX, qubit, NULL, false);
/// qk_dag_apply_gate(replacement, QkGate_RZ, qubit, pi_param, false);
///
/// qk_dag_substitute_node_with_dag(dag, node_to_replace, replacement, NULL);
///
/// // Free the replacement dag, register, dag, and register
/// qk_quantum_register_free(replacement_qr);
/// qk_dag_free(replacement);
/// qk_quantum_register_free(qr);
/// qk_dag_free(dag);
/// ```
///
/// # Safety
///
/// Behavior is undefined if ``dag`` and ``replacement`` are not a valid, non-null pointer to a
/// ``QkDag``. ``error`` must be a valid pointer to a ``char`` pointer or ``NULL``.
#[unsafe(no_mangle)]
#[cfg(feature = "cbinding")]
pub unsafe extern "C" fn qk_dag_substitute_node_with_dag(
dag: *mut DAGCircuit,
node: u32,
replacement: *const DAGCircuit,
Copy link
Member

Choose a reason for hiding this comment

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

We maybe don't want to meaningfully change things in this PR since it stems from Rust, but is "borrow" semantics definitely the right choice for replacement? From C it makes things awkward, because we have to manually free each replacement DAG, whereas if it was "move" semantics, then I think most cases would be more natural, and we'd be able to inline qk_dag_copy calls for those that weren't.

Not a big deal, since the Rust code already has the same interface, but seeing it written out again here made me think.

Copy link
Member Author

Choose a reason for hiding this comment

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

I can make it take ownership of the dag and free it as part of the function. It's easy to do if you think that's a better interface for this. I just did it this way because it was how the rust interface worked. But there isn't a reason we can't take ownership with this function.

Copy link
Member

Choose a reason for hiding this comment

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

I'm uncertain. This does feel like it wants to be a "move" to me, but given we never really had that option in Python, I don't really trust my intuition around it yet.

error: *mut *mut c_char,
) -> ExitCode {
// SAFETY: Per documentation, ``dag`` is non-null and valid.
let dag = unsafe { mut_ptr_as_ref(dag) };
let replacement = unsafe { const_ptr_as_ref(replacement) };

match dag.substitute_node_with_dag(NodeIndex::new(node as usize), replacement, None, None, None)
{
Ok(_) => ExitCode::Success,
Err(e) => {
if !error.is_null() {
let err: Error = e.into();
// SAFETY: Per the documentation error is either null or a valid and aligned
// pointer to a pointer of a C string which is safe to write a pointer to the
// Rust heap into.
unsafe {
// Right now we return a backtrace of the error. This at least gives a hint as to
// which pass failed when we have rust errors normalized we can actually have error
// messages which are user facing. But most likely this will be a PyErr and panic
// when trying to extract the string.
*error = CString::new(format!(
"Transpilation failed with this backtrace: {}",
err.backtrace()
))
.unwrap()
.into_raw();
}
}
ExitCode::DagError
}
}
}
2 changes: 2 additions & 0 deletions crates/cext/src/exit_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub enum ExitCode {
TargetInvalidInstKey = 304,
/// Transpilation failed
TranspilerError = 400,
/// QkDag operation error
DagError = 500,
}

impl From<ArithmeticError> for ExitCode {
Expand Down
2 changes: 1 addition & 1 deletion crates/transpiler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ qiskit-quantum-info.workspace = true
thiserror.workspace = true
bytemuck.workspace = true
fixedbitset = "0.5.7"
anyhow = "1.0"
anyhow.workspace = true

[dependencies.uuid]
workspace = true
Expand Down
45 changes: 45 additions & 0 deletions test/c/test_dag.c
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,50 @@ static int test_unitary_gates(void) {
return res;
}

static int test_substitute_node_with_dag(void) {
int res = Ok;
QkDag *dag = qk_dag_new();
QkQuantumRegister *qr = qk_quantum_register_new(1, "my_register");
qk_dag_add_quantum_register(dag, qr);

uint32_t qubit[1] = {0};
uint32_t node_to_replace = qk_dag_apply_gate(dag, QkGate_H, qubit, NULL, false);
qk_dag_apply_gate(dag, QkGate_S, qubit, NULL, false);

// Build replacement dag for H
QkDag *replacement = qk_dag_new();
QkQuantumRegister *replacement_qr = qk_quantum_register_new(1, "other");
qk_dag_add_quantum_register(replacement, replacement_qr);
double pi_param[1] = {
3.14159,
};
qk_dag_apply_gate(replacement, QkGate_RZ, qubit, pi_param, false);
qk_dag_apply_gate(replacement, QkGate_SX, qubit, NULL, false);
qk_dag_apply_gate(replacement, QkGate_RZ, qubit, pi_param, false);
char *error = NULL;
int ret_code = qk_dag_substitute_node_with_dag(dag, node_to_replace, replacement, &error);
if (ret_code != QkExitCode_Success) {
res = EqualityError;
printf("Substitute call failed: %s\n", error);
goto cleanup;
}

if (qk_dag_num_op_nodes(dag) != 4) {
res = EqualityError;
printf("Number of instructions is %zd but expected 4\n", qk_dag_num_op_nodes(dag));
goto cleanup;
}

cleanup:
// Free the replacement dag, register, dag, and register
qk_quantum_register_free(replacement_qr);
qk_dag_free(replacement);
qk_quantum_register_free(qr);
qk_dag_free(dag);

return res;
}

int test_dag(void) {
int num_failed = 0;
num_failed += RUN_TEST(test_empty);
Expand All @@ -520,6 +564,7 @@ int test_dag(void) {
num_failed += RUN_TEST(test_op_node_bits_explicit);
num_failed += RUN_TEST(test_dag_topological_op_nodes);
num_failed += RUN_TEST(test_unitary_gates);
num_failed += RUN_TEST(test_substitute_node_with_dag);

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