Skip to content

Circuit.stim_circuit reorders merged Clifford-angle rotations (wrong circuit, is_clifford=True) #225

Open

Description

@rafaelha

Circuit.stim_circuit expands Clifford-angle parametric rotations into Clifford gates, but it applies the gate sequence over the instruction's whole target list instead of once per target (or per Pauli product). When an instruction has several targets that act on the same qubit, the gates end up regrouped, and the returned circuit implements a different unitary.

Ordinary circuits trigger this, because stim merges consecutive identical instructions:

  • R_PAULI(1.0) X0 followed by R_PAULI(1.0) Z0 becomes SPP[R_PAULI(theta=1.0*pi)] X0 Z0, which expands to SPP X0 Z0 X0 Z0 instead of SPP X0 X0 Z0 Z0. The X and Z products don't commute, so the order matters.
  • Two identical U3(-1.5, -1.5, -1.0) 0 lines become I[U3(...)] 0 0, which expands to S_DAG 0 0; SQRT_X_DAG 0 0 instead of S_DAG 0; SQRT_X_DAG 0; S_DAG 0; SQRT_X_DAG 0. Among half-π angle triples, 176 of 512 give a different unitary.

is_clifford is True in both cases, so a user would reasonably hand c.stim_circuit to stim, and stim then samples the wrong distribution. tsim's own samplers parse the unexpanded circuit, so compile_sampler() is unaffected. Circuit.from_stim_program(c.stim_circuit) inherits the error.

Reproducer

"""Circuit.stim_circuit reorders merged Clifford-angle rotations."""
import numpy as np
import tsim

cases = {
    # Two R_PAULI lines; stim merges them into `SPP[R_PAULI(theta=1.0*pi)] X0 Z0`.
    "R_PAULI": "H 0\nR_PAULI(1.0) X0\nR_PAULI(1.0) Z0\nH 0\nM 0",
    # Two identical U3 lines; stim merges them into one instruction targeting `0 0`.
    "U3": "U3(-1.5, -1.5, -1.0) 0\nU3(-1.5, -1.5, -1.0) 0\nM 0",
}
for name, text in cases.items():
    c = tsim.Circuit(text)
    tsim_p1 = np.asarray(c.compile_sampler(seed=1).sample(20000)).mean()
    stim_p1 = c.stim_circuit.compile_sampler(seed=1).sample(20000).mean()
    print(f"{name}: is_clifford={c.is_clifford}")
    print(f"  internal:     {str(c._stim_circ)!r}")
    print(f"  stim_circuit: {str(c.stim_circuit)!r}")
    print(f"  P(1): tsim sampler {tsim_p1:.3f}, stim on c.stim_circuit {stim_p1:.3f}")

Output on main (be0e8af):

R_PAULI: is_clifford=True
  internal:     'H 0\nSPP[R_PAULI(theta=1.0*pi)] X0 Z0\nH 0\nM 0'
  stim_circuit: 'H 0\nSPP X0 Z0 X0 Z0\nH 0\nM 0'
  P(1): tsim sampler 1.000, stim on c.stim_circuit 0.499
U3: is_clifford=True
  internal:     'I[U3(theta=-1.5*pi, phi=-1.5*pi, lambda=-1.0*pi)] 0 0\nM 0'
  stim_circuit: 'S_DAG 0 0\nSQRT_X_DAG 0 0\nM 0'
  P(1): tsim sampler 0.506, stim on c.stim_circuit 1.000

The exact values are P(1) = 1 for the first circuit and 0.5 for the second, which matches tsim's sampler.

Cause

In src/tsim/utils/clifford.py:

  • _expand_clifford_spp runs for gate in repeats: out.append(gate, targets, []) with all of the instruction's products.
  • expand_clifford_rotations runs for gate in gates: out.append(gate, targets, []) with all of the I instruction's targets.

Emitting the sequence per product (split on combiners) and per target fixes both. Stim re-merges adjacent same-name instructions in time order, so the output stays compact. I have a patch with regression tests if that's useful.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions