"""Pauli string expectation value measurement via basis rotation."""
__all__ = ["pauli_expectation", "PauliExpectation", "pauli_expectation_example"]
from typing import Union
import numpy as np
from uniqc._error_hints import format_enriched_message
from uniqc.circuit_builder import Circuit
from uniqc.simulator import 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 = Simulator(backend_type="statevector")
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; the leftmost char corresponds to qubit n-1 (MSB),
# so bitstring[n-1-q] is the measurement bit of qubit q (qubit 0 = LSB).
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 = Simulator(backend_type="statevector")
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)
def _normalize_pauli_string(
pauli_spec: str | list[tuple],
n_qubits: int,
) -> str:
"""Normalize the various supported Pauli-string formats to compact form.
Accepted inputs:
- **Compact string** (``len == n_qubits``): ``"ZIZ"``, ``"XYZ"``, ``"IZI"`` —
``pauli_spec[i]`` is the operator on qubit ``i`` (left-to-right).
- **Indexed string**: ``"Z0Z1"``, ``"X0Y2"`` — operator-index pairs; any
qubit not listed is treated as identity. Indices may appear in any
order but must be in ``[0, n_qubits)``.
- **List of tuples**: ``[("Z", 0), ("Z", 1)]`` — same semantics as the
indexed-string form.
Returns the compact-string representation (length exactly ``n_qubits``,
upper-cased, characters in ``"IXYZ"``).
Raises:
ValueError: If ``pauli_spec`` is malformed, contains invalid Pauli
characters, or addresses qubit indices outside ``[0, n_qubits)``.
"""
import re as _re
# Form 3: list[tuple[str, int]]
if isinstance(pauli_spec, list):
out = ["I"] * n_qubits
for item in pauli_spec:
if not (isinstance(item, tuple) and len(item) == 2):
raise ValueError(
format_enriched_message(
f"pauli_string list entries must be (Pauli, qubit) tuples, got: {item!r}", "measurement"
)
)
op, idx = item
op = str(op).upper()
if op not in ("I", "X", "Y", "Z"):
raise ValueError(
format_enriched_message(f"pauli_string contains invalid operator: {op!r}", "measurement")
)
if not isinstance(idx, int) or idx < 0 or idx >= n_qubits:
raise ValueError(
format_enriched_message(
f"pauli_string qubit index out of range [0, {n_qubits}): {idx!r}", "measurement"
)
)
out[idx] = op
return "".join(out)
if not isinstance(pauli_spec, str):
raise ValueError(
format_enriched_message(
f"pauli_string must be a str or list[(op, qubit)], got: {type(pauli_spec).__name__}", "measurement"
)
)
upper = pauli_spec.upper().replace(" ", "")
# Form 2: indexed string like "Z0Z1" / "X0Y2"
if _re.fullmatch(r"([IXYZ]\d+)+", upper):
out = ["I"] * n_qubits
for op, idx_str in _re.findall(r"([IXYZ])(\d+)", upper):
idx = int(idx_str)
if idx < 0 or idx >= n_qubits:
raise ValueError(
format_enriched_message(
f"pauli_string qubit index out of range [0, {n_qubits}): {idx}", "measurement"
)
)
out[idx] = op
return "".join(out)
# Form 1: compact string
if len(upper) != n_qubits:
raise ValueError(
format_enriched_message(
f"pauli_string length ({len(upper)}) must match circuit n_qubits "
f"({n_qubits}), or use indexed form like 'Z0Z1' / [('Z', 0), ('Z', 1)]",
"measurement",
)
)
for ch in upper:
if ch not in ("I", "X", "Y", "Z"):
raise ValueError(
format_enriched_message(f"pauli_string must contain only I/X/Y/Z, got: {pauli_spec!r}", "measurement")
)
return upper
[docs]
def pauli_expectation(
circuit: Circuit,
pauli_string: str | list[tuple],
shots: int | None = None,
) -> float:
"""Measure the expectation value of a Pauli string on a circuit.
For each qubit i, the measurement basis is determined by the operator
assigned to that qubit:
- ``'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
``Simulator`` and end with measurement instructions.
pauli_string: Pauli string in any of three accepted forms (case-
insensitive):
- **Compact** (length == n_qubits): ``"XYZ"``, ``"IZI"``.
- **Indexed** (``"Z0Z1"``): operator-index pairs; any qubit not
listed is identity. Matches the convention of
:func:`uniqc.algorithms.core.ansatz.qaoa_ansatz.cost_hamiltonian`.
- **Tuple list**: ``[("Z", 0), ("Z", 1)]`` — same semantics as
the indexed form.
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`` is malformed, contains invalid characters,
or its length / qubit indices do not fit the circuit.
ValueError: ``shots`` is not a positive integer.
Example:
>>> from uniqc.circuit_builder import Circuit
>>> from uniqc.algorithms.core.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") # compact form
1.0
>>> pauli_expectation(c, "Z0Z1") # indexed form
1.0
>>> pauli_expectation(c, [("Z", 0), ("Z", 1)]) # tuple-list form
1.0
"""
n_qubits = circuit.max_qubit + 1
pauli_upper = _normalize_pauli_string(pauli_string, n_qubits)
if shots is not None:
if not isinstance(shots, int) or shots <= 0:
raise ValueError(format_enriched_message(f"shots must be a positive integer, got: {shots}", "measurement"))
return _shots_expectation(circuit, pauli_upper, shots)
return _statevector_expectation(circuit, pauli_upper)
[docs]
class PauliExpectation:
"""Class-based interface for Pauli-string expectation measurement.
Convention: the constructor accepts a *clean* state-preparation
:class:`Circuit` (no measurement instructions). The class adds basis
rotations and measurements internally; the input circuit is **not**
mutated.
Example::
from uniqc.circuit_builder import Circuit
from uniqc.algorithms.core.measurement import PauliExpectation
c = Circuit()
c.h(0); c.cx(0, 1)
meas = PauliExpectation(c, "ZZ", shots=10000)
readouts = meas.get_readout_circuits() # list[Circuit]
value = meas.execute("statevector") # float in [-1, 1]
"""
def __init__(
self,
circuit: Circuit,
pauli_string: str | list[tuple],
shots: int | None = None,
) -> None:
n_qubits = circuit.max_qubit + 1
pauli_upper = _normalize_pauli_string(pauli_string, n_qubits)
if shots is not None and (not isinstance(shots, int) or shots <= 0):
raise ValueError(format_enriched_message(f"shots must be a positive integer, got: {shots}", "measurement"))
self.circuit = circuit.copy()
self.pauli_string = pauli_upper
self.shots = shots
[docs]
def get_readout_circuits(self) -> list[Circuit]:
"""Return the list of readout circuits (one per measurement basis).
For a single Pauli string this returns a one-element list containing
the basis-rotated, measured circuit.
"""
rot = _apply_basis_rotation(self.circuit, self.pauli_string)
n = rot.max_qubit + 1
for q in range(n):
rot.measure(q)
return [rot]
[docs]
def execute(
self,
backend: Union[str, "object"] = "statevector",
*,
program_type: str = "qasm",
**kwargs,
) -> float:
"""Execute the readout circuits and return the expectation value.
Args:
backend: Either a backend-type name (string, e.g. ``"statevector"``)
or a pre-built simulator object exposing ``simulate_statevector``
/ ``simulate_shots``.
program_type: Currently only ``"qasm"`` is supported.
**kwargs: Forwarded to simulator construction when *backend* is a
string.
"""
if program_type != "qasm":
raise ValueError(
format_enriched_message(
f"PauliExpectation currently supports program_type='qasm' only, got {program_type!r}", "measurement"
)
)
if self.shots is None:
return _statevector_expectation(self.circuit, self.pauli_string)
return _shots_expectation(self.circuit, self.pauli_string, self.shots)
[docs]
def pauli_expectation_example() -> float:
"""Tiny PauliExpectation demo that returns ⟨ZZ⟩ on a Bell state (≈ 1.0)."""
c = Circuit()
c.h(0)
c.cx(0, 1)
return PauliExpectation(c, "ZZ").execute("statevector")