Upload 6 files
Browse files- quread/__init__.py +0 -0
- quread/circuit_diagram.py +70 -0
- quread/engine.py +154 -0
- quread/exporters.py +84 -0
- quread/gates.py +26 -0
- quread/llm_explain.py +135 -0
quread/__init__.py
ADDED
|
File without changes
|
quread/circuit_diagram.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from typing import List, Dict, Any, Tuple
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import matplotlib.patches as patches
|
| 5 |
+
|
| 6 |
+
Op = Dict[str, Any]
|
| 7 |
+
|
| 8 |
+
def draw_circuit(ops: List[Op], n_qubits: int, title: str = "Circuit") -> plt.Figure:
|
| 9 |
+
# Basic, clean matplotlib circuit diagram: wires + boxes + CNOT dots
|
| 10 |
+
fig_w = max(7, 0.9*max(6, len(ops)+2))
|
| 11 |
+
fig_h = max(2.5, 0.6*n_qubits + 1)
|
| 12 |
+
fig, ax = plt.subplots(figsize=(fig_w, fig_h))
|
| 13 |
+
|
| 14 |
+
# coordinate system
|
| 15 |
+
x0 = 1.0
|
| 16 |
+
dx = 1.2
|
| 17 |
+
y_top = n_qubits - 1
|
| 18 |
+
|
| 19 |
+
# wires
|
| 20 |
+
for q in range(n_qubits):
|
| 21 |
+
y = y_top - q
|
| 22 |
+
ax.plot([0.5, x0 + dx*(len(ops)+1)], [y, y])
|
| 23 |
+
ax.text(0.1, y, f"q{q}", va="center")
|
| 24 |
+
|
| 25 |
+
# gates
|
| 26 |
+
for i, op in enumerate(ops):
|
| 27 |
+
x = x0 + dx*i
|
| 28 |
+
t = op.get("type")
|
| 29 |
+
if t == "single":
|
| 30 |
+
q = op["target"]
|
| 31 |
+
y = y_top - q
|
| 32 |
+
label = op["gate"]
|
| 33 |
+
if label in ("RX","RY","RZ"):
|
| 34 |
+
label = f"{label}({op['theta']:.2f})"
|
| 35 |
+
_box(ax, x, y, label)
|
| 36 |
+
elif t == "cnot":
|
| 37 |
+
c = op["control"]; tgt = op["target"]
|
| 38 |
+
yc = y_top - c; yt = y_top - tgt
|
| 39 |
+
# vertical line
|
| 40 |
+
ax.plot([x, x], [yc, yt])
|
| 41 |
+
# control dot
|
| 42 |
+
ax.add_patch(patches.Circle((x, yc), radius=0.12, fill=True))
|
| 43 |
+
# target plus in circle
|
| 44 |
+
ax.add_patch(patches.Circle((x, yt), radius=0.18, fill=False))
|
| 45 |
+
ax.plot([x-0.18, x+0.18], [yt, yt])
|
| 46 |
+
ax.plot([x, x], [yt-0.18, yt+0.18])
|
| 47 |
+
elif t == "measure":
|
| 48 |
+
# draw measurement on all wires (simple)
|
| 49 |
+
for q in range(n_qubits):
|
| 50 |
+
y = y_top - q
|
| 51 |
+
_box(ax, x, y, "M")
|
| 52 |
+
# stop drawing after measure
|
| 53 |
+
break
|
| 54 |
+
|
| 55 |
+
ax.set_title(title)
|
| 56 |
+
ax.set_xlim(0, x0 + dx*(len(ops)+2))
|
| 57 |
+
ax.set_ylim(-1, n_qubits)
|
| 58 |
+
ax.axis("off")
|
| 59 |
+
fig.tight_layout()
|
| 60 |
+
return fig
|
| 61 |
+
|
| 62 |
+
def _box(ax, x: float, y: float, text: str) -> None:
|
| 63 |
+
w, h = 0.8, 0.55
|
| 64 |
+
rect = patches.FancyBboxPatch(
|
| 65 |
+
(x - w/2, y - h/2), w, h,
|
| 66 |
+
boxstyle="round,pad=0.08,rounding_size=0.08",
|
| 67 |
+
linewidth=1.2, facecolor="white"
|
| 68 |
+
)
|
| 69 |
+
ax.add_patch(rect)
|
| 70 |
+
ax.text(x, y, text, ha="center", va="center", fontsize=10)
|
quread/engine.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import numpy as np
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import List, Dict, Any, Optional, Tuple
|
| 5 |
+
|
| 6 |
+
from .gates import I, SINGLE_QUBIT_GATES, rx, ry, rz
|
| 7 |
+
|
| 8 |
+
Op = Dict[str, Any]
|
| 9 |
+
|
| 10 |
+
def _normalize_state(state: np.ndarray) -> np.ndarray:
|
| 11 |
+
norm = np.linalg.norm(state)
|
| 12 |
+
if norm == 0:
|
| 13 |
+
raise ValueError("State norm is zero; cannot normalize.")
|
| 14 |
+
return state / norm
|
| 15 |
+
|
| 16 |
+
def _bit(value: int, bit_index_from_right: int) -> int:
|
| 17 |
+
# bit_index_from_right: 0 means least significant bit
|
| 18 |
+
return (value >> bit_index_from_right) & 1
|
| 19 |
+
|
| 20 |
+
def _flip_bit(value: int, bit_index_from_right: int) -> int:
|
| 21 |
+
return value ^ (1 << bit_index_from_right)
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class QuantumStateVector:
|
| 25 |
+
n_qubits: int
|
| 26 |
+
state: np.ndarray = field(init=False)
|
| 27 |
+
history: List[Op] = field(default_factory=list)
|
| 28 |
+
|
| 29 |
+
def __post_init__(self) -> None:
|
| 30 |
+
if self.n_qubits < 1:
|
| 31 |
+
raise ValueError("n_qubits must be >= 1.")
|
| 32 |
+
dim = 2 ** self.n_qubits
|
| 33 |
+
self.state = np.zeros(dim, dtype=complex)
|
| 34 |
+
self.state[0] = 1.0 + 0j # |0...0>
|
| 35 |
+
|
| 36 |
+
def reset(self) -> None:
|
| 37 |
+
dim = 2 ** self.n_qubits
|
| 38 |
+
self.state = np.zeros(dim, dtype=complex)
|
| 39 |
+
self.state[0] = 1.0 + 0j
|
| 40 |
+
self.history.clear()
|
| 41 |
+
|
| 42 |
+
# --------- Gate application (matrix-free, beginner friendly) ---------
|
| 43 |
+
def apply_single(self, gate_name: str, target: int, theta: Optional[float] = None) -> None:
|
| 44 |
+
if not (0 <= target < self.n_qubits):
|
| 45 |
+
raise ValueError("target out of range")
|
| 46 |
+
gate = None
|
| 47 |
+
if gate_name in SINGLE_QUBIT_GATES:
|
| 48 |
+
gate = SINGLE_QUBIT_GATES[gate_name]
|
| 49 |
+
elif gate_name == "RX":
|
| 50 |
+
if theta is None: raise ValueError("RX requires theta")
|
| 51 |
+
gate = rx(float(theta))
|
| 52 |
+
elif gate_name == "RY":
|
| 53 |
+
if theta is None: raise ValueError("RY requires theta")
|
| 54 |
+
gate = ry(float(theta))
|
| 55 |
+
elif gate_name == "RZ":
|
| 56 |
+
if theta is None: raise ValueError("RZ requires theta")
|
| 57 |
+
gate = rz(float(theta))
|
| 58 |
+
else:
|
| 59 |
+
raise ValueError(f"Unknown gate: {gate_name}")
|
| 60 |
+
|
| 61 |
+
# Apply using pairwise amplitude updates (no full 2^n x 2^n matrix)
|
| 62 |
+
# Convention: qubit 0 is the TOP wire in UI, but in basis indexing we treat
|
| 63 |
+
# qubit 0 as the most-significant bit (MSB). This keeps bitstrings readable.
|
| 64 |
+
msb_index = self.n_qubits - 1 - target # convert "wire index" -> bit position from right
|
| 65 |
+
|
| 66 |
+
new_state = self.state.copy()
|
| 67 |
+
dim = len(self.state)
|
| 68 |
+
for basis in range(dim):
|
| 69 |
+
if _bit(basis, msb_index) == 0:
|
| 70 |
+
partner = _flip_bit(basis, msb_index)
|
| 71 |
+
a0 = self.state[basis]
|
| 72 |
+
a1 = self.state[partner]
|
| 73 |
+
# [a0'; a1'] = gate * [a0; a1]
|
| 74 |
+
new_state[basis] = gate[0,0]*a0 + gate[0,1]*a1
|
| 75 |
+
new_state[partner] = gate[1,0]*a0 + gate[1,1]*a1
|
| 76 |
+
self.state = _normalize_state(new_state)
|
| 77 |
+
|
| 78 |
+
op: Op = {"type": "single", "gate": gate_name, "target": target}
|
| 79 |
+
if theta is not None:
|
| 80 |
+
op["theta"] = float(theta)
|
| 81 |
+
self.history.append(op)
|
| 82 |
+
|
| 83 |
+
def apply_cnot(self, control: int, target: int) -> None:
|
| 84 |
+
if control == target:
|
| 85 |
+
raise ValueError("control and target must be different")
|
| 86 |
+
if not (0 <= control < self.n_qubits) or not (0 <= target < self.n_qubits):
|
| 87 |
+
raise ValueError("control/target out of range")
|
| 88 |
+
|
| 89 |
+
c_bit = self.n_qubits - 1 - control
|
| 90 |
+
t_bit = self.n_qubits - 1 - target
|
| 91 |
+
|
| 92 |
+
new_state = self.state.copy()
|
| 93 |
+
dim = len(self.state)
|
| 94 |
+
visited = set()
|
| 95 |
+
|
| 96 |
+
for basis in range(dim):
|
| 97 |
+
if basis in visited:
|
| 98 |
+
continue
|
| 99 |
+
if _bit(basis, c_bit) == 1:
|
| 100 |
+
flipped = _flip_bit(basis, t_bit)
|
| 101 |
+
# swap amplitudes basis <-> flipped
|
| 102 |
+
visited.add(basis); visited.add(flipped)
|
| 103 |
+
new_state[basis], new_state[flipped] = self.state[flipped], self.state[basis]
|
| 104 |
+
|
| 105 |
+
self.state = _normalize_state(new_state)
|
| 106 |
+
self.history.append({"type": "cnot", "control": control, "target": target})
|
| 107 |
+
|
| 108 |
+
# --------- Measurement ---------
|
| 109 |
+
def probabilities(self) -> np.ndarray:
|
| 110 |
+
probs = np.abs(self.state) ** 2
|
| 111 |
+
total = float(np.sum(probs))
|
| 112 |
+
if total == 0:
|
| 113 |
+
return probs
|
| 114 |
+
return probs / total
|
| 115 |
+
|
| 116 |
+
def sample(self, shots: int = 1024) -> Dict[str, int]:
|
| 117 |
+
probs = self.probabilities()
|
| 118 |
+
dim = len(probs)
|
| 119 |
+
outcomes = np.random.choice(np.arange(dim), size=int(shots), p=probs)
|
| 120 |
+
counts: Dict[str, int] = {}
|
| 121 |
+
for idx in outcomes:
|
| 122 |
+
b = format(int(idx), f"0{self.n_qubits}b")
|
| 123 |
+
counts[b] = counts.get(b, 0) + 1
|
| 124 |
+
return dict(sorted(counts.items()))
|
| 125 |
+
|
| 126 |
+
def measure_collapse(self) -> str:
|
| 127 |
+
probs = self.probabilities()
|
| 128 |
+
dim = len(probs)
|
| 129 |
+
idx = int(np.random.choice(np.arange(dim), p=probs))
|
| 130 |
+
collapsed = np.zeros(dim, dtype=complex)
|
| 131 |
+
collapsed[idx] = 1.0 + 0j
|
| 132 |
+
self.state = collapsed
|
| 133 |
+
bitstring = format(idx, f"0{self.n_qubits}b")
|
| 134 |
+
self.history.append({"type": "measure", "result": bitstring})
|
| 135 |
+
return bitstring
|
| 136 |
+
|
| 137 |
+
# --------- Convenience helpers ---------
|
| 138 |
+
def ket_notation(self, max_terms: int = 16, tol: float = 1e-9) -> str:
|
| 139 |
+
# human readable statevector
|
| 140 |
+
terms = []
|
| 141 |
+
for i, amp in enumerate(self.state):
|
| 142 |
+
if abs(amp) > tol:
|
| 143 |
+
b = format(i, f"0{self.n_qubits}b")
|
| 144 |
+
terms.append((amp, b))
|
| 145 |
+
# sort by magnitude desc
|
| 146 |
+
terms.sort(key=lambda x: abs(x[0]), reverse=True)
|
| 147 |
+
terms = terms[:max_terms]
|
| 148 |
+
if not terms:
|
| 149 |
+
return "0"
|
| 150 |
+
parts = []
|
| 151 |
+
for amp, b in terms:
|
| 152 |
+
a = complex(amp)
|
| 153 |
+
parts.append(f"({a.real:+.4f}{a.imag:+.4f}j)|{b}⟩")
|
| 154 |
+
return " + ".join(parts).lstrip("+").strip()
|
quread/exporters.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from typing import List, Dict, Any
|
| 3 |
+
|
| 4 |
+
Op = Dict[str, Any]
|
| 5 |
+
|
| 6 |
+
def to_openqasm2(ops: List[Op], n_qubits: int) -> str:
|
| 7 |
+
lines = ["OPENQASM 2.0;", 'include "qelib1.inc";', f"qreg q[{n_qubits}];", f"creg c[{n_qubits}];", ""]
|
| 8 |
+
for op in ops:
|
| 9 |
+
t = op.get("type")
|
| 10 |
+
if t == "single":
|
| 11 |
+
g = op["gate"].lower()
|
| 12 |
+
q = op["target"]
|
| 13 |
+
if op["gate"] in ("RX","RY","RZ"):
|
| 14 |
+
theta = op["theta"]
|
| 15 |
+
g = op["gate"].lower()
|
| 16 |
+
lines.append(f"{g}({theta}) q[{q}];")
|
| 17 |
+
else:
|
| 18 |
+
lines.append(f"{g} q[{q}];")
|
| 19 |
+
elif t == "cnot":
|
| 20 |
+
lines.append(f"cx q[{op['control']}],q[{op['target']}];")
|
| 21 |
+
elif t == "measure":
|
| 22 |
+
# measure all qubits
|
| 23 |
+
lines.append("measure q -> c;")
|
| 24 |
+
break
|
| 25 |
+
return "\n".join(lines).strip() + "\n"
|
| 26 |
+
|
| 27 |
+
def to_qiskit(ops: List[Op], n_qubits: int) -> str:
|
| 28 |
+
lines = [
|
| 29 |
+
"from qiskit import QuantumCircuit",
|
| 30 |
+
"",
|
| 31 |
+
f"qc = QuantumCircuit({n_qubits}, {n_qubits})",
|
| 32 |
+
""
|
| 33 |
+
]
|
| 34 |
+
for op in ops:
|
| 35 |
+
t = op.get("type")
|
| 36 |
+
if t == "single":
|
| 37 |
+
g = op["gate"]
|
| 38 |
+
q = op["target"]
|
| 39 |
+
if g in ("RX","RY","RZ"):
|
| 40 |
+
lines.append(f"qc.{g.lower()}({op['theta']}, {q})")
|
| 41 |
+
else:
|
| 42 |
+
lines.append(f"qc.{g.lower()}({q})")
|
| 43 |
+
elif t == "cnot":
|
| 44 |
+
lines.append(f"qc.cx({op['control']}, {op['target']})")
|
| 45 |
+
elif t == "measure":
|
| 46 |
+
lines.append("qc.measure(range(qc.num_qubits), range(qc.num_clbits))")
|
| 47 |
+
break
|
| 48 |
+
lines.append("")
|
| 49 |
+
lines.append("print(qc)")
|
| 50 |
+
return "\n".join(lines).strip() + "\n"
|
| 51 |
+
|
| 52 |
+
def to_cirq(ops: List[Op], n_qubits: int) -> str:
|
| 53 |
+
lines = [
|
| 54 |
+
"import cirq",
|
| 55 |
+
"",
|
| 56 |
+
f"q = cirq.LineQubit.range({n_qubits})",
|
| 57 |
+
"circuit = cirq.Circuit()",
|
| 58 |
+
""
|
| 59 |
+
]
|
| 60 |
+
for op in ops:
|
| 61 |
+
t = op.get("type")
|
| 62 |
+
if t == "single":
|
| 63 |
+
g = op["gate"]
|
| 64 |
+
qu = op["target"]
|
| 65 |
+
if g in ("RX","RY","RZ"):
|
| 66 |
+
theta = op["theta"]
|
| 67 |
+
# Cirq uses radians in exponent form
|
| 68 |
+
if g == "RX":
|
| 69 |
+
lines.append(f"circuit.append(cirq.rx({theta}).on(q[{qu}]))")
|
| 70 |
+
elif g == "RY":
|
| 71 |
+
lines.append(f"circuit.append(cirq.ry({theta}).on(q[{qu}]))")
|
| 72 |
+
else:
|
| 73 |
+
lines.append(f"circuit.append(cirq.rz({theta}).on(q[{qu}]))")
|
| 74 |
+
else:
|
| 75 |
+
map_ = {"H":"H","X":"X","Y":"Y","Z":"Z","S":"S","T":"T","I":"I"}
|
| 76 |
+
lines.append(f"circuit.append(cirq.{map_[g]}.on(q[{qu}]))")
|
| 77 |
+
elif t == "cnot":
|
| 78 |
+
lines.append(f"circuit.append(cirq.CNOT(q[{op['control']}], q[{op['target']}]))")
|
| 79 |
+
elif t == "measure":
|
| 80 |
+
lines.append("circuit.append(cirq.measure(*q, key='m'))")
|
| 81 |
+
break
|
| 82 |
+
lines.append("")
|
| 83 |
+
lines.append("print(circuit)")
|
| 84 |
+
return "\n".join(lines).strip() + "\n"
|
quread/gates.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
# Single-qubit gates (2x2)
|
| 4 |
+
I = np.eye(2, dtype=complex)
|
| 5 |
+
X = np.array([[0, 1], [1, 0]], dtype=complex)
|
| 6 |
+
Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
|
| 7 |
+
Z = np.array([[1, 0], [0, -1]], dtype=complex)
|
| 8 |
+
H = (1/np.sqrt(2)) * np.array([[1, 1], [1, -1]], dtype=complex)
|
| 9 |
+
S = np.array([[1, 0], [0, 1j]], dtype=complex)
|
| 10 |
+
T = np.array([[1, 0], [0, np.exp(1j*np.pi/4)]], dtype=complex)
|
| 11 |
+
|
| 12 |
+
def rz(theta: float) -> np.ndarray:
|
| 13 |
+
return np.array([[np.exp(-1j*theta/2), 0],
|
| 14 |
+
[0, np.exp(1j*theta/2)]], dtype=complex)
|
| 15 |
+
|
| 16 |
+
def ry(theta: float) -> np.ndarray:
|
| 17 |
+
return np.array([[np.cos(theta/2), -np.sin(theta/2)],
|
| 18 |
+
[np.sin(theta/2), np.cos(theta/2)]], dtype=complex)
|
| 19 |
+
|
| 20 |
+
def rx(theta: float) -> np.ndarray:
|
| 21 |
+
return np.array([[np.cos(theta/2), -1j*np.sin(theta/2)],
|
| 22 |
+
[-1j*np.sin(theta/2), np.cos(theta/2)]], dtype=complex)
|
| 23 |
+
|
| 24 |
+
SINGLE_QUBIT_GATES = {
|
| 25 |
+
"I": I, "X": X, "Y": Y, "Z": Z, "H": H, "S": S, "T": T
|
| 26 |
+
}
|
quread/llm_explain.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# quread/llm_explain.py
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
from huggingface_hub import InferenceClient
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class ExplainConfig:
|
| 13 |
+
model_id: str = "HuggingFaceH4/zephyr-7b-beta" # you can change later
|
| 14 |
+
provider: str = "hf-inference"
|
| 15 |
+
max_new_tokens: int = 280
|
| 16 |
+
temperature: float = 0.2
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _build_grounded_prompt(
|
| 20 |
+
n_qubits: int,
|
| 21 |
+
history: List[Dict[str, Any]],
|
| 22 |
+
state_ket: str,
|
| 23 |
+
probs_top: List[Tuple[str, float]],
|
| 24 |
+
shots: Optional[int] = None,
|
| 25 |
+
) -> str:
|
| 26 |
+
"""
|
| 27 |
+
Prompt is explicitly grounded: it includes only circuit + computed outputs.
|
| 28 |
+
The model is instructed not to invent values.
|
| 29 |
+
"""
|
| 30 |
+
ops_lines = []
|
| 31 |
+
for op in history:
|
| 32 |
+
if op.get("type") == "single":
|
| 33 |
+
ops_lines.append(f"- {op['gate']}(q{op['target']})")
|
| 34 |
+
elif op.get("type") == "cnot":
|
| 35 |
+
ops_lines.append(f"- CNOT(q{op['control']} -> q{op['target']})")
|
| 36 |
+
else:
|
| 37 |
+
ops_lines.append(f"- {op}")
|
| 38 |
+
|
| 39 |
+
top_lines = [f"- {b}: {p:.4f}" for b, p in probs_top]
|
| 40 |
+
|
| 41 |
+
shots_line = f"Shots: {shots}\n" if shots is not None else ""
|
| 42 |
+
|
| 43 |
+
return f"""
|
| 44 |
+
You are a quantum computing tutor and debugger.
|
| 45 |
+
Only use the data provided below. Do not invent values not present here.
|
| 46 |
+
|
| 47 |
+
Task:
|
| 48 |
+
1) Explain what the circuit did, gate-by-gate, in plain English.
|
| 49 |
+
2) Explain whether the result suggests superposition and/or entanglement.
|
| 50 |
+
3) Explain why the top measurement outcomes make sense from interference.
|
| 51 |
+
4) Provide 2 debugging tips (e.g., qubit ordering, control/target confusion).
|
| 52 |
+
|
| 53 |
+
Data:
|
| 54 |
+
Qubits: {n_qubits}
|
| 55 |
+
Circuit operations:
|
| 56 |
+
{chr(10).join(ops_lines)}
|
| 57 |
+
|
| 58 |
+
Final state (ket-like summary):
|
| 59 |
+
{state_ket}
|
| 60 |
+
|
| 61 |
+
Top measurement probabilities:
|
| 62 |
+
{chr(10).join(top_lines)}
|
| 63 |
+
|
| 64 |
+
{shots_line}
|
| 65 |
+
Return a concise explanation with bullet points and short paragraphs.
|
| 66 |
+
""".strip()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _get_token() -> Optional[str]:
|
| 70 |
+
return os.getenv("HF_TOKEN")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def explain_circuit_with_hf(
|
| 74 |
+
n_qubits: int,
|
| 75 |
+
history: List[Dict[str, Any]],
|
| 76 |
+
state_ket: str,
|
| 77 |
+
probs_top: List[Tuple[str, float]],
|
| 78 |
+
*,
|
| 79 |
+
shots: Optional[int] = None,
|
| 80 |
+
cfg: Optional[ExplainConfig] = None,
|
| 81 |
+
) -> str:
|
| 82 |
+
"""
|
| 83 |
+
Calls Hugging Face Inference Providers via huggingface_hub.InferenceClient.
|
| 84 |
+
|
| 85 |
+
Tries chat-completion first (if supported by model), then text-generation.
|
| 86 |
+
"""
|
| 87 |
+
cfg = cfg or ExplainConfig()
|
| 88 |
+
token = _get_token()
|
| 89 |
+
if not token:
|
| 90 |
+
return "HF_TOKEN is not set. Please set it as an environment variable."
|
| 91 |
+
|
| 92 |
+
client = InferenceClient(provider=cfg.provider, token=token)
|
| 93 |
+
|
| 94 |
+
prompt = _build_grounded_prompt(
|
| 95 |
+
n_qubits=n_qubits,
|
| 96 |
+
history=history,
|
| 97 |
+
state_ket=state_ket,
|
| 98 |
+
probs_top=probs_top,
|
| 99 |
+
shots=shots,
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# 1) Try Chat Completion
|
| 103 |
+
try:
|
| 104 |
+
resp = client.chat_completion(
|
| 105 |
+
model=cfg.model_id,
|
| 106 |
+
messages=[
|
| 107 |
+
{"role": "system", "content": "You are a helpful quantum tutor."},
|
| 108 |
+
{"role": "user", "content": prompt},
|
| 109 |
+
],
|
| 110 |
+
max_tokens=cfg.max_new_tokens,
|
| 111 |
+
temperature=cfg.temperature,
|
| 112 |
+
)
|
| 113 |
+
# huggingface_hub returns a structured object
|
| 114 |
+
return resp.choices[0].message.content.strip()
|
| 115 |
+
except Exception:
|
| 116 |
+
pass
|
| 117 |
+
|
| 118 |
+
# 2) Fallback: Text Generation
|
| 119 |
+
try:
|
| 120 |
+
out = client.text_generation(
|
| 121 |
+
model=cfg.model_id,
|
| 122 |
+
prompt=prompt,
|
| 123 |
+
max_new_tokens=cfg.max_new_tokens,
|
| 124 |
+
temperature=cfg.temperature,
|
| 125 |
+
)
|
| 126 |
+
return out.strip()
|
| 127 |
+
except Exception as e:
|
| 128 |
+
return (
|
| 129 |
+
"LLM call failed.\n\n"
|
| 130 |
+
f"Model: {cfg.model_id}\n"
|
| 131 |
+
f"Provider: {cfg.provider}\n"
|
| 132 |
+
f"Error: {repr(e)}\n\n"
|
| 133 |
+
"Try changing the model_id to a different HF model that supports "
|
| 134 |
+
"text-generation or chat-completion via Inference Providers."
|
| 135 |
+
)
|