Source code for uniqc.algorithmics.measurement.pauli_expectation

"""Pauli string expectation value measurement via basis rotation."""

__all__ = ["pauli_expectation"]

from typing import Optional

import numpy as np

from uniqc.circuit_builder import Circuit
from uniqc.simulator.qasm_simulator import QASM_Simulator


def _parity(bitstring: str, pauli_string: str) -> int:
    """Compute parity contribution of a measurement outcome for a Pauli string.

    Note: bitstring uses big-endian convention (MSB first) matching the
    measurement output format where q[0] corresponds to the rightmost bit.
    Pauli string is reversed to align with this convention.

    For each qubit i:
      Z → contributes 1 if bit[i] == '1'
      X → contributes 1 if bit[i] == '1'
      Y → contributes 1 if bit[i] == '1'
    The total parity is the XOR (sum mod 2) of all contributions.
    Returns 0 for even parity (+1 eigenvalue) or 1 for odd parity (-1 eigenvalue).
    """
    parity = 0
    # Reverse pauli_string to match big-endian bitstring convention
    # where pauli_string[0] corresponds to qubit 0 (rightmost bit)
    for pauli, bit in zip(reversed(pauli_string), bitstring):
        if bit == '1' and pauli.upper() in ('Z', 'X', 'Y'):
            parity ^= 1
    return parity


def _apply_basis_rotation(circuit: Circuit, pauli_string: str) -> Circuit:
    """Add basis-rotation gates to a circuit copy for measuring a Pauli string.

    For each qubit i with pauli_string[i]:
      Z → no rotation
      X → H gate
      Y → Sdag then H (equivalently, Sdg-H sequence)
      I → no rotation
    """
    rot_circuit = circuit.copy()
    for i, pauli in enumerate(pauli_string):
        p = pauli.upper()
        if p == 'X':
            rot_circuit.h(i)
        elif p == 'Y':
            rot_circuit.sdg(i)
            rot_circuit.h(i)
        # Z and I: no rotation needed
    return rot_circuit


def _statevector_expectation(circuit: Circuit, pauli_string: str) -> float:
    """Compute the exact ⟨pauli_string⟩ expectation from the statevector.

    Applies basis-rotation gates to rotate to the measurement basis, then
    computes the expectation analytically from the final statevector.
    """
    rot_circuit = _apply_basis_rotation(circuit, pauli_string)
    n = rot_circuit.max_qubit + 1

    # Use QASM simulator in statevector mode
    sim = QASM_Simulator(backend_type='statevector', n_qubits=n)
    qasm = rot_circuit.qasm
    result = sim.simulate_statevector(qasm)

    # result is a list of complex amplitudes, convert to probabilities
    probs = np.abs(result) ** 2
    exp_val = 0.0
    for idx, p in enumerate(probs):
        # Build bitstring for this index (big-endian: qubit 0 is MSB)
        bitstring = format(idx, f'0{n}b')
        parity = _parity(bitstring, pauli_string.upper())
        if parity == 0:
            exp_val += p
        else:
            exp_val -= p
    return float(exp_val)


def _shots_expectation(circuit: Circuit, pauli_string: str, shots: int) -> float:
    """Estimate ⟨pauli_string⟩ via basis rotation + shots on QASM simulator."""
    rot_circuit = _apply_basis_rotation(circuit, pauli_string)
    n = rot_circuit.max_qubit + 1

    sim = QASM_Simulator(backend_type='statevector', n_qubits=n)
    qasm = rot_circuit.qasm
    counts = sim.simulate_shots(qasm, shots=shots)
    total = sum(counts.values())

    exp_val = 0.0
    for bitstring_int, count in counts.items():
        # Convert int to bitstring and pad to n qubits
        bitstring = format(bitstring_int, f'0{n}b')
        parity = _parity(bitstring, pauli_string.upper())
        p = count / total
        if parity == 0:
            exp_val += p
        else:
            exp_val -= p
    return float(exp_val)


[docs] def pauli_expectation( circuit: Circuit, pauli_string: str, shots: Optional[int] = None, ) -> float: """Measure the expectation value of a Pauli string on a circuit. For each qubit i, the measurement basis is determined by ``pauli_string[i]``: - ``'I'``: trace out (identity, contributes trivially) - ``'Z'``: measure in the computational (Z) basis — no rotation needed - ``'X'``: apply Hadamard before Z measurement - ``'Y'``: apply Sdag then Hadamard before Z measurement When ``shots`` is ``None``, the statevector simulator is used to compute the exact expectation analytically. When ``shots`` is given, the circuit is simulated ``shots`` times and the empirical frequency is used. Args: circuit: Quantum circuit. Must contain only gates supported by ``QASM_Simulator`` and end with measurement instructions. pauli_string: Case-insensitive Pauli string (e.g. ``"XYZ"``, ``"IZI"``). Characters must be ``I``, ``X``, ``Y``, or ``Z``. shots: Number of measurement shots. ``None`` uses statevector mode for the exact analytical value. Returns: Expectation value ⟨psi|P|psi⟩ as a float in the interval ``[-1, 1]``. Raises: ValueError: ``pauli_string`` contains invalid characters or its length does not match the number of qubits in ``circuit``. ValueError: ``shots`` is not a positive integer. Example: >>> from uniqc.circuit_builder import Circuit >>> from uniqc.algorithmics.measurement import pauli_expectation >>> c = Circuit() >>> c.h(0) >>> c.cx(0, 1) # Bell state (|00⟩+|11⟩)/√2 >>> c.measure(0, 1) >>> pauli_expectation(c, "ZZ", shots=None) # exact: 1.0 1.0 >>> abs(pauli_expectation(c, "ZZ", shots=10000) - 1.0) < 0.1 True """ # Validate pauli_string pauli_upper = pauli_string.upper() n_qubits = circuit.max_qubit + 1 if len(pauli_upper) != n_qubits: raise ValueError( f"pauli_string length ({len(pauli_string)}) must match " f"circuit n_qubits ({n_qubits})" ) for ch in pauli_upper: if ch not in ('I', 'X', 'Y', 'Z'): raise ValueError( f"pauli_string must contain only I/X/Y/Z, got: {pauli_string!r}" ) if shots is not None: if not isinstance(shots, int) or shots <= 0: raise ValueError(f"shots must be a positive integer, got: {shots}") return _shots_expectation(circuit, pauli_string, shots) return _statevector_expectation(circuit, pauli_string)