Skip to content

Commit

Permalink
feat: drop pyo3 core dep (#355)
Browse files Browse the repository at this point in the history
Adds a python-side definition of `Tk2Op` and `Pauli`, so we can drop the
dependency on `pyo3` from the main rust package.

In addition of being necessary for #350, having only one package
depending on `pyo3` removes building headaches due to it linking to
python multiple times.

Blocked by #352
  • Loading branch information
aborgna-q committed May 24, 2024
1 parent 847bca3 commit 9f7d415
Show file tree
Hide file tree
Showing 17 changed files with 749 additions and 436 deletions.
752 changes: 379 additions & 373 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ tket2::passes::apply_greedy_commutation(&mut circ);

## Features

- `pyo3`
Enables some python bindings via pyo3. See the `tket2-py` folder for more.

- `portmatching`
Enables pattern matching using the `portmatching` crate.

Expand Down
4 changes: 1 addition & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ build:

# Run all the tests.
test language="[rust|python]" : (_run_lang language \
"poetry run cargo test --all-features" \
"poetry run cargo test --all-features --workspace" \
"poetry run maturin develop && poetry run pytest"
)
# Note: We cannot use `cargo test --workspace` because there
# are two crates that use pyo3, and that causes a linking conflict.

# Auto-fix all clippy warnings.
fix language="[rust|python]": (_run_lang language \
Expand Down
3 changes: 2 additions & 1 deletion tket2-py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test = false
bench = false

[dependencies]
tket2 = { workspace = true, features = ["pyo3", "portmatching"] }
tket2 = { workspace = true, features = ["portmatching"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tket-json-rs = { workspace = true, features = ["pyo3"] }
Expand All @@ -29,3 +29,4 @@ num_cpus = { workspace = true }
derive_more = { workspace = true }
itertools = { workspace = true }
portmatching = { workspace = true }
strum = { workspace = true }
2 changes: 0 additions & 2 deletions tket2-py/src/circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,8 @@ pub fn module(py: Python<'_>) -> PyResult<Bound<'_, PyModule>> {
m.add_class::<PyWire>()?;
m.add_class::<WireIter>()?;
m.add_class::<PyCircuitCost>()?;
m.add_class::<Tk2Op>()?;
m.add_class::<PyCustom>()?;
m.add_class::<PyHugrType>()?;
m.add_class::<Pauli>()?;
m.add_class::<PyTypeBound>()?;

m.add_function(wrap_pyfunction!(validate_hugr, &m)?)?;
Expand Down
4 changes: 3 additions & 1 deletion tket2-py/src/circuit/tk2circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use tket2::passes::CircuitChunks;
use tket2::{Circuit, Tk2Op};
use tket_json_rs::circuit_json::SerialCircuit;

use crate::ops::PyTk2Op;
use crate::rewrite::PyCircuitRewrite;
use crate::utils::ConvertPyErr;

Expand Down Expand Up @@ -127,7 +128,8 @@ impl Tk2Circuit {
"Could not convert circuit operation to a `Tk2Op`: {e}"
))
})?;
let cost = cost_fn.call1((tk2_op,))?;
let tk2_py_op = PyTk2Op::from(tk2_op);
let cost = cost_fn.call1((tk2_py_op,))?;
Ok(PyCircuitCost {
cost: cost.to_object(py),
})
Expand Down
2 changes: 2 additions & 0 deletions tket2-py/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Python bindings for TKET2.
pub mod circuit;
pub mod ops;
pub mod optimiser;
pub mod passes;
pub mod pattern;
Expand All @@ -12,6 +13,7 @@ use pyo3::prelude::*;
#[pymodule]
fn _tket2(py: Python, m: &Bound<PyModule>) -> PyResult<()> {
add_submodule(py, m, circuit::module(py)?)?;
add_submodule(py, m, ops::module(py)?)?;
add_submodule(py, m, optimiser::module(py)?)?;
add_submodule(py, m, passes::module(py)?)?;
add_submodule(py, m, pattern::module(py)?)?;
Expand Down
202 changes: 202 additions & 0 deletions tket2-py/src/ops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//! Bindings for rust-defined operations

use derive_more::From;
use hugr::ops::NamedOp;
use pyo3::prelude::*;
use std::str::FromStr;
use strum::IntoEnumIterator;
use tket2::{Pauli, Tk2Op};

/// The module definition
pub fn module(py: Python<'_>) -> PyResult<Bound<'_, PyModule>> {
let m = PyModule::new_bound(py, "ops")?;
m.add_class::<PyTk2Op>()?;
m.add_class::<PyPauli>()?;
Ok(m)
}

/// Enum of Tket2 operations in hugr.
///
/// Python equivalent of [`Tk2Op`].
///
/// [`Tk2Op`]: tket2::ops::Tk2Op
#[pyclass]
#[pyo3(name = "Tk2Op")]
#[derive(Debug, Clone, From)]
#[repr(transparent)]
pub struct PyTk2Op {
/// Rust representation of the operation.
pub op: Tk2Op,
}

#[pymethods]
impl PyTk2Op {
/// Initialize a new `PyTk2Op` from a python string.
#[new]
fn new(op: &str) -> PyResult<Self> {
Ok(Self {
op: Tk2Op::from_str(op)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?,
})
}

/// Iterate over the operations.
#[staticmethod]
pub fn values() -> PyTk2OpIter {
PyTk2OpIter { it: Tk2Op::iter() }
}

/// Get the string name of the operation.
#[getter]
pub fn name(&self) -> String {
self.op.name().to_string()
}

/// Get the qualified name of the operation, including the extension.
#[getter]
pub fn qualified_name(&self) -> String {
self.op.exposed_name().to_string()
}

/// String representation of the operation.
pub fn __repr__(&self) -> String {
self.qualified_name()
}

/// String representation of the operation.
pub fn __str__(&self) -> String {
self.name()
}

/// Check if two operations are equal.
pub fn __eq__(&self, other: &PyTk2Op) -> bool {
self.op == other.op
}
}

/// Iterator over the operations.
#[pyclass]
#[pyo3(name = "Tk2OpIter")]
#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct PyTk2OpIter {
/// Iterator over the operations.
pub it: <Tk2Op as IntoEnumIterator>::Iterator,
}

#[pymethods]
impl PyTk2OpIter {
/// Iterate over the operations.
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}

/// Get the next operation.
pub fn __next__(&mut self) -> Option<PyTk2Op> {
self.it.next().map(|op| PyTk2Op { op })
}
}

/// Pauli matrices
///
/// Python equivalent of [`Pauli`].
///
/// [`Pauli`]: tket2::ops::Pauli
#[pyclass]
#[pyo3(name = "Pauli")]
#[derive(Debug, Clone, From)]
#[repr(transparent)]
pub struct PyPauli {
/// Rust representation of the pauli matrix.
pub p: Pauli,
}

#[pymethods]
impl PyPauli {
/// Initialize a new `PyPauli` from a python string.
#[new]
fn new(p: &str) -> PyResult<Self> {
Ok(Self {
p: Pauli::from_str(p)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?,
})
}

/// Iterate over the pauli matrices.
#[staticmethod]
pub fn values() -> PyPauliIter {
PyPauliIter { it: Pauli::iter() }
}

/// Return the Pauli matrix as a string.
#[getter]
pub fn name(&self) -> String {
format!("{}", self.p)
}

/// Pauli identity matrix.
#[staticmethod]
#[pyo3(name = "I")]
fn i() -> Self {
Self { p: Pauli::I }
}

/// Pauli X matrix.
#[staticmethod]
#[pyo3(name = "X")]
fn x() -> Self {
Self { p: Pauli::X }
}

/// Pauli Y matrix.
#[staticmethod]
#[pyo3(name = "Y")]
fn y() -> Self {
Self { p: Pauli::Y }
}

/// Pauli Z matrix.
#[pyo3(name = "Z")]
#[staticmethod]
fn z() -> Self {
Self { p: Pauli::Z }
}

/// String representation of the Pauli matrix.
pub fn __repr__(&self) -> String {
format!("{:?}", self.p)
}

/// String representation of the Pauli matrix.
pub fn __str__(&self) -> String {
format!("{}", self.p)
}

/// Check if two Pauli matrices are equal.
pub fn __eq__(&self, other: &PyPauli) -> bool {
self.p == other.p
}
}

/// Iterator over the Pauli matrices.
#[pyclass]
#[pyo3(name = "PauliIter")]
#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct PyPauliIter {
/// Iterator over the Pauli matrices.
pub it: <Pauli as IntoEnumIterator>::Iterator,
}

#[pymethods]
impl PyPauliIter {
/// Iterate over the operations.
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}

/// Get the next Pauli matrix.
pub fn __next__(&mut self) -> Option<PyPauli> {
self.it.next().map(|p| PyPauli { p })
}
}
2 changes: 1 addition & 1 deletion tket2-py/test/test_circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from tket2.circuit import (
Tk2Circuit,
Tk2Op,
to_hugr_dot,
)
from tket2.ops import Tk2Op


@dataclass
Expand Down
18 changes: 18 additions & 0 deletions tket2-py/test/test_ops.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import tket2
from tket2.ops import Tk2Op, Pauli


def test_ops_roundtrip():
for op in Tk2Op:
assert Tk2Op._from_rs(op._to_rs()) == op

for op in tket2._tket2.ops.Tk2Op.values():
assert Tk2Op._from_rs(op)._to_rs() == op


def test_pauli_roundtrip():
for pauli in Pauli:
assert Pauli._from_rs(pauli._to_rs()) == pauli

for pauli in tket2._tket2.ops.Pauli.values():
assert Pauli._from_rs(pauli)._to_rs() == pauli
36 changes: 2 additions & 34 deletions tket2-py/tket2/_tket2/circuit.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum, auto
from enum import Enum
from typing import Any, Callable
from pytket._tket.circuit import Circuit
from tket2._tket2.ops import Tk2Op

class Tk2Circuit:
"""Rust representation of a TKET2 circuit."""
Expand Down Expand Up @@ -116,31 +117,6 @@ class CircuitCost:
The cost object must implement __add__, __sub__, __eq__, and __lt__."""

class Tk2Op(Enum):
"""A Tket2 built-in operation."""

H = auto()
CX = auto()
T = auto()
S = auto()
X = auto()
Y = auto()
Z = auto()
Tdg = auto()
Sdg = auto()
ZZMax = auto()
Measure = auto()
RzF64 = auto()
RxF64 = auto()
PhasedX = auto()
ZZPhase = auto()
AngleAdd = auto()
CZ = auto()
TK1 = auto()
QAlloc = auto()
QFree = auto()
Reset = auto()

class CustomOp:
"""A HUGR custom operation."""

Expand Down Expand Up @@ -177,14 +153,6 @@ class HugrType:
def bool() -> HugrType:
"""Boolean type (HUGR 2-ary unit sum)."""

class Pauli(Enum):
"""Simple enum representation of Pauli matrices."""

I = auto() # noqa: E741
X = auto()
Y = auto()
Z = auto()

class TypeBound(Enum):
"""HUGR type bounds."""

Expand Down
Loading

0 comments on commit 9f7d415

Please sign in to comment.