Skip to content

Commit 317a057

Browse files
committed
Fully port Optimize1qGatesDecomposition to Rust
This commit builds off of Qiskit#12550 and the other data model in Rust infrastructure and migrates the Optimize1qGatesDecomposition pass to operate fully in Rust. The full path of the transpiler pass now never leaves rust until it has finished modifying the DAGCircuit. There is still some python interaction necessary to handle parts of the data model that are still in Python, mainly calibrations and parameter expressions (for global phase). But otherwise the entirety of the pass operates in rust now. This is just a first pass at the migration here, it moves the pass to be a single for loop in rust. The next steps here are to look at operating the pass in parallel. There is no data dependency between the optimizations being done by the pass so we should be able to the throughput of the pass by leveraging multithreading to handle each run in parallel. This commit does not attempt this though, because of the Python dependency and also the data structures around gates and the dag aren't really setup for multithreading yet and there likely will need to be some work to support that (this pass is a good candidate to work through the bugs on that). Part of Qiskit#12208
1 parent d3040a0 commit 317a057

File tree

7 files changed

+423
-160
lines changed

7 files changed

+423
-160
lines changed

crates/accelerate/src/euler_one_qubit_decomposer.rs

+227-88
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
#![allow(clippy::too_many_arguments)]
1414
#![allow(clippy::upper_case_acronyms)]
1515

16-
use hashbrown::HashMap;
16+
use ahash::RandomState;
17+
use hashbrown::{HashMap, HashSet};
18+
use indexmap::IndexSet;
1719
use num_complex::{Complex64, ComplexFloat};
1820
use smallvec::{smallvec, SmallVec};
1921
use std::cmp::Ordering;
@@ -29,14 +31,19 @@ use pyo3::Python;
2931
use ndarray::prelude::*;
3032
use numpy::PyReadonlyArray2;
3133
use pyo3::pybacked::PyBackedStr;
34+
use rustworkx_core::petgraph::stable_graph::NodeIndex;
3235

3336
use qiskit_circuit::circuit_data::CircuitData;
37+
use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType};
3438
use qiskit_circuit::dag_node::DAGOpNode;
3539
use qiskit_circuit::operations::{Operation, Param, StandardGate};
3640
use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex};
3741
use qiskit_circuit::util::c64;
3842
use qiskit_circuit::Qubit;
3943

44+
use crate::nlayout::PhysicalQubit;
45+
use crate::target_transpiler::Target;
46+
4047
pub const ANGLE_ZERO_EPSILON: f64 = 1e-12;
4148

4249
#[pyclass(module = "qiskit._accelerate.euler_one_qubit_decomposer")]
@@ -69,6 +76,7 @@ impl OneQubitGateErrorMap {
6976
}
7077
}
7178

79+
#[derive(Debug)]
7280
#[pyclass(sequence)]
7381
pub struct OneQubitGateSequence {
7482
pub gates: Vec<(StandardGate, SmallVec<[f64; 3]>)>,
@@ -571,7 +579,7 @@ pub fn generate_circuit(
571579
Ok(res)
572580
}
573581

574-
#[derive(Clone, Debug, Copy)]
582+
#[derive(Clone, Debug, Copy, Eq, Hash, PartialEq)]
575583
#[pyclass(module = "qiskit._accelerate.euler_one_qubit_decomposer")]
576584
pub enum EulerBasis {
577585
U321,
@@ -684,24 +692,6 @@ fn compare_error_fn(
684692
}
685693
}
686694

687-
fn compute_error(
688-
gates: &[(StandardGate, SmallVec<[f64; 3]>)],
689-
error_map: Option<&OneQubitGateErrorMap>,
690-
qubit: usize,
691-
) -> (f64, usize) {
692-
match error_map {
693-
Some(err_map) => {
694-
let num_gates = gates.len();
695-
let gate_fidelities: f64 = gates
696-
.iter()
697-
.map(|gate| 1. - err_map.error_map[qubit].get(gate.0.name()).unwrap_or(&0.))
698-
.product();
699-
(1. - gate_fidelities, num_gates)
700-
}
701-
None => (gates.len() as f64, gates.len()),
702-
}
703-
}
704-
705695
fn compute_error_term(gate: &str, error_map: &OneQubitGateErrorMap, qubit: usize) -> f64 {
706696
1. - error_map.error_map[qubit].get(gate).unwrap_or(&0.)
707697
}
@@ -724,15 +714,6 @@ fn compute_error_str(
724714
}
725715
}
726716

727-
#[pyfunction]
728-
pub fn compute_error_one_qubit_sequence(
729-
circuit: &OneQubitGateSequence,
730-
qubit: usize,
731-
error_map: Option<&OneQubitGateErrorMap>,
732-
) -> (f64, usize) {
733-
compute_error(&circuit.gates, error_map, qubit)
734-
}
735-
736717
#[pyfunction]
737718
pub fn compute_error_list(
738719
circuit: Vec<PyRef<DAGOpNode>>,
@@ -965,72 +946,231 @@ pub fn params_zxz(unitary: PyReadonlyArray2<Complex64>) -> [f64; 4] {
965946
params_zxz_inner(mat)
966947
}
967948

968-
type OptimizeDecompositionReturn = Option<((f64, usize), (f64, usize), OneQubitGateSequence)>;
949+
fn compute_error_term_from_target(gate: &str, target: &Target, qubit: PhysicalQubit) -> f64 {
950+
1. - target.get_error(gate, &[qubit]).unwrap_or(0.)
951+
}
952+
953+
fn compute_error_from_target_one_qubit_sequence(
954+
circuit: &OneQubitGateSequence,
955+
qubit: PhysicalQubit,
956+
target: Option<&Target>,
957+
) -> (f64, usize) {
958+
match target {
959+
Some(target) => {
960+
let num_gates = circuit.gates.len();
961+
let gate_fidelities: f64 = circuit
962+
.gates
963+
.iter()
964+
.map(|gate| compute_error_term_from_target(gate.0.name(), target, qubit))
965+
.product();
966+
(1. - gate_fidelities, num_gates)
967+
}
968+
None => (circuit.gates.len() as f64, circuit.gates.len()),
969+
}
970+
}
969971

970972
#[pyfunction]
971-
pub fn optimize_1q_gates_decomposition(
972-
runs: Vec<Vec<PyRef<DAGOpNode>>>,
973-
qubits: Vec<usize>,
974-
bases: Vec<Vec<PyBackedStr>>,
975-
simplify: bool,
976-
error_map: Option<&OneQubitGateErrorMap>,
977-
atol: Option<f64>,
978-
) -> Vec<OptimizeDecompositionReturn> {
979-
runs.iter()
980-
.enumerate()
981-
.map(|(index, raw_run)| -> OptimizeDecompositionReturn {
982-
let mut error = match error_map {
983-
Some(_) => 1.,
984-
None => raw_run.len() as f64,
973+
#[pyo3(signature = (dag, *, target=None, basis_gates=None, global_decomposers=None))]
974+
pub(crate) fn optimize_1q_gates_decomposition(
975+
py: Python,
976+
dag: &mut DAGCircuit,
977+
target: Option<&Target>,
978+
basis_gates: Option<HashSet<String>>,
979+
global_decomposers: Option<Vec<String>>,
980+
) -> PyResult<()> {
981+
let runs: Vec<Vec<NodeIndex>> = dag.collect_1q_runs().unwrap().collect();
982+
let dag_qubits = dag.num_qubits();
983+
let mut target_basis_per_qubit: Vec<Option<IndexSet<EulerBasis, RandomState>>> =
984+
vec![None; dag_qubits];
985+
let mut basis_gates_per_qubit: Vec<Option<HashSet<&str>>> = vec![None; dag_qubits];
986+
for raw_run in runs {
987+
let mut error = match target {
988+
Some(_) => 1.,
989+
None => raw_run.len() as f64,
990+
};
991+
let qubit: PhysicalQubit = if let NodeType::Operation(inst) = &dag.dag[raw_run[0]] {
992+
dag.get_qubits(inst.qubits)[0].into()
993+
} else {
994+
unreachable!("nodes in runs will always be op nodes")
995+
};
996+
if !dag.calibrations_empty() {
997+
let mut has_calibration = false;
998+
for node in &raw_run {
999+
if dag.has_calibration_for_index(py, *node)? {
1000+
has_calibration = true;
1001+
break;
1002+
}
1003+
}
1004+
if has_calibration {
1005+
continue;
1006+
}
1007+
}
1008+
if basis_gates_per_qubit[qubit.0 as usize].is_none() {
1009+
let basis_gates = match target {
1010+
Some(target) => Some(
1011+
target
1012+
.operation_names_for_qargs(Some(&smallvec![qubit]))
1013+
.unwrap(),
1014+
),
1015+
None => {
1016+
let basis = basis_gates.as_ref();
1017+
basis.map(|basis| basis.iter().map(|x| x.as_str()).collect())
1018+
}
9851019
};
986-
let qubit = qubits[index];
987-
let operator = &raw_run
988-
.iter()
989-
.map(|node| {
990-
if let Some(err_map) = error_map {
991-
error *=
992-
compute_error_term(node.instruction.operation.name(), err_map, qubit)
993-
}
994-
node.instruction
995-
.operation
996-
.matrix(&node.instruction.params)
997-
.expect("No matrix defined for operation")
998-
})
999-
.fold(
1000-
[
1001-
[Complex64::new(1., 0.), Complex64::new(0., 0.)],
1002-
[Complex64::new(0., 0.), Complex64::new(1., 0.)],
1003-
],
1004-
|mut operator, node| {
1005-
matmul_1q(&mut operator, node);
1006-
operator
1020+
basis_gates_per_qubit[qubit.0 as usize] = basis_gates;
1021+
}
1022+
let basis_gates = &basis_gates_per_qubit[qubit.0 as usize].as_ref();
1023+
1024+
if target_basis_per_qubit[qubit.0 as usize].is_none() {
1025+
let mut target_basis_set: IndexSet<EulerBasis, RandomState> = match target {
1026+
Some(_target) => EULER_BASIS_MAP
1027+
.iter()
1028+
.enumerate()
1029+
.filter_map(|(idx, gates)| {
1030+
if !gates
1031+
.iter()
1032+
.all(|gate| basis_gates.as_ref().unwrap().contains(gate))
1033+
{
1034+
return None;
1035+
}
1036+
let basis = EULER_BASIS_NAMES[idx];
1037+
Some(basis)
1038+
})
1039+
.collect(),
1040+
None => match &global_decomposers {
1041+
Some(bases) => bases
1042+
.iter()
1043+
.map(|basis| EulerBasis::__new__(basis).unwrap())
1044+
.collect(),
1045+
None => match basis_gates {
1046+
Some(gates) => EULER_BASIS_MAP
1047+
.iter()
1048+
.enumerate()
1049+
.filter_map(|(idx, basis_gates)| {
1050+
if !gates.iter().all(|gate| basis_gates.as_ref().contains(gate)) {
1051+
return None;
1052+
}
1053+
let basis = EULER_BASIS_NAMES[idx];
1054+
Some(basis)
1055+
})
1056+
.collect(),
1057+
None => EULER_BASIS_NAMES.iter().copied().collect(),
10071058
},
1008-
);
1009-
let old_error = if error_map.is_some() {
1010-
(1. - error, raw_run.len())
1011-
} else {
1012-
(error, raw_run.len())
1059+
},
10131060
};
1014-
let target_basis_vec: Vec<EulerBasis> = bases[index]
1015-
.iter()
1016-
.map(|basis| EulerBasis::__new__(basis).unwrap())
1017-
.collect();
1018-
unitary_to_gate_sequence_inner(
1019-
aview2(operator),
1020-
&target_basis_vec,
1021-
qubit,
1022-
error_map,
1023-
simplify,
1024-
atol,
1025-
)
1026-
.map(|out_seq| {
1027-
let new_error = compute_error_one_qubit_sequence(&out_seq, qubit, error_map);
1028-
(old_error, new_error, out_seq)
1061+
if target_basis_set.contains(&EulerBasis::U3)
1062+
&& target_basis_set.contains(&EulerBasis::U321)
1063+
{
1064+
target_basis_set.swap_remove(&EulerBasis::U3);
1065+
}
1066+
if target_basis_set.contains(&EulerBasis::ZSX)
1067+
&& target_basis_set.contains(&EulerBasis::ZSXX)
1068+
{
1069+
target_basis_set.swap_remove(&EulerBasis::ZSX);
1070+
}
1071+
target_basis_per_qubit[qubit.0 as usize] = Some(target_basis_set);
1072+
}
1073+
let target_basis_set = target_basis_per_qubit[qubit.0 as usize].as_ref().unwrap();
1074+
let target_basis_vec: Vec<EulerBasis> = target_basis_set.iter().copied().collect();
1075+
let operator = raw_run
1076+
.iter()
1077+
.map(|node_index| {
1078+
let node = &dag.dag[*node_index];
1079+
if let NodeType::Operation(inst) = node {
1080+
if let Some(target) = target {
1081+
error *= compute_error_term_from_target(inst.op.name(), target, qubit);
1082+
}
1083+
inst.op.matrix(inst.params_view()).unwrap()
1084+
} else {
1085+
unreachable!("Can only have op nodes here")
1086+
}
10291087
})
1030-
})
1031-
.collect()
1088+
.fold(
1089+
[
1090+
[Complex64::new(1., 0.), Complex64::new(0., 0.)],
1091+
[Complex64::new(0., 0.), Complex64::new(1., 0.)],
1092+
],
1093+
|mut operator, node| {
1094+
matmul_1q(&mut operator, node);
1095+
operator
1096+
},
1097+
);
1098+
1099+
let old_error = if target.is_some() {
1100+
(1. - error, raw_run.len())
1101+
} else {
1102+
(error, raw_run.len())
1103+
};
1104+
let sequence = unitary_to_gate_sequence_inner(
1105+
aview2(&operator),
1106+
&target_basis_vec,
1107+
qubit.0 as usize,
1108+
None,
1109+
true,
1110+
None,
1111+
);
1112+
let sequence = match sequence {
1113+
Some(seq) => seq,
1114+
None => continue,
1115+
};
1116+
let new_error = compute_error_from_target_one_qubit_sequence(&sequence, qubit, target);
1117+
1118+
let mut outside_basis = false;
1119+
if let Some(basis) = basis_gates {
1120+
for node in &raw_run {
1121+
if let NodeType::Operation(inst) = &dag.dag[*node] {
1122+
if !basis.contains(inst.op.name()) {
1123+
outside_basis = true;
1124+
break;
1125+
}
1126+
}
1127+
}
1128+
} else {
1129+
outside_basis = false;
1130+
}
1131+
if outside_basis
1132+
|| new_error < old_error
1133+
|| new_error.0.abs() < 1e-9 && old_error.0.abs() >= 1e-9
1134+
{
1135+
for gate in sequence.gates {
1136+
dag.insert_1q_on_incoming_qubit((gate.0, &gate.1), raw_run[0]);
1137+
}
1138+
dag.add_global_phase(py, &Param::Float(sequence.global_phase))?;
1139+
dag.remove_1q_sequence(&raw_run);
1140+
}
1141+
}
1142+
Ok(())
10321143
}
10331144

1145+
static EULER_BASIS_MAP: [&[&str]; 12] = [
1146+
&["u3"],
1147+
&["u3", "u2", "u1"],
1148+
&["u"],
1149+
&["p", "sx"],
1150+
&["u1", "rx"],
1151+
&["r"],
1152+
&["rz", "ry"],
1153+
&["rz", "rx"],
1154+
&["rz", "rx"],
1155+
&["rx", "ry"],
1156+
&["rz", "sx", "x"],
1157+
&["rz", "sx"],
1158+
];
1159+
static EULER_BASIS_NAMES: [EulerBasis; 12] = [
1160+
EulerBasis::U3,
1161+
EulerBasis::U321,
1162+
EulerBasis::U,
1163+
EulerBasis::PSX,
1164+
EulerBasis::U1X,
1165+
EulerBasis::RR,
1166+
EulerBasis::ZYZ,
1167+
EulerBasis::ZXZ,
1168+
EulerBasis::XZX,
1169+
EulerBasis::XYX,
1170+
EulerBasis::ZSXX,
1171+
EulerBasis::ZSX,
1172+
];
1173+
10341174
fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2<Complex64>) {
10351175
*operator = [
10361176
[
@@ -1054,7 +1194,6 @@ pub fn euler_one_qubit_decomposer(m: &Bound<PyModule>) -> PyResult<()> {
10541194
m.add_wrapped(wrap_pyfunction!(generate_circuit))?;
10551195
m.add_wrapped(wrap_pyfunction!(unitary_to_gate_sequence))?;
10561196
m.add_wrapped(wrap_pyfunction!(unitary_to_circuit))?;
1057-
m.add_wrapped(wrap_pyfunction!(compute_error_one_qubit_sequence))?;
10581197
m.add_wrapped(wrap_pyfunction!(compute_error_list))?;
10591198
m.add_wrapped(wrap_pyfunction!(optimize_1q_gates_decomposition))?;
10601199
m.add_class::<OneQubitGateSequence>()?;

0 commit comments

Comments
 (0)