spacecore 0.4.0__tar.gz → 0.4.2__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 (356) hide show
  1. {spacecore-0.4.0 → spacecore-0.4.2}/CHANGELOG.md +120 -4
  2. {spacecore-0.4.0 → spacecore-0.4.2}/PKG-INFO +3 -1
  3. spacecore-0.4.2/docs/dev/0.4.1-bench-surface.md +159 -0
  4. spacecore-0.4.2/docs/dev/adr/021_lazy_operator_algebra_and_simplification.md +131 -0
  5. spacecore-0.4.2/docs/dev/adr/022_caching.md +156 -0
  6. spacecore-0.4.2/docs/dev/adr/023_benchmark_framework.md +415 -0
  7. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/README.md +3 -0
  8. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/release_notes.rst +53 -0
  9. spacecore-0.4.2/docs/source/tutorials/09_kernels_and_fusion.rst +531 -0
  10. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/index.rst +6 -0
  11. {spacecore-0.4.0 → spacecore-0.4.2}/pyproject.toml +5 -0
  12. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/__init__.py +16 -0
  13. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_contextual/_state.py +7 -2
  14. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_version.py +1 -1
  15. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/numpy/_ops.py +7 -13
  16. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/__init__.py +16 -0
  17. spacecore-0.4.2/spacecore/functional/_algebra.py +440 -0
  18. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/_base.py +95 -10
  19. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/__init__.py +1 -0
  20. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/sparse.py +10 -0
  21. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/__init__.py +6 -1
  22. spacecore-0.4.2/spacecore/kernels/specs/_batched.py +234 -0
  23. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/_policy.py +1 -2
  24. spacecore-0.4.2/spacecore/kernels/specs/block_batched.py +358 -0
  25. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/block_diagonal.py +1 -1
  26. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/composed.py +1 -1
  27. spacecore-0.4.2/spacecore/kernels/specs/stacked_batched.py +167 -0
  28. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_algebra.py +97 -0
  29. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_base.py +37 -0
  30. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/_block.py +78 -4
  31. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/_from_single.py +34 -1
  32. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/_to_single.py +34 -1
  33. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/optimize/__init__.py +2 -1
  34. spacecore-0.4.2/spacecore/optimize/_optax.py +529 -0
  35. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_jordan.py +12 -0
  36. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_dense_vector.py +14 -0
  37. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_hermitian.py +14 -0
  38. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_stacked.py +25 -0
  39. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_tree_space.py +75 -6
  40. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/PKG-INFO +3 -1
  41. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/SOURCES.txt +21 -0
  42. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/requires.txt +2 -0
  43. spacecore-0.4.2/tests/bench/test_bare_baseline.py +49 -0
  44. spacecore-0.4.2/tests/bench/test_bench_smoke.py +581 -0
  45. spacecore-0.4.2/tests/bench/test_macro_aggregate.py +140 -0
  46. spacecore-0.4.2/tests/bench/test_macro_dashboard.py +200 -0
  47. spacecore-0.4.2/tests/bench/test_macro_runner_smoke.py +220 -0
  48. spacecore-0.4.2/tests/bench/test_macro_schema.py +162 -0
  49. spacecore-0.4.2/tests/bench/test_regimes.py +131 -0
  50. spacecore-0.4.2/tests/functional/test_algebra.py +297 -0
  51. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_functional_base.py +69 -0
  52. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/functionals.py +77 -0
  53. {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_public_api.py +1 -1
  54. spacecore-0.4.2/tests/integration/test_smoke_torch.py +63 -0
  55. {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_kernels_match_generic.py +234 -0
  56. spacecore-0.4.2/tests/kernels/test_materialized_cache.py +266 -0
  57. spacecore-0.4.2/tests/linops/test_fuse.py +323 -0
  58. {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/test_contracts.py +4 -4
  59. spacecore-0.4.2/tests/optimize/test_minimize_optax.py +349 -0
  60. spacecore-0.4.2/tests/spaces/__init__.py +0 -0
  61. spacecore-0.4.2/tests/spaces/test_jordan_invariants.py +185 -0
  62. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_stacked_space.py +9 -0
  63. spacecore-0.4.2/tests/spaces/test_tree_spectral_decomposition.py +238 -0
  64. spacecore-0.4.2/tutorials/09_kernels_and_fusion.ipynb +810 -0
  65. {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/README.md +7 -0
  66. spacecore-0.4.0/spacecore/kernels/specs/block_batched.py +0 -159
  67. spacecore-0.4.0/spacecore/optimize/_optax.py +0 -154
  68. spacecore-0.4.0/tests/integration/test_smoke_torch.py +0 -34
  69. spacecore-0.4.0/tests/optimize/test_minimize_optax.py +0 -113
  70. spacecore-0.4.0/tests/spaces/test_tree_spectral_decomposition.py +0 -79
  71. {spacecore-0.4.0 → spacecore-0.4.2}/CONTRIBUTING.md +0 -0
  72. {spacecore-0.4.0 → spacecore-0.4.2}/LICENSE +0 -0
  73. {spacecore-0.4.0 → spacecore-0.4.2}/MANIFEST.in +0 -0
  74. {spacecore-0.4.0 → spacecore-0.4.2}/README.md +0 -0
  75. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/0.3.1-docs-inventory.md +0 -0
  76. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/0.3.1-docstring-audit.md +0 -0
  77. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/001_backend_layer.md +0 -0
  78. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/002_context_and_conversion.md +0 -0
  79. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/003_space_hierarchy.md +0 -0
  80. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/004_inner_product_and_geometry.md +0 -0
  81. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/005_space_subclasses_and_capabilities.md +0 -0
  82. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/006_current_batching_model.md +0 -0
  83. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/007_linop_contract.md +0 -0
  84. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/008_linop_subclasses.md +0 -0
  85. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/009_metric_adjoint.md +0 -0
  86. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/010_functional_contract.md +0 -0
  87. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/011_linalg_contract.md +0 -0
  88. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/012_jordan_spectrum.md +0 -0
  89. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/013_tree_structured_spaces.md +0 -0
  90. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/014_check_policy.md +0 -0
  91. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/015_dtype_default_vs_scalar_field.md +0 -0
  92. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/016_kernel_layers.md +0 -0
  93. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/017_tensor_product_spaces.md +0 -0
  94. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/018_external_optimizer_adapters.md +0 -0
  95. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/019_everyday_toolbox.md +0 -0
  96. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/020_sets_and_projection.md +0 -0
  97. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/template.md +0 -0
  98. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/architecture.md +0 -0
  99. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/labels.md +0 -0
  100. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/linop_generators.md +0 -0
  101. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/prerequisites.md +0 -0
  102. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/process.md +0 -0
  103. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/setup.md +0 -0
  104. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/current.md +0 -0
  105. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/docstring_style.md +0 -0
  106. {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/vision.md +0 -0
  107. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/_static/custom.css +0 -0
  108. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/backend.rst +0 -0
  109. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/context.rst +0 -0
  110. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/functionals.rst +0 -0
  111. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/index.rst +0 -0
  112. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/linalg.rst +0 -0
  113. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/linops.rst +0 -0
  114. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/optimize.rst +0 -0
  115. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/spaces.rst +0 -0
  116. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/conf.py +0 -0
  117. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/backend_conformance.rst +0 -0
  118. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/backend_deviations.rst +0 -0
  119. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/backend_ops_array_api.rst +0 -0
  120. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/batching.rst +0 -0
  121. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/batching_test_policy.rst +0 -0
  122. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/capability_dispatch.rst +0 -0
  123. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/checking_policy.rst +0 -0
  124. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/context_ownership.rst +0 -0
  125. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/conversion_policy.rst +0 -0
  126. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/dtype_policy.rst +0 -0
  127. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/geometry.rst +0 -0
  128. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/index.rst +0 -0
  129. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/jax_integration.rst +0 -0
  130. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/kernels_policy.rst +0 -0
  131. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/performance.rst +0 -0
  132. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/tree_spaces.rst +0 -0
  133. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/dev/adr.rst +0 -0
  134. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/dev/contributing.rst +0 -0
  135. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/dev/index.rst +0 -0
  136. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/dev/vision.rst +0 -0
  137. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/index.rst +0 -0
  138. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/01_backend_and_context.rst +0 -0
  139. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/02_linear_algebra.rst +0 -0
  140. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/03_functionals.rst +0 -0
  141. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/04_tree_spaces.rst +0 -0
  142. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/05_weighted_tikhonov.rst +0 -0
  143. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/06_optimal_transport.rst +0 -0
  144. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/07_manifold_descent.rst +0 -0
  145. {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/08_pdhg_conic_program.rst +0 -0
  146. {spacecore-0.4.0 → spacecore-0.4.2}/setup.cfg +0 -0
  147. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_batching.py +0 -0
  148. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_check_policy.py +0 -0
  149. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_checks.py +0 -0
  150. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_contextual/__init__.py +0 -0
  151. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_contextual/_bound.py +0 -0
  152. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_contextual/_policies.py +0 -0
  153. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_repr.py +0 -0
  154. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/__init__.py +0 -0
  155. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/_context.py +0 -0
  156. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/_eager.py +0 -0
  157. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/_family.py +0 -0
  158. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/_ops.py +0 -0
  159. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/cupy/__init__.py +0 -0
  160. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/cupy/_ops.py +0 -0
  161. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/jax/__init__.py +0 -0
  162. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/jax/_ops.py +0 -0
  163. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/jax/_pytree.py +0 -0
  164. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/numpy/__init__.py +0 -0
  165. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/torch/__init__.py +0 -0
  166. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/torch/_ops.py +0 -0
  167. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/_composed.py +0 -0
  168. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/_linear.py +0 -0
  169. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/_quadratic.py +0 -0
  170. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/__init__.py +0 -0
  171. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_coordinate.py +0 -0
  172. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_entropy.py +0 -0
  173. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_huber.py +0 -0
  174. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_least_squares.py +0 -0
  175. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_norms.py +0 -0
  176. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_proximal.py +0 -0
  177. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_spectral.py +0 -0
  178. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/__init__.py +0 -0
  179. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/_rules.py +0 -0
  180. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/algebra.py +0 -0
  181. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/dense.py +0 -0
  182. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/diagonal.py +0 -0
  183. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/functional.py +0 -0
  184. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/_dispatch.py +0 -0
  185. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/_registry.py +0 -0
  186. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/composed_simplify.py +0 -0
  187. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/__init__.py +0 -0
  188. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_cg.py +0 -0
  189. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_expm.py +0 -0
  190. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_lanczos.py +0 -0
  191. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_lsqr.py +0 -0
  192. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_power.py +0 -0
  193. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_utils.py +0 -0
  194. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/__init__.py +0 -0
  195. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_dense.py +0 -0
  196. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_diagonal.py +0 -0
  197. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_metric.py +0 -0
  198. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_sparse.py +0 -0
  199. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/__init__.py +0 -0
  200. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/_base.py +0 -0
  201. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/optimize/_common.py +0 -0
  202. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/optimize/_scipy.py +0 -0
  203. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/__init__.py +0 -0
  204. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/__init__.py +0 -0
  205. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_coordinate.py +0 -0
  206. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_inner_product.py +0 -0
  207. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_space.py +0 -0
  208. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_star.py +0 -0
  209. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_vector.py +0 -0
  210. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/checks/__init__.py +0 -0
  211. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/checks/_base.py +0 -0
  212. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/checks/_coordinate.py +0 -0
  213. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/__init__.py +0 -0
  214. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_dense_coordinate.py +0 -0
  215. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/types/__init__.py +0 -0
  216. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/types/_array.py +0 -0
  217. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/types/_dtype.py +0 -0
  218. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/types/_misc.py +0 -0
  219. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/dependency_links.txt +0 -0
  220. {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/top_level.txt +0 -0
  221. {spacecore-0.4.0 → spacecore-0.4.2}/tests/__init__.py +0 -0
  222. {spacecore-0.4.0 → spacecore-0.4.2}/tests/_conformance.py +0 -0
  223. {spacecore-0.4.0 → spacecore-0.4.2}/tests/_helpers.py +0 -0
  224. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/__init__.py +0 -0
  225. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/_conformance.py +0 -0
  226. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/_references.py +0 -0
  227. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/conftest.py +0 -0
  228. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_backend_family.py +0 -0
  229. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_context.py +0 -0
  230. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_cupy_ops.py +0 -0
  231. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_jax_ops.py +0 -0
  232. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_jax_pytree_class.py +0 -0
  233. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_lazy_namespace.py +0 -0
  234. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_numpy_ops.py +0 -0
  235. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_operations.py +0 -0
  236. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_torch_ops.py +0 -0
  237. {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_types_protocols.py +0 -0
  238. {spacecore-0.4.0/tests/context → spacecore-0.4.2/tests/bench}/__init__.py +0 -0
  239. {spacecore-0.4.0 → spacecore-0.4.2}/tests/conftest.py +0 -0
  240. {spacecore-0.4.0/tests/integration → spacecore-0.4.2/tests/context}/__init__.py +0 -0
  241. {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/conftest.py +0 -0
  242. {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_check_policy.py +0 -0
  243. {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_check_policy_helpers.py +0 -0
  244. {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_checked_method.py +0 -0
  245. {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_compatibility.py +0 -0
  246. {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_context_bound.py +0 -0
  247. {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_enable_checks.py +0 -0
  248. {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_policies_errors.py +0 -0
  249. {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_state_free_functions.py +0 -0
  250. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/conftest.py +0 -0
  251. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_composed_functional.py +0 -0
  252. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_generated_functionals.py +0 -0
  253. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_inner_product_functional.py +0 -0
  254. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_linear_functional.py +0 -0
  255. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_linop_quadratic_form.py +0 -0
  256. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_matrix_free_linear_functional.py +0 -0
  257. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_metric_gradient.py +0 -0
  258. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_quadratic_form.py +0 -0
  259. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/tools/test_battery_functionals.py +0 -0
  260. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/tools/test_least_squares.py +0 -0
  261. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/tools/test_proximal.py +0 -0
  262. {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/tools/test_spectral.py +0 -0
  263. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/__init__.py +0 -0
  264. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_arrays.py +0 -0
  265. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_contexts.py +0 -0
  266. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_hermitian.py +0 -0
  267. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_metrics.py +0 -0
  268. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_params.py +0 -0
  269. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_protocol.py +0 -0
  270. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_seed.py +0 -0
  271. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_trees.py +0 -0
  272. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/linops.py +0 -0
  273. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/spaces.py +0 -0
  274. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/test_registry_completeness.py +0 -0
  275. {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/test_smoke.py +0 -0
  276. {spacecore-0.4.0/tests/kernels → spacecore-0.4.2/tests/integration}/__init__.py +0 -0
  277. {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_github_labels.py +0 -0
  278. {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_imports.py +0 -0
  279. {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_smoke_jax.py +0 -0
  280. {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_smoke_numpy.py +0 -0
  281. {spacecore-0.4.0/tests/linops → spacecore-0.4.2/tests/kernels}/__init__.py +0 -0
  282. {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/conftest.py +0 -0
  283. {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_core_kernel_dispatch.py +0 -0
  284. {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_kernel_dispatch.py +0 -0
  285. {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_kernel_registry.py +0 -0
  286. {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_kernel_spec.py +0 -0
  287. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/__init__.py +0 -0
  288. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/_helpers.py +0 -0
  289. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/conftest.py +0 -0
  290. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_cg.py +0 -0
  291. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_core_resolution.py +0 -0
  292. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_expm_multiply.py +0 -0
  293. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_generated_solver_matrix.py +0 -0
  294. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_lanczos.py +0 -0
  295. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_lsqr.py +0 -0
  296. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_power_iteration.py +0 -0
  297. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_result_types.py +0 -0
  298. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_solver_contracts.py +0 -0
  299. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_utils.py +0 -0
  300. {spacecore-0.4.0/tests/optim → spacecore-0.4.2/tests/linops}/__init__.py +0 -0
  301. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/conftest.py +0 -0
  302. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_algebra_factories.py +0 -0
  303. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_algebra_linops.py +0 -0
  304. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_block_diagonal_linop.py +0 -0
  305. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_block_matrix_linop.py +0 -0
  306. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_dense_linop.py +0 -0
  307. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_diagonal_linop.py +0 -0
  308. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_fused_algebra_overhead.py +0 -0
  309. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_generated_linop_laws.py +0 -0
  310. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_linop_base.py +0 -0
  311. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_linop_jit.py +0 -0
  312. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_metric_helpers.py +0 -0
  313. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_sparse_linop.py +0 -0
  314. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_stacked_linop.py +0 -0
  315. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_sum_to_single_linop.py +0 -0
  316. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_tree_helpers.py +0 -0
  317. {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_tree_linop_base.py +0 -0
  318. {spacecore-0.4.0/tests/optimize → spacecore-0.4.2/tests/optim}/__init__.py +0 -0
  319. {spacecore-0.4.0 → spacecore-0.4.2}/tests/optim/test_cached_member_checks.py +0 -0
  320. {spacecore-0.4.0/tests/spaces → spacecore-0.4.2/tests/optimize}/__init__.py +0 -0
  321. {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/_helpers.py +0 -0
  322. {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/test_line_search_scipy.py +0 -0
  323. {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/test_metric_handoff.py +0 -0
  324. {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/test_minimize_scipy.py +0 -0
  325. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/_generated_helpers.py +0 -0
  326. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/conftest.py +0 -0
  327. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_check_batched.py +0 -0
  328. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_coordinate_space_base.py +0 -0
  329. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_dense_coordinate_space.py +0 -0
  330. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_dense_vector_space.py +0 -0
  331. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_elementwise_jordan_spaces.py +0 -0
  332. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_dense_coordinate.py +0 -0
  333. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_dense_vector.py +0 -0
  334. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_inner_product.py +0 -0
  335. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_jordan_space.py +0 -0
  336. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_space_laws.py +0 -0
  337. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_tree_space.py +0 -0
  338. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_hermitian_space.py +0 -0
  339. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_inner_product.py +0 -0
  340. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_inner_product_space_base.py +0 -0
  341. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_jordan_algebra_spaces.py +0 -0
  342. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_space_base.py +0 -0
  343. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_space_checks.py +0 -0
  344. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_star_space_base.py +0 -0
  345. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_tree_space.py +0 -0
  346. {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_vector_space_base.py +0 -0
  347. {spacecore-0.4.0 → spacecore-0.4.2}/tests/test_repr.py +0 -0
  348. {spacecore-0.4.0 → spacecore-0.4.2}/tests/test_weighted_tikhonov.py +0 -0
  349. {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/01_backend_and_context.ipynb +0 -0
  350. {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/02_linear_algebra.ipynb +0 -0
  351. {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/03_functionals.ipynb +0 -0
  352. {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/04_tree_spaces.ipynb +0 -0
  353. {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/05_weighted_tikhonov.ipynb +0 -0
  354. {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/06_optimal_transport.ipynb +0 -0
  355. {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/07_manifold_descent.ipynb +0 -0
  356. {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/08_pdhg_conic_program.ipynb +0 -0
@@ -7,8 +7,70 @@ and the project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.2] — 2026-07-01
11
+
10
12
  ### Added
11
13
 
14
+ - **Jordan spectral primitives** on every Jordan-algebra space: `trace`,
15
+ `determinant`, and the algebra identity `unit()`, derived from the spectrum and
16
+ batch-axis preserving. Hermitian `trace` reads the diagonal (no eigendecomposition);
17
+ tree/stacked spaces are direct sums, so `trace` is additive and `determinant`
18
+ multiplicative (#56).
19
+ - **Lazy functional algebra** mirroring `LinOp`: `SumFunctional`, `ScaledFunctional`,
20
+ `ShiftedFunctional` (affine `F + c`), and `ZeroFunctional`, wired to
21
+ `Functional.__add__` / `__sub__` / `__neg__` / `__mul__` / `__rmul__` and the
22
+ `make_functional_sum` / `make_scaled_functional` / `make_shifted_functional`
23
+ factories. Gradients combine through the domain's `add`/`scale` (metric-correct on
24
+ tree/stacked/weighted domains), and a complex scalar conjugates on the gradient
25
+ side (#60).
26
+ - **`Functional.value_and_grad`** — a fused evaluator (base default returns
27
+ `(value, grad)`; subclasses may override with a single-pass AD) — and
28
+ `value`/`grad` now accept auxiliary `*args, **kwargs` (#51).
29
+ - **Structure-preserving tree spectra**: `TreeSpace.spectrum(x, structured=True)`
30
+ returns a `treedef`-shaped tree of per-leaf eigenvalues, and
31
+ `TreeSpectralDecomposition` carries the `treedef` with a `to_tree()` view. The
32
+ flat `spectrum(x)` is unchanged (#58).
33
+
34
+ ### Changed
35
+
36
+ - **`minimize_optax` is now a compiled, convergence-aware driver** (breaking): the
37
+ whole loop runs inside `jax.jit(jax.lax.while_loop(...))` with the fused
38
+ `value_and_grad` cached once per iteration; on-device `grad_norm <= tol` and
39
+ finiteness stopping; four-column progress logging (iteration · value · ΔF ·
40
+ grad norm) + a preallocated history buffer with post-hoc `progress_callback`
41
+ replay; an optional `project` retraction hook; and a rich `OptaxResult`
42
+ (success/status/message/iters/`nfev`/`njev`/`n_linesearch_steps`/history/timing/
43
+ `x_element`). The old fixed-`steps` eager loop is removed (#54).
44
+
45
+ ## [0.4.1] — 2026-06-28
46
+
47
+ ### Added
48
+
49
+ - The **`bench` benchmark submodule** is brought under version control and
50
+ aligned to the agreed 0.4.1 micro-surface (ADR-023,
51
+ `docs/dev/0.4.1-bench-surface.md`): probes measure per-call SpaceCore overhead
52
+ against a hand-optimal pure-array-library bare, across `space` / `linop` /
53
+ `functional` only (linalg, the synthetic kernel-comparison probes,
54
+ `check_member`, tree-space, and generated-linop probes removed). New
55
+ **configuration-regime axis** (`baseline` / `dispatch` / `dispatch_cache` /
56
+ `verify`) times the same probe under the ADR-016 dispatch and ADR-022 caching
57
+ toggles, recording a within-run `regime_speedup`; block operators are measured
58
+ uniform **and** ragged. The interactive HTML dashboard gains a problem-size
59
+ range slider, a backend/family/status/size/check/search/speedup filter set
60
+ that drives a fully **filter-reactive** summary and diagnosis section, a
61
+ bottom-of-page tag legend, and zero-count-status hiding. Every backend runs in
62
+ float64 for a fair comparison against the NumPy reference (JAX via
63
+ `enable_jax_x64`, Torch via `enable_torch_x64`); the sole exception is Apple
64
+ MPS (float32-only hardware), where the device probe builds a float32 case and
65
+ the correctness gate widens accordingly. The float64-aware tolerance is shared
66
+ by the verdict and diagnosis layers. **JAX is benchmarked only at
67
+ `check_level="none"`** and **jits both the SpaceCore call and the bare
68
+ reference**, comparing their post-compile steady state (the warmup absorbs
69
+ compilation, so the speedup is jitted-vs-jitted with compilation excluded);
70
+ each side's compile latency is recorded and reported separately (two
71
+ `sc compile` / `bare compile` columns). The pair is resolved **symmetrically** —
72
+ if either side is not jittable, both are timed eagerly, so a comparison is never
73
+ eager-vs-jitted. Tooling only — `spacecore` never imports `bench`.
12
74
  - ADR-016 optimized-kernel **dispatch** is accepted and implemented (off by
13
75
  default). `KernelSpec` gains `dispatch_key`, `priority`, and an optional
14
76
  shape-only `cost` estimator (`KernelCost`); a spec that names a `dispatch_key`
@@ -36,10 +98,64 @@ and the project adheres to [Semantic Versioning](https://semver.org/).
36
98
  shape-only `KernelCost` and the dispatcher gates it on the memory budget. All
37
99
  three route only under `dispatch_mode("on"/"verify")`; dispatch stays off by
38
100
  default. Each has a correctness reference and a `python -m bench` probe.
39
- - `NumpyOps.free_memory_bytes()` reports available system RAM via the optional
40
- `psutil` dependency (returns `None` — "no fuse" — when `psutil` is absent), so
41
- the dispatcher's memory gate can size materializing fast paths on the CPU
42
- backend.
101
+ - `NumpyOps.free_memory_bytes()` reports available system RAM via `psutil` (now a
102
+ required core dependency), so the dispatcher's memory gate can size
103
+ materializing fast paths on the CPU backend.
104
+ - Five more dispatch-eligible batched-matmul kernels (exact, `rtol=atol=0`,
105
+ NumPy-only until cross-backend bit-exactness is verified) extend the catalog to
106
+ the adjoint and batched directions, each wired through `dispatch` at its own new
107
+ key with a shape-only `KernelCost` and a `Class::method` correctness reference:
108
+ `block-diagonal-uniform-dense-batched-rapply` / `-vapply` / `-rvapply` (under
109
+ `linop.block_diagonal.rapply` / `.vapply` / `.rvapply`) and the broadcast-no-sum
110
+ folds `stacked-uniform-dense-batched-apply` /
111
+ `sum-to-single-uniform-dense-batched-rapply` (under `linop.stacked.apply` /
112
+ `linop.sum_to_single.rapply`). The adjoint folds exploit the Euclidean-flat
113
+ adjoint and so guard on `EUCLIDEAN_FLAT`; the batched folds use the dense core's
114
+ transpose-on-right orientation. All five reuse one shared
115
+ `spacecore.kernels.specs._batched` helper (stack uniform flat-dense matrices into
116
+ a single batched `matmul`) instead of duplicating the fold. The
117
+ `BlockDiagonalLinOp` (`rapply`/`vapply`/`rvapply`), `StackedLinOp` (`apply`) and
118
+ `SumToSingleLinOp` (`rapply`) call sites are wired through `dispatch`;
119
+ dispatch-off is result-identical to the prior inline paths. Off by default.
120
+ - `SparseLinOp` is confirmed to need no dispatch spec: every direction is already a
121
+ single optimal backend call — one SpMV for `apply`/`rapply` and one batched SpMV
122
+ over the stacked right-hand side for `vapply`/`rvapply` — so the reserved
123
+ `linop.matvec.sparse` key stays inert (a spec would add only dispatch overhead).
124
+ - ADR-021 **lazy-operator-algebra `fuse()`** (Tier-2 explicit simplification):
125
+ `LinOp.fuse(*, materialize=False)` collapses each maximal subtree of dense
126
+ operators into a single materialized `DenseLinOp` — a composition becomes the
127
+ matrix product `M_A @ M_B`, a scalar folds into the matrix, a sum of dense terms
128
+ is added into one matrix, an adjoint fuses its operand, and block/tree operators
129
+ fuse each component (so a composed-dense block becomes foldable by the
130
+ block-diagonal dispatch spec). It is mathematically equal to the original up to
131
+ floating-point rounding (fusion reassociates the arithmetic) and is
132
+ adjoint-consistent on any geometry — the shared middle-space Riesz maps cancel,
133
+ so fusion is not restricted to Euclidean spaces. A matrix-free operand is
134
+ **never** densified by the default `fuse()`; `fuse(materialize=True)` is the
135
+ explicit opt-in that densifies it (via the `to_dense` basis probe) so an
136
+ enclosing expression can collapse. Lives in `spacecore.linop`, separate from the
137
+ ADR-016 dispatch layer.
138
+ - ADR-022 **materialized-form cache** for the uniform batched folds: the
139
+ input-independent stacked block-matrix array `ops.stack([matrix(p) for p in
140
+ parts])` is now built once and reused across applies instead of re-stacked every
141
+ call. `BlockDiagonalLinOp`, `StackedLinOp`, and `SumToSingleLinOp` carry their
142
+ `parts` in a `spacecore.kernels.CachedStackParts` tuple that memoizes the stack
143
+ per matrix accessor (`_A2` apply, `_A2H` adjoint, `_A2T`/`_A2H.T` batched), so a
144
+ routed fold pays one `matmul` with no re-stack. The memo is built lazily on first
145
+ optimized use (NumPy-only, and only while dispatch is `on`/`verify`), so the
146
+ default `off` path is untouched. It is a derived value: excluded from operator
147
+ identity (`__eq__`/`__hash__`) and from the pytree (`tree_flatten` re-normalizes
148
+ `parts` to a plain tuple, so a round-trip rebuilds an empty cache), and a
149
+ matrix-free operand is never cache-materialized (the fold is inapplicable). The
150
+ dispatcher stays stateless. Dispatch-decision caching remains out of scope
151
+ (ADR-022 §"Decision caching").
152
+
153
+ ### Fixed
154
+
155
+ - Corrected stale `correctness_ref` node ids on two shipped `KernelSpec`s
156
+ (`composed-chain-apply`, `block-diagonal-dense-apply`) that named non-existent
157
+ module-level test nodes instead of the real `Class::method` ids (ADR-016
158
+ requires the reference pin an existing test).
43
159
 
44
160
  ## [0.4.0] — 2026-06-24
45
161
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spacecore
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Backend-agnostic vector spaces and linear operators.
5
5
  Author: Pavlo Pelikh
6
6
  License-Expression: Apache-2.0
@@ -17,6 +17,7 @@ License-File: LICENSE
17
17
  Requires-Dist: array-api-compat>=1.14.0
18
18
  Requires-Dist: numpy>=2.0.0
19
19
  Requires-Dist: optree>=0.17.0
20
+ Requires-Dist: psutil>=5.9.0
20
21
  Requires-Dist: scipy>=1.17
21
22
  Provides-Extra: jax
22
23
  Requires-Dist: jax>=0.9.1; extra == "jax"
@@ -35,6 +36,7 @@ Requires-Dist: pydata-sphinx-theme>=0.16; extra == "docs"
35
36
  Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
36
37
  Requires-Dist: sphinx-design>=0.6; extra == "docs"
37
38
  Requires-Dist: numpydoc>=1.8; extra == "docs"
39
+ Requires-Dist: ipython>=8.0; extra == "docs"
38
40
  Provides-Extra: dev
39
41
  Requires-Dist: pytest>=8.0; extra == "dev"
40
42
  Requires-Dist: pytest-cov>=5; extra == "dev"
@@ -0,0 +1,159 @@
1
+ # 0.4.1 Benchmark surface
2
+
3
+ Implementation checklist for the microbenchmark surface specified by
4
+ [ADR-023](adr/023_benchmark_framework.md). This is the **agreed scope** of *what
5
+ gets measured* and *what each operation is compared against*. Once an operation
6
+ is listed here it gets a probe whose `bare` callable is the **hand-optimal
7
+ pure-array-library** implementation (see ADR-023 "The pure-array-library
8
+ baseline").
9
+
10
+ ## Scope decisions
11
+
12
+ Settled in review:
13
+
14
+ - **`linalg` module — out.** Iterative solvers are not in the micro surface.
15
+ - **`kernel` probe family — out as a *family*.** The optimized folds are not
16
+ probed directly. They are still exercised: the `linop.block_diagonal` /
17
+ `composed` / `stacked` / `sum_to_single` probes route through them under the
18
+ `dispatch` / `dispatch+cache` regimes (below), so the kernel payoff is measured
19
+ on the *real operator*, not a synthetic kernel probe.
20
+ - **`check_member` and similar — out.** Operations with no real array-library
21
+ counterpart (the bare floor is a no-op) are not measured; they would report
22
+ "pure abstraction overhead" with nothing to compare against.
23
+ - **Tree operations — out.** `space.tree.*` is composition of leaf methods: if
24
+ the leaf methods are efficient, the tree op is efficient modulo `optree`
25
+ traversal time. Measuring it adds no information beyond the leaf probes plus a
26
+ fixed `optree` cost.
27
+ - **Block-structured operators — uniform AND ragged.** `block_diagonal`,
28
+ `stacked`, and `sum_to_single` each get a **uniform-blocks** variant (where a
29
+ batched-`matmul` fold exists) and a **ragged-blocks** variant (where it does
30
+ not). The pair is the point: uniform shows the fold's win; ragged shows the
31
+ abstraction adds ~nothing when no fast path applies.
32
+
33
+ Author rulings on the three open items (revise if disagreed):
34
+
35
+ - **`generated_{dense,diagonal}` — drop.** They duplicate `dense`/`diagonal`
36
+ built via a generator; generator-construction is not a per-call hot path.
37
+ - **Elementwise-Jordan ops — keep, low priority.** `spectrum` /
38
+ `spectral_decompose` / `star` / `from_spectrum` are mathematically thin on an
39
+ elementwise algebra but exercise the real Jordan API path; kept, flagged P2.
40
+ - **Bare for ragged blocks** = idiomatic per-block Python loop (or
41
+ `scipy.linalg.block_diag @ x` where assembling is the honest user choice), not
42
+ a forced batched form.
43
+
44
+ ## Configuration regimes
45
+
46
+ Every `linop` probe is timed under each regime so the ADR-016 dispatch and
47
+ ADR-022 caching wins are *measured*, not assumed. Regimes are driven by the real
48
+ API (`spacecore.kernels.dispatch_mode`, `set_memory_budget_fraction`):
49
+
50
+ | Regime | Dispatch | Materialized-form cache | How |
51
+ |---|---|---|---|
52
+ | `baseline` | off | inert | `dispatch_mode("off")` — today's default path |
53
+ | `dispatch` | on | cold each timed call | `dispatch_mode("on")` + per-call cache reset |
54
+ | `dispatch+cache` | on | warm (reused) | `dispatch_mode("on")`, operator reused across calls |
55
+ | `verify` | verify | warm | `dispatch_mode("verify")` — correctness, not headline speed |
56
+
57
+ **Cache reality (important).** SpaceCore has *no* cache on/off flag: the ADR-022
58
+ memo (`CachedStackParts`) builds lazily on first optimized use and lives on the
59
+ operator instance. Because `bench.harness.time_op` reuses one operator across all
60
+ repeats, the steady-state dispatch-on measurement *already* includes the cache.
61
+ To isolate the cache's contribution we therefore need two regimes:
62
+
63
+ - `dispatch+cache` is the natural reuse path (memo warm) — the full optimized
64
+ result.
65
+ - `dispatch` forces a **cold cache each timed call**. ADR-022 documents that a
66
+ pytree round-trip re-normalizes `parts` to a plain tuple and rebuilds an empty
67
+ cache, so the runner resets the memo via that round-trip before each sample.
68
+ This requires the probe to **expose its routed operator** to the runner (a
69
+ small `ProbeCase` extension), so the cache can be reset without rebuilding the
70
+ whole operator.
71
+
72
+ `space` and `functional` probes have no dispatch path and run `baseline` only.
73
+ The regime is part of every result row's coordinate and a group-by / pivot axis
74
+ in the dashboard.
75
+
76
+ ## Operation surface
77
+
78
+ `Obvious bare?`: ✅ one idiomatic library expression · 🟡 idiomatic but
79
+ multi-line / per-backend divergence · P-tier = build priority.
80
+
81
+ ### Family: `space`
82
+
83
+ | Operation | Bare (idiomatic, per backend) | Obvious? | Tier | Notes |
84
+ |---|---|---|---|---|
85
+ | `space.add` | `x + y` | ✅ | P0 | |
86
+ | `space.scale` | `a * x` | ✅ | P0 | |
87
+ | `space.inner` | `np.vdot(x, y)`; weighted `vdot(x, w*y)` | ✅ | P0 | weighted metric folds weight in |
88
+ | `space.norm` | `np.linalg.norm(x)`; weighted `sqrt(vdot(x, w*x))` | ✅ | P0 | |
89
+ | `space.zeros` | `np.zeros(shape, dtype)` | ✅ | P1 | |
90
+ | `space.flatten` / `unflatten` | `x.ravel()` / `x.reshape(shape)` | ✅ | P1 | |
91
+ | `space.convert.self` | `np.asarray` / `jnp.asarray` / `torch.as_tensor` | ✅ | P1 | cross-backend conversion |
92
+ | `space.stacked.{add,inner,zeros}` | vectorized op over leading batch axis | ✅ | P1 | |
93
+ | `space.hermitian.inner` | `np.vdot(A, B).real` (Frobenius) | ✅ | P1 | |
94
+ | `space.hermitian.symmetrize` | `(A + A.conj().T) / 2` | ✅ | P1 | |
95
+ | `space.hermitian.spectral_decompose` | `np.linalg.eigh` | ✅ | P1 | `spectrum` dropped — `spectrum`=`eigh(x)[0]`, same `eigh` |
96
+ | `space.hermitian.from_spectrum` | `(U * λ) @ U.conj().T` | ✅ | P1 | |
97
+ | `space.elementwise_jordan.{spectral_decompose,star,from_spectrum}` | identity / `x.conj()` | 🟡 | P2 | thin; `spectrum` dropped (== `spectral_decompose` minus the `None`) |
98
+
99
+ ### Family: `linop` (each run under all four regimes)
100
+
101
+ | Operation | Bare (idiomatic, per backend) | Obvious? | Tier | Notes |
102
+ |---|---|---|---|---|
103
+ | `linop.dense.{apply,rapply}` | `A @ x` / `A.conj().T @ y` | ✅ | P0 | rapply = conj-transpose on Euclidean |
104
+ | `linop.dense.{vapply,rvapply}` | `A @ X` (batched matmul) | ✅ | P0 | |
105
+ | `linop.dense.to_dense` | return `A` | ✅ | P2 | trivial |
106
+ | `linop.diagonal.{apply,rapply,vapply,rvapply}` | `d * x` / `d.conj() * y` | ✅ | P0 | |
107
+ | `linop.sparse.{apply,rapply}` | `scipy.sparse` SpMV; jax `BCOO`; `torch.sparse` | 🟡 | P1 | numpy obvious; jax/torch sparse less standard |
108
+ | `linop.identity.apply` | `x` | ✅ | P2 | |
109
+ | `linop.zero.{apply,rapply}` | `np.zeros_like(codomain)` | ✅ | P2 | |
110
+ | `linop.scaled.apply` | `a * (A @ x)` | ✅ | P1 | |
111
+ | `linop.composed.{apply,rapply}` | `A @ (B @ x)` | ✅ | P0 | dispatch: zero/identity elision |
112
+ | `linop.summed.apply` | `A @ x + B @ x` | ✅ | P1 | |
113
+ | `linop.block_diagonal.{apply,rapply}` — **uniform** | one batched `matmul` over stacked blocks | ✅ | P0 | dispatch fold target |
114
+ | `linop.block_diagonal.{apply,rapply}` — **ragged** | per-block loop / `block_diag @ x` | 🟡 | P0 | no fold → loop is honest floor |
115
+ | `linop.stacked.apply` — **uniform** / **ragged** | batched matmul / loop + concat | ✅ / 🟡 | P1 | |
116
+ | `linop.sum_to_single.apply` — **uniform** / **ragged** | batched matmul + reduce / loop + sum | ✅ / 🟡 | P1 | |
117
+ | `linop.matrix_free.diagonal_action.{apply,rapply}` | `d * x` | ✅ | P1 | |
118
+ | `linop.matrix_free.fft_action.apply` | `np.fft.fft(x)` (`jnp.fft`, `torch.fft`) | ✅ | P1 | |
119
+ | `linop.matrix_free.shift_action.apply` | `np.roll(x, k)` | ✅ | P1 | |
120
+
121
+ ### Family: `functional`
122
+
123
+ | Operation | Bare (idiomatic, per backend) | Obvious? | Tier | Notes |
124
+ |---|---|---|---|---|
125
+ | `functional.inner_product.value` | `np.vdot(a, x)` | ✅ | P0 | |
126
+ | `functional.inner_product.grad` | return `a` (Riesz rep) | ✅ | P0 | metric maps coordinate↔Riesz on weighted spaces |
127
+ | `functional.quadratic.value` | `0.5*vdot(x, A@x)` or `0.5*‖Ax−b‖²` | ✅ | P1 | |
128
+ | `functional.matrix_free.value` | `np.vdot(a, x)` | ✅ | P1 | |
129
+ | `functional.generated_linear.value` | `np.vdot(a, x)` | ✅ | P2 | |
130
+
131
+ ## Implementation phases
132
+
133
+ 1. **Regime infrastructure** — `bench/_regimes.py` (regime enum + per-regime
134
+ context), `regime` field on `ProbeResult` (+ `_io` round-trip), runner sweep
135
+ (`regimes=("baseline",)` default, non-baseline only for dispatch-eligible
136
+ families), `regime_speedup` derived metric, smoke test. *Non-destructive.*
137
+ 2. **Cache regime** — extend `ProbeCase` to expose the routed operator; runner
138
+ resets the memo (pytree round-trip) before each timed sample for the
139
+ `dispatch` regime; `dispatch+cache` reuses. Test the cold/warm distinction
140
+ actually moves on a uniform block fold.
141
+ 3. **Ragged block probes** — add ragged variants for `block_diagonal` /
142
+ `stacked` / `sum_to_single` with per-block-loop bares.
143
+ 4. **Bare-baseline audit** — verify every `bare` is hand-optimal per backend and
144
+ matches the reference value (runner already records `bare` error); fix any
145
+ strawman bares.
146
+ 5. **HTML aggregation** — embed raw cells + client-side group-by/pivot
147
+ (size / module / operation / backend / device / check_level / **regime**),
148
+ geometric-mean speedup, worst-case min, correctness gate.
149
+ 6. **Comparison / track** — `bench/_compare.py` join-then-diff at the group
150
+ level (regime in the join key), seed-jitter noise band, environment guard;
151
+ `track` for release-over-release trend.
152
+
153
+ Phases 1–4 are the measurement core; 5–6 are reporting. The out-of-scope
154
+ families have been removed from the suite to match this table: `linalg` probes,
155
+ the synthetic `kernel`-comparison probes (folds still measured via `linop` under
156
+ the dispatch regimes), `space.check_member`, the `space.tree.*` probes, and the
157
+ `linop.generated_{dense,diagonal}` probes are deleted; `functional.generated_linear.value`
158
+ and the elementwise-Jordan probes are kept. Final surface: `space` / `linop` /
159
+ `functional` only.
@@ -0,0 +1,131 @@
1
+ # ADR-021: Lazy operator algebra and simplification
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ SpaceCore builds operator expressions lazily: `A @ B`, `A + B`, `c * A`, `A.H`
10
+ construct `ComposedLinOp` / `SumLinOp` / `ScaledLinOp` / `_AdjointViewLinOp`
11
+ nodes rather than evaluating anything. Two distinct concerns have been
12
+ conflated:
13
+
14
+ 1. **Dispatch** — mapping an *operation* to its most efficient *array*
15
+ implementation at apply time (one batched `matmul` vs a per-block loop, an
16
+ SpMV, etc.). This is [ADR-016](016_kernel_layers.md), now scoped to dense and
17
+ sparse array execution, and held to a strict per-call rail: a routed spec
18
+ must be **bit-exact** to the inline path (`rtol == atol == 0`).
19
+
20
+ 2. **Lazy algebra** — simplifying the operator *expression* itself and
21
+ *multiplying operators where possible*: collapse `A @ I @ B` to `A @ B`, fold
22
+ `c · (d · A)` to `(c·d) · A`, multiply two dense operators into one matrix,
23
+ combine like terms. This rewrites the operator *once*, at construction or an
24
+ explicit step — not per call.
25
+
26
+ These are different mechanisms at different times, and the dispatch exactness
27
+ rail is the wrong constraint for the second: multiplying two dense operators and
28
+ then applying is not bit-identical to applying them in sequence (floating-point
29
+ addition is not associative), so under the ADR-016 rail "multiply operators
30
+ where possible" could never fire automatically. Yet a one-time build is not a
31
+ per-call equivalence contract — the user who asks to fuse `A @ B` into one
32
+ matrix has accepted that single reassociation.
33
+
34
+ Today the `make_composed` / `make_sum` / `make_scaled` factories already perform
35
+ cheap local rewrites at construction (`A @ I → A`, `A @ 0 → 0`,
36
+ `c · (d · A) → (c·d) · A`, flatten nested sums and drop zero terms, `(A*)* → A`
37
+ via the cached adjoint view). There is no operator *multiplication*
38
+ (`dense ∘ dense → one matrix`), no like-term combination, no composition-chain
39
+ flattening into a single operator, and no user-facing control over fusion.
40
+
41
+ ## Proposed design
42
+
43
+ Operator-expression simplification lives in `spacecore.linop` (the `make_*`
44
+ factories plus a `fuse` API), **not** in `spacecore.kernels`, and splits into two
45
+ tiers.
46
+
47
+ **Tier 1 — automatic at construction.** Cheap, local, non-materializing rewrites
48
+ run inside the `make_*` factories with no caller action. A rewrite is automatic
49
+ iff it is `O(1)`-local in the expression and allocates no array proportional to
50
+ operand size: identity elision, zero annihilation, scalar folding, sum
51
+ flattening + zero-term removal, the adjoint involution. These already largely
52
+ exist; this ADR names them a tier and fixes the boundary.
53
+
54
+ **Tier 2 — explicit, opt-in fusion.** Materializing or contract-changing
55
+ rewrites the caller must *request*, through a dedicated method / argument whose
56
+ arguments decide what is fused: multiply `dense ∘ dense` into one matrix, form a
57
+ Gram product `A.H @ A` as one materialized operator, flatten a composition chain
58
+ into a single fused operator, combine like terms (`2A + 3A → 5A`). Surface:
59
+ `expr.fuse(...)` and/or a `fuse=` argument on the algebra constructors (e.g.
60
+ `fuse="dense"`, `materialize=True`). The materialization itself is the subject of
61
+ [ADR-022](022_caching.md) (build it once, amortize over many applies).
62
+
63
+ **The auto/opt-in boundary is cost and locality, not strict bit-exactness.**
64
+ Tier-1 rewrites are cheap and local even when they reassociate at the ulp level
65
+ (scalar folding computes `c·d` first, which can differ from `c·(d·…)` by one
66
+ rounding); that is acceptable because the user invoking lazy algebra has accepted
67
+ construction-time reassociation. Tier-2 fusion is gated on *materialization /
68
+ contract change*, not on exactness.
69
+
70
+ **Matrix-free is a hard rail in both tiers** ([ADR-008](008_linop_subclasses.md)):
71
+ a matrix-free operand is never silently densified. Tier-1 rules never materialize
72
+ it; Tier-2 fusion of a matrix-free operand requires the caller to explicitly opt
73
+ into densification and thereby give up the matrix-free contract.
74
+
75
+ ## Decision
76
+
77
+ 1. Operator-expression simplification is a concern distinct from kernel dispatch
78
+ ([ADR-016](016_kernel_layers.md)); it lives in `spacecore.linop` (the `make_*`
79
+ factories and a `fuse` API), never in `spacecore.kernels`, and is not gated on
80
+ the ADR-016 bit-exact rail.
81
+ 2. Two tiers: cheap, local, non-materializing rewrites run automatically at
82
+ construction; materializing or contract-changing fusion is explicit and
83
+ argument-driven.
84
+ 3. The auto/opt-in line is cost and locality. Tier-1 rewrites may reassociate at
85
+ the ulp level; using lazy algebra accepts that.
86
+ 4. A matrix-free operand is never silently materialized in either tier.
87
+
88
+ ## Rationale
89
+
90
+ Keeping the runtime dispatch rail strict and exact while giving the user a way to
91
+ *buy* operator multiplication when they want it puts each kind of work where it
92
+ belongs: construction-time tree rewriting for the algebra, runtime kernel
93
+ selection for execution. It also resolves the contradiction that the dispatch
94
+ exactness rail otherwise forbids the very "multiply operators where possible"
95
+ the lazy algebra is meant to provide.
96
+
97
+ ## Alternatives considered
98
+
99
+ - **Route operator multiplication through ADR-016 dispatch with `rtol=atol=0`.**
100
+ Rejected: the exact rail forbids the floating-point reassociation operator
101
+ multiplication inevitably introduces, so it would never auto-fire; and a
102
+ one-time build is not a per-call equivalence contract.
103
+ - **Always-on eager fusion** (multiply operators at construction). Rejected:
104
+ surprising memory blowups (Gram, `dense ∘ dense`) and silent densification of
105
+ matrix-free operands. Materialization must be opt-in.
106
+ - **No fusion at all** (keep only the existing cheap rewrites). Rejected: it
107
+ leaves "multiply operators where possible" — an explicit goal — unaddressed.
108
+
109
+ ## Consequences
110
+
111
+ - New public API surface: a `fuse` method / argument with documented options;
112
+ each fusion documents what it materializes and what it costs.
113
+ - The existing `make_*` cheap rewrites are retroactively framed as Tier 1.
114
+ - Tests pin Tier-1 rewrites as result-equivalent (within ulp where they
115
+ reassociate) and Tier-2 fusion as opt-in and matrix-free-safe.
116
+ - This ADR is the prerequisite framing for [ADR-022](022_caching.md): the cached
117
+ thing is the Tier-2 materialized form.
118
+
119
+ ## Contributor invariants
120
+
121
+ - Cheap, local, non-materializing operator rewrites go in the `make_*` factories
122
+ (Tier 1). Materializing or contract-changing fusion is explicit, argument-
123
+ driven, and never automatic (Tier 2).
124
+ - A matrix-free operand ([ADR-008](008_linop_subclasses.md)) is never densified
125
+ without explicit caller opt-in.
126
+ - Expression simplification is not kernel dispatch: do not add operator-algebra
127
+ rewrites to `spacecore.kernels`, and do not gate them on the ADR-016 exact
128
+ rail.
129
+ - A construction-time rewrite must preserve the operator's mathematical action
130
+ (within ulp where it reassociates) and its declared domain/codomain and
131
+ scalar-field/dtype identity.
@@ -0,0 +1,156 @@
1
+ # ADR-022: Caching of materialized and fused operator forms
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Context
8
+
9
+ Two parts of SpaceCore materialize an array from operands:
10
+
11
+ - The [ADR-016](016_kernel_layers.md) dispatch folds (uniform block-diagonal and
12
+ stacked / sum-to-single batched `matmul`) stack `K` cached matrices into one
13
+ `(K, m, n)` array per call.
14
+ - The [ADR-021](021_lazy_operator_algebra_and_simplification.md) explicit fusions
15
+ (`dense ∘ dense → one matrix`, Gram `A.H @ A`, a flattened composition chain)
16
+ build a fused matrix.
17
+
18
+ Done per call, this materialization is repeated work. The 0.4.1 dispatch
19
+ measurements showed the per-call stacking allocation is a meaningful fraction of
20
+ the optimized path and a reason the dense folds do **not** beat the per-block
21
+ generic loop on the NumPy/CPU backend: NumPy's small-matmul loop is already
22
+ cheap, and re-stacking every apply only adds cost.
23
+
24
+ ADR-016 already states the principle — a super-linear materialization is a
25
+ *build-time* decision amortized over many applies, not a per-call one — but no
26
+ caching mechanism exists yet. Two cache candidates surfaced during the 0.4.1
27
+ work:
28
+
29
+ 1. caching the **materialized array** (the stacked / fused matrix) on the
30
+ operator, so each apply skips the re-stack; and
31
+ 2. caching the **dispatch decision** (which spec applies for a fixed operator),
32
+ so each apply skips the `applicable()` re-scan.
33
+
34
+ The maintainer scoped (2) out for now. This ADR therefore decides (1) — the
35
+ build-time materialized form — and records (2) as considered and deferred.
36
+
37
+ ## Proposed design
38
+
39
+ **What is cached (precisely).** The cache target is the **input-independent
40
+ factor** of a batched fold: the stacked operand-matrix array
41
+
42
+ ```python
43
+ ops.stack([matrix(p) for p in parts]) # shape (K, r, c)
44
+ ```
45
+
46
+ built today on every apply in `batched_matvec` / `batched_right_matmul`
47
+ (`spacecore/kernels/specs/_batched.py`). It depends only on the operator's fixed
48
+ blocks, so it is identical across applies — yet a uniform block-diagonal /
49
+ stacked / sum-to-single operator rebuilds it each call because it has no field to
50
+ keep it in. That, and only that, is what gets cached. Explicitly:
51
+
52
+ - **Cached:** the stacked operand matrices, *per matrix accessor exercised* —
53
+ `_A2` for the forward action (apply), `_A2H` for the Euclidean-adjoint action
54
+ (rapply), and the right-matmul stack. An operator applied in more than one
55
+ direction therefore holds more than one cached stack.
56
+ - **Not cached:** the stacked *input* vectors (`ops.stack(list(vecs))`) and the
57
+ `matmul` result — both depend on the call argument and differ every apply.
58
+ - **Not new mechanism:** the [ADR-021](021_lazy_operator_algebra_and_simplification.md)
59
+ fusions (`dense ∘ dense`, Gram `A.H @ A`, a flattened chain) already persist
60
+ their materialized matrix in the `DenseLinOp` that `fuse()` returns. They need
61
+ no cache field; they appear here only as the explicit opt-in amortization
62
+ branch below. The genuinely uncached case is the dispatch fold, which
63
+ materializes transiently on the per-call path and returns a result rather than
64
+ an operator.
65
+
66
+ **Build-time materialized-form cache.** An operator that benefits from a
67
+ materialized fast path (uniform block-diagonal, stacked / sum-to-single) caches
68
+ the stacked operand-matrix array(s) above once — at construction or first use —
69
+ behind two gates:
70
+
71
+ - the ADR-016 **memory gate**: the predicted `peak_bytes` fits the context's
72
+ memory budget, and a matrix-free operand is never materialized; and
73
+ - an **amortization check**: the form is cached only when the operator is
74
+ expected to be applied enough times to repay the materialization, or when the
75
+ caller explicitly opts in via [ADR-021](021_lazy_operator_algebra_and_simplification.md)
76
+ fusion.
77
+
78
+ Subsequent applies route to the already-materialized array at `O(1)` extra, so
79
+ the per-apply cost is one `matmul` with no re-stacking. This is the lever that
80
+ makes a materializing fold competitive once it routes — most importantly on
81
+ accelerators, where the batched call is the win and the one-time stack is
82
+ negligible against many applies.
83
+
84
+ **Pytree hazard (mandatory).** `BlockDiagonalLinOp` / `StackedLinOp` /
85
+ `SumToSingleLinOp` and the dense operators are `@jax_pytree_class`. A cached
86
+ array must not corrupt `tree_flatten` / `tree_unflatten`: the cache is a derived,
87
+ reconstructable value, so it is either excluded from the pytree (recomputed
88
+ lazily after `tree_unflatten`) or carried consistently as aux — never silently
89
+ turned into an extra differentiable/transformable leaf. For the same reason the
90
+ cache is **excluded from the operator's mathematical identity** (`__eq__` /
91
+ `__hash__`), exactly as raw array values are.
92
+
93
+ **Decision caching (deferred).** Caching the applicable-spec selection per
94
+ operator instance — to remove the per-call `applicable()` re-scan over the `K`
95
+ blocks — was considered. It is deferred: the maintainer scoped it out, and the
96
+ 0.4.1 measurement showed the dispatcher's per-call overhead (≈6–7 µs, including a
97
+ `psutil` free-memory query) is dominated by the public method's boundary
98
+ validation, so decision caching is not the first lever. If it is revisited, the
99
+ cache key is the operator's structural signature (invariant for a fixed
100
+ operator), the memo lives in the **dispatcher** (not operator/functional class
101
+ bodies, per ADR-016), and it must respect the dynamic `dispatch_mode` (cache only
102
+ the `"on"` selection; `"verify"` must always run both; the cheap mode check still
103
+ runs each call).
104
+
105
+ ## Decision
106
+
107
+ 1. The stacked operand-matrix array(s) of a batched fold — the input-independent
108
+ `ops.stack([matrix(p) for p in parts])`, per accessor (`_A2`, `_A2H`,
109
+ right-matmul) — may be cached on the operator as a derived value, gated by the
110
+ ADR-016 memory gate **plus** an amortization check, and excluded from operator
111
+ identity and (pytree-safely) from `tree_flatten`. The input-vector stack and
112
+ the result are never cached. ADR-021 fusions already persist their matrix in
113
+ the returned `DenseLinOp` and need no new field.
114
+ 2. Caching never materializes a matrix-free operand ([ADR-008](008_linop_subclasses.md)).
115
+ 3. Dispatch-decision caching is out of scope for now. If added later, it lives in
116
+ the dispatcher, is keyed by structural signature, and respects `dispatch_mode`.
117
+
118
+ ## Rationale
119
+
120
+ Turning a repeated per-call materialization into a one-time build amortized over
121
+ many applies is the ADR-016 intent made concrete, and the measured way to make a
122
+ materializing fold pay for itself once it routes. Keeping the cache a property of
123
+ the operator (not the per-call path) preserves the matrix-free rail and the
124
+ operator's mathematical identity, and keeps the dispatcher stateless.
125
+
126
+ ## Alternatives considered
127
+
128
+ - **Per-call materialization (status quo).** Correct but re-stacks every apply —
129
+ the measured reason the dense folds lose on CPU.
130
+ - **Eager, always-on materialization at construction.** Rejected: memory blowups
131
+ and silent densification of matrix-free operands; must be gated, amortized, and
132
+ opt-in.
133
+ - **Decision caching as the primary lever.** Deferred: not the dominant cost per
134
+ the 0.4.1 measurement, and explicitly scoped out by the maintainer.
135
+
136
+ ## Consequences
137
+
138
+ - A cache field (one slot per stacked accessor — `_A2`, `_A2H`, right-matmul) on
139
+ the block-diagonal / stacked / sum-to-single operators, with pytree-safe
140
+ handling and identity exclusion.
141
+ - An amortization heuristic or explicit opt-in, tied to ADR-021 fusion.
142
+ - Tests pin: cached stack == the per-call `ops.stack(...)` and cached apply
143
+ result == uncached result; the cache is excluded from `__eq__` / `__hash__` and
144
+ survives a pytree round-trip; a fold containing a matrix-free operand is never
145
+ cache-materialized.
146
+
147
+ ## Contributor invariants
148
+
149
+ - A cached materialized form is derived and reconstructable: it is excluded from
150
+ `__eq__` / `__hash__` and from the operator's mathematical identity, and a
151
+ pytree round-trip must not depend on it.
152
+ - Materialization caching obeys the ADR-016 memory gate and never densifies a
153
+ matrix-free operand ([ADR-008](008_linop_subclasses.md)).
154
+ - Decision caching, if ever added, lives in the dispatcher (not operator /
155
+ functional class bodies, per [ADR-016](016_kernel_layers.md)) and respects the
156
+ active `dispatch_mode`.