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,745 @@
1
+ Metadata-Version: 2.5
2
+ Name: qmlkit
3
+ Version: 0.1.0
4
+ Summary: A quantum machine learning library with the diagnostics to catch mistakes that do not raise, on SpinQit, Qiskit, Cirq and PyTorch
5
+ Project-URL: Homepage, https://ziadt160.github.io/qmlkit/
6
+ Project-URL: Documentation, https://ziadt160.github.io/qmlkit/
7
+ Project-URL: Source, https://github.com/Ziadt160/qmlkit
8
+ Project-URL: Changelog, https://github.com/Ziadt160/qmlkit/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/Ziadt160/qmlkit/issues
10
+ Author: Ziad Tarek Mohammed
11
+ License-Expression: Apache-2.0
12
+ License-File: LICENSE
13
+ License-File: NOTICE
14
+ Keywords: cirq,machine-learning,pytorch,qiskit,qml,quantum,spinqit,variational
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
26
+ Classifier: Topic :: Scientific/Engineering :: Physics
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: numpy>=1.21
30
+ Provides-Extra: aer
31
+ Requires-Dist: qiskit-aer>=0.14; extra == 'aer'
32
+ Requires-Dist: qiskit>=1.0; extra == 'aer'
33
+ Provides-Extra: cirq
34
+ Requires-Dist: cirq-core>=1.3; extra == 'cirq'
35
+ Provides-Extra: dev
36
+ Requires-Dist: hypothesis>=6; extra == 'dev'
37
+ Requires-Dist: mypy>=1.8; extra == 'dev'
38
+ Requires-Dist: pytest-cov>=4; extra == 'dev'
39
+ Requires-Dist: pytest>=7; extra == 'dev'
40
+ Requires-Dist: ruff<0.16,>=0.15; extra == 'dev'
41
+ Provides-Extra: docs
42
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
43
+ Requires-Dist: mkdocs>=1.6; extra == 'docs'
44
+ Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
45
+ Provides-Extra: pennylane
46
+ Requires-Dist: pennylane>=0.40; extra == 'pennylane'
47
+ Provides-Extra: qiskit
48
+ Requires-Dist: qiskit>=1.0; extra == 'qiskit'
49
+ Provides-Extra: sklearn
50
+ Requires-Dist: scikit-learn>=1.0; extra == 'sklearn'
51
+ Provides-Extra: spinqit
52
+ Requires-Dist: numpy<2; (python_version < '3.11') and extra == 'spinqit'
53
+ Requires-Dist: spinqit==0.2.4; (python_version < '3.11') and extra == 'spinqit'
54
+ Provides-Extra: torch
55
+ Requires-Dist: torch>=2.0; extra == 'torch'
56
+ Provides-Extra: viz
57
+ Requires-Dist: matplotlib>=3.5; extra == 'viz'
58
+ Description-Content-Type: text/markdown
59
+
60
+ # qmlkit
61
+
62
+ **A quantum machine learning library built to refuse a plausible wrong number.**
63
+
64
+ In this field a mistake usually does not raise. A re-uploading model whose trainable
65
+ rotations commute with its encoding will build, bind, differentiate and train — and
66
+ reach one Fourier frequency instead of the eight you designed for. A quantum kernel
67
+ concentrates until every pair of points looks alike, and still returns a Gram matrix.
68
+ A quantum model beats its classical baseline by less than the spread between folds,
69
+ and gets written up as a result. All three run. All three return numbers in the right
70
+ range. All three are wrong.
71
+
72
+ So the parts of qmlkit that catch those are not an add-on:
73
+
74
+ ```python
75
+ qk.diagnose(model) # the failures that return a number instead of raising
76
+ qk.baseline(X, y) # the classical bar, on identical folds, before you start
77
+ qk.plan(model) # what the run costs in circuits, before you pay for it
78
+ qk.selfcheck(...) # every exact gradient route, compared against every other
79
+ ```
80
+
81
+ and the same principle runs through the rest: `adjoint` refuses on a noisy backend
82
+ rather than silently differentiating a noiseless one, an unknown gate is refused by
83
+ name rather than approximated, and `qk.baseline` will not call a lead inside the fold
84
+ spread a result.
85
+
86
+ Underneath that it is the ML layer quantum SDKs leave out — feature maps, an ansatz
87
+ vocabulary, quantum kernels, torch layers, and a general-purpose parameter-shift
88
+ gradient you can point at *any* circuit and observable — running unchanged on
89
+ **SpinQit**, **Qiskit**, **Cirq**, **PyTorch**, or the built-in NumPy reference.
90
+
91
+ **Simulator-only** for the whole `0.x` line. Expectations are exact unless you ask for
92
+ shots, and two mixed-state backends take a noise model when you want to ask what a
93
+ device would have done. Exact here means no shot noise and no finite-difference bias —
94
+ not bit-identical arithmetic; expectations and gradients agree with the analytic value
95
+ to machine precision.
96
+
97
+ The core depends on **NumPy and nothing else**, and CI enforces it.
98
+
99
+ **[Documentation](https://ziadt160.github.io/qmlkit/)** — tutorials, guides and a generated API reference.
100
+ **[HANDOFF.md](https://github.com/Ziadt160/qmlkit/blob/main/HANDOFF.md)** — status, conventions, known traps, and what to do next, if you are picking this up.
101
+
102
+ ## Two lines
103
+
104
+ ```python
105
+ import qmlkit as qk
106
+
107
+ model = qk.VQC(n_features=4, n_classes=2).fit(X, y)
108
+ model.score(X, y)
109
+ ```
110
+
111
+ That is a full hybrid quantum-classical classifier: angle encoding, a
112
+ hardware-efficient ansatz, exact gradients, and a torch training loop. Every
113
+ default in it is one keyword away from being something else.
114
+
115
+ ## Three layers, and you pick where to stand
116
+
117
+ ```python
118
+ # 1. a ready-made model
119
+ qk.VQC(n_features=4, n_classes=2).fit(X, y)
120
+
121
+ # 2. a torch layer, in any nn.Module you like
122
+ layer = qk.QuantumLayer(qk.ZZFeatureMap(4), qk.hardware_efficient(4, 2), [qk.Z(0), qk.Z(1)])
123
+ net = nn.Sequential(nn.Linear(8, 4), nn.Tanh(), layer, nn.Linear(2, 2))
124
+
125
+ # 3. circuits and gradients directly
126
+ spec = qk.angle_encode([0.7])
127
+ qk.expectation(spec, qk.Z(0)) # 0.7648... == cos(0.7) to machine precision
128
+ qk.grad(spec, theta, qk.Z(0)) # exact, method chosen for you
129
+ ```
130
+
131
+ Nothing at a higher layer hides a lower one. `VQC` is built from `QuantumLayer`,
132
+ which is built from the IR — and you can drop to any level without giving up what
133
+ the level above was doing for you.
134
+
135
+ ## Built for research
136
+
137
+ Every extension point is a registry, and registering makes your thing a first-class
138
+ citizen everywhere the library takes that kind of argument.
139
+
140
+ ```python
141
+ # a new ansatz -- one line, and it inherits correct gradients, resource counting,
142
+ # and a torch layer without opting into any of them
143
+ brick = qk.Ansatz(6, qk.repeat(3, qk.RotationLayer("ry")
144
+ + qk.EntanglerLayer("cz", "alternating")))
145
+
146
+ # ...and registering makes it reachable by name
147
+ qk.register_ansatz("brick_wall", lambda n_qubits, n_layers=3: ...) # qk.get_ansatz("brick_wall")
148
+ qk.register_gradient("my_estimator", fn) # method="my_estimator"
149
+ qk.register_backend("my_device", factory, requires="my_sdk") # backend="my_device"
150
+
151
+ # a custom gate: declare the generator frequencies and parameter-shift works on it;
152
+ # add dmatrix and adjoint differentiation works too
153
+ qk.register_gate(qk.GateDef(
154
+ "xy", n_qubits=2, n_params=1,
155
+ matrix=lambda t: ...,
156
+ frequencies=(1.0,), # without this, differentiation is refused, not guessed
157
+ dmatrix=lambda t: ..., # optional: enables the fast adjoint path
158
+ ))
159
+ ```
160
+
161
+ | Want to change | Do this |
162
+ |---|---|
163
+ | The circuit structure | Compose `Block`s, or drop to `Custom(fn)` and write against the builder |
164
+ | A gate the library lacks | `register_gate` with its generator frequencies — parameter-shift then works on it |
165
+ | How gradients are estimated | `register_gradient`, then pass `method="yours"` anywhere |
166
+ | Where circuits run | `register_backend`, or subclass `Backend` and implement `statevector` |
167
+ | The data encoding | Subclass `FeatureMap`: `angles`, `n_angles`, `_emit` |
168
+ | How data is re-uploaded | Compose `EncodingLayer` with any block — see below |
169
+ | The training loop | Use `QuantumLayer` directly and write your own |
170
+
171
+ ### Data re-uploading is a pattern, not a structure
172
+
173
+ Which encoding, which trainable block, what order, how much sharing — all of it is a
174
+ design choice, so none of it is hardcoded:
175
+
176
+ ```python
177
+ qk.reupload(fmap, n_layers=3) # S W S W S W
178
+ qk.reupload(fmap, n_layers=3, order="WS") # vary before the first upload
179
+ qk.reupload(fmap, n_layers=3, share_weights=True) # one tied block, reused
180
+ qk.reupload(fmap, n_layers=3, block=qk.RotationLayer("ry") + qk.EntanglerLayer("cz", "ring"))
181
+
182
+ # or compose directly — two different feature maps in one model
183
+ qk.Ansatz(2, qk.EncodingLayer(zz) + qk.RotationLayer("ry") + qk.EncodingLayer(angle),
184
+ n_inputs=3)
185
+ ```
186
+
187
+ Any of these drops straight into a `QuantumLayer` — or into `VQC` directly, since a
188
+ re-uploading model is its own encoding *and* its own trainable block:
189
+
190
+ ```python
191
+ qk.VQC(n_features=4, n_classes=2,
192
+ feature_map=qk.reupload(qk.AngleFeatureMap(4), n_layers=3)).fit(X, y)
193
+ ```
194
+
195
+ Input gradients included, so classical layers placed before the quantum one train.
196
+
197
+ > **One trap the library catches for you.** `L` uploads reach frequencies `0..L`
198
+ > only when the trainable block does **not** commute with the encoding rotation. If
199
+ > it does — `Ry(x) Ry(θ₁) Ry(x) Ry(θ₂) = Ry(2x + θ₁ + θ₂)` — the uploads merge into a
200
+ > single rotation, the model reaches one frequency, and every weight becomes a phase
201
+ > shift. It looks like a `3L`-parameter model; it is a one-parameter family.
202
+ > `reupload()` warns. Verify any model with `qmlkit.fourier.spectrum`.
203
+
204
+ ## Built to be used by a model
205
+
206
+ Most code written against a library now is written by one, and a model does not read
207
+ a documentation site before it types. It guesses a name, runs it, reads the
208
+ traceback, and tries again. qmlkit treats that loop as the interface — which helps a
209
+ human on their first afternoon exactly as much.
210
+
211
+ **A wrong name answers with the right one.** The training data for any model holds
212
+ far more PennyLane and Qiskit than qmlkit, so the first guess is usually theirs:
213
+
214
+ ```python
215
+ qk.AngleEmbedding
216
+ # AttributeError: module 'qmlkit' has no attribute 'AngleEmbedding'.
217
+ # 'AngleEmbedding' is PennyLane's name for qmlkit.AngleFeatureMap
218
+ # (or angle_encode(x) for a one-shot circuit).
219
+
220
+ qk.get_ansatz("hardware-efficient")
221
+ # KeyError: unknown ansatz 'hardware-efficient'. Did you mean 'hardware_efficient'?
222
+ # Valid: basic_entangler, hardware_efficient, mps, qaoa, ...
223
+ ```
224
+
225
+ A translation, not an alias — the foreign name still raises, because an alias would
226
+ become API and would hide the semantic drift underneath it.
227
+
228
+ **`qk.diagnose(model)` catches what does not raise.** In this field a mistake usually
229
+ returns a plausible number rather than an exception. This model builds, binds,
230
+ differentiates and trains, and reaches one Fourier frequency instead of three:
231
+
232
+ ```python
233
+ fmap = qk.AngleFeatureMap(2, rotation="ry")
234
+ model = qk.Ansatz(2, qk.repeat(3, qk.EncodingLayer(fmap) + qk.RotationLayer("ry")),
235
+ n_inputs=2)
236
+ print(qk.diagnose(model))
237
+ # [error] ENCODING_COMMUTES: 3 uploads, but every trainable rotation is 'ry', the
238
+ # same generator the encoding uses ... the model reaches 1 frequency rather than
239
+ # 0..3. Fix: Use a non-commuting block, e.g. RotationLayer(('rz', 'ry', 'rz')).
240
+ ```
241
+
242
+ It takes an ansatz, anything holding one, or a Gram matrix, and returns findings with
243
+ a stable code, the number measured, and the edit that fixes it. Empty means nothing
244
+ found, so `if qk.diagnose(model):` reads the way it should.
245
+
246
+ **The whole library in one fetch.** [`/llms.txt`](https://ziadt160.github.io/qmlkit/llms.txt)
247
+ indexes the site and the constraints that are not inferable from the API;
248
+ [`/llms-full.txt`](https://ziadt160.github.io/qmlkit/llms-full.txt) is every tutorial
249
+ and guide plus the entire public API with signatures. Both are generated from the
250
+ pages and the package, committed, and checked in CI, so neither can drift.
251
+
252
+ [Working with a coding agent](https://ziadt160.github.io/qmlkit/guides/agents/) is
253
+ the long version; [`AGENTS.md`](https://github.com/Ziadt160/qmlkit/blob/main/AGENTS.md) is for working *on* qmlkit.
254
+
255
+ ## Install
256
+
257
+ ```bash
258
+ pip install qmlkit
259
+ ```
260
+
261
+ Every SDK is optional and imported lazily, so `import qmlkit` never requires any
262
+ of them:
263
+
264
+ ```bash
265
+ pip install "qmlkit[qiskit]" # Qiskit backend
266
+ pip install "qmlkit[cirq]" # Cirq backend
267
+ pip install "qmlkit[spinqit]" # SpinQit — needs a Python 3.8-3.10 interpreter
268
+ ```
269
+
270
+ SpinQit ships wheels for Python 3.8–3.10 only and pins `numpy<2`, so it sits
271
+ behind an environment marker and resolves cleanly to nothing on newer Pythons.
272
+
273
+ ## Backends
274
+
275
+ ```python
276
+ qk.backend_report()
277
+ # qmlkit backends:
278
+ # [ok] cirq
279
+ # [ok] cirq-density
280
+ # [ok] numpy
281
+ # [ok] qiskit
282
+ # [ok] qiskit-aer
283
+ # [missing] spinqit -> pip install 'qmlkit[spinqit]'
284
+ # [ok] torch
285
+
286
+ qk.expectation(spec, qk.Z(0), backend="qiskit") # per call
287
+ qk.set_default_backend("spinqit") # for the session
288
+ ```
289
+
290
+ `QMLKIT_BACKEND=cirq python train.py` switches an existing script without editing
291
+ it. Asking for a backend whose SDK is missing raises `BackendNotAvailable` with an
292
+ install command — never an `ImportError` traceback.
293
+
294
+ Each backend also exposes its native circuit, so you can draw, transpile or hand
295
+ it to that SDK's own tooling:
296
+
297
+ ```python
298
+ qk.get_backend("qiskit").to_qiskit(spec).draw()
299
+ qk.get_backend("cirq").to_cirq(spec)
300
+ qk.get_backend("spinqit").to_spinqit(spec)
301
+ ```
302
+
303
+ ### Noise, when you ask for it by name
304
+
305
+ Two backends evolve a density matrix instead of a state, so a circuit can be run on a
306
+ simulator that makes mistakes:
307
+
308
+ ```python
309
+ import cirq
310
+ backend = qk.get_backend("cirq-density", noise=cirq.depolarize(0.01))
311
+ qk.expectation(spec, qk.Z(0), backend=backend)
312
+ ```
313
+
314
+ `qiskit-aer` does the same with a `qiskit_aer.noise.NoiseModel`, including one lifted
315
+ off real hardware with `NoiseModel.from_backend(...)`. Aer is a separate distribution
316
+ from Qiskit: `pip install 'qmlkit[aer]'`.
317
+
318
+ **Noise never picks a simulator for you.** `get_backend(noise=...)` without naming a
319
+ mixed-state backend raises and lists the ones that would work. A noisy run costs more,
320
+ refuses two of the gradient methods, and answers a different question — so which
321
+ simulator produced a number stays written down in the code that produced it.
322
+
323
+ **Two error sources, kept separate.** `shots=None` still means shot-free: the density
324
+ matrix is evolved exactly, so the answer is exact *given the noise model*. Decoherence
325
+ and sampling error both pull a number around, and studying one with the other layered
326
+ on top means never knowing which you are looking at. Ask for `shots=N` when you want
327
+ both — that is what a device gives you.
328
+
329
+ **What is refused, and why.** There is no statevector, so `adjoint` and `backprop`
330
+ decline: they would have differentiated a *noiseless* circuit and returned a
331
+ machine-precision gradient to someone asking about a noisy one. `parameter-shift` and
332
+ `grad_batch` work, because a shift rule never inspects a state.
333
+
334
+ With no noise model these backends reproduce the pure-state ones to machine precision
335
+ — the case that makes the noisy numbers trustworthy, and one the tests assert across
336
+ circuits, observables and both SDKs. Depolarizing noise is checked against the closed
337
+ form `cos(θ)(1 − 4p/3)`, not only against itself.
338
+
339
+ What noise does to trainability is the number worth knowing before the experiment. On
340
+ a 3-qubit, 3-layer ansatz the gradient *direction* survives — correlation above 0.99
341
+ with the noiseless one at `p = 0.05` — while the norm falls to 0.36 of it, and to 0.14
342
+ at `p = 0.1`. [The noise guide](https://ziadt160.github.io/qmlkit/guides/noise/) has
343
+ the table, the traps, and the boundary: error mitigation belongs to
344
+ [Mitiq](https://mitiq.readthedocs.io) and error correction to
345
+ [Stim](https://github.com/quantumlib/Stim).
346
+
347
+ ### And circuits come back in
348
+
349
+ One-way interop is the difference between a library someone *tries* and one someone
350
+ *adopts*: an existing project has circuits already.
351
+
352
+ ```python
353
+ qk.from_qasm(text) # OpenQASM 2.0 -- standard library only, no extras needed
354
+ qk.from_qiskit(circuit) # a QuantumCircuit, unbound Parameters included
355
+ qk.from_pennylane(qnode) # a tape, QNode or quantum function; templates decompose
356
+ qk.from_cirq(circuit) # a cirq.Circuit, sympy symbols included
357
+ ```
358
+
359
+ `from_qasm` takes no dependency on anything: Qiskit, Cirq, Braket, t|ket> and Q# all
360
+ export QASM 2.0, so one stdlib parser reaches all of them. The other three exist
361
+ alongside it because QASM cannot carry a *free parameter*: `from_qiskit` maps unbound
362
+ `Parameter`s onto `ParamRef` in Qiskit's own order, and `from_cirq` does the same for
363
+ `sympy` symbols — `cirq.rx(2 * t)` arrives as `ParamRef(i, scale=2.0)`, since `ParamRef`
364
+ carries `scale * theta + offset` and that is exactly the linear form Cirq produces.
365
+
366
+ Cirq is the importer with nothing to look up: `cirq.S`, `cirq.T` and `cirq.rz` are all
367
+ a `ZPowGate`, separated only by exponent and `global_shift`, so it classifies rather
368
+ than reads a name. One asymmetry worth knowing: Cirq has no declared register, so a
369
+ qubit no operation touches is not in the circuit — the same logical circuit imports
370
+ two qubits wide from Qiskit and one from Cirq.
371
+
372
+ Qubit order is where importers actually break, so it is what the tests check:
373
+ `from_qiskit(to_qiskit(spec))` reproduces the **statevector** to `1e-12` across
374
+ randomly generated circuits, not merely the same list of gates. Qiskit and QASM are
375
+ little-endian and get flipped; PennyLane is big-endian like qmlkit and does not — and
376
+ that claim is asserted against PennyLane's own simulator rather than assumed.
377
+
378
+ A gate qmlkit has no definition for is refused by name, never approximated. The one
379
+ exception is the `u`/`u3` family, decomposed into rotations with a warning that an
380
+ overall phase was dropped — unobservable alone, observable inside a controlled block.
381
+
382
+ ### Why the translations are trustworthy
383
+
384
+ `tests/test_cross_backend.py` runs the same circuit zoo through every installed
385
+ backend and asserts agreement with the NumPy reference on statevectors,
386
+ probabilities, expectations over X/Y/Z and two-body terms, seeded sampling, and
387
+ parameter-shift gradients. The zoo deliberately targets where SDKs differ —
388
+ endianness, controlled-gate qubit order, idle qubits, basis rotations.
389
+
390
+ Three findings from building it, all now handled:
391
+
392
+ | Finding | Handling |
393
+ |---|---|
394
+ | Qiskit is little-endian; qmlkit is big-endian | qmlkit qubit `i` maps to Qiskit qubit `n-1-i` at build time, so the index conventions coincide and no vector reversal is needed |
395
+ | Cirq silently drops qubits a circuit never touches | an explicit `qubit_order` is always passed |
396
+ | **SpinQit's `CY` applies `-iY`**, not `Y`, to the control-1 subspace | emitted as `Sd·CX·S` instead. This is a *relative* phase between control branches, so it changes measurement statistics — not a harmless global phase. SpinQit's single-qubit `Y` is correct |
397
+
398
+ SpinQit's simulator also carries a precision floor near `1e-10` rather than machine
399
+ precision, so it is compared at a looser tolerance. `verify_conventions()` re-checks
400
+ bit order and gate definitions against a live install in one call.
401
+
402
+ ### Cross-validated against PennyLane
403
+
404
+ A library's own test suite can only catch the bugs its author thought of. Agreeing
405
+ with a second, independently written implementation catches the rest.
406
+ `tests/test_pennylane_parity.py` is **301 parity cases** across every layer both
407
+ libraries implement, and it runs in CI like any other test:
408
+
409
+ ```bash
410
+ pip install pennylane
411
+ pytest tests/test_pennylane_parity.py
412
+ ```
413
+
414
+ | Layer | What is compared | Agreement |
415
+ |---|---|---|
416
+ | Gates | all 20 gate matrices at 6 angles, and every closed-form `dU/dθ` against a differenced PennyLane matrix | `1e-12` |
417
+ | Circuits | 40 **randomly generated** circuits over the full gate set, 1–5 qubits — statevectors, probabilities, and random multi-term observables | `1e-12` |
418
+ | Gradients | 5 ansätze × 4 observables; all four exact methods; PennyLane's own four methods back against ours; randomised circuits | `1e-10` |
419
+ | Encodings | angle (X/Y/Z), amplitude, basis, IQP | `1e-12` |
420
+ | Templates | `BasicEntanglerLayers`, `StronglyEntanglingLayers` | `1e-12` |
421
+ | Kernels | full Gram matrices, fidelity and swap-test estimators | `1e-10` |
422
+ | Quantum info | reduced DMs, von Neumann entropy, purity, mutual information, fidelity — over random states | `1e-10` |
423
+ | Fourier | re-uploading spectra at depths 1–4 | `1e-10` |
424
+ | Geometry | Fubini–Study metric (full and diagonal), QFIM | `1e-12` |
425
+ | Optimisers | Rotosolve and QNG trajectories, step by step | `1e-10` |
426
+
427
+ The randomised tests are the ones that matter. Hand-picked cases confirm what the
428
+ author already believed; a fuzzer explores the space, and every bug found in this
429
+ project so far has been of the plausible-wrong-number kind that only a second opinion
430
+ catches.
431
+
432
+ **Four real convention differences surfaced.** None is a bug in either library, and
433
+ each is pinned by its own test so it stays deliberate:
434
+
435
+ | Difference | Detail |
436
+ |---|---|
437
+ | IQP angle convention | PennyLane's `IQPEmbedding` emits `RZ(x_i)` / `MultiRZ(x_i x_j)`; qmlkit follows Qiskit and emits `Rz(2φ)`. Halving the data map reconciles them exactly |
438
+ | Amplitude encoding phase | qmlkit builds it from uniformly-controlled rotations and drops one overall factor. Unobservable — but it stops being global inside a *controlled* block, which the docstring warns about |
439
+ | Two-qubit "ring" | A ring on two qubits would revisit the same pair, so qmlkit collapses it to one `CX`; PennyLane's templates emit both `CNOT(0,1)` and `CNOT(1,0)` |
440
+ | `approx="block-diag"` | PennyLane blocks the metric by *layer* and zeroes cross-layer entries. qmlkit computes the exact metric — free on a simulator — so the same keyword does not port |
441
+
442
+ That last one is not just cosmetic. On a 3-qubit, 2-layer problem at equal step count
443
+ and step size, qmlkit's QNG reaches `-2.9999999` where PennyLane's default
444
+ `block-diag` QNG stalls at `-2.22`. Pointed at the exact metric (`approx=None`),
445
+ PennyLane's optimiser traces qmlkit's trajectory to `1e-8`.
446
+
447
+ One place qmlkit is measurably more accurate: `state_fidelity` hits the analytic
448
+ `|⟨a|b⟩|²` to `1e-16`, while `qml.math.fidelity` takes matrix square roots of rank-1
449
+ density matrices and loses about eight digits.
450
+
451
+ ### Speed
452
+
453
+ `examples/benchmark_pennylane.py` times identical work on both libraries — against
454
+ PennyLane's **fastest** configuration, not its reference one. `pennylane-lightning`
455
+ ships with every PennyLane install, so `lightning.qubit` is always available, and
456
+ `qml.adjoint_metric_tensor` is an `O(P)` route sitting next to the `O(P²)` Hadamard-test
457
+ `qml.metric_tensor`. Benchmarking against the slow option when the fast one is one
458
+ string away would flatter the author.
459
+
460
+ | Operation | qmlkit | PennyLane (best) | | vs `default.qubit` |
461
+ |---|---|---|---|---|
462
+ | Expectation, 12 qubits | 3.9 ms | 4.6 ms `lightning` | 1.2× | 2.9× |
463
+ | Gradient, 8 qubits, `P=96` | 11.0 ms | 11.3 ms `lightning-adjoint` | 1.02× | 6.1× |
464
+ | Parameter-shift, 6 qubits, `P=72` | 300 ms | 347 ms `lightning` | 1.2× | 3.9× |
465
+ | 20×20 kernel Gram matrix | 3.2 ms | 219 ms `default` | **69×** | 69× |
466
+ | Exact metric tensor, `P=24` | 6.8 ms | 715 ms `adjoint_metric` | **105×** | 276× |
467
+
468
+ qmlkit is ahead on 13 of 14 cases, and the honest median is **1.7×**, not the 6.1× an
469
+ earlier version of this file reported against `default.qubit` alone. The 8-qubit
470
+ gradient is a tie either way.
471
+
472
+ **What that means.** The first three rows are dispatch and interpreter overhead rather
473
+ than arithmetic: qmlkit does less per call, so it leads at small register sizes and the
474
+ gap closes as `2ⁿ` starts to dominate — the 8-qubit gradient is a tie. Anyone quoting
475
+ the `default.qubit` column as qmlkit's speed advantage is quoting the wrong number.
476
+
477
+ Two results are real. The **kernel Gram matrix** is ~69×: every entry is the same circuit
478
+ at different angles, so the whole matrix is one batched evaluation, while PennyLane makes
479
+ one QNode call per pair — per-call overhead dominates so completely there that `lightning`
480
+ is actually *slower* than `default.qubit`. And the **metric tensor** is different in kind: closed-form
481
+ differentiation of the state, `P` derivative states from one forward sweep, agreeing with
482
+ PennyLane's own routes to `1.7e-16` and *widening* with parameter count (49× at `P=12`,
483
+ 105× at `P=24`) rather than narrowing. That is the one speed claim here worth making.
484
+
485
+ JAX is not installed on the benchmark machine, so jit-compiled PennyLane is untested and
486
+ unclaimed; it would narrow the overhead rows further.
487
+
488
+ ## What it does today
489
+
490
+ `0.1.0` is the foundation layer:
491
+
492
+ - **A backend-neutral circuit IR.** A circuit is data — a list of `Op`. Backends
493
+ compile it; gradients, resource counting and drawing all read it.
494
+ - **Four backends behind one protocol** — NumPy (exact reference), SpinQit,
495
+ Qiskit and Cirq — with a cross-backend equivalence suite proving they agree.
496
+ A backend supplies a statevector; sampling, basis rotation and expectation
497
+ semantics are defined once in the base class.
498
+ - **Pauli observables** — `Z(0)`, `ZZ(0, 1)`, weighted sums — with one
499
+ `expectation()` that is correct for any register width.
500
+ - **The full encoding layer** — basis, angle, amplitude, Hamiltonian and data
501
+ re-uploading, plus the `Z`/`ZZ`/`Pauli` feature-map family.
502
+ - **Exact parameter-shift gradients**, including the two cases that are easy to
503
+ get silently wrong (see below).
504
+ - **Shot-budget arithmetic** — standard errors, and the shots needed for a target
505
+ precision.
506
+ - **An ansatz vocabulary** — `RotationLayer`, `EntanglerLayer`, `PoolLayer`,
507
+ `repeat`, `share` — plus a zoo (hardware-efficient, strongly-entangling,
508
+ two-design, QAOA, TTN, MPS, QCNN) that is written *in* that vocabulary.
509
+ - **Six gradient methods** behind one `grad()`: adjoint and backprop (exact, one
510
+ pass), parameter-shift and Hadamard-test (exact, hardware-valid), SPSA (two
511
+ evaluations, any `P`), and finite differences for debugging.
512
+ - **A PyTorch bridge** — `QuantumLayer`, `VQC`, `VQRegressor` — where inputs get
513
+ gradients, so classical layers placed *before* the quantum one actually train.
514
+ - **Quantum kernels** — fidelity / swap / Hadamard estimators, Gram matrices with PSD
515
+ repair, `QSVC`/`QSVR`, trainable kernels, and projected kernels that survive the
516
+ concentration that kills the fidelity kernel at width.
517
+ - **Structured models** — `QCNNLayer`, `MPSLayer`, `QLSTM`, `DressedQuantumNet`; and
518
+ generative ones — `QCBM`, `QGAN`, `QuantumBoltzmannMachine`, `QuantumHopfield`.
519
+ - **Analysis** — expressibility, Meyer–Wallach entanglement, barren-plateau scans,
520
+ effective dimension, Fourier spectra, and `draw()` / `specs()`.
521
+ - **Diagnostics** — `diagnose()` for the circuit (dead parameters, collapsed
522
+ re-uploading, concentrated kernels, flat gradients) and `selfcheck()` for the number
523
+ (every exact gradient route compared against every other, every backend against the
524
+ reference).
525
+ - **Evaluation** — `qk.evaluate` returns every metric a task needs at once and says
526
+ when one is misleading; `qk.imbalance` handles the loss and the split that skew
527
+ breaks; `qk.baseline` runs the classical bar on identical folds and refuses to call
528
+ a lead inside the fold spread a result.
529
+ - **Budget and provenance** — `qk.plan()` costs a run in circuits before it starts;
530
+ `qk.fingerprint()` records the versions, backend and seed that produced a number.
531
+ - **Circuit import** — `from_qasm` (standard library only), `from_qiskit` (unbound
532
+ parameters included), `from_pennylane` (templates decompose) and `from_cirq`
533
+ (`sympy` symbols carried onto `ParamRef`), so circuits come back in as well as out.
534
+ - **Noise, named explicitly** — `cirq-density` and `qiskit-aer` evolve a density
535
+ matrix and take a noise model. `shots=None` still means shot-free, so decoherence
536
+ and sampling error stay separable; the gradients that need a pure state refuse.
537
+ - **Batched execution** — one circuit at many parameter vectors in a single pass;
538
+ 3.6–24× on the training forward pass up to 10 qubits.
539
+
540
+ ## Gradients
541
+
542
+ ```python
543
+ qk.grad(spec, theta, obs) # auto: adjoint when it can, shift when it can't
544
+ qk.grad(spec, theta, obs, method="parameter-shift", shots=4096)
545
+ ```
546
+
547
+ | Method | Cost | Exact | On hardware |
548
+ |---|---|---|---|
549
+ | `adjoint` | one backward pass | yes | no — needs the statevector |
550
+ | `backprop` | one autograd pass (torch) | yes | no — needs the statevector |
551
+ | `hadamard` | `P` circuits, one ancilla | yes | yes, if the ancilla can reach every wire |
552
+ | `parameter-shift` | `2P` circuits (more for multi-frequency gates) | yes | yes |
553
+ | `spsa` | 2 evaluations, any `P` | no — unbiased estimate | yes |
554
+ | `finite-diff` | `2P` | no — `O(h²)` bias | debugging only |
555
+
556
+ All four exact methods agree to machine precision — they are four independent
557
+ routes to the same number, which is exactly why disagreement between them is a
558
+ useful bug detector. Measured on a 5-qubit hardware-efficient ansatz with a
559
+ two-term observable:
560
+
561
+ | `P` | `adjoint` | `backprop` | `hadamard` | `parameter-shift` | `finite-diff` |
562
+ |---|---|---|---|---|---|
563
+ | 20 | **2.2 ms** | 8.5 ms | 15 ms | 28 ms | 29 ms |
564
+ | 60 | **6.2 ms** | 24 ms | 109 ms | 213 ms | 226 ms |
565
+ | 120 | **12.6 ms** | 50 ms | 404 ms | 823 ms | 870 ms |
566
+
567
+ Adjoint is the default on a simulator because its cost does not grow with `P` —
568
+ **65× faster than parameter-shift at `P=120`**, and the gap widens from there.
569
+ `hadamard` halves the circuit count against parameter-shift, and on a simulator
570
+ that shows up as roughly half the wall-clock too; the trade is an ancilla that
571
+ must couple to every wire the generator touches. On real hardware that routing
572
+ cost usually eats the saving, which is why parameter-shift stays the default
573
+ there. `backprop` needs `pip install 'qmlkit[torch]'` and exists mainly so a
574
+ circuit can sit inside an autograd graph — for a standalone gradient, adjoint is
575
+ both faster and lighter on memory.
576
+
577
+ Second derivatives come from differencing the *exact* gradient, so only the outer
578
+ derivative is approximate:
579
+
580
+ ```python
581
+ qk.hessian(spec, theta, obs) # (P, P), symmetric
582
+ qk.gradient_cost(spec, "parameter-shift") # circuits one gradient would cost
583
+ ```
584
+
585
+ ## The parameter-shift rule, done properly
586
+
587
+ Two things about parameter-shift produce a *plausible wrong number* rather than an
588
+ exception. Both are handled here, and both have tests.
589
+
590
+ **Shift rules belong to the gate, not the call.** A gate's rule is determined by
591
+ the unique positive gaps between its generator's eigenvalues. `ry` has one
592
+ frequency (the familiar ±π/2, ±½ rule); `crz` has two and needs four terms. A
593
+ circuit mixing them needs both, looked up per gate:
594
+
595
+ ```python
596
+ qc = qk.QCircuit(2)
597
+ qc.ry(0, qk.ParamRef(0))
598
+ qc.crz(0, 1, qk.ParamRef(1))
599
+ spec = qc.to_spec()
600
+
601
+ qk.grad_circuit_cost(spec) # 6, not 2*2 -- the CRZ costs four evaluations
602
+ qk.param_shift_grad_circuit(spec, theta, qk.Z(1))
603
+ ```
604
+
605
+ Rules are **derived**, not transcribed: declare a gate's `frequencies` and the
606
+ right rule is solved for. A gate with no declared frequencies is refused rather
607
+ than differentiated incorrectly.
608
+
609
+ **Shared parameters shift one occurrence at a time.** When one logical parameter
610
+ drives several gates — weight tying, as in a QCNN's shared convolution block — the
611
+ derivative is the *sum over occurrences*, each shifted on its own. Shifting them
612
+ together computes something else entirely:
613
+
614
+ ```python
615
+ qc = qk.QCircuit(3)
616
+ shared = qc.param()
617
+ qc.rotation_layer(("ry",), shared=shared) # one parameter, three gates
618
+ spec = qc.to_spec()
619
+
620
+ len(spec.occurrences_of(0)) # 3
621
+ qk.param_shift_grad_circuit(spec, [0.7], qk.Z(0) + qk.Z(1) + qk.Z(2))
622
+ ```
623
+
624
+ **Gradients flow through the encoding too.** `∂f/∂x` is available from the same
625
+ rule, which is what lets a classical pre-net in a hybrid stack actually train:
626
+
627
+ ```python
628
+ spec = qk.angle_encode([0.4, 1.1], trainable=True)
629
+ qk.param_shift_grad_circuit(spec, [0.4, 1.1], qk.Z(0)) # -> [-sin(0.4), 0]
630
+ ```
631
+
632
+ ## Encoding
633
+
634
+ ```python
635
+ qk.angle_encode([0.3, 1.1]) # one feature per qubit
636
+ qk.amplitude_encode([1, 2, 3, 4]) # 2**n numbers in n qubits
637
+ qk.hamiltonian_encode(x, t=1.0, steps=3) # data-dependent Ising evolution
638
+
639
+ fm = qk.ZZFeatureMap(n_features=3, reps=2)
640
+ fm.build(x) # U(x)
641
+ fm.adjoint(x) # U(x)^dagger -- the other half of a kernel
642
+ ```
643
+
644
+ The compute-uncompute kernel falls straight out of `adjoint()`, and matches the
645
+ exact overlap to machine precision:
646
+
647
+ ```python
648
+ k = qk.probabilities(fm.build(x).compose(fm.adjoint(xp)))[0] # P(all zeros) IS k(x, x')
649
+ ```
650
+
651
+ **`PauliFeatureMap` is built from its two pieces, and both are public.** A Pauli
652
+ feature map needs a basis change that diagonalises each string and a data map that
653
+ turns features into angles; they are usually left implicit inside one function. Here
654
+ they are `basis_change` and `default_data_map`, so either can be replaced without
655
+ rewriting the map — and the maps are tested against the analytic kernels they are
656
+ supposed to induce rather than against themselves: the angle map's `cos²((x−x')/2)`,
657
+ the Z map's factorisation, and the ZZ map's failure to factorise.
658
+
659
+ **Amplitude encoding is built from uniformly-controlled rotations**, not a backend
660
+ state-preparation primitive. So it emits only `ry`/`rz`/`cx`, runs identically on
661
+ every backend, and its exponential gate cost is visible rather than hidden inside
662
+ an SDK call.
663
+
664
+ ```python
665
+ qk.reduce_to_qubits(X, n_qubits=3) # PCA (plain SVD) + rescale into [0, 2pi)
666
+ qk.AngleScaler().fit(X_train).transform(X_test) # one range for both splits
667
+ ```
668
+
669
+ ## Shots are opt-in
670
+
671
+ `shots=None` (the default) returns the exact expectation. Pass `shots=N` to model
672
+ a real device, and ask for the uncertainty alongside the value:
673
+
674
+ ```python
675
+ value, err = qk.expectation(spec, qk.Z(0), shots=4096, return_std=True)
676
+ qk.shots_for_precision(0.01) # what a target precision actually costs
677
+ ```
678
+
679
+ ## Roadmap
680
+
681
+ | Phase | Status |
682
+ |---|---|
683
+ | 0 · IR, NumPy backend, observables, execution, parameter-shift | **done** |
684
+ | 1 · SpinQit, Qiskit and Cirq backends + cross-backend suite | **done** |
685
+ | 2 · Encodings and feature maps (Z, ZZ, Pauli, re-uploading) | **done** |
686
+ | 3 · Gradients: adjoint, SPSA, dispatch registry | **done** |
687
+ | 3c · Hadamard-test and backprop gradients, `hessian`, `gradient_cost` | **done** |
688
+ | 3b · Ansatz vocabulary, zoo, and registry | **done** |
689
+ | 4 · Torch bridge: `QuantumLayer`, `VQC`, `VQRegressor` | **done** |
690
+ | 5 · Quantum kernels, `QSVC`/`QSVR` | **done** |
691
+ | 6 · QCNN, QLSTM, MPS; QCBM, qGAN, QBM | **done** |
692
+ | 7 · Docs, tutorials, `v0.1.0` on PyPI | docs and tutorials **done**; PyPI pending |
693
+
694
+ Beyond `0.1.0`, in the order they are likely to matter: a *mitigation verdict* (whether error mitigation improved an
695
+ estimate or only traded bias for variance, on identical seeds with the shot cost
696
+ stated — the implementations belong to [Mitiq](https://mitiq.readthedocs.io)),
697
+ gradients through a noise channel, and batched/async submission for a real device.
698
+
699
+ ## Development
700
+
701
+ ```bash
702
+ pip install -e ".[dev]"
703
+ pytest
704
+ ruff check src tests
705
+ mypy
706
+ ```
707
+
708
+ Runnable examples, none of which quotes a number it did not compute:
709
+
710
+ ```bash
711
+ python examples/quickstart.py # every layer of the library, end to end
712
+ python examples/credit_risk.py # a real dataset, start to finish, with every
713
+ # decision made by a diagnostic
714
+ python examples/accelerate_pennylane.py # borrow three inner loops, migrate nothing
715
+ python examples/compare_pennylane.py # readable cross-check against PennyLane
716
+ python examples/benchmark_pennylane.py # wall-clock, same work on both sides
717
+ ```
718
+
719
+ `credit_risk.py` is the one to read if you want to know what the library is *for*. It
720
+ takes 32,581 loan applications, works through the skew, the classical bar, the circuit
721
+ budget, an ansatz chosen from measured expressibility and gradient variance, and a
722
+ kernel that is diagnosed rather than fitted — and it ends by reporting that the
723
+ quantum model **lost**, on the same folds and the same inputs. A negative result you
724
+ can defend is worth more than a positive one you cannot.
725
+
726
+ The exhaustive version of the second one lives in the test suite, so it guards every
727
+ future change rather than only today's:
728
+
729
+ ```bash
730
+ pytest tests/test_pennylane_parity.py # 301 parity cases
731
+ ```
732
+
733
+ Before a release, verify the *built* artifact rather than the source tree — an
734
+ editable install imports out of `src/` and keeps working even if a module never made
735
+ it into the wheel:
736
+
737
+ ```bash
738
+ python -m build && python -m venv /tmp/clean && /tmp/clean/bin/pip install dist/qmlkit-*.whl && /tmp/clean/bin/python scripts/verify_install.py
739
+ ```
740
+
741
+ [`RELEASING.md`](https://github.com/Ziadt160/qmlkit/blob/main/RELEASING.md) has the rest of the process.
742
+
743
+ ## License
744
+
745
+ Apache-2.0 · © 2026 Ziad Tarek Mohammed