From 4ac77827bdd8f570bacd8a6a453daadd15322c85 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 27 May 2024 20:58:38 -0400 Subject: [PATCH] Use rust gates for Optimize1QGatesDecomposition This commit moves to using rust gates for the Optimize1QGatesDecomposition transpiler pass. It takes in a sequence of runs (which are a list of DAGOpNodes) from the python side of the transpiler pass which are generated from DAGCircuit.collect_1q_runs() (which in the future should be moved to rust after #12550 merges). The rust portion of the pass now iterates over each run, performs the matrix multiplication to compute the unitary of the run, then synthesizes that unitary, computes the estimated error of the circuit synthesis and returns a tuple of the circuit sequence in terms of rust StandardGate enums. The python portion of the code then takes those sequences and does inplace substitution of each run with the sequence returned from rust. Once #12550 merges we should be able to move the input collect_1q_runs() call and perform the output node substitions in rust making the full pass execute in the rust domain without any python interaction. Additionally, the OneQubitEulerDecomposer class is updated to use rust for circuit generation instead of doing this python side. The internal changes done to use rust gates in the transpiler pass meant we were half way to this already by emitting rust StandardGates instead of python gate objects. The dag handling is still done in Python however until #12550 merges. This also includes an implementation of the r gate, I temporarily added this to unblock this effort as it was the only gate missing needed to complete this. We can rebase this if a standalone implementation of the gate merges before this. --- .../src/euler_one_qubit_decomposer.rs | 276 +++++++++++++++--- crates/accelerate/src/two_qubit_decompose.rs | 19 +- crates/circuit/src/dag_node.rs | 51 +++- crates/circuit/src/gate_matrix.rs | 11 + crates/circuit/src/operations.rs | 63 +++- qiskit/circuit/library/standard_gates/r.py | 3 + qiskit/dagcircuit/dagcircuit.py | 55 ++-- .../one_qubit/one_qubit_decompose.py | 50 ++-- .../optimization/optimize_1q_decomposition.py | 83 ++++-- 9 files changed, 470 insertions(+), 141 deletions(-) diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 1fd5fd7834ff..6fc5b1482934 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -31,7 +31,10 @@ use ndarray::prelude::*; use numpy::PyReadonlyArray2; use pyo3::pybacked::PyBackedStr; -use qiskit_circuit::SliceOrInt; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::dag_node::DAGOpNode; +use qiskit_circuit::operations::{Operation, Param, StandardGate}; +use qiskit_circuit::{Qubit, SliceOrInt}; pub const ANGLE_ZERO_EPSILON: f64 = 1e-12; @@ -67,12 +70,12 @@ impl OneQubitGateErrorMap { #[pyclass(sequence)] pub struct OneQubitGateSequence { - pub gates: Vec<(String, SmallVec<[f64; 3]>)>, + pub gates: Vec<(StandardGate, SmallVec<[f64; 3]>)>, #[pyo3(get)] pub global_phase: f64, } -type OneQubitGateSequenceState = (Vec<(String, SmallVec<[f64; 3]>)>, f64); +type OneQubitGateSequenceState = (Vec<(StandardGate, SmallVec<[f64; 3]>)>, f64); #[pymethods] impl OneQubitGateSequence { @@ -101,7 +104,7 @@ impl OneQubitGateSequence { SliceOrInt::Slice(slc) => { let len = self.gates.len().try_into().unwrap(); let indices = slc.indices(len)?; - let mut out_vec: Vec<(String, SmallVec<[f64; 3]>)> = Vec::new(); + let mut out_vec: Vec<(StandardGate, SmallVec<[f64; 3]>)> = Vec::new(); // Start and stop will always be positive the slice api converts // negatives to the index for example: // list(range(5))[-1:-3:-1] @@ -145,15 +148,15 @@ fn circuit_kak( phi: f64, lam: f64, phase: f64, - k_gate: &str, - a_gate: &str, + k_gate: StandardGate, + a_gate: StandardGate, simplify: bool, atol: Option, ) -> OneQubitGateSequence { let mut lam = lam; let mut theta = theta; let mut phi = phi; - let mut circuit: Vec<(String, SmallVec<[f64; 3]>)> = Vec::with_capacity(3); + let mut circuit: Vec<(StandardGate, SmallVec<[f64; 3]>)> = Vec::with_capacity(3); let mut atol = match atol { Some(atol) => atol, None => ANGLE_ZERO_EPSILON, @@ -169,7 +172,7 @@ fn circuit_kak( // slippage coming from _mod_2pi injecting multiples of 2pi. lam = mod_2pi(lam, atol); if lam.abs() > atol { - circuit.push((String::from(k_gate), smallvec![lam])); + circuit.push((k_gate, smallvec![lam])); global_phase += lam / 2.; } return OneQubitGateSequence { @@ -190,13 +193,13 @@ fn circuit_kak( lam = mod_2pi(lam, atol); if lam.abs() > atol { global_phase += lam / 2.; - circuit.push((String::from(k_gate), smallvec![lam])); + circuit.push((k_gate, smallvec![lam])); } - circuit.push((String::from(a_gate), smallvec![theta])); + circuit.push((a_gate, smallvec![theta])); phi = mod_2pi(phi, atol); if phi.abs() > atol { global_phase += phi / 2.; - circuit.push((String::from(k_gate), smallvec![phi])); + circuit.push((k_gate, smallvec![phi])); } OneQubitGateSequence { gates: circuit, @@ -220,7 +223,7 @@ fn circuit_u3( let phi = mod_2pi(phi, atol); let lam = mod_2pi(lam, atol); if !simplify || theta.abs() > atol || phi.abs() > atol || lam.abs() > atol { - circuit.push((String::from("u3"), smallvec![theta, phi, lam])); + circuit.push((StandardGate::U3Gate, smallvec![theta, phi, lam])); } OneQubitGateSequence { gates: circuit, @@ -247,16 +250,16 @@ fn circuit_u321( if theta.abs() < atol { let tot = mod_2pi(phi + lam, atol); if tot.abs() > atol { - circuit.push((String::from("u1"), smallvec![tot])); + circuit.push((StandardGate::U1Gate, smallvec![tot])); } } else if (theta - PI / 2.).abs() < atol { circuit.push(( - String::from("u2"), + StandardGate::U2Gate, smallvec![mod_2pi(phi, atol), mod_2pi(lam, atol)], )); } else { circuit.push(( - String::from("u3"), + StandardGate::U3Gate, smallvec![theta, mod_2pi(phi, atol), mod_2pi(lam, atol)], )); } @@ -285,7 +288,7 @@ fn circuit_u( let phi = mod_2pi(phi, atol); let lam = mod_2pi(lam, atol); if theta.abs() > atol || phi.abs() > atol || lam.abs() > atol { - circuit.push((String::from("u"), smallvec![theta, phi, lam])); + circuit.push((StandardGate::UGate, smallvec![theta, phi, lam])); } OneQubitGateSequence { gates: circuit, @@ -388,7 +391,7 @@ fn circuit_rr( // This can be expressed as a single R gate if theta.abs() > atol { circuit.push(( - String::from("r"), + StandardGate::RGate, smallvec![theta, mod_2pi(PI / 2. + phi, atol)], )); } @@ -396,12 +399,12 @@ fn circuit_rr( // General case: use two R gates if (theta - PI).abs() > atol { circuit.push(( - String::from("r"), + StandardGate::RGate, smallvec![theta - PI, mod_2pi(PI / 2. - lam, atol)], )); } circuit.push(( - String::from("r"), + StandardGate::RGate, smallvec![PI, mod_2pi(0.5 * (phi - lam + PI), atol)], )); } @@ -423,10 +426,46 @@ pub fn generate_circuit( atol: Option, ) -> PyResult { let res = match target_basis { - EulerBasis::ZYZ => circuit_kak(theta, phi, lam, phase, "rz", "ry", simplify, atol), - EulerBasis::ZXZ => circuit_kak(theta, phi, lam, phase, "rz", "rx", simplify, atol), - EulerBasis::XZX => circuit_kak(theta, phi, lam, phase, "rx", "rz", simplify, atol), - EulerBasis::XYX => circuit_kak(theta, phi, lam, phase, "rx", "ry", simplify, atol), + EulerBasis::ZYZ => circuit_kak( + theta, + phi, + lam, + phase, + StandardGate::RZGate, + StandardGate::RYGate, + simplify, + atol, + ), + EulerBasis::ZXZ => circuit_kak( + theta, + phi, + lam, + phase, + StandardGate::RZGate, + StandardGate::RXGate, + simplify, + atol, + ), + EulerBasis::XZX => circuit_kak( + theta, + phi, + lam, + phase, + StandardGate::RXGate, + StandardGate::RZGate, + simplify, + atol, + ), + EulerBasis::XYX => circuit_kak( + theta, + phi, + lam, + phase, + StandardGate::RXGate, + StandardGate::RYGate, + simplify, + atol, + ), EulerBasis::U3 => circuit_u3(theta, phi, lam, phase, simplify, atol), EulerBasis::U321 => circuit_u321(theta, phi, lam, phase, simplify, atol), EulerBasis::U => circuit_u(theta, phi, lam, phase, simplify, atol), @@ -441,11 +480,13 @@ pub fn generate_circuit( let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { - circuit.gates.push((String::from("p"), smallvec![phi])); + circuit + .gates + .push((StandardGate::PhaseGate, smallvec![phi])); } }; let fnx = |circuit: &mut OneQubitGateSequence| { - circuit.gates.push((String::from("sx"), SmallVec::new())); + circuit.gates.push((StandardGate::SXGate, SmallVec::new())); }; circuit_psx_gen( @@ -471,12 +512,12 @@ pub fn generate_circuit( let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { - circuit.gates.push((String::from("rz"), smallvec![phi])); + circuit.gates.push((StandardGate::RZGate, smallvec![phi])); circuit.global_phase += phi / 2.; } }; let fnx = |circuit: &mut OneQubitGateSequence| { - circuit.gates.push((String::from("sx"), SmallVec::new())); + circuit.gates.push((StandardGate::SXGate, SmallVec::new())); }; circuit_psx_gen( theta, @@ -501,12 +542,14 @@ pub fn generate_circuit( let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { - circuit.gates.push((String::from("u1"), smallvec![phi])); + circuit.gates.push((StandardGate::U1Gate, smallvec![phi])); } }; let fnx = |circuit: &mut OneQubitGateSequence| { circuit.global_phase += PI / 4.; - circuit.gates.push((String::from("rx"), smallvec![PI / 2.])); + circuit + .gates + .push((StandardGate::RXGate, smallvec![PI / 2.])); }; circuit_psx_gen( theta, @@ -531,15 +574,15 @@ pub fn generate_circuit( let fnz = |circuit: &mut OneQubitGateSequence, phi: f64| { let phi = mod_2pi(phi, inner_atol); if phi.abs() > inner_atol { - circuit.gates.push((String::from("rz"), smallvec![phi])); + circuit.gates.push((StandardGate::RZGate, smallvec![phi])); circuit.global_phase += phi / 2.; } }; let fnx = |circuit: &mut OneQubitGateSequence| { - circuit.gates.push((String::from("sx"), SmallVec::new())); + circuit.gates.push((StandardGate::SXGate, SmallVec::new())); }; let fnxpi = |circuit: &mut OneQubitGateSequence| { - circuit.gates.push((String::from("x"), SmallVec::new())); + circuit.gates.push((StandardGate::XGate, SmallVec::new())); }; circuit_psx_gen( theta, @@ -663,7 +706,7 @@ fn compare_error_fn( let fidelity_product: f64 = circuit .gates .iter() - .map(|x| 1. - err_map.get(&x.0).unwrap_or(&0.)) + .map(|x| 1. - err_map.get(x.0.name()).unwrap_or(&0.)) .product(); (1. - fidelity_product, circuit.gates.len()) } @@ -672,6 +715,24 @@ fn compare_error_fn( } fn compute_error( + gates: &[(StandardGate, SmallVec<[f64; 3]>)], + error_map: Option<&OneQubitGateErrorMap>, + qubit: usize, +) -> (f64, usize) { + match error_map { + Some(err_map) => { + let num_gates = gates.len(); + let gate_fidelities: f64 = gates + .iter() + .map(|x| 1. - err_map.error_map[qubit].get(x.0.name()).unwrap_or(&0.)) + .product(); + (1. - gate_fidelities, num_gates) + } + None => (gates.len() as f64, gates.len()), + } +} + +fn compute_error_str( gates: &[(String, SmallVec<[f64; 3]>)], error_map: Option<&OneQubitGateErrorMap>, qubit: usize, @@ -700,11 +761,20 @@ pub fn compute_error_one_qubit_sequence( #[pyfunction] pub fn compute_error_list( - circuit: Vec<(String, SmallVec<[f64; 3]>)>, + circuit: Vec>, qubit: usize, error_map: Option<&OneQubitGateErrorMap>, ) -> (f64, usize) { - compute_error(&circuit, error_map, qubit) + let circuit_list: Vec<(String, SmallVec<[f64; 3]>)> = circuit + .iter() + .map(|node| { + ( + node.instruction.operation.name().to_string(), + smallvec![], // Params not needed in this path + ) + }) + .collect(); + compute_error_str(&circuit_list, error_map, qubit) } #[pyfunction] @@ -755,6 +825,48 @@ pub fn unitary_to_gate_sequence_inner( }) } +#[pyfunction] +#[pyo3(signature = (unitary, target_basis_list, qubit, error_map=None, simplify=true, atol=None))] +pub fn unitary_to_circuit( + py: Python, + unitary: PyReadonlyArray2, + target_basis_list: Vec, + qubit: usize, + error_map: Option<&OneQubitGateErrorMap>, + simplify: bool, + atol: Option, +) -> PyResult> { + let mut target_basis_vec: Vec = Vec::with_capacity(target_basis_list.len()); + for basis in target_basis_list { + let basis_enum = EulerBasis::__new__(basis.deref())?; + target_basis_vec.push(basis_enum) + } + let unitary_mat = unitary.as_array(); + let circuit_sequence = unitary_to_gate_sequence_inner( + unitary_mat, + &target_basis_vec, + qubit, + error_map, + simplify, + atol, + ); + Ok(circuit_sequence.map(|seq| { + CircuitData::from_standard_gates( + py, + 1, + seq.gates.into_iter().map(|(gate, params)| { + ( + gate, + params.into_iter().map(Param::Float).collect(), + smallvec![Qubit(0)], + ) + }), + Param::Float(seq.global_phase), + ) + .expect("Unexpected Qiskit python bug") + })) +} + #[inline] pub fn det_one_qubit(mat: ArrayView2) -> Complex64 { mat[[0, 0]] * mat[[1, 1]] - mat[[0, 1]] * mat[[1, 0]] @@ -883,6 +995,97 @@ pub fn params_zxz(unitary: PyReadonlyArray2) -> [f64; 4] { params_zxz_inner(mat) } +type OptimizeDecompositionReturn = Option<((f64, usize), (f64, usize), OneQubitGateSequence)>; + +#[pyfunction] +pub fn optimize_1q_gates_decomposition( + runs: Vec>>, + qubits: Vec, + bases: Vec>, + simplify: bool, + error_map: Option<&OneQubitGateErrorMap>, + atol: Option, +) -> Vec { + // The array collection is done serially because we need to use a PyRef to get a reference + // input to the DAGOpNode (as a side effect of being a subclass in rust space). This prohibits + // using paralleism because PyRef is neither sync or send. + runs.iter() + .enumerate() + .map( + |(index, raw_run)| -> OptimizeDecompositionReturn { + let run = raw_run + .iter() + .map(|node| { + node.instruction + .operation + .matrix(&node.instruction.params) + .unwrap() + }) + .collect::>>(); + let qubit = qubits[index]; + let circuit_list: Vec<(String, SmallVec<[f64; 3]>)> = raw_run + .iter() + .map(|node| { + ( + node.instruction.operation.name().to_string(), + smallvec![], // Params not needed in this path + ) + }) + .collect(); + let old_error = compute_error_str(&circuit_list, error_map, qubit); + let target_basis_list = &bases[index]; + let mut operator: Array2 = run[0].to_owned(); + for node in &run[1..] { + operator = node.dot(&operator); + } + let mut target_basis_vec: Vec = + Vec::with_capacity(target_basis_list.len()); + for basis in target_basis_list { + let basis_enum = EulerBasis::__new__(basis.deref()).unwrap(); + target_basis_vec.push(basis_enum) + } + unitary_to_gate_sequence_inner( + operator.view(), + &target_basis_vec, + qubit, + error_map, + simplify, + atol, + ) + .map(|out_seq| { + let new_error = compute_error_one_qubit_sequence(&out_seq, qubit, error_map); + (old_error, new_error, out_seq) + }) + }, + ) + .collect() +} + +#[pyfunction] +pub fn collect_1q_runs_filter(py: Python, node: PyObject) -> bool { + let op_node = node.extract::>(py); + match op_node { + Ok(node) => { + node.instruction.operation.num_qubits() == 1 + && node.instruction.operation.num_clbits() == 0 + && node + .instruction + .operation + .matrix(&node.instruction.params) + .is_some() + && (node.instruction.extra_attrs.is_none() + || node + .instruction + .extra_attrs + .as_ref() + .unwrap() + .condition + .is_none()) + } + Err(_) => false, + } +} + #[pymodule] pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(params_zyz))?; @@ -893,8 +1096,11 @@ pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(params_u1x))?; m.add_wrapped(wrap_pyfunction!(generate_circuit))?; m.add_wrapped(wrap_pyfunction!(unitary_to_gate_sequence))?; + m.add_wrapped(wrap_pyfunction!(unitary_to_circuit))?; m.add_wrapped(wrap_pyfunction!(compute_error_one_qubit_sequence))?; m.add_wrapped(wrap_pyfunction!(compute_error_list))?; + m.add_wrapped(wrap_pyfunction!(optimize_1q_gates_decomposition))?; + m.add_wrapped(wrap_pyfunction!(collect_1q_runs_filter))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index e8c572b04039..12d8a9ebbff7 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -52,6 +52,7 @@ use rand_distr::StandardNormal; use rand_pcg::Pcg64Mcg; use qiskit_circuit::gate_matrix::{CX_GATE, H_GATE, ONE_QUBIT_IDENTITY, SX_GATE, X_GATE}; +use qiskit_circuit::operations::Operation; use qiskit_circuit::SliceOrInt; const PI2: f64 = PI / 2.0; @@ -1097,7 +1098,7 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c2r.gates { - gate_sequence.push((gate.0, gate.1, smallvec![0])) + gate_sequence.push((gate.0.name().to_string(), gate.1, smallvec![0])) } global_phase += c2r.global_phase; let c2l = unitary_to_gate_sequence_inner( @@ -1110,7 +1111,7 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c2l.gates { - gate_sequence.push((gate.0, gate.1, smallvec![1])) + gate_sequence.push((gate.0.name().to_string(), gate.1, smallvec![1])) } global_phase += c2l.global_phase; self.weyl_gate( @@ -1129,7 +1130,7 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c1r.gates { - gate_sequence.push((gate.0, gate.1, smallvec![0])) + gate_sequence.push((gate.0.name().to_string(), gate.1, smallvec![0])) } global_phase += c2r.global_phase; let c1l = unitary_to_gate_sequence_inner( @@ -1142,7 +1143,7 @@ impl TwoQubitWeylDecomposition { ) .unwrap(); for gate in c1l.gates { - gate_sequence.push((gate.0, gate.1, smallvec![1])) + gate_sequence.push((gate.0.name().to_string(), gate.1, smallvec![1])) } Ok(TwoQubitGateSequence { gates: gate_sequence, @@ -1542,7 +1543,7 @@ impl TwoQubitBasisDecomposer { if let Some(sequence) = sequence { *global_phase += sequence.global_phase; for gate in sequence.gates { - gates.push((gate.0, gate.1, smallvec![qubit])); + gates.push((gate.0.name().to_string(), gate.1, smallvec![qubit])); } } } @@ -1955,13 +1956,13 @@ impl TwoQubitBasisDecomposer { for i in 0..best_nbasis as usize { if let Some(euler_decomp) = &euler_decompositions[2 * i] { for gate in &euler_decomp.gates { - gates.push((gate.0.clone(), gate.1.clone(), smallvec![0])); + gates.push((gate.0.name().to_string(), gate.1.clone(), smallvec![0])); } global_phase += euler_decomp.global_phase } if let Some(euler_decomp) = &euler_decompositions[2 * i + 1] { for gate in &euler_decomp.gates { - gates.push((gate.0.clone(), gate.1.clone(), smallvec![1])); + gates.push((gate.0.name().to_string(), gate.1.clone(), smallvec![1])); } global_phase += euler_decomp.global_phase } @@ -1969,13 +1970,13 @@ impl TwoQubitBasisDecomposer { } if let Some(euler_decomp) = &euler_decompositions[2 * best_nbasis as usize] { for gate in &euler_decomp.gates { - gates.push((gate.0.clone(), gate.1.clone(), smallvec![0])); + gates.push((gate.0.name().to_string(), gate.1.clone(), smallvec![0])); } global_phase += euler_decomp.global_phase } if let Some(euler_decomp) = &euler_decompositions[2 * best_nbasis as usize + 1] { for gate in &euler_decomp.gates { - gates.push((gate.0.clone(), gate.1.clone(), smallvec![1])); + gates.push((gate.0.name().to_string(), gate.1.clone(), smallvec![1])); } global_phase += euler_decomp.global_phase } diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index c8b6a4c8b082..d0fde754b60c 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -17,7 +17,7 @@ use crate::circuit_instruction::{ use crate::operations::Operation; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; -use pyo3::{intern, PyObject, PyResult}; +use pyo3::{intern, PyObject, PyResult, ToPyObject}; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. #[pyclass(module = "qiskit._accelerate.circuit", subclass)] @@ -144,6 +144,50 @@ impl DAGOpNode { )) } + #[staticmethod] + fn from_instruction( + py: Python, + instruction: CircuitInstruction, + dag: Option<&Bound>, + ) -> PyResult { + let qargs = instruction.qubits.clone_ref(py).into_bound(py); + let cargs = instruction.clbits.clone_ref(py).into_bound(py); + + let sort_key = match dag { + Some(dag) => { + let cache = dag + .getattr(intern!(py, "_key_cache"))? + .downcast_into_exact::()?; + let cache_key = PyTuple::new_bound(py, [&qargs, &cargs]); + match cache.get_item(&cache_key)? { + Some(key) => key, + None => { + let indices: PyResult> = qargs + .iter() + .chain(cargs.iter()) + .map(|bit| { + dag.call_method1(intern!(py, "find_bit"), (bit,))? + .getattr(intern!(py, "index")) + }) + .collect(); + let index_strs: Vec<_> = + indices?.into_iter().map(|i| format!("{:04}", i)).collect(); + let key = PyString::new_bound(py, index_strs.join(",").as_str()); + cache.set_item(&cache_key, &key)?; + key.into_any() + } + } + } + None => qargs.str()?.into_any(), + }; + let base = PyClassInitializer::from(DAGNode { _node_id: -1 }); + let sub = base.add_subclass(DAGOpNode { + instruction, + sort_key: sort_key.unbind(), + }); + Ok(Py::new(py, sub)?.to_object(py)) + } + fn __reduce__(slf: PyRef, py: Python) -> PyResult { let state = (slf.as_ref()._node_id, &slf.sort_key); Ok(( @@ -229,6 +273,11 @@ impl DAGOpNode { Ok(()) } + #[getter] + fn op_name(&self) -> &str { + self.instruction.operation.name() + } + /// Returns a representation of the DAGOpNode fn __repr__(&self, py: Python) -> PyResult { Ok(format!( diff --git a/crates/circuit/src/gate_matrix.rs b/crates/circuit/src/gate_matrix.rs index 2e5f55d6ddcb..02db2db12528 100644 --- a/crates/circuit/src/gate_matrix.rs +++ b/crates/circuit/src/gate_matrix.rs @@ -324,3 +324,14 @@ pub fn xx_plus_yy_gate(theta: f64, beta: f64) -> [[Complex64; 4]; 4] { [c64(0., 0.), c64(0., 0.), c64(0., 0.), c64(1., 0.)], ] } + +pub fn r_gate(theta: f64, phi: f64) -> [[Complex64; 2]; 2] { + let cos = (theta / 2.).cos(); + let sin = (theta / 2.).sin(); + let exp_m = c64(0., -phi).exp(); + let exp_p = c64(0., phi).exp(); + [ + [c64(cos, 0.), c64(0., -1.) * exp_m * sin], + [c64(0., -1.) * exp_p * sin, c64(cos, 0.)], + ] +} diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index af7dabc86216..79fdfdb3bdaf 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -234,12 +234,18 @@ pub enum StandardGate { RZXGate = 52, } +impl ToPyObject for StandardGate { + fn to_object(&self, py: Python) -> PyObject { + self.into_py(py) + } +} + // TODO: replace all 34s (placeholders) with actual number static STANDARD_GATE_NUM_QUBITS: [u32; STANDARD_GATE_SIZE] = [ 1, 1, 1, 2, 2, 2, 3, 1, 1, 1, // 0-9 2, 2, 1, 0, 1, 1, 1, 1, 1, 1, // 10-19 1, 1, 1, 2, 2, 2, 1, 1, 1, 34, // 20-29 - 34, 34, 34, 2, 2, 2, 2, 2, 3, 2, // 30-39 + 34, 34, 1, 2, 2, 2, 2, 2, 3, 2, // 30-39 2, 2, 34, 34, 34, 34, 34, 34, 34, 34, // 40-49 34, 34, 34, // 50-52 ]; @@ -249,7 +255,7 @@ static STANDARD_GATE_NUM_PARAMS: [u32; STANDARD_GATE_SIZE] = [ 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, // 0-9 0, 0, 0, 1, 0, 0, 1, 3, 0, 0, // 10-19 0, 0, 0, 0, 2, 2, 1, 2, 3, 34, // 20-29 - 34, 34, 34, 0, 1, 0, 0, 0, 0, 3, // 30-39 + 34, 34, 2, 0, 1, 0, 0, 0, 0, 3, // 30-39 1, 3, 34, 34, 34, 34, 34, 34, 34, 34, // 40-49 34, 34, 34, // 50-52 ]; @@ -514,7 +520,12 @@ impl Operation for StandardGate { _ => None, }, Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), - Self::RGate => todo!(), + Self::RGate => match params { + [Param::Float(theta), Param::Float(phi)] => { + Some(aview2(&gate_matrix::r_gate(*theta, *phi)).to_owned()) + } + _ => None, + }, Self::CHGate => todo!(), Self::CPhaseGate => todo!(), Self::CSGate => todo!(), @@ -954,7 +965,23 @@ impl Operation for StandardGate { ) }), Self::CRXGate | Self::CRYGate | Self::CRZGate => todo!(), - Self::RGate => todo!(), + Self::RGate => Python::with_gil(|py| -> Option { + let out_phi = subtract_param(¶ms[1], PI2, py); + let out_lam = add_param(&multiply_param(¶ms[1], -1., py), PI2, py); + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::UGate, + smallvec![params[0].clone(), out_phi, out_lam], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::CHGate => todo!(), Self::CPhaseGate => todo!(), Self::CSGate => todo!(), @@ -987,7 +1014,33 @@ fn multiply_param(param: &Param, mult: f64, py: Python) -> Param { theta .clone_ref(py) .call_method1(py, intern!(py, "__rmul__"), (mult,)) - .expect("Parameter expression for global phase failed"), + .expect("Parameter expression for multiplication failed"), + ), + Param::Obj(_) => unreachable!(), + } +} + +fn subtract_param(param: &Param, other: f64, py: Python) -> Param { + match param { + Param::Float(theta) => Param::Float(*theta - other), + Param::ParameterExpression(theta) => Param::ParameterExpression( + theta + .clone_ref(py) + .call_method1(py, intern!(py, "__sub__"), (other,)) + .expect("Parameter expression for subtraction failed"), + ), + Param::Obj(_) => unreachable!(), + } +} + +fn add_param(param: &Param, other: f64, py: Python) -> Param { + match param { + Param::Float(theta) => Param::Float(*theta + other), + Param::ParameterExpression(theta) => Param::ParameterExpression( + theta + .clone_ref(py) + .call_method1(py, intern!(py, "__add__"), (other,)) + .expect("Parameter expression for addition failed"), ), Param::Obj(_) => unreachable!(), } diff --git a/qiskit/circuit/library/standard_gates/r.py b/qiskit/circuit/library/standard_gates/r.py index 9d4905e27866..22c30e24bf6a 100644 --- a/qiskit/circuit/library/standard_gates/r.py +++ b/qiskit/circuit/library/standard_gates/r.py @@ -20,6 +20,7 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType +from qiskit._accelerate.circuit import StandardGate class RGate(Gate): @@ -49,6 +50,8 @@ class RGate(Gate): \end{pmatrix} """ + _standard_gate = StandardGate.RGate + def __init__( self, theta: ParameterValueType, diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index d14340a8cb9b..65aa9ff90c38 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -54,6 +54,7 @@ from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode from qiskit.circuit.bit import Bit from qiskit.pulse import Schedule +from qiskit._accelerate.euler_one_qubit_decomposer import collect_1q_runs_filter BitLocations = namedtuple("BitLocations", ("index", "registers")) # The allowable arguments to :meth:`DAGCircuit.copy_empty_like`'s ``vars_mode``. @@ -642,17 +643,17 @@ def _check_wires(self, args: Iterable[Bit | expr.Var], amap: dict[Bit | expr.Var if wire not in amap: raise DAGCircuitError(f"wire {wire} not found in {amap}") - def _increment_op(self, op): - if op.name in self._op_names: - self._op_names[op.name] += 1 + def _increment_op(self, op_name): + if op_name in self._op_names: + self._op_names[op_name] += 1 else: - self._op_names[op.name] = 1 + self._op_names[op_name] = 1 - def _decrement_op(self, op): - if self._op_names[op.name] == 1: - del self._op_names[op.name] + def _decrement_op(self, op_name): + if self._op_names[op_name] == 1: + del self._op_names[op_name] else: - self._op_names[op.name] -= 1 + self._op_names[op_name] -= 1 def copy_empty_like(self, *, vars_mode: _VarsMode = "alike"): """Return a copy of self with the same structure but empty. @@ -759,7 +760,7 @@ def apply_operation_back( node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) node._node_id = self._multi_graph.add_node(node) - self._increment_op(op) + self._increment_op(op.name) # Add new in-edges from predecessors of the output nodes to the # operation node while deleting the old in-edges of the output nodes @@ -811,7 +812,7 @@ def apply_operation_front( node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self) node._node_id = self._multi_graph.add_node(node) - self._increment_op(op) + self._increment_op(op.name) # Add new out-edges to successors of the input nodes from the # operation node while deleting the old out-edges of the input nodes @@ -1351,10 +1352,10 @@ def replace_block_with_op( "Replacing the specified node block would introduce a cycle" ) from ex - self._increment_op(op) + self._increment_op(op.name) for nd in node_block: - self._decrement_op(nd.op) + self._decrement_op(nd.op_name) return new_node @@ -1565,7 +1566,7 @@ def edge_weight_map(wire): node_map = self._multi_graph.substitute_node_with_subgraph( node._node_id, in_dag._multi_graph, edge_map_fn, filter_fn, edge_weight_map ) - self._decrement_op(node.op) + self._decrement_op(node.op_name) variable_mapper = _classical_resource_map.VariableMapper( self.cregs.values(), wire_map, add_register=self.add_creg @@ -1596,7 +1597,7 @@ def edge_weight_map(wire): new_node = DAGOpNode(m_op, qargs=m_qargs, cargs=m_cargs, dag=self) new_node._node_id = new_node_index self._multi_graph[new_node_index] = new_node - self._increment_op(new_node.op) + self._increment_op(new_node.op_name) return {k: self._multi_graph[v] for k, v in node_map.items()} @@ -1668,17 +1669,17 @@ def substitute_node(self, node: DAGOpNode, op, inplace: bool = False, propagate_ if inplace: if op.name != node.op.name: - self._increment_op(op) - self._decrement_op(node.op) + self._increment_op(op.name) + self._decrement_op(node.op_name) node.op = op return node new_node = copy.copy(node) new_node.op = op self._multi_graph[node._node_id] = new_node - if op.name != node.op.name: - self._increment_op(op) - self._decrement_op(node.op) + if op.name != node.op_name: + self._increment_op(op.name) + self._decrement_op(node.op_name) return new_node def separable_circuits( @@ -1959,7 +1960,7 @@ def remove_op_node(self, node): self._multi_graph.remove_node_retain_edges( node._node_id, use_outgoing=False, condition=lambda edge1, edge2: edge1 == edge2 ) - self._decrement_op(node.op) + self._decrement_op(node.op_name) def remove_ancestors_of(self, node): """Remove all of the ancestor operation nodes of node.""" @@ -2124,19 +2125,7 @@ def filter_fn(node): def collect_1q_runs(self) -> list[list[DAGOpNode]]: """Return a set of non-conditional runs of 1q "op" nodes.""" - - def filter_fn(node): - return ( - isinstance(node, DAGOpNode) - and len(node.qargs) == 1 - and len(node.cargs) == 0 - and isinstance(node.op, Gate) - and hasattr(node.op, "__array__") - and getattr(node.op, "condition", None) is None - and not node.op.is_parameterized() - ) - - return rx.collect_runs(self._multi_graph, filter_fn) + return rx.collect_runs(self._multi_graph, collect_1q_runs_filter) def collect_2q_runs(self): """Return a set of non-conditional runs of 2q "op" nodes.""" diff --git a/qiskit/synthesis/one_qubit/one_qubit_decompose.py b/qiskit/synthesis/one_qubit/one_qubit_decompose.py index c84db761b7f0..f60f20f9524e 100644 --- a/qiskit/synthesis/one_qubit/one_qubit_decompose.py +++ b/qiskit/synthesis/one_qubit/one_qubit_decompose.py @@ -161,29 +161,19 @@ def build_circuit(self, gates, global_phase) -> QuantumCircuit | DAGCircuit: if len(gates) > 0 and isinstance(gates[0], tuple): lookup_gate = True - if self.use_dag: - from qiskit.dagcircuit import dagcircuit - - dag = dagcircuit.DAGCircuit() - dag.global_phase = global_phase - dag.add_qubits(qr) - for gate_entry in gates: - if lookup_gate: - gate = NAME_MAP[gate_entry[0]](*gate_entry[1]) - else: - gate = gate_entry - - dag.apply_operation_back(gate, (qr[0],), check=False) - return dag - else: - circuit = QuantumCircuit(qr, global_phase=global_phase) - for gate_entry in gates: - if lookup_gate: - gate = NAME_MAP[gate_entry[0]](*gate_entry[1]) - else: - gate = gate_entry - circuit._append(gate, [qr[0]], []) - return circuit + from qiskit.dagcircuit import dagcircuit + + dag = dagcircuit.DAGCircuit() + dag.global_phase = global_phase + dag.add_qubits(qr) + for gate_entry in gates: + if lookup_gate: + gate = NAME_MAP[gate_entry[0].name](*gate_entry[1]) + else: + gate = gate_entry.name + + dag.apply_operation_back(gate, (qr[0],), check=False) + return dag def __call__( self, @@ -225,11 +215,17 @@ def __call__( return self._decompose(unitary, simplify=simplify, atol=atol) def _decompose(self, unitary, simplify=True, atol=DEFAULT_ATOL): - circuit_sequence = euler_one_qubit_decomposer.unitary_to_gate_sequence( - unitary, [self.basis], 0, None, simplify, atol + if self.use_dag: + circuit_sequence = euler_one_qubit_decomposer.unitary_to_gate_sequence( + unitary, [self.basis], 0, None, simplify, atol + ) + circuit = self.build_circuit(circuit_sequence, circuit_sequence.global_phase) + return circuit + return QuantumCircuit._from_circuit_data( + euler_one_qubit_decomposer.unitary_to_circuit( + unitary, [self.basis], 0, None, simplify, atol + ) ) - circuit = self.build_circuit(circuit_sequence, circuit_sequence.global_phase) - return circuit @property def basis(self): diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 3f8d07839c0f..02d36ff6f953 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -33,6 +33,7 @@ XGate, ) from qiskit.circuit import Qubit +from qiskit.circuit.quantumcircuitdata import CircuitInstruction from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.dagcircuit.dagnode import DAGOpNode @@ -149,10 +150,13 @@ def _gate_sequence_to_dag(self, best_synth_circuit): out_dag.global_phase = best_synth_circuit.global_phase for gate_name, angles in best_synth_circuit: - out_dag.apply_operation_back(NAME_MAP[gate_name](*angles), qubits, check=False) + op = CircuitInstruction(gate_name, qubits=qubits, params=angles) + out_dag.apply_operation_back(op.operation, qubits, check=False) return out_dag - def _substitution_checks(self, dag, old_run, new_circ, basis, qubit): + def _substitution_checks( + self, dag, old_run, new_circ, basis, qubit, old_error=None, new_error=None + ): """ Returns `True` when it is recommended to replace `old_run` with `new_circ` over `basis`. """ @@ -176,11 +180,14 @@ def _substitution_checks(self, dag, old_run, new_circ, basis, qubit): # if we're outside of the basis set, we're obligated to logically decompose. # if we're outside of the set of gates for which we have physical definitions, # then we _try_ to decompose, using the results if we see improvement. - new_error = 0.0 - old_error = 0.0 if not uncalibrated_and_not_basis_p: - new_error = self._error(new_circ, qubit) - old_error = self._error(old_run, qubit) + if new_error is None: + new_error = self._error(new_circ, qubit) + if old_error is None: + old_error = self._error(old_run, qubit) + else: + new_error = 0.0 + old_error = 0.0 return ( uncalibrated_and_not_basis_p @@ -198,32 +205,47 @@ def run(self, dag): Returns: DAGCircuit: the optimized DAG. """ - runs = dag.collect_1q_runs() - for run in runs: + runs = [] + qubits = [] + bases = [] + for run in dag.collect_1q_runs(): qubit = dag.find_bit(run[0].qargs[0]).index - operator = run[0].op.to_matrix() - for node in run[1:]: - operator = node.op.to_matrix().dot(operator) - best_circuit_sequence = self._resynthesize_run(operator, qubit) - if self._target is None: basis = self._basis_gates else: basis = self._target.operation_names_for_qargs((qubit,)) - - if best_circuit_sequence is not None and self._substitution_checks( - dag, run, best_circuit_sequence, basis, qubit - ): - for gate_name, angles in best_circuit_sequence: - op = NAME_MAP[gate_name](*angles) - node = DAGOpNode(NAME_MAP[gate_name](*angles), run[0].qargs, dag=dag) - node._node_id = dag._multi_graph.add_node(node) - dag._increment_op(op) - dag._multi_graph.insert_node_on_in_edges(node._node_id, run[0]._node_id) - dag.global_phase += best_circuit_sequence.global_phase - # Delete the other nodes in the run - for current_node in run: - dag.remove_op_node(current_node) + runs.append(run) + qubits.append(qubit) + bases.append(_possible_decomposers(basis)) + best_sequences = euler_one_qubit_decomposer.optimize_1q_gates_decomposition( + runs, qubits, bases, simplify=True, error_map=self.error_map + ) + for index, best_circuit_sequence in enumerate(best_sequences): + run = runs[index] + qubit = qubits[index] + if best_circuit_sequence is not None: + (old_error, new_error, best_circuit_sequence) = best_circuit_sequence + if self._substitution_checks( + dag, + run, + best_circuit_sequence, + basis, + qubit, + old_error=old_error, + new_error=new_error, + ): + first_node_id = run[0]._node_id + qubit = run[0].qargs + for gate, angles in best_circuit_sequence: + op = CircuitInstruction(gate, qubits=qubit, params=angles) + node = DAGOpNode.from_instruction(op, dag=dag) + node._node_id = dag._multi_graph.add_node(node) + dag._increment_op(gate.name) + dag._multi_graph.insert_node_on_in_edges(node._node_id, first_node_id) + dag.global_phase += best_circuit_sequence.global_phase + # Delete the other nodes in the run + for current_node in run: + dag.remove_op_node(current_node) return dag @@ -237,14 +259,13 @@ def _error(self, circuit, qubit): of circuit as a weak proxy for error. """ if isinstance(circuit, euler_one_qubit_decomposer.OneQubitGateSequence): + if not isinstance(qubit, int): + dag return euler_one_qubit_decomposer.compute_error_one_qubit_sequence( circuit, qubit, self.error_map ) else: - circuit_list = [(x.op.name, []) for x in circuit] - return euler_one_qubit_decomposer.compute_error_list( - circuit_list, qubit, self.error_map - ) + return euler_one_qubit_decomposer.compute_error_list(circuit, qubit, self.error_map) def _possible_decomposers(basis_set):