qmlkit 0.1.0__tar.gz → 0.2.0__tar.gz
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-0.1.0 → qmlkit-0.2.0}/.github/workflows/docs.yml +6 -1
- qmlkit-0.2.0/AGENTS.md +144 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/CHANGELOG.md +441 -0
- qmlkit-0.2.0/HANDOFF.md +682 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/PKG-INFO +238 -113
- {qmlkit-0.1.0 → qmlkit-0.2.0}/README.md +237 -112
- {qmlkit-0.1.0 → qmlkit-0.2.0}/RELEASING.md +27 -31
- qmlkit-0.2.0/docs/about/validation.md +213 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/agents.md +1 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/choosing-a-gradient.md +38 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/evaluation.md +2 -0
- qmlkit-0.2.0/docs/guides/from-pennylane.md +142 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/index.md +2 -0
- qmlkit-0.2.0/docs/guides/watching-a-run.md +124 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/index.md +39 -32
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/llms-full.txt +683 -80
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/llms.txt +14 -5
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/evaluation.md +18 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/02-quantum-kernels.md +10 -6
- qmlkit-0.2.0/docs/studies/08-selective-classification.md +134 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/index.md +3 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/06-quantum-kernels.md +6 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/credit_risk.py +21 -11
- {qmlkit-0.1.0 → qmlkit-0.2.0}/mkdocs.yml +4 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/pyproject.toml +1 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/scripts/generate_llms_txt.py +10 -4
- qmlkit-0.2.0/scripts/probe_dispatch.py +79 -0
- qmlkit-0.2.0/scripts/probe_fusion.py +77 -0
- qmlkit-0.2.0/scripts/probe_qrack.py +84 -0
- qmlkit-0.2.0/scripts/probe_threads.py +88 -0
- qmlkit-0.2.0/scripts/proto_fusion.py +123 -0
- qmlkit-0.2.0/scripts/proto_fusion2.py +109 -0
- qmlkit-0.2.0/scripts/proto_fusion3.py +119 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/scripts/verify_install.py +12 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/__init__.py +6 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/_aliases.py +1 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/adapt.py +2 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/autoencoder.py +7 -10
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/qaoa.py +1 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/vqe.py +11 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/ansatz/blocks.py +92 -8
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/ansatz/library.py +72 -5
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/ansatz/reupload.py +32 -23
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/baselines.py +10 -3
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/base.py +32 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/torch_backend.py +8 -2
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/execute.py +15 -2
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/observables.py +80 -6
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/diagnostics.py +234 -16
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/evaluate.py +196 -7
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/info.py +33 -5
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/kernels/matrix.py +66 -15
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/layer.py +10 -2
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/models.py +39 -13
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/optim.py +101 -1
- qmlkit-0.2.0/src/qmlkit/progress.py +367 -0
- qmlkit-0.2.0/src/qmlkit/report.py +238 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/utils/shots.py +15 -3
- qmlkit-0.2.0/tests/densesim.py +206 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_advanced.py +129 -4
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_analysis.py +431 -0
- qmlkit-0.2.0/tests/test_contribution.py +105 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_core.py +4 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_docs.py +35 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_evaluate.py +122 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_kernels.py +40 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_noisy_backends.py +1 -1
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_observables.py +76 -0
- qmlkit-0.2.0/tests/test_optimizer_wiring.py +77 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_pennylane_parity.py +3 -1
- qmlkit-0.2.0/tests/test_progress.py +182 -0
- qmlkit-0.2.0/tests/test_report.py +188 -0
- qmlkit-0.2.0/tests/test_torture.py +388 -0
- qmlkit-0.1.0/AGENTS.md +0 -81
- qmlkit-0.1.0/HANDOFF.md +0 -228
- qmlkit-0.1.0/docs/about/validation.md +0 -141
- {qmlkit-0.1.0 → qmlkit-0.2.0}/.gitattributes +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/.github/workflows/ci.yml +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/.github/workflows/release.yml +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/.gitignore +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/LICENSE +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/NOTICE +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/about/changelog.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/about/releasing.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/about/stability.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/backends.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/extending.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/noise.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/parameter-shift.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/install.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/javascripts/mathjax.js +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/algorithms.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/analysis.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/ansatz.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/core.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/encoding.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/gradients.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/index.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/kernels.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/nn.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/01-imbalanced-classification.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/03-regression.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/04-chemistry.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/05-beyond-classification.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/06-clinical.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/07-images-and-structure.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/01-first-circuit.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/02-encoding-data.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/03-gradients.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/04-ansatz-design.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/05-training-torch.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/07-reuploading.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/08-trainability.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/index.md +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/accelerate_pennylane.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/benchmark_pennylane.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/compare_pennylane.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/credit_data.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/experiments.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/head_to_head.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/quickstart.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/toward_hardware.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/__init__.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/chemistry.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/clustering.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/hamiltonians.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/molecule.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/rl.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/ansatz/__init__.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/budget.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/__init__.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/__init__.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/_sampling.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/cirq_backend.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/cirq_density_backend.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/noisy.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/numpy_backend.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/qiskit_aer_backend.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/qiskit_backend.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/registry.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/spinqit_backend.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/builder.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/gates.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/ir.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/datasets.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/draw.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/__init__.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/amplitude.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/angle.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/feature_maps.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/hamiltonian.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/pipeline.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/scaling.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/fourier.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/generative.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/__init__.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/adjoint.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/batch.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/dispatch.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/hadamard.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/parameter_shift.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/rules.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/spsa.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/imbalance.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/interop.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/kernels/__init__.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/kernels/estimators.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/kernels/models.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/metrics.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/__init__.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/advanced.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/losses.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/provenance.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/py.typed +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/search.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/shadows.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/utils/__init__.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/utils/errors.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_agent_api.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_algorithms.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_ansatz.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_baseline.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_batch.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_budget.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_builder.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_cross_backend.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_encoding.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_grad_batch.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_gradient_methods.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_gradients.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_imbalance.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_import.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_injection.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_interop.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_nn.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_provenance.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_search.py +0 -0
- {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_spinqit_backend.py +0 -0
|
@@ -46,7 +46,12 @@ jobs:
|
|
|
46
46
|
|
|
47
47
|
deploy:
|
|
48
48
|
name: Publish to GitHub Pages
|
|
49
|
-
|
|
49
|
+
# A manual run has to be able to publish, or `workflow_dispatch` is a build
|
|
50
|
+
# button with no effect - which is exactly when it is reached for, because the
|
|
51
|
+
# push trigger did not fire.
|
|
52
|
+
if: >-
|
|
53
|
+
github.ref == 'refs/heads/main'
|
|
54
|
+
&& (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
|
50
55
|
needs: build
|
|
51
56
|
runs-on: ubuntu-latest
|
|
52
57
|
permissions:
|
qmlkit-0.2.0/AGENTS.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Instructions for a coding agent working **on** qmlkit. To *use* the library, read
|
|
4
|
+
[`docs/llms.txt`](docs/llms.txt) instead — it is the same information written for
|
|
5
|
+
the caller rather than the contributor.
|
|
6
|
+
|
|
7
|
+
[`HANDOFF.md`](HANDOFF.md) is the long version of this file: current status, why
|
|
8
|
+
each convention exists, and what to do next. Read it before anything non-trivial.
|
|
9
|
+
This page is the short list of things that will break the build if you get them
|
|
10
|
+
wrong.
|
|
11
|
+
|
|
12
|
+
## The map
|
|
13
|
+
|
|
14
|
+
19,000 lines over 79 modules, 196 names in the top-level `__all__`. Two halves:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/qmlkit/
|
|
18
|
+
core/ the IR, and only the IR: ir · builder · gates · observables · execute
|
|
19
|
+
core/backends/ seven of them. base.py supplies the semantics; a backend supplies
|
|
20
|
+
statevector() and inherits sampling, grouping, expectation, batching
|
|
21
|
+
ansatz/ encoding/ gradients/ kernels/ nn/ the ML layer
|
|
22
|
+
algorithms/ VQE · ADAPT · QAOA · chemistry · autoencoder · clustering · rl
|
|
23
|
+
*.py the honesty layer — diagnostics · baselines · budget · evaluate ·
|
|
24
|
+
imbalance · provenance · search · progress · report · metrics
|
|
25
|
+
utils/errors.py how every "unknown X" error in the library is built
|
|
26
|
+
_aliases.py PennyLane and Qiskit names, answered with the qmlkit one
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The top-level modules are the point of the project. `core/` is machinery in service
|
|
30
|
+
of them.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e ".[dev,torch,qiskit,cirq,sklearn,pennylane]"
|
|
36
|
+
pytest -q # the whole suite
|
|
37
|
+
pytest -q -m "not pennylane" # faster, skips the parity cases
|
|
38
|
+
ruff check src tests && ruff format --check src tests && mypy
|
|
39
|
+
python scripts/generate_llms_txt.py # after any docs or public-API change
|
|
40
|
+
python -m mkdocs build --strict # after any docs change; CI runs it
|
|
41
|
+
python scripts/verify_install.py # the core really does import with only NumPy
|
|
42
|
+
QMLKIT_TORTURE_EXAMPLES=1500 pytest tests/test_torture.py # ~10 min, before a release
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
SpinQit needs its own interpreter — it ships wheels for Python 3.8–3.10 only and
|
|
46
|
+
pins `numpy<2`:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
C:/Users/pc/miniconda3/envs/spinq_env/python.exe -m pytest -m spinqit
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Rules the tests enforce
|
|
53
|
+
|
|
54
|
+
1. **An algorithm owns its loop, not its circuit.** Every model takes `ansatz=` /
|
|
55
|
+
`feature_map=` / `filter=` and must actually use it. `tests/test_injection.py`
|
|
56
|
+
injects two different sizes and asserts the parameter count follows.
|
|
57
|
+
2. **Estimators must be scikit-learn clonable.** Constructor arguments stored under
|
|
58
|
+
their own names, plus `SklearnCompatible`. This keeps scikit-learn optional while
|
|
59
|
+
letting QSVC/QSVR run inside `Pipeline` and `GridSearchCV`.
|
|
60
|
+
3. **The core depends on NumPy and nothing else.** Not SciPy — use `math.erf` and the
|
|
61
|
+
stdlib. CI installs nothing else in the `core` jobs, and this has been broken once.
|
|
62
|
+
4. **Documentation is executable.** `tests/test_docs.py` runs every Python block on
|
|
63
|
+
every page, so an API change and its docs go in the same commit.
|
|
64
|
+
5. **Names have to stay findable.** `qmlkit/_aliases.py` maps what PennyLane and
|
|
65
|
+
Qiskit call each thing; `tests/test_agent_api.py` asserts every target still
|
|
66
|
+
exists. Rename a public name and you update that table in the same commit.
|
|
67
|
+
6. **`docs/llms.txt` is generated and committed.** Change the docs or the public API
|
|
68
|
+
and regenerate it, or CI fails on the stale copy.
|
|
69
|
+
|
|
70
|
+
## Traps that have already cost time
|
|
71
|
+
|
|
72
|
+
- **Never use a NumPy-2-only API** (`np.trapezoid`, `np.in1d`, …) in `src/` or
|
|
73
|
+
`tests/`. SpinQit pins `numpy<2` and the suite must pass in both environments.
|
|
74
|
+
- **Always write `npt.NDArray[Any]`, never bare `np.ndarray`.** Type-parameter
|
|
75
|
+
defaults only arrived in NumPy 2.3, so 3.10 CI fails with 60 `type-arg` errors.
|
|
76
|
+
- **Never set `python_version` in `[tool.mypy]`.** It makes mypy parse dependency
|
|
77
|
+
stubs at that version too, and NumPy's stubs use PEP 695 `type` statements, which
|
|
78
|
+
are a syntax error before 3.12. Cross-version signal comes from CI running mypy
|
|
79
|
+
on 3.10.
|
|
80
|
+
- **The PennyLane parity fuzzer draws gate names from a snapshot taken at import**,
|
|
81
|
+
not from the live registry — other test modules register throwaway gates at run
|
|
82
|
+
time, which made it pass alone and fail in a full run.
|
|
83
|
+
- **Extend `tests/test_pennylane_parity.py` when adding a gate.** A test there
|
|
84
|
+
asserts the mapping covers every built-in gate, so a new one cannot escape
|
|
85
|
+
cross-validation. Every bug found in this project has been the
|
|
86
|
+
plausible-wrong-number kind that only a second implementation catches.
|
|
87
|
+
|
|
88
|
+
## Where a change has to land
|
|
89
|
+
|
|
90
|
+
The library is built on registries, so adding something is usually one call — and
|
|
91
|
+
then three or four places that will not fail loudly if you forget them.
|
|
92
|
+
|
|
93
|
+
| Adding | Also touch |
|
|
94
|
+
|---|---|
|
|
95
|
+
| **A gate** | `frequencies` on the `GateDef` or differentiation is refused; `dmatrix` or adjoint is refused; the PennyLane mapping in `tests/test_pennylane_parity.py`, which asserts it covers every built-in gate |
|
|
96
|
+
| **An ansatz or conv filter** | `register_*`, and check it is not inert — an all-`rz` filter does nothing at all from `|0…0⟩`, which is why `test_no_shipped_filter_is_inert` exists |
|
|
97
|
+
| **A backend** | `supports_exact` and `supports_statevector` are a contract, not metadata. Then **sweep the public API against it**: adding the noisy backends broke `diagnose`, `expressibility`, `entangling_capability`, `fidelity_samples`, `metric_tensor` and `qng_step`, and the suite did not notice |
|
|
98
|
+
| **A diagnostic finding** | Write the test so it asserts the finding is **true**, not that it fired. The old tests asserted firing, which is how three wrong findings survived. A false positive here is worse than a false negative: it teaches people to ignore the tool |
|
|
99
|
+
| **Anything public** | `docs/llms.txt` regenerated, a reference page entry, and a CHANGELOG entry |
|
|
100
|
+
| **A version bump** | `pyproject.toml` **and** `src/qmlkit/__init__.py`. A third hardcoded copy in `scripts/verify_install.py` once failed the release gate |
|
|
101
|
+
|
|
102
|
+
**Releasing is a tag on this repository**, which `release.yml` turns into a TestPyPI
|
|
103
|
+
then PyPI publish over Trusted Publishing - no token is handled anywhere. It asserts
|
|
104
|
+
the tag matches `pyproject.toml` before it builds. A PyPI version number can never be
|
|
105
|
+
reused, so tags stay unpushed until the release is actually wanted, and a release that
|
|
106
|
+
half-completes burns the number. `RELEASING.md` has the procedure.
|
|
107
|
+
|
|
108
|
+
Until 2026-09-13 this repo was a `git subtree split` of a subdirectory in the lecture
|
|
109
|
+
repository. If you find a document that still says so, it is stale - fix it.
|
|
110
|
+
|
|
111
|
+
## Writing
|
|
112
|
+
|
|
113
|
+
The prose is a deliberate artefact. Match it rather than inventing a second voice.
|
|
114
|
+
|
|
115
|
+
- **Claims enter through the failure they prevent**, never through the feature name.
|
|
116
|
+
The order is: here is a way to be wrong, here is why it does not raise, here is the
|
|
117
|
+
call. Almost nothing opens with "qmlkit provides".
|
|
118
|
+
- **Every number carries its provenance.** "Measured on a 5-qubit hardware-efficient
|
|
119
|
+
ansatz"; "measured: five frequencies". A bare number reads as an unsupported claim.
|
|
120
|
+
- **Caveats get their own sentence, in the same voice as the claims**, usually last and
|
|
121
|
+
usually against the author's interest — "JAX is not installed on the benchmark
|
|
122
|
+
machine, so jit-compiled PennyLane is untested and unclaimed."
|
|
123
|
+
- **Headings are assertions, not labels**: "Data re-uploading is a pattern, not a
|
|
124
|
+
structure"; "Noise, when you ask for it by name". Label headings mark reference
|
|
125
|
+
sections, and the contrast is the point.
|
|
126
|
+
- A long sentence that does the analysis, then a short one that lands it. British
|
|
127
|
+
spelling. Second person about the reader's actions, never their level.
|
|
128
|
+
|
|
129
|
+
## Design commitments
|
|
130
|
+
|
|
131
|
+
- **Simulator-only for the whole 0.x line.** This is a constraint that propagates,
|
|
132
|
+
not a scope trim: it makes `adjoint` the correct default gradient, makes shot
|
|
133
|
+
noise opt-in, and demotes anything whose value is cutting *measurement* cost.
|
|
134
|
+
Parameter-shift stays the teaching subject and the reference that validates
|
|
135
|
+
adjoint — never the performance default.
|
|
136
|
+
- **Simple on top, open underneath.** Three layers, and nothing at a higher one
|
|
137
|
+
hides a lower one. Every extension point is a registry.
|
|
138
|
+
- **When something is named after a pattern rather than a structure, make it a
|
|
139
|
+
composition, not a class.** Data re-uploading is `EncodingLayer` in the block
|
|
140
|
+
vocabulary, with `reupload()` as a convenience over it.
|
|
141
|
+
- **The error message is the documentation.** Most callers are models that will not
|
|
142
|
+
read the docs site; they read the traceback. An error about a name says what was
|
|
143
|
+
wrong, what was probably meant, and what is allowed — build it with
|
|
144
|
+
`qmlkit.utils.errors.unknown`.
|
|
@@ -4,6 +4,447 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-09-13
|
|
8
|
+
|
|
9
|
+
**Upgrading from 0.1.0? This release contains eleven correctness fixes as well as the
|
|
10
|
+
features below, and four of those defects corrupt a result silently.** They were
|
|
11
|
+
prepared as 0.1.1, which was never published — everything in that section further down
|
|
12
|
+
ships here. The worst of them is in the torch backend, which computed a *different
|
|
13
|
+
circuit* from every other backend, so `backend="torch"` and `method="backprop"` returned
|
|
14
|
+
wrong numbers and nothing raised. If you are on 0.1.0, upgrade.
|
|
15
|
+
|
|
16
|
+
### Fixed - a documented optimiser that never ran
|
|
17
|
+
|
|
18
|
+
`OPTIMIZERS` has four entries and two of them need the gradient injected by the caller,
|
|
19
|
+
but every injection site named `gradient-descent` alone. `optimizer="adam"` therefore
|
|
20
|
+
raised `TypeError: _adam() missing 1 required keyword-only argument: 'grad'` from `VQE`,
|
|
21
|
+
`QAOA` and `AdaptVQE` alike — one of the four documented optimiser names could not be
|
|
22
|
+
used from any of the three algorithms that list it.
|
|
23
|
+
|
|
24
|
+
`tests/test_optimizer_wiring.py` now parametrises over `OPTIMIZERS` itself rather than a
|
|
25
|
+
hand-written list, so a fifth entry cannot be added without being covered, and one test
|
|
26
|
+
drives both gradient routes to the known ground state — an injected gradient that was
|
|
27
|
+
*wrong* would pass a wiring test and fail that one.
|
|
28
|
+
|
|
29
|
+
Also fixed: two docstrings whose LaTeX had been eaten by a shell heredoc (`\rho` and
|
|
30
|
+
`\rangle` became line breaks, `\theta` became a tab), and three references to things
|
|
31
|
+
that do not exist — `kernel.matrix(X)` in a user-facing error message where the call is
|
|
32
|
+
`kernel(X)`, `qk.datasets.moons` in a live doctest where it is `make_moons`, and an
|
|
33
|
+
"AmplitudeEncoder feature map" in the PennyLane alias table.
|
|
34
|
+
|
|
35
|
+
### Added - you can watch a run instead of waiting for it
|
|
36
|
+
|
|
37
|
+
`qk.plan` says what a run will cost before it starts and `qk.diagnose` says what went
|
|
38
|
+
wrong after it finishes. In between there was nothing, and in between is where the
|
|
39
|
+
hours go — a quantum kernel on a few hundred points is tens of thousands of circuits
|
|
40
|
+
behind a single silent call that returns when it returns. "How much is left" is one
|
|
41
|
+
of the most common questions asked about every library in this field, and none of
|
|
42
|
+
them answers it.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
with qk.progress() as run:
|
|
46
|
+
gram = kernel(X)
|
|
47
|
+
model.fit(X, y)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
kernel gram 3,412/12,720 27% 14.2s elapsed ~37s left
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
and afterwards, where the time actually went:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
>>> print(run.report())
|
|
58
|
+
Run finished in 51.4s
|
|
59
|
+
kernel gram 12,720 items 47.9s 3.77 ms/item
|
|
60
|
+
fit VQC 600 items 3.2s 5.33 ms/item
|
|
61
|
+
(untracked) 0.3s - setup, data handling, and anything outside a task
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Three properties it holds to, in priority order, each with a test:
|
|
65
|
+
|
|
66
|
+
1. **It does not change any number.** A seeded kernel and a seeded circuit produce
|
|
67
|
+
bit-identical results watched and unwatched. A reporter that perturbed a result
|
|
68
|
+
would be a worse defect than the silence it replaces.
|
|
69
|
+
2. **It is free when nobody is watching.** With no active reporter, the tracking
|
|
70
|
+
calls inside the library cost one comparison against `None`, and the live line
|
|
71
|
+
redraws at most ten times a second however fast the loop runs.
|
|
72
|
+
3. **It does not claim to know what it does not.** An estimate from four items in
|
|
73
|
+
half a second says more about scheduling noise than about the run, so it prints
|
|
74
|
+
`estimating` until there is evidence — the same rule the rest of the library
|
|
75
|
+
follows about reporting a measurement.
|
|
76
|
+
|
|
77
|
+
Wired into the two loops that actually take the time: the pair-at-a-time kernel Gram
|
|
78
|
+
(sampled kernels, non-inversion estimators, and any backend without a statevector)
|
|
79
|
+
and `HybridModel.fit`, which covers `VQC` and `VQRegressor`. `qk.track` wraps any
|
|
80
|
+
iterable, and `qmlkit.progress.task` is what library code calls — it returns a silent
|
|
81
|
+
stand-in when nothing is watching, so no loop needs to branch on it.
|
|
82
|
+
|
|
83
|
+
Nothing is printed unless `qk.progress()` is entered, and the live line goes to
|
|
84
|
+
stderr so piping a script's stdout to a file does not collect redraws.
|
|
85
|
+
|
|
86
|
+
### Added - the run writes itself up
|
|
87
|
+
|
|
88
|
+
A run produces a number, and a month later the number is all that is left. A run now
|
|
89
|
+
also records its *trajectory* — `run.log(name, value, step)` — and can write the whole
|
|
90
|
+
thing out as one HTML file:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
with qk.progress() as run:
|
|
94
|
+
run.note(dataset="breast-cancer", seed=0)
|
|
95
|
+
model.fit(X, y)
|
|
96
|
+
|
|
97
|
+
run.save_html("run.html")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`HybridModel.fit` logs three series per epoch, so `VQC` and `VQRegressor` get this
|
|
101
|
+
without asking: **loss**, **gradient norm** and **parameter norm**. The second is
|
|
102
|
+
there because loss alone cannot tell a solved problem from a plateau, and the third
|
|
103
|
+
because weights running away looks like nothing at all in a loss curve. Computing
|
|
104
|
+
them costs a pass over the parameters, so it happens only when something is watching.
|
|
105
|
+
|
|
106
|
+
The page is deliberately a *file*, not a server. A dashboard you have to start,
|
|
107
|
+
connect to and keep alive is a dependency, a port and a process; a page you can open,
|
|
108
|
+
email, and drop next to the result in a directory is none of those and outlives all
|
|
109
|
+
of them. The charts are inline SVG for the same reason: nothing to fetch, nothing to
|
|
110
|
+
pin, nothing to break in two years. Tests assert it — no `<script>`, no `src=`, no
|
|
111
|
+
`http`, and every label and metadata value escaped, because a report is exactly the
|
|
112
|
+
kind of artefact that gets passed around.
|
|
113
|
+
|
|
114
|
+
Both chart edge cases are handled and tested because both are real runs: a single
|
|
115
|
+
point has no range to scale against, and a flat series has zero range — which is the
|
|
116
|
+
loss curve of a model that never learned, the run you most want to look at. That one
|
|
117
|
+
is labelled `flat — never moved` rather than drawn as a misleading straight line at
|
|
118
|
+
an arbitrary height.
|
|
119
|
+
|
|
120
|
+
### Added - `diagnose` can now be asked whether the quantum layer earned its place
|
|
121
|
+
|
|
122
|
+
`qk.diagnose(model, X, y)` takes a *trained* model and the data it was trained on,
|
|
123
|
+
and answers the question the structural checks cannot: not "is this architecture
|
|
124
|
+
broken" before the run, but "it trained, it converged, and it works just as well
|
|
125
|
+
without the quantum part". That complaint is the most common one in the field and
|
|
126
|
+
nothing in any library answers it.
|
|
127
|
+
|
|
128
|
+
The new finding is `QUANTUM_LAYER_BYPASSED`. A forward hook captures what the
|
|
129
|
+
quantum layer received and what it returned; the same hyperparameter-free probe
|
|
130
|
+
scores both, averaged over five splits. The finding fires only when separability
|
|
131
|
+
measurably *dropped* across the layer by more than the spread across those splits —
|
|
132
|
+
the same verdict rule `qk.baseline` uses, where a lead inside the spread is not a
|
|
133
|
+
lead. It reports both numbers, so the claim can be argued with:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
[warning] QUANTUM_LAYER_BYPASSED: The quantum layer reduced separability: the same
|
|
137
|
+
probe scores 0.951 on what the layer received and 0.578 on what it returned, a drop
|
|
138
|
+
of 0.373 against a spread of 0.028 across 5 splits. The classical layers around it
|
|
139
|
+
are carrying this model.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The probe is `NearestCentroid` precisely because it has no solver and no
|
|
143
|
+
hyperparameter: neither side of the comparison can win by having been tuned better,
|
|
144
|
+
so a difference in score is a difference in the activations. Classification targets
|
|
145
|
+
only — a continuous target is declined rather than guessed at. Without `X` and `y`,
|
|
146
|
+
or without torch, `diagnose` behaves exactly as before.
|
|
147
|
+
|
|
148
|
+
`X` and `y` are optional positional parameters, so every existing call is unchanged.
|
|
149
|
+
|
|
150
|
+
## [0.1.1] - prepared, never published
|
|
151
|
+
|
|
152
|
+
This version was cut, verified and then superseded before it was tagged: the tree
|
|
153
|
+
gained features while it sat, so the correctness work below shipped in **0.2.0**
|
|
154
|
+
instead. There is no `0.1.1` on PyPI and there never will be. The section is kept in
|
|
155
|
+
full because it is the account of eleven defects that were in 0.1.0, and a reader
|
|
156
|
+
upgrading from 0.1.0 needs it.
|
|
157
|
+
|
|
158
|
+
**Upgrade from 0.1.0.** Everything below was found after 0.1.0 was published, so it is
|
|
159
|
+
all still present in the version on PyPI. One defect was reported by a reader using the
|
|
160
|
+
library; ten came from an adversarial audit told to find a case where qmlkit returns a
|
|
161
|
+
*wrong number* rather than an error. Four of the ten corrupt a result silently, and the
|
|
162
|
+
worst of those is in the torch backend, which computed a different circuit from every
|
|
163
|
+
other backend - so `backend="torch"` and `method="backprop"` returned wrong numbers,
|
|
164
|
+
reachable from the built-in `conv_block(filter="su4")`. Nothing raised in any of the
|
|
165
|
+
four.
|
|
166
|
+
|
|
167
|
+
The audit's account is split over three sections below by where each defect lived:
|
|
168
|
+
*three silent wrong numbers*, *five defects*, and the diagnostic in *a diagnostic that
|
|
169
|
+
asserted what it had only inferred*. They are one audit, settled against one dense
|
|
170
|
+
reference simulator that shares no code with qmlkit.
|
|
171
|
+
|
|
172
|
+
### Fixed - a composition the README recommended built the wrong circuit
|
|
173
|
+
|
|
174
|
+
Reported by a reader who tried the two-feature-map example and read the parameter
|
|
175
|
+
indices off the built circuit.
|
|
176
|
+
|
|
177
|
+
**Two feature maps in one model shared each other's angles.** `EncodingLayer`
|
|
178
|
+
reserved circuit input slots per *angle* rather than per feature map, always taking
|
|
179
|
+
slots `0..n_angles-1`. So in the composition the README advertised verbatim -
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# docs: skip - this is the defect, kept as it was written
|
|
183
|
+
qk.Ansatz(2, qk.EncodingLayer(zz) + qk.RotationLayer("ry") + qk.EncodingLayer(angle),
|
|
184
|
+
n_inputs=3)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
— the `ZZFeatureMap` took slots 0, 1, 2, the rotation layer took 3, 4, and the second
|
|
188
|
+
encoding took **0 and 1 again**. Those slots already held the ZZ map's *transformed*
|
|
189
|
+
angles: `ZZFeatureMap(2).angles([0.3, 0.7])` is `[0.6, 1.4, 13.876]`, because a Z term
|
|
190
|
+
follows the `Rz(2 phi)` convention. The trailing `Ry` therefore encoded `2*x_i` where
|
|
191
|
+
the caller asked for `x_i`. Nothing raised. The circuit built, bound, differentiated,
|
|
192
|
+
trained and converged - on the wrong numbers. That is the failure class this library
|
|
193
|
+
exists to refuse, in an example of its own.
|
|
194
|
+
|
|
195
|
+
Each feature map now owns a **disjoint** range of input slots, allocated in the order
|
|
196
|
+
the maps first appear; the *same* map re-used keeps the range it already has, which is
|
|
197
|
+
what makes re-uploading feed the same data in again rather than consume new features.
|
|
198
|
+
`Ansatz.angles` concatenates the maps' angles in slot order and `Ansatz.angle_jacobian`
|
|
199
|
+
stacks their Jacobians, so `bind(x, weights)` and the chain rule down to the data both
|
|
200
|
+
follow. A composed model now also satisfies the `Combined` protocol, so it can go
|
|
201
|
+
straight into a `QuantumLayer` - which the README promised and which had never worked.
|
|
202
|
+
|
|
203
|
+
Slots could not instead be keyed by *feature*, so that both maps read the raw `x`:
|
|
204
|
+
a slot is referenced by a `ParamRef`, which is affine in one parameter
|
|
205
|
+
(`scale * theta[i] + offset`), and a Pauli map's higher-order angle is
|
|
206
|
+
`2 * prod_j (pi - x_j)` - nonlinear, in several features at once. The map from features
|
|
207
|
+
to angles has to stay classical, which is what `angle_jacobian` is for.
|
|
208
|
+
|
|
209
|
+
**The error message steered users into the bug.** With `n_inputs=2` the same
|
|
210
|
+
composition raised *"the circuit reserves 2 input slots but this feature map needs 3"*,
|
|
211
|
+
which tells you to raise `n_inputs` - and raising it to 3 is exactly what produced the
|
|
212
|
+
silent collision. `n_inputs` is now **inferred** from the block, so there is no count
|
|
213
|
+
to get wrong; passing one that disagrees with what the encodings reserve raises and
|
|
214
|
+
names the sum. This closes the last hand-counted number in `Ansatz`, whose docstring
|
|
215
|
+
already promised that parameter counts are "inferred from a dry build, never
|
|
216
|
+
hand-counted, so a miscount is not a failure mode".
|
|
217
|
+
|
|
218
|
+
**The README example is fixed, and now runs.** `tests/test_docs.py` executes every
|
|
219
|
+
Python block in `docs/`, but never reached `README.md` - the most-read page was the
|
|
220
|
+
one page not under the executable-documentation rule, which is why this survived. The
|
|
221
|
+
README is a reference rather than a tutorial and most of its blocks are deliberate
|
|
222
|
+
fragments naming an API, so it opts *in*: a self-contained block marked `# docs: run`
|
|
223
|
+
is executed by `test_readme_blocks_marked_runnable_do_run`.
|
|
224
|
+
|
|
225
|
+
### Fixed - observable arithmetic that QML cost functions need
|
|
226
|
+
|
|
227
|
+
`I - Z` and its relatives are how a projector is written, and most of the ways to
|
|
228
|
+
write one did not work. `Z(0) + Z(1)` and `2.0 * Z(0)` did; these did not:
|
|
229
|
+
|
|
230
|
+
| Expression | Was |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `sum([Z(0), Z(1)])` | `TypeError` - no `__radd__` for `sum`'s `0` start value |
|
|
233
|
+
| `Z(0) + 1` | `AttributeError: 'int' object has no attribute 'terms'` |
|
|
234
|
+
| `1 - Z(0)`, `Z(0) - Z(1)`, `I() - Z(0)` | `TypeError` - no `__sub__` or `__rsub__` |
|
|
235
|
+
| `-(Z(0) + Z(1))` | `TypeError` - `PauliSum` had no `__neg__` |
|
|
236
|
+
| `Z(0) / 2` | `TypeError` - no `__truediv__` |
|
|
237
|
+
|
|
238
|
+
`PauliString` and `PauliSum` now implement `__add__`/`__radd__`, `__sub__`/`__rsub__`,
|
|
239
|
+
`__neg__` and `__truediv__`. A scalar promotes to that multiple of the identity, and
|
|
240
|
+
the additive identity is dropped rather than carried as `0*I`, which is what lets
|
|
241
|
+
`sum(...)` return the sum itself. Numbers are recognised through `numbers.Complex`, so
|
|
242
|
+
NumPy scalars work - `np.int64` is not an `int` subclass. An operand with no sensible
|
|
243
|
+
reading returns `NotImplemented`, so `Z(0) + "x"` reports unsupported operand types
|
|
244
|
+
instead of failing somewhere inside qmlkit.
|
|
245
|
+
|
|
246
|
+
### Fixed - five defects found by an adversarial audit
|
|
247
|
+
|
|
248
|
+
An agent was asked to break qmlkit 0.1.0 through its public API, and to settle every
|
|
249
|
+
disagreement against a dense simulator it wrote itself - one that reads `spec.ops` and
|
|
250
|
+
never calls qmlkit's own execution code. Most of the library held: all 20 gate
|
|
251
|
+
matrices column by column, four exact gradient routes against an analytic product
|
|
252
|
+
rule, every metric against scikit-learn, the noisy backends against their closed form
|
|
253
|
+
to 1e-16. What it found was the other kind of defect - the one where a plausible
|
|
254
|
+
number comes back and nothing raises. Five of those were in the kernel, analysis and
|
|
255
|
+
metric layers.
|
|
256
|
+
|
|
257
|
+
**`QuantumKernel(estimator="hadamard")` returned `Re(<x'|x>)^2` instead of
|
|
258
|
+
`|<x'|x>|^2`.** The wrapper squared `hadamard_test`, whose `part` defaults to
|
|
259
|
+
`"real"`, so wherever a feature map produced a complex overlap the estimator dropped
|
|
260
|
+
the imaginary component. Worst observed error 0.831, on a quantity bounded by 1. The
|
|
261
|
+
three estimators are documented as agreeing on a simulator; two of them did.
|
|
262
|
+
|
|
263
|
+
Nothing downstream could catch it. Squaring the real part alone leaves a Gram matrix
|
|
264
|
+
that is still symmetric, still positive semi-definite and still unit-diagonal -
|
|
265
|
+
`K(x, x)` is real for every feature map, so the diagonal is 1 either way - and
|
|
266
|
+
`diagnose` therefore reported nothing. A `QSVC` fitted on the wrong kernel scored
|
|
267
|
+
*higher* than one fitted on the right one. A Hadamard test measures one *component*
|
|
268
|
+
of a complex overlap, so the modulus takes two circuits: the estimator now runs both
|
|
269
|
+
and returns `Re^2 + Im^2`, and `n_evaluations` reports 2 per pair rather than 1,
|
|
270
|
+
because a caller budgeting circuits is entitled to the real count.
|
|
271
|
+
|
|
272
|
+
**`reduced_dm` silently ignored the order of `wires`.** It did
|
|
273
|
+
`keep = sorted(set(wires))`, so `reduced_dm(psi, [1, 0])` returned the `[0, 1]`
|
|
274
|
+
matrix. That result is still Hermitian, still has trace 1 and still has the right
|
|
275
|
+
eigenvalues, so `purity`, `vn_entropy`, `mutual_info` and every other
|
|
276
|
+
basis-independent reading built on it agreed with the truth - which is how a
|
|
277
|
+
subsystem-ordering defect sits under a passing test suite indefinitely. `wires` is now
|
|
278
|
+
honoured as given: qubit `wires[0]` is the returned matrix's most significant bit, and
|
|
279
|
+
`[1, 0]` is `SWAP . rho . SWAP` rather than `rho`. `[0, 0]` used to de-duplicate itself
|
|
280
|
+
into a 2x2 matrix and now raises, naming the repeated qubit. The PennyLane parity
|
|
281
|
+
suite compares descending pairs against `qml.math.reduce_dm`, so a second
|
|
282
|
+
implementation holds the convention in place.
|
|
283
|
+
|
|
284
|
+
**`KERNEL_AT_CONCENTRATION_SCALE` fired on kernels that separate their classes
|
|
285
|
+
perfectly, and its message contradicted its own numbers.** Two defects in six lines.
|
|
286
|
+
The rule compared against `2 * kernel_spread(n)` while the message printed
|
|
287
|
+
`kernel_spread(n)`, so it reported *"spread 5.00e-01 is at or below ... 2.50e-01"* - a
|
|
288
|
+
sentence refuted by the two numbers inside it. And because off-diagonal kernel entries
|
|
289
|
+
live in `[0, 1]`, their standard deviation cannot exceed 0.5, which is exactly the
|
|
290
|
+
threshold at two qubits: below three qubits the check fired on **every** Gram matrix,
|
|
291
|
+
including one built from mutually orthogonal points. `bool(qk.diagnose(K))` was
|
|
292
|
+
therefore true for a good kernel - the same cry-wolf failure as `ENCODING_COMMUTES`
|
|
293
|
+
above, and the one that teaches people to stop reading diagnostics at all. The message
|
|
294
|
+
now quotes the threshold it actually used, and the check is skipped where that
|
|
295
|
+
threshold exceeds what the statistic can attain, because a comparison nothing can pass
|
|
296
|
+
is not a test.
|
|
297
|
+
|
|
298
|
+
**`geometric_difference(K, K)` returned `sqrt(N)` rather than 1.** Huang et al.'s `g`
|
|
299
|
+
is compared *against* `sqrt(N)`; that threshold had been folded into the statistic
|
|
300
|
+
itself, which left a number agreeing with no published one and a self-comparison whose
|
|
301
|
+
value depended on the sample count. It now returns
|
|
302
|
+
`sqrt(||sqrt(K_Q) K_C^-1 sqrt(K_Q)||)`, which is 1 for identical kernels, and the
|
|
303
|
+
docstring names `sqrt(N)` as the bar for the caller to apply. The kernel study and the
|
|
304
|
+
credit-risk example both compared `g` against a hard-coded `10`; both now compare
|
|
305
|
+
against `sqrt(N)`, which is the literature's test and the only one that scales with
|
|
306
|
+
the data.
|
|
307
|
+
|
|
308
|
+
**`evaluate.regression` reported `r2 = 0.0` for a perfect fit on a constant target.**
|
|
309
|
+
R2 scores a model against the variance baseline - predict the mean everywhere - and a
|
|
310
|
+
constant target has no variance, so the ratio is `0/0`. Reporting 0.0 made a perfect
|
|
311
|
+
prediction indistinguishable from one wrong by a factor of 33: both scored zero, in
|
|
312
|
+
the module whose premise is that a metric says when it is misleading. `r2` and
|
|
313
|
+
`explained_variance` are now `nan` there, with a note naming the value every target
|
|
314
|
+
takes and pointing at `mse` and `max_error`, which need no baseline. This is a
|
|
315
|
+
deliberate departure from scikit-learn, which returns 1.0 for the perfect case and 0.0
|
|
316
|
+
for the rest - a convention that cannot be read back, since a 0.0 could mean either
|
|
317
|
+
"undefined" or "no better than the mean".
|
|
318
|
+
|
|
319
|
+
### Fixed - three silent wrong numbers, found by attacking the library
|
|
320
|
+
|
|
321
|
+
An agent was asked to break qmlkit: to find a case where it returns a *wrong number*
|
|
322
|
+
rather than an error. Its ground truth was a dense simulator it wrote itself, which
|
|
323
|
+
reads only `spec.ops`, builds every gate matrix by hand, and never calls qmlkit
|
|
324
|
+
execution code. It found ten defects; these three were the ones that corrupt a result
|
|
325
|
+
without saying anything.
|
|
326
|
+
|
|
327
|
+
**The torch backend computed a different circuit.** `_apply_torch` reimplements
|
|
328
|
+
`np.moveaxis` and drops the `sorted(zip(destination, source))` numpy does, so for a
|
|
329
|
+
two-qubit gate on wires `(a, b)` with `a > b` the *untouched* wires came out permuted.
|
|
330
|
+
`Z(3)` on a four-qubit circuit read `-0.1288` where NumPy, Qiskit and Cirq all agreed
|
|
331
|
+
on `-0.7374`. `method="backprop"` differentiates through the same function, so its
|
|
332
|
+
gradients were wrong too, and `qk.conv_block(filter="su4")` reaches it without anyone
|
|
333
|
+
hand-writing a circuit. `qk.selfcheck` catches this and names the cause exactly -
|
|
334
|
+
nothing was running it.
|
|
335
|
+
|
|
336
|
+
**`expectation(..., return_std=True)` reported a fabricated error bar.** It fed any
|
|
337
|
+
observable into the single-Pauli formula `sqrt((1 - z^2)/shots)`, which for a sum is
|
|
338
|
+
too tight, too loose, or - once `|<O>|` reaches 1 - *exactly zero*. Every molecular
|
|
339
|
+
Hamiltonian came back with `+-0.00000`, an error bar that looks converged and does not
|
|
340
|
+
move with the shot count. It is now computed rather than approximated: inside a
|
|
341
|
+
qubit-wise-commuting group every term is diagonal in the measured basis, so the
|
|
342
|
+
group's variance follows from the probabilities the exact path already reads, and
|
|
343
|
+
groups measured on independent shots add. Checked against the empirical spread of 400
|
|
344
|
+
repeated samplings, it agrees within 2% for single terms, sums, weighted sums and
|
|
345
|
+
two-basis observables alike.
|
|
346
|
+
|
|
347
|
+
**`purity(state, backend=<mixed-state backend>)` returned a hard-coded 1.0**, on the
|
|
348
|
+
premise that a statevector is pure by construction - true, and not what was asked when
|
|
349
|
+
the caller named a density-matrix backend. It reported 1.0 for a state whose purity
|
|
350
|
+
was 0.309.
|
|
351
|
+
|
|
352
|
+
### Added - property-based torture tests
|
|
353
|
+
|
|
354
|
+
`tests/test_torture.py`: thirteen invariants that hold *by mathematics* rather than by
|
|
355
|
+
design, checked over randomly generated circuits with Hypothesis, which shrinks a
|
|
356
|
+
failure to the smallest circuit that still shows it. `hypothesis` had been a dev
|
|
357
|
+
dependency and was unused.
|
|
358
|
+
|
|
359
|
+
The properties are deliberately not comparisons against stored values: a test that
|
|
360
|
+
pins today's output catches a change, a test that pins an invariant catches a mistake,
|
|
361
|
+
and only the second is worth running against random input. All exact gradient routes
|
|
362
|
+
agree; every backend agrees with the reference; batched equals looped; a tied weight's
|
|
363
|
+
gradient is the sum over its occurrences; adjoints undo, states normalise, expectation
|
|
364
|
+
is linear in the observable and inside its spectrum; Qiskit and Cirq round trips
|
|
365
|
+
reproduce statevectors; sampling lands inside its error bars. Depth is tunable with
|
|
366
|
+
`QMLKIT_TORTURE_EXAMPLES` - cheap in CI, and a campaign at 1,500 examples per property
|
|
367
|
+
(~19,500 circuits) passes clean.
|
|
368
|
+
|
|
369
|
+
One caveat worth recording: the backend-agreement property originally skipped torch,
|
|
370
|
+
on the reasoning that a differentiable simulator is a different kind of backend. That
|
|
371
|
+
exclusion is exactly what let the permutation bug above survive. It no longer skips it,
|
|
372
|
+
and the reason is written into the test.
|
|
373
|
+
|
|
374
|
+
### Added - Adam, selective classification, and Study 8
|
|
375
|
+
|
|
376
|
+
**`adam`** joins `rotosolve`, `spsa` and `gradient-descent`. Its absence was found
|
|
377
|
+
the hard way: an agent reproducing a published method outside the torch bridge had to
|
|
378
|
+
hand-roll one, and its first run put the paper's method *below* the baseline - its own
|
|
379
|
+
diverging optimiser, not the method. A hand-rolled optimiser that diverges looks
|
|
380
|
+
exactly like a technique that does not work. `qmlkit.optim.minimize_adam`, with
|
|
381
|
+
`adam_step` and `AdamState` public for when the loop is yours; keep the state between
|
|
382
|
+
steps or Adam quietly becomes gradient descent with a decaying learning rate.
|
|
383
|
+
|
|
384
|
+
**`qk.evaluate.selective` and `qk.evaluate.risk_coverage`** score a classifier that is
|
|
385
|
+
allowed to decline. Selective accuracy - accuracy on the samples the model chose to
|
|
386
|
+
answer - rises monotonically as it abstains more, reaching 1.000 on the single sample
|
|
387
|
+
it is surest about, so quoting it against a model that answered everything compares
|
|
388
|
+
two different questions rather than two models. `selective` reports coverage beside it
|
|
389
|
+
and makes the *comparable* number the primary one, so `scores.score` cannot quietly
|
|
390
|
+
become the flattering one. `risk_coverage` gives the whole trade plus AURC, which
|
|
391
|
+
abstaining more cannot inflate.
|
|
392
|
+
|
|
393
|
+
**Study 8** measures it on a real model: a `VQC` scoring 0.8947 answering everything
|
|
394
|
+
climbs to a selective **0.9479** at threshold 0.80 while its comparable accuracy
|
|
395
|
+
*falls* to **0.7982**. Abstention made the reported number better and the model worse,
|
|
396
|
+
and only one of the two columns says so.
|
|
397
|
+
|
|
398
|
+
### Fixed - a diagnostic that asserted what it had only inferred
|
|
399
|
+
|
|
400
|
+
**`ENCODING_COMMUTES` reported an error on correct architectures.** The check was
|
|
401
|
+
structural: matching rotations implied a collapse. But `Ry(x) Ry(t) Ry(x) Ry(t)`
|
|
402
|
+
merges *on one wire with nothing in between* - any entanglement breaks it, and the
|
|
403
|
+
check could see neither an entangler in the trainable block nor an entangling feature
|
|
404
|
+
map. Measured against the library's own `fourier.spectrum`, it claimed one frequency
|
|
405
|
+
where the band was `0..4`. It now uses the structural test as a trigger and confirms
|
|
406
|
+
with the spectrum before reporting. A false positive in the honesty layer is worse
|
|
407
|
+
than a false negative: it teaches people to ignore the tool.
|
|
408
|
+
|
|
409
|
+
The same blindness sat in `reupload()`'s construction-time warning, whose message had
|
|
410
|
+
a second defect - it interpolated the `rotations` *parameter* rather than the gates
|
|
411
|
+
actually found, so passing an explicit block produced "the trainable block only uses
|
|
412
|
+
('rz','ry','rz') ... Use a non-commuting block such as ("rz","ry","rz")", recommending
|
|
413
|
+
the thing it was complaining about.
|
|
414
|
+
|
|
415
|
+
**`list_baselines()` repeated a name registered for both tasks.** The registry is
|
|
416
|
+
keyed by task and name deliberately, so `rbf-kernel-ridge` serves classification and
|
|
417
|
+
regression; the listing read the values and never deduplicated.
|
|
418
|
+
|
|
419
|
+
**`Scores.get("precision")` returned `None`.** `__getitem__` already answered a wrong
|
|
420
|
+
key with a did-you-mean and `.get` bypassed it, so a near-miss became a silent `None`
|
|
421
|
+
that surfaced later as a `TypeError` from inside numpy. An explicit default is still
|
|
422
|
+
honoured without comment.
|
|
423
|
+
|
|
424
|
+
### Changed
|
|
425
|
+
|
|
426
|
+
- `Ansatz(..., n_inputs=)` now defaults to `None`, meaning *infer*. Existing calls that
|
|
427
|
+
passed the correct total keep working; one that passed a different number now raises
|
|
428
|
+
rather than silently building a circuit whose encodings overlap.
|
|
429
|
+
- `Ansatz` gained `feature_maps`, `angles`, `angle_jacobian` and `n_features`.
|
|
430
|
+
`ReuploadModel`'s own `angles`/`angle_jacobian` were identical for its single map and
|
|
431
|
+
are now inherited.
|
|
432
|
+
- Composing feature maps that read different numbers of features now raises. Every map
|
|
433
|
+
in one model is handed the same `x`, so such a model could never have been bound.
|
|
434
|
+
- `scripts/verify_install.py` compared `__version__` against a hardcoded `"0.1.0"` - a
|
|
435
|
+
third copy of the version that had to be bumped by hand, and the gate failed on this
|
|
436
|
+
release for that reason alone. It now reads the installed distribution metadata and
|
|
437
|
+
checks the module constant against it, which is what the check was named for.
|
|
438
|
+
- `QuantumLayer` treats a model as carrying its own encoding only when it reserves
|
|
439
|
+
input slots. Every `Ansatz` can map data onto its slots now, so the four-attribute
|
|
440
|
+
check alone no longer distinguishes a model from a bare ansatz.
|
|
441
|
+
- **`geometric_difference` now returns values `sqrt(N)` times smaller than 0.1.0's.**
|
|
442
|
+
Code that compared it against a hand-picked constant will read differently and
|
|
443
|
+
should compare against `sqrt(N)` instead, which is the comparison the statistic was
|
|
444
|
+
always for.
|
|
445
|
+
- `QuantumKernel(estimator="hadamard").n_evaluations` counts two circuits per pair
|
|
446
|
+
rather than one, which is how many it now runs.
|
|
447
|
+
|
|
7
448
|
## [0.1.0] - 2026-09-12
|
|
8
449
|
|
|
9
450
|
First release.
|