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.
Files changed (198) hide show
  1. {qmlkit-0.1.0 → qmlkit-0.2.0}/.github/workflows/docs.yml +6 -1
  2. qmlkit-0.2.0/AGENTS.md +144 -0
  3. {qmlkit-0.1.0 → qmlkit-0.2.0}/CHANGELOG.md +441 -0
  4. qmlkit-0.2.0/HANDOFF.md +682 -0
  5. {qmlkit-0.1.0 → qmlkit-0.2.0}/PKG-INFO +238 -113
  6. {qmlkit-0.1.0 → qmlkit-0.2.0}/README.md +237 -112
  7. {qmlkit-0.1.0 → qmlkit-0.2.0}/RELEASING.md +27 -31
  8. qmlkit-0.2.0/docs/about/validation.md +213 -0
  9. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/agents.md +1 -1
  10. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/choosing-a-gradient.md +38 -0
  11. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/evaluation.md +2 -0
  12. qmlkit-0.2.0/docs/guides/from-pennylane.md +142 -0
  13. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/index.md +2 -0
  14. qmlkit-0.2.0/docs/guides/watching-a-run.md +124 -0
  15. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/index.md +39 -32
  16. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/llms-full.txt +683 -80
  17. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/llms.txt +14 -5
  18. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/evaluation.md +18 -0
  19. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/02-quantum-kernels.md +10 -6
  20. qmlkit-0.2.0/docs/studies/08-selective-classification.md +134 -0
  21. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/index.md +3 -1
  22. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/06-quantum-kernels.md +6 -0
  23. {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/credit_risk.py +21 -11
  24. {qmlkit-0.1.0 → qmlkit-0.2.0}/mkdocs.yml +4 -1
  25. {qmlkit-0.1.0 → qmlkit-0.2.0}/pyproject.toml +1 -1
  26. {qmlkit-0.1.0 → qmlkit-0.2.0}/scripts/generate_llms_txt.py +10 -4
  27. qmlkit-0.2.0/scripts/probe_dispatch.py +79 -0
  28. qmlkit-0.2.0/scripts/probe_fusion.py +77 -0
  29. qmlkit-0.2.0/scripts/probe_qrack.py +84 -0
  30. qmlkit-0.2.0/scripts/probe_threads.py +88 -0
  31. qmlkit-0.2.0/scripts/proto_fusion.py +123 -0
  32. qmlkit-0.2.0/scripts/proto_fusion2.py +109 -0
  33. qmlkit-0.2.0/scripts/proto_fusion3.py +119 -0
  34. {qmlkit-0.1.0 → qmlkit-0.2.0}/scripts/verify_install.py +12 -1
  35. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/__init__.py +6 -1
  36. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/_aliases.py +1 -1
  37. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/adapt.py +2 -1
  38. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/autoencoder.py +7 -10
  39. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/qaoa.py +1 -1
  40. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/vqe.py +11 -1
  41. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/ansatz/blocks.py +92 -8
  42. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/ansatz/library.py +72 -5
  43. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/ansatz/reupload.py +32 -23
  44. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/baselines.py +10 -3
  45. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/base.py +32 -0
  46. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/torch_backend.py +8 -2
  47. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/execute.py +15 -2
  48. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/observables.py +80 -6
  49. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/diagnostics.py +234 -16
  50. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/evaluate.py +196 -7
  51. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/info.py +33 -5
  52. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/kernels/matrix.py +66 -15
  53. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/layer.py +10 -2
  54. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/models.py +39 -13
  55. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/optim.py +101 -1
  56. qmlkit-0.2.0/src/qmlkit/progress.py +367 -0
  57. qmlkit-0.2.0/src/qmlkit/report.py +238 -0
  58. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/utils/shots.py +15 -3
  59. qmlkit-0.2.0/tests/densesim.py +206 -0
  60. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_advanced.py +129 -4
  61. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_analysis.py +431 -0
  62. qmlkit-0.2.0/tests/test_contribution.py +105 -0
  63. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_core.py +4 -1
  64. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_docs.py +35 -1
  65. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_evaluate.py +122 -0
  66. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_kernels.py +40 -1
  67. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_noisy_backends.py +1 -1
  68. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_observables.py +76 -0
  69. qmlkit-0.2.0/tests/test_optimizer_wiring.py +77 -0
  70. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_pennylane_parity.py +3 -1
  71. qmlkit-0.2.0/tests/test_progress.py +182 -0
  72. qmlkit-0.2.0/tests/test_report.py +188 -0
  73. qmlkit-0.2.0/tests/test_torture.py +388 -0
  74. qmlkit-0.1.0/AGENTS.md +0 -81
  75. qmlkit-0.1.0/HANDOFF.md +0 -228
  76. qmlkit-0.1.0/docs/about/validation.md +0 -141
  77. {qmlkit-0.1.0 → qmlkit-0.2.0}/.gitattributes +0 -0
  78. {qmlkit-0.1.0 → qmlkit-0.2.0}/.github/workflows/ci.yml +0 -0
  79. {qmlkit-0.1.0 → qmlkit-0.2.0}/.github/workflows/release.yml +0 -0
  80. {qmlkit-0.1.0 → qmlkit-0.2.0}/.gitignore +0 -0
  81. {qmlkit-0.1.0 → qmlkit-0.2.0}/LICENSE +0 -0
  82. {qmlkit-0.1.0 → qmlkit-0.2.0}/NOTICE +0 -0
  83. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/about/changelog.md +0 -0
  84. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/about/releasing.md +0 -0
  85. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/about/stability.md +0 -0
  86. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/backends.md +0 -0
  87. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/extending.md +0 -0
  88. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/noise.md +0 -0
  89. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/guides/parameter-shift.md +0 -0
  90. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/install.md +0 -0
  91. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/javascripts/mathjax.js +0 -0
  92. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/algorithms.md +0 -0
  93. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/analysis.md +0 -0
  94. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/ansatz.md +0 -0
  95. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/core.md +0 -0
  96. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/encoding.md +0 -0
  97. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/gradients.md +0 -0
  98. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/index.md +0 -0
  99. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/kernels.md +0 -0
  100. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/reference/nn.md +0 -0
  101. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/01-imbalanced-classification.md +0 -0
  102. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/03-regression.md +0 -0
  103. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/04-chemistry.md +0 -0
  104. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/05-beyond-classification.md +0 -0
  105. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/06-clinical.md +0 -0
  106. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/studies/07-images-and-structure.md +0 -0
  107. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/01-first-circuit.md +0 -0
  108. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/02-encoding-data.md +0 -0
  109. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/03-gradients.md +0 -0
  110. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/04-ansatz-design.md +0 -0
  111. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/05-training-torch.md +0 -0
  112. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/07-reuploading.md +0 -0
  113. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/08-trainability.md +0 -0
  114. {qmlkit-0.1.0 → qmlkit-0.2.0}/docs/tutorials/index.md +0 -0
  115. {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/accelerate_pennylane.py +0 -0
  116. {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/benchmark_pennylane.py +0 -0
  117. {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/compare_pennylane.py +0 -0
  118. {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/credit_data.py +0 -0
  119. {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/experiments.py +0 -0
  120. {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/head_to_head.py +0 -0
  121. {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/quickstart.py +0 -0
  122. {qmlkit-0.1.0 → qmlkit-0.2.0}/examples/toward_hardware.py +0 -0
  123. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/__init__.py +0 -0
  124. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/chemistry.py +0 -0
  125. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/clustering.py +0 -0
  126. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/hamiltonians.py +0 -0
  127. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/molecule.py +0 -0
  128. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/algorithms/rl.py +0 -0
  129. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/ansatz/__init__.py +0 -0
  130. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/budget.py +0 -0
  131. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/__init__.py +0 -0
  132. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/__init__.py +0 -0
  133. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/_sampling.py +0 -0
  134. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/cirq_backend.py +0 -0
  135. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/cirq_density_backend.py +0 -0
  136. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/noisy.py +0 -0
  137. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/numpy_backend.py +0 -0
  138. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/qiskit_aer_backend.py +0 -0
  139. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/qiskit_backend.py +0 -0
  140. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/registry.py +0 -0
  141. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/backends/spinqit_backend.py +0 -0
  142. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/builder.py +0 -0
  143. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/gates.py +0 -0
  144. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/core/ir.py +0 -0
  145. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/datasets.py +0 -0
  146. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/draw.py +0 -0
  147. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/__init__.py +0 -0
  148. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/amplitude.py +0 -0
  149. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/angle.py +0 -0
  150. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/feature_maps.py +0 -0
  151. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/hamiltonian.py +0 -0
  152. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/pipeline.py +0 -0
  153. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/encoding/scaling.py +0 -0
  154. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/fourier.py +0 -0
  155. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/generative.py +0 -0
  156. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/__init__.py +0 -0
  157. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/adjoint.py +0 -0
  158. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/batch.py +0 -0
  159. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/dispatch.py +0 -0
  160. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/hadamard.py +0 -0
  161. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/parameter_shift.py +0 -0
  162. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/rules.py +0 -0
  163. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/gradients/spsa.py +0 -0
  164. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/imbalance.py +0 -0
  165. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/interop.py +0 -0
  166. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/kernels/__init__.py +0 -0
  167. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/kernels/estimators.py +0 -0
  168. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/kernels/models.py +0 -0
  169. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/metrics.py +0 -0
  170. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/__init__.py +0 -0
  171. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/advanced.py +0 -0
  172. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/nn/losses.py +0 -0
  173. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/provenance.py +0 -0
  174. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/py.typed +0 -0
  175. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/search.py +0 -0
  176. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/shadows.py +0 -0
  177. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/utils/__init__.py +0 -0
  178. {qmlkit-0.1.0 → qmlkit-0.2.0}/src/qmlkit/utils/errors.py +0 -0
  179. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_agent_api.py +0 -0
  180. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_algorithms.py +0 -0
  181. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_ansatz.py +0 -0
  182. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_baseline.py +0 -0
  183. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_batch.py +0 -0
  184. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_budget.py +0 -0
  185. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_builder.py +0 -0
  186. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_cross_backend.py +0 -0
  187. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_encoding.py +0 -0
  188. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_grad_batch.py +0 -0
  189. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_gradient_methods.py +0 -0
  190. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_gradients.py +0 -0
  191. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_imbalance.py +0 -0
  192. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_import.py +0 -0
  193. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_injection.py +0 -0
  194. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_interop.py +0 -0
  195. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_nn.py +0 -0
  196. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_provenance.py +0 -0
  197. {qmlkit-0.1.0 → qmlkit-0.2.0}/tests/test_search.py +0 -0
  198. {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
- if: github.ref == 'refs/heads/main' && github.event_name == 'push'
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.