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
@@ -0,0 +1,315 @@
1
+ """Kernel models: sklearn estimators, a trainable kernel, and projected kernels.
2
+
3
+ The division of labour a quantum kernel method rests on: the **quantum** part fills
4
+ the Gram matrix, and the **classical** part solves a convex problem on it. That
5
+ means `QSVC` is a real SVM — same convergence guarantees, same solver — with one
6
+ matrix supplied from a circuit.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Sequence
12
+ from typing import Any
13
+
14
+ import numpy as np
15
+ import numpy.typing as npt
16
+ from numpy.typing import ArrayLike
17
+
18
+ from qmlkit.core.execute import BackendLike
19
+ from qmlkit.encoding.feature_maps import FeatureMap
20
+ from qmlkit.encoding.pipeline import SklearnCompatible
21
+ from qmlkit.info import reduced_dm
22
+ from qmlkit.kernels.matrix import QuantumKernel, closest_psd_matrix, is_psd, target_alignment
23
+
24
+ __all__ = [
25
+ "QSVC",
26
+ "QSVR",
27
+ "NearestFidelityClassifier",
28
+ "TrainableKernel",
29
+ "projected_kernel_matrix",
30
+ "rkhs_model",
31
+ ]
32
+
33
+
34
+ def _require_sklearn(what: str) -> Any:
35
+ try:
36
+ import sklearn.svm as svm
37
+ except ImportError as exc: # pragma: no cover - depends on the environment
38
+ raise ImportError(
39
+ f"{what} wraps scikit-learn's solver, which is an optional extra:\n"
40
+ " pip install 'qmlkit[sklearn]'"
41
+ ) from exc
42
+ return svm
43
+
44
+
45
+ class _KernelEstimator(SklearnCompatible):
46
+ """Shared plumbing: fill the Gram matrix, hand it to a precomputed-kernel solver."""
47
+
48
+ _svm: Any
49
+
50
+ def __init__(
51
+ self,
52
+ feature_map: FeatureMap,
53
+ shots: int | None = None,
54
+ backend: BackendLike = None,
55
+ bandwidth: float = 1.0,
56
+ estimator: str = "inversion",
57
+ repair_psd: str | None = "threshold",
58
+ seed: int | None = None,
59
+ **solver_kwargs: Any,
60
+ ) -> None:
61
+ # Every constructor argument is stored under its own name, unchanged. That is
62
+ # scikit-learn's one requirement for `clone`, and `clone` is what Pipeline,
63
+ # GridSearchCV and cross_val_score are built on -- so this is what lets a
64
+ # quantum estimator sit inside an ordinary scikit-learn workflow.
65
+ self.feature_map = feature_map
66
+ self.estimator = estimator
67
+ self.shots = shots
68
+ self.backend = backend
69
+ self.bandwidth = bandwidth
70
+ self.seed = seed
71
+ self.repair_psd = repair_psd
72
+ self.solver_kwargs = solver_kwargs
73
+ self.kernel = QuantumKernel(
74
+ feature_map,
75
+ estimator=estimator,
76
+ shots=shots,
77
+ backend=backend,
78
+ bandwidth=bandwidth,
79
+ seed=seed,
80
+ )
81
+ self.X_train_: npt.NDArray[Any] | None = None
82
+
83
+ def _gram(self, X: npt.NDArray[Any], Y: npt.NDArray[Any] | None = None) -> npt.NDArray[Any]:
84
+ K = self.kernel(X, Y)
85
+ # only a square training matrix needs (or admits) a PSD repair
86
+ if Y is None and self.repair_psd and not is_psd(K):
87
+ K = closest_psd_matrix(K, self.repair_psd)
88
+ return K
89
+
90
+ def fit(self, X: npt.NDArray[Any], y: npt.NDArray[Any]) -> _KernelEstimator:
91
+ self.X_train_ = np.atleast_2d(np.asarray(X, dtype=float))
92
+ self._svm.fit(self._gram(self.X_train_), np.asarray(y).ravel())
93
+ return self
94
+
95
+ def _check_fitted(self) -> npt.NDArray[Any]:
96
+ if self.X_train_ is None:
97
+ raise ValueError(f"{type(self).__name__} must be fitted before use")
98
+ return self.X_train_
99
+
100
+ def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]:
101
+ train = self._check_fitted()
102
+ return np.asarray(self._svm.predict(self.kernel(np.atleast_2d(X), train)))
103
+
104
+ def decision_function(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]:
105
+ train = self._check_fitted()
106
+ return np.asarray(self._svm.decision_function(self.kernel(np.atleast_2d(X), train)))
107
+
108
+ @property
109
+ def n_circuit_evaluations(self) -> int:
110
+ return self.kernel.n_evaluations
111
+
112
+
113
+ class QSVC(_KernelEstimator):
114
+ _estimator_type = "classifier"
115
+
116
+ """Quantum-kernel support vector classifier.
117
+
118
+ clf = QSVC(qk.ZZFeatureMap(2)).fit(X, y)
119
+ clf.score(X_test, y_test)
120
+ """
121
+
122
+ def __init__(self, feature_map: FeatureMap, C: float = 1.0, **kwargs: Any) -> None:
123
+ super().__init__(feature_map, **kwargs)
124
+ self.C = C # stored under its own name, so get_params/clone can see it
125
+ svm = _require_sklearn("QSVC")
126
+ self._svm = svm.SVC(kernel="precomputed", C=C, **self.solver_kwargs)
127
+
128
+ def score(self, X: npt.NDArray[Any], y: npt.NDArray[Any]) -> float:
129
+ return float((self.predict(X) == np.asarray(y).ravel()).mean())
130
+
131
+
132
+ class QSVR(_KernelEstimator):
133
+ """Quantum-kernel support vector regressor."""
134
+
135
+ _estimator_type = "regressor"
136
+
137
+ def __init__(
138
+ self, feature_map: FeatureMap, C: float = 1.0, epsilon: float = 0.1, **kwargs: Any
139
+ ) -> None:
140
+ super().__init__(feature_map, **kwargs)
141
+ self.C = C
142
+ self.epsilon = epsilon
143
+ svm = _require_sklearn("QSVR")
144
+ self._svm = svm.SVR(kernel="precomputed", C=C, epsilon=epsilon, **self.solver_kwargs)
145
+
146
+ def score(self, X: npt.NDArray[Any], y: npt.NDArray[Any]) -> float:
147
+ """R^2."""
148
+ pred = np.asarray(self.predict(X)).ravel()
149
+ truth = np.asarray(y, dtype=float).ravel()
150
+ ss_res = float(((truth - pred) ** 2).sum())
151
+ ss_tot = float(((truth - truth.mean()) ** 2).sum())
152
+ return 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0
153
+
154
+
155
+ class NearestFidelityClassifier:
156
+ """Classify by fidelity to each class centroid — no solver, no sklearn.
157
+
158
+ The simplest quantum classifier there is: encode every training point, average
159
+ within each class, and predict whichever class anchor a new point overlaps most.
160
+ """
161
+
162
+ def __init__(
163
+ self,
164
+ feature_map: FeatureMap,
165
+ shots: int | None = None,
166
+ backend: BackendLike = None,
167
+ ) -> None:
168
+ self.feature_map = feature_map
169
+ self.shots = shots
170
+ self.backend = backend
171
+ self.classes_: npt.NDArray[Any] | None = None
172
+ self.anchors_: dict[Any, npt.NDArray[Any]] = {}
173
+
174
+ def fit(self, X: npt.NDArray[Any], y: npt.NDArray[Any]) -> NearestFidelityClassifier:
175
+ from qmlkit.core.execute import statevector
176
+
177
+ rows = np.atleast_2d(np.asarray(X, dtype=float))
178
+ labels = np.asarray(y).ravel()
179
+ self.classes_ = np.unique(labels)
180
+ for c in self.classes_:
181
+ states = [
182
+ statevector(self.feature_map.build(r), backend=self.backend)
183
+ for r in rows[labels == c]
184
+ ]
185
+ mean = np.mean(states, axis=0)
186
+ norm = np.linalg.norm(mean)
187
+ self.anchors_[c] = mean / norm if norm > 1e-12 else mean
188
+ return self
189
+
190
+ def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]:
191
+ from qmlkit.core.execute import statevector
192
+
193
+ if self.classes_ is None:
194
+ raise ValueError("NearestFidelityClassifier must be fitted before use")
195
+ rows = np.atleast_2d(np.asarray(X, dtype=float))
196
+ out = []
197
+ for r in rows:
198
+ psi = statevector(self.feature_map.build(r), backend=self.backend)
199
+ scores = {c: abs(np.vdot(a, psi)) ** 2 for c, a in self.anchors_.items()}
200
+ out.append(max(scores, key=lambda c: scores[c]))
201
+ return np.array(out)
202
+
203
+ def score(self, X: npt.NDArray[Any], y: npt.NDArray[Any]) -> float:
204
+ return float((self.predict(X) == np.asarray(y).ravel()).mean())
205
+
206
+
207
+ class TrainableKernel:
208
+ """Train the *feature map itself* by maximising kernel-target alignment.
209
+
210
+ A fixed feature map is a guess. Alignment gives a differentiable score for how
211
+ well a kernel matches the labels, so the embedding's own parameters can be
212
+ optimised before any classifier is fitted — usually a bigger win than tuning the
213
+ classifier afterwards.
214
+ """
215
+
216
+ def __init__(
217
+ self,
218
+ feature_map_factory: Any,
219
+ n_params: int,
220
+ shots: int | None = None,
221
+ backend: BackendLike = None,
222
+ ) -> None:
223
+ self.factory = feature_map_factory
224
+ self.n_params = n_params
225
+ self.shots = shots
226
+ self.backend = backend
227
+ self.params_: npt.NDArray[Any] | None = None
228
+ self.history_: list[float] = []
229
+
230
+ def alignment(self, params: ArrayLike, X: npt.NDArray[Any], y: npt.NDArray[Any]) -> float:
231
+ kernel = QuantumKernel(self.factory(params), shots=self.shots, backend=self.backend)
232
+ return target_alignment(kernel(X), y)
233
+
234
+ def fit(
235
+ self,
236
+ X: npt.NDArray[Any],
237
+ y: npt.NDArray[Any],
238
+ n_iterations: int = 40,
239
+ theta0: Sequence[float] | None = None,
240
+ seed: int | None = None,
241
+ ) -> TrainableKernel:
242
+ """Maximise alignment with SPSA — two evaluations per step, any parameter count."""
243
+ from qmlkit.gradients.spsa import minimize_spsa
244
+
245
+ rng = np.random.default_rng(seed)
246
+ start = (
247
+ np.asarray(theta0, dtype=float)
248
+ if theta0 is not None
249
+ else rng.uniform(0.5, 1.5, self.n_params)
250
+ )
251
+
252
+ def loss(p: npt.NDArray[Any]) -> float:
253
+ return -self.alignment(p, X, y) # minimise the negative
254
+
255
+ # minimize_spsa returns the *final* iterate, not the best one seen, so a short
256
+ # run can legitimately end below where it started. history_ is the alignment
257
+ # trajectory and its last entry always corresponds to params_.
258
+ final, history = minimize_spsa(loss, start, n_iterations=n_iterations, seed=seed)
259
+ self.params_ = final
260
+ self.history_ = [-h for h in history]
261
+ return self
262
+
263
+ def kernel(self) -> QuantumKernel:
264
+ if self.params_ is None:
265
+ raise ValueError("TrainableKernel must be fitted before use")
266
+ return QuantumKernel(self.factory(self.params_), shots=self.shots, backend=self.backend)
267
+
268
+
269
+ def projected_kernel_matrix(
270
+ feature_map: FeatureMap,
271
+ X: npt.NDArray[Any],
272
+ gamma: float = 1.0,
273
+ backend: BackendLike = None,
274
+ ) -> npt.NDArray[Any]:
275
+ r"""Projected quantum kernel — the standard answer to exponential concentration.
276
+
277
+ Instead of a *global* fidelity, compare the **one-qubit reduced density
278
+ matrices**:
279
+
280
+ .. math:: k(x, x') = \exp\left(-\gamma \sum_i \|\rho_i(x) - \rho_i(x')\|_F^2\right)
281
+
282
+ Global overlaps concentrate as the register widens — every pair of inputs ends
283
+ up looking equally similar, and the kernel stops carrying information. Local
284
+ reduced states do not, so this stays informative where the fidelity kernel has
285
+ already collapsed (Huang et al. 2021).
286
+ """
287
+ from qmlkit.core.execute import statevector
288
+
289
+ rows = np.atleast_2d(np.asarray(X, dtype=float))
290
+ n = feature_map.n_qubits
291
+ # one reduced density matrix per qubit per sample
292
+ rdms = []
293
+ for r in rows:
294
+ psi = statevector(feature_map.build(r), backend=backend)
295
+ rdms.append([reduced_dm(psi, [q], n) for q in range(n)])
296
+
297
+ m = len(rows)
298
+ out = np.ones((m, m))
299
+ for i in range(m):
300
+ for j in range(i + 1, m):
301
+ dist = sum(float(np.linalg.norm(rdms[i][q] - rdms[j][q], "fro") ** 2) for q in range(n))
302
+ out[i, j] = out[j, i] = float(np.exp(-gamma * dist))
303
+ return out
304
+
305
+
306
+ def rkhs_model(
307
+ alphas: Sequence[float],
308
+ anchors: npt.NDArray[Any],
309
+ x: Sequence[float],
310
+ kernel: Any,
311
+ ) -> float:
312
+ """``f(x) = sum_i alpha_i k(x_i, x)`` — a kernel model is a weighted similarity sum."""
313
+ a = np.asarray(alphas, dtype=float)
314
+ pts = np.atleast_2d(np.asarray(anchors, dtype=float))
315
+ return float(sum(ai * kernel(p, x) for ai, p in zip(a, pts, strict=True)))