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.
Files changed (83) hide show
  1. qmlkit/__init__.py +495 -0
  2. qmlkit/_aliases.py +135 -0
  3. qmlkit/algorithms/__init__.py +82 -0
  4. qmlkit/algorithms/adapt.py +297 -0
  5. qmlkit/algorithms/autoencoder.py +206 -0
  6. qmlkit/algorithms/chemistry.py +222 -0
  7. qmlkit/algorithms/clustering.py +149 -0
  8. qmlkit/algorithms/hamiltonians.py +143 -0
  9. qmlkit/algorithms/molecule.py +442 -0
  10. qmlkit/algorithms/qaoa.py +208 -0
  11. qmlkit/algorithms/rl.py +198 -0
  12. qmlkit/algorithms/vqe.py +198 -0
  13. qmlkit/ansatz/__init__.py +68 -0
  14. qmlkit/ansatz/blocks.py +348 -0
  15. qmlkit/ansatz/library.py +570 -0
  16. qmlkit/ansatz/reupload.py +168 -0
  17. qmlkit/baselines.py +604 -0
  18. qmlkit/budget.py +234 -0
  19. qmlkit/core/__init__.py +1 -0
  20. qmlkit/core/backends/__init__.py +22 -0
  21. qmlkit/core/backends/_sampling.py +43 -0
  22. qmlkit/core/backends/base.py +256 -0
  23. qmlkit/core/backends/cirq_backend.py +110 -0
  24. qmlkit/core/backends/cirq_density_backend.py +71 -0
  25. qmlkit/core/backends/noisy.py +86 -0
  26. qmlkit/core/backends/numpy_backend.py +276 -0
  27. qmlkit/core/backends/qiskit_aer_backend.py +79 -0
  28. qmlkit/core/backends/qiskit_backend.py +104 -0
  29. qmlkit/core/backends/registry.py +210 -0
  30. qmlkit/core/backends/spinqit_backend.py +233 -0
  31. qmlkit/core/backends/torch_backend.py +185 -0
  32. qmlkit/core/builder.py +189 -0
  33. qmlkit/core/execute.py +193 -0
  34. qmlkit/core/gates.py +243 -0
  35. qmlkit/core/ir.py +320 -0
  36. qmlkit/core/observables.py +269 -0
  37. qmlkit/datasets.py +178 -0
  38. qmlkit/diagnostics.py +719 -0
  39. qmlkit/draw.py +177 -0
  40. qmlkit/encoding/__init__.py +63 -0
  41. qmlkit/encoding/amplitude.py +178 -0
  42. qmlkit/encoding/angle.py +61 -0
  43. qmlkit/encoding/feature_maps.py +353 -0
  44. qmlkit/encoding/hamiltonian.py +206 -0
  45. qmlkit/encoding/pipeline.py +198 -0
  46. qmlkit/encoding/scaling.py +139 -0
  47. qmlkit/evaluate.py +686 -0
  48. qmlkit/fourier.py +124 -0
  49. qmlkit/generative.py +406 -0
  50. qmlkit/gradients/__init__.py +61 -0
  51. qmlkit/gradients/adjoint.py +138 -0
  52. qmlkit/gradients/batch.py +275 -0
  53. qmlkit/gradients/dispatch.py +247 -0
  54. qmlkit/gradients/hadamard.py +108 -0
  55. qmlkit/gradients/parameter_shift.py +142 -0
  56. qmlkit/gradients/rules.py +151 -0
  57. qmlkit/gradients/spsa.py +134 -0
  58. qmlkit/imbalance.py +335 -0
  59. qmlkit/info.py +153 -0
  60. qmlkit/interop.py +778 -0
  61. qmlkit/kernels/__init__.py +69 -0
  62. qmlkit/kernels/estimators.py +206 -0
  63. qmlkit/kernels/matrix.py +439 -0
  64. qmlkit/kernels/models.py +315 -0
  65. qmlkit/metrics.py +394 -0
  66. qmlkit/nn/__init__.py +18 -0
  67. qmlkit/nn/advanced.py +254 -0
  68. qmlkit/nn/layer.py +343 -0
  69. qmlkit/nn/losses.py +124 -0
  70. qmlkit/nn/models.py +245 -0
  71. qmlkit/optim.py +306 -0
  72. qmlkit/provenance.py +271 -0
  73. qmlkit/py.typed +0 -0
  74. qmlkit/search.py +561 -0
  75. qmlkit/shadows.py +117 -0
  76. qmlkit/utils/__init__.py +19 -0
  77. qmlkit/utils/errors.py +130 -0
  78. qmlkit/utils/shots.py +55 -0
  79. qmlkit-0.1.0.dist-info/METADATA +745 -0
  80. qmlkit-0.1.0.dist-info/RECORD +83 -0
  81. qmlkit-0.1.0.dist-info/WHEEL +4 -0
  82. qmlkit-0.1.0.dist-info/licenses/LICENSE +202 -0
  83. 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