hchevva commited on
Commit
c0033b7
·
verified ·
1 Parent(s): 7adad6c

Upload 3 files

Browse files
Files changed (3) hide show
  1. quread/exporters.py +122 -24
  2. quread/heatmap.py +62 -18
  3. quread/llm_explain_openai.py +21 -9
quread/exporters.py CHANGED
@@ -1,21 +1,94 @@
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":
@@ -31,15 +104,30 @@ def to_qiskit(ops: List[Op], n_qubits: int) -> str:
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":
@@ -57,23 +145,33 @@ def to_cirq(ops: List[Op], n_qubits: int) -> str:
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":
@@ -169,4 +267,4 @@ def csv_to_skill(csv_text: str, n_qubits: int | None = None) -> str:
169
  out.append("; )")
170
  out.append("")
171
 
172
- return "\n".join(out) + "\n"
 
1
  from __future__ import annotations
2
+ import math
3
+ import re
4
+ from typing import List, Dict, Any, Tuple
5
 
6
  Op = Dict[str, Any]
7
 
8
+ _ROT_GATE_RE = re.compile(r"^(R[XYZ])\((.+)\)$", re.IGNORECASE)
9
+
10
+
11
+ def _parse_angle(value: Any) -> float:
12
+ if isinstance(value, (int, float)):
13
+ return float(value)
14
+
15
+ s = str(value).strip().lower().replace(" ", "")
16
+ if s in ("π", "pi"):
17
+ return float(math.pi)
18
+ if s in ("π/2", "pi/2"):
19
+ return float(math.pi / 2.0)
20
+ return float(s)
21
+
22
+
23
+ def _fmt_theta(theta: float) -> str:
24
+ return f"{float(theta):.15g}"
25
+
26
+
27
+ def _canonical_single_gate(op: Op) -> Tuple[str, float | None]:
28
+ gate_raw = str(op.get("gate", "")).strip()
29
+ if not gate_raw:
30
+ raise ValueError("Single-qubit op is missing gate name.")
31
+
32
+ aliases = {
33
+ "S†": "SDG",
34
+ "SDG": "SDG",
35
+ "T†": "TDG",
36
+ "TDG": "TDG",
37
+ "√X": "SQRTX",
38
+ "SX": "SQRTX",
39
+ "√Z": "SQRTZ",
40
+ "SZ": "SQRTZ",
41
+ "I†": "I",
42
+ "IDG": "I",
43
+ "ID": "I",
44
+ }
45
+
46
+ g_upper = gate_raw.upper()
47
+ if g_upper in aliases:
48
+ return aliases[g_upper], None
49
+
50
+ rot = _ROT_GATE_RE.match(gate_raw)
51
+ if rot:
52
+ return rot.group(1).upper(), _parse_angle(rot.group(2))
53
+
54
+ if g_upper in ("RX", "RY", "RZ"):
55
+ if "theta" not in op:
56
+ raise ValueError(f"{g_upper} is missing theta in op history.")
57
+ return g_upper, _parse_angle(op["theta"])
58
+
59
+ if g_upper in ("I", "H", "X", "Y", "Z", "S", "T", "SQRTX", "SQRTZ"):
60
+ return g_upper, None
61
+
62
+ raise ValueError(f"Unsupported gate in history: {gate_raw}")
63
+
64
+
65
  def to_openqasm2(ops: List[Op], n_qubits: int) -> str:
66
  lines = ["OPENQASM 2.0;", 'include "qelib1.inc";', f"qreg q[{n_qubits}];", f"creg c[{n_qubits}];", ""]
67
+ gate_map = {
68
+ "I": "id",
69
+ "H": "h",
70
+ "X": "x",
71
+ "Y": "y",
72
+ "Z": "z",
73
+ "S": "s",
74
+ "SDG": "sdg",
75
+ "T": "t",
76
+ "TDG": "tdg",
77
+ }
78
  for op in ops:
79
  t = op.get("type")
80
  if t == "single":
81
+ g, theta = _canonical_single_gate(op)
82
+ q = int(op["target"])
83
+ if g in ("RX", "RY", "RZ"):
84
+ lines.append(f"{g.lower()}({_fmt_theta(theta)}) q[{q}];")
85
+ elif g == "SQRTX":
86
+ # Equivalent up to global phase, more portable than sx in some parsers.
87
+ lines.append(f"rx({_fmt_theta(math.pi / 2.0)}) q[{q}];")
88
+ elif g == "SQRTZ":
89
+ lines.append(f"s q[{q}];")
90
  else:
91
+ lines.append(f"{gate_map[g]} q[{q}];")
92
  elif t == "cnot":
93
  lines.append(f"cx q[{op['control']}],q[{op['target']}];")
94
  elif t == "measure":
 
104
  f"qc = QuantumCircuit({n_qubits}, {n_qubits})",
105
  ""
106
  ]
107
+ gate_map = {
108
+ "I": "id",
109
+ "H": "h",
110
+ "X": "x",
111
+ "Y": "y",
112
+ "Z": "z",
113
+ "S": "s",
114
+ "SDG": "sdg",
115
+ "T": "t",
116
+ "TDG": "tdg",
117
+ }
118
  for op in ops:
119
  t = op.get("type")
120
  if t == "single":
121
+ g, theta = _canonical_single_gate(op)
122
+ q = int(op["target"])
123
+ if g in ("RX", "RY", "RZ"):
124
+ lines.append(f"qc.{g.lower()}({_fmt_theta(theta)}, {q})")
125
+ elif g == "SQRTX":
126
+ lines.append(f"qc.sx({q})")
127
+ elif g == "SQRTZ":
128
+ lines.append(f"qc.s({q})")
129
  else:
130
+ lines.append(f"qc.{gate_map[g]}({q})")
131
  elif t == "cnot":
132
  lines.append(f"qc.cx({op['control']}, {op['target']})")
133
  elif t == "measure":
 
145
  "circuit = cirq.Circuit()",
146
  ""
147
  ]
148
+ gate_expr = {
149
+ "I": "cirq.I",
150
+ "H": "cirq.H",
151
+ "X": "cirq.X",
152
+ "Y": "cirq.Y",
153
+ "Z": "cirq.Z",
154
+ "S": "cirq.S",
155
+ "SDG": "(cirq.S**-1)",
156
+ "T": "cirq.T",
157
+ "TDG": "(cirq.T**-1)",
158
+ "SQRTX": "(cirq.X**0.5)",
159
+ "SQRTZ": "(cirq.Z**0.5)",
160
+ }
161
  for op in ops:
162
  t = op.get("type")
163
  if t == "single":
164
+ g, theta = _canonical_single_gate(op)
165
+ qu = int(op["target"])
166
+ if g in ("RX", "RY", "RZ"):
 
 
167
  if g == "RX":
168
+ lines.append(f"circuit.append(cirq.rx({_fmt_theta(theta)}).on(q[{qu}]))")
169
  elif g == "RY":
170
+ lines.append(f"circuit.append(cirq.ry({_fmt_theta(theta)}).on(q[{qu}]))")
171
  else:
172
+ lines.append(f"circuit.append(cirq.rz({_fmt_theta(theta)}).on(q[{qu}]))")
173
  else:
174
+ lines.append(f"circuit.append({gate_expr[g]}.on(q[{qu}]))")
 
175
  elif t == "cnot":
176
  lines.append(f"circuit.append(cirq.CNOT(q[{op['control']}], q[{op['target']}]))")
177
  elif t == "measure":
 
267
  out.append("; )")
268
  out.append("")
269
 
270
+ return "\n".join(out) + "\n"
quread/heatmap.py CHANGED
@@ -3,12 +3,15 @@ from __future__ import annotations
3
 
4
  import csv
5
  import io
 
6
  from dataclasses import dataclass
7
- from typing import Dict, List, Optional, Tuple
8
 
9
  import numpy as np
10
  import matplotlib.pyplot as plt
11
 
 
 
12
 
13
  @dataclass
14
  class HeatmapConfig:
@@ -32,23 +35,53 @@ def _default_qubit_coords(n_qubits: int, rows: int, cols: int) -> Dict[int, Tupl
32
  return m
33
 
34
 
35
- def _parse_history_rows(csv_text: str) -> List[dict]:
36
  """
37
  Expects the CSV you generate in Task 2A:
38
  step,gate,target,control,theta
39
  """
 
 
 
 
 
 
 
 
 
40
  f = io.StringIO(csv_text or "")
41
  reader = csv.DictReader(f)
42
- rows = []
 
43
  for r in reader:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  rows.append({
45
- "step": int(r.get("step", "0") or 0),
46
- "gate": (r.get("gate") or "").strip(),
47
- "target": r.get("target", "").strip(),
48
- "control": r.get("control", "").strip(),
49
  "theta": (r.get("theta") or "").strip(),
50
  })
51
- return rows
 
52
 
53
 
54
  def make_activity_heatmap(
@@ -70,21 +103,19 @@ def make_activity_heatmap(
70
  # count per qubit
71
  counts = np.zeros((n_qubits,), dtype=float)
72
 
73
- rows = _parse_history_rows(csv_text)
74
  for r in rows:
75
  gate = r["gate"].upper()
76
 
77
  # target
78
- if r["target"] != "":
79
- t = int(r["target"])
80
- if 0 <= t < n_qubits:
81
- counts[t] += 1.0
82
 
83
  # control for CNOT
84
- if gate == "CNOT" and r["control"] != "":
85
- c = int(r["control"])
86
- if 0 <= c < n_qubits:
87
- counts[c] += 1.0
88
 
89
  # place into chip grid
90
  for q, (rr, cc) in coords.items():
@@ -99,6 +130,19 @@ def make_activity_heatmap(
99
  ax.set_ylabel("Chip row")
100
  fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  # annotate qubit ids on mapped cells
103
  for q, (rr, cc) in coords.items():
104
  if 0 <= rr < cfg.rows and 0 <= cc < cfg.cols:
@@ -107,4 +151,4 @@ def make_activity_heatmap(
107
  ax.set_xticks(range(cfg.cols))
108
  ax.set_yticks(range(cfg.rows))
109
  fig.tight_layout()
110
- return fig
 
3
 
4
  import csv
5
  import io
6
+ import logging
7
  from dataclasses import dataclass
8
+ from typing import Dict, List, Optional, Tuple, Any
9
 
10
  import numpy as np
11
  import matplotlib.pyplot as plt
12
 
13
+ logger = logging.getLogger(__name__)
14
+
15
 
16
  @dataclass
17
  class HeatmapConfig:
 
35
  return m
36
 
37
 
38
+ def _parse_history_rows(csv_text: str) -> Tuple[List[dict], int]:
39
  """
40
  Expects the CSV you generate in Task 2A:
41
  step,gate,target,control,theta
42
  """
43
+ def _to_int_or_none(v: Any) -> Optional[int]:
44
+ s = str(v).strip()
45
+ if s == "":
46
+ return None
47
+ try:
48
+ return int(s)
49
+ except Exception:
50
+ return None
51
+
52
  f = io.StringIO(csv_text or "")
53
  reader = csv.DictReader(f)
54
+ rows: List[dict] = []
55
+ skipped = 0
56
  for r in reader:
57
+ gate = (r.get("gate") or "").strip()
58
+ target_raw = r.get("target", "")
59
+ control_raw = r.get("control", "")
60
+ target = _to_int_or_none(target_raw)
61
+ control = _to_int_or_none(control_raw)
62
+ step = _to_int_or_none(r.get("step", ""))
63
+
64
+ malformed = False
65
+ if not gate:
66
+ malformed = True
67
+ if str(target_raw).strip() != "" and target is None:
68
+ malformed = True
69
+ if str(control_raw).strip() != "" and control is None:
70
+ malformed = True
71
+
72
+ if malformed:
73
+ skipped += 1
74
+ continue
75
+
76
  rows.append({
77
+ "step": 0 if step is None else step,
78
+ "gate": gate,
79
+ "target": target,
80
+ "control": control,
81
  "theta": (r.get("theta") or "").strip(),
82
  })
83
+
84
+ return rows, skipped
85
 
86
 
87
  def make_activity_heatmap(
 
103
  # count per qubit
104
  counts = np.zeros((n_qubits,), dtype=float)
105
 
106
+ rows, skipped = _parse_history_rows(csv_text)
107
  for r in rows:
108
  gate = r["gate"].upper()
109
 
110
  # target
111
+ t = r["target"]
112
+ if t is not None and 0 <= t < n_qubits:
113
+ counts[t] += 1.0
 
114
 
115
  # control for CNOT
116
+ c = r["control"]
117
+ if gate == "CNOT" and c is not None and 0 <= c < n_qubits:
118
+ counts[c] += 1.0
 
119
 
120
  # place into chip grid
121
  for q, (rr, cc) in coords.items():
 
130
  ax.set_ylabel("Chip row")
131
  fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
132
 
133
+ if skipped:
134
+ logger.warning("Skipped %d malformed CSV rows while building heatmap.", skipped)
135
+ ax.text(
136
+ 0.01,
137
+ 1.02,
138
+ f"Skipped {skipped} malformed CSV row(s)",
139
+ transform=ax.transAxes,
140
+ fontsize=8,
141
+ color="#b91c1c",
142
+ ha="left",
143
+ va="bottom",
144
+ )
145
+
146
  # annotate qubit ids on mapped cells
147
  for q, (rr, cc) in coords.items():
148
  if 0 <= rr < cfg.rows and 0 <= cc < cfg.cols:
 
151
  ax.set_xticks(range(cfg.cols))
152
  ax.set_yticks(range(cfg.rows))
153
  fig.tight_layout()
154
+ return fig
quread/llm_explain_openai.py CHANGED
@@ -3,7 +3,12 @@ from typing import Any, Dict, List, Optional, Tuple
3
  import os
4
  from openai import OpenAI
5
 
6
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
 
 
 
 
 
7
 
8
 
9
  def explain_with_gpt4o(
@@ -69,11 +74,18 @@ Shots: {shots}
69
  Answer:
70
  """
71
 
72
- response = client.chat.completions.create(
73
- model="gpt-4o",
74
- messages=[{"role": "user", "content": prompt}],
75
- temperature=0.2,
76
- max_tokens=800,
77
- )
78
-
79
- return response.choices[0].message.content.strip()
 
 
 
 
 
 
 
 
3
  import os
4
  from openai import OpenAI
5
 
6
+
7
+ def _build_client() -> OpenAI:
8
+ api_key = os.getenv("OPENAI_API_KEY")
9
+ if not api_key:
10
+ raise RuntimeError("OPENAI_API_KEY is not set.")
11
+ return OpenAI(api_key=api_key)
12
 
13
 
14
  def explain_with_gpt4o(
 
74
  Answer:
75
  """
76
 
77
+ try:
78
+ client = _build_client()
79
+ response = client.chat.completions.create(
80
+ model="gpt-4o",
81
+ messages=[{"role": "user", "content": prompt}],
82
+ temperature=0.2,
83
+ max_tokens=800,
84
+ )
85
+ except Exception as exc:
86
+ raise RuntimeError(f"OpenAI request failed ({type(exc).__name__}).") from exc
87
+
88
+ content = response.choices[0].message.content
89
+ if not content:
90
+ raise RuntimeError("OpenAI returned an empty explanation.")
91
+ return content.strip()