diff --git a/CLAUDE.md b/CLAUDE.md index cb26437..02496ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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 diff --git a/src/PBCCompiler.jl b/src/PBCCompiler.jl index 913fc12..de6febf 100644 --- a/src/PBCCompiler.jl +++ b/src/PBCCompiler.jl @@ -60,6 +60,8 @@ const Circuit = Vector{CircuitOp.Type} using .CircuitOp: Measurement, Pauli, ExpHalfPiPauli, ExpQuatPiPauli, ExpEighPiPauli, PrepMagic, PauliConditional, BitConditional +include("traversal.jl") + ## """TODO docstring""" diff --git a/src/traversal.jl b/src/traversal.jl new file mode 100644 index 0000000..b58f90d --- /dev/null +++ b/src/traversal.jl @@ -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 diff --git a/test/test_traversal.jl b/test/test_traversal.jl new file mode 100644 index 0000000..43ee92d --- /dev/null +++ b/test/test_traversal.jl @@ -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