Skip to content

Support routing for directed device graphs#7810

Merged
pavoljuhas merged 20 commits intoquantumlib:mainfrom
Vivek1106-04:directed-device-graph
Feb 11, 2026
Merged

Support routing for directed device graphs#7810
pavoljuhas merged 20 commits intoquantumlib:mainfrom
Vivek1106-04:directed-device-graph

Conversation

@Vivek1106-04
Copy link
Contributor

@Vivek1106-04 Vivek1106-04 commented Dec 17, 2025

Enhance the RouteCQC transformer to allow directed device graphs.
The routing algorithm is the same as for bidirectional graphs,
except for unidirectional edges q1 -> q2 where inserted SWAP
operations are replaced by their Hadamard decomposition

CNOT(q1, q2)
H(q1), H(q2)
CNOT(q1, q2)
H(q1), H(q2)
CNOT(q1, q2)

Fixes #5863

@Vivek1106-04 Vivek1106-04 requested review from a team and vtomole as code owners December 17, 2025 07:26
@Vivek1106-04 Vivek1106-04 requested a review from maffoo December 17, 2025 07:26
@github-actions github-actions bot added size: M 50< lines changed <250 size: L 250< lines changed <1000 and removed size: M 50< lines changed <250 labels Dec 17, 2025
@Vivek1106-04
Copy link
Contributor Author

Added additional tests to cover edge cases for directed device graphs and the tag_inserted_swaps parameter.

@codecov
Copy link

codecov bot commented Dec 18, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.61%. Comparing base (bd4c30b) to head (dc784ff).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7810      +/-   ##
==========================================
- Coverage   99.62%   99.61%   -0.01%     
==========================================
  Files        1104     1104              
  Lines       98959    99067     +108     
==========================================
+ Hits        98583    98690     +107     
- Misses        376      377       +1     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Vivek1106-04 Vivek1106-04 force-pushed the directed-device-graph branch from bced992 to e71ea5f Compare January 9, 2026 16:38
@review-notebook-app
Copy link

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@Vivek1106-04
Copy link
Contributor Author

Used pylint disable for argument-count warnings

Refactoring to reduce args would change public API, breaking
notebooks that test against released Cirq. Disables are the
pragmatic choice for API stability.

@Vivek1106-04
Copy link
Contributor Author

@tanujkhattar ! can you review this pr.

Comment on lines +408 to +409
mm.int_to_physical_qid[mm.logical_to_physical[swap[0]]],
mm.int_to_physical_qid[mm.logical_to_physical[swap[1]]],
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we still use mm.int_to_logical_qid like we did earlier?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we still use mm.int_to_logical_qid and mm.int_to_physical_qid exactly as before. The
_route method uses mm to map logical qubits to physical qubits when emitting SWAPs, and the return statement in
route_circuit builds the swap permutation map using mm.int_to_logical_qid[k]. The only change is that we now emit standard SWAP gates during routing, and the directional decomposition happens in a separate post-processing step using map_operations_and_unroll

Copy link
Collaborator

Choose a reason for hiding this comment

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

Out of curiosity -- Was this is an AI generated answer? It feels like one (nothing wrong with it, I'm just curious :))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

when you specially asked about mm.int_to_logical_qid i thought did i break the code some where else. to cross check myself i used ai to understand where mm is used .

qubit_pair: tuple[cirq.Qid, cirq.Qid],
device_graph: nx.Graph,
tag_inserted_swaps: bool = False,
) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'll be cleaner if you make the change "Replace a SWAP with a Directional SWAP" in the route_circuit method between Step-4 and Step-5. You can inspect whether a gate has a RoutingTag or not and replace it with a directional swap gate in the routing_ops. You can define a map_func and use cirq.map_operations in order to do this replacement. This will help make the API cleaner and communicate the fact that routing logic doesn't care about direction, and we just use this little trick to decompose the swap gate using directional gates in the end.

AFAIR, the router tries to preserve moment structure so you can also choose to encapsulate the decomposition of directional swap in a cirq.CircuitOperation or define a new DirectionalSwap gate that performs the decomposition and use it here. But I don't have strong opinions here.

Also add a little comment to the class that explains how the directional graphs are handled (i.e. the routing logic is equivalent to that of an undirected graph; you just use a directional swap decomposition to decompose the swap gates into CNOTs and single qubit gates)

Copy link
Collaborator

@tanujkhattar tanujkhattar left a comment

Choose a reason for hiding this comment

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

Final comment. Looks good overall

Comment on lines +360 to +445
inserted_swap = mm.mapped_op(
ops.SWAP(mm.int_to_logical_qid[swap[0]], mm.int_to_logical_qid[swap[1]])
# Emit a standard SWAP gate (will be replaced with directional
# decomposition in post-processing if needed for directed graphs)
swap_qubits = (
mm.int_to_physical_qid[mm.logical_to_physical[swap[0]]],
mm.int_to_physical_qid[mm.logical_to_physical[swap[1]]],
)
swap_op = ops.SWAP(*swap_qubits)
if tag_inserted_swaps:
inserted_swap = inserted_swap.with_tags(ops.RoutingSwapTag())
routed_ops[timestep].append(inserted_swap)
swap_op = swap_op.with_tags(ops.RoutingSwapTag())
routed_ops[timestep].append(swap_op)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe this can be reverted to it's original form? Do we need this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will revert back - just felt like it was easier on the eyes.

@Vivek1106-04 Vivek1106-04 force-pushed the directed-device-graph branch from c7abc62 to 3376049 Compare February 5, 2026 07:16
@Vivek1106-04
Copy link
Contributor Author

@pavoljuhas ! review .

Copy link
Collaborator

@pavoljuhas pavoljuhas left a comment

Choose a reason for hiding this comment

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

The routing result should not depend on the tag_inserted_swaps value. That argument is a flag for tagging the new operations or not, but otherwise it should have no effect.

Also, please see the inline comments.

Comment on lines +84 to +90
**Handling Directed Graphs:**
When the device_graph is directed (i.e., edges represent unidirectional CNOT constraints),
the routing logic still operates as if the graph were undirected. This is because SWAP
gates are logically symmetric regardless of underlying gate direction constraints.
After routing completes, any inserted SWAP gates tagged with `RoutingSwapTag` are
decomposed into a directional-aware sequence using the Hadamard trick:
``SWAP = CNOT(ctrl,tgt) - H⊗H - CNOT(ctrl,tgt) - H⊗H - CNOT(ctrl,tgt)``
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit - tweak the section for formatting consistency and remove the possibly confusing detail on tagging:

Suggested change
**Handling Directed Graphs:**
When the device_graph is directed (i.e., edges represent unidirectional CNOT constraints),
the routing logic still operates as if the graph were undirected. This is because SWAP
gates are logically symmetric regardless of underlying gate direction constraints.
After routing completes, any inserted SWAP gates tagged with `RoutingSwapTag` are
decomposed into a directional-aware sequence using the Hadamard trick:
``SWAP = CNOT(ctrl,tgt) - HH - CNOT(ctrl,tgt) - HH - CNOT(ctrl,tgt)``
Handling Directed Graphs:
When the device_graph is directed (e.g., edges represent unidirectional CNOT constraints),
the routing logic still operates as if the graph were undirected. This is because SWAP
gates are logically symmetric regardless of underlying gate direction constraints.
After routing completes, any inserted SWAP gates are decomposed into a directional-aware
sequence using the Hadamard trick:
``SWAP = CNOT(ctrl, tgt) - HH - CNOT(ctrl, tgt) - HH - CNOT(ctrl, tgt)``


# 5. Return the routed circuit by packing each inner list of ops as densely as possible and
# preserving outer moment structure. Also return initial map and swap permutation map.
# 4.5. Replace tagged SWAP gates with directional decompositions if needed.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit - renumber to 5 and below to 6, etc.

Comment on lines +243 to +245
final_circuit = self._replace_swaps_with_directional_decomposition(
routed_circuit, tag_inserted_swaps
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add a if nx.is_directed(self.device_graph): guard here so that we do not call possibly expensive _replace_swaps_with_directional_decomposition when it is a no-op.

Comment on lines +321 to +325
if not tag_inserted_swaps:
# If swaps weren't tagged, we can't identify which ones to decompose.
# In this case, we assume the graph is undirected or the user will
# handle decomposition elsewhere.
return circuit
Copy link
Collaborator

Choose a reason for hiding this comment

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

The resulting circuit should not depend on tag_inserted_swaps as that is a markup-only flag for whether the new operations should be tagged or not.

I suggest to construct a set of routing-added SWAP operations in the caller and pass it here. The decomposition should happen for those SWAPs only. The tag_inserted_swaps argument can be removed, because line 357 already copies the tags from the decomposed operation.

Comment on lines +332 to +333
if not any(isinstance(tag, ops.RoutingSwapTag) for tag in op.tags):
return op
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please remove, the decomposition should happen also with tag_inserted_swaps=False when no tags are added.

Comment on lines +370 to +371
assert len(tagged_hadamards) > 0, "Expected tagged Hadamard gates!"
assert len(tagged_cnots) > 0, "Expected tagged CNOT gates!"
Copy link
Collaborator

Choose a reason for hiding this comment

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

As a general style thing - either use assert tagged_hadamards to check if non-empty or compare with the actual expected length. Also please avoid non-essential assertion messages.

)

# Verify the routed circuit is mathematically equivalent to the original
cirq.testing.assert_circuits_have_same_unitary_given_final_permutation(
Copy link
Collaborator

@pavoljuhas pavoljuhas Feb 6, 2026

Choose a reason for hiding this comment

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

I'd suggest to move this check to test_directed_device_reverse_only_edge which will give it a bit more check if that test routes a circuit with bidirectional and directed edges.

result = router._replace_swaps_with_directional_decomposition(circuit, True)

# The SWAP should be unchanged since there's no edge between q0 and q2
assert list(result.all_operations()) == [swap_on_non_edge]
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is testing impossible condition. While you can call the internal function with any arguments, in the actual code the _replace_swaps_with_directional_decomposition is passed an already routed circuit which can have only q[0], q[1] from device_graph. Comparing with operation that uses other qubit q[2] is not sensible.

Please either compare with expected routing result or if easier just delete this test.

@Vivek1106-04
Copy link
Contributor Author

done !, removed some tests and added a new test for 100% coverage lines

And move `test_repr` to the end so it is not mixed with new tests.
Also replace `router.route_circuit` with `router.__call__` when
extra outputs are unused and be a bit more specific in assertions.
Check routing with CNOT-s on bi-directional edge in addition
to reverse-oriented CNOT.
@pavoljuhas pavoljuhas changed the title support for directed device graphs Support routing for directed device graphs Feb 11, 2026
Copy link
Collaborator

@pavoljuhas pavoljuhas left a comment

Choose a reason for hiding this comment

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

LGTM with a few tweaks pushed in. I have also shortened
and updated the PR description as it will be used for
commit message so it should be of similar length and style.

Thank you for contributing this!

@pavoljuhas pavoljuhas added this pull request to the merge queue Feb 11, 2026
github-merge-queue bot pushed a commit that referenced this pull request Feb 11, 2026
Enhance the `RouteCQC` transformer to allow directed device graphs.
The routing algorithm is the same as for bidirectional graphs,
except for unidirectional edges q1 -> q2 where inserted SWAP
operations are replaced by their Hadamard decomposition

    CNOT(q1, q2)
    H(q1), H(q2)
    CNOT(q1, q2)
    H(q1), H(q2)
    CNOT(q1, q2)

Fixes #5863

---------

Co-authored-by: Pavol Juhas <juhas@google.com>
@pavoljuhas pavoljuhas removed this pull request from the merge queue due to a manual request Feb 11, 2026
@@ -34,7 +34,8 @@ def test_directed_device() -> None:
cirq.testing.assert_same_circuits(routed_circuit, circuit)


This comment was marked as outdated.

@pavoljuhas pavoljuhas enabled auto-merge February 11, 2026 01:47
@pavoljuhas pavoljuhas added this pull request to the merge queue Feb 11, 2026
Merged via the queue into quantumlib:main with commit 4f3f6ea Feb 11, 2026
41 checks passed
@Vivek1106-04 Vivek1106-04 deleted the directed-device-graph branch February 11, 2026 02:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: L 250< lines changed <1000

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Routing] Add support for directed device graphs.

3 participants