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/kernels/models.py
ADDED
|
@@ -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)))
|