Skip to content

Add R_XX, R_YY, R_ZZ, and R_PAULI parametric Pauli-product rotation gates#161

Open
ahkatlio wants to merge 1 commit into
QuEraComputing:mainfrom
ahkatlio:Implement-R_XX,-R_ZZ,-R_YY,-and-R_PAULI-gates
Open

Add R_XX, R_YY, R_ZZ, and R_PAULI parametric Pauli-product rotation gates#161
ahkatlio wants to merge 1 commit into
QuEraComputing:mainfrom
ahkatlio:Implement-R_XX,-R_ZZ,-R_YY,-and-R_PAULI-gates

Conversation

@ahkatlio

Copy link
Copy Markdown

Closes #142.


What this adds

Four new gates following the semantics of the clifft two-qubit Pauli rotations reference:

Gate Shorthand Semantics
R_XX R_XX(alpha) q0 q1 exp(-i alpha pi/2 XX)
R_YY R_YY(alpha) q0 q1 exp(-i alpha pi/2 YY)
R_ZZ R_ZZ(alpha) q0 q1 exp(-i alpha pi/2 ZZ)
R_PAULI R_PAULI(alpha) X0*Y1*Z2 exp(-i alpha pi/2 P) for any Pauli product P

R_XX, R_YY, and R_ZZ are just the two-qubit special cases of R_PAULI. All four accept arbitrary real angles, not just Clifford multiples.


Implementation overview

Storage encoding

The gates reuse the existing SPP/tag encoding that TPP and T already rely on. Each gate is stored as a stim SPP instruction whose tag carries the gate name and angle:

R_XX(0.25) 0 1   <-->   SPP[R_XX(theta=0.25*pi)] X0*X1
R_PAULI(0.3) X0*Y1*Z2   <-->   SPP[R_PAULI(theta=0.3*pi)] X0*Y1*Z2

This means no new stim instructions are needed and the full existing infrastructure (tagging, canonicalization, round-trip) works without modification.

ZX graph backend: r_pauli in instructions.py

A single new function r_pauli(b, paulis, theta, dagger=False) handles all four gates. It reuses _pauli_product_phase, the same ladder-of-CNOTs decomposition already used by spp and tpp:

  1. Rotate each qubit into the Z basis.
  2. Accumulate Z-parity onto the last qubit with CNOTs.
  3. Apply a parametric r_z(theta) (or its negative) on the parity qubit.
  4. Uncompute.

The dagger flag negates theta before dispatch, which is the cleanest way to handle SPP_DAG-tagged instructions that come from stim when building an inverse circuit.

Parse dispatch: parse.py

_PARAMETRIC_GATE_PARAMS gains entries for all four gate names. The parse_stim_circuit function gets a new branch that fires before the existing SPP[T] branch:

if name in ("SPP", "SPP_DAG") and instruction.tag and instruction.tag != "T":
    parsed = parse_parametric_tag(instruction)
    if parsed is not None:
        gate_name, params = parsed
        if gate_name in ("R_PAULI", "R_XX", "R_YY", "R_ZZ"):
            is_dag = name == "SPP_DAG"
            for paulis, invert in _iter_pauli_products(instruction):
                r_pauli(b, paulis, params["theta"], dagger=is_dag ^ invert)
            continue

This correctly handles both SPP and SPP_DAG forms, including inverted Pauli targets.

Shorthand text: program_text.py

shorthand_to_stim gains two new regex substitutions (applied before the single-qubit R_X/R_Y/R_Z rule to avoid partial matches):

  • R_XX(alpha) q0 q1 -> SPP[R_XX(theta=alpha*pi)] Xq0*Xq1
  • R_PAULI(alpha) X0*Y1*... -> SPP[R_PAULI(theta=alpha*pi)] X0*Y1*...

stim_to_shorthand gains matching back-conversion rules so that str(Circuit(...)) always produces the human-readable shorthand. The R_XX/R_YY/R_ZZ rule fires before the general R_PAULI rule so that a two-qubit same-axis product round-trips as R_ZZ(0.3) 0 1, not as R_PAULI(0.3) Z0*Z1.

Duplicate qubit detection for R_XX/R_YY/R_ZZ is done at the regex replacement step, so the error is raised at circuit construction time rather than during later graph traversal.

Inverse: circuit.py

Stim's own .inverse() method already handles SPP correctly by flipping it to SPP_DAG and reversing the target order, leaving the tag unchanged. Because our parse.py dispatch treats SPP_DAG[R_PAULI(theta)] as exp(+i theta pi/2 P), which is exactly the inverse of exp(-i theta pi/2 P), stim's output is already correct. The fix_tags loop in Circuit.inverse() simply passes these instructions through without modification.

circuit.py append API

Circuit.append gains branches for all four gate names. The R_XX/R_YY/R_ZZ branch validates target count and duplicate qubits, builds the Pauli targets list, and re-encodes to SPP. The R_PAULI branch does the same without qubit validation since the caller provides raw Pauli targets directly.

Clifford detection: clifford.py

is_clifford is extended to recognize SPP/SPP_DAG instructions tagged with R_PAULI/R_XX/R_YY/R_ZZ: if the theta is a half-pi multiple the gate is Clifford, otherwise it is not.


Files changed

File What changed
src/tsim/core/instructions.py New r_pauli function
src/tsim/core/parse.py Import r_pauli, extend _PARAMETRIC_GATE_PARAMS, add SPP dispatch branch
src/tsim/utils/program_text.py Shorthand <-> stim conversions for all four gates
src/tsim/circuit.py append branches for all four gates, inverse pass-through
src/tsim/utils/clifford.py Extend is_clifford to cover SPP-encoded parametric rotations
test/unit/test_r_pauli_rotations.py New test module (91 tests)

Tests

91 new tests in test/unit/test_r_pauli_rotations.py organized into six classes:

  1. TestShorthandRoundTrip -- text -> stim -> text stability for all four gate syntaxes, including negative angles, scientific notation, and multi-qubit products.

  2. TestAppendAPI -- Circuit.append for all four gates, including error cases for missing angles, duplicate qubits, and wrong target counts.

  3. TestCliffordAngleParity -- at every integer alpha, R_PP(alpha) 0 1 matches stim's reference tableau for the corresponding Clifford gate. Parametrized over all three two-qubit gates and alpha in {0, 1, 2, 3}.

  4. TestAnalyticCorrectness -- unitary matrices compared against closed-form formulas:

    • R_XX(alpha) = cos(alpha pi/2) I - i sin(alpha pi/2) XX
    • R_ZZ(alpha) = diag(e^{-i alpha pi/2}, e^{+i alpha pi/2}, e^{+i alpha pi/2}, e^{-i alpha pi/2})
    • R_YY verified via S-gate conjugation
    • Single-qubit R_PAULI Z0 matches R_Z, single-qubit R_PAULI X0 matches R_X
  5. TestInverse -- (C + C.inverse()).to_matrix() is identity up to global phase for all four gates, including negative angles, non-Clifford angles, and the SPP_DAG form that comes out of stim's inverse.

  6. TestMixedCircuits -- composition with CNOT, three-qubit R_PAULI, back-to-back cancellation, REPEAT blocks, and mixed R_XX + R_ZZ circuits.

All 841 previously passing tests still pass.


Usage examples

from tsim import Circuit

# Two-qubit Pauli rotations
c = Circuit("""
    H 0
    R_ZZ(0.5) 0 1
    R_XX(0.25) 0 1
    R_YY(-0.1) 1 2
    M 0 1 2
""")

# General multi-qubit Pauli product rotation
c2 = Circuit("R_PAULI(0.3) X0*Y1*Z2")

# Via Circuit.append
c3 = Circuit()
c3.append("R_ZZ", [0, 1], arg=0.5)

# Inverse round-trips correctly
assert (c2 + c2.inverse()).is_clifford is False  # non-Clifford, but...
# ...the combined unitary is identity up to global phase

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64f4ee4aa4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +150 to +152
if gate_name in ("R_PAULI", "R_XX", "R_YY", "R_ZZ"):
if not is_half_pi_multiple(params["theta"]):
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expand Clifford Pauli rotations before declaring them Clifford

For half-π Pauli rotations other than theta=0.5, this makes Circuit.is_clifford return true even though Circuit.stim_circuit cannot produce an equivalent Clifford circuit: stim_circuit calls expand_clifford_rotations, but _try_clifford_expansion only expands tagged I instructions, so tagged SPP rotations are returned unchanged. For example, R_XX(0) 0 1 and R_XX(1) 0 1 are now reported Clifford, but the exported Stim circuit is still native SPP[...] X0*X1, which Stim interprets as the fixed sqrt-Pauli rotation instead of identity/XX.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement R_XX, R_ZZ, R_YY, and R_PAULI gates

1 participant