From 10f6520cb08b42ef0ab488b57accdadaa6d9c06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 20 Jan 2025 15:19:11 +0100 Subject: [PATCH 1/7] Change DecomposerElement to store std gate + params --- crates/accelerate/src/unitary_synthesis.rs | 29 +++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 439b9ba80f67..9277dbd1d382 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -59,13 +59,15 @@ enum DecomposerType { struct DecomposerElement { decomposer: DecomposerType, - gate: NormalOperation, + gate: StandardGate, + params: SmallVec<[Param; 3]>, } #[derive(Clone, Debug)] struct TwoQubitUnitarySequence { gate_sequence: TwoQubitGateSequence, - decomp_gate: NormalOperation, + decomp_gate: StandardGate, + decomp_params: SmallVec<[Param; 3]>, } // Used in get_2q_decomposers. If the found 2q basis is a subset of GOODBYE_SET, @@ -134,13 +136,13 @@ fn apply_synth_sequence( 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(), + None => sequence.decomp_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())), + None => Some(Box::new(sequence.decomp_params.clone())), }; let instruction = PackedInstruction { op: PackedOperation::from_standard(gate_node), @@ -460,12 +462,7 @@ fn run_2q_unitary_synthesis( inst_qubits, ), None => ( - sequence - .decomp_gate - .operation - .standard_gate() - .name() - .to_string(), + sequence.decomp_gate.name().to_string(), Some(params.iter().map(|p| Param::Float(*p)).collect()), inst_qubits, ), @@ -681,7 +678,8 @@ fn get_2q_decomposers_from_target( decomposers.push(DecomposerElement { decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), - gate: gate.clone(), + gate: gate.operation.standard_gate().clone(), + params: gate.params.clone(), }); } } @@ -788,7 +786,8 @@ fn get_2q_decomposers_from_target( decomposers.push(DecomposerElement { decomposer: DecomposerType::XXDecomposer(decomposer.into()), - gate: decomposer_gate, + gate: decomposer_gate.operation.standard_gate().clone(), + params: decomposer_gate.params, }); } } @@ -811,8 +810,8 @@ fn preferred_direction( let compute_cost = |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( + let cost = match target.qargs_for_operation_name(decomposer.gate.name()) { + Ok(_) => match target[decomposer.gate.name()].get(Some( &q_tuple .into_iter() .collect::>(), @@ -895,6 +894,7 @@ fn synth_su4_sequence( let sequence = TwoQubitUnitarySequence { gate_sequence: synth, decomp_gate: decomposer_2q.gate.clone(), + decomp_params: decomposer_2q.params.clone(), }; match preferred_direction { @@ -970,6 +970,7 @@ fn reversed_synth_su4_sequence( let sequence = TwoQubitUnitarySequence { gate_sequence: reversed_synth, decomp_gate: decomposer_2q.gate.clone(), + decomp_params: decomposer_2q.params.clone(), }; Ok(sequence) } From 778fa5a1cec693342db6e3f785f82e60d8d1b236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Mon, 20 Jan 2025 17:41:21 +0100 Subject: [PATCH 2/7] Remove BackendV1 from unitary synthesis unit tests --- .../transpiler/test_unitary_synthesis.py | 203 +++++++----------- 1 file changed, 83 insertions(+), 120 deletions(-) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index d7940ab1a763..3b247df88268 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -22,7 +22,7 @@ from ddt import ddt, data from qiskit import transpile -from qiskit.providers.fake_provider import Fake5QV1, GenericBackendV2 +from qiskit.providers.fake_provider import GenericBackendV2 from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit.circuit.library import quantum_volume from qiskit.converters import circuit_to_dag, dag_to_circuit @@ -70,6 +70,7 @@ from test.python.providers.fake_mumbai_v2 import ( # pylint: disable=wrong-import-order FakeMumbaiFractionalCX, ) +from ..legacy_cmaps import YORKTOWN_CMAP class FakeBackend2QV2(GenericBackendV2): @@ -147,78 +148,18 @@ def test_two_qubit_synthesis_to_basis(self, basis_gates): out = UnitarySynthesis(basis_gates).run(dag) self.assertTrue(set(out.count_ops()).issubset(basis_gates)) - @combine(gate=["unitary", "swap"], natural_direction=[True, False]) - def test_two_qubit_synthesis_to_directional_cx(self, gate, natural_direction): - """Verify two qubit unitaries are synthesized to match basis gates.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - coupling_map = CouplingMap(conf.coupling_map) - triv_layout_pass = TrivialLayout(coupling_map) - - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - if gate == "unitary": - qc.unitary(random_unitary(4, seed=12), [0, 1]) - elif gate == "swap": - qc.swap(qr[0], qr[1]) - - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=natural_direction, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - - @data(True, False) - def test_two_qubit_synthesis_to_directional_cx_multiple_registers(self, natural_direction): - """Verify two qubit unitaries are synthesized to match basis gates - across multiple registers.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr0 = QuantumRegister(1) - qr1 = QuantumRegister(1) - coupling_map = CouplingMap(conf.coupling_map) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr0, qr1) - qc.unitary(random_unitary(4, seed=12), [qr0[0], qr1[0]]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=None, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=natural_direction, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - self.assertEqual(Operator(qc), Operator(qc_out)) - @data(True, False, None) def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_direction): """Verify natural cx direction is used when specified in coupling map.""" - # TODO: should make check more explicit e.g. explicitly set gate - # direction in test instead of using specific fake backend - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() + qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=True, natural_direction=natural_direction, ) @@ -239,9 +180,7 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_d def test_two_qubit_synthesis_not_pulse_optimal(self): """Verify not attempting pulse optimal decomposition when pulse_optimize==False.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() + qr = QuantumRegister(2) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) @@ -250,9 +189,8 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): [ TrivialLayout(coupling_map), UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=False, natural_direction=True, ), @@ -262,9 +200,8 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): [ TrivialLayout(coupling_map), UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=True, natural_direction=True, ), @@ -275,21 +212,18 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): self.assertGreater(qc_nonoptimal.count_ops()["sx"], qc_optimal.count_ops()["sx"]) def test_two_qubit_pulse_optimal_true_raises(self): - """Verify raises if pulse optimal==True but cx is not in the backend basis.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() + """Verify raises if pulse optimal==True but cx is not in the basis.""" + basis_gates = ['id', 'rz', 'sx', 'x', 'cx', 'reset'] # this assumes iswap pulse optimal decomposition doesn't exist - conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] + basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=basis_gates, coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=True, natural_direction=True, ) @@ -297,47 +231,17 @@ def test_two_qubit_pulse_optimal_true_raises(self): with self.assertRaises(QiskitError): pm.run(qc) - def test_two_qubit_natural_direction_true_duration_fallback(self): - """Verify fallback path when pulse_optimize==True.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - qr = QuantumRegister(2) - coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) - triv_layout_pass = TrivialLayout(coupling_map) - qc = QuantumCircuit(qr) - qc.unitary(random_unitary(4, seed=12), [0, 1]) - unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - coupling_map=coupling_map, - backend_props=backend.properties(), - pulse_optimize=True, - natural_direction=True, - ) - pm = PassManager([triv_layout_pass, unisynth_pass]) - qc_out = pm.run(qc) - self.assertTrue( - all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx"))) - ) - def test_two_qubit_natural_direction_true_gate_length_raises(self): """Verify that error is raised if preferred direction cannot be inferred from gate lenghts/errors. """ - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - for _, nduv in backend.properties()._gates["cx"].items(): - nduv["gate_length"] = (4e-7, nduv["gate_length"][1]) - nduv["gate_error"] = (7e-3, nduv["gate_error"][1]) qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, - backend_props=backend.properties(), + basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], pulse_optimize=True, natural_direction=True, ) @@ -347,18 +251,14 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): def test_two_qubit_pulse_optimal_none_optimal(self): """Verify pulse optimal decomposition when pulse_optimize==None.""" - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=None, natural_direction=True, ) @@ -374,20 +274,17 @@ def test_two_qubit_pulse_optimal_none_optimal(self): def test_two_qubit_pulse_optimal_none_no_raise(self): """Verify pulse optimal decomposition when pulse_optimize==None doesn't raise when pulse optimal decomposition unknown.""" - # this assumes iswawp pulse optimal decomposition doesn't exist - with self.assertWarns(DeprecationWarning): - backend = Fake5QV1() - conf = backend.configuration() - conf.basis_gates = [gate if gate != "cx" else "iswap" for gate in conf.basis_gates] + basis_gates = ['id', 'rz', 'sx', 'x', 'cx', 'reset'] + # this assumes iswap pulse optimal decomposition doesn't exist + basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=conf.basis_gates, + basis_gates=basis_gates, coupling_map=coupling_map, - backend_props=backend.properties(), pulse_optimize=None, natural_direction=True, ) @@ -934,6 +831,72 @@ def test_target_with_global_gates(self): tqc = transpile(qc, target=target) self.assertTrue(set(tqc.count_ops()).issubset(basis_gates)) + @combine(gate=["unitary", "swap"], natural_direction=[True, False]) + def test_two_qubit_synthesis_to_directional_cx_target(self, gate, natural_direction): + """Verify two qubit unitaries are synthesized to match basis gates.""" + # TODO: should make check more explicit e.g. explicitly set gate + # direction in test instead of using specific fake backend + backend = GenericBackendV2(num_qubits=5, basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], coupling_map=YORKTOWN_CMAP) + coupling_map = CouplingMap(backend.coupling_map) + triv_layout_pass = TrivialLayout(coupling_map) + + qr = QuantumRegister(2) + qc = QuantumCircuit(qr) + if gate == "unitary": + qc.unitary(random_unitary(4, seed=12), [0, 1]) + elif gate == "swap": + qc.swap(qr[0], qr[1]) + + unisynth_pass = UnitarySynthesis( + target=backend.target, + pulse_optimize=True, + natural_direction=natural_direction, + ) + pm = PassManager([triv_layout_pass, unisynth_pass]) + qc_out = pm.run(qc) + self.assertEqual(Operator(qc), Operator(qc_out)) + + @data(True, False) + def test_two_qubit_synthesis_to_directional_cx_multiple_registers_target(self, natural_direction): + """Verify two qubit unitaries are synthesized to match basis gates + across multiple registers.""" + # TODO: should make check more explicit e.g. explicitly set gate + # direction in test instead of using specific fake backend + backend = GenericBackendV2(num_qubits=5, basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], coupling_map=YORKTOWN_CMAP) + qr0 = QuantumRegister(1) + qr1 = QuantumRegister(1) + coupling_map = CouplingMap(backend.coupling_map) + triv_layout_pass = TrivialLayout(coupling_map) + qc = QuantumCircuit(qr0, qr1) + qc.unitary(random_unitary(4, seed=12), [qr0[0], qr1[0]]) + unisynth_pass = UnitarySynthesis( + target = backend.target, + pulse_optimize=True, + natural_direction=natural_direction, + ) + pm = PassManager([triv_layout_pass, unisynth_pass]) + qc_out = pm.run(qc) + self.assertEqual(Operator(qc), Operator(qc_out)) + + def test_two_qubit_natural_direction_true_duration_fallback_target(self): + """Verify fallback path when pulse_optimize==True.""" + basis_gates = ['id', 'rz', 'sx', 'x', 'cx', 'reset'] + qr = QuantumRegister(2) + coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) + backend = GenericBackendV2(num_qubits=5, basis_gates=basis_gates, coupling_map=coupling_map) + triv_layout_pass = TrivialLayout(coupling_map) + qc = QuantumCircuit(qr) + qc.unitary(random_unitary(4, seed=12), [0, 1]) + unisynth_pass = UnitarySynthesis( + target = backend.target, + pulse_optimize=True, + natural_direction=True, + ) + pm = PassManager([triv_layout_pass, unisynth_pass]) + qc_out = pm.run(qc) + self.assertTrue( + all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx"))) + ) if __name__ == "__main__": unittest.main() From a261d8d3e49c38e0f5e5e292c10127cc7dc1c264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Thu, 16 Jan 2025 17:47:32 +0100 Subject: [PATCH 3/7] Migrate basis_gates path to Rust. Fix relevant tests. --- crates/accelerate/src/unitary_synthesis.rs | 218 ++++++++++++++---- .../passes/synthesis/unitary_synthesis.py | 7 +- .../transpiler/test_unitary_synthesis.py | 5 +- 3 files changed, 181 insertions(+), 49 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 9277dbd1d382..bc2f80aa4ed0 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -17,7 +17,7 @@ use std::sync::OnceLock; use approx::relative_eq; use hashbrown::{HashMap, HashSet}; -use indexmap::IndexMap; +use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; use ndarray::prelude::*; use num_complex::{Complex, Complex64}; @@ -75,34 +75,45 @@ struct TwoQubitUnitarySequence { // no need to bother trying the XXDecomposer. static GOODBYE_SET: [&str; 3] = ["cx", "cz", "ecr"]; +fn get_euler_basis_set(basis_list: IndexSet<&str>) -> EulerBasisSet { + let mut euler_basis_set: EulerBasisSet = EulerBasisSet::new(); + 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| euler_basis_set.add_basis(basis)); + + if euler_basis_set.basis_supported(EulerBasis::U3) + && euler_basis_set.basis_supported(EulerBasis::U321) + { + euler_basis_set.remove(EulerBasis::U3); + } + if euler_basis_set.basis_supported(EulerBasis::ZSX) + && euler_basis_set.basis_supported(EulerBasis::ZSXX) + { + euler_basis_set.remove(EulerBasis::ZSX); + } + euler_basis_set +} + 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)); + target_basis_set = get_euler_basis_set(basis_list.into_iter().collect()); + } + Err(_) => { + target_basis_set.support_all(); + target_basis_set.remove(EulerBasis::U3); + target_basis_set.remove(EulerBasis::ZSX); } - 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 } @@ -220,13 +231,14 @@ 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_main_loop", signature=(dag, qubit_indices, min_qubits, target, basis_gates, coupling_edges, approximation_degree=None, natural_direction=None))] fn py_run_main_loop( py: Python, dag: &mut DAGCircuit, qubit_indices: Vec, min_qubits: usize, - target: &Target, + target: Option<&Target>, + basis_gates: HashSet, coupling_edges: HashSet<[PhysicalQubit; 2]>, approximation_degree: Option, natural_direction: Option, @@ -270,6 +282,7 @@ fn py_run_main_loop( new_ids, min_qubits, target, + basis_gates.clone(), coupling_edges.clone(), approximation_degree, natural_direction, @@ -301,11 +314,19 @@ fn py_run_main_loop( 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 target_basis_set = match target { + Some(target) => get_target_basis_set(target, PhysicalQubit::new(qubit.0)), + None => { + let basis_gates: IndexSet<&str> = + basis_gates.iter().map(String::as_str).collect(); + get_euler_basis_set(basis_gates) + } + }; let sequence = unitary_to_gate_sequence_inner( unitary.view(), &target_basis_set, @@ -356,6 +377,7 @@ fn py_run_main_loop( ref_qubits, &coupling_edges, target, + basis_gates.clone(), approximation_degree, natural_direction, &mut out_dag, @@ -387,28 +409,44 @@ fn run_2q_unitary_synthesis( unitary: Array2, ref_qubits: &[PhysicalQubit; 2], coupling_edges: &HashSet<[PhysicalQubit; 2]>, - target: &Target, + target: Option<&Target>, + basis_gates: HashSet, approximation_degree: Option, natural_direction: Option, out_dag: &mut DAGCircuit, out_qargs: &[Qubit], mut apply_original_op: impl FnMut(&mut DAGCircuit) -> PyResult<()>, ) -> PyResult<()> { - let decomposers = { - let decomposers_2q = - get_2q_decomposers_from_target(py, target, ref_qubits, approximation_degree)?; - decomposers_2q.unwrap_or_default() + let decomposers = match target { + Some(target) => { + let decomposers_2q = + get_2q_decomposers_from_target(py, target, ref_qubits, approximation_degree)?; + decomposers_2q.unwrap_or_default() + } + None => { + let basis_gates: IndexSet<&str> = + basis_gates.iter().map(String::as_str).collect(); + let decomposer_item: Option = + get_2q_decomposer_from_basis(py, basis_gates, approximation_degree)?; + if decomposer_item.is_none() { + return Ok(()); + }; + vec![decomposer_item.unwrap()] + } }; // 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, - ref_qubits, - natural_direction, - coupling_edges, - target, - )?; + let preferred_dir = match target { + Some(target) => preferred_direction_target( + decomposer_item, + ref_qubits, + natural_direction, + coupling_edges, + target, + )?, + None => preferred_direction_basis(ref_qubits, natural_direction, coupling_edges)?, + }; match decomposer_item.decomposer { DecomposerType::TwoQubitBasisDecomposer(_) => { let synth = synth_su4_sequence( @@ -436,12 +474,12 @@ fn run_2q_unitary_synthesis( let mut synth_errors_sequence = Vec::new(); let mut synth_errors_dag = Vec::new(); for decomposer in &decomposers { - let preferred_dir = preferred_direction( + let preferred_dir = preferred_direction_target( decomposer, ref_qubits, natural_direction, coupling_edges, - target, + target.unwrap(), )?; match &decomposer.decomposer { DecomposerType::TwoQubitBasisDecomposer(_) => { @@ -468,7 +506,7 @@ fn run_2q_unitary_synthesis( ), } }); - let synth_error_from_target = synth_error(py, scoring_info, target); + let synth_error_from_target = synth_error(py, scoring_info, target.unwrap()); synth_errors_sequence.push((sequence, synth_error_from_target)); } DecomposerType::XXDecomposer(_) => { @@ -497,7 +535,7 @@ fn run_2q_unitary_synthesis( inst_qubits, ) }); - let synth_error_from_target = synth_error(py, scoring_info, target); + let synth_error_from_target = synth_error(py, scoring_info, target.unwrap()); synth_errors_dag.push((synth_dag, synth_error_from_target)); } } @@ -530,6 +568,61 @@ fn run_2q_unitary_synthesis( Ok(()) } +fn get_2q_decomposer_from_basis( + py: Python, + basis_gates: IndexSet<&str>, + approximation_degree: Option, +) -> PyResult> { + let mut kak_gate_names: IndexMap<&str, (StandardGate, &[Param])> = IndexMap::new(); + kak_gate_names.insert("cx", (StandardGate::CXGate, &[])); + kak_gate_names.insert("cz", (StandardGate::CZGate, &[])); + kak_gate_names.insert("iswap", (StandardGate::ISwapGate, &[])); + kak_gate_names.insert("rxx", (StandardGate::RXXGate, &[Param::Float(PI2)])); + kak_gate_names.insert("ecr", (StandardGate::ECRGate, &[])); + kak_gate_names.insert("rzx", (StandardGate::RZXGate, &[Param::Float(PI4)])); + + let kak_keys: IndexSet<&str> = kak_gate_names.keys().copied().collect(); + let kak_gates: Vec<&str> = kak_keys.intersection(&basis_gates).copied().collect(); + + let euler_basis = match get_euler_basis_set(basis_gates) + .get_bases() + .map(|basis| basis.as_str()) + .next(){ + Some(basis) => basis, + None => return Ok(None) + }; + let basis_fidelity = approximation_degree.unwrap_or(1.0); + + if kak_gates[0] == "rzx" { + let xx_decomposer: &Bound<'_, PyAny> = imports::XX_DECOMPOSER.get_bound(py); + let decomposer = xx_decomposer.call1((PyString::new_bound(py, euler_basis),))?; + let decomposer_gate = decomposer + .getattr(intern!(py, "gate"))? + .extract::()?; + + Ok(Some(DecomposerElement { + decomposer: DecomposerType::XXDecomposer(decomposer.into()), + gate: decomposer_gate.operation.standard_gate().clone(), + params: decomposer_gate.params, + })) + } else if !kak_gates.is_empty() { + let kak_gate = *kak_gate_names.get(kak_gates[0]).unwrap(); + let decomposer = TwoQubitBasisDecomposer::new_inner( + kak_gate.0.name().to_string(), + kak_gate.0.matrix(kak_gate.1).unwrap().view(), + basis_fidelity, + euler_basis, + None, + )?; + Ok(Some(DecomposerElement { + decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), + gate: kak_gate.0, + params: kak_gate.1.into(), + })) + } else { + Ok(None) + } +} fn get_2q_decomposers_from_target( py: Python, target: &Target, @@ -794,7 +887,7 @@ fn get_2q_decomposers_from_target( Ok(Some(decomposers)) } -fn preferred_direction( +fn preferred_direction_target( decomposer: &DecomposerElement, ref_qubits: &[PhysicalQubit; 2], natural_direction: Option, @@ -879,6 +972,45 @@ fn preferred_direction( Ok(preferred_direction) } +fn preferred_direction_basis( + ref_qubits: &[PhysicalQubit; 2], + natural_direction: Option, + coupling_edges: &HashSet<[PhysicalQubit; 2]>, +) -> 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: [PhysicalQubit; 2] = *ref_qubits; + let mut reverse_qubits: [PhysicalQubit; 2] = qubits; + reverse_qubits.reverse(); + + let preferred_direction = match natural_direction { + Some(false) => None, + _ => { + // None or Some(true) + let zero_one = coupling_edges.contains(&qubits); + let one_zero = coupling_edges.contains(&[qubits[1], qubits[0]]); + + match (zero_one, one_zero) { + (true, false) => Some(true), + (false, true) => Some(false), + _ => None, + } + } + }; + + if natural_direction == Some(true) && preferred_direction.is_none() { + 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) +} fn synth_su4_sequence( su4_mat: &Array2, decomposer_2q: &DecomposerElement, diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index a2bd044c7341..3b24640ab73d 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -73,7 +73,7 @@ from qiskit.transpiler.passes.synthesis import plugin from qiskit.transpiler.target import Target -from qiskit._accelerate.unitary_synthesis import run_default_main_loop +from qiskit._accelerate.unitary_synthesis import run_main_loop GATE_NAME_MAP = { "cx": CXGate._standard_gate, @@ -472,16 +472,17 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else {} ) - if self.method == "default" and self._target is not None: + if self.method == "default" and not (self._target is not None and len(self._target.operation_names) ==0): _coupling_edges = ( set(self._coupling_map.get_edges()) if self._coupling_map is not None else set() ) - out = run_default_main_loop( + out = run_main_loop( dag, list(qubit_indices.values()), self._min_qubits, self._target, + self._basis_gates, _coupling_edges, self._approximation_degree, self._natural_direction, diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 3b247df88268..05be41f0a7f1 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -213,9 +213,8 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): def test_two_qubit_pulse_optimal_true_raises(self): """Verify raises if pulse optimal==True but cx is not in the basis.""" - basis_gates = ['id', 'rz', 'sx', 'x', 'cx', 'reset'] + basis_gates = ['id', 'rz', 'sx', 'x','iswap', 'reset'] # this assumes iswap pulse optimal decomposition doesn't exist - basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) triv_layout_pass = TrivialLayout(coupling_map) @@ -246,7 +245,7 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): natural_direction=True, ) pm = PassManager([triv_layout_pass, unisynth_pass]) - with self.assertRaises(TranspilerError): + with self.assertRaises(QiskitError): pm.run(qc) def test_two_qubit_pulse_optimal_none_optimal(self): From 563566c5a34cc09c0e9a7db60b1bd44f26a784e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 21 Jan 2025 11:23:00 +0100 Subject: [PATCH 4/7] Add pulse_optimize to Rust path, fix relevant tests. --- crates/accelerate/src/unitary_synthesis.rs | 41 +++++++++++------ .../passes/synthesis/unitary_synthesis.py | 5 ++- .../transpiler/test_unitary_synthesis.py | 45 +++++++++++++------ 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index bc2f80aa4ed0..8eca4b9b7b0f 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -231,7 +231,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_main_loop", signature=(dag, qubit_indices, min_qubits, target, basis_gates, coupling_edges, approximation_degree=None, natural_direction=None))] +#[pyo3(name = "run_main_loop", signature=(dag, qubit_indices, min_qubits, target, basis_gates, coupling_edges, approximation_degree=None, natural_direction=None, pulse_optimize=None))] fn py_run_main_loop( py: Python, dag: &mut DAGCircuit, @@ -242,6 +242,7 @@ fn py_run_main_loop( coupling_edges: HashSet<[PhysicalQubit; 2]>, approximation_degree: Option, natural_direction: Option, + pulse_optimize: 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`. @@ -286,6 +287,7 @@ fn py_run_main_loop( coupling_edges.clone(), approximation_degree, natural_direction, + pulse_optimize, )?; new_blocks.push(dag_to_circuit.call1((res,))?); } @@ -380,6 +382,7 @@ fn py_run_main_loop( basis_gates.clone(), approximation_degree, natural_direction, + pulse_optimize, &mut out_dag, out_qargs, apply_original_op, @@ -413,21 +416,30 @@ fn run_2q_unitary_synthesis( basis_gates: HashSet, approximation_degree: Option, natural_direction: Option, + pulse_optimize: Option, out_dag: &mut DAGCircuit, out_qargs: &[Qubit], mut apply_original_op: impl FnMut(&mut DAGCircuit) -> PyResult<()>, ) -> PyResult<()> { let decomposers = match target { Some(target) => { - let decomposers_2q = - get_2q_decomposers_from_target(py, target, ref_qubits, approximation_degree)?; + let decomposers_2q = get_2q_decomposers_from_target( + py, + target, + ref_qubits, + approximation_degree, + pulse_optimize, + )?; decomposers_2q.unwrap_or_default() } None => { - let basis_gates: IndexSet<&str> = - basis_gates.iter().map(String::as_str).collect(); - let decomposer_item: Option = - get_2q_decomposer_from_basis(py, basis_gates, approximation_degree)?; + let basis_gates: IndexSet<&str> = basis_gates.iter().map(String::as_str).collect(); + let decomposer_item: Option = get_2q_decomposer_from_basis( + py, + basis_gates, + approximation_degree, + pulse_optimize, + )?; if decomposer_item.is_none() { return Ok(()); }; @@ -572,6 +584,7 @@ fn get_2q_decomposer_from_basis( py: Python, basis_gates: IndexSet<&str>, approximation_degree: Option, + pulse_optimize: Option, ) -> PyResult> { let mut kak_gate_names: IndexMap<&str, (StandardGate, &[Param])> = IndexMap::new(); kak_gate_names.insert("cx", (StandardGate::CXGate, &[])); @@ -587,10 +600,11 @@ fn get_2q_decomposer_from_basis( let euler_basis = match get_euler_basis_set(basis_gates) .get_bases() .map(|basis| basis.as_str()) - .next(){ - Some(basis) => basis, - None => return Ok(None) - }; + .next() + { + Some(basis) => basis, + None => return Ok(None), + }; let basis_fidelity = approximation_degree.unwrap_or(1.0); if kak_gates[0] == "rzx" { @@ -612,7 +626,7 @@ fn get_2q_decomposer_from_basis( kak_gate.0.matrix(kak_gate.1).unwrap().view(), basis_fidelity, euler_basis, - None, + pulse_optimize, )?; Ok(Some(DecomposerElement { decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), @@ -628,6 +642,7 @@ fn get_2q_decomposers_from_target( target: &Target, qubits: &[PhysicalQubit; 2], approximation_degree: Option, + pulse_optimize: Option, ) -> PyResult>> { let qubits: SmallVec<[PhysicalQubit; 2]> = SmallVec::from_buf(*qubits); let reverse_qubits: SmallVec<[PhysicalQubit; 2]> = qubits.iter().rev().copied().collect(); @@ -766,7 +781,7 @@ fn get_2q_decomposers_from_target( gate.operation.matrix(&gate.params).unwrap().view(), basis_2q_fidelity, basis_1q, - None, + pulse_optimize, )?; decomposers.push(DecomposerElement { diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 3b24640ab73d..455acd1bdf8e 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -472,7 +472,9 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else {} ) - if self.method == "default" and not (self._target is not None and len(self._target.operation_names) ==0): + if self.method == "default" and not ( + self._target is not None and len(self._target.operation_names) == 0 + ): _coupling_edges = ( set(self._coupling_map.get_edges()) if self._coupling_map is not None else set() ) @@ -486,6 +488,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: _coupling_edges, self._approximation_degree, self._natural_direction, + self._pulse_optimize, ) return out else: diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 05be41f0a7f1..3c9c9147a400 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -158,7 +158,7 @@ def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_d qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, pulse_optimize=True, natural_direction=natural_direction, @@ -189,7 +189,7 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): [ TrivialLayout(coupling_map), UnitarySynthesis( - basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, pulse_optimize=False, natural_direction=True, @@ -200,7 +200,7 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): [ TrivialLayout(coupling_map), UnitarySynthesis( - basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, pulse_optimize=True, natural_direction=True, @@ -213,7 +213,7 @@ def test_two_qubit_synthesis_not_pulse_optimal(self): def test_two_qubit_pulse_optimal_true_raises(self): """Verify raises if pulse optimal==True but cx is not in the basis.""" - basis_gates = ['id', 'rz', 'sx', 'x','iswap', 'reset'] + basis_gates = ["id", "rz", "sx", "x", "iswap", "reset"] # this assumes iswap pulse optimal decomposition doesn't exist qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]]) @@ -240,7 +240,7 @@ def test_two_qubit_natural_direction_true_gate_length_raises(self): qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], pulse_optimize=True, natural_direction=True, ) @@ -256,7 +256,7 @@ def test_two_qubit_pulse_optimal_none_optimal(self): qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], coupling_map=coupling_map, pulse_optimize=None, natural_direction=True, @@ -273,7 +273,7 @@ def test_two_qubit_pulse_optimal_none_optimal(self): def test_two_qubit_pulse_optimal_none_no_raise(self): """Verify pulse optimal decomposition when pulse_optimize==None doesn't raise when pulse optimal decomposition unknown.""" - basis_gates = ['id', 'rz', 'sx', 'x', 'cx', 'reset'] + basis_gates = ["id", "rz", "sx", "x", "cx", "reset"] # this assumes iswap pulse optimal decomposition doesn't exist basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates] qr = QuantumRegister(2) @@ -835,7 +835,12 @@ def test_two_qubit_synthesis_to_directional_cx_target(self, gate, natural_direct """Verify two qubit unitaries are synthesized to match basis gates.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend - backend = GenericBackendV2(num_qubits=5, basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], coupling_map=YORKTOWN_CMAP) + backend = GenericBackendV2( + num_qubits=5, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=YORKTOWN_CMAP, + seed=1, + ) coupling_map = CouplingMap(backend.coupling_map) triv_layout_pass = TrivialLayout(coupling_map) @@ -856,12 +861,19 @@ def test_two_qubit_synthesis_to_directional_cx_target(self, gate, natural_direct self.assertEqual(Operator(qc), Operator(qc_out)) @data(True, False) - def test_two_qubit_synthesis_to_directional_cx_multiple_registers_target(self, natural_direction): + def test_two_qubit_synthesis_to_directional_cx_multiple_registers_target( + self, natural_direction + ): """Verify two qubit unitaries are synthesized to match basis gates across multiple registers.""" # TODO: should make check more explicit e.g. explicitly set gate # direction in test instead of using specific fake backend - backend = GenericBackendV2(num_qubits=5, basis_gates=['id', 'rz', 'sx', 'x', 'cx', 'reset'], coupling_map=YORKTOWN_CMAP) + backend = GenericBackendV2( + num_qubits=5, + basis_gates=["id", "rz", "sx", "x", "cx", "reset"], + coupling_map=YORKTOWN_CMAP, + seed=1, + ) qr0 = QuantumRegister(1) qr1 = QuantumRegister(1) coupling_map = CouplingMap(backend.coupling_map) @@ -869,7 +881,7 @@ def test_two_qubit_synthesis_to_directional_cx_multiple_registers_target(self, n qc = QuantumCircuit(qr0, qr1) qc.unitary(random_unitary(4, seed=12), [qr0[0], qr1[0]]) unisynth_pass = UnitarySynthesis( - target = backend.target, + target=backend.target, pulse_optimize=True, natural_direction=natural_direction, ) @@ -879,16 +891,19 @@ def test_two_qubit_synthesis_to_directional_cx_multiple_registers_target(self, n def test_two_qubit_natural_direction_true_duration_fallback_target(self): """Verify fallback path when pulse_optimize==True.""" - basis_gates = ['id', 'rz', 'sx', 'x', 'cx', 'reset'] + basis_gates = ["id", "rz", "sx", "x", "cx", "reset"] qr = QuantumRegister(2) coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]]) - backend = GenericBackendV2(num_qubits=5, basis_gates=basis_gates, coupling_map=coupling_map) + + backend = GenericBackendV2( + num_qubits=5, basis_gates=basis_gates, coupling_map=coupling_map, seed=1 + ) triv_layout_pass = TrivialLayout(coupling_map) qc = QuantumCircuit(qr) qc.unitary(random_unitary(4, seed=12), [0, 1]) unisynth_pass = UnitarySynthesis( - target = backend.target, + target=backend.target, pulse_optimize=True, natural_direction=True, ) @@ -897,5 +912,7 @@ def test_two_qubit_natural_direction_true_duration_fallback_target(self): self.assertTrue( all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx"))) ) + + if __name__ == "__main__": unittest.main() From 86465a0ff8021347b2dfa22e9aea0578c532492b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 21 Jan 2025 14:08:06 +0100 Subject: [PATCH 5/7] Fix test for basis_gates None && target None, minor code cleanup --- crates/accelerate/src/unitary_synthesis.rs | 195 ++++++++---------- .../passes/synthesis/unitary_synthesis.py | 6 +- 2 files changed, 86 insertions(+), 115 deletions(-) diff --git a/crates/accelerate/src/unitary_synthesis.rs b/crates/accelerate/src/unitary_synthesis.rs index 8eca4b9b7b0f..b378b5093a1b 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -390,17 +390,22 @@ fn py_run_main_loop( } // 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( - py, - QuantumCircuitData::extract_bound(&synth_circ)?, - false, - None, - None, - )?; - let out_qargs = dag.get_qargs(packed_instr.qubits); - apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?; + if basis_gates.is_empty() && matches!(target, None) { + out_dag.push_back(py, packed_instr.clone())?; + } else { + 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( + py, + QuantumCircuitData::extract_bound(&synth_circ)?, + false, + None, + None, + )?; + let out_qargs = dag.get_qargs(packed_instr.qubits); + apply_synth_dag(py, &mut out_dag, out_qargs, &synth_dag)?; + } } } } @@ -441,6 +446,7 @@ fn run_2q_unitary_synthesis( pulse_optimize, )?; if decomposer_item.is_none() { + apply_original_op(out_dag)?; return Ok(()); }; vec![decomposer_item.unwrap()] @@ -449,16 +455,14 @@ fn run_2q_unitary_synthesis( // If there's a single decomposer, avoid computing synthesis score if decomposers.len() == 1 { let decomposer_item = decomposers.first().unwrap(); - let preferred_dir = match target { - Some(target) => preferred_direction_target( - decomposer_item, - ref_qubits, - natural_direction, - coupling_edges, - target, - )?, - None => preferred_direction_basis(ref_qubits, natural_direction, coupling_edges)?, - }; + let preferred_dir = preferred_direction( + ref_qubits, + natural_direction, + coupling_edges, + target, + decomposer_item, + )?; + match decomposer_item.decomposer { DecomposerType::TwoQubitBasisDecomposer(_) => { let synth = synth_su4_sequence( @@ -486,12 +490,12 @@ fn run_2q_unitary_synthesis( let mut synth_errors_sequence = Vec::new(); let mut synth_errors_dag = Vec::new(); for decomposer in &decomposers { - let preferred_dir = preferred_direction_target( - decomposer, + let preferred_dir = preferred_direction( ref_qubits, natural_direction, coupling_edges, - target.unwrap(), + target, + decomposer, )?; match &decomposer.decomposer { DecomposerType::TwoQubitBasisDecomposer(_) => { @@ -605,6 +609,7 @@ fn get_2q_decomposer_from_basis( Some(basis) => basis, None => return Ok(None), }; + let basis_fidelity = approximation_degree.unwrap_or(1.0); if kak_gates[0] == "rzx" { @@ -616,7 +621,7 @@ fn get_2q_decomposer_from_basis( Ok(Some(DecomposerElement { decomposer: DecomposerType::XXDecomposer(decomposer.into()), - gate: decomposer_gate.operation.standard_gate().clone(), + gate: decomposer_gate.operation.standard_gate(), params: decomposer_gate.params, })) } else if !kak_gates.is_empty() { @@ -786,7 +791,7 @@ fn get_2q_decomposers_from_target( decomposers.push(DecomposerElement { decomposer: DecomposerType::TwoQubitBasisDecomposer(Box::new(decomposer)), - gate: gate.operation.standard_gate().clone(), + gate: gate.operation.standard_gate(), params: gate.params.clone(), }); } @@ -894,7 +899,7 @@ fn get_2q_decomposers_from_target( decomposers.push(DecomposerElement { decomposer: DecomposerType::XXDecomposer(decomposer.into()), - gate: decomposer_gate.operation.standard_gate().clone(), + gate: decomposer_gate.operation.standard_gate(), params: decomposer_gate.params, }); } @@ -902,12 +907,12 @@ fn get_2q_decomposers_from_target( Ok(Some(decomposers)) } -fn preferred_direction_target( - decomposer: &DecomposerElement, +fn preferred_direction( ref_qubits: &[PhysicalQubit; 2], natural_direction: Option, coupling_edges: &HashSet<[PhysicalQubit; 2]>, - target: &Target, + target: Option<&Target>, + decomposer: &DecomposerElement, ) -> PyResult> { // Returns: // * true if gate qubits are in the hardware-native direction @@ -916,28 +921,6 @@ fn preferred_direction_target( 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.gate.name()) { - Ok(_) => match target[decomposer.gate.name()].get(Some( - &q_tuple - .into_iter() - .collect::>(), - )) { - Some(Some(_props)) => { - if lengths { - _props.duration.unwrap_or(in_cost) - } else { - _props.error.unwrap_or(in_cost) - } - } - _ => in_cost, - }, - Err(_) => in_cost, - }; - Ok(cost) - }; - let preferred_direction = match natural_direction { Some(false) => None, _ => { @@ -949,25 +932,54 @@ fn preferred_direction_target( (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)?; - } + match target { + Some(target) => { + let mut cost_0_1: f64 = f64::INFINITY; + let mut cost_1_0: f64 = f64::INFINITY; + + let compute_cost = |lengths: bool, + q_tuple: [PhysicalQubit; 2], + in_cost: f64| + -> PyResult { + let cost = + match target.qargs_for_operation_name(decomposer.gate.name()) { + Ok(_) => match target[decomposer.gate.name()].get(Some( + &q_tuple + .into_iter() + .collect::>(), + )) { + Some(Some(_props)) => { + if lengths { + _props.duration.unwrap_or(in_cost) + } else { + _props.error.unwrap_or(in_cost) + } + } + _ => in_cost, + }, + Err(_) => in_cost, + }; + Ok(cost) + }; + // 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 + } + } + None => None, } } } @@ -987,45 +999,6 @@ fn preferred_direction_target( Ok(preferred_direction) } -fn preferred_direction_basis( - ref_qubits: &[PhysicalQubit; 2], - natural_direction: Option, - coupling_edges: &HashSet<[PhysicalQubit; 2]>, -) -> 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: [PhysicalQubit; 2] = *ref_qubits; - let mut reverse_qubits: [PhysicalQubit; 2] = qubits; - reverse_qubits.reverse(); - - let preferred_direction = match natural_direction { - Some(false) => None, - _ => { - // None or Some(true) - let zero_one = coupling_edges.contains(&qubits); - let one_zero = coupling_edges.contains(&[qubits[1], qubits[0]]); - - match (zero_one, one_zero) { - (true, false) => Some(true), - (false, true) => Some(false), - _ => None, - } - } - }; - - if natural_direction == Some(true) && preferred_direction.is_none() { - 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) -} fn synth_su4_sequence( su4_mat: &Array2, decomposer_2q: &DecomposerElement, @@ -1040,7 +1013,7 @@ fn synth_su4_sequence( }; let sequence = TwoQubitUnitarySequence { gate_sequence: synth, - decomp_gate: decomposer_2q.gate.clone(), + decomp_gate: decomposer_2q.gate, decomp_params: decomposer_2q.params.clone(), }; @@ -1116,7 +1089,7 @@ fn reversed_synth_su4_sequence( reversed_synth.set_state((reversed_gates, synth.global_phase())); let sequence = TwoQubitUnitarySequence { gate_sequence: reversed_synth, - decomp_gate: decomposer_2q.gate.clone(), + decomp_gate: decomposer_2q.gate, decomp_params: decomposer_2q.params.clone(), }; Ok(sequence) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 455acd1bdf8e..08c7727255bd 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -150,6 +150,7 @@ def _decomposer_2q_from_basis_gates(basis_gates, pulse_optimize=None, approximat decomposer2q = None kak_gate = _choose_kak_gate(basis_gates) euler_basis = _choose_euler_basis(basis_gates) + basis_fidelity = approximation_degree or 1.0 if isinstance(kak_gate, RZXGate): backup_optimizer = TwoQubitBasisDecomposer( @@ -472,13 +473,10 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: else {} ) - if self.method == "default" and not ( - self._target is not None and len(self._target.operation_names) == 0 - ): + if self.method == "default": _coupling_edges = ( set(self._coupling_map.get_edges()) if self._coupling_map is not None else set() ) - out = run_main_loop( dag, list(qubit_indices.values()), From cca430dcc5d04e7f15e290699a882ca61a2bb963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Tue, 21 Jan 2025 14:28:24 +0100 Subject: [PATCH 6/7] Remove backend_properties input --- .../passes/synthesis/unitary_synthesis.py | 81 ++++--------------- .../preset_passmanagers/builtin_plugins.py | 2 - .../transpiler/preset_passmanagers/common.py | 3 - 3 files changed, 15 insertions(+), 71 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 08c7727255bd..7ff29df589fd 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -55,7 +55,6 @@ from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.dagcircuit.dagnode import DAGOpNode from qiskit.exceptions import QiskitError -from qiskit.providers.models.backendproperties import BackendProperties from qiskit.quantum_info import Operator from qiskit.synthesis.one_qubit import one_qubit_decompose from qiskit.synthesis.two_qubit.xx_decompose import XXDecomposer, XXEmbodiments @@ -321,7 +320,6 @@ def __init__( basis_gates: list[str] = None, approximation_degree: float | None = 1.0, coupling_map: CouplingMap = None, - backend_props: BackendProperties = None, pulse_optimize: bool | None = None, natural_direction: bool | None = None, synth_gates: list[str] | None = None, @@ -333,7 +331,7 @@ def __init__( """Synthesize unitaries over some basis gates. This pass can approximate 2-qubit unitaries given some - gate fidelities (either via ``backend_props`` or ``target``). + gate fidelities (via ``target``). More approximation can be forced by setting a heuristic dial ``approximation_degree``. @@ -351,8 +349,6 @@ def __init__( directionality of the coupling_map will be taken into account if ``pulse_optimize`` is ``True``/``None`` and ``natural_direction`` is ``True``/``None``. - backend_props (BackendProperties): Properties of a backend to - synthesize for (e.g. gate fidelities). pulse_optimize (bool): Whether to optimize pulses during synthesis. A value of ``None`` will attempt it but fall back if it does not succeed. A value of ``True`` will raise @@ -364,7 +360,7 @@ def __init__( coupling map is unidirectional. If there is no coupling map or the coupling map is bidirectional, the gate direction with the shorter - duration from the backend properties will be used. If + duration from the target properties will be used. If set to True, and a natural direction can not be determined, raises :class:`.TranspilerError`. If set to None, no exception will be raised if a natural direction can @@ -384,7 +380,7 @@ def __init__( your unitary synthesis plugin on how to use this. target: The optional :class:`~.Target` for the target device the pass is compiling for. If specified this will supersede the values - set for ``basis_gates``, ``coupling_map``, and ``backend_props``. + set for ``basis_gates`` and ``coupling_map``. Raises: TranspilerError: if ``method`` was specified but is not found in the @@ -400,7 +396,6 @@ def __init__( if method != "default": self.plugins = plugin.UnitarySynthesisPluginManager() self._coupling_map = coupling_map - self._backend_props = backend_props self._pulse_optimize = pulse_optimize self._natural_direction = natural_direction self._plugin_config = plugin_config @@ -498,23 +493,19 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if method.supports_pulse_optimize: kwargs["pulse_optimize"] = self._pulse_optimize if method.supports_gate_lengths: - _gate_lengths = _gate_lengths or _build_gate_lengths( - self._backend_props, self._target - ) + _gate_lengths = _gate_lengths or _build_gate_lengths(self._target) kwargs["gate_lengths"] = _gate_lengths if method.supports_gate_errors: - _gate_errors = _gate_errors or _build_gate_errors( - self._backend_props, self._target - ) + _gate_errors = _gate_errors or _build_gate_errors(self._target) kwargs["gate_errors"] = _gate_errors if method.supports_gate_lengths_by_qubit: _gate_lengths_by_qubit = _gate_lengths_by_qubit or _build_gate_lengths_by_qubit( - self._backend_props, self._target + self._target ) kwargs["gate_lengths_by_qubit"] = _gate_lengths_by_qubit if method.supports_gate_errors_by_qubit: _gate_errors_by_qubit = _gate_errors_by_qubit or _build_gate_errors_by_qubit( - self._backend_props, self._target + self._target ) kwargs["gate_errors_by_qubit"] = _gate_errors_by_qubit supported_bases = method.supported_bases @@ -612,9 +603,8 @@ def _run_main_loop( return out_dag -def _build_gate_lengths(props=None, target=None): - """Builds a ``gate_lengths`` dictionary from either ``props`` (BackendV1) - or ``target`` (BackendV2). +def _build_gate_lengths(target=None): + """Builds a ``gate_lengths`` dictionary from ``target`` (BackendV2). The dictionary has the form: {gate_name: {(qubits,): duration}} @@ -626,21 +616,11 @@ def _build_gate_lengths(props=None, target=None): for qubit, gate_props in prop_dict.items(): if gate_props is not None and gate_props.duration is not None: gate_lengths[gate][qubit] = gate_props.duration - elif props is not None: - for gate in props._gates: - gate_lengths[gate] = {} - for k, v in props._gates[gate].items(): - length = v.get("gate_length") - if length: - gate_lengths[gate][k] = length[0] - if not gate_lengths[gate]: - del gate_lengths[gate] return gate_lengths -def _build_gate_errors(props=None, target=None): - """Builds a ``gate_error`` dictionary from either ``props`` (BackendV1) - or ``target`` (BackendV2). +def _build_gate_errors(target=None): + """Builds a ``gate_error`` dictionary from ``target`` (BackendV2). The dictionary has the form: {gate_name: {(qubits,): error_rate}} @@ -652,22 +632,12 @@ def _build_gate_errors(props=None, target=None): for qubit, gate_props in prop_dict.items(): if gate_props is not None and gate_props.error is not None: gate_errors[gate][qubit] = gate_props.error - if props is not None: - for gate in props._gates: - gate_errors[gate] = {} - for k, v in props._gates[gate].items(): - error = v.get("gate_error") - if error: - gate_errors[gate][k] = error[0] - if not gate_errors[gate]: - del gate_errors[gate] return gate_errors -def _build_gate_lengths_by_qubit(props=None, target=None): +def _build_gate_lengths_by_qubit(target=None): """ - Builds a `gate_lengths` dictionary from either `props` (BackendV1) - or `target (BackendV2)`. + Builds a `gate_lengths` dictionary from `target (BackendV2)`. The dictionary has the form: {(qubits): [Gate, duration]} @@ -684,23 +654,12 @@ def _build_gate_lengths_by_qubit(props=None, target=None): operation_and_durations.append((operation, duration)) if operation_and_durations: gate_lengths[qubits] = operation_and_durations - elif props is not None: - for gate_name, gate_props in props._gates.items(): - gate = GateNameToGate[gate_name] - for qubits, properties in gate_props.items(): - duration = properties.get("gate_length", [0.0])[0] - operation_and_durations = (gate, duration) - if qubits in gate_lengths: - gate_lengths[qubits].append(operation_and_durations) - else: - gate_lengths[qubits] = [operation_and_durations] return gate_lengths -def _build_gate_errors_by_qubit(props=None, target=None): +def _build_gate_errors_by_qubit(target=None): """ - Builds a `gate_error` dictionary from either `props` (BackendV1) - or `target (BackendV2)`. + Builds a `gate_error` dictionary from `target (BackendV2)`. The dictionary has the form: {(qubits): [Gate, error]} @@ -717,16 +676,6 @@ def _build_gate_errors_by_qubit(props=None, target=None): operation_and_errors.append((operation, error)) if operation_and_errors: gate_errors[qubits] = operation_and_errors - elif props is not None: - for gate_name, gate_props in props._gates.items(): - gate = GateNameToGate[gate_name] - for qubits, properties in gate_props.items(): - error = properties.get("gate_error", [0.0])[0] - operation_and_errors = (gate, error) - if qubits in gate_errors: - gate_errors[qubits].append(operation_and_errors) - else: - gate_errors[qubits] = [operation_and_errors] return gate_errors diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index 1e5ca4519864..2dfc683c99ae 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -606,7 +606,6 @@ def _opt_control(property_set): pass_manager_config.basis_gates, approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, method=pass_manager_config.unitary_synthesis_method, plugin_config=pass_manager_config.unitary_synthesis_plugin_config, target=pass_manager_config.target, @@ -653,7 +652,6 @@ def _unroll_condition(property_set): pass_manager_config.basis_gates, approximation_degree=pass_manager_config.approximation_degree, coupling_map=pass_manager_config.coupling_map, - backend_props=pass_manager_config.backend_properties, method=pass_manager_config.unitary_synthesis_method, plugin_config=pass_manager_config.unitary_synthesis_plugin_config, target=pass_manager_config.target, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 431089f657dd..aef77d285baf 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -465,7 +465,6 @@ def generate_translation_passmanager( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, target=target, @@ -489,7 +488,6 @@ def generate_translation_passmanager( basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, min_qubits=3, @@ -514,7 +512,6 @@ def generate_translation_passmanager( basis_gates=basis_gates, approximation_degree=approximation_degree, coupling_map=coupling_map, - backend_props=backend_props, plugin_config=unitary_synthesis_plugin_config, method=unitary_synthesis_method, target=target, From dce1419951c32d920ef8bef550df7e6c0a747cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= Date: Wed, 22 Jan 2025 10:53:57 +0100 Subject: [PATCH 7/7] Access vec items properly --- 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 b378b5093a1b..e3ac603b1a91 100644 --- a/crates/accelerate/src/unitary_synthesis.rs +++ b/crates/accelerate/src/unitary_synthesis.rs @@ -612,7 +612,7 @@ fn get_2q_decomposer_from_basis( let basis_fidelity = approximation_degree.unwrap_or(1.0); - if kak_gates[0] == "rzx" { + if matches!(kak_gates.first(), Some(&"rzx")) { let xx_decomposer: &Bound<'_, PyAny> = imports::XX_DECOMPOSER.get_bound(py); let decomposer = xx_decomposer.call1((PyString::new_bound(py, euler_basis),))?; let decomposer_gate = decomposer