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.
- {spacecore-0.4.0 → spacecore-0.4.2}/CHANGELOG.md +120 -4
- {spacecore-0.4.0 → spacecore-0.4.2}/PKG-INFO +3 -1
- spacecore-0.4.2/docs/dev/0.4.1-bench-surface.md +159 -0
- spacecore-0.4.2/docs/dev/adr/021_lazy_operator_algebra_and_simplification.md +131 -0
- spacecore-0.4.2/docs/dev/adr/022_caching.md +156 -0
- spacecore-0.4.2/docs/dev/adr/023_benchmark_framework.md +415 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/README.md +3 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/release_notes.rst +53 -0
- spacecore-0.4.2/docs/source/tutorials/09_kernels_and_fusion.rst +531 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/index.rst +6 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/pyproject.toml +5 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/__init__.py +16 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_contextual/_state.py +7 -2
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_version.py +1 -1
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/numpy/_ops.py +7 -13
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/__init__.py +16 -0
- spacecore-0.4.2/spacecore/functional/_algebra.py +440 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/_base.py +95 -10
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/__init__.py +1 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/sparse.py +10 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/__init__.py +6 -1
- spacecore-0.4.2/spacecore/kernels/specs/_batched.py +234 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/_policy.py +1 -2
- spacecore-0.4.2/spacecore/kernels/specs/block_batched.py +358 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/block_diagonal.py +1 -1
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/composed.py +1 -1
- spacecore-0.4.2/spacecore/kernels/specs/stacked_batched.py +167 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_algebra.py +97 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_base.py +37 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/_block.py +78 -4
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/_from_single.py +34 -1
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/_to_single.py +34 -1
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/optimize/__init__.py +2 -1
- spacecore-0.4.2/spacecore/optimize/_optax.py +529 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_jordan.py +12 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_dense_vector.py +14 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_hermitian.py +14 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_stacked.py +25 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_tree_space.py +75 -6
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/PKG-INFO +3 -1
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/SOURCES.txt +21 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/requires.txt +2 -0
- spacecore-0.4.2/tests/bench/test_bare_baseline.py +49 -0
- spacecore-0.4.2/tests/bench/test_bench_smoke.py +581 -0
- spacecore-0.4.2/tests/bench/test_macro_aggregate.py +140 -0
- spacecore-0.4.2/tests/bench/test_macro_dashboard.py +200 -0
- spacecore-0.4.2/tests/bench/test_macro_runner_smoke.py +220 -0
- spacecore-0.4.2/tests/bench/test_macro_schema.py +162 -0
- spacecore-0.4.2/tests/bench/test_regimes.py +131 -0
- spacecore-0.4.2/tests/functional/test_algebra.py +297 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_functional_base.py +69 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/functionals.py +77 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_public_api.py +1 -1
- spacecore-0.4.2/tests/integration/test_smoke_torch.py +63 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_kernels_match_generic.py +234 -0
- spacecore-0.4.2/tests/kernels/test_materialized_cache.py +266 -0
- spacecore-0.4.2/tests/linops/test_fuse.py +323 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/test_contracts.py +4 -4
- spacecore-0.4.2/tests/optimize/test_minimize_optax.py +349 -0
- spacecore-0.4.2/tests/spaces/__init__.py +0 -0
- spacecore-0.4.2/tests/spaces/test_jordan_invariants.py +185 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_stacked_space.py +9 -0
- spacecore-0.4.2/tests/spaces/test_tree_spectral_decomposition.py +238 -0
- spacecore-0.4.2/tutorials/09_kernels_and_fusion.ipynb +810 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/README.md +7 -0
- spacecore-0.4.0/spacecore/kernels/specs/block_batched.py +0 -159
- spacecore-0.4.0/spacecore/optimize/_optax.py +0 -154
- spacecore-0.4.0/tests/integration/test_smoke_torch.py +0 -34
- spacecore-0.4.0/tests/optimize/test_minimize_optax.py +0 -113
- spacecore-0.4.0/tests/spaces/test_tree_spectral_decomposition.py +0 -79
- {spacecore-0.4.0 → spacecore-0.4.2}/CONTRIBUTING.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/LICENSE +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/MANIFEST.in +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/README.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/0.3.1-docs-inventory.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/0.3.1-docstring-audit.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/001_backend_layer.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/002_context_and_conversion.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/003_space_hierarchy.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/004_inner_product_and_geometry.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/005_space_subclasses_and_capabilities.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/006_current_batching_model.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/007_linop_contract.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/008_linop_subclasses.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/009_metric_adjoint.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/010_functional_contract.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/011_linalg_contract.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/012_jordan_spectrum.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/013_tree_structured_spaces.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/014_check_policy.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/015_dtype_default_vs_scalar_field.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/016_kernel_layers.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/017_tensor_product_spaces.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/018_external_optimizer_adapters.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/019_everyday_toolbox.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/020_sets_and_projection.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/adr/template.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/architecture.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/labels.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/linop_generators.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/prerequisites.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/process.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/contributing/setup.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/current.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/docstring_style.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/dev/vision.md +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/_static/custom.css +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/backend.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/context.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/functionals.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/index.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/linalg.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/linops.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/optimize.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/api/spaces.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/conf.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/backend_conformance.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/backend_deviations.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/backend_ops_array_api.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/batching.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/batching_test_policy.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/capability_dispatch.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/checking_policy.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/context_ownership.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/conversion_policy.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/dtype_policy.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/geometry.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/index.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/jax_integration.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/kernels_policy.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/performance.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/design/tree_spaces.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/dev/adr.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/dev/contributing.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/dev/index.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/dev/vision.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/index.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/01_backend_and_context.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/02_linear_algebra.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/03_functionals.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/04_tree_spaces.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/05_weighted_tikhonov.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/06_optimal_transport.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/07_manifold_descent.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/docs/source/tutorials/08_pdhg_conic_program.rst +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/setup.cfg +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_batching.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_check_policy.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_checks.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_contextual/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_contextual/_bound.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_contextual/_policies.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/_repr.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/_context.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/_eager.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/_family.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/_ops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/cupy/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/cupy/_ops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/jax/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/jax/_ops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/jax/_pytree.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/numpy/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/torch/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/backend/torch/_ops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/_composed.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/_linear.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/_quadratic.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_coordinate.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_entropy.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_huber.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_least_squares.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_norms.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_proximal.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/functional/tools/_spectral.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/_rules.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/algebra.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/dense.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/diagonal.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/core/functional.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/_dispatch.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/_registry.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/kernels/specs/composed_simplify.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_cg.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_expm.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_lanczos.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_lsqr.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_power.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linalg/_utils.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_dense.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_diagonal.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_metric.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/_sparse.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/linop/tree/_base.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/optimize/_common.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/optimize/_scipy.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_coordinate.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_inner_product.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_space.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_star.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/base/_vector.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/checks/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/checks/_base.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/checks/_coordinate.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/space/concrete/_dense_coordinate.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/types/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/types/_array.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/types/_dtype.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore/types/_misc.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/dependency_links.txt +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/spacecore.egg-info/top_level.txt +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/_conformance.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/_helpers.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/_conformance.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/_references.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/conftest.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_backend_family.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_context.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_cupy_ops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_jax_ops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_jax_pytree_class.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_lazy_namespace.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_numpy_ops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_operations.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_torch_ops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/backend/test_types_protocols.py +0 -0
- {spacecore-0.4.0/tests/context → spacecore-0.4.2/tests/bench}/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/conftest.py +0 -0
- {spacecore-0.4.0/tests/integration → spacecore-0.4.2/tests/context}/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/conftest.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_check_policy.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_check_policy_helpers.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_checked_method.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_compatibility.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_context_bound.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_enable_checks.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_policies_errors.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/context/test_state_free_functions.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/conftest.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_composed_functional.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_generated_functionals.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_inner_product_functional.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_linear_functional.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_linop_quadratic_form.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_matrix_free_linear_functional.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_metric_gradient.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/test_quadratic_form.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/tools/test_battery_functionals.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/tools/test_least_squares.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/tools/test_proximal.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/functional/tools/test_spectral.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_arrays.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_contexts.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_hermitian.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_metrics.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_params.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_protocol.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_seed.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/_trees.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/linops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/spaces.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/test_registry_completeness.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/generators/test_smoke.py +0 -0
- {spacecore-0.4.0/tests/kernels → spacecore-0.4.2/tests/integration}/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_github_labels.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_imports.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_smoke_jax.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/integration/test_smoke_numpy.py +0 -0
- {spacecore-0.4.0/tests/linops → spacecore-0.4.2/tests/kernels}/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/conftest.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_core_kernel_dispatch.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_kernel_dispatch.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_kernel_registry.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/kernels/test_kernel_spec.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/_helpers.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/conftest.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_cg.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_core_resolution.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_expm_multiply.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_generated_solver_matrix.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_lanczos.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_lsqr.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_power_iteration.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_result_types.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_solver_contracts.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linalg/test_utils.py +0 -0
- {spacecore-0.4.0/tests/optim → spacecore-0.4.2/tests/linops}/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/conftest.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_algebra_factories.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_algebra_linops.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_block_diagonal_linop.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_block_matrix_linop.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_dense_linop.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_diagonal_linop.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_fused_algebra_overhead.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_generated_linop_laws.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_linop_base.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_linop_jit.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_metric_helpers.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_sparse_linop.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_stacked_linop.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_sum_to_single_linop.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_tree_helpers.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/linops/test_tree_linop_base.py +0 -0
- {spacecore-0.4.0/tests/optimize → spacecore-0.4.2/tests/optim}/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/optim/test_cached_member_checks.py +0 -0
- {spacecore-0.4.0/tests/spaces → spacecore-0.4.2/tests/optimize}/__init__.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/_helpers.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/test_line_search_scipy.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/test_metric_handoff.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/optimize/test_minimize_scipy.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/_generated_helpers.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/conftest.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_check_batched.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_coordinate_space_base.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_dense_coordinate_space.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_dense_vector_space.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_elementwise_jordan_spaces.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_dense_coordinate.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_dense_vector.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_inner_product.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_jordan_space.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_space_laws.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_generated_tree_space.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_hermitian_space.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_inner_product.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_inner_product_space_base.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_jordan_algebra_spaces.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_space_base.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_space_checks.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_star_space_base.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_tree_space.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/spaces/test_vector_space_base.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/test_repr.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tests/test_weighted_tikhonov.py +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/01_backend_and_context.ipynb +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/02_linear_algebra.ipynb +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/03_functionals.ipynb +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/04_tree_spaces.ipynb +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/05_weighted_tikhonov.ipynb +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/06_optimal_transport.ipynb +0 -0
- {spacecore-0.4.0 → spacecore-0.4.2}/tutorials/07_manifold_descent.ipynb +0 -0
- {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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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`.
|