qledger 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.
qledger/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """QLedger — The Universal Quantum Experiment Lifecycle Platform.
2
+
3
+ Execute circuits across Qiskit, Cirq, and PennyLane. Track noise.
4
+ Benchmark hardware. Version circuits. Persist everything.
5
+ """
6
+
7
+ from qledger.core.engine import QLedger
8
+ from qledger.schema.circuit import Instruction, Measurement, UniversalCircuit
9
+ from qledger.schema.noise import (
10
+ GateFidelity,
11
+ NoiseSnapshot,
12
+ QubitProperties,
13
+ ReadoutError,
14
+ )
15
+ from qledger.schema.result import ExecutionResult
16
+ from qledger.storage.database import DatabaseError, QLedgerStore
17
+
18
+ __all__ = [
19
+ "DatabaseError",
20
+ "ExecutionResult",
21
+ "GateFidelity",
22
+ "Instruction",
23
+ "Measurement",
24
+ "NoiseSnapshot",
25
+ "QLedger",
26
+ "QLedgerStore",
27
+ "QubitProperties",
28
+ "ReadoutError",
29
+ "UniversalCircuit",
30
+ ]
31
+
32
+ __version__ = "0.1.0"
@@ -0,0 +1,20 @@
1
+ """Cross-framework adapters for quantum circuit interoperability.
2
+
3
+ Each adapter implements the ``BaseAdapter`` interface, providing:
4
+ * Circuit conversion (native ↔ ``UniversalCircuit``)
5
+ * Circuit execution on the framework's backends
6
+ * Noise profile extraction from hardware calibration data
7
+
8
+ Adapters are loaded lazily — importing ``qledger`` does not require any
9
+ quantum framework to be installed. Only when you instantiate an adapter
10
+ does it import the corresponding library.
11
+ """
12
+
13
+ from .base import AdapterError, BaseAdapter
14
+ from .registry import AdapterRegistry
15
+
16
+ __all__ = [
17
+ "AdapterError",
18
+ "AdapterRegistry",
19
+ "BaseAdapter",
20
+ ]
@@ -0,0 +1,174 @@
1
+ """Abstract base class for all framework adapters.
2
+
3
+ Every adapter must implement this interface. The contract is deliberately
4
+ narrow so that adding support for a new framework (e.g. Amazon Braket,
5
+ Azure Quantum) requires minimal boilerplate.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any
12
+
13
+ from qledger.schema.circuit import UniversalCircuit
14
+ from qledger.schema.noise import NoiseSnapshot
15
+ from qledger.schema.result import ExecutionResult
16
+
17
+
18
+ class AdapterError(Exception):
19
+ """Raised when an adapter operation fails."""
20
+
21
+
22
+ class BaseAdapter(ABC):
23
+ """Interface that every framework adapter must implement.
24
+
25
+ Subclasses must define
26
+ ----------------------
27
+ * ``framework_name`` — identifier used in gate alias lookups and storage.
28
+ * ``from_native`` — convert a native circuit to ``UniversalCircuit``.
29
+ * ``to_native`` — convert a ``UniversalCircuit`` back to a native circuit.
30
+ * ``execute`` — run a circuit on a backend and return an ``ExecutionResult``.
31
+ * ``get_noise_snapshot`` — extract calibration data from a backend.
32
+ * ``list_backends`` — enumerate available backends.
33
+ """
34
+
35
+ @property
36
+ @abstractmethod
37
+ def framework_name(self) -> str:
38
+ """Short lowercase identifier (e.g. ``"qiskit"``, ``"cirq"``)."""
39
+ ...
40
+
41
+ @property
42
+ @abstractmethod
43
+ def framework_version(self) -> str:
44
+ """Installed version string of the framework."""
45
+ ...
46
+
47
+ # ------------------------------------------------------------------
48
+ # Circuit conversion
49
+ # ------------------------------------------------------------------
50
+
51
+ @abstractmethod
52
+ def from_native(self, circuit: Any, name: str = "") -> UniversalCircuit:
53
+ """Convert a native circuit object to ``UniversalCircuit``.
54
+
55
+ Parameters
56
+ ----------
57
+ circuit : Any
58
+ Framework-specific circuit object.
59
+ name : str
60
+ Optional label for the circuit.
61
+
62
+ Returns
63
+ -------
64
+ UniversalCircuit
65
+
66
+ Raises
67
+ ------
68
+ AdapterError
69
+ If the circuit contains gates that cannot be mapped.
70
+ """
71
+ ...
72
+
73
+ @abstractmethod
74
+ def to_native(self, circuit: UniversalCircuit) -> Any:
75
+ """Convert a ``UniversalCircuit`` to the framework's native type.
76
+
77
+ Parameters
78
+ ----------
79
+ circuit : UniversalCircuit
80
+
81
+ Returns
82
+ -------
83
+ Any
84
+ Native circuit object.
85
+
86
+ Raises
87
+ ------
88
+ AdapterError
89
+ If a gate in the universal circuit has no mapping in this framework.
90
+ """
91
+ ...
92
+
93
+ # ------------------------------------------------------------------
94
+ # Execution
95
+ # ------------------------------------------------------------------
96
+
97
+ @abstractmethod
98
+ def execute(
99
+ self,
100
+ circuit: UniversalCircuit,
101
+ backend: Any | None = None,
102
+ *,
103
+ shots: int = 1024,
104
+ seed_simulator: int | None = None,
105
+ optimization_level: int | None = None,
106
+ transpiler_seed: int | None = None,
107
+ save_statevector: bool = False,
108
+ save_memory: bool = False,
109
+ extra_run_options: dict[str, Any] | None = None,
110
+ ) -> ExecutionResult:
111
+ """Execute a universal circuit and return a structured result.
112
+
113
+ Parameters
114
+ ----------
115
+ circuit : UniversalCircuit
116
+ Circuit in the universal IR.
117
+ backend : Any, optional
118
+ Backend to execute on. If None, the adapter should provide
119
+ a sensible default (e.g. local simulator).
120
+ shots : int
121
+ Number of measurement shots.
122
+ seed_simulator : int, optional
123
+ Seed for the simulator's random number generator.
124
+ optimization_level : int, optional
125
+ Transpiler optimisation level.
126
+ transpiler_seed : int, optional
127
+ Seed for transpiler stochastic passes.
128
+ save_statevector : bool
129
+ Request statevector output (simulator-only).
130
+ save_memory : bool
131
+ Request per-shot measurement memory.
132
+ extra_run_options : dict, optional
133
+ Additional framework-specific options passed to the runner.
134
+
135
+ Returns
136
+ -------
137
+ ExecutionResult
138
+ """
139
+ ...
140
+
141
+ # ------------------------------------------------------------------
142
+ # Noise profiling
143
+ # ------------------------------------------------------------------
144
+
145
+ @abstractmethod
146
+ def get_noise_snapshot(self, backend: Any) -> NoiseSnapshot:
147
+ """Extract current calibration / noise data from a backend.
148
+
149
+ Parameters
150
+ ----------
151
+ backend : Any
152
+ A backend instance (real hardware or simulator with a noise model).
153
+
154
+ Returns
155
+ -------
156
+ NoiseSnapshot
157
+ """
158
+ ...
159
+
160
+ # ------------------------------------------------------------------
161
+ # Backend discovery
162
+ # ------------------------------------------------------------------
163
+
164
+ @abstractmethod
165
+ def list_backends(self, **filters: Any) -> list[dict[str, Any]]:
166
+ """Enumerate available backends.
167
+
168
+ Returns
169
+ -------
170
+ list[dict[str, Any]]
171
+ Each dict contains at least ``{"name": str, "num_qubits": int,
172
+ "simulator": bool}``.
173
+ """
174
+ ...
@@ -0,0 +1,368 @@
1
+ """Cirq adapter — converts between Google Cirq circuits and UniversalCircuit.
2
+
3
+ Supports cirq-core >= 1.3.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import time
10
+ from datetime import datetime, timezone
11
+ from typing import Any
12
+
13
+ from qledger.schema.circuit import Instruction, Measurement, UniversalCircuit
14
+ from qledger.schema.gates import StandardGates
15
+ from qledger.schema.noise import NoiseSnapshot
16
+ from qledger.schema.result import ExecutionResult
17
+
18
+ from .base import AdapterError, BaseAdapter
19
+ from .registry import AdapterRegistry
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Cirq gate class name → canonical name (None = handled via decomposition)
24
+ _CIRQ_TO_CANONICAL: dict[str, str | None] = {
25
+ "HPowGate": "h",
26
+ "XPowGate": "x",
27
+ "YPowGate": "y",
28
+ "ZPowGate": "z",
29
+ "CXPowGate": "cx",
30
+ "CNotPowGate": "cx",
31
+ "CZPowGate": "cz",
32
+ "SwapPowGate": "swap",
33
+ "ISwapPowGate": "iswap",
34
+ "CCXPowGate": "ccx",
35
+ "CCZPowGate": "ccx",
36
+ "MeasurementGate": "measure",
37
+ "_InverseCompositeGate": None, # handled via decomposition
38
+ }
39
+
40
+
41
+ @AdapterRegistry.register
42
+ class CirqAdapter(BaseAdapter):
43
+ """Adapter for Google Cirq."""
44
+
45
+ _FRAMEWORK_NAME = "cirq"
46
+
47
+ def __init__(self) -> None:
48
+ import cirq
49
+
50
+ self._cirq = cirq
51
+
52
+ @property
53
+ def framework_name(self) -> str:
54
+ return "cirq"
55
+
56
+ @property
57
+ def framework_version(self) -> str:
58
+ return str(self._cirq.__version__)
59
+
60
+ # ------------------------------------------------------------------
61
+ # Cirq → UniversalCircuit
62
+ # ------------------------------------------------------------------
63
+
64
+ def from_native(self, circuit: Any, name: str = "") -> UniversalCircuit:
65
+ cirq = self._cirq
66
+
67
+ if not isinstance(circuit, cirq.Circuit):
68
+ raise AdapterError(f"Expected cirq.Circuit, got {type(circuit).__name__}")
69
+
70
+ # Collect all qubits and assign contiguous indices
71
+ all_qubits = sorted(circuit.all_qubits())
72
+ qubit_map = {q: i for i, q in enumerate(all_qubits)}
73
+
74
+ instructions: list[Instruction] = []
75
+ measurements: list[Measurement] = []
76
+ clbit_counter = 0
77
+
78
+ for moment in circuit:
79
+ for op in moment:
80
+ gate = op.gate
81
+ qubit_indices = tuple(qubit_map[q] for q in op.qubits)
82
+
83
+ # Handle measurements
84
+ if isinstance(gate, cirq.MeasurementGate):
85
+ for qi in qubit_indices:
86
+ measurements.append(Measurement(qubit=qi, clbit=clbit_counter))
87
+ clbit_counter += 1
88
+ continue
89
+
90
+ # Resolve gate name
91
+ canonical = self._resolve_cirq_gate(gate)
92
+ params = self._extract_cirq_params(gate)
93
+
94
+ instructions.append(Instruction(
95
+ gate=canonical,
96
+ qubits=qubit_indices,
97
+ params=params,
98
+ ))
99
+
100
+ num_qubits = len(all_qubits)
101
+ num_clbits = clbit_counter if clbit_counter > 0 else num_qubits
102
+
103
+ return UniversalCircuit(
104
+ num_qubits=num_qubits,
105
+ num_clbits=num_clbits,
106
+ instructions=instructions,
107
+ measurements=measurements,
108
+ name=name,
109
+ metadata={"source_framework": "cirq", "cirq_version": self.framework_version},
110
+ )
111
+
112
+ def _resolve_cirq_gate(self, gate: Any) -> str:
113
+ """Map a Cirq gate to its canonical name."""
114
+ gate_type = type(gate).__name__
115
+
116
+ # Check our mapping table first
117
+ canonical = _CIRQ_TO_CANONICAL.get(gate_type)
118
+ if canonical is not None:
119
+ # For power gates, check if the exponent is 1.0 (standard gate)
120
+ if hasattr(gate, "exponent"):
121
+ exp = gate.exponent
122
+ if gate_type == "HPowGate" and exp == 1.0:
123
+ return "h"
124
+ elif gate_type == "XPowGate":
125
+ if exp == 1.0:
126
+ return "x"
127
+ elif exp == 0.5:
128
+ return "sx"
129
+ else:
130
+ return "rx" # treat as rotation
131
+ elif gate_type == "YPowGate":
132
+ return "y" if exp == 1.0 else "ry"
133
+ elif gate_type == "ZPowGate":
134
+ if exp == 1.0: # noqa: SIM116
135
+ return "z"
136
+ elif exp == 0.5:
137
+ return "s"
138
+ elif exp == 0.25:
139
+ return "t"
140
+ else:
141
+ return "rz"
142
+ elif gate_type == "CXPowGate" and exp == 1.0:
143
+ return "cx"
144
+ elif gate_type == "CZPowGate" and exp == 1.0:
145
+ return "cz"
146
+ elif gate_type == "SwapPowGate" and exp == 1.0:
147
+ return "swap"
148
+ return canonical
149
+
150
+ # Try string matching
151
+ gate_str = str(gate).upper()
152
+ resolved = StandardGates.resolve(gate_str)
153
+ if resolved is not None:
154
+ return resolved.canonical_name
155
+
156
+ # Fallback: use the type name in lowercase
157
+ logger.warning("Unknown Cirq gate %s — storing as-is.", gate_type)
158
+ return gate_type.lower()
159
+
160
+ def _extract_cirq_params(self, gate: Any) -> tuple[float, ...]:
161
+ """Extract continuous parameters from a Cirq gate."""
162
+ import math
163
+
164
+ if hasattr(gate, "exponent"):
165
+ exp = gate.exponent
166
+ if isinstance(exp, (int, float)):
167
+ # Cirq stores rotations as exponents of π
168
+ # RX(θ) in Qiskit = X^(θ/π) in Cirq
169
+ # Only return params for parametric gates
170
+ gate_type = type(gate).__name__
171
+ if (
172
+ gate_type in ("HPowGate", "CXPowGate", "CZPowGate", "SwapPowGate")
173
+ and exp == 1.0
174
+ ):
175
+ return () # standard gate, no params
176
+ if gate_type in ("XPowGate", "YPowGate", "ZPowGate"):
177
+ if exp in (0.0, 0.25, 0.5, 1.0):
178
+ return () # known fixed gates
179
+ return (float(exp) * math.pi,)
180
+ if "Pow" in gate_type:
181
+ return (float(exp) * math.pi,)
182
+ if hasattr(gate, "rads"):
183
+ return (float(gate.rads),)
184
+ return ()
185
+
186
+ # ------------------------------------------------------------------
187
+ # UniversalCircuit → Cirq
188
+ # ------------------------------------------------------------------
189
+
190
+ def to_native(self, circuit: UniversalCircuit) -> Any:
191
+ cirq = self._cirq
192
+ import math
193
+
194
+ qubits = cirq.LineQubit.range(circuit.num_qubits)
195
+
196
+ ops: list[Any] = []
197
+
198
+ gate_map: dict[str, Any] = {
199
+ "id": cirq.I,
200
+ "h": cirq.H,
201
+ "x": cirq.X,
202
+ "y": cirq.Y,
203
+ "z": cirq.Z,
204
+ "s": cirq.S,
205
+ "t": cirq.T,
206
+ "cx": cirq.CNOT,
207
+ "cz": cirq.CZ,
208
+ "swap": cirq.SWAP,
209
+ "iswap": cirq.ISWAP,
210
+ "ccx": cirq.CCX,
211
+ }
212
+
213
+ for inst in circuit.instructions:
214
+ target_qubits = [qubits[q] for q in inst.qubits]
215
+
216
+ if inst.gate in gate_map:
217
+ ops.append(gate_map[inst.gate].on(*target_qubits))
218
+ elif inst.gate == "rx" and inst.params:
219
+ ops.append(cirq.rx(inst.params[0]).on(*target_qubits))
220
+ elif inst.gate == "ry" and inst.params:
221
+ ops.append(cirq.ry(inst.params[0]).on(*target_qubits))
222
+ elif inst.gate == "rz" and inst.params:
223
+ ops.append(cirq.rz(inst.params[0]).on(*target_qubits))
224
+ elif inst.gate == "sx":
225
+ ops.append((cirq.X ** 0.5).on(*target_qubits))
226
+ elif inst.gate == "sdg":
227
+ ops.append((cirq.S ** -1).on(*target_qubits))
228
+ elif inst.gate == "tdg":
229
+ ops.append((cirq.T ** -1).on(*target_qubits))
230
+ elif inst.gate == "p" and inst.params:
231
+ ops.append(cirq.ZPowGate(exponent=inst.params[0] / math.pi).on(*target_qubits))
232
+ elif inst.gate == "cp" and inst.params:
233
+ ops.append(cirq.CZPowGate(exponent=inst.params[0] / math.pi).on(*target_qubits))
234
+ else:
235
+ raise AdapterError(
236
+ f"Gate {inst.gate!r} has no Cirq mapping."
237
+ )
238
+
239
+ # Add measurements
240
+ if circuit.measurements:
241
+ measured_qubits = [qubits[m.qubit] for m in circuit.measurements]
242
+ ops.append(cirq.measure(*measured_qubits, key="result"))
243
+
244
+ return cirq.Circuit(ops)
245
+
246
+ # ------------------------------------------------------------------
247
+ # Execution
248
+ # ------------------------------------------------------------------
249
+
250
+ def execute(
251
+ self,
252
+ circuit: UniversalCircuit,
253
+ backend: Any | None = None,
254
+ *,
255
+ shots: int = 1024,
256
+ seed_simulator: int | None = None,
257
+ optimization_level: int | None = None,
258
+ transpiler_seed: int | None = None,
259
+ save_statevector: bool = False,
260
+ save_memory: bool = False,
261
+ extra_run_options: dict[str, Any] | None = None,
262
+ ) -> ExecutionResult:
263
+ cirq = self._cirq
264
+
265
+ native_circuit = self.to_native(circuit)
266
+
267
+ # Resolve simulator
268
+ if backend is None:
269
+ backend = cirq.Simulator(seed=seed_simulator)
270
+ backend_name = type(backend).__name__
271
+
272
+ t0 = time.perf_counter()
273
+ try:
274
+ if save_statevector and not circuit.measurements:
275
+ sim_result = backend.simulate(native_circuit)
276
+ elapsed_ms = (time.perf_counter() - t0) * 1000.0
277
+ sv = list(sim_result.final_state_vector)
278
+ return ExecutionResult(
279
+ counts={},
280
+ shots=0,
281
+ backend_name=backend_name,
282
+ success=True,
283
+ statevector=sv,
284
+ execution_time_ms=elapsed_ms,
285
+ seed_simulator=seed_simulator,
286
+ )
287
+
288
+ result = backend.run(native_circuit, repetitions=shots)
289
+ elapsed_ms = (time.perf_counter() - t0) * 1000.0
290
+
291
+ # Extract counts from Cirq result
292
+ counts: dict[str, int] = {}
293
+ hist = result.histogram(key="result")
294
+ num_bits = circuit.num_clbits or circuit.num_qubits
295
+ for val, count in hist.items():
296
+ bitstring = format(val, f"0{num_bits}b")
297
+ counts[bitstring] = count
298
+
299
+ memory_data = None
300
+ if save_memory:
301
+ measurements_array = result.measurements.get("result")
302
+ if measurements_array is not None:
303
+ memory_data = [
304
+ "".join(str(b) for b in row) for row in measurements_array
305
+ ]
306
+
307
+ return ExecutionResult(
308
+ counts=counts,
309
+ shots=shots,
310
+ backend_name=backend_name,
311
+ success=True,
312
+ memory=memory_data,
313
+ execution_time_ms=elapsed_ms,
314
+ seed_simulator=seed_simulator,
315
+ )
316
+
317
+ except Exception as exc:
318
+ elapsed_ms = (time.perf_counter() - t0) * 1000.0
319
+ logger.error("Cirq execution failed: %s", exc)
320
+ return ExecutionResult(
321
+ counts={},
322
+ shots=shots,
323
+ backend_name=backend_name,
324
+ success=False,
325
+ error_message=str(exc),
326
+ execution_time_ms=elapsed_ms,
327
+ seed_simulator=seed_simulator,
328
+ )
329
+
330
+ # ------------------------------------------------------------------
331
+ # Noise profiling
332
+ # ------------------------------------------------------------------
333
+
334
+ def get_noise_snapshot(self, backend: Any) -> NoiseSnapshot:
335
+ backend_name = type(backend).__name__
336
+
337
+ snapshot = NoiseSnapshot(
338
+ backend_name=backend_name,
339
+ timestamp=datetime.now(timezone.utc),
340
+ )
341
+
342
+ # Cirq noise models expose properties differently depending on device
343
+ if hasattr(backend, "metadata"):
344
+ meta = backend.metadata
345
+ if hasattr(meta, "qubit_set"):
346
+ snapshot.num_qubits = len(meta.qubit_set)
347
+
348
+ return snapshot
349
+
350
+ # ------------------------------------------------------------------
351
+ # Backend discovery
352
+ # ------------------------------------------------------------------
353
+
354
+ def list_backends(self, **filters: Any) -> list[dict[str, Any]]:
355
+ return [
356
+ {
357
+ "name": "Simulator",
358
+ "num_qubits": 30,
359
+ "simulator": True,
360
+ "framework": "cirq",
361
+ },
362
+ {
363
+ "name": "DensityMatrixSimulator",
364
+ "num_qubits": 20,
365
+ "simulator": True,
366
+ "framework": "cirq",
367
+ },
368
+ ]