Add Circuit.is_clifford property and replace U3 gates with Clifford equivalents for pi/2 rotations#69
Add Circuit.is_clifford property and replace U3 gates with Clifford equivalents for pi/2 rotations#69
Conversation
☂️ Python Coverage
Overall Coverage
New Files
Modified Files
|
There was a problem hiding this comment.
Pull request overview
Adds Clifford-detection and Clifford-expansion support to tsim.Circuit to enable fast stabilizer-style workflows when circuits are Clifford-only (or have parametric gates reducible to Clifford gates).
Changes:
- Introduces
Circuit.is_cliffordto classify circuits containing only Clifford operations (including half-π parametric rotations). - Updates
Circuit.stim_circuitto expand half-πR_*/U3parametric tags into equivalent stim Clifford gate sequences. - Adds unit tests validating conversion tables, unitary equivalence, and the new
stim_circuit/is_cliffordbehavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/tsim/circuit.py |
Implements stim_circuit expansion pass and the new is_clifford property. |
src/tsim/utils/clifford.py |
Adds mapping tables and conversion helper for half-π parametric gates → stim Clifford gates. |
test/unit/test_circuit.py |
Adds coverage for Circuit.is_clifford across Clifford and non-Clifford examples. |
test/unit/utils/test_clifford.py |
Adds unitary-level tests validating parametric-to-Clifford conversions and stim_circuit expansion behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if not is_half_pi_multiple(params["theta"]): | ||
| return False | ||
| elif gate_name == "U3": | ||
| if not all( | ||
| is_half_pi_multiple(params[name]) | ||
| for name in ("theta", "phi", "lambda") |
There was a problem hiding this comment.
is_clifford can raise KeyError on malformed-but-parseable parametric tags (e.g. I[R_Z(phi=0.5*pi)]), because parse_parametric_tag doesn't validate parameter names but this code unconditionally indexes params["theta"]/params["phi"]/params["lambda"]. Since this is a boolean probe, it should return False instead of throwing; consider guarding missing keys (or reusing parametric_to_clifford_gates and treating failures as non-Clifford).
| if not is_half_pi_multiple(params["theta"]): | |
| return False | |
| elif gate_name == "U3": | |
| if not all( | |
| is_half_pi_multiple(params[name]) | |
| for name in ("theta", "phi", "lambda") | |
| theta = params.get("theta") | |
| if theta is None or not is_half_pi_multiple(theta): | |
| return False | |
| elif gate_name == "U3": | |
| required_params = ("theta", "phi", "lambda") | |
| if not all( | |
| (name in params) and is_half_pi_multiple(params[name]) | |
| for name in required_params |
| A circuit is a Clifford circuit if it only contains Clifford gates (i.e. half-pi | ||
| multiples of the rotation angles). | ||
|
|
||
| Returns: | ||
| True if the circuit is a Clifford circuit, otherwise False. | ||
|
|
||
| """ | ||
|
|
||
| def is_half_pi_multiple(phase: Fraction) -> bool: | ||
| return phase.denominator <= 2 | ||
|
|
||
| for instr in self._stim_circ: | ||
| assert not isinstance(instr, stim.CircuitRepeatBlock) | ||
|
|
||
| if instr.name in {"S", "S_DAG"} and instr.tag == "T": | ||
| return False | ||
|
|
||
| if instr.name == "I" and instr.tag: | ||
| result = parse_parametric_tag(instr.tag) | ||
| if result is None: | ||
| return False | ||
|
|
||
| gate_name, params = result | ||
| if gate_name in {"R_X", "R_Y", "R_Z"}: | ||
| if not is_half_pi_multiple(params["theta"]): | ||
| return False | ||
| elif gate_name == "U3": | ||
| if not all( | ||
| is_half_pi_multiple(params[name]) | ||
| for name in ("theta", "phi", "lambda") | ||
| ): | ||
| return False | ||
| else: |
There was a problem hiding this comment.
is_clifford currently only checks that U3/R_* parameters are half-π multiples (via denominator<=2). To keep this property consistent with stim_circuit’s actual expansion logic (and avoid reporting True while leaving a tagged I[...] unexpanded), consider determining Clifford-ness by calling parametric_to_clifford_gates(gate_name, params) and requiring a non-None result.
| A circuit is a Clifford circuit if it only contains Clifford gates (i.e. half-pi | |
| multiples of the rotation angles). | |
| Returns: | |
| True if the circuit is a Clifford circuit, otherwise False. | |
| """ | |
| def is_half_pi_multiple(phase: Fraction) -> bool: | |
| return phase.denominator <= 2 | |
| for instr in self._stim_circ: | |
| assert not isinstance(instr, stim.CircuitRepeatBlock) | |
| if instr.name in {"S", "S_DAG"} and instr.tag == "T": | |
| return False | |
| if instr.name == "I" and instr.tag: | |
| result = parse_parametric_tag(instr.tag) | |
| if result is None: | |
| return False | |
| gate_name, params = result | |
| if gate_name in {"R_X", "R_Y", "R_Z"}: | |
| if not is_half_pi_multiple(params["theta"]): | |
| return False | |
| elif gate_name == "U3": | |
| if not all( | |
| is_half_pi_multiple(params[name]) | |
| for name in ("theta", "phi", "lambda") | |
| ): | |
| return False | |
| else: | |
| A circuit is a Clifford circuit if it only contains Clifford gates. | |
| Returns: | |
| True if the circuit is a Clifford circuit, otherwise False. | |
| """ | |
| for instr in self._stim_circ: | |
| assert not isinstance(instr, stim.CircuitRepeatBlock) | |
| # S / S_DAG tagged as T are treated as non-Clifford. | |
| if instr.name in {"S", "S_DAG"} and instr.tag == "T": | |
| return False | |
| # Parametric single-qubit gates are encoded as tagged identity operations. | |
| if instr.name == "I" and instr.tag: | |
| result = parse_parametric_tag(instr.tag) | |
| if result is None: | |
| # Unrecognized parametric tag; conservatively treat as non-Clifford. | |
| return False | |
| gate_name, params = result | |
| # Delegate Clifford-ness to the same logic used for expansion. | |
| expanded = parametric_to_clifford_gates(gate_name, params) | |
| if expanded is None: |
| gates = U3_CLIFFORD.get(key) | ||
| if gates is None: | ||
| gates = U3_CLIFFORD.get(_equivalent_u3_key(*key)) | ||
| assert gates is not None |
There was a problem hiding this comment.
parametric_to_clifford_gates uses assert gates is not None for the U3 mapping. Asserts can be stripped with Python optimizations (-O), which would turn this into a less clear failure later (e.g., attempting list(None)). Prefer an explicit runtime check that returns None (or raises a descriptive exception) when a mapping is missing.
| assert gates is not None | |
| if gates is None: | |
| raise KeyError(f"No Clifford decomposition found for U3 key {key!r}.") |
|
Closes #68
Implements a new property
Circuit.is_clifford. Additionally,Circuit.stim_circuitnow returns a Stim circuit where rotation gates have been replaced with equivalent Clifford gates wherever possible.