qmlkit 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.
- qmlkit/__init__.py +495 -0
- qmlkit/_aliases.py +135 -0
- qmlkit/algorithms/__init__.py +82 -0
- qmlkit/algorithms/adapt.py +297 -0
- qmlkit/algorithms/autoencoder.py +206 -0
- qmlkit/algorithms/chemistry.py +222 -0
- qmlkit/algorithms/clustering.py +149 -0
- qmlkit/algorithms/hamiltonians.py +143 -0
- qmlkit/algorithms/molecule.py +442 -0
- qmlkit/algorithms/qaoa.py +208 -0
- qmlkit/algorithms/rl.py +198 -0
- qmlkit/algorithms/vqe.py +198 -0
- qmlkit/ansatz/__init__.py +68 -0
- qmlkit/ansatz/blocks.py +348 -0
- qmlkit/ansatz/library.py +570 -0
- qmlkit/ansatz/reupload.py +168 -0
- qmlkit/baselines.py +604 -0
- qmlkit/budget.py +234 -0
- qmlkit/core/__init__.py +1 -0
- qmlkit/core/backends/__init__.py +22 -0
- qmlkit/core/backends/_sampling.py +43 -0
- qmlkit/core/backends/base.py +256 -0
- qmlkit/core/backends/cirq_backend.py +110 -0
- qmlkit/core/backends/cirq_density_backend.py +71 -0
- qmlkit/core/backends/noisy.py +86 -0
- qmlkit/core/backends/numpy_backend.py +276 -0
- qmlkit/core/backends/qiskit_aer_backend.py +79 -0
- qmlkit/core/backends/qiskit_backend.py +104 -0
- qmlkit/core/backends/registry.py +210 -0
- qmlkit/core/backends/spinqit_backend.py +233 -0
- qmlkit/core/backends/torch_backend.py +185 -0
- qmlkit/core/builder.py +189 -0
- qmlkit/core/execute.py +193 -0
- qmlkit/core/gates.py +243 -0
- qmlkit/core/ir.py +320 -0
- qmlkit/core/observables.py +269 -0
- qmlkit/datasets.py +178 -0
- qmlkit/diagnostics.py +719 -0
- qmlkit/draw.py +177 -0
- qmlkit/encoding/__init__.py +63 -0
- qmlkit/encoding/amplitude.py +178 -0
- qmlkit/encoding/angle.py +61 -0
- qmlkit/encoding/feature_maps.py +353 -0
- qmlkit/encoding/hamiltonian.py +206 -0
- qmlkit/encoding/pipeline.py +198 -0
- qmlkit/encoding/scaling.py +139 -0
- qmlkit/evaluate.py +686 -0
- qmlkit/fourier.py +124 -0
- qmlkit/generative.py +406 -0
- qmlkit/gradients/__init__.py +61 -0
- qmlkit/gradients/adjoint.py +138 -0
- qmlkit/gradients/batch.py +275 -0
- qmlkit/gradients/dispatch.py +247 -0
- qmlkit/gradients/hadamard.py +108 -0
- qmlkit/gradients/parameter_shift.py +142 -0
- qmlkit/gradients/rules.py +151 -0
- qmlkit/gradients/spsa.py +134 -0
- qmlkit/imbalance.py +335 -0
- qmlkit/info.py +153 -0
- qmlkit/interop.py +778 -0
- qmlkit/kernels/__init__.py +69 -0
- qmlkit/kernels/estimators.py +206 -0
- qmlkit/kernels/matrix.py +439 -0
- qmlkit/kernels/models.py +315 -0
- qmlkit/metrics.py +394 -0
- qmlkit/nn/__init__.py +18 -0
- qmlkit/nn/advanced.py +254 -0
- qmlkit/nn/layer.py +343 -0
- qmlkit/nn/losses.py +124 -0
- qmlkit/nn/models.py +245 -0
- qmlkit/optim.py +306 -0
- qmlkit/provenance.py +271 -0
- qmlkit/py.typed +0 -0
- qmlkit/search.py +561 -0
- qmlkit/shadows.py +117 -0
- qmlkit/utils/__init__.py +19 -0
- qmlkit/utils/errors.py +130 -0
- qmlkit/utils/shots.py +55 -0
- qmlkit-0.1.0.dist-info/METADATA +745 -0
- qmlkit-0.1.0.dist-info/RECORD +83 -0
- qmlkit-0.1.0.dist-info/WHEEL +4 -0
- qmlkit-0.1.0.dist-info/licenses/LICENSE +202 -0
- qmlkit-0.1.0.dist-info/licenses/NOTICE +4 -0
qmlkit/provenance.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Two questions a result has to answer: is it right, and can it be reproduced.
|
|
2
|
+
|
|
3
|
+
A quantum machine learning number is produced by a stack — library version, SDK
|
|
4
|
+
version, backend, seed, shot count — where any layer can change the answer and none
|
|
5
|
+
of them is usually recorded. Six months later the same script gives a different
|
|
6
|
+
number and there is no way to tell which layer moved.
|
|
7
|
+
|
|
8
|
+
:func:`fingerprint` records the stack. :func:`selfcheck` asks whether the number was
|
|
9
|
+
right in the first place, by computing it more than one way.
|
|
10
|
+
|
|
11
|
+
The second is the more unusual. This library ships four *independent* exact routes
|
|
12
|
+
to a gradient — adjoint, backprop, Hadamard-test and parameter-shift — and any two
|
|
13
|
+
of them agreeing to machine precision is strong evidence that both are correct,
|
|
14
|
+
because they share almost no code. Disagreement localises a bug that no single
|
|
15
|
+
implementation could have caught::
|
|
16
|
+
|
|
17
|
+
>>> import numpy as np, qmlkit as qk
|
|
18
|
+
>>> a = qk.hardware_efficient(3, 2)
|
|
19
|
+
>>> spec = a.build()
|
|
20
|
+
>>> report = qk.selfcheck(spec, np.full(a.n_params, 0.3), qk.Z(0))
|
|
21
|
+
>>> bool(report) # falsy when every route agrees
|
|
22
|
+
False
|
|
23
|
+
|
|
24
|
+
That is the parity idea from ``tests/test_pennylane_parity.py`` turned into
|
|
25
|
+
something a user can point at their own circuit.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import platform
|
|
31
|
+
import sys
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from importlib import import_module
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
import numpy.typing as npt
|
|
38
|
+
from numpy.typing import ArrayLike
|
|
39
|
+
|
|
40
|
+
from qmlkit.core.ir import CircuitSpec
|
|
41
|
+
from qmlkit.core.observables import Observable
|
|
42
|
+
|
|
43
|
+
__all__ = ["Fingerprint", "fingerprint", "selfcheck"]
|
|
44
|
+
|
|
45
|
+
#: Four exact routes to the same gradient. Two agreeing is evidence; four is proof
|
|
46
|
+
#: enough for a working scientist.
|
|
47
|
+
_EXACT_METHODS = ("adjoint", "backprop", "hadamard", "parameter-shift")
|
|
48
|
+
|
|
49
|
+
#: Exact methods agree to machine precision. This is loose enough to survive the
|
|
50
|
+
#: accumulation over a few hundred gates and tight enough that a wrong shift rule,
|
|
51
|
+
#: a transposed matrix or a bit-order slip cannot hide under it.
|
|
52
|
+
_AGREEMENT = 1e-9
|
|
53
|
+
|
|
54
|
+
#: SpinQit's simulator carries a precision floor near 1e-10 rather than machine
|
|
55
|
+
#: precision, so a cross-backend comparison involving it needs more room.
|
|
56
|
+
_BACKEND_AGREEMENT = 1e-8
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _version(module_name: str) -> str | None:
|
|
60
|
+
try:
|
|
61
|
+
return str(getattr(import_module(module_name), "__version__", "installed"))
|
|
62
|
+
except Exception: # noqa: BLE001 - absence and breakage are the same answer here
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Fingerprint:
|
|
68
|
+
"""Everything that could change a number, recorded in one object.
|
|
69
|
+
|
|
70
|
+
Paste :meth:`as_dict` into a results file, or :func:`str` into a paper
|
|
71
|
+
appendix. The point is that it is cheap enough to attach to every run.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
qmlkit: str
|
|
75
|
+
python: str
|
|
76
|
+
platform: str
|
|
77
|
+
numpy: str
|
|
78
|
+
default_backend: str
|
|
79
|
+
backends: dict[str, str | None] = field(default_factory=dict)
|
|
80
|
+
optional: dict[str, str | None] = field(default_factory=dict)
|
|
81
|
+
seed: int | None = None
|
|
82
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
def as_dict(self) -> dict[str, Any]:
|
|
85
|
+
"""A plain, JSON-serialisable mapping."""
|
|
86
|
+
return {
|
|
87
|
+
"qmlkit": self.qmlkit,
|
|
88
|
+
"python": self.python,
|
|
89
|
+
"platform": self.platform,
|
|
90
|
+
"numpy": self.numpy,
|
|
91
|
+
"default_backend": self.default_backend,
|
|
92
|
+
"backends": dict(self.backends),
|
|
93
|
+
"optional": dict(self.optional),
|
|
94
|
+
"seed": self.seed,
|
|
95
|
+
**self.extra,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def __str__(self) -> str:
|
|
99
|
+
installed = {k: v for k, v in self.backends.items() if v is not None}
|
|
100
|
+
lines = [
|
|
101
|
+
f"qmlkit {self.qmlkit} | python {self.python} | numpy {self.numpy}",
|
|
102
|
+
f" platform {self.platform}",
|
|
103
|
+
f" default backend {self.default_backend}",
|
|
104
|
+
" backends "
|
|
105
|
+
+ (", ".join(f"{k} {v}" for k, v in sorted(installed.items())) or "numpy only"),
|
|
106
|
+
]
|
|
107
|
+
present = {k: v for k, v in self.optional.items() if v is not None}
|
|
108
|
+
if present:
|
|
109
|
+
lines.append(
|
|
110
|
+
" optional " + ", ".join(f"{k} {v}" for k, v in sorted(present.items()))
|
|
111
|
+
)
|
|
112
|
+
if self.seed is not None:
|
|
113
|
+
lines.append(f" seed {self.seed}")
|
|
114
|
+
lines.extend(f" {k:<16} {v}" for k, v in self.extra.items())
|
|
115
|
+
return "\n".join(lines)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def fingerprint(seed: int | None = None, **extra: Any) -> Fingerprint:
|
|
119
|
+
"""The versions and settings that decide what a number comes out as.
|
|
120
|
+
|
|
121
|
+
``seed`` and any keyword extras are carried verbatim, so the run's own
|
|
122
|
+
parameters — shot count, ansatz name, dataset — sit alongside the environment
|
|
123
|
+
that produced them.
|
|
124
|
+
"""
|
|
125
|
+
from qmlkit import __version__
|
|
126
|
+
from qmlkit.core.backends.registry import default_backend
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
current = default_backend().name
|
|
130
|
+
except Exception: # pragma: no cover - a broken default should not break the record
|
|
131
|
+
current = "unavailable"
|
|
132
|
+
|
|
133
|
+
return Fingerprint(
|
|
134
|
+
qmlkit=__version__,
|
|
135
|
+
python=sys.version.split()[0],
|
|
136
|
+
platform=f"{platform.system()} {platform.release()} ({platform.machine()})",
|
|
137
|
+
numpy=np.__version__,
|
|
138
|
+
default_backend=current,
|
|
139
|
+
backends={name: _version(name) for name in ("qiskit", "cirq", "spinqit")},
|
|
140
|
+
optional={name: _version(name) for name in ("torch", "sklearn", "matplotlib")},
|
|
141
|
+
seed=seed,
|
|
142
|
+
extra=dict(extra),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _gradient_routes(
|
|
147
|
+
spec: CircuitSpec, theta: ArrayLike, obs: Observable, backend: Any
|
|
148
|
+
) -> dict[str, npt.NDArray[Any]]:
|
|
149
|
+
"""Every exact gradient method that can run here, each computed independently."""
|
|
150
|
+
from qmlkit.gradients.dispatch import grad
|
|
151
|
+
|
|
152
|
+
out: dict[str, npt.NDArray[Any]] = {}
|
|
153
|
+
for method in _EXACT_METHODS:
|
|
154
|
+
try:
|
|
155
|
+
out[method] = np.asarray(grad(spec, theta, obs, method=method, backend=backend), float)
|
|
156
|
+
except Exception: # noqa: BLE001 - an unavailable route is not a failure
|
|
157
|
+
continue
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def selfcheck(
|
|
162
|
+
spec: CircuitSpec,
|
|
163
|
+
theta: ArrayLike,
|
|
164
|
+
obs: Observable,
|
|
165
|
+
backend: Any = None,
|
|
166
|
+
cross_backend: bool = True,
|
|
167
|
+
) -> Any:
|
|
168
|
+
"""Compute this circuit's value and gradient every available way, and compare.
|
|
169
|
+
|
|
170
|
+
Returns a :class:`~qmlkit.diagnostics.Report`, falsy when everything agrees.
|
|
171
|
+
|
|
172
|
+
Two independent checks run:
|
|
173
|
+
|
|
174
|
+
* **Gradient routes.** Adjoint, backprop, Hadamard-test and parameter-shift are
|
|
175
|
+
four separate derivations of the same quantity. They share the circuit IR and
|
|
176
|
+
almost nothing else, so agreement is evidence and disagreement localises the
|
|
177
|
+
wrong one — the method that stands alone against the others.
|
|
178
|
+
* **Backends.** When more than one SDK is installed, the same circuit is run
|
|
179
|
+
through each. This catches the translation-layer mistakes that no amount of
|
|
180
|
+
testing against a single simulator can: endianness, controlled-gate qubit
|
|
181
|
+
order, dropped idle qubits.
|
|
182
|
+
|
|
183
|
+
``cross_backend=False`` skips the second, which is the slower one.
|
|
184
|
+
|
|
185
|
+
This is what to run when a number looks wrong and nothing raised.
|
|
186
|
+
"""
|
|
187
|
+
from qmlkit.core.backends.registry import available_backends, get_backend
|
|
188
|
+
from qmlkit.core.execute import expectation
|
|
189
|
+
from qmlkit.diagnostics import Finding, Report
|
|
190
|
+
|
|
191
|
+
values = np.asarray(theta, dtype=float)
|
|
192
|
+
findings: list[Finding] = []
|
|
193
|
+
|
|
194
|
+
routes = _gradient_routes(spec, values, obs, backend)
|
|
195
|
+
if spec.n_params == 0:
|
|
196
|
+
# every route returns an empty gradient, which agrees trivially and says
|
|
197
|
+
# nothing. Report that rather than comparing zero-length arrays.
|
|
198
|
+
findings.append(
|
|
199
|
+
Finding(
|
|
200
|
+
"selfcheck.one-route",
|
|
201
|
+
"info",
|
|
202
|
+
"this circuit has no parameters, so there is no gradient to cross-check",
|
|
203
|
+
"selfcheck compares gradients; for a fixed circuit the backend "
|
|
204
|
+
"comparison below is the whole check",
|
|
205
|
+
0.0,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
elif len(routes) < 2:
|
|
209
|
+
findings.append(
|
|
210
|
+
Finding(
|
|
211
|
+
"selfcheck.one-route",
|
|
212
|
+
"info",
|
|
213
|
+
f"only {len(routes)} exact gradient route could run here "
|
|
214
|
+
f"({', '.join(routes) or 'none'}), so nothing was cross-checked",
|
|
215
|
+
"pip install 'qmlkit[torch]' adds backprop as a second opinion",
|
|
216
|
+
float(len(routes)),
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
names = list(routes)
|
|
221
|
+
reference = names[0]
|
|
222
|
+
for name in names[1:]:
|
|
223
|
+
delta = float(np.max(np.abs(routes[name] - routes[reference])))
|
|
224
|
+
if delta > _AGREEMENT:
|
|
225
|
+
findings.append(
|
|
226
|
+
Finding(
|
|
227
|
+
"selfcheck.gradient-disagreement",
|
|
228
|
+
"error",
|
|
229
|
+
f"{name} and {reference} disagree by {delta:.3e}, which is far above "
|
|
230
|
+
f"the {_AGREEMENT:.0e} these exact methods agree to. One of them is "
|
|
231
|
+
"computing something else",
|
|
232
|
+
"compare against a third method to see which one stands alone; a "
|
|
233
|
+
"custom gate with wrong `frequencies` is the usual cause",
|
|
234
|
+
delta,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if cross_backend:
|
|
239
|
+
installed = [n for n in available_backends() if n != "numpy"]
|
|
240
|
+
if installed:
|
|
241
|
+
reference_value = expectation(spec, obs, values, backend="numpy")
|
|
242
|
+
for name in installed:
|
|
243
|
+
try:
|
|
244
|
+
other = expectation(spec, obs, values, backend=get_backend(name))
|
|
245
|
+
except Exception as exc: # noqa: BLE001 - report, do not raise
|
|
246
|
+
findings.append(
|
|
247
|
+
Finding(
|
|
248
|
+
"selfcheck.backend-failed",
|
|
249
|
+
"warning",
|
|
250
|
+
f"the {name!r} backend could not run this circuit: {exc}",
|
|
251
|
+
"qk.backend_report() lists what is installed and working",
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
continue
|
|
255
|
+
delta = abs(other - reference_value)
|
|
256
|
+
if delta > _BACKEND_AGREEMENT:
|
|
257
|
+
findings.append(
|
|
258
|
+
Finding(
|
|
259
|
+
"selfcheck.backend-disagreement",
|
|
260
|
+
"error",
|
|
261
|
+
f"{name} gives {other:.12g} where the NumPy reference gives "
|
|
262
|
+
f"{reference_value:.12g} (difference {delta:.3e})",
|
|
263
|
+
f"qk.get_backend({name!r}).to_{name}(spec) shows the translated "
|
|
264
|
+
"circuit; bit order and controlled-gate qubit order are where "
|
|
265
|
+
"backends differ",
|
|
266
|
+
delta,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
subject = f"circuit ({spec.n_qubits} qubits, {spec.n_params} parameters)"
|
|
271
|
+
return Report(subject, tuple(findings))
|
qmlkit/py.typed
ADDED
|
File without changes
|