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.
- hilbertbench/__init__.py +78 -0
- hilbertbench/active/__init__.py +38 -0
- hilbertbench/active/probe.py +408 -0
- hilbertbench/analysis/__init__.py +105 -0
- hilbertbench/analysis/_util.py +129 -0
- hilbertbench/analysis/circuit.py +293 -0
- hilbertbench/analysis/expressibility.py +292 -0
- hilbertbench/analysis/measurement.py +270 -0
- hilbertbench/analysis/noise.py +312 -0
- hilbertbench/analysis/optimization.py +257 -0
- hilbertbench/analysis/trainability.py +157 -0
- hilbertbench/integrations/__init__.py +0 -0
- hilbertbench/integrations/pennylane.py +560 -0
- hilbertbench/integrations/qiskit.py +1177 -0
- hilbertbench/models/__init__.py +42 -0
- hilbertbench/models/v1_0/__init__.py +4 -0
- hilbertbench/models/v1_0/artifact.py +79 -0
- hilbertbench/models/v1_0/catalog.py +33 -0
- hilbertbench/models/v1_0/span.py +126 -0
- hilbertbench/models/v1_0/trace.py +92 -0
- hilbertbench/py.typed +0 -0
- hilbertbench/reader/__init__.py +0 -0
- hilbertbench/reader/verify.py +453 -0
- hilbertbench/recorder/__init__.py +26 -0
- hilbertbench/recorder/storage/__init__.py +0 -0
- hilbertbench/recorder/storage/writer.py +267 -0
- hilbertbench/recorder/tape.py +848 -0
- hilbertbench/trace/__init__.py +29 -0
- hilbertbench/trace/span.py +412 -0
- hilbertbench/trace/trace.py +738 -0
- hilbertbench-1.0.0.dist-info/METADATA +208 -0
- hilbertbench-1.0.0.dist-info/RECORD +36 -0
- hilbertbench-1.0.0.dist-info/WHEEL +5 -0
- hilbertbench-1.0.0.dist-info/entry_points.txt +3 -0
- hilbertbench-1.0.0.dist-info/licenses/LICENSE +21 -0
- hilbertbench-1.0.0.dist-info/top_level.txt +1 -0
hilbertbench/__init__.py
ADDED
|
@@ -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
|