Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 30 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Tools for Pauli Based Computation (PBC), a modality of quantum computation.
## Project Structure

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

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

### Circuit Traversal
The `traversal` function (`src/traversal.jl`) applies transformations to adjacent pairs of circuit operations:
```julia
traversal(circuit, pair_transformation, direction=:right, starting_index=1, end_index=:end)
```
- `pair_transformation(op1, op2)` returns:
- `(new_op1, new_op2)` tuple to replace the pair
- Single operation to combine the pair into one
- `nothing` to keep unchanged
- Supports left-to-right (`:right`) or right-to-left (`:left`) traversal
- Used for gate commutation, simplification, and compilation passes

**Note on Moshi types**: Use `Moshi.Data.isa_variant(op, CircuitOp.Pauli)` instead of `op isa CircuitOp.Pauli` to check variant types.

## Development

Run tests:
### Workflow
1. Always pull latest master: `git pull`
2. Create feature branches for new work
3. Commit often at each change
4. Update CLAUDE.md with new functionality
5. Run tests before creating PRs

### Run tests
```julia
using Pkg
Pkg.test("PBCCompiler")
```

### Related source code
- QuantumClifford.jl source: `../QuantumClifford.jl`
- QuantumInterface.jl source: `../QuantumInterface.jl`

### Reference paper
- "Game of Surface Codes" - https://quantum-journal.org/papers/q-2019-03-05-128/pdf/

## Related Packages

- [QuantumClifford.jl](https://github.com/QuantumSavory/QuantumClifford.jl) - Stabilizer formalism
Expand Down
2 changes: 2 additions & 0 deletions src/PBCCompiler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const Circuit = Vector{CircuitOp.Type}

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

include("traversal.jl")

##

"""TODO docstring"""
Expand Down
114 changes: 114 additions & 0 deletions src/traversal.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""
Circuit traversal utilities for gate simplifications and transformations.
"""

"""
traversal(circuit::Circuit, pair_transformation, direction=:right, starting_index=1, end_index=:end)

Traverse a circuit and apply `pair_transformation` to each pair of adjacent operations.

# Arguments
- `circuit::Circuit`: The circuit to traverse (modified in-place)
- `pair_transformation`: A function that takes two operations and returns:
- A tuple of operations `(op1, op2)` to replace the current pair
- A single operation to replace both operations (combining them)
- `nothing` to keep the original operations unchanged
- `direction`: `:right` to traverse left-to-right, `:left` to traverse right-to-left (default: `:right`)
- `starting_index`: Index to start traversal from (default: `1`)
- `end_index`: Index to end traversal at, or `:end` for the last valid pair (default: `:end`)

# Returns
The modified circuit.

# Example
```julia
# Swap adjacent operations
swap_transform(op1, op2) = (op2, op1)
traversal(circuit, swap_transform)
```
"""
function traversal(circuit::Circuit, pair_transformation, direction::Symbol=:right, starting_index::Int=1, end_index::Union{Int,Symbol}=:end)
if isempty(circuit) || length(circuit) < 2
return circuit
end

# Resolve end_index
actual_end = end_index === :end ? length(circuit) - 1 : end_index

# Validate indices
if starting_index < 1 || starting_index > length(circuit) - 1
return circuit
end
if actual_end < 1 || actual_end > length(circuit) - 1
actual_end = length(circuit) - 1
end

if direction === :right
_traversal_right!(circuit, pair_transformation, starting_index, actual_end)
elseif direction === :left
_traversal_left!(circuit, pair_transformation, starting_index, actual_end)
else
throw(ArgumentError("direction must be :right or :left, got :$direction"))
end

return circuit
end

"""
Internal: Traverse left-to-right, applying pair_transformation to adjacent pairs.
"""
function _traversal_right!(circuit::Circuit, pair_transformation, start_idx::Int, end_idx::Int)
i = start_idx
while i <= end_idx && i <= length(circuit) - 1
op1 = circuit[i]
op2 = circuit[i + 1]

result = pair_transformation(op1, op2)

if result === nothing
# No change, move to next pair
i += 1
elseif result isa Tuple && !(result isa CircuitOp.Type) && length(result) == 2
# Replace with tuple elements (explicitly check it's a 2-tuple and not a CircuitOp)
circuit[i] = result[1]
circuit[i + 1] = result[2]
i += 1
else
# Single operation replaces both - splice to remove one element
circuit[i] = result
deleteat!(circuit, i + 1)
# Adjust end_idx since we removed an element
end_idx = min(end_idx, length(circuit) - 1)
# Don't increment i, check the new pair at this position
end
end
end

"""
Internal: Traverse right-to-left, applying pair_transformation to adjacent pairs.
"""
function _traversal_left!(circuit::Circuit, pair_transformation, start_idx::Int, end_idx::Int)
i = end_idx
while i >= start_idx && i >= 1
op1 = circuit[i]
op2 = circuit[i + 1]

result = pair_transformation(op1, op2)

if result === nothing
# No change, move to previous pair
i -= 1
elseif result isa Tuple && !(result isa CircuitOp.Type) && length(result) == 2
# Replace with tuple elements (explicitly check it's a 2-tuple and not a CircuitOp)
circuit[i] = result[1]
circuit[i + 1] = result[2]
i -= 1
else
# Single operation replaces both - splice to remove one element
circuit[i] = result
deleteat!(circuit, i + 1)
# Move to previous pair
i -= 1
end
end
end
156 changes: 156 additions & 0 deletions test/test_traversal.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
@testitem "Traversal" tags=[:traversal] begin

using PBCCompiler
using PBCCompiler: Circuit, CircuitOp, Pauli, Measurement, ExpHalfPiPauli, traversal
using QuantumClifford: @P_str
using Moshi.Data: isa_variant

@testset "Basic traversal" begin
# Test empty circuit
circuit = Circuit()
traversal(circuit, (a, b) -> nothing)
@test isempty(circuit)

# Test single-element circuit (no pairs to traverse)
circuit = Circuit([Pauli(P"X", [1])])
traversal(circuit, (a, b) -> nothing)
@test length(circuit) == 1
end

@testset "Swap transformation" begin
# Simple swap: swap adjacent operations
swap_transform(op1, op2) = (op2, op1)

# Create circuit [X, Y, Z]
circuit = Circuit([
Pauli(P"X", [1]),
Pauli(P"Y", [1]),
Pauli(P"Z", [1])
])

# After traversal with swap:
# Pair (X, Y) -> (Y, X): [Y, X, Z]
# Pair (X, Z) -> (Z, X): [Y, Z, X]
traversal(circuit, swap_transform)

@test isa_variant(circuit[1], CircuitOp.Pauli)
@test isa_variant(circuit[2], CircuitOp.Pauli)
@test isa_variant(circuit[3], CircuitOp.Pauli)
# Check that X has moved to the end (bubble sort behavior)
@test circuit[1].pauli == P"Y"
@test circuit[2].pauli == P"Z"
@test circuit[3].pauli == P"X"
end

@testset "No-op transformation" begin
# Transformation that returns nothing (no change)
no_op(op1, op2) = nothing

circuit = Circuit([
Pauli(P"X", [1]),
Pauli(P"Y", [1]),
Pauli(P"Z", [1])
])

original_length = length(circuit)
traversal(circuit, no_op)

@test length(circuit) == original_length
@test circuit[1].pauli == P"X"
@test circuit[2].pauli == P"Y"
@test circuit[3].pauli == P"Z"
end

@testset "Combining transformation" begin
# Transformation that combines two operations into one
# For testing, combine any two Paulis into a single X
combine_paulis(op1, op2) = begin
if isa_variant(op1, CircuitOp.Pauli) && isa_variant(op2, CircuitOp.Pauli)
return Pauli(P"X", [1]) # Combine into single X
end
return nothing
end

circuit = Circuit([
Pauli(P"X", [1]),
Pauli(P"Y", [1]),
Pauli(P"Z", [1])
])

# After first combination: [X, Z] (X and Y combined into X)
# After second combination: [X] (X and Z combined into X)
traversal(circuit, combine_paulis)

@test length(circuit) == 1
@test isa_variant(circuit[1], CircuitOp.Pauli)
end

@testset "Left direction traversal" begin
swap_transform(op1, op2) = (op2, op1)

circuit = Circuit([
Pauli(P"X", [1]),
Pauli(P"Y", [1]),
Pauli(P"Z", [1])
])

# With left direction, start from the right side
# Pair (Y, Z) -> (Z, Y): [X, Z, Y]
# Pair (X, Z) -> (Z, X): [Z, X, Y]
traversal(circuit, swap_transform, :left)

@test circuit[1].pauli == P"Z"
@test circuit[2].pauli == P"X"
@test circuit[3].pauli == P"Y"
end

@testset "Partial traversal with indices" begin
swap_transform(op1, op2) = (op2, op1)

circuit = Circuit([
Pauli(P"X", [1]),
Pauli(P"Y", [1]),
Pauli(P"Z", [1]),
Pauli(P"I", [1])
])

# Only traverse from index 2 to 2 (pair at positions 2,3)
traversal(circuit, swap_transform, :right, 2, 2)

# Only Y and Z should be swapped
@test circuit[1].pauli == P"X"
@test circuit[2].pauli == P"Z"
@test circuit[3].pauli == P"Y"
@test circuit[4].pauli == P"I"
end

@testset "Conditional transformation" begin
# Only swap if first operation is an X Pauli
conditional_swap(op1, op2) = begin
if isa_variant(op1, CircuitOp.Pauli) && op1.pauli == P"X"
return (op2, op1)
end
return nothing
end

circuit = Circuit([
Pauli(P"X", [1]),
Pauli(P"Y", [1]),
Pauli(P"Z", [1])
])

# Pair (X, Y): X is first, so swap -> [Y, X, Z]
# Pair (X, Z): X is first, so swap -> [Y, Z, X]
traversal(circuit, conditional_swap)

@test circuit[1].pauli == P"Y"
@test circuit[2].pauli == P"Z"
@test circuit[3].pauli == P"X"
end

@testset "Invalid direction error" begin
circuit = Circuit([Pauli(P"X", [1]), Pauli(P"Y", [1])])
@test_throws ArgumentError traversal(circuit, (a,b) -> nothing, :invalid)
end

end
Loading