Skip to content

Commit 2737ab7

Browse files
chloe-quantinuumsjdilkescqc-alec
authored
feat: Pauli Graph Resynthesis (#2020)
Implements a tket2 pass that takes flat HUGR regions, converts them to pauli graphs, applies the greedy resynth pass, and converts the resulting pauli graph back into a HUGR. Note: currently the result of greedy resynth does not simplify single qubit gate sequences so it might be useful to add a simple single qubit gate merging pass after resynthesis in the future (`RotationMergingPass` is only for pauli graphs in canonical form) --------- Co-authored-by: Silas Dilkes <36165522+sjdilkes@users.noreply.github.com> Co-authored-by: Alec Edgington <54802828+cqc-alec@users.noreply.github.com>
1 parent f734367 commit 2737ab7

11 files changed

Lines changed: 2040 additions & 391 deletions

File tree

‎Cargo.lock‎

Lines changed: 231 additions & 390 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ cool_asserts = "2.0.4"
121121
zstd = "0.14.0"
122122
anyhow = "1.0.102"
123123
num-rational = "0.4.2"
124+
pg-core = { package = "tk-pg-core", path = "pg-libs/tk-pg-core", version = "0.1.0" }
125+
pg-canonical-form = { package = "tk-pg-canonical-form", path = "pg-libs/pg-passes/tk-pg-canonical-form", version = "0.1.0" }
126+
pg-greedy-synth = { package = "tk-pg-greedy-synth", path = "pg-libs/pg-passes/tk-pg-greedy-synth", version = "0.1.0" }
127+
pg-optimise = { package = "tk-pg-optimize", path = "pg-libs/pg-passes/tk-pg-optimize", version = "0.1.0" }
128+
pg-rebase = { package = "tk-pg-rebase", path = "pg-libs/pg-passes/tk-pg-rebase", version = "0.1.0" }
124129

125130
[profile.release.package.tket-py]
126131
# Some configurations to reduce the size of tket wheels

‎tket/tket-py/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ tket-qsystem = { path = "../tket-qsystem", version = "0.29.0" }
2828
tket1-passes = { path = "../tket1-passes", version = "0.0.0" }
2929

3030
derive_more = { workspace = true, features = ["into", "from"] }
31+
pg-greedy-synth = { workspace = true }
3132
hugr = { workspace = true }
3233
itertools = { workspace = true }
3334
num_cpus = { workspace = true }

‎tket/tket-py/src/passes.rs‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub fn module(py: Python<'_>) -> PyResult<Bound<'_, PyModule>> {
3434
m.add_class::<self::chunks::PyCircuitChunks>()?;
3535
m.add_function(wrap_pyfunction!(self::chunks::chunks, &m)?)?;
3636
m.add_function(wrap_pyfunction!(self::tket1::tket1_pass, &m)?)?;
37+
m.add_function(wrap_pyfunction!(pauli_graph_resynthesis, &m)?)?;
3738
m.add_function(wrap_pyfunction!(resolve_modifiers, &m)?)?;
3839
m.add_function(wrap_pyfunction!(qsystem::qsystem_rebase_pass, &m)?)?;
3940
m.add_function(wrap_pyfunction!(qsystem::qsystem_llvm_pass, &m)?)?;
@@ -43,6 +44,10 @@ pub fn module(py: Python<'_>) -> PyResult<Bound<'_, PyModule>> {
4344
py.get_type::<PyInlineFunctionsError>(),
4445
)?;
4546
m.add("TK1PassError", py.get_type::<tket1::PytketPassError>())?;
47+
m.add(
48+
"PauliGraphResynthesisError",
49+
py.get_type::<PauliGraphResynthesisError>(),
50+
)?;
4651
Ok(m)
4752
}
4853

@@ -64,6 +69,12 @@ create_py_exception!(
6469
"Errors from the modifer resolver pass."
6570
);
6671

72+
create_py_exception!(
73+
tket::passes::pauli_graph_resynthesis::PauliGraphResynthesisErrors,
74+
PauliGraphResynthesisError,
75+
"Errors from the Pauli graph resynthesis pass."
76+
);
77+
6778
create_py_exception!(
6879
tket::passes::inline_funcs::InlineFuncsError,
6980
PyInlineFunctionsError,
@@ -230,3 +241,61 @@ fn resolve_modifiers(circ: &mut CompilationState, scope: Option<PyPassScope>) ->
230241
pass.run(&mut circ.hugr).convert_pyerrs()?;
231242
Ok(())
232243
}
244+
245+
struct PyParallelMode(pg_greedy_synth::ParallelMode);
246+
247+
impl<'a, 'py> FromPyObject<'a, 'py> for PyParallelMode {
248+
type Error = PyErr;
249+
250+
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
251+
use pg_greedy_synth::ParallelMode;
252+
253+
let enum_type = ob.py().import("tket.passes")?.getattr("ParallelMode")?;
254+
255+
for (name, mode) in [
256+
("Auto", ParallelMode::Auto),
257+
("On", ParallelMode::On),
258+
("Off", ParallelMode::Off),
259+
] {
260+
if ob.is(&enum_type.getattr(name)?) {
261+
return Ok(Self(mode));
262+
}
263+
}
264+
265+
Err(pyo3::exceptions::PyTypeError::new_err(
266+
"parallel_mode must be an instance of the ParallelMode enum",
267+
))
268+
}
269+
}
270+
271+
#[pyfunction]
272+
#[pyo3(signature = (circ, scope = None, window_size=None, pool_size=None, top_up_size=None, seed=None, parallel_mode=None))]
273+
fn pauli_graph_resynthesis(
274+
circ: &mut CompilationState,
275+
scope: Option<PyPassScope>,
276+
window_size: Option<usize>,
277+
pool_size: Option<usize>,
278+
top_up_size: Option<usize>,
279+
seed: Option<usize>,
280+
parallel_mode: Option<PyParallelMode>,
281+
) -> PyResult<()> {
282+
let py_scope = scope.unwrap_or_default();
283+
let mut pass = tket::passes::PauliGraphResynthesis::default_with_scope(py_scope.scope);
284+
if let Some(ws) = window_size {
285+
pass = pass.with_window_size(ws);
286+
}
287+
if let Some(ps) = pool_size {
288+
pass = pass.with_pool_size(ps);
289+
}
290+
if let Some(tus) = top_up_size {
291+
pass = pass.with_top_up_size(tus);
292+
}
293+
if let Some(s) = seed {
294+
pass = pass.with_seed(s as u64);
295+
}
296+
let parallel_mode = parallel_mode.map_or(pg_greedy_synth::ParallelMode::Auto, |mode| mode.0);
297+
pass = pass.with_parallel_mode(parallel_mode);
298+
299+
pass.run(&mut circ.hugr).convert_pyerrs()?;
300+
Ok(())
301+
}

‎tket/tket-py/test/test_pass.py‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections.abc import Callable
55
from pathlib import Path
66
from typing import Any
7+
from unittest.mock import ANY, patch
78

89
import hypothesis.strategies as st
910
import numpy as np
@@ -17,12 +18,16 @@
1718
from tket._ops import TketOp
1819
from tket._pattern import Rule, RuleMatcher
1920
from tket._state import CompilationState
21+
from tket._state.build import H, from_coms
22+
from tket._tket import passes as rust_passes
2023
from tket.passes import (
2124
GlobalScope,
2225
InlineFunctions,
2326
ModifierResolverPass,
2427
Normalize,
2528
NormalizeGuppy,
29+
ParallelMode,
30+
PauliGraphResynthesis,
2631
PlatformTarget,
2732
PytketHugrPass,
2833
QSystemRebasePass,
@@ -507,3 +512,94 @@ def test_python_qsystem_pass_with_modifiers() -> None:
507512
except Exception as exc: # noqa: BLE001
508513
failures.append(f"{hugr_path}: {exc}")
509514
assert not failures, "QSystem pass failures:\n" + "\n".join(failures)
515+
516+
517+
def _count_hadamards(hugr: Hugr) -> int:
518+
return sum(
519+
data.op.name().rsplit(".", maxsplit=1)[-1] == "H" for _, data in hugr.nodes()
520+
)
521+
522+
523+
def test_resynthesis_with_default_options() -> None:
524+
hugr = from_coms(H(0), H(0)).to_python().modules[0]
525+
optimisation = PauliGraphResynthesis()
526+
527+
result = optimisation.run(hugr, inplace=False)
528+
529+
assert optimisation.parallel_mode is ParallelMode.Auto
530+
assert optimisation.window_size is None
531+
assert optimisation.pool_size is None
532+
assert optimisation.top_up_size is None
533+
assert optimisation.seed is None
534+
assert result.results == [("PauliGraphResynthesis", None)]
535+
assert _count_hadamards(result.hugr) == 0
536+
assert _count_hadamards(hugr) == 2
537+
CompilationState.from_python(result.hugr).validate()
538+
539+
540+
def test_custom_options_and_scope() -> None:
541+
hugr = from_coms(H(0), H(0)).to_python().modules[0]
542+
optimisation = PauliGraphResynthesis(
543+
window_size=16,
544+
pool_size=32,
545+
top_up_size=4,
546+
seed=7,
547+
parallel_mode=ParallelMode.On,
548+
)
549+
assert optimisation.with_scope(GlobalScope.PRESERVE_ALL) is optimisation
550+
551+
with patch.object(
552+
rust_passes,
553+
"pauli_graph_resynthesis",
554+
wraps=rust_passes.pauli_graph_resynthesis,
555+
) as resynthesis:
556+
result = optimisation.run(hugr, inplace=False)
557+
558+
resynthesis.assert_called_once_with(
559+
ANY,
560+
scope=GlobalScope.PRESERVE_ALL,
561+
window_size=16,
562+
pool_size=32,
563+
top_up_size=4,
564+
seed=7,
565+
parallel_mode=ParallelMode.On,
566+
)
567+
assert _count_hadamards(result.hugr) == 0
568+
569+
570+
@pytest.mark.parametrize(
571+
("parameter", "value", "message"),
572+
[
573+
("window_size", -1, "window_size must be positive"),
574+
("window_size", 0, "window_size must be positive"),
575+
("pool_size", -1, "pool_size must be positive"),
576+
("pool_size", 0, "pool_size must be positive"),
577+
("top_up_size", -1, "top_up_size must be positive"),
578+
("top_up_size", 0, "top_up_size must be positive"),
579+
("seed", -1, "seed must be non-negative"),
580+
],
581+
)
582+
def test_resynthesis_rejects_invalid_parameters(
583+
parameter: str, value: int, message: str
584+
) -> None:
585+
hugr = from_coms(H(0), H(0)).to_python().modules[0]
586+
options: dict[str, Any] = {parameter: value}
587+
588+
with pytest.raises(ValueError, match=f"^{message}$"):
589+
PauliGraphResynthesis(**options)
590+
591+
optimisation = PauliGraphResynthesis()
592+
setattr(optimisation, parameter, value)
593+
594+
with pytest.raises(ValueError, match=f"^{message}$"):
595+
optimisation.run(hugr, inplace=True)
596+
597+
assert _count_hadamards(hugr) == 2
598+
599+
600+
def test_wrapper_requires_enum() -> None:
601+
invalid_mode: Any = "on"
602+
with pytest.raises(
603+
TypeError, match="parallel_mode must be an instance of the ParallelMode enum"
604+
):
605+
PauliGraphResynthesis(parallel_mode=invalid_mode)

‎tket/tket-py/tket/_tket/passes.pyi‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from typing import Literal
33

44
from hugr.passes.scope import GlobalScope, PassScope
55

6-
from ..passes import inline_funcs
6+
from ..passes import ParallelMode, inline_funcs
77
from .optimiser import BadgerOptimiser
88
from .state import CompilationState
99

@@ -20,6 +20,9 @@ class CircuitChunks:
2020
class PullForwardError(Exception):
2121
"""Error from a `PullForward` operation."""
2222

23+
class PauliGraphResynthesisError(Exception):
24+
"""Error from the Pauli graph resynthesis pass."""
25+
2326
# ruff: ignore[B008]
2427
def normalize_guppy(
2528
circ: CompilationState,
@@ -102,6 +105,36 @@ def tket1_pass(
102105
Defaults to the platform-agnostic ``"tket"`` target.
103106
"""
104107

108+
def pauli_graph_resynthesis(
109+
circ: CompilationState,
110+
scope: PassScope = GlobalScope.PRESERVE_PUBLIC,
111+
window_size: int | None = None,
112+
pool_size: int | None = None,
113+
top_up_size: int | None = None,
114+
seed: int | None = None,
115+
parallel_mode: ParallelMode | None = None,
116+
) -> None:
117+
"""
118+
Resynthesizes a Clifford + Rz circuit by converting it to a Pauli graph and applying various
119+
optimisation techniques such as:
120+
- phase folding
121+
- a synthesis algorithm from pauli graph to Clifford + Rz aimed at reducing the number of 2
122+
qubit gates
123+
124+
Rotation angles must be numeric as symbolic angles are not supported currently.
125+
126+
Parameters:
127+
- window_size: Sets the size of the sliding window used for lookahead during synthesis.
128+
- pool_size: Sets the number of candidate gates to maintain in the pool.
129+
- top_up_size: Sets the number of candidate gates to add after each TQE gate.
130+
- seed: Sets the random seed used to sample candidate gates.
131+
- parallel_mode: ParallelMode.Auto, ParallelMode.On, or ParallelMode.Off.
132+
Omitted or None selects ParallelMode.Auto.
133+
134+
Raises:
135+
PauliGraphResynthesisError: If the resynthesis pass fails.
136+
"""
137+
105138
def resolve_modifiers(
106139
circ: CompilationState, scope: PassScope = GlobalScope.PRESERVE_PUBLIC
107140
) -> None:

‎tket/tket-py/tket/passes/__init__.py‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,24 @@
3535
"ModifierResolverPass",
3636
"Normalize",
3737
"NormalizeGuppy",
38+
"ParallelMode",
3839
"PassResult",
40+
"PauliGraphResynthesis",
3941
"PlatformTarget",
4042
"PytketHugrPass",
4143
"QSystemRebasePass",
4244
"_QSystemLLVMPass",
4345
]
4446

4547

48+
class ParallelMode(Enum):
49+
"""Parallel processing mode for Pauli graph resynthesis."""
50+
51+
Auto = "Auto" # Let synthesis choose when to use parallel processing.
52+
On = "On" # Enable parallel processing.
53+
Off = "Off" # Disable parallel processing.
54+
55+
4656
class PlatformTarget(Enum):
4757
"""A hardware platform that passes can target.
4858
@@ -505,6 +515,79 @@ def _run_tk(self, program: _state.CompilationState) -> _state.CompilationState:
505515
return program
506516

507517

518+
@dataclass
519+
class PauliGraphResynthesis(ComposablePass):
520+
"""
521+
An optimisation pass that resynthesizes a Clifford + Rz circuit by converting it to a Pauli Graph
522+
and applying various optimisation techniques such as:
523+
- phase folding
524+
- a synthesis algorithm from Pauli Graph to Clifford + Rz aimed at reducing the number of 2
525+
qubit gates
526+
527+
Rotation angles must be numeric as symbolic angles are not supported currently.
528+
529+
Parameters:
530+
- window_size: Sets the size of the sliding window used for lookahead during synthesis. Must be positive.
531+
- pool_size: Sets the number of candidate gates to maintain in the pool. Must be positive.
532+
- top_up_size: Sets the number of candidate gates to add after each TQE gate. Must be positive.
533+
- seed: Sets the random seed used to sample candidate gates. Must be non-negative.
534+
- parallel_mode: A :class:`ParallelMode` for candidate synthesis.
535+
Defaults to :attr:`ParallelMode.Auto`.
536+
"""
537+
538+
window_size: int | None = None
539+
pool_size: int | None = None
540+
top_up_size: int | None = None
541+
seed: int | None = None
542+
parallel_mode: ParallelMode = ParallelMode.Auto
543+
_scope: PassScope = GlobalScope.PRESERVE_PUBLIC
544+
545+
def __post_init__(self) -> None:
546+
self._validate_parameters()
547+
548+
def _validate_parameters(self) -> None:
549+
for parameter in ("window_size", "pool_size", "top_up_size"):
550+
value = getattr(self, parameter)
551+
if value is not None and value <= 0:
552+
raise ValueError(f"{parameter} must be positive")
553+
if self.seed is not None and self.seed < 0:
554+
raise ValueError("seed must be non-negative")
555+
if not isinstance(self.parallel_mode, ParallelMode):
556+
raise TypeError(
557+
"parallel_mode must be an instance of the ParallelMode enum"
558+
)
559+
560+
def with_scope(self, scope: PassScope) -> PauliGraphResynthesis:
561+
"""Set the scope of this pass and return self."""
562+
self._scope = scope
563+
return self
564+
565+
def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult:
566+
return implement_pass_run(
567+
self,
568+
hugr=hugr,
569+
inplace=inplace,
570+
copy_call=lambda h: self._pauli_graph_resynthesis(h, inplace),
571+
)
572+
573+
def _pauli_graph_resynthesis(self, hugr: Hugr, inplace: bool) -> PassResult:
574+
self._validate_parameters()
575+
program = _state.CompilationState.from_python(hugr)
576+
_passes.pauli_graph_resynthesis(
577+
program._inner,
578+
scope=self._scope,
579+
window_size=self.window_size,
580+
pool_size=self.pool_size,
581+
top_up_size=self.top_up_size,
582+
seed=self.seed,
583+
parallel_mode=self.parallel_mode,
584+
)
585+
package = program.to_python()
586+
return PassResult.for_pass(
587+
self, hugr=package.modules[0], inplace=inplace, result=None
588+
)
589+
590+
508591
@dataclass(kw_only=True)
509592
class _QSystemLLVMPass(ComposablePass):
510593
"""Prepare a QSystem program for LLVM lowering.

0 commit comments

Comments
 (0)