-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Add C API for two qubit unitary peephole optimization pass #16088
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4d160bc
decee9a
746758f
ee70e65
4d4df68
cb6b70f
f06a070
90b16e8
a175ee8
af0c144
79a46c5
839b4c9
d9399a6
b4c4360
aefdc90
746b953
f2bb1fb
55b05c0
7756d1e
6a01332
d83562b
5f73b93
5e6c4cb
8c7e67c
f41e855
3cdee4f
fd96145
89931a4
19368bb
14e421c
02785e1
c7ba671
089d3b6
9f820c1
bfdfe46
f72df7d
bb31eb5
1985ff4
ed262ac
817c2b1
24b3e44
dbbcb37
439e1eb
b24088a
117b793
3b92602
0374185
3fce976
8acceee
a64f6c0
785a02b
437e900
2f682d5
79f2466
1cdaa61
4503c14
a7794ff
8bcc573
4ecb01e
76b40ab
76f5935
2d40a18
b88de4a
ce3dddf
158725f
a8a3fe7
a1643a8
8c58c3a
dfbe65d
9356125
9e3a651
500a964
a8dce96
2784217
469c06b
b9ce054
12b14ee
f80db21
8c03af3
819110d
56ea5aa
31f616f
d91d1da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,324 @@ | ||||||
| // This code is part of Qiskit. | ||||||
| // | ||||||
| // (C) Copyright IBM 2026 | ||||||
| // | ||||||
| // This code is licensed under the Apache License, Version 2.0. You may | ||||||
| // obtain a copy of this license in the LICENSE.txt file in the root directory | ||||||
| // of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. | ||||||
| // | ||||||
| // Any modifications or derivative works of this code must retain this | ||||||
| // copyright notice, and modified files need to carry a notice indicating | ||||||
| // that they have been altered from the originals. | ||||||
|
|
||||||
| use crate::pointers::{const_ptr_as_ref, mut_ptr_as_ref}; | ||||||
|
|
||||||
| use qiskit_circuit::circuit_data::CircuitData; | ||||||
| use qiskit_circuit::dag_circuit::DAGCircuit; | ||||||
| use qiskit_transpiler::passes::two_qubit_unitary_peephole_optimize; | ||||||
| use qiskit_transpiler::target::Target; | ||||||
|
|
||||||
| /// @ingroup QkTranspilerPassesStandalone | ||||||
| /// Run the TwoQubitPeepholeOptimization transpiler pass. | ||||||
| /// | ||||||
| /// This transpiler pass is designed to perform two qubit unitary peephole | ||||||
| /// optimization. This pass finds all the 2 qubit blocks in the circuit, | ||||||
| /// computes the unitary of that block, and then synthesizes that unitary. | ||||||
| /// If the synthesized two qubit unitary is "better" than the original | ||||||
| /// subcircuit that subcircuit is used to replace the original. The heuristic | ||||||
| /// used to determine if it's better first looks at the two qubit gate count | ||||||
| /// in the circuit, and prefers the synthesis with fewer two qubit gates, if | ||||||
| /// the two qubit gate counts are the same then it looks at the estimated | ||||||
| /// fidelity of the circuit and picks the subcircuit with higher estimated | ||||||
| /// fidelity, and finally if needed it picks the subcircuit with the fewest | ||||||
| /// total gates. | ||||||
| /// | ||||||
| /// In case the target is overcomplete the pass will try all the | ||||||
| /// decomposers supported for all the gates supported on a given qubit. | ||||||
| /// The decomposition that has the best expected performance using the above | ||||||
| /// heuristic will be selected and used to replace the block. | ||||||
| /// | ||||||
| /// This pass is designed to be run on a physical circuit and the details of | ||||||
| /// operations on a given qubit is assumed to be the hardware qubit from the | ||||||
| /// target. However, the output of the pass might not use hardware operations, | ||||||
| /// specifically single qubit gates might be emitted outside the target's supported | ||||||
| /// operations, typically only if a parameterized gate supported by the | ||||||
| /// :class:`.TwoQubitControlledUDecomposer` is used for synthesis. As such if running | ||||||
| /// this pass in a physical optimization stage (such as :ref:`transpiler-preset-stage-optimization`) | ||||||
| /// this should be paired with passes such as :class:`.BasisTranslator` and/or | ||||||
| /// :class:`.Optimize1qGatesDecomposition` to ensure that these errant single qubit | ||||||
| /// gates are replaced with hardware supported operations prior to exiting the stage. | ||||||
| /// | ||||||
| /// This pass is multithreaded, and will perform the analysis in parallel | ||||||
| /// and use all the cores available on your local system. You can refer to | ||||||
| /// the `configuration guide <https://docs.quantum.ibm.com/guides/configure-qiskit-local>`__ | ||||||
| /// for details on how to control the threading behavior for Qiskit more broadly | ||||||
| /// which will also control this pass | ||||||
|
Comment on lines
+23
to
+55
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In #15614 we went with the approach of having single source of truth for the general description of a given pass. We used the dag-based passes as such and had the standalone ones point to the dag-based functions for more detail. Should we follow this here as well? |
||||||
| /// | ||||||
| /// @param circuit A pointer to the circuit to run TwoQubitPeepholeOptimization on | ||||||
| /// @param target A pointer to the target to run TwoQubitPeepholeOptimization with | ||||||
| /// @param approximation_degree heuristic dial used for circuit approximation | ||||||
| /// (1.0=no approximation, 0.0=maximal approximation). Approximation can | ||||||
| /// make the synthesized circuit smaller at the cost of straying from | ||||||
| /// the original unitary. If NAN, the target approximation is based on gate fidelities | ||||||
| /// in the ``target``. | ||||||
| /// | ||||||
| /// # Example | ||||||
| /// | ||||||
| /// ```c | ||||||
| /// QkTarget *target = qk_target_new(2); | ||||||
| /// uint32_t current_num_qubits = qk_target_num_qubits(target); | ||||||
| /// QkTargetEntry *cx_entry = qk_target_entry_new(QkGate_CX); | ||||||
| /// for (uint32_t i = 0; i < current_num_qubits - 1; i++) { | ||||||
| /// uint32_t qargs[2] = {i, i + 1}; | ||||||
| /// double inst_error = 0.0090393 * (current_num_qubits - i); | ||||||
| /// double inst_duration = 0.020039; | ||||||
| /// qk_target_entry_add_property(cx_entry, qargs, 2, inst_duration, inst_error); | ||||||
| /// } | ||||||
| /// QkExitCode result_cx = qk_target_add_instruction(target, cx_entry); | ||||||
| /// QkCircuit *qc = qk_circuit_new(2, 0); | ||||||
| /// uint32_t forward[2] = {0, 1}; | ||||||
| /// uint32_t reverse[2] = {1, 0}; | ||||||
| /// for (int i = 0; i < 10; i++) { | ||||||
| /// if (i % 2) { | ||||||
| /// qk_circuit_gate(qc, QkGate_CX, forward, NULL); | ||||||
| /// } else { | ||||||
| /// qk_circuit_gate(QkGate_CX, reverse, NULL); | ||||||
| /// } | ||||||
| /// } | ||||||
| /// qk_transpiler_pass_standalone_two_qubit_peephole_optimization(qc, target, 1.0); | ||||||
| /// ``` | ||||||
| /// | ||||||
| /// # Safety | ||||||
| /// | ||||||
| /// Behavior is undefined if ``circuit`` or ``target`` is not a valid, non-null pointer to a ``QkCircuit`` and ``QkTarget``. | ||||||
| #[unsafe(no_mangle)] | ||||||
| pub unsafe extern "C" fn qk_transpiler_pass_standalone_two_qubit_peephole_optimization( | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A suggestion, for your discretion: replace |
||||||
| circuit: *mut CircuitData, | ||||||
| target: *const Target, | ||||||
| approximation_degree: f64, | ||||||
| ) { | ||||||
| // SAFETY: Per documentation, the pointer is non-null and aligned. | ||||||
| let circuit = unsafe { mut_ptr_as_ref(circuit) }; | ||||||
| let target = unsafe { const_ptr_as_ref(target) }; | ||||||
| let dag = match DAGCircuit::from_circuit_data(circuit, false, None, None, None, None) { | ||||||
| Ok(dag) => dag, | ||||||
| Err(e) => panic!("{}", e), | ||||||
| }; | ||||||
| let approximation = if approximation_degree.is_nan() { | ||||||
| None | ||||||
| } else { | ||||||
| Some(approximation_degree) | ||||||
| }; | ||||||
| let out_dag = match two_qubit_unitary_peephole_optimize(&dag, target.into(), approximation) { | ||||||
| Ok(dag) => dag, | ||||||
| Err(e) => panic!("{}", e), | ||||||
| }; | ||||||
| if let Some(out_dag) = out_dag { | ||||||
| *circuit = CircuitData::from_dag_ref(&out_dag).unwrap(); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /// @ingroup QkTranspilerPasses | ||||||
| /// Run the TwoQubitPeepholeOptimization transpiler pass. | ||||||
| /// | ||||||
| /// This transpiler pass is designed to perform two qubit unitary peephole | ||||||
| /// optimization. This pass finds all the 2 qubit blocks in the circuit, | ||||||
| /// computes the unitary of that block, and then synthesizes that unitary. | ||||||
| /// If the synthesized two qubit unitary is "better" than the original | ||||||
| /// subcircuit that subcircuit is used to replace the original. The heuristic | ||||||
| /// used to determine if it's better first looks at the two qubit gate count | ||||||
| /// in the circuit, and prefers the synthesis with fewer two qubit gates, if | ||||||
| /// the two qubit gate counts are the same then it looks at the estimated | ||||||
| /// fidelity of the circuit and picks the subcircuit with higher estimated | ||||||
| /// fidelity, and finally if needed it picks the subcircuit with the fewest | ||||||
| /// total gates. | ||||||
| /// | ||||||
| /// In case the target is overcomplete the pass will try all the | ||||||
| /// decomposers supported for all the gates supported on a given qubit. | ||||||
| /// The decomposition that has the best expected performance using the above | ||||||
| /// heuristic will be selected and used to replace the block. | ||||||
| /// | ||||||
| /// This pass is designed to be run on a physical circuit and the details of | ||||||
| /// operations on a given qubit is assumed to be the hardware qubit from the | ||||||
| /// target. However, the output of the pass might not use hardware operations, | ||||||
| /// specifically single qubit gates might be emitted outside the target's supported | ||||||
| /// operations, typically only if a parameterized gate supported by the | ||||||
| /// :class:`.TwoQubitControlledUDecomposer` is used for synthesis. As such if running | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Python-side Sphinx crossrefs don't render nicely on the C API pages. Should we just use backticks? |
||||||
| /// this pass in a physical optimization stage (such as :ref:`transpiler-preset-stage-optimization`) | ||||||
| /// this should be paired with passes such as :class:`.BasisTranslator` and/or | ||||||
| /// :class:`.Optimize1qGatesDecomposition` to ensure that these errant single qubit | ||||||
| /// gates are replaced with hardware supported operations prior to exiting the stage. | ||||||
| /// | ||||||
| /// This pass is multithreaded, and will perform the analysis in parallel | ||||||
| /// and use all the cores available on your local system. You can refer to | ||||||
| /// the `configuration guide <https://docs.quantum.ibm.com/guides/configure-qiskit-local>`__ | ||||||
| /// for details on how to control the threading behavior for Qiskit more broadly | ||||||
| /// which will also control this pass | ||||||
| /// | ||||||
| /// @param circuit A pointer to the circuit to run TwoQubitPeepholeOptimization on | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// @param target A pointer to the target to run TwoQubitPeepholeOptimization with | ||||||
| /// @param approximation_degree heuristic dial used for circuit approximation | ||||||
| /// (1.0=no approximation, 0.0=maximal approximation). Approximation can | ||||||
| /// make the synthesized circuit smaller at the cost of straying from | ||||||
| /// the original unitary. If NAN, the target approximation is based on gate fidelities | ||||||
| /// in the ``target``. | ||||||
| /// | ||||||
| /// # Example | ||||||
| /// | ||||||
| /// ```c | ||||||
| /// QkTarget *target = qk_target_new(2); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpicking: we don't need to indent code examples anymore |
||||||
| /// uint32_t current_num_qubits = qk_target_num_qubits(target); | ||||||
| /// QkTargetEntry *cx_entry = qk_target_entry_new(QkGate_CX); | ||||||
| /// for (uint32_t i = 0; i < current_num_qubits - 1; i++) { | ||||||
| /// uint32_t qargs[2] = {i, i + 1}; | ||||||
| /// double inst_error = 0.0090393 * (current_num_qubits - i); | ||||||
| /// double inst_duration = 0.020039; | ||||||
| /// qk_target_entry_add_property(cx_entry, qargs, 2, inst_duration, inst_error); | ||||||
| /// } | ||||||
| /// QkExitCode result_cx = qk_target_add_instruction(target, cx_entry); | ||||||
| /// QkCircuit *qc = qk_circuit_new(2, 0); | ||||||
| /// uint32_t forward[2] = {0, 1}; | ||||||
| /// uint32_t reverse[2] = {1, 0}; | ||||||
| /// for (int i = 0; i < 10; i++) { | ||||||
| /// if (i % 2) { | ||||||
| /// qk_circuit_gate(qc, QkGate_CX, forward, NULL); | ||||||
| /// } else { | ||||||
| /// qk_circuit_gate(QkGate_CX, reverse, NULL); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// } | ||||||
| /// } | ||||||
| /// QkDag *dag = qk_circuit_to_dag(qc); | ||||||
| /// qk_transpiler_pass_two_qubit_peephole_optimization(dag, target, 1.0); | ||||||
| /// ``` | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be nice to add the |
||||||
| /// | ||||||
| /// # Safety | ||||||
| /// | ||||||
| /// Behavior is undefined if ``circuit`` or ``target`` is not a valid, non-null pointer to a ``QkCircuit`` and ``QkTarget``. | ||||||
| #[unsafe(no_mangle)] | ||||||
| pub unsafe extern "C" fn qk_transpiler_pass_two_qubit_peephole_optimization( | ||||||
| dag: *mut DAGCircuit, | ||||||
| target: *const Target, | ||||||
| approximation_degree: f64, | ||||||
| ) { | ||||||
| // SAFETY: Per documentation, the pointer is non-null and aligned. | ||||||
| let dag = unsafe { mut_ptr_as_ref(dag) }; | ||||||
| let target = unsafe { const_ptr_as_ref(target) }; | ||||||
| let approximation = if approximation_degree.is_nan() { | ||||||
| None | ||||||
| } else { | ||||||
| Some(approximation_degree) | ||||||
| }; | ||||||
| let out_dag = match two_qubit_unitary_peephole_optimize(&dag, target.into(), approximation) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpicking: I think we don't need the |
||||||
| Ok(dag) => dag, | ||||||
| Err(e) => panic!("{}", e), | ||||||
| }; | ||||||
| if let Some(out_dag) = out_dag { | ||||||
| *dag = out_dag; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| #[cfg(all(test, not(miri)))] | ||||||
| mod tests { | ||||||
| use super::*; | ||||||
|
|
||||||
| use qiskit_circuit::Qubit; | ||||||
| use qiskit_circuit::bit::ShareableQubit; | ||||||
| use qiskit_circuit::circuit_data::CircuitData; | ||||||
| use qiskit_circuit::instruction::Parameters; | ||||||
| use qiskit_circuit::operations::{ArrayType, Param, StandardGate, UnitaryGate}; | ||||||
| use qiskit_circuit::packed_instruction::PackedOperation; | ||||||
| use qiskit_circuit::parameter::parameter_expression::ParameterExpression; | ||||||
| use qiskit_circuit::parameter::symbol_expr::Symbol; | ||||||
| use smallvec::smallvec; | ||||||
| use std::sync::Arc; | ||||||
|
|
||||||
| #[test] | ||||||
| fn test_pass_peephole() { | ||||||
| let mut qc = CircuitData::new( | ||||||
| Some((0..3).map(|_| ShareableQubit::new_anonymous()).collect()), | ||||||
| None, | ||||||
| (0.).into(), | ||||||
| ) | ||||||
| .unwrap(); | ||||||
| let array = StandardGate::CZ.matrix(&[]).unwrap(); | ||||||
| let gate = UnitaryGate { | ||||||
| array: ArrayType::NDArray(array), | ||||||
| }; | ||||||
| let operation = PackedOperation::from_unitary(Box::new(gate)); | ||||||
| qc.push_packed_operation(operation, None, &[Qubit(0), Qubit(1)], &[]) | ||||||
| .unwrap(); | ||||||
| let array = StandardGate::DCX.matrix(&[]).unwrap(); | ||||||
| let gate = UnitaryGate { | ||||||
| array: ArrayType::NDArray(array), | ||||||
| }; | ||||||
| let operation = PackedOperation::from_unitary(Box::new(gate)); | ||||||
| qc.push_packed_operation(operation, None, &[Qubit(1), Qubit(2)], &[]) | ||||||
| .unwrap(); | ||||||
| let array = StandardGate::DCX.matrix(&[]).unwrap(); | ||||||
| let gate = UnitaryGate { | ||||||
| array: ArrayType::NDArray(array), | ||||||
| }; | ||||||
| let operation = PackedOperation::from_unitary(Box::new(gate)); | ||||||
| qc.push_packed_operation(operation, None, &[Qubit(1), Qubit(2)], &[]) | ||||||
| .unwrap(); | ||||||
| let array = StandardGate::Tdg.matrix(&[]).unwrap(); | ||||||
| let gate = UnitaryGate { | ||||||
| array: ArrayType::NDArray(array), | ||||||
| }; | ||||||
| let operation = PackedOperation::from_unitary(Box::new(gate)); | ||||||
| qc.push_packed_operation(operation, None, &[Qubit(1)], &[]) | ||||||
| .unwrap(); | ||||||
| let array = StandardGate::CY.matrix(&[]).unwrap(); | ||||||
| let gate = UnitaryGate { | ||||||
| array: ArrayType::NDArray(array), | ||||||
| }; | ||||||
| let operation = PackedOperation::from_unitary(Box::new(gate)); | ||||||
| qc.push_packed_operation(operation, None, &[Qubit(0), Qubit(2)], &[]) | ||||||
| .unwrap(); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps also add to the circuit some parametrized gate as well? |
||||||
|
|
||||||
| let mut target = Target::new( | ||||||
| Some("Fake Target".to_string()), | ||||||
| Some(3), // num_qubits | ||||||
| None, // dt | ||||||
| None, // granularity | ||||||
| None, // min_length | ||||||
| None, // pulse_alignment | ||||||
| None, // acquire_alignment | ||||||
| None, // qubit_properties | ||||||
| None, // concurrent_measurements | ||||||
| ) | ||||||
| .unwrap(); | ||||||
| let params = Some(Parameters::Params(smallvec![ | ||||||
| Param::ParameterExpression(Arc::new(ParameterExpression::from_symbol(Symbol::new( | ||||||
| "ϴ", None, None, | ||||||
| )))), | ||||||
| Param::ParameterExpression(Arc::new(ParameterExpression::from_symbol(Symbol::new( | ||||||
| "φ", None, None, | ||||||
| )))), | ||||||
| Param::ParameterExpression(Arc::new(ParameterExpression::from_symbol(Symbol::new( | ||||||
| "λ", None, None, | ||||||
| )))), | ||||||
| ])); | ||||||
| target | ||||||
| .add_instruction( | ||||||
| PackedOperation::from_standard_gate(StandardGate::U), | ||||||
| params, | ||||||
| None, | ||||||
| None, | ||||||
| ) | ||||||
| .unwrap(); | ||||||
| target | ||||||
| .add_instruction( | ||||||
| PackedOperation::from_standard_gate(StandardGate::CX), | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps also test also a parametrized 2-qubit gate in the target? |
||||||
| None, | ||||||
| None, | ||||||
| None, | ||||||
| ) | ||||||
| .unwrap(); | ||||||
| unsafe { | ||||||
| qk_transpiler_pass_standalone_two_qubit_peephole_optimization(&mut qc, &target, 1.0); | ||||||
| }; | ||||||
| let mut gate_names = qc.count_ops().keys().copied().collect::<Vec<_>>(); | ||||||
| gate_names.sort(); | ||||||
| assert_eq!(gate_names, vec!["cx", "u"]); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any particular reason why we test this here rather than in `test_two_qubit_peephole.c'? This doesn't seem like something we can't cover in the C API testing. |
||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we must have the same documentation twice for both functions? the standalone and the other one?
it's quite lengthy...