Skip to content

feat: gridsynth pass#1346

Draft
km-campbell wants to merge 50 commits intomainfrom
kc/gridsynth_pass
Draft

feat: gridsynth pass#1346
km-campbell wants to merge 50 commits intomainfrom
kc/gridsynth_pass

Conversation

@km-campbell
Copy link

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
Copy link

codecov bot commented Jan 6, 2026

Codecov Report

❌ Patch coverage is 94.11765% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.97%. Comparing base (85a6250) to head (ce8dd55).
⚠️ Report is 27 commits behind head on main.

Files with missing lines Patch % Lines
tket/src/passes/gridsynth.rs 94.20% 14 Missing and 5 partials ⚠️
tket-py/tket/passes.py 91.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1346      +/-   ##
==========================================
+ Coverage   79.74%   79.97%   +0.23%     
==========================================
  Files         158      159       +1     
  Lines       20509    20869     +360     
  Branches    19542    19868     +326     
==========================================
+ Hits        16355    16691     +336     
- Misses       3173     3192      +19     
- Partials      981      986       +5     
Flag Coverage Δ
python 92.55% <91.66%> (-0.38%) ⬇️
qis-compiler 100.00% <ø> (ø)
rust 79.32% <94.20%> (+0.25%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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
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?

# 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
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).

# 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
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
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.

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
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
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.

}

/// 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
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.

// 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
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.

Merging into the open draft PR #1346 with some improvements
Copy link
Contributor

@PabloAndresCQ PabloAndresCQ left a comment

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.

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.

3 participants