Skip to content

Commit 9d19eae

Browse files
authored
Merge branch 'main' into py39-now-and-forever
2 parents 0a992f7 + 86a1b49 commit 9d19eae

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1510
-508
lines changed

crates/accelerate/src/check_map.rs

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2024
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::HashSet;
14+
use pyo3::intern;
15+
use pyo3::prelude::*;
16+
use pyo3::wrap_pyfunction;
17+
18+
use qiskit_circuit::circuit_data::CircuitData;
19+
use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType};
20+
use qiskit_circuit::imports::CIRCUIT_TO_DAG;
21+
use qiskit_circuit::operations::{Operation, OperationRef};
22+
use qiskit_circuit::Qubit;
23+
24+
fn recurse<'py>(
25+
py: Python<'py>,
26+
dag: &'py DAGCircuit,
27+
edge_set: &'py HashSet<[u32; 2]>,
28+
wire_map: Option<&'py [Qubit]>,
29+
) -> PyResult<Option<(String, [u32; 2])>> {
30+
let check_qubits = |qubits: &[Qubit]| -> bool {
31+
match wire_map {
32+
Some(wire_map) => {
33+
let mapped_bits = [
34+
wire_map[qubits[0].0 as usize],
35+
wire_map[qubits[1].0 as usize],
36+
];
37+
edge_set.contains(&[mapped_bits[0].into(), mapped_bits[1].into()])
38+
}
39+
None => edge_set.contains(&[qubits[0].into(), qubits[1].into()]),
40+
}
41+
};
42+
for node in dag.op_nodes(false) {
43+
if let NodeType::Operation(inst) = &dag.dag[node] {
44+
let qubits = dag.get_qargs(inst.qubits);
45+
if inst.op.control_flow() {
46+
if let OperationRef::Instruction(py_inst) = inst.op.view() {
47+
let raw_blocks = py_inst.instruction.getattr(py, "blocks")?;
48+
let circuit_to_dag = CIRCUIT_TO_DAG.get_bound(py);
49+
for raw_block in raw_blocks.bind(py).iter().unwrap() {
50+
let block_obj = raw_block?;
51+
let block = block_obj
52+
.getattr(intern!(py, "_data"))?
53+
.downcast::<CircuitData>()?
54+
.borrow();
55+
let new_dag: DAGCircuit =
56+
circuit_to_dag.call1((block_obj.clone(),))?.extract()?;
57+
let wire_map = (0..block.num_qubits())
58+
.map(|inner| {
59+
let outer = qubits[inner];
60+
match wire_map {
61+
Some(wire_map) => wire_map[outer.0 as usize],
62+
None => outer,
63+
}
64+
})
65+
.collect::<Vec<_>>();
66+
let res = recurse(py, &new_dag, edge_set, Some(&wire_map))?;
67+
if res.is_some() {
68+
return Ok(res);
69+
}
70+
}
71+
}
72+
} else if qubits.len() == 2
73+
&& (dag.calibrations_empty() || !dag.has_calibration_for_index(py, node)?)
74+
&& !check_qubits(qubits)
75+
{
76+
return Ok(Some((
77+
inst.op.name().to_string(),
78+
[qubits[0].0, qubits[1].0],
79+
)));
80+
}
81+
}
82+
}
83+
Ok(None)
84+
}
85+
86+
#[pyfunction]
87+
pub fn check_map(
88+
py: Python,
89+
dag: &DAGCircuit,
90+
edge_set: HashSet<[u32; 2]>,
91+
) -> PyResult<Option<(String, [u32; 2])>> {
92+
recurse(py, dag, &edge_set, None)
93+
}
94+
95+
pub fn check_map_mod(m: &Bound<PyModule>) -> PyResult<()> {
96+
m.add_wrapped(wrap_pyfunction!(check_map))?;
97+
Ok(())
98+
}

crates/accelerate/src/commutation_analysis.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const MAX_NUM_QUBITS: u32 = 3;
5050
/// commutation_set = {0: [[0], [2, 3], [4], [1]]}
5151
/// node_indices = {(0, 0): 0, (1, 0): 3, (2, 0): 1, (3, 0): 1, (4, 0): 2}
5252
///
53-
fn analyze_commutations_inner(
53+
pub(crate) fn analyze_commutations_inner(
5454
py: Python,
5555
dag: &mut DAGCircuit,
5656
commutation_checker: &mut CommutationChecker,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2024
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 std::f64::consts::PI;
14+
15+
use hashbrown::{HashMap, HashSet};
16+
use pyo3::exceptions::PyRuntimeError;
17+
use pyo3::prelude::*;
18+
use pyo3::{pyfunction, pymodule, wrap_pyfunction, Bound, PyResult, Python};
19+
use rustworkx_core::petgraph::stable_graph::NodeIndex;
20+
use smallvec::{smallvec, SmallVec};
21+
22+
use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire};
23+
use qiskit_circuit::operations::StandardGate::{
24+
CXGate, CYGate, CZGate, HGate, PhaseGate, RXGate, RZGate, SGate, TGate, U1Gate, XGate, YGate,
25+
ZGate,
26+
};
27+
use qiskit_circuit::operations::{Operation, Param, StandardGate};
28+
use qiskit_circuit::Qubit;
29+
30+
use crate::commutation_analysis::analyze_commutations_inner;
31+
use crate::commutation_checker::CommutationChecker;
32+
use crate::{euler_one_qubit_decomposer, QiskitError};
33+
34+
const _CUTOFF_PRECISION: f64 = 1e-5;
35+
static ROTATION_GATES: [&str; 4] = ["p", "u1", "rz", "rx"];
36+
static HALF_TURNS: [&str; 2] = ["z", "x"];
37+
static QUARTER_TURNS: [&str; 1] = ["s"];
38+
static EIGHTH_TURNS: [&str; 1] = ["t"];
39+
40+
static VAR_Z_MAP: [(&str, StandardGate); 3] = [("rz", RZGate), ("p", PhaseGate), ("u1", U1Gate)];
41+
static Z_ROTATIONS: [StandardGate; 6] = [PhaseGate, ZGate, U1Gate, RZGate, TGate, SGate];
42+
static X_ROTATIONS: [StandardGate; 2] = [XGate, RXGate];
43+
static SUPPORTED_GATES: [StandardGate; 5] = [CXGate, CYGate, CZGate, HGate, YGate];
44+
45+
#[derive(Hash, Eq, PartialEq, Debug)]
46+
enum GateOrRotation {
47+
Gate(StandardGate),
48+
ZRotation,
49+
XRotation,
50+
}
51+
#[derive(Hash, Eq, PartialEq, Debug)]
52+
struct CancellationSetKey {
53+
gate: GateOrRotation,
54+
qubits: SmallVec<[Qubit; 2]>,
55+
com_set_index: usize,
56+
second_index: Option<usize>,
57+
}
58+
59+
#[pyfunction]
60+
#[pyo3(signature = (dag, commutation_checker, basis_gates=None))]
61+
pub(crate) fn cancel_commutations(
62+
py: Python,
63+
dag: &mut DAGCircuit,
64+
commutation_checker: &mut CommutationChecker,
65+
basis_gates: Option<HashSet<String>>,
66+
) -> PyResult<()> {
67+
let basis: HashSet<String> = if let Some(basis) = basis_gates {
68+
basis
69+
} else {
70+
HashSet::new()
71+
};
72+
let z_var_gate = dag
73+
.get_op_counts()
74+
.keys()
75+
.find_map(|g| {
76+
VAR_Z_MAP
77+
.iter()
78+
.find(|(key, _)| *key == g.as_str())
79+
.map(|(_, gate)| gate)
80+
})
81+
.or_else(|| {
82+
basis.iter().find_map(|g| {
83+
VAR_Z_MAP
84+
.iter()
85+
.find(|(key, _)| *key == g.as_str())
86+
.map(|(_, gate)| gate)
87+
})
88+
});
89+
// Fallback to the first matching key from basis if there is no match in dag.op_names
90+
91+
// Gate sets to be cancelled
92+
/* Traverse each qubit to generate the cancel dictionaries
93+
Cancel dictionaries:
94+
- For 1-qubit gates the key is (gate_type, qubit_id, commutation_set_id),
95+
the value is the list of gates that share the same gate type, qubit, commutation set.
96+
- For 2qbit gates the key: (gate_type, first_qbit, sec_qbit, first commutation_set_id,
97+
sec_commutation_set_id), the value is the list gates that share the same gate type,
98+
qubits and commutation sets.
99+
*/
100+
let (commutation_set, node_indices) = analyze_commutations_inner(py, dag, commutation_checker)?;
101+
let mut cancellation_sets: HashMap<CancellationSetKey, Vec<NodeIndex>> = HashMap::new();
102+
103+
(0..dag.num_qubits() as u32).for_each(|qubit| {
104+
let wire = Qubit(qubit);
105+
if let Some(wire_commutation_set) = commutation_set.get(&Wire::Qubit(wire)) {
106+
for (com_set_idx, com_set) in wire_commutation_set.iter().enumerate() {
107+
if let Some(&nd) = com_set.first() {
108+
if !matches!(dag.dag[nd], NodeType::Operation(_)) {
109+
continue;
110+
}
111+
} else {
112+
continue;
113+
}
114+
for node in com_set.iter() {
115+
let instr = match &dag.dag[*node] {
116+
NodeType::Operation(instr) => instr,
117+
_ => panic!("Unexpected type in commutation set."),
118+
};
119+
let num_qargs = dag.get_qargs(instr.qubits).len();
120+
// no support for cancellation of parameterized gates
121+
if instr.is_parameterized() {
122+
continue;
123+
}
124+
if let Some(op_gate) = instr.op.try_standard_gate() {
125+
if num_qargs == 1 && SUPPORTED_GATES.contains(&op_gate) {
126+
cancellation_sets
127+
.entry(CancellationSetKey {
128+
gate: GateOrRotation::Gate(op_gate),
129+
qubits: smallvec![wire],
130+
com_set_index: com_set_idx,
131+
second_index: None,
132+
})
133+
.or_insert_with(Vec::new)
134+
.push(*node);
135+
}
136+
137+
if num_qargs == 1 && Z_ROTATIONS.contains(&op_gate) {
138+
cancellation_sets
139+
.entry(CancellationSetKey {
140+
gate: GateOrRotation::ZRotation,
141+
qubits: smallvec![wire],
142+
com_set_index: com_set_idx,
143+
second_index: None,
144+
})
145+
.or_insert_with(Vec::new)
146+
.push(*node);
147+
}
148+
if num_qargs == 1 && X_ROTATIONS.contains(&op_gate) {
149+
cancellation_sets
150+
.entry(CancellationSetKey {
151+
gate: GateOrRotation::XRotation,
152+
qubits: smallvec![wire],
153+
com_set_index: com_set_idx,
154+
second_index: None,
155+
})
156+
.or_insert_with(Vec::new)
157+
.push(*node);
158+
}
159+
// Don't deal with Y rotation, because Y rotation doesn't commute with
160+
// CNOT, so it should be dealt with by optimized1qgate pass
161+
if num_qargs == 2 && dag.get_qargs(instr.qubits)[0] == wire {
162+
let second_qarg = dag.get_qargs(instr.qubits)[1];
163+
cancellation_sets
164+
.entry(CancellationSetKey {
165+
gate: GateOrRotation::Gate(op_gate),
166+
qubits: smallvec![wire, second_qarg],
167+
com_set_index: com_set_idx,
168+
second_index: node_indices
169+
.get(&(*node, Wire::Qubit(second_qarg)))
170+
.copied(),
171+
})
172+
.or_insert_with(Vec::new)
173+
.push(*node);
174+
}
175+
}
176+
}
177+
}
178+
}
179+
});
180+
181+
for (cancel_key, cancel_set) in &cancellation_sets {
182+
if cancel_set.len() > 1 {
183+
if let GateOrRotation::Gate(g) = cancel_key.gate {
184+
if SUPPORTED_GATES.contains(&g) {
185+
for &c_node in &cancel_set[0..(cancel_set.len() / 2) * 2] {
186+
dag.remove_op_node(c_node);
187+
}
188+
}
189+
continue;
190+
}
191+
if matches!(cancel_key.gate, GateOrRotation::ZRotation) && z_var_gate.is_none() {
192+
continue;
193+
}
194+
if matches!(
195+
cancel_key.gate,
196+
GateOrRotation::ZRotation | GateOrRotation::XRotation
197+
) {
198+
let mut total_angle: f64 = 0.0;
199+
let mut total_phase: f64 = 0.0;
200+
for current_node in cancel_set {
201+
let node_op = match &dag.dag[*current_node] {
202+
NodeType::Operation(instr) => instr,
203+
_ => panic!("Unexpected type in commutation set run."),
204+
};
205+
let node_op_name = node_op.op.name();
206+
207+
let node_angle = if ROTATION_GATES.contains(&node_op_name) {
208+
match node_op.params_view().first() {
209+
Some(Param::Float(f)) => Ok(*f),
210+
_ => return Err(QiskitError::new_err(format!(
211+
"Rotational gate with parameter expression encountered in cancellation {:?}",
212+
node_op.op
213+
)))
214+
}
215+
} else if HALF_TURNS.contains(&node_op_name) {
216+
Ok(PI)
217+
} else if QUARTER_TURNS.contains(&node_op_name) {
218+
Ok(PI / 2.0)
219+
} else if EIGHTH_TURNS.contains(&node_op_name) {
220+
Ok(PI / 4.0)
221+
} else {
222+
Err(PyRuntimeError::new_err(format!(
223+
"Angle for operation {} is not defined",
224+
node_op_name
225+
)))
226+
};
227+
total_angle += node_angle?;
228+
229+
let Param::Float(new_phase) = node_op
230+
.op
231+
.definition(node_op.params_view())
232+
.unwrap()
233+
.global_phase()
234+
.clone()
235+
else {
236+
unreachable!()
237+
};
238+
total_phase += new_phase
239+
}
240+
241+
let new_op = match cancel_key.gate {
242+
GateOrRotation::ZRotation => z_var_gate.unwrap(),
243+
GateOrRotation::XRotation => &RXGate,
244+
_ => unreachable!(),
245+
};
246+
247+
let gate_angle = euler_one_qubit_decomposer::mod_2pi(total_angle, 0.);
248+
249+
let new_op_phase: f64 = if gate_angle.abs() > _CUTOFF_PRECISION {
250+
dag.insert_1q_on_incoming_qubit((*new_op, &[total_angle]), cancel_set[0]);
251+
let Param::Float(new_phase) = new_op
252+
.definition(&[Param::Float(total_angle)])
253+
.unwrap()
254+
.global_phase()
255+
.clone()
256+
else {
257+
unreachable!();
258+
};
259+
new_phase
260+
} else {
261+
0.0
262+
};
263+
264+
dag.add_global_phase(py, &Param::Float(total_phase - new_op_phase))?;
265+
266+
for node in cancel_set {
267+
dag.remove_op_node(*node);
268+
}
269+
}
270+
}
271+
}
272+
273+
Ok(())
274+
}
275+
276+
#[pymodule]
277+
pub fn commutation_cancellation(m: &Bound<PyModule>) -> PyResult<()> {
278+
m.add_wrapped(wrap_pyfunction!(cancel_commutations))?;
279+
Ok(())
280+
}

crates/accelerate/src/euler_one_qubit_decomposer.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ pub fn det_one_qubit(mat: ArrayView2<Complex64>) -> Complex64 {
924924

925925
/// Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π
926926
#[inline]
927-
fn mod_2pi(angle: f64, atol: f64) -> f64 {
927+
pub(crate) fn mod_2pi(angle: f64, atol: f64) -> f64 {
928928
// f64::rem_euclid() isn't exactly the same as Python's % operator, but because
929929
// the RHS here is a constant and positive it is effectively equivalent for
930930
// this case

0 commit comments

Comments
 (0)