testing_space / QuantumCircuits.py
everydaytok's picture
Update QuantumCircuits.py
51595bf verified
"""
QuantumCircuits.py
Native PyTorch Micro-VM for Heisenberg-picture quantum circuits.
UPDATE 24.4: Bypassed SymPy entirely. This module now compiles circuits
into a direct, vectorized PyTorch execution graph. Evaluates the BBGKY
Cumulant Expansion natively on the GPU/CPU for massive speedup and zero crashes.
"""
from __future__ import annotations
import math
import torch
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Any
GATE_TYPES = {"Rx", "Ry", "Rz", "H", "S", "T", "CNOT", "CZ"}
PAULI = ("x", "y", "z")
PAULI2 = [p1 + p2 for p1 in PAULI for p2 in PAULI]
@dataclass
class QGate:
type: str
qubits: List[int]
params: List[str] = field(default_factory=list)
layer: int = 0
def validate(self):
if self.type not in GATE_TYPES:
raise ValueError(f"Unknown gate: {self.type}")
@dataclass
class QuantumCircuit:
n_qubits: int
gates: List[QGate]
param_bounds: Dict[str, Tuple[float, float]] = field(default_factory=dict)
initial_state: str = "zero"
initial_obs: Dict[str, float] = field(default_factory=dict)
expected_observables: Dict[str, float] = field(default_factory=dict)
include_2body: bool = True
name: str = "QuantumCircuit"
description: str = ""
@property
def n_layers(self) -> int:
return max([g.layer for g in self.gates] + [-1]) + 1
@property
def all_params(self) -> List[str]:
seen = set()
params = []
for g in self.gates:
for p in g.params:
if p not in seen:
seen.add(p); params.append(p)
return params
def obs1(pauli: str, qubit: int, layer: int) -> str:
return f"{pauli}{qubit}_L{layer}"
def obs2_safe(pauli1: str, q1: int, pauli2: str, q2: int, layer: int) -> str:
if q1 == q2: raise ValueError(f"Invalid 2-body observable on same qubit: {q1}")
if q1 > q2: return f"{pauli2}{pauli1}{q2}{q1}_L{layer}"
return f"{pauli1}{pauli2}{q1}{q2}_L{layer}"
def _initial_obs_values(circuit: QuantumCircuit) -> Dict[str, float]:
obs = {}
if circuit.initial_state == "zero":
for q in range(circuit.n_qubits):
obs[obs1("z", q, 0)] = 1.0; obs[obs1("x", q, 0)] = 0.0; obs[obs1("y", q, 0)] = 0.0
if circuit.include_2body:
for q1 in range(circuit.n_qubits):
for q2 in range(q1 + 1, circuit.n_qubits):
for p1, p2 in PAULI2:
obs[obs2_safe(p1, q1, p2, q2, 0)] = 1.0 if (p1 == "z" and p2 == "z") else 0.0
return obs
# ── PyTorch Native Micro-VM ──────────────────────────────────────────
class NativeQuantumVM:
"""Executes Heisenberg topological constraints directly on PyTorch Tensors."""
def __init__(self, circuit: QuantumCircuit, var_idx: Dict[str, int]):
self.circuit = circuit
self.var_idx = var_idx
self.n_layers = circuit.n_layers
self.n = circuit.n_qubits
self.include_2b = circuit.include_2body
def idx(self, name: str) -> int:
return self.var_idx[name]
def execute(self, X: torch.Tensor, step_ratio: float, device: torch.device) -> torch.Tensor:
B = X.shape[0] if X.dim() == 2 else 1
loss = torch.zeros(B, device=device, dtype=torch.float32)
# Helper to smoothly penalize differences (L2 MSE)
def enforce(actual_idx, expected_tensor):
actual = X[:, actual_idx] if X.dim() == 2 else X[actual_idx]
return (actual - expected_tensor) ** 2
# 1. Bloch Sphere Bounds (Purity)
for layer in range(self.n_layers + 1):
for q in range(self.n):
x, y, z = X[..., self.idx(obs1('x', q, layer))], X[..., self.idx(obs1('y', q, layer))], X[..., self.idx(obs1('z', q, layer))]
purity = 1.0 - x**2 - y**2 - z**2
loss += (torch.relu(-purity) ** 2) * 2.0
# 2. Layer Transitions (Native Tensor Math)
for layer_in in range(self.n_layers):
layer_out = layer_in + 1
layer_gates = [g for g in self.circuit.gates if g.layer == layer_in]
affected = set()
for gate in layer_gates:
if gate.type in ("Rz", "Rx"):
q = gate.qubits[0]; affected.add(q)
theta = X[..., self.idx(gate.params[0])]
c, s = torch.cos(theta), torch.sin(theta)
xi, yi, zi = X[..., self.idx(obs1('x', q, layer_in))], X[..., self.idx(obs1('y', q, layer_in))], X[..., self.idx(obs1('z', q, layer_in))]
if gate.type == "Rz":
loss += enforce(self.idx(obs1('x', q, layer_out)), c * xi - s * yi) * 5.0
loss += enforce(self.idx(obs1('y', q, layer_out)), s * xi + c * yi) * 5.0
loss += enforce(self.idx(obs1('z', q, layer_out)), zi) * 5.0
else: # Rx
loss += enforce(self.idx(obs1('x', q, layer_out)), xi) * 5.0
loss += enforce(self.idx(obs1('y', q, layer_out)), c * yi - s * zi) * 5.0
loss += enforce(self.idx(obs1('z', q, layer_out)), s * yi + c * zi) * 5.0
elif gate.type == "H":
q = gate.qubits[0]; affected.add(q)
loss += enforce(self.idx(obs1('x', q, layer_out)), X[..., self.idx(obs1('z', q, layer_in))]) * 5.0
loss += enforce(self.idx(obs1('y', q, layer_out)), -X[..., self.idx(obs1('y', q, layer_in))]) * 5.0
loss += enforce(self.idx(obs1('z', q, layer_out)), X[..., self.idx(obs1('x', q, layer_in))]) * 5.0
elif gate.type == "CNOT":
k, j = gate.qubits[0], gate.qubits[1]; affected.update([k, j])
# 1-body to 2-body Entanglement
loss += enforce(self.idx(obs1('x', k, layer_out)), X[..., self.idx(obs2_safe('x', k, 'x', j, layer_in))]) * 5.0
loss += enforce(self.idx(obs1('y', k, layer_out)), X[..., self.idx(obs2_safe('y', k, 'x', j, layer_in))]) * 5.0
loss += enforce(self.idx(obs1('z', k, layer_out)), X[..., self.idx(obs1('z', k, layer_in))]) * 5.0
loss += enforce(self.idx(obs1('x', j, layer_out)), X[..., self.idx(obs1('x', j, layer_in))]) * 5.0
loss += enforce(self.idx(obs1('y', j, layer_out)), X[..., self.idx(obs2_safe('z', k, 'y', j, layer_in))]) * 5.0
loss += enforce(self.idx(obs1('z', j, layer_out)), X[..., self.idx(obs2_safe('z', k, 'z', j, layer_in))]) * 5.0
# 2-body internal mixing
loss += enforce(self.idx(obs2_safe('x', k, 'x', j, layer_out)), X[..., self.idx(obs1('x', k, layer_in))]) * 5.0
loss += enforce(self.idx(obs2_safe('z', k, 'z', j, layer_out)), X[..., self.idx(obs1('z', j, layer_in))]) * 5.0
loss += enforce(self.idx(obs2_safe('x', k, 'z', j, layer_out)), -X[..., self.idx(obs2_safe('y', k, 'y', j, layer_in))]) * 5.0
loss += enforce(self.idx(obs2_safe('y', k, 'z', j, layer_out)), X[..., self.idx(obs2_safe('x', k, 'y', j, layer_in))]) * 5.0
# NATIVE BBGKY CUMULANT EXPANSION (Spectators)
for r in range(self.n):
if r in (k, j): continue
if self.include_2b:
for pr in PAULI:
# Example: X_k P_r -> X_k X_j P_r (3-body)
# Cumulant: <Xk Xj><Pr> + <Xk Pr><Xj> + <Xj Pr><Xk> - 2<Xk><Xj><Pr>
o_k_j = X[..., self.idx(obs2_safe('x', k, 'x', j, layer_in))]
o_k_r = X[..., self.idx(obs2_safe('x', k, pr, r, layer_in))]
o_j_r = X[..., self.idx(obs2_safe('x', j, pr, r, layer_in))]
o_k = X[..., self.idx(obs1('x', k, layer_in))]
o_j = X[..., self.idx(obs1('x', j, layer_in))]
o_r = X[..., self.idx(obs1(pr, r, layer_in))]
c3 = (o_k_j * o_r) + (o_k_r * o_j) + (o_j_r * o_k) - (2.0 * o_k * o_j * o_r)
loss += enforce(self.idx(obs2_safe('x', k, pr, r, layer_out)), c3) * 4.0
# Identity for unaffected qubits
unaffected = [q for q in range(self.n) if q not in affected]
for q in unaffected:
for p in PAULI:
loss += enforce(self.idx(obs1(p, q, layer_out)), X[..., self.idx(obs1(p, q, layer_in))]) * 5.0
if self.include_2b:
for q2 in range(self.n):
if q2 == q or q2 in affected: continue
for p1p2 in PAULI2:
loss += enforce(self.idx(obs2_safe(p1p2[0], q, p1p2[1], q2, layer_out)), X[..., self.idx(obs2_safe(p1p2[0], q, p1p2[1], q2, layer_in))]) * 5.0
return loss.squeeze()
# ── Main builder ──────────────────────────────────────────────────────
def build_quantum_axl(circuit: QuantumCircuit, axl_problem_class: Any, axl_invariant_class: Any) -> Any:
for g in circuit.gates: g.validate()
n = circuit.n_qubits
n_layers = circuit.n_layers
include_2b = circuit.include_2body
variables = []
for pname in circuit.all_params:
lo, hi = circuit.param_bounds.get(pname, (-math.pi, math.pi))
variables.append({"name": pname, "lo": lo, "hi": hi})
for layer in range(n_layers + 1):
for q in range(n):
for p in PAULI:
variables.append({"name": obs1(p, q, layer), "lo": -1.0, "hi": 1.0})
if include_2b:
for q1 in range(n):
for q2 in range(q1 + 1, n):
for p1p2 in PAULI2:
variables.append({"name": obs2_safe(p1p2[0], q1, p1p2[1], q2, layer), "lo": -1.0, "hi": 1.0})
obs_fixed = _initial_obs_values(circuit)
anchors = []
for obs, val in circuit.expected_observables.items():
if len(obs) == 2: expr = obs1(obs[0], int(obs[1:]), n_layers)
elif len(obs) == 4: expr = obs2_safe(obs[0], int(obs[2]), obs[1], int(obs[3]), n_layers)
anchors.append(axl_invariant_class(name=f"verify_{obs}", expr=f"{expr} - ({val})", tolerance=0.05, mode="eq"))
# Empty constraints/scopes because Native PyTorch VM will handle evaluation
prob = axl_problem_class(
name=circuit.name, description=circuit.description,
axioms={"CONTINUOUS", "CONSERVED", "TRANSITIVE", "BILINEAR", "METRIC", "SUPERPOSITION", "SYMMETRIC"},
variables=variables, constraints=[], scopes=[], anchors=anchors, observations=obs_fixed
)
prob.is_quantum = True
prob.circuit = circuit
return prob
# ══════════════════════════════════════════════════════════════════════
# EXAMPLE CIRCUITS
# ══════════════════════════════════════════════════════════════════════
def example_bell_state() -> QuantumCircuit:
return QuantumCircuit(
n_qubits=2, name="BellStatePrep",
gates=[QGate("H", [0], layer=0), QGate("CNOT", [0, 1], layer=1)],
initial_state="zero", include_2body=True,
expected_observables={"zz01": 1.0, "xx01": 1.0}
)
def example_vqe_h2() -> QuantumCircuit:
return QuantumCircuit(
n_qubits=2, name="VQE_H2",
gates=[
QGate("Ry", [0], ["theta0"], layer=0),
QGate("Ry", [1], ["theta1"], layer=0),
QGate("CNOT", [0, 1], layer=1),
],
param_bounds={"theta0": (-math.pi, math.pi), "theta1": (-math.pi, math.pi)},
expected_observables={"z0": -0.5, "z1": -0.5, "zz01": 0.25, "xx01": -0.5}
)
def example_qaoa_maxcut_p1() -> QuantumCircuit:
return QuantumCircuit(
n_qubits=3, name="QAOA_MaxCut_Deep",
gates=[
QGate("H", [0], layer=0), QGate("H", [1], layer=0), QGate("H", [2], layer=0),
QGate("CNOT", [0, 1], layer=1), QGate("Rz", [1], ["gamma"], layer=2), QGate("CNOT", [0, 1], layer=3),
QGate("CNOT", [1, 2], layer=4), QGate("Rz", [2], ["gamma"], layer=5), QGate("CNOT", [1, 2], layer=6),
QGate("CNOT", [0, 2], layer=7), QGate("Rz", [2], ["gamma"], layer=8), QGate("CNOT", [0, 2], layer=9),
QGate("Rx", [0], ["beta"], layer=10), QGate("Rx", [1], ["beta"], layer=10), QGate("Rx", [2], ["beta"], layer=10),
],
param_bounds={"gamma": (0, math.pi), "beta": (0, math.pi / 2)},
expected_observables={"zz01": -0.5, "zz12": -0.5, "zz02": -0.5}
)