Skip to content
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

refactor: Move python bindings from tket2 to tket2-py #235

Merged
merged 3 commits into from
Nov 10, 2023
Merged
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
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ license-file = "LICENCE"
[workspace.dependencies]

tket2 = { path = "./tket2" }
quantinuum-hugr = { git = "https://github.com/CQCL-DEV/hugr", rev = "b256c2b" }
quantinuum-hugr = { git = "https://github.com/CQCL/hugr", rev = "78faf6d" }
portgraph = { version = "0.10" }
pyo3 = { version = "0.20" }
itertools = { version = "0.11.0" }
tket-json-rs = "0.2.0"
tket-json-rs = { version = "0.3.0" }
tracing = "0.1.37"
portmatching = { git = "https://github.com/lmondada/portmatching", rev = "738c91c" }
3 changes: 2 additions & 1 deletion tket2-py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ tket2 = { workspace = true, features = ["pyo3", "portmatching"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tket-json-rs = { workspace = true, features = ["pyo3"] }
quantinuum-hugr = { workspace = true }
quantinuum-hugr = { workspace = true, features = ["pyo3"] }
portgraph = { workspace = true, features = ["pyo3", "serde"] }
pyo3 = { workspace = true, features = ["extension-module"] }
num_cpus = "1.16.0"
derive_more = "0.99.17"
itertools = { workspace = true }
portmatching = { workspace = true }
84 changes: 33 additions & 51 deletions tket2-py/src/circuit.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
//! Circuit-related functionality and utilities.
#![allow(unused)]

pub mod convert;

use derive_more::{From, Into};
use pyo3::prelude::*;
use std::fmt;

use hugr::{Hugr, HugrView};
use tket2::extension::REGISTRY;
use tket2::json::TKETDecode;
use tket2::passes::CircuitChunks;
use tket2::rewrite::CircuitRewrite;
use tket_json_rs::circuit_json::SerialCircuit;

pub use self::convert::{try_update_hugr, try_with_hugr, update_hugr, with_hugr, T2Circuit};

/// The module definition
pub fn module(py: Python) -> PyResult<&PyModule> {
let m = PyModule::new(py, "_circuit")?;
m.add_class::<T2Circuit>()?;
m.add_class::<PyNode>()?;
m.add_class::<tket2::T2Op>()?;
m.add_class::<tket2::Pauli>()?;
m.add_class::<tket2::passes::CircuitChunks>()?;

m.add_function(wrap_pyfunction!(validate_hugr, m)?)?;
m.add_function(wrap_pyfunction!(to_hugr_dot, m)?)?;
m.add_function(wrap_pyfunction!(to_hugr, m)?)?;
m.add_function(wrap_pyfunction!(chunks, m)?)?;

m.add("HugrError", py.get_type::<hugr::hugr::PyHugrError>())?;
m.add("BuildError", py.get_type::<hugr::builder::PyBuildError>())?;
Expand All @@ -41,72 +45,50 @@ pub fn module(py: Python) -> PyResult<&PyModule> {
Ok(m)
}

/// Apply a fallible function expecting a hugr on a pytket circuit.
pub fn try_with_hugr<T, E, F>(circ: Py<PyAny>, f: F) -> PyResult<T>
where
E: Into<PyErr>,
F: FnOnce(Hugr) -> Result<T, E>,
{
let hugr = SerialCircuit::_from_tket1(circ).decode()?;
(f)(hugr).map_err(|e| e.into())
}

/// Apply a function expecting a hugr on a pytket circuit.
pub fn with_hugr<T, F: FnOnce(Hugr) -> T>(circ: Py<PyAny>, f: F) -> PyResult<T> {
try_with_hugr(circ, |hugr| Ok::<T, PyErr>((f)(hugr)))
}

/// Apply a hugr-to-hugr function on a pytket circuit, and return the modified circuit.
pub fn try_update_hugr<E: Into<PyErr>, F: FnOnce(Hugr) -> Result<Hugr, E>>(
circ: Py<PyAny>,
f: F,
) -> PyResult<Py<PyAny>> {
let hugr = try_with_hugr(circ, f)?;
SerialCircuit::encode(&hugr)?.to_tket1()
}

/// Apply a hugr-to-hugr function on a pytket circuit, and return the modified circuit.
pub fn update_hugr<F: FnOnce(Hugr) -> Hugr>(circ: Py<PyAny>, f: F) -> PyResult<Py<PyAny>> {
let hugr = with_hugr(circ, f)?;
SerialCircuit::encode(&hugr)?.to_tket1()
}

/// Run the validation checks on a circuit.
#[pyfunction]
pub fn validate_hugr(c: Py<PyAny>) -> PyResult<()> {
try_with_hugr(c, |hugr| hugr.validate(&REGISTRY))
}

/// Return a Graphviz DOT string representation of the circuit.
#[pyfunction]
pub fn to_hugr_dot(c: Py<PyAny>) -> PyResult<String> {
with_hugr(c, |hugr| hugr.dot_string())
}

/// Downcast a python object to a [`Hugr`].
#[pyfunction]
pub fn to_hugr(c: Py<PyAny>) -> PyResult<Hugr> {
with_hugr(c, |hugr| hugr)
}

#[pyfunction]
pub fn chunks(c: Py<PyAny>, max_chunk_size: usize) -> PyResult<CircuitChunks> {
with_hugr(c, |hugr| CircuitChunks::split(&hugr, max_chunk_size))
pub fn to_hugr(c: Py<PyAny>) -> PyResult<T2Circuit> {
with_hugr(c, |hugr| hugr.into())
}

/// A [`hugr::Node`] wrapper for Python.
#[pyclass]
/// A manager for tket 2 operations on a tket 1 Circuit.
pub struct T2Circuit(pub Hugr);
#[pyo3(name = "Node")]
#[repr(transparent)]
#[derive(From, Into, PartialEq, Eq, Hash, Clone, Copy)]
pub struct PyNode {
/// Rust representation of the node
pub node: hugr::Node,
}

#[pymethods]
impl T2Circuit {
#[new]
fn from_circuit(circ: PyObject) -> PyResult<Self> {
Ok(Self(to_hugr(circ)?))
impl fmt::Display for PyNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.node.fmt(f)
}
}

fn finish(&self) -> PyResult<PyObject> {
SerialCircuit::encode(&self.0)?.to_tket1()
impl fmt::Debug for PyNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.node.fmt(f)
}
}

fn apply_match(&mut self, rw: CircuitRewrite) {
rw.apply(&mut self.0).expect("Apply error.");
#[pymethods]
impl PyNode {
/// A string representation of the pattern.
pub fn __repr__(&self) -> String {
format!("{:?}", self)
}
}
91 changes: 91 additions & 0 deletions tket2-py/src/circuit/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Utilities for calling Hugr functions on generic python objects.

use pyo3::{prelude::*, PyTypeInfo};

use derive_more::From;
use hugr::{Hugr, HugrView};
use tket2::extension::REGISTRY;
use tket2::json::TKETDecode;
use tket2::passes::CircuitChunks;
use tket_json_rs::circuit_json::SerialCircuit;

use crate::pattern::rewrite::PyCircuitRewrite;

/// A manager for tket 2 operations on a tket 1 Circuit.
#[pyclass]
#[derive(Clone, Debug, PartialEq, From)]
pub struct T2Circuit {
/// Rust representation of the circuit.
pub hugr: Hugr,
}

#[pymethods]
impl T2Circuit {
#[new]
fn from_circuit(circ: PyObject) -> PyResult<Self> {
Ok(Self {
hugr: with_hugr(circ, |hugr| hugr)?,
})
}

fn finish(&self) -> PyResult<PyObject> {
SerialCircuit::encode(&self.hugr)?.to_tket1_with_gil()
}

fn apply_match(&mut self, rw: PyCircuitRewrite) {
rw.rewrite.apply(&mut self.hugr).expect("Apply error.");
}
}
impl T2Circuit {
/// Tries to extract a T2Circuit from a python object.
///
/// Returns an error if the py object is not a T2Circuit.
pub fn try_extract(circ: Py<PyAny>) -> PyResult<Self> {
Python::with_gil(|py| circ.as_ref(py).extract::<T2Circuit>())
}
}

/// Apply a fallible function expecting a hugr on a pytket circuit.
pub fn try_with_hugr<T, E, F>(circ: Py<PyAny>, f: F) -> PyResult<T>
where
E: Into<PyErr>,
F: FnOnce(Hugr) -> Result<T, E>,
{
let hugr = Python::with_gil(|py| -> PyResult<Hugr> {
let circ = circ.as_ref(py);
match T2Circuit::extract(circ) {
// hugr circuit
Ok(t2circ) => Ok(t2circ.hugr),
// tket1 circuit
Err(_) => Ok(SerialCircuit::from_tket1(circ)?.decode()?),
}
})?;
(f)(hugr).map_err(|e| e.into())
}

/// Apply a function expecting a hugr on a pytket circuit.
pub fn with_hugr<T, F>(circ: Py<PyAny>, f: F) -> PyResult<T>
where
F: FnOnce(Hugr) -> T,
{
try_with_hugr(circ, |hugr| Ok::<T, PyErr>((f)(hugr)))
}

/// Apply a hugr-to-hugr function on a pytket circuit, and return the modified circuit.
pub fn try_update_hugr<E, F>(circ: Py<PyAny>, f: F) -> PyResult<Py<PyAny>>
where
E: Into<PyErr>,
F: FnOnce(Hugr) -> Result<Hugr, E>,
{
let hugr = try_with_hugr(circ, f)?;
SerialCircuit::encode(&hugr)?.to_tket1_with_gil()
}

/// Apply a hugr-to-hugr function on a pytket circuit, and return the modified circuit.
pub fn update_hugr<F>(circ: Py<PyAny>, f: F) -> PyResult<Py<PyAny>>
where
F: FnOnce(Hugr) -> Hugr,
{
let hugr = with_hugr(circ, f)?;
SerialCircuit::encode(&hugr)?.to_tket1_with_gil()
}
8 changes: 4 additions & 4 deletions tket2-py/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
//! Python bindings for TKET2.
#![warn(missing_docs)]

mod circuit;
mod optimiser;
mod passes;
mod pattern;
pub mod circuit;
pub mod optimiser;
pub mod passes;
pub mod pattern;

use pyo3::prelude::*;

Expand Down
9 changes: 7 additions & 2 deletions tket2-py/src/passes.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//! Passes for optimising circuits.

pub mod chunks;

use std::{cmp::min, convert::TryInto, fs, num::NonZeroUsize, path::PathBuf};

use pyo3::{prelude::*, types::IntoPyDict};
Expand All @@ -16,7 +20,8 @@ pub fn module(py: Python) -> PyResult<&PyModule> {
let m = PyModule::new(py, "_passes")?;
m.add_function(wrap_pyfunction!(greedy_depth_reduce, m)?)?;
m.add_function(wrap_pyfunction!(badger_optimise, m)?)?;
m.add_class::<tket2::T2Op>()?;
m.add_class::<self::chunks::PyCircuitChunks>()?;
m.add_function(wrap_pyfunction!(self::chunks::chunks, m)?)?;
m.add(
"PullForwardError",
py.get_type::<tket2::passes::PyPullForwardError>(),
Expand All @@ -28,7 +33,7 @@ pub fn module(py: Python) -> PyResult<&PyModule> {
fn greedy_depth_reduce(py_c: PyObject) -> PyResult<(PyObject, u32)> {
try_with_hugr(py_c, |mut h| {
let n_moves = apply_greedy_commutation(&mut h)?;
let py_c = SerialCircuit::encode(&h)?.to_tket1()?;
let py_c = SerialCircuit::encode(&h)?.to_tket1_with_gil()?;
PyResult::Ok((py_c, n_moves))
})
}
Expand Down
69 changes: 69 additions & 0 deletions tket2-py/src/passes/chunks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Circuit chunking utilities.

use derive_more::From;
use pyo3::exceptions::PyAttributeError;
use pyo3::prelude::*;
use tket2::json::TKETDecode;
use tket2::passes::CircuitChunks;
use tket2::Circuit;
use tket_json_rs::circuit_json::SerialCircuit;

use crate::circuit::{with_hugr, T2Circuit};

/// Split a circuit into chunks of a given size.
#[pyfunction]
pub fn chunks(c: Py<PyAny>, max_chunk_size: usize) -> PyResult<PyCircuitChunks> {
with_hugr(c, |hugr| {
// TODO: Detect if the circuit is in tket1 format or T2Circuit.
let is_tket1 = true;
let chunks = CircuitChunks::split(&hugr, max_chunk_size);
(chunks, is_tket1).into()
})
}

/// A pattern that match a circuit exactly
///
/// Python equivalent of [`CircuitChunks`].
///
/// [`CircuitChunks`]: tket2::passes::chunks::CircuitChunks
#[pyclass]
#[pyo3(name = "CircuitChunks")]
#[derive(Debug, Clone, From)]
pub struct PyCircuitChunks {
/// Rust representation of the circuit chunks.
pub chunks: CircuitChunks,
/// Whether to reassemble the circuit in the tket1 format.
pub in_tket1: bool,
}

#[pymethods]
impl PyCircuitChunks {
/// Reassemble the chunks into a circuit.
fn reassemble(&self) -> PyResult<Py<PyAny>> {
let hugr = self.clone().chunks.reassemble()?;
Python::with_gil(|py| match self.in_tket1 {
true => Ok(SerialCircuit::encode(&hugr)?.to_tket1(py)?.into_py(py)),
false => Ok(T2Circuit { hugr }.into_py(py)),
})
}

/// Returns clones of the split circuits.
fn circuits(&self) -> PyResult<Vec<Py<PyAny>>> {
self.chunks
.iter()
.map(|hugr| SerialCircuit::encode(hugr)?.to_tket1_with_gil())
.collect()
}

/// Replaces a chunk's circuit with an updated version.
fn update_circuit(&mut self, index: usize, new_circ: Py<PyAny>) -> PyResult<()> {
let hugr = SerialCircuit::from_tket1_with_gil(new_circ)?.decode()?;
if hugr.circuit_signature() != self.chunks[index].circuit_signature() {
return Err(PyAttributeError::new_err(
"The new circuit has a different signature.",
));
}
self.chunks[index] = hugr;
Ok(())
}
}
Loading