hchevva commited on
Commit
ea2ca81
·
verified ·
1 Parent(s): 50d4abf

Upload 6 files

Browse files
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
+ )