Source code for mimiqcircuits.operations.measurepauli

#
# 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.
#
"""Pauli product measurement (MeasurePauli)."""

from typing import Union

from mimiqcircuits.operations.operation import Operation
from mimiqcircuits.operations.measure import AbstractMeasurement, Measure
from mimiqcircuits.operations.gates.generalized.paulistring import PauliString
import mimiqcircuits as mc


[docs] class MeasurePauli(AbstractMeasurement): r"""Pauli product measurement. Projective measurement of the :math:`N`-qubit observable :math:`P = P_1 \otimes P_2 \otimes \dots \otimes P_N`, with each :math:`P_i \in \{I, X, Y, Z\}`. The instruction targets :math:`N` qubits and one classical bit. The state is projected onto the :math:`+1` or :math:`-1` eigenspace of :math:`P` and the bit records the outcome: ``0`` for :math:`+1`, ``1`` for :math:`-1`. Qubits marked ``I`` are targets of the instruction but are not measured. To measure :math:`-P`, measure :math:`P` and flip the bit with :class:`Not`. Args: pauli (PauliString or str): the observable, as a :class:`PauliString` or a string of ``I``, ``X``, ``Y``, ``Z``. Raises: TypeError: if ``pauli`` is neither a :class:`PauliString` nor a ``str``. ValueError: if the string is empty, contains a character other than ``I``, ``X``, ``Y``, ``Z``, or is the identity. See Also: :class:`PauliString`, :class:`RPauli`, :class:`MeasureZZ`, :class:`MeasureXX`, :class:`MeasureYY`, :func:`pauli_based_computation` Examples: Measure :math:`X_0 Z_2` on qubits 0 to 2 and store the outcome in bit 0. Qubit 1 is a target, but is left untouched: >>> from mimiqcircuits import * >>> MeasurePauli("XIZ") MP(XIZ) >>> c = Circuit() >>> c.push(MeasurePauli("XIZ"), 0, 1, 2, 0) 3-qubit, 1-bit circuit with 1 instruction: └── MP(XIZ) @ q[0:2], c[0] <BLANKLINE> Decomposition: Each measured qubit is rotated to the Z basis (``H`` for ``X``, ``SDG`` then ``H`` for ``Y``), CX gates collect the parity of the measured qubits on the last one, that qubit is measured, and the CX gates and rotations are undone: >>> decompose_step(MeasurePauli("XZ")) 2-qubit, 1-bit circuit with 5 instructions: ├── H @ q[0] ├── CX @ q[0], q[1] ├── M @ q[1], c[0] ├── CX @ q[0], q[1] └── H @ q[0] <BLANKLINE> Only ``H``, ``S``, ``SDG``, ``CX`` and ``Measure`` appear, so :class:`QASMBasis` decomposes it. :class:`StimBasis` keeps it, as Stim measures Pauli products natively (``MPP``), and so does :class:`CliffordTBasis`. """ _name = "MeasurePauli" _num_bits = 1 _num_cregs = 1 _cregsizes = [1] _num_zvars = 0
[docs] def __init__(self, pauli: Union[PauliString, str]): if isinstance(pauli, str): pauli = PauliString(pauli) elif not isinstance(pauli, PauliString): raise TypeError( f"MeasurePauli takes a PauliString or a str, got {type(pauli).__name__}." ) if pauli.is_identity(): raise ValueError("Cannot measure the identity Pauli string.") self.pauli = pauli self._num_qubits = pauli.num_qubits self._qregsizes = [pauli.num_qubits]
[docs] def pstring(self): """Return the Pauli string as a plain ``str``.""" return self.pauli.pauli
[docs] def inverse(self): raise TypeError("MeasurePauli is not inversible")
[docs] def power(self, p): raise TypeError("MeasurePauli^p is not defined.")
[docs] def control(self, num_qubits): raise TypeError("Controlled MeasurePauli is not defined.")
[docs] def iswrapper(self): return False
[docs] def get_operation(self): return self
def __str__(self): return f"MP({self.pstring()})"
[docs] def asciiwidth(self, qubits, bits, zvars): # Leave room for the per-qubit index labels drawn inside the box. return Operation.asciiwidth(self, qubits, bits, zvars)
def _decompose(self, circ, qubits, bits, zvars): active = [(p, q) for p, q in zip(self.pstring(), qubits) if p != "I"] target = active[-1][1] # Keep the decomposed circuit as wide as the instruction. max_targ = max(qubits) if all(q != max_targ for _, q in active): circ.push(mc.GateID(), max_targ) # H·S† maps Y to Z using only gates that the Stim and QASM bases keep. for p, q in active: if p == "Y": circ.push(mc.GateSDG(), q) if p != "Z": circ.push(mc.GateH(), q) for _, q in active: if q != target: circ.push(mc.GateCX(), q, target) circ.push(Measure(), target, bits[0]) for _, q in reversed(active): if q != target: circ.push(mc.GateCX(), q, target) for p, q in reversed(active): if p != "Z": circ.push(mc.GateH(), q) if p == "Y": circ.push(mc.GateS(), q) return circ
__all__ = ["MeasurePauli"]