hilbertbench 1.0.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.
Files changed (36) hide show
  1. hilbertbench/__init__.py +78 -0
  2. hilbertbench/active/__init__.py +38 -0
  3. hilbertbench/active/probe.py +408 -0
  4. hilbertbench/analysis/__init__.py +105 -0
  5. hilbertbench/analysis/_util.py +129 -0
  6. hilbertbench/analysis/circuit.py +293 -0
  7. hilbertbench/analysis/expressibility.py +292 -0
  8. hilbertbench/analysis/measurement.py +270 -0
  9. hilbertbench/analysis/noise.py +312 -0
  10. hilbertbench/analysis/optimization.py +257 -0
  11. hilbertbench/analysis/trainability.py +157 -0
  12. hilbertbench/integrations/__init__.py +0 -0
  13. hilbertbench/integrations/pennylane.py +560 -0
  14. hilbertbench/integrations/qiskit.py +1177 -0
  15. hilbertbench/models/__init__.py +42 -0
  16. hilbertbench/models/v1_0/__init__.py +4 -0
  17. hilbertbench/models/v1_0/artifact.py +79 -0
  18. hilbertbench/models/v1_0/catalog.py +33 -0
  19. hilbertbench/models/v1_0/span.py +126 -0
  20. hilbertbench/models/v1_0/trace.py +92 -0
  21. hilbertbench/py.typed +0 -0
  22. hilbertbench/reader/__init__.py +0 -0
  23. hilbertbench/reader/verify.py +453 -0
  24. hilbertbench/recorder/__init__.py +26 -0
  25. hilbertbench/recorder/storage/__init__.py +0 -0
  26. hilbertbench/recorder/storage/writer.py +267 -0
  27. hilbertbench/recorder/tape.py +848 -0
  28. hilbertbench/trace/__init__.py +29 -0
  29. hilbertbench/trace/span.py +412 -0
  30. hilbertbench/trace/trace.py +738 -0
  31. hilbertbench-1.0.0.dist-info/METADATA +208 -0
  32. hilbertbench-1.0.0.dist-info/RECORD +36 -0
  33. hilbertbench-1.0.0.dist-info/WHEEL +5 -0
  34. hilbertbench-1.0.0.dist-info/entry_points.txt +3 -0
  35. hilbertbench-1.0.0.dist-info/licenses/LICENSE +21 -0
  36. hilbertbench-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # file: hilbertbench/__init__.py
4
+ #
5
+ # revision history:
6
+ # 20260604 (am): cleaned up to project coding standards
7
+ #
8
+ # HilbertBench — non-intrusive diagnostic framework for quantum machine
9
+ # learning. The recorder, reader, and models import only the standard
10
+ # library. The analysis layer is exposed lazily so that recorder-only
11
+ # users never pay for numpy/pandas imports.
12
+ #
13
+ # from hilbertbench import HilbertTrace
14
+ # trace = HilbertTrace("runs/20260605_xxx")
15
+ #
16
+ # Source comments reference architectural invariants as INV-NNN (e.g.
17
+ # INV-001). These are the framework's non-negotiable guarantees; the
18
+ # canonical list lives in docs/reference/invariants.md.
19
+ #------------------------------------------------------------------------------
20
+
21
+ # import system modules
22
+ #
23
+ from typing import Any
24
+
25
+ #------------------------------------------------------------------------------
26
+ #
27
+ # global variables are listed here
28
+ #
29
+ #------------------------------------------------------------------------------
30
+
31
+ # define the public API
32
+ #
33
+ __all__ = ["HilbertTrace", "SpanView"]
34
+
35
+ #------------------------------------------------------------------------------
36
+ #
37
+ # functions are listed here
38
+ #
39
+ #------------------------------------------------------------------------------
40
+
41
+ def __getattr__(name: str) -> Any:
42
+ """
43
+ function: __getattr__
44
+
45
+ arguments:
46
+ name: the attribute name being accessed
47
+
48
+ return:
49
+ the requested attribute object
50
+
51
+ description:
52
+ PEP 562 lazy attribute access — keeps `import hilbertbench`
53
+ lightweight. The analysis layer (numpy/pandas) is only imported
54
+ when explicitly requested by the caller.
55
+ """
56
+
57
+ # resolve HilbertTrace lazily
58
+ #
59
+ if name == "HilbertTrace":
60
+ from hilbertbench.trace import HilbertTrace
61
+ return HilbertTrace
62
+
63
+ # resolve SpanView lazily
64
+ #
65
+ if name == "SpanView":
66
+ from hilbertbench.trace import SpanView
67
+ return SpanView
68
+
69
+ # exit ungracefully — unknown attribute
70
+ #
71
+ raise AttributeError(
72
+ f"module 'hilbertbench' has no attribute {name!r}"
73
+ )
74
+ #
75
+ # end of function
76
+
77
+ #
78
+ # end of file
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # file: hilbertbench/active/__init__.py
4
+ #
5
+ # revision history:
6
+ # 20260604 (am): cleaned up to project coding standards
7
+ #
8
+ # hilbertbench.active — Active Mode: controlled, explicitly-authorized
9
+ # sampling for diagnostics (e.g. expressibility) that passive observation
10
+ # cannot provide.
11
+ #
12
+ # from hilbertbench.active import active_probe_qiskit
13
+ #------------------------------------------------------------------------------
14
+
15
+ # import active mode components
16
+ #
17
+ from hilbertbench.active.probe import (
18
+ active_probe_pennylane,
19
+ active_probe_qiskit,
20
+ probe_expressibility,
21
+ )
22
+
23
+ #------------------------------------------------------------------------------
24
+ #
25
+ # global variables are listed here
26
+ #
27
+ #------------------------------------------------------------------------------
28
+
29
+ # define the public API
30
+ #
31
+ __all__ = [
32
+ "probe_expressibility",
33
+ "active_probe_qiskit",
34
+ "active_probe_pennylane",
35
+ ]
36
+
37
+ #
38
+ # end of file
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # file: hilbertbench/active/probe.py
4
+ #
5
+ # revision history:
6
+ # 20260604 (am): cleaned up to project coding standards
7
+ #
8
+ # Active Mode — controlled, explicitly-authorized circuit sampling.
9
+ #
10
+ # Passive recording observes whatever circuits an optimizer chooses to
11
+ # run. That is the right data for trainability but cannot measure
12
+ # expressibility: to compare an ansatz against the Haar measure you
13
+ # need output states under parameters drawn uniformly at random over
14
+ # the full parameter space, which a training trajectory never provides.
15
+ #
16
+ # Active Mode does exactly that: given a parameterized circuit and a
17
+ # way to obtain its statevector, it draws num_samples random parameter
18
+ # vectors, runs each, and records the resulting statevectors into a
19
+ # mode="active" trace. Feed that trace to kl_expressibility.
20
+ #
21
+ # from hilbertbench.active import active_probe_qiskit
22
+ # run_dir = active_probe_qiskit(
23
+ # ansatz, num_samples=1000, output_root="runs"
24
+ # )
25
+ #
26
+ # This is an explicit user action that runs new circuits — never
27
+ # invoked automatically. Honoring INV-001 (the passive recorder never
28
+ # re-executes circuits).
29
+ #------------------------------------------------------------------------------
30
+
31
+ # future imports must come first
32
+ #
33
+ from __future__ import annotations
34
+
35
+ # import system modules
36
+ #
37
+ import json
38
+ import math
39
+ import os
40
+ import tempfile
41
+ from pathlib import Path
42
+ from typing import Any, Callable, Optional
43
+
44
+ # import third-party modules
45
+ #
46
+ import numpy as np
47
+
48
+ # import hilbertbench modules
49
+ #
50
+ from hilbertbench.models import Encoding, Kind, Mode
51
+ from hilbertbench.recorder.tape import HilbertTape
52
+
53
+ #------------------------------------------------------------------------------
54
+ #
55
+ # global variables are listed here
56
+ #
57
+ #------------------------------------------------------------------------------
58
+
59
+ # set the filename using basename
60
+ #
61
+ __FILE__ = os.path.basename(__file__)
62
+
63
+ # statevectors at or below this serialized size are embedded inline;
64
+ # larger ones (deep circuits / many qubits) spill to a .npy file
65
+ #
66
+ _INLINE_BYTES = 65_536
67
+
68
+ #------------------------------------------------------------------------------
69
+ #
70
+ # functions are listed here
71
+ #
72
+ #------------------------------------------------------------------------------
73
+
74
+ def _serialize_statevector(psi: np.ndarray) -> str:
75
+ """
76
+ function: _serialize_statevector
77
+
78
+ arguments:
79
+ psi: a complex numpy array (the statevector)
80
+
81
+ return:
82
+ a JSON string in [[re, im], ...] form
83
+
84
+ description:
85
+ Serializes a complex statevector to JSON component-by-component.
86
+ The [[re, im], ...] form round-trips exactly through
87
+ _to_statevector in the expressibility analyzer.
88
+ """
89
+
90
+ # flatten and serialize real and imaginary parts component-wise
91
+ #
92
+ flat = np.asarray(psi).ravel()
93
+
94
+ # exit gracefully
95
+ #
96
+ return json.dumps([[float(x.real), float(x.imag)] for x in flat])
97
+ #
98
+ # end of function
99
+
100
+
101
+ def probe_expressibility(
102
+ state_fn: Callable[[np.ndarray], np.ndarray],
103
+ num_params: int,
104
+ num_samples: int,
105
+ output_root: Path | str,
106
+ *,
107
+ circuit_qasm: Optional[str] = None,
108
+ param_low: float = 0.0,
109
+ param_high: float = 2.0 * math.pi,
110
+ seed: Optional[int] = None,
111
+ tags: Optional[dict] = None,
112
+ backend_id: str = "active_probe",
113
+ ) -> Path:
114
+ """
115
+ function: probe_expressibility
116
+
117
+ arguments:
118
+ state_fn callable (theta: np.ndarray) -> complex statevector;
119
+ must return a 1-D complex array
120
+ num_params dimension of the parameter vector to sample
121
+ num_samples number of random parameter draws
122
+ output_root directory under which the run directory is created
123
+ circuit_qasm optional QASM string stored once as a circuit_qasm
124
+ artifact (deduplicates across all steps)
125
+ param_low lower bound of uniform parameter sampling (default 0)
126
+ param_high upper bound of uniform parameter sampling
127
+ (default 2π)
128
+ seed optional RNG seed for reproducibility
129
+ tags additional trace tags merged into run tags
130
+ backend_id backend label written into span records
131
+
132
+ return:
133
+ path to the created run directory
134
+
135
+ description:
136
+ Records an Active Mode expressibility trace. Draws num_samples
137
+ random parameter vectors uniformly from [param_low, param_high],
138
+ evaluates state_fn on each, and records the statevector inline
139
+ (or in the file store for large states). One span per sample,
140
+ each with an ACTIVE_SAMPLE event carrying the sample index.
141
+ """
142
+
143
+ # initialise the RNG and build merged trace tags
144
+ #
145
+ rng = np.random.default_rng(seed)
146
+ run_tags = {"probe": "expressibility"}
147
+ if tags:
148
+ run_tags.update(tags)
149
+
150
+ with HilbertTape(output_root, mode=Mode.active, tags=run_tags) as tape:
151
+
152
+ # store the ansatz circuit once; fall back to a generic blob
153
+ # descriptor when no QASM is available
154
+ #
155
+ if circuit_qasm is not None:
156
+ with tempfile.NamedTemporaryFile(
157
+ delete=False,
158
+ mode="w",
159
+ suffix=".qasm",
160
+ encoding="utf-8",
161
+ ) as f:
162
+ f.write(circuit_qasm)
163
+ tmp = f.name
164
+ payload_ref = tape.attach_artifact(
165
+ src_path=tmp,
166
+ kind=Kind.circuit_qasm,
167
+ encoding=Encoding.openqasm,
168
+ producer="active_probe",
169
+ )
170
+ os.remove(tmp)
171
+ else:
172
+ descriptor = f"active_probe: num_params={num_params}"
173
+ with tempfile.NamedTemporaryFile(
174
+ delete=False,
175
+ mode="w",
176
+ suffix=".txt",
177
+ encoding="utf-8",
178
+ ) as f:
179
+ f.write(descriptor)
180
+ tmp = f.name
181
+ payload_ref = tape.attach_artifact(
182
+ src_path=tmp,
183
+ kind=Kind.generic_blob,
184
+ encoding=Encoding.plaintext,
185
+ producer="active_probe",
186
+ )
187
+ os.remove(tmp)
188
+
189
+ # draw samples and record one span per sample
190
+ #
191
+ for i in range(num_samples):
192
+ theta = rng.uniform(param_low, param_high, size=num_params)
193
+ psi = np.asarray(state_fn(theta)).ravel()
194
+
195
+ with tape.execution_span(
196
+ payload_ref=payload_ref,
197
+ backend_id=backend_id,
198
+ ) as span:
199
+
200
+ # attach the parameter vector as an inline artifact
201
+ #
202
+ span.attach_inline(
203
+ json.dumps(theta.tolist()),
204
+ kind="parameters",
205
+ encoding="json",
206
+ producer="active_probe",
207
+ )
208
+
209
+ # attach statevector inline; spill to .npy when large
210
+ #
211
+ ser = _serialize_statevector(psi)
212
+ if len(ser.encode()) <= _INLINE_BYTES:
213
+ span.outcome_ref = span.attach_inline(
214
+ ser,
215
+ kind="execution_outcome",
216
+ encoding="json",
217
+ producer="active_probe",
218
+ )
219
+ else:
220
+ with tempfile.NamedTemporaryFile(
221
+ delete=False, suffix=".npy"
222
+ ) as fn:
223
+ np.save(fn, psi)
224
+ npy_path = Path(fn.name)
225
+ try:
226
+ span.outcome_ref = tape.attach_artifact(
227
+ src_path=npy_path,
228
+ kind=Kind.execution_outcome,
229
+ encoding=Encoding.numpy_binary,
230
+ producer="active_probe",
231
+ )
232
+ finally:
233
+ npy_path.unlink(missing_ok=True)
234
+
235
+ # emit the per-sample diagnostic event
236
+ #
237
+ span.add_event(
238
+ "ACTIVE_SAMPLE", {"sample_index": i}
239
+ )
240
+
241
+ # exit gracefully
242
+ #
243
+ return tape.dir_path
244
+ #
245
+ # end of function
246
+
247
+
248
+ def active_probe_qiskit(
249
+ circuit: Any,
250
+ num_samples: int,
251
+ output_root: Path | str,
252
+ *,
253
+ seed: Optional[int] = None,
254
+ tags: Optional[dict] = None,
255
+ ) -> Path:
256
+ """
257
+ function: active_probe_qiskit
258
+
259
+ arguments:
260
+ circuit: a parameterized Qiskit QuantumCircuit
261
+ num_samples: number of random parameter draws
262
+ output_root: directory under which the run directory is created
263
+ seed: optional RNG seed for reproducibility
264
+ tags: additional trace tags
265
+
266
+ return:
267
+ path to the created run directory
268
+
269
+ description:
270
+ Active Mode probe for a parameterized Qiskit circuit. Uses exact
271
+ statevector simulation (no shots) — expressibility is a property
272
+ of the state, not of sampling noise.
273
+
274
+ The circuit is decomposed before QASM serialization so that
275
+ library ansätze (e.g. RealAmplitudes) expose their underlying
276
+ gates rather than an opaque compound gate. Falls back to the
277
+ un-decomposed form, then to no QASM, on any failure.
278
+ """
279
+
280
+ # import Qiskit modules locally to respect INV-004
281
+ #
282
+ from qiskit import qasm3
283
+ from qiskit.quantum_info import Statevector
284
+
285
+ # build a state function that binds parameters and returns data
286
+ #
287
+ params = list(circuit.parameters)
288
+
289
+ def state_fn(theta: np.ndarray) -> np.ndarray:
290
+ """Bind theta into the circuit and return its statevector data."""
291
+ bound = circuit.assign_parameters(dict(zip(params, theta)))
292
+ return np.asarray(Statevector(bound).data)
293
+
294
+ # serialize the decomposed circuit to QASM; fall back gracefully
295
+ #
296
+ try:
297
+ circuit_qasm = qasm3.dumps(circuit.decompose())
298
+ except Exception:
299
+ try:
300
+ circuit_qasm = qasm3.dumps(circuit)
301
+ except Exception:
302
+ circuit_qasm = None
303
+
304
+ # exit gracefully
305
+ #
306
+ return probe_expressibility(
307
+ state_fn,
308
+ num_params=len(params),
309
+ num_samples=num_samples,
310
+ output_root=output_root,
311
+ circuit_qasm=circuit_qasm,
312
+ seed=seed,
313
+ tags={"framework": "qiskit", **(tags or {})},
314
+ )
315
+ #
316
+ # end of function
317
+
318
+
319
+ def active_probe_pennylane(
320
+ circuit_fn: Callable,
321
+ num_qubits: int,
322
+ num_params: int,
323
+ num_samples: int,
324
+ output_root: Path | str,
325
+ *,
326
+ seed: Optional[int] = None,
327
+ tags: Optional[dict] = None,
328
+ ) -> Path:
329
+ """
330
+ function: active_probe_pennylane
331
+
332
+ arguments:
333
+ circuit_fn: a function (theta) -> applies gates; the wrapper
334
+ appends qml.state() and runs on default.qubit
335
+ num_qubits: wire count
336
+ num_params: parameter-vector dimension
337
+ num_samples: number of random parameter draws
338
+ output_root: directory under which the run directory is created
339
+ seed: optional RNG seed for reproducibility
340
+ tags: additional trace tags
341
+
342
+ return:
343
+ path to the created run directory
344
+
345
+ description:
346
+ Active Mode probe for a PennyLane ansatz. Wraps circuit_fn with
347
+ a default.qubit qml.state() qnode to obtain exact statevectors.
348
+ Generates the QASM template once at the zero-parameter point and
349
+ stores it as a circuit_qasm artifact for the file store to
350
+ deduplicate across all samples.
351
+ """
352
+
353
+ # import PennyLane modules locally to respect INV-004; PennyLane is
354
+ # an optional extra, so guide the user if it is not installed
355
+ #
356
+ try:
357
+ import pennylane as qml
358
+ except ImportError as exc:
359
+ raise ImportError(
360
+ "PennyLane is required for the PennyLane active probe. "
361
+ "Install it with: pip install 'hilbertbench[pennylane]'"
362
+ ) from exc
363
+ from hilbertbench.integrations.pennylane import _qasm_to_template
364
+
365
+ # build a statevector qnode wrapping the user's circuit function
366
+ #
367
+ dev = qml.device("default.qubit", wires=num_qubits)
368
+
369
+ @qml.qnode(dev) # type: ignore[untyped-decorator]
370
+ def qnode(theta: Any) -> Any:
371
+ """Execute the user circuit at theta and return the statevector."""
372
+ circuit_fn(theta)
373
+ return qml.state()
374
+
375
+ def state_fn(theta: np.ndarray) -> np.ndarray:
376
+ """Return the circuit statevector at theta as a numpy array."""
377
+ return np.asarray(qnode(theta))
378
+
379
+ # generate the QASM template from the zero-parameter point;
380
+ # falls back to None on any error (e.g. unsupported gates)
381
+ #
382
+ circuit_qasm: Optional[str] = None
383
+ try:
384
+ qscript = qml.tape.make_qscript(
385
+ lambda: circuit_fn(np.zeros(num_params))
386
+ )()
387
+ circuit_qasm = _qasm_to_template(
388
+ qml.to_openqasm(qscript, wires=range(num_qubits))
389
+ )
390
+ except Exception:
391
+ circuit_qasm = None
392
+
393
+ # exit gracefully
394
+ #
395
+ return probe_expressibility(
396
+ state_fn,
397
+ num_params=num_params,
398
+ num_samples=num_samples,
399
+ output_root=output_root,
400
+ circuit_qasm=circuit_qasm,
401
+ seed=seed,
402
+ tags={"framework": "pennylane", **(tags or {})},
403
+ )
404
+ #
405
+ # end of function
406
+
407
+ #
408
+ # end of file
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # file: hilbertbench/analysis/__init__.py
4
+ #
5
+ # revision history:
6
+ # 20260604 (am): cleaned up to project coding standards
7
+ #
8
+ # Public surface for the hilbertbench.analysis package. Every built-in
9
+ # diagnostic is a plain function: it accepts a HilbertTrace (or a run-
10
+ # directory path) and returns a plain dict. Functions are composable
11
+ # and hold no state.
12
+ #
13
+ # from hilbertbench.analysis import detect_barren_plateau
14
+ # from hilbertbench.analysis import shot_noise_ratio
15
+ # detect_barren_plateau("runs/20260605_xxx")
16
+ # shot_noise_ratio("runs/20260605_xxx")
17
+ #
18
+ # summary(trace) runs all built-in axes and returns a combined report.
19
+ #------------------------------------------------------------------------------
20
+
21
+ # future imports must come first
22
+ #
23
+ from __future__ import annotations
24
+
25
+ # import system modules
26
+ #
27
+ import os
28
+ from typing import Any
29
+
30
+ # import hilbertbench modules
31
+ #
32
+ from hilbertbench.analysis._util import TraceLike, as_trace
33
+ from hilbertbench.analysis.circuit import circuit_structure
34
+ from hilbertbench.analysis.expressibility import kl_expressibility
35
+ from hilbertbench.analysis.measurement import shot_noise_ratio
36
+ from hilbertbench.analysis.noise import noise_profile
37
+ from hilbertbench.analysis.optimization import optimization_convergence
38
+ from hilbertbench.analysis.trainability import detect_barren_plateau
39
+
40
+ #------------------------------------------------------------------------------
41
+ #
42
+ # global variables are listed here
43
+ #
44
+ #------------------------------------------------------------------------------
45
+
46
+ # set the filename using basename
47
+ #
48
+ __FILE__ = os.path.basename(__file__)
49
+
50
+ __all__ = [
51
+ "detect_barren_plateau",
52
+ "shot_noise_ratio",
53
+ "optimization_convergence",
54
+ "circuit_structure",
55
+ "kl_expressibility",
56
+ "noise_profile",
57
+ "summary",
58
+ ]
59
+
60
+ #------------------------------------------------------------------------------
61
+ #
62
+ # functions are listed here
63
+ #
64
+ #------------------------------------------------------------------------------
65
+
66
+ def summary(trace: TraceLike) -> dict[str, Any]:
67
+ """
68
+ function: summary
69
+
70
+ arguments:
71
+ trace: a HilbertTrace or run-directory path
72
+
73
+ return:
74
+ a combined diagnostic report dict keyed by analysis axis
75
+
76
+ description:
77
+ Runs every built-in analyzer and returns one combined report.
78
+ Top-level keys: trace, trainability, measurement, optimization,
79
+ circuit. Use individual functions for finer control.
80
+ """
81
+
82
+ # resolve the trace object
83
+ #
84
+ t = as_trace(trace)
85
+
86
+ # run all built-in analyzers and combine into one report
87
+ #
88
+ return {
89
+ "trace": {
90
+ "status": t.status,
91
+ "mode": t.mode,
92
+ "num_spans": len(t),
93
+ "tags": t.tags,
94
+ },
95
+ "trainability": detect_barren_plateau(t),
96
+ "measurement": shot_noise_ratio(t),
97
+ "optimization": optimization_convergence(t),
98
+ "circuit": circuit_structure(t),
99
+ "noise": noise_profile(t),
100
+ }
101
+ #
102
+ # end of function
103
+
104
+ #
105
+ # end of file