Source code for mimiqcircuits.decomposition.basis.stim

#
# Copyright © 2022-2024 University of Strasbourg. All Rights Reserved.
# Copyright © 2023-2025 QPerfect. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Stim stabilizer simulator decomposition basis."""

from __future__ import annotations

from typing import TYPE_CHECKING, Sequence

from mimiqcircuits.decomposition.abstract import DecompositionBasis, DecompositionError

if TYPE_CHECKING:
    from mimiqcircuits import Circuit, Operation


# Terminality per operation type, for the types where the type alone decides
# it. Filled on first use and shared by all instances.
_TERMINAL_BY_TYPE: dict[type, bool] = {}


[docs] class StimBasis(DecompositionBasis): """Decomposition basis targeting the Stim stabilizer simulator gate set. `Stim <https://github.com/quantumlib/Stim>`_ is a high-performance stabilizer circuit simulator by Craig Gidney. It supports only **Clifford gates**, stabilizer operations, noise channels, and quantum error correction annotations. Decomposition Pipeline ---------------------- The decomposition follows this priority order: 1. A ``GateU`` whose angles are all zero (a pure global phase) is dropped. 2. **ZYZRewrite**: ``GateU`` -> ``RZ * RY * RZ`` 3. **SpecialAngleRewrite** (Clifford-only): Rotations at ``k*pi/2`` -> Clifford gates 4. **CanonicalRewrite**: Fallback for other gates Non-Clifford rotations (angles not multiples of pi/2) will raise a :class:`DecompositionError`. Terminal Operations ------------------- **Single-qubit Clifford gates:** - ``GateID`` - Pauli: ``GateX``, ``GateY``, ``GateZ`` - Hadamard: ``GateH``, ``GateHXY``, ``GateHYZ`` - Phase gates: ``GateS``, ``GateSDG`` - Square root gates: ``GateSX``, ``GateSXDG``, ``GateSY``, ``GateSYDG`` **Two-qubit Clifford gates:** - Controlled Pauli: ``GateCX``, ``GateCY``, ``GateCZ`` - Swap family: ``GateSWAP``, ``GateISWAP``, ``ISWAP†`` - ``GateDCX``, ``GateECR`` **Pauli rotations by +-pi/2:** - ``GateRXX``, ``GateRYY``, ``GateRZZ`` with angle +-pi/2 - ``RPauli`` with angle +-pi/2 and a non-identity Pauli string Angles are compared modulo 2*pi, since a rotation by 3*pi/2 equals a rotation by -pi/2 up to a global phase. Other angles are decomposed. **Feedback:** - ``IfStatement`` of ``GateX``, ``GateY`` or ``GateZ`` conditioned on a single bit being ``1``. The same gate conditioned on the bit being ``0`` is decomposed into the gate followed by the gate conditioned on ``1``. Any other conditional operation has its body decomposed while staying conditional, and raises a :class:`DecompositionError` if that does not lead to a conditional Pauli. **Measurements (all Pauli bases):** - ``Measure``, ``MeasureX``, ``MeasureY`` - Two-qubit parities: ``MeasureXX``, ``MeasureYY``, ``MeasureZZ`` - Pauli products: ``MeasurePauli`` **Resets (all Pauli bases):** - ``Reset``, ``ResetX``, ``ResetY`` **Measure-and-reset (all Pauli bases):** - ``MeasureReset``, ``MeasureResetX``, ``MeasureResetY`` **Noise channels:** - ``PauliX``, ``PauliY``, ``PauliZ`` - ``Depolarizing1``, ``Depolarizing2`` - ``PauliNoise`` **QEC annotations:** - ``Detector``, ``ObservableInclude``, ``QubitCoordinates`` - ``Barrier`` Stim Gate Mapping ----------------- =========================== ============================================================================= MIMIQ Operation Stim Operation =========================== ============================================================================= ``GateID`` ``I`` ``GateX`` ``X`` ``GateY`` ``Y`` ``GateZ`` ``Z`` ``GateH`` ``H`` ``GateHXY`` ``H_XY`` ``GateHYZ`` ``H_YZ`` ``GateS`` ``S`` ``GateSDG`` ``S_DAG`` ``GateSX`` ``SQRT_X`` ``GateSXDG`` ``SQRT_X_DAG`` ``GateSY`` ``SQRT_Y`` ``GateSYDG`` ``SQRT_Y_DAG`` ``GateCX`` ``CNOT`` / ``CX`` ``GateCY`` ``CY`` ``GateCZ`` ``CZ`` ``GateSWAP`` ``SWAP`` ``GateISWAP`` ``ISWAP`` ``ISWAP†`` ``ISWAP_DAG`` ``GateDCX`` ``DCX`` (custom) ``GateECR`` ``ECR`` (custom) ``GateRXX(+-pi/2)`` ``SQRT_XX`` / ``SQRT_XX_DAG`` ``GateRYY(+-pi/2)`` ``SQRT_YY`` / ``SQRT_YY_DAG`` ``GateRZZ(+-pi/2)`` ``SQRT_ZZ`` / ``SQRT_ZZ_DAG`` ``RPauli(P, +-pi/2)`` ``SPP`` / ``SPP_DAG`` ``IfStatement(GateX(), 1)`` ``CX rec[-k] q`` ``IfStatement(GateY(), 1)`` ``CY rec[-k] q`` ``IfStatement(GateZ(), 1)`` ``CZ rec[-k] q`` ``Measure`` ``M`` ``MeasureX`` ``MX`` ``MeasureY`` ``MY`` ``MeasureXX`` ``MXX`` ``MeasureYY`` ``MYY`` ``MeasureZZ`` ``MZZ`` ``MeasurePauli`` ``MPP`` ``Reset`` ``R`` ``ResetX`` ``RX`` ``ResetY`` ``RY`` ``MeasureReset`` ``MR`` ``MeasureResetX`` ``MRX`` ``MeasureResetY`` ``MRY`` ``PauliX`` ``X_ERROR`` ``PauliY`` ``Y_ERROR`` ``PauliZ`` ``Z_ERROR`` ``Depolarizing1`` ``DEPOLARIZE1`` ``Depolarizing2`` ``DEPOLARIZE2`` ``PauliNoise`` ``PAULI_CHANNEL_1``, ``PAULI_CHANNEL_2`` or ``E`` / ``ELSE_CORRELATED_ERROR`` ``Detector`` ``DETECTOR`` ``ObservableInclude`` ``OBSERVABLE_INCLUDE`` ``QubitCoordinates`` ``QUBIT_COORDS`` =========================== ============================================================================= Example: >>> from mimiqcircuits import Circuit, GateH, GateS, GateCX >>> from mimiqcircuits.decomposition import decompose, StimBasis >>> from symengine import pi >>> # Decompose Clifford circuit to Stim-compatible gates >>> c = Circuit() >>> c.push(GateH(), 0) 1-qubit circuit with 1 instruction: └── H @ q[0] <BLANKLINE> >>> c.push(GateS(), 0) 1-qubit circuit with 2 instructions: ├── H @ q[0] └── S @ q[0] <BLANKLINE> >>> c.push(GateCX(), 0, 1) 2-qubit circuit with 3 instructions: ├── H @ q[0] ├── S @ q[0] └── CX @ q[0], q[1] <BLANKLINE> >>> decomposed = decompose(c, StimBasis()) >>> # RZ(pi/2) = S is Clifford, so this works >>> from mimiqcircuits import GateRZ >>> c2 = Circuit() >>> c2.push(GateRZ(pi/2), 0) 1-qubit circuit with 1 instruction: └── RZ((1/2)*pi) @ q[0] <BLANKLINE> >>> decomposed2 = decompose(c2, StimBasis()) Note: Non-Clifford rotations will raise a DecompositionError:: >>> from mimiqcircuits import * >>> c3 = Circuit() >>> c3.push(GateRZ(pi/4), 0) # T gate is non-Clifford 1-qubit circuit with 1 instruction: └── RZ((1/4)*pi) @ q[0] <BLANKLINE> >>> decompose(c3, StimBasis()) # raises DecompositionError doctest: +ELLIPSIS Traceback (most recent call last): ... mimiqcircuits.decomposition.abstract.DecompositionError: Rotation RZ cannot be decomposed into the Stim basis. Only rotations with angles that are multiples of pi/2 are Clifford gates. See Also: :class:`CliffordTBasis`, :class:`QASMBasis`, :class:`CanonicalBasis` """
[docs] def isterminal(self, op: Operation) -> bool: """Check if an operation is terminal in the Stim basis. Args: op: The operation to check. Returns: True if the operation is in the Stim Clifford gate set. """ t = type(op) known = _TERMINAL_BY_TYPE.get(t) if known is not None: return known import mimiqcircuits as mc # These are decided by their values, so they never reach the cache. # Pauli rotations by +-pi/2 are Stim's SQRT_XX, SQRT_YY, SQRT_ZZ, SPP. if isinstance(op, (mc.GateRXX, mc.GateRYY, mc.GateRZZ)): return _is_half_pi(op.theta) if isinstance(op, mc.RPauli): return any(p != "I" for p in str(op.pauli)) and _is_half_pi(op.theta) # Depolarizing1 and Depolarizing2 build a Depolarizing. if isinstance(op, mc.Depolarizing): return op.num_qubits in (1, 2) # Feedback: Stim's CX/CY/CZ with a measurement record as control. if isinstance(op, mc.IfStatement): return isinstance(op.op, (mc.GateX, mc.GateY, mc.GateZ)) and _is_one( op.bitstring ) known = isinstance( op, ( # Single-qubit Cliffords mc.GateID, mc.GateX, mc.GateY, mc.GateZ, mc.GateH, mc.GateHXY, mc.GateHYZ, mc.GateS, mc.GateSDG, mc.GateSX, mc.GateSXDG, mc.GateSY, mc.GateSYDG, # Two-qubit Cliffords mc.GateCX, mc.GateCY, mc.GateCZ, mc.GateSWAP, mc.GateISWAP, mc.GateISWAPDG, mc.GateDCX, mc.GateECR, # Measurements, resets and measure-resets in all Pauli bases mc.Measure, mc.MeasureX, mc.MeasureY, mc.MeasureXX, mc.MeasureYY, mc.MeasureZZ, mc.MeasurePauli, mc.Reset, mc.ResetX, mc.ResetY, mc.MeasureReset, mc.MeasureResetX, mc.MeasureResetY, mc.Barrier, # Pauli noise mc.PauliX, mc.PauliY, mc.PauliZ, mc.PauliNoise, # QEC and generic annotations mc.Detector, mc.ObservableInclude, mc.QubitCoordinates, mc.AbstractAnnotation, ), ) _TERMINAL_BY_TYPE[t] = known return known
[docs] def decompose( self, op: Operation, qubits: Sequence[int], bits: Sequence[int], zvars: Sequence[int], ) -> Circuit: """Decompose a non-terminal operation toward the Stim Clifford gate set. Applies rules in priority order: 1. Drops a GateU whose angles are all zero (a global phase) 2. ZYZRewrite for GateU 3. SpecialAngleRewrite (Clifford-only) for rotations at k*pi/2 4. CanonicalRewrite as fallback Non-Clifford rotations (angles not multiples of pi/2) will raise an error. Args: op: The operation to decompose. qubits: Qubit indices for the operation. bits: Classical bit indices for the operation. zvars: Z-variable indices for the operation. Returns: A Circuit containing the decomposed instructions. Raises: DecompositionError: If the operation cannot be decomposed to Clifford gates. """ import mimiqcircuits as mc from mimiqcircuits.decomposition.rules import ( CanonicalRewrite, SpecialAngleRewrite, ZYZRewrite, ) from mimiqcircuits.decomposition.rules.zyz import _iszero if isinstance(op, mc.IfStatement): return self._decompose_ifstatement(op, qubits, bits, zvars) # A bare GateU with zero angles is only a global phase, so it expands # to nothing; ZYZRewrite refuses it. Under a Control the phase is a # relative phase; that case is a Control, not a GateU, and goes to # CanonicalRewrite. if isinstance(op, mc.GateU) and all( _iszero(p) for p in (op.theta, op.phi, op.lmbda) ): return mc.Circuit() # Priority 1: GateU -> ZYZ decomposition (RZ * RY * RZ) zyz = ZYZRewrite() if zyz.matches(op): return zyz.decompose_step(op, qubits, bits, zvars) # Priority 2: Special angles (Clifford-only) - handles rotations at k*pi/2 special_angle_clifford = SpecialAngleRewrite(only_cliffords=True) if special_angle_clifford.matches(op): return special_angle_clifford.decompose_step(op, qubits, bits, zvars) # Explicitly reject non-Clifford rotations # This prevents infinite loops (RZ -> U -> ZYZ -> RZ) and gives clear errors if isinstance(op, (mc.GateRX, mc.GateRY, mc.GateRZ)): raise DecompositionError( f"Rotation {op.name} cannot be decomposed into the Stim basis. " "Only rotations with angles that are multiples of pi/2 are Clifford gates." ) # Priority 3: Fallback to canonical decomposition canonical = CanonicalRewrite() if canonical.matches(op): return canonical.decompose_step(op, qubits, bits, zvars) raise DecompositionError( f"Operation {op.name} cannot be decomposed into the Stim basis." )
def _decompose_ifstatement(self, op, qubits, bits, zvars): import mimiqcircuits as mc from mimiqcircuits.decomposition.rules import SpecialAngleRewrite from mimiqcircuits.operations.ifstatement import _decompose_ifstatement inner = op.op condition = op.bitstring if len(condition) != 1: raise DecompositionError( f"IfStatement with a {len(condition)}-bit condition cannot be decomposed " "into the Stim basis. Stim only conditions X, Y and Z gates on a single " "measurement record." ) # P if c == 0 equals P, then P if c == 1, since P^2 = I. if isinstance(inner, (mc.GateX, mc.GateY, mc.GateZ)): circ = mc.Circuit() circ.push(inner, *qubits) circ.push(mc.IfStatement(inner, mc.BitString("1")), *qubits, *bits) return circ if self.isterminal(inner): raise DecompositionError( f"IfStatement of {inner} cannot be decomposed into the Stim basis. " "Stim only conditions X, Y and Z gates on a single measurement record." ) # Rotations by multiples of pi go straight to the Pauli they equal; the # general pipeline would pass through gates that cannot be conditioned. special_angle_clifford = SpecialAngleRewrite(only_cliffords=True) if special_angle_clifford.matches(inner): return _decompose_ifstatement( special_angle_clifford, op, qubits, bits, zvars ) return _decompose_ifstatement(self, op, qubits, bits, zvars)
def _is_half_pi(theta) -> bool: from mimiqcircuits.decomposition.rules.special_angle import _get_pi_factor return _get_pi_factor(theta) in (2, 6) def _is_one(bitstring) -> bool: return len(bitstring) == 1 and bitstring[0] __all__ = ["StimBasis"]