Skip to content

Commit 959b640

Browse files
authored
Merge pull request #4 from QuantumSavory/feature/circuit-traversal
Add circuit traversal function for gate transformations
1 parent 46e1ee7 commit 959b640

File tree

4 files changed

+302
-1
lines changed

4 files changed

+302
-1
lines changed

CLAUDE.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Tools for Pauli Based Computation (PBC), a modality of quantum computation.
55
## Project Structure
66

77
- `src/PBCCompiler.jl` - Main module with circuit operations and compiler infrastructure
8+
- `src/traversal.jl` - Circuit traversal utilities for gate simplifications
89
- `test/` - Test suite using TestItemRunner.jl
910

1011
## Dependencies
@@ -37,14 +38,42 @@ The `preprocess_circuit` function transforms circuits through stages:
3738
- `MockRuntime` - Testing runtime where measurements return deterministic results
3839
- `ComputerState` - Tracks circuit, instruction pointer, and memory state
3940

41+
### Circuit Traversal
42+
The `traversal` function (`src/traversal.jl`) applies transformations to adjacent pairs of circuit operations:
43+
```julia
44+
traversal(circuit, pair_transformation, direction=:right, starting_index=1, end_index=:end)
45+
```
46+
- `pair_transformation(op1, op2)` returns:
47+
- `(new_op1, new_op2)` tuple to replace the pair
48+
- Single operation to combine the pair into one
49+
- `nothing` to keep unchanged
50+
- Supports left-to-right (`:right`) or right-to-left (`:left`) traversal
51+
- Used for gate commutation, simplification, and compilation passes
52+
53+
**Note on Moshi types**: Use `Moshi.Data.isa_variant(op, CircuitOp.Pauli)` instead of `op isa CircuitOp.Pauli` to check variant types.
54+
4055
## Development
4156

42-
Run tests:
57+
### Workflow
58+
1. Always pull latest master: `git pull`
59+
2. Create feature branches for new work
60+
3. Commit often at each change
61+
4. Update CLAUDE.md with new functionality
62+
5. Run tests before creating PRs
63+
64+
### Run tests
4365
```julia
4466
using Pkg
4567
Pkg.test("PBCCompiler")
4668
```
4769

70+
### Related source code
71+
- QuantumClifford.jl source: `../QuantumClifford.jl`
72+
- QuantumInterface.jl source: `../QuantumInterface.jl`
73+
74+
### Reference paper
75+
- "Game of Surface Codes" - https://quantum-journal.org/papers/q-2019-03-05-128/pdf/
76+
4877
## Related Packages
4978

5079
- [QuantumClifford.jl](https://github.com/QuantumSavory/QuantumClifford.jl) - Stabilizer formalism

src/PBCCompiler.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const Circuit = Vector{CircuitOp.Type}
6060

6161
using .CircuitOp: Measurement, Pauli, ExpHalfPiPauli, ExpQuatPiPauli, ExpEighPiPauli, PrepMagic, PauliConditional, BitConditional
6262

63+
include("traversal.jl")
64+
6365
##
6466

6567
"""TODO docstring"""

src/traversal.jl

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
Circuit traversal utilities for gate simplifications and transformations.
3+
"""
4+
5+
"""
6+
traversal(circuit::Circuit, pair_transformation, direction=:right, starting_index=1, end_index=:end)
7+
8+
Traverse a circuit and apply `pair_transformation` to each pair of adjacent operations.
9+
10+
# Arguments
11+
- `circuit::Circuit`: The circuit to traverse (modified in-place)
12+
- `pair_transformation`: A function that takes two operations and returns:
13+
- A tuple of operations `(op1, op2)` to replace the current pair
14+
- A single operation to replace both operations (combining them)
15+
- `nothing` to keep the original operations unchanged
16+
- `direction`: `:right` to traverse left-to-right, `:left` to traverse right-to-left (default: `:right`)
17+
- `starting_index`: Index to start traversal from (default: `1`)
18+
- `end_index`: Index to end traversal at, or `:end` for the last valid pair (default: `:end`)
19+
20+
# Returns
21+
The modified circuit.
22+
23+
# Example
24+
```julia
25+
# Swap adjacent operations
26+
swap_transform(op1, op2) = (op2, op1)
27+
traversal(circuit, swap_transform)
28+
```
29+
"""
30+
function traversal(circuit::Circuit, pair_transformation, direction::Symbol=:right, starting_index::Int=1, end_index::Union{Int,Symbol}=:end)
31+
if isempty(circuit) || length(circuit) < 2
32+
return circuit
33+
end
34+
35+
# Resolve end_index
36+
actual_end = end_index === :end ? length(circuit) - 1 : end_index
37+
38+
# Validate indices
39+
if starting_index < 1 || starting_index > length(circuit) - 1
40+
return circuit
41+
end
42+
if actual_end < 1 || actual_end > length(circuit) - 1
43+
actual_end = length(circuit) - 1
44+
end
45+
46+
if direction === :right
47+
_traversal_right!(circuit, pair_transformation, starting_index, actual_end)
48+
elseif direction === :left
49+
_traversal_left!(circuit, pair_transformation, starting_index, actual_end)
50+
else
51+
throw(ArgumentError("direction must be :right or :left, got :$direction"))
52+
end
53+
54+
return circuit
55+
end
56+
57+
"""
58+
Internal: Traverse left-to-right, applying pair_transformation to adjacent pairs.
59+
"""
60+
function _traversal_right!(circuit::Circuit, pair_transformation, start_idx::Int, end_idx::Int)
61+
i = start_idx
62+
while i <= end_idx && i <= length(circuit) - 1
63+
op1 = circuit[i]
64+
op2 = circuit[i + 1]
65+
66+
result = pair_transformation(op1, op2)
67+
68+
if result === nothing
69+
# No change, move to next pair
70+
i += 1
71+
elseif result isa Tuple && !(result isa CircuitOp.Type) && length(result) == 2
72+
# Replace with tuple elements (explicitly check it's a 2-tuple and not a CircuitOp)
73+
circuit[i] = result[1]
74+
circuit[i + 1] = result[2]
75+
i += 1
76+
else
77+
# Single operation replaces both - splice to remove one element
78+
circuit[i] = result
79+
deleteat!(circuit, i + 1)
80+
# Adjust end_idx since we removed an element
81+
end_idx = min(end_idx, length(circuit) - 1)
82+
# Don't increment i, check the new pair at this position
83+
end
84+
end
85+
end
86+
87+
"""
88+
Internal: Traverse right-to-left, applying pair_transformation to adjacent pairs.
89+
"""
90+
function _traversal_left!(circuit::Circuit, pair_transformation, start_idx::Int, end_idx::Int)
91+
i = end_idx
92+
while i >= start_idx && i >= 1
93+
op1 = circuit[i]
94+
op2 = circuit[i + 1]
95+
96+
result = pair_transformation(op1, op2)
97+
98+
if result === nothing
99+
# No change, move to previous pair
100+
i -= 1
101+
elseif result isa Tuple && !(result isa CircuitOp.Type) && length(result) == 2
102+
# Replace with tuple elements (explicitly check it's a 2-tuple and not a CircuitOp)
103+
circuit[i] = result[1]
104+
circuit[i + 1] = result[2]
105+
i -= 1
106+
else
107+
# Single operation replaces both - splice to remove one element
108+
circuit[i] = result
109+
deleteat!(circuit, i + 1)
110+
# Move to previous pair
111+
i -= 1
112+
end
113+
end
114+
end

test/test_traversal.jl

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
@testitem "Traversal" tags=[:traversal] begin
2+
3+
using PBCCompiler
4+
using PBCCompiler: Circuit, CircuitOp, Pauli, Measurement, ExpHalfPiPauli, traversal
5+
using QuantumClifford: @P_str
6+
using Moshi.Data: isa_variant
7+
8+
@testset "Basic traversal" begin
9+
# Test empty circuit
10+
circuit = Circuit()
11+
traversal(circuit, (a, b) -> nothing)
12+
@test isempty(circuit)
13+
14+
# Test single-element circuit (no pairs to traverse)
15+
circuit = Circuit([Pauli(P"X", [1])])
16+
traversal(circuit, (a, b) -> nothing)
17+
@test length(circuit) == 1
18+
end
19+
20+
@testset "Swap transformation" begin
21+
# Simple swap: swap adjacent operations
22+
swap_transform(op1, op2) = (op2, op1)
23+
24+
# Create circuit [X, Y, Z]
25+
circuit = Circuit([
26+
Pauli(P"X", [1]),
27+
Pauli(P"Y", [1]),
28+
Pauli(P"Z", [1])
29+
])
30+
31+
# After traversal with swap:
32+
# Pair (X, Y) -> (Y, X): [Y, X, Z]
33+
# Pair (X, Z) -> (Z, X): [Y, Z, X]
34+
traversal(circuit, swap_transform)
35+
36+
@test isa_variant(circuit[1], CircuitOp.Pauli)
37+
@test isa_variant(circuit[2], CircuitOp.Pauli)
38+
@test isa_variant(circuit[3], CircuitOp.Pauli)
39+
# Check that X has moved to the end (bubble sort behavior)
40+
@test circuit[1].pauli == P"Y"
41+
@test circuit[2].pauli == P"Z"
42+
@test circuit[3].pauli == P"X"
43+
end
44+
45+
@testset "No-op transformation" begin
46+
# Transformation that returns nothing (no change)
47+
no_op(op1, op2) = nothing
48+
49+
circuit = Circuit([
50+
Pauli(P"X", [1]),
51+
Pauli(P"Y", [1]),
52+
Pauli(P"Z", [1])
53+
])
54+
55+
original_length = length(circuit)
56+
traversal(circuit, no_op)
57+
58+
@test length(circuit) == original_length
59+
@test circuit[1].pauli == P"X"
60+
@test circuit[2].pauli == P"Y"
61+
@test circuit[3].pauli == P"Z"
62+
end
63+
64+
@testset "Combining transformation" begin
65+
# Transformation that combines two operations into one
66+
# For testing, combine any two Paulis into a single X
67+
combine_paulis(op1, op2) = begin
68+
if isa_variant(op1, CircuitOp.Pauli) && isa_variant(op2, CircuitOp.Pauli)
69+
return Pauli(P"X", [1]) # Combine into single X
70+
end
71+
return nothing
72+
end
73+
74+
circuit = Circuit([
75+
Pauli(P"X", [1]),
76+
Pauli(P"Y", [1]),
77+
Pauli(P"Z", [1])
78+
])
79+
80+
# After first combination: [X, Z] (X and Y combined into X)
81+
# After second combination: [X] (X and Z combined into X)
82+
traversal(circuit, combine_paulis)
83+
84+
@test length(circuit) == 1
85+
@test isa_variant(circuit[1], CircuitOp.Pauli)
86+
end
87+
88+
@testset "Left direction traversal" begin
89+
swap_transform(op1, op2) = (op2, op1)
90+
91+
circuit = Circuit([
92+
Pauli(P"X", [1]),
93+
Pauli(P"Y", [1]),
94+
Pauli(P"Z", [1])
95+
])
96+
97+
# With left direction, start from the right side
98+
# Pair (Y, Z) -> (Z, Y): [X, Z, Y]
99+
# Pair (X, Z) -> (Z, X): [Z, X, Y]
100+
traversal(circuit, swap_transform, :left)
101+
102+
@test circuit[1].pauli == P"Z"
103+
@test circuit[2].pauli == P"X"
104+
@test circuit[3].pauli == P"Y"
105+
end
106+
107+
@testset "Partial traversal with indices" begin
108+
swap_transform(op1, op2) = (op2, op1)
109+
110+
circuit = Circuit([
111+
Pauli(P"X", [1]),
112+
Pauli(P"Y", [1]),
113+
Pauli(P"Z", [1]),
114+
Pauli(P"I", [1])
115+
])
116+
117+
# Only traverse from index 2 to 2 (pair at positions 2,3)
118+
traversal(circuit, swap_transform, :right, 2, 2)
119+
120+
# Only Y and Z should be swapped
121+
@test circuit[1].pauli == P"X"
122+
@test circuit[2].pauli == P"Z"
123+
@test circuit[3].pauli == P"Y"
124+
@test circuit[4].pauli == P"I"
125+
end
126+
127+
@testset "Conditional transformation" begin
128+
# Only swap if first operation is an X Pauli
129+
conditional_swap(op1, op2) = begin
130+
if isa_variant(op1, CircuitOp.Pauli) && op1.pauli == P"X"
131+
return (op2, op1)
132+
end
133+
return nothing
134+
end
135+
136+
circuit = Circuit([
137+
Pauli(P"X", [1]),
138+
Pauli(P"Y", [1]),
139+
Pauli(P"Z", [1])
140+
])
141+
142+
# Pair (X, Y): X is first, so swap -> [Y, X, Z]
143+
# Pair (X, Z): X is first, so swap -> [Y, Z, X]
144+
traversal(circuit, conditional_swap)
145+
146+
@test circuit[1].pauli == P"Y"
147+
@test circuit[2].pauli == P"Z"
148+
@test circuit[3].pauli == P"X"
149+
end
150+
151+
@testset "Invalid direction error" begin
152+
circuit = Circuit([Pauli(P"X", [1]), Pauli(P"Y", [1])])
153+
@test_throws ArgumentError traversal(circuit, (a,b) -> nothing, :invalid)
154+
end
155+
156+
end

0 commit comments

Comments
 (0)