Skip to content

Commit 44482b6

Browse files
committed
feat: integrate compute_output_permutation into _optimize_unitary
Extract with `up_to_perm=True` so that basic_optimization runs on a SWAP-free circuit, then prepend the output permutation as SWAP gate objects (1 gate each instead of 3 CNOTs).
1 parent 6d18e28 commit 44482b6

5 files changed

Lines changed: 226 additions & 62 deletions

File tree

9 Bytes
Loading
-45 Bytes
Loading

benchmarking/benchmarks_output.txt

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ Depth - original: 12, optimized: 12 (1.00), zx: 12 (1.00)
1919
Number of non-local gates - original: 8, optimized: 8, zx: 8, ratio: 1.00
2020

2121
Circuit name: dnn_n2
22-
Size - original: 228, optimized: 13 (17.54), zx: 148 (1.54)
23-
Depth - original: 155, optimized: 8 (19.38), zx: 97 (1.60)
24-
Number of non-local gates - original: 42, optimized: 3, zx: 24, ratio: 0.12
22+
Size - original: 228, optimized: 13 (17.54), zx: 146 (1.56)
23+
Depth - original: 155, optimized: 8 (19.38), zx: 95 (1.63)
24+
Number of non-local gates - original: 42, optimized: 3, zx: 22, ratio: 0.14
2525

2626
Circuit name: qrng_n4
2727
Size - original: 8, optimized: 8 (1.00), zx: 8 (1.00)
@@ -44,29 +44,29 @@ Depth - original: 5, optimized: 5 (1.00), zx: 5 (1.00)
4444
Number of non-local gates - original: 3, optimized: 3, zx: 3, ratio: 1.00
4545

4646
Circuit name: basis_trotter_n4
47-
Size - original: 1510, optimized: 545 (2.77), zx: 395 (3.82)
48-
Depth - original: 815, optimized: 248 (3.29), zx: 297 (2.74)
49-
Number of non-local gates - original: 462, optimized: 179, zx: 177, ratio: 1.01
47+
Size - original: 1510, optimized: 545 (2.77), zx: 391 (3.86)
48+
Depth - original: 815, optimized: 248 (3.29), zx: 293 (2.78)
49+
Number of non-local gates - original: 462, optimized: 179, zx: 173, ratio: 1.03
5050

5151
Circuit name: qec_en_n5
52-
Size - original: 30, optimized: 27 (1.11), zx: 20 (1.50)
53-
Depth - original: 18, optimized: 15 (1.20), zx: 16 (1.12)
54-
Number of non-local gates - original: 10, optimized: 10, zx: 12, ratio: 0.83
52+
Size - original: 30, optimized: 27 (1.11), zx: 18 (1.67)
53+
Depth - original: 18, optimized: 15 (1.20), zx: 11 (1.64)
54+
Number of non-local gates - original: 10, optimized: 10, zx: 10, ratio: 1.00
5555

5656
Circuit name: toffoli_n3
5757
Size - original: 21, optimized: 18 (1.17), zx: 21 (1.00)
5858
Depth - original: 13, optimized: 12 (1.08), zx: 13 (1.00)
5959
Number of non-local gates - original: 6, optimized: 6, zx: 6, ratio: 1.00
6060

6161
Circuit name: grover_n2
62-
Size - original: 18, optimized: 9 (2.00), zx: 10 (1.80)
63-
Depth - original: 12, optimized: 6 (2.00), zx: 8 (1.50)
64-
Number of non-local gates - original: 2, optimized: 2, zx: 3, ratio: 0.67
62+
Size - original: 18, optimized: 9 (2.00), zx: 6 (3.00)
63+
Depth - original: 12, optimized: 6 (2.00), zx: 4 (3.00)
64+
Number of non-local gates - original: 2, optimized: 2, zx: 2, ratio: 1.00
6565

6666
Circuit name: hs4_n4
67-
Size - original: 32, optimized: 16 (2.00), zx: 21 (1.52)
67+
Size - original: 32, optimized: 16 (2.00), zx: 18 (1.78)
6868
Depth - original: 10, optimized: 6 (1.67), zx: 7 (1.43)
69-
Number of non-local gates - original: 4, optimized: 4, zx: 5, ratio: 0.80
69+
Number of non-local gates - original: 4, optimized: 4, zx: 4, ratio: 1.00
7070

7171
Circuit name: qaoa_n3
7272
Size - original: 18, optimized: 17 (1.06), zx: 18 (1.00)
@@ -84,9 +84,9 @@ Depth - original: 5, optimized: 5 (1.00), zx: 4 (1.25)
8484
Number of non-local gates - original: 2, optimized: 2, zx: 2, ratio: 1.00
8585

8686
Circuit name: vqe_uccsd_n4
87-
Size - original: 224, optimized: 147 (1.52), zx: 79 (2.84)
88-
Depth - original: 146, optimized: 111 (1.32), zx: 55 (2.65)
89-
Number of non-local gates - original: 88, optimized: 71, zx: 40, ratio: 1.77
87+
Size - original: 224, optimized: 147 (1.52), zx: 76 (2.95)
88+
Depth - original: 146, optimized: 111 (1.32), zx: 53 (2.75)
89+
Number of non-local gates - original: 88, optimized: 71, zx: 37, ratio: 1.92
9090

9191
Circuit name: quantumwalks_n2
9292
Size - original: 13, optimized: 13 (1.00), zx: 13 (1.00)
@@ -119,14 +119,14 @@ Depth - original: 22, optimized: 22 (1.00), zx: 22 (1.00)
119119
Number of non-local gates - original: 10, optimized: 10, zx: 10, ratio: 1.00
120120

121121
Circuit name: vqe_uccsd_n6
122-
Size - original: 2288, optimized: 1615 (1.42), zx: 339 (6.75)
123-
Depth - original: 1487, optimized: 1298 (1.15), zx: 204 (7.29)
124-
Number of non-local gates - original: 1052, optimized: 923, zx: 188, ratio: 4.91
122+
Size - original: 2288, optimized: 1615 (1.42), zx: 331 (6.91)
123+
Depth - original: 1487, optimized: 1298 (1.15), zx: 200 (7.43)
124+
Number of non-local gates - original: 1052, optimized: 923, zx: 180, ratio: 5.13
125125

126126
Circuit name: ising_n10
127-
Size - original: 490, optimized: 220 (2.23), zx: 388 (1.26)
128-
Depth - original: 71, optimized: 42 (1.69), zx: 114 (0.62)
129-
Number of non-local gates - original: 90, optimized: 90, zx: 158, ratio: 0.57
127+
Size - original: 490, optimized: 220 (2.23), zx: 380 (1.29)
128+
Depth - original: 71, optimized: 42 (1.69), zx: 109 (0.65)
129+
Number of non-local gates - original: 90, optimized: 90, zx: 150, ratio: 0.60
130130

131131
Circuit name: simon_n6
132132
Size - original: 22, optimized: 46 (0.48), zx: 21 (1.05)
@@ -139,29 +139,29 @@ Depth - original: 21, optimized: 85 (0.25), zx: 21 (1.00)
139139
Number of non-local gates - original: 18, optimized: 43, zx: 18, ratio: 2.39
140140

141141
Circuit name: qaoa_n6
142-
Size - original: 276, optimized: 120 (2.30), zx: 135 (2.04)
143-
Depth - original: 110, optimized: 46 (2.39), zx: 68 (1.62)
144-
Number of non-local gates - original: 54, optimized: 36, zx: 71, ratio: 0.51
142+
Size - original: 276, optimized: 120 (2.30), zx: 127 (2.17)
143+
Depth - original: 110, optimized: 46 (2.39), zx: 60 (1.83)
144+
Number of non-local gates - original: 54, optimized: 36, zx: 63, ratio: 0.57
145145

146146
Circuit name: bb84_n8
147147
Size - original: 43, optimized: 27 (1.59), zx: 31 (1.39)
148148
Depth - original: 7, optimized: 4 (1.75), zx: 5 (1.40)
149149
Number of non-local gates - original: 0, optimized: 0, zx: 0
150150

151151
Circuit name: vqe_uccsd_n8
152-
Size - original: 10816, optimized: 7771 (1.39), zx: 1226 (8.82)
153-
Depth - original: 7253, optimized: 6424 (1.13), zx: 795 (9.12)
154-
Number of non-local gates - original: 5488, optimized: 4815, zx: 712, ratio: 6.76
152+
Size - original: 10816, optimized: 7771 (1.39), zx: 1214 (8.91)
153+
Depth - original: 7253, optimized: 6424 (1.13), zx: 788 (9.20)
154+
Number of non-local gates - original: 5488, optimized: 4815, zx: 701, ratio: 6.87
155155

156156
Circuit name: adder_n10
157157
Size - original: 19, optimized: 139 (0.14), zx: 19 (1.00)
158158
Depth - original: 11, optimized: 96 (0.11), zx: 11 (1.00)
159159
Number of non-local gates - original: 9, optimized: 65, zx: 9, ratio: 7.22
160160

161161
Circuit name: dnn_n8
162-
Size - original: 1016, optimized: 208 (4.88), zx: 650 (1.56)
163-
Depth - original: 173, optimized: 34 (5.09), zx: 146 (1.18)
164-
Number of non-local gates - original: 192, optimized: 64, zx: 144, ratio: 0.44
162+
Size - original: 1016, optimized: 208 (4.88), zx: 638 (1.59)
163+
Depth - original: 173, optimized: 34 (5.09), zx: 143 (1.21)
164+
Number of non-local gates - original: 192, optimized: 64, zx: 132, ratio: 0.48
165165

166166
Circuit name: bv_n14
167167
Size - original: 54, optimized: 53 (1.02), zx: 54 (1.00)
@@ -179,7 +179,7 @@ Depth - original: 51, optimized: 388 (0.13), zx: 51 (1.00)
179179
Number of non-local gates - original: 42, optimized: 252, zx: 42, ratio: 6.00
180180

181181
Circuit name: qft_n18
182-
Size - original: 801, optimized: 665 (1.20), zx: 752 (1.07)
183-
Depth - original: 134, optimized: 118 (1.14), zx: 372 (0.36)
184-
Number of non-local gates - original: 306, optimized: 306, zx: 518, ratio: 0.59
182+
Size - original: 801, optimized: 665 (1.20), zx: 722 (1.11)
183+
Depth - original: 134, optimized: 118 (1.14), zx: 362 (0.37)
184+
Number of non-local gates - original: 306, optimized: 306, zx: 488, ratio: 0.63
185185

test/test_zxpass.py

Lines changed: 137 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from typing import Any, Callable, Optional
2121
import pyzx as zx
2222
from pyzx.circuit.gates import Measurement as PyzxMeasurement, Reset as PyzxReset
23-
from pyzx.circuit.gates import ConditionalGate
23+
from pyzx.circuit.gates import ConditionalGate, SWAP
2424
import numpy as np
2525
import pytest
2626

@@ -694,6 +694,121 @@ def test_compute_output_permutation_non_bijective() -> None:
694694
compute_output_permutation(g)
695695

696696

697+
def test_permutation_swaps_in_pipeline() -> None:
698+
"""Test that _optimize_unitary prepends SWAP gates for the output permutation.
699+
700+
After extraction with up_to_perm=True, the output permutation should be
701+
prepended as SWAP gate objects. Each SWAP counts as one gate instead of
702+
three CNOTs, improving the gate-count comparison.
703+
"""
704+
# pylint: disable=import-outside-toplevel
705+
from fractions import Fraction
706+
from zxpass.zxpass import (
707+
_optimize_unitary,
708+
_permutation_to_swaps,
709+
compute_output_permutation,
710+
)
711+
712+
# Build a circuit that produces a non-trivial output permutation and is
713+
# large enough that the gate-count fallback does not trigger.
714+
c = zx.Circuit(4)
715+
for j in range(4):
716+
c.add_gate("HAD", j)
717+
for _ in range(3):
718+
for i in range(3):
719+
c.add_gate("CNOT", i, i + 1)
720+
for j in range(4):
721+
c.add_gate("ZPhase", j, phase=Fraction(1, 2 + j))
722+
for i in range(3):
723+
c.add_gate("CNOT", i, i + 1)
724+
725+
# Replay the same extraction path to compute the SWAP prefix that
726+
# ``_optimize_unitary`` should prepend.
727+
g = c.to_graph()
728+
zx.simplify.full_reduce(g)
729+
zx.extract.extract_circuit(g, up_to_perm=True)
730+
expected_swaps = _permutation_to_swaps(compute_output_permutation(g))
731+
# Sanity-check that this circuit exercises the SWAP-prefix path, so the
732+
# test does not silently degenerate into a trivial no-op assertion.
733+
assert expected_swaps, "Test circuit should produce a non-trivial permutation."
734+
735+
optimized = _optimize_unitary(c)
736+
737+
# Confirm the fallback did not trigger for this circuit; otherwise the
738+
# SWAP-prefix assertions below would be vacuously skipped.
739+
assert list(optimized.gates) != list(c.gates), (
740+
"Test circuit unexpectedly hit the gate-count fallback."
741+
)
742+
743+
# The optimized circuit should begin with exactly the expected SWAP prefix.
744+
assert len(optimized.gates) >= len(expected_swaps)
745+
for k, (i, j) in enumerate(expected_swaps):
746+
gate = optimized.gates[k]
747+
assert isinstance(gate, SWAP), (
748+
f"Expected SWAP at position {k}, got {type(gate).__name__}."
749+
)
750+
assert {gate.control, gate.target} == {i, j}, (
751+
f"SWAP at position {k} acts on {{{gate.control}, {gate.target}}}, "
752+
f"expected {{{i}, {j}}}."
753+
)
754+
# No SWAPs should appear after the prefix.
755+
for gate in optimized.gates[len(expected_swaps):]:
756+
assert not isinstance(gate, SWAP), (
757+
"SWAP gates should only appear at the beginning of the circuit."
758+
)
759+
760+
761+
def test_permutation_to_swaps_correctness() -> None:
762+
"""Test that _permutation_to_swaps produces a correct SWAP sequence."""
763+
from zxpass.zxpass import _permutation_to_swaps # pylint: disable=import-outside-toplevel
764+
765+
# Identity permutation: no SWAPs needed.
766+
assert not _permutation_to_swaps({0: 0, 1: 1, 2: 2})
767+
768+
# Simple transposition.
769+
swaps = _permutation_to_swaps({0: 1, 1: 0})
770+
assert len(swaps) == 1
771+
assert set(swaps[0]) == {0, 1}
772+
773+
# 3-cycle: needs 2 transpositions.
774+
perm = {0: 1, 1: 2, 2: 0}
775+
swaps = _permutation_to_swaps(perm)
776+
# Verify the SWAPs implement the permutation.
777+
state = list(range(3))
778+
for i, j in swaps:
779+
state[i], state[j] = state[j], state[i]
780+
assert state == [perm[k] for k in range(3)]
781+
782+
# Larger permutation with multiple cycles.
783+
perm = {0: 2, 1: 0, 2: 1, 3: 4, 4: 3}
784+
swaps = _permutation_to_swaps(perm)
785+
state = list(range(5))
786+
for i, j in swaps:
787+
state[i], state[j] = state[j], state[i]
788+
assert state == [perm[k] for k in range(5)]
789+
790+
791+
def test_permutation_to_swaps_invalid_input() -> None:
792+
"""Test that _permutation_to_swaps rejects non-bijective input."""
793+
from zxpass.zxpass import _permutation_to_swaps # pylint: disable=import-outside-toplevel
794+
795+
# Non-contiguous keys (missing 1).
796+
with pytest.raises(ValueError, match="keys to be range"):
797+
_permutation_to_swaps({0: 0, 2: 2})
798+
799+
# Out-of-range keys (shifted by 1).
800+
with pytest.raises(ValueError, match="keys to be range"):
801+
_permutation_to_swaps({1: 1, 2: 2})
802+
803+
# Values outside range.
804+
with pytest.raises(ValueError, match="values to be a permutation"):
805+
_permutation_to_swaps({0: 5, 1: 1})
806+
807+
# Repeated values (not a permutation).
808+
with pytest.raises(ValueError, match="values to be a permutation"):
809+
_permutation_to_swaps({0: 1, 1: 1})
810+
811+
697812
def test_post_extraction_cleanup() -> None:
698813
"""Test that ``_optimize_unitary`` applies ``basic_optimization`` after extraction.
699814
@@ -738,32 +853,35 @@ def test_post_extraction_cleanup() -> None:
738853

739854

740855
def test_post_extraction_cleanup_equivalence() -> None:
741-
"""Test that the post-extraction cleanup preserves circuit equivalence.
742-
743-
Runs multiple circuits through the full ZXPass pipeline and verifies
744-
statevector equivalence after the basic_optimization post-pass.
745-
"""
746-
circuits = []
747-
748-
# Bell state preparation with extra gates.
856+
"""Test that post-extraction cleanup and permutation integration preserve equivalence."""
749857
qc1 = QuantumCircuit(3)
750858
qc1.h(0)
751859
qc1.cx(0, 1)
752860
qc1.cx(1, 2)
753861
qc1.h(2)
754862
qc1.cx(2, 0)
755-
circuits.append(qc1)
756863

757-
# Multi-CX circuit.
758-
qc2 = QuantumCircuit(4)
759-
for i in range(3):
864+
qc2 = QuantumCircuit(5)
865+
for i in range(5):
866+
qc2.h(i)
867+
for i in range(4):
760868
qc2.cx(i, i + 1)
761-
qc2.h(0)
762-
qc2.cx(3, 0)
763-
qc2.h(1)
764-
circuits.append(qc2)
765-
766-
for qc in circuits:
869+
qc2.cx(4, 0)
870+
for i in range(5):
871+
qc2.rz(0.3 * (i + 1), i)
872+
873+
qc3 = QuantumCircuit(4)
874+
qc3.h(0)
875+
qc3.cx(0, 1)
876+
qc3.h(1)
877+
qc3.cx(1, 2)
878+
qc3.h(2)
879+
qc3.cx(2, 3)
880+
qc3.cx(3, 0)
881+
qc3.cx(2, 1)
882+
qc3.cx(1, 0)
883+
884+
for qc in [qc1, qc2, qc3]:
767885
assert _run_zxpass(qc), f"Equivalence failed for circuit:\n{qc}"
768886

769887

zxpass/zxpass.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,21 +177,67 @@ def compute_output_permutation(g: Any) -> Dict[int, int]:
177177
return perm
178178

179179

180+
def _permutation_to_swaps(perm: Dict[int, int]) -> List[Tuple[int, int]]:
181+
"""Decompose a permutation into transpositions (SWAP pairs).
182+
183+
Uses cycle decomposition. The returned list, applied left to right,
184+
implements the forward permutation (wire *j* ends up holding the state
185+
of input qubit ``perm[j]``).
186+
187+
Raises ``ValueError`` if ``perm`` is not a bijection on
188+
``range(len(perm))`` (i.e. if its keys or values are not exactly the
189+
set of integers ``{0, 1, ..., len(perm) - 1}``).
190+
"""
191+
n = len(perm)
192+
expected = set(range(n))
193+
if set(perm.keys()) != expected:
194+
raise ValueError(
195+
f"Expected permutation keys to be range({n}), "
196+
f"got {sorted(perm.keys())}."
197+
)
198+
if set(perm.values()) != expected:
199+
raise ValueError(
200+
f"Expected permutation values to be a permutation of range({n}), "
201+
f"got {sorted(perm.values())}."
202+
)
203+
current = [perm[i] for i in range(n)]
204+
swaps: List[Tuple[int, int]] = []
205+
for i in range(n):
206+
while current[i] != i:
207+
j = current[i]
208+
swaps.append((i, j))
209+
current[i], current[j] = current[j], current[i]
210+
swaps.reverse()
211+
return swaps
212+
213+
180214
def _optimize_unitary(c: zx.Circuit) -> zx.Circuit:
181215
"""Optimise a purely unitary PyZX circuit using full_reduce and extraction.
182216
183-
After extraction, ``basic_optimization`` converts HAD-CZ-HAD sequences to
184-
CNOTs and cancels redundant single-qubit gates. If the result still has at
185-
least as many gates as the original circuit, the original is returned
186-
unchanged to avoid regressions on small circuits with compact multi-qubit
187-
gates (e.g. Toffoli, Fredkin). The comparison counts PyZX gate objects
188-
directly; since ``_recover_dag`` emits one Qiskit op per PyZX gate, this
189-
matches the Qiskit-side ``size()`` that downstream passes see.
217+
Extracts with ``up_to_perm=True`` so that ``basic_optimization`` runs on a
218+
circuit free of SWAP-decomposition clutter. The output permutation is then
219+
prepended as SWAP gates (each counting as one gate rather than three CNOTs),
220+
giving a fairer gate-count comparison against the original circuit. If the
221+
result still has at least as many gates as the original, the original is
222+
returned unchanged to avoid regressions on small circuits with compact
223+
multi-qubit gates (e.g. Toffoli, Fredkin). The comparison counts PyZX gate
224+
objects directly; since ``_recover_dag`` emits one Qiskit op per PyZX gate,
225+
this matches the Qiskit-side ``size()`` that downstream passes see.
190226
"""
191227
g = c.to_graph()
192228
zx.simplify.full_reduce(g)
193-
optimized = zx.extract.extract_circuit(g)
229+
optimized = zx.extract.extract_circuit(g, up_to_perm=True)
230+
perm = compute_output_permutation(g)
194231
optimized = basic_optimization(optimized.to_basic_gates(), do_swaps=False)
232+
# Prepend SWAP gates for the output permutation.
233+
swap_pairs = _permutation_to_swaps(perm)
234+
if swap_pairs:
235+
with_perm = zx.Circuit(c.qubits)
236+
for i, j in swap_pairs:
237+
with_perm.add_gate(SWAP(i, j))
238+
for gate in optimized.gates:
239+
with_perm.add_gate(gate)
240+
optimized = with_perm
195241
# TODO: Consider a two-axis comparison keyed primarily on 2-qubit gate
196242
# count (``twoqubitcount()``), with total gate count as a tiebreaker. The
197243
# 2-qubit count is the dominant hardware cost and is naturally apples-to-

0 commit comments

Comments
 (0)