|
|
from __future__ import annotations |
|
|
import numpy as np |
|
|
from dataclasses import dataclass, field |
|
|
from typing import List, Dict, Any, Optional, Tuple |
|
|
|
|
|
from .gates import I, SINGLE_QUBIT_GATES, rx, ry, rz |
|
|
from quread.gates import single_qubit_gate_matrix |
|
|
|
|
|
Op = Dict[str, Any] |
|
|
|
|
|
def _normalize_state(state: np.ndarray) -> np.ndarray: |
|
|
norm = np.linalg.norm(state) |
|
|
if norm == 0: |
|
|
raise ValueError("State norm is zero; cannot normalize.") |
|
|
return state / norm |
|
|
|
|
|
def _bit(value: int, bit_index_from_right: int) -> int: |
|
|
|
|
|
return (value >> bit_index_from_right) & 1 |
|
|
|
|
|
def _flip_bit(value: int, bit_index_from_right: int) -> int: |
|
|
return value ^ (1 << bit_index_from_right) |
|
|
|
|
|
@dataclass |
|
|
class QuantumStateVector: |
|
|
n_qubits: int |
|
|
state: np.ndarray = field(init=False) |
|
|
history: List[Op] = field(default_factory=list) |
|
|
|
|
|
def __post_init__(self) -> None: |
|
|
if self.n_qubits < 1: |
|
|
raise ValueError("n_qubits must be >= 1.") |
|
|
dim = 2 ** self.n_qubits |
|
|
self.state = np.zeros(dim, dtype=complex) |
|
|
self.state[0] = 1.0 + 0j |
|
|
|
|
|
def reset(self) -> None: |
|
|
dim = 2 ** self.n_qubits |
|
|
self.state = np.zeros(dim, dtype=complex) |
|
|
self.state[0] = 1.0 + 0j |
|
|
self.history.clear() |
|
|
|
|
|
|
|
|
def apply_single(self, gate_name: str, target: int, theta: Optional[float] = None) -> None: |
|
|
if not (0 <= target < self.n_qubits): |
|
|
raise ValueError("target out of range") |
|
|
|
|
|
def _parse_angle(label: str) -> float: |
|
|
|
|
|
s = label.strip().lower().replace(" ", "") |
|
|
if s in ("π", "pi"): |
|
|
return float(np.pi) |
|
|
if s in ("π/2", "pi/2"): |
|
|
return float(np.pi / 2) |
|
|
raise ValueError(f"Unsupported angle in {gate_name}. Use π or π/2.") |
|
|
|
|
|
g = gate_name.strip() |
|
|
|
|
|
|
|
|
|
|
|
if g in ("T†", "Tdg"): |
|
|
g = "Tdg" |
|
|
if g in ("S†", "Sdg"): |
|
|
g = "Sdg" |
|
|
|
|
|
if g in ("I†", "Idg"): |
|
|
g = "I" |
|
|
|
|
|
if g in ("√X", "SX"): |
|
|
g = "SQRTX" |
|
|
if g in ("√Z", "SZ"): |
|
|
g = "SQRTZ" |
|
|
|
|
|
|
|
|
gate = None |
|
|
|
|
|
|
|
|
if g.startswith(("Rx(", "RX(")) and g.endswith(")"): |
|
|
ang = _parse_angle(g[g.find("(")+1 : -1]) |
|
|
gate = rx(float(ang)) |
|
|
g = f"RX({g[g.find('(')+1:-1]})" |
|
|
|
|
|
elif g.startswith(("Ry(", "RY(")) and g.endswith(")"): |
|
|
ang = _parse_angle(g[g.find("(")+1 : -1]) |
|
|
gate = ry(float(ang)) |
|
|
g = f"RY({g[g.find('(')+1:-1]})" |
|
|
|
|
|
elif g.startswith(("Rz(", "RZ(")) and g.endswith(")"): |
|
|
ang = _parse_angle(g[g.find("(")+1 : -1]) |
|
|
gate = rz(float(ang)) |
|
|
g = f"RZ({g[g.find('(')+1:-1]})" |
|
|
|
|
|
|
|
|
elif g == "RX": |
|
|
if theta is None: |
|
|
raise ValueError("RX requires theta") |
|
|
gate = rx(float(theta)) |
|
|
|
|
|
elif g == "RY": |
|
|
if theta is None: |
|
|
raise ValueError("RY requires theta") |
|
|
gate = ry(float(theta)) |
|
|
|
|
|
elif g == "RZ": |
|
|
if theta is None: |
|
|
raise ValueError("RZ requires theta") |
|
|
gate = rz(float(theta)) |
|
|
|
|
|
|
|
|
elif g in SINGLE_QUBIT_GATES: |
|
|
gate = SINGLE_QUBIT_GATES[g] |
|
|
|
|
|
else: |
|
|
raise ValueError(f"Unknown gate: {gate_name}") |
|
|
|
|
|
|
|
|
msb_index = self.n_qubits - 1 - target |
|
|
|
|
|
new_state = self.state.copy() |
|
|
dim = len(self.state) |
|
|
for basis in range(dim): |
|
|
if _bit(basis, msb_index) == 0: |
|
|
partner = _flip_bit(basis, msb_index) |
|
|
a0 = self.state[basis] |
|
|
a1 = self.state[partner] |
|
|
new_state[basis] = gate[0, 0] * a0 + gate[0, 1] * a1 |
|
|
new_state[partner] = gate[1, 0] * a0 + gate[1, 1] * a1 |
|
|
|
|
|
self.state = _normalize_state(new_state) |
|
|
|
|
|
op: Op = {"type": "single", "gate": g, "target": target} |
|
|
if theta is not None and g in ("RX", "RY", "RZ"): |
|
|
op["theta"] = float(theta) |
|
|
self.history.append(op) |
|
|
|
|
|
def apply_cnot(self, control: int, target: int) -> None: |
|
|
if control == target: |
|
|
raise ValueError("control and target must be different") |
|
|
if not (0 <= control < self.n_qubits) or not (0 <= target < self.n_qubits): |
|
|
raise ValueError("control/target out of range") |
|
|
|
|
|
c_bit = self.n_qubits - 1 - control |
|
|
t_bit = self.n_qubits - 1 - target |
|
|
|
|
|
new_state = self.state.copy() |
|
|
dim = len(self.state) |
|
|
visited = set() |
|
|
|
|
|
for basis in range(dim): |
|
|
if basis in visited: |
|
|
continue |
|
|
if _bit(basis, c_bit) == 1: |
|
|
flipped = _flip_bit(basis, t_bit) |
|
|
|
|
|
visited.add(basis); visited.add(flipped) |
|
|
new_state[basis], new_state[flipped] = self.state[flipped], self.state[basis] |
|
|
|
|
|
self.state = _normalize_state(new_state) |
|
|
self.history.append({"type": "cnot", "control": control, "target": target}) |
|
|
|
|
|
|
|
|
def probabilities(self) -> np.ndarray: |
|
|
probs = np.abs(self.state) ** 2 |
|
|
total = float(np.sum(probs)) |
|
|
if total == 0: |
|
|
return probs |
|
|
return probs / total |
|
|
|
|
|
def sample(self, shots: int = 1024) -> Dict[str, int]: |
|
|
probs = self.probabilities() |
|
|
dim = len(probs) |
|
|
outcomes = np.random.choice(np.arange(dim), size=int(shots), p=probs) |
|
|
counts: Dict[str, int] = {} |
|
|
for idx in outcomes: |
|
|
b = format(int(idx), f"0{self.n_qubits}b") |
|
|
counts[b] = counts.get(b, 0) + 1 |
|
|
return dict(sorted(counts.items())) |
|
|
|
|
|
def measure_collapse(self) -> str: |
|
|
probs = self.probabilities() |
|
|
dim = len(probs) |
|
|
idx = int(np.random.choice(np.arange(dim), p=probs)) |
|
|
collapsed = np.zeros(dim, dtype=complex) |
|
|
collapsed[idx] = 1.0 + 0j |
|
|
self.state = collapsed |
|
|
bitstring = format(idx, f"0{self.n_qubits}b") |
|
|
self.history.append({"type": "measure", "result": bitstring}) |
|
|
return bitstring |
|
|
|
|
|
|
|
|
def ket_notation(self, max_terms: int = 16, tol: float = 1e-9) -> str: |
|
|
|
|
|
terms = [] |
|
|
for i, amp in enumerate(self.state): |
|
|
if abs(amp) > tol: |
|
|
b = format(i, f"0{self.n_qubits}b") |
|
|
terms.append((amp, b)) |
|
|
|
|
|
terms.sort(key=lambda x: abs(x[0]), reverse=True) |
|
|
terms = terms[:max_terms] |
|
|
if not terms: |
|
|
return "0" |
|
|
parts = [] |
|
|
for amp, b in terms: |
|
|
a = complex(amp) |
|
|
parts.append(f"({a.real:+.4f}{a.imag:+.4f}j)|{b}⟩") |
|
|
return " + ".join(parts).lstrip("+").strip() |
|
|
|