From 3fb175390d5fa007150fb76a23b914059010788a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 2 Jul 2024 15:13:48 -0400 Subject: [PATCH 01/15] Fix cargo clippy and rustfmt for Rust 1.79 (#12710) After the recently merged #12543 when working with Rust 1.79 cargo fmt makes a small formatting change that rust 1.70 wouldn't and clippy makes flags a &Vec<_> that should be a slice &[_] instead. This commit makes these two small chagnes so they're not an issue for people building with the latest stable version of rust, not just our MSRV. --- crates/accelerate/src/synthesis/permutation/utils.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/synthesis/permutation/utils.rs b/crates/accelerate/src/synthesis/permutation/utils.rs index 620ce4628741..47a9e1c3a7a9 100644 --- a/crates/accelerate/src/synthesis/permutation/utils.rs +++ b/crates/accelerate/src/synthesis/permutation/utils.rs @@ -120,8 +120,10 @@ pub fn pattern_to_cycles(pattern: &ArrayView1) -> Vec> { /// Periodic (or Python-like) access to a vector. /// Util used below in ``decompose_cycles``. #[inline] -fn pget(vec: &Vec, index: isize) -> Result { - let SequenceIndex::Int(wrapped) = PySequenceIndex::Int(index).with_len(vec.len())? else {unreachable!()}; +fn pget(vec: &[usize], index: isize) -> Result { + let SequenceIndex::Int(wrapped) = PySequenceIndex::Int(index).with_len(vec.len())? else { + unreachable!() + }; Ok(vec[wrapped]) } From bfc69a499c035bc3b8af31b37ba0469d7e89e67a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 3 Jul 2024 00:18:40 -0400 Subject: [PATCH 02/15] Use rust gates for Optimize1QGatesDecomposition (#12650) * 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. * Cache target decompositions for each qubit Previously this PR was re-computing the target bases to synthesize with for each run found in the circuit. But in cases where there were multiple runs repeated on a qubit this was unecessary work. Prior to moving this code to rust there was already caching code to make this optimization, but the rust path short circuited around this. This commit fixes this so we're caching the target bases for each qubit and only computing it once. * Optimize rust implementation slightly * Avoid extra allocations by inlining matrix multiplication * Remove unnecessary comment * Remove stray code block * Add import path for rust gate * Use rust gate in circuit constructor * Remove duplicated op_name getter and just use existing name getter * Apply suggestions from code review Co-authored-by: John Lapeyre * Simplify construction of target_basis_vec * Fix rebase issue * Update crates/accelerate/src/euler_one_qubit_decomposer.rs Co-authored-by: John Lapeyre * Update crates/accelerate/src/euler_one_qubit_decomposer.rs --------- Co-authored-by: John Lapeyre --- .../src/euler_one_qubit_decomposer.rs | 301 +++++++++++++++--- crates/accelerate/src/two_qubit_decompose.rs | 19 +- crates/circuit/src/dag_node.rs | 50 ++- crates/circuit/src/operations.rs | 6 + qiskit/dagcircuit/dagcircuit.py | 57 ++-- .../one_qubit/one_qubit_decompose.py | 50 ++- .../optimization/optimize_1q_decomposition.py | 105 +++--- 7 files changed, 431 insertions(+), 157 deletions(-) diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 01725269bb84..8c3a87ce51ec 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -18,7 +18,6 @@ use num_complex::{Complex64, ComplexFloat}; use smallvec::{smallvec, SmallVec}; use std::cmp::Ordering; use std::f64::consts::PI; -use std::ops::Deref; use std::str::FromStr; use pyo3::exceptions::PyValueError; @@ -31,8 +30,12 @@ use ndarray::prelude::*; use numpy::PyReadonlyArray2; use pyo3::pybacked::PyBackedStr; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::dag_node::DAGOpNode; +use qiskit_circuit::operations::{Operation, Param, StandardGate}; use qiskit_circuit::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::c64; +use qiskit_circuit::Qubit; pub const ANGLE_ZERO_EPSILON: f64 = 1e-12; @@ -68,12 +71,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 { @@ -115,15 +118,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, @@ -139,7 +142,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 { @@ -160,13 +163,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, @@ -190,7 +193,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, @@ -217,16 +220,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)], )); } @@ -255,7 +258,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, @@ -358,7 +361,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)], )); } @@ -366,12 +369,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)], )); } @@ -393,10 +396,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), @@ -411,11 +450,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( @@ -441,12 +482,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, @@ -471,12 +512,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, @@ -501,15 +544,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, @@ -633,7 +676,7 @@ fn compare_error_fn( let fidelity_product: f64 = circuit .gates .iter() - .map(|x| 1. - err_map.get(&x.0).unwrap_or(&0.)) + .map(|gate| 1. - err_map.get(gate.0.name()).unwrap_or(&0.)) .product(); (1. - fidelity_product, circuit.gates.len()) } @@ -642,6 +685,28 @@ 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(|gate| 1. - err_map.error_map[qubit].get(gate.0.name()).unwrap_or(&0.)) + .product(); + (1. - gate_fidelities, num_gates) + } + None => (gates.len() as f64, gates.len()), + } +} + +fn compute_error_term(gate: &str, error_map: &OneQubitGateErrorMap, qubit: usize) -> f64 { + 1. - error_map.error_map[qubit].get(gate).unwrap_or(&0.) +} + +fn compute_error_str( gates: &[(String, SmallVec<[f64; 3]>)], error_map: Option<&OneQubitGateErrorMap>, qubit: usize, @@ -651,7 +716,7 @@ fn compute_error( let num_gates = gates.len(); let gate_fidelities: f64 = gates .iter() - .map(|x| 1. - err_map.error_map[qubit].get(&x.0).unwrap_or(&0.)) + .map(|gate| compute_error_term(gate.0.as_str(), err_map, qubit)) .product(); (1. - gate_fidelities, num_gates) } @@ -670,11 +735,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] @@ -687,15 +761,13 @@ pub fn unitary_to_gate_sequence( 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 target_basis_vec: PyResult> = target_basis_list + .iter() + .map(|basis| EulerBasis::__new__(basis)) + .collect(); Ok(unitary_to_gate_sequence_inner( - unitary_mat, - &target_basis_vec, + unitary.as_array(), + &target_basis_vec?, qubit, error_map, simplify, @@ -725,6 +797,46 @@ 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 target_basis_vec: PyResult> = target_basis_list + .iter() + .map(|basis| EulerBasis::__new__(basis)) + .collect(); + let circuit_sequence = unitary_to_gate_sequence_inner( + unitary.as_array(), + &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]] @@ -853,6 +965,106 @@ 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 { + runs.iter() + .enumerate() + .map(|(index, raw_run)| -> OptimizeDecompositionReturn { + let mut error = match error_map { + Some(_) => 1., + None => raw_run.len() as f64, + }; + let qubit = qubits[index]; + let operator = &raw_run + .iter() + .map(|node| { + if let Some(err_map) = error_map { + error *= + compute_error_term(node.instruction.operation.name(), err_map, qubit) + } + node.instruction + .operation + .matrix(&node.instruction.params) + .expect("No matrix defined for operation") + }) + .fold( + [ + [Complex64::new(1., 0.), Complex64::new(0., 0.)], + [Complex64::new(0., 0.), Complex64::new(1., 0.)], + ], + |mut operator, node| { + matmul_1q(&mut operator, node); + operator + }, + ); + let old_error = if error_map.is_some() { + (1. - error, raw_run.len()) + } else { + (error, raw_run.len()) + }; + let target_basis_vec: Vec = bases[index] + .iter() + .map(|basis| EulerBasis::__new__(basis).unwrap()) + .collect(); + unitary_to_gate_sequence_inner( + aview2(operator), + &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() +} + +fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { + *operator = [ + [ + other[[0, 0]] * operator[0][0] + other[[0, 1]] * operator[1][0], + other[[0, 0]] * operator[0][1] + other[[0, 1]] * operator[1][1], + ], + [ + other[[1, 0]] * operator[0][0] + other[[1, 1]] * operator[1][0], + other[[1, 0]] * operator[0][1] + other[[1, 1]] * operator[1][1], + ], + ]; +} + +#[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() + && match &node.instruction.extra_attrs { + None => true, + Some(attrs) => attrs.condition.is_none(), + } + } + Err(_) => false, + } +} + #[pymodule] pub fn euler_one_qubit_decomposer(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(params_zyz))?; @@ -863,8 +1075,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 37061d5159f4..568206925c2b 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::slice::{PySequenceIndex, SequenceIndex}; use qiskit_circuit::util::{c64, GateArray1Q, GateArray2Q, C_M_ONE, C_ONE, C_ZERO, IM, M_IM}; @@ -1045,7 +1046,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( @@ -1058,7 +1059,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( @@ -1077,7 +1078,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( @@ -1090,7 +1091,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, @@ -1459,7 +1460,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])); } } } @@ -1847,13 +1848,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 } @@ -1861,13 +1862,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 ffd7920a36fd..44ba5f7a6bf0 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -18,7 +18,7 @@ use crate::operations::Operation; use numpy::IntoPyArray; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PySequence, PyString, PyTuple}; -use pyo3::{intern, IntoPy, PyObject, PyResult}; +use pyo3::{intern, IntoPy, PyObject, PyResult, ToPyObject}; use smallvec::smallvec; /// Parent class for DAGOpNode, DAGInNode, and DAGOutNode. @@ -135,6 +135,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(( @@ -206,8 +250,8 @@ impl DAGOpNode { /// Returns the Instruction name corresponding to the op for this node #[getter] - fn get_name(&self, py: Python) -> PyObject { - self.instruction.operation.name().to_object(py) + fn get_name(&self) -> &str { + self.instruction.operation.name() } #[getter] diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 8935e72e0ad5..77458e2fc35f 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -242,6 +242,12 @@ 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 diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index a0d2c42be17e..b93a90e47f7b 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. @@ -724,7 +725,7 @@ def _apply_op_node_back(self, node: DAGOpNode): additional = set(_additional_wires(node)).difference(node.cargs) node._node_id = self._multi_graph.add_node(node) - self._increment_op(node) + self._increment_op(node.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 @@ -780,7 +781,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 @@ -832,7 +833,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 @@ -1379,10 +1380,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.name) return new_node @@ -1593,7 +1594,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.name) variable_mapper = _classical_resource_map.VariableMapper( self.cregs.values(), wire_map, add_register=self.add_creg @@ -1624,7 +1625,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.name) return {k: self._multi_graph[v] for k, v in node_map.items()} @@ -1696,17 +1697,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.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.name: + self._increment_op(op.name) + self._decrement_op(node.name) return new_node def separable_circuits( @@ -1987,7 +1988,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.name) def remove_ancestors_of(self, node): """Remove all of the ancestor operation nodes of node.""" @@ -2152,19 +2153,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..04d95312aa6d 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 @@ -110,16 +111,7 @@ def _build_error_map(self): else: return None - def _resynthesize_run(self, matrix, qubit=None): - """ - Re-synthesizes one 2x2 `matrix`, typically extracted via `dag.collect_1q_runs`. - - Returns the newly synthesized circuit in the indicated basis, or None - if no synthesis routine applied. - - When multiple synthesis options are available, it prefers the one with the lowest - error when the circuit is applied to `qubit`. - """ + def _get_decomposer(self, qubit=None): # include path for when target exists but target.num_qubits is None (BasicSimulator) if self._target is not None and self._target.num_qubits is not None: if qubit is not None: @@ -133,6 +125,19 @@ def _resynthesize_run(self, matrix, qubit=None): decomposers = _possible_decomposers(available_1q_basis) else: decomposers = self._global_decomposers + return decomposers + + def _resynthesize_run(self, matrix, qubit=None): + """ + Re-synthesizes one 2x2 `matrix`, typically extracted via `dag.collect_1q_runs`. + + Returns the newly synthesized circuit in the indicated basis, or None + if no synthesis routine applied. + + When multiple synthesis options are available, it prefers the one with the lowest + error when the circuit is applied to `qubit`. + """ + decomposers = self._get_decomposer(qubit) best_synth_circuit = euler_one_qubit_decomposer.unitary_to_gate_sequence( matrix, @@ -149,10 +154,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 +184,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 +209,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) - + runs.append(run) + qubits.append(qubit) + bases.append(self._get_decomposer(qubit)) + 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 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) + 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 @@ -241,10 +267,7 @@ def _error(self, circuit, qubit): 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): From 0f585bd75d3a06445960cc4d19439e6b931094cc Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 3 Jul 2024 09:42:41 +0300 Subject: [PATCH 03/15] Port `synth_clifford_greedy` to Rust (#12601) * starting to experiment * porting code * messy code porting * printing statements to enable debugging * fixes * fixing phase * removing some of the printing statements * fixing inaccuracy for cost computation * Moving some of the functionality to SymplecticMatrix class * reducing the number of warnings * formatting * replacing expensive adjoint and compose operations for symplectic matrices by significantly cheaper in-place prepend operations * resolving merge conflicts * cleanup * code cleanup * cleanup * cleanup * cleanup * cleanup * cleanup * cleanup * cleanup * using fast lookup * cleanup * clippy * including params in gate_seq to avoid mapping * removing unnecessary inner function * cleanup * renaming * changes on the python side * reno * adding error handling * improved error handling * removing redundant Ok(Some(...)) * using random_clifford in tests * reorganizing clifford code * fixes * formatting * improved error handling * do not panic * formatting * Applying refactoring suggestions d/utils.rs from code review * release notes update * adding comment --- .../synthesis/clifford/greedy_synthesis.rs | 441 ++++++++++++++++++ .../accelerate/src/synthesis/clifford/mod.rs | 48 ++ .../src/synthesis/clifford/utils.rs | 289 ++++++++++++ crates/accelerate/src/synthesis/linear/mod.rs | 2 +- crates/accelerate/src/synthesis/mod.rs | 6 +- qiskit/__init__.py | 1 + .../clifford/clifford_decompose_greedy.py | 312 +------------ ...ynth-clifford-greedy-0739e9688bc4eedd.yaml | 6 + .../synthesis/test_clifford_sythesis.py | 4 +- 9 files changed, 801 insertions(+), 308 deletions(-) create mode 100644 crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs create mode 100644 crates/accelerate/src/synthesis/clifford/mod.rs create mode 100644 crates/accelerate/src/synthesis/clifford/utils.rs create mode 100644 releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml diff --git a/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs new file mode 100644 index 000000000000..e53e25282005 --- /dev/null +++ b/crates/accelerate/src/synthesis/clifford/greedy_synthesis.rs @@ -0,0 +1,441 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// 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 http://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 indexmap::IndexSet; +use ndarray::{s, ArrayView2}; +use smallvec::smallvec; + +use crate::synthesis::clifford::utils::CliffordGatesVec; +use crate::synthesis::clifford::utils::{adjust_final_pauli_gates, SymplecticMatrix}; +use qiskit_circuit::operations::StandardGate; +use qiskit_circuit::Qubit; + +/// Converts a pair of Paulis pauli_x and pauli_z acting on a specific qubit +/// to the corresponding index in [PauliPairsClass] or [SingleQubitGate] classes. +/// The input is given as a 4-tuple: (pauli_x stabilizer, pauli_x destabilizer, +/// pauli_z stabilizer, pauli_z destabilizer), and the output is an unsigned +/// integer from 0 to 15. +fn pauli_pair_to_index(xs: bool, xd: bool, zs: bool, zd: bool) -> usize { + ((xs as usize) << 3) | ((xd as usize) << 2) | ((zs as usize) << 1) | (zd as usize) +} + +/// The five classes of Pauli 2-qubit operators as described in the paper. +#[derive(Clone, Copy)] +enum PauliPairsClass { + ClassA, + ClassB, + ClassC, + ClassD, + ClassE, +} + +/// The 16 Pauli 2-qubit operators are divided into 5 equivalence classes +/// under the action of single-qubit Cliffords. +static PAULI_INDEX_TO_CLASS: [PauliPairsClass; 16] = [ + PauliPairsClass::ClassE, // 'II' + PauliPairsClass::ClassD, // 'IX' + PauliPairsClass::ClassD, // 'IZ' + PauliPairsClass::ClassD, // 'IY' + PauliPairsClass::ClassC, // 'XI' + PauliPairsClass::ClassB, // 'XX' + PauliPairsClass::ClassA, // 'XZ' + PauliPairsClass::ClassA, // 'XY' + PauliPairsClass::ClassC, // 'ZI' + PauliPairsClass::ClassA, // 'ZX' + PauliPairsClass::ClassB, // 'ZZ' + PauliPairsClass::ClassA, // 'ZY' + PauliPairsClass::ClassC, // 'YI' + PauliPairsClass::ClassA, // 'YX' + PauliPairsClass::ClassA, // 'YZ' + PauliPairsClass::ClassB, // 'YY' +]; + +/// Single-qubit Clifford gates modulo Paulis. +#[derive(Clone, Copy)] +enum SingleQubitGate { + GateI, + GateS, + GateH, + GateSH, + GateHS, + GateSHS, +} + +/// Maps pair of pauli operators to the single-qubit gate required +/// for the decoupling step. +static PAULI_INDEX_TO_1Q_GATE: [SingleQubitGate; 16] = [ + SingleQubitGate::GateI, // 'II' + SingleQubitGate::GateH, // 'IX' + SingleQubitGate::GateI, // 'IZ' + SingleQubitGate::GateSH, // 'IY' + SingleQubitGate::GateI, // 'XI' + SingleQubitGate::GateI, // 'XX' + SingleQubitGate::GateI, // 'XZ' + SingleQubitGate::GateSHS, // 'XY' + SingleQubitGate::GateH, // 'ZI' + SingleQubitGate::GateH, // 'ZX' + SingleQubitGate::GateH, // 'ZZ' + SingleQubitGate::GateSH, // 'ZY' + SingleQubitGate::GateS, // 'YI' + SingleQubitGate::GateHS, // 'YX' + SingleQubitGate::GateS, // 'YZ' + SingleQubitGate::GateS, // 'YY' +]; + +pub struct GreedyCliffordSynthesis<'a> { + /// The Clifford tableau to be synthesized. + tableau: ArrayView2<'a, bool>, + + /// The total number of qubits. + num_qubits: usize, + + /// Symplectic matrix being reduced. + symplectic_matrix: SymplecticMatrix, + + /// Unprocessed qubits. + unprocessed_qubits: IndexSet, +} + +impl GreedyCliffordSynthesis<'_> { + pub(crate) fn new(tableau: ArrayView2) -> Result, String> { + let tableau_shape = tableau.shape(); + if (tableau_shape[0] % 2 == 1) || (tableau_shape[1] != tableau_shape[0] + 1) { + return Err("The shape of the Clifford tableau is invalid".to_string()); + } + + let num_qubits = tableau_shape[0] / 2; + + // We are going to modify symplectic_matrix in-place until it + // becomes the identity. + let symplectic_matrix = SymplecticMatrix { + num_qubits, + smat: tableau.slice(s![.., 0..2 * num_qubits]).to_owned(), + }; + + let unprocessed_qubits: IndexSet = (0..num_qubits).collect(); + + Ok(GreedyCliffordSynthesis { + tableau, + num_qubits, + symplectic_matrix, + unprocessed_qubits, + }) + } + + /// Computes the CX cost of decoupling the symplectic matrix on the + /// given qubit. + fn compute_cost(&self, qubit: usize) -> Result { + let mut a_num = 0; + let mut b_num = 0; + let mut c_num = 0; + let mut d_num = 0; + + let mut qubit_is_in_a = false; + + for q in &self.unprocessed_qubits { + let pauli_pair_index = pauli_pair_to_index( + self.symplectic_matrix.smat[[*q, qubit + self.num_qubits]], + self.symplectic_matrix.smat[[*q + self.num_qubits, qubit + self.num_qubits]], + self.symplectic_matrix.smat[[*q, qubit]], + self.symplectic_matrix.smat[[*q + self.num_qubits, qubit]], + ); + let pauli_class = PAULI_INDEX_TO_CLASS[pauli_pair_index]; + + match pauli_class { + PauliPairsClass::ClassA => { + a_num += 1; + if *q == qubit { + qubit_is_in_a = true; + } + } + PauliPairsClass::ClassB => { + b_num += 1; + } + PauliPairsClass::ClassC => { + c_num += 1; + } + PauliPairsClass::ClassD => { + d_num += 1; + } + PauliPairsClass::ClassE => {} + } + } + + if a_num % 2 == 0 { + return Err("Symplectic Gaussian elimination failed.".to_string()); + } + + let mut cnot_cost: usize = + 3 * (a_num - 1) / 2 + (b_num + 1) * ((b_num > 0) as usize) + c_num + d_num; + + if !qubit_is_in_a { + cnot_cost += 3; + } + + Ok(cnot_cost) + } + + /// Calculate a decoupling operator D: + /// D^{-1} * Ox * D = x1 + /// D^{-1} * Oz * D = z1 + /// and reduces the clifford such that it will act trivially on min_qubit. + fn decouple_qubit( + &mut self, + gate_seq: &mut CliffordGatesVec, + min_qubit: usize, + ) -> Result<(), String> { + let mut a_qubits = IndexSet::new(); + let mut b_qubits = IndexSet::new(); + let mut c_qubits = IndexSet::new(); + let mut d_qubits = IndexSet::new(); + + for qubit in &self.unprocessed_qubits { + let pauli_pair_index = pauli_pair_to_index( + self.symplectic_matrix.smat[[*qubit, min_qubit + self.num_qubits]], + self.symplectic_matrix.smat + [[*qubit + self.num_qubits, min_qubit + self.num_qubits]], + self.symplectic_matrix.smat[[*qubit, min_qubit]], + self.symplectic_matrix.smat[[*qubit + self.num_qubits, min_qubit]], + ); + + let single_qubit_gate = PAULI_INDEX_TO_1Q_GATE[pauli_pair_index]; + match single_qubit_gate { + SingleQubitGate::GateS => { + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_s(*qubit); + } + SingleQubitGate::GateH => { + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_h(*qubit); + } + SingleQubitGate::GateSH => { + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_s(*qubit); + self.symplectic_matrix.prepend_h(*qubit); + } + SingleQubitGate::GateHS => { + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_h(*qubit); + self.symplectic_matrix.prepend_s(*qubit); + } + SingleQubitGate::GateSHS => { + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + gate_seq.push(( + StandardGate::SGate, + smallvec![], + smallvec![Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_s(*qubit); + self.symplectic_matrix.prepend_h(*qubit); + self.symplectic_matrix.prepend_s(*qubit); + } + SingleQubitGate::GateI => {} + } + + let pauli_class = PAULI_INDEX_TO_CLASS[pauli_pair_index]; + match pauli_class { + PauliPairsClass::ClassA => { + a_qubits.insert(*qubit); + } + PauliPairsClass::ClassB => { + b_qubits.insert(*qubit); + } + PauliPairsClass::ClassC => { + c_qubits.insert(*qubit); + } + PauliPairsClass::ClassD => { + d_qubits.insert(*qubit); + } + PauliPairsClass::ClassE => {} + } + } + + if a_qubits.len() % 2 != 1 { + return Err("Symplectic Gaussian elimination failed.".to_string()); + } + + if !a_qubits.contains(&min_qubit) { + let qubit_a = a_qubits[0]; + gate_seq.push(( + StandardGate::SwapGate, + smallvec![], + smallvec![Qubit(min_qubit as u32), Qubit(qubit_a as u32)], + )); + self.symplectic_matrix.prepend_swap(min_qubit, qubit_a); + + if b_qubits.contains(&min_qubit) { + b_qubits.swap_remove(&min_qubit); + b_qubits.insert(qubit_a); + } else if c_qubits.contains(&min_qubit) { + c_qubits.swap_remove(&min_qubit); + c_qubits.insert(qubit_a); + } else if d_qubits.contains(&min_qubit) { + d_qubits.swap_remove(&min_qubit); + d_qubits.insert(qubit_a); + } + + a_qubits.swap_remove(&qubit_a); + a_qubits.insert(min_qubit); + } + + for qubit in c_qubits { + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(min_qubit as u32), Qubit(qubit as u32)], + )); + self.symplectic_matrix.prepend_cx(min_qubit, qubit); + } + + for qubit in d_qubits { + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(qubit as u32), Qubit(min_qubit as u32)], + )); + self.symplectic_matrix.prepend_cx(qubit, min_qubit); + } + + if b_qubits.len() > 1 { + let qubit_b = b_qubits[0]; + for qubit in &b_qubits[1..] { + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(qubit_b as u32), Qubit(*qubit as u32)], + )); + self.symplectic_matrix.prepend_cx(qubit_b, *qubit); + } + } + + if !b_qubits.is_empty() { + let qubit_b = b_qubits[0]; + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(min_qubit as u32), Qubit(qubit_b as u32)], + )); + self.symplectic_matrix.prepend_cx(min_qubit, qubit_b); + + gate_seq.push(( + StandardGate::HGate, + smallvec![], + smallvec![Qubit(qubit_b as u32)], + )); + self.symplectic_matrix.prepend_h(qubit_b); + + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(qubit_b as u32), Qubit(min_qubit as u32)], + )); + self.symplectic_matrix.prepend_cx(qubit_b, min_qubit); + } + + let a_len: usize = (a_qubits.len() - 1) / 2; + if a_len > 0 { + a_qubits.swap_remove(&min_qubit); + } + + for qubit in 0..a_len { + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![ + Qubit(a_qubits[2 * qubit + 1] as u32), + Qubit(a_qubits[2 * qubit] as u32) + ], + )); + self.symplectic_matrix + .prepend_cx(a_qubits[2 * qubit + 1], a_qubits[2 * qubit]); + + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(a_qubits[2 * qubit] as u32), Qubit(min_qubit as u32)], + )); + self.symplectic_matrix + .prepend_cx(a_qubits[2 * qubit], min_qubit); + + gate_seq.push(( + StandardGate::CXGate, + smallvec![], + smallvec![ + Qubit(min_qubit as u32), + Qubit(a_qubits[2 * qubit + 1] as u32) + ], + )); + self.symplectic_matrix + .prepend_cx(min_qubit, a_qubits[2 * qubit + 1]); + } + + Ok(()) + } + + /// The main synthesis function. + pub(crate) fn run(&mut self) -> Result<(usize, CliffordGatesVec), String> { + let mut clifford_gates = CliffordGatesVec::new(); + + while !self.unprocessed_qubits.is_empty() { + let costs: Vec<(usize, usize)> = self + .unprocessed_qubits + .iter() + .map(|q| self.compute_cost(*q).map(|cost| (cost, *q))) + .collect::, _>>()?; + + let min_cost_qubit = costs.iter().min_by_key(|(cost, _)| cost).unwrap().1; + + self.decouple_qubit(&mut clifford_gates, min_cost_qubit)?; + + self.unprocessed_qubits.swap_remove(&min_cost_qubit); + } + + adjust_final_pauli_gates(&mut clifford_gates, self.tableau, self.num_qubits)?; + + Ok((self.num_qubits, clifford_gates)) + } +} diff --git a/crates/accelerate/src/synthesis/clifford/mod.rs b/crates/accelerate/src/synthesis/clifford/mod.rs new file mode 100644 index 000000000000..6772228acf88 --- /dev/null +++ b/crates/accelerate/src/synthesis/clifford/mod.rs @@ -0,0 +1,48 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// 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 http://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. + +mod greedy_synthesis; +mod utils; + +use crate::synthesis::clifford::greedy_synthesis::GreedyCliffordSynthesis; +use crate::QiskitError; +use numpy::PyReadonlyArray2; +use pyo3::prelude::*; +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::Param; + +/// Create a circuit that synthesizes a given Clifford operator represented as a tableau. +/// +/// This is an implementation of the "greedy Clifford compiler" presented in +/// Appendix A of the paper "Clifford Circuit Optimization with Templates and Symbolic +/// Pauli Gates" by Bravyi, Shaydulin, Hu, and Maslov (2021), ``__. +/// +/// This method typically yields better CX cost compared to the Aaronson-Gottesman method. +/// +/// Note that this function only implements the greedy Clifford compiler and not the +/// templates and symbolic Pauli gates optimizations that are also described in the paper. +#[pyfunction] +#[pyo3(signature = (clifford))] +fn synth_clifford_greedy(py: Python, clifford: PyReadonlyArray2) -> PyResult { + let tableau = clifford.as_array(); + let mut greedy_synthesis = + GreedyCliffordSynthesis::new(tableau.view()).map_err(QiskitError::new_err)?; + let (num_qubits, clifford_gates) = greedy_synthesis.run().map_err(QiskitError::new_err)?; + + CircuitData::from_standard_gates(py, num_qubits as u32, clifford_gates, Param::Float(0.0)) +} + +#[pymodule] +pub fn clifford(m: &Bound) -> PyResult<()> { + m.add_function(wrap_pyfunction!(synth_clifford_greedy, m)?)?; + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/clifford/utils.rs b/crates/accelerate/src/synthesis/clifford/utils.rs new file mode 100644 index 000000000000..766d84ed179d --- /dev/null +++ b/crates/accelerate/src/synthesis/clifford/utils.rs @@ -0,0 +1,289 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// 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 http://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::synthesis::linear::utils::calc_inverse_matrix_inner; +use ndarray::{azip, s, Array1, Array2, ArrayView2}; +use qiskit_circuit::operations::{Param, StandardGate}; +use qiskit_circuit::Qubit; +use smallvec::{smallvec, SmallVec}; + +/// Symplectic matrix. +/// Currently this class is internal to the synthesis library. +pub struct SymplecticMatrix { + /// Number of qubits. + pub num_qubits: usize, + /// Matrix with dimensions (2 * num_qubits) x (2 * num_qubits). + pub smat: Array2, +} + +/// Clifford. +/// Currently this class is internal to the synthesis library and +/// has a very different functionality from Qiskit's python-based +/// Clifford class. +pub struct Clifford { + /// Number of qubits. + pub num_qubits: usize, + /// Matrix with dimensions (2 * num_qubits) x (2 * num_qubits + 1). + pub tableau: Array2, +} + +impl SymplecticMatrix { + /// Modifies the matrix in-place by appending S-gate + #[allow(dead_code)] + pub fn append_s(&mut self, qubit: usize) { + let (x, mut z) = self + .smat + .multi_slice_mut((s![.., qubit], s![.., self.num_qubits + qubit])); + azip!((z in &mut z, &x in &x) *z ^= x); + } + + /// Modifies the matrix in-place by prepending S-gate + pub fn prepend_s(&mut self, qubit: usize) { + let (x, mut z) = self + .smat + .multi_slice_mut((s![self.num_qubits + qubit, ..], s![qubit, ..])); + azip!((z in &mut z, &x in &x) *z ^= x); + } + + /// Modifies the matrix in-place by appending H-gate + #[allow(dead_code)] + pub fn append_h(&mut self, qubit: usize) { + let (mut x, mut z) = self + .smat + .multi_slice_mut((s![.., qubit], s![.., self.num_qubits + qubit])); + azip!((x in &mut x, z in &mut z) (*x, *z) = (*z, *x)); + } + + /// Modifies the matrix in-place by prepending H-gate + pub fn prepend_h(&mut self, qubit: usize) { + let (mut x, mut z) = self + .smat + .multi_slice_mut((s![qubit, ..], s![self.num_qubits + qubit, ..])); + azip!((x in &mut x, z in &mut z) (*x, *z) = (*z, *x)); + } + + /// Modifies the matrix in-place by appending SWAP-gate + #[allow(dead_code)] + pub fn append_swap(&mut self, qubit0: usize, qubit1: usize) { + let (mut x0, mut z0, mut x1, mut z1) = self.smat.multi_slice_mut(( + s![.., qubit0], + s![.., self.num_qubits + qubit0], + s![.., qubit1], + s![.., self.num_qubits + qubit1], + )); + azip!((x0 in &mut x0, x1 in &mut x1) (*x0, *x1) = (*x1, *x0)); + azip!((z0 in &mut z0, z1 in &mut z1) (*z0, *z1) = (*z1, *z0)); + } + + /// Modifies the matrix in-place by prepending SWAP-gate + pub fn prepend_swap(&mut self, qubit0: usize, qubit1: usize) { + let (mut x0, mut z0, mut x1, mut z1) = self.smat.multi_slice_mut(( + s![qubit0, ..], + s![self.num_qubits + qubit0, ..], + s![qubit1, ..], + s![self.num_qubits + qubit1, ..], + )); + azip!((x0 in &mut x0, x1 in &mut x1) (*x0, *x1) = (*x1, *x0)); + azip!((z0 in &mut z0, z1 in &mut z1) (*z0, *z1) = (*z1, *z0)); + } + + /// Modifies the matrix in-place by appending CX-gate + #[allow(dead_code)] + pub fn append_cx(&mut self, qubit0: usize, qubit1: usize) { + let (x0, mut z0, mut x1, z1) = self.smat.multi_slice_mut(( + s![.., qubit0], + s![.., self.num_qubits + qubit0], + s![.., qubit1], + s![.., self.num_qubits + qubit1], + )); + azip!((x1 in &mut x1, &x0 in &x0) *x1 ^= x0); + azip!((z0 in &mut z0, &z1 in &z1) *z0 ^= z1); + } + + /// Modifies the matrix in-place by prepending CX-gate + pub fn prepend_cx(&mut self, qubit0: usize, qubit1: usize) { + let (x0, mut z0, mut x1, z1) = self.smat.multi_slice_mut(( + s![qubit1, ..], + s![self.num_qubits + qubit1, ..], + s![qubit0, ..], + s![self.num_qubits + qubit0, ..], + )); + azip!((x1 in &mut x1, &x0 in &x0) *x1 ^= x0); + azip!((z0 in &mut z0, &z1 in &z1) *z0 ^= z1); + } +} + +impl Clifford { + /// Modifies the tableau in-place by appending S-gate + pub fn append_s(&mut self, qubit: usize) { + let (x, mut z, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit], + s![.., self.num_qubits + qubit], + s![.., 2 * self.num_qubits], + )); + + azip!((p in &mut p, &x in &x, &z in &z) *p ^= x & z); + azip!((z in &mut z, &x in &x) *z ^= x); + } + + /// Modifies the tableau in-place by appending Sdg-gate + #[allow(dead_code)] + pub fn append_sdg(&mut self, qubit: usize) { + let (x, mut z, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit], + s![.., self.num_qubits + qubit], + s![.., 2 * self.num_qubits], + )); + + azip!((p in &mut p, &x in &x, &z in &z) *p ^= x & !z); + azip!((z in &mut z, &x in &x) *z ^= x); + } + + /// Modifies the tableau in-place by appending H-gate + pub fn append_h(&mut self, qubit: usize) { + let (mut x, mut z, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit], + s![.., self.num_qubits + qubit], + s![.., 2 * self.num_qubits], + )); + + azip!((p in &mut p, &x in &x, &z in &z) *p ^= x & z); + azip!((x in &mut x, z in &mut z) (*x, *z) = (*z, *x)); + } + + /// Modifies the tableau in-place by appending SWAP-gate + pub fn append_swap(&mut self, qubit0: usize, qubit1: usize) { + let (mut x0, mut z0, mut x1, mut z1) = self.tableau.multi_slice_mut(( + s![.., qubit0], + s![.., self.num_qubits + qubit0], + s![.., qubit1], + s![.., self.num_qubits + qubit1], + )); + azip!((x0 in &mut x0, x1 in &mut x1) (*x0, *x1) = (*x1, *x0)); + azip!((z0 in &mut z0, z1 in &mut z1) (*z0, *z1) = (*z1, *z0)); + } + + /// Modifies the tableau in-place by appending CX-gate + pub fn append_cx(&mut self, qubit0: usize, qubit1: usize) { + let (x0, mut z0, mut x1, z1, mut p) = self.tableau.multi_slice_mut(( + s![.., qubit0], + s![.., self.num_qubits + qubit0], + s![.., qubit1], + s![.., self.num_qubits + qubit1], + s![.., 2 * self.num_qubits], + )); + azip!((p in &mut p, &x0 in &x0, &z0 in &z0, &x1 in &x1, &z1 in &z1) *p ^= (x1 ^ z0 ^ true) & z1 & x0); + azip!((x1 in &mut x1, &x0 in &x0) *x1 ^= x0); + azip!((z0 in &mut z0, &z1 in &z1) *z0 ^= z1); + } + + /// Creates a Clifford from a given sequence of Clifford gates. + /// In essence, starts from the identity tableau and modifies it + /// based on the gates in the sequence. + pub fn from_gate_sequence( + gate_seq: &CliffordGatesVec, + num_qubits: usize, + ) -> Result { + // create the identity + let mut clifford = Clifford { + num_qubits, + tableau: Array2::from_shape_fn((2 * num_qubits, 2 * num_qubits + 1), |(i, j)| i == j), + }; + + gate_seq + .iter() + .try_for_each(|(gate, _params, qubits)| match *gate { + StandardGate::SGate => { + clifford.append_s(qubits[0].0 as usize); + Ok(()) + } + StandardGate::HGate => { + clifford.append_h(qubits[0].0 as usize); + Ok(()) + } + StandardGate::CXGate => { + clifford.append_cx(qubits[0].0 as usize, qubits[1].0 as usize); + Ok(()) + } + StandardGate::SwapGate => { + clifford.append_swap(qubits[0].0 as usize, qubits[1].0 as usize); + Ok(()) + } + _ => Err(format!("Unsupported gate {:?}", gate)), + })?; + Ok(clifford) + } +} + +/// A sequence of Clifford gates. +/// Represents the return type of Clifford synthesis algorithms. +pub type CliffordGatesVec = Vec<(StandardGate, SmallVec<[Param; 3]>, SmallVec<[Qubit; 2]>)>; + +/// Given a sequence of Clifford gates that correctly implements the symplectic matrix +/// of the target clifford tableau, adds the Pauli gates to also match the phase of +/// the tableau. +pub fn adjust_final_pauli_gates( + gate_seq: &mut CliffordGatesVec, + target_tableau: ArrayView2, + num_qubits: usize, +) -> Result<(), String> { + // simulate the clifford circuit that we have constructed + let simulated_clifford = Clifford::from_gate_sequence(gate_seq, num_qubits)?; + + // compute the phase difference + let target_phase = target_tableau.column(2 * num_qubits); + let sim_phase = simulated_clifford.tableau.column(2 * num_qubits); + + let delta_phase: Vec = target_phase + .iter() + .zip(sim_phase.iter()) + .map(|(&a, &b)| a ^ b) + .collect(); + + // compute inverse of the symplectic matrix + let smat = target_tableau.slice(s![.., ..2 * num_qubits]); + let smat_inv = calc_inverse_matrix_inner(smat, false)?; + + // compute smat_inv * delta_phase + let arr1 = smat_inv.map(|v| *v as usize); + let vec2: Vec = delta_phase.into_iter().map(|v| v as usize).collect(); + let arr2 = Array1::from(vec2); + let delta_phase_pre = arr1.dot(&arr2).map(|v| v % 2 == 1); + + // add pauli gates + for qubit in 0..num_qubits { + if delta_phase_pre[qubit] && delta_phase_pre[qubit + num_qubits] { + // println!("=> Adding Y-gate on {}", qubit); + gate_seq.push(( + StandardGate::YGate, + smallvec![], + smallvec![Qubit(qubit as u32)], + )); + } else if delta_phase_pre[qubit] { + // println!("=> Adding Z-gate on {}", qubit); + gate_seq.push(( + StandardGate::ZGate, + smallvec![], + smallvec![Qubit(qubit as u32)], + )); + } else if delta_phase_pre[qubit + num_qubits] { + // println!("=> Adding X-gate on {}", qubit); + gate_seq.push(( + StandardGate::XGate, + smallvec![], + smallvec![Qubit(qubit as u32)], + )); + } + } + + Ok(()) +} diff --git a/crates/accelerate/src/synthesis/linear/mod.rs b/crates/accelerate/src/synthesis/linear/mod.rs index 2fa158ea761f..b184a170fa5f 100644 --- a/crates/accelerate/src/synthesis/linear/mod.rs +++ b/crates/accelerate/src/synthesis/linear/mod.rs @@ -14,7 +14,7 @@ use crate::QiskitError; use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2, PyReadwriteArray2}; use pyo3::prelude::*; -mod utils; +pub mod utils; #[pyfunction] #[pyo3(signature = (mat, ncols=None, full_elim=false))] diff --git a/crates/accelerate/src/synthesis/mod.rs b/crates/accelerate/src/synthesis/mod.rs index db28751437f6..1b9908ef80cf 100644 --- a/crates/accelerate/src/synthesis/mod.rs +++ b/crates/accelerate/src/synthesis/mod.rs @@ -10,7 +10,8 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -mod linear; +mod clifford; +pub mod linear; mod permutation; use pyo3::prelude::*; @@ -18,7 +19,8 @@ use pyo3::wrap_pymodule; #[pymodule] pub fn synthesis(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pymodule!(permutation::permutation))?; m.add_wrapped(wrap_pymodule!(linear::linear))?; + m.add_wrapped(wrap_pymodule!(permutation::permutation))?; + m.add_wrapped(wrap_pymodule!(clifford::clifford))?; Ok(()) } diff --git a/qiskit/__init__.py b/qiskit/__init__.py index aca555da8cb8..9aaa7a76a68e 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -82,6 +82,7 @@ sys.modules["qiskit._accelerate.vf2_layout"] = _accelerate.vf2_layout sys.modules["qiskit._accelerate.synthesis.permutation"] = _accelerate.synthesis.permutation sys.modules["qiskit._accelerate.synthesis.linear"] = _accelerate.synthesis.linear +sys.modules["qiskit._accelerate.synthesis.clifford"] = _accelerate.synthesis.clifford from qiskit.exceptions import QiskitError, MissingOptionalLibraryError diff --git a/qiskit/synthesis/clifford/clifford_decompose_greedy.py b/qiskit/synthesis/clifford/clifford_decompose_greedy.py index 784e6706d62e..0a679a8a7a6f 100644 --- a/qiskit/synthesis/clifford/clifford_decompose_greedy.py +++ b/qiskit/synthesis/clifford/clifford_decompose_greedy.py @@ -12,22 +12,16 @@ """ Circuit synthesis for the Clifford class. """ -# pylint: disable=invalid-name # --------------------------------------------------------------------- # Synthesis based on Bravyi et. al. greedy clifford compiler # --------------------------------------------------------------------- - -import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.quantum_info import Clifford, Pauli -from qiskit.quantum_info.operators.symplectic.clifford_circuits import ( - _append_cx, - _append_h, - _append_s, - _append_swap, +from qiskit.quantum_info import Clifford + +from qiskit._accelerate.synthesis.clifford import ( + synth_clifford_greedy as synth_clifford_greedy_inner, ) @@ -56,296 +50,8 @@ def synth_clifford_greedy(clifford: Clifford) -> QuantumCircuit: *Clifford Circuit Optimization with Templates and Symbolic Pauli Gates*, `arXiv:2105.02291 [quant-ph] `_ """ - - num_qubits = clifford.num_qubits - circ = QuantumCircuit(num_qubits, name=str(clifford)) - qubit_list = list(range(num_qubits)) - clifford_cpy = clifford.copy() - - # Reducing the original Clifford to identity - # via symplectic Gaussian elimination - while len(qubit_list) > 0: - # Calculate the adjoint of clifford_cpy without the phase - clifford_adj = clifford_cpy.copy() - tmp = clifford_adj.destab_x.copy() - clifford_adj.destab_x = clifford_adj.stab_z.T - clifford_adj.destab_z = clifford_adj.destab_z.T - clifford_adj.stab_x = clifford_adj.stab_x.T - clifford_adj.stab_z = tmp.T - - list_greedy_cost = [] - for qubit in qubit_list: - pauli_x = Pauli("I" * (num_qubits - qubit - 1) + "X" + "I" * qubit) - pauli_x = pauli_x.evolve(clifford_adj, frame="s") - - pauli_z = Pauli("I" * (num_qubits - qubit - 1) + "Z" + "I" * qubit) - pauli_z = pauli_z.evolve(clifford_adj, frame="s") - list_pairs = [] - pauli_count = 0 - - # Compute the CNOT cost in order to find the qubit with the minimal cost - for i in qubit_list: - typeq = _from_pair_paulis_to_type(pauli_x, pauli_z, i) - list_pairs.append(typeq) - pauli_count += 1 - cost = _compute_greedy_cost(list_pairs) - list_greedy_cost.append([cost, qubit]) - - _, min_qubit = (sorted(list_greedy_cost))[0] - - # Gaussian elimination step for the qubit with minimal CNOT cost - pauli_x = Pauli("I" * (num_qubits - min_qubit - 1) + "X" + "I" * min_qubit) - pauli_x = pauli_x.evolve(clifford_adj, frame="s") - - pauli_z = Pauli("I" * (num_qubits - min_qubit - 1) + "Z" + "I" * min_qubit) - pauli_z = pauli_z.evolve(clifford_adj, frame="s") - - # Compute the decoupling operator of cliff_ox and cliff_oz - decouple_circ, decouple_cliff = _calc_decoupling( - pauli_x, pauli_z, qubit_list, min_qubit, num_qubits, clifford_cpy - ) - circ = circ.compose(decouple_circ) - - # Now the clifford acts trivially on min_qubit - clifford_cpy = decouple_cliff.adjoint().compose(clifford_cpy) - qubit_list.remove(min_qubit) - - # Add the phases (Pauli gates) to the Clifford circuit - for qubit in range(num_qubits): - stab = clifford_cpy.stab_phase[qubit] - destab = clifford_cpy.destab_phase[qubit] - if destab and stab: - circ.y(qubit) - elif not destab and stab: - circ.x(qubit) - elif destab and not stab: - circ.z(qubit) - - return circ - - -# --------------------------------------------------------------------- -# Helper functions for Bravyi et. al. greedy clifford compiler -# --------------------------------------------------------------------- - -# Global arrays of the 16 pairs of Pauli operators -# divided into 5 equivalence classes under the action of single-qubit Cliffords - -# Class A - canonical representative is 'XZ' -A_class = [ - [[False, True], [True, True]], # 'XY' - [[False, True], [True, False]], # 'XZ' - [[True, True], [False, True]], # 'YX' - [[True, True], [True, False]], # 'YZ' - [[True, False], [False, True]], # 'ZX' - [[True, False], [True, True]], -] # 'ZY' - -# Class B - canonical representative is 'XX' -B_class = [ - [[True, False], [True, False]], # 'ZZ' - [[False, True], [False, True]], # 'XX' - [[True, True], [True, True]], -] # 'YY' - -# Class C - canonical representative is 'XI' -C_class = [ - [[True, False], [False, False]], # 'ZI' - [[False, True], [False, False]], # 'XI' - [[True, True], [False, False]], -] # 'YI' - -# Class D - canonical representative is 'IZ' -D_class = [ - [[False, False], [False, True]], # 'IX' - [[False, False], [True, False]], # 'IZ' - [[False, False], [True, True]], -] # 'IY' - -# Class E - only 'II' -E_class = [[[False, False], [False, False]]] # 'II' - - -def _from_pair_paulis_to_type(pauli_x, pauli_z, qubit): - """Converts a pair of Paulis pauli_x and pauli_z into a type""" - - type_x = [pauli_x.z[qubit], pauli_x.x[qubit]] - type_z = [pauli_z.z[qubit], pauli_z.x[qubit]] - return [type_x, type_z] - - -def _compute_greedy_cost(list_pairs): - """Compute the CNOT cost of one step of the algorithm""" - - A_num = 0 - B_num = 0 - C_num = 0 - D_num = 0 - - for pair in list_pairs: - if pair in A_class: - A_num += 1 - elif pair in B_class: - B_num += 1 - elif pair in C_class: - C_num += 1 - elif pair in D_class: - D_num += 1 - - if (A_num % 2) == 0: - raise QiskitError("Symplectic Gaussian elimination fails.") - - # Calculate the CNOT cost - cost = 3 * (A_num - 1) / 2 + (B_num + 1) * (B_num > 0) + C_num + D_num - if list_pairs[0] not in A_class: # additional SWAP - cost += 3 - - return cost - - -def _calc_decoupling(pauli_x, pauli_z, qubit_list, min_qubit, num_qubits, cliff): - """Calculate a decoupling operator D: - D^{-1} * Ox * D = x1 - D^{-1} * Oz * D = z1 - and reduce the clifford such that it will act trivially on min_qubit - """ - - circ = QuantumCircuit(num_qubits) - - # decouple_cliff is initialized to an identity clifford - decouple_cliff = cliff.copy() - num_qubits = decouple_cliff.num_qubits - decouple_cliff.phase = np.zeros(2 * num_qubits) - decouple_cliff.symplectic_matrix = np.eye(2 * num_qubits) - - qubit0 = min_qubit # The qubit for the symplectic Gaussian elimination - - # Reduce the pair of Paulis to a representative in the equivalence class - # ['XZ', 'XX', 'XI', 'IZ', 'II'] by adding single-qubit gates - for qubit in qubit_list: - - typeq = _from_pair_paulis_to_type(pauli_x, pauli_z, qubit) - - if typeq in [ - [[True, True], [False, False]], # 'YI' - [[True, True], [True, True]], # 'YY' - [[True, True], [True, False]], - ]: # 'YZ': - circ.s(qubit) - _append_s(decouple_cliff, qubit) - - elif typeq in [ - [[True, False], [False, False]], # 'ZI' - [[True, False], [True, False]], # 'ZZ' - [[True, False], [False, True]], # 'ZX' - [[False, False], [False, True]], - ]: # 'IX' - circ.h(qubit) - _append_h(decouple_cliff, qubit) - - elif typeq in [ - [[False, False], [True, True]], # 'IY' - [[True, False], [True, True]], - ]: # 'ZY' - circ.s(qubit) - circ.h(qubit) - _append_s(decouple_cliff, qubit) - _append_h(decouple_cliff, qubit) - - elif typeq == [[True, True], [False, True]]: # 'YX' - circ.h(qubit) - circ.s(qubit) - _append_h(decouple_cliff, qubit) - _append_s(decouple_cliff, qubit) - - elif typeq == [[False, True], [True, True]]: # 'XY' - circ.s(qubit) - circ.h(qubit) - circ.s(qubit) - _append_s(decouple_cliff, qubit) - _append_h(decouple_cliff, qubit) - _append_s(decouple_cliff, qubit) - - # Reducing each pair of Paulis (except of qubit0) to 'II' - # by adding two-qubit gates and single-qubit gates - A_qubits = [] - B_qubits = [] - C_qubits = [] - D_qubits = [] - - for qubit in qubit_list: - typeq = _from_pair_paulis_to_type(pauli_x, pauli_z, qubit) - if typeq in A_class: - A_qubits.append(qubit) - elif typeq in B_class: - B_qubits.append(qubit) - elif typeq in C_class: - C_qubits.append(qubit) - elif typeq in D_class: - D_qubits.append(qubit) - - if len(A_qubits) % 2 != 1: - raise QiskitError("Symplectic Gaussian elimination fails.") - - if qubit0 not in A_qubits: # SWAP qubit0 and qubitA - qubitA = A_qubits[0] - circ.swap(qubit0, qubitA) - _append_swap(decouple_cliff, qubit0, qubitA) - if qubit0 in B_qubits: - B_qubits.remove(qubit0) - B_qubits.append(qubitA) - A_qubits.remove(qubitA) - A_qubits.append(qubit0) - elif qubit0 in C_qubits: - C_qubits.remove(qubit0) - C_qubits.append(qubitA) - A_qubits.remove(qubitA) - A_qubits.append(qubit0) - elif qubit0 in D_qubits: - D_qubits.remove(qubit0) - D_qubits.append(qubitA) - A_qubits.remove(qubitA) - A_qubits.append(qubit0) - else: - A_qubits.remove(qubitA) - A_qubits.append(qubit0) - - # Reduce pairs in Class C to 'II' - for qubit in C_qubits: - circ.cx(qubit0, qubit) - _append_cx(decouple_cliff, qubit0, qubit) - - # Reduce pairs in Class D to 'II' - for qubit in D_qubits: - circ.cx(qubit, qubit0) - _append_cx(decouple_cliff, qubit, qubit0) - - # Reduce pairs in Class B to 'II' - if len(B_qubits) > 1: - for qubit in B_qubits[1:]: - qubitB = B_qubits[0] - circ.cx(qubitB, qubit) - _append_cx(decouple_cliff, qubitB, qubit) - - if len(B_qubits) > 0: - qubitB = B_qubits[0] - circ.cx(qubit0, qubitB) - circ.h(qubitB) - circ.cx(qubitB, qubit0) - _append_cx(decouple_cliff, qubit0, qubitB) - _append_h(decouple_cliff, qubitB) - _append_cx(decouple_cliff, qubitB, qubit0) - - # Reduce pairs in Class A (except of qubit0) to 'II' - Alen = int((len(A_qubits) - 1) / 2) - if Alen > 0: - A_qubits.remove(qubit0) - for qubit in range(Alen): - circ.cx(A_qubits[2 * qubit + 1], A_qubits[2 * qubit]) - circ.cx(A_qubits[2 * qubit], qubit0) - circ.cx(qubit0, A_qubits[2 * qubit + 1]) - _append_cx(decouple_cliff, A_qubits[2 * qubit + 1], A_qubits[2 * qubit]) - _append_cx(decouple_cliff, A_qubits[2 * qubit], qubit0) - _append_cx(decouple_cliff, qubit0, A_qubits[2 * qubit + 1]) - - return circ, decouple_cliff + circuit = QuantumCircuit._from_circuit_data( + synth_clifford_greedy_inner(clifford.tableau.astype(bool)) + ) + circuit.name = str(clifford) + return circuit diff --git a/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml b/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml new file mode 100644 index 000000000000..044fb7d44315 --- /dev/null +++ b/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml @@ -0,0 +1,6 @@ +--- +upgrade_synthesis: + - | + The function :func:`.synth_clifford_greedy` that synthesizes :class:`.Clifford` operators + was ported to Rust, leading to a significant increase in performance for all numbers of + qubits. For Cliffords over 50 qubits, the speedup is on the order of 1000 times. diff --git a/test/python/synthesis/test_clifford_sythesis.py b/test/python/synthesis/test_clifford_sythesis.py index 887f1af5ad99..8ca11f1ef251 100644 --- a/test/python/synthesis/test_clifford_sythesis.py +++ b/test/python/synthesis/test_clifford_sythesis.py @@ -16,6 +16,7 @@ import numpy as np from ddt import ddt from qiskit.circuit.random import random_clifford_circuit +from qiskit.quantum_info import random_clifford from qiskit.quantum_info.operators import Clifford from qiskit.synthesis.clifford import ( synth_clifford_full, @@ -99,8 +100,7 @@ def test_synth_greedy(self, num_qubits): rng = np.random.default_rng(1234) samples = 50 for _ in range(samples): - circ = random_clifford_circuit(num_qubits, 5 * num_qubits, seed=rng) - target = Clifford(circ) + target = random_clifford(num_qubits, rng) synth_circ = synth_clifford_greedy(target) value = Clifford(synth_circ) self.assertEqual(value, target) From ba486b739daf8ba410f7c0943fb035d689110dc2 Mon Sep 17 00:00:00 2001 From: Hirmay Sandesara <56473003+Hirmay@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:17:55 +0530 Subject: [PATCH 04/15] Add Uniform State Preparation (#12112) * Update state_preparation.py * Update state_preparation.py * Add files via upload * Update __init__.py * Update test_generalized_uniform_superposition_gate.py * Update test_generalized_uniform_superposition_gate.py * Update test_gate_definitions.py * made the format consistent with StatePreparation class * Put description and arguments in init * replaced assert with raise ValueError * small mistake in Returns * added test cases for ValueError cases * Update state_preparation.py * incorporate Steve's helpful suggestions * small bug * test_case fix * function for returning M * oops..forgot "" marks * Update state_preparation.py * removed get methods for num_qubit and M * added power of 2 condition * included test function when num_qubits is None * blacked init * blacked state_prep * blacked test_generalized_uniform_superposition_gate.py * reblacked state_prep * reblacked test_generalized_uniform_superposition_gate.py * shorterned max line length * reblacked state_prep * reblacked state_prep * for pyline * pylinted state_preparation.py * pylinted test_generalized_uniform_superposition_gate.py * pylinted test_gate_definitions.py * pylinted test_generalized_uniform_superposition_gate.py * Added GeneralizedUniformSuperposition gate Added GeneralizedUniformSuperposition gate class to the StatePreparation file in Circuit library. * modified: releasenotes/notes/generalized-uniform-superposition-gate-3bd95ffdf05ef18c.yaml * Updated release notes based on Steve's suggestions * Update release note * implemented the changes * fixed error * Update test_uniform_superposition_gate.py * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml * Update qiskit/circuit/library/data_preparation/state_preparation.py Sounds good! Co-authored-by: Julien Gacon * Update qiskit/circuit/library/data_preparation/state_preparation.py Okay! Co-authored-by: Julien Gacon * Update qiskit/circuit/library/data_preparation/state_preparation.py oh that's interesting...I didn't know that. Thanks for the info! Co-authored-by: Julien Gacon * Update releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml Ahhh...that's nice! Co-authored-by: Julien Gacon * Update releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml You're quite right, will help for others who might look at the code in future. Co-authored-by: Luciano Bello * Update state_preparation.py incorporated Julien's changes! * Update test_uniform_superposition_gate.py Incorporated Julien's optimization suggestion. * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml implemented both reviewers' suggestions. * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml removed SV24 * Update state_preparation.py * Update state_preparation.py * Update test_uniform_superposition_gate.py * Update uniform-superposition-gate-3bd95ffdf05ef18c.yaml * Update qiskit/circuit/library/data_preparation/state_preparation.py Co-authored-by: Julien Gacon * blacked state_preparation.py * Update test_uniform_superposition_gate.py * pylinted state_preparation.py * removed transpile test_uniform_superposition_gate.py --------- Co-authored-by: Julien Gacon Co-authored-by: Luciano Bello --- .../library/data_preparation/__init__.py | 11 ++- .../data_preparation/state_preparation.py | 96 +++++++++++++++++- ...m-superposition-gate-3bd95ffdf05ef18c.yaml | 27 +++++ test/python/circuit/test_gate_definitions.py | 1 + .../test_uniform_superposition_gate.py | 98 +++++++++++++++++++ 5 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml create mode 100644 test/python/circuit/test_uniform_superposition_gate.py diff --git a/qiskit/circuit/library/data_preparation/__init__.py b/qiskit/circuit/library/data_preparation/__init__.py index 38611c911fa4..192308a3a7f5 100644 --- a/qiskit/circuit/library/data_preparation/__init__.py +++ b/qiskit/circuit/library/data_preparation/__init__.py @@ -41,7 +41,14 @@ from .pauli_feature_map import PauliFeatureMap from .z_feature_map import ZFeatureMap from .zz_feature_map import ZZFeatureMap -from .state_preparation import StatePreparation +from .state_preparation import StatePreparation, UniformSuperpositionGate from .initializer import Initialize -__all__ = ["PauliFeatureMap", "ZFeatureMap", "ZZFeatureMap", "StatePreparation", "Initialize"] +__all__ = [ + "PauliFeatureMap", + "ZFeatureMap", + "ZZFeatureMap", + "StatePreparation", + "UniformSuperpositionGate", + "Initialize", +] diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 1d9ad7f7b082..26c37334cfe4 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -25,7 +25,9 @@ from qiskit.circuit.library.standard_gates.s import SGate, SdgGate from qiskit.circuit.library.generalized_gates import Isometry from qiskit.circuit.exceptions import CircuitError -from qiskit.quantum_info.states.statevector import Statevector # pylint: disable=cyclic-import +from qiskit.quantum_info.states.statevector import ( + Statevector, +) # pylint: disable=cyclic-import _EPS = 1e-10 # global variable used to chop very small numbers to zero @@ -240,3 +242,95 @@ def validate_parameter(self, parameter): def _return_repeat(self, exponent: float) -> "Gate": return Gate(name=f"{self.name}*{exponent}", num_qubits=self.num_qubits, params=[]) + + +class UniformSuperpositionGate(Gate): + r"""Implements a uniform superposition state. + + This gate is used to create the uniform superposition state + :math:`\frac{1}{\sqrt{M}} \sum_{j=0}^{M-1} |j\rangle` when it acts on an input + state :math:`|0...0\rangle`. Note, that `M` is not required to be + a power of 2, in which case the uniform superposition could be + prepared by a single layer of Hadamard gates. + + .. note:: + + This class uses the Shukla-Vedula algorithm [1], which only needs + :math:`O(\log_2 (M))` qubits and :math:`O(\log_2 (M))` gates, + to prepare the superposition. + + **References:** + [1]: A. Shukla and P. Vedula (2024), An efficient quantum algorithm for preparation + of uniform quantum superposition states, `Quantum Inf Process 23, 38 + `_. + """ + + def __init__( + self, + num_superpos_states: int = 2, + num_qubits: Optional[int] = None, + ): + r""" + Args: + num_superpos_states (int): + A positive integer M = num_superpos_states (> 1) representing the number of computational + basis states with an amplitude of 1/sqrt(M) in the uniform superposition + state (:math:`\frac{1}{\sqrt{M}} \sum_{j=0}^{M-1} |j\rangle`, where + :math:`1< M <= 2^n`). Note that the remaining (:math:`2^n - M`) computational basis + states have zero amplitudes. Here M need not be an integer power of 2. + + num_qubits (int): + A positive integer representing the number of qubits used. If num_qubits is None + or is not specified, then num_qubits is set to ceil(log2(num_superpos_states)). + + Raises: + ValueError: num_qubits must be an integer greater than or equal to log2(num_superpos_states). + + """ + if num_superpos_states <= 1: + raise ValueError("num_superpos_states must be a positive integer greater than 1.") + if num_qubits is None: + num_qubits = int(math.ceil(math.log2(num_superpos_states))) + else: + if not (isinstance(num_qubits, int) and (num_qubits >= math.log2(num_superpos_states))): + raise ValueError( + "num_qubits must be an integer greater than or equal to log2(num_superpos_states)." + ) + super().__init__("USup", num_qubits, [num_superpos_states]) + + def _define(self): + + qc = QuantumCircuit(self._num_qubits) + + num_superpos_states = self.params[0] + + if ( + num_superpos_states & (num_superpos_states - 1) + ) == 0: # if num_superpos_states is an integer power of 2 + m = int(math.log2(num_superpos_states)) + qc.h(range(m)) + self.definition = qc + return + + n_value = [int(x) for x in reversed(np.binary_repr(num_superpos_states))] + k = len(n_value) + l_value = [index for (index, item) in enumerate(n_value) if item == 1] # Locations of '1's + + qc.x(l_value[1:k]) + m_current_value = 2 ** l_value[0] + theta = -2 * np.arccos(np.sqrt(m_current_value / num_superpos_states)) + + if l_value[0] > 0: # if num_superpos_states is even + qc.h(range(l_value[0])) + qc.ry(theta, l_value[1]) + qc.ch(l_value[1], range(l_value[0], l_value[1]), ctrl_state="0") + + for m in range(1, len(l_value) - 1): + theta = -2 * np.arccos( + np.sqrt(2 ** l_value[m] / (num_superpos_states - m_current_value)) + ) + qc.cry(theta, l_value[m], l_value[m + 1], ctrl_state="0") + qc.ch(l_value[m + 1], range(l_value[m], l_value[m + 1]), ctrl_state="0") + m_current_value = m_current_value + 2 ** l_value[m] + + self.definition = qc diff --git a/releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml b/releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml new file mode 100644 index 000000000000..6017979748ea --- /dev/null +++ b/releasenotes/notes/uniform-superposition-gate-3bd95ffdf05ef18c.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + Implemented :class:`.UniformSuperpositionGate` class, which allows + the creation of a uniform superposition state using + the Shukla-Vedula algorithm. This feature facilitates the + creation of quantum circuits that produce a uniform superposition + state :math:`\frac{1}{\sqrt{M}} \sum_{j=0}^{M-1} |j\rangle`, where + :math:`M` is a positive integer representing the number of + computational basis states with an amplitude of + :math:`\frac{1}{\sqrt{M}}`. This implementation supports the + efficient creation of uniform superposition states, + requiring only :math:`O(\log_2 (M))` qubits and + :math:`O(\log_2 (M))` gates. Usage example: + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit.circuit.library.data_preparation import UniformSuperpositionGate + + M = 5 + num_qubits = 3 + usp_gate = UniformSuperpositionGate(M, num_qubits) + qc = QuantumCircuit(num_qubits) + qc.append(usp_gate, list(range(num_qubits))) + + qc.draw() diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index 38bf7046cae5..c5df22a0e8a1 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -283,6 +283,7 @@ class TestGateEquivalenceEqual(QiskitTestCase): "ClassicalFunction", "ClassicalElement", "StatePreparation", + "UniformSuperpositionGate", "LinearFunction", "PermutationGate", "Commuting2qBlock", diff --git a/test/python/circuit/test_uniform_superposition_gate.py b/test/python/circuit/test_uniform_superposition_gate.py new file mode 100644 index 000000000000..019d6144ac5e --- /dev/null +++ b/test/python/circuit/test_uniform_superposition_gate.py @@ -0,0 +1,98 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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 http://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. + +""" +Uniform Superposition Gate test. +""" + +import unittest +import math +from test import QiskitTestCase +import numpy as np +from ddt import ddt, data + +from qiskit import QuantumCircuit +from qiskit.quantum_info import Operator, Statevector + +from qiskit.circuit.library.data_preparation import ( + UniformSuperpositionGate, +) + + +@ddt +class TestUniformSuperposition(QiskitTestCase): + """Test initialization with UniformSuperpositionGate class""" + + @data(2, 3, 5) + def test_uniform_superposition_gate(self, num_superpos_states): + """Test Uniform Superposition Gate""" + n = int(math.ceil(math.log2(num_superpos_states))) + desired_sv = (1 / np.sqrt(num_superpos_states)) * np.array( + [1.0] * num_superpos_states + [0.0] * (2**n - num_superpos_states) + ) + gate = UniformSuperpositionGate(num_superpos_states, n) + actual_sv = Statevector(gate) + np.testing.assert_allclose(desired_sv, actual_sv) + + @data(2, 3, 5, 13) + def test_inverse_uniform_superposition_gate(self, num_superpos_states): + """Test Inverse Uniform Superposition Gate""" + n = int(math.ceil(math.log2(num_superpos_states))) + gate = UniformSuperpositionGate(num_superpos_states, n) + qc = QuantumCircuit(n) + qc.append(gate, list(range(n))) + qc.append(gate.inverse(annotated=True), list(range(n))) + actual_unitary_matrix = np.array(Operator(qc).data) + desired_unitary_matrix = np.eye(2**n) + np.testing.assert_allclose(desired_unitary_matrix, actual_unitary_matrix, atol=1e-14) + + @data(-2, -1, 0, 1) + def test_incompatible_num_superpos_states(self, num_superpos_states): + """Test error raised if num_superpos_states not valid""" + n = 1 + with self.assertRaises(ValueError): + UniformSuperpositionGate(num_superpos_states, n) + + @data(1, 2, 3, 4) + def test_incompatible_int_num_superpos_states_and_qubit_args(self, n): + """Test error raised if number of qubits not compatible with integer + state num_superpos_states (n >= log2(num_superpos_states) )""" + num_superpos_states = 50 + with self.assertRaises(ValueError): + UniformSuperpositionGate(num_superpos_states, n) + + @data(2, 3, 5) + def test_extra_qubits(self, num_superpos_states): + """Tests for cases where n >= log2(num_superpos_states)""" + num_extra_qubits = 2 + n = int(math.ceil(math.log2(num_superpos_states))) + num_extra_qubits + desired_sv = (1 / np.sqrt(num_superpos_states)) * np.array( + [1.0] * num_superpos_states + [0.0] * (2**n - num_superpos_states) + ) + gate = UniformSuperpositionGate(num_superpos_states, n) + actual_sv = Statevector(gate) + np.testing.assert_allclose(desired_sv, actual_sv) + + @data(2, 3, 5) + def test_no_qubit_args(self, num_superpos_states): + """Test Uniform Superposition Gate without passing the number of qubits as an argument""" + n = int(math.ceil(math.log2(num_superpos_states))) + desired_sv = (1 / np.sqrt(num_superpos_states)) * np.array( + [1.0] * num_superpos_states + [0.0] * (2**n - num_superpos_states) + ) + gate = UniformSuperpositionGate(num_superpos_states) + actual_sv = Statevector(gate) + np.testing.assert_allclose(desired_sv, actual_sv) + + +if __name__ == "__main__": + unittest.main() From 419f40e9e72b4981a6536d08d6b0fa91c13021d5 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 3 Jul 2024 14:04:47 +0300 Subject: [PATCH 05/15] fixing synthesis release notes (#12715) --- releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml | 2 +- releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml | 2 +- .../notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml b/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml index 6bd6761e0355..532d3e8fa55e 100644 --- a/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml +++ b/releasenotes/notes/oxidize-acg-0294a87c0d5974fa.yaml @@ -1,5 +1,5 @@ --- -upgrade_synthesis: +features_synthesis: - | Port :func:`.synth_permutation_acg`, used to synthesize qubit permutations, to Rust. This produces an approximate 3x performance improvement on 1000 qubit circuits. diff --git a/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml index e770aa1ca31b..bd8c969be934 100644 --- a/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml +++ b/releasenotes/notes/oxidize-permbasic-be27578187ac472f.yaml @@ -1,4 +1,4 @@ --- -upgrade_synthesis: +features_synthesis: - | Port :func:`.synth_permutation_basic`, used to synthesize qubit permutations, to Rust. diff --git a/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml b/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml index 044fb7d44315..ea492e29a7fc 100644 --- a/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml +++ b/releasenotes/notes/oxidize-synth-clifford-greedy-0739e9688bc4eedd.yaml @@ -1,5 +1,5 @@ --- -upgrade_synthesis: +features_synthesis: - | The function :func:`.synth_clifford_greedy` that synthesizes :class:`.Clifford` operators was ported to Rust, leading to a significant increase in performance for all numbers of From 9571ea1b3aea4c3ff89ee71f03b31b53d5bfa49a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 3 Jul 2024 10:00:10 -0400 Subject: [PATCH 06/15] Avoid Python op creation in commutative cancellation (#12701) * Avoid Python op creation in commutative cancellation This commit updates the commutative cancellation and commutation analysis transpiler pass. It builds off of #12692 to adjust access patterns in the python transpiler path to avoid eagerly creating a Python space operation object. The goal of this PR is to mitigate the performance regression on these passes introduced by the extra conversion cost of #12459. * Remove stray print * Don't add __array__ to DAGOpNode or CircuitInstruction --- crates/circuit/src/circuit_instruction.rs | 6 ++++ crates/circuit/src/dag_node.rs | 30 ++++++++++++++++ crates/circuit/src/operations.rs | 34 ++++++++++++++++-- qiskit/circuit/commutation_checker.py | 35 ++++++++++++++++++- .../optimization/commutation_analysis.py | 9 +---- .../optimization/commutative_cancellation.py | 14 ++++---- 6 files changed, 111 insertions(+), 17 deletions(-) diff --git a/crates/circuit/src/circuit_instruction.rs b/crates/circuit/src/circuit_instruction.rs index d6516722fbac..ed1c358cbc5b 100644 --- a/crates/circuit/src/circuit_instruction.rs +++ b/crates/circuit/src/circuit_instruction.rs @@ -460,6 +460,12 @@ impl CircuitInstruction { .and_then(|attrs| attrs.unit.as_deref()) } + pub fn is_parameterized(&self) -> bool { + self.params + .iter() + .any(|x| matches!(x, Param::ParameterExpression(_))) + } + /// Creates a shallow copy with the given fields replaced. /// /// Returns: diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 44ba5f7a6bf0..55a40c83dc39 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -14,6 +14,7 @@ use crate::circuit_instruction::{ convert_py_to_operation_type, operation_type_to_py, CircuitInstruction, ExtraInstructionAttributes, }; +use crate::imports::QUANTUM_CIRCUIT; use crate::operations::Operation; use numpy::IntoPyArray; use pyo3::prelude::*; @@ -228,6 +229,16 @@ impl DAGOpNode { Ok(()) } + #[getter] + fn num_qubits(&self) -> u32 { + self.instruction.operation.num_qubits() + } + + #[getter] + fn num_clbits(&self) -> u32 { + self.instruction.operation.num_clbits() + } + #[getter] fn get_qargs(&self, py: Python) -> Py { self.instruction.qubits.clone_ref(py) @@ -259,6 +270,10 @@ impl DAGOpNode { self.instruction.params.to_object(py) } + pub fn is_parameterized(&self) -> bool { + self.instruction.is_parameterized() + } + #[getter] fn matrix(&self, py: Python) -> Option { let matrix = self.instruction.operation.matrix(&self.instruction.params); @@ -325,6 +340,21 @@ impl DAGOpNode { } } + #[getter] + fn definition<'py>(&self, py: Python<'py>) -> PyResult>> { + let definition = self + .instruction + .operation + .definition(&self.instruction.params); + definition + .map(|data| { + QUANTUM_CIRCUIT + .get_bound(py) + .call_method1(intern!(py, "_from_circuit_data"), (data,)) + }) + .transpose() + } + /// Sets the Instruction name corresponding to the op for this node #[setter] fn set_name(&mut self, py: Python, new_name: PyObject) -> PyResult<()> { diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 77458e2fc35f..3bfef81d29ce 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -715,8 +715,38 @@ impl Operation for StandardGate { .expect("Unexpected Qiskit python bug"), ) }), - Self::RXGate => todo!("Add when we have R"), - Self::RYGate => todo!("Add when we have R"), + Self::RXGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::RGate, + smallvec![theta.clone(), FLOAT_ZERO], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), + Self::RYGate => Python::with_gil(|py| -> Option { + let theta = ¶ms[0]; + Some( + CircuitData::from_standard_gates( + py, + 1, + [( + Self::RGate, + smallvec![theta.clone(), Param::Float(PI / 2.0)], + smallvec![Qubit(0)], + )], + FLOAT_ZERO, + ) + .expect("Unexpected Qiskit python bug"), + ) + }), Self::RZGate => Python::with_gil(|py| -> Option { let theta = ¶ms[0]; Some( diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py index e758674829f8..79f04a65714d 100644 --- a/qiskit/circuit/commutation_checker.py +++ b/qiskit/circuit/commutation_checker.py @@ -21,6 +21,7 @@ from qiskit.circuit.operation import Operation from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.quantum_info.operators import Operator +from qiskit._accelerate.circuit import StandardGate _skipped_op_names = {"measure", "reset", "delay", "initialize"} _no_cache_op_names = {"annotated"} @@ -57,6 +58,23 @@ def __init__(self, standard_gate_commutations: dict = None, cache_max_entries: i self._cache_miss = 0 self._cache_hit = 0 + def commute_nodes( + self, + op1, + op2, + max_num_qubits: int = 3, + ) -> bool: + """Checks if two DAGOpNodes commute.""" + qargs1 = op1.qargs + cargs1 = op2.cargs + if not isinstance(op1._raw_op, StandardGate): + op1 = op1.op + qargs2 = op2.qargs + cargs2 = op2.cargs + if not isinstance(op2._raw_op, StandardGate): + op2 = op2.op + return self.commute(op1, qargs1, cargs1, op2, qargs2, cargs2, max_num_qubits) + def commute( self, op1: Operation, @@ -255,9 +273,15 @@ def is_commutation_skipped(op, qargs, max_num_qubits): if getattr(op, "is_parameterized", False) and op.is_parameterized(): return True + from qiskit.dagcircuit.dagnode import DAGOpNode + # we can proceed if op has defined: to_operator, to_matrix and __array__, or if its definition can be # recursively resolved by operations that have a matrix. We check this by constructing an Operator. - if (hasattr(op, "to_matrix") and hasattr(op, "__array__")) or hasattr(op, "to_operator"): + if ( + isinstance(op, DAGOpNode) + or (hasattr(op, "to_matrix") and hasattr(op, "__array__")) + or hasattr(op, "to_operator") + ): return False return False @@ -409,6 +433,15 @@ def _commute_matmul( first_qarg = tuple(qarg[q] for q in first_qargs) second_qarg = tuple(qarg[q] for q in second_qargs) + from qiskit.dagcircuit.dagnode import DAGOpNode + + # If we have a DAGOpNode here we've received a StandardGate definition from + # rust and we can manually pull the matrix to use for the Operators + if isinstance(first_ops, DAGOpNode): + first_ops = first_ops.matrix + if isinstance(second_op, DAGOpNode): + second_op = second_op.matrix + # try to generate an Operator out of op, if this succeeds we can determine commutativity, otherwise # return false try: diff --git a/qiskit/transpiler/passes/optimization/commutation_analysis.py b/qiskit/transpiler/passes/optimization/commutation_analysis.py index eddb659f0a25..61c77de552b9 100644 --- a/qiskit/transpiler/passes/optimization/commutation_analysis.py +++ b/qiskit/transpiler/passes/optimization/commutation_analysis.py @@ -72,14 +72,7 @@ def run(self, dag): does_commute = ( isinstance(current_gate, DAGOpNode) and isinstance(prev_gate, DAGOpNode) - and self.comm_checker.commute( - current_gate.op, - current_gate.qargs, - current_gate.cargs, - prev_gate.op, - prev_gate.qargs, - prev_gate.cargs, - ) + and self.comm_checker.commute_nodes(current_gate, prev_gate) ) if not does_commute: break diff --git a/qiskit/transpiler/passes/optimization/commutative_cancellation.py b/qiskit/transpiler/passes/optimization/commutative_cancellation.py index 4c6c487a0ea3..5c0b7317aabd 100644 --- a/qiskit/transpiler/passes/optimization/commutative_cancellation.py +++ b/qiskit/transpiler/passes/optimization/commutative_cancellation.py @@ -24,7 +24,7 @@ from qiskit.circuit.library.standard_gates.rx import RXGate from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.library.standard_gates.rz import RZGate -from qiskit.circuit import ControlFlowOp +from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES _CUTOFF_PRECISION = 1e-5 @@ -138,14 +138,14 @@ def run(self, dag): total_phase = 0.0 for current_node in run: if ( - getattr(current_node.op, "condition", None) is not None + current_node.condition is not None or len(current_node.qargs) != 1 or current_node.qargs[0] != run_qarg ): raise RuntimeError("internal error") if current_node.name in ["p", "u1", "rz", "rx"]: - current_angle = float(current_node.op.params[0]) + current_angle = float(current_node.params[0]) elif current_node.name in ["z", "x"]: current_angle = np.pi elif current_node.name == "t": @@ -159,8 +159,8 @@ def run(self, dag): # Compose gates total_angle = current_angle + total_angle - if current_node.op.definition: - total_phase += current_node.op.definition.global_phase + if current_node.definition: + total_phase += current_node.definition.global_phase # Replace the data of the first node in the run if cancel_set_key[0] == "z_rotation": @@ -200,7 +200,9 @@ def _handle_control_flow_ops(self, dag): """ pass_manager = PassManager([CommutationAnalysis(), self]) - for node in dag.op_nodes(ControlFlowOp): + for node in dag.op_nodes(): + if node.name not in CONTROL_FLOW_OP_NAMES: + continue mapped_blocks = [] for block in node.op.blocks: new_circ = pass_manager.run(block) From 5a4d598966f4ffea84dfc7871543d3e6268f90ce Mon Sep 17 00:00:00 2001 From: Shraddha Aangiras <63237790+shraddha-aangiras@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:54:31 +0530 Subject: [PATCH 07/15] Fixed typo leading to missing examples (#12689) --- qiskit/transpiler/layout.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index 4117e2987bb6..bece19671794 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -454,7 +454,7 @@ class TranspileLayout: qubits in the circuit as it fits the circuit to the target backend. For example, let the input circuit be: - .. plot: + .. plot:: :include-source: from qiskit.circuit import QuantumCircuit, QuantumRegister @@ -469,7 +469,7 @@ class TranspileLayout: Suppose that during the layout stage the transpiler reorders the qubits to be: - .. plot: + .. plot:: :include-source: from qiskit import QuantumCircuit @@ -497,7 +497,7 @@ class TranspileLayout: the transpiler needs to insert swap gates, and the output circuit becomes: - .. plot: + .. plot:: :include-source: from qiskit import QuantumCircuit From eed8f452457c8e9fc93cded53a467a4b1a2ba506 Mon Sep 17 00:00:00 2001 From: Hwa Date: Wed, 3 Jul 2024 23:53:25 +0800 Subject: [PATCH 08/15] Add warning about Tweedledum support in ClassicalFunction (#12652) * Add warning about Tweedledum support in ClassicalFunction * Update qiskit/circuit/classicalfunction/__init__.py Co-authored-by: Luciano Bello * Update __init__.py line break to comply to pylint --------- Co-authored-by: Luciano Bello --- qiskit/circuit/classicalfunction/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/qiskit/circuit/classicalfunction/__init__.py b/qiskit/circuit/classicalfunction/__init__.py index a072d910f97a..b045227b167e 100644 --- a/qiskit/circuit/classicalfunction/__init__.py +++ b/qiskit/circuit/classicalfunction/__init__.py @@ -51,6 +51,14 @@ def grover_oracle(a: Int1, b: Int1, c: Int1, d: Int1) -> Int1: Following Qiskit's little-endian bit ordering convention, the left-most bit (``a``) is the most significant bit and the right-most bit (``d``) is the least significant bit. +.. warning:: + + The functionality of `qiskit.circuit.classicalfunction` requires `tweedledum`, + which isn't available on all platforms (up to Python version 3.11). + See `tweedledum installation guide + `_ + for more details. + Supplementary Information ========================= From a897d707f76205615a2403fdc9aad8cf9a9f3673 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Wed, 3 Jul 2024 12:26:15 -0400 Subject: [PATCH 09/15] Accept `Option<&str>` instead of `&Option`, etc (#12593) * Accept Option<&str> instead of &Option, etc In a few places, this removes unnecessary object copies. Accept a wider range of input types, and more idiomatic input types in a few functions. This affects code added in the gates-in-rust PR. The great majority of the changes here were obsoleted by #12594. The original commit has been cherry picked on top of main. * Remove unnecessary as_ref() --- crates/circuit/src/circuit_data.rs | 8 +------- crates/circuit/src/parameter_table.rs | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/circuit/src/circuit_data.rs b/crates/circuit/src/circuit_data.rs index f10911cc440f..501645415874 100644 --- a/crates/circuit/src/circuit_data.rs +++ b/crates/circuit/src/circuit_data.rs @@ -1048,13 +1048,7 @@ impl CircuitData { Ok(PySet::new_bound(py, self.param_table.uuid_map.values())?.unbind()) } - pub fn pop_param( - &mut self, - py: Python, - uuid: u128, - name: String, - default: PyObject, - ) -> PyObject { + pub fn pop_param(&mut self, py: Python, uuid: u128, name: &str, default: PyObject) -> PyObject { match self.param_table.pop(uuid, name) { Some(res) => res.into_py(py), None => default.clone_ref(py), diff --git a/crates/circuit/src/parameter_table.rs b/crates/circuit/src/parameter_table.rs index 48c779eed3a3..9e5b31245391 100644 --- a/crates/circuit/src/parameter_table.rs +++ b/crates/circuit/src/parameter_table.rs @@ -153,8 +153,8 @@ impl ParamTable { self.uuid_map.clear(); } - pub fn pop(&mut self, key: u128, name: String) -> Option { - self.names.remove(&name); + pub fn pop(&mut self, key: u128, name: &str) -> Option { + self.names.remove(name); self.uuid_map.remove(&key); self.table.remove(&key) } From 51bf91e6ebdfef20eca0667eb51687119f10ea4b Mon Sep 17 00:00:00 2001 From: Shraddha Aangiras <63237790+shraddha-aangiras@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:34:10 +0530 Subject: [PATCH 10/15] Fixed typo in RZZGate example eqn (#12678) Co-authored-by: Luciano Bello --- qiskit/circuit/library/standard_gates/rzz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/library/standard_gates/rzz.py b/qiskit/circuit/library/standard_gates/rzz.py index 119dd370e20c..554ad4954a31 100644 --- a/qiskit/circuit/library/standard_gates/rzz.py +++ b/qiskit/circuit/library/standard_gates/rzz.py @@ -72,7 +72,7 @@ class RZZGate(Gate): .. math:: - R_{ZZ}(\theta = \pi) = - Z \otimes Z + R_{ZZ}(\theta = \pi) = - i Z \otimes Z .. math:: From 2c3ddf1e6f81c497297b1d3531f20ea62b4cb620 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 3 Jul 2024 16:56:55 -0400 Subject: [PATCH 11/15] Optimize DAGCircuit.collect_1q_runs() filter function (#12719) In the recently merged #12650 a new rust function was added for the filter function of the collect_1q_runs() method's internal filter function. As part of this we need to do exclude any non-DAGOpNode dag nodes passed to the filter function. This was previously implemented using the pyo3 extract() method so that if the pyobject can be coerced into a DAGOpNode for later use we continue but if it errors we know the input object is not a DAGOpNode and return false. However, as we don't need to return the error to python we should be using downcast instead of extract (as per the pyo3 performance guide [1]) to avoid the error conversion cost. This was an oversight in #12650 and I only used extract() out of habit. [1] https://pyo3.rs/v0.22.0/performance#extract-versus-downcast --- crates/accelerate/src/euler_one_qubit_decomposer.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 8c3a87ce51ec..24c4f6e87c2a 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -1045,10 +1045,11 @@ fn matmul_1q(operator: &mut [[Complex64; 2]; 2], other: Array2) { } #[pyfunction] -pub fn collect_1q_runs_filter(py: Python, node: PyObject) -> bool { - let op_node = node.extract::>(py); +pub fn collect_1q_runs_filter(node: &Bound) -> bool { + let op_node = node.downcast::(); match op_node { - Ok(node) => { + Ok(bound_node) => { + let node = bound_node.borrow(); node.instruction.operation.num_qubits() == 1 && node.instruction.operation.num_clbits() == 0 && node From 63b459eb125949fb7602a8617b56c71e1500f0be Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 4 Jul 2024 11:15:10 +0200 Subject: [PATCH 12/15] set a dummy QISKIT_SETTINGS so tests do not interact with local env (#12463) * mock QISKIT_SETTINGS to avoid interfering with my own env * set a dummy QISKIT_SETTINGS to avoid interfering with local env * apply to all of them * revert --- test/utils/base.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/utils/base.py b/test/utils/base.py index ce9509709bad..98f2fae9b7ff 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -287,6 +287,17 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr)) self.useFixture(fixtures.LoggerFixture(nuke_handlers=False, level=None)) + # a dummy setting config to make sure it does not intervene + # with the test runner environment. See https://github.com/Qiskit/qiskit/pull/12463 + self._mock_setting = unittest.mock.patch.dict( + os.environ, {"QISKIT_SETTINGS": "dummy_setting.conf"} + ) + self._mock_setting.start() + + def tearDown(self): + super().tearDown() + self._mock_setting.stop() + def dicts_almost_equal(dict1, dict2, delta=None, places=None, default_value=0): """Test if two dictionaries with numeric values are almost equal. From 722a5bc7ef627149d575d886e9796849b8a92589 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Thu, 4 Jul 2024 13:49:35 +0200 Subject: [PATCH 13/15] Oxidize `synth_cnot_count_full_pmh` (#12588) * port PMH synth to Rust * Raise an error if the section size is invalid Co-authored-by: Abdalla01001 Co-authored-by: Tarun-Kumar07 * Review comments of Shelly & Sasha --------- Co-authored-by: Abdalla01001 Co-authored-by: Tarun-Kumar07 --- crates/accelerate/src/synthesis/linear/mod.rs | 2 + crates/accelerate/src/synthesis/linear/pmh.rs | 195 ++++++++++++++++++ qiskit/synthesis/linear/cnot_synth.py | 117 ++--------- .../notes/oxidize-pmh-ec74e4002510eaad.yaml | 17 ++ .../python/synthesis/test_linear_synthesis.py | 36 ++++ 5 files changed, 272 insertions(+), 95 deletions(-) create mode 100644 crates/accelerate/src/synthesis/linear/pmh.rs create mode 100644 releasenotes/notes/oxidize-pmh-ec74e4002510eaad.yaml diff --git a/crates/accelerate/src/synthesis/linear/mod.rs b/crates/accelerate/src/synthesis/linear/mod.rs index b184a170fa5f..49dfeefd3869 100644 --- a/crates/accelerate/src/synthesis/linear/mod.rs +++ b/crates/accelerate/src/synthesis/linear/mod.rs @@ -14,6 +14,7 @@ use crate::QiskitError; use numpy::{IntoPyArray, PyArray2, PyReadonlyArray2, PyReadwriteArray2}; use pyo3::prelude::*; +mod pmh; pub mod utils; #[pyfunction] @@ -186,5 +187,6 @@ pub fn linear(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(binary_matmul))?; m.add_wrapped(wrap_pyfunction!(random_invertible_binary_matrix))?; m.add_wrapped(wrap_pyfunction!(check_invertible_binary_matrix))?; + m.add_wrapped(wrap_pyfunction!(pmh::synth_cnot_count_full_pmh))?; Ok(()) } diff --git a/crates/accelerate/src/synthesis/linear/pmh.rs b/crates/accelerate/src/synthesis/linear/pmh.rs new file mode 100644 index 000000000000..d7283fd580e4 --- /dev/null +++ b/crates/accelerate/src/synthesis/linear/pmh.rs @@ -0,0 +1,195 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// 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 http://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 hashbrown::HashMap; +use ndarray::{s, Array1, Array2, ArrayViewMut2, Axis}; +use numpy::PyReadonlyArray2; +use smallvec::smallvec; +use std::cmp; + +use qiskit_circuit::circuit_data::CircuitData; +use qiskit_circuit::operations::{Param, StandardGate}; +use qiskit_circuit::Qubit; + +use pyo3::prelude::*; + +use super::utils::_add_row_or_col; + +/// This helper function allows transposed access to a matrix. +fn _index(transpose: bool, i: usize, j: usize) -> (usize, usize) { + if transpose { + (j, i) + } else { + (i, j) + } +} + +fn _ceil_fraction(numerator: usize, denominator: usize) -> usize { + let mut fraction = numerator / denominator; + if numerator % denominator > 0 { + fraction += 1; + } + fraction +} + +/// This function is a helper function of the algorithm for optimal synthesis +/// of linear reversible circuits (the Patel–Markov–Hayes algorithm). It works +/// like gaussian elimination, except that it works a lot faster, and requires +/// fewer steps (and therefore fewer CNOTs). It takes the matrix and +/// splits it into sections of size section_size. Then it eliminates all non-zero +/// sub-rows within each section, which are the same as a non-zero sub-row +/// above. Once this has been done, it continues with normal gaussian elimination. +/// The benefit is that with small section sizes, most of the sub-rows will +/// be cleared in the first step, resulting in a factor ``section_size`` fewer row row operations +/// during Gaussian elimination. +/// +/// The algorithm is described in detail in the following paper +/// "Optimal synthesis of linear reversible circuits." +/// Patel, Ketan N., Igor L. Markov, and John P. Hayes. +/// Quantum Information & Computation 8.3 (2008): 282-294. +/// +/// Note: +/// This implementation tweaks the Patel, Markov, and Hayes algorithm by adding +/// a "back reduce" which adds rows below the pivot row with a high degree of +/// overlap back to it. The intuition is to avoid a high-weight pivot row +/// increasing the weight of lower rows. +/// +/// Args: +/// matrix: square matrix, describing a linear quantum circuit +/// section_size: the section size the matrix columns are divided into +/// +/// Returns: +/// A vector of CX locations (control, target) that need to be applied. +fn lower_cnot_synth( + mut matrix: ArrayViewMut2, + section_size: usize, + transpose: bool, +) -> Vec<(usize, usize)> { + // The vector of CNOTs to be applied. Called ``circuit`` here for consistency with the paper. + let mut circuit: Vec<(usize, usize)> = Vec::new(); + let cutoff = 1; + + // to apply to the transposed matrix, we can just set axis = 1 + let row_axis = if transpose { Axis(1) } else { Axis(0) }; + + // get number of columns (same as rows) and the number of sections + let n = matrix.raw_dim()[0]; + let num_sections = _ceil_fraction(n, section_size); + + // iterate over the columns + for section in 1..num_sections + 1 { + // store sub section row patterns here, which we saw already + let mut patterns: HashMap, usize> = HashMap::new(); + let section_slice = s![(section - 1) * section_size..cmp::min(section * section_size, n)]; + + // iterate over the rows (note we only iterate from the diagonal downwards) + for row_idx in (section - 1) * section_size..n { + // we need to keep track of the rows we saw already, called ``pattern`` here + let pattern: Array1 = matrix + .index_axis(row_axis, row_idx) + .slice(section_slice) + .to_owned(); + + // skip if the row is empty (i.e. all elements are false) + if pattern.iter().any(|&el| el) { + if patterns.contains_key(&pattern) { + // store CX location + circuit.push((patterns[&pattern], row_idx)); + // remove the row + _add_row_or_col(matrix.view_mut(), &transpose, patterns[&pattern], row_idx); + } else { + // if we have not seen this pattern yet, keep track of it + patterns.insert(pattern, row_idx); + } + } + } + + // gaussian eliminate the remainder of the section + for col_idx in (section - 1) * section_size..cmp::min(section * section_size, n) { + let mut diag_el = matrix[[col_idx, col_idx]]; + + for r in col_idx + 1..n { + if matrix[_index(transpose, r, col_idx)] { + if !diag_el { + _add_row_or_col(matrix.view_mut(), &transpose, r, col_idx); + circuit.push((r, col_idx)); + diag_el = true + } + _add_row_or_col(matrix.view_mut(), &transpose, col_idx, r); + circuit.push((col_idx, r)); + } + + // back-reduce to the pivot row: this one-line-magic checks if the logical AND + // between the two target rows has more ``true`` elements is larger than the cutoff + if matrix + .index_axis(row_axis, col_idx) + .iter() + .zip(matrix.index_axis(row_axis, r).iter()) + .map(|(&i, &j)| i & j) + .filter(|&x| x) + .count() + > cutoff + { + _add_row_or_col(matrix.view_mut(), &transpose, r, col_idx); + circuit.push((r, col_idx)); + } + } + } + } + circuit +} + +/// Synthesize a linear function, given by a boolean square matrix, into a circuit. +/// This function uses the Patel-Markov-Hayes algorithm, described in arXiv:quant-ph/0302002, +/// using section-wise elimination of the rows. +#[pyfunction] +#[pyo3(signature = (matrix, section_size=None))] +pub fn synth_cnot_count_full_pmh( + py: Python, + matrix: PyReadonlyArray2, + section_size: Option, +) -> PyResult { + let arrayview = matrix.as_array(); + let mut mat: Array2 = arrayview.to_owned(); + let num_qubits = mat.nrows(); // is a quadratic matrix + + // If given, use the user-specified input size. If None, we default to + // alpha * log2(num_qubits), as suggested in the paper, where the coefficient alpha + // is calibrated to minimize the upper bound on the number of row operations. + // In addition, we at least set a block size of 2, which, in practice, minimizes the bound + // until ~100 qubits. + let alpha = 0.56; + let blocksize = match section_size { + Some(section_size) => section_size as usize, + None => std::cmp::max(2, (alpha * (num_qubits as f64).log2()).floor() as usize), + }; + + // compute the synthesis for the lower triangular part of the matrix, and then + // apply it on the transposed part for the full synthesis + let lower_cnots = lower_cnot_synth(mat.view_mut(), blocksize, false); + let upper_cnots = lower_cnot_synth(mat.view_mut(), blocksize, true); + + // iterator over the gates + let instructions = upper_cnots + .iter() + .map(|(i, j)| (*j, *i)) + .chain(lower_cnots.into_iter().rev()) + .map(|(ctrl, target)| { + ( + StandardGate::CXGate, + smallvec![], + smallvec![Qubit(ctrl as u32), Qubit(target as u32)], + ) + }); + + CircuitData::from_standard_gates(py, num_qubits as u32, instructions, Param::Float(0.0)) +} diff --git a/qiskit/synthesis/linear/cnot_synth.py b/qiskit/synthesis/linear/cnot_synth.py index 699523a7e75d..f900ff86d17e 100644 --- a/qiskit/synthesis/linear/cnot_synth.py +++ b/qiskit/synthesis/linear/cnot_synth.py @@ -17,33 +17,37 @@ """ from __future__ import annotations -import copy + import numpy as np from qiskit.circuit import QuantumCircuit -from qiskit.exceptions import QiskitError + +from qiskit._accelerate.synthesis.linear import synth_cnot_count_full_pmh as fast_pmh def synth_cnot_count_full_pmh( - state: list[list[bool]] | np.ndarray[bool], section_size: int = 2 + state: list[list[bool]] | np.ndarray[bool], section_size: int | None = None ) -> QuantumCircuit: - """ + r""" Synthesize linear reversible circuits for all-to-all architecture using Patel, Markov and Hayes method. This function is an implementation of the Patel, Markov and Hayes algorithm from [1] for optimal synthesis of linear reversible circuits for all-to-all architecture, - as specified by an :math:`n \\times n` matrix. + as specified by an :math:`n \times n` matrix. Args: - state: :math:`n \\times n` boolean invertible matrix, describing - the state of the input circuit + state: :math:`n \times n` boolean invertible matrix, describing + the state of the input circuit. section_size: The size of each section in the Patel–Markov–Hayes algorithm [1]. + If ``None`` it is chosen to be :math:`\max(2, \alpha\log_2(n))` with + :math:`\alpha = 0.56`, which approximately minimizes the upper bound on the number + of row operations given in [1] Eq. (3). Returns: - QuantumCircuit: a CX-only circuit implementing the linear transformation. + A CX-only circuit implementing the linear transformation. Raises: - QiskitError: when variable ``state`` isn't of type ``numpy.ndarray`` + ValueError: When ``section_size`` is larger than the number of columns. References: 1. Patel, Ketan N., Igor L. Markov, and John P. Hayes, @@ -51,92 +55,15 @@ def synth_cnot_count_full_pmh( Quantum Information & Computation 8.3 (2008): 282-294. `arXiv:quant-ph/0302002 [quant-ph] `_ """ - if not isinstance(state, (list, np.ndarray)): - raise QiskitError( - f"state should be of type list or numpy.ndarray, but was of the type {type(state)}" + normalized = np.asarray(state).astype(bool) + if section_size is not None and normalized.shape[1] < section_size: + raise ValueError( + f"The section_size ({section_size}) cannot be larger than the number of columns " + f"({normalized.shape[1]})." ) - state = np.array(state) - # Synthesize lower triangular part - [state, circuit_l] = _lwr_cnot_synth(state, section_size) - state = np.transpose(state) - # Synthesize upper triangular part - [state, circuit_u] = _lwr_cnot_synth(state, section_size) - circuit_l.reverse() - for i in circuit_u: - i.reverse() - # Convert the list into a circuit of C-NOT gates - circ = QuantumCircuit(state.shape[0]) - for i in circuit_u + circuit_l: - circ.cx(i[0], i[1]) - return circ - - -def _lwr_cnot_synth(state, section_size): - """ - This function is a helper function of the algorithm for optimal synthesis - of linear reversible circuits (the Patel–Markov–Hayes algorithm). It works - like gaussian elimination, except that it works a lot faster, and requires - fewer steps (and therefore fewer CNOTs). It takes the matrix "state" and - splits it into sections of size section_size. Then it eliminates all non-zero - sub-rows within each section, which are the same as a non-zero sub-row - above. Once this has been done, it continues with normal gaussian elimination. - The benefit is that with small section sizes (m), most of the sub-rows will - be cleared in the first step, resulting in a factor m fewer row row operations - during Gaussian elimination. - The algorithm is described in detail in the following paper - "Optimal synthesis of linear reversible circuits." - Patel, Ketan N., Igor L. Markov, and John P. Hayes. - Quantum Information & Computation 8.3 (2008): 282-294. - - Note: - This implementation tweaks the Patel, Markov, and Hayes algorithm by adding - a "back reduce" which adds rows below the pivot row with a high degree of - overlap back to it. The intuition is to avoid a high-weight pivot row - increasing the weight of lower rows. - - Args: - state (ndarray): n x n matrix, describing a linear quantum circuit - section_size (int): the section size the matrix columns are divided into - - Returns: - numpy.matrix: n by n matrix, describing the state of the output circuit - list: a k by 2 list of C-NOT operations that need to be applied - """ - circuit = [] - num_qubits = state.shape[0] - cutoff = 1 + # call Rust implementation with normalized input + circuit_data = fast_pmh(normalized, section_size) - # Iterate over column sections - for sec in range(1, int(np.floor(num_qubits / section_size) + 1)): - # Remove duplicate sub-rows in section sec - patt = {} - for row in range((sec - 1) * section_size, num_qubits): - sub_row_patt = copy.deepcopy(state[row, (sec - 1) * section_size : sec * section_size]) - if np.sum(sub_row_patt) == 0: - continue - if str(sub_row_patt) not in patt: - patt[str(sub_row_patt)] = row - else: - state[row, :] ^= state[patt[str(sub_row_patt)], :] - circuit.append([patt[str(sub_row_patt)], row]) - # Use gaussian elimination for remaining entries in column section - for col in range((sec - 1) * section_size, sec * section_size): - # Check if 1 on diagonal - diag_one = 1 - if state[col, col] == 0: - diag_one = 0 - # Remove ones in rows below column col - for row in range(col + 1, num_qubits): - if state[row, col] == 1: - if diag_one == 0: - state[col, :] ^= state[row, :] - circuit.append([row, col]) - diag_one = 1 - state[row, :] ^= state[col, :] - circuit.append([col, row]) - # Back reduce the pivot row using the current row - if sum(state[col, :] & state[row, :]) > cutoff: - state[col, :] ^= state[row, :] - circuit.append([row, col]) - return [state, circuit] + # construct circuit from the data + return QuantumCircuit._from_circuit_data(circuit_data) diff --git a/releasenotes/notes/oxidize-pmh-ec74e4002510eaad.yaml b/releasenotes/notes/oxidize-pmh-ec74e4002510eaad.yaml new file mode 100644 index 000000000000..b25adbec21f9 --- /dev/null +++ b/releasenotes/notes/oxidize-pmh-ec74e4002510eaad.yaml @@ -0,0 +1,17 @@ +--- +features_synthesis: + - | + Port :func:`.synth_cnot_full_pmh`, used to synthesize a linear function + into a CX network, to Rust. This produces approximately 44x speedup, + as measured on 100 qubit circuits. + - | + The function :func:`.synth_cnot_full_pmh` now allows choosing the + (heuristically) optimal ``section_size`` by setting it to ``None``. Then, a value is + chosen which attempts to minimize the upper bound on the number of CX gates, that is + :math:`\alpha \log_2(n)` where :math:`n` is the number of qubits and :math:`\alpha \approx 0.56`. +fixes: + - | + Fixed a bug in :func:`.synth_cnot_full_pmh` where providing a ``section_size`` that did + not divide the number of qubits without remainder could lead to wrong results. + Now any ``section_size`` (at most equal to the number of qubits) synthesizes the correct + circuit. For a (heuristically) optimal value, set ``section_size=None``. diff --git a/test/python/synthesis/test_linear_synthesis.py b/test/python/synthesis/test_linear_synthesis.py index bbfab20a30fc..3aec57647b22 100644 --- a/test/python/synthesis/test_linear_synthesis.py +++ b/test/python/synthesis/test_linear_synthesis.py @@ -139,6 +139,42 @@ def test_synth_lnn_kms(self, num_qubits): dist = abs(q0 - q1) self.assertEqual(dist, 1) + @data(None, 2, 4, 5) + def test_synth_full_pmh(self, section_size): + """Test the PMH synthesis on pseudo-random matrices.""" + num_qubits = 5 + rng = np.random.default_rng(1234) + num_trials = 10 + seeds = rng.integers(100000, size=num_trials, dtype=np.uint64) + for seed in seeds: + mat = random_invertible_binary_matrix(num_qubits, seed=seed) + mat = np.array(mat, dtype=bool) + + qc = synth_cnot_count_full_pmh(mat, section_size) + self.assertEqual(LinearFunction(qc), LinearFunction(mat)) + + @data(5, 11) + def test_pmh_section_sizes(self, num_qubits): + """Test the PMH algorithm for different section sizes. + + Regression test of Qiskit/qiskit#12166. + """ + qc = QuantumCircuit(num_qubits) + for i in range(num_qubits - 1): + qc.cx(i, i + 1) + lin = LinearFunction(qc) + + for section_size in range(1, num_qubits + 1): + synthesized = synth_cnot_count_full_pmh(lin.linear, section_size=section_size) + with self.subTest(section_size=section_size): + self.assertEqual(lin, LinearFunction(synthesized)) + + def test_pmh_invalid_section_size(self): + """Test that a section size larger than the number of qubits raises an error.""" + mat = [[True, False], [True, False]] + with self.assertRaises(ValueError): + _ = synth_cnot_count_full_pmh(mat, section_size=3) + if __name__ == "__main__": unittest.main() From 7f1cc7fa980c848709262595bc0cf0c58fe0688f Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Fri, 5 Jul 2024 10:36:47 +0200 Subject: [PATCH 14/15] =?UTF-8?q?Revert=20"set=20a=20dummy=20QISKIT=5FSETT?= =?UTF-8?q?INGS=20so=20tests=20do=20not=20interact=20with=20local=20env=20?= =?UTF-8?q?(=E2=80=A6"=20(#12723)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 63b459eb125949fb7602a8617b56c71e1500f0be. --- test/utils/base.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/utils/base.py b/test/utils/base.py index 98f2fae9b7ff..ce9509709bad 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -287,17 +287,6 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr)) self.useFixture(fixtures.LoggerFixture(nuke_handlers=False, level=None)) - # a dummy setting config to make sure it does not intervene - # with the test runner environment. See https://github.com/Qiskit/qiskit/pull/12463 - self._mock_setting = unittest.mock.patch.dict( - os.environ, {"QISKIT_SETTINGS": "dummy_setting.conf"} - ) - self._mock_setting.start() - - def tearDown(self): - super().tearDown() - self._mock_setting.stop() - def dicts_almost_equal(dict1, dict2, delta=None, places=None, default_value=0): """Test if two dictionaries with numeric values are almost equal. From bb60891a60ac20e5305a481418058e90ec31379f Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Fri, 5 Jul 2024 13:19:10 +0200 Subject: [PATCH 15/15] Simplify base TestCases (#12720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Merge base classes for testing * Update test/utils/base.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update test/utils/base.py Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- test/utils/base.py | 209 ++++++++++++++++++++++----------------------- 1 file changed, 100 insertions(+), 109 deletions(-) diff --git a/test/utils/base.py b/test/utils/base.py index ce9509709bad..e4dbd7f67072 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -14,7 +14,7 @@ """Base TestCases for the unit tests. -Implementors of unit tests for Terra are encouraged to subclass +Implementors of unit tests for Qiskit should subclass ``QiskitTestCase`` in order to take advantage of utility functions (for example, the environment variables for customizing different options), and the decorators in the ``decorators`` package. @@ -65,11 +65,97 @@ class BaseTestCase(unittest.TestCase): @enforce_subclasses_call(["setUp", "setUpClass", "tearDown", "tearDownClass"]) -class BaseQiskitTestCase(BaseTestCase): - """Additions for test cases for all Qiskit-family packages. +class QiskitTestCase(BaseTestCase): + """Additions for Qiskit test cases.""" - The additions here are intended for all packages, not just Terra. Terra-specific logic should - be in the Terra-specific classes.""" + @classmethod + def setUpClass(cls): + super().setUpClass() + # Set logging to file and stdout if the LOG_LEVEL envar is set. + cls.log = logging.getLogger(cls.__name__) + + if log_level := os.getenv("LOG_LEVEL"): + log_fmt = f"{cls.log.name}.%(funcName)s:%(levelname)s:%(asctime)s: %(message)s" + formatter = logging.Formatter(log_fmt) + file_handler = logging.FileHandler(f"{os.path.splitext(inspect.getfile(cls))[0]}.log") + file_handler.setFormatter(formatter) + cls.log.addHandler(file_handler) + + if os.getenv("STREAM_LOG"): + # Set up the stream handler. + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(formatter) + cls.log.addHandler(stream_handler) + + # Set the logging level from the environment variable, defaulting + # to INFO if it is not a valid level. + level = logging._nameToLevel.get(log_level, logging.INFO) + cls.log.setLevel(level) + + warnings.filterwarnings("error", category=DeprecationWarning) + warnings.filterwarnings("error", category=QiskitWarning) + + # Numpy 2 made a few new modules private, and have warnings that trigger if you try to + # access attributes that _would_ have existed. Unfortunately, Python's `warnings` module + # adds a field called `__warningregistry__` to any module that triggers a warning, and + # `unittest.TestCase.assertWarns` then queries said fields on all existing modules. On + # macOS ARM, we see some (we think harmless) warnings come out of `numpy.linalg._linalg` (a + # now-private module) during transpilation, which means that subsequent `assertWarns` calls + # can spuriously trick Numpy into sending out a nonsense `DeprecationWarning`. + # Tracking issue: https://github.com/Qiskit/qiskit/issues/12679 + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=r".*numpy\.(\w+\.)*__warningregistry__", + ) + + # We only use pandas transitively through seaborn, so it's their responsibility to mark if + # their use of pandas would be a problem. + warnings.filterwarnings( + "default", + category=DeprecationWarning, + # The `(?s)` magic is to force use of the `re.DOTALL` flag, because the Pandas message + # includes hard-break newlines all over the place. + message="(?s).*Pyarrow.*required dependency.*next major release of pandas", + module=r"seaborn(\..*)?", + ) + + # Safe to remove once https://github.com/Qiskit/qiskit-aer/pull/2179 is in a release version + # of Aer. + warnings.filterwarnings( + "default", + category=DeprecationWarning, + message="Treating CircuitInstruction as an iterable is deprecated", + module=r"qiskit_aer(\.[a-zA-Z0-9_]+)*", + ) + + allow_DeprecationWarning_modules = [ + "test.python.pulse.test_builder", + "test.python.pulse.test_block", + "test.python.quantum_info.operators.symplectic.test_legacy_pauli", + "qiskit.quantum_info.operators.pauli", + "pybobyqa", + "numba", + "qiskit.utils.measurement_error_mitigation", + "qiskit.circuit.library.standard_gates.x", + "qiskit.pulse.schedule", + "qiskit.pulse.instructions.instruction", + "qiskit.pulse.instructions.play", + "qiskit.pulse.library.parametric_pulses", + "qiskit.quantum_info.operators.symplectic.pauli", + ] + for mod in allow_DeprecationWarning_modules: + warnings.filterwarnings("default", category=DeprecationWarning, module=mod) + allow_DeprecationWarning_message = [ + r"elementwise comparison failed.*", + r"The jsonschema validation included in qiskit-terra.*", + r"The DerivativeBase.parameter_expression_grad method.*", + r"The property ``qiskit\.circuit\.bit\.Bit\.(register|index)`` is deprecated.*", + # Caused by internal scikit-learn scipy usage + r"The 'sym_pos' keyword is deprecated and should be replaced by using", + ] + for msg in allow_DeprecationWarning_message: + warnings.filterwarnings("default", category=DeprecationWarning, message=msg) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -99,6 +185,13 @@ def tearDown(self): ) self.__teardown_called = True + # Reset the default providers, as in practice they acts as a singleton + # due to importing the instances from the top-level qiskit namespace. + from qiskit.providers.basic_provider import BasicProvider + + with self.assertWarns(DeprecationWarning): + BasicProvider()._backends = BasicProvider()._verify_backends() + def assertQuantumCircuitEqual(self, qc1, qc2, msg=None): """Extra assertion method to give a better error message when two circuits are unequal.""" if qc1 == qc2: @@ -165,112 +258,10 @@ def set_parallel_env(name, value): os.environ["QISKIT_PARALLEL"] = parallel_default -class QiskitTestCase(BaseQiskitTestCase): - """Terra-specific extra functionality for test cases.""" - - def tearDown(self): - super().tearDown() - # Reset the default providers, as in practice they acts as a singleton - # due to importing the instances from the top-level qiskit namespace. - from qiskit.providers.basic_provider import BasicProvider - - with self.assertWarns(DeprecationWarning): - BasicProvider()._backends = BasicProvider()._verify_backends() - - @classmethod - def setUpClass(cls): - super().setUpClass() - # Set logging to file and stdout if the LOG_LEVEL envar is set. - cls.log = logging.getLogger(cls.__name__) - - if log_level := os.getenv("LOG_LEVEL"): - log_fmt = f"{cls.log.name}.%(funcName)s:%(levelname)s:%(asctime)s: %(message)s" - formatter = logging.Formatter(log_fmt) - file_handler = logging.FileHandler(f"{os.path.splitext(inspect.getfile(cls))[0]}.log") - file_handler.setFormatter(formatter) - cls.log.addHandler(file_handler) - - if os.getenv("STREAM_LOG"): - # Set up the stream handler. - stream_handler = logging.StreamHandler() - stream_handler.setFormatter(formatter) - cls.log.addHandler(stream_handler) - - # Set the logging level from the environment variable, defaulting - # to INFO if it is not a valid level. - level = logging._nameToLevel.get(log_level, logging.INFO) - cls.log.setLevel(level) - - warnings.filterwarnings("error", category=DeprecationWarning) - warnings.filterwarnings("error", category=QiskitWarning) - - # Numpy 2 made a few new modules private, and have warnings that trigger if you try to - # access attributes that _would_ have existed. Unfortunately, Python's `warnings` module - # adds a field called `__warningregistry__` to any module that triggers a warning, and - # `unittest.TestCase.assertWarns` then queries said fields on all existing modules. On - # macOS ARM, we see some (we think harmless) warnings come out of `numpy.linalg._linalg` (a - # now-private module) during transpilation, which means that subsequent `assertWarns` calls - # can spuriously trick Numpy into sending out a nonsense `DeprecationWarning`. - # Tracking issue: https://github.com/Qiskit/qiskit/issues/12679 - warnings.filterwarnings( - "ignore", - category=DeprecationWarning, - message=r".*numpy\.(\w+\.)*__warningregistry__", - ) - - # We only use pandas transitively through seaborn, so it's their responsibility to mark if - # their use of pandas would be a problem. - warnings.filterwarnings( - "default", - category=DeprecationWarning, - # The `(?s)` magic is to force use of the `re.DOTALL` flag, because the Pandas message - # includes hard-break newlines all over the place. - message="(?s).*Pyarrow.*required dependency.*next major release of pandas", - module=r"seaborn(\..*)?", - ) - - # Safe to remove once https://github.com/Qiskit/qiskit-aer/pull/2179 is in a release version - # of Aer. - warnings.filterwarnings( - "default", - category=DeprecationWarning, - message="Treating CircuitInstruction as an iterable is deprecated", - module=r"qiskit_aer(\.[a-zA-Z0-9_]+)*", - ) - - allow_DeprecationWarning_modules = [ - "test.python.pulse.test_builder", - "test.python.pulse.test_block", - "test.python.quantum_info.operators.symplectic.test_legacy_pauli", - "qiskit.quantum_info.operators.pauli", - "pybobyqa", - "numba", - "qiskit.utils.measurement_error_mitigation", - "qiskit.circuit.library.standard_gates.x", - "qiskit.pulse.schedule", - "qiskit.pulse.instructions.instruction", - "qiskit.pulse.instructions.play", - "qiskit.pulse.library.parametric_pulses", - "qiskit.quantum_info.operators.symplectic.pauli", - ] - for mod in allow_DeprecationWarning_modules: - warnings.filterwarnings("default", category=DeprecationWarning, module=mod) - allow_DeprecationWarning_message = [ - r"elementwise comparison failed.*", - r"The jsonschema validation included in qiskit-terra.*", - r"The DerivativeBase.parameter_expression_grad method.*", - r"The property ``qiskit\.circuit\.bit\.Bit\.(register|index)`` is deprecated.*", - # Caused by internal scikit-learn scipy usage - r"The 'sym_pos' keyword is deprecated and should be replaced by using", - ] - for msg in allow_DeprecationWarning_message: - warnings.filterwarnings("default", category=DeprecationWarning, message=msg) - - class FullQiskitTestCase(QiskitTestCase): - """Terra-specific further additions for test cases, if ``testtools`` is available. + """further additions for Qiskit test cases, if ``testtools`` is available. - It is not normally safe to derive from this class by name; on import, Terra checks if the + It is not normally safe to derive from this class by name; on import, Qiskit checks if the necessary packages are available, and binds this class to the name :obj:`~QiskitTestCase` if so. If you derive directly from it, you may try and instantiate the class without satisfying its dependencies."""