"""Basis-rotation measurement for arbitrary single-qubit measurement bases."""
__all__ = ["basis_rotation_measurement"]
from uniqc._error_hints import format_enriched_message
from uniqc.circuit_builder import Circuit
from uniqc.simulator import Simulator
[docs]
def basis_rotation_measurement(
circuit: Circuit,
qubits: list[int] | None = None,
basis: str | list[str] | None = None,
shots: int | None = None,
) -> dict[str, float] | list[float]:
"""Measure a circuit by applying basis-rotation gates and then
measuring in the computational (Z) basis.
For each qubit, the rotation applied before measurement is determined
by the corresponding entry in ``basis``:
- ``"Z"`` — no rotation (Z basis, default)
- ``"X"`` — Hadamard gate (H), measures X basis
- ``"Y"`` — ``S^dagger H``, measures Y basis
- ``"I"`` — no rotation (Z basis, identity)
When ``shots`` is ``None``, the statevector simulator is used to return
the exact probability distribution. When ``shots`` is given, the
distribution is estimated from that many samples.
Args:
circuit: Quantum circuit (must contain MEASURE instructions).
qubits: Indices of qubits to include. ``None`` means all qubits.
basis: Per-qubit measurement basis. Accepts a single string such as
``"XYZ"`` (applied left-to-right to ``qubits``, with each
character one of ``"I"`` / ``"X"`` / ``"Y"`` / ``"Z"``), a list
of strings such as ``["X", "Y", "Z"]``, or ``None`` (default,
Z basis for all qubits).
shots: Number of measurement shots. ``None`` returns the exact
probability vector from the statevector simulator.
Returns:
When ``shots`` is ``None``, a ``dict`` mapping each computational-basis
outcome string (e.g. ``"01"``) to its probability. When ``shots`` is
given, a ``dict`` mapping outcome strings to integer counts
(frequency).
Raises:
ValueError: ``len(basis)`` does not match ``len(qubits)``.
ValueError: ``shots`` is not a positive integer.
ValueError: ``basis`` contains invalid characters.
Example:
>>> from uniqc.circuit_builder import Circuit
>>> from uniqc.algorithms.core.measurement import basis_rotation_measurement
>>> c = Circuit()
>>> c.h(0) # |0⟩ → (|0⟩+|1⟩)/√2
>>> c.cx(0, 1) # Bell state (|00⟩+|11⟩)/√2
>>> c.measure(0, 1)
>>> # Measure qubit 0 in X basis, qubit 1 in Z basis
>>> probs = basis_rotation_measurement(c, basis="XZ")
>>> abs(probs["00"] - 0.5) < 1e-6 # P(0) in X basis for |+⟩ is 0.5
True
"""
n_qubits = circuit.max_qubit + 1
# Guard: this implementation injects basis-rotation gates immediately
# before existing MEASURE instructions in the circuit's QASM. If the
# caller has not already added MEASURE instructions, no rotations are
# applied and the result will silently fall back to the Z-basis
# distribution — which is **wrong** for X/Y measurements. Catch that
# here so users get an actionable error instead of bad numbers.
if not getattr(circuit, "measure_list", None):
raise ValueError(
format_enriched_message(
"basis_rotation_measurement requires the circuit to already contain "
"MEASURE instructions (e.g. `circuit.measure(*qubits)`); "
"without them, basis rotations cannot be injected and the "
"returned distribution would silently be wrong for X/Y bases.",
"measurement",
)
)
if qubits is None:
qubits = list(range(n_qubits))
else:
qubits = list(qubits)
n = len(qubits)
# Parse basis argument
if basis is None:
basis_strs: list[str] = ["Z"] * n
elif isinstance(basis, str):
basis_strs = list(basis.upper())
if len(basis_strs) != n:
raise ValueError(
format_enriched_message(
f"basis string length ({len(basis_strs)}) must match len(qubits) ({n})", "measurement"
)
)
elif isinstance(basis, list):
if len(basis) != n:
raise ValueError(
format_enriched_message(f"len(basis) ({len(basis)}) must match len(qubits) ({n})", "measurement")
)
basis_strs = [b.upper() for b in basis]
else:
raise TypeError(
format_enriched_message(f"basis must be str, list, or None, got {type(basis).__name__}", "measurement")
)
for b in basis_strs:
if b not in ("I", "X", "Y", "Z"):
raise ValueError(format_enriched_message(f"basis must only contain I/X/Y/Z, got: {b!r}", "measurement"))
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"))
# Build rotation gate injection map per qubit index
rot_gates: dict[int, list[str]] = {i: [] for i in range(n)}
for i, b in enumerate(basis_strs):
if b == "X":
rot_gates[i].append(f"h q[{i}];")
elif b == "Y":
# Sdg then H maps Y eigenstates → Z eigenstates
rot_gates[i].append(f"sdg q[{i}];")
rot_gates[i].append(f"h q[{i}];")
# Inject rotations before each MEASURE line
lines = circuit.qasm.splitlines()
new_lines: list[str] = []
for line in lines:
stripped = line.strip()
if stripped.startswith("measure "):
left = stripped.split("->")[0].strip()
qi = int(left.split("[")[1].split("]")[0])
for gate in rot_gates[qi]:
new_lines.append(gate)
new_lines.append(line)
modified_qasm = "\n".join(new_lines)
# Simulate
sim = Simulator()
if shots is None:
probs = sim.simulate_pmeasure(modified_qasm)
return {f"{i:0{n}b}": float(p) for i, p in enumerate(probs)}
else:
counts = sim.simulate_shots(modified_qasm, shots=shots)
return {f"{k:0{n}b}": v for k, v in counts.items()}
__all__ = list(
set(
globals().get("__all__", [])
+ [
"basis_rotation_measurement",
"BasisRotationMeasurement",
"basis_rotation_measurement_example",
]
)
)
[docs]
class BasisRotationMeasurement:
"""Class-based interface for single-/multi-basis rotation measurement."""
def __init__(
self,
circuit: Circuit,
qubits: list[int] | None = None,
basis: str | list[str] | None = None,
shots: int | None = None,
) -> None:
self.circuit = circuit.copy()
self.qubits = qubits
self.basis = basis
self.shots = shots
[docs]
def get_readout_circuits(self) -> list[Circuit]:
"""Return the basis-rotated, measured circuit(s).
For a single-basis measurement returns a one-element list.
"""
rot = self.circuit.copy()
n = rot.max_qubit + 1
basis = self.basis
if isinstance(basis, str):
basis = list(basis)
if basis is None:
basis = ["Z"] * n
for i, b in enumerate(basis):
if b == "X":
rot.h(i)
elif b == "Y":
rot.sdg(i)
rot.h(i)
for q in range(n):
rot.measure(q)
return [rot]
[docs]
def execute(self, backend="statevector", *, program_type="qasm", **kwargs):
"""Run the measurement and return the existing function's output."""
measured = self.circuit.copy()
n = measured.max_qubit + 1
for q in range(n):
measured.measure(q)
return basis_rotation_measurement(measured, qubits=self.qubits, basis=self.basis, shots=self.shots)
[docs]
def basis_rotation_measurement_example():
"""Tiny example: measure a |+⟩ state in the X basis."""
c = Circuit()
c.h(0)
return BasisRotationMeasurement(c, basis="X", shots=1024).execute()