Skip to content

Conversation

@aborgna-q
Copy link
Collaborator

@aborgna-q aborgna-q commented Oct 31, 2025

For previous reviews, see

This PR adds an EncoderCircuit in between the encoding/decoding process Hugr <-> tket_json_rs::SerialCircuit.

This new instruction carries multiple SerialCircuits associated with the Hugr region they encode. This lets us replace the old regions with modified ones in the original hugr, after optimising the SerialCircuits.

The opaque barriers that represent sections of the original hugr not encodable as pytket commands now have two payload variants:

  • OpaqueSubgraphPayload::Inline as before encodes the unsupported hugr envelope, plus some boundary edge ids that we can use to reconstruct links between different inline payloads.
    This variant does not support all circuits. Subgraphs must be flat and contain no non-local edges (just as before, but now we detect and error out on those cases).
  • OpaqueSubgraphPayload::External. This references a subgraph in the original hugr with a simple ID. This allows us to re-use the nodes from the initial hugr, keeping all the hierarchy and edges.
    The second case only works for Hugrs due to HugrBuilder limitations, but it's the usecase we'll use for tket1 optimisation.
    The first one is kept for cases where we need to extract standalone pytket circuits.

For testing, this includes

  • Several new roundtrip tests in serialize::pytket::tests, including error tests
  • An integration test in tket1-passes that encodes a flat hugr, applies a pass from pytket, and reassembles it inline.

BREAKING CHANGE: OpConvertError renamed to PytketEncodeOpError
BREAKING CHANGE: Some minor changes to PytketDecodeError variant attributes.
BREAKING CHANGE: fn_name moved from DecodeOptions to DecodeInsertionTarget::Function

@aborgna-q aborgna-q requested a review from a team as a code owner October 31, 2025 16:14
@aborgna-q aborgna-q requested review from acl-cqc and ss2165 October 31, 2025 16:14
@codecov
Copy link

codecov bot commented Oct 31, 2025

Codecov Report

❌ Patch coverage is 73.09340% with 314 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.92%. Comparing base (6f6dce9) to head (c78a23d).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
tket/src/serialize/pytket/decoder/subgraph.rs 75.54% 73 Missing and 5 partials ⚠️
tket/src/serialize/pytket/decoder/wires.rs 54.19% 69 Missing and 2 partials ⚠️
tket/src/serialize/pytket/encoder.rs 67.30% 43 Missing and 8 partials ⚠️
tket/src/serialize/pytket/opaque/payload.rs 69.44% 20 Missing and 2 partials ⚠️
tket/src/serialize/pytket/circuit.rs 88.82% 11 Missing and 10 partials ⚠️
tket/src/serialize/pytket/decoder.rs 80.19% 18 Missing and 2 partials ⚠️
tket/src/serialize/pytket/options.rs 36.66% 19 Missing ⚠️
tket/src/serialize/pytket/opaque.rs 81.39% 11 Missing and 5 partials ⚠️
tket/src/serialize/pytket/tests.rs 74.07% 7 Missing ⚠️
tket/src/serialize/pytket.rs 55.55% 1 Missing and 3 partials ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1211      +/-   ##
==========================================
+ Coverage   78.77%   78.92%   +0.14%     
==========================================
  Files         155      159       +4     
  Lines       19177    20157     +980     
  Branches    18075    19055     +980     
==========================================
+ Hits        15107    15908     +801     
- Misses       3112     3275     +163     
- Partials      958      974      +16     
Flag Coverage Δ
python 92.65% <ø> (ø)
qis-compiler 68.40% <ø> (ø)
rust 78.55% <73.09%> (+0.17%) ⬆️

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.

@aborgna-q aborgna-q changed the title feat: pytket EncodedCircuit struct for in-place pytket optimisation feat!: pytket EncodedCircuit struct for in-place pytket optimisation Oct 31, 2025
Copy link
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.

Well, massive PR...but great work @aborgna-q, looks like it was worth it! :-)
Corresponding mass of comments, but to summarize

  • A few issues in subgraph.rs where I'm not sure what you're trying to do with edges
    • That pending_encoded_edge_connections thing needs addressing but I think is straightforward to move into WireTracker (?)
  • Other than that, everything is minor, although it'd be nice to see
    • Something nicer than the mutable Option<PytketDecoderConfig> + expect
    • PytketDecoderContext::new() taking an Option (more like the encoder!!) rather than having a separate fn register_opaque_subgraphs
    • OpaqueSubgraphs::new(encoder_count) instead using Node::index()

I'm not very familiar with encoder.rs so assume that's all ok 😉 😆 and haven't looked at tests yet, but definitely feels like we are nearly there.

pub serial_circuit: SerialCircuit,
/// A subgraph of the region that does not contain any operation encodable
/// as a pytket command, and hence was not encoded in [`serial_circuit`].
#[expect(unused)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a TODO associated here?

Copy link
Contributor

Choose a reason for hiding this comment

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

How come only one....or can a SubgraphId reference something that's disconnected (and likely very nonconvex)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's one of the commented-out failing tests with a TODO.
These are subgraphs that connect only to the input and output node (if anything), using non-pytket types.

There's at most one of these, since they can always be merged together (I think we can have two disconnected components in a subgraph? I'll change it into a vec if needed later)

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah ok. There could be multiple parallel such, in which case they could be merged; but not sequential - there'd have to be a break between them, i.e. a supported/encoded node, in which case....these nodes (between Input and SerialCircuit, or between SerialCircuit and Output) would be included in a "normal" unsupported subgraph? (Right?)

/// [`EncodedCircuit::new_standalone`] or call
/// [`EncodedCircuit::ensure_standalone`].
#[derive(Debug, Clone)]
pub struct EncodedCircuit<Node: HugrNode> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this (not storing the Hugr/View in here) mean that the Hugr could be mutated independently?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It could, yes.

This pattern is common in other "non-borrowing" structures though, like petgraph's Toposort: https://docs.rs/petgraph/latest/petgraph/visit/struct.Topo.html

}

impl EncodedCircuit<Node> {
/// Encode a HugrView into a [`EncodedCircuit`].
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Encode a HugrView into a [`EncodedCircuit`].
/// Encode a Hugr into a [`EncodedCircuit`].

I mean, Node=Node and AsRef and AsMut is close enough to Hugr, right. (But I'm a little surprised by the need for AsMut - presumably some trait needs this? I mean, you can't actually mutate through the &Circuit<H>, can you?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, AsMut is pretty much a Hugr.

The limitation are the Hugr builder traits, that require AsMut/AsRef. That's why decoding is only done on Hugrs.

// connected once the outgoing port is created.
//
// This handles the case where unsupported subgraphs in opaque barriers on
// the pytket circuit get reordered and input ports are seen before their
Copy link
Contributor

Choose a reason for hiding this comment

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

Eeek. I assume that this cannot create a cycle of dataflow edges...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It shouldn't happen normally... the user is free to modify their circuit and see invalid hugrs though.

I didn't focus to much on this usecase for this PR, we may be able to improve the UX for standalone extracting/re-encoding later.

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 this should be ok? (Modification excepted.) You'd have to have two barriers, which are "parallel" from the pytket view (no qubits between them) so they could be reordered, but with an unsupported wire between them - in which case the unsupported subgraphs would be merged into a single barrier?

qubit_args: &mut &[TrackedQubit],
bit_args: &mut &[TrackedBit],
params: &mut &[LoadedParameter],
unsupported_wire: Option<EncodedEdgeID>,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: it's a little surprising that the unsupported_wire here has no relation to self.unsupported_wires (at least within find_typed_wire)....the parametrization trick suggested elsewhere might make that more obvious, as would renaming self.unsupported_wires to self.pending_edge_connections

// TODO: Test that unsupported subgraphs that don't affect any qubit/bit registers
// TODO: Test that opaque subgraphs that don't affect any qubit/bit registers
// are correctly encoded in pytket commands.
let mut extra_subgraph: Option<BTreeSet<H::Node>> = None;
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 this might be simpler just to use a non-Option BTreeSet and check whether it's empty

@aborgna-q aborgna-q requested a review from acl-cqc November 4, 2025 15:08
@hugrbot
Copy link
Collaborator

hugrbot commented Nov 4, 2025

This PR contains breaking changes to the public Rust API.

cargo-semver-checks summary
    Building tket v0.16.0 (current)
     Built [  41.551s] (current)
   Parsing tket v0.16.0 (current)
    Parsed [   0.097s] (current)
  Building tket v0.16.0 (baseline)
     Built [  41.340s] (baseline)
   Parsing tket v0.16.0 (baseline)
    Parsed [   0.084s] (baseline)
  Checking tket v0.16.0 -> v0.16.0 (assume minor change)
   Checked [   0.073s] 159 checks: 150 pass, 9 fail, 0 warn, 41 skip

--- failure auto_trait_impl_removed: auto trait no longer implemented ---

Description:
A public type has stopped implementing one or more auto traits. This can break downstream code that depends on the traits being implemented.
      ref: https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.45.0/src/lints/auto_trait_impl_removed.ron

Failed in:
type PytketDecodeError is no longer UnwindSafe, in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:131
type PytketDecodeError is no longer RefUnwindSafe, in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:131
type PytketDecodeErrorInner is no longer UnwindSafe, in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:192
type PytketDecodeErrorInner is no longer RefUnwindSafe, in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:192

--- failure derive_trait_impl_removed: built-in derived trait no longer implemented ---

Description:
A public type has stopped deriving one or more traits. This can break downstream code that depends on those types implementing those traits.
      ref: https://doc.rust-lang.org/reference/attributes/derive.html#derive
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.45.0/src/lints/derive_trait_impl_removed.ron

Failed in:
type PytketDecodeError no longer derives Clone, in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:131
type DecodeInsertionTarget no longer derives Copy, in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/options.rs:120
type PytketDecodeErrorInner no longer derives Clone, in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:192

--- failure enum_missing: pub enum removed or renamed ---

Description:
A publicly-visible enum cannot be imported by its prior path. A `pub use` may have been removed, or the enum itself may have been renamed or removed entirely.
      ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.45.0/src/lints/enum_missing.ron

Failed in:
enum tket::serialize::pytket::OpConvertError, previously in file /home/runner/work/tket2/tket2/BASELINE_BRANCH/tket/src/serialize/pytket/error.rs:16

--- failure enum_no_repr_variant_discriminant_changed: enum variant had its discriminant change value ---

Description:
The enum's variant had its discriminant value change. This breaks downstream code that used its value via a numeric cast like `as isize`.
      ref: https://doc.rust-lang.org/reference/items/enumerations.html#assigning-discriminant-values
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.45.0/src/lints/enum_no_repr_variant_discriminant_changed.ron

Failed in:
variant PytketDecodeErrorInner::NotEnoughInputRegisters 14 -> 15 in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:367
variant PytketDecodeErrorInner::NotEnoughOutputRegisters 15 -> 16 in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:382
variant PytketDecodeErrorInner::OutdatedQubit 16 -> 17 in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:392
variant PytketDecodeErrorInner::OutdatedBit 17 -> 18 in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/error.rs:398

--- failure enum_struct_variant_field_missing: pub enum struct variant's field removed or renamed ---

Description:
A publicly-visible enum has a struct variant whose field is no longer available under its prior name. It may have been renamed or removed entirely.
      ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.45.0/src/lints/enum_struct_variant_field_missing.ron

Failed in:
field actual_types of variant PytketDecodeErrorInner::InvalidOutputSignature, previously in file /home/runner/work/tket2/tket2/BASELINE_BRANCH/tket/src/serialize/pytket/error.rs:224

--- failure enum_unit_variant_changed_kind: An enum unit variant changed kind ---

Description:
A public enum's exhaustive unit variant has changed to a different kind of enum variant, breaking possible instantiations and patterns.
      ref: https://doc.rust-lang.org/reference/items/enumerations.html
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.45.0/src/lints/enum_unit_variant_changed_kind.ron

Failed in:
variant DecodeInsertionTarget::Function in /home/runner/work/tket2/tket2/PR_BRANCH/tket/src/serialize/pytket/options.rs:128

--- failure enum_variant_missing: pub enum variant removed or renamed ---

Description:
A publicly-visible enum has at least one variant that is no longer available under its prior name. It may have been renamed or removed entirely.
      ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.45.0/src/lints/enum_variant_missing.ron

Failed in:
variant PytketEncodeError::OpConversionError, previously in file /home/runner/work/tket2/tket2/BASELINE_BRANCH/tket/src/serialize/pytket/error.rs:79

--- failure inherent_method_missing: pub method removed or renamed ---

Description:
A publicly-visible method or associated fn is no longer available under its prior name. It may have been renamed or removed entirely.
      ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.45.0/src/lints/inherent_method_missing.ron

Failed in:
DecodeOptions::with_fn_name, previously in file /home/runner/work/tket2/tket2/BASELINE_BRANCH/tket/src/serialize/pytket/options.rs:67

--- failure struct_pub_field_missing: pub struct's pub field removed or renamed ---

Description:
A publicly-visible struct has at least one public field that is no longer available under its prior name. It may have been renamed or removed entirely.
      ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
     impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.45.0/src/lints/struct_pub_field_missing.ron

Failed in:
field fn_name of struct DecodeOptions, previously in file /home/runner/work/tket2/tket2/BASELINE_BRANCH/tket/src/serialize/pytket/options.rs:29

   Summary semver requires new major version: 9 major and 0 minor checks failed
  Finished [  84.969s] tket
  Building tket-qsystem v0.22.0 (current)
     Built [  43.214s] (current)
   Parsing tket-qsystem v0.22.0 (current)
    Parsed [   0.028s] (current)
  Building tket-qsystem v0.22.0 (baseline)
     Built [  42.828s] (baseline)
   Parsing tket-qsystem v0.22.0 (baseline)
    Parsed [   0.028s] (baseline)
  Checking tket-qsystem v0.22.0 -> v0.22.0 (assume minor change)
   Checked [   0.047s] 159 checks: 159 pass, 41 skip
   Summary no semver update required
  Finished [  87.904s] tket-qsystem

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

Great work, thanks @agustin. Particularly like the balance you've struck between bits that are the same between external/inline and bits that are different. Looks good to me, tho I think it's worth

  • adding issues for some of the TODOs that don't work yet
  • combining the IndexMaps (I think that's the only change I suggest here that's not trivial)
  • please doc what extra_subgraph is for ;)

The most controversial point is probably that you haven't stored an immutable reference to the Hugr in the encoded circuit, but I'm happy with that - I slightly laugh at (but like) your ExternalSubgraphWasModified error 😁. I'll admit to being wrong on a few of my suggestions before, too....

/// A subgraph of the region that does not contain any operation encodable
/// as a pytket command, and hence was not encoded in [`serial_circuit`].
#[expect(unused)]
pub extra_subgraph: Option<SubgraphId>,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is now read :+1 but AFAICS never written?? I think the comment could be better - all unsupported subgraphs "do not contain any operation encodable as a pytket command" so is this for (a) if the entire region did not contain any pytket commands, or (b) if there were unsupported nodes that were disconnected from the pytket-encoded ones, or (c) something else ???

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is set by EncoderContext::finish, when creating the EncodedCircuitInfo.

I extended the comment a bit.

@aborgna-q aborgna-q added this pull request to the merge queue Nov 5, 2025
Merged via the queue into main with commit ad16531 Nov 5, 2025
23 of 24 checks passed
@aborgna-q aborgna-q deleted the ab/encoded-circuit branch November 5, 2025 13:13
@hugrbot hugrbot mentioned this pull request Nov 5, 2025
github-merge-queue bot pushed a commit that referenced this pull request Nov 5, 2025
…1224)

Depends on #1211

These are not represented in the pytket circuit, and cannot be encoded
into a SiblingSubgraph.

We just track them as an additional item in the `EncodedCircuit`'s
`EncodedCircuitInfo`.

Note that the info is only store for circuits we encode directly, and
not for nested regions inside circuit boxes.
This means that the info is lost when encoding those.
I added a commented-out test with a TODO to fix that (we'll need some
extra plumbing to match circ boxes to external metadata).

drive-by: Make sure the encoder's WireTracker stores the input parameter
names, and passes it along.

I'm ignoring breaking changes since they only affect unpublished code.
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