coda-node 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
coda_node/server/ir.py ADDED
@@ -0,0 +1,182 @@
1
+ """Native-gate intermediate representation (IR) for quantum circuits.
2
+
3
+ Jobs received from the Coda cloud carry a JSON-serialized circuit in
4
+ this IR format. The IR is target-aware: each hardware target (e.g.
5
+ ``cz``, ``cnot``, ``pcz``) defines a legal gate set, and
6
+ the validators reject programs that use gates outside that set or
7
+ reference qubits beyond the device's capacity.
8
+
9
+ Schema (version ``"1.0"``)::
10
+
11
+ {
12
+ "version": "1.0",
13
+ "target": "cz",
14
+ "num_qubits": 5,
15
+ "gates": [{"gate": "rx", "qubits": [0], "params": [1.57]}],
16
+ "measurements": [0, 1],
17
+ "metadata": {
18
+ "source_hash": "abc123",
19
+ "compiled_at": "2026-01-01T00:00:00Z"
20
+ }
21
+ }
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from enum import StrEnum
27
+ from typing import Literal
28
+
29
+ from pydantic import BaseModel, Field, field_validator, model_validator
30
+
31
+ __all__ = [
32
+ "GATE_SPECS",
33
+ "LEGAL_GATES",
34
+ "GateOp",
35
+ "IRMetadata",
36
+ "NativeGate",
37
+ "NativeGateIR",
38
+ ]
39
+
40
+
41
+ class NativeGate(StrEnum):
42
+ """Supported native gate identifiers across all hardware targets."""
43
+
44
+ RX = "rx"
45
+ RY = "ry"
46
+ RZ = "rz"
47
+ CZ = "cz"
48
+ ISWAP = "iswap"
49
+ CP = "cp"
50
+ ID = "id"
51
+ X90 = "x90"
52
+ Y_MINUS_90 = "y_minus_90"
53
+ VIRTUAL_Z = "virtual_z"
54
+ CNOT = "cnot"
55
+
56
+
57
+ GATE_SPECS: dict[str, dict[str, int]] = {
58
+ "rx": {"qubits": 1, "params": 1},
59
+ "ry": {"qubits": 1, "params": 1},
60
+ "rz": {"qubits": 1, "params": 1},
61
+ "cz": {"qubits": 2, "params": 0},
62
+ "iswap": {"qubits": 2, "params": 0},
63
+ "cp": {"qubits": 2, "params": 1},
64
+ "id": {"qubits": 1, "params": 1},
65
+ "x90": {"qubits": 1, "params": 0},
66
+ "y_minus_90": {"qubits": 1, "params": 0},
67
+ "virtual_z": {"qubits": 1, "params": 1},
68
+ "cnot": {"qubits": 2, "params": 0},
69
+ }
70
+
71
+ LEGAL_GATES: dict[str, set[str]] = {
72
+ "cz": {"rx", "ry", "rz", "cz", "id"},
73
+ "iswap": {"rx", "ry", "rz", "iswap", "id"},
74
+ "pcz": {"rx", "ry", "rz", "cp", "id"},
75
+ "cnot": {
76
+ "rx",
77
+ "ry",
78
+ "rz",
79
+ "x90",
80
+ "y_minus_90",
81
+ "virtual_z",
82
+ "cnot",
83
+ "id",
84
+ },
85
+ }
86
+
87
+
88
+ class GateOp(BaseModel):
89
+ """A single gate operation: gate name, target qubits, and parameters."""
90
+
91
+ gate: NativeGate
92
+ qubits: list[int]
93
+ params: list[float] = Field(default_factory=list)
94
+
95
+ @model_validator(mode="after")
96
+ def validate_gate_shape(self) -> GateOp:
97
+ """Ensure qubit and parameter counts match the gate specification."""
98
+ gate = self.gate.value
99
+ if gate in GATE_SPECS:
100
+ expected_qubits = GATE_SPECS[gate]["qubits"]
101
+ if len(self.qubits) != expected_qubits:
102
+ raise ValueError(
103
+ f"Gate {gate} requires {expected_qubits} qubit(s), got {len(self.qubits)}"
104
+ )
105
+
106
+ expected_params = GATE_SPECS[gate]["params"]
107
+ if len(self.params) != expected_params:
108
+ raise ValueError(
109
+ f"Gate {gate} requires {expected_params} param(s), got {len(self.params)}"
110
+ )
111
+ return self
112
+
113
+
114
+ class IRMetadata(BaseModel):
115
+ """Provenance metadata attached to a compiled circuit."""
116
+
117
+ source_hash: str
118
+ compiled_at: str
119
+ compiler_version: str = "0.1.0"
120
+ optimization_level: int = Field(ge=0, le=3, default=2)
121
+
122
+
123
+ class NativeGateIR(BaseModel):
124
+ """A fully validated native-gate circuit ready for execution.
125
+
126
+ Validation enforces that every gate is legal for the declared target,
127
+ qubit indices are within ``[0, num_qubits)``, and each gate has the
128
+ correct number of qubit operands and parameters.
129
+ """
130
+
131
+ version: Literal["1.0"] = "1.0"
132
+ target: str
133
+ num_qubits: int = Field(ge=1, le=50)
134
+ gates: list[GateOp]
135
+ measurements: list[int]
136
+ metadata: IRMetadata
137
+
138
+ @field_validator("target")
139
+ @classmethod
140
+ def validate_target(cls, value: str) -> str:
141
+ """Reject targets not present in :data:`LEGAL_GATES`."""
142
+ if value not in LEGAL_GATES:
143
+ raise ValueError(
144
+ f"Unknown target '{value}'. Valid: {list(LEGAL_GATES.keys())}"
145
+ )
146
+ return value
147
+
148
+ @model_validator(mode="after")
149
+ def validate_consistency(self) -> NativeGateIR:
150
+ """Validate gate legality and qubit indices against the target."""
151
+ legal = LEGAL_GATES[self.target]
152
+ for index, op in enumerate(self.gates):
153
+ if op.gate.value not in legal:
154
+ raise ValueError(
155
+ f"Gate '{op.gate.value}' not legal for target '{self.target}' at index {index}"
156
+ )
157
+ for qubit in op.qubits:
158
+ if qubit < 0 or qubit >= self.num_qubits:
159
+ raise ValueError(
160
+ f"Qubit {qubit} out of range [0, {self.num_qubits}) at gate index {index}"
161
+ )
162
+
163
+ for qubit in self.measurements:
164
+ if qubit < 0 or qubit >= self.num_qubits:
165
+ raise ValueError(
166
+ f"Measurement qubit {qubit} out of range [0, {self.num_qubits})"
167
+ )
168
+ return self
169
+
170
+ @classmethod
171
+ def from_json(cls, json_str: str) -> NativeGateIR:
172
+ """Deserialize and validate a JSON-encoded IR program.
173
+
174
+ Raises:
175
+ pydantic.ValidationError: If the JSON is malformed or the
176
+ circuit violates gate-set or qubit-range constraints.
177
+ """
178
+ return cls.model_validate_json(json_str)
179
+
180
+ def to_json(self) -> str:
181
+ """Serialize the IR to a pretty-printed JSON string."""
182
+ return self.model_dump_json(indent=2)
@@ -0,0 +1,377 @@
1
+ """OpenQASM 3.0 <-> NativeGateIR round-trip helpers.
2
+
3
+ These helpers support a deliberately small OpenQASM subset so tests can
4
+ round-trip circuits through text and keep validating that ``NativeGateIR``
5
+ remains a valid, stable IR representation going forward. They are not meant
6
+ to define a broader OpenQASM compatibility surface.
7
+
8
+ Supports the ``cz`` and ``cnot`` IR targets for the OpenQASM round-trip subset.
9
+ Only the QASM subset needed by these targets is handled; symbolic parameter
10
+ expressions (for example ``pi/2``) are not supported, so callers must use
11
+ numeric literals.
12
+
13
+ Known limitation: the ``id`` gate carries a duration parameter in the IR
14
+ (nanoseconds) that has no OpenQASM counterpart. ``ir_to_openqasm`` drops
15
+ the duration, and ``openqasm_to_ir`` fills it with ``0.0``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ import re
22
+ from datetime import UTC, datetime
23
+
24
+ from coda_node.server.ir import GateOp, IRMetadata, NativeGate, NativeGateIR
25
+
26
+
27
+ class QASMConversionError(Exception):
28
+ """Raised when an OpenQASM program cannot be converted."""
29
+
30
+
31
+ _HALF_PI = math.pi / 2
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Regex patterns for the supported QASM subset
35
+ # TODO: Replace regex parsing with a proper AST parser if the supported QASM
36
+ # subset grows (e.g. multi-register circuits, barrier instructions, or new gates).
37
+ # ---------------------------------------------------------------------------
38
+
39
+ _HEADER_RE = re.compile(r"OPENQASM\s+[\d.]+\s*;")
40
+ _INCLUDE_RE = re.compile(r'include\s+"[^"]+"\s*;')
41
+ _QUBIT_DECL_RE = re.compile(r"qubit\[(\d+)]\s+\w+\s*;")
42
+ _BIT_DECL_RE = re.compile(r"bit\[(\d+)]\s+\w+\s*;")
43
+ _MEASURE_RE = re.compile(r"\w+\[(\d+)]\s*=\s*measure\s+\w+\[(\d+)]\s*;")
44
+ _GATE_RE = re.compile(
45
+ r"(\w+)"
46
+ r"(?:\(([^)]*)\))?"
47
+ r"\s+"
48
+ r"((?:\w+\[\d+])(?:\s*,\s*\w+\[\d+])*)"
49
+ r"\s*;"
50
+ )
51
+ _QUBIT_REF_RE = re.compile(r"\w+\[(\d+)]")
52
+
53
+
54
+ def _format_float(value: float) -> str:
55
+ """Format a numeric parameter for stable QASM output.
56
+
57
+ Args:
58
+ value: Numeric value to serialize.
59
+
60
+ Returns:
61
+ String form with enough precision for round-trip comparisons.
62
+ """
63
+ return f"{value:.15g}"
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # OpenQASM → NativeGateIR
68
+ # ---------------------------------------------------------------------------
69
+
70
+
71
+ def openqasm_to_ir(
72
+ qasm: str,
73
+ *,
74
+ target: str,
75
+ metadata: IRMetadata | None = None,
76
+ ) -> NativeGateIR:
77
+ """Parse supported OpenQASM text into ``NativeGateIR``.
78
+
79
+ This parser exists primarily to support round-trip validation of the IR.
80
+ It accepts only the subset of OpenQASM needed to confirm that the current
81
+ ``NativeGateIR`` encoding can be serialized and reconstructed reliably.
82
+
83
+ Args:
84
+ qasm: OpenQASM 3.0 source text.
85
+ target: Native gate target to validate against.
86
+ metadata: Optional metadata to attach to the returned IR. When omitted,
87
+ placeholder metadata is generated.
88
+
89
+ Returns:
90
+ Parsed native-gate IR object.
91
+
92
+ Raises:
93
+ QASMConversionError: If the QASM text uses unsupported syntax, gates,
94
+ or targets, or if the resulting IR fails validation.
95
+ """
96
+ num_qubits: int | None = None
97
+ gates: list[GateOp] = []
98
+ measurements: list[int] = []
99
+
100
+ for raw in qasm.strip().splitlines():
101
+ line = raw.strip()
102
+ if (
103
+ not line
104
+ or line.startswith("//")
105
+ or _HEADER_RE.match(line)
106
+ or _INCLUDE_RE.match(line)
107
+ ):
108
+ continue
109
+ if _BIT_DECL_RE.match(line):
110
+ continue
111
+
112
+ m_q = _QUBIT_DECL_RE.match(line)
113
+ if m_q:
114
+ if num_qubits is not None:
115
+ raise QASMConversionError(
116
+ "Multiple qubit register declarations are not supported"
117
+ )
118
+ num_qubits = int(m_q.group(1))
119
+ continue
120
+
121
+ m_m = _MEASURE_RE.match(line)
122
+ if m_m:
123
+ measurements.append(int(m_m.group(2)))
124
+ continue
125
+
126
+ m_g = _GATE_RE.match(line)
127
+ if m_g:
128
+ gate_name = m_g.group(1)
129
+ params = (
130
+ [float(p.strip()) for p in m_g.group(2).split(",")]
131
+ if m_g.group(2)
132
+ else []
133
+ )
134
+ qubits = [int(q) for q in _QUBIT_REF_RE.findall(m_g.group(3))]
135
+ gates.append(_openqasm_gate_to_ir(gate_name, params, qubits, target))
136
+ continue
137
+
138
+ raise QASMConversionError(f"Unsupported QASM line: {line!r}")
139
+
140
+ if num_qubits is None:
141
+ raise QASMConversionError("No qubit register declaration found")
142
+
143
+ if metadata is None:
144
+ metadata = IRMetadata(
145
+ source_hash="sha256:from-qasm",
146
+ compiled_at=datetime.now(UTC).isoformat(),
147
+ )
148
+
149
+ try:
150
+ return NativeGateIR(
151
+ target=target,
152
+ num_qubits=num_qubits,
153
+ gates=gates,
154
+ measurements=measurements,
155
+ metadata=metadata,
156
+ )
157
+ except Exception as exc:
158
+ raise QASMConversionError(str(exc)) from exc
159
+
160
+
161
+ # ---------------------------------------------------------------------------
162
+ # NativeGateIR → OpenQASM
163
+ # ---------------------------------------------------------------------------
164
+
165
+
166
+ def ir_to_openqasm(ir: NativeGateIR) -> str:
167
+ """Serialize ``NativeGateIR`` into the supported OpenQASM subset.
168
+
169
+ The output is intentionally limited to the subset understood by
170
+ :func:`openqasm_to_ir` so tests can round-trip circuits through QASM and
171
+ keep treating ``NativeGateIR`` as the durable representation. The only
172
+ known lossy case is the ``id`` gate duration.
173
+
174
+ Args:
175
+ ir: Native-gate IR to serialize.
176
+
177
+ Returns:
178
+ OpenQASM 3.0 source text.
179
+
180
+ Raises:
181
+ QASMConversionError: If the IR contains a gate that has no supported
182
+ mapping for the selected target.
183
+ """
184
+ lines: list[str] = [
185
+ "OPENQASM 3.0;",
186
+ 'include "stdgates.inc";',
187
+ f"qubit[{ir.num_qubits}] q;",
188
+ ]
189
+ if ir.measurements:
190
+ lines.append(f"bit[{len(ir.measurements)}] c;")
191
+
192
+ for op in ir.gates:
193
+ lines.append(_ir_gate_to_openqasm(op, ir.target))
194
+
195
+ for i, qubit in enumerate(ir.measurements):
196
+ lines.append(f"c[{i}] = measure q[{qubit}];")
197
+
198
+ return "\n".join(lines) + "\n"
199
+
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # Internal gate mapping helpers
203
+ # ---------------------------------------------------------------------------
204
+
205
+
206
+ def _openqasm_gate_to_ir(
207
+ name: str, params: list[float], qubits: list[int], target: str
208
+ ) -> GateOp:
209
+ """Map a parsed OpenQASM gate into a target-specific IR operation.
210
+
211
+ Args:
212
+ name: Parsed OpenQASM gate name.
213
+ params: Parsed numeric gate parameters.
214
+ qubits: Parsed qubit indices referenced by the gate.
215
+ target: Native gate target that determines the mapping rules.
216
+
217
+ Returns:
218
+ Corresponding native-gate operation.
219
+
220
+ Raises:
221
+ QASMConversionError: If the target is unsupported.
222
+ """
223
+ if target == "cz":
224
+ return _openqasm_gate_to_ir_cz(name, params, qubits)
225
+ if target == "cnot":
226
+ return _openqasm_gate_to_ir_cnot(name, params, qubits)
227
+ raise QASMConversionError(f"Unsupported target: {target}")
228
+
229
+
230
+ def _openqasm_gate_to_ir_cz(
231
+ name: str, params: list[float], qubits: list[int]
232
+ ) -> GateOp:
233
+ """Map an OpenQASM gate into the ``cz`` gate set.
234
+
235
+ Args:
236
+ name: Parsed OpenQASM gate name.
237
+ params: Parsed numeric gate parameters.
238
+ qubits: Parsed qubit indices referenced by the gate.
239
+
240
+ Returns:
241
+ Corresponding native-gate operation.
242
+
243
+ Raises:
244
+ QASMConversionError: If the gate is not representable for this target.
245
+ """
246
+ _map: dict[str, tuple[NativeGate, bool]] = {
247
+ "rx": (NativeGate.RX, True),
248
+ "ry": (NativeGate.RY, True),
249
+ "rz": (NativeGate.RZ, True),
250
+ "cz": (NativeGate.CZ, False),
251
+ "id": (NativeGate.ID, False),
252
+ }
253
+ if name not in _map:
254
+ raise QASMConversionError(f"Gate '{name}' not supported for cz")
255
+ ir_gate, has_params = _map[name]
256
+ if ir_gate == NativeGate.ID:
257
+ return GateOp(gate=ir_gate, qubits=qubits, params=[0.0])
258
+ return GateOp(gate=ir_gate, qubits=qubits, params=params if has_params else [])
259
+
260
+
261
+ def _openqasm_gate_to_ir_cnot(
262
+ name: str, params: list[float], qubits: list[int]
263
+ ) -> GateOp:
264
+ """Map an OpenQASM gate into the ``cnot`` gate set.
265
+
266
+ Args:
267
+ name: Parsed QASM gate name.
268
+ params: Parsed numeric gate parameters.
269
+ qubits: Parsed qubit indices referenced by the gate.
270
+
271
+ Returns:
272
+ Corresponding native-gate operation.
273
+
274
+ Raises:
275
+ QASMConversionError: If the gate is not representable for this target.
276
+ """
277
+ _simple: dict[str, tuple[NativeGate, bool]] = {
278
+ "sx": (NativeGate.X90, False),
279
+ "rz": (NativeGate.VIRTUAL_Z, True),
280
+ "cx": (NativeGate.CNOT, False),
281
+ "id": (NativeGate.ID, False),
282
+ }
283
+ if name in _simple:
284
+ ir_gate, has_params = _simple[name]
285
+ if ir_gate == NativeGate.ID:
286
+ return GateOp(gate=ir_gate, qubits=qubits, params=[0.0])
287
+ return GateOp(gate=ir_gate, qubits=qubits, params=params if has_params else [])
288
+
289
+ if name == "ry" and len(params) == 1:
290
+ if math.isclose(params[0], -_HALF_PI, rel_tol=1e-12, abs_tol=1e-12):
291
+ return GateOp(gate=NativeGate.Y_MINUS_90, qubits=qubits, params=[])
292
+ raise QASMConversionError(
293
+ f"ry({params[0]}) not representable in cnot "
294
+ f"(only ry(-π/2) maps to y_minus_90)"
295
+ )
296
+
297
+ raise QASMConversionError(f"Gate '{name}' not supported for cnot")
298
+
299
+
300
+ def _ir_gate_to_openqasm(op: GateOp, target: str) -> str:
301
+ """Map an IR operation into target-specific OpenQASM text.
302
+
303
+ Args:
304
+ op: IR gate operation to serialize.
305
+ target: Native gate target that determines the mapping rules.
306
+
307
+ Returns:
308
+ Single OpenQASM instruction line.
309
+
310
+ Raises:
311
+ QASMConversionError: If the target is unsupported.
312
+ """
313
+ name = op.gate.value
314
+ qubits_str = ", ".join(f"q[{q}]" for q in op.qubits)
315
+
316
+ if target == "cz":
317
+ return _ir_gate_to_openqasm_cz(name, op.params, qubits_str)
318
+ if target == "cnot":
319
+ return _ir_gate_to_openqasm_cnot(name, op.params, qubits_str)
320
+ raise QASMConversionError(f"Unsupported target: {target}")
321
+
322
+
323
+ def _ir_gate_to_openqasm_cz(name: str, params: list[float], qubits_str: str) -> str:
324
+ """Serialize a ``cz`` IR gate into OpenQASM text.
325
+
326
+ Args:
327
+ name: Native gate name.
328
+ params: Gate parameters to emit.
329
+ qubits_str: Preformatted OpenQASM qubit operand list.
330
+
331
+ Returns:
332
+ Single OpenQASM instruction line.
333
+
334
+ Raises:
335
+ QASMConversionError: If the gate has no supported OpenQASM mapping.
336
+ """
337
+ _parameterized = {"rx", "ry", "rz"}
338
+ _parameterless = {"cz", "id"}
339
+ if name in _parameterized:
340
+ ps = ", ".join(_format_float(p) for p in params)
341
+ return f"{name}({ps}) {qubits_str};"
342
+ if name == "virtual_z":
343
+ if not params:
344
+ raise QASMConversionError(
345
+ "IR gate 'virtual_z' requires exactly one parameter"
346
+ )
347
+ return f"rz({_format_float(params[0])}) {qubits_str};"
348
+ if name in _parameterless:
349
+ return f"{name} {qubits_str};"
350
+ raise QASMConversionError(f"IR gate '{name}' has no OpenQASM mapping for cz")
351
+
352
+
353
+ def _ir_gate_to_openqasm_cnot(name: str, params: list[float], qubits_str: str) -> str:
354
+ """Serialize a ``cnot`` IR gate into OpenQASM text.
355
+
356
+ Args:
357
+ name: Native gate name.
358
+ params: Gate parameters to emit.
359
+ qubits_str: Preformatted OpenQASM qubit operand list.
360
+
361
+ Returns:
362
+ Single OpenQASM instruction line.
363
+
364
+ Raises:
365
+ QASMConversionError: If the gate has no supported OpenQASM mapping.
366
+ """
367
+ if name == "x90":
368
+ return f"sx {qubits_str};"
369
+ if name == "y_minus_90":
370
+ return f"ry({_format_float(-_HALF_PI)}) {qubits_str};"
371
+ if name == "virtual_z":
372
+ return f"rz({_format_float(params[0])}) {qubits_str};"
373
+ if name == "cnot":
374
+ return f"cx {qubits_str};"
375
+ if name == "id":
376
+ return f"id {qubits_str};"
377
+ raise QASMConversionError(f"IR gate '{name}' has no OpenQASM mapping for cnot")