Skip to content

feat: gridsynth pass - #1346

Open
km-campbell wants to merge 66 commits into
mainfrom
kc/gridsynth_pass
Open

feat: gridsynth pass#1346
km-campbell wants to merge 66 commits into
mainfrom
kc/gridsynth_pass

Conversation

@km-campbell

Copy link
Copy Markdown

Adding a pass that applies an open source rust implementation of the gridsynth algorithm to all Rz gates in a HUGR. Gridsynth decomposes Rz gates into the Clifford + T basis. Python bindings to enable users to directly modify Guppy-generated HUGRs in Python are included.

As part of the pass, the constant node used to load the angle inputted to the Rz and any intermediary nodes are cleaned up using a garbage collection strategy.

A related demo for the pass is available in the private https://github.com/Quantinuum/gridsynth_guppy_demo.git repo. The demo also functions as a series of manual integration tests for using the pass on Guppy generated HUGRs in Python, albeit using a Jupyter notebook.

@km-campbell
km-campbell requested a review from a team as a code owner January 6, 2026 15:55
@km-campbell
km-campbell requested a review from acl-cqc January 6, 2026 15:55
@codecov

codecov Bot commented Jan 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.75410% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.20%. Comparing base (e70a548) to head (5656a1e).

Files with missing lines Patch % Lines
tket/src/passes/gridsynth.rs 92.17% 14 Missing and 4 partials ⚠️
tket-py/tket/passes/__init__.py 50.00% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1346      +/-   ##
==========================================
+ Coverage   87.17%   87.20%   +0.02%     
==========================================
  Files         201      202       +1     
  Lines       31957    32201     +244     
  Branches    30377    30607     +230     
==========================================
+ Hits        27859    28081     +222     
- Misses       2893     2911      +18     
- Partials     1205     1209       +4     
Flag Coverage Δ
python 91.41% <50.00%> (-0.41%) ⬇️
qis-compiler 74.88% <ø> (ø)
rust 86.98% <92.17%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@PabloAndresCQ
PabloAndresCQ marked this pull request as draft January 6, 2026 16:17
@PabloAndresCQ

Copy link
Copy Markdown
Contributor

I'll have a look through the PR and play with it a bit, then mark it as ready for review when satisfied on my end.

@acl-cqc, I've seen that the Cargo.lock has a bunch of new packages pulled by the rsgridsynth dependency. Is this OK, or do you want to keep dependencies in tket2 to a minimum?

Comment thread tket-py/tket/passes.py Outdated
# is not run first then Gridsynth is likely to fail. Maybe issue the warning if
# the option to run NormalizeGuppy is set to False. The option would be specified
# as a field of the dataclass (would also need to add @dataclass decorator)
# like for NormalizeGuppy above

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine that NormalizeGuppy is always run at the start of the pass, as it is a pre-requesite. Is it a problem if NormalizeGuppy is run more than once? I expect it should leave the HUGR unchanged (I haven't checked).

Comment thread tket-py/tket/passes.py Outdated
# like for NormalizeGuppy above
def run(self, hugr: Hugr, *, inplace: bool = True) -> PassResult:
# inplace option does nothing for now but I retain for consistency of
# API with other passes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect we don't need to do anything special with inplace here, is it not enough to delegate to implement_pass_run as done below?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, implement_pass_run takes care of dealing with the missing case.

Comment thread tket/src/passes/gridsynth.rs Outdated
fn add_references(&mut self, node: Node, increment: usize) {
// if reference not in references add it with the default value 1, else increment count
let count = self.references.entry(node.index()).or_insert(1);
*count += increment;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering that you are incremeting by increment later, shouldn't you use default value 0 when the entry does not exist?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I have looked at the HUGRs that come out of the pass, and the constant nodes are not garbage collected. Likely due to this.

Comment thread tket/src/passes/gridsynth.rs Outdated
Comment thread tket/src/passes/gridsynth.rs Outdated
}

/// If there are no references remaining to const_node, remove it and the nodes leading to it
fn collect(&mut self, hugr: &mut Hugr, const_node: Node) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunate name: collect in Rust is used as a method that converts an iterable to some data structure. Sure, here you meant to say "request garbage collection", but probably a different name should be used. Maybe clear_garbage.

Comment thread tket/src/passes/gridsynth.rs Outdated
Comment thread tket/src/passes/gridsynth.rs Outdated
// value: reference counter for that node
path: HashMap<usize, Vec<Node>>, // key: node index (of Const node containing angle),
// value: the nodes leading up to the constant node and the constant
// node itself

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is a single Vec<Node> per Const node key in path, it implies that there is an assumption that this constant node is used in a single place (otherwise there'd be multiple paths!). As such, it follows that the values of references should always be 1.

I don't see the point of using a GarbageCollector unless we admit the same Const node being used in multiple places in the HUGR.

Comment thread tket/src/passes/gridsynth.rs Outdated
Comment thread tket/src/passes/gridsynth.rs Outdated
Comment thread tket/src/passes/gridsynth.rs Outdated
Merging into the open draft PR #1346 with some improvements

@PabloAndresCQ PabloAndresCQ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a case that broke in the current state of the gridsynth PR. If we have something like this:

@guppy
def foo():
  // Interesting quantum program here

@guppy
def bar():
  foo()

If you run Gridsynth on foo.compile() it works, but if you run it on bar.foo() it doesn't. It complains with a "index out of bounds: len 0 but index 0" coming from the Rust code. A workaround is to call inline_acyclic to inline every function. I do not know what's actually causing this, it'd be good to solve it with a less drastic approach.

Also, this should be a simple unit test to include.

# Conflicts:
#	Cargo.lock
#	tket-py/src/passes.rs
#	tket-py/tket/passes.py
#	tket/Cargo.toml
#	tket/src/passes.rs
#	tket1-passes/conanfile.txt
# Conflicts:
#	Cargo.lock
#	tket-py/pyproject.toml
#	tket/Cargo.toml
#	uv.lock
@chloe-quantinuum

Copy link
Copy Markdown

Comments on the updated GridSynth pass and when it succeeds or fails:

The pass is designed to succeed if all rotation angles can be known statically, and throw an UndefinedAngleError otherwise. For now the pass only makes a naive attempt at restructuring the HUGR to identify rotation angles by calling InlineFunctionsPass and Normalize. This allows the pass to be run on HUGRs generated from guppy with nested functions, such as:

@guppy
def apply_rz(q: qubit, theta: float) -> None:
    rz(q, angle(theta))

@guppy
def main() -> None:
    theta = 0.5
    q = qubit()
    apply_rz(q, theta)
    discard(q)

program = main.compile()

or guppy with certain types of classical control flow such as an angle dependent on an if statement where the rotation is applied locally to the branches:

@guppy
def main() -> None:
    q1 = qubit()
    q2 = qubit()
    h(q1)
    theta = angle(0.5)
    if measure(q1):
        rz(q2, theta)
    else:
        rz(q2, -theta)
    discard(q2)


program = main.compile()

However, if for example we have an angle dependent on an if statement where the rotation is applied outside the branches, such as:

@guppy
def main() -> None:
    q1 = qubit()
    q2 = qubit()
    h(q1)
    result = measure(q1)
    if result:
        theta = angle(0.5)
    else:
        theta = angle(1.0)
    rz(q2, theta)
    discard(q2)


program = main.compile()

the pass will not be able to determine the angle and throw an UndefinedAngleError. To resolve such issues we can either:

  • create more pre-processing passes to flatten the program
  • feedback information to users about how they can rewrite their programs - basically make them flatten it for us 😅

I'm not sure how much of a priority this would be (provided we get runtime gridsynth), but the work would likely have a lot of overlap with potential future work to further flatten the HUGR for optimisation purposes. The main passes that would be useful are loop unrolling, and a variant of hoisting that transforms

if condition:
    C1;
else:
    C2;
C3;

into

if condition:
    C1;
    C3;
else:
    C2;
    C3;

Unfortunately, both of these passes would make the size of the HUGR explode.

@chloe-quantinuum
chloe-quantinuum marked this pull request as ready for review September 3, 2026 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants