From 20e1549be435d9d7b9e5b6b878d8120b33d0f211 Mon Sep 17 00:00:00 2001 From: Richie Yeung Date: Sat, 12 Apr 2025 01:21:44 +0100 Subject: [PATCH 1/4] feat: implement PyCliffordTableau --- src/data_structures/clifford_tableau.rs | 2 +- synpy/Cargo.lock | 1 + synpy/Cargo.toml | 1 + synpy/python/synpy/qiskit/plugin.py | 28 +++++++++ synpy/python/synpy/synpy_rust.pyi | 10 +++ synpy/python/synpy/utils.py | 17 ++++++ synpy/src/lib.rs | 5 +- synpy/src/tableau.rs | 81 +++++++++++++++++++++++++ 8 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 synpy/python/synpy/qiskit/plugin.py create mode 100644 synpy/src/tableau.rs diff --git a/src/data_structures/clifford_tableau.rs b/src/data_structures/clifford_tableau.rs index 2a056fff..95949855 100644 --- a/src/data_structures/clifford_tableau.rs +++ b/src/data_structures/clifford_tableau.rs @@ -55,7 +55,7 @@ impl CliffordTableau { &self.pauli_columns[i] } - pub(crate) fn compose(&self, rhs: &Self) -> Self { + pub fn compose(&self, rhs: &Self) -> Self { rhs.prepend(self) } diff --git a/synpy/Cargo.lock b/synpy/Cargo.lock index ac5a14ff..e173122f 100644 --- a/synpy/Cargo.lock +++ b/synpy/Cargo.lock @@ -234,6 +234,7 @@ dependencies = [ name = "synpy" version = "0.1.0" dependencies = [ + "bitvec", "pyo3", "syn 0.1.0", ] diff --git a/synpy/Cargo.toml b/synpy/Cargo.toml index 1f3b7ba3..8ab4bee4 100644 --- a/synpy/Cargo.toml +++ b/synpy/Cargo.toml @@ -8,5 +8,6 @@ name = "synpy_rust" crate-type = ["cdylib"] [dependencies] +bitvec = "1.0.1" pyo3 = "0.23.3" syn = { path = "../" } diff --git a/synpy/python/synpy/qiskit/plugin.py b/synpy/python/synpy/qiskit/plugin.py new file mode 100644 index 00000000..0373d59a --- /dev/null +++ b/synpy/python/synpy/qiskit/plugin.py @@ -0,0 +1,28 @@ +from qiskit.transpiler import CouplingMap, Target +from qiskit.transpiler.passes.synthesis.plugin import HighLevelSynthesisPlugin +from qiskit.quantum_info import Clifford +from qiskit import QuantumCircuit + +from synpy.synpy_rust import PyCliffordTableau +from synpy.utils import pycommand_to_qasm + + +class SynPyPlugin(HighLevelSynthesisPlugin): + def __init__(self) -> None: + super().__init__() + + def run(self, clifford: Clifford, coupling_map: CouplingMap, target: Target, qubits: list) -> QuantumCircuit: + n = clifford.num_qubits + tableau_x = clifford.tableau[:, :n] + tableau_z = clifford.tableau[:, n : 2 * n] + + pauli_columns = ["".join(["IZXY"[2 * x + z] for x, z in zip(col_x, col_z)]) for col_x, col_z in zip(tableau_x.T, tableau_z.T)] + signs = clifford.tableau[:, -1].tolist() + + # Convert Clifford to CliffordTableau + synpy_tableau = PyCliffordTableau.from_parts(pauli_columns, signs) + + # Synthesize CliffordTableau + commands = synpy_tableau.synthesize() + qasm = pycommand_to_qasm(n, commands) + return QuantumCircuit.from_qasm_str(qasm) diff --git a/synpy/python/synpy/synpy_rust.pyi b/synpy/python/synpy/synpy_rust.pyi index 3215557f..a0abcc4d 100644 --- a/synpy/python/synpy/synpy_rust.pyi +++ b/synpy/python/synpy/synpy_rust.pyi @@ -28,7 +28,17 @@ class PyPauliString(object): def __init__(self, *args: Any, **kwargs: Any) -> None: ... def __new__(*args: Any, **kwargs: Any) -> Any: ... +class PyCliffordTableau(object): + def __init__(self, *args: Any, **kwargs: Any) -> None: ... + def __new__(*args: Any, **kwargs: Any) -> Any: ... + @classmethod + def from_parts(cls, *args: Any, **kwargs: Any) -> Any: ... + def synthesize(self, *args: Any, **kwargs: Any) -> Any: ... + def size(self, *args: Any, **kwargs: Any) -> Any: ... + def compose(self, *args: Any, **kwargs: Any) -> Any: ... + __all__ = [ + "PyCliffordTableau", "PyPauliString", "PyCommand", "synthesize_pauli_exponential", diff --git a/synpy/python/synpy/utils.py b/synpy/python/synpy/utils.py index e7a2587a..422ebf01 100644 --- a/synpy/python/synpy/utils.py +++ b/synpy/python/synpy/utils.py @@ -64,3 +64,20 @@ def pycommand_to_tuple(cmd: PyCommand) -> tuple: return "CZ", cmd[0], cmd[1] else: raise ValueError("Unhandled PyCommand variant: " + str(type(cmd))) + + +def pycommand_to_qasm(n_qubits: int, commands: list[PyCommand]) -> str: + out = [ + "OPENQASM 2.0;", + 'include "qelib1.inc";', + f"qreg q[{n_qubits}];", + ] + + for command in commands: + op, *args = pycommand_to_tuple(command) + op = op.lower() + args = [f"q[{arg}]" for arg in args] + s = f'{op} {", ".join(args)};' + out.append(s) + + return "\n".join(out) diff --git a/synpy/src/lib.rs b/synpy/src/lib.rs index ee88f666..09533ded 100644 --- a/synpy/src/lib.rs +++ b/synpy/src/lib.rs @@ -1,8 +1,10 @@ mod synthesis; +mod tableau; mod validation; use crate::synthesis::synthesize_pauli_exponential; use crate::synthesis::{PyCommand, PyPauliString}; +use crate::tableau::PyCliffordTableau; use pyo3::prelude::{PyModule, PyModuleMethods}; use pyo3::{pymodule, wrap_pyfunction, Bound, FromPyObject, PyRef, PyResult, Python}; use std::ops::Deref; @@ -14,6 +16,7 @@ fn synpy_rust(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; let _ = m.add_function(wrap_pyfunction!(synthesize_pauli_exponential, m)?); + m.add_class::()?; Ok(()) -} +} \ No newline at end of file diff --git a/synpy/src/tableau.rs b/synpy/src/tableau.rs new file mode 100644 index 00000000..670826d6 --- /dev/null +++ b/synpy/src/tableau.rs @@ -0,0 +1,81 @@ +use crate::synthesis::{CommandCollector, PyCommand}; + +use bitvec::prelude::BitVec; +use pyo3::basic::CompareOp; +use pyo3::prelude::*; +use pyo3::PyResult; +use syn::data_structures::CliffordTableau as CliffordTableau; +use syn::data_structures::PauliString; +use syn::data_structures::PropagateClifford; + +use syn::ir::clifford_tableau::NaiveCliffordSynthesizer; +use syn::ir::Synthesizer; + +#[pyclass(unsendable)] +pub struct PyCliffordTableau { + tableau: CliffordTableau, +} + +#[pymethods] +impl PyCliffordTableau { + #[new] + fn new(n: usize) -> Self { + PyCliffordTableau { + tableau: CliffordTableau::new(n), + } + } + + #[staticmethod] + pub fn from_parts(pauli_strings: Vec, signs: Vec) -> Self { + let pauli_columns: Vec = pauli_strings + .iter() + .map(|pauli_string| PauliString::from_text(pauli_string)) + .collect(); + let signs_bitvec: BitVec = signs.iter().copied().collect(); + let tableau = CliffordTableau::from_parts(pauli_columns, signs_bitvec); + + PyCliffordTableau { tableau } + } + + pub fn size(&self) -> usize { + self.tableau.size() + } + + pub(crate) fn compose(&self, rhs: &Self) -> Self { + PyCliffordTableau { + tableau: self.tableau.compose(&rhs.tableau), + } + } + + fn __richcmp__(&self, other: &PyCliffordTableau, op: CompareOp) -> PyResult { + match op { + CompareOp::Eq => Ok(self.tableau == other.tableau), + CompareOp::Ne => Ok(self.tableau != other.tableau), + _ => Ok(false), + } + } + + pub fn synthesize(&self) -> Vec { + let mut tracker = CommandCollector::new(); + let mut synthesizer = NaiveCliffordSynthesizer::default(); + synthesizer.synthesize(self.tableau.clone(), &mut tracker); + tracker.commands() + } +} + +impl PropagateClifford for PyCliffordTableau { + fn cx(&mut self, control: usize, target: usize) -> &mut Self { + self.tableau.cx(control, target); + self + } + + fn s(&mut self, target: usize) -> &mut Self { + self.tableau.s(target); + self + } + + fn v(&mut self, target: usize) -> &mut Self { + self.tableau.v(target); + self + } +} \ No newline at end of file From baf973a29c6792f1c929adfee350e920a72a3ef4 Mon Sep 17 00:00:00 2001 From: Richie Yeung Date: Sun, 13 Apr 2025 19:49:19 +0100 Subject: [PATCH 2/4] chore: clean imports --- synpy/src/lib.rs | 4 +--- synpy/src/synthesis.rs | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/synpy/src/lib.rs b/synpy/src/lib.rs index 09533ded..37c89047 100644 --- a/synpy/src/lib.rs +++ b/synpy/src/lib.rs @@ -6,9 +6,7 @@ use crate::synthesis::synthesize_pauli_exponential; use crate::synthesis::{PyCommand, PyPauliString}; use crate::tableau::PyCliffordTableau; use pyo3::prelude::{PyModule, PyModuleMethods}; -use pyo3::{pymodule, wrap_pyfunction, Bound, FromPyObject, PyRef, PyResult, Python}; -use std::ops::Deref; -use syn::ir::Synthesizer; +use pyo3::{pymodule, wrap_pyfunction, Bound, PyResult}; #[pymodule] #[pyo3(name = "synpy_rust")] diff --git a/synpy/src/synthesis.rs b/synpy/src/synthesis.rs index 601eaa25..5c6f7ff8 100644 --- a/synpy/src/synthesis.rs +++ b/synpy/src/synthesis.rs @@ -1,7 +1,5 @@ use std::ops::Deref; -use pyo3::{pyfunction, Py}; -use crate::PyRef; -use crate::Python; +use pyo3::{pyfunction, PyRef, PyResult}; use pyo3::exceptions::PyException; use pyo3::{pyclass, pymethods, PyErr}; use syn::data_structures::{CliffordTableau, PauliPolynomial}; @@ -11,7 +9,6 @@ use syn::ir::CliffordGates; use syn::ir::Gates; use syn::ir::pauli_exponential::{PauliExponential, PauliExponentialSynthesizer}; use syn::ir::pauli_polynomial::PauliPolynomialSynthStrategy; -use crate::PyResult; use crate::validation::validate; use syn::ir::Synthesizer; From 2e8d031af62364717ce1f2fd9f2c473c311a3161 Mon Sep 17 00:00:00 2001 From: Richie Yeung Date: Mon, 11 Aug 2025 12:10:23 +0100 Subject: [PATCH 3/4] Address comments and add test --- synpy/integration_tests/test_qiskit.py | 17 +++++++++++++++++ synpy/src/synthesis.rs | 4 ++++ synpy/src/tableau.rs | 16 +++++++++++----- 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 synpy/integration_tests/test_qiskit.py diff --git a/synpy/integration_tests/test_qiskit.py b/synpy/integration_tests/test_qiskit.py new file mode 100644 index 00000000..0969f1f4 --- /dev/null +++ b/synpy/integration_tests/test_qiskit.py @@ -0,0 +1,17 @@ +from qiskit.quantum_info import Clifford +from qiskit import QuantumCircuit + +from synpy.qiskit.plugin import SynPyPlugin + + +def test_qiskit_bell() -> None: + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + cliff = Clifford(qc) + + plugin = SynPyPlugin() + circ = plugin.run(cliff, None, None, []) + # circ.draw() + + assert circ == qc diff --git a/synpy/src/synthesis.rs b/synpy/src/synthesis.rs index 5c6f7ff8..a31e6f19 100644 --- a/synpy/src/synthesis.rs +++ b/synpy/src/synthesis.rs @@ -96,6 +96,10 @@ pub fn parse_clifford_commands( Ok(tableau) } +pub trait Synthesize { + fn synthesize(&self) -> Vec; +} + #[pyclass] #[derive(Clone)] pub struct PyPauliString { diff --git a/synpy/src/tableau.rs b/synpy/src/tableau.rs index 670826d6..e56f2e86 100644 --- a/synpy/src/tableau.rs +++ b/synpy/src/tableau.rs @@ -1,4 +1,4 @@ -use crate::synthesis::{CommandCollector, PyCommand}; +use crate::synthesis::{CommandCollector, PyCommand, Synthesize}; use bitvec::prelude::BitVec; use pyo3::basic::CompareOp; @@ -56,10 +56,7 @@ impl PyCliffordTableau { } pub fn synthesize(&self) -> Vec { - let mut tracker = CommandCollector::new(); - let mut synthesizer = NaiveCliffordSynthesizer::default(); - synthesizer.synthesize(self.tableau.clone(), &mut tracker); - tracker.commands() + ::synthesize(self) } } @@ -78,4 +75,13 @@ impl PropagateClifford for PyCliffordTableau { self.tableau.v(target); self } +} + +impl Synthesize for PyCliffordTableau { + fn synthesize(&self) -> Vec { + let mut tracker = CommandCollector::new(); + let mut synthesizer = NaiveCliffordSynthesizer::default(); + synthesizer.synthesize(self.tableau.clone(), &mut tracker); + tracker.commands() + } } \ No newline at end of file From d054242c27788c4ed5e0d495741f4e1b9e871739 Mon Sep 17 00:00:00 2001 From: Richie Yeung Date: Tue, 12 Aug 2025 16:34:45 +0100 Subject: [PATCH 4/4] Rename plugin --- synpy/integration_tests/test_qiskit.py | 4 ++-- synpy/python/synpy/qiskit/plugin.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/synpy/integration_tests/test_qiskit.py b/synpy/integration_tests/test_qiskit.py index 0969f1f4..3df06263 100644 --- a/synpy/integration_tests/test_qiskit.py +++ b/synpy/integration_tests/test_qiskit.py @@ -1,7 +1,7 @@ from qiskit.quantum_info import Clifford from qiskit import QuantumCircuit -from synpy.qiskit.plugin import SynPyPlugin +from synpy.qiskit.plugin import SynPyCliffordPlugin def test_qiskit_bell() -> None: @@ -10,7 +10,7 @@ def test_qiskit_bell() -> None: qc.cx(0, 1) cliff = Clifford(qc) - plugin = SynPyPlugin() + plugin = SynPyCliffordPlugin() circ = plugin.run(cliff, None, None, []) # circ.draw() diff --git a/synpy/python/synpy/qiskit/plugin.py b/synpy/python/synpy/qiskit/plugin.py index 0373d59a..d41ac09a 100644 --- a/synpy/python/synpy/qiskit/plugin.py +++ b/synpy/python/synpy/qiskit/plugin.py @@ -7,7 +7,7 @@ from synpy.utils import pycommand_to_qasm -class SynPyPlugin(HighLevelSynthesisPlugin): +class SynPyCliffordPlugin(HighLevelSynthesisPlugin): def __init__(self) -> None: super().__init__()