Skip to content

Commit a776456

Browse files
mtreinishraynelfss
andauthored
Add angle bound support to target constraints (Qiskit#14406)
* Add angle bound support to target constraints This commit adds a new capability to the target for representing angle bounds on a gate in the target and a transpiler pass to enforce the bounds are respected. To enable this functionality it requires that the target constructor provides a function to do the angle wrapping/folding so that the transpiler knows how to do the transformation of arbitrary gate appropriately. Since the transpiler can't make any assumption about the bounds or the angles in use, or the gates that will have bounds applied. So coming up with a general transpiler pass is not possible, however a user providing the conversion function when applying a bound makes it possible for the preset pass maangers to reason about gates with angle bounds and work with them. This new feature is strictly additive to the Target which keeps it backwards compatible. It does mean that for existing workflows a user will need to opt-in to checking the angle bounds. Fixes Qiskit#13784 * Fix lint * Add missing pickle of angle bounds * Fix fold rzz function and fix failing test * Add in bounds angle to workflow test * Missing imports from release note example * Handle no-target in translation generation * Fix release note example * Fix gitignore * Pivot to standalone registry This commit pivots the user interface to the wrapping callbacks. Instead of putting it all in the target this adds a separate data structure analgous to the equivalence library for the basis translator but for WrapAngles. This makes the target solely concerned with the QPU constraints (which is it's purpose) and the backend provider is responsible for populating the global registry instance to make the pass work. * Change default on check_angle_bounds param * Rename gate_has_angle_bound -> gate_has_angle_bounds * Fix lint * Remove unsound PyCapsule usage Returning a PyCapsule of the function pointer from Rust to pickle isn't going to do anything useful since the pickle is typically used for IPC and the pointer won't be valid in a separate process. This removes that and just raises an error. * Add missing import to release note example * Improve docs This commit expands the target side documentation for angle bounds adding an overview of the data model to the class docstring and adding missing details to the method docstrings. * Fix release note example * Improve WrapAngles pass documentation * Fix docs build failures * Update for review comments * Apply suggestions from code review Co-authored-by: Raynel Sanchez <[email protected]> * Mention doc example isn't a useful transformation * Update qiskit/transpiler/target.py Co-authored-by: Raynel Sanchez <[email protected]> * Update qiskit/transpiler/target.py --------- Co-authored-by: Raynel Sanchez <[email protected]>
1 parent 042561d commit a776456

File tree

23 files changed

+1096
-11
lines changed

23 files changed

+1096
-11
lines changed

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,6 @@ instance/
7777
.scrapy
7878

7979

80-
# PyBuilder
81-
target/
82-
8380
# Jupyter Notebook
8481
.ipynb_checkpoints
8582

crates/pyext/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,7 @@ fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
8282
add_submodule(m, ::qiskit_circuit::converters::converters, "converters")?;
8383
add_submodule(m, ::qiskit_qasm2::qasm2, "qasm2")?;
8484
add_submodule(m, ::qiskit_qasm3::qasm3, "qasm3")?;
85+
add_submodule(m, ::qiskit_transpiler::angle_bound_registry::angle_bound_mod, "angle_bound_registry")?;
86+
add_submodule(m, ::qiskit_transpiler::passes::wrap_angles_mod, "wrap_angles")?;
8587
Ok(())
8688
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2025
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use hashbrown::HashMap;
14+
15+
use pyo3::exceptions::PyKeyError;
16+
use pyo3::prelude::*;
17+
use pyo3::types::PyDict;
18+
use pyo3::Python;
19+
20+
use crate::TranspilerError;
21+
use qiskit_circuit::dag_circuit::DAGCircuit;
22+
use qiskit_circuit::PhysicalQubit;
23+
24+
#[derive(Clone)]
25+
pub(crate) enum CallbackType {
26+
Python(PyObject),
27+
Native(fn(&[f64], &[PhysicalQubit]) -> DAGCircuit),
28+
}
29+
30+
impl CallbackType {
31+
fn call(&self, angles: &[f64], qubits: &[PhysicalQubit]) -> PyResult<DAGCircuit> {
32+
match self {
33+
Self::Python(inner) => {
34+
let qubits: Vec<usize> = qubits.iter().map(|x| x.index()).collect();
35+
Python::with_gil(|py| inner.bind(py).call1((angles, qubits))?.extract())
36+
}
37+
Self::Native(inner) => Ok(inner(angles, qubits)),
38+
}
39+
}
40+
}
41+
42+
/// Registry of Angle Wrapping function
43+
///
44+
/// This class internally contains a mapping of instruction names from a :class:`.Target` to
45+
/// callbacks for wrapping angles that are outside the specified bounds.
46+
#[pyclass(module = "qiskit._accelerate.angle_bound_registry")]
47+
#[pyo3(name = "WrapAngleRegistry")]
48+
pub struct PyWrapAngleRegistry(WrapAngleRegistry);
49+
50+
#[pymethods]
51+
impl PyWrapAngleRegistry {
52+
#[new]
53+
pub fn new() -> Self {
54+
PyWrapAngleRegistry(WrapAngleRegistry::new())
55+
}
56+
57+
/// Get a replacement circuit for
58+
pub fn substitute_angle_bounds(
59+
&self,
60+
name: &str,
61+
angles: Vec<f64>,
62+
qubits: Vec<PhysicalQubit>,
63+
) -> PyResult<Option<DAGCircuit>> {
64+
self.0.substitute_angle_bounds(name, &angles, &qubits)
65+
}
66+
67+
pub fn add_wrapper(&mut self, name: String, callback: PyObject) {
68+
self.0.registry.insert(name, CallbackType::Python(callback));
69+
}
70+
71+
fn __getstate__(&self, py: Python) -> PyResult<Py<PyDict>> {
72+
let bounds_dict = PyDict::new(py);
73+
for (name, bound) in self.get_inner().registry.iter() {
74+
if let CallbackType::Python(obj) = bound {
75+
bounds_dict.set_item(name, obj.clone_ref(py))?;
76+
} else {
77+
return Err(TranspilerError::new_err(
78+
"Target contains native code bounds callbacks which can't be serialized",
79+
));
80+
}
81+
}
82+
Ok(bounds_dict.unbind())
83+
}
84+
85+
fn __setstate__(&mut self, data: Bound<PyDict>) -> PyResult<()> {
86+
for (key, val) in data.iter() {
87+
let name: String = key.extract()?;
88+
self.add_wrapper(name, val.unbind());
89+
}
90+
Ok(())
91+
}
92+
}
93+
94+
impl PyWrapAngleRegistry {
95+
pub fn get_inner(&self) -> &WrapAngleRegistry {
96+
&self.0
97+
}
98+
}
99+
100+
/// Store the mapping between gate names and callbacks for wrapping that instructions' angles
101+
/// which are outside the specified bounds.
102+
pub struct WrapAngleRegistry {
103+
registry: HashMap<String, CallbackType>,
104+
}
105+
106+
impl WrapAngleRegistry {
107+
pub fn new() -> Self {
108+
WrapAngleRegistry {
109+
registry: HashMap::new(),
110+
}
111+
}
112+
113+
pub fn add_native(
114+
&mut self,
115+
name: String,
116+
callback: fn(&[f64], &[PhysicalQubit]) -> DAGCircuit,
117+
) {
118+
self.registry.insert(name, CallbackType::Native(callback));
119+
}
120+
121+
/// Get a replacement circuit for an instruction outside the specified bounds.
122+
pub fn substitute_angle_bounds(
123+
&self,
124+
name: &str,
125+
angles: &[f64],
126+
qubits: &[PhysicalQubit],
127+
) -> PyResult<Option<DAGCircuit>> {
128+
if let Some(callback) = self.registry.get(name) {
129+
Some(callback.call(angles, qubits)).transpose()
130+
} else {
131+
Err(PyKeyError::new_err("Name: {} not in registry"))
132+
}
133+
}
134+
}
135+
136+
impl Default for WrapAngleRegistry {
137+
fn default() -> Self {
138+
Self::new()
139+
}
140+
}
141+
142+
impl Default for PyWrapAngleRegistry {
143+
fn default() -> Self {
144+
Self::new()
145+
}
146+
}
147+
148+
pub fn angle_bound_mod(m: &Bound<PyModule>) -> PyResult<()> {
149+
m.add_class::<PyWrapAngleRegistry>()?;
150+
Ok(())
151+
}

crates/transpiler/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// copyright notice, and modified files need to carry a notice indicating
1111
// that they have been altered from the originals.
1212

13+
pub mod angle_bound_registry;
1314
pub mod commutation_checker;
1415
pub mod equivalence;
1516
pub mod passes;

crates/transpiler/src/passes/gate_direction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ pub fn fix_direction_target(dag: &mut DAGCircuit, target: &Target) -> PyResult<(
199199
qargs.into(),
200200
None,
201201
Some(inst.params_view().to_vec()),
202+
false,
202203
)
203204
.unwrap_or(false);
204205
}

crates/transpiler/src/passes/gates_in_basis.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use qiskit_circuit::circuit_data::CircuitData;
1616

1717
use crate::target::{Qargs, Target};
1818
use qiskit_circuit::dag_circuit::DAGCircuit;
19-
use qiskit_circuit::operations::Operation;
19+
use qiskit_circuit::operations::{Operation, Param};
2020
use qiskit_circuit::packed_instruction::PackedInstruction;
2121
use qiskit_circuit::PhysicalQubit;
2222
use qiskit_circuit::Qubit;
@@ -39,6 +39,24 @@ pub fn gates_missing_from_target(dag: &DAGCircuit, target: &Target) -> PyResult<
3939
if !target.instruction_supported(gate.op.name(), &qargs_mapped) {
4040
return Ok(true);
4141
}
42+
if target.has_angle_bounds()
43+
&& target.gate_has_angle_bounds(gate.op.name())
44+
&& !gate.is_parameterized()
45+
{
46+
let params: Vec<f64> = gate
47+
.params
48+
.as_ref()
49+
.unwrap()
50+
.iter()
51+
.map(|x| {
52+
let Param::Float(val) = x else { unreachable!() };
53+
*val
54+
})
55+
.collect();
56+
if !target.gate_supported_angle_bound(gate.op.name(), &params) {
57+
return Ok(true);
58+
}
59+
}
4260

4361
if gate.op.control_flow() {
4462
for block in gate.op.blocks() {

crates/transpiler/src/passes/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ mod split_2q_unitaries;
4848
mod unitary_synthesis;
4949
mod unroll_3q_or_more;
5050
mod vf2;
51+
mod wrap_angles;
5152

5253
pub use alap_schedule_analysis::{alap_schedule_analysis_mod, run_alap_schedule_analysis};
5354
pub use apply_layout::{apply_layout, apply_layout_mod, update_layout};
@@ -88,3 +89,4 @@ pub use split_2q_unitaries::{run_split_2q_unitaries, split_2q_unitaries_mod};
8889
pub use unitary_synthesis::{run_unitary_synthesis, unitary_synthesis_mod};
8990
pub use unroll_3q_or_more::{run_unroll_3q_or_more, unroll_3q_or_more_mod};
9091
pub use vf2::{error_map_mod, score_layout, vf2_layout_mod, vf2_layout_pass, ErrorMap};
92+
pub use wrap_angles::{run_wrap_angles, wrap_angles_mod};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2025
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use pyo3::prelude::*;
14+
15+
use rustworkx_core::petgraph::prelude::*;
16+
17+
use crate::angle_bound_registry::{PyWrapAngleRegistry, WrapAngleRegistry};
18+
use crate::target::Target;
19+
use qiskit_circuit::dag_circuit::DAGCircuit;
20+
use qiskit_circuit::operations::{Operation, Param};
21+
use qiskit_circuit::PhysicalQubit;
22+
23+
#[pyfunction]
24+
#[pyo3(name = "wrap_angles")]
25+
pub fn py_run_wrap_angles(
26+
dag: &mut DAGCircuit,
27+
target: &Target,
28+
bounds_registry: &PyWrapAngleRegistry,
29+
) -> PyResult<()> {
30+
run_wrap_angles(dag, target, bounds_registry.get_inner())
31+
}
32+
33+
pub fn run_wrap_angles(
34+
dag: &mut DAGCircuit,
35+
target: &Target,
36+
bounds_registry: &WrapAngleRegistry,
37+
) -> PyResult<()> {
38+
if !target.has_angle_bounds() {
39+
return Ok(());
40+
}
41+
let nodes_to_sub: Vec<NodeIndex> = dag
42+
.op_nodes(false)
43+
.filter_map(|(index, inst)| {
44+
if target.gate_has_angle_bounds(inst.op.name()) && !inst.is_parameterized() {
45+
Some(index)
46+
} else {
47+
None
48+
}
49+
})
50+
.collect();
51+
for node in nodes_to_sub {
52+
let inst = dag[node].unwrap_operation();
53+
let params: Vec<_> = inst
54+
.params_view()
55+
.iter()
56+
.map(|param| {
57+
let Param::Float(param) = param else {
58+
unreachable!()
59+
};
60+
*param
61+
})
62+
.collect();
63+
if !target.gate_supported_angle_bound(inst.op.name(), &params) {
64+
let qargs: Vec<_> = dag
65+
.get_qargs(inst.qubits)
66+
.iter()
67+
.map(|x| PhysicalQubit(x.0))
68+
.collect();
69+
let new_dag =
70+
bounds_registry.substitute_angle_bounds(inst.op.name(), &params, &qargs)?;
71+
if let Some(new_dag) = new_dag {
72+
dag.substitute_node_with_dag(node, &new_dag, None, None, None)?;
73+
}
74+
}
75+
}
76+
Ok(())
77+
}
78+
79+
pub fn wrap_angles_mod(m: &Bound<PyModule>) -> PyResult<()> {
80+
m.add_wrapped(wrap_pyfunction!(py_run_wrap_angles))?;
81+
Ok(())
82+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2025
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use super::errors::TargetError;
14+
use smallvec::SmallVec;
15+
16+
/// Model bounds on angle parameters for a gate
17+
///
18+
/// `None` represents no bound, while `Some([f64; 2])` represents an inclusive bound set
19+
/// on the lower and upper allowed values respectively.
20+
#[derive(Clone, Debug)]
21+
pub(crate) struct AngleBound(SmallVec<[Option<[f64; 2]>; 3]>);
22+
23+
impl AngleBound {
24+
pub fn bounds(&self) -> &[Option<[f64; 2]>] {
25+
&self.0
26+
}
27+
28+
pub fn new(bounds: SmallVec<[Option<[f64; 2]>; 3]>) -> Result<Self, TargetError> {
29+
for [low, high] in bounds.iter().flatten() {
30+
if low >= high {
31+
return Err(TargetError::InvalidBounds {
32+
low: *low,
33+
high: *high,
34+
});
35+
}
36+
}
37+
Ok(Self(bounds))
38+
}
39+
40+
pub fn angles_supported(&self, angles: &[f64]) -> bool {
41+
angles
42+
.iter()
43+
.zip(&self.0)
44+
.all(|(angle, bound)| match bound {
45+
Some([low, high]) => !(angle < low || angle > high),
46+
None => true,
47+
})
48+
}
49+
}

crates/transpiler/src/target/errors.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,7 @@ pub enum TargetError {
4545
/// not operated on by any instruction.
4646
#[error["{0} not in Target."]]
4747
QargsWithoutInstruction(String),
48+
///The specified bounds for the instruction are not valid.
49+
#[error["Lower bound {low} is not less than higher bound {high}."]]
50+
InvalidBounds { low: f64, high: f64 },
4851
}

0 commit comments

Comments
 (0)