From 7528db9ed363edbb29483d75266e137127ede5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 12 Sep 2024 12:02:09 +0200 Subject: [PATCH 01/45] Inital attempt at oxidizing unitary synthesis. --- .../src/euler_one_qubit_decomposer.rs | 4 +- crates/accelerate/src/lib.rs | 1 + .../target_transpiler/nullable_index_map.rs | 2 +- crates/accelerate/src/two_qubit_decompose.rs | 21 +- crates/accelerate/src/unitary_synthesis.rs | 1238 +++++++++++++++++ crates/circuit/src/dag_circuit.rs | 23 +- crates/circuit/src/packed_instruction.rs | 4 +- crates/pyext/src/lib.rs | 5 +- qiskit/__init__.py | 1 + .../passes/synthesis/unitary_synthesis.py | 34 +- 10 files changed, 1302 insertions(+), 31 deletions(-) create mode 100644 crates/accelerate/src/unitary_synthesis.rs diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 98333cad39d2..e6ca094186ff 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -579,7 +579,7 @@ pub fn generate_circuit( const EULER_BASIS_SIZE: usize = 12; -static EULER_BASES: [&[&str]; EULER_BASIS_SIZE] = [ +pub static EULER_BASES: [&[&str]; EULER_BASIS_SIZE] = [ &["u3"], &["u3", "u2", "u1"], &["u"], @@ -593,7 +593,7 @@ static EULER_BASES: [&[&str]; EULER_BASIS_SIZE] = [ &["rz", "sx", "x"], &["rz", "sx"], ]; -static EULER_BASIS_NAMES: [EulerBasis; EULER_BASIS_SIZE] = [ +pub static EULER_BASIS_NAMES: [EulerBasis; EULER_BASIS_SIZE] = [ EulerBasis::U3, EulerBasis::U321, EulerBasis::U, diff --git a/crates/accelerate/src/lib.rs b/crates/accelerate/src/lib.rs index 9111f932e270..c0a5fe8c03e1 100644 --- a/crates/accelerate/src/lib.rs +++ b/crates/accelerate/src/lib.rs @@ -43,6 +43,7 @@ pub mod synthesis; pub mod target_transpiler; pub mod two_qubit_decompose; pub mod uc_gate; +pub mod unitary_synthesis; pub mod utils; pub mod vf2_layout; diff --git a/crates/accelerate/src/target_transpiler/nullable_index_map.rs b/crates/accelerate/src/target_transpiler/nullable_index_map.rs index e6e2a0fca3a3..614a1e089c88 100644 --- a/crates/accelerate/src/target_transpiler/nullable_index_map.rs +++ b/crates/accelerate/src/target_transpiler/nullable_index_map.rs @@ -42,7 +42,7 @@ where K: Eq + Hash + Clone, V: Clone, { - map: BaseMap, + pub map: BaseMap, null_val: Option, } diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 92ad4724682f..0330d3ad4029 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -404,11 +404,11 @@ impl Specialization { #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitWeylDecomposition { #[pyo3(get)] - a: f64, + pub a: f64, #[pyo3(get)] - b: f64, + pub b: f64, #[pyo3(get)] - c: f64, + pub c: f64, #[pyo3(get)] pub global_phase: f64, K1l: Array2, @@ -1146,17 +1146,18 @@ impl TwoQubitWeylDecomposition { type TwoQubitSequenceVec = Vec<(Option, SmallVec<[f64; 3]>, SmallVec<[u8; 2]>)>; +#[derive(Clone, Debug)] #[pyclass(sequence)] pub struct TwoQubitGateSequence { - gates: TwoQubitSequenceVec, + pub gates: TwoQubitSequenceVec, #[pyo3(get)] - global_phase: f64, + pub global_phase: f64, } #[pymethods] impl TwoQubitGateSequence { #[new] - fn new() -> Self { + pub fn new() -> Self { TwoQubitGateSequence { gates: Vec::new(), global_phase: 0., @@ -1188,10 +1189,12 @@ impl TwoQubitGateSequence { } } } + +#[derive(Clone, Debug)] #[allow(non_snake_case)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitBasisDecomposer { - gate: String, + pub gate: String, basis_fidelity: f64, euler_basis: EulerBasis, pulse_optimize: Option, @@ -1575,7 +1578,7 @@ impl TwoQubitBasisDecomposer { Ok(res) } - fn new_inner( + pub fn new_inner( gate: String, gate_matrix: ArrayView2, basis_fidelity: f64, @@ -1713,7 +1716,7 @@ impl TwoQubitBasisDecomposer { }) } - fn call_inner( + pub fn call_inner( &self, unitary: ArrayView2, basis_fidelity: Option, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs new file mode 100644 index 000000000000..6ea5a49758b4 --- /dev/null +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -0,0 +1,1238 @@ +// 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. + +// #[cfg(feature = "cache_pygates")] +use std::cell::OnceCell; + +use std::f64::consts::PI; +use std::hash::Hash; + +use approx::relative_eq; +use core::panic; +use hashbrown::{HashMap, HashSet}; +use indexmap::{IndexMap, IndexSet}; +use ndarray::prelude::*; +use num_complex::{Complex, Complex64}; +use numpy::IntoPyArray; +use smallvec::{smallvec, SmallVec}; + +// use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::pybacked::PyBackedStr; +use pyo3::types::{IntoPyDict, PyDict, PyList, PyString, PyTuple}; +use pyo3::wrap_pyfunction; +use pyo3::Python; + +use rustworkx_core::petgraph::stable_graph::NodeIndex; + +use qiskit_circuit::dag_circuit::{add_global_phase, DAGCircuit, NodeType}; +use qiskit_circuit::imports; +use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; +use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation, PackedOperationType}; +use qiskit_circuit::Qubit; + +use crate::euler_one_qubit_decomposer::{ + optimize_1q_gates_decomposition, EulerBasis, EulerBasisSet, EULER_BASES, EULER_BASIS_NAMES, +}; +use crate::nlayout::PhysicalQubit; +use crate::target_transpiler::{NormalOperation, Target}; +use crate::two_qubit_decompose::{ + TwoQubitBasisDecomposer, TwoQubitGateSequence, TwoQubitWeylDecomposition, +}; +use crate::QiskitError; + +const PI2: f64 = PI / 2.; +const PI4: f64 = PI / 4.; + +#[derive(Clone, Debug)] +enum DecomposerType { + TwoQubitBasisDecomposer(Box), + XXDecomposer(PyObject), +} + +#[derive(Clone, Debug)] +enum UnitarySynthesisReturnType { + DAGType(Box), + TwoQSequenceType(TwoQubitUnitarySequence), +} + +#[derive(Clone, Debug)] +pub struct TwoQubitUnitarySequence { + pub gate_sequence: TwoQubitGateSequence, + pub decomp_gate: Option, + pub decomp_gate_params: Option>, +} + +impl TwoQubitUnitarySequence { + pub fn get_decomp_gate(&self) -> Option { + match self.decomp_gate.as_deref() { + Some("ch") => Some(StandardGate::CHGate), // 21 + Some("cx") => Some(StandardGate::CXGate), // 22 + Some("cy") => Some(StandardGate::CYGate), // 23 + Some("cz") => Some(StandardGate::CZGate), // 24 + Some("dcx") => Some(StandardGate::DCXGate), // 25 + Some("ecr") => Some(StandardGate::ECRGate), // 26 + Some("swap") => Some(StandardGate::SwapGate), // 27 + Some("iswap") => Some(StandardGate::ISwapGate), // 28 + Some("cp") => Some(StandardGate::CPhaseGate), // 29 + Some("crx") => Some(StandardGate::CRXGate), // 30 + Some("cry") => Some(StandardGate::CRYGate), // 31 + Some("crz") => Some(StandardGate::CRZGate), // 32 + Some("cs") => Some(StandardGate::CSGate), // 33 + Some("csdg") => Some(StandardGate::CSdgGate), // 34 + Some("csx") => Some(StandardGate::CSXGate), // 35 + Some("cu") => Some(StandardGate::CUGate), // 36 + Some("cu1") => Some(StandardGate::CU1Gate), // 37 + Some("cu3") => Some(StandardGate::CU3Gate), // 38 + Some("rxx") => Some(StandardGate::RXXGate), // 39 + Some("ryy") => Some(StandardGate::RYYGate), // 40 + Some("rzz") => Some(StandardGate::RZZGate), // 41 + Some("rzx") => Some(StandardGate::RZXGate), // 42 + Some("xx_minus_yy") => Some(StandardGate::XXMinusYYGate), // 43 + Some("xx_plus_yy") => Some(StandardGate::XXPlusYYGate), // 44 + _ => None, + } + } +} + +#[derive(Hash, Eq, PartialEq)] +struct InteractionStrength((u64, i16, i8)); + +// f64 is not hashable so we divide it into a mantissa-exponent-sign triplet +fn integer_decode_f64(val: f64) -> (u64, i16, i8) { + let bits: u64 = val.to_bits(); + let sign: i8 = if bits >> 63 == 0 { 1 } else { -1 }; + let mut exponent: i16 = ((bits >> 52) & 0x7ff) as i16; + let mantissa = if exponent == 0 { + (bits & 0xfffffffffffff) << 1 + } else { + (bits & 0xfffffffffffff) | 0x10000000000000 + }; + + exponent -= 1023 + 52; + (mantissa, exponent, sign) +} +fn integer_encode_f64(mantissa: u64, exponent: i16, sign: i8) -> f64 { + let exponent = (exponent + 1023 + 52) as u64; + let sign_bit = if sign == -1 { 1u64 << 63 } else { 0 }; + let exponent_bits = (exponent & 0x7ff) << 52; + let mantissa_bits = mantissa & 0xfffffffffffff; + let bits = sign_bit | exponent_bits | mantissa_bits; + f64::from_bits(bits) +} + +impl InteractionStrength { + fn new(val: f64) -> InteractionStrength { + InteractionStrength(integer_decode_f64(val)) + } + fn to_f64(&self) -> f64 { + let (mantissa, exponent, sign) = self.0; + integer_encode_f64(mantissa, exponent, sign) + } +} + +fn dag_from_2q_gate_sequence( + py: Python<'_>, + sequence: TwoQubitUnitarySequence, +) -> PyResult { + let gate_vec = &sequence.gate_sequence.gates; + let mut target_dag = DAGCircuit::new(py)?; + let _ = target_dag.set_global_phase(Param::Float(sequence.gate_sequence.global_phase)); + + let mut instructions = Vec::new(); + + let qubit: &Bound = imports::QUBIT.get_bound(py); + let mut qubits: Vec = vec![]; + + let qubit_obj = qubit.call0()?; + qubits.push(target_dag.add_qubit_unchecked(py, &qubit_obj)?); + + for (gate, params, qubit_ids) in gate_vec { + let gate_node = match gate { + None => sequence.get_decomp_gate().unwrap(), // should be some + Some(gate) => *gate, + }; + + let mut gate_qubits = Vec::new(); + + for id in qubit_ids { + while *id as usize >= qubits.len() { + let qubit_obj = qubit.call0()?; + qubits.push(target_dag.add_qubit_unchecked(py, &qubit_obj)?); + } + gate_qubits.push(qubits[*id as usize]); + } + + let new_params: Option>> = match gate { + Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), + None => sequence + .decomp_gate_params + .as_ref() + .map(|params| Box::new(params.clone())), + }; + + let pi = PackedInstruction { + op: PackedOperation::from_standard(gate_node), + qubits: target_dag.qargs_interner.insert(&gate_qubits), + clbits: target_dag.cargs_interner.get_default(), + params: new_params, + extra_attrs: None, + // #[cfg(feature = "cache_pygates")] + py_op: OnceCell::new(), + }; + instructions.push(pi); + } + + let _ = target_dag.extend(py, instructions.into_iter()); + + Ok(target_dag) +} + +// This is the cost function for choosing the best 2q synthesis output. +// Used in `run_2q_unitary_synthesis`. +fn compute_2q_error( + py: Python<'_>, + synth_circuit: &UnitarySynthesisReturnType, + target: &Option, + wire_map: &IndexMap, +) -> f64 { + match target { + None => { + if let UnitarySynthesisReturnType::DAGType(synth_dag) = synth_circuit { + return synth_dag.op_nodes(false).count() as f64; + } else { + panic!("Synth output is not a DAG"); + } + } + Some(target) => { + let mut gate_fidelities = Vec::new(); + let mut score_instruction = |instruction: &PackedInstruction, + inst_qubits: SmallVec<[PhysicalQubit; 2]>| + -> PyResult<()> { + // these are the keys + match target.operation_names_for_qargs(Some(&inst_qubits)) { + Ok(names) => { + for name in names { + let target_op = target.operation_from_name(name).unwrap(); + let op = instruction; + let are_params_close = match &op.params { + Some(params) => { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Don't fail please I beg you") + }) + } + None => false, + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == op.op.name() + && (is_parametrized || are_params_close) + { + match target[name].get(Some(&inst_qubits)) { + None => gate_fidelities.push(1.0), + Some(props) => gate_fidelities.push( + 1.0 - match props.clone() { + Some(props) => props.error.unwrap_or(0.0), + None => 0.0, + }, + ), + } + break; + } + } + Ok(()) + } + Err(_) => { + Err(QiskitError::new_err( + format!("Encountered a bad synthesis. Target has no {instruction:?} on qubits {inst_qubits:?}.") + )) + } + } + }; + + if let UnitarySynthesisReturnType::DAGType(synth_dag) = synth_circuit { + for node in synth_dag + .topological_op_nodes() + .expect("cannot return error here (don't know how to handle it later)") + { + if let NodeType::Operation(inst) = &synth_dag.dag()[node] { + let inst_qubits = synth_dag + .get_qargs(inst.qubits) + .iter() + .map(|q| wire_map[q]) + .collect(); + let _ = score_instruction(inst, inst_qubits); + } + } + } else { + panic!("Synth output is not a DAG"); + } + 1.0 - gate_fidelities.into_iter().product::() + } + } +} + +// This is the outer-most run function. It is meant to be called from Python inside `UnitarySynthesis.run()` +// This loop iterates over the dag and calls `run_2q_unitary_synthesis` (defined below). +#[pyfunction] +#[pyo3(name = "run_default_main_loop")] +fn py_run_default_main_loop( + py: Python, + dag: &mut DAGCircuit, + qubit_indices: &Bound<'_, PyList>, + min_qubits: usize, + approximation_degree: Option, + basis_gates: Option>, + coupling_map: Option, + natural_direction: Option, + pulse_optimize: Option, + gate_lengths: Option>, + gate_errors: Option>, + target: Option, +) -> PyResult { + // Create new empty dag + let mut out_dag = dag.copy_empty_like(py, "alike")?; + // Collect node ids into vec not to mix mutable and unmutable refs + let node_ids: Vec = dag.op_nodes(false).collect(); + // Iterate recursively over control flow blocks and "unwrap" + for node in node_ids { + if let NodeType::Operation(inst) = &dag.dag()[node] { + if inst.op.control_flow() { + if let OperationRef::Instruction(py_inst) = inst.op.view() { + let raw_blocks = py_inst.instruction.getattr(py, "blocks")?; + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + let dag_to_circuit = imports::DAG_TO_CIRCUIT.get_bound(py); + + let mut new_blocks = Vec::new(); + let raw_blocks = raw_blocks.bind(py).iter()?; + for raw_block in raw_blocks { + let block_obj = raw_block?; + let mut new_dag: DAGCircuit = + circuit_to_dag.call1((block_obj.clone(),))?.extract()?; + let block_qargs = dag.get_qargs(inst.qubits); + + // map qubit indices + let new_ids = block_qargs + .iter() + .map(|qarg| qubit_indices.get_item(qarg.0 as usize).expect("plz")); + let new_qubit_indices = PyList::new_bound(py, new_ids); + + let res = py_run_default_main_loop( + py, + &mut new_dag, + &new_qubit_indices, + min_qubits, + approximation_degree, + basis_gates.clone(), + coupling_map.clone(), + natural_direction, + pulse_optimize, + gate_lengths.clone(), + gate_errors.clone(), + target.clone(), + )?; + let res_circuit = dag_to_circuit.call1((res,))?; + new_blocks.push(res_circuit); + } + + let bound_py_inst = py_inst.instruction.bind(py); + let new_op = bound_py_inst.call_method1("replace_blocks", (new_blocks,))?; + let other_node = dag.get_node(py, node)?.clone(); + let _ = dag.substitute_node(other_node.bind(py), &new_op, true, false); + } + } + } + } + + // Iterate over nodes, find decomposers and run synthesis + // [E] implement as an iterator once it works (if it ever works) + for node in dag.topological_op_nodes()? { + if let NodeType::Operation(packed_instr) = &dag.dag()[node] { + if packed_instr.op.name() == "unitary" + && packed_instr.op.num_qubits() >= min_qubits as u32 + { + let unitary: Array, Dim<[usize; 2]>> = + match packed_instr.op.matrix(&[]) { + Some(unitary) => unitary, + None => return Err(QiskitError::new_err("Unitary not found")), + }; + match unitary.shape() { + [2, 2] => { + // do 1q stuff + let basis_set: Option> = basis_gates + .as_ref() + .map(|gates| gates.iter().map(|item| item.to_string()).collect()); + let qargs = dag.qargs_interner.get(packed_instr.qubits).to_vec(); + packed_instr.to_owned().qubits = out_dag.qargs_interner.insert_owned(qargs); + let _ = out_dag.push_back(py, packed_instr.clone()); + + optimize_1q_gates_decomposition( + py, + &mut out_dag, + target.as_ref(), + basis_set, + None, + )?; + } + [4, 4] => { + // local to global qubit mapping + let mut wire_map = IndexMap::new(); + for (i, q) in dag.get_qargs(packed_instr.qubits).iter().enumerate() { + wire_map.insert( + Qubit(i as u32), + PhysicalQubit::new( + qubit_indices.get_item(q.0 as usize)?.extract()?, + ), + ); + } + let raw_synth_output: Option = + run_2q_unitary_synthesis( + py, + unitary, + &wire_map, + approximation_degree, + &basis_gates, + &coupling_map, + natural_direction, + pulse_optimize, + &gate_lengths, + &gate_errors, + &target, + )?; + + // the output can be None, a DAGCircuit or a TwoQubitGateSequence + match raw_synth_output { + None => { + let _ = out_dag.push_back(py, packed_instr.clone()); + } + + Some(synth_output) => { + // synth dag is in the relative basis + let synth_dag = match synth_output { + UnitarySynthesisReturnType::DAGType(synth_dag) => synth_dag, + UnitarySynthesisReturnType::TwoQSequenceType( + synth_sequence, + ) => Box::new(dag_from_2q_gate_sequence(py, synth_sequence)?), + }; + + let _ = out_dag.set_global_phase(add_global_phase( + py, + &out_dag.get_global_phase(), + &synth_dag.get_global_phase(), + )?); + + for out_node in synth_dag.topological_op_nodes()? { + if let NodeType::Operation(mut out_packed_instr) = + synth_dag.dag()[out_node].clone() + { + let synth_qargs = + synth_dag.get_qargs(out_packed_instr.qubits); + let out_qargs = dag.get_qargs(packed_instr.qubits); + let mapped_qargs: Vec = synth_qargs + .iter() + .map(|qarg| out_qargs[qarg.0 as usize]) + .collect(); + + out_packed_instr.qubits = + out_dag.qargs_interner.insert(&mapped_qargs); + + let _ = out_dag.push_back(py, out_packed_instr.clone()); + } + } + } + } + } + _ => { + // do qsd stuff + todo!() + } + } + } else { + let _ = out_dag.push_back(py, packed_instr.clone()); + } + } + } + Ok(out_dag) +} + +// This function is meant to be used instead of `DefaultUnitarySynthesisPlugin.run()` +fn run_2q_unitary_synthesis( + py: Python, + unitary: Array2, + wire_map: &IndexMap, + approximation_degree: Option, + _basis_gates: &Option>, + coupling_map: &Option, + natural_direction: Option, + _pulse_optimize: Option, + gate_lengths: &Option>, + gate_errors: &Option>, + target: &Option, +) -> PyResult> { + // run 2q decomposition (in Rust except for XXDecomposer) -> Return types will vary. + // step1: select decomposers + let decomposers = match &target { + Some(target_ref) => { + let physical_qubits = wire_map.values().copied().collect(); + let decomposers_2q = get_2q_decomposers_from_target( + py, + target_ref, + &physical_qubits, + approximation_degree, + )?; + match decomposers_2q { + Some(decomp) => decomp, + None => Vec::new(), + } + } + None => todo!(), // HERE decomposer_2q_from_basis_gates -> this one uses pulse_optimize + }; + + // If we have a single TwoQubitBasisDecomposer, skip dag creation as we don't need to + // store and can instead manually create the synthesized gates directly in the output dag + if decomposers.len() == 1 { + let decomposer_item = decomposers.first().unwrap().clone(); + if let DecomposerType::TwoQubitBasisDecomposer(ref decomposer) = decomposer_item.0 { + let preferred_dir = preferred_direction( + py, + &decomposer_item.0, + wire_map, + natural_direction, + coupling_map, + gate_lengths, + gate_errors, + )?; + let synth = synth_su4_no_dag( + py, + &unitary, + decomposer, + preferred_dir, + approximation_degree, + &decomposer_item.1, + )?; + return Ok(Some(synth)); + } + } + + let mut synth_circuits = Vec::new(); + + for decomposer in &decomposers { + let preferred_dir = preferred_direction( + py, + &decomposer.0, + wire_map, + natural_direction, + coupling_map, + gate_lengths, + gate_errors, + )?; + let synth_circuit: Result = synth_su4( + py, + &unitary, + &decomposer.0, + preferred_dir, + approximation_degree, + &decomposer.1, + ); + synth_circuits.push(synth_circuit); + } + + let synth_circuit: Option = if !synth_circuits.is_empty() { + // get the minimum of synth_circuits and return + let mut synth_errors = Vec::new(); + let mut synth_circuits_filt = Vec::new(); + // the synth circuits should all be in "relative basis" + for circuit in synth_circuits.iter().flatten() { + let error = compute_2q_error(py, circuit, target, wire_map); + synth_errors.push(error); + synth_circuits_filt.push(circuit); + } + synth_errors + .iter() + .enumerate() + .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) + .map(|(index, _)| synth_circuits_filt[index].clone()) + } else { + None + }; + Ok(synth_circuit) + // The output at this point will be a DAG, the sequence may be returned in the special case for TwoQubitBasisDecomposer +} + +// This function collects a bunch of decomposer instances that will be used in `run_2q_unitary_synthesis` +fn get_2q_decomposers_from_target( + py: Python, + target: &Target, + qubits: &SmallVec<[PhysicalQubit; 2]>, + approximation_degree: Option, +) -> PyResult>)>>> { + let qubits: SmallVec<[PhysicalQubit; 2]> = qubits.clone(); + let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); + // HERE: caching + // TODO: here return cache --> implementation? + let mut available_2q_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); + let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); + + // try both directions for the qubits tuple + let mut qubit_gate_map = IndexMap::new(); + match target.operation_names_for_qargs(Some(&qubits)) { + Ok(direct_keys) => { + qubit_gate_map.insert(qubits.clone(), direct_keys); + if let Ok(reverse_keys) = target.operation_names_for_qargs(Some(&reverse_qubits)) { + qubit_gate_map.insert(reverse_qubits.clone(), reverse_keys); + } + } + Err(_) => { + if let Ok(reverse_keys) = target.operation_names_for_qargs(Some(&reverse_qubits)) { + qubit_gate_map.insert(reverse_qubits.clone(), reverse_keys); + } else { + return Err(QiskitError::new_err( + "Target has no gates available on qubits to synthesize over.", + )); + } + } + } + + fn replace_parametrized_gate(mut op: NormalOperation) -> NormalOperation { + if let Some(std_gate) = op.operation.try_standard_gate() { + match std_gate.name() { + "rxx" => { + if let Param::ParameterExpression(_) = op.params[0] { + op.params[0] = Param::Float(PI2) + } + } + "rzx" => { + if let Param::ParameterExpression(_) = op.params[0] { + op.params[0] = Param::Float(PI4) + } + } + "rzz" => { + if let Param::ParameterExpression(_) = op.params[0] { + op.params[0] = Param::Float(PI2) + } + } + _ => (), + } + } + op + } + + for (q_pair, gates) in &qubit_gate_map { + for &key in gates { + match target.operation_from_name(key) { + Ok(op) => { + // if it's not a gate, move on to next iteration + match op.operation.discriminant() { + PackedOperationType::Gate => (), + PackedOperationType::StandardGate => (), + _ => continue, + } + + available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); + + // Note that I had to make the map attribute public + if !target[key].map.is_empty() { + available_2q_props.insert( + key, + match &target[key][Some(q_pair)] { + Some(props) => (props.duration, props.error), + None => (None, None), + }, + ); + } + } + _ => continue, + } + } + } + + if available_2q_basis.is_empty() { + return Err(QiskitError::new_err( + "Target has no gates available on qubits to synthesize over.", + )); + } + + // Jump (target_basis_per_qubit) + let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); + // here we do the part in possible_decomposers + let target_basis_list = target.operation_names_for_qargs(Some(&smallvec![qubits[0]])); + + match target_basis_list { + Ok(basis_list) => { + EULER_BASES + .iter() + .enumerate() + .filter_map(|(idx, gates)| { + if !gates.iter().all(|gate| basis_list.contains(gate)) { + return None; + } + let basis = EULER_BASIS_NAMES[idx]; + Some(basis) + }) + .for_each(|basis| target_basis_set.add_basis(basis)); + } + Err(_) => target_basis_set.support_all(), + } + + if target_basis_set.basis_supported(EulerBasis::U3) + && target_basis_set.basis_supported(EulerBasis::U321) + { + target_basis_set.remove(EulerBasis::U3); + } + if target_basis_set.basis_supported(EulerBasis::ZSX) + && target_basis_set.basis_supported(EulerBasis::ZSXX) + { + target_basis_set.remove(EulerBasis::ZSX); + } + + let available_1q_basis: IndexSet<&str> = + IndexSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); + + // find all decomposers + let mut decomposers: Vec<(DecomposerType, Option>)> = Vec::new(); + + fn is_supercontrolled(op: &NormalOperation) -> bool { + match op.operation.matrix(&op.params) { + None => false, + Some(unitary_matrix) => { + let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) + .unwrap(); + relative_eq!(kak.a, PI4) && relative_eq!(kak.c, 0.0) + } + } + } + + fn is_controlled(op: &NormalOperation) -> bool { + match op.operation.matrix(&op.params) { + None => false, + Some(unitary_matrix) => { + let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) + .unwrap(); + relative_eq!(kak.b, 0.0) && relative_eq!(kak.c, 0.0) + } + } + } + + // Iterate over 1q and 2q supercontrolled basis, append TwoQubitBasisDecomposers + let supercontrolled_basis: IndexMap<&str, NormalOperation> = available_2q_basis + .clone() + .into_iter() + .filter(|(_, v)| is_supercontrolled(v)) + .collect(); + + for basis_1q in &available_1q_basis { + for basis_2q in supercontrolled_basis.keys() { + let mut basis_2q_fidelity: f64 = match available_2q_props.get(basis_2q) { + Some(&(_, Some(e))) => 1.0 - e, + Some(&(_, None)) => 1.0, + None => 1.0, + }; + if let Some(approx_degree) = approximation_degree { + basis_2q_fidelity *= approx_degree; + } + let gate = &supercontrolled_basis[basis_2q]; + let decomposer = TwoQubitBasisDecomposer::new_inner( + gate.operation.name().to_owned(), + gate.operation.matrix(&gate.params).unwrap().view(), + basis_2q_fidelity, + basis_1q, + None, + )?; + decomposers.push(( + DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), + Some(gate.params.clone()), + )); + } + } + + // If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer + // is an ideal decomposition and there is no need to bother calculating the XX embodiments + // or try the XX decomposer + let mut goodbye_set = IndexSet::new(); + goodbye_set.insert("cx"); + goodbye_set.insert("cz"); + goodbye_set.insert("ecr"); + + let available_basis_set: IndexSet<&str> = + IndexSet::from_iter(available_2q_basis.clone().keys().copied()); + + if goodbye_set.is_superset(&available_basis_set) { + // TODO: decomposer cache thingy + return Ok(Some(decomposers)); + } + + // Let's now look for possible controlled decomposers (i.e. XXDecomposer) + let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis + .clone() + .into_iter() + .filter(|(_, v)| is_controlled(v)) + .collect(); + + let mut basis_2q_fidelity: IndexMap = IndexMap::new(); + + // the embodiments will be a list of circuit representations + let mut embodiments: IndexMap> = IndexMap::new(); + let mut pi2_basis: Option<&str> = None; + + for (k, v) in controlled_basis.iter() { + let strength = 2.0 + * TwoQubitWeylDecomposition::new_inner( + v.operation.matrix(&v.params).unwrap().view(), + None, + None, + ) + .unwrap() + .a; + // each strength has its own fidelity + let fidelity_value = match available_2q_props.get(k) { + Some(&(_, error)) => 1.0 - error.unwrap_or(0.0), + None => 1.0, + }; + basis_2q_fidelity.insert(InteractionStrength::new(strength), fidelity_value); + + // rewrite XX of the same strength in terms of it + let xx_embodiments = PyModule::import_bound(py, "qiskit.synthesis.two_qubit.xx_decompose")? + .getattr("XXEmbodiments")?; + + // The embodiment should be a py object representing a quantum circuit + let embodiment = + xx_embodiments.get_item(v.clone().into_py(py).getattr(py, "base_class")?)?; //XXEmbodiments[v.base_class]; + + // This is 100% gonna fail + if embodiment.getattr("parameters")?.len()? == 1 { + embodiments.insert( + InteractionStrength::new(strength), + embodiment.call_method1("assign_parameters", (vec![strength],))?, + ); + } else { + embodiments.insert(InteractionStrength::new(strength), embodiment); + } + + // basis equivalent to CX are well optimized so use for the pi/2 angle if available + if relative_eq!(strength, PI2) && supercontrolled_basis.contains_key(k) { + pi2_basis = Some(v.operation.name()); + } + } + + // if we are using the approximation_degree knob, use it to scale already-given fidelities + if let Some(approx_degree) = approximation_degree { + for fidelity in basis_2q_fidelity.values_mut() { + *fidelity *= approx_degree; + } + } + + // Iterate over 2q fidelities ans select decomposers + if !basis_2q_fidelity.is_empty() { + let xx_decomposer: Bound<'_, PyAny> = + PyModule::import_bound(py, "qiskit.synthesis.two_qubit.xx_decompose")? + .getattr("XXDecomposer")?; + + for basis_1q in &available_1q_basis { + let pi2_decomposer = if let Some(pi_2_basis) = pi2_basis { + if pi_2_basis == "cx" && *basis_1q == "ZSX" { + let fidelity = match approximation_degree { + Some(approx_degree) => approx_degree, + None => match &target["cx"][Some(&qubits)] { + Some(props) => 1.0 - props.error.unwrap_or(0.0), + None => 1.0, + }, + }; + Some(TwoQubitBasisDecomposer::new_inner( + pi_2_basis.to_string(), + StandardGate::CXGate.matrix(&[]).unwrap().view(), + fidelity, + basis_1q, + Some(true), + )?) + } else { + None + } + } else { + None + }; + + let basis_2q_fidelity_dict = PyDict::new_bound(py); + for (k, v) in basis_2q_fidelity.iter() { + basis_2q_fidelity_dict.set_item(k.to_f64(), v)?; + } + + let embodiments_dict = PyDict::new_bound(py); + + // Use iterator to populate PyDict + embodiments.iter().for_each(|(key, value)| { + embodiments_dict + .set_item(key.to_f64(), value.clone().into_py(py)) + .unwrap(); + }); + let decomposer = xx_decomposer.call1(( + basis_2q_fidelity_dict, + PyString::new_bound(py, basis_1q), + embodiments_dict, + pi2_decomposer, + ))?; //instantiate properly + decomposers.push((DecomposerType::XXDecomposer(decomposer.into()), None)); + } + } + Ok(Some(decomposers)) +} + +// This is used in synth_su4 & company. I changed the logic with respect to the python reference code to return a bool +// instead of a tuple of qubits. The equivalence is the following: +// true = [0,1] +// false = [1,0] +// gate_lengths comes from Python and is a dict of {qubits_tuple: (gate_name, duration)} +// gate_errors comes from Python and is a dict of {qubits_tuple: (gate_name, error)} +fn preferred_direction( + py: Python, + decomposer: &DecomposerType, + wire_map: &IndexMap, + natural_direction: Option, + coupling_map: &Option, + gate_lengths: &Option>, + gate_errors: &Option>, +) -> PyResult> { + // `decomposer2q` decomposes an SU(4) over `qubits`. A user sets `natural_direction` + // to indicate whether they prefer synthesis in a hardware-native direction. + // If yes, we return the `preferred_direction` here. If no hardware direction is + // preferred, we raise an error (unless natural_direction is None). + // We infer this from `coupling_map`, `gate_lengths`, `gate_errors`. + + // Returns [0, 1] if qubits are correct in the hardware-native direction. -> True + // Returns [1, 0] if qubits must be flipped to match hardware-native direction. -> False + + // these are physical qubits + let qubits: SmallVec<[PhysicalQubit; 2]> = wire_map.values().copied().collect(); + let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); + let decomposer2q_gate = match decomposer { + DecomposerType::TwoQubitBasisDecomposer(decomp) => decomp.gate.clone(), + // Reason for clone: creates a temporary value which is freed while still in use + DecomposerType::XXDecomposer(decomp) => decomp + .getattr(py, "gate")? + .getattr(py, "name")? + .extract::(py)?, + }; + + // In python, gate_dict has the form: {(qubits,): {gate_name: duration}} (wrongly documented in the docstring. Fix!) + let compute_cost = |gate_dict: &Option>, + q_tuple: &SmallVec<[PhysicalQubit; 2]>, + in_cost: f64| + -> PyResult { + let ids: Vec = vec![q_tuple[0].0, q_tuple[1].0]; + match gate_dict { + Some(gate_dict) => { + let gate_value_dict = gate_dict.get_item(&decomposer2q_gate)?; + let cost = match gate_value_dict { + Some(val_dict) => match val_dict + .extract::<&PyDict>()? + .iter() + .map(|(qargs, value)| { + ( + qargs + .extract::>() + .expect("Cannot use ? inside this closure. Find solution"), + value + .extract::() + .expect("Cannot use ? inside this closure. Find solution"), + ) + }) + .find(|(qargs, _)| *qargs == ids) + .map(|(_, value)| value) + { + Some(val) => val, + None => in_cost, + }, + None => in_cost, + }; + Ok(cost) + } + None => Ok(in_cost), + } + }; + + let mut preferred_direction: Option = None; + + match natural_direction { + Some(false) => (), + _ => { + // None or Some(true) + // find native gate directions from a (non-bidirectional) coupling map + match coupling_map { + Some(cmap) => { + let neighbors0 = cmap + .call_method1(py, "neighbors", PyTuple::new_bound(py, [qubits[0].0]))? + .extract::>(py)?; + let zero_one = neighbors0.contains(&qubits[1].0); + let neighbors1 = cmap + .call_method1(py, "neighbors", PyTuple::new_bound(py, [qubits[1].0]))? + .extract::>(py)?; + let one_zero = neighbors1.contains(&qubits[0].0); + match (zero_one, one_zero) { + (true, false) => preferred_direction = Some(true), + (false, true) => preferred_direction = Some(false), + _ => (), + } + } + None => (), + } + + if preferred_direction.is_none() && (gate_lengths.is_some() || gate_errors.is_some()) { + let mut cost_0_1: f64 = f64::INFINITY; + let mut cost_1_0: f64 = f64::INFINITY; + + // Try to find the cost in gate_lengths + cost_0_1 = compute_cost(gate_lengths, &qubits, cost_0_1)?; + cost_1_0 = compute_cost(gate_lengths, &reverse_qubits, cost_1_0)?; + + // If no valid cost was found in gate_lengths, check gate_errors + if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { + cost_0_1 = compute_cost(gate_errors, &qubits, cost_0_1)?; + cost_1_0 = compute_cost(gate_errors, &reverse_qubits, cost_1_0)?; + } + + if cost_0_1 < cost_1_0 { + preferred_direction = Some(true) + } else if cost_1_0 < cost_0_1 { + preferred_direction = Some(false) + } + } + } + } + + if natural_direction == Some(true) && preferred_direction.is_none() { + return Err(QiskitError::new_err( + format!("No preferred direction of gate on qubits {qubits:?} could be determined from coupling map or gate lengths / gate errors.") + )); + } + + Ok(preferred_direction) +} + +// generic synth function for 2q gates (4x4) +// used in `run_2q_unitary_synthesis` +fn synth_su4( + py: Python, + su4_mat: &Array2, + decomposer_2q: &DecomposerType, + preferred_direction: Option, + approximation_degree: Option, + decomposer_gate_params: &Option>, +) -> PyResult { + // double check approximation_degree None + let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; + let synth_dag = match decomposer_2q { + // the output will be a dag in the relative basis + DecomposerType::XXDecomposer(decomposer) => { + let mut kwargs = HashMap::<&str, bool>::new(); + kwargs.insert("approximate", is_approximate); + kwargs.insert("use_dag", true); + // can we avoid cloning the matrix to pass it to python? + decomposer + .call_method_bound( + py, + "__call__", + (su4_mat.clone().into_pyarray_bound(py),), + Some(&kwargs.into_py_dict_bound(py)), + )? + .extract::(py)? + } + // the output will be a sequence in the relative basis + DecomposerType::TwoQubitBasisDecomposer(decomposer) => { + // we don't have access to basis_fidelity, right??? + let synth = decomposer.call_inner(su4_mat.view(), None, is_approximate, None)?; + let sequence = TwoQubitUnitarySequence { + gate_sequence: synth, + decomp_gate: Some(decomposer.gate.clone()), + decomp_gate_params: decomposer_gate_params.clone(), + }; + dag_from_2q_gate_sequence(py, sequence)? + } + }; + + match preferred_direction { + None => Ok(UnitarySynthesisReturnType::DAGType(Box::new(synth_dag))), + Some(preferred_dir) => { + let mut synth_direction: Option> = None; + for node in synth_dag.topological_op_nodes()? { + if let NodeType::Operation(inst) = &synth_dag.dag()[node] { + if inst.op.num_qubits() == 2 { + // not sure if these are the right qargs + let qargs = synth_dag.get_qargs(inst.qubits); + synth_direction = Some(vec![qargs[0].0, qargs[1].0]); + } + } + } + // synth direction is in the relative basis + match synth_direction { + None => Ok(UnitarySynthesisReturnType::DAGType(Box::new(synth_dag))), + Some(synth_direction) => { + let synth_dir = match synth_direction.as_slice() { + [0, 1] => true, + [1, 0] => false, + _ => panic!(), + }; + if synth_dir != preferred_dir { + reversed_synth_su4( + py, + su4_mat, + decomposer_2q, + approximation_degree, + decomposer_gate_params, + ) + } else { + Ok(UnitarySynthesisReturnType::DAGType(Box::new(synth_dag))) + } + } + } + } + } +} + +// special-case synth function for the TwoQubitBasisDecomposer +// used in `run_2q_unitary_synthesis` +fn synth_su4_no_dag( + py: Python<'_>, + su4_mat: &Array2, + decomposer_2q: &TwoQubitBasisDecomposer, + preferred_direction: Option, + approximation_degree: Option, + decomp_gate_params: &Option>, +) -> PyResult { + let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; + let synth = decomposer_2q.call_inner(su4_mat.view(), None, is_approximate, None)?; + let decomp_gate = decomposer_2q.gate.clone(); + + let sequence = TwoQubitUnitarySequence { + gate_sequence: synth.clone(), + decomp_gate: Some(decomp_gate), + decomp_gate_params: decomp_gate_params.clone(), + }; + + //synth_direction is calculated in terms of logical qubits + match preferred_direction { + None => Ok(UnitarySynthesisReturnType::TwoQSequenceType(sequence)), + Some(preferred_dir) => { + let mut synth_direction: Option> = None; + for (gate, _, qubits) in synth.gates { + if gate.is_none() || gate.unwrap().name() == "cx" { + synth_direction = Some(qubits); + } + } + + match synth_direction { + None => Ok(UnitarySynthesisReturnType::TwoQSequenceType(sequence)), + Some(synth_direction) => { + let synth_dir = match synth_direction.as_slice() { + [0, 1] => true, + [1, 0] => false, + _ => panic!(), + }; + if synth_dir != preferred_dir { + reversed_synth_su4( + py, + su4_mat, + &DecomposerType::TwoQubitBasisDecomposer(Box::new( + decomposer_2q.clone(), + )), + approximation_degree, + decomp_gate_params, + ) + } else { + Ok(UnitarySynthesisReturnType::TwoQSequenceType(sequence)) + } + } + } + } + } +} + +// generic synth function for 2q gates (4x4) called from synth_su4 +fn reversed_synth_su4( + py: Python<'_>, + su4_mat: &Array2, + decomposer_2q: &DecomposerType, + approximation_degree: Option, + decomp_gate_params: &Option>, +) -> PyResult { + let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; + let mut su4_mat_mm = su4_mat.clone(); + + // Swap rows 1 and 2 + let row_1 = su4_mat_mm.slice(s![1, ..]).to_owned(); + let row_2 = su4_mat_mm.slice(s![2, ..]).to_owned(); + su4_mat_mm.slice_mut(s![1, ..]).assign(&row_2); + su4_mat_mm.slice_mut(s![2, ..]).assign(&row_1); + + // Swap columns 1 and 2 + let col_1 = su4_mat_mm.slice(s![.., 1]).to_owned(); + let col_2 = su4_mat_mm.slice(s![.., 2]).to_owned(); + su4_mat_mm.slice_mut(s![.., 1]).assign(&col_2); + su4_mat_mm.slice_mut(s![.., 2]).assign(&col_1); + + let synth_dag = match decomposer_2q { + DecomposerType::XXDecomposer(decomposer) => { + // the output will be a dag in the relative basis + let mut kwargs = HashMap::<&str, bool>::new(); + kwargs.insert("approximate", is_approximate); + kwargs.insert("use_dag", true); + decomposer + .call_method_bound( + py, + "__call__", + (su4_mat_mm.clone().into_pyarray_bound(py),), + Some(&kwargs.into_py_dict_bound(py)), + )? + .extract::(py)? + } + DecomposerType::TwoQubitBasisDecomposer(decomposer) => { + // we don't have access to basis_fidelity, right??? + let synth = decomposer.call_inner(su4_mat_mm.view(), None, is_approximate, None)?; + let decomp_gate = decomposer.gate.clone(); + let sequence = TwoQubitUnitarySequence { + gate_sequence: synth, + decomp_gate: Some(decomp_gate), + decomp_gate_params: decomp_gate_params.clone(), + }; + // the output will be a sequence in the relative basis + dag_from_2q_gate_sequence(py, sequence)? + } + }; + + let mut target_dag = synth_dag.copy_empty_like(py, "alike")?; + let flip_bits: Vec = (0..synth_dag.num_qubits()) + .map(|id| (Qubit(id as u32))) + .rev() + .collect(); + + for node in synth_dag.topological_op_nodes()? { + if let NodeType::Operation(mut inst) = synth_dag.dag()[node].clone() { + let qubits = synth_dag + .qargs_interner + .get(inst.qubits) + .iter() + // .rev() + .map(|x| flip_bits[x.0 as usize]) + .collect(); + inst.qubits = target_dag.qargs_interner.insert_owned(qubits); + let _ = target_dag.push_back(py, inst.clone()); + } + } + Ok(UnitarySynthesisReturnType::DAGType(Box::new(target_dag))) +} + +#[pymodule] +pub fn unitary_synthesis(m: &Bound) -> PyResult<()> { + m.add_wrapped(wrap_pyfunction!(py_run_default_main_loop))?; + Ok(()) +} diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index d872806c5315..e098661ee7b1 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -233,9 +233,9 @@ pub struct DAGCircuit { cregs: Py, /// The cache used to intern instruction qargs. - qargs_interner: Interner<[Qubit]>, + pub qargs_interner: Interner<[Qubit]>, /// The cache used to intern instruction cargs. - cargs_interner: Interner<[Clbit]>, + pub cargs_interner: Interner<[Clbit]>, /// Qubits registered in the circuit. qubits: BitData, /// Clbits registered in the circuit. @@ -782,7 +782,7 @@ impl DAGCircuit { /// Return the global phase of the circuit. #[getter] - fn get_global_phase(&self) -> Param { + pub fn get_global_phase(&self) -> Param { self.global_phase.clone() } @@ -791,7 +791,7 @@ impl DAGCircuit { /// Args: /// angle (float, :class:`.ParameterExpression`): The phase angle. #[setter] - fn set_global_phase(&mut self, angle: Param) -> PyResult<()> { + pub fn set_global_phase(&mut self, angle: Param) -> PyResult<()> { match angle { Param::Float(angle) => { self.global_phase = Param::Float(angle.rem_euclid(2. * PI)); @@ -987,7 +987,7 @@ def _format(operand): } /// Add all wires in a quantum register. - fn add_qreg(&mut self, py: Python, qreg: &Bound) -> PyResult<()> { + pub fn add_qreg(&mut self, py: Python, qreg: &Bound) -> PyResult<()> { if !qreg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? { return Err(DAGCircuitError::new_err("not a QuantumRegister instance.")); } @@ -1557,7 +1557,7 @@ def _format(operand): /// Returns: /// DAGCircuit: An empty copy of self. #[pyo3(signature = (*, vars_mode="alike"))] - fn copy_empty_like(&self, py: Python, vars_mode: &str) -> PyResult { + pub fn copy_empty_like(&self, py: Python, vars_mode: &str) -> PyResult { let mut target_dag = DAGCircuit::with_capacity( py, self.num_qubits(), @@ -3398,7 +3398,7 @@ def _format(operand): /// DAGCircuitError: If replacement operation was incompatible with /// location of target node. #[pyo3(signature = (node, op, inplace=false, propagate_condition=true))] - fn substitute_node( + pub fn substitute_node( &mut self, node: &Bound, op: &Bound, @@ -5144,7 +5144,7 @@ impl DAGCircuit { /// This is mostly used to apply operations from one DAG to /// another that was created from the first via /// [DAGCircuit::copy_empty_like]. - fn push_back(&mut self, py: Python, instr: PackedInstruction) -> PyResult { + pub fn push_back(&mut self, py: Python, instr: PackedInstruction) -> PyResult { let op_name = instr.op.name(); let (all_cbits, vars): (Vec, Option>) = { if self.may_have_additional_wires(py, &instr) { @@ -5567,7 +5567,7 @@ impl DAGCircuit { Ok(()) } - fn add_qubit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + pub fn add_qubit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { let qubit = self.qubits.add(py, bit, false)?; self.qubit_locations.bind(py).set_item( bit, @@ -5583,7 +5583,7 @@ impl DAGCircuit { Ok(qubit) } - fn add_clbit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + pub fn add_clbit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { let clbit = self.clbits.add(py, bit, false)?; self.clbit_locations.bind(py).set_item( bit, @@ -6540,7 +6540,6 @@ impl DAGCircuit { // Get the correct qubit indices let qubits_id = instr.qubits; - // Insert op-node to graph. let new_node = self.dag.add_node(NodeType::Operation(instr)); new_nodes.push(new_node); @@ -6848,7 +6847,7 @@ impl DAGCircuit { /// Add to global phase. Global phase can only be Float or ParameterExpression so this /// does not handle the full possibility of parameter values. -fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { +pub fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { Ok(match [phase, other] { [Param::Float(a), Param::Float(b)] => Param::Float(a + b), [Param::Float(a), Param::ParameterExpression(b)] => Param::ParameterExpression( diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index df8f9801314a..33bbb0cfcb7c 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -34,7 +34,7 @@ use crate::{Clbit, Qubit}; /// The logical discriminant of `PackedOperation`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] -enum PackedOperationType { +pub enum PackedOperationType { // It's important that the `StandardGate` item is 0, so that zeroing out a `PackedOperation` // will make it appear as a standard gate, which will never allow accidental dangling-pointer // dereferencing. @@ -132,7 +132,7 @@ impl PackedOperation { /// Extract the discriminant of the operation. #[inline] - fn discriminant(&self) -> PackedOperationType { + pub fn discriminant(&self) -> PackedOperationType { ::bytemuck::checked::cast((self.0 & Self::DISCRIMINANT_MASK) as u8) } diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 6033c7c47e49..f498af98cd92 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -25,8 +25,8 @@ use qiskit_accelerate::{ sabre::sabre, sampled_exp_val::sampled_exp_val, sparse_pauli_op::sparse_pauli_op, split_2q_unitaries::split_2q_unitaries_mod, star_prerouting::star_prerouting, stochastic_swap::stochastic_swap, synthesis::synthesis, target_transpiler::target, - two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, utils::utils, - vf2_layout::vf2_layout, + two_qubit_decompose::two_qubit_decompose, uc_gate::uc_gate, + unitary_synthesis::unitary_synthesis, utils::utils, vf2_layout::vf2_layout, }; #[inline(always)] @@ -74,6 +74,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, target, "target")?; add_submodule(m, two_qubit_decompose, "two_qubit_decompose")?; add_submodule(m, uc_gate, "uc_gate")?; + add_submodule(m, unitary_synthesis, "unitary_synthesis")?; add_submodule(m, utils, "utils")?; add_submodule(m, vf2_layout, "vf2_layout")?; add_submodule(m, gate_direction, "gate_direction")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 25137d7a5918..70c1ac3f431d 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -78,6 +78,7 @@ sys.modules["qiskit._accelerate.stochastic_swap"] = _accelerate.stochastic_swap sys.modules["qiskit._accelerate.target"] = _accelerate.target sys.modules["qiskit._accelerate.two_qubit_decompose"] = _accelerate.two_qubit_decompose +sys.modules["qiskit._accelerate.unitary_synthesis"] = _accelerate.unitary_synthesis 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 diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index e31f6918f452..0052e192bf68 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -28,6 +28,7 @@ from functools import partial import numpy as np +from qiskit._accelerate.unitary_synthesis import run_default_main_loop from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.circuit import Gate, Parameter, CircuitInstruction from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping @@ -255,8 +256,10 @@ def _preferred_direction( if coupling_map is not None: neighbors0 = coupling_map.neighbors(qubits[0]) zero_one = qubits[1] in neighbors0 + neighbors1 = coupling_map.neighbors(qubits[1]) one_zero = qubits[0] in neighbors1 + if zero_one and not one_zero: preferred_direction = [0, 1] if one_zero and not zero_one: @@ -302,6 +305,7 @@ def _preferred_direction( preferred_direction = [0, 1] elif cost_1_0 < cost_0_1: preferred_direction = [1, 0] + if natural_direction is True and preferred_direction is None: raise TranspilerError( f"No preferred direction of gate on qubits {qubits} " @@ -501,9 +505,30 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if plugin_method.supports_coupling_map or default_method.supports_coupling_map else {} ) - return self._run_main_loop( - dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs - ) + + if self.method == "default" and isinstance(kwargs["target"], Target): + print("RUST") + _gate_lengths = _gate_lengths or _build_gate_lengths(self._backend_props, self._target) + _gate_errors = _gate_errors or _build_gate_errors(self._backend_props, self._target) + out = run_default_main_loop( + dag, + list(qubit_indices.values()), + self._min_qubits, + self._approximation_degree, + kwargs["basis_gates"], + self._coupling_map, + kwargs["natural_direction"], + kwargs["pulse_optimize"], + _gate_lengths, + _gate_errors, + kwargs["target"], + ) + return out + else: + out = self._run_main_loop( + dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs + ) + return out def _run_main_loop( self, dag, qubit_indices, plugin_method, plugin_kwargs, default_method, default_kwargs @@ -880,6 +905,7 @@ def is_controlled(gate): if error is None: error = 0.0 basis_2q_fidelity[strength] = 1 - error + # rewrite XX of the same strength in terms of it embodiment = XXEmbodiments[v.base_class] if len(embodiment.parameters) == 1: @@ -1042,12 +1068,14 @@ def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) + out_dag = DAGCircuit() out_dag.global_phase = synth_circ.global_phase out_dag.add_qubits(list(reversed(synth_circ.qubits))) flip_bits = out_dag.qubits[::-1] for node in synth_circ.topological_op_nodes(): qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) + node = DAGOpNode.from_instruction( node._to_circuit_instruction().replace(qubits=qubits, params=node.params) ) From 12962343051edd7b8dce7c4ef424fcf4d8a1f5d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 12 Sep 2024 13:47:35 +0200 Subject: [PATCH 02/45] Add cacache pygates feature to accelerate --- crates/accelerate/Cargo.toml | 3 +++ crates/accelerate/src/unitary_synthesis.rs | 4 ++-- crates/pyext/Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 4b5c18e5eb93..4e1e1317ab5a 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -58,3 +58,6 @@ features = ["ndarray"] [dependencies.pulp] version = "0.18.22" features = ["macro"] + +[features] +cache_pygates = ["qiskit-circuit/cache_pygates"] \ No newline at end of file diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 6ea5a49758b4..99b2602cdcd6 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -10,7 +10,7 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -// #[cfg(feature = "cache_pygates")] +#[cfg(feature = "cache_pygates")] use std::cell::OnceCell; use std::f64::consts::PI; @@ -186,7 +186,7 @@ fn dag_from_2q_gate_sequence( clbits: target_dag.cargs_interner.get_default(), params: new_params, extra_attrs: None, - // #[cfg(feature = "cache_pygates")] + #[cfg(feature = "cache_pygates")] py_op: OnceCell::new(), }; instructions.push(pi); diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index 413165e84b1f..eccc3ba8a87a 100644 --- a/crates/pyext/Cargo.toml +++ b/crates/pyext/Cargo.toml @@ -17,7 +17,7 @@ crate-type = ["cdylib"] # crates as standalone binaries, executables, we need `libpython` to be linked in, so we make the # feature a default, and run `cargo test --no-default-features` to turn it off. default = ["pyo3/extension-module"] -cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates"] +cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates", "qiskit-accelerate/cache_pygates"] [dependencies] pyo3.workspace = true From 4456b36ee519df9efc131c25c0be39797a3a66e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Fri, 13 Sep 2024 12:22:10 +0200 Subject: [PATCH 03/45] Use sabre target to speed up preferred_direction --- crates/accelerate/src/sabre/mod.rs | 4 +- crates/accelerate/src/unitary_synthesis.rs | 61 +++++++++++-------- .../passes/synthesis/unitary_synthesis.py | 16 ++++- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/crates/accelerate/src/sabre/mod.rs b/crates/accelerate/src/sabre/mod.rs index 77057b69c272..80be8fcd8f9b 100644 --- a/crates/accelerate/src/sabre/mod.rs +++ b/crates/accelerate/src/sabre/mod.rs @@ -13,8 +13,8 @@ mod heuristic; mod layer; mod layout; -mod neighbor_table; -mod route; +pub mod neighbor_table; +pub mod route; pub mod sabre_dag; pub mod swap_map; diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 99b2602cdcd6..bfeaae6825d8 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -22,7 +22,7 @@ use hashbrown::{HashMap, HashSet}; use indexmap::{IndexMap, IndexSet}; use ndarray::prelude::*; use num_complex::{Complex, Complex64}; -use numpy::IntoPyArray; +use numpy::{IntoPyArray, PyReadonlyArray2}; use smallvec::{smallvec, SmallVec}; // use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; @@ -44,6 +44,8 @@ use crate::euler_one_qubit_decomposer::{ optimize_1q_gates_decomposition, EulerBasis, EulerBasisSet, EULER_BASES, EULER_BASIS_NAMES, }; use crate::nlayout::PhysicalQubit; +use crate::sabre::neighbor_table::NeighborTable; +use crate::sabre::route::RoutingTargetView; use crate::target_transpiler::{NormalOperation, Target}; use crate::two_qubit_decompose::{ TwoQubitBasisDecomposer, TwoQubitGateSequence, TwoQubitWeylDecomposition, @@ -295,7 +297,9 @@ fn py_run_default_main_loop( min_qubits: usize, approximation_degree: Option, basis_gates: Option>, - coupling_map: Option, + neighbor_table: Option, + distance_matrix: Option>, + // coupling_map: Option, natural_direction: Option, pulse_optimize: Option, gate_lengths: Option>, @@ -336,7 +340,9 @@ fn py_run_default_main_loop( min_qubits, approximation_degree, basis_gates.clone(), - coupling_map.clone(), + neighbor_table.clone(), + distance_matrix.clone(), + // coupling_map.clone(), natural_direction, pulse_optimize, gate_lengths.clone(), @@ -404,7 +410,9 @@ fn py_run_default_main_loop( &wire_map, approximation_degree, &basis_gates, - &coupling_map, + // &coupling_map, + neighbor_table.clone(), + distance_matrix.clone(), natural_direction, pulse_optimize, &gate_lengths, @@ -474,7 +482,9 @@ fn run_2q_unitary_synthesis( wire_map: &IndexMap, approximation_degree: Option, _basis_gates: &Option>, - coupling_map: &Option, + neighbor_table: Option, + distance_matrix: Option>, + // coupling_map: &Option, natural_direction: Option, _pulse_optimize: Option, gate_lengths: &Option>, @@ -510,7 +520,8 @@ fn run_2q_unitary_synthesis( &decomposer_item.0, wire_map, natural_direction, - coupling_map, + &neighbor_table, + &distance_matrix, gate_lengths, gate_errors, )?; @@ -534,7 +545,8 @@ fn run_2q_unitary_synthesis( &decomposer.0, wire_map, natural_direction, - coupling_map, + &neighbor_table, + &distance_matrix, gate_lengths, gate_errors, )?; @@ -899,7 +911,8 @@ fn preferred_direction( decomposer: &DecomposerType, wire_map: &IndexMap, natural_direction: Option, - coupling_map: &Option, + neighbor_table: &Option, + distance_matrix: &Option>, gate_lengths: &Option>, gate_errors: &Option>, ) -> PyResult> { @@ -967,24 +980,22 @@ fn preferred_direction( Some(false) => (), _ => { // None or Some(true) - // find native gate directions from a (non-bidirectional) coupling map - match coupling_map { - Some(cmap) => { - let neighbors0 = cmap - .call_method1(py, "neighbors", PyTuple::new_bound(py, [qubits[0].0]))? - .extract::>(py)?; - let zero_one = neighbors0.contains(&qubits[1].0); - let neighbors1 = cmap - .call_method1(py, "neighbors", PyTuple::new_bound(py, [qubits[1].0]))? - .extract::>(py)?; - let one_zero = neighbors1.contains(&qubits[0].0); - match (zero_one, one_zero) { - (true, false) => preferred_direction = Some(true), - (false, true) => preferred_direction = Some(false), - _ => (), - } + if let (Some(table), Some(matrix)) = (neighbor_table, distance_matrix) { + let routing_target = RoutingTargetView { + neighbors: table, + coupling: &table.coupling_graph(), + distance: matrix.as_array(), + }; + // find native gate directions from a (non-bidirectional) coupling map + let neighbors0 = &routing_target.neighbors[qubits[0]]; + let zero_one = neighbors0.contains(&qubits[1]); + let neighbors1 = &routing_target.neighbors[qubits[1]]; + let one_zero = neighbors1.contains(&qubits[0]); + match (zero_one, one_zero) { + (true, false) => preferred_direction = Some(true), + (false, true) => preferred_direction = Some(false), + _ => (), } - None => (), } if preferred_direction.is_none() && (gate_lengths.is_some() || gate_errors.is_some()) { diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 0052e192bf68..76d70860798f 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -508,15 +508,29 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.method == "default" and isinstance(kwargs["target"], Target): print("RUST") + from qiskit._accelerate.sabre import NeighborTable + import rustworkx + _gate_lengths = _gate_lengths or _build_gate_lengths(self._backend_props, self._target) _gate_errors = _gate_errors or _build_gate_errors(self._backend_props, self._target) + if self._coupling_map is not None: + _dist_matrix = self._coupling_map.distance_matrix + _neighbor_table = NeighborTable( + rustworkx.adjacency_matrix(self._coupling_map.graph) + ) + else: + _dist_matrix = None + _neighbor_table = None + out = run_default_main_loop( dag, list(qubit_indices.values()), self._min_qubits, self._approximation_degree, kwargs["basis_gates"], - self._coupling_map, + _neighbor_table, + _dist_matrix, + # self._coupling_map, kwargs["natural_direction"], kwargs["pulse_optimize"], _gate_lengths, From c49acc187f16f741c34adc6b5a4634820ff18892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Fri, 13 Sep 2024 12:45:17 +0200 Subject: [PATCH 04/45] Adapt code to changes in PackedInstruction --- crates/accelerate/src/unitary_synthesis.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index bfeaae6825d8..853bf264806b 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -23,6 +23,7 @@ use indexmap::{IndexMap, IndexSet}; use ndarray::prelude::*; use num_complex::{Complex, Complex64}; use numpy::{IntoPyArray, PyReadonlyArray2}; +use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; use smallvec::{smallvec, SmallVec}; // use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; @@ -187,7 +188,7 @@ fn dag_from_2q_gate_sequence( qubits: target_dag.qargs_interner.insert(&gate_qubits), clbits: target_dag.cargs_interner.get_default(), params: new_params, - extra_attrs: None, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), #[cfg(feature = "cache_pygates")] py_op: OnceCell::new(), }; From 739a5c847cae8d55e492505649a4b30864f63953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Fri, 13 Sep 2024 13:31:51 +0200 Subject: [PATCH 05/45] Use target in preferred_direction --- crates/accelerate/src/unitary_synthesis.rs | 162 +++++++----------- .../passes/synthesis/unitary_synthesis.py | 2 +- 2 files changed, 63 insertions(+), 101 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 853bf264806b..00973e6397cd 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -205,24 +205,15 @@ fn dag_from_2q_gate_sequence( fn compute_2q_error( py: Python<'_>, synth_circuit: &UnitarySynthesisReturnType, - target: &Option, + target: &Target, wire_map: &IndexMap, ) -> f64 { - match target { - None => { - if let UnitarySynthesisReturnType::DAGType(synth_dag) = synth_circuit { - return synth_dag.op_nodes(false).count() as f64; - } else { - panic!("Synth output is not a DAG"); - } - } - Some(target) => { - let mut gate_fidelities = Vec::new(); - let mut score_instruction = |instruction: &PackedInstruction, - inst_qubits: SmallVec<[PhysicalQubit; 2]>| - -> PyResult<()> { - // these are the keys - match target.operation_names_for_qargs(Some(&inst_qubits)) { + let mut gate_fidelities = Vec::new(); + let mut score_instruction = |instruction: &PackedInstruction, + inst_qubits: SmallVec<[PhysicalQubit; 2]>| + -> PyResult<()> { + // these are the keys + match target.operation_names_for_qargs(Some(&inst_qubits)) { Ok(names) => { for name in names { let target_op = target.operation_from_name(name).unwrap(); @@ -263,28 +254,26 @@ fn compute_2q_error( )) } } - }; + }; - if let UnitarySynthesisReturnType::DAGType(synth_dag) = synth_circuit { - for node in synth_dag - .topological_op_nodes() - .expect("cannot return error here (don't know how to handle it later)") - { - if let NodeType::Operation(inst) = &synth_dag.dag()[node] { - let inst_qubits = synth_dag - .get_qargs(inst.qubits) - .iter() - .map(|q| wire_map[q]) - .collect(); - let _ = score_instruction(inst, inst_qubits); - } - } - } else { - panic!("Synth output is not a DAG"); + if let UnitarySynthesisReturnType::DAGType(synth_dag) = synth_circuit { + for node in synth_dag + .topological_op_nodes() + .expect("cannot return error here (don't know how to handle it later)") + { + if let NodeType::Operation(inst) = &synth_dag.dag()[node] { + let inst_qubits = synth_dag + .get_qargs(inst.qubits) + .iter() + .map(|q| wire_map[q]) + .collect(); + let _ = score_instruction(inst, inst_qubits); } - 1.0 - gate_fidelities.into_iter().product::() } + } else { + panic!("Synth output is not a DAG"); } + 1.0 - gate_fidelities.into_iter().product::() } // This is the outer-most run function. It is meant to be called from Python inside `UnitarySynthesis.run()` @@ -296,6 +285,7 @@ fn py_run_default_main_loop( dag: &mut DAGCircuit, qubit_indices: &Bound<'_, PyList>, min_qubits: usize, + target: &Target, approximation_degree: Option, basis_gates: Option>, neighbor_table: Option, @@ -305,7 +295,6 @@ fn py_run_default_main_loop( pulse_optimize: Option, gate_lengths: Option>, gate_errors: Option>, - target: Option, ) -> PyResult { // Create new empty dag let mut out_dag = dag.copy_empty_like(py, "alike")?; @@ -339,6 +328,7 @@ fn py_run_default_main_loop( &mut new_dag, &new_qubit_indices, min_qubits, + target, approximation_degree, basis_gates.clone(), neighbor_table.clone(), @@ -348,7 +338,6 @@ fn py_run_default_main_loop( pulse_optimize, gate_lengths.clone(), gate_errors.clone(), - target.clone(), )?; let res_circuit = dag_to_circuit.call1((res,))?; new_blocks.push(res_circuit); @@ -388,7 +377,7 @@ fn py_run_default_main_loop( optimize_1q_gates_decomposition( py, &mut out_dag, - target.as_ref(), + Some(target), basis_set, None, )?; @@ -418,7 +407,7 @@ fn py_run_default_main_loop( pulse_optimize, &gate_lengths, &gate_errors, - &target, + target, )?; // the output can be None, a DAGCircuit or a TwoQubitGateSequence @@ -490,25 +479,18 @@ fn run_2q_unitary_synthesis( _pulse_optimize: Option, gate_lengths: &Option>, gate_errors: &Option>, - target: &Option, + target: &Target, ) -> PyResult> { // run 2q decomposition (in Rust except for XXDecomposer) -> Return types will vary. // step1: select decomposers - let decomposers = match &target { - Some(target_ref) => { - let physical_qubits = wire_map.values().copied().collect(); - let decomposers_2q = get_2q_decomposers_from_target( - py, - target_ref, - &physical_qubits, - approximation_degree, - )?; - match decomposers_2q { - Some(decomp) => decomp, - None => Vec::new(), - } + let decomposers = { + let physical_qubits = wire_map.values().copied().collect(); + let decomposers_2q = + get_2q_decomposers_from_target(py, target, &physical_qubits, approximation_degree)?; + match decomposers_2q { + Some(decomp) => decomp, + None => Vec::new(), } - None => todo!(), // HERE decomposer_2q_from_basis_gates -> this one uses pulse_optimize }; // If we have a single TwoQubitBasisDecomposer, skip dag creation as we don't need to @@ -523,8 +505,7 @@ fn run_2q_unitary_synthesis( natural_direction, &neighbor_table, &distance_matrix, - gate_lengths, - gate_errors, + &target, )?; let synth = synth_su4_no_dag( py, @@ -548,8 +529,7 @@ fn run_2q_unitary_synthesis( natural_direction, &neighbor_table, &distance_matrix, - gate_lengths, - gate_errors, + &target, )?; let synth_circuit: Result = synth_su4( py, @@ -905,8 +885,7 @@ fn get_2q_decomposers_from_target( // instead of a tuple of qubits. The equivalence is the following: // true = [0,1] // false = [1,0] -// gate_lengths comes from Python and is a dict of {qubits_tuple: (gate_name, duration)} -// gate_errors comes from Python and is a dict of {qubits_tuple: (gate_name, error)} + fn preferred_direction( py: Python, decomposer: &DecomposerType, @@ -914,8 +893,7 @@ fn preferred_direction( natural_direction: Option, neighbor_table: &Option, distance_matrix: &Option>, - gate_lengths: &Option>, - gate_errors: &Option>, + target: &Target, ) -> PyResult> { // `decomposer2q` decomposes an SU(4) over `qubits`. A user sets `natural_direction` // to indicate whether they prefer synthesis in a hardware-native direction. @@ -938,42 +916,26 @@ fn preferred_direction( .extract::(py)?, }; - // In python, gate_dict has the form: {(qubits,): {gate_name: duration}} (wrongly documented in the docstring. Fix!) - let compute_cost = |gate_dict: &Option>, - q_tuple: &SmallVec<[PhysicalQubit; 2]>, - in_cost: f64| - -> PyResult { - let ids: Vec = vec![q_tuple[0].0, q_tuple[1].0]; - match gate_dict { - Some(gate_dict) => { - let gate_value_dict = gate_dict.get_item(&decomposer2q_gate)?; - let cost = match gate_value_dict { - Some(val_dict) => match val_dict - .extract::<&PyDict>()? - .iter() - .map(|(qargs, value)| { - ( - qargs - .extract::>() - .expect("Cannot use ? inside this closure. Find solution"), - value - .extract::() - .expect("Cannot use ? inside this closure. Find solution"), - ) - }) - .find(|(qargs, _)| *qargs == ids) - .map(|(_, value)| value) - { - Some(val) => val, + let compute_cost = + |lengths: bool, q_tuple: &SmallVec<[PhysicalQubit; 2]>, in_cost: f64| -> PyResult { + let cost = match target.qargs_for_operation_name(&decomposer2q_gate) { + Ok(_) => match target[&decomposer2q_gate].get(Some(q_tuple)) { + None => in_cost, + Some(props) => match props { + Some(_props) => { + if lengths { + _props.duration.unwrap_or(in_cost) + } else { + _props.error.unwrap_or(in_cost) + } + } None => in_cost, }, - None => in_cost, - }; - Ok(cost) - } - None => Ok(in_cost), - } - }; + }, + Err(_) => in_cost, + }; + Ok(cost) + }; let mut preferred_direction: Option = None; @@ -999,18 +961,18 @@ fn preferred_direction( } } - if preferred_direction.is_none() && (gate_lengths.is_some() || gate_errors.is_some()) { + if preferred_direction.is_none() { let mut cost_0_1: f64 = f64::INFINITY; let mut cost_1_0: f64 = f64::INFINITY; // Try to find the cost in gate_lengths - cost_0_1 = compute_cost(gate_lengths, &qubits, cost_0_1)?; - cost_1_0 = compute_cost(gate_lengths, &reverse_qubits, cost_1_0)?; + cost_0_1 = compute_cost(true, &qubits, cost_0_1)?; + cost_1_0 = compute_cost(true, &reverse_qubits, cost_1_0)?; // If no valid cost was found in gate_lengths, check gate_errors if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { - cost_0_1 = compute_cost(gate_errors, &qubits, cost_0_1)?; - cost_1_0 = compute_cost(gate_errors, &reverse_qubits, cost_1_0)?; + cost_0_1 = compute_cost(false, &qubits, cost_0_1)?; + cost_1_0 = compute_cost(false, &reverse_qubits, cost_1_0)?; } if cost_0_1 < cost_1_0 { diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 76d70860798f..de044cda457c 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -526,6 +526,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: dag, list(qubit_indices.values()), self._min_qubits, + kwargs["target"], self._approximation_degree, kwargs["basis_gates"], _neighbor_table, @@ -535,7 +536,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: kwargs["pulse_optimize"], _gate_lengths, _gate_errors, - kwargs["target"], ) return out else: From f527018b6309be4c10b6012dcbbfc274697d6c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Fri, 13 Sep 2024 16:33:18 +0200 Subject: [PATCH 06/45] Some cleanup --- crates/accelerate/src/two_qubit_decompose.rs | 7 +- crates/accelerate/src/unitary_synthesis.rs | 310 ++++++++---------- .../passes/synthesis/unitary_synthesis.py | 15 +- 3 files changed, 140 insertions(+), 192 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 0330d3ad4029..bef24863d0e1 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -1163,7 +1163,6 @@ impl TwoQubitGateSequence { global_phase: 0., } } - fn __getstate__(&self) -> (TwoQubitSequenceVec, f64) { (self.gates.clone(), self.global_phase) } @@ -1190,6 +1189,12 @@ impl TwoQubitGateSequence { } } +impl Default for TwoQubitGateSequence { + fn default() -> Self { + Self::new() + } +} + #[derive(Clone, Debug)] #[allow(non_snake_case)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 00973e6397cd..ebca5091472c 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -9,6 +9,7 @@ // 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. +#![allow(clippy::too_many_arguments)] #[cfg(feature = "cache_pygates")] use std::cell::OnceCell; @@ -26,10 +27,8 @@ use numpy::{IntoPyArray, PyReadonlyArray2}; use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; use smallvec::{smallvec, SmallVec}; -// use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; use pyo3::prelude::*; -use pyo3::pybacked::PyBackedStr; -use pyo3::types::{IntoPyDict, PyDict, PyList, PyString, PyTuple}; +use pyo3::types::{IntoPyDict, PyDict, PyList, PyString}; use pyo3::wrap_pyfunction; use pyo3::Python; @@ -161,7 +160,7 @@ fn dag_from_2q_gate_sequence( for (gate, params, qubit_ids) in gate_vec { let gate_node = match gate { - None => sequence.get_decomp_gate().unwrap(), // should be some + None => sequence.get_decomp_gate().unwrap(), Some(gate) => *gate, }; @@ -210,56 +209,53 @@ fn compute_2q_error( ) -> f64 { let mut gate_fidelities = Vec::new(); let mut score_instruction = |instruction: &PackedInstruction, - inst_qubits: SmallVec<[PhysicalQubit; 2]>| + inst_qubits: &SmallVec<[PhysicalQubit; 2]>| -> PyResult<()> { - // these are the keys - match target.operation_names_for_qargs(Some(&inst_qubits)) { - Ok(names) => { - for name in names { - let target_op = target.operation_from_name(name).unwrap(); - let op = instruction; - let are_params_close = match &op.params { - Some(params) => { - params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { - p1.is_close(py, p2, 1e-10) - .expect("Don't fail please I beg you") - }) - } - None => false, - }; - let is_parametrized = target_op - .params - .iter() - .any(|param| matches!(param, Param::ParameterExpression(_))); - if target_op.operation.name() == op.op.name() - && (is_parametrized || are_params_close) - { - match target[name].get(Some(&inst_qubits)) { - None => gate_fidelities.push(1.0), - Some(props) => gate_fidelities.push( - 1.0 - match props.clone() { - Some(props) => props.error.unwrap_or(0.0), - None => 0.0, - }, - ), - } - break; - } + match target.operation_names_for_qargs(Some(inst_qubits)) { + Ok(names) => { + for name in names { + let target_op = target.operation_from_name(name).unwrap(); + let are_params_close = if let Some(params) = &instruction.params { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) + } else { + false + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == instruction.op.name() + && (is_parametrized || are_params_close) + { + match target[name].get(Some(inst_qubits)) { + None => gate_fidelities.push(1.0), + Some(props) => gate_fidelities.push( + 1.0 - match props.clone() { + Some(props) => props.error.unwrap_or(0.0), + None => 0.0, + }, + ), } - Ok(()) - } - Err(_) => { - Err(QiskitError::new_err( - format!("Encountered a bad synthesis. Target has no {instruction:?} on qubits {inst_qubits:?}.") - )) + break; } } + Ok(()) + } + Err(_) => { + Err(QiskitError::new_err( + format!("Encountered a bad synthesis. Target has no {instruction:?} on qubits {inst_qubits:?}.") + )) + } + } }; if let UnitarySynthesisReturnType::DAGType(synth_dag) = synth_circuit { for node in synth_dag .topological_op_nodes() - .expect("cannot return error here (don't know how to handle it later)") + .expect("Unexpected error in dag.topological_op_nodes()") { if let NodeType::Operation(inst) = &synth_dag.dag()[node] { let inst_qubits = synth_dag @@ -267,7 +263,7 @@ fn compute_2q_error( .iter() .map(|q| wire_map[q]) .collect(); - let _ = score_instruction(inst, inst_qubits); + let _ = score_instruction(inst, &inst_qubits); } } } else { @@ -277,7 +273,7 @@ fn compute_2q_error( } // This is the outer-most run function. It is meant to be called from Python inside `UnitarySynthesis.run()` -// This loop iterates over the dag and calls `run_2q_unitary_synthesis` (defined below). +// This loop iterates over the dag and calls `run_2q_unitary_synthesis` #[pyfunction] #[pyo3(name = "run_default_main_loop")] fn py_run_default_main_loop( @@ -287,73 +283,52 @@ fn py_run_default_main_loop( min_qubits: usize, target: &Target, approximation_degree: Option, - basis_gates: Option>, - neighbor_table: Option, + neighbor_table: Option<&NeighborTable>, distance_matrix: Option>, - // coupling_map: Option, natural_direction: Option, - pulse_optimize: Option, - gate_lengths: Option>, - gate_errors: Option>, ) -> PyResult { - // Create new empty dag - let mut out_dag = dag.copy_empty_like(py, "alike")?; - // Collect node ids into vec not to mix mutable and unmutable refs + let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); + let dag_to_circuit = imports::DAG_TO_CIRCUIT.get_bound(py); + let node_ids: Vec = dag.op_nodes(false).collect(); - // Iterate recursively over control flow blocks and "unwrap" for node in node_ids { if let NodeType::Operation(inst) = &dag.dag()[node] { if inst.op.control_flow() { if let OperationRef::Instruction(py_inst) = inst.op.view() { - let raw_blocks = py_inst.instruction.getattr(py, "blocks")?; - let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); - let dag_to_circuit = imports::DAG_TO_CIRCUIT.get_bound(py); - let mut new_blocks = Vec::new(); - let raw_blocks = raw_blocks.bind(py).iter()?; - for raw_block in raw_blocks { - let block_obj = raw_block?; - let mut new_dag: DAGCircuit = - circuit_to_dag.call1((block_obj.clone(),))?.extract()?; - let block_qargs = dag.get_qargs(inst.qubits); - - // map qubit indices - let new_ids = block_qargs - .iter() - .map(|qarg| qubit_indices.get_item(qarg.0 as usize).expect("plz")); - let new_qubit_indices = PyList::new_bound(py, new_ids); - + for raw_block in py_inst.instruction.getattr(py, "blocks")?.bind(py).iter()? { + let new_ids = dag.get_qargs(inst.qubits).iter().map(|qarg| { + qubit_indices + .get_item(qarg.0 as usize) + .expect("Unexpected index error in DAG") + }); let res = py_run_default_main_loop( py, - &mut new_dag, - &new_qubit_indices, + &mut circuit_to_dag.call1((raw_block?,))?.extract()?, + &PyList::new_bound(py, new_ids), min_qubits, target, approximation_degree, - basis_gates.clone(), - neighbor_table.clone(), + neighbor_table, distance_matrix.clone(), - // coupling_map.clone(), natural_direction, - pulse_optimize, - gate_lengths.clone(), - gate_errors.clone(), )?; - let res_circuit = dag_to_circuit.call1((res,))?; - new_blocks.push(res_circuit); + new_blocks.push(dag_to_circuit.call1((res,))?); } - - let bound_py_inst = py_inst.instruction.bind(py); - let new_op = bound_py_inst.call_method1("replace_blocks", (new_blocks,))?; - let other_node = dag.get_node(py, node)?.clone(); - let _ = dag.substitute_node(other_node.bind(py), &new_op, true, false); + let old_node = dag.get_node(py, node)?.clone(); + let new_node = py_inst + .instruction + .bind(py) + .call_method1("replace_blocks", (new_blocks,))?; + let _ = dag.substitute_node(old_node.bind(py), &new_node, true, false); } } } } + let mut out_dag = dag.copy_empty_like(py, "alike")?; + // Iterate over nodes, find decomposers and run synthesis - // [E] implement as an iterator once it works (if it ever works) for node in dag.topological_op_nodes()? { if let NodeType::Operation(packed_instr) = &dag.dag()[node] { if packed_instr.op.name() == "unitary" @@ -365,26 +340,32 @@ fn py_run_default_main_loop( None => return Err(QiskitError::new_err("Unitary not found")), }; match unitary.shape() { + // Run 1Q synthesis [2, 2] => { - // do 1q stuff - let basis_set: Option> = basis_gates - .as_ref() - .map(|gates| gates.iter().map(|item| item.to_string()).collect()); - let qargs = dag.qargs_interner.get(packed_instr.qubits).to_vec(); - packed_instr.to_owned().qubits = out_dag.qargs_interner.insert_owned(qargs); - let _ = out_dag.push_back(py, packed_instr.clone()); - + let new_qargs = dag.qargs_interner.get(packed_instr.qubits).to_vec(); + let mut owned_instr = packed_instr.clone(); + owned_instr.qubits = out_dag.qargs_interner.insert_owned(new_qargs); + let _ = out_dag.push_back(py, owned_instr); + let basis_set: HashSet = target + .operation_names() + .map(|item| item.to_string()) + .collect(); optimize_1q_gates_decomposition( py, &mut out_dag, Some(target), - basis_set, + Some(basis_set), None, )?; } + // Run 2Q synthesis [4, 4] => { - // local to global qubit mapping - let mut wire_map = IndexMap::new(); + // This variable maps "relative qubits" in the instruction (which are Qubits) + // to "absolute qubits" in the DAG (which are PhysicalQubits). + // The synthesis algorithms will return an output in "relative qubits" and this + // map will be used to sort out the proper synthesis direction. + // Better names are welcome. + let mut wire_map: IndexMap = IndexMap::new(); for (i, q) in dag.get_qargs(packed_instr.qubits).iter().enumerate() { wire_map.insert( Qubit(i as u32), @@ -393,38 +374,32 @@ fn py_run_default_main_loop( ), ); } + + // The 2Q synth. output can be None, a DAGCircuit or a TwoQubitGateSequence let raw_synth_output: Option = run_2q_unitary_synthesis( py, unitary, &wire_map, approximation_degree, - &basis_gates, - // &coupling_map, - neighbor_table.clone(), - distance_matrix.clone(), + &neighbor_table, + &distance_matrix, natural_direction, - pulse_optimize, - &gate_lengths, - &gate_errors, target, )?; - // the output can be None, a DAGCircuit or a TwoQubitGateSequence match raw_synth_output { None => { let _ = out_dag.push_back(py, packed_instr.clone()); } Some(synth_output) => { - // synth dag is in the relative basis let synth_dag = match synth_output { UnitarySynthesisReturnType::DAGType(synth_dag) => synth_dag, UnitarySynthesisReturnType::TwoQSequenceType( synth_sequence, ) => Box::new(dag_from_2q_gate_sequence(py, synth_sequence)?), }; - let _ = out_dag.set_global_phase(add_global_phase( py, &out_dag.get_global_phase(), @@ -465,20 +440,14 @@ fn py_run_default_main_loop( Ok(out_dag) } -// This function is meant to be used instead of `DefaultUnitarySynthesisPlugin.run()` fn run_2q_unitary_synthesis( py: Python, unitary: Array2, wire_map: &IndexMap, approximation_degree: Option, - _basis_gates: &Option>, - neighbor_table: Option, - distance_matrix: Option>, - // coupling_map: &Option, + neighbor_table: &Option<&NeighborTable>, + distance_matrix: &Option>, natural_direction: Option, - _pulse_optimize: Option, - gate_lengths: &Option>, - gate_errors: &Option>, target: &Target, ) -> PyResult> { // run 2q decomposition (in Rust except for XXDecomposer) -> Return types will vary. @@ -496,16 +465,16 @@ fn run_2q_unitary_synthesis( // If we have a single TwoQubitBasisDecomposer, skip dag creation as we don't need to // store and can instead manually create the synthesized gates directly in the output dag if decomposers.len() == 1 { - let decomposer_item = decomposers.first().unwrap().clone(); + let decomposer_item = decomposers.first().unwrap(); if let DecomposerType::TwoQubitBasisDecomposer(ref decomposer) = decomposer_item.0 { let preferred_dir = preferred_direction( py, &decomposer_item.0, wire_map, natural_direction, - &neighbor_table, - &distance_matrix, - &target, + neighbor_table, + distance_matrix, + target, )?; let synth = synth_su4_no_dag( py, @@ -519,39 +488,39 @@ fn run_2q_unitary_synthesis( } } - let mut synth_circuits = Vec::new(); - - for decomposer in &decomposers { - let preferred_dir = preferred_direction( - py, - &decomposer.0, - wire_map, - natural_direction, - &neighbor_table, - &distance_matrix, - &target, - )?; - let synth_circuit: Result = synth_su4( - py, - &unitary, - &decomposer.0, - preferred_dir, - approximation_degree, - &decomposer.1, - ); - synth_circuits.push(synth_circuit); - } + let synth_circuits: Vec> = decomposers + .iter() + .map(|decomposer| { + let preferred_dir = preferred_direction( + py, + &decomposer.0, + wire_map, + natural_direction, + neighbor_table, + distance_matrix, + target, + )?; + synth_su4( + py, + &unitary, + &decomposer.0, + preferred_dir, + approximation_degree, + &decomposer.1, + ) + }) + .collect(); let synth_circuit: Option = if !synth_circuits.is_empty() { - // get the minimum of synth_circuits and return let mut synth_errors = Vec::new(); let mut synth_circuits_filt = Vec::new(); - // the synth circuits should all be in "relative basis" + for circuit in synth_circuits.iter().flatten() { let error = compute_2q_error(py, circuit, target, wire_map); synth_errors.push(error); synth_circuits_filt.push(circuit); } + synth_errors .iter() .enumerate() @@ -623,7 +592,7 @@ fn get_2q_decomposers_from_target( } for (q_pair, gates) in &qubit_gate_map { - for &key in gates { + for key in gates { match target.operation_from_name(key) { Ok(op) => { // if it's not a gate, move on to next iteration @@ -657,11 +626,8 @@ fn get_2q_decomposers_from_target( )); } - // Jump (target_basis_per_qubit) let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); - // here we do the part in possible_decomposers let target_basis_list = target.operation_names_for_qargs(Some(&smallvec![qubits[0]])); - match target_basis_list { Ok(basis_list) => { EULER_BASES @@ -729,8 +695,7 @@ fn get_2q_decomposers_from_target( for basis_2q in supercontrolled_basis.keys() { let mut basis_2q_fidelity: f64 = match available_2q_props.get(basis_2q) { Some(&(_, Some(e))) => 1.0 - e, - Some(&(_, None)) => 1.0, - None => 1.0, + _ => 1.0, }; if let Some(approx_degree) = approximation_degree { basis_2q_fidelity *= approx_degree; @@ -759,7 +724,7 @@ fn get_2q_decomposers_from_target( goodbye_set.insert("ecr"); let available_basis_set: IndexSet<&str> = - IndexSet::from_iter(available_2q_basis.clone().keys().copied()); + IndexSet::from_iter(available_2q_basis.keys().copied()); if goodbye_set.is_superset(&available_basis_set) { // TODO: decomposer cache thingy @@ -881,35 +846,23 @@ fn get_2q_decomposers_from_target( Ok(Some(decomposers)) } -// This is used in synth_su4 & company. I changed the logic with respect to the python reference code to return a bool -// instead of a tuple of qubits. The equivalence is the following: -// true = [0,1] -// false = [1,0] - fn preferred_direction( py: Python, decomposer: &DecomposerType, wire_map: &IndexMap, natural_direction: Option, - neighbor_table: &Option, + neighbor_table: &Option<&NeighborTable>, distance_matrix: &Option>, target: &Target, ) -> PyResult> { - // `decomposer2q` decomposes an SU(4) over `qubits`. A user sets `natural_direction` - // to indicate whether they prefer synthesis in a hardware-native direction. - // If yes, we return the `preferred_direction` here. If no hardware direction is - // preferred, we raise an error (unless natural_direction is None). - // We infer this from `coupling_map`, `gate_lengths`, `gate_errors`. - - // Returns [0, 1] if qubits are correct in the hardware-native direction. -> True - // Returns [1, 0] if qubits must be flipped to match hardware-native direction. -> False + // Returns: + // * true if gate qubits are in the hardware-native direction + // * false if gate qubits must be flipped to match hardware-native direction - // these are physical qubits let qubits: SmallVec<[PhysicalQubit; 2]> = wire_map.values().copied().collect(); let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); let decomposer2q_gate = match decomposer { DecomposerType::TwoQubitBasisDecomposer(decomp) => decomp.gate.clone(), - // Reason for clone: creates a temporary value which is freed while still in use DecomposerType::XXDecomposer(decomp) => decomp .getattr(py, "gate")? .getattr(py, "name")? @@ -920,17 +873,14 @@ fn preferred_direction( |lengths: bool, q_tuple: &SmallVec<[PhysicalQubit; 2]>, in_cost: f64| -> PyResult { let cost = match target.qargs_for_operation_name(&decomposer2q_gate) { Ok(_) => match target[&decomposer2q_gate].get(Some(q_tuple)) { - None => in_cost, - Some(props) => match props { - Some(_props) => { - if lengths { - _props.duration.unwrap_or(in_cost) - } else { - _props.error.unwrap_or(in_cost) - } + Some(Some(_props)) => { + if lengths { + _props.duration.unwrap_or(in_cost) + } else { + _props.error.unwrap_or(in_cost) } - None => in_cost, - }, + } + _ => in_cost, }, Err(_) => in_cost, }; diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index de044cda457c..4c64bdc6485d 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -27,8 +27,8 @@ from itertools import product from functools import partial import numpy as np +import rustworkx -from qiskit._accelerate.unitary_synthesis import run_default_main_loop from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.circuit import Gate, Parameter, CircuitInstruction from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping @@ -74,6 +74,8 @@ from qiskit.transpiler.passes.synthesis import plugin from qiskit.transpiler.target import Target +from qiskit._accelerate.sabre import NeighborTable +from qiskit._accelerate.unitary_synthesis import run_default_main_loop GATE_NAME_MAP = { "cx": CXGate._standard_gate, @@ -507,10 +509,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: ) if self.method == "default" and isinstance(kwargs["target"], Target): - print("RUST") - from qiskit._accelerate.sabre import NeighborTable - import rustworkx - _gate_lengths = _gate_lengths or _build_gate_lengths(self._backend_props, self._target) _gate_errors = _gate_errors or _build_gate_errors(self._backend_props, self._target) if self._coupling_map is not None: @@ -521,21 +519,16 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else: _dist_matrix = None _neighbor_table = None - + out = run_default_main_loop( dag, list(qubit_indices.values()), self._min_qubits, kwargs["target"], self._approximation_degree, - kwargs["basis_gates"], _neighbor_table, _dist_matrix, - # self._coupling_map, kwargs["natural_direction"], - kwargs["pulse_optimize"], - _gate_lengths, - _gate_errors, ) return out else: From eb815d191caac9b15e3d4a16480205a61a58b266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 16 Sep 2024 13:18:43 +0200 Subject: [PATCH 07/45] More cleanup --- crates/accelerate/src/unitary_synthesis.rs | 432 +++++++++------------ 1 file changed, 182 insertions(+), 250 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index ebca5091472c..bf6a1a244934 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -15,12 +15,11 @@ use std::cell::OnceCell; use std::f64::consts::PI; -use std::hash::Hash; use approx::relative_eq; use core::panic; use hashbrown::{HashMap, HashSet}; -use indexmap::{IndexMap, IndexSet}; +use indexmap::IndexMap; use ndarray::prelude::*; use num_complex::{Complex, Complex64}; use numpy::{IntoPyArray, PyReadonlyArray2}; @@ -56,7 +55,7 @@ const PI2: f64 = PI / 2.; const PI4: f64 = PI / 4.; #[derive(Clone, Debug)] -enum DecomposerType { +pub enum DecomposerType { TwoQubitBasisDecomposer(Box), XXDecomposer(PyObject), } @@ -67,6 +66,12 @@ enum UnitarySynthesisReturnType { TwoQSequenceType(TwoQubitUnitarySequence), } +pub struct DecomposerElement { + pub decomposer: DecomposerType, + pub decomp_gate: String, + pub decomp_gate_params: Option>, +} + #[derive(Clone, Debug)] pub struct TwoQubitUnitarySequence { pub gate_sequence: TwoQubitGateSequence, @@ -106,42 +111,6 @@ impl TwoQubitUnitarySequence { } } -#[derive(Hash, Eq, PartialEq)] -struct InteractionStrength((u64, i16, i8)); - -// f64 is not hashable so we divide it into a mantissa-exponent-sign triplet -fn integer_decode_f64(val: f64) -> (u64, i16, i8) { - let bits: u64 = val.to_bits(); - let sign: i8 = if bits >> 63 == 0 { 1 } else { -1 }; - let mut exponent: i16 = ((bits >> 52) & 0x7ff) as i16; - let mantissa = if exponent == 0 { - (bits & 0xfffffffffffff) << 1 - } else { - (bits & 0xfffffffffffff) | 0x10000000000000 - }; - - exponent -= 1023 + 52; - (mantissa, exponent, sign) -} -fn integer_encode_f64(mantissa: u64, exponent: i16, sign: i8) -> f64 { - let exponent = (exponent + 1023 + 52) as u64; - let sign_bit = if sign == -1 { 1u64 << 63 } else { 0 }; - let exponent_bits = (exponent & 0x7ff) << 52; - let mantissa_bits = mantissa & 0xfffffffffffff; - let bits = sign_bit | exponent_bits | mantissa_bits; - f64::from_bits(bits) -} - -impl InteractionStrength { - fn new(val: f64) -> InteractionStrength { - InteractionStrength(integer_decode_f64(val)) - } - fn to_f64(&self) -> f64 { - let (mantissa, exponent, sign) = self.0; - integer_encode_f64(mantissa, exponent, sign) - } -} - fn dag_from_2q_gate_sequence( py: Python<'_>, sequence: TwoQubitUnitarySequence, @@ -199,79 +168,6 @@ fn dag_from_2q_gate_sequence( Ok(target_dag) } -// This is the cost function for choosing the best 2q synthesis output. -// Used in `run_2q_unitary_synthesis`. -fn compute_2q_error( - py: Python<'_>, - synth_circuit: &UnitarySynthesisReturnType, - target: &Target, - wire_map: &IndexMap, -) -> f64 { - let mut gate_fidelities = Vec::new(); - let mut score_instruction = |instruction: &PackedInstruction, - inst_qubits: &SmallVec<[PhysicalQubit; 2]>| - -> PyResult<()> { - match target.operation_names_for_qargs(Some(inst_qubits)) { - Ok(names) => { - for name in names { - let target_op = target.operation_from_name(name).unwrap(); - let are_params_close = if let Some(params) = &instruction.params { - params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { - p1.is_close(py, p2, 1e-10) - .expect("Unexpected parameter expression error.") - }) - } else { - false - }; - let is_parametrized = target_op - .params - .iter() - .any(|param| matches!(param, Param::ParameterExpression(_))); - if target_op.operation.name() == instruction.op.name() - && (is_parametrized || are_params_close) - { - match target[name].get(Some(inst_qubits)) { - None => gate_fidelities.push(1.0), - Some(props) => gate_fidelities.push( - 1.0 - match props.clone() { - Some(props) => props.error.unwrap_or(0.0), - None => 0.0, - }, - ), - } - break; - } - } - Ok(()) - } - Err(_) => { - Err(QiskitError::new_err( - format!("Encountered a bad synthesis. Target has no {instruction:?} on qubits {inst_qubits:?}.") - )) - } - } - }; - - if let UnitarySynthesisReturnType::DAGType(synth_dag) = synth_circuit { - for node in synth_dag - .topological_op_nodes() - .expect("Unexpected error in dag.topological_op_nodes()") - { - if let NodeType::Operation(inst) = &synth_dag.dag()[node] { - let inst_qubits = synth_dag - .get_qargs(inst.qubits) - .iter() - .map(|q| wire_map[q]) - .collect(); - let _ = score_instruction(inst, &inst_qubits); - } - } - } else { - panic!("Synth output is not a DAG"); - } - 1.0 - gate_fidelities.into_iter().product::() -} - // This is the outer-most run function. It is meant to be called from Python inside `UnitarySynthesis.run()` // This loop iterates over the dag and calls `run_2q_unitary_synthesis` #[pyfunction] @@ -428,8 +324,15 @@ fn py_run_default_main_loop( } } _ => { - // do qsd stuff - todo!() + let qs_decomposition = + PyModule::import_bound(py, "qiskit.synthesis.unitary.qsd")? + .getattr("qs_decomposition")?; + + let synth_circ = + qs_decomposition.call1((unitary.clone().into_pyarray_bound(py),))?; + + let synth_dag = circuit_to_dag.call1((synth_circ,))?.extract()?; + out_dag = synth_dag; } } } else { @@ -466,10 +369,9 @@ fn run_2q_unitary_synthesis( // store and can instead manually create the synthesized gates directly in the output dag if decomposers.len() == 1 { let decomposer_item = decomposers.first().unwrap(); - if let DecomposerType::TwoQubitBasisDecomposer(ref decomposer) = decomposer_item.0 { + if let DecomposerType::TwoQubitBasisDecomposer(_) = decomposer_item.decomposer { let preferred_dir = preferred_direction( - py, - &decomposer_item.0, + decomposer_item, wire_map, natural_direction, neighbor_table, @@ -479,10 +381,9 @@ fn run_2q_unitary_synthesis( let synth = synth_su4_no_dag( py, &unitary, - decomposer, + decomposer_item, preferred_dir, approximation_degree, - &decomposer_item.1, )?; return Ok(Some(synth)); } @@ -492,8 +393,7 @@ fn run_2q_unitary_synthesis( .iter() .map(|decomposer| { let preferred_dir = preferred_direction( - py, - &decomposer.0, + decomposer, wire_map, natural_direction, neighbor_table, @@ -503,14 +403,81 @@ fn run_2q_unitary_synthesis( synth_su4( py, &unitary, - &decomposer.0, + decomposer, preferred_dir, approximation_degree, - &decomposer.1, ) }) .collect(); + fn compute_2q_error( + py: Python<'_>, + synth_circuit: &UnitarySynthesisReturnType, + target: &Target, + wire_map: &IndexMap, + ) -> f64 { + let mut gate_fidelities = Vec::new(); + let mut score_instruction = |instruction: &PackedInstruction, + inst_qubits: &SmallVec<[PhysicalQubit; 2]>| + -> PyResult<()> { + match target.operation_names_for_qargs(Some(inst_qubits)) { + Ok(names) => { + for name in names { + let target_op = target.operation_from_name(name).unwrap(); + let are_params_close = if let Some(params) = &instruction.params { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) + } else { + false + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == instruction.op.name() + && (is_parametrized || are_params_close) + { + match target[name].get(Some(inst_qubits)) { + Some(Some(props)) => gate_fidelities.push( + 1.0 - props.error.unwrap_or(0.0) + ), + _ => gate_fidelities.push(1.0), + } + break; + } + } + Ok(()) + } + Err(_) => { + Err(QiskitError::new_err( + format!("Encountered a bad synthesis. Target has no {instruction:?} on qubits {inst_qubits:?}.") + )) + } + } + }; + + if let UnitarySynthesisReturnType::DAGType(synth_dag) = synth_circuit { + for node in synth_dag + .topological_op_nodes() + .expect("Unexpected error in dag.topological_op_nodes()") + { + if let NodeType::Operation(inst) = &synth_dag.dag()[node] { + let inst_qubits = synth_dag + .get_qargs(inst.qubits) + .iter() + .map(|q| wire_map[q]) + .collect(); + let _ = score_instruction(inst, &inst_qubits); + } + } + } else { + panic!("Synth output is not a DAG"); + } + 1.0 - gate_fidelities.into_iter().product::() + } + let synth_circuit: Option = if !synth_circuits.is_empty() { let mut synth_errors = Vec::new(); let mut synth_circuits_filt = Vec::new(); @@ -539,7 +506,7 @@ fn get_2q_decomposers_from_target( target: &Target, qubits: &SmallVec<[PhysicalQubit; 2]>, approximation_degree: Option, -) -> PyResult>)>>> { +) -> PyResult>> { let qubits: SmallVec<[PhysicalQubit; 2]> = qubits.clone(); let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); // HERE: caching @@ -656,11 +623,11 @@ fn get_2q_decomposers_from_target( target_basis_set.remove(EulerBasis::ZSX); } - let available_1q_basis: IndexSet<&str> = - IndexSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); + let available_1q_basis: HashSet<&str> = + HashSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); // find all decomposers - let mut decomposers: Vec<(DecomposerType, Option>)> = Vec::new(); + let mut decomposers: Vec = Vec::new(); fn is_supercontrolled(op: &NormalOperation) -> bool { match op.operation.matrix(&op.params) { @@ -708,24 +675,20 @@ fn get_2q_decomposers_from_target( basis_1q, None, )?; - decomposers.push(( - DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), - Some(gate.params.clone()), - )); + + decomposers.push(DecomposerElement { + decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), + decomp_gate: gate.operation.name().to_string(), + decomp_gate_params: Some(gate.params.clone()), + }); } } // If our 2q basis gates are a subset of cx, ecr, or cz then we know TwoQubitBasisDecomposer // is an ideal decomposition and there is no need to bother calculating the XX embodiments // or try the XX decomposer - let mut goodbye_set = IndexSet::new(); - goodbye_set.insert("cx"); - goodbye_set.insert("cz"); - goodbye_set.insert("ecr"); - - let available_basis_set: IndexSet<&str> = - IndexSet::from_iter(available_2q_basis.keys().copied()); - + let available_basis_set: HashSet<&str> = available_2q_basis.keys().copied().collect(); + let goodbye_set: HashSet<&str> = vec!["cx", "cz", "ecr"].into_iter().collect(); if goodbye_set.is_superset(&available_basis_set) { // TODO: decomposer cache thingy return Ok(Some(decomposers)); @@ -733,66 +696,57 @@ fn get_2q_decomposers_from_target( // Let's now look for possible controlled decomposers (i.e. XXDecomposer) let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis - .clone() .into_iter() .filter(|(_, v)| is_controlled(v)) .collect(); - - let mut basis_2q_fidelity: IndexMap = IndexMap::new(); - - // the embodiments will be a list of circuit representations - let mut embodiments: IndexMap> = IndexMap::new(); let mut pi2_basis: Option<&str> = None; + let xx_embodiments = PyModule::import_bound(py, "qiskit.synthesis.two_qubit.xx_decompose")? + .getattr("XXEmbodiments")?; + + // The xx decomposer args are the interaction strength (f64), basis_2q_fidelity (f64), + // and embodiments (Bound<'_, PyAny>). + let xx_decomposer_args = controlled_basis.iter().map( + |(name, op)| -> PyResult<(f64, f64, pyo3::Bound<'_, pyo3::PyAny>)> { + let strength = 2.0 + * TwoQubitWeylDecomposition::new_inner( + op.operation.matrix(&op.params).unwrap().view(), + None, + None, + ) + .unwrap() + .a; + let mut fidelity_value = match available_2q_props.get(name) { + Some(&(_, error)) => 1.0 - error.unwrap_or(0.0), + None => 1.0, + }; + if let Some(approx_degree) = approximation_degree { + fidelity_value *= approx_degree; + } + let mut embodiment = + xx_embodiments.get_item(op.clone().into_py(py).getattr(py, "base_class")?)?; //XXEmbodiments[v.base_class]; - for (k, v) in controlled_basis.iter() { - let strength = 2.0 - * TwoQubitWeylDecomposition::new_inner( - v.operation.matrix(&v.params).unwrap().view(), - None, - None, - ) - .unwrap() - .a; - // each strength has its own fidelity - let fidelity_value = match available_2q_props.get(k) { - Some(&(_, error)) => 1.0 - error.unwrap_or(0.0), - None => 1.0, - }; - basis_2q_fidelity.insert(InteractionStrength::new(strength), fidelity_value); - - // rewrite XX of the same strength in terms of it - let xx_embodiments = PyModule::import_bound(py, "qiskit.synthesis.two_qubit.xx_decompose")? - .getattr("XXEmbodiments")?; - - // The embodiment should be a py object representing a quantum circuit - let embodiment = - xx_embodiments.get_item(v.clone().into_py(py).getattr(py, "base_class")?)?; //XXEmbodiments[v.base_class]; - - // This is 100% gonna fail - if embodiment.getattr("parameters")?.len()? == 1 { - embodiments.insert( - InteractionStrength::new(strength), - embodiment.call_method1("assign_parameters", (vec![strength],))?, - ); - } else { - embodiments.insert(InteractionStrength::new(strength), embodiment); - } - - // basis equivalent to CX are well optimized so use for the pi/2 angle if available - if relative_eq!(strength, PI2) && supercontrolled_basis.contains_key(k) { - pi2_basis = Some(v.operation.name()); - } - } - - // if we are using the approximation_degree knob, use it to scale already-given fidelities - if let Some(approx_degree) = approximation_degree { - for fidelity in basis_2q_fidelity.values_mut() { - *fidelity *= approx_degree; - } + if embodiment.getattr("parameters")?.len()? == 1 { + embodiment = embodiment.call_method1("assign_parameters", (vec![strength],))?; + } + // basis equivalent to CX are well optimized so use for the pi/2 angle if available + if relative_eq!(strength, PI2) && supercontrolled_basis.contains_key(name) { + pi2_basis = Some(op.operation.name()); + } + Ok((strength, fidelity_value, embodiment)) + }, + ); + + let basis_2q_fidelity_dict = PyDict::new_bound(py); + let embodiments_dict = PyDict::new_bound(py); + let mut not_empty = false; + for (strength, fidelity, embodiment) in xx_decomposer_args.flatten() { + not_empty = true; + basis_2q_fidelity_dict.set_item(strength, fidelity)?; + embodiments_dict.set_item(strength, embodiment.into_py(py))?; } // Iterate over 2q fidelities ans select decomposers - if !basis_2q_fidelity.is_empty() { + if not_empty { let xx_decomposer: Bound<'_, PyAny> = PyModule::import_bound(py, "qiskit.synthesis.two_qubit.xx_decompose")? .getattr("XXDecomposer")?; @@ -821,34 +775,33 @@ fn get_2q_decomposers_from_target( None }; - let basis_2q_fidelity_dict = PyDict::new_bound(py); - for (k, v) in basis_2q_fidelity.iter() { - basis_2q_fidelity_dict.set_item(k.to_f64(), v)?; - } - - let embodiments_dict = PyDict::new_bound(py); - - // Use iterator to populate PyDict - embodiments.iter().for_each(|(key, value)| { - embodiments_dict - .set_item(key.to_f64(), value.clone().into_py(py)) - .unwrap(); - }); let decomposer = xx_decomposer.call1(( - basis_2q_fidelity_dict, + &basis_2q_fidelity_dict, PyString::new_bound(py, basis_1q), - embodiments_dict, + &embodiments_dict, pi2_decomposer, - ))?; //instantiate properly - decomposers.push((DecomposerType::XXDecomposer(decomposer.into()), None)); + ))?; + let decomposer_gate = decomposer.getattr("gate")?; + + // .getattr("name")? + // .extract::()?; + + decomposers.push(DecomposerElement { + decomposer: DecomposerType::XXDecomposer(decomposer.into()), + decomp_gate: decomposer_gate.getattr("name")?.extract::()?, + decomp_gate_params: Some( + decomposer_gate + .getattr("params")? + .extract::>()?, + ), + }); } } Ok(Some(decomposers)) } fn preferred_direction( - py: Python, - decomposer: &DecomposerType, + decomposer: &DecomposerElement, wire_map: &IndexMap, natural_direction: Option, neighbor_table: &Option<&NeighborTable>, @@ -861,18 +814,11 @@ fn preferred_direction( let qubits: SmallVec<[PhysicalQubit; 2]> = wire_map.values().copied().collect(); let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); - let decomposer2q_gate = match decomposer { - DecomposerType::TwoQubitBasisDecomposer(decomp) => decomp.gate.clone(), - DecomposerType::XXDecomposer(decomp) => decomp - .getattr(py, "gate")? - .getattr(py, "name")? - .extract::(py)?, - }; let compute_cost = |lengths: bool, q_tuple: &SmallVec<[PhysicalQubit; 2]>, in_cost: f64| -> PyResult { - let cost = match target.qargs_for_operation_name(&decomposer2q_gate) { - Ok(_) => match target[&decomposer2q_gate].get(Some(q_tuple)) { + let cost = match target.qargs_for_operation_name(&decomposer.decomp_gate) { + Ok(_) => match target[&decomposer.decomp_gate].get(Some(q_tuple)) { Some(Some(_props)) => { if lengths { _props.duration.unwrap_or(in_cost) @@ -948,14 +894,13 @@ fn preferred_direction( fn synth_su4( py: Python, su4_mat: &Array2, - decomposer_2q: &DecomposerType, + decomposer_2q: &DecomposerElement, preferred_direction: Option, approximation_degree: Option, - decomposer_gate_params: &Option>, ) -> PyResult { // double check approximation_degree None let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth_dag = match decomposer_2q { + let synth_dag = match &decomposer_2q.decomposer { // the output will be a dag in the relative basis DecomposerType::XXDecomposer(decomposer) => { let mut kwargs = HashMap::<&str, bool>::new(); @@ -978,7 +923,7 @@ fn synth_su4( let sequence = TwoQubitUnitarySequence { gate_sequence: synth, decomp_gate: Some(decomposer.gate.clone()), - decomp_gate_params: decomposer_gate_params.clone(), + decomp_gate_params: decomposer_2q.decomp_gate_params.clone(), }; dag_from_2q_gate_sequence(py, sequence)? } @@ -1004,16 +949,10 @@ fn synth_su4( let synth_dir = match synth_direction.as_slice() { [0, 1] => true, [1, 0] => false, - _ => panic!(), + _ => panic!("Only 2 possible synth directions."), }; if synth_dir != preferred_dir { - reversed_synth_su4( - py, - su4_mat, - decomposer_2q, - approximation_degree, - decomposer_gate_params, - ) + reversed_synth_su4(py, su4_mat, decomposer_2q, approximation_degree) } else { Ok(UnitarySynthesisReturnType::DAGType(Box::new(synth_dag))) } @@ -1028,19 +967,21 @@ fn synth_su4( fn synth_su4_no_dag( py: Python<'_>, su4_mat: &Array2, - decomposer_2q: &TwoQubitBasisDecomposer, + decomposer_2q: &DecomposerElement, preferred_direction: Option, approximation_degree: Option, - decomp_gate_params: &Option>, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth = decomposer_2q.call_inner(su4_mat.view(), None, is_approximate, None)?; - let decomp_gate = decomposer_2q.gate.clone(); + let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else { + panic!("synth su4 no dag should only be called for TwoQubitBasisDecomposer") + }; let sequence = TwoQubitUnitarySequence { gate_sequence: synth.clone(), - decomp_gate: Some(decomp_gate), - decomp_gate_params: decomp_gate_params.clone(), + decomp_gate: Some(decomposer_2q.decomp_gate.clone()), + decomp_gate_params: decomposer_2q.decomp_gate_params.clone(), }; //synth_direction is calculated in terms of logical qubits @@ -1063,15 +1004,7 @@ fn synth_su4_no_dag( _ => panic!(), }; if synth_dir != preferred_dir { - reversed_synth_su4( - py, - su4_mat, - &DecomposerType::TwoQubitBasisDecomposer(Box::new( - decomposer_2q.clone(), - )), - approximation_degree, - decomp_gate_params, - ) + reversed_synth_su4(py, su4_mat, decomposer_2q, approximation_degree) } else { Ok(UnitarySynthesisReturnType::TwoQSequenceType(sequence)) } @@ -1085,9 +1018,8 @@ fn synth_su4_no_dag( fn reversed_synth_su4( py: Python<'_>, su4_mat: &Array2, - decomposer_2q: &DecomposerType, + decomposer_2q: &DecomposerElement, approximation_degree: Option, - decomp_gate_params: &Option>, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; let mut su4_mat_mm = su4_mat.clone(); @@ -1104,7 +1036,7 @@ fn reversed_synth_su4( su4_mat_mm.slice_mut(s![.., 1]).assign(&col_2); su4_mat_mm.slice_mut(s![.., 2]).assign(&col_1); - let synth_dag = match decomposer_2q { + let synth_dag = match &decomposer_2q.decomposer { DecomposerType::XXDecomposer(decomposer) => { // the output will be a dag in the relative basis let mut kwargs = HashMap::<&str, bool>::new(); @@ -1126,7 +1058,7 @@ fn reversed_synth_su4( let sequence = TwoQubitUnitarySequence { gate_sequence: synth, decomp_gate: Some(decomp_gate), - decomp_gate_params: decomp_gate_params.clone(), + decomp_gate_params: decomposer_2q.decomp_gate_params.clone(), }; // the output will be a sequence in the relative basis dag_from_2q_gate_sequence(py, sequence)? From 749841954859f4b2a5d74173c8206c83235a293b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:53:06 +0200 Subject: [PATCH 08/45] Apply inline suggestions from Matt's code review Co-authored-by: Matthew Treinish --- crates/accelerate/src/unitary_synthesis.rs | 34 +++++++++------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index bf6a1a244934..0511e3f3f592 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -712,8 +712,7 @@ fn get_2q_decomposers_from_target( op.operation.matrix(&op.params).unwrap().view(), None, None, - ) - .unwrap() + )? .a; let mut fidelity_value = match available_2q_props.get(name) { Some(&(_, error)) => 1.0 - error.unwrap_or(0.0), @@ -781,17 +780,15 @@ fn get_2q_decomposers_from_target( &embodiments_dict, pi2_decomposer, ))?; - let decomposer_gate = decomposer.getattr("gate")?; + let decomposer_gate = decomposer.getattr(intern!(py, "gate"))?; - // .getattr("name")? - // .extract::()?; decomposers.push(DecomposerElement { decomposer: DecomposerType::XXDecomposer(decomposer.into()), - decomp_gate: decomposer_gate.getattr("name")?.extract::()?, + decomp_gate: decomposer_gate.getattr(intern!(py, "name"))?.extract::()?, decomp_gate_params: Some( decomposer_gate - .getattr("params")? + .getattr(intern!(py, "params"))? .extract::>()?, ), }); @@ -816,7 +813,7 @@ fn preferred_direction( let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); let compute_cost = - |lengths: bool, q_tuple: &SmallVec<[PhysicalQubit; 2]>, in_cost: f64| -> PyResult { + |lengths: bool, q_tuple: &[PhysicalQubit; 2], in_cost: f64| -> PyResult { let cost = match target.qargs_for_operation_name(&decomposer.decomp_gate) { Ok(_) => match target[&decomposer.decomp_gate].get(Some(q_tuple)) { Some(Some(_props)) => { @@ -903,14 +900,15 @@ fn synth_su4( let synth_dag = match &decomposer_2q.decomposer { // the output will be a dag in the relative basis DecomposerType::XXDecomposer(decomposer) => { - let mut kwargs = HashMap::<&str, bool>::new(); - kwargs.insert("approximate", is_approximate); - kwargs.insert("use_dag", true); + let kwargs: HashMap<&str, bool> = [ + ("approximate", is_approximate), + ("use_dag", true) + ].iter().collect(); // can we avoid cloning the matrix to pass it to python? decomposer .call_method_bound( py, - "__call__", + intern!(py, "__call__"), (su4_mat.clone().into_pyarray_bound(py),), Some(&kwargs.into_py_dict_bound(py)), )? @@ -1025,16 +1023,12 @@ fn reversed_synth_su4( let mut su4_mat_mm = su4_mat.clone(); // Swap rows 1 and 2 - let row_1 = su4_mat_mm.slice(s![1, ..]).to_owned(); - let row_2 = su4_mat_mm.slice(s![2, ..]).to_owned(); - su4_mat_mm.slice_mut(s![1, ..]).assign(&row_2); - su4_mat_mm.slice_mut(s![2, ..]).assign(&row_1); + let (mut row_1, mut row_2) = su4_mat_mm.multi_slice_mut((s![1, ..], s![2, ..])); + azip!((x in &mut row_1, y in &mut row2) (*x, *y) = (*y, *x)) // Swap columns 1 and 2 - let col_1 = su4_mat_mm.slice(s![.., 1]).to_owned(); - let col_2 = su4_mat_mm.slice(s![.., 2]).to_owned(); - su4_mat_mm.slice_mut(s![.., 1]).assign(&col_2); - su4_mat_mm.slice_mut(s![.., 2]).assign(&col_1); + let (mut col_1, mut col_2) = su4_mat_mm.multi_slice_mut((s![.., 1], s![.., 2])); + azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)) let synth_dag = match &decomposer_2q.decomposer { DecomposerType::XXDecomposer(decomposer) => { From 3d8891cd040781c3660eb71fc226f578d01f214a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 17 Sep 2024 12:58:36 +0200 Subject: [PATCH 09/45] Apply remaining review suggestions: * Fix details after applying inline suggestions * Keep TwoQubitWeilDecomposition attributes private. Add getter. * Initialize new_blocks using size hint * Remove basis_set as it's not used if there is a target. * Use ref_qubits ([PhysicalQubit; 2]) instead of wire_map (IndexMap) * Define static GOODBYE_SET as suggested * Use ImportOnceCell for XXDecomposer and XXEmbodiments to avoid importing in a loop. * Set preferred_direction without making it mutable. * Fix check_goodbye * Privatize assets * Use the add_global_phase method instead of the private function. * Add qs_decomposition to imports * Simplify flip_bits * Use NormalOperation to pass around decomposer gate and params info * First attempt at attaching synth circuits directly * Second attempt at attaching synth circuits directly * Use edge set for coupling map * Avoid exposing internals from NullableIndexMap * Use unitary_to_gate_sequence_inner instead of optimize_1q_gates_decomposition. * Use concat! in long error message. --- .../target_transpiler/nullable_index_map.rs | 2 +- crates/accelerate/src/two_qubit_decompose.rs | 15 +- crates/accelerate/src/unitary_synthesis.rs | 542 ++++++++---------- crates/circuit/src/dag_circuit.rs | 2 +- crates/circuit/src/imports.rs | 6 + .../passes/synthesis/unitary_synthesis.py | 17 +- 6 files changed, 278 insertions(+), 306 deletions(-) diff --git a/crates/accelerate/src/target_transpiler/nullable_index_map.rs b/crates/accelerate/src/target_transpiler/nullable_index_map.rs index 614a1e089c88..e6e2a0fca3a3 100644 --- a/crates/accelerate/src/target_transpiler/nullable_index_map.rs +++ b/crates/accelerate/src/target_transpiler/nullable_index_map.rs @@ -42,7 +42,7 @@ where K: Eq + Hash + Clone, V: Clone, { - pub map: BaseMap, + map: BaseMap, null_val: Option, } diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index ab0c52baaf80..6198807475fc 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -472,11 +472,11 @@ impl Specialization { #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitWeylDecomposition { #[pyo3(get)] - pub a: f64, + a: f64, #[pyo3(get)] - pub b: f64, + b: f64, #[pyo3(get)] - pub c: f64, + c: f64, #[pyo3(get)] pub global_phase: f64, K1l: Array2, @@ -494,6 +494,15 @@ pub struct TwoQubitWeylDecomposition { } impl TwoQubitWeylDecomposition { + pub fn a(&self) -> &f64 { + &self.a + } + pub fn b(&self) -> &f64 { + &self.b + } + pub fn c(&self) -> &f64 { + &self.c + } fn weyl_gate( &self, simplify: bool, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 0511e3f3f592..6aa380ebb812 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -22,10 +22,12 @@ use hashbrown::{HashMap, HashSet}; use indexmap::IndexMap; use ndarray::prelude::*; use num_complex::{Complex, Complex64}; -use numpy::{IntoPyArray, PyReadonlyArray2}; +use numpy::IntoPyArray; +use pyo3::conversion::FromPyObjectBound; use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; use smallvec::{smallvec, SmallVec}; +use pyo3::intern; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyDict, PyList, PyString}; use pyo3::wrap_pyfunction; @@ -33,18 +35,16 @@ use pyo3::Python; use rustworkx_core::petgraph::stable_graph::NodeIndex; -use qiskit_circuit::dag_circuit::{add_global_phase, DAGCircuit, NodeType}; +use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; use qiskit_circuit::imports; use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation, PackedOperationType}; use qiskit_circuit::Qubit; use crate::euler_one_qubit_decomposer::{ - optimize_1q_gates_decomposition, EulerBasis, EulerBasisSet, EULER_BASES, EULER_BASIS_NAMES, + unitary_to_gate_sequence_inner, EulerBasis, EulerBasisSet, EULER_BASES, EULER_BASIS_NAMES, }; use crate::nlayout::PhysicalQubit; -use crate::sabre::neighbor_table::NeighborTable; -use crate::sabre::route::RoutingTargetView; use crate::target_transpiler::{NormalOperation, Target}; use crate::two_qubit_decompose::{ TwoQubitBasisDecomposer, TwoQubitGateSequence, TwoQubitWeylDecomposition, @@ -55,105 +55,73 @@ const PI2: f64 = PI / 2.; const PI4: f64 = PI / 4.; #[derive(Clone, Debug)] -pub enum DecomposerType { +enum DecomposerType { TwoQubitBasisDecomposer(Box), XXDecomposer(PyObject), } +struct DecomposerElement { + decomposer: DecomposerType, + gate: NormalOperation, +} + #[derive(Clone, Debug)] enum UnitarySynthesisReturnType { DAGType(Box), TwoQSequenceType(TwoQubitUnitarySequence), } - -pub struct DecomposerElement { - pub decomposer: DecomposerType, - pub decomp_gate: String, - pub decomp_gate_params: Option>, -} - #[derive(Clone, Debug)] -pub struct TwoQubitUnitarySequence { - pub gate_sequence: TwoQubitGateSequence, - pub decomp_gate: Option, - pub decomp_gate_params: Option>, +struct TwoQubitUnitarySequence { + gate_sequence: TwoQubitGateSequence, + decomp_gate: NormalOperation, } -impl TwoQubitUnitarySequence { - pub fn get_decomp_gate(&self) -> Option { - match self.decomp_gate.as_deref() { - Some("ch") => Some(StandardGate::CHGate), // 21 - Some("cx") => Some(StandardGate::CXGate), // 22 - Some("cy") => Some(StandardGate::CYGate), // 23 - Some("cz") => Some(StandardGate::CZGate), // 24 - Some("dcx") => Some(StandardGate::DCXGate), // 25 - Some("ecr") => Some(StandardGate::ECRGate), // 26 - Some("swap") => Some(StandardGate::SwapGate), // 27 - Some("iswap") => Some(StandardGate::ISwapGate), // 28 - Some("cp") => Some(StandardGate::CPhaseGate), // 29 - Some("crx") => Some(StandardGate::CRXGate), // 30 - Some("cry") => Some(StandardGate::CRYGate), // 31 - Some("crz") => Some(StandardGate::CRZGate), // 32 - Some("cs") => Some(StandardGate::CSGate), // 33 - Some("csdg") => Some(StandardGate::CSdgGate), // 34 - Some("csx") => Some(StandardGate::CSXGate), // 35 - Some("cu") => Some(StandardGate::CUGate), // 36 - Some("cu1") => Some(StandardGate::CU1Gate), // 37 - Some("cu3") => Some(StandardGate::CU3Gate), // 38 - Some("rxx") => Some(StandardGate::RXXGate), // 39 - Some("ryy") => Some(StandardGate::RYYGate), // 40 - Some("rzz") => Some(StandardGate::RZZGate), // 41 - Some("rzx") => Some(StandardGate::RZXGate), // 42 - Some("xx_minus_yy") => Some(StandardGate::XXMinusYYGate), // 43 - Some("xx_plus_yy") => Some(StandardGate::XXPlusYYGate), // 44 - _ => None, - } - } -} +static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; fn dag_from_2q_gate_sequence( py: Python<'_>, sequence: TwoQubitUnitarySequence, + out_dag_info: Option<(DAGCircuit, &[Qubit])>, ) -> PyResult { - let gate_vec = &sequence.gate_sequence.gates; - let mut target_dag = DAGCircuit::new(py)?; - let _ = target_dag.set_global_phase(Param::Float(sequence.gate_sequence.global_phase)); - - let mut instructions = Vec::new(); - let qubit: &Bound = imports::QUBIT.get_bound(py); - let mut qubits: Vec = vec![]; - let qubit_obj = qubit.call0()?; - qubits.push(target_dag.add_qubit_unchecked(py, &qubit_obj)?); + let (mut target_dag, mut out_qargs) = match out_dag_info { + Some((dag, qargs)) => (dag, qargs.to_vec()), + None => { + let mut out_qargs: Vec = vec![]; + let mut target_dag = DAGCircuit::new(py)?; + let qubit_obj = qubit.call0()?; + out_qargs.push(target_dag.add_qubit_unchecked(py, &qubit_obj)?); + (target_dag, out_qargs) + } + }; + + let _ = target_dag.add_global_phase(py, &Param::Float(sequence.gate_sequence.global_phase)); - for (gate, params, qubit_ids) in gate_vec { + let mut instructions = Vec::new(); + for (gate, params, qubit_ids) in &sequence.gate_sequence.gates { let gate_node = match gate { - None => sequence.get_decomp_gate().unwrap(), + None => sequence.decomp_gate.operation.standard_gate(), Some(gate) => *gate, }; - let mut gate_qubits = Vec::new(); - + let mut mapped_qargs = Vec::new(); for id in qubit_ids { - while *id as usize >= qubits.len() { + while *id as usize >= out_qargs.len() { let qubit_obj = qubit.call0()?; - qubits.push(target_dag.add_qubit_unchecked(py, &qubit_obj)?); + out_qargs.push(target_dag.add_qubit_unchecked(py, &qubit_obj)?); } - gate_qubits.push(qubits[*id as usize]); + mapped_qargs.push(out_qargs[*id as usize]); } let new_params: Option>> = match gate { Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), - None => sequence - .decomp_gate_params - .as_ref() - .map(|params| Box::new(params.clone())), + None => Some(Box::new(sequence.decomp_gate.params.clone())), }; let pi = PackedInstruction { op: PackedOperation::from_standard(gate_node), - qubits: target_dag.qargs_interner.insert(&gate_qubits), + qubits: target_dag.qargs_interner.insert(&mapped_qargs), clbits: target_dag.cargs_interner.get_default(), params: new_params, extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), @@ -165,9 +133,41 @@ fn dag_from_2q_gate_sequence( let _ = target_dag.extend(py, instructions.into_iter()); - Ok(target_dag) + Ok(target_dag.clone()) } +fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { + let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); + let target_basis_list = target.operation_names_for_qargs(Some(&smallvec![qubit])); + match target_basis_list { + Ok(basis_list) => { + EULER_BASES + .iter() + .enumerate() + .filter_map(|(idx, gates)| { + if !gates.iter().all(|gate| basis_list.contains(gate)) { + return None; + } + let basis = EULER_BASIS_NAMES[idx]; + Some(basis) + }) + .for_each(|basis| target_basis_set.add_basis(basis)); + } + Err(_) => target_basis_set.support_all(), + } + + if target_basis_set.basis_supported(EulerBasis::U3) + && target_basis_set.basis_supported(EulerBasis::U321) + { + target_basis_set.remove(EulerBasis::U3); + } + if target_basis_set.basis_supported(EulerBasis::ZSX) + && target_basis_set.basis_supported(EulerBasis::ZSXX) + { + target_basis_set.remove(EulerBasis::ZSX); + } + target_basis_set +} // This is the outer-most run function. It is meant to be called from Python inside `UnitarySynthesis.run()` // This loop iterates over the dag and calls `run_2q_unitary_synthesis` #[pyfunction] @@ -179,8 +179,7 @@ fn py_run_default_main_loop( min_qubits: usize, target: &Target, approximation_degree: Option, - neighbor_table: Option<&NeighborTable>, - distance_matrix: Option>, + coupling_edges: Option<&Bound<'_, PyAny>>, natural_direction: Option, ) -> PyResult { let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); @@ -191,8 +190,14 @@ fn py_run_default_main_loop( if let NodeType::Operation(inst) = &dag.dag()[node] { if inst.op.control_flow() { if let OperationRef::Instruction(py_inst) = inst.op.view() { - let mut new_blocks = Vec::new(); - for raw_block in py_inst.instruction.getattr(py, "blocks")?.bind(py).iter()? { + let raw_blocks: Vec>> = py_inst + .instruction + .getattr(py, "blocks")? + .bind(py) + .iter()? + .collect(); + let mut new_blocks = Vec::with_capacity(raw_blocks.len()); + for raw_block in raw_blocks { let new_ids = dag.get_qargs(inst.qubits).iter().map(|qarg| { qubit_indices .get_item(qarg.0 as usize) @@ -205,8 +210,7 @@ fn py_run_default_main_loop( min_qubits, target, approximation_degree, - neighbor_table, - distance_matrix.clone(), + coupling_edges, natural_direction, )?; new_blocks.push(dag_to_circuit.call1((res,))?); @@ -238,99 +242,97 @@ fn py_run_default_main_loop( match unitary.shape() { // Run 1Q synthesis [2, 2] => { - let new_qargs = dag.qargs_interner.get(packed_instr.qubits).to_vec(); - let mut owned_instr = packed_instr.clone(); - owned_instr.qubits = out_dag.qargs_interner.insert_owned(new_qargs); - let _ = out_dag.push_back(py, owned_instr); - let basis_set: HashSet = target - .operation_names() - .map(|item| item.to_string()) - .collect(); - optimize_1q_gates_decomposition( - py, - &mut out_dag, - Some(target), - Some(basis_set), + let qubit = dag.get_qargs(packed_instr.qubits)[0]; + let target_basis_set = + get_target_basis_set(target, PhysicalQubit::new(qubit.0)); + let sequence = unitary_to_gate_sequence_inner( + unitary.view(), + &target_basis_set, + qubit.0 as usize, None, - )?; + true, + None, + ) + .unwrap(); + + for gate in sequence.gates { + out_dag.insert_1q_on_incoming_qubit( + (gate.0, &gate.1), + NodeIndex::new(qubit.0 as usize), + ); + } + out_dag.add_global_phase(py, &Param::Float(sequence.global_phase))?; } // Run 2Q synthesis [4, 4] => { - // This variable maps "relative qubits" in the instruction (which are Qubits) - // to "absolute qubits" in the DAG (which are PhysicalQubits). - // The synthesis algorithms will return an output in "relative qubits" and this - // map will be used to sort out the proper synthesis direction. - // Better names are welcome. - let mut wire_map: IndexMap = IndexMap::new(); - for (i, q) in dag.get_qargs(packed_instr.qubits).iter().enumerate() { - wire_map.insert( - Qubit(i as u32), - PhysicalQubit::new( - qubit_indices.get_item(q.0 as usize)?.extract()?, - ), - ); - } - + let ref_qargs = dag.get_qargs(packed_instr.qubits); + // How to use ref_qubits: + // * index = output qubit from synthesis algorithm + // * value = correspoding physical qubit in dag/out_dag + let ref_qubits: [PhysicalQubit; 2] = [ + PhysicalQubit::new( + qubit_indices.get_item(ref_qargs[0].0 as usize)?.extract()?, + ), + PhysicalQubit::new( + qubit_indices.get_item(ref_qargs[1].0 as usize)?.extract()?, + ), + ]; // The 2Q synth. output can be None, a DAGCircuit or a TwoQubitGateSequence let raw_synth_output: Option = run_2q_unitary_synthesis( py, unitary, - &wire_map, + &ref_qubits, approximation_degree, - &neighbor_table, - &distance_matrix, + &coupling_edges, natural_direction, target, )?; + let out_qargs = dag.get_qargs(packed_instr.qubits); match raw_synth_output { None => { let _ = out_dag.push_back(py, packed_instr.clone()); } - Some(synth_output) => { - let synth_dag = match synth_output { - UnitarySynthesisReturnType::DAGType(synth_dag) => synth_dag, - UnitarySynthesisReturnType::TwoQSequenceType( - synth_sequence, - ) => Box::new(dag_from_2q_gate_sequence(py, synth_sequence)?), - }; - let _ = out_dag.set_global_phase(add_global_phase( - py, - &out_dag.get_global_phase(), - &synth_dag.get_global_phase(), - )?); - - for out_node in synth_dag.topological_op_nodes()? { - if let NodeType::Operation(mut out_packed_instr) = - synth_dag.dag()[out_node].clone() - { - let synth_qargs = - synth_dag.get_qargs(out_packed_instr.qubits); - let out_qargs = dag.get_qargs(packed_instr.qubits); - let mapped_qargs: Vec = synth_qargs - .iter() - .map(|qarg| out_qargs[qarg.0 as usize]) - .collect(); - - out_packed_instr.qubits = - out_dag.qargs_interner.insert(&mapped_qargs); - - let _ = out_dag.push_back(py, out_packed_instr.clone()); + Some(synth_output) => match synth_output { + UnitarySynthesisReturnType::DAGType(synth_dag) => { + out_dag.add_global_phase(py, &synth_dag.get_global_phase())?; + + for out_node in synth_dag.topological_op_nodes()? { + if let NodeType::Operation(mut out_packed_instr) = + synth_dag.dag()[out_node].clone() + { + let synth_qargs = + synth_dag.get_qargs(out_packed_instr.qubits); + let mapped_qargs: Vec = synth_qargs + .iter() + .map(|qarg| out_qargs[qarg.0 as usize]) + .collect(); + + out_packed_instr.qubits = + out_dag.qargs_interner.insert(&mapped_qargs); + + let _ = out_dag.push_back(py, out_packed_instr.clone()); + } } } - } + UnitarySynthesisReturnType::TwoQSequenceType(sequence) => { + out_dag = dag_from_2q_gate_sequence( + py, + sequence, + Some((out_dag, out_qargs)), + )?; + } + }, } } + // Run 3Q+ synthesis _ => { - let qs_decomposition = - PyModule::import_bound(py, "qiskit.synthesis.unitary.qsd")? - .getattr("qs_decomposition")?; - + let qs_decomposition: &Bound<'_, PyAny> = + imports::QS_DECOMPOSITION.get_bound(py); let synth_circ = qs_decomposition.call1((unitary.clone().into_pyarray_bound(py),))?; - let synth_dag = circuit_to_dag.call1((synth_circ,))?.extract()?; out_dag = synth_dag; } @@ -346,19 +348,18 @@ fn py_run_default_main_loop( fn run_2q_unitary_synthesis( py: Python, unitary: Array2, - wire_map: &IndexMap, + ref_qubits: &[PhysicalQubit; 2], approximation_degree: Option, - neighbor_table: &Option<&NeighborTable>, - distance_matrix: &Option>, + coupling_edges: &Option<&Bound<'_, PyAny>>, natural_direction: Option, target: &Target, ) -> PyResult> { // run 2q decomposition (in Rust except for XXDecomposer) -> Return types will vary. // step1: select decomposers let decomposers = { - let physical_qubits = wire_map.values().copied().collect(); + // let ref_qubits = ref_qubits; let decomposers_2q = - get_2q_decomposers_from_target(py, target, &physical_qubits, approximation_degree)?; + get_2q_decomposers_from_target(py, target, ref_qubits, approximation_degree)?; match decomposers_2q { Some(decomp) => decomp, None => Vec::new(), @@ -372,10 +373,9 @@ fn run_2q_unitary_synthesis( if let DecomposerType::TwoQubitBasisDecomposer(_) = decomposer_item.decomposer { let preferred_dir = preferred_direction( decomposer_item, - wire_map, + ref_qubits, natural_direction, - neighbor_table, - distance_matrix, + coupling_edges, target, )?; let synth = synth_su4_no_dag( @@ -394,10 +394,9 @@ fn run_2q_unitary_synthesis( .map(|decomposer| { let preferred_dir = preferred_direction( decomposer, - wire_map, + ref_qubits, natural_direction, - neighbor_table, - distance_matrix, + coupling_edges, target, )?; synth_su4( @@ -414,7 +413,7 @@ fn run_2q_unitary_synthesis( py: Python<'_>, synth_circuit: &UnitarySynthesisReturnType, target: &Target, - wire_map: &IndexMap, + ref_qubits: &[PhysicalQubit; 2], ) -> f64 { let mut gate_fidelities = Vec::new(); let mut score_instruction = |instruction: &PackedInstruction, @@ -467,7 +466,7 @@ fn run_2q_unitary_synthesis( let inst_qubits = synth_dag .get_qargs(inst.qubits) .iter() - .map(|q| wire_map[q]) + .map(|q| ref_qubits[q.0 as usize]) .collect(); let _ = score_instruction(inst, &inst_qubits); } @@ -483,7 +482,7 @@ fn run_2q_unitary_synthesis( let mut synth_circuits_filt = Vec::new(); for circuit in synth_circuits.iter().flatten() { - let error = compute_2q_error(py, circuit, target, wire_map); + let error = compute_2q_error(py, circuit, target, ref_qubits); synth_errors.push(error); synth_circuits_filt.push(circuit); } @@ -504,10 +503,10 @@ fn run_2q_unitary_synthesis( fn get_2q_decomposers_from_target( py: Python, target: &Target, - qubits: &SmallVec<[PhysicalQubit; 2]>, + qubits: &[PhysicalQubit; 2], approximation_degree: Option, ) -> PyResult>> { - let qubits: SmallVec<[PhysicalQubit; 2]> = qubits.clone(); + let qubits: SmallVec<[PhysicalQubit; 2]> = SmallVec::from_buf(*qubits); let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); // HERE: caching // TODO: here return cache --> implementation? @@ -571,16 +570,18 @@ fn get_2q_decomposers_from_target( available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - // Note that I had to make the map attribute public - if !target[key].map.is_empty() { - available_2q_props.insert( - key, - match &target[key][Some(q_pair)] { - Some(props) => (props.duration, props.error), - None => (None, None), - }, - ); - } + match target.qargs_for_operation_name(key) { + Ok(_) => { + available_2q_props.insert( + key, + match &target[key].get(Some(q_pair)) { + Some(Some(props)) => (props.duration, props.error), + _ => (None, None), + }, + ); + } + _ => continue, + }; } _ => continue, } @@ -593,35 +594,7 @@ fn get_2q_decomposers_from_target( )); } - let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); - let target_basis_list = target.operation_names_for_qargs(Some(&smallvec![qubits[0]])); - match target_basis_list { - Ok(basis_list) => { - EULER_BASES - .iter() - .enumerate() - .filter_map(|(idx, gates)| { - if !gates.iter().all(|gate| basis_list.contains(gate)) { - return None; - } - let basis = EULER_BASIS_NAMES[idx]; - Some(basis) - }) - .for_each(|basis| target_basis_set.add_basis(basis)); - } - Err(_) => target_basis_set.support_all(), - } - - if target_basis_set.basis_supported(EulerBasis::U3) - && target_basis_set.basis_supported(EulerBasis::U321) - { - target_basis_set.remove(EulerBasis::U3); - } - if target_basis_set.basis_supported(EulerBasis::ZSX) - && target_basis_set.basis_supported(EulerBasis::ZSXX) - { - target_basis_set.remove(EulerBasis::ZSX); - } + let target_basis_set = get_target_basis_set(target, qubits[0]); let available_1q_basis: HashSet<&str> = HashSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); @@ -629,24 +602,26 @@ fn get_2q_decomposers_from_target( // find all decomposers let mut decomposers: Vec = Vec::new(); + #[inline] fn is_supercontrolled(op: &NormalOperation) -> bool { match op.operation.matrix(&op.params) { None => false, Some(unitary_matrix) => { let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) .unwrap(); - relative_eq!(kak.a, PI4) && relative_eq!(kak.c, 0.0) + relative_eq!(*kak.a(), PI4) && relative_eq!(*kak.c(), 0.0) } } } + #[inline] fn is_controlled(op: &NormalOperation) -> bool { match op.operation.matrix(&op.params) { None => false, Some(unitary_matrix) => { let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) .unwrap(); - relative_eq!(kak.b, 0.0) && relative_eq!(kak.c, 0.0) + relative_eq!(*kak.b(), 0.0) && relative_eq!(*kak.c(), 0.0) } } } @@ -678,8 +653,7 @@ fn get_2q_decomposers_from_target( decomposers.push(DecomposerElement { decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), - decomp_gate: gate.operation.name().to_string(), - decomp_gate_params: Some(gate.params.clone()), + gate: gate.clone(), }); } } @@ -688,8 +662,13 @@ fn get_2q_decomposers_from_target( // is an ideal decomposition and there is no need to bother calculating the XX embodiments // or try the XX decomposer let available_basis_set: HashSet<&str> = available_2q_basis.keys().copied().collect(); - let goodbye_set: HashSet<&str> = vec!["cx", "cz", "ecr"].into_iter().collect(); - if goodbye_set.is_superset(&available_basis_set) { + + #[inline] + fn check_goodbye(basis_set: &HashSet<&str>) -> bool { + !basis_set.iter().any(|gate| !GOODBYE_SET.contains(gate)) + } + + if check_goodbye(&available_basis_set) { // TODO: decomposer cache thingy return Ok(Some(decomposers)); } @@ -700,8 +679,7 @@ fn get_2q_decomposers_from_target( .filter(|(_, v)| is_controlled(v)) .collect(); let mut pi2_basis: Option<&str> = None; - let xx_embodiments = PyModule::import_bound(py, "qiskit.synthesis.two_qubit.xx_decompose")? - .getattr("XXEmbodiments")?; + let xx_embodiments: &Bound<'_, PyAny> = imports::XX_EMBODIMENTS.get_bound(py); // The xx decomposer args are the interaction strength (f64), basis_2q_fidelity (f64), // and embodiments (Bound<'_, PyAny>). @@ -712,8 +690,9 @@ fn get_2q_decomposers_from_target( op.operation.matrix(&op.params).unwrap().view(), None, None, - )? - .a; + ) + .unwrap() + .a(); let mut fidelity_value = match available_2q_props.get(name) { Some(&(_, error)) => 1.0 - error.unwrap_or(0.0), None => 1.0, @@ -746,10 +725,7 @@ fn get_2q_decomposers_from_target( // Iterate over 2q fidelities ans select decomposers if not_empty { - let xx_decomposer: Bound<'_, PyAny> = - PyModule::import_bound(py, "qiskit.synthesis.two_qubit.xx_decompose")? - .getattr("XXDecomposer")?; - + let xx_decomposer: &Bound<'_, PyAny> = imports::XX_DECOMPOSER.get_bound(py); for basis_1q in &available_1q_basis { let pi2_decomposer = if let Some(pi_2_basis) = pi2_basis { if pi_2_basis == "cx" && *basis_1q == "ZSX" { @@ -780,17 +756,13 @@ fn get_2q_decomposers_from_target( &embodiments_dict, pi2_decomposer, ))?; - let decomposer_gate = decomposer.getattr(intern!(py, "gate"))?; - + let decomposer_gate = decomposer + .getattr(intern!(py, "gate"))? + .extract::()?; decomposers.push(DecomposerElement { decomposer: DecomposerType::XXDecomposer(decomposer.into()), - decomp_gate: decomposer_gate.getattr(intern!(py, "name"))?.extract::()?, - decomp_gate_params: Some( - decomposer_gate - .getattr(intern!(py, "params"))? - .extract::>()?, - ), + gate: decomposer_gate, }); } } @@ -799,23 +771,27 @@ fn get_2q_decomposers_from_target( fn preferred_direction( decomposer: &DecomposerElement, - wire_map: &IndexMap, + ref_qubits: &[PhysicalQubit; 2], natural_direction: Option, - neighbor_table: &Option<&NeighborTable>, - distance_matrix: &Option>, + coupling_edges: &Option<&Bound<'_, PyAny>>, target: &Target, ) -> PyResult> { // Returns: // * true if gate qubits are in the hardware-native direction // * false if gate qubits must be flipped to match hardware-native direction - let qubits: SmallVec<[PhysicalQubit; 2]> = wire_map.values().copied().collect(); - let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); + let qubits: [PhysicalQubit; 2] = *ref_qubits; + let mut reverse_qubits: [PhysicalQubit; 2] = qubits; + reverse_qubits.reverse(); let compute_cost = - |lengths: bool, q_tuple: &[PhysicalQubit; 2], in_cost: f64| -> PyResult { - let cost = match target.qargs_for_operation_name(&decomposer.decomp_gate) { - Ok(_) => match target[&decomposer.decomp_gate].get(Some(q_tuple)) { + |lengths: bool, q_tuple: [PhysicalQubit; 2], in_cost: f64| -> PyResult { + let cost = match target.qargs_for_operation_name(decomposer.gate.operation.name()) { + Ok(_) => match target[decomposer.gate.operation.name()].get(Some( + &q_tuple + .into_iter() + .collect::>(), + )) { Some(Some(_props)) => { if lengths { _props.duration.unwrap_or(in_cost) @@ -830,57 +806,56 @@ fn preferred_direction( Ok(cost) }; - let mut preferred_direction: Option = None; - - match natural_direction { - Some(false) => (), + let preferred_direction = match natural_direction { + Some(false) => None, _ => { // None or Some(true) - if let (Some(table), Some(matrix)) = (neighbor_table, distance_matrix) { - let routing_target = RoutingTargetView { - neighbors: table, - coupling: &table.coupling_graph(), - distance: matrix.as_array(), - }; - // find native gate directions from a (non-bidirectional) coupling map - let neighbors0 = &routing_target.neighbors[qubits[0]]; - let zero_one = neighbors0.contains(&qubits[1]); - let neighbors1 = &routing_target.neighbors[qubits[1]]; - let one_zero = neighbors1.contains(&qubits[0]); - match (zero_one, one_zero) { - (true, false) => preferred_direction = Some(true), - (false, true) => preferred_direction = Some(false), - _ => (), - } - } + if let Some(edges) = coupling_edges { + let edge_set: HashSet<(u32, u32)> = + HashSet::from_py_object_bound(edges.as_borrowed())?; + let zero_one = edge_set.contains(&(qubits[0].0, qubits[1].0)); + let one_zero = edge_set.contains(&(qubits[1].0, qubits[0].0)); - if preferred_direction.is_none() { - let mut cost_0_1: f64 = f64::INFINITY; - let mut cost_1_0: f64 = f64::INFINITY; + match (zero_one, one_zero) { + (true, false) => Some(true), + (false, true) => Some(false), + _ => { + let mut cost_0_1: f64 = f64::INFINITY; + let mut cost_1_0: f64 = f64::INFINITY; - // Try to find the cost in gate_lengths - cost_0_1 = compute_cost(true, &qubits, cost_0_1)?; - cost_1_0 = compute_cost(true, &reverse_qubits, cost_1_0)?; + // Try to find the cost in gate_lengths + cost_0_1 = compute_cost(true, qubits, cost_0_1)?; + cost_1_0 = compute_cost(true, reverse_qubits, cost_1_0)?; - // If no valid cost was found in gate_lengths, check gate_errors - if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { - cost_0_1 = compute_cost(false, &qubits, cost_0_1)?; - cost_1_0 = compute_cost(false, &reverse_qubits, cost_1_0)?; - } + // If no valid cost was found in gate_lengths, check gate_errors + if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { + cost_0_1 = compute_cost(false, qubits, cost_0_1)?; + cost_1_0 = compute_cost(false, reverse_qubits, cost_1_0)?; + } - if cost_0_1 < cost_1_0 { - preferred_direction = Some(true) - } else if cost_1_0 < cost_0_1 { - preferred_direction = Some(false) + if cost_0_1 < cost_1_0 { + Some(true) + } else if cost_1_0 < cost_0_1 { + Some(false) + } else { + None + } + } } + } else { + None } } - } + }; if natural_direction == Some(true) && preferred_direction.is_none() { - return Err(QiskitError::new_err( - format!("No preferred direction of gate on qubits {qubits:?} could be determined from coupling map or gate lengths / gate errors.") - )); + return Err(QiskitError::new_err(format!( + concat!( + "No preferred direction of gate on qubits {:?} ", + "could be determined from coupling map or gate lengths / gate errors." + ), + qubits + ))); } Ok(preferred_direction) @@ -900,10 +875,9 @@ fn synth_su4( let synth_dag = match &decomposer_2q.decomposer { // the output will be a dag in the relative basis DecomposerType::XXDecomposer(decomposer) => { - let kwargs: HashMap<&str, bool> = [ - ("approximate", is_approximate), - ("use_dag", true) - ].iter().collect(); + let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] + .into_iter() + .collect(); // can we avoid cloning the matrix to pass it to python? decomposer .call_method_bound( @@ -920,10 +894,9 @@ fn synth_su4( let synth = decomposer.call_inner(su4_mat.view(), None, is_approximate, None)?; let sequence = TwoQubitUnitarySequence { gate_sequence: synth, - decomp_gate: Some(decomposer.gate.clone()), - decomp_gate_params: decomposer_2q.decomp_gate_params.clone(), + decomp_gate: decomposer_2q.gate.clone(), }; - dag_from_2q_gate_sequence(py, sequence)? + dag_from_2q_gate_sequence(py, sequence, None)? } }; @@ -978,8 +951,7 @@ fn synth_su4_no_dag( let sequence = TwoQubitUnitarySequence { gate_sequence: synth.clone(), - decomp_gate: Some(decomposer_2q.decomp_gate.clone()), - decomp_gate_params: decomposer_2q.decomp_gate_params.clone(), + decomp_gate: decomposer_2q.gate.clone(), }; //synth_direction is calculated in terms of logical qubits @@ -1024,11 +996,11 @@ fn reversed_synth_su4( // Swap rows 1 and 2 let (mut row_1, mut row_2) = su4_mat_mm.multi_slice_mut((s![1, ..], s![2, ..])); - azip!((x in &mut row_1, y in &mut row2) (*x, *y) = (*y, *x)) + azip!((x in &mut row_1, y in &mut row_2) (*x, *y) = (*y, *x)); // Swap columns 1 and 2 let (mut col_1, mut col_2) = su4_mat_mm.multi_slice_mut((s![.., 1], s![.., 2])); - azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)) + azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); let synth_dag = match &decomposer_2q.decomposer { DecomposerType::XXDecomposer(decomposer) => { @@ -1048,22 +1020,16 @@ fn reversed_synth_su4( DecomposerType::TwoQubitBasisDecomposer(decomposer) => { // we don't have access to basis_fidelity, right??? let synth = decomposer.call_inner(su4_mat_mm.view(), None, is_approximate, None)?; - let decomp_gate = decomposer.gate.clone(); let sequence = TwoQubitUnitarySequence { gate_sequence: synth, - decomp_gate: Some(decomp_gate), - decomp_gate_params: decomposer_2q.decomp_gate_params.clone(), + decomp_gate: decomposer_2q.gate.clone(), }; - // the output will be a sequence in the relative basis - dag_from_2q_gate_sequence(py, sequence)? + dag_from_2q_gate_sequence(py, sequence, None)? } }; let mut target_dag = synth_dag.copy_empty_like(py, "alike")?; - let flip_bits: Vec = (0..synth_dag.num_qubits()) - .map(|id| (Qubit(id as u32))) - .rev() - .collect(); + let flip_bits: [Qubit; 2] = [Qubit(1), Qubit(0)]; for node in synth_dag.topological_op_nodes()? { if let NodeType::Operation(mut inst) = synth_dag.dag()[node].clone() { diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 56374c377405..ca652c9822bf 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -6885,7 +6885,7 @@ impl DAGCircuit { /// Add to global phase. Global phase can only be Float or ParameterExpression so this /// does not handle the full possibility of parameter values. -pub fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { +fn add_global_phase(py: Python, phase: &Param, other: &Param) -> PyResult { Ok(match [phase, other] { [Param::Float(a), Param::Float(b)] => Param::Float(a + b), [Param::Float(a), Param::ParameterExpression(b)] => Param::ParameterExpression( diff --git a/crates/circuit/src/imports.rs b/crates/circuit/src/imports.rs index 5c77f8ef7d17..ed396d0f405a 100644 --- a/crates/circuit/src/imports.rs +++ b/crates/circuit/src/imports.rs @@ -114,6 +114,12 @@ pub static UNITARY_GATE: ImportOnceCell = ImportOnceCell::new( "qiskit.circuit.library.generalized_gates.unitary", "UnitaryGate", ); +pub static QS_DECOMPOSITION: ImportOnceCell = + ImportOnceCell::new("qiskit.synthesis.unitary.qsd", "qs_decomposition"); +pub static XX_DECOMPOSER: ImportOnceCell = + ImportOnceCell::new("qiskit.synthesis.two_qubit.xx_decompose", "XXDecomposer"); +pub static XX_EMBODIMENTS: ImportOnceCell = + ImportOnceCell::new("qiskit.synthesis.two_qubit.xx_decompose", "XXEmbodiments"); /// A mapping from the enum variant in crate::operations::StandardGate to the python /// module path and class name to import it. This is used to populate the conversion table diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 4c64bdc6485d..fbc20a1aa391 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -509,25 +509,16 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: ) if self.method == "default" and isinstance(kwargs["target"], Target): - _gate_lengths = _gate_lengths or _build_gate_lengths(self._backend_props, self._target) - _gate_errors = _gate_errors or _build_gate_errors(self._backend_props, self._target) - if self._coupling_map is not None: - _dist_matrix = self._coupling_map.distance_matrix - _neighbor_table = NeighborTable( - rustworkx.adjacency_matrix(self._coupling_map.graph) - ) - else: - _dist_matrix = None - _neighbor_table = None - + _coupling_edges = ( + set(self._coupling_map.get_edges()) if self._coupling_map is not None else None + ) out = run_default_main_loop( dag, list(qubit_indices.values()), self._min_qubits, kwargs["target"], self._approximation_degree, - _neighbor_table, - _dist_matrix, + _coupling_edges, kwargs["natural_direction"], ) return out From 7fc0a8ab6a7c66d47538a7c4b2328e2f4a91d0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 18 Sep 2024 12:16:19 +0200 Subject: [PATCH 10/45] Use dag.apply_operation_back in 1q case. Additional cleanup. --- crates/accelerate/src/sabre/mod.rs | 4 ++-- crates/accelerate/src/two_qubit_decompose.rs | 2 +- crates/accelerate/src/unitary_synthesis.rs | 14 +++++++++++--- crates/circuit/src/dag_circuit.rs | 2 +- .../passes/synthesis/unitary_synthesis.py | 8 -------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/accelerate/src/sabre/mod.rs b/crates/accelerate/src/sabre/mod.rs index 80be8fcd8f9b..77057b69c272 100644 --- a/crates/accelerate/src/sabre/mod.rs +++ b/crates/accelerate/src/sabre/mod.rs @@ -13,8 +13,8 @@ mod heuristic; mod layer; mod layout; -pub mod neighbor_table; -pub mod route; +mod neighbor_table; +mod route; pub mod sabre_dag; pub mod swap_map; diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 6198807475fc..7089a783cce6 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -1276,7 +1276,7 @@ impl Default for TwoQubitGateSequence { #[allow(non_snake_case)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] pub struct TwoQubitBasisDecomposer { - pub gate: String, + gate: String, basis_fidelity: f64, euler_basis: EulerBasis, pulse_optimize: Option, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 6aa380ebb812..e03df9319f42 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -256,9 +256,17 @@ fn py_run_default_main_loop( .unwrap(); for gate in sequence.gates { - out_dag.insert_1q_on_incoming_qubit( - (gate.0, &gate.1), - NodeIndex::new(qubit.0 as usize), + let new_params: SmallVec<[Param; 3]> = + gate.1.iter().map(|p| Param::Float(*p)).collect(); + let _ = out_dag.apply_operation_back( + py, + gate.0.into(), + &[qubit], + &[], + Some(new_params), + ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + None, ); } out_dag.add_global_phase(py, &Param::Float(sequence.global_phase))?; diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index ca652c9822bf..8576e7c1b42b 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -792,7 +792,7 @@ impl DAGCircuit { /// Args: /// angle (float, :class:`.ParameterExpression`): The phase angle. #[setter] - pub fn set_global_phase(&mut self, angle: Param) -> PyResult<()> { + fn set_global_phase(&mut self, angle: Param) -> PyResult<()> { match angle { Param::Float(angle) => { self.global_phase = Param::Float(angle.rem_euclid(2. * PI)); diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index fbc20a1aa391..0f540ca4c85a 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -27,7 +27,6 @@ from itertools import product from functools import partial import numpy as np -import rustworkx from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit.circuit import Gate, Parameter, CircuitInstruction @@ -74,7 +73,6 @@ from qiskit.transpiler.passes.synthesis import plugin from qiskit.transpiler.target import Target -from qiskit._accelerate.sabre import NeighborTable from qiskit._accelerate.unitary_synthesis import run_default_main_loop GATE_NAME_MAP = { @@ -258,10 +256,8 @@ def _preferred_direction( if coupling_map is not None: neighbors0 = coupling_map.neighbors(qubits[0]) zero_one = qubits[1] in neighbors0 - neighbors1 = coupling_map.neighbors(qubits[1]) one_zero = qubits[0] in neighbors1 - if zero_one and not one_zero: preferred_direction = [0, 1] if one_zero and not zero_one: @@ -307,7 +303,6 @@ def _preferred_direction( preferred_direction = [0, 1] elif cost_1_0 < cost_0_1: preferred_direction = [1, 0] - if natural_direction is True and preferred_direction is None: raise TranspilerError( f"No preferred direction of gate on qubits {qubits} " @@ -903,7 +898,6 @@ def is_controlled(gate): if error is None: error = 0.0 basis_2q_fidelity[strength] = 1 - error - # rewrite XX of the same strength in terms of it embodiment = XXEmbodiments[v.base_class] if len(embodiment.parameters) == 1: @@ -1066,14 +1060,12 @@ def _reversed_synth_su4(self, su4_mat, decomposer2q, approximation_degree): su4_mat_mm[[1, 2]] = su4_mat_mm[[2, 1]] su4_mat_mm[:, [1, 2]] = su4_mat_mm[:, [2, 1]] synth_circ = decomposer2q(su4_mat_mm, approximate=approximate, use_dag=True) - out_dag = DAGCircuit() out_dag.global_phase = synth_circ.global_phase out_dag.add_qubits(list(reversed(synth_circ.qubits))) flip_bits = out_dag.qubits[::-1] for node in synth_circ.topological_op_nodes(): qubits = tuple(flip_bits[synth_circ.find_bit(x).index] for x in node.qargs) - node = DAGOpNode.from_instruction( node._to_circuit_instruction().replace(qubits=qubits, params=node.params) ) From e1b68551d181f1c2299223d794597352c0335661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Fri, 20 Sep 2024 14:14:40 +0200 Subject: [PATCH 11/45] Don't return ref to float in two qubit decomposer. --- crates/accelerate/src/two_qubit_decompose.rs | 12 ++++++------ crates/accelerate/src/unitary_synthesis.rs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 7089a783cce6..4a97db653032 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -494,14 +494,14 @@ pub struct TwoQubitWeylDecomposition { } impl TwoQubitWeylDecomposition { - pub fn a(&self) -> &f64 { - &self.a + pub fn a(&self) -> f64 { + self.a } - pub fn b(&self) -> &f64 { - &self.b + pub fn b(&self) -> f64 { + self.b } - pub fn c(&self) -> &f64 { - &self.c + pub fn c(&self) -> f64 { + self.c } fn weyl_gate( &self, diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index e03df9319f42..7f43b695eb2d 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -617,7 +617,7 @@ fn get_2q_decomposers_from_target( Some(unitary_matrix) => { let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) .unwrap(); - relative_eq!(*kak.a(), PI4) && relative_eq!(*kak.c(), 0.0) + relative_eq!(kak.a(), PI4) && relative_eq!(kak.c(), 0.0) } } } @@ -629,7 +629,7 @@ fn get_2q_decomposers_from_target( Some(unitary_matrix) => { let kak = TwoQubitWeylDecomposition::new_inner(unitary_matrix.view(), None, None) .unwrap(); - relative_eq!(*kak.b(), 0.0) && relative_eq!(*kak.c(), 0.0) + relative_eq!(kak.b(), 0.0) && relative_eq!(kak.c(), 0.0) } } } From 8418fdb6485a3971dc50dc670afbe5785299952f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 23 Sep 2024 10:00:07 +0200 Subject: [PATCH 12/45] Use PyList as input type for coupling_edges (previous approach was innefficient) --- crates/accelerate/src/unitary_synthesis.rs | 79 ++++++++++--------- .../passes/synthesis/unitary_synthesis.py | 5 +- 2 files changed, 45 insertions(+), 39 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 7f43b695eb2d..d0385806693f 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -23,7 +23,6 @@ use indexmap::IndexMap; use ndarray::prelude::*; use num_complex::{Complex, Complex64}; use numpy::IntoPyArray; -use pyo3::conversion::FromPyObjectBound; use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; use smallvec::{smallvec, SmallVec}; @@ -178,8 +177,8 @@ fn py_run_default_main_loop( qubit_indices: &Bound<'_, PyList>, min_qubits: usize, target: &Target, + coupling_edges: &Bound<'_, PyList>, approximation_degree: Option, - coupling_edges: Option<&Bound<'_, PyAny>>, natural_direction: Option, ) -> PyResult { let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); @@ -209,8 +208,8 @@ fn py_run_default_main_loop( &PyList::new_bound(py, new_ids), min_qubits, target, - approximation_degree, coupling_edges, + approximation_degree, natural_direction, )?; new_blocks.push(dag_to_circuit.call1((res,))?); @@ -291,8 +290,8 @@ fn py_run_default_main_loop( py, unitary, &ref_qubits, + coupling_edges, approximation_degree, - &coupling_edges, natural_direction, target, )?; @@ -357,8 +356,8 @@ fn run_2q_unitary_synthesis( py: Python, unitary: Array2, ref_qubits: &[PhysicalQubit; 2], + coupling_edges: &Bound<'_, PyList>, approximation_degree: Option, - coupling_edges: &Option<&Bound<'_, PyAny>>, natural_direction: Option, target: &Target, ) -> PyResult> { @@ -781,7 +780,7 @@ fn preferred_direction( decomposer: &DecomposerElement, ref_qubits: &[PhysicalQubit; 2], natural_direction: Option, - coupling_edges: &Option<&Bound<'_, PyAny>>, + coupling_edges: &Bound<'_, PyList>, target: &Target, ) -> PyResult> { // Returns: @@ -818,40 +817,46 @@ fn preferred_direction( Some(false) => None, _ => { // None or Some(true) - if let Some(edges) = coupling_edges { - let edge_set: HashSet<(u32, u32)> = - HashSet::from_py_object_bound(edges.as_borrowed())?; - let zero_one = edge_set.contains(&(qubits[0].0, qubits[1].0)); - let one_zero = edge_set.contains(&(qubits[1].0, qubits[0].0)); - - match (zero_one, one_zero) { - (true, false) => Some(true), - (false, true) => Some(false), - _ => { - let mut cost_0_1: f64 = f64::INFINITY; - let mut cost_1_0: f64 = f64::INFINITY; - - // Try to find the cost in gate_lengths - cost_0_1 = compute_cost(true, qubits, cost_0_1)?; - cost_1_0 = compute_cost(true, reverse_qubits, cost_1_0)?; - - // If no valid cost was found in gate_lengths, check gate_errors - if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { - cost_0_1 = compute_cost(false, qubits, cost_0_1)?; - cost_1_0 = compute_cost(false, reverse_qubits, cost_1_0)?; - } + let mut edge_set = HashSet::new(); + for item in coupling_edges.iter() { + if let Ok(tuple) = item.extract::<(usize, usize)>() { + edge_set.insert(tuple); + } else if let Ok(inner_list) = item.extract::<&PyList>() { + if inner_list.len() == 2 { + let first: usize = inner_list.get_item(0)?.extract()?; + let second: usize = inner_list.get_item(1)?.extract()?; + edge_set.insert((first, second)); + } + } + } + let zero_one = edge_set.contains(&(qubits[0].0 as usize, qubits[1].0 as usize)); + let one_zero = edge_set.contains(&(qubits[1].0 as usize, qubits[0].0 as usize)); + + match (zero_one, one_zero) { + (true, false) => Some(true), + (false, true) => Some(false), + _ => { + let mut cost_0_1: f64 = f64::INFINITY; + let mut cost_1_0: f64 = f64::INFINITY; + + // Try to find the cost in gate_lengths + cost_0_1 = compute_cost(true, qubits, cost_0_1)?; + cost_1_0 = compute_cost(true, reverse_qubits, cost_1_0)?; + + // If no valid cost was found in gate_lengths, check gate_errors + if !(cost_0_1 < f64::INFINITY || cost_1_0 < f64::INFINITY) { + cost_0_1 = compute_cost(false, qubits, cost_0_1)?; + cost_1_0 = compute_cost(false, reverse_qubits, cost_1_0)?; + } - if cost_0_1 < cost_1_0 { - Some(true) - } else if cost_1_0 < cost_0_1 { - Some(false) - } else { - None - } + if cost_0_1 < cost_1_0 { + Some(true) + } else if cost_1_0 < cost_0_1 { + Some(false) + } else { + None } } - } else { - None } } }; diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 0f540ca4c85a..883dbb6c4d68 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -505,15 +505,16 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if self.method == "default" and isinstance(kwargs["target"], Target): _coupling_edges = ( - set(self._coupling_map.get_edges()) if self._coupling_map is not None else None + list(self._coupling_map.get_edges()) if self._coupling_map is not None else [] ) + out = run_default_main_loop( dag, list(qubit_indices.values()), self._min_qubits, kwargs["target"], - self._approximation_degree, _coupling_edges, + self._approximation_degree, kwargs["natural_direction"], ) return out From 369c0129ed00cf37b7e492e807d092512079eb7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 23 Sep 2024 11:37:11 +0200 Subject: [PATCH 13/45] Avoid using UnitarySynthesisReturn type, avoid intermediate dag creation in all cases except XXDecomposer. --- crates/accelerate/src/unitary_synthesis.rs | 609 ++++++++++++--------- 1 file changed, 348 insertions(+), 261 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index d0385806693f..96173883ea39 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -64,11 +64,6 @@ struct DecomposerElement { gate: NormalOperation, } -#[derive(Clone, Debug)] -enum UnitarySynthesisReturnType { - DAGType(Box), - TwoQSequenceType(TwoQubitUnitarySequence), -} #[derive(Clone, Debug)] struct TwoQubitUnitarySequence { gate_sequence: TwoQubitGateSequence, @@ -80,21 +75,9 @@ static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; fn dag_from_2q_gate_sequence( py: Python<'_>, sequence: TwoQubitUnitarySequence, - out_dag_info: Option<(DAGCircuit, &[Qubit])>, -) -> PyResult { - let qubit: &Bound = imports::QUBIT.get_bound(py); - - let (mut target_dag, mut out_qargs) = match out_dag_info { - Some((dag, qargs)) => (dag, qargs.to_vec()), - None => { - let mut out_qargs: Vec = vec![]; - let mut target_dag = DAGCircuit::new(py)?; - let qubit_obj = qubit.call0()?; - out_qargs.push(target_dag.add_qubit_unchecked(py, &qubit_obj)?); - (target_dag, out_qargs) - } - }; - + target_dag: &mut DAGCircuit, + out_qargs: &[Qubit], +) -> PyResult<()> { let _ = target_dag.add_global_phase(py, &Param::Float(sequence.gate_sequence.global_phase)); let mut instructions = Vec::new(); @@ -104,14 +87,7 @@ fn dag_from_2q_gate_sequence( Some(gate) => *gate, }; - let mut mapped_qargs = Vec::new(); - for id in qubit_ids { - while *id as usize >= out_qargs.len() { - let qubit_obj = qubit.call0()?; - out_qargs.push(target_dag.add_qubit_unchecked(py, &qubit_obj)?); - } - mapped_qargs.push(out_qargs[*id as usize]); - } + let mapped_qargs: Vec = qubit_ids.iter().map(|id| out_qargs[*id as usize]).collect(); let new_params: Option>> = match gate { Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), @@ -132,7 +108,7 @@ fn dag_from_2q_gate_sequence( let _ = target_dag.extend(py, instructions.into_iter()); - Ok(target_dag.clone()) + Ok(()) } fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { @@ -272,67 +248,71 @@ fn py_run_default_main_loop( } // Run 2Q synthesis [4, 4] => { - let ref_qargs = dag.get_qargs(packed_instr.qubits); + let out_qargs = dag.get_qargs(packed_instr.qubits); + + let process_synth_dag = |out_dag: &mut DAGCircuit, synth_dag: DAGCircuit| -> PyResult<()>{ + + out_dag.add_global_phase(py, &synth_dag.get_global_phase())?; + + for out_node in synth_dag.topological_op_nodes()? { + if let NodeType::Operation(mut out_packed_instr) = + synth_dag.dag()[out_node].clone() + { + let synth_qargs = + synth_dag.get_qargs(out_packed_instr.qubits); + let mapped_qargs: Vec = synth_qargs + .iter() + .map(|qarg| out_qargs[qarg.0 as usize]) + .collect(); + + out_packed_instr.qubits = + out_dag.qargs_interner.insert(&mapped_qargs); + + let _ = out_dag.push_back(py, out_packed_instr.clone()); + } + } + Ok(()) + }; + + let process_synth_sequence = |out_dag: &mut DAGCircuit, sequence: TwoQubitUnitarySequence| -> PyResult<()> { + dag_from_2q_gate_sequence( + py, + sequence, + out_dag, + out_qargs, + )?; + Ok(()) + }; + + let process_none = |out_dag: &mut DAGCircuit| -> PyResult<()>{ + let _ = out_dag.push_back(py, packed_instr.clone()); + Ok(()) + }; + // How to use ref_qubits: // * index = output qubit from synthesis algorithm // * value = correspoding physical qubit in dag/out_dag let ref_qubits: [PhysicalQubit; 2] = [ PhysicalQubit::new( - qubit_indices.get_item(ref_qargs[0].0 as usize)?.extract()?, + qubit_indices.get_item(out_qargs[0].0 as usize)?.extract()?, ), PhysicalQubit::new( - qubit_indices.get_item(ref_qargs[1].0 as usize)?.extract()?, + qubit_indices.get_item(out_qargs[1].0 as usize)?.extract()?, ), ]; - // The 2Q synth. output can be None, a DAGCircuit or a TwoQubitGateSequence - let raw_synth_output: Option = - run_2q_unitary_synthesis( - py, - unitary, - &ref_qubits, - coupling_edges, - approximation_degree, - natural_direction, - target, - )?; - - let out_qargs = dag.get_qargs(packed_instr.qubits); - match raw_synth_output { - None => { - let _ = out_dag.push_back(py, packed_instr.clone()); - } - - Some(synth_output) => match synth_output { - UnitarySynthesisReturnType::DAGType(synth_dag) => { - out_dag.add_global_phase(py, &synth_dag.get_global_phase())?; - - for out_node in synth_dag.topological_op_nodes()? { - if let NodeType::Operation(mut out_packed_instr) = - synth_dag.dag()[out_node].clone() - { - let synth_qargs = - synth_dag.get_qargs(out_packed_instr.qubits); - let mapped_qargs: Vec = synth_qargs - .iter() - .map(|qarg| out_qargs[qarg.0 as usize]) - .collect(); - - out_packed_instr.qubits = - out_dag.qargs_interner.insert(&mapped_qargs); - - let _ = out_dag.push_back(py, out_packed_instr.clone()); - } - } - } - UnitarySynthesisReturnType::TwoQSequenceType(sequence) => { - out_dag = dag_from_2q_gate_sequence( - py, - sequence, - Some((out_dag, out_qargs)), - )?; - } - }, - } + run_2q_unitary_synthesis( + py, + unitary, + &ref_qubits, + coupling_edges, + &mut out_dag, + process_synth_dag, + process_synth_sequence, + process_none, + approximation_degree, + natural_direction, + target, + )?; } // Run 3Q+ synthesis _ => { @@ -357,12 +337,14 @@ fn run_2q_unitary_synthesis( unitary: Array2, ref_qubits: &[PhysicalQubit; 2], coupling_edges: &Bound<'_, PyList>, + out_dag: &mut DAGCircuit, + mut process_synth_dag: impl FnMut(&mut DAGCircuit, DAGCircuit) -> PyResult<()>, + mut process_synth_sequence: impl FnMut(&mut DAGCircuit, TwoQubitUnitarySequence) -> PyResult<()>, + mut process_none: impl FnMut(&mut DAGCircuit) -> PyResult<()>, approximation_degree: Option, natural_direction: Option, target: &Target, -) -> PyResult> { - // run 2q decomposition (in Rust except for XXDecomposer) -> Return types will vary. - // step1: select decomposers +) -> PyResult<()> { let decomposers = { // let ref_qubits = ref_qubits; let decomposers_2q = @@ -375,30 +357,92 @@ fn run_2q_unitary_synthesis( // If we have a single TwoQubitBasisDecomposer, skip dag creation as we don't need to // store and can instead manually create the synthesized gates directly in the output dag - if decomposers.len() == 1 { + if decomposers.len() == 1 && matches!(decomposers.first().unwrap().decomposer, DecomposerType::TwoQubitBasisDecomposer(_)) { let decomposer_item = decomposers.first().unwrap(); - if let DecomposerType::TwoQubitBasisDecomposer(_) = decomposer_item.decomposer { - let preferred_dir = preferred_direction( - decomposer_item, - ref_qubits, - natural_direction, - coupling_edges, - target, - )?; - let synth = synth_su4_no_dag( - py, - &unitary, - decomposer_item, - preferred_dir, - approximation_degree, - )?; - return Ok(Some(synth)); + let preferred_dir = preferred_direction( + decomposer_item, + ref_qubits, + natural_direction, + coupling_edges, + target, + )?; + let synth = synth_su4_sequence( + py, + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + process_synth_sequence(out_dag, synth); + Ok(()) + } else { + fn compute_2q_error( + py: Python<'_>, + synth_circuit: impl Iterator< + Item = ( + String, + Option>, + SmallVec<[PhysicalQubit; 2]>, + ), + >, + target: &Target, + ) -> f64 { + let mut gate_fidelities = Vec::new(); + let mut score_instruction = |inst_name: &str, + inst_params: &Option>, + inst_qubits: &SmallVec<[PhysicalQubit; 2]>| + -> PyResult<()> { + match target.operation_names_for_qargs(Some(inst_qubits)) { + Ok(names) => { + for name in names { + let target_op = target.operation_from_name(name).unwrap(); + let are_params_close = if let Some(params) = inst_params { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) + } else { + false + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == inst_name + && (is_parametrized || are_params_close) + { + match target[name].get(Some(inst_qubits)) { + Some(Some(props)) => gate_fidelities.push( + 1.0 - props.error.unwrap_or(0.0) + ), + _ => gate_fidelities.push(1.0), + } + break; + } + } + Ok(()) + } + Err(_) => { + Err(QiskitError::new_err( + format!("Encountered a bad synthesis. Target has no instruction {inst_name:?} on qubits {inst_qubits:?}.") + )) + } + } + }; + + for (inst_name, inst_params, inst_qubits) in synth_circuit { + let _ = score_instruction(&inst_name, &inst_params, &inst_qubits); + } + 1.0 - gate_fidelities.into_iter().product::() } - } - - let synth_circuits: Vec> = decomposers - .iter() - .map(|decomposer| { + + let mut synth_errors_sequence = Vec::new(); + let mut synth_sequences = Vec::new(); + + let mut synth_errors_dag = Vec::new(); + let mut synth_dags = Vec::new(); + + for decomposer in &decomposers { let preferred_dir = preferred_direction( decomposer, ref_qubits, @@ -406,103 +450,122 @@ fn run_2q_unitary_synthesis( coupling_edges, target, )?; - synth_su4( - py, - &unitary, - decomposer, - preferred_dir, - approximation_degree, - ) - }) - .collect(); - - fn compute_2q_error( - py: Python<'_>, - synth_circuit: &UnitarySynthesisReturnType, - target: &Target, - ref_qubits: &[PhysicalQubit; 2], - ) -> f64 { - let mut gate_fidelities = Vec::new(); - let mut score_instruction = |instruction: &PackedInstruction, - inst_qubits: &SmallVec<[PhysicalQubit; 2]>| - -> PyResult<()> { - match target.operation_names_for_qargs(Some(inst_qubits)) { - Ok(names) => { - for name in names { - let target_op = target.operation_from_name(name).unwrap(); - let are_params_close = if let Some(params) = &instruction.params { - params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { - p1.is_close(py, p2, 1e-10) - .expect("Unexpected parameter expression error.") - }) + match &decomposer.decomposer { + DecomposerType::TwoQubitBasisDecomposer(_) => { + let sequence = synth_su4_sequence( + py, + &unitary, + decomposer, + preferred_dir, + approximation_degree, + )?; + let scoring_info = sequence + .gate_sequence + .gates + .iter() + .map(|(gate, params, qubit_ids)| { + let inst_qubits = + qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); + match gate { + Some(gate) => ( + gate.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + None => ( + sequence + .decomp_gate + .operation + .standard_gate() + .name() + .to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + } + }) + .collect::>, + SmallVec<[PhysicalQubit; 2]>, + )>>() + .into_iter(); + let error = compute_2q_error(py, scoring_info, target); + synth_errors_sequence.push(error); + synth_sequences.push(sequence); + } + DecomposerType::XXDecomposer(_) => { + let synth_dag = synth_su4_dag( + py, + &unitary, + decomposer, + preferred_dir, + approximation_degree, + )?; + let scoring_info = synth_dag + .topological_op_nodes() + .expect("Unexpected error in dag.topological_op_nodes()") + .map(|node| { + if let NodeType::Operation(inst) = &synth_dag.dag()[node] { + let inst_qubits = synth_dag + .get_qargs(inst.qubits) + .iter() + .map(|q| ref_qubits[q.0 as usize]) + .collect(); + ( + inst.op.name().to_string(), + inst.params.clone().map(|boxed| *boxed), + inst_qubits, + ) } else { - false - }; - let is_parametrized = target_op - .params - .iter() - .any(|param| matches!(param, Param::ParameterExpression(_))); - if target_op.operation.name() == instruction.op.name() - && (is_parametrized || are_params_close) - { - match target[name].get(Some(inst_qubits)) { - Some(Some(props)) => gate_fidelities.push( - 1.0 - props.error.unwrap_or(0.0) - ), - _ => gate_fidelities.push(1.0), - } - break; + panic!("All nodes are expected to be operations"); } - } - Ok(()) - } - Err(_) => { - Err(QiskitError::new_err( - format!("Encountered a bad synthesis. Target has no {instruction:?} on qubits {inst_qubits:?}.") - )) - } + }) + .collect::>, + SmallVec<[PhysicalQubit; 2]>, + )>>() + .into_iter(); + let error = compute_2q_error(py, scoring_info, target); + synth_errors_dag.push(error); + synth_dags.push(synth_dag); } - }; + } + } + + let synth_sequence =synth_errors_sequence + .iter() + .enumerate() + .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) + .map(|(index, _)| (synth_sequences[index].clone(), synth_errors_sequence[index].clone())); - if let UnitarySynthesisReturnType::DAGType(synth_dag) = synth_circuit { - for node in synth_dag - .topological_op_nodes() - .expect("Unexpected error in dag.topological_op_nodes()") - { - if let NodeType::Operation(inst) = &synth_dag.dag()[node] { - let inst_qubits = synth_dag - .get_qargs(inst.qubits) - .iter() - .map(|q| ref_qubits[q.0 as usize]) - .collect(); - let _ = score_instruction(inst, &inst_qubits); + let synth_dag = synth_errors_dag + .iter() + .enumerate() + .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) + .map(|(index, _)| (synth_dags[index].clone(), synth_errors_dag[index].clone())); + + match (synth_sequence, synth_dag){ + (None, None) => process_none(out_dag)?, + (Some(sequence), None) => process_synth_sequence(out_dag, sequence.0)?, + (None, Some(dag)) => process_synth_dag(out_dag, dag.0)?, + (Some((sequence, sequence_error)), Some((dag, dag_error))) => { + if sequence_error > dag_error { + process_synth_dag(out_dag, dag)? + } else { + process_synth_sequence(out_dag, sequence)? } } - } else { - panic!("Synth output is not a DAG"); - } - 1.0 - gate_fidelities.into_iter().product::() + }; + // let synth_circuit= synth_errors + // .iter() + // .enumerate() + // .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) + // .map(|(index, _)| synth_circuits_filt[index].clone()); + + Ok(()) } - - let synth_circuit: Option = if !synth_circuits.is_empty() { - let mut synth_errors = Vec::new(); - let mut synth_circuits_filt = Vec::new(); - - for circuit in synth_circuits.iter().flatten() { - let error = compute_2q_error(py, circuit, target, ref_qubits); - synth_errors.push(error); - synth_circuits_filt.push(circuit); - } - - synth_errors - .iter() - .enumerate() - .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) - .map(|(index, _)| synth_circuits_filt[index].clone()) - } else { - None - }; - Ok(synth_circuit) // The output at this point will be a DAG, the sequence may be returned in the special case for TwoQubitBasisDecomposer } @@ -876,45 +939,34 @@ fn preferred_direction( // generic synth function for 2q gates (4x4) // used in `run_2q_unitary_synthesis` -fn synth_su4( +fn synth_su4_dag( py: Python, su4_mat: &Array2, decomposer_2q: &DecomposerElement, preferred_direction: Option, approximation_degree: Option, -) -> PyResult { +) -> PyResult { // double check approximation_degree None let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth_dag = match &decomposer_2q.decomposer { - // the output will be a dag in the relative basis - DecomposerType::XXDecomposer(decomposer) => { - let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] - .into_iter() - .collect(); - // can we avoid cloning the matrix to pass it to python? - decomposer - .call_method_bound( - py, - intern!(py, "__call__"), - (su4_mat.clone().into_pyarray_bound(py),), - Some(&kwargs.into_py_dict_bound(py)), - )? - .extract::(py)? - } - // the output will be a sequence in the relative basis - DecomposerType::TwoQubitBasisDecomposer(decomposer) => { - // we don't have access to basis_fidelity, right??? - let synth = decomposer.call_inner(su4_mat.view(), None, is_approximate, None)?; - let sequence = TwoQubitUnitarySequence { - gate_sequence: synth, - decomp_gate: decomposer_2q.gate.clone(), - }; - dag_from_2q_gate_sequence(py, sequence, None)? - } + let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] + .into_iter() + .collect(); + // can we avoid cloning the matrix to pass it to python? + decomposer + .call_method_bound( + py, + intern!(py, "__call__"), + (su4_mat.clone().into_pyarray_bound(py),), + Some(&kwargs.into_py_dict_bound(py)), + )? + .extract::(py)? + } else { + panic!("synth su4 dag should only be called for XXDecomposer") }; match preferred_direction { - None => Ok(UnitarySynthesisReturnType::DAGType(Box::new(synth_dag))), + None => Ok(synth_dag), Some(preferred_dir) => { let mut synth_direction: Option> = None; for node in synth_dag.topological_op_nodes()? { @@ -928,7 +980,7 @@ fn synth_su4( } // synth direction is in the relative basis match synth_direction { - None => Ok(UnitarySynthesisReturnType::DAGType(Box::new(synth_dag))), + None => Ok(synth_dag), Some(synth_direction) => { let synth_dir = match synth_direction.as_slice() { [0, 1] => true, @@ -936,9 +988,9 @@ fn synth_su4( _ => panic!("Only 2 possible synth directions."), }; if synth_dir != preferred_dir { - reversed_synth_su4(py, su4_mat, decomposer_2q, approximation_degree) + reversed_synth_su4_dag(py, su4_mat, decomposer_2q, approximation_degree) } else { - Ok(UnitarySynthesisReturnType::DAGType(Box::new(synth_dag))) + Ok(synth_dag) } } } @@ -948,13 +1000,13 @@ fn synth_su4( // special-case synth function for the TwoQubitBasisDecomposer // used in `run_2q_unitary_synthesis` -fn synth_su4_no_dag( +fn synth_su4_sequence( py: Python<'_>, su4_mat: &Array2, decomposer_2q: &DecomposerElement, preferred_direction: Option, approximation_degree: Option, -) -> PyResult { +) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? @@ -969,7 +1021,7 @@ fn synth_su4_no_dag( //synth_direction is calculated in terms of logical qubits match preferred_direction { - None => Ok(UnitarySynthesisReturnType::TwoQSequenceType(sequence)), + None => Ok(sequence), Some(preferred_dir) => { let mut synth_direction: Option> = None; for (gate, _, qubits) in synth.gates { @@ -979,7 +1031,7 @@ fn synth_su4_no_dag( } match synth_direction { - None => Ok(UnitarySynthesisReturnType::TwoQSequenceType(sequence)), + None => Ok(sequence), Some(synth_direction) => { let synth_dir = match synth_direction.as_slice() { [0, 1] => true, @@ -987,9 +1039,14 @@ fn synth_su4_no_dag( _ => panic!(), }; if synth_dir != preferred_dir { - reversed_synth_su4(py, su4_mat, decomposer_2q, approximation_degree) + reversed_synth_su4_sequence( + py, + su4_mat, + decomposer_2q, + approximation_degree, + ) } else { - Ok(UnitarySynthesisReturnType::TwoQSequenceType(sequence)) + Ok(sequence) } } } @@ -998,12 +1055,12 @@ fn synth_su4_no_dag( } // generic synth function for 2q gates (4x4) called from synth_su4 -fn reversed_synth_su4( +fn reversed_synth_su4_dag( py: Python<'_>, su4_mat: &Array2, decomposer_2q: &DecomposerElement, approximation_degree: Option, -) -> PyResult { +) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; let mut su4_mat_mm = su4_mat.clone(); @@ -1015,30 +1072,21 @@ fn reversed_synth_su4( let (mut col_1, mut col_2) = su4_mat_mm.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); - let synth_dag = match &decomposer_2q.decomposer { - DecomposerType::XXDecomposer(decomposer) => { - // the output will be a dag in the relative basis - let mut kwargs = HashMap::<&str, bool>::new(); - kwargs.insert("approximate", is_approximate); - kwargs.insert("use_dag", true); - decomposer - .call_method_bound( - py, - "__call__", - (su4_mat_mm.clone().into_pyarray_bound(py),), - Some(&kwargs.into_py_dict_bound(py)), - )? - .extract::(py)? - } - DecomposerType::TwoQubitBasisDecomposer(decomposer) => { - // we don't have access to basis_fidelity, right??? - let synth = decomposer.call_inner(su4_mat_mm.view(), None, is_approximate, None)?; - let sequence = TwoQubitUnitarySequence { - gate_sequence: synth, - decomp_gate: decomposer_2q.gate.clone(), - }; - dag_from_2q_gate_sequence(py, sequence, None)? - } + let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] + .into_iter() + .collect(); + // can we avoid cloning the matrix to pass it to python? + decomposer + .call_method_bound( + py, + intern!(py, "__call__"), + (su4_mat_mm.clone().into_pyarray_bound(py),), + Some(&kwargs.into_py_dict_bound(py)), + )? + .extract::(py)? + } else { + panic!("synth su4 dag should only be called for XXDecomposer") }; let mut target_dag = synth_dag.copy_empty_like(py, "alike")?; @@ -1057,7 +1105,46 @@ fn reversed_synth_su4( let _ = target_dag.push_back(py, inst.clone()); } } - Ok(UnitarySynthesisReturnType::DAGType(Box::new(target_dag))) + Ok(target_dag) +} + +// generic synth function for 2q gates (4x4) called from synth_su4 +fn reversed_synth_su4_sequence( + py: Python<'_>, + su4_mat: &Array2, + decomposer_2q: &DecomposerElement, + approximation_degree: Option, +) -> PyResult { + let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; + let mut su4_mat_mm = su4_mat.clone(); + + // Swap rows 1 and 2 + let (mut row_1, mut row_2) = su4_mat_mm.multi_slice_mut((s![1, ..], s![2, ..])); + azip!((x in &mut row_1, y in &mut row_2) (*x, *y) = (*y, *x)); + + // Swap columns 1 and 2 + let (mut col_1, mut col_2) = su4_mat_mm.multi_slice_mut((s![.., 1], s![.., 2])); + azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); + + let mut synth = + if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else { + panic!("synth su4 no dag should only be called for TwoQubitBasisDecomposer") + }; + + let flip_bits: [u8; 2] = [1, 0]; + for (_, _, qubit_ids) in synth.gates.iter_mut() { + *qubit_ids = qubit_ids + .into_iter() + .map(|x| flip_bits[*x as usize]) + .collect::>() + } + let sequence = TwoQubitUnitarySequence { + gate_sequence: synth.clone(), + decomp_gate: decomposer_2q.gate.clone(), + }; + Ok(sequence) } #[pymodule] From 49584253f84c95fe13f458f7bd1faaf26ccedd00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 23 Sep 2024 14:17:33 +0200 Subject: [PATCH 14/45] Refactor appending to out_dag, remove unnecesary clones. --- crates/accelerate/src/unitary_synthesis.rs | 475 ++++++++++----------- 1 file changed, 214 insertions(+), 261 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 96173883ea39..a04114fa5eab 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -72,45 +72,6 @@ struct TwoQubitUnitarySequence { static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; -fn dag_from_2q_gate_sequence( - py: Python<'_>, - sequence: TwoQubitUnitarySequence, - target_dag: &mut DAGCircuit, - out_qargs: &[Qubit], -) -> PyResult<()> { - let _ = target_dag.add_global_phase(py, &Param::Float(sequence.gate_sequence.global_phase)); - - let mut instructions = Vec::new(); - for (gate, params, qubit_ids) in &sequence.gate_sequence.gates { - let gate_node = match gate { - None => sequence.decomp_gate.operation.standard_gate(), - Some(gate) => *gate, - }; - - let mapped_qargs: Vec = qubit_ids.iter().map(|id| out_qargs[*id as usize]).collect(); - - let new_params: Option>> = match gate { - Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), - None => Some(Box::new(sequence.decomp_gate.params.clone())), - }; - - let pi = PackedInstruction { - op: PackedOperation::from_standard(gate_node), - qubits: target_dag.qargs_interner.insert(&mapped_qargs), - clbits: target_dag.cargs_interner.get_default(), - params: new_params, - extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), - #[cfg(feature = "cache_pygates")] - py_op: OnceCell::new(), - }; - instructions.push(pi); - } - - let _ = target_dag.extend(py, instructions.into_iter()); - - Ok(()) -} - fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { let mut target_basis_set: EulerBasisSet = EulerBasisSet::new(); let target_basis_list = target.operation_names_for_qargs(Some(&smallvec![qubit])); @@ -143,8 +104,66 @@ fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet } target_basis_set } + +fn apply_synth_dag( + py: Python<'_>, + out_dag: &mut DAGCircuit, + out_qargs: &[Qubit], + synth_dag: &DAGCircuit, +) -> PyResult<()> { + out_dag.add_global_phase(py, &synth_dag.get_global_phase())?; + + for out_node in synth_dag.topological_op_nodes()? { + if let NodeType::Operation(mut out_packed_instr) = synth_dag.dag()[out_node].clone() { + let synth_qargs = synth_dag.get_qargs(out_packed_instr.qubits); + let mapped_qargs: Vec = synth_qargs + .iter() + .map(|qarg| out_qargs[qarg.0 as usize]) + .collect(); + + out_packed_instr.qubits = out_dag.qargs_interner.insert(&mapped_qargs); + + out_dag.push_back(py, out_packed_instr.clone())?; + } + } + Ok(()) +} + +fn apply_synth_sequence( + py: Python<'_>, + out_dag: &mut DAGCircuit, + out_qargs: &[Qubit], + sequence: &TwoQubitUnitarySequence, +) -> PyResult<()> { + out_dag.add_global_phase(py, &Param::Float(sequence.gate_sequence.global_phase))?; + let mut instructions = Vec::new(); + for (gate, params, qubit_ids) in &sequence.gate_sequence.gates { + let gate_node = match gate { + None => sequence.decomp_gate.operation.standard_gate(), + Some(gate) => *gate, + }; + let mapped_qargs: Vec = qubit_ids.iter().map(|id| out_qargs[*id as usize]).collect(); + let new_params: Option>> = match gate { + Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), + None => Some(Box::new(sequence.decomp_gate.params.clone())), + }; + let pi = PackedInstruction { + op: PackedOperation::from_standard(gate_node), + qubits: out_dag.qargs_interner.insert(&mapped_qargs), + clbits: out_dag.cargs_interner.get_default(), + params: new_params, + extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + py_op: OnceCell::new(), + }; + instructions.push(pi); + } + + out_dag.extend(py, instructions.into_iter())?; + Ok(()) +} + // This is the outer-most run function. It is meant to be called from Python inside `UnitarySynthesis.run()` -// This loop iterates over the dag and calls `run_2q_unitary_synthesis` #[pyfunction] #[pyo3(name = "run_default_main_loop")] fn py_run_default_main_loop( @@ -227,67 +246,40 @@ fn py_run_default_main_loop( None, true, None, - ) - .unwrap(); - - for gate in sequence.gates { - let new_params: SmallVec<[Param; 3]> = - gate.1.iter().map(|p| Param::Float(*p)).collect(); - let _ = out_dag.apply_operation_back( - py, - gate.0.into(), - &[qubit], - &[], - Some(new_params), - ExtraInstructionAttributes::new(None, None, None, None), - #[cfg(feature = "cache_pygates")] - None, - ); + ); + + match sequence { + Some(sequence) => { + for gate in sequence.gates { + let new_params: SmallVec<[Param; 3]> = + gate.1.iter().map(|p| Param::Float(*p)).collect(); + let _ = out_dag.apply_operation_back( + py, + gate.0.into(), + &[qubit], + &[], + Some(new_params), + ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + None, + ); + } + out_dag + .add_global_phase(py, &Param::Float(sequence.global_phase))?; + } + None => { + let _ = out_dag.push_back(py, packed_instr.clone()); + } } - out_dag.add_global_phase(py, &Param::Float(sequence.global_phase))?; } // Run 2Q synthesis [4, 4] => { let out_qargs = dag.get_qargs(packed_instr.qubits); - let process_synth_dag = |out_dag: &mut DAGCircuit, synth_dag: DAGCircuit| -> PyResult<()>{ - - out_dag.add_global_phase(py, &synth_dag.get_global_phase())?; - - for out_node in synth_dag.topological_op_nodes()? { - if let NodeType::Operation(mut out_packed_instr) = - synth_dag.dag()[out_node].clone() - { - let synth_qargs = - synth_dag.get_qargs(out_packed_instr.qubits); - let mapped_qargs: Vec = synth_qargs - .iter() - .map(|qarg| out_qargs[qarg.0 as usize]) - .collect(); - - out_packed_instr.qubits = - out_dag.qargs_interner.insert(&mapped_qargs); - - let _ = out_dag.push_back(py, out_packed_instr.clone()); - } - } + let apply_original_op = |out_dag: &mut DAGCircuit| -> PyResult<()> { + let _ = out_dag.push_back(py, packed_instr.clone()); Ok(()) }; - - let process_synth_sequence = |out_dag: &mut DAGCircuit, sequence: TwoQubitUnitarySequence| -> PyResult<()> { - dag_from_2q_gate_sequence( - py, - sequence, - out_dag, - out_qargs, - )?; - Ok(()) - }; - - let process_none = |out_dag: &mut DAGCircuit| -> PyResult<()>{ - let _ = out_dag.push_back(py, packed_instr.clone()); - Ok(()) - }; // How to use ref_qubits: // * index = output qubit from synthesis algorithm @@ -305,13 +297,12 @@ fn py_run_default_main_loop( unitary, &ref_qubits, coupling_edges, - &mut out_dag, - process_synth_dag, - process_synth_sequence, - process_none, + target, approximation_degree, natural_direction, - target, + &mut out_dag, + out_qargs, + apply_original_op, )?; } // Run 3Q+ synthesis @@ -319,7 +310,7 @@ fn py_run_default_main_loop( let qs_decomposition: &Bound<'_, PyAny> = imports::QS_DECOMPOSITION.get_bound(py); let synth_circ = - qs_decomposition.call1((unitary.clone().into_pyarray_bound(py),))?; + qs_decomposition.call1((unitary.into_pyarray_bound(py),))?; let synth_dag = circuit_to_dag.call1((synth_circ,))?.extract()?; out_dag = synth_dag; } @@ -337,16 +328,14 @@ fn run_2q_unitary_synthesis( unitary: Array2, ref_qubits: &[PhysicalQubit; 2], coupling_edges: &Bound<'_, PyList>, - out_dag: &mut DAGCircuit, - mut process_synth_dag: impl FnMut(&mut DAGCircuit, DAGCircuit) -> PyResult<()>, - mut process_synth_sequence: impl FnMut(&mut DAGCircuit, TwoQubitUnitarySequence) -> PyResult<()>, - mut process_none: impl FnMut(&mut DAGCircuit) -> PyResult<()>, + target: &Target, approximation_degree: Option, natural_direction: Option, - target: &Target, + out_dag: &mut DAGCircuit, + out_qargs: &[Qubit], + mut apply_original_op: impl FnMut(&mut DAGCircuit) -> PyResult<()>, ) -> PyResult<()> { let decomposers = { - // let ref_qubits = ref_qubits; let decomposers_2q = get_2q_decomposers_from_target(py, target, ref_qubits, approximation_degree)?; match decomposers_2q { @@ -357,7 +346,12 @@ fn run_2q_unitary_synthesis( // If we have a single TwoQubitBasisDecomposer, skip dag creation as we don't need to // store and can instead manually create the synthesized gates directly in the output dag - if decomposers.len() == 1 && matches!(decomposers.first().unwrap().decomposer, DecomposerType::TwoQubitBasisDecomposer(_)) { + if decomposers.len() == 1 + && matches!( + decomposers.first().unwrap().decomposer, + DecomposerType::TwoQubitBasisDecomposer(_) + ) + { let decomposer_item = decomposers.first().unwrap(); let preferred_dir = preferred_direction( decomposer_item, @@ -367,14 +361,12 @@ fn run_2q_unitary_synthesis( target, )?; let synth = synth_su4_sequence( - py, &unitary, decomposer_item, preferred_dir, approximation_degree, )?; - process_synth_sequence(out_dag, synth); - Ok(()) + apply_synth_sequence(py, out_dag, out_qargs, &synth) } else { fn compute_2q_error( py: Python<'_>, @@ -429,16 +421,16 @@ fn run_2q_unitary_synthesis( } } }; - + for (inst_name, inst_params, inst_qubits) in synth_circuit { let _ = score_instruction(&inst_name, &inst_params, &inst_qubits); } 1.0 - gate_fidelities.into_iter().product::() } - + let mut synth_errors_sequence = Vec::new(); let mut synth_sequences = Vec::new(); - + let mut synth_errors_dag = Vec::new(); let mut synth_dags = Vec::new(); @@ -453,7 +445,6 @@ fn run_2q_unitary_synthesis( match &decomposer.decomposer { DecomposerType::TwoQubitBasisDecomposer(_) => { let sequence = synth_su4_sequence( - py, &unitary, decomposer, preferred_dir, @@ -533,43 +524,35 @@ fn run_2q_unitary_synthesis( } } } - - let synth_sequence =synth_errors_sequence - .iter() - .enumerate() - .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) - .map(|(index, _)| (synth_sequences[index].clone(), synth_errors_sequence[index].clone())); + + let synth_sequence = synth_errors_sequence + .iter() + .enumerate() + .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) + .map(|(index, _)| (&synth_sequences[index], synth_errors_sequence[index])); let synth_dag = synth_errors_dag - .iter() - .enumerate() - .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) - .map(|(index, _)| (synth_dags[index].clone(), synth_errors_dag[index].clone())); - - match (synth_sequence, synth_dag){ - (None, None) => process_none(out_dag)?, - (Some(sequence), None) => process_synth_sequence(out_dag, sequence.0)?, - (None, Some(dag)) => process_synth_dag(out_dag, dag.0)?, + .iter() + .enumerate() + .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) + .map(|(index, _)| (&synth_dags[index], synth_errors_dag[index])); + + match (synth_sequence, synth_dag) { + (None, None) => apply_original_op(out_dag)?, + (Some((sequence, _)), None) => apply_synth_sequence(py, out_dag, out_qargs, sequence)?, + (None, Some((dag, _))) => apply_synth_dag(py, out_dag, out_qargs, dag)?, (Some((sequence, sequence_error)), Some((dag, dag_error))) => { if sequence_error > dag_error { - process_synth_dag(out_dag, dag)? + apply_synth_dag(py, out_dag, out_qargs, dag)? } else { - process_synth_sequence(out_dag, sequence)? + apply_synth_sequence(py, out_dag, out_qargs, sequence)? } } }; - // let synth_circuit= synth_errors - // .iter() - // .enumerate() - // .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) - // .map(|(index, _)| synth_circuits_filt[index].clone()); - Ok(()) } - // The output at this point will be a DAG, the sequence may be returned in the special case for TwoQubitBasisDecomposer } -// This function collects a bunch of decomposer instances that will be used in `run_2q_unitary_synthesis` fn get_2q_decomposers_from_target( py: Python, target: &Target, @@ -578,23 +561,20 @@ fn get_2q_decomposers_from_target( ) -> PyResult>> { let qubits: SmallVec<[PhysicalQubit; 2]> = SmallVec::from_buf(*qubits); let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); - // HERE: caching - // TODO: here return cache --> implementation? let mut available_2q_basis: IndexMap<&str, NormalOperation> = IndexMap::new(); let mut available_2q_props: IndexMap<&str, (Option, Option)> = IndexMap::new(); - // try both directions for the qubits tuple let mut qubit_gate_map = IndexMap::new(); match target.operation_names_for_qargs(Some(&qubits)) { Ok(direct_keys) => { - qubit_gate_map.insert(qubits.clone(), direct_keys); + qubit_gate_map.insert(&qubits, direct_keys); if let Ok(reverse_keys) = target.operation_names_for_qargs(Some(&reverse_qubits)) { - qubit_gate_map.insert(reverse_qubits.clone(), reverse_keys); + qubit_gate_map.insert(&reverse_qubits, reverse_keys); } } Err(_) => { if let Ok(reverse_keys) = target.operation_names_for_qargs(Some(&reverse_qubits)) { - qubit_gate_map.insert(reverse_qubits.clone(), reverse_keys); + qubit_gate_map.insert(&reverse_qubits, reverse_keys); } else { return Err(QiskitError::new_err( "Target has no gates available on qubits to synthesize over.", @@ -603,6 +583,7 @@ fn get_2q_decomposers_from_target( } } + #[inline] fn replace_parametrized_gate(mut op: NormalOperation) -> NormalOperation { if let Some(std_gate) = op.operation.try_standard_gate() { match std_gate.name() { @@ -627,11 +608,10 @@ fn get_2q_decomposers_from_target( op } - for (q_pair, gates) in &qubit_gate_map { + for (q_pair, gates) in qubit_gate_map { for key in gates { match target.operation_from_name(key) { Ok(op) => { - // if it's not a gate, move on to next iteration match op.operation.discriminant() { PackedOperationType::Gate => (), PackedOperationType::StandardGate => (), @@ -657,7 +637,6 @@ fn get_2q_decomposers_from_target( } } } - if available_2q_basis.is_empty() { return Err(QiskitError::new_err( "Target has no gates available on qubits to synthesize over.", @@ -665,11 +644,8 @@ fn get_2q_decomposers_from_target( } let target_basis_set = get_target_basis_set(target, qubits[0]); - let available_1q_basis: HashSet<&str> = HashSet::from_iter(target_basis_set.get_bases().map(|basis| basis.as_str())); - - // find all decomposers let mut decomposers: Vec = Vec::new(); #[inline] @@ -796,9 +772,9 @@ fn get_2q_decomposers_from_target( // Iterate over 2q fidelities ans select decomposers if not_empty { let xx_decomposer: &Bound<'_, PyAny> = imports::XX_DECOMPOSER.get_bound(py); - for basis_1q in &available_1q_basis { + for basis_1q in available_1q_basis { let pi2_decomposer = if let Some(pi_2_basis) = pi2_basis { - if pi_2_basis == "cx" && *basis_1q == "ZSX" { + if pi_2_basis == "cx" && basis_1q == "ZSX" { let fidelity = match approximation_degree { Some(approx_degree) => approx_degree, None => match &target["cx"][Some(&qubits)] { @@ -848,8 +824,7 @@ fn preferred_direction( ) -> PyResult> { // Returns: // * true if gate qubits are in the hardware-native direction - // * false if gate qubits must be flipped to match hardware-native direction - + // * false if gate qubits must be flipped to match hardware-native direction∂ let qubits: [PhysicalQubit; 2] = *ref_qubits; let mut reverse_qubits: [PhysicalQubit; 2] = qubits; reverse_qubits.reverse(); @@ -937,60 +912,45 @@ fn preferred_direction( Ok(preferred_direction) } -// generic synth function for 2q gates (4x4) -// used in `run_2q_unitary_synthesis` -fn synth_su4_dag( - py: Python, +fn synth_su4_sequence( su4_mat: &Array2, decomposer_2q: &DecomposerElement, preferred_direction: Option, approximation_degree: Option, -) -> PyResult { - // double check approximation_degree None +) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { - let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] - .into_iter() - .collect(); - // can we avoid cloning the matrix to pass it to python? - decomposer - .call_method_bound( - py, - intern!(py, "__call__"), - (su4_mat.clone().into_pyarray_bound(py),), - Some(&kwargs.into_py_dict_bound(py)), - )? - .extract::(py)? + let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None, is_approximate, None)? } else { - panic!("synth su4 dag should only be called for XXDecomposer") + panic!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") + }; + let sequence = TwoQubitUnitarySequence { + gate_sequence: synth, + decomp_gate: decomposer_2q.gate.clone(), }; match preferred_direction { - None => Ok(synth_dag), + None => Ok(sequence), Some(preferred_dir) => { - let mut synth_direction: Option> = None; - for node in synth_dag.topological_op_nodes()? { - if let NodeType::Operation(inst) = &synth_dag.dag()[node] { - if inst.op.num_qubits() == 2 { - // not sure if these are the right qargs - let qargs = synth_dag.get_qargs(inst.qubits); - synth_direction = Some(vec![qargs[0].0, qargs[1].0]); - } + let mut synth_direction: Option> = None; + for (gate, _, qubits) in &sequence.gate_sequence.gates { + if gate.is_none() || gate.unwrap().name() == "cx" { + synth_direction = Some(qubits.clone()); } } - // synth direction is in the relative basis + match synth_direction { - None => Ok(synth_dag), + None => Ok(sequence), Some(synth_direction) => { let synth_dir = match synth_direction.as_slice() { [0, 1] => true, [1, 0] => false, - _ => panic!("Only 2 possible synth directions."), + _ => panic!(), }; if synth_dir != preferred_dir { - reversed_synth_su4_dag(py, su4_mat, decomposer_2q, approximation_degree) + reversed_synth_su4_sequence(su4_mat, decomposer_2q, approximation_degree) } else { - Ok(synth_dag) + Ok(sequence) } } } @@ -998,55 +958,91 @@ fn synth_su4_dag( } } -// special-case synth function for the TwoQubitBasisDecomposer -// used in `run_2q_unitary_synthesis` -fn synth_su4_sequence( - py: Python<'_>, +fn reversed_synth_su4_sequence( su4_mat: &Array2, decomposer_2q: &DecomposerElement, - preferred_direction: Option, approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { - decomp.call_inner(su4_mat.view(), None, is_approximate, None)? - } else { - panic!("synth su4 no dag should only be called for TwoQubitBasisDecomposer") - }; + let mut su4_mat_mm = su4_mat.clone(); + + // Swap rows 1 and 2 + let (mut row_1, mut row_2) = su4_mat_mm.multi_slice_mut((s![1, ..], s![2, ..])); + azip!((x in &mut row_1, y in &mut row_2) (*x, *y) = (*y, *x)); + // Swap columns 1 and 2 + let (mut col_1, mut col_2) = su4_mat_mm.multi_slice_mut((s![.., 1], s![.., 2])); + azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); + + let mut synth = + if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else { + panic!("reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") + }; + + let flip_bits: [u8; 2] = [1, 0]; + for (_, _, qubit_ids) in synth.gates.iter_mut() { + *qubit_ids = qubit_ids + .into_iter() + .map(|x| flip_bits[*x as usize]) + .collect::>() + } let sequence = TwoQubitUnitarySequence { - gate_sequence: synth.clone(), + gate_sequence: synth, decomp_gate: decomposer_2q.gate.clone(), }; + Ok(sequence) +} + +fn synth_su4_dag( + py: Python, + su4_mat: &Array2, + decomposer_2q: &DecomposerElement, + preferred_direction: Option, + approximation_degree: Option, +) -> PyResult { + let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; + let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { + let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] + .into_iter() + .collect(); + decomposer + .call_method_bound( + py, + intern!(py, "__call__"), + (su4_mat.clone().into_pyarray_bound(py),), + Some(&kwargs.into_py_dict_bound(py)), + )? + .extract::(py)? + } else { + panic!("synth_su4_dag should only be called for XXDecomposer.") + }; - //synth_direction is calculated in terms of logical qubits match preferred_direction { - None => Ok(sequence), + None => Ok(synth_dag), Some(preferred_dir) => { - let mut synth_direction: Option> = None; - for (gate, _, qubits) in synth.gates { - if gate.is_none() || gate.unwrap().name() == "cx" { - synth_direction = Some(qubits); + let mut synth_direction: Option> = None; + for node in synth_dag.topological_op_nodes()? { + if let NodeType::Operation(inst) = &synth_dag.dag()[node] { + if inst.op.num_qubits() == 2 { + let qargs = synth_dag.get_qargs(inst.qubits); + synth_direction = Some(vec![qargs[0].0, qargs[1].0]); + } } } - match synth_direction { - None => Ok(sequence), + None => Ok(synth_dag), Some(synth_direction) => { let synth_dir = match synth_direction.as_slice() { [0, 1] => true, [1, 0] => false, - _ => panic!(), + _ => panic!("There are no more than 2 possible synth directions."), }; if synth_dir != preferred_dir { - reversed_synth_su4_sequence( - py, - su4_mat, - decomposer_2q, - approximation_degree, - ) + reversed_synth_su4_dag(py, su4_mat, decomposer_2q, approximation_degree) } else { - Ok(sequence) + Ok(synth_dag) } } } @@ -1054,7 +1050,6 @@ fn synth_su4_sequence( } } -// generic synth function for 2q gates (4x4) called from synth_su4 fn reversed_synth_su4_dag( py: Python<'_>, su4_mat: &Array2, @@ -1076,7 +1071,6 @@ fn reversed_synth_su4_dag( let kwargs: HashMap<&str, bool> = [("approximate", is_approximate), ("use_dag", true)] .into_iter() .collect(); - // can we avoid cloning the matrix to pass it to python? decomposer .call_method_bound( py, @@ -1086,67 +1080,26 @@ fn reversed_synth_su4_dag( )? .extract::(py)? } else { - panic!("synth su4 dag should only be called for XXDecomposer") + panic!("reversed_synth_su4_dag should only be called for XXDecomposer") }; let mut target_dag = synth_dag.copy_empty_like(py, "alike")?; let flip_bits: [Qubit; 2] = [Qubit(1), Qubit(0)]; - for node in synth_dag.topological_op_nodes()? { if let NodeType::Operation(mut inst) = synth_dag.dag()[node].clone() { - let qubits = synth_dag + let qubits: Vec = synth_dag .qargs_interner .get(inst.qubits) .iter() - // .rev() .map(|x| flip_bits[x.0 as usize]) .collect(); inst.qubits = target_dag.qargs_interner.insert_owned(qubits); - let _ = target_dag.push_back(py, inst.clone()); + let _ = target_dag.push_back(py, inst); } } Ok(target_dag) } -// generic synth function for 2q gates (4x4) called from synth_su4 -fn reversed_synth_su4_sequence( - py: Python<'_>, - su4_mat: &Array2, - decomposer_2q: &DecomposerElement, - approximation_degree: Option, -) -> PyResult { - let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let mut su4_mat_mm = su4_mat.clone(); - - // Swap rows 1 and 2 - let (mut row_1, mut row_2) = su4_mat_mm.multi_slice_mut((s![1, ..], s![2, ..])); - azip!((x in &mut row_1, y in &mut row_2) (*x, *y) = (*y, *x)); - - // Swap columns 1 and 2 - let (mut col_1, mut col_2) = su4_mat_mm.multi_slice_mut((s![.., 1], s![.., 2])); - azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); - - let mut synth = - if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { - decomp.call_inner(su4_mat.view(), None, is_approximate, None)? - } else { - panic!("synth su4 no dag should only be called for TwoQubitBasisDecomposer") - }; - - let flip_bits: [u8; 2] = [1, 0]; - for (_, _, qubit_ids) in synth.gates.iter_mut() { - *qubit_ids = qubit_ids - .into_iter() - .map(|x| flip_bits[*x as usize]) - .collect::>() - } - let sequence = TwoQubitUnitarySequence { - gate_sequence: synth.clone(), - decomp_gate: decomposer_2q.gate.clone(), - }; - Ok(sequence) -} - #[pymodule] pub fn unitary_synthesis(m: &Bound) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(py_run_default_main_loop))?; From 0773ea50ac83a1837f04f08b58ee4c37c255bc2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 25 Sep 2024 16:41:55 +0200 Subject: [PATCH 15/45] Cosmetic changes. Reduce indentation. --- crates/accelerate/src/unitary_synthesis.rs | 679 ++++++++++----------- 1 file changed, 331 insertions(+), 348 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index a04114fa5eab..6be30fbfc1c4 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -13,7 +13,6 @@ #[cfg(feature = "cache_pygates")] use std::cell::OnceCell; - use std::f64::consts::PI; use approx::relative_eq; @@ -91,7 +90,6 @@ fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet } Err(_) => target_basis_set.support_all(), } - if target_basis_set.basis_supported(EulerBasis::U3) && target_basis_set.basis_supported(EulerBasis::U321) { @@ -111,21 +109,19 @@ fn apply_synth_dag( out_qargs: &[Qubit], synth_dag: &DAGCircuit, ) -> PyResult<()> { - out_dag.add_global_phase(py, &synth_dag.get_global_phase())?; - for out_node in synth_dag.topological_op_nodes()? { - if let NodeType::Operation(mut out_packed_instr) = synth_dag.dag()[out_node].clone() { - let synth_qargs = synth_dag.get_qargs(out_packed_instr.qubits); - let mapped_qargs: Vec = synth_qargs - .iter() - .map(|qarg| out_qargs[qarg.0 as usize]) - .collect(); - - out_packed_instr.qubits = out_dag.qargs_interner.insert(&mapped_qargs); - - out_dag.push_back(py, out_packed_instr.clone())?; - } + let NodeType::Operation(mut out_packed_instr) = synth_dag.dag()[out_node].clone() else { + panic!("DAG node must be an instruction") + }; + let synth_qargs = synth_dag.get_qargs(out_packed_instr.qubits); + let mapped_qargs: Vec = synth_qargs + .iter() + .map(|qarg| out_qargs[qarg.0 as usize]) + .collect(); + out_packed_instr.qubits = out_dag.qargs_interner.insert(&mapped_qargs); + out_dag.push_back(py, out_packed_instr)?; } + out_dag.add_global_phase(py, &synth_dag.get_global_phase())?; Ok(()) } @@ -135,7 +131,6 @@ fn apply_synth_sequence( out_qargs: &[Qubit], sequence: &TwoQubitUnitarySequence, ) -> PyResult<()> { - out_dag.add_global_phase(py, &Param::Float(sequence.gate_sequence.global_phase))?; let mut instructions = Vec::new(); for (gate, params, qubit_ids) in &sequence.gate_sequence.gates { let gate_node = match gate { @@ -147,7 +142,7 @@ fn apply_synth_sequence( Some(_) => Some(Box::new(params.iter().map(|p| Param::Float(*p)).collect())), None => Some(Box::new(sequence.decomp_gate.params.clone())), }; - let pi = PackedInstruction { + let instruction = PackedInstruction { op: PackedOperation::from_standard(gate_node), qubits: out_dag.qargs_interner.insert(&mapped_qargs), clbits: out_dag.cargs_interner.get_default(), @@ -156,10 +151,10 @@ fn apply_synth_sequence( #[cfg(feature = "cache_pygates")] py_op: OnceCell::new(), }; - instructions.push(pi); + instructions.push(instruction); } - out_dag.extend(py, instructions.into_iter())?; + out_dag.add_global_phase(py, &Param::Float(sequence.gate_sequence.global_phase))?; Ok(()) } @@ -181,142 +176,133 @@ fn py_run_default_main_loop( let node_ids: Vec = dag.op_nodes(false).collect(); for node in node_ids { - if let NodeType::Operation(inst) = &dag.dag()[node] { - if inst.op.control_flow() { - if let OperationRef::Instruction(py_inst) = inst.op.view() { - let raw_blocks: Vec>> = py_inst - .instruction - .getattr(py, "blocks")? - .bind(py) - .iter()? - .collect(); - let mut new_blocks = Vec::with_capacity(raw_blocks.len()); - for raw_block in raw_blocks { - let new_ids = dag.get_qargs(inst.qubits).iter().map(|qarg| { - qubit_indices - .get_item(qarg.0 as usize) - .expect("Unexpected index error in DAG") - }); - let res = py_run_default_main_loop( - py, - &mut circuit_to_dag.call1((raw_block?,))?.extract()?, - &PyList::new_bound(py, new_ids), - min_qubits, - target, - coupling_edges, - approximation_degree, - natural_direction, - )?; - new_blocks.push(dag_to_circuit.call1((res,))?); - } - let old_node = dag.get_node(py, node)?.clone(); - let new_node = py_inst - .instruction - .bind(py) - .call_method1("replace_blocks", (new_blocks,))?; - let _ = dag.substitute_node(old_node.bind(py), &new_node, true, false); - } - } + let NodeType::Operation(inst) = &dag.dag()[node] else { + panic!("DAG node must be an instruction") + }; + if !inst.op.control_flow() { + continue; + } + let OperationRef::Instruction(py_inst) = inst.op.view() else { + panic!("Control flow op must be an instruction") + }; + let raw_blocks: Vec>> = py_inst + .instruction + .getattr(py, "blocks")? + .bind(py) + .iter()? + .collect(); + let mut new_blocks = Vec::with_capacity(raw_blocks.len()); + for raw_block in raw_blocks { + let new_ids = dag.get_qargs(inst.qubits).iter().map(|qarg| { + qubit_indices + .get_item(qarg.0 as usize) + .expect("Unexpected index error in DAG") + }); + let res = py_run_default_main_loop( + py, + &mut circuit_to_dag.call1((raw_block?,))?.extract()?, + &PyList::new_bound(py, new_ids), + min_qubits, + target, + coupling_edges, + approximation_degree, + natural_direction, + )?; + new_blocks.push(dag_to_circuit.call1((res,))?); } + let old_node = dag.get_node(py, node)?.clone(); + let new_node = py_inst + .instruction + .bind(py) + .call_method1("replace_blocks", (new_blocks,))?; + dag.substitute_node(old_node.bind(py), &new_node, true, false)?; } - let mut out_dag = dag.copy_empty_like(py, "alike")?; // Iterate over nodes, find decomposers and run synthesis for node in dag.topological_op_nodes()? { - if let NodeType::Operation(packed_instr) = &dag.dag()[node] { - if packed_instr.op.name() == "unitary" - && packed_instr.op.num_qubits() >= min_qubits as u32 - { - let unitary: Array, Dim<[usize; 2]>> = - match packed_instr.op.matrix(&[]) { - Some(unitary) => unitary, - None => return Err(QiskitError::new_err("Unitary not found")), - }; - match unitary.shape() { - // Run 1Q synthesis - [2, 2] => { - let qubit = dag.get_qargs(packed_instr.qubits)[0]; - let target_basis_set = - get_target_basis_set(target, PhysicalQubit::new(qubit.0)); - let sequence = unitary_to_gate_sequence_inner( - unitary.view(), - &target_basis_set, - qubit.0 as usize, - None, - true, - None, - ); - - match sequence { - Some(sequence) => { - for gate in sequence.gates { - let new_params: SmallVec<[Param; 3]> = - gate.1.iter().map(|p| Param::Float(*p)).collect(); - let _ = out_dag.apply_operation_back( - py, - gate.0.into(), - &[qubit], - &[], - Some(new_params), - ExtraInstructionAttributes::new(None, None, None, None), - #[cfg(feature = "cache_pygates")] - None, - ); - } - out_dag - .add_global_phase(py, &Param::Float(sequence.global_phase))?; - } - None => { - let _ = out_dag.push_back(py, packed_instr.clone()); - } + let NodeType::Operation(packed_instr) = &dag.dag()[node] else { + panic!("DAG node must be an instruction") + }; + if !(packed_instr.op.name() == "unitary" + && packed_instr.op.num_qubits() >= min_qubits as u32) + { + out_dag.push_back(py, packed_instr.clone())?; + continue; + } + let unitary: Array, Dim<[usize; 2]>> = match packed_instr.op.matrix(&[]) { + Some(unitary) => unitary, + None => return Err(QiskitError::new_err("Unitary not found")), + }; + match unitary.shape() { + // Run 1Q synthesis + [2, 2] => { + let qubit = dag.get_qargs(packed_instr.qubits)[0]; + let target_basis_set = get_target_basis_set(target, PhysicalQubit::new(qubit.0)); + let sequence = unitary_to_gate_sequence_inner( + unitary.view(), + &target_basis_set, + qubit.0 as usize, + None, + true, + None, + ); + match sequence { + Some(sequence) => { + for gate in sequence.gates { + let new_params: SmallVec<[Param; 3]> = + gate.1.iter().map(|p| Param::Float(*p)).collect(); + out_dag.apply_operation_back( + py, + gate.0.into(), + &[qubit], + &[], + Some(new_params), + ExtraInstructionAttributes::new(None, None, None, None), + #[cfg(feature = "cache_pygates")] + None, + )?; } + out_dag.add_global_phase(py, &Param::Float(sequence.global_phase))?; } - // Run 2Q synthesis - [4, 4] => { - let out_qargs = dag.get_qargs(packed_instr.qubits); - - let apply_original_op = |out_dag: &mut DAGCircuit| -> PyResult<()> { - let _ = out_dag.push_back(py, packed_instr.clone()); - Ok(()) - }; - - // How to use ref_qubits: - // * index = output qubit from synthesis algorithm - // * value = correspoding physical qubit in dag/out_dag - let ref_qubits: [PhysicalQubit; 2] = [ - PhysicalQubit::new( - qubit_indices.get_item(out_qargs[0].0 as usize)?.extract()?, - ), - PhysicalQubit::new( - qubit_indices.get_item(out_qargs[1].0 as usize)?.extract()?, - ), - ]; - run_2q_unitary_synthesis( - py, - unitary, - &ref_qubits, - coupling_edges, - target, - approximation_degree, - natural_direction, - &mut out_dag, - out_qargs, - apply_original_op, - )?; - } - // Run 3Q+ synthesis - _ => { - let qs_decomposition: &Bound<'_, PyAny> = - imports::QS_DECOMPOSITION.get_bound(py); - let synth_circ = - qs_decomposition.call1((unitary.into_pyarray_bound(py),))?; - let synth_dag = circuit_to_dag.call1((synth_circ,))?.extract()?; - out_dag = synth_dag; + None => { + out_dag.push_back(py, packed_instr.clone())?; } } - } else { - let _ = out_dag.push_back(py, packed_instr.clone()); + } + // Run 2Q synthesis + [4, 4] => { + let out_qargs = dag.get_qargs(packed_instr.qubits); + let apply_original_op = |out_dag: &mut DAGCircuit| -> PyResult<()> { + out_dag.push_back(py, packed_instr.clone())?; + Ok(()) + }; + // How to use ref_qubits: + // * index = output qubit from synthesis algorithm + // * value = correspoding physical qubit in dag/out_dag + let ref_qubits: [PhysicalQubit; 2] = [ + PhysicalQubit::new(qubit_indices.get_item(out_qargs[0].0 as usize)?.extract()?), + PhysicalQubit::new(qubit_indices.get_item(out_qargs[1].0 as usize)?.extract()?), + ]; + run_2q_unitary_synthesis( + py, + unitary, + &ref_qubits, + coupling_edges, + target, + approximation_degree, + natural_direction, + &mut out_dag, + out_qargs, + apply_original_op, + )?; + } + // Run 3Q+ synthesis + _ => { + let qs_decomposition: &Bound<'_, PyAny> = imports::QS_DECOMPOSITION.get_bound(py); + let synth_circ = qs_decomposition.call1((unitary.into_pyarray_bound(py),))?; + let synth_dag = circuit_to_dag.call1((synth_circ,))?.extract()?; + out_dag = synth_dag; } } } @@ -366,191 +352,187 @@ fn run_2q_unitary_synthesis( preferred_dir, approximation_degree, )?; - apply_synth_sequence(py, out_dag, out_qargs, &synth) - } else { - fn compute_2q_error( - py: Python<'_>, - synth_circuit: impl Iterator< - Item = ( - String, - Option>, - SmallVec<[PhysicalQubit; 2]>, - ), - >, - target: &Target, - ) -> f64 { - let mut gate_fidelities = Vec::new(); - let mut score_instruction = |inst_name: &str, - inst_params: &Option>, - inst_qubits: &SmallVec<[PhysicalQubit; 2]>| - -> PyResult<()> { - match target.operation_names_for_qargs(Some(inst_qubits)) { - Ok(names) => { - for name in names { - let target_op = target.operation_from_name(name).unwrap(); - let are_params_close = if let Some(params) = inst_params { - params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { - p1.is_close(py, p2, 1e-10) - .expect("Unexpected parameter expression error.") - }) - } else { - false - }; - let is_parametrized = target_op - .params - .iter() - .any(|param| matches!(param, Param::ParameterExpression(_))); - if target_op.operation.name() == inst_name - && (is_parametrized || are_params_close) - { - match target[name].get(Some(inst_qubits)) { - Some(Some(props)) => gate_fidelities.push( - 1.0 - props.error.unwrap_or(0.0) - ), - _ => gate_fidelities.push(1.0), - } - break; - } - } - Ok(()) - } - Err(_) => { - Err(QiskitError::new_err( - format!("Encountered a bad synthesis. Target has no instruction {inst_name:?} on qubits {inst_qubits:?}.") - )) - } - } - }; - - for (inst_name, inst_params, inst_qubits) in synth_circuit { - let _ = score_instruction(&inst_name, &inst_params, &inst_qubits); - } - 1.0 - gate_fidelities.into_iter().product::() - } - - let mut synth_errors_sequence = Vec::new(); - let mut synth_sequences = Vec::new(); - - let mut synth_errors_dag = Vec::new(); - let mut synth_dags = Vec::new(); + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + return Ok(()); + } - for decomposer in &decomposers { - let preferred_dir = preferred_direction( - decomposer, - ref_qubits, - natural_direction, - coupling_edges, - target, - )?; - match &decomposer.decomposer { - DecomposerType::TwoQubitBasisDecomposer(_) => { - let sequence = synth_su4_sequence( - &unitary, - decomposer, - preferred_dir, - approximation_degree, - )?; - let scoring_info = sequence - .gate_sequence - .gates - .iter() - .map(|(gate, params, qubit_ids)| { - let inst_qubits = - qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); - match gate { - Some(gate) => ( - gate.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - None => ( - sequence - .decomp_gate - .operation - .standard_gate() - .name() - .to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, + fn compute_2q_error( + py: Python<'_>, + synth_circuit: impl Iterator< + Item = ( + String, + Option>, + SmallVec<[PhysicalQubit; 2]>, + ), + >, + target: &Target, + ) -> f64 { + let mut gate_fidelities = Vec::new(); + let mut score_instruction = |inst_name: &str, + inst_params: &Option>, + inst_qubits: &SmallVec<[PhysicalQubit; 2]>| + -> PyResult<()> { + match target.operation_names_for_qargs(Some(inst_qubits)) { + Ok(names) => { + for name in names { + let target_op = target.operation_from_name(name).unwrap(); + let are_params_close = if let Some(params) = inst_params { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) + } else { + false + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == inst_name + && (is_parametrized || are_params_close) + { + match target[name].get(Some(inst_qubits)) { + Some(Some(props)) => gate_fidelities.push( + 1.0 - props.error.unwrap_or(0.0) ), + _ => gate_fidelities.push(1.0), } - }) - .collect::>, - SmallVec<[PhysicalQubit; 2]>, - )>>() - .into_iter(); - let error = compute_2q_error(py, scoring_info, target); - synth_errors_sequence.push(error); - synth_sequences.push(sequence); + break; + } + } + Ok(()) } - DecomposerType::XXDecomposer(_) => { - let synth_dag = synth_su4_dag( - py, - &unitary, - decomposer, - preferred_dir, - approximation_degree, - )?; - let scoring_info = synth_dag - .topological_op_nodes() - .expect("Unexpected error in dag.topological_op_nodes()") - .map(|node| { - if let NodeType::Operation(inst) = &synth_dag.dag()[node] { - let inst_qubits = synth_dag - .get_qargs(inst.qubits) - .iter() - .map(|q| ref_qubits[q.0 as usize]) - .collect(); - ( - inst.op.name().to_string(), - inst.params.clone().map(|boxed| *boxed), - inst_qubits, - ) - } else { - panic!("All nodes are expected to be operations"); - } - }) - .collect::>, - SmallVec<[PhysicalQubit; 2]>, - )>>() - .into_iter(); - let error = compute_2q_error(py, scoring_info, target); - synth_errors_dag.push(error); - synth_dags.push(synth_dag); + Err(_) => { + Err(QiskitError::new_err( + format!("Encountered a bad synthesis. Target has no instruction {inst_name:?} on qubits {inst_qubits:?}.") + )) } } + }; + + for (inst_name, inst_params, inst_qubits) in synth_circuit { + let _ = score_instruction(&inst_name, &inst_params, &inst_qubits); } + 1.0 - gate_fidelities.into_iter().product::() + } - let synth_sequence = synth_errors_sequence - .iter() - .enumerate() - .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) - .map(|(index, _)| (&synth_sequences[index], synth_errors_sequence[index])); + let mut synth_errors_sequence = Vec::new(); + let mut synth_sequences = Vec::new(); - let synth_dag = synth_errors_dag - .iter() - .enumerate() - .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) - .map(|(index, _)| (&synth_dags[index], synth_errors_dag[index])); - - match (synth_sequence, synth_dag) { - (None, None) => apply_original_op(out_dag)?, - (Some((sequence, _)), None) => apply_synth_sequence(py, out_dag, out_qargs, sequence)?, - (None, Some((dag, _))) => apply_synth_dag(py, out_dag, out_qargs, dag)?, - (Some((sequence, sequence_error)), Some((dag, dag_error))) => { - if sequence_error > dag_error { - apply_synth_dag(py, out_dag, out_qargs, dag)? - } else { - apply_synth_sequence(py, out_dag, out_qargs, sequence)? - } + let mut synth_errors_dag = Vec::new(); + let mut synth_dags = Vec::new(); + + for decomposer in &decomposers { + let preferred_dir = preferred_direction( + decomposer, + ref_qubits, + natural_direction, + coupling_edges, + target, + )?; + match &decomposer.decomposer { + DecomposerType::TwoQubitBasisDecomposer(_) => { + let sequence = + synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; + let scoring_info = sequence + .gate_sequence + .gates + .iter() + .map(|(gate, params, qubit_ids)| { + let inst_qubits = + qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); + match gate { + Some(gate) => ( + gate.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + None => ( + sequence + .decomp_gate + .operation + .standard_gate() + .name() + .to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + } + }) + .collect::>, + SmallVec<[PhysicalQubit; 2]>, + )>>() + .into_iter(); + let error = compute_2q_error(py, scoring_info, target); + synth_errors_sequence.push(error); + synth_sequences.push(sequence); } - }; - Ok(()) + DecomposerType::XXDecomposer(_) => { + let synth_dag = synth_su4_dag( + py, + &unitary, + decomposer, + preferred_dir, + approximation_degree, + )?; + let scoring_info = synth_dag + .topological_op_nodes() + .expect("Unexpected error in dag.topological_op_nodes()") + .map(|node| { + let NodeType::Operation(inst) = &synth_dag.dag()[node] else { + panic!("DAG node must be an instruction") + }; + let inst_qubits = synth_dag + .get_qargs(inst.qubits) + .iter() + .map(|q| ref_qubits[q.0 as usize]) + .collect(); + ( + inst.op.name().to_string(), + inst.params.clone().map(|boxed| *boxed), + inst_qubits, + ) + }) + .collect::>, + SmallVec<[PhysicalQubit; 2]>, + )>>() + .into_iter(); + let error = compute_2q_error(py, scoring_info, target); + synth_errors_dag.push(error); + synth_dags.push(synth_dag); + } + } } + + let synth_sequence = synth_errors_sequence + .iter() + .enumerate() + .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) + .map(|(index, _)| (&synth_sequences[index], synth_errors_sequence[index])); + + let synth_dag = synth_errors_dag + .iter() + .enumerate() + .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) + .map(|(index, _)| (&synth_dags[index], synth_errors_dag[index])); + + match (synth_sequence, synth_dag) { + (None, None) => apply_original_op(out_dag)?, + (Some((sequence, _)), None) => apply_synth_sequence(py, out_dag, out_qargs, sequence)?, + (None, Some((dag, _))) => apply_synth_dag(py, out_dag, out_qargs, dag)?, + (Some((sequence, sequence_error)), Some((dag, dag_error))) => { + if sequence_error > dag_error { + apply_synth_dag(py, out_dag, out_qargs, dag)? + } else { + apply_synth_sequence(py, out_dag, out_qargs, sequence)? + } + } + }; + Ok(()) } fn get_2q_decomposers_from_target( @@ -620,18 +602,17 @@ fn get_2q_decomposers_from_target( available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - match target.qargs_for_operation_name(key) { - Ok(_) => { - available_2q_props.insert( - key, - match &target[key].get(Some(q_pair)) { - Some(Some(props)) => (props.duration, props.error), - _ => (None, None), - }, - ); - } - _ => continue, - }; + if let Ok(_) = target.qargs_for_operation_name(key) { + available_2q_props.insert( + key, + match &target[key].get(Some(q_pair)) { + Some(Some(props)) => (props.duration, props.error), + _ => (None, None), + }, + ); + } else { + continue; + } } _ => continue, } @@ -747,7 +728,7 @@ fn get_2q_decomposers_from_target( fidelity_value *= approx_degree; } let mut embodiment = - xx_embodiments.get_item(op.clone().into_py(py).getattr(py, "base_class")?)?; //XXEmbodiments[v.base_class]; + xx_embodiments.get_item(op.clone().into_py(py).getattr(py, "base_class")?)?; if embodiment.getattr("parameters")?.len()? == 1 { embodiment = embodiment.call_method1("assign_parameters", (vec![strength],))?; @@ -769,7 +750,7 @@ fn get_2q_decomposers_from_target( embodiments_dict.set_item(strength, embodiment.into_py(py))?; } - // Iterate over 2q fidelities ans select decomposers + // Iterate over 2q fidelities and select decomposers if not_empty { let xx_decomposer: &Bound<'_, PyAny> = imports::XX_DECOMPOSER.get_bound(py); for basis_1q in available_1q_basis { @@ -1024,11 +1005,12 @@ fn synth_su4_dag( Some(preferred_dir) => { let mut synth_direction: Option> = None; for node in synth_dag.topological_op_nodes()? { - if let NodeType::Operation(inst) = &synth_dag.dag()[node] { - if inst.op.num_qubits() == 2 { - let qargs = synth_dag.get_qargs(inst.qubits); - synth_direction = Some(vec![qargs[0].0, qargs[1].0]); - } + let NodeType::Operation(inst) = &synth_dag.dag()[node] else { + panic!("DAG node must be an instruction") + }; + if inst.op.num_qubits() == 2 { + let qargs = synth_dag.get_qargs(inst.qubits); + synth_direction = Some(vec![qargs[0].0, qargs[1].0]); } } match synth_direction { @@ -1086,16 +1068,17 @@ fn reversed_synth_su4_dag( let mut target_dag = synth_dag.copy_empty_like(py, "alike")?; let flip_bits: [Qubit; 2] = [Qubit(1), Qubit(0)]; for node in synth_dag.topological_op_nodes()? { - if let NodeType::Operation(mut inst) = synth_dag.dag()[node].clone() { - let qubits: Vec = synth_dag - .qargs_interner - .get(inst.qubits) - .iter() - .map(|x| flip_bits[x.0 as usize]) - .collect(); - inst.qubits = target_dag.qargs_interner.insert_owned(qubits); - let _ = target_dag.push_back(py, inst); - } + let NodeType::Operation(mut inst) = synth_dag.dag()[node].clone() else { + panic!("DAG node must be an instruction") + }; + let qubits: Vec = synth_dag + .qargs_interner + .get(inst.qubits) + .iter() + .map(|x| flip_bits[x.0 as usize]) + .collect(); + inst.qubits = target_dag.qargs_interner.insert_owned(qubits); + target_dag.push_back(py, inst)?; } Ok(target_dag) } From 2a11b9ec0c74b26840b3b661c99c0ad892c4813e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 25 Sep 2024 17:22:03 +0200 Subject: [PATCH 16/45] Squash bug --- crates/accelerate/src/unitary_synthesis.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 6be30fbfc1c4..376d3e795f6d 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -914,6 +914,9 @@ fn synth_su4_sequence( None => Ok(sequence), Some(preferred_dir) => { let mut synth_direction: Option> = None; + // if the gates in synthesis are in the opposite direction of the preferred direction + // resynthesize a new operator which is the original conjugated by swaps. + // this new operator is doubly mirrored from the original and is locally equivalent. for (gate, _, qubits) in &sequence.gate_sequence.gates { if gate.is_none() || gate.unwrap().name() == "cx" { synth_direction = Some(qubits.clone()); @@ -957,7 +960,7 @@ fn reversed_synth_su4_sequence( let mut synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { - decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + decomp.call_inner(su4_mat_mm.view(), None, is_approximate, None)? } else { panic!("reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; From b1cda3650bd124309f250386e92e6630c84e4a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 26 Sep 2024 09:24:51 +0200 Subject: [PATCH 17/45] Fix clippy --- crates/accelerate/src/unitary_synthesis.rs | 226 ++++++++++----------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 376d3e795f6d..3ece11af0919 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -69,6 +69,9 @@ struct TwoQubitUnitarySequence { decomp_gate: NormalOperation, } +// Used in get_2q_decomposers. If the found 2q basis is a subset of GOODBYE_SET, +// then we know TwoQubitBasisDecomposer is an ideal decomposition and there is +// no need to bother trying the XXDecomposer. static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; fn get_target_basis_set(target: &Target, qubit: PhysicalQubit) -> EulerBasisSet { @@ -158,10 +161,71 @@ fn apply_synth_sequence( Ok(()) } -// This is the outer-most run function. It is meant to be called from Python inside `UnitarySynthesis.run()` +fn synth_error( + py: Python<'_>, + synth_circuit: impl Iterator< + Item = ( + String, + Option>, + SmallVec<[PhysicalQubit; 2]>, + ), + >, + target: &Target, +) -> f64 { + let mut gate_fidelities = Vec::new(); + let mut score_instruction = |inst_name: &str, + inst_params: &Option>, + inst_qubits: &SmallVec<[PhysicalQubit; 2]>| + -> PyResult<()> { + match target.operation_names_for_qargs(Some(inst_qubits)) { + Ok(names) => { + for name in names { + let target_op = target.operation_from_name(name).unwrap(); + let are_params_close = if let Some(params) = inst_params { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) + } else { + false + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == inst_name + && (is_parametrized || are_params_close) + { + match target[name].get(Some(inst_qubits)) { + Some(Some(props)) => gate_fidelities.push( + 1.0 - props.error.unwrap_or(0.0) + ), + _ => gate_fidelities.push(1.0), + } + break; + } + } + Ok(()) + } + Err(_) => { + Err(QiskitError::new_err( + format!("Encountered a bad synthesis. Target has no instruction {inst_name:?} on qubits {inst_qubits:?}.") + )) + } + } + }; + + for (inst_name, inst_params, inst_qubits) in synth_circuit { + let _ = score_instruction(&inst_name, &inst_params, &inst_qubits); + } + 1.0 - gate_fidelities.into_iter().product::() +} + +// This is the outer-most run function. It is meant to be called from Python +// in `UnitarySynthesis.run()`. #[pyfunction] #[pyo3(name = "run_default_main_loop")] -fn py_run_default_main_loop( +fn py_run_main_loop( py: Python, dag: &mut DAGCircuit, qubit_indices: &Bound<'_, PyList>, @@ -174,6 +238,7 @@ fn py_run_default_main_loop( let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); let dag_to_circuit = imports::DAG_TO_CIRCUIT.get_bound(py); + // Run synthesis recursively over control flow ops, mapping qubit indices let node_ids: Vec = dag.op_nodes(false).collect(); for node in node_ids { let NodeType::Operation(inst) = &dag.dag()[node] else { @@ -198,7 +263,7 @@ fn py_run_default_main_loop( .get_item(qarg.0 as usize) .expect("Unexpected index error in DAG") }); - let res = py_run_default_main_loop( + let res = py_run_main_loop( py, &mut circuit_to_dag.call1((raw_block?,))?.extract()?, &PyList::new_bound(py, new_ids), @@ -219,7 +284,7 @@ fn py_run_default_main_loop( } let mut out_dag = dag.copy_empty_like(py, "alike")?; - // Iterate over nodes, find decomposers and run synthesis + // Iterate over dag nodes and determine unitary synthesis approach for node in dag.topological_op_nodes()? { let NodeType::Operation(packed_instr) = &dag.dag()[node] else { panic!("DAG node must be an instruction") @@ -235,7 +300,7 @@ fn py_run_default_main_loop( None => return Err(QiskitError::new_err("Unitary not found")), }; match unitary.shape() { - // Run 1Q synthesis + // Run 1q synthesis [2, 2] => { let qubit = dag.get_qargs(packed_instr.qubits)[0]; let target_basis_set = get_target_basis_set(target, PhysicalQubit::new(qubit.0)); @@ -249,12 +314,12 @@ fn py_run_default_main_loop( ); match sequence { Some(sequence) => { - for gate in sequence.gates { + for (gate, params) in sequence.gates { let new_params: SmallVec<[Param; 3]> = - gate.1.iter().map(|p| Param::Float(*p)).collect(); + params.iter().map(|p| Param::Float(*p)).collect(); out_dag.apply_operation_back( py, - gate.0.into(), + gate.into(), &[qubit], &[], Some(new_params), @@ -270,24 +335,23 @@ fn py_run_default_main_loop( } } } - // Run 2Q synthesis + // Run 2q synthesis [4, 4] => { + // "out_qargs" is used to append the synthesized instructions to the output dag let out_qargs = dag.get_qargs(packed_instr.qubits); + // "ref_qubits" is used to access properties in the target. It accounts for control flow mapping. + let ref_qubits: &[PhysicalQubit; 2] = &[ + PhysicalQubit::new(qubit_indices.get_item(out_qargs[0].0 as usize)?.extract()?), + PhysicalQubit::new(qubit_indices.get_item(out_qargs[1].0 as usize)?.extract()?), + ]; let apply_original_op = |out_dag: &mut DAGCircuit| -> PyResult<()> { out_dag.push_back(py, packed_instr.clone())?; Ok(()) }; - // How to use ref_qubits: - // * index = output qubit from synthesis algorithm - // * value = correspoding physical qubit in dag/out_dag - let ref_qubits: [PhysicalQubit; 2] = [ - PhysicalQubit::new(qubit_indices.get_item(out_qargs[0].0 as usize)?.extract()?), - PhysicalQubit::new(qubit_indices.get_item(out_qargs[1].0 as usize)?.extract()?), - ]; run_2q_unitary_synthesis( py, unitary, - &ref_qubits, + ref_qubits, coupling_edges, target, approximation_degree, @@ -297,7 +361,7 @@ fn py_run_default_main_loop( apply_original_op, )?; } - // Run 3Q+ synthesis + // Run 3q+ synthesis _ => { let qs_decomposition: &Bound<'_, PyAny> = imports::QS_DECOMPOSITION.get_bound(py); let synth_circ = qs_decomposition.call1((unitary.into_pyarray_bound(py),))?; @@ -329,15 +393,8 @@ fn run_2q_unitary_synthesis( None => Vec::new(), } }; - - // If we have a single TwoQubitBasisDecomposer, skip dag creation as we don't need to - // store and can instead manually create the synthesized gates directly in the output dag - if decomposers.len() == 1 - && matches!( - decomposers.first().unwrap().decomposer, - DecomposerType::TwoQubitBasisDecomposer(_) - ) - { + // If there's a single decomposer, avoid computing synthesis score + if decomposers.len() == 1 { let decomposer_item = decomposers.first().unwrap(); let preferred_dir = preferred_direction( decomposer_item, @@ -346,82 +403,32 @@ fn run_2q_unitary_synthesis( coupling_edges, target, )?; - let synth = synth_su4_sequence( - &unitary, - decomposer_item, - preferred_dir, - approximation_degree, - )?; - apply_synth_sequence(py, out_dag, out_qargs, &synth)?; - return Ok(()); - } - - fn compute_2q_error( - py: Python<'_>, - synth_circuit: impl Iterator< - Item = ( - String, - Option>, - SmallVec<[PhysicalQubit; 2]>, - ), - >, - target: &Target, - ) -> f64 { - let mut gate_fidelities = Vec::new(); - let mut score_instruction = |inst_name: &str, - inst_params: &Option>, - inst_qubits: &SmallVec<[PhysicalQubit; 2]>| - -> PyResult<()> { - match target.operation_names_for_qargs(Some(inst_qubits)) { - Ok(names) => { - for name in names { - let target_op = target.operation_from_name(name).unwrap(); - let are_params_close = if let Some(params) = inst_params { - params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { - p1.is_close(py, p2, 1e-10) - .expect("Unexpected parameter expression error.") - }) - } else { - false - }; - let is_parametrized = target_op - .params - .iter() - .any(|param| matches!(param, Param::ParameterExpression(_))); - if target_op.operation.name() == inst_name - && (is_parametrized || are_params_close) - { - match target[name].get(Some(inst_qubits)) { - Some(Some(props)) => gate_fidelities.push( - 1.0 - props.error.unwrap_or(0.0) - ), - _ => gate_fidelities.push(1.0), - } - break; - } - } - Ok(()) - } - Err(_) => { - Err(QiskitError::new_err( - format!("Encountered a bad synthesis. Target has no instruction {inst_name:?} on qubits {inst_qubits:?}.") - )) - } + match decomposer_item.decomposer { + DecomposerType::TwoQubitBasisDecomposer(_) => { + let synth = synth_su4_sequence( + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_sequence(py, out_dag, out_qargs, &synth)?; + } + DecomposerType::XXDecomposer(_) => { + let synth = synth_su4_dag( + py, + &unitary, + decomposer_item, + preferred_dir, + approximation_degree, + )?; + apply_synth_dag(py, out_dag, out_qargs, &synth)?; } - }; - - for (inst_name, inst_params, inst_qubits) in synth_circuit { - let _ = score_instruction(&inst_name, &inst_params, &inst_qubits); } - 1.0 - gate_fidelities.into_iter().product::() + return Ok(()); } let mut synth_errors_sequence = Vec::new(); - let mut synth_sequences = Vec::new(); - let mut synth_errors_dag = Vec::new(); - let mut synth_dags = Vec::new(); - for decomposer in &decomposers { let preferred_dir = preferred_direction( decomposer, @@ -465,9 +472,7 @@ fn run_2q_unitary_synthesis( SmallVec<[PhysicalQubit; 2]>, )>>() .into_iter(); - let error = compute_2q_error(py, scoring_info, target); - synth_errors_sequence.push(error); - synth_sequences.push(sequence); + synth_errors_sequence.push((sequence, synth_error(py, scoring_info, target))); } DecomposerType::XXDecomposer(_) => { let synth_dag = synth_su4_dag( @@ -501,9 +506,7 @@ fn run_2q_unitary_synthesis( SmallVec<[PhysicalQubit; 2]>, )>>() .into_iter(); - let error = compute_2q_error(py, scoring_info, target); - synth_errors_dag.push(error); - synth_dags.push(synth_dag); + synth_errors_dag.push((synth_dag, synth_error(py, scoring_info, target))); } } } @@ -511,14 +514,14 @@ fn run_2q_unitary_synthesis( let synth_sequence = synth_errors_sequence .iter() .enumerate() - .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) - .map(|(index, _)| (&synth_sequences[index], synth_errors_sequence[index])); + .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) + .map(|(index, _)| &synth_errors_sequence[index]); let synth_dag = synth_errors_dag .iter() .enumerate() - .min_by(|error1, error2| error1.1.partial_cmp(error2.1).unwrap()) - .map(|(index, _)| (&synth_dags[index], synth_errors_dag[index])); + .min_by(|error1, error2| error1.1 .1.partial_cmp(&error2.1 .1).unwrap()) + .map(|(index, _)| &synth_errors_dag[index]); match (synth_sequence, synth_dag) { (None, None) => apply_original_op(out_dag)?, @@ -602,7 +605,7 @@ fn get_2q_decomposers_from_target( available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - if let Ok(_) = target.qargs_for_operation_name(key) { + if target.qargs_for_operation_name(key).is_ok() { available_2q_props.insert( key, match &target[key].get(Some(q_pair)) { @@ -696,7 +699,6 @@ fn get_2q_decomposers_from_target( } if check_goodbye(&available_basis_set) { - // TODO: decomposer cache thingy return Ok(Some(decomposers)); } @@ -743,15 +745,13 @@ fn get_2q_decomposers_from_target( let basis_2q_fidelity_dict = PyDict::new_bound(py); let embodiments_dict = PyDict::new_bound(py); - let mut not_empty = false; for (strength, fidelity, embodiment) in xx_decomposer_args.flatten() { - not_empty = true; basis_2q_fidelity_dict.set_item(strength, fidelity)?; embodiments_dict.set_item(strength, embodiment.into_py(py))?; } // Iterate over 2q fidelities and select decomposers - if not_empty { + if basis_2q_fidelity_dict.len() > 0 { let xx_decomposer: &Bound<'_, PyAny> = imports::XX_DECOMPOSER.get_bound(py); for basis_1q in available_1q_basis { let pi2_decomposer = if let Some(pi_2_basis) = pi2_basis { @@ -1088,6 +1088,6 @@ fn reversed_synth_su4_dag( #[pymodule] pub fn unitary_synthesis(m: &Bound) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(py_run_default_main_loop))?; + m.add_wrapped(wrap_pyfunction!(py_run_main_loop))?; Ok(()) } From 47f90cf74f9e1a331e94a1611cf77e4b889a1162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 26 Sep 2024 14:31:40 +0200 Subject: [PATCH 18/45] Add qsd test --- test/python/transpiler/test_unitary_synthesis.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 4abf6511d8d2..aaad7b71279b 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -18,6 +18,7 @@ import unittest import numpy as np +import scipy from ddt import ddt, data from qiskit import transpile @@ -60,11 +61,14 @@ from qiskit.circuit import Measure from qiskit.circuit.controlflow import IfElseOp from qiskit.circuit import Parameter, Gate +from qiskit.synthesis.unitary.qsd import qs_decomposition + from test import combine # pylint: disable=wrong-import-order from test import QiskitTestCase # pylint: disable=wrong-import-order from test.python.providers.fake_mumbai_v2 import ( # pylint: disable=wrong-import-order FakeMumbaiFractionalCX, ) + from ..legacy_cmaps import YORKTOWN_CMAP @@ -1033,6 +1037,18 @@ def test_parameterized_basis_gate_in_target(self): self.assertTrue(set(opcount).issubset({"rz", "rx", "rxx"})) self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + @data(1, 2, 3) + def test_qsd(self, opt): + """Test that the unitary synthesis pass runs qsd successfully with a target.""" + num_qubits = 3 + target = Target(num_qubits=num_qubits) + target.add_instruction(UGate(Parameter("theta"), Parameter("phi"), Parameter("lam"))) + target.add_instruction(CXGate()) + mat = scipy.stats.ortho_group.rvs(2**num_qubits) + qc = qs_decomposition(mat, opt_a1=True, opt_a2=False) + qc_transpiled = transpile(qc, target=target, optimization_level=opt) + self.assertTrue(np.allclose(mat, Operator(qc_transpiled).data)) + if __name__ == "__main__": unittest.main() From 5a925ca17288c1647053c6acf297c83ed708aa41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 26 Sep 2024 15:48:32 +0200 Subject: [PATCH 19/45] Avoid making unnecessary variables public --- crates/accelerate/src/two_qubit_decompose.rs | 8 +------- crates/circuit/src/dag_circuit.rs | 7 ++++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 4a97db653032..6f31000492b3 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -1234,7 +1234,7 @@ pub struct TwoQubitGateSequence { #[pymethods] impl TwoQubitGateSequence { #[new] - pub fn new() -> Self { + fn new() -> Self { TwoQubitGateSequence { gates: Vec::new(), global_phase: 0., @@ -1266,12 +1266,6 @@ impl TwoQubitGateSequence { } } -impl Default for TwoQubitGateSequence { - fn default() -> Self { - Self::new() - } -} - #[derive(Clone, Debug)] #[allow(non_snake_case)] #[pyclass(module = "qiskit._accelerate.two_qubit_decompose", subclass)] diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 8576e7c1b42b..69195a22c076 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -988,7 +988,7 @@ def _format(operand): } /// Add all wires in a quantum register. - pub fn add_qreg(&mut self, py: Python, qreg: &Bound) -> PyResult<()> { + fn add_qreg(&mut self, py: Python, qreg: &Bound) -> PyResult<()> { if !qreg.is_instance(imports::QUANTUM_REGISTER.get_bound(py))? { return Err(DAGCircuitError::new_err("not a QuantumRegister instance.")); } @@ -5605,7 +5605,7 @@ impl DAGCircuit { Ok(()) } - pub fn add_qubit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + fn add_qubit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { let qubit = self.qubits.add(py, bit, false)?; self.qubit_locations.bind(py).set_item( bit, @@ -5621,7 +5621,7 @@ impl DAGCircuit { Ok(qubit) } - pub fn add_clbit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { + fn add_clbit_unchecked(&mut self, py: Python, bit: &Bound) -> PyResult { let clbit = self.clbits.add(py, bit, false)?; self.clbit_locations.bind(py).set_item( bit, @@ -6578,6 +6578,7 @@ impl DAGCircuit { // Get the correct qubit indices let qubits_id = instr.qubits; + // Insert op-node to graph. let new_node = self.dag.add_node(NodeType::Operation(instr)); new_nodes.push(new_node); From dda1de081670fd43d22c9ab893596c470036d09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 1 Oct 2024 13:24:38 +0200 Subject: [PATCH 20/45] Use OperationRef instead of exposing discriminant --- crates/accelerate/src/unitary_synthesis.rs | 8 ++++---- crates/circuit/src/packed_instruction.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 3ece11af0919..3ad8e7b8ef0c 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -36,7 +36,7 @@ use rustworkx_core::petgraph::stable_graph::NodeIndex; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; use qiskit_circuit::imports; use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; -use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation, PackedOperationType}; +use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; use qiskit_circuit::Qubit; use crate::euler_one_qubit_decomposer::{ @@ -597,9 +597,9 @@ fn get_2q_decomposers_from_target( for key in gates { match target.operation_from_name(key) { Ok(op) => { - match op.operation.discriminant() { - PackedOperationType::Gate => (), - PackedOperationType::StandardGate => (), + match op.operation.view() { + OperationRef::Gate(_) => (), + OperationRef::Standard(_) => (), _ => continue, } diff --git a/crates/circuit/src/packed_instruction.rs b/crates/circuit/src/packed_instruction.rs index 63e2dff01b2e..af72b3226a7c 100644 --- a/crates/circuit/src/packed_instruction.rs +++ b/crates/circuit/src/packed_instruction.rs @@ -34,7 +34,7 @@ use crate::{Clbit, Qubit}; /// The logical discriminant of `PackedOperation`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] -pub enum PackedOperationType { +enum PackedOperationType { // It's important that the `StandardGate` item is 0, so that zeroing out a `PackedOperation` // will make it appear as a standard gate, which will never allow accidental dangling-pointer // dereferencing. @@ -132,7 +132,7 @@ impl PackedOperation { /// Extract the discriminant of the operation. #[inline] - pub fn discriminant(&self) -> PackedOperationType { + fn discriminant(&self) -> PackedOperationType { ::bytemuck::checked::cast((self.0 & Self::DISCRIMINANT_MASK) as u8) } From f3c6f62e3c5b5169894d6a393980237c9d80ec17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 1 Oct 2024 13:31:31 +0200 Subject: [PATCH 21/45] Rename DAGCircuit::substitue_node to py_substitute_node Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- crates/accelerate/src/unitary_synthesis.rs | 2 +- crates/circuit/src/dag_circuit.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 3ad8e7b8ef0c..3299c860f063 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -280,7 +280,7 @@ fn py_run_main_loop( .instruction .bind(py) .call_method1("replace_blocks", (new_blocks,))?; - dag.substitute_node(old_node.bind(py), &new_node, true, false)?; + dag.py_substitute_node(old_node.bind(py), &new_node, true, false)?; } let mut out_dag = dag.copy_empty_like(py, "alike")?; diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index e8f2302cffe7..202787109605 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -3363,7 +3363,7 @@ def _format(operand): /// DAGCircuitError: If replacement operation was incompatible with /// location of target node. #[pyo3(signature = (node, op, inplace=false, propagate_condition=true))] - pub fn substitute_node( + pub fn py_substitute_node( &mut self, node: &Bound, op: &Bound, From f1d6d9cbdfee92afaff68d321c41f4d0e8fad6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 1 Oct 2024 14:30:53 +0200 Subject: [PATCH 22/45] Restore name in Python world --- crates/circuit/src/dag_circuit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/circuit/src/dag_circuit.rs b/crates/circuit/src/dag_circuit.rs index 202787109605..c80488a301bb 100644 --- a/crates/circuit/src/dag_circuit.rs +++ b/crates/circuit/src/dag_circuit.rs @@ -3362,7 +3362,7 @@ def _format(operand): /// Raises: /// DAGCircuitError: If replacement operation was incompatible with /// location of target node. - #[pyo3(signature = (node, op, inplace=false, propagate_condition=true))] + #[pyo3(name = "substitute_node", signature = (node, op, inplace=false, propagate_condition=true))] pub fn py_substitute_node( &mut self, node: &Bound, From 8c11b0f1b8ab7f3ef6215f9aca41134f4d92f9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:47:24 +0200 Subject: [PATCH 23/45] Rewrite using flatten Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- crates/accelerate/src/unitary_synthesis.rs | 39 ++++++++++------------ 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 3299c860f063..42eecdff4179 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -594,30 +594,25 @@ fn get_2q_decomposers_from_target( } for (q_pair, gates) in qubit_gate_map { - for key in gates { - match target.operation_from_name(key) { - Ok(op) => { - match op.operation.view() { - OperationRef::Gate(_) => (), - OperationRef::Standard(_) => (), - _ => continue, - } + for op in gates.iter().map(|key|target.operation_from_name(*key)).flatten() { + match op.operation.view() { + OperationRef::Gate(_) => (), + OperationRef::Standard(_) => (), + _ => continue, + } - available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); + available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - if target.qargs_for_operation_name(key).is_ok() { - available_2q_props.insert( - key, - match &target[key].get(Some(q_pair)) { - Some(Some(props)) => (props.duration, props.error), - _ => (None, None), - }, - ); - } else { - continue; - } - } - _ => continue, + if target.qargs_for_operation_name(key).is_ok() { + available_2q_props.insert( + key, + match &target[key].get(Some(q_pair)) { + Some(Some(props)) => (props.duration, props.error), + _ => (None, None), + }, + ); + } else { + continue; } } } From 6afc8cc400bdecdf8a2c80642702e22a47d8b0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Thu, 3 Oct 2024 09:51:23 +0200 Subject: [PATCH 24/45] Apply suggestions from Ray's code review Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- crates/accelerate/src/unitary_synthesis.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 42eecdff4179..ed76a6001156 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -653,9 +653,9 @@ fn get_2q_decomposers_from_target( // Iterate over 1q and 2q supercontrolled basis, append TwoQubitBasisDecomposers let supercontrolled_basis: IndexMap<&str, NormalOperation> = available_2q_basis - .clone() - .into_iter() + .iter() .filter(|(_, v)| is_supercontrolled(v)) + .map(|(k, v)| (*k, v.clone())) .collect(); for basis_1q in &available_1q_basis { @@ -691,7 +691,7 @@ fn get_2q_decomposers_from_target( #[inline] fn check_goodbye(basis_set: &HashSet<&str>) -> bool { !basis_set.iter().any(|gate| !GOODBYE_SET.contains(gate)) - } + basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) if check_goodbye(&available_basis_set) { return Ok(Some(decomposers)); @@ -699,8 +699,9 @@ fn get_2q_decomposers_from_target( // Let's now look for possible controlled decomposers (i.e. XXDecomposer) let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis - .into_iter() - .filter(|(_, v)| is_controlled(v)) + .iter() + .filter(|(_, v)| is_supercontrolled(v)) + .map(|(k, v)| (*k, v.clone())) .collect(); let mut pi2_basis: Option<&str> = None; let xx_embodiments: &Bound<'_, PyAny> = imports::XX_EMBODIMENTS.get_bound(py); @@ -726,7 +727,7 @@ fn get_2q_decomposers_from_target( } let mut embodiment = xx_embodiments.get_item(op.clone().into_py(py).getattr(py, "base_class")?)?; - + xx_embodiments.get_item(op.to_object(py).getattr(py, "base_class")?)?; if embodiment.getattr("parameters")?.len()? == 1 { embodiment = embodiment.call_method1("assign_parameters", (vec![strength],))?; } @@ -1070,7 +1071,7 @@ fn reversed_synth_su4_dag( panic!("DAG node must be an instruction") }; let qubits: Vec = synth_dag - .qargs_interner + .qargs_interner() .get(inst.qubits) .iter() .map(|x| flip_bits[x.0 as usize]) From 442867344b86ca9eea415a9c033f20c53cc5732d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 3 Oct 2024 11:01:12 +0200 Subject: [PATCH 25/45] Adjust suggestions from code review. Apply remaining feedback: * Use circuit_to_dag from crates/circuit/converters.rs * Simplify iteration over coupling edges * Replace cumbersome call_method_bound with call_bound --- crates/accelerate/src/unitary_synthesis.rs | 43 +++++++++++++--------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index ed76a6001156..4b0f298cf2d0 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -33,6 +33,7 @@ use pyo3::Python; use rustworkx_core::petgraph::stable_graph::NodeIndex; +use qiskit_circuit::converters::{circuit_to_dag, QuantumCircuitData}; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; use qiskit_circuit::imports; use qiskit_circuit::operations::{Operation, OperationRef, Param, StandardGate}; @@ -235,7 +236,6 @@ fn py_run_main_loop( approximation_degree: Option, natural_direction: Option, ) -> PyResult { - let circuit_to_dag = imports::CIRCUIT_TO_DAG.get_bound(py); let dag_to_circuit = imports::DAG_TO_CIRCUIT.get_bound(py); // Run synthesis recursively over control flow ops, mapping qubit indices @@ -265,7 +265,13 @@ fn py_run_main_loop( }); let res = py_run_main_loop( py, - &mut circuit_to_dag.call1((raw_block?,))?.extract()?, + &mut circuit_to_dag( + py, + QuantumCircuitData::extract_bound(&raw_block?)?, + false, + None, + None, + )?, &PyList::new_bound(py, new_ids), min_qubits, target, @@ -365,7 +371,13 @@ fn py_run_main_loop( _ => { let qs_decomposition: &Bound<'_, PyAny> = imports::QS_DECOMPOSITION.get_bound(py); let synth_circ = qs_decomposition.call1((unitary.into_pyarray_bound(py),))?; - let synth_dag = circuit_to_dag.call1((synth_circ,))?.extract()?; + let synth_dag = circuit_to_dag( + py, + QuantumCircuitData::extract_bound(&synth_circ)?, + false, + None, + None, + )?; out_dag = synth_dag; } } @@ -594,7 +606,10 @@ fn get_2q_decomposers_from_target( } for (q_pair, gates) in qubit_gate_map { - for op in gates.iter().map(|key|target.operation_from_name(*key)).flatten() { + for (key, op) in gates + .iter() + .zip(gates.iter().flat_map(|key| target.operation_from_name(key))) + { match op.operation.view() { OperationRef::Gate(_) => (), OperationRef::Standard(_) => (), @@ -690,8 +705,8 @@ fn get_2q_decomposers_from_target( #[inline] fn check_goodbye(basis_set: &HashSet<&str>) -> bool { - !basis_set.iter().any(|gate| !GOODBYE_SET.contains(gate)) basis_set.iter().all(|gate| GOODBYE_SET.contains(gate)) + } if check_goodbye(&available_basis_set) { return Ok(Some(decomposers)); @@ -699,8 +714,8 @@ fn get_2q_decomposers_from_target( // Let's now look for possible controlled decomposers (i.e. XXDecomposer) let controlled_basis: IndexMap<&str, NormalOperation> = available_2q_basis - .iter() - .filter(|(_, v)| is_supercontrolled(v)) + .iter() + .filter(|(_, v)| is_controlled(v)) .map(|(k, v)| (*k, v.clone())) .collect(); let mut pi2_basis: Option<&str> = None; @@ -727,7 +742,7 @@ fn get_2q_decomposers_from_target( } let mut embodiment = xx_embodiments.get_item(op.clone().into_py(py).getattr(py, "base_class")?)?; - xx_embodiments.get_item(op.to_object(py).getattr(py, "base_class")?)?; + xx_embodiments.get_item(op.to_object(py).getattr(py, "base_class")?)?; if embodiment.getattr("parameters")?.len()? == 1 { embodiment = embodiment.call_method1("assign_parameters", (vec![strength],))?; } @@ -836,12 +851,6 @@ fn preferred_direction( for item in coupling_edges.iter() { if let Ok(tuple) = item.extract::<(usize, usize)>() { edge_set.insert(tuple); - } else if let Ok(inner_list) = item.extract::<&PyList>() { - if inner_list.len() == 2 { - let first: usize = inner_list.get_item(0)?.extract()?; - let second: usize = inner_list.get_item(1)?.extract()?; - edge_set.insert((first, second)); - } } } let zero_one = edge_set.contains(&(qubits[0].0 as usize, qubits[1].0 as usize)); @@ -988,9 +997,8 @@ fn synth_su4_dag( .into_iter() .collect(); decomposer - .call_method_bound( + .call_bound( py, - intern!(py, "__call__"), (su4_mat.clone().into_pyarray_bound(py),), Some(&kwargs.into_py_dict_bound(py)), )? @@ -1053,9 +1061,8 @@ fn reversed_synth_su4_dag( .into_iter() .collect(); decomposer - .call_method_bound( + .call_bound( py, - intern!(py, "__call__"), (su4_mat_mm.clone().into_pyarray_bound(py),), Some(&kwargs.into_py_dict_bound(py)), )? From 46bc6f938bf5730ce18393d70db5f6639be98952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:41:54 +0200 Subject: [PATCH 26/45] Apply ownership suggestion from Ray Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- crates/accelerate/src/unitary_synthesis.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 4b0f298cf2d0..7d5dd04792ad 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -948,13 +948,11 @@ fn synth_su4_sequence( } fn reversed_synth_su4_sequence( - su4_mat: &Array2, + su4_mat: Array2, decomposer_2q: &DecomposerElement, approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let mut su4_mat_mm = su4_mat.clone(); - // Swap rows 1 and 2 let (mut row_1, mut row_2) = su4_mat_mm.multi_slice_mut((s![1, ..], s![2, ..])); azip!((x in &mut row_1, y in &mut row_2) (*x, *y) = (*y, *x)); From 9451cca2b49e491377d815a5b31a6c3a7f4765c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 3 Oct 2024 12:07:17 +0200 Subject: [PATCH 27/45] Finish applying ownership suggestion from Ray --- crates/accelerate/src/unitary_synthesis.rs | 34 +++++++++++++--------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 7d5dd04792ad..8a08963f3314 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -741,8 +741,8 @@ fn get_2q_decomposers_from_target( fidelity_value *= approx_degree; } let mut embodiment = - xx_embodiments.get_item(op.clone().into_py(py).getattr(py, "base_class")?)?; - xx_embodiments.get_item(op.to_object(py).getattr(py, "base_class")?)?; + xx_embodiments.get_item(op.to_object(py).getattr(py, "base_class")?)?; + if embodiment.getattr("parameters")?.len()? == 1 { embodiment = embodiment.call_method1("assign_parameters", (vec![strength],))?; } @@ -937,7 +937,11 @@ fn synth_su4_sequence( _ => panic!(), }; if synth_dir != preferred_dir { - reversed_synth_su4_sequence(su4_mat, decomposer_2q, approximation_degree) + reversed_synth_su4_sequence( + su4_mat.clone(), + decomposer_2q, + approximation_degree, + ) } else { Ok(sequence) } @@ -948,22 +952,22 @@ fn synth_su4_sequence( } fn reversed_synth_su4_sequence( - su4_mat: Array2, + mut su4_mat: Array2, decomposer_2q: &DecomposerElement, approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; // Swap rows 1 and 2 - let (mut row_1, mut row_2) = su4_mat_mm.multi_slice_mut((s![1, ..], s![2, ..])); + let (mut row_1, mut row_2) = su4_mat.multi_slice_mut((s![1, ..], s![2, ..])); azip!((x in &mut row_1, y in &mut row_2) (*x, *y) = (*y, *x)); // Swap columns 1 and 2 - let (mut col_1, mut col_2) = su4_mat_mm.multi_slice_mut((s![.., 1], s![.., 2])); + let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); let mut synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { - decomp.call_inner(su4_mat_mm.view(), None, is_approximate, None)? + decomp.call_inner(su4_mat.view(), None, is_approximate, None)? } else { panic!("reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; @@ -1027,7 +1031,12 @@ fn synth_su4_dag( _ => panic!("There are no more than 2 possible synth directions."), }; if synth_dir != preferred_dir { - reversed_synth_su4_dag(py, su4_mat, decomposer_2q, approximation_degree) + reversed_synth_su4_dag( + py, + su4_mat.clone(), + decomposer_2q, + approximation_degree, + ) } else { Ok(synth_dag) } @@ -1039,19 +1048,18 @@ fn synth_su4_dag( fn reversed_synth_su4_dag( py: Python<'_>, - su4_mat: &Array2, + mut su4_mat: Array2, decomposer_2q: &DecomposerElement, approximation_degree: Option, ) -> PyResult { let is_approximate = approximation_degree.is_none() || approximation_degree.unwrap() != 1.0; - let mut su4_mat_mm = su4_mat.clone(); // Swap rows 1 and 2 - let (mut row_1, mut row_2) = su4_mat_mm.multi_slice_mut((s![1, ..], s![2, ..])); + let (mut row_1, mut row_2) = su4_mat.multi_slice_mut((s![1, ..], s![2, ..])); azip!((x in &mut row_1, y in &mut row_2) (*x, *y) = (*y, *x)); // Swap columns 1 and 2 - let (mut col_1, mut col_2) = su4_mat_mm.multi_slice_mut((s![.., 1], s![.., 2])); + let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); let synth_dag = if let DecomposerType::XXDecomposer(decomposer) = &decomposer_2q.decomposer { @@ -1061,7 +1069,7 @@ fn reversed_synth_su4_dag( decomposer .call_bound( py, - (su4_mat_mm.clone().into_pyarray_bound(py),), + (su4_mat.clone().into_pyarray_bound(py),), Some(&kwargs.into_py_dict_bound(py)), )? .extract::(py)? From 78848ecd70bc226247cdf8830a5131670c0078cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 3 Oct 2024 14:21:12 +0200 Subject: [PATCH 28/45] Changes to synth_error function: remove error that wasn't handled, deal with new panic calling operation_from_name --- crates/accelerate/src/unitary_synthesis.rs | 25 ++++++++-------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 8a08963f3314..00ffa3fc5087 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -177,16 +177,15 @@ fn synth_error( let mut score_instruction = |inst_name: &str, inst_params: &Option>, inst_qubits: &SmallVec<[PhysicalQubit; 2]>| - -> PyResult<()> { - match target.operation_names_for_qargs(Some(inst_qubits)) { - Ok(names) => { - for name in names { - let target_op = target.operation_from_name(name).unwrap(); + -> () { + if let Ok(names) = target.operation_names_for_qargs(Some(inst_qubits)) { + for name in names { + if let Ok(target_op) = target.operation_from_name(name){ let are_params_close = if let Some(params) = inst_params { - params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { - p1.is_close(py, p2, 1e-10) - .expect("Unexpected parameter expression error.") - }) + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) } else { false }; @@ -206,18 +205,12 @@ fn synth_error( break; } } - Ok(()) - } - Err(_) => { - Err(QiskitError::new_err( - format!("Encountered a bad synthesis. Target has no instruction {inst_name:?} on qubits {inst_qubits:?}.") - )) } } }; for (inst_name, inst_params, inst_qubits) in synth_circuit { - let _ = score_instruction(&inst_name, &inst_params, &inst_qubits); + score_instruction(&inst_name, &inst_params, &inst_qubits); } 1.0 - gate_fidelities.into_iter().product::() } From b4cadb1444f2c26e2f9bc4ef79e99852054e34e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Fri, 4 Oct 2024 14:55:44 +0200 Subject: [PATCH 29/45] Undo flatten --- crates/accelerate/src/unitary_synthesis.rs | 50 +++++++++++----------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 00ffa3fc5087..e1804f21698b 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -180,7 +180,7 @@ fn synth_error( -> () { if let Ok(names) = target.operation_names_for_qargs(Some(inst_qubits)) { for name in names { - if let Ok(target_op) = target.operation_from_name(name){ + if let Ok(target_op) = target.operation_from_name(name) { let are_params_close = if let Some(params) = inst_params { params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { p1.is_close(py, p2, 1e-10) @@ -197,9 +197,9 @@ fn synth_error( && (is_parametrized || are_params_close) { match target[name].get(Some(inst_qubits)) { - Some(Some(props)) => gate_fidelities.push( - 1.0 - props.error.unwrap_or(0.0) - ), + Some(Some(props)) => { + gate_fidelities.push(1.0 - props.error.unwrap_or(0.0)) + } _ => gate_fidelities.push(1.0), } break; @@ -599,28 +599,30 @@ fn get_2q_decomposers_from_target( } for (q_pair, gates) in qubit_gate_map { - for (key, op) in gates - .iter() - .zip(gates.iter().flat_map(|key| target.operation_from_name(key))) - { - match op.operation.view() { - OperationRef::Gate(_) => (), - OperationRef::Standard(_) => (), - _ => continue, - } + for key in gates { + match target.operation_from_name(key) { + Ok(op) => { + match op.operation.view() { + OperationRef::Gate(_) => (), + OperationRef::Standard(_) => (), + _ => continue, + } - available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); + available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - if target.qargs_for_operation_name(key).is_ok() { - available_2q_props.insert( - key, - match &target[key].get(Some(q_pair)) { - Some(Some(props)) => (props.duration, props.error), - _ => (None, None), - }, - ); - } else { - continue; + if target.qargs_for_operation_name(key).is_ok() { + available_2q_props.insert( + key, + match &target[key].get(Some(q_pair)) { + Some(Some(props)) => (props.duration, props.error), + _ => (None, None), + }, + ); + } else { + continue; + } + } + _ => continue, } } } From 1bf99568dd3ab2e8be19c526abe94e7f578846c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 7 Oct 2024 13:49:46 +0200 Subject: [PATCH 30/45] Fix style --- crates/accelerate/src/unitary_synthesis.rs | 58 +++++++++++----------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index e1804f21698b..6e9443fc4e35 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -174,40 +174,40 @@ fn synth_error( target: &Target, ) -> f64 { let mut gate_fidelities = Vec::new(); - let mut score_instruction = |inst_name: &str, - inst_params: &Option>, - inst_qubits: &SmallVec<[PhysicalQubit; 2]>| - -> () { - if let Ok(names) = target.operation_names_for_qargs(Some(inst_qubits)) { - for name in names { - if let Ok(target_op) = target.operation_from_name(name) { - let are_params_close = if let Some(params) = inst_params { - params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { - p1.is_close(py, p2, 1e-10) - .expect("Unexpected parameter expression error.") - }) - } else { - false - }; - let is_parametrized = target_op - .params - .iter() - .any(|param| matches!(param, Param::ParameterExpression(_))); - if target_op.operation.name() == inst_name - && (is_parametrized || are_params_close) - { - match target[name].get(Some(inst_qubits)) { - Some(Some(props)) => { - gate_fidelities.push(1.0 - props.error.unwrap_or(0.0)) + let mut score_instruction = + |inst_name: &str, + inst_params: &Option>, + inst_qubits: &SmallVec<[PhysicalQubit; 2]>| { + if let Ok(names) = target.operation_names_for_qargs(Some(inst_qubits)) { + for name in names { + if let Ok(target_op) = target.operation_from_name(name) { + let are_params_close = if let Some(params) = inst_params { + params.iter().zip(target_op.params.iter()).all(|(p1, p2)| { + p1.is_close(py, p2, 1e-10) + .expect("Unexpected parameter expression error.") + }) + } else { + false + }; + let is_parametrized = target_op + .params + .iter() + .any(|param| matches!(param, Param::ParameterExpression(_))); + if target_op.operation.name() == inst_name + && (is_parametrized || are_params_close) + { + match target[name].get(Some(inst_qubits)) { + Some(Some(props)) => { + gate_fidelities.push(1.0 - props.error.unwrap_or(0.0)) + } + _ => gate_fidelities.push(1.0), } - _ => gate_fidelities.push(1.0), + break; } - break; } } } - } - }; + }; for (inst_name, inst_params, inst_qubits) in synth_circuit { score_instruction(&inst_name, &inst_params, &inst_qubits); From cd77e87cb36d1e8329b266f5e49767216174eb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 7 Oct 2024 14:54:30 +0200 Subject: [PATCH 31/45] Add getters and setters for TwoQubitGateSequence, expose new, keep struct attributes private. --- crates/accelerate/src/two_qubit_decompose.rs | 28 +++++++++++++++-- crates/accelerate/src/unitary_synthesis.rs | 32 +++++++++++--------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/crates/accelerate/src/two_qubit_decompose.rs b/crates/accelerate/src/two_qubit_decompose.rs index 6f31000492b3..dcaeb186aea9 100644 --- a/crates/accelerate/src/two_qubit_decompose.rs +++ b/crates/accelerate/src/two_qubit_decompose.rs @@ -1226,20 +1226,42 @@ type TwoQubitSequenceVec = Vec<(Option, SmallVec<[f64; 3]>, SmallV #[derive(Clone, Debug)] #[pyclass(sequence)] pub struct TwoQubitGateSequence { - pub gates: TwoQubitSequenceVec, + gates: TwoQubitSequenceVec, #[pyo3(get)] - pub global_phase: f64, + global_phase: f64, +} + +impl TwoQubitGateSequence { + pub fn gates(&self) -> &TwoQubitSequenceVec { + &self.gates + } + + pub fn global_phase(&self) -> f64 { + self.global_phase + } + + pub fn set_state(&mut self, state: (TwoQubitSequenceVec, f64)) { + self.gates = state.0; + self.global_phase = state.1; + } +} + +impl Default for TwoQubitGateSequence { + fn default() -> Self { + Self::new() + } } #[pymethods] impl TwoQubitGateSequence { #[new] - fn new() -> Self { + pub fn new() -> Self { TwoQubitGateSequence { gates: Vec::new(), global_phase: 0., } } + fn __getstate__(&self) -> (TwoQubitSequenceVec, f64) { (self.gates.clone(), self.global_phase) } diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 6e9443fc4e35..2e963620a2fb 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -136,7 +136,7 @@ fn apply_synth_sequence( sequence: &TwoQubitUnitarySequence, ) -> PyResult<()> { let mut instructions = Vec::new(); - for (gate, params, qubit_ids) in &sequence.gate_sequence.gates { + for (gate, params, qubit_ids) in sequence.gate_sequence.gates() { let gate_node = match gate { None => sequence.decomp_gate.operation.standard_gate(), Some(gate) => *gate, @@ -158,7 +158,7 @@ fn apply_synth_sequence( instructions.push(instruction); } out_dag.extend(py, instructions.into_iter())?; - out_dag.add_global_phase(py, &Param::Float(sequence.gate_sequence.global_phase))?; + out_dag.add_global_phase(py, &Param::Float(sequence.gate_sequence.global_phase()))?; Ok(()) } @@ -448,7 +448,7 @@ fn run_2q_unitary_synthesis( synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; let scoring_info = sequence .gate_sequence - .gates + .gates() .iter() .map(|(gate, params, qubit_ids)| { let inst_qubits = @@ -917,7 +917,7 @@ fn synth_su4_sequence( // if the gates in synthesis are in the opposite direction of the preferred direction // resynthesize a new operator which is the original conjugated by swaps. // this new operator is doubly mirrored from the original and is locally equivalent. - for (gate, _, qubits) in &sequence.gate_sequence.gates { + for (gate, _, qubits) in sequence.gate_sequence.gates() { if gate.is_none() || gate.unwrap().name() == "cx" { synth_direction = Some(qubits.clone()); } @@ -960,22 +960,26 @@ fn reversed_synth_su4_sequence( let (mut col_1, mut col_2) = su4_mat.multi_slice_mut((s![.., 1], s![.., 2])); azip!((x in &mut col_1, y in &mut col_2) (*x, *y) = (*y, *x)); - let mut synth = - if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { - decomp.call_inner(su4_mat.view(), None, is_approximate, None)? - } else { - panic!("reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") - }; + let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { + decomp.call_inner(su4_mat.view(), None, is_approximate, None)? + } else { + panic!("reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") + }; let flip_bits: [u8; 2] = [1, 0]; - for (_, _, qubit_ids) in synth.gates.iter_mut() { - *qubit_ids = qubit_ids + let mut reversed_gates = Vec::with_capacity(synth.gates().len()); + for (gate, params, qubit_ids) in synth.gates() { + let new_qubit_ids = qubit_ids .into_iter() .map(|x| flip_bits[*x as usize]) - .collect::>() + .collect::>(); + reversed_gates.push((*gate, params.clone(), new_qubit_ids.clone())); } + + let mut reversed_synth: TwoQubitGateSequence = TwoQubitGateSequence::new(); + reversed_synth.set_state((reversed_gates, synth.global_phase())); let sequence = TwoQubitUnitarySequence { - gate_sequence: synth, + gate_sequence: reversed_synth, decomp_gate: decomposer_2q.gate.clone(), }; Ok(sequence) From 4d8955a5a9d71dfa1f6dc13ed60a042391c3a316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:19:35 +0200 Subject: [PATCH 32/45] Apply Ray's suggestion to use unwrap_operation Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- crates/accelerate/src/unitary_synthesis.rs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 2e963620a2fb..2522efc68b34 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -114,9 +114,7 @@ fn apply_synth_dag( synth_dag: &DAGCircuit, ) -> PyResult<()> { for out_node in synth_dag.topological_op_nodes()? { - let NodeType::Operation(mut out_packed_instr) = synth_dag.dag()[out_node].clone() else { - panic!("DAG node must be an instruction") - }; + let mut out_packed_instr = synth_dag.dag()[out_node].unwrap_operation().clone(); let synth_qargs = synth_dag.get_qargs(out_packed_instr.qubits); let mapped_qargs: Vec = synth_qargs .iter() @@ -234,9 +232,7 @@ fn py_run_main_loop( // Run synthesis recursively over control flow ops, mapping qubit indices let node_ids: Vec = dag.op_nodes(false).collect(); for node in node_ids { - let NodeType::Operation(inst) = &dag.dag()[node] else { - panic!("DAG node must be an instruction") - }; + let inst = &dag.dag()[node].unwrap_operation(); if !inst.op.control_flow() { continue; } @@ -285,9 +281,7 @@ fn py_run_main_loop( // Iterate over dag nodes and determine unitary synthesis approach for node in dag.topological_op_nodes()? { - let NodeType::Operation(packed_instr) = &dag.dag()[node] else { - panic!("DAG node must be an instruction") - }; + let packed_instr = dag.dag()[node].unwrap_operation(); if !(packed_instr.op.name() == "unitary" && packed_instr.op.num_qubits() >= min_qubits as u32) { @@ -1013,9 +1007,7 @@ fn synth_su4_dag( Some(preferred_dir) => { let mut synth_direction: Option> = None; for node in synth_dag.topological_op_nodes()? { - let NodeType::Operation(inst) = &synth_dag.dag()[node] else { - panic!("DAG node must be an instruction") - }; + let inst = &synth_dag.dag()[node].unwrap_operation(); if inst.op.num_qubits() == 2 { let qargs = synth_dag.get_qargs(inst.qubits); synth_direction = Some(vec![qargs[0].0, qargs[1].0]); @@ -1079,9 +1071,7 @@ fn reversed_synth_su4_dag( let mut target_dag = synth_dag.copy_empty_like(py, "alike")?; let flip_bits: [Qubit; 2] = [Qubit(1), Qubit(0)]; for node in synth_dag.topological_op_nodes()? { - let NodeType::Operation(mut inst) = synth_dag.dag()[node].clone() else { - panic!("DAG node must be an instruction") - }; + let mut inst = synth_dag.dag()[node].unwrap_operation().clone(); let qubits: Vec = synth_dag .qargs_interner() .get(inst.qubits) From 78429ddb0a28063bacd335792f17a31447693c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 9 Oct 2024 10:25:09 +0200 Subject: [PATCH 33/45] Use ExtraInstructionAttributes::default() --- crates/accelerate/src/unitary_synthesis.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 2522efc68b34..f69fd22d6a65 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -149,7 +149,7 @@ fn apply_synth_sequence( qubits: out_dag.qargs_interner.insert(&mapped_qargs), clbits: out_dag.cargs_interner.get_default(), params: new_params, - extra_attrs: ExtraInstructionAttributes::new(None, None, None, None), + extra_attrs: ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] py_op: OnceCell::new(), }; @@ -316,7 +316,7 @@ fn py_run_main_loop( &[qubit], &[], Some(new_params), - ExtraInstructionAttributes::new(None, None, None, None), + ExtraInstructionAttributes::default(), #[cfg(feature = "cache_pygates")] None, )?; From a5d561a126f7608fd0e805b3fc5fa0ea9f3dec82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 9 Oct 2024 10:32:30 +0200 Subject: [PATCH 34/45] Apply suggestion to avoid intermediate collection --- crates/accelerate/src/unitary_synthesis.rs | 75 ++++++++++------------ 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index f69fd22d6a65..dac198476c0e 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -165,7 +165,7 @@ fn synth_error( synth_circuit: impl Iterator< Item = ( String, - Option>, + Option>, SmallVec<[PhysicalQubit; 2]>, ), >, @@ -440,38 +440,34 @@ fn run_2q_unitary_synthesis( DecomposerType::TwoQubitBasisDecomposer(_) => { let sequence = synth_su4_sequence(&unitary, decomposer, preferred_dir, approximation_degree)?; - let scoring_info = sequence - .gate_sequence - .gates() - .iter() - .map(|(gate, params, qubit_ids)| { - let inst_qubits = - qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); - match gate { - Some(gate) => ( - gate.name().to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - None => ( - sequence - .decomp_gate - .operation - .standard_gate() - .name() - .to_string(), - Some(params.iter().map(|p| Param::Float(*p)).collect()), - inst_qubits, - ), - } - }) - .collect::>, - SmallVec<[PhysicalQubit; 2]>, - )>>() - .into_iter(); - synth_errors_sequence.push((sequence, synth_error(py, scoring_info, target))); + let scoring_info = + sequence + .gate_sequence + .gates() + .iter() + .map(|(gate, params, qubit_ids)| { + let inst_qubits = + qubit_ids.iter().map(|q| ref_qubits[*q as usize]).collect(); + match gate { + Some(gate) => ( + gate.name().to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + None => ( + sequence + .decomp_gate + .operation + .standard_gate() + .name() + .to_string(), + Some(params.iter().map(|p| Param::Float(*p)).collect()), + inst_qubits, + ), + } + }); + let synth_error_from_target = synth_error(py, scoring_info, target); + synth_errors_sequence.push((sequence, synth_error_from_target)); } DecomposerType::XXDecomposer(_) => { let synth_dag = synth_su4_dag( @@ -498,14 +494,9 @@ fn run_2q_unitary_synthesis( inst.params.clone().map(|boxed| *boxed), inst_qubits, ) - }) - .collect::>, - SmallVec<[PhysicalQubit; 2]>, - )>>() - .into_iter(); - synth_errors_dag.push((synth_dag, synth_error(py, scoring_info, target))); + }); + let synth_error_from_target = synth_error(py, scoring_info, target); + synth_errors_dag.push((synth_dag, synth_error_from_target)); } } } @@ -723,7 +714,7 @@ fn get_2q_decomposers_from_target( .unwrap() .a(); let mut fidelity_value = match available_2q_props.get(name) { - Some(&(_, error)) => 1.0 - error.unwrap_or(0.0), + Some(&(_, error)) => 1.0 - error.unwrap_or_default(), // default is 0.0 None => 1.0, }; if let Some(approx_degree) = approximation_degree { From b3988dbd031e647878c60b9021d6d6b044d0863b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 9 Oct 2024 10:35:42 +0200 Subject: [PATCH 35/45] Use target::contains_key --- crates/accelerate/src/unitary_synthesis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index dac198476c0e..c780d7319bee 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -595,7 +595,7 @@ fn get_2q_decomposers_from_target( available_2q_basis.insert(key, replace_parametrized_gate(op.clone())); - if target.qargs_for_operation_name(key).is_ok() { + if target.contains_key(key) { available_2q_props.insert( key, match &target[key].get(Some(q_pair)) { From 56fa788843222e623fa741edf5f72cb7dcd0ceb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:36:58 +0200 Subject: [PATCH 36/45] Apply suggestion to use iter() instead of keys() Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- crates/accelerate/src/unitary_synthesis.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 2522efc68b34..386a3b735e28 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -663,7 +663,7 @@ fn get_2q_decomposers_from_target( .collect(); for basis_1q in &available_1q_basis { - for basis_2q in supercontrolled_basis.keys() { + for (basis_2q, gate) in supercontrolled_basis.iter() { let mut basis_2q_fidelity: f64 = match available_2q_props.get(basis_2q) { Some(&(_, Some(e))) => 1.0 - e, _ => 1.0, @@ -671,7 +671,6 @@ fn get_2q_decomposers_from_target( if let Some(approx_degree) = approximation_degree { basis_2q_fidelity *= approx_degree; } - let gate = &supercontrolled_basis[basis_2q]; let decomposer = TwoQubitBasisDecomposer::new_inner( gate.operation.name().to_owned(), gate.operation.matrix(&gate.params).unwrap().view(), From 08518fee6438850bd1355b2857e047cd8ae414e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 9 Oct 2024 10:59:03 +0200 Subject: [PATCH 37/45] Final touches --- crates/accelerate/src/unitary_synthesis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index fb2c7e2e04e8..9f7f14d7f2f6 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -795,7 +795,7 @@ fn preferred_direction( ) -> PyResult> { // Returns: // * true if gate qubits are in the hardware-native direction - // * false if gate qubits must be flipped to match hardware-native direction∂ + // * false if gate qubits must be flipped to match hardware-native direction let qubits: [PhysicalQubit; 2] = *ref_qubits; let mut reverse_qubits: [PhysicalQubit; 2] = qubits; reverse_qubits.reverse(); From b4263dc570d65bea2bf65c2a9aabef4c9270b8b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 9 Oct 2024 10:59:28 +0200 Subject: [PATCH 38/45] Final touches --- crates/accelerate/src/unitary_synthesis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 9f7f14d7f2f6..347f64cf99f9 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -749,7 +749,7 @@ fn get_2q_decomposers_from_target( let fidelity = match approximation_degree { Some(approx_degree) => approx_degree, None => match &target["cx"][Some(&qubits)] { - Some(props) => 1.0 - props.error.unwrap_or(0.0), + Some(props) => 1.0 - props.error.unwrap_or_default(), None => 1.0, }, }; From 3c234706a500242326d83310ab0df4e8acee62ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 15 Oct 2024 17:29:20 +0200 Subject: [PATCH 39/45] Replace panics with unreachable --- crates/accelerate/src/unitary_synthesis.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 347f64cf99f9..45ed16f36e98 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -16,7 +16,6 @@ use std::cell::OnceCell; use std::f64::consts::PI; use approx::relative_eq; -use core::panic; use hashbrown::{HashMap, HashSet}; use indexmap::IndexMap; use ndarray::prelude::*; @@ -237,7 +236,7 @@ fn py_run_main_loop( continue; } let OperationRef::Instruction(py_inst) = inst.op.view() else { - panic!("Control flow op must be an instruction") + unreachable!("Control flow op must be an instruction") }; let raw_blocks: Vec>> = py_inst .instruction @@ -482,7 +481,7 @@ fn run_2q_unitary_synthesis( .expect("Unexpected error in dag.topological_op_nodes()") .map(|node| { let NodeType::Operation(inst) = &synth_dag.dag()[node] else { - panic!("DAG node must be an instruction") + unreachable!("DAG node must be an instruction") }; let inst_qubits = synth_dag .get_qargs(inst.qubits) @@ -887,7 +886,7 @@ fn synth_su4_sequence( let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? } else { - panic!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") + unreachable!("synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; let sequence = TwoQubitUnitarySequence { gate_sequence: synth, @@ -913,7 +912,7 @@ fn synth_su4_sequence( let synth_dir = match synth_direction.as_slice() { [0, 1] => true, [1, 0] => false, - _ => panic!(), + _ => unreachable!(), }; if synth_dir != preferred_dir { reversed_synth_su4_sequence( @@ -947,7 +946,7 @@ fn reversed_synth_su4_sequence( let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? } else { - panic!("reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") + unreachable!("reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") }; let flip_bits: [u8; 2] = [1, 0]; @@ -989,7 +988,7 @@ fn synth_su4_dag( )? .extract::(py)? } else { - panic!("synth_su4_dag should only be called for XXDecomposer.") + unreachable!("synth_su4_dag should only be called for XXDecomposer.") }; match preferred_direction { @@ -1009,7 +1008,7 @@ fn synth_su4_dag( let synth_dir = match synth_direction.as_slice() { [0, 1] => true, [1, 0] => false, - _ => panic!("There are no more than 2 possible synth directions."), + _ => unreachable!("There are no more than 2 possible synth directions."), }; if synth_dir != preferred_dir { reversed_synth_su4_dag( @@ -1055,7 +1054,7 @@ fn reversed_synth_su4_dag( )? .extract::(py)? } else { - panic!("reversed_synth_su4_dag should only be called for XXDecomposer") + unreachable!("reversed_synth_su4_dag should only be called for XXDecomposer") }; let mut target_dag = synth_dag.copy_empty_like(py, "alike")?; From 228f470e3fe27f4bd645d97aa45c9b69e9b54b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 17 Oct 2024 10:08:36 +0200 Subject: [PATCH 40/45] Format --- crates/accelerate/src/unitary_synthesis.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 45ed16f36e98..10ef6f4591f8 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -946,7 +946,9 @@ fn reversed_synth_su4_sequence( let synth = if let DecomposerType::TwoQubitBasisDecomposer(decomp) = &decomposer_2q.decomposer { decomp.call_inner(su4_mat.view(), None, is_approximate, None)? } else { - unreachable!("reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer.") + unreachable!( + "reversed_synth_su4_sequence should only be called for TwoQubitBasisDecomposer." + ) }; let flip_bits: [u8; 2] = [1, 0]; From e21d0d7c41ecb2b068dc61049164972d5f28b7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:59:44 +0200 Subject: [PATCH 41/45] Apply suggestions from Matt's code review Co-authored-by: Matthew Treinish --- crates/accelerate/src/unitary_synthesis.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 10ef6f4591f8..91eaff0bc274 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -132,7 +132,7 @@ fn apply_synth_sequence( out_qargs: &[Qubit], sequence: &TwoQubitUnitarySequence, ) -> PyResult<()> { - let mut instructions = Vec::new(); + let mut instructions = Vec::with_capacity(sequence.gate_sequence.gates().len()); for (gate, params, qubit_ids) in sequence.gate_sequence.gates() { let gate_node = match gate { None => sequence.decomp_gate.operation.standard_gate(), @@ -170,7 +170,11 @@ fn synth_error( >, target: &Target, ) -> f64 { - let mut gate_fidelities = Vec::new(); + let (lower_bound, upper_bound) = synth_circuit.size_hint(); + let mut gate_fidelities = match upper_bound { + Some(bound) => Vec::with_capacity(bound), + None => Vec::with_capacity(lower_bound), + }; let mut score_instruction = |inst_name: &str, inst_params: &Option>, @@ -215,7 +219,7 @@ fn synth_error( // This is the outer-most run function. It is meant to be called from Python // in `UnitarySynthesis.run()`. #[pyfunction] -#[pyo3(name = "run_default_main_loop")] +#[pyo3(name = "run_default_main_loop", signature=(dag, qubit_indices, min_qubits, target, coupling_edges, approximation_degree=None, natural_direction=None)] fn py_run_main_loop( py: Python, dag: &mut DAGCircuit, From 7f5eb02740da869d490f395c5b626dca419e7e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Fri, 18 Oct 2024 16:01:55 +0200 Subject: [PATCH 42/45] Fix suggestions from Matt's code review --- crates/accelerate/src/unitary_synthesis.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 91eaff0bc274..73215cc1f8ed 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -219,7 +219,7 @@ fn synth_error( // This is the outer-most run function. It is meant to be called from Python // in `UnitarySynthesis.run()`. #[pyfunction] -#[pyo3(name = "run_default_main_loop", signature=(dag, qubit_indices, min_qubits, target, coupling_edges, approximation_degree=None, natural_direction=None)] +#[pyo3(name = "run_default_main_loop", signature=(dag, qubit_indices, min_qubits, target, coupling_edges, approximation_degree=None, natural_direction=None))] fn py_run_main_loop( py: Python, dag: &mut DAGCircuit, From 5deb7f584257ee8dd59a616fea927511ba3d0f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Fri, 18 Oct 2024 16:46:17 +0200 Subject: [PATCH 43/45] Apply remaining suggestions --- crates/accelerate/src/unitary_synthesis.rs | 116 +++++++++++---------- 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 73215cc1f8ed..459b01f35951 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -18,10 +18,11 @@ use std::f64::consts::PI; use approx::relative_eq; use hashbrown::{HashMap, HashSet}; use indexmap::IndexMap; +use itertools::Itertools; use ndarray::prelude::*; use num_complex::{Complex, Complex64}; use numpy::IntoPyArray; -use qiskit_circuit::circuit_instruction::ExtraInstructionAttributes; +use qiskit_circuit::circuit_instruction::{ExtraInstructionAttributes, OperationFromPython}; use smallvec::{smallvec, SmallVec}; use pyo3::intern; @@ -30,8 +31,6 @@ use pyo3::types::{IntoPyDict, PyDict, PyList, PyString}; use pyo3::wrap_pyfunction; use pyo3::Python; -use rustworkx_core::petgraph::stable_graph::NodeIndex; - use qiskit_circuit::converters::{circuit_to_dag, QuantumCircuitData}; use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType}; use qiskit_circuit::imports; @@ -223,7 +222,7 @@ fn synth_error( fn py_run_main_loop( py: Python, dag: &mut DAGCircuit, - qubit_indices: &Bound<'_, PyList>, + qubit_indices: Vec, min_qubits: usize, target: &Target, coupling_edges: &Bound<'_, PyList>, @@ -232,63 +231,66 @@ fn py_run_main_loop( ) -> PyResult { let dag_to_circuit = imports::DAG_TO_CIRCUIT.get_bound(py); - // Run synthesis recursively over control flow ops, mapping qubit indices - let node_ids: Vec = dag.op_nodes(false).collect(); - for node in node_ids { - let inst = &dag.dag()[node].unwrap_operation(); - if !inst.op.control_flow() { - continue; - } - let OperationRef::Instruction(py_inst) = inst.op.view() else { - unreachable!("Control flow op must be an instruction") - }; - let raw_blocks: Vec>> = py_inst - .instruction - .getattr(py, "blocks")? - .bind(py) - .iter()? - .collect(); - let mut new_blocks = Vec::with_capacity(raw_blocks.len()); - for raw_block in raw_blocks { - let new_ids = dag.get_qargs(inst.qubits).iter().map(|qarg| { - qubit_indices - .get_item(qarg.0 as usize) - .expect("Unexpected index error in DAG") - }); - let res = py_run_main_loop( - py, - &mut circuit_to_dag( - py, - QuantumCircuitData::extract_bound(&raw_block?)?, - false, - None, - None, - )?, - &PyList::new_bound(py, new_ids), - min_qubits, - target, - coupling_edges, - approximation_degree, - natural_direction, - )?; - new_blocks.push(dag_to_circuit.call1((res,))?); - } - let old_node = dag.get_node(py, node)?.clone(); - let new_node = py_inst - .instruction - .bind(py) - .call_method1("replace_blocks", (new_blocks,))?; - dag.py_substitute_node(old_node.bind(py), &new_node, true, false)?; - } let mut out_dag = dag.copy_empty_like(py, "alike")?; // Iterate over dag nodes and determine unitary synthesis approach for node in dag.topological_op_nodes()? { - let packed_instr = dag.dag()[node].unwrap_operation(); + let mut packed_instr = dag.dag()[node].unwrap_operation().clone(); + + if packed_instr.op.control_flow() { + let OperationRef::Instruction(py_instr) = packed_instr.op.view() else { + unreachable!("Control flow op must be an instruction") + }; + let raw_blocks: Vec>> = py_instr + .instruction + .getattr(py, "blocks")? + .bind(py) + .iter()? + .collect(); + let mut new_blocks = Vec::with_capacity(raw_blocks.len()); + for raw_block in raw_blocks { + let new_ids = dag + .get_qargs(packed_instr.qubits) + .iter() + .map(|qarg| qubit_indices[qarg.0 as usize]) + .collect_vec(); + let res = py_run_main_loop( + py, + &mut circuit_to_dag( + py, + QuantumCircuitData::extract_bound(&raw_block?)?, + false, + None, + None, + )?, + new_ids, + min_qubits, + target, + coupling_edges, + approximation_degree, + natural_direction, + )?; + new_blocks.push(dag_to_circuit.call1((res,))?); + } + let new_node = py_instr + .instruction + .bind(py) + .call_method1("replace_blocks", (new_blocks,))?; + let new_node_op: OperationFromPython = new_node.extract()?; + packed_instr = PackedInstruction { + op: new_node_op.operation, + qubits: packed_instr.qubits, + clbits: packed_instr.clbits, + params: (!new_node_op.params.is_empty()).then(|| Box::new(new_node_op.params)), + extra_attrs: new_node_op.extra_attrs, + #[cfg(feature = "cache_pygates")] + py_op: new_node.unbind().into(), + }; + } if !(packed_instr.op.name() == "unitary" && packed_instr.op.num_qubits() >= min_qubits as u32) { - out_dag.push_back(py, packed_instr.clone())?; + out_dag.push_back(py, packed_instr)?; continue; } let unitary: Array, Dim<[usize; 2]>> = match packed_instr.op.matrix(&[]) { @@ -327,7 +329,7 @@ fn py_run_main_loop( out_dag.add_global_phase(py, &Param::Float(sequence.global_phase))?; } None => { - out_dag.push_back(py, packed_instr.clone())?; + out_dag.push_back(py, packed_instr)?; } } } @@ -337,8 +339,8 @@ fn py_run_main_loop( let out_qargs = dag.get_qargs(packed_instr.qubits); // "ref_qubits" is used to access properties in the target. It accounts for control flow mapping. let ref_qubits: &[PhysicalQubit; 2] = &[ - PhysicalQubit::new(qubit_indices.get_item(out_qargs[0].0 as usize)?.extract()?), - PhysicalQubit::new(qubit_indices.get_item(out_qargs[1].0 as usize)?.extract()?), + PhysicalQubit::new(qubit_indices[out_qargs[0].0 as usize] as u32), + PhysicalQubit::new(qubit_indices[out_qargs[1].0 as usize] as u32), ]; let apply_original_op = |out_dag: &mut DAGCircuit| -> PyResult<()> { out_dag.push_back(py, packed_instr.clone())?; From 40314f314ec8ace7f203df495aeba90b537becfb Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:08:12 -0400 Subject: [PATCH 44/45] Apply final suggestions from code review --- crates/accelerate/src/unitary_synthesis.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 459b01f35951..fb8ddc96dfe1 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -229,6 +229,8 @@ fn py_run_main_loop( approximation_degree: Option, natural_direction: Option, ) -> PyResult { + // We need to use the python converter because the currently available Rust conversion + // is lossy. We need `QuantumCircuit` instances to be used in `replace_blocks`. let dag_to_circuit = imports::DAG_TO_CIRCUIT.get_bound(py); let mut out_dag = dag.copy_empty_like(py, "alike")?; @@ -239,7 +241,7 @@ fn py_run_main_loop( if packed_instr.op.control_flow() { let OperationRef::Instruction(py_instr) = packed_instr.op.view() else { - unreachable!("Control flow op must be an instruction") + unreachable!("Control flow op must be an instruction") }; let raw_blocks: Vec>> = py_instr .instruction @@ -668,7 +670,7 @@ fn get_2q_decomposers_from_target( basis_2q_fidelity *= approx_degree; } let decomposer = TwoQubitBasisDecomposer::new_inner( - gate.operation.name().to_owned(), + gate.operation.name().to_string(), gate.operation.matrix(&gate.params).unwrap().view(), basis_2q_fidelity, basis_1q, From 0bbf16a28b4ef29d851eb42e508842e24e9e6119 Mon Sep 17 00:00:00 2001 From: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:18:07 -0400 Subject: [PATCH 45/45] Lint: Use `unwrap_or_default()`. --- crates/accelerate/src/unitary_synthesis.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index fb8ddc96dfe1..1931f1e97b2b 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -394,10 +394,7 @@ fn run_2q_unitary_synthesis( let decomposers = { let decomposers_2q = get_2q_decomposers_from_target(py, target, ref_qubits, approximation_degree)?; - match decomposers_2q { - Some(decomp) => decomp, - None => Vec::new(), - } + decomposers_2q.unwrap_or_default() }; // If there's a single decomposer, avoid computing synthesis score if decomposers.len() == 1 {