Skip to content

feat: Define a wire tracker for the new pytket decoder #1036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 15, 2025
Merged
Show file tree
Hide file tree
Changes from 11 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
6 changes: 6 additions & 0 deletions tket-py/src/circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ create_py_exception!(
"Error type for the conversion between tket and tket1 operations."
);

create_py_exception!(
tket::serialize::pytket::PytketDecodeError,
PyTK1DecodeError,
"Error type for the conversion between tket1 and tket operations."
);

/// Run the validation checks on a circuit.
#[pyfunction]
pub fn validate_circuit(c: &Bound<PyAny>) -> PyResult<()> {
Expand Down
267 changes: 267 additions & 0 deletions tket/src/serialize/pytket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use tket_json_rs::circuit_json::SerialCircuit;
use tket_json_rs::register::{Bit, ElementId, Qubit};

use crate::circuit::Circuit;
use crate::serialize::pytket::extension::RegisterCount;

use self::decoder::Tk1DecoderContext;
pub use crate::passes::pytket::lower_to_pytket;
Expand Down Expand Up @@ -279,6 +280,272 @@ impl<N> PytketEncodeError<N> {
}
}

/// Error type for conversion between tket2 ops and pytket operations.
#[derive(derive_more::Debug, Display, Error, Clone)]
#[non_exhaustive]
#[display(
"{inner}{context}",
context = {
match (pytket_op, hugr_op) {
(Some(pytket_op), Some(hugr_op)) => format!(". While decoding a pytket {pytket_op} as a hugr {hugr_op}"),
(Some(pytket_op), None) => format!(". While decoding a pytket {pytket_op}"),
(None, Some(hugr_op)) => format!(". While decoding a hugr {hugr_op}"),
(None, None) => String::new(),
}
},
)]
pub struct PytketDecodeError {
/// The kind of error.
pub inner: PytketDecodeErrorInner,
/// The pytket operation that caused the error, if applicable.
pub pytket_op: Option<String>,
/// The hugr operation that caused the error, if applicable.
pub hugr_op: Option<String>,
}

impl PytketDecodeError {
/// Create a new error with a custom message.
pub fn custom(msg: impl ToString) -> Self {
PytketDecodeErrorInner::CustomError {
msg: msg.to_string(),
}
.into()
}

/// Create an error for an unknown qubit register.
pub fn unknown_qubit_reg(register: &tket_json_rs::register::ElementId) -> Self {
PytketDecodeErrorInner::UnknownQubitRegister {
register: register.to_string(),
}
.into()
}

/// Create an error for an unknown bit register.
pub fn unknown_bit_reg(register: &tket_json_rs::register::ElementId) -> Self {
PytketDecodeErrorInner::UnknownBitRegister {
register: register.to_string(),
}
.into()
}

/// Add the pytket operation name to the error.
pub fn pytket_op(mut self, op: &tket_json_rs::OpType) -> Self {
self.pytket_op = Some(format!("{op:?}"));
self
}

/// Add the hugr operation name to the error.
pub fn hugr_op(mut self, op: impl ToString) -> Self {
self.hugr_op = Some(op.to_string());
self
}
}

impl From<PytketDecodeErrorInner> for PytketDecodeError {
fn from(inner: PytketDecodeErrorInner) -> Self {
Self {
inner,
pytket_op: None,
hugr_op: None,
}
}
}

/// Error variants of [`PytketDecodeError`], signalling errors during the
/// conversion between tket2 ops and pytket operations.
#[derive(derive_more::Debug, Display, Error, Clone)]
#[non_exhaustive]
pub enum PytketDecodeErrorInner {
/// The pytket circuit uses multi-indexed registers.
//
// This could be supported in the future, if there is a need for it.
#[display("Register {register} in the circuit has multiple indices. Tket2 does not support multi-indexed registers")]
MultiIndexedRegister {
/// The register name.
register: String,
},
/// Found an unexpected register name.
#[display("Found an unknown qubit register name: {register}")]
UnknownQubitRegister {
/// The unknown register name.
register: String,
},
/// Found an unexpected bit register name.
#[display("Found an unknown bit register name: {register}")]
UnknownBitRegister {
/// The unknown register name.
register: String,
},
/// The given signature to use for the HUGR's input wires is not compatible with the number of qubits and bits in the pytket circuit.
///
/// The expected number of qubits and bits may be different depending on the [`PytketTypeTranslator`][extension::PytketTypeTranslator]s used in the decoder config.
#[display(
"The given input types {input_types} to use for the HUGR's input wires are not compatible with the number of qubits and bits in the pytket circuit. Expected {expected_qubits} qubits and {expected_bits} bits, but found {circ_qubits} qubits and {circ_bits} bits",
input_types = input_types.iter().join(", "),
)]
InvalidInputSignature {
/// The given input types.
input_types: Vec<String>,
/// The expected number of qubits in the signature.
expected_qubits: usize,
/// The expected number of bits in the signature.
expected_bits: usize,
/// The number of qubits in the pytket circuit.
circ_qubits: usize,
/// The number of bits in the pytket circuit.
circ_bits: usize,
},
/// The signature to use for the HUGR's output wires is not compatible with the number of qubits and bits in the pytket circuit.
///
/// We don't do any kind of type conversion, so this depends solely on the last operation to update each register.
#[display(
"The expected output types {expected_types} are not compatible with the actual output types {actual_types}, obtained from decoding the pytket circuit",
expected_types = expected_types.iter().join(", "),
actual_types = actual_types.iter().join(", "),
)]
InvalidOutputSignature {
/// The expected types of the input wires.
expected_types: Vec<String>,
/// The actual types of the input wires.
actual_types: Vec<String>,
},
/// A pytket operation had some input registers that couldn't be mapped to hugr wires.
//
// Some of this errors will be avoided in the future once we are able to decompose complex types automatically.
#[display(
"Could not find a wire with the required qubit arguments [{qubit_args:?}] and bit arguments [{bit_args:?}]",
qubit_args = qubit_args.iter().join(", "),
bit_args = bit_args.iter().join(", "),
)]
ArgumentCouldNotBeMapped {
/// The qubit arguments that couldn't be mapped.
qubit_args: Vec<String>,
/// The bit arguments that couldn't be mapped.
bit_args: Vec<String>,
},
/// Found an unexpected number of input wires when decoding an operation.
#[display(
"Expected {expected_values} input value wires{expected_types} and {expected_params} input parameters, but found {actual_values} values{actual_types} and {actual_params} parameters",
expected_types = match expected_types {
None => "".to_string(),
Some(tys) => format!(" with types [{}]", tys.iter().join(", ")),
},
actual_types = match actual_types {
None => "".to_string(),
Some(tys) => format!(" with types [{}]", tys.iter().join(", ")),
},
)]
UnexpectedInputWires {
/// The expected amount of input wires.
expected_values: usize,
/// The expected amount of input parameters.
expected_params: usize,
/// The actual amount of input wires.
actual_values: usize,
/// The actual amount of input parameters.
actual_params: usize,
/// The expected types of the input wires.
expected_types: Option<Vec<String>>,
/// The actual types of the input wires.
actual_types: Option<Vec<String>>,
},
/// Found an unexpected input type when decoding an operation.
#[display(
"Found an unexpected type {unknown_type} in the input wires, in input signature ({all_types})",
all_types = all_types.iter().join(", "),
)]
UnexpectedInputType {
/// The unknown type.
unknown_type: String,
/// All the input types specified for the operation.
all_types: Vec<String>,
},
/// Tried to track the output wires of a node, but the number of tracked elements didn't match the ones in the output wires.
#[display(
"Tried to track the output wires of a node, but the number of tracked elements didn't match the ones in the output wires. Expected {expected_qubits} qubits and {expected_bits} bits, but found {circ_qubits} qubits and {circ_bits} bits in the node outputs"
)]
UnexpectedNodeOutput {
/// The expected number of qubits.
expected_qubits: usize,
/// The expected number of bits.
expected_bits: usize,
/// The number of qubits in HUGR node outputs.
circ_qubits: usize,
/// The number of bits in HUGR node output.
circ_bits: usize,
},
/// Custom user-defined error raised while encoding an operation.
#[display("Error while decoding operation: {msg}")]
CustomError {
/// The custom error message
msg: String,
},
/// Input parameter was defined multiple times.
#[display("Parameter {param} was defined multiple times in the input signature")]
DuplicatedParameter {
/// The parameter name.
param: String,
},
/// Not enough parameter names given for the input signature.
#[display("Tried to initialize a pytket circuit decoder with {num_params_given} given parameter names, but more were required by the input signature")]
MissingParametersInInput {
/// The number of parameters given.
num_params_given: usize,
},
/// We don't support complex types containing parameters in the input.
//
// This restriction may be relaxed in the future.
#[display("Complex type {ty} contains {num_params} inside it. We only support input parameters in standalone 'float' or 'rotation'-typed wires")]
UnsupportedParametersInInput {
/// The type that contains the parameters.
ty: String,
/// The number of parameters in the type.
num_params: usize,
},
/// We couldn't find a wire that contains the required type.
#[display(
"Could not find a wire with type {ty} that contains {expected_arguments}",
expected_arguments = match (qubit_args.is_empty(), bit_args.is_empty()) {
(true, true) => "no arguments".to_string(),
(true, false) => format!("pytket bit arguments [{}]", bit_args.iter().join(", ")),
(false, true) => format!("pytket qubit arguments [{}]", qubit_args.iter().join(", ")),
(false, false) => format!("pytket qubit and bit arguments [{}] and [{}]", qubit_args.iter().join(", "), bit_args.iter().join(", ")),
},
)]
NoMatchingWire {
/// The type that couldn't be found.
ty: String,
/// The qubit registers expected in the wire.
qubit_args: Vec<String>,
/// The bit registers expected in the wire.
bit_args: Vec<String>,
},
/// The number of pytket registers expected for an operation is not enough.
///
/// This is usually caused by a mismatch between the input signature and the number of registers in the pytket circuit.
///
/// The expected number of registers may be different depending on the [`PytketTypeTranslator`][extension::PytketTypeTranslator]s used in the decoder config.
#[display(
"Expected {expected_count} to map types ({expected_types}), but only got {actual_count}",
expected_types = expected_types.iter().join(", "),
)]
NotEnoughPytketRegisters {
/// The types we tried to get wires for.
expected_types: Vec<String>,
/// The number of registers required by the types.
expected_count: RegisterCount,
/// The number of registers we actually got.
actual_count: RegisterCount,
},
}

impl PytketDecodeErrorInner {
/// Wrap the error in a [`PytketDecodeError`].
pub fn wrap(self) -> PytketDecodeError {
PytketDecodeError::from(self)
}
}

/// A hashed register, used to identify registers in the [`Tk1Decoder::register_wire`] map,
/// avoiding string and vector clones on lookup.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
Expand Down
9 changes: 9 additions & 0 deletions tket/src/serialize/pytket/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

mod op;
mod param;
mod tracked_elem;
mod wires;

#[expect(
unused_imports,
reason = "Temporarily unused while we refactor the pytket decoder"
)]
pub use param::{LoadedParameter, LoadedParameterType};
pub use tracked_elem::{TrackedBit, TrackedQubit};
#[expect(
unused_imports,
reason = "Temporarily unused while we refactor the pytket decoder"
)]
pub use wires::{TrackedWires, WireData, WireTracker};

use std::collections::{HashMap, HashSet};

Expand All @@ -19,6 +27,7 @@ use hugr::ops::{OpType, Value};
use hugr::std_extensions::arithmetic::float_types::ConstF64;
use hugr::types::Signature;
use hugr::{Hugr, Wire};
use tracked_elem::{TrackedBitId, TrackedQubitId};

use indexmap::IndexMap;
use itertools::{EitherOrBoth, Itertools};
Expand Down
4 changes: 0 additions & 4 deletions tket/src/serialize/pytket/decoder/param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ impl LoadedParameter {
}

/// Returns the hugr type for the parameter.
#[expect(
dead_code,
reason = "Temporarily unused while we refactor the pytket decoder"
)]
pub fn wire_type(&self) -> &Type {
static FLOAT_TYPE: LazyLock<Type> = LazyLock::new(float64_type);
static ROTATION_TYPE: LazyLock<Type> = LazyLock::new(rotation_type);
Expand Down
Loading
Loading