Skip to content

feat: Allow running arbitrary serializable pytket passes on hugrs#1266

Merged
aborgna-q merged 7 commits intomainfrom
ab/pytket-pass-json
Nov 25, 2025
Merged

feat: Allow running arbitrary serializable pytket passes on hugrs#1266
aborgna-q merged 7 commits intomainfrom
ab/pytket-pass-json

Conversation

@aborgna-q
Copy link
Copy Markdown
Collaborator

Bumps the tket-c-api dependency to include Quantinuum/tket#2064 and replaces the pass bindings in tket1-passes with a generic method that takes a serialized pass and applies it.

I kept the three "clifford_simp"/"two_qubit_squash"/"squash_phasedx_rz" pass functions available in tket-py.passes, but they now initialize a pytket pass and call the new generic binding.
The docs are copied directly from pytket's.

Those python functions will be replaced soon with @CalMacCQ's PytketPass definition, so no need to look to much into them.

@aborgna-q aborgna-q requested a review from a team as a code owner November 18, 2025 14:32
@aborgna-q aborgna-q requested a review from acl-cqc November 18, 2025 14:32
Comment on lines +15 to +16
/// JSON encoding of the clifford simp pytket pass.
const CLIFFORD_SIMP_STR: &str = r#"{"StandardPass": {"allow_swaps": true, "name": "CliffordSimp", "target_2qb_gate": "CX"}, "pass_class": "StandardPass"}"#;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We may want to add the pass schema to tket_json_rs and use that here instead (with some helpers for common passes).

All the tket1-passes code is private, so that can be done later without breaking the API

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.

Duplicating strings that I'd rather not see at all in both internal tests and external tests is a bit of a PITA, indeed. (And these strings are the same as those built, much more nicely, in tket-py, right?)

We wouldn't want to publish them from tket1-passes (say) so that we can use them with this repo as a temporary measure, because they'd have to be pub in some crate? (Given the right way to make these strings is currently only available in python, your longer-term solution of tket_json_rs sounds good.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

See the other comment about keeping the unsafe bindings crate small.

If we're defining builders for serialized passes we should put that in tket-json-rs instead.
I added an issue Quantinuum/tket-json-rs#159

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.

Yah OK, maybe there is no better interim solution.

Copy link
Copy Markdown
Collaborator Author

@aborgna-q aborgna-q Nov 18, 2025

Choose a reason for hiding this comment

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

tket-c-api still exposes the specific pass bindings, but we don't need to use them here.

Dropping them from this file lets us reduce the unsafe surface.

@codecov
Copy link
Copy Markdown

codecov bot commented Nov 18, 2025

Codecov Report

❌ Patch coverage is 62.00000% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.44%. Comparing base (baf3b32) to head (3e7c110).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
tket-py/tket/passes.py 50.00% 13 Missing ⚠️
tket1-passes/src/lib.rs 75.00% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1266      +/-   ##
==========================================
- Coverage   79.46%   79.44%   -0.03%     
==========================================
  Files         160      160              
  Lines       20378    20381       +3     
  Branches    19446    19424      -22     
==========================================
- Hits        16194    16192       -2     
- Misses       3201     3205       +4     
- Partials      983      984       +1     
Flag Coverage Δ
python 91.42% <50.00%> (-1.23%) ⬇️
rust 78.83% <75.00%> (+0.01%) ⬆️

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.

"""Split a circuit into chunks of at most `max_chunk_size` gates."""

def clifford_simp(
def tket1_pass(
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.

Would the return type of tket1_pass eventaully be Hugr?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We'll always return a rust-backed Hugr here, so multiple calls can be chained together.

Copy link
Copy Markdown
Contributor

@acl-cqc acl-cqc left a comment

Choose a reason for hiding this comment

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

Looks good to me, but before I approve, I am confused about this business about tket-c-api / conan "configuration"...?

let py = circ.py();
encoded_circ
.par_iter_mut()
.try_for_each(|(_, circ)| -> Result<(), tket1_passes::PassError> {
Copy link
Copy Markdown
Contributor

@acl-cqc acl-cqc Nov 19, 2025

Choose a reason for hiding this comment

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

How about moving this parallel/loop, possibly the whole try_with_circ, into tket1-passes passing a closure Fn(&mut Tket1Circuit) and probably that qsystem_decoder_config()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Into tket1-passes? That crate is just providing bindings and constraining the unsafe surface.

It doesn't know anything about EncodedCircuit (or even the tket crate), I'm not sure we want to grow the library more than necessary.

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.

Ah OK. Hadn't realized quite how narrow tket1-passes is (i.e. that it didn't even reference tket2).

Not for this PR, but I have wonder if tket1-passes even belongs in the tket2 repo TBH. What does it provide - rust wrappers around serialized tket1 json? That sounds like tket-json-rs, no?

Copy link
Copy Markdown
Contributor

@acl-cqc acl-cqc Nov 24, 2025

Choose a reason for hiding this comment

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

I nonetheless wonder whether there is somewhere we could expose a rust function that does this circuit-encoding, run pass everywhere, decode trick. I guess it would have to be the main tket(2) crate, so that it could be used both here and by tket-qsystem (tests)...I admit the latter are only tests but it's always good for the test to exercise more of the actual "production" code path!!

Copy link
Copy Markdown
Collaborator Author

@aborgna-q aborgna-q Nov 24, 2025

Choose a reason for hiding this comment

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

That sounds like tket-json-rs, no?

🤔
Not a bad idea, though it may be annoying to share the custom build + conan config in CI across repos. I'll look into it.

Copy link
Copy Markdown
Collaborator Author

@aborgna-q aborgna-q Nov 24, 2025

Choose a reason for hiding this comment

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

A rust function that does this circuit-encoding, run pass everywhere, decode trick.

Yeah, EncodedCircuit could have a run_everywhere(self, SerialCircuit -> SerialCircuit) call. I added an issue: #1279

tk1_circ.squash_phasedx_rz()
})?;
encoded_circ
.reassemble_inplace(circ.hugr_mut(), Some(Arc::new(qsystem_decoder_config())))
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.

should qsystem_decoder_config() instead be a parameter to fn tket1_pass ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We don't expose the encoding/decoding config to python. There's no reason to offer anything other than the full set of available extensions here.

Due to ABI compatibility, new extension encoder/decoders can only be linked at the rust level so python code cannot add new ones.

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 guess it's the "qsystem" in qsystem_decoder_config that makes me think this might not always be wanted, e.g. if we were compiling for a non-qsystem...(qsystem_encoder_config above similarly).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Uhm, true.

We haven't really decided what to do to support all the pytket operations that are not part of our core set (#430 (comment)). Once/if we do that we may want to choose different configs here...

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.

Is this considered public?

If so I'd add the encoder and/or decoder config as parameters. You don't even need to expose them in python.

If it's not public...then nvm for now; we can add optional extra parameters to the python function later. (Possibly even without making anything qsystem explicitly available, just as a None = default, if we care.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not public, all the rust code in tket-py is used solely for the bindings.

circ: Circuit,
*,
allow_swaps: bool = True,
target_2qb_gate: str = "CX",
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.

str | PyTketOp perhaps?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

There's only CX in TketOp, tk2 is part of qsystem instead.

This is just temporary code waiting for the Pass implementation. I think Callum was going to define an enum for this gate selection there.

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.

sounds good...ok, but do you want this as public api or hidden/internal/etc. somehow?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I only made it public here to avoid removing existing functionality (since the pass call was already being exported).

Callum's #1269 will remove this and replace it with a Pass, I just didn't want to drop the function before that's in.

circ: Circuit,
*,
allow_swaps: bool = True,
target_2qb_gate: str = "CX",
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.

similarly



def clifford_simp(
circ: Circuit,
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 guess the Circuit -> Circuit here might (in future) change to a function returning a ComposeablePass....

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We're dropping this altogether, and defining only the ComposablePass.

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.

Is that gonna be in another PR, then?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines +15 to +16
/// JSON encoding of the clifford simp pytket pass.
const CLIFFORD_SIMP_STR: &str = r#"{"StandardPass": {"allow_swaps": true, "name": "CliffordSimp", "target_2qb_gate": "CX"}, "pass_class": "StandardPass"}"#;
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.

Duplicating strings that I'd rather not see at all in both internal tests and external tests is a bit of a PITA, indeed. (And these strings are the same as those built, much more nicely, in tket-py, right?)

We wouldn't want to publish them from tket1-passes (say) so that we can use them with this repo as a temporary measure, because they'd have to be pub in some crate? (Given the right way to make these strings is currently only available in python, your longer-term solution of tket_json_rs sounds good.)

Copy link
Copy Markdown
Contributor

@acl-cqc acl-cqc left a comment

Choose a reason for hiding this comment

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

Ok, thanks @aborgna-q - this is pretty cool and useful, I think there may be a few rough edges but let's get this in.

tk1_circ.squash_phasedx_rz()
})?;
encoded_circ
.reassemble_inplace(circ.hugr_mut(), Some(Arc::new(qsystem_decoder_config())))
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.

Is this considered public?

If so I'd add the encoder and/or decoder config as parameters. You don't even need to expose them in python.

If it's not public...then nvm for now; we can add optional extra parameters to the python function later. (Possibly even without making anything qsystem explicitly available, just as a None = default, if we care.)



def clifford_simp(
circ: Circuit,
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.

Is that gonna be in another PR, then?

circ: Circuit,
*,
allow_swaps: bool = True,
target_2qb_gate: str = "CX",
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.

sounds good...ok, but do you want this as public api or hidden/internal/etc. somehow?

Parameters:
:param allow_swaps: Whether to allow implicit wire swaps
:param target_2qb_gate: Target two-qubit gate (either CX or TK2)
:param cx_fidelity: Estimated CX gate fidelity, used when `target_2qb_gate` is CX
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.

Of course this raises the possibility of dropping target_2qb_gate and having here target_cx_fidelity: float | None = 1.0 where None means to target TK2 and anything else means to target CX....

that does seem a bit non-obvious tho, happy as is.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

None means to target TK2 and anything else means to target CX

That sounds awful 😅
Again, this is just copying pytket's API for now, but maybe we'll want to have an enum in #1269


- Two-qubit operations can always be expressed in a minimal form using at most three CXs, or as a single TK2 gate (also known as the KAK or Cartan decomposition).
- It is generally recommended to squash to TK2 gates, and then use the DecomposeTK2 pass for noise-aware decomposition to other gate sets.
- For backward compatibility, decompositions to CX are also supported. In this case, `cx_fidelity` can be provided to perform approximate decompositions to CX gates.
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.

It does seem a little odd for the default parameters to be those for the backward compatibility mode....

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That's verbatim from pytket docs.

I promise well clean it up in the pass update :)

@CalMacCQ
Copy link
Copy Markdown
Contributor

Nice, as soon as this is merged I can finish #1269

@aborgna-q aborgna-q added this pull request to the merge queue Nov 25, 2025
Merged via the queue into main with commit 24875e0 Nov 25, 2025
36 of 38 checks passed
@aborgna-q aborgna-q deleted the ab/pytket-pass-json branch November 25, 2025 08:38
This was referenced Nov 25, 2025
github-merge-queue bot pushed a commit that referenced this pull request Dec 10, 2025
🤖 I have created a release *beep* *boop*
---


##
[0.12.13](tket-py-v0.12.12...tket-py-v0.12.13)
(2025-12-10)


### Features

* Allow running arbitrary serializable pytket passes on hugrs
([#1266](#1266))
([24875e0](24875e0))
* do constant folding by default in NormalizeGuppy
([#1309](#1309))
([3838c49](3838c49))
* implement `ComposablePass` for normalize Guppy pass
([#1286](#1286))
([d72d84d](d72d84d))
* implement `ComposeablePass` interface for any serializable pytket
pass. ([#1269](#1269))
([71cb1f2](71cb1f2))


### Bug Fixes

* update incomplete API docs for passes module
([#1308](#1308))
([0276ba2](0276ba2))
* update incorrect version info in notebook
([#1312](#1312))
([af160ed](af160ed))
* Use qsystem extensions on Tk2Circuit.from_bytes/str
([#1296](#1296))
([df9f52a](df9f52a))


### Documentation

* add notebook on optimization of Guppy programs
([#1287](#1287))
([e82f959](e82f959))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: Agustín Borgna <[email protected]>
github-merge-queue bot pushed a commit that referenced this pull request Feb 3, 2026
## 🤖 New release

* `tket`: 0.16.0 -> 0.17.0 (✓ API compatible changes)
* `tket-qsystem`: 0.22.0 -> 0.23.0 (✓ API compatible changes)

<details><summary><i><b>Changelog</b></i></summary><p>

## `tket`

<blockquote>

##
[0.17.0](tket-v0.16.0...tket-v0.17.0)
- 2026-02-02

### Bug Fixes

- *(encoded-circ)* Track unsupported wires between input and output
([#1224](#1224))
- Multiple fixes to the pytket encoder
([#1226](#1226))
- Don't use opgroup in pytket barrier encoding
([#1251](#1251))
- guppy_to_circuit always returns num_operations = 0
([#1200](#1200))
- *(pytket-decoder)* Avoid QAllocating and immediately freeing qubits
([#1256](#1256))
- Encoding of opaque subgraphs with no associated qubit/bit
([#1295](#1295))
- [**breaking**] Don't rely on command params for pytket barriers
([#1298](#1298))
- Track output qubits in CircuitInfo
([#1304](#1304))
- Wrongly reused qubit IDs in pytket encoding
([#1358](#1358))

### New Features

- Deprecate local find_tuple_unpack rewrite
([#1188](#1188))
- Add CopyableExpressionAST
([#1209](#1209))
- `NormalizeGuppy` pass to simplify generated structure
([#1220](#1220))
- [**breaking**] pytket EncodedCircuit struct for in-place pytket
optimisation ([#1211](#1211))
- [**breaking**] Interval is independent of resource IDs and scope
position ([#1205](#1205))
- Don't translate usizes to pytket
([#1241](#1241))
- BorrowSquashPass to elide redundant borrow/return ops
([#1159](#1159))
- [**breaking**] Bump hugr to 0.25.0
([#1325](#1325))
- Remove order edges in NormalizeGuppy pass
([#1326](#1326))
- [**breaking**] Remove deprecated unpack tuple pass
([#1387](#1387))

### Refactor

- Remove contain_qubits, use TypeUnpacker
([#1283](#1283))
- [**breaking**] Replace Subcircuit with SiblingSubgraph
([#1288](#1288))
- *(metadata)* [**breaking**] Migrate all metadata keys onto the new
metadata traits ([#1328](#1328))
</blockquote>

## `tket-qsystem`

<blockquote>

##
[0.23.0](tket-qsystem-v0.22.0...tket-qsystem-v0.23.0)
- 2026-02-02

### Bug Fixes

- [**breaking**] Don't rely on command params for pytket barriers
([#1298](#1298))
- Wrongly reused qubit IDs in pytket encoding
([#1358](#1358))

### New Features

- `NormalizeGuppy` pass to simplify generated structure
([#1220](#1220))
- Allow running arbitrary serializable pytket passes on hugrs
([#1266](#1266))
- BorrowSquashPass to elide redundant borrow/return ops
([#1159](#1159))
- [**breaking**] Bump hugr to 0.25.0
([#1325](#1325))
- Remove order edges in NormalizeGuppy pass
([#1326](#1326))
- hide new public funcs introduced by linearization
([#1333](#1333))

### Testing

- regenerate guppy_opt examples, and count gates
([#1249](#1249))
- run pytket on guppy_opt tests, measure (very limited) success
([#1250](#1250))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
@hugrbot hugrbot mentioned this pull request Feb 5, 2026
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