diff --git a/crates/transpiler/src/passes/unitary_synthesis/decomposers.rs b/crates/transpiler/src/passes/unitary_synthesis/decomposers.rs index f6a9db9669af..d351d2d0fa34 100644 --- a/crates/transpiler/src/passes/unitary_synthesis/decomposers.rs +++ b/crates/transpiler/src/passes/unitary_synthesis/decomposers.rs @@ -38,8 +38,8 @@ use super::{ Approximation, DecompositionDirection2q, NormalizedFidelity, QpuConstraint, QpuConstraintKind, UnitarySynthesisConfig, UsePulseOptimizer, }; -use crate::QiskitError; use crate::passes::optimize_clifford_t::CLIFFORD_T_GATE_NAMES; +use crate::passes::unitary_synthesis::UnitarySynthesisError; use crate::target::{NormalOperation, Target, TargetOperation}; use qiskit_circuit::circuit_data::{CircuitData, PyCircuitData}; use qiskit_circuit::instruction::Instruction; @@ -481,7 +481,8 @@ impl DecomposerCache { qubits: [PhysicalQubit; 2], config: &UnitarySynthesisConfig, constraint: QpuConstraint, - ) -> PyResult> { + ) -> Result, UnitarySynthesisError> + { // We can't use `Entry::or_insert_with` because our creator function is fallible and we // might have to propagate its error. let entry = match self.decomposers_2q.entry(qubits) { @@ -520,7 +521,7 @@ fn get_2q_decomposers( qubits: [PhysicalQubit; 2], config: &UnitarySynthesisConfig, constraint: QpuConstraint, -) -> PyResult> { +) -> Result, UnitarySynthesisError> { let choose_flip = |direction: AllowedDirection2q, constructor: &Decomposer2qConstructor| -> FlipDirection { match direction { @@ -561,13 +562,7 @@ fn get_2q_decomposers( DecompositionDirection2q::UniquelyBestValid if direction == AllowedDirection2q::Both => { - 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 - ))); + return Err(UnitarySynthesisError::NoPreferredDirection(qubits)); } DecompositionDirection2q::UniquelyBestValid | DecompositionDirection2q::BestValid => direction, @@ -607,11 +602,10 @@ fn get_2q_decomposers( gate: kak_gate.into(), params: smallvec![], }; - let fidelity = config.approximation.synthesis_fidelity(0.0).map_err(|e| { - PyValueError::new_err(format!( - "requested synthesis fidelity is out of range: {e}" - )) - })?; + let fidelity = config + .approximation + .synthesis_fidelity(0.0) + .map_err(UnitarySynthesisError::SynthesisFidelityOutOfRange)?; let constructor = Decomposer2qConstructor::StaticKak(StaticKakConstructor { source, euler, @@ -705,11 +699,7 @@ fn get_2q_decomposers( let fidelity = config .approximation .synthesis_fidelity(candidate.error) - .map_err(|e| { - PyValueError::new_err(format!( - "requested synthesis fidelity is out of range: {e}" - )) - })?; + .map_err(UnitarySynthesisError::SynthesisFidelityOutOfRange)?; // TODO: the 2q decomposers internally already do everything that's needed to handle // _all_ of the 1q bases simultaneously without further decompositions, but don't // expose that functionality. This wastes huge amounts of time and needs a fix. @@ -742,6 +732,7 @@ fn get_2q_decomposers( get_xx_decomposers(py, cache, &euler_bases, &candidates_2q, config) }) .map(|maybe| maybe.into_iter().collect()) + .map_err(UnitarySynthesisError::XXDecomposer) } else { Ok(Default::default()) } diff --git a/crates/transpiler/src/passes/unitary_synthesis/mod.rs b/crates/transpiler/src/passes/unitary_synthesis/mod.rs index ee0dc9865deb..980351ca21f3 100644 --- a/crates/transpiler/src/passes/unitary_synthesis/mod.rs +++ b/crates/transpiler/src/passes/unitary_synthesis/mod.rs @@ -17,6 +17,7 @@ use indexmap::IndexSet; use nalgebra::Matrix2; use ndarray::prelude::*; use num_complex::Complex64; +use pyo3::exceptions::PyValueError; use std::hash; use numpy::PyReadonlyArray2; @@ -29,18 +30,59 @@ use self::decomposers::{Decomposer2q, DecomposerCache, FlipDirection}; use crate::QiskitError; use crate::target::Target; use qiskit_circuit::bit::QuantumRegister; -use qiskit_circuit::dag_circuit::{DAGCircuit, DAGCircuitBuilder}; +use qiskit_circuit::dag_circuit::{DAGCircuit, DAGCircuitBuilder, DAGError}; use qiskit_circuit::instruction::Parameters; use qiskit_circuit::operations::{Operation, OperationRef, Param, PythonOperation, StandardGate}; use qiskit_circuit::packed_instruction::{PackedInstruction, PackedOperation}; use qiskit_circuit::{BlocksMode, PhysicalQubit, Qubit, VarsMode}; use qiskit_synthesis::euler_one_qubit_decomposer::unitary_to_gate_sequence_inner; -use qiskit_synthesis::qsd::quantum_shannon_decomposition; +use qiskit_synthesis::qsd::{QSDError, quantum_shannon_decomposition}; use qiskit_synthesis::two_qubit_decompose::TwoQubitGateSequence; #[cfg(feature = "cache_pygates")] use std::sync::OnceLock; +/// Errors that can be thrown by UnitarySynthesis +#[derive(Debug, thiserror::Error)] +pub enum UnitarySynthesisError { + #[error( + "No preferred direction of gate on qubits {0:?} could be determined from coupling map or gate lengths / gate errors." + )] + NoPreferredDirection([PhysicalQubit; 2]), + #[error("requested synthesis fidelity is out of range: {0}")] + SynthesisFidelityOutOfRange(f64), + #[error(transparent)] + QSD(#[from] QSDError), + #[error(transparent)] + DAGCircuit(#[from] DAGError), + // TODO: Remove once XXDecomposer is in Rust. + #[error(transparent)] + XXDecomposer(PyErr), + // TODO: Replace with Rust native versions of errors for Decomposer2q + #[error(transparent)] + Decomposer2q(PyErr), + #[error(transparent)] + PyGate(PyErr), +} + +impl From for PyErr { + fn from(value: UnitarySynthesisError) -> Self { + match value { + UnitarySynthesisError::NoPreferredDirection(_) => { + QiskitError::new_err(value.to_string()) + } + UnitarySynthesisError::SynthesisFidelityOutOfRange(_) => { + PyValueError::new_err(value.to_string()) + } + UnitarySynthesisError::DAGCircuit(error) => error.into(), + UnitarySynthesisError::XXDecomposer(py_err) => py_err, + UnitarySynthesisError::QSD(qsderror) => qsderror.into(), + UnitarySynthesisError::Decomposer2q(py_err) => py_err, + UnitarySynthesisError::PyGate(py_err) => py_err, + } + } +} + /// The fidelity of the 2q basis gate used in a decomposer. /// /// This is "normalised" in the sense that the value is guaranteed (when constructed safely) to be @@ -309,14 +351,14 @@ pub fn run_unitary_synthesis( qubit_indices: &[PhysicalQubit], state: &mut UnitarySynthesisState, constraint: QpuConstraint, -) -> PyResult> { +) -> Result, UnitarySynthesisError> { // This method is the actual distribution logic of unitary synthesis, but there are several // paths through it that return `Ok(false)`, meaning "no error and no synthesis needed", so the // caller is responsible for propagating the old instruction through to wherever is necessary. let synthesize_onto = |out: &mut DAGCircuitBuilder, state: &mut UnitarySynthesisState, inst: &PackedInstruction| - -> PyResult { + -> Result { if !(synth_gates.contains(inst.op.name()) && inst.op.num_qubits() >= min_qubits as u32) { return Ok(false); } @@ -381,7 +423,7 @@ pub fn run_unitary_synthesis( } }) }) - .collect::>()?; + .collect::>()?; out.push_back(PackedInstruction::from_control_flow( inst.op.control_flow().clone(), blocks, @@ -401,7 +443,7 @@ fn synthesize_matrix_onto( qubits_local: &[Qubit], state: &mut UnitarySynthesisState, constraint: QpuConstraint, -) -> PyResult { +) -> Result { let num_qubits = qubits_local.len(); debug_assert_eq!(unitary.shape(), &[1 << num_qubits, 1 << num_qubits]); match *qubits_local { @@ -445,7 +487,7 @@ fn synthesize_1q_matrix_onto( qubit_virt: Qubit, state: &mut UnitarySynthesisState, constraint: QpuConstraint, -) -> PyResult { +) -> Result { // TODO: we possibly want to invert this logic and do Euler synthesis if possible, and SK only // if we have to. If nothing else, it simplifies the logic of `try_solovay_kitaev` - instead of // "is this _only_ Clifford+T?" it can become "does this permit a Clifford+T decomposition", @@ -581,7 +623,7 @@ pub(crate) fn synthesize_2q_matrix( state: &mut UnitarySynthesisState, constraint: QpuConstraint, mut fidelity_calculation: F, -) -> PyResult>> +) -> Result>, UnitarySynthesisError> where F: FnMut(&Direction2q, &TwoQubitGateSequence, &QpuConstraint, [PhysicalQubit; 2]) -> S, S: PartialOrd, @@ -601,14 +643,15 @@ where }; let mut sequences = decomposer_cache .get_2q(qargs_phys, config, constraint)? - .map(|(decomposer, flip)| -> PyResult<_> { + .map(|(decomposer, flip)| -> Result<_, UnitarySynthesisError> { match flip { FlipDirection::No => single_decomposition(decomposer, Direction2q::Forwards) .map(|seq| (Direction2q::Forwards, seq)), FlipDirection::Yes => single_decomposition(decomposer, Direction2q::Backwards) .map(|seq| (Direction2q::Backwards, seq)), FlipDirection::Ensure(dir) => { - let normal = single_decomposition(decomposer, Direction2q::Forwards)?; + let normal = single_decomposition(decomposer, Direction2q::Forwards) + .map_err(UnitarySynthesisError::Decomposer2q)?; if normal .gates() .iter() @@ -626,6 +669,7 @@ where } } } + .map_err(UnitarySynthesisError::Decomposer2q) }); let Some(first) = sequences.next().transpose()? else { @@ -671,7 +715,7 @@ fn synthesize_2q_matrix_onto( qargs_virt: [Qubit; 2], state: &mut UnitarySynthesisState, constraint: QpuConstraint, -) -> PyResult { +) -> Result { let Some(result) = synthesize_2q_matrix(unitary, qargs_phys, state, constraint, fidelity_2q_sequence)? else { @@ -701,7 +745,8 @@ fn synthesize_2q_matrix_onto( let inst = inst.py_copy(py)?; inst.ob.setattr(py, intern!(py, "params"), params)?; Ok(inst.into()) - })?, + }) + .map_err(UnitarySynthesisError::PyGate)?, _ => panic!("internal logic error: decomposed sequence contains a non-gate"), }; let params = (!params.is_empty()).then(|| { @@ -779,6 +824,7 @@ pub fn py_unitary_synthesis( &mut state, constraint, ) + .map_err(Into::into) } #[allow(clippy::too_many_arguments)]