diff --git a/tket2-py/src/circuit.rs b/tket2-py/src/circuit.rs index 49bd6905..1b066599 100644 --- a/tket2-py/src/circuit.rs +++ b/tket2-py/src/circuit.rs @@ -25,8 +25,8 @@ use crate::utils::ConvertPyErr; pub use self::convert::{try_update_hugr, try_with_hugr, update_hugr, with_hugr, CircuitType}; pub use self::cost::PyCircuitCost; +use self::tk2circuit::Dfg; pub use self::tk2circuit::Tk2Circuit; -use self::tk2circuit::{into_vec, Dfg}; pub use tket2::{Pauli, Tk2Op}; /// The module definition @@ -38,13 +38,10 @@ pub fn module(py: Python<'_>) -> PyResult> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_function(wrap_pyfunction!(validate_hugr, &m)?)?; - m.add_function(wrap_pyfunction!(to_hugr_dot, &m)?)?; - m.add_function(wrap_pyfunction!(to_hugr_mermaid, &m)?)?; + m.add_function(wrap_pyfunction!(validate_circuit, &m)?)?; + m.add_function(wrap_pyfunction!(render_circuit_dot, &m)?)?; + m.add_function(wrap_pyfunction!(render_circuit_mermaid, &m)?)?; m.add("HugrError", py.get_type_bound::())?; m.add("BuildError", py.get_type_bound::())?; @@ -90,19 +87,19 @@ create_py_exception!( /// Run the validation checks on a circuit. #[pyfunction] -pub fn validate_hugr(c: &Bound) -> PyResult<()> { +pub fn validate_circuit(c: &Bound) -> PyResult<()> { try_with_hugr(c, |hugr, _| hugr.validate(®ISTRY)) } /// Return a Graphviz DOT string representation of the circuit. #[pyfunction] -pub fn to_hugr_dot(c: &Bound) -> PyResult { +pub fn render_circuit_dot(c: &Bound) -> PyResult { with_hugr(c, |hugr, _| hugr.dot_string()) } /// Return a Mermaid diagram representation of the circuit. #[pyfunction] -pub fn to_hugr_mermaid(c: &Bound) -> PyResult { +pub fn render_circuit_mermaid(c: &Bound) -> PyResult { with_hugr(c, |hugr, _| hugr.mermaid_string()) } @@ -210,117 +207,3 @@ impl PyWire { self.wire.source().index() } } - -#[pyclass] -#[pyo3(name = "CustomOp")] -#[repr(transparent)] -#[derive(From, Into, PartialEq, Clone)] -struct PyCustom(CustomOp); - -impl fmt::Debug for PyCustom { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl From for OpType { - fn from(op: PyCustom) -> Self { - op.0.into() - } -} - -#[pymethods] -impl PyCustom { - #[new] - fn new( - extension: &str, - op_name: &str, - input_types: Vec, - output_types: Vec, - ) -> PyResult { - Ok(CustomOp::new_opaque(OpaqueOp::new( - IdentList::new(extension).unwrap(), - op_name, - Default::default(), - [], - FunctionType::new(into_vec(input_types), into_vec(output_types)), - )) - .into()) - } - - fn to_custom(&self) -> Self { - self.clone() - } - pub fn __repr__(&self) -> String { - format!("{:?}", self) - } - - fn name(&self) -> String { - self.0.name().to_string() - } -} -#[pyclass] -#[pyo3(name = "TypeBound")] -#[derive(PartialEq, Clone, Debug)] -enum PyTypeBound { - Any, - Copyable, - Eq, -} - -impl From for TypeBound { - fn from(bound: PyTypeBound) -> Self { - match bound { - PyTypeBound::Any => TypeBound::Any, - PyTypeBound::Copyable => TypeBound::Copyable, - PyTypeBound::Eq => TypeBound::Eq, - } - } -} - -impl From for PyTypeBound { - fn from(bound: TypeBound) -> Self { - match bound { - TypeBound::Any => PyTypeBound::Any, - TypeBound::Copyable => PyTypeBound::Copyable, - TypeBound::Eq => PyTypeBound::Eq, - } - } -} - -#[pyclass] -#[pyo3(name = "HugrType")] -#[repr(transparent)] -#[derive(From, Into, PartialEq, Clone)] -struct PyHugrType(Type); - -impl fmt::Debug for PyHugrType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -#[pymethods] -impl PyHugrType { - #[new] - fn new(extension: &str, type_name: &str, bound: PyTypeBound) -> Self { - Self(Type::new_extension(CustomType::new_simple( - type_name.into(), - IdentList::new(extension).unwrap(), - bound.into(), - ))) - } - #[staticmethod] - fn qubit() -> Self { - Self(QB_T) - } - - #[staticmethod] - fn bool() -> Self { - Self(BOOL_T) - } - - pub fn __repr__(&self) -> String { - format!("{:?}", self) - } -} diff --git a/tket2-py/src/circuit/convert.rs b/tket2-py/src/circuit/convert.rs index f46df25a..1af8d509 100644 --- a/tket2-py/src/circuit/convert.rs +++ b/tket2-py/src/circuit/convert.rs @@ -28,7 +28,7 @@ use tket_json_rs::circuit_json::SerialCircuit; use crate::rewrite::PyCircuitRewrite; use crate::utils::ConvertPyErr; -use super::{cost, PyCircuitCost, PyCustom, PyHugrType, PyNode, PyWire, Tk2Circuit}; +use super::{cost, PyCircuitCost, PyNode, PyWire, Tk2Circuit}; /// A flag to indicate the encoding of a circuit. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] diff --git a/tket2-py/src/circuit/tk2circuit.rs b/tket2-py/src/circuit/tk2circuit.rs index 78fc540f..39fd4ba7 100644 --- a/tket2-py/src/circuit/tk2circuit.rs +++ b/tket2-py/src/circuit/tk2circuit.rs @@ -11,7 +11,7 @@ use itertools::Itertools; use pyo3::exceptions::{PyAttributeError, PyValueError}; use pyo3::types::{PyAnyMethods, PyModule, PyString, PyTypeMethods}; use pyo3::{ - pyclass, pymethods, Bound, FromPyObject, PyAny, PyErr, PyObject, PyRefMut, PyResult, + pyclass, pymethods, Bound, FromPyObject, PyAny, PyErr, PyObject, PyRef, PyRefMut, PyResult, PyTypeInfo, Python, ToPyObject, }; @@ -26,11 +26,12 @@ use tket2::serialize::TKETDecode; use tket2::{Circuit, Tk2Op}; use tket_json_rs::circuit_json::SerialCircuit; -use crate::ops::PyTk2Op; +use crate::ops::{PyCustomOp, PyTk2Op}; use crate::rewrite::PyCircuitRewrite; -use crate::utils::ConvertPyErr; +use crate::types::PyHugrType; +use crate::utils::{into_vec, ConvertPyErr}; -use super::{cost, with_hugr, PyCircuitCost, PyCustom, PyHugrType, PyNode, PyWire}; +use super::{cost, with_hugr, PyCircuitCost, PyNode, PyWire}; /// A circuit in tket2 format. /// @@ -177,7 +178,7 @@ impl Tk2Circuit { Ok(self.clone()) } - fn node_op(&self, node: PyNode) -> PyResult { + fn node_op(&self, node: PyNode) -> PyResult { let custom: CustomOp = self .circ .hugr() @@ -248,8 +249,18 @@ impl Dfg { self.builder.input_wires().map_into().collect() } - fn add_op(&mut self, op: PyCustom, inputs: Vec) -> PyResult { - let custom: CustomOp = op.into(); + fn add_op(&mut self, op: Bound, inputs: Vec) -> PyResult { + // TODO: Once we wrap `Dfg` in a pure python class we can make the conversion there, + // and have a concrete `op: PyCustomOp` argument here. + let custom: PyCustomOp = op + .call_method0("to_custom") + .map_err(|_| { + PyErr::new::( + "The operation must implement the `ToCustomOp` protocol.", + ) + })? + .extract()?; + let custom: CustomOp = custom.into(); self.builder .add_dataflow_op(custom, inputs.into_iter().map_into()) .convert_pyerrs() @@ -267,7 +278,3 @@ impl Dfg { }) } } - -pub(super) fn into_vec>(v: impl IntoIterator) -> Vec { - v.into_iter().map_into().collect() -} diff --git a/tket2-py/src/lib.rs b/tket2-py/src/lib.rs index cdf9afc0..1ebf3969 100644 --- a/tket2-py/src/lib.rs +++ b/tket2-py/src/lib.rs @@ -5,6 +5,7 @@ pub mod optimiser; pub mod passes; pub mod pattern; pub mod rewrite; +pub mod types; pub mod utils; use pyo3::prelude::*; @@ -18,6 +19,7 @@ fn _tket2(py: Python, m: &Bound) -> PyResult<()> { add_submodule(py, m, passes::module(py)?)?; add_submodule(py, m, pattern::module(py)?)?; add_submodule(py, m, rewrite::module(py)?)?; + add_submodule(py, m, types::module(py)?)?; Ok(()) } diff --git a/tket2-py/src/ops.rs b/tket2-py/src/ops.rs index a973bce0..2e9aec28 100644 --- a/tket2-py/src/ops.rs +++ b/tket2-py/src/ops.rs @@ -1,17 +1,26 @@ //! Bindings for rust-defined operations -use derive_more::From; -use hugr::ops::NamedOp; +use derive_more::{From, Into}; +use hugr::hugr::IdentList; +use hugr::ops::custom::{ExtensionOp, OpaqueOp}; +use hugr::types::FunctionType; use pyo3::prelude::*; +use std::fmt; use std::str::FromStr; use strum::IntoEnumIterator; + +use hugr::ops::{CustomOp, NamedOp, OpType}; use tket2::{Pauli, Tk2Op}; +use crate::types::PyHugrType; +use crate::utils::into_vec; + /// The module definition pub fn module(py: Python<'_>) -> PyResult> { let m = PyModule::new_bound(py, "ops")?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(m) } @@ -58,6 +67,12 @@ impl PyTk2Op { self.op.exposed_name().to_string() } + /// Wrap the operation as a custom operation. + pub fn to_custom(&self) -> PyCustomOp { + let custom: ExtensionOp = self.op.into_extension_op(); + CustomOp::new_extension(custom).into() + } + /// String representation of the operation. pub fn __repr__(&self) -> String { self.qualified_name() @@ -203,3 +218,56 @@ impl PyPauliIter { self.it.next().map(|p| PyPauli { p }) } } + +/// A wrapped custom operation. +#[pyclass] +#[pyo3(name = "CustomOp")] +#[repr(transparent)] +#[derive(From, Into, PartialEq, Clone)] +pub struct PyCustomOp(CustomOp); + +impl fmt::Debug for PyCustomOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl From for OpType { + fn from(op: PyCustomOp) -> Self { + op.0.into() + } +} + +#[pymethods] +impl PyCustomOp { + #[new] + fn new( + extension: &str, + op_name: &str, + input_types: Vec, + output_types: Vec, + ) -> PyResult { + Ok(CustomOp::new_opaque(OpaqueOp::new( + IdentList::new(extension).unwrap(), + op_name, + Default::default(), + [], + FunctionType::new(into_vec(input_types), into_vec(output_types)), + )) + .into()) + } + + fn to_custom(&self) -> Self { + self.clone() + } + + /// String representation of the operation. + pub fn __repr__(&self) -> String { + format!("{:?}", self) + } + + #[getter] + fn name(&self) -> String { + self.0.name().to_string() + } +} diff --git a/tket2-py/src/types.rs b/tket2-py/src/types.rs new file mode 100644 index 00000000..95964dda --- /dev/null +++ b/tket2-py/src/types.rs @@ -0,0 +1,89 @@ +//! Hugr types + +use derive_more::{From, Into}; +use hugr::extension::prelude::{BOOL_T, QB_T}; +use hugr::hugr::IdentList; +use hugr::types::{CustomType, Type, TypeBound}; +use pyo3::prelude::*; +use std::fmt; + +/// The module definition +pub fn module(py: Python<'_>) -> PyResult> { + let m = PyModule::new_bound(py, "types")?; + m.add_class::()?; + m.add_class::()?; + + Ok(m) +} + +/// Bounds on the valid operations on a type in a HUGR program. +#[pyclass] +#[pyo3(name = "TypeBound")] +#[derive(PartialEq, Clone, Debug)] +pub enum PyTypeBound { + /// No bound on the type. + Any, + /// The type can be copied in the program. + Copyable, + /// The equality operation is valid on this type. + Eq, +} + +impl From for TypeBound { + fn from(bound: PyTypeBound) -> Self { + match bound { + PyTypeBound::Any => TypeBound::Any, + PyTypeBound::Copyable => TypeBound::Copyable, + PyTypeBound::Eq => TypeBound::Eq, + } + } +} + +impl From for PyTypeBound { + fn from(bound: TypeBound) -> Self { + match bound { + TypeBound::Any => PyTypeBound::Any, + TypeBound::Copyable => PyTypeBound::Copyable, + TypeBound::Eq => PyTypeBound::Eq, + } + } +} + +/// A HUGR type +#[pyclass] +#[pyo3(name = "HugrType")] +#[repr(transparent)] +#[derive(From, Into, PartialEq, Clone)] +pub struct PyHugrType(Type); + +impl fmt::Debug for PyHugrType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[pymethods] +impl PyHugrType { + #[new] + fn new(extension: &str, type_name: &str, bound: PyTypeBound) -> Self { + Self(Type::new_extension(CustomType::new_simple( + type_name.into(), + IdentList::new(extension).unwrap(), + bound.into(), + ))) + } + #[staticmethod] + fn qubit() -> Self { + Self(QB_T) + } + + #[staticmethod] + fn bool() -> Self { + Self(BOOL_T) + } + + /// A string representation of the type. + pub fn __repr__(&self) -> String { + format!("{:?}", self) + } +} diff --git a/tket2-py/src/utils.rs b/tket2-py/src/utils.rs index 4d0f8e36..9bc4ac20 100644 --- a/tket2-py/src/utils.rs +++ b/tket2-py/src/utils.rs @@ -45,3 +45,9 @@ macro_rules! create_py_exception { }; } pub(crate) use create_py_exception; +use itertools::Itertools; + +/// Convert an iterator of one type into vector of another type. +pub fn into_vec>(v: impl IntoIterator) -> Vec { + v.into_iter().map_into().collect() +} diff --git a/tket2-py/test/test_circuit.py b/tket2-py/test/test_circuit.py index 6e2913ae..714ac16e 100644 --- a/tket2-py/test/test_circuit.py +++ b/tket2-py/test/test_circuit.py @@ -4,7 +4,7 @@ from tket2.circuit import ( Tk2Circuit, - to_hugr_dot, + render_circuit_dot, ) from tket2.ops import Tk2Op @@ -42,10 +42,10 @@ def test_hash(): def test_conversion(): tk1 = Circuit(4).CX(0, 2).CX(1, 2).CX(1, 3) - tk1_dot = to_hugr_dot(tk1) + tk1_dot = render_circuit_dot(tk1) tk2 = Tk2Circuit(tk1) - tk2_dot = to_hugr_dot(tk2) + tk2_dot = render_circuit_dot(tk2) assert type(tk2) == Tk2Circuit assert tk1_dot == tk2_dot diff --git a/tket2-py/test/test_pauli_prop.py b/tket2-py/test/test_pauli_prop.py index f33d69b6..be910a07 100644 --- a/tket2-py/test/test_pauli_prop.py +++ b/tket2-py/test/test_pauli_prop.py @@ -3,8 +3,8 @@ import pytest from pytket._tket.circuit import Circuit +from tket2.ops import CustomOp, Tk2Op, Not from tket2.circuit import ( - CustomOp, Dfg, Wire, Tk2Circuit, @@ -14,15 +14,11 @@ QB_T, CircBuild, H, - Measure, - Not, from_coms, CX, PauliX, PauliY, PauliZ, - QAlloc, - QFree, ) from tket2.pattern import Rule, RuleMatcher # type: ignore from tket2.rewrite import CircuitRewrite, Subcircuit # type: ignore @@ -69,12 +65,12 @@ def measure_rules() -> list[Rule]: r_build = Dfg([QB_T], [QB_T, BOOL_T]) qs = r_build.inputs() qs = r_build.add_op(PauliX.op(), qs).outs(1) - q, b = r_build.add_op(Measure, qs).outs(2) + q, b = r_build.add_op(Tk2Op.Measure, qs).outs(2) ltk = r_build.finish([q, b]) r_build = Dfg([QB_T], [QB_T, BOOL_T]) qs = r_build.inputs() - q, b = r_build.add_op(Measure, qs).outs(2) + q, b = r_build.add_op(Tk2Op.Measure, qs).outs(2) b = r_build.add_op(Not, [b])[0] rtk = r_build.finish([q, b]) @@ -84,12 +80,12 @@ def measure_rules() -> list[Rule]: r_build = Dfg([QB_T], [QB_T, BOOL_T]) qs = r_build.inputs() qs = r_build.add_op(PauliZ.op(), qs).outs(1) - q, b = r_build.add_op(Measure, qs).outs(2) + q, b = r_build.add_op(Tk2Op.Measure, qs).outs(2) ltk = r_build.finish([q, b]) r_build = Dfg([QB_T], [QB_T, BOOL_T]) qs = r_build.inputs() - q, b = r_build.add_op(Measure, qs).outs(2) + q, b = r_build.add_op(Tk2Op.Measure, qs).outs(2) rtk = r_build.finish([q, b]) rules.append(Rule(ltk, rtk)) @@ -139,7 +135,7 @@ def final_pauli_string(circ: Tk2Circuit) -> str: def map_op(op: CustomOp) -> str: # strip the extension name - n = op.name()[len("quantum.tket2.") :] + n = op.name[len("quantum.tket2.") :] return n if n in ("X", "Y", "Z") else "I" # TODO ignore non-qubit outputs @@ -188,8 +184,8 @@ def test_cat(propagate_matcher: RuleMatcher): def test_alloc_free(): c = CircBuild(0) - alloc = c.dfg.add_op(QAlloc, []) - c.dfg.add_op(QFree, alloc.outs(1)) + alloc = c.dfg.add_op(Tk2Op.QAlloc, []) + c.dfg.add_op(Tk2Op.QFree, alloc.outs(1)) c.finish() # validates @@ -197,7 +193,7 @@ def test_measure(propagate_matcher: RuleMatcher): c = Dfg([QB_T, QB_T], [QB_T, BOOL_T, QB_T, BOOL_T]) q0, q1 = c.inputs() q0 = c.add_op(PauliX.op(), [q0])[0] - outs = [w for q in (q0, q1) for w in c.add_op(Measure, [q]).outs(2)] + outs = [w for q in (q0, q1) for w in c.add_op(Tk2Op.Measure, [q]).outs(2)] before = c.finish(outs) """ ──►X───►Measure─► @@ -211,7 +207,7 @@ def test_measure(propagate_matcher: RuleMatcher): c = Dfg([QB_T, QB_T], [QB_T, BOOL_T, QB_T, BOOL_T]) q0, q1 = c.inputs() - q0, b0, q1, b1 = [w for q in (q0, q1) for w in c.add_op(Measure, [q]).outs(2)] + q0, b0, q1, b1 = [w for q in (q0, q1) for w in c.add_op(Tk2Op.Measure, [q]).outs(2)] b0 = c.add_op(Not, [b0])[0] after = c.finish([q0, b0, q1, b1]) """ diff --git a/tket2-py/tket2/_tket2/circuit.pyi b/tket2-py/tket2/_tket2/circuit.pyi index 101718ba..2ba4a108 100644 --- a/tket2-py/tket2/_tket2/circuit.pyi +++ b/tket2-py/tket2/_tket2/circuit.pyi @@ -1,12 +1,13 @@ -from enum import Enum from typing import Any, Callable -from pytket._tket.circuit import Circuit -from tket2._tket2.ops import Tk2Op +from pytket._tket.circuit import Circuit as Tk1Circuit + +from tket2._tket2.ops import Tk2Op, CustomOp +from tket2._tket2.types import HugrType class Tk2Circuit: """Rust representation of a TKET2 circuit.""" - def __init__(self, circ: Circuit) -> None: + def __init__(self, circ: Tk1Circuit) -> None: """Create a Tk2Circuit from a pytket Circuit.""" def __hash__(self) -> int: @@ -35,7 +36,7 @@ class Tk2Circuit: def node_op(self, node: Node) -> CustomOp: """If the node corresponds to a custom op, return it. Otherwise, raise an error.""" - def to_tket1(self) -> Circuit: + def to_tket1(self) -> Tk1Circuit: """Convert to pytket Circuit.""" def apply_rewrite(self, rw) -> None: @@ -84,7 +85,7 @@ class Dfg: def inputs(self) -> list[Wire]: """The output wires of the input node in the DFG, one for each input type.""" - def add_op(self, op: CustomOp, wires: list[Wire]) -> Node: + def add_op(self, op: CustomOp | Any, wires: list[Wire]) -> Node: """Add a custom operation to the DFG, wiring in input wires.""" def finish(self, outputs: list[Wire]) -> Tk2Circuit: @@ -129,48 +130,9 @@ class CircuitCost: The cost object must implement __add__, __sub__, __eq__, and __lt__.""" -class CustomOp: - """A HUGR custom operation.""" - - def __init__( - self, - extension: str, - op_name: str, - input_types: list[HugrType], - output_types: list[HugrType], - ) -> None: - """Create a new custom operation from name and input/output types.""" - - def to_custom(self) -> CustomOp: - """Convert to a custom operation. Identity operation.""" - - def name(self) -> str: - """Fully qualified (include extension) name of the operation.""" - -class HugrType: - """Value types in HUGR.""" - - def __init__(self, extension: str, type_name: str, bound: TypeBound) -> None: - """Create a new named Custom type.""" - - @staticmethod - def qubit() -> HugrType: - """Qubit type from HUGR prelude.""" - - @staticmethod - def bool() -> HugrType: - """Boolean type (HUGR 2-ary unit sum).""" - -class TypeBound(Enum): - """HUGR type bounds.""" - - Any = 0 # Any type - Copyable = 1 # Copyable type - Eq = 2 # Equality-comparable type - -def to_hugr_dot(hugr: Tk2Circuit | Circuit) -> str: ... -def to_hugr_mermaid(hugr: Tk2Circuit | Circuit) -> str: ... -def validate_hugr(hugr: Tk2Circuit | Circuit) -> None: ... +def render_circuit_dot(hugr: Tk2Circuit | Tk1Circuit) -> str: ... +def render_circuit_mermaid(hugr: Tk2Circuit | Tk1Circuit) -> str: ... +def validate_circuit(hugr: Tk2Circuit | Tk1Circuit) -> None: ... class HugrError(Exception): ... class BuildError(Exception): ... diff --git a/tket2-py/tket2/_tket2/ops.pyi b/tket2-py/tket2/_tket2/ops.pyi index 81bca46a..70824c47 100644 --- a/tket2-py/tket2/_tket2/ops.pyi +++ b/tket2-py/tket2/_tket2/ops.pyi @@ -1,6 +1,8 @@ from enum import Enum from typing import Any, Iterable +from tket2._tket2.types import HugrType + class Tk2Op(Enum): """A rust-backed Tket2 built-in operation.""" @@ -11,6 +13,9 @@ class Tk2Op(Enum): def values() -> Iterable[Tk2Op]: """Iterate over all operation variants.""" + def to_custom(self) -> CustomOp: + """Convert to a custom operation.""" + @property def name(self) -> str: """Get the string name of the operation.""" @@ -58,3 +63,22 @@ class Pauli(Enum): """Get the string name of the Pauli.""" def __eq__(self, value: Any) -> bool: ... + +class CustomOp: + """A HUGR custom operation.""" + + def __init__( + self, + extension: str, + op_name: str, + input_types: list[HugrType], + output_types: list[HugrType], + ) -> None: + """Create a new custom operation from name and input/output types.""" + + def to_custom(self) -> CustomOp: + """Convert to a custom operation. Identity operation.""" + + @property + def name(self) -> str: + """Fully qualified (including extension) name of the operation.""" diff --git a/tket2-py/tket2/_tket2/types.pyi b/tket2-py/tket2/_tket2/types.pyi new file mode 100644 index 00000000..acd5aa06 --- /dev/null +++ b/tket2-py/tket2/_tket2/types.pyi @@ -0,0 +1,22 @@ +from enum import Enum + +class HugrType: + """Value types in HUGR.""" + + def __init__(self, extension: str, type_name: str, bound: TypeBound) -> None: + """Create a new named Custom type.""" + + @staticmethod + def qubit() -> HugrType: + """Qubit type from HUGR prelude.""" + + @staticmethod + def bool() -> HugrType: + """Boolean type (HUGR 2-ary unit sum).""" + +class TypeBound(Enum): + """HUGR type bounds.""" + + Any = 0 # Any type + Copyable = 1 # Copyable type + Eq = 2 # Equality-comparable type diff --git a/tket2-py/tket2/circuit/__init__.py b/tket2-py/tket2/circuit/__init__.py index db726e35..efb4f13f 100644 --- a/tket2-py/tket2/circuit/__init__.py +++ b/tket2-py/tket2/circuit/__init__.py @@ -6,11 +6,9 @@ Node, Wire, CircuitCost, - CustomOp, - HugrType, - validate_hugr, - to_hugr_dot, - to_hugr_mermaid, + validate_circuit, + render_circuit_dot, + render_circuit_mermaid, HugrError, BuildError, ValidationError, @@ -30,11 +28,9 @@ "Node", "Wire", "CircuitCost", - "CustomOp", - "HugrType", - "validate_hugr", - "to_hugr_dot", - "to_hugr_mermaid", + "validate_circuit", + "render_circuit_dot", + "render_circuit_mermaid", "HugrError", "BuildError", "ValidationError", diff --git a/tket2-py/tket2/circuit/build.py b/tket2-py/tket2/circuit/build.py index cf742720..a495e279 100644 --- a/tket2-py/tket2/circuit/build.py +++ b/tket2-py/tket2/circuit/build.py @@ -1,10 +1,9 @@ from typing import Protocol, Iterable -from tket2.circuit import HugrType, CustomOp, Dfg, Node, Wire, Tk2Circuit +from tket2.circuit import Dfg, Node, Wire, Tk2Circuit +from tket2.types import QB_T, BOOL_T +from tket2.ops import CustomOp, Tk2Op, ToCustomOp from dataclasses import dataclass -QB_T = HugrType.qubit() -BOOL_T = HugrType.bool() - class Command(Protocol): """Interface to specify a custom operation over some qubits and linear bits. @@ -36,9 +35,10 @@ def __init__(self, n_qb: int) -> None: self.dfg = Dfg([QB_T] * n_qb, [QB_T] * n_qb) self.qbs = self.dfg.inputs() - def add(self, op: CustomOp, indices: list[int]) -> Node: + def add(self, op: ToCustomOp, indices: list[int]) -> Node: """Add a Custom operation to some qubits and update the qubit list.""" qbs = [self.qbs[i] for i in indices] + op = op.to_custom() n = self.dfg.add_op(op, qbs) outs = n.outs(len(indices)) for i, o in zip(indices, outs): @@ -48,7 +48,7 @@ def add(self, op: CustomOp, indices: list[int]) -> Node: def measure_all(self) -> list[Wire]: """Append a measurement to all qubits and return the measurement result wires.""" - return [self.add(Measure, [i]).outs(2)[1] for i in range(len(self.qbs))] + return [self.add(Tk2Op.Measure, [i]).outs(2)[1] for i in range(len(self.qbs))] def add_command(self, command: Command) -> Node: """Add a Command to the circuit and return the new node.""" @@ -135,11 +135,3 @@ class PauliY(Command): def qubits(self) -> list[int]: return [self.qubit] - - -# Define CustomOps for common operations that don't have an (n qubits in, n -# qubits out) signature -QAlloc = CustomOp("quantum.tket2", "QAlloc", [], [QB_T]) -QFree = CustomOp("quantum.tket2", "QFree", [QB_T], []) -Measure = CustomOp("quantum.tket2", "Measure", [QB_T], [QB_T, BOOL_T]) -Not = CustomOp("logic", "Not", [BOOL_T], [BOOL_T]) diff --git a/tket2-py/tket2/ops.py b/tket2-py/tket2/ops.py index 8ce31301..1e4736c8 100644 --- a/tket2-py/tket2/ops.py +++ b/tket2-py/tket2/ops.py @@ -1,10 +1,33 @@ +from __future__ import annotations + from enum import Enum, auto +from typing import Protocol import tket2 +from tket2._tket2.ops import CustomOp +from tket2.circuit.build import QB_T +from tket2.types import BOOL_T + +__all__ = ["CustomOp", "ToCustomOp", "Tk2Op", "Pauli"] + + +class ToCustomOp(Protocol): + """Operation that can be converted to a HUGR CustomOp.""" + + def to_custom(self) -> CustomOp: + """Convert to a custom operation.""" + + @property + def name(self) -> str: + """Name of the operation.""" + class Tk2Op(Enum): - """A Tket2 built-in operation.""" + """A Tket2 built-in operation. + + Implements the `ToCustomOp` protocol. + """ H = auto() CX = auto() @@ -28,6 +51,10 @@ class Tk2Op(Enum): QFree = auto() Reset = auto() + def to_custom(self) -> CustomOp: + """Convert to a custom operation.""" + return self._to_rs().to_custom() + def _to_rs(self) -> tket2._tket2.ops.Tk2Op: """Convert to the Rust-backed Tk2Op representation.""" return tket2._tket2.ops.Tk2Op(self.name) @@ -49,13 +76,21 @@ def __eq__(self, other: object) -> bool: class Pauli(Enum): - """Simple enum representation of Pauli matrices.""" + """Simple enum representation of Pauli matrices. + + Implements the `ToCustomOp` protocol. + """ I = auto() # noqa: E741 X = auto() Y = auto() Z = auto() + def to_custom(self) -> CustomOp: + extension_name = "quantum.tket2" + gate_name = self.name + return CustomOp(extension_name, gate_name, [QB_T], [QB_T]) + def _to_rs(self) -> tket2._tket2.ops.Pauli: """Convert to the Rust-backed Pauli representation.""" return tket2._tket2.ops.Pauli(self.name) @@ -74,3 +109,7 @@ def __eq__(self, other: object) -> bool: elif isinstance(other, str): return self.name == other return False + + +# Define other common operations +Not = CustomOp("logic", "Not", [BOOL_T], [BOOL_T]) diff --git a/tket2-py/tket2/types.py b/tket2-py/tket2/types.py new file mode 100644 index 00000000..712a508e --- /dev/null +++ b/tket2-py/tket2/types.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from tket2._tket2.types import HugrType, TypeBound + +__all__ = ["HugrType", "TypeBound", "QB_T", "BOOL_T"] + + +QB_T = HugrType.qubit() +BOOL_T = HugrType.bool() diff --git a/tket2/src/ops.rs b/tket2/src/ops.rs index 42ea2ab9..6565bf5e 100644 --- a/tket2/src/ops.rs +++ b/tket2/src/ops.rs @@ -1,6 +1,7 @@ use crate::extension::{ SYM_EXPR_T, SYM_OP_ID, TKET2_EXTENSION as EXTENSION, TKET2_EXTENSION_ID as EXTENSION_ID, }; +use hugr::ops::custom::ExtensionOp; use hugr::ops::NamedOp; use hugr::{ extension::{ @@ -71,6 +72,12 @@ impl Tk2Op { pub fn exposed_name(&self) -> smol_str::SmolStr { >::into(*self).name() } + + /// Wraps the operation in an [`ExtensionOp`] + pub fn into_extension_op(self) -> ExtensionOp { + ::to_extension_op(self) + .expect("Failed to convert to extension op.") + } } /// Whether an op is a given Tk2Op. @@ -159,6 +166,7 @@ impl Tk2Op { match self { X | RxF64 => vec![(0, Pauli::X)], + Y => vec![(0, Pauli::Y)], T | Z | S | Tdg | Sdg | RzF64 | Measure => vec![(0, Pauli::Z)], CX => vec![(0, Pauli::Z), (1, Pauli::X)], ZZMax | ZZPhase | CZ => vec![(0, Pauli::Z), (1, Pauli::Z)], @@ -249,6 +257,7 @@ impl TryFrom<&OpType> for Tk2Op { #[cfg(test)] pub(crate) mod test { + use std::str::FromStr; use std::sync::Arc; use hugr::extension::simple_op::MakeOpDef; @@ -262,6 +271,7 @@ pub(crate) mod test { use crate::circuit::Circuit; use crate::extension::{TKET2_EXTENSION as EXTENSION, TKET2_EXTENSION_ID as EXTENSION_ID}; use crate::utils::build_simple_circuit; + use crate::Pauli; fn get_opdef(op: impl NamedOp) -> Option<&'static Arc> { EXTENSION.get_op(&op.name()) } @@ -311,4 +321,32 @@ pub(crate) mod test { // 5 commands: alloc, reset, cx, measure, free assert_eq!(h.commands().count(), 5); } + + #[test] + fn tk2op_properties() { + for op in Tk2Op::iter() { + // The exposed name should start with "quantum.tket2." + assert!(op.exposed_name().starts_with(&EXTENSION_ID.to_string())); + + let ext_op = op.into_extension_op(); + assert_eq!(ext_op.args(), &[]); + assert_eq!(ext_op.def().extension(), &EXTENSION_ID); + let name = ext_op.def().name(); + assert_eq!(Tk2Op::from_str(name), Ok(op)); + } + + // Other calls + assert!(Tk2Op::H.is_quantum()); + assert!(!Tk2Op::Measure.is_quantum()); + + for (op, pauli) in [ + (Tk2Op::X, Pauli::X), + (Tk2Op::Y, Pauli::Y), + (Tk2Op::Z, Pauli::Z), + ] + .iter() + { + assert_eq!(op.qubit_commutation(), &[(0, *pauli)]); + } + } }