Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 11 additions & 20 deletions crates/transpiler/src/passes/unitary_synthesis/decomposers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -481,7 +481,8 @@ impl DecomposerCache {
qubits: [PhysicalQubit; 2],
config: &UnitarySynthesisConfig,
constraint: QpuConstraint,
) -> PyResult<impl ExactSizeIterator<Item = (&Decomposer2q, FlipDirection)>> {
) -> Result<impl ExactSizeIterator<Item = (&Decomposer2q, FlipDirection)>, 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) {
Expand Down Expand Up @@ -520,7 +521,7 @@ fn get_2q_decomposers(
qubits: [PhysicalQubit; 2],
config: &UnitarySynthesisConfig,
constraint: QpuConstraint,
) -> PyResult<Vec<(usize, FlipDirection)>> {
) -> Result<Vec<(usize, FlipDirection)>, UnitarySynthesisError> {
let choose_flip =
|direction: AllowedDirection2q, constructor: &Decomposer2qConstructor| -> FlipDirection {
match direction {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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())
}
Expand Down
70 changes: 58 additions & 12 deletions crates/transpiler/src/passes/unitary_synthesis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<UnitarySynthesisError> for PyErr {
fn from(value: UnitarySynthesisError) -> Self {
match value {
UnitarySynthesisError::NoPreferredDirection(_) => {
QiskitError::new_err(value.to_string())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using QiskitError for one and PyValueError for the other?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how the original error was.

This is what it looked like for NoPreferredDirection:

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
)));

This is for SynthesisFidelityOutOfRange

}
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
Expand Down Expand Up @@ -309,14 +351,14 @@ pub fn run_unitary_synthesis(
qubit_indices: &[PhysicalQubit],
state: &mut UnitarySynthesisState,
constraint: QpuConstraint,
) -> PyResult<Option<DAGCircuit>> {
) -> Result<Option<DAGCircuit>, 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<bool> {
-> Result<bool, UnitarySynthesisError> {
if !(synth_gates.contains(inst.op.name()) && inst.op.num_qubits() >= min_qubits as u32) {
return Ok(false);
}
Expand Down Expand Up @@ -381,7 +423,7 @@ pub fn run_unitary_synthesis(
}
})
})
.collect::<PyResult<_>>()?;
.collect::<Result<_, UnitarySynthesisError>>()?;
out.push_back(PackedInstruction::from_control_flow(
inst.op.control_flow().clone(),
blocks,
Expand All @@ -401,7 +443,7 @@ fn synthesize_matrix_onto(
qubits_local: &[Qubit],
state: &mut UnitarySynthesisState,
constraint: QpuConstraint,
) -> PyResult<bool> {
) -> Result<bool, UnitarySynthesisError> {
let num_qubits = qubits_local.len();
debug_assert_eq!(unitary.shape(), &[1 << num_qubits, 1 << num_qubits]);
match *qubits_local {
Expand Down Expand Up @@ -445,7 +487,7 @@ fn synthesize_1q_matrix_onto(
qubit_virt: Qubit,
state: &mut UnitarySynthesisState,
constraint: QpuConstraint,
) -> PyResult<bool> {
) -> Result<bool, UnitarySynthesisError> {
// 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",
Expand Down Expand Up @@ -581,7 +623,7 @@ pub(crate) fn synthesize_2q_matrix<F, S>(
state: &mut UnitarySynthesisState,
constraint: QpuConstraint,
mut fidelity_calculation: F,
) -> PyResult<Option<TwoQSynthesisResult<S>>>
) -> Result<Option<TwoQSynthesisResult<S>>, UnitarySynthesisError>
where
F: FnMut(&Direction2q, &TwoQubitGateSequence, &QpuConstraint, [PhysicalQubit; 2]) -> S,
S: PartialOrd,
Expand All @@ -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()
Expand All @@ -626,6 +669,7 @@ where
}
}
}
.map_err(UnitarySynthesisError::Decomposer2q)
});

let Some(first) = sequences.next().transpose()? else {
Expand Down Expand Up @@ -671,7 +715,7 @@ fn synthesize_2q_matrix_onto(
qargs_virt: [Qubit; 2],
state: &mut UnitarySynthesisState,
constraint: QpuConstraint,
) -> PyResult<bool> {
) -> Result<bool, UnitarySynthesisError> {
let Some(result) =
synthesize_2q_matrix(unitary, qargs_phys, state, constraint, fidelity_2q_sequence)?
else {
Expand Down Expand Up @@ -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(|| {
Expand Down Expand Up @@ -779,6 +824,7 @@ pub fn py_unitary_synthesis(
&mut state,
constraint,
)
.map_err(Into::into)
}

#[allow(clippy::too_many_arguments)]
Expand Down
Loading