Repository navigation
Circuit.stim_circuit reorders merged Clifford-angle rotations (wrong circuit, is_clifford=True) #225
Description
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) X0followed byR_PAULI(1.0) Z0becomesSPP[R_PAULI(theta=1.0*pi)] X0 Z0, which expands toSPP X0 Z0 X0 Z0instead ofSPP 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) 0lines becomeI[U3(...)] 0 0, which expands toS_DAG 0 0; SQRT_X_DAG 0 0instead ofS_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_spprunsfor gate in repeats: out.append(gate, targets, [])with all of the instruction's products.expand_clifford_rotationsrunsfor gate in gates: out.append(gate, targets, [])with all of theIinstruction'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.
Activity