Skip to content

feat: Add implement_ops and encode pass to the toy_k2 architecture - #407

Draft
PabloAndresCQ wants to merge 1 commit into
feat/toyk2_logicalsfrom
feat/toyk2_encode
Draft

PabloAndresCQ wants to merge 1 commit into
feat/toyk2_logicalsfrom
feat/toyk2_encode

Conversation

@PabloAndresCQ

@PabloAndresCQ PabloAndresCQ commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #243

DISCLAIMER

I'm putting this up so that we can get the discussion started. This is very much still WIP. Consider what I've put up a fleshed out "design intent". There's plenty of work left before this can be merged:

  • Major caveat! This has not been tested yet. I expect there'll be multiple small bugs.
  • For the sake of reviewing and testing, it would be better to split up this PR into incremental additions. At the time of implementing this, I had trouble finding effective ways of splitting up the problem, so I yolo'ed it here.
  • There's some TODOs in inline comments that need addressing.
  • There's duplication of the non-primitives dynamic ops. See the comment highlighted here: https://github.com/Quantinuum/guppyft/pull/407/changes#r4029285636

@PabloAndresCQ
PabloAndresCQ added this pull request to stack #322 September 16, 2026 18:12
Comment on lines +648 to +654
# Another alternative is to define the addressable gates in
# `guppyft.code.toy_k2.logical` acting on the dynamic_qubit type and
# adding HUGR extension ops to inspect these types, checking if two are
# in the same block (which is implemented by comparing ints during
# implement_ops), etc. In that case, we would just replace a
# computational CX with this logical CX via the compose_op replacement,
# and *not* have the dyn ops in the extension.

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.

This is my current preference. Happy to explain it in more detail.

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.

If I understand correctly, the proposal would be to add a cx gate to logicals.py that would look like

def cx(control: Qubit, target: Qubit) -> None:
    if same_block(control, target):
        cx_intra(...)
    else:
        cx_inter(...)

where cx_inter/intra are primitives that act on Block types with int arguments to determine the location of the qubit to be addressed.

In that case, we would also need some operation that consumes Qubit types and returns the Block and int address to be passed to the cx_inter/intra primitives.

@hsemenenko hsemenenko left a comment

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.

Adding a few comments.

Comment on lines +198 to +202
@guppy
@no_type_check
def is_borrowed(self) -> bool:
"""Check if any address in the block is borrowed."""
return self.borrowed_addr[0] or self.borrowed_addr[1]

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.

Can't we use the existing Guppy .is_borrowed method on the array?

Comment on lines +270 to +281
# Remove the remaining addresses of this block from the stack
# NOTE: Ideally, `avail_dyn_addrs` would be a data
# structure with fast deletion (e.g. a set).
# Generally, this stack will be small, though, so it
# shouldn't be a performance bottleneck.
aux_stack: Stack[int, comptime(n_blocks)] = empty_stack()
for _ in range(len(self.avail_dyn_addrs)):
addr = self.avail_dyn_addrs.pop()
if addr[0] != blk_id:
aux_stack.push(addr)
for addr in aux_stack:
self.avail_dyn_addrs.push(addr)

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.

Could we instead have a counter on each block to indicate how many "live" qubits there are? Once it reaches 0, all addresses are pushed back to the Stack.


@guppy
@no_type_check
def allocate_block(self, logical_block: LogicalBlock[4] @ owned) -> int:

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.

This function doesn't allocation the block, so perhaps allocate_block_addr is more appropriate.

@guppy
@no_type_check
@link_name("guppyft.toy_k2._h_dynq")
def _h_dynq(addr: tuple[int, int]) -> tuple[int, int]:

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.

If h_dynq needs an ancilla block, shouldn't we instead treat it like t-state injection where the op accepts a logical block?

@guppy
@no_type_check
@link_name("guppyft.toy_k2._alloc_dynq")
def _alloc_dynq() -> tuple[int, int]:

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.

This is required to match Guppy and HUGR types I think.

Suggested change
def _alloc_dynq() -> tuple[int, int]:
def _alloc_dynq() -> tuple[tuple[int, int]]:

Comment on lines +508 to +510
state.qed_policy(
array(blk_id), comptime(qed_policy.costs.measure_z_all)
)

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.

This isn't needed if the block has been released/consumed.

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.

[Feature]: Add encode for "Toy k2" architecture

2 participants