Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0475526
Add support to capture + compile symbolic decomp rules for adjoint
maliasadi Sep 9, 2026
e7c3cef
Update op2 qref prim
maliasadi Sep 9, 2026
b6b1e4c
More fixes
maliasadi Sep 9, 2026
d0d972b
Update
maliasadi Sep 9, 2026
620e6e5
Merge branch 'main' into decomp/fix_sym_rules
maliasadi Sep 9, 2026
9efcdec
Update changelog
maliasadi Sep 9, 2026
659a26f
Add tests
maliasadi Sep 9, 2026
a723884
Update
maliasadi Sep 9, 2026
0e90720
Collect ctrl symbolic rules
maliasadi Sep 9, 2026
366b2cd
Capture + LOwer rules
maliasadi Sep 9, 2026
64d0d28
Update
maliasadi Sep 9, 2026
fd9a435
Test the collector
maliasadi Sep 10, 2026
2dfc8cc
Update
maliasadi Sep 10, 2026
dd6d9dc
One more try
maliasadi Sep 10, 2026
5416f18
Update
maliasadi Sep 10, 2026
6601400
Update
maliasadi Sep 11, 2026
655f140
Merge with main
maliasadi Sep 14, 2026
ddb24b9
Merge branch 'main' into decomp/add_ctrl_sym_rules
maliasadi Sep 15, 2026
f9fd6e7
Update format
maliasadi Sep 15, 2026
de227f4
Fix the inconsistency with compiler ids and graphOpId ids corner case…
maliasadi Sep 15, 2026
a5ca4bc
Fix the multi-ctrl special rules
maliasadi Sep 15, 2026
870e0d4
Update changelog
maliasadi Sep 15, 2026
b667b77
Update
maliasadi Sep 15, 2026
d958048
Apply code review suggestions
maliasadi Sep 15, 2026
fe46a60
Fix kind=adjoint
maliasadi Sep 15, 2026
6bb7f47
Update GraphOpId to be the single src of truth for sym ids
maliasadi Sep 15, 2026
63bc327
Update tests
maliasadi Sep 15, 2026
7626d95
Update
maliasadi Sep 15, 2026
229d912
Merge branch 'main' into decomp/add_ctrl_sym_rules
maliasadi Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@
adjoint.
[(#3129)](https://github.com/PennyLaneAI/catalyst/pull/3129)
[(#3127)](https://github.com/PennyLaneAI/catalyst/pull/3127)
[(#3213)](https://github.com/PennyLaneAI/catalyst/pull/3213)

Control is folded into the operator identity *control-outermost* (e.g. `C(Adjoint(Op))`), so
`ctrl(adjoint(Op))` and `adjoint(ctrl(Op))` collapse to a single node, while a distinct control
count is its own node keyed. For a target gate set, `<n>C(Op)` is reached through:
1. Rules registered directly for `<n>C(Op)` (e.g. named `CNOT`/`CRX`, `ctrl_decomp_zyz`), and
1. Rules registered directly for `C(Op)` (PennyLane names a controlled operator `C(Op)` whatever
its control count), and
2. Rules *synthesized by distribution* (`decompose(C(Op)) = ctrl(decompose(Op))`), controlling
each produced gate with the same control count.

Expand All @@ -57,6 +59,48 @@
`(decompose-lowering -> ctrl-lowering -> adjoint-lowering)` to a fixpoint.
Controlled basis gates are only free when their own `<n>C(...)` id is in the target gate set.

Pathway 1 covers the rules PennyLane registers against the *symbolic* operator, i.e.
`rule(base, control_wires, control_values, work_wires, work_wire_type)` rather than the base op's
`(*params, wires)` — the `flip_zero_ctrl_values(...)` family. These are the rules that terminate a
controlled chain in plain gates (`C(Hadamard) -> CH`, `2C(Hadamard) -> H, RY, Toffoli`), so
`<n>C(Op)` can now reach a target gate set at all. A resource that is itself a generic symbolic
operator is spelled the way the compiler spells a modified operator: the modifier is folded into
the base op's id (`2C(S){}{wires:1}{}`), not the wrapper's own arguments. The number of controls
an operator instance carries now reaches the rule closure from lowering, so `<n>C(...)` rules are
synthesized at trace time rather than only on demand.

Comment thread
maliasadi marked this conversation as resolved.
The id a distributed rule declares for each gate it produces is now *generated* with its
modifiers, through the same `build_graph_op_id` builder the rest of the frontend uses, instead of
being spliced into a finished id string. The modifiers are placed canonically by construction, so
a resource that is already symbolic composes (`Adjoint(C(X))` spells `C(Adjoint(X))`) rather than
being rejected as out of order.

A rule may also *produce* a multi-controlled gate. The control count a resource carries is now
kept when the closure explores it through its base, so the `<n>C(...)` node that resource names
gets rules of its own instead of only `C(...)`; an operator reached again under more controls
pays just for the variants it is still missing. The name paired with such an id is spelled the
way PennyLane's registry spells it, since a multi-controlled id reads `<n>C(...)` and PennyLane
names every controlled operator `C(...)` whatever its control count.

Right now, a rule registered against `C(Op)` is traced with every control value on, matching
the all-ones controls a `<n>C(...)` graphOpId denotes: it records only the control *count*,
so an operator with a zero control value maps to the same node and is given the same rule,
without the `X` flips that value needs. Zero control values are therefore not yet supported
through this pathway; they still decompose correctly through rule distribution, whose
`qref.ctrl` region is given the control values at runtime.

Two defects in applying a register-mode controlled rule are fixed along with it:

- The rule's operands are now emitted as `func(qreg, param*, inWires*, inCtrlWires*)`, matching
what `DecomposeLoweringPass` reads back. They were previously emitted control-wires-first
(`qp.capture.subroutine` traces through `jax.jit`, which flattens keyword arguments in sorted
order, and `_ctrl_wires` sorts ahead of `wires`), which silently swapped a rule's control and
target for a single control and mismatched the tensor shapes for more than one.

- `DecomposeLoweringPass` now inserts the *control* qubits into the register it hands to a
register-mode rule, not just the base qubits. A control qubit left out was read back at its
pre-decomposition state, dropping whatever had been applied to it earlier in the circuit.

* The `local-random` unitary folding option for :func:`~.mitigate_with_zne` is now implemented,
reproducing Mitiq's ``fold_gates_at_random``: every gate is folded ``floor((scale_factor-1)/2)``
times, then a random subset is folded once more (without replacement) to reach ``scale_factor * n``
Expand Down
Loading
Loading