cvmatrix 3.2.3__tar.gz → 3.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/.github/actions/release/action.yml +1 -1
  2. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/.github/actions/test/action.yml +20 -0
  3. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/.github/workflows/pull_request_test_workflow.yml +7 -2
  4. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/.github/workflows/test_workflow.yml +7 -2
  5. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/CHANGELOG.md +14 -0
  6. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/CONTRIBUTING.md +45 -11
  7. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/PKG-INFO +2 -1
  8. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/cvmatrix/__init__.py +1 -1
  9. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/cvmatrix/cvmatrix.py +258 -71
  10. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/pyproject.toml +2 -1
  11. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/tests/test_cvmatrix.py +175 -0
  12. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/uv.lock +38 -38
  13. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/.github/actions/build/action.yml +0 -0
  14. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/.github/workflows/package_workflow.yml +0 -0
  15. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/.github/workflows/pull_request_package_workflow.yml +0 -0
  16. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/.gitignore +0 -0
  17. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/.readthedocs.yaml +0 -0
  18. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/LICENSE +0 -0
  19. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/README.md +0 -0
  20. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/README.md +0 -0
  21. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/__init__.py +0 -0
  22. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/benchmark.py +0 -0
  23. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/benchmark_cvmatrix.png +0 -0
  24. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/benchmark_cvmatrix_numpy_vs_jax.png +0 -0
  25. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/benchmark_cvmatrix_vs_naive.png +0 -0
  26. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/benchmark_jax_variants.png +0 -0
  27. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/benchmark_results.csv +0 -0
  28. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/benchmark_results_jax.csv +0 -0
  29. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/benchmarks/plot_benchmark.py +0 -0
  30. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/cvmatrix/partitioner.py +0 -0
  31. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/CONTRIBUTING.md +0 -0
  32. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/Makefile +0 -0
  33. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/README.md +0 -0
  34. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/api.rst +0 -0
  35. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/conf.py +0 -0
  36. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/index.rst +0 -0
  37. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/make.bat +0 -0
  38. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/requirements.txt +0 -0
  39. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/source/cvmatrix.rst +0 -0
  40. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/source/cvmatrix_module.rst +0 -0
  41. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/docs/source/partitioner_module.rst +0 -0
  42. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/examples/__init__.py +0 -0
  43. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/examples/training_matrices.py +0 -0
  44. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/examples/training_matrices_jax.py +0 -0
  45. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/tests/__init__.py +0 -0
  46. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/tests/load_data.py +0 -0
  47. {cvmatrix-3.2.3 → cvmatrix-3.3.0}/tests/naive_cvmatrix.py +0 -0
@@ -16,7 +16,7 @@ runs:
16
16
  name: python-package-distributions
17
17
  path: dist/
18
18
  - name: Sign the dists with Sigstore
19
- uses: sigstore/gh-action-sigstore-python@v3.4.0
19
+ uses: sigstore/gh-action-sigstore-python@v3.5.0
20
20
  with:
21
21
  inputs: >-
22
22
  ./dist/*.tar.gz
@@ -28,6 +28,26 @@ runs:
28
28
  run: uv sync --group dev
29
29
  shell: bash
30
30
 
31
+ # On free-threaded Python (e.g. 3.15t), importing an extension module that does not
32
+ # support free threading silently re-enables the GIL, and the tests would then pass
33
+ # without exercising free threading. Fail if any test dependency re-enables it.
34
+ - name: Check that the GIL stays disabled (free-threaded Python only)
35
+ if: ${{ endsWith(inputs.PYTHON_VERSION, 't') }}
36
+ run: |
37
+ uv run python - <<'EOF'
38
+ import importlib.util
39
+ import sys
40
+
41
+ import coverage, cvmatrix, numpy, pandas, pytest, typeguard # noqa: E401, F401
42
+
43
+ if importlib.util.find_spec("jax") is not None:
44
+ import jax.numpy # noqa: F401
45
+ if sys._is_gil_enabled():
46
+ sys.exit("The GIL was re-enabled by an imported extension module.")
47
+ print("The GIL stays disabled with all test dependencies imported.")
48
+ EOF
49
+ shell: bash
50
+
31
51
  # The same test suite runs in both modes. With the JAX backend installed it also
32
52
  # exercises the parametrized backend="jax" tests; without JAX those parametrizations
33
53
  # are skipped (pytest.importorskip), verifying the numpy-only path.
@@ -18,7 +18,12 @@ jobs:
18
18
  fail-fast: false
19
19
  matrix:
20
20
  os: [ubuntu-latest, windows-latest, macos-latest]
21
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
21
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.15t"]
22
+ # jaxlib publishes no free-threaded (cp315t) wheel for Windows, and no sdist.
23
+ # Windows x 3.15t is still tested by the numpy-only job below.
24
+ exclude:
25
+ - os: windows-latest
26
+ python-version: "3.15t"
22
27
  steps:
23
28
  - uses: actions/checkout@v7
24
29
  - uses: ./.github/actions/test
@@ -42,7 +47,7 @@ jobs:
42
47
  fail-fast: false
43
48
  matrix:
44
49
  os: [ubuntu-latest, windows-latest, macos-latest]
45
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
50
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.15t"]
46
51
  steps:
47
52
  - uses: actions/checkout@v7
48
53
  - uses: ./.github/actions/test
@@ -18,7 +18,12 @@ jobs:
18
18
  fail-fast: false
19
19
  matrix:
20
20
  os: [ubuntu-latest, windows-latest, macos-latest]
21
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
21
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.15t"]
22
+ # jaxlib publishes no free-threaded (cp315t) wheel for Windows, and no sdist.
23
+ # Windows x 3.15t is still tested by the numpy-only job below.
24
+ exclude:
25
+ - os: windows-latest
26
+ python-version: "3.15t"
22
27
  steps:
23
28
  - uses: actions/checkout@v7
24
29
  - uses: ./.github/actions/test
@@ -42,7 +47,7 @@ jobs:
42
47
  fail-fast: false
43
48
  matrix:
44
49
  os: [ubuntu-latest, windows-latest, macos-latest]
45
- python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
50
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15", "3.15t"]
46
51
  steps:
47
52
  - uses: actions/checkout@v7
48
53
  - uses: ./.github/actions/test
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.3.0] - 2026-10-30
9
+
10
+ ### Added
11
+ - `compute_XTX` and `compute_XTY` constructor parameters (both default `True`, the previous behavior) that control whether `fit` computes the dataset-wide `XTX` (`X^T W X`) and `XTY` (`X^T W Y`). A product that is never used can now be skipped: e.g., `compute_XTX=False` avoids the O(NK²) time and K² memory of `XTX` when only `training_XTY` and/or `training_statistics` are used, which can dominate `fit` for wide `X`. A product that is not computed is `None`, and the training methods that need it raise a `ValueError`. The products are still computed in `fit`, so they are pickled along with a fitted `CVMatrix` and are not recomputed per task when cross-validating in parallel worker processes (e.g., with `joblib`).
12
+ - Support for free-threaded Python. The CI test matrices now include free-threaded Python 3.15 (`3.15t`) on Linux, macOS, and Windows, with a check that no dependency re-enables the GIL. The JAX backend is tested on `3.15t` on Linux and macOS only, as jaxlib publishes no free-threaded wheel for Windows. The package declares the `Programming Language :: Python :: Free Threading :: 3 - Stable` classifier. A fitted `CVMatrix` can be shared by multiple threads, as the per-fold training methods only read it.
13
+
14
+ ### Changed
15
+ - The CI test workflow for pushes to `main` now also tests Python 3.15, matching the pull-request workflow.
16
+ - The per-fold training methods now gather only the validation set rows they use. Previously, the unweighted validation set rows of `X` were gathered, but not used, by `training_XTY` and `training_statistics`. With `weights` on the numpy backend, this makes `training_XTY` ~20% faster and `training_statistics` ~25-45% faster. Results are unchanged. Unweighted computations and computations traced by `jax.jit` were not affected.
17
+
18
+ ### Fixed
19
+ - A fitted `CVMatrix` can be pickled with the standard library again. Since 3.2.0, the instance held the array namespace `xp` (the `numpy` or `jax.numpy` module), which cannot be pickled, so `pickle`, `copy.deepcopy`, `multiprocessing`, and `concurrent.futures.ProcessPoolExecutor` failed with `TypeError: cannot pickle 'module' object` (`joblib`'s default `loky` backend was not affected, as it pickles with `cloudpickle`). The module is now left out of the pickled state and resolved again from `backend` when unpickling. With `backend="jax"`, JAX's 64-bit mode is enabled before the arrays are restored, so float64 data stays float64 in fresh worker processes, where it would otherwise silently become float32.
20
+ - Re-fitting a `CVMatrix` now replaces all state derived from `Y` instead of keeping it from the previous fit. Re-fitting without `Y` resets `XTY`, `WY`, `sum_Y`, `sq_Y`, and `sum_sq_Y` to `None`, and re-fitting with weights that do not require `WY` (no `center_X`, `center_Y`, or `scale_Y`) resets `WY` to `None`; likewise, `sq_X` is reset to `None` when `scale_X` is `False`. Results were not affected, but the stale arrays were kept in memory and pickled along with the fitted model (e.g., to parallel worker processes).
21
+
8
22
  ## [3.2.3] - 2026-10-30
9
23
  - Increased the maximum supported Python version from 3.14 to 3.15. Python 3.15 is now included in the CI test matrices. No functional changes.
10
24
 
@@ -30,18 +30,28 @@ The sections below outline the steps in each case.
30
30
  1. (**Important**) Announce your plan to the rest of the community *before you start working*. This announcement should be in the form of a (new) issue;
31
31
  2. (**Important**) Wait until some kind of consensus is reached about your idea being a good idea;
32
32
  3. If needed, fork the repository to your own Github profile and create your own feature branch off of the latest main commit. While working on your feature branch, make sure to stay up to date with the main branch by pulling in changes, possibly from the 'upstream' repository (follow the instructions [here](https://help.github.com/articles/configuring-a-remote-for-a-fork/) and [here](https://help.github.com/articles/syncing-a-fork/));
33
- 4. Make sure the existing tests still work by following the instructions in [Run the test suite](#run-the-test-suite);
34
- 5. Add your own tests (if necessary);
35
- 6. If you added your own tests, make sure they pass by following the instructions in [Run the test suite](#run-the-test-suite);
36
- 7. If your contribution is a performance enhancement, make sure to include benchmarks. See [Benchmarking](#benchmarking) for more information;
37
- 8. Update or expand the documentation;
38
- 9. Make sure the documentation builds without errors by following the instructions in [Build the documentation](#build-the-documentation);
39
- 10. Check that you can build the package locally and that it passes twine check. See [Build from source](#build-from-source) for more information;
40
- 11. [Push](http://rogerdudler.github.io/git-guide/) your feature branch to (your fork of) the CVMatrix repository on GitHub;
41
- 12. Create the pull request, e.g. following the instructions [here](https://help.github.com/articles/creating-a-pull-request/).
33
+ 4. Follow the [Design guidelines](#design-guidelines) when changing `CVMatrix`;
34
+ 5. Make sure the existing tests still work by following the instructions in [Run the test suite](#run-the-test-suite);
35
+ 6. Add your own tests (if necessary);
36
+ 7. If you added your own tests, make sure they pass by following the instructions in [Run the test suite](#run-the-test-suite);
37
+ 8. If your contribution is a performance enhancement, make sure to include benchmarks. See [Benchmarking](#benchmarking) for more information;
38
+ 9. Update or expand the documentation;
39
+ 10. Make sure the documentation builds without errors by following the instructions in [Build the documentation](#build-the-documentation);
40
+ 11. Check that you can build the package locally and that it passes twine check. See [Build from source](#build-from-source) for more information;
41
+ 12. [Push](http://rogerdudler.github.io/git-guide/) your feature branch to (your fork of) the CVMatrix repository on GitHub;
42
+ 13. Create the pull request, e.g. following the instructions [here](https://help.github.com/articles/creating-a-pull-request/).
42
43
 
43
44
  If you feel like you've made a valuable contribution, but you don't know how to write or run tests for it or generate the documentation, don't let this discourage you from making the pull request; we can help you! Just submit the pull request, but remember that you might be asked to append additional commits to your pull request.
44
45
 
46
+ ## Design guidelines
47
+
48
+ `CVMatrix` is used in several execution settings: traced by `jax.jit`/`jax.vmap` with `backend="jax"`, called concurrently from multiple threads (including on free-threaded Python, which has no GIL), and pickled to worker processes (e.g., with `joblib`, `multiprocessing`, or `concurrent.futures.ProcessPoolExecutor`). To keep it correct and fast in all of them:
49
+
50
+ - **The per-fold methods only read the fitted state.** `training_XTX`, `training_XTY`, `training_XTX_XTY`, and `training_statistics` must not modify the instance, e.g., by caching results on it. Compute anything dataset-wide in `fit`. A cache written during a per-fold call would leak tracers under `jax.jit`, race between threads, and, in worker processes, be filled in a throwaway copy, i.e., recomputed for every task.
51
+ - **Route array operations through `self.xp`**, not `np`, so that the NumPy and JAX backends share one implementation and the per-fold methods stay traceable. Data-dependent checks that cannot run under tracing are skipped for tracer inputs (see `_TRACER_TYPES`). The NumPy backend's results must not change.
52
+ - **Keep instances picklable with the standard library's `pickle`.** Do not store modules, locks, open files, or other unpicklable objects on the instance. If one is unavoidable, leave it out in `__reduce__` and restore it in `_reconstruct`, as is done for the array namespace `xp`. `test_pickle` and `test_process_pool` check this.
53
+ - **Test both backends.** Every test runs with both `backend="numpy"` and `backend="jax"` through the `_set_backend` fixture in `tests/test_cvmatrix.py`; JAX-only tests skip themselves under the NumPy parametrization.
54
+
45
55
  ## Build from source
46
56
 
47
57
  CVMatrix uses [uv](https://docs.astral.sh/uv/) to manage its dependencies and packaging. To build the package from source, follow these steps:
@@ -101,9 +111,25 @@ To run the test suite, follow these steps:
101
111
  3. Now, the tests can be run with the following command:
102
112
 
103
113
  ```shell
104
- uv run pytest tests --doctest-modules --junitxml=junit/test-results.xml --cov=cvmatrix/ --cov-report=xml --cov-report=html --typeguard-packages=cvmatrix/
114
+ uv run pytest tests --doctest-modules
105
115
  ```
106
116
 
117
+ The pytest options in `pyproject.toml` enable runtime type checking with [typeguard](https://typeguard.readthedocs.io/) (`--typeguard-packages=cvmatrix`) and coverage reports. Note that `--typeguard-packages` takes the package name `cvmatrix`, not a path: `cvmatrix/` silently instruments nothing.
118
+
119
+ ### Test on free-threaded Python
120
+
121
+ CVMatrix also runs on free-threaded builds of Python (e.g., `3.15t`), which have no GIL. To run the test suite on one without touching your default environment, let uv create a temporary, isolated environment:
122
+
123
+ ```shell
124
+ uv run --isolated --python 3.15t --extra jax --group dev pytest tests --doctest-modules
125
+ ```
126
+
127
+ A free-threaded interpreter silently re-enables the GIL if it imports an extension module that does not support free threading. Check that the GIL stays disabled with all dependencies imported; this must print `False`:
128
+
129
+ ```shell
130
+ uv run --isolated --python 3.15t --extra jax python -c "import sys, cvmatrix, jax; print(sys._is_gil_enabled())"
131
+ ```
132
+
107
133
  ## Build the documentation
108
134
 
109
135
  1. Install sphinx, the sphinx-rtd-theme, and the MyST-Parser:
@@ -126,4 +152,12 @@ To run the test suite, follow these steps:
126
152
 
127
153
  ## Benchmarking
128
154
 
129
- To run benchmarks, follow the instructions [here](https://github.com/Sm00thix/CVMatrix/blob/main/benchmarks/README.md).
155
+ To run benchmarks, follow the instructions [here](https://github.com/Sm00thix/CVMatrix/blob/main/benchmarks/README.md).
156
+
157
+ `benchmarks/benchmark.py` can be configured with environment variables, e.g., `BENCH_CONFIGS=plot` (only the three preprocessing combinations in the plots), `BENCH_PS=3,100,10000` (the numbers of folds), `BENCH_N`/`BENCH_K`/`BENCH_M` (the data size), and `BENCH_CSV` (the output file):
158
+
159
+ ```shell
160
+ OPENBLAS_NUM_THREADS=1 BENCH_CONFIGS=plot BENCH_PS=3,100,10000 BENCH_CSV=my_results.csv uv run python benchmarks/benchmark.py
161
+ ```
162
+
163
+ When you compare the performance of your change against `main`, run both versions on the same machine with a single BLAS thread, as above, and alternate between them over several rounds, also with the order reversed. The per-fold timings depend on the memory allocation history of the process, so identical code can differ by several percent between runs, and a fixed run order can look like a consistent difference.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cvmatrix
3
- Version: 3.2.3
3
+ Version: 3.3.0
4
4
  Summary: Fast computation of possibly weighted and possibly centered/scaled training set kernel matrices in a cross-validation setting.
5
5
  Project-URL: Homepage, https://cvmatrix.readthedocs.io/en/latest/
6
6
  Project-URL: Repository, https://github.com/Sm00thix/CVMatrix
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Programming Language :: Python :: 3.15
17
+ Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
17
18
  Requires-Python: <3.16,>=3.10
18
19
  Requires-Dist: numpy<3.0.0,>=2.0.0
19
20
  Provides-Extra: jax
@@ -1,4 +1,4 @@
1
- __version__ = "3.2.3"
1
+ __version__ = "3.3.0"
2
2
  __all__ = ["CVMatrix", "Partitioner"]
3
3
  from .cvmatrix import CVMatrix
4
4
  from .partitioner import Partitioner
@@ -100,6 +100,37 @@ def _resolve_backend(backend: str):
100
100
  raise ValueError(f"Invalid backend: {backend!r}. Must be 'numpy' or 'jax'.")
101
101
 
102
102
 
103
+ def _reconstruct(cls: type, backend: str, dtype: FloatDType) -> CVMatrix:
104
+ """
105
+ Creates an empty instance of `cls`, a `CVMatrix` or a subclass, with its array
106
+ backend set up. Used by `CVMatrix.__reduce__` for unpickling: pickle calls it before
107
+ restoring the pickled arrays, so that, with the jax backend and a 64-bit `dtype`,
108
+ JAX's 64-bit mode is enabled before the arrays are rebuilt. Otherwise, float64
109
+ arrays would silently become float32 in a fresh process, e.g., a worker process.
110
+
111
+ Parameters
112
+ ----------
113
+ cls : type
114
+ The class of the pickled instance.
115
+
116
+ backend : {"numpy", "jax"}
117
+ The array backend of the pickled instance.
118
+
119
+ dtype : type[np.floating]
120
+ The data type of the pickled instance.
121
+
122
+ Returns
123
+ -------
124
+ CVMatrix
125
+ An instance of `cls` with `backend`, `dtype`, and `xp` set.
126
+ """
127
+ obj = cls.__new__(cls)
128
+ obj.backend = backend
129
+ obj.dtype = dtype
130
+ obj._init_backend()
131
+ return obj
132
+
133
+
103
134
  class CVMatrix:
104
135
  r"""
105
136
  Implements the fast cross-validation algorithms for kernel matrix-based models such
@@ -156,6 +187,31 @@ class CVMatrix:
156
187
  input arrays are not NumPy arrays of type `dtype`, then a copy is made. If
157
188
  `True` a copy is always made. If no copy is made, then external modifications
158
189
  to `X` or `Y` will result in undefined behavior.
190
+
191
+ backend : {"numpy", "jax"}, optional, default="numpy"
192
+ The array backend used for the computations. `"numpy"` uses NumPy, which is
193
+ the only required dependency. `"jax"` uses `jax.numpy` so that the per-fold
194
+ computations of `training_XTX`, `training_XTY`, `training_XTX_XTY`, and
195
+ `training_statistics` can be traced by `jax.jit` and batched over folds with
196
+ `jax.vmap` on CPU, GPU, or TPU. It requires the optional JAX dependency, which
197
+ is installed with `pip install cvmatrix[jax]`; otherwise, an `ImportError` is
198
+ raised. If `"jax"` is used with a 64-bit `dtype`, JAX's 64-bit mode
199
+ (`jax_enable_x64`) is enabled globally so that `dtype` is honored instead of
200
+ being truncated to 32 bits.
201
+
202
+ compute_XTX : bool, optional, default=True
203
+ Whether `fit` computes the dataset-wide
204
+ :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{X}`. Its cost grows with the
205
+ square of the number of predictor variables in `X`, so for wide `X` it can
206
+ dominate `fit`. Set this to `False` if neither `training_XTX` nor
207
+ `training_XTX_XTY` will be called. If `False`, these methods raise a
208
+ `ValueError`.
209
+
210
+ compute_XTY : bool, optional, default=True
211
+ Whether `fit` computes the dataset-wide
212
+ :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{Y}` when `Y` is provided. Set
213
+ this to `False` if neither `training_XTY` nor `training_XTX_XTY` will be
214
+ called. If `False`, these methods raise a `ValueError`.
159
215
  """
160
216
 
161
217
  def __init__(
@@ -168,6 +224,8 @@ class CVMatrix:
168
224
  dtype: FloatDType = np.float64,
169
225
  copy: bool = True,
170
226
  backend: Literal["numpy", "jax"] = "numpy",
227
+ compute_XTX: bool = True,
228
+ compute_XTY: bool = True,
171
229
  ) -> None:
172
230
  self.center_X = center_X
173
231
  self.center_Y = center_Y
@@ -176,18 +234,10 @@ class CVMatrix:
176
234
  self.ddof = ddof
177
235
  self.dtype = dtype.type if isinstance(dtype, np.dtype) else dtype
178
236
  self.copy = copy
237
+ self.compute_XTX = compute_XTX
238
+ self.compute_XTY = compute_XTY
179
239
  self.backend = backend
180
- # Array namespace: numpy (default) or jax.numpy. All array operations are routed
181
- # through `self.xp` so that, with backend="jax", the per-fold `training_*`
182
- # methods can be traced by jax.jit/jax.vmap. The numpy backend is byte-identical
183
- # to previous releases.
184
- self.xp = _resolve_backend(backend)
185
- if backend == "jax" and np.dtype(self.dtype).itemsize >= 8:
186
- # JAX defaults to 32-bit; enable 64-bit precision so a requested float64
187
- # dtype is honored instead of being silently truncated to float32.
188
- import jax
189
-
190
- jax.config.update("jax_enable_x64", True)
240
+ self._init_backend()
191
241
  self.resolution = np.finfo(dtype).resolution * 10
192
242
  self.X = None
193
243
  self.Y = None
@@ -208,6 +258,18 @@ class CVMatrix:
208
258
  self.sum_w = None
209
259
  self.num_nonzero_w = None
210
260
 
261
+ def __reduce__(self) -> tuple:
262
+ """
263
+ Supports pickling with the standard library, e.g., to send a fitted `CVMatrix`
264
+ to worker processes with `multiprocessing` or
265
+ `concurrent.futures.ProcessPoolExecutor`. The array namespace `xp` is a module,
266
+ which cannot be pickled, so it is left out of the pickled state and resolved
267
+ again from `backend` by `_reconstruct` before the state is restored.
268
+ """
269
+ state = self.__dict__.copy()
270
+ del state["xp"]
271
+ return _reconstruct, (type(self), self.backend, self.dtype), state
272
+
211
273
  def fit(
212
274
  self,
213
275
  X: npt.ArrayLike,
@@ -216,8 +278,9 @@ class CVMatrix:
216
278
  ) -> None:
217
279
  r"""
218
280
  Loads and stores `X`, `Y`, and "weights", for cross-validation. Computes
219
- dataset-wide :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{X}` and, if `Y` is
220
- not `None`, :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{Y}`. If `center_X`,
281
+ dataset-wide :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{X}` if
282
+ `compute_XTX` is `True` and, if `Y` is not `None` and `compute_XTY` is `True`,
283
+ :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{Y}`. If `center_X`,
221
284
  `center_Y`, `scale_X`, or `scale_Y` is `True`, the corresponding global
222
285
  statistics are also computed.
223
286
 
@@ -252,13 +315,14 @@ class CVMatrix:
252
315
  M : int or None
253
316
  The number of response variables in `Y`. If `Y` is `None`, this is `None`.
254
317
 
255
- XTX : Array
318
+ XTX : Array or None
256
319
  The total matrix :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{X}` for the
257
- entire dataset.
320
+ entire dataset. This is computed only if `compute_XTX` is `True`.
258
321
 
259
322
  XTY : Array or None
260
323
  The total matrix :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{Y}` for the
261
- entire dataset. This is computed only if `Y` is not `None`.
324
+ entire dataset. This is computed only if `Y` is not `None` and
325
+ `compute_XTY` is `True`.
262
326
 
263
327
  sum_X : Array or None
264
328
  The row of column-wise weighted sums of `X` for the entire dataset. This is
@@ -362,6 +426,7 @@ class CVMatrix:
362
426
  Raises
363
427
  ------
364
428
  ValueError
429
+ - If `fit` did not compute `XTX` because `compute_XTX` was `False`.
365
430
  - If `self.center_X` or `self.scale_X` is `True` but cannot be computed due
366
431
  to the training set having no non-zero weights.
367
432
  - If `self.scale_X` is `True` and the corresponding weighted standard
@@ -426,6 +491,7 @@ class CVMatrix:
426
491
  ------
427
492
  ValueError
428
493
  - If `Y` is `None`.
494
+ - If `fit` did not compute `XTY` because `compute_XTY` was `False`.
429
495
  - If `self.center_X`, `self.center_Y`, `self.scale_X`, or `self.scale_Y` is
430
496
  `True` and the corresponding weighted means and standard deviations
431
497
  cannot be computed due to the training set having no non-zero
@@ -496,6 +562,8 @@ class CVMatrix:
496
562
  ------
497
563
  ValueError
498
564
  - If `Y` is `None`.
565
+ - If `fit` did not compute `XTX` because `compute_XTX` was `False`.
566
+ - If `fit` did not compute `XTY` because `compute_XTY` was `False`.
499
567
  - If `self.center_X`, `self.center_Y`, `self.scale_X`, or `self.scale_Y` is
500
568
  `True` and the corresponding weighted means and standard deviations
501
569
  cannot be computed due to the training set having no non-zero
@@ -562,16 +630,22 @@ class CVMatrix:
562
630
  set.
563
631
 
564
632
  """
565
- X_val, X_val_unweighted, Y_val, Y_val_unweighted = self._get_val_matrices(
566
- val_indices=validation_indices, return_XTY=self.Y is not None
633
+ return_X_mean = self.center_X or self.scale_X
634
+ return_Y_mean = (self.center_Y or self.scale_Y) and self.Y is not None
635
+ X_val, _, Y_val, _ = self._get_val_matrices(
636
+ val_indices=validation_indices,
637
+ return_X_val=return_X_mean,
638
+ return_X_val_unweighted=False,
639
+ return_Y_val=return_Y_mean,
640
+ return_Y_val_unweighted=False,
567
641
  )
568
642
  return self._compute_training_stats(
569
643
  val_indices=validation_indices,
570
644
  X_val=X_val,
571
645
  Y_val=Y_val,
572
- return_X_mean=self.center_X or self.scale_X,
646
+ return_X_mean=return_X_mean,
573
647
  return_X_std=self.scale_X,
574
- return_Y_mean=(self.center_Y or self.scale_Y) and self.Y is not None,
648
+ return_Y_mean=return_Y_mean,
575
649
  return_Y_std=self.scale_Y and self.Y is not None,
576
650
  )[:-1] # Exclude the sum of training weights from the return tuple
577
651
 
@@ -775,11 +849,11 @@ class CVMatrix:
775
849
  Whether to return the training set
776
850
  :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{X}`.
777
851
 
778
- return_XTY : bool, optional, default=False
852
+ return_XTY : bool
779
853
  Whether to return the training set
780
854
  :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{Y}`.
781
855
 
782
- validation_indices : IndexArray
856
+ val_indices : IndexArray
783
857
  An integer array of indices for the validation set for which to
784
858
  return the corresponding training set
785
859
  :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{X}` and
@@ -802,8 +876,10 @@ class CVMatrix:
802
876
  Raises
803
877
  ------
804
878
  ValueError
805
- If both `return_XTX` and `return_XTY` are `False` or if `return_XTY` is
806
- `True` and `Y` is `None`.
879
+ If both `return_XTX` and `return_XTY` are `False`, if `return_XTY` is
880
+ `True` and `Y` is `None`, if `return_XTX` is `True` and `fit` did not
881
+ compute `XTX` because `compute_XTX` was `False`, or if `return_XTY` is
882
+ `True` and `fit` did not compute `XTY` because `compute_XTY` was `False`.
807
883
  """
808
884
  if not return_XTX and not return_XTY:
809
885
  raise ValueError(
@@ -811,8 +887,28 @@ class CVMatrix:
811
887
  )
812
888
  if return_XTY and self.Y is None:
813
889
  raise ValueError("Response variables `Y` are not provided.")
890
+ # Check the fitted products rather than the flags, so that the flags cannot
891
+ # disagree with what `fit` actually computed.
892
+ if return_XTX and self.XTX is None:
893
+ raise ValueError(
894
+ "`XTX` was not computed by `fit`. Fit a CVMatrix constructed with "
895
+ "`compute_XTX=True` to use `training_XTX` or `training_XTX_XTY`."
896
+ )
897
+ if return_XTY and self.XTY is None:
898
+ raise ValueError(
899
+ "`XTY` was not computed by `fit`. Fit a CVMatrix constructed with "
900
+ "`compute_XTY=True` to use `training_XTY` or `training_XTX_XTY`."
901
+ )
902
+ # The weighted `Y_val` feeds the weighted mean and standard deviation of `Y`,
903
+ # which centering (`center_X` or `center_Y`) and scaling (`scale_Y`) of `XTY`
904
+ # use.
905
+ return_Y_val = return_XTY and self._needs_Y_stats
814
906
  X_val, X_val_unweighted, Y_val, Y_val_unweighted = self._get_val_matrices(
815
- val_indices=val_indices, return_XTY=return_XTY
907
+ val_indices=val_indices,
908
+ return_X_val=True,
909
+ return_X_val_unweighted=return_XTX,
910
+ return_Y_val=return_Y_val,
911
+ return_Y_val_unweighted=return_XTY,
816
912
  )
817
913
  X_train_mean, X_train_std, Y_train_mean, Y_train_std, sum_w_train = (
818
914
  self._compute_training_stats(
@@ -822,11 +918,7 @@ class CVMatrix:
822
918
  if self.center_X or self.scale_X or (return_XTY and self.center_Y)
823
919
  else None
824
920
  ),
825
- Y_val=(
826
- Y_val
827
- if return_XTY and (self.center_X or self.center_Y or self.scale_Y)
828
- else None
829
- ),
921
+ Y_val=Y_val,
830
922
  return_X_mean=self.center_X or (return_XTY and self.center_Y),
831
923
  return_Y_mean=return_XTY and (self.center_X or self.center_Y),
832
924
  return_X_std=self.scale_X,
@@ -898,50 +990,106 @@ class CVMatrix:
898
990
  )
899
991
 
900
992
  def _get_val_matrices(
901
- self, val_indices: IndexArray, return_XTY: bool
993
+ self,
994
+ val_indices: IndexArray,
995
+ *,
996
+ return_X_val: bool,
997
+ return_X_val_unweighted: bool,
998
+ return_Y_val: bool,
999
+ return_Y_val_unweighted: bool,
902
1000
  ) -> Tuple[
903
- Array,
904
- Array,
1001
+ Optional[Array],
1002
+ Optional[Array],
905
1003
  Optional[Array],
906
1004
  Optional[Array],
907
1005
  ]:
908
1006
  """
909
- Returns the validation set matrices for a given fold.
1007
+ Returns the requested validation set matrices for a given fold. Only the
1008
+ requested matrices are gathered, as gathering the validation set rows is a
1009
+ significant part of the cost of each fold.
1010
+
910
1011
  Parameters
911
1012
  ----------
912
1013
  val_indices : Array of shape (N_val,)
913
1014
  The indices of the validation set samples for the given fold.
914
- return_XTY : bool
915
- Whether to return the validation set of response variables `Y`. If `False`,
916
- the returned `Y_val` and `Y_val_unweighted` will be `None`.
1015
+
1016
+ return_X_val : bool
1017
+ Whether to return the validation set of weighted predictor variables.
1018
+
1019
+ return_X_val_unweighted : bool
1020
+ Whether to return the validation set of unweighted predictor variables.
1021
+
1022
+ return_Y_val : bool
1023
+ Whether to return the validation set of response variables, weighted if
1024
+ `WY` is computed and unweighted otherwise.
1025
+
1026
+ return_Y_val_unweighted : bool
1027
+ Whether to return the validation set of unweighted response variables.
1028
+
917
1029
  Returns
918
1030
  -------
919
- Tuple of arrays of shapes (N_val, K), (N_val, K), (N_val, M), and (N_val, M)
920
- The validation set of predictor variables `X`, the validation set of
921
- unweighted predictor variables `X_unweighted`, the validation set of
922
- response variables `Y`, and the validation set of unweighted response
923
- variables `Y_unweighted`. If `return_XTY` is `False`, `Y` and
924
- `Y_unweighted` will be `None`.
1031
+ Tuple of four elements of Optional[Array]
1032
+ The validation sets of predictor variables `X_val` of shape (N_val, K),
1033
+ unweighted predictor variables `X_val_unweighted` of shape (N_val, K),
1034
+ response variables `Y_val` of shape (N_val, M), and unweighted response
1035
+ variables `Y_val_unweighted` of shape (N_val, M). A matrix that is not
1036
+ requested is `None`.
925
1037
  """
926
- X_val = self.WX[val_indices]
927
- if self.weights is None:
928
- X_val_unweighted = X_val
929
- else:
930
- X_val_unweighted = self.X[val_indices]
931
- if return_XTY:
932
- if self.weights is None or not (
933
- self.center_X or self.center_Y or self.scale_Y
934
- ):
935
- Y_val = self.Y[val_indices]
936
- Y_val_unweighted = Y_val
937
- else:
938
- Y_val = self.WY[val_indices]
939
- Y_val_unweighted = self.Y[val_indices]
1038
+ X_val, X_val_unweighted = self._gather_val_pair(
1039
+ val_indices, self.WX, self.X, return_X_val, return_X_val_unweighted
1040
+ )
1041
+ if self.weights is None or not self._needs_Y_stats:
1042
+ Y_maybe_weighted = self.Y
940
1043
  else:
941
- Y_val = None
942
- Y_val_unweighted = None
1044
+ Y_maybe_weighted = self.WY
1045
+ Y_val, Y_val_unweighted = self._gather_val_pair(
1046
+ val_indices, Y_maybe_weighted, self.Y, return_Y_val, return_Y_val_unweighted
1047
+ )
943
1048
  return X_val, X_val_unweighted, Y_val, Y_val_unweighted
944
1049
 
1050
+ @staticmethod
1051
+ def _gather_val_pair(
1052
+ val_indices: IndexArray,
1053
+ mat: Optional[Array],
1054
+ mat_unweighted: Optional[Array],
1055
+ return_val: bool,
1056
+ return_val_unweighted: bool,
1057
+ ) -> Tuple[Optional[Array], Optional[Array]]:
1058
+ """
1059
+ Gathers the requested validation set rows of a possibly weighted matrix and of
1060
+ its unweighted counterpart. If both are requested and they are the same array
1061
+ (e.g., if no weights are used), the rows are gathered only once.
1062
+
1063
+ Parameters
1064
+ ----------
1065
+ val_indices : Array of shape (N_val,)
1066
+ The indices of the validation set samples for the given fold.
1067
+
1068
+ mat : None or Array of shape (N, K) or (N, M)
1069
+ The possibly weighted matrix, `WX` or `WY`.
1070
+
1071
+ mat_unweighted : None or Array of shape (N, K) or (N, M)
1072
+ The unweighted matrix, `X` or `Y`.
1073
+
1074
+ return_val : bool
1075
+ Whether to return the validation set rows of `mat`.
1076
+
1077
+ return_val_unweighted : bool
1078
+ Whether to return the validation set rows of `mat_unweighted`.
1079
+
1080
+ Returns
1081
+ -------
1082
+ Tuple of two elements of Optional[Array]
1083
+ The validation set rows of `mat` and of `mat_unweighted`. A matrix that is
1084
+ not requested is `None`.
1085
+ """
1086
+ mat_val = mat[val_indices] if return_val else None
1087
+ if not return_val_unweighted:
1088
+ return mat_val, None
1089
+ if mat_val is not None and mat is mat_unweighted:
1090
+ return mat_val, mat_val
1091
+ return mat_val, mat_unweighted[val_indices]
1092
+
945
1093
  def _training_kernel_matrix(
946
1094
  self,
947
1095
  total_kernel_mat: Array,
@@ -1130,6 +1278,21 @@ class CVMatrix:
1130
1278
  mat_train_std = xp.where(mat_train_std <= self.resolution, 1, mat_train_std)
1131
1279
  return mat_train_std
1132
1280
 
1281
+ def _init_backend(self) -> None:
1282
+ """
1283
+ Resolves the array namespace `xp` from `backend`: numpy (the default) or
1284
+ jax.numpy. All array operations are routed through `xp` so that, with the jax
1285
+ backend, the per-fold `training_*` methods can be traced by jax.jit/jax.vmap.
1286
+ The numpy backend is byte-identical to previous releases. JAX defaults to 32
1287
+ bits, so with the jax backend and a 64-bit `dtype`, JAX's 64-bit mode is enabled
1288
+ so that `dtype` is honored instead of being silently truncated to float32.
1289
+ """
1290
+ self.xp = _resolve_backend(self.backend)
1291
+ if self.backend == "jax" and np.dtype(self.dtype).itemsize >= 8:
1292
+ import jax
1293
+
1294
+ jax.config.update("jax_enable_x64", True)
1295
+
1133
1296
  def _init_mat(self, mat: Array) -> Array:
1134
1297
  """
1135
1298
  Casts the matrix to the dtype specified in the constructor and reshapes it if
@@ -1192,35 +1355,55 @@ class CVMatrix:
1192
1355
  else:
1193
1356
  self.weights = None
1194
1357
 
1358
+ @property
1359
+ def _needs_Y_stats(self) -> bool:
1360
+ r"""
1361
+ Whether the training set :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{Y}`
1362
+ uses the weighted statistics of `Y`: the row of column-wise weighted means for
1363
+ centering (`center_X` or `center_Y`) and, additionally, the row of column-wise
1364
+ weighted standard deviations for scaling (`scale_Y`). Only then are `WY` (with
1365
+ weights) and `sum_Y` computed and the weighted validation set rows of `Y`
1366
+ gathered.
1367
+ """
1368
+ return self.center_X or self.center_Y or self.scale_Y
1369
+
1195
1370
  def _init_weighted_mats(self):
1196
1371
  """
1197
- Initializes the weighted matrices `WX` and `WY` if weights are
1198
- provided. These matrices are computed as the product of the original matrices
1199
- `X` and `Y` with the `weights`. If `Y` is `None`, then `WY` is not initialized.
1200
- If `w` is `None`, then this method does nothing.
1372
+ Initializes the weighted matrices `WX` and `WY`. Without weights, they are `X`
1373
+ and `Y` themselves. With weights, they are computed as the product of `X` and
1374
+ `Y` with the `weights`, where `WY` is computed only if `Y` is not `None` and its
1375
+ weighted statistics are needed (see `_needs_Y_stats`). If `WY` is not
1376
+ computed, it is set to `None` so that no `WY` from a previous fit is kept.
1201
1377
  """
1202
1378
  if self.weights is None:
1203
1379
  self.WX = self.X
1204
- if self.Y is not None:
1205
- self.WY = self.Y
1380
+ self.WY = self.Y
1206
1381
  else:
1207
1382
  self.WX = self.X * self.weights
1208
- if self.Y is not None and (self.center_X or self.center_Y or self.scale_Y):
1383
+ if self.Y is not None and self._needs_Y_stats:
1209
1384
  self.WY = self.Y * self.weights
1385
+ else:
1386
+ self.WY = None
1210
1387
 
1211
1388
  def _init_matrix_products(self) -> None:
1212
1389
  r"""
1213
1390
  Initializes the global matrix products `XTX` and `XTY` for the
1214
1391
  entire dataset. These are :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{X}`
1215
- and :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{Y}`, respectively.
1392
+ and :math:`\mathbf{X}^{\mathbf{T}}\mathbf{W}\mathbf{Y}`, respectively. A product
1393
+ that is not computed, due to `compute_XTX` or `compute_XTY` being `False` or
1394
+ `Y` being `None`, is set to `None`.
1216
1395
  """
1217
- self.XTX = self.WX.T @ self.X
1218
- if self.Y is not None:
1396
+ self.XTX = self.WX.T @ self.X if self.compute_XTX else None
1397
+ if self.compute_XTY and self.Y is not None:
1219
1398
  self.XTY = self.WX.T @ self.Y
1399
+ else:
1400
+ self.XTY = None
1220
1401
 
1221
1402
  def _init_stats(self) -> None:
1222
1403
  """
1223
- Initializes the global statistics for `X` and `Y`.
1404
+ Initializes the global statistics for `X` and `Y`. The statistics of `Y` and
1405
+ the squared matrices that are not computed are set to `None` so that none from
1406
+ a previous fit are kept.
1224
1407
  """
1225
1408
  if self.center_X or self.center_Y or self.scale_X or self.scale_Y:
1226
1409
  if self.weights is not None:
@@ -1231,15 +1414,19 @@ class CVMatrix:
1231
1414
  self.num_nonzero_w = self.N
1232
1415
  if self.center_X or self.center_Y or self.scale_X:
1233
1416
  self.sum_X = self.xp.sum(self.WX, axis=0, keepdims=True)
1234
- if (self.center_X or self.center_Y or self.scale_Y) and self.Y is not None:
1417
+ if self._needs_Y_stats and self.Y is not None:
1235
1418
  self.sum_Y = self.xp.sum(self.WY, axis=0, keepdims=True)
1419
+ else:
1420
+ self.sum_Y = None
1236
1421
  if self.scale_X:
1237
1422
  self.sq_X = self.WX * self.X
1238
1423
  self.sum_sq_X = self.xp.sum(self.sq_X, axis=0, keepdims=True)
1239
1424
  else:
1425
+ self.sq_X = None
1240
1426
  self.sum_sq_X = None
1241
1427
  if self.scale_Y and self.Y is not None:
1242
1428
  self.sq_Y = self.WY * self.Y
1243
1429
  self.sum_sq_Y = self.xp.sum(self.sq_Y, axis=0, keepdims=True)
1244
1430
  else:
1431
+ self.sq_Y = None
1245
1432
  self.sum_sq_Y = None
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cvmatrix"
3
- version = "3.2.3"
3
+ version = "3.3.0"
4
4
  description = "Fast computation of possibly weighted and possibly centered/scaled training set kernel matrices in a cross-validation setting."
5
5
  authors = [{ name = "Sm00thix", email = "oleemail@icloud.com" }]
6
6
  maintainers = [{ name = "Sm00thix", email = "oleemail@icloud.com" }]
@@ -14,6 +14,7 @@ classifiers = [
14
14
  "Programming Language :: Python :: 3.13",
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Programming Language :: Python :: 3.15",
17
+ "Programming Language :: Python :: Free Threading :: 3 - Stable",
17
18
  ]
18
19
  dependencies = [
19
20
  "numpy>=2.0.0,<3.0.0",
@@ -15,8 +15,12 @@ Author: Ole-Christian Galbo Engstrøm
15
15
  E-mail: ocge@foss.dk
16
16
  """
17
17
 
18
+ import multiprocessing
19
+ import pickle
18
20
  import sys
19
21
  from collections.abc import Hashable
22
+ from concurrent.futures import ProcessPoolExecutor
23
+ from copy import deepcopy
20
24
  from itertools import product
21
25
  from typing import Iterable, Union
22
26
 
@@ -682,6 +686,177 @@ class TestClass:
682
686
  (XTWX, _), _ = jax.jit(cvm.training_XTX_XTY)(jnp.asarray(degenerate_val))
683
687
  assert XTWX.shape == (4, 4)
684
688
 
689
+ def test_compute_matrix_products(self):
690
+ """
691
+ Tests that `compute_XTX` and `compute_XTY` control which dataset-wide matrix
692
+ products `fit` computes, that the training methods needing a product that is
693
+ not computed raise a ValueError, and that the remaining methods give the same
694
+ results as when both products are computed.
695
+ """
696
+
697
+ def leaves(result):
698
+ if isinstance(result, tuple):
699
+ return [leaf for r in result for leaf in leaves(r)]
700
+ return [result]
701
+
702
+ X = self.load_X()[:, :5]
703
+ Y = self.load_Y(["Protein", "Moisture"])
704
+ weights = self.load_weights(random=True)
705
+ folds = self.load_Y(["split"]).squeeze()
706
+ p = Partitioner(folds)
707
+ val_indices = p.get_validation_indices(next(iter(p.folds_dict)))
708
+ reference = CVMatrix(backend=self.backend)
709
+ reference.fit(X, Y, weights)
710
+ for compute_XTX, compute_XTY in product([True, False], repeat=2):
711
+ fast = CVMatrix(
712
+ backend=self.backend, compute_XTX=compute_XTX, compute_XTY=compute_XTY
713
+ )
714
+ fast.fit(X, Y, weights)
715
+ assert (fast.XTX is not None) == compute_XTX
716
+ assert (fast.XTY is not None) == compute_XTY
717
+ allowed = {
718
+ "training_XTX": compute_XTX,
719
+ "training_XTY": compute_XTY,
720
+ "training_XTX_XTY": compute_XTX and compute_XTY,
721
+ "training_statistics": True,
722
+ }
723
+ for method, is_allowed in allowed.items():
724
+ if not is_allowed:
725
+ with pytest.raises(ValueError, match="was not computed by `fit`"):
726
+ getattr(fast, method)(val_indices)
727
+ continue
728
+ actual = leaves(getattr(fast, method)(val_indices))
729
+ desired = leaves(getattr(reference, method)(val_indices))
730
+ assert len(actual) == len(desired)
731
+ for a, d in zip(actual, desired):
732
+ if d is None:
733
+ assert a is None
734
+ else:
735
+ assert_allclose(a, d)
736
+
737
+ # The check is on the fitted products, not the flags, so changing a flag after
738
+ # `fit` still raises the documented ValueError.
739
+ fast = CVMatrix(backend=self.backend, compute_XTX=False, compute_XTY=False)
740
+ fast.fit(X, Y, weights)
741
+ fast.compute_XTX = fast.compute_XTY = True
742
+ for method in ("training_XTX", "training_XTY", "training_XTX_XTY"):
743
+ with pytest.raises(ValueError, match="was not computed by `fit`"):
744
+ getattr(fast, method)(val_indices)
745
+
746
+ def test_refit_resets_Y_derived_state(self):
747
+ """
748
+ Tests that re-fitting replaces all state derived from `Y` instead of keeping
749
+ it from the previous fit: re-fitting without `Y` resets `XTY`, `WY`, `sum_Y`,
750
+ `sq_Y`, and `sum_sq_Y` to `None`, and re-fitting with weights that do not need
751
+ `WY` resets `WY` to `None` instead of keeping the previous `Y`.
752
+ """
753
+ X = self.load_X()[:, :5]
754
+ Y = self.load_Y(["Protein", "Moisture"])
755
+ weights = self.load_weights(random=True)
756
+ Y_derived = ("XTY", "WY", "sum_Y", "sq_Y", "sum_sq_Y")
757
+ fast = CVMatrix(backend=self.backend)
758
+ fast.fit(X, Y, weights)
759
+ assert all(getattr(fast, attr) is not None for attr in Y_derived)
760
+ fast.fit(X, None, weights)
761
+ assert all(getattr(fast, attr) is None for attr in Y_derived)
762
+
763
+ fast = CVMatrix(
764
+ center_X=False, center_Y=False, scale_X=False, scale_Y=False,
765
+ backend=self.backend,
766
+ )
767
+ fast.fit(X, Y) # Unweighted: `WY` is `Y` itself.
768
+ assert fast.WY is fast.Y
769
+ fast.fit(X, Y, weights) # Weighted, but no statistics of `Y` are needed.
770
+ assert fast.WY is None
771
+
772
+ def test_get_val_matrices_returns_only_requested(self):
773
+ """
774
+ Tests that `_get_val_matrices` gathers only the requested validation set
775
+ matrices, returning `None` for the others, that the requested ones equal the
776
+ validation rows of `WX`, `X`, `WY`, and `Y`, and that without weights a
777
+ weighted and unweighted matrix requested together share a single gather.
778
+ """
779
+ X = self.load_X()[:, :5]
780
+ Y = self.load_Y(["Protein", "Moisture"])
781
+ val_indices = np.arange(0, X.shape[0], 7)
782
+ for weights in (None, self.load_weights(random=True)):
783
+ fast = CVMatrix(backend=self.backend)
784
+ fast.fit(X, Y, weights)
785
+ w = np.ones((X.shape[0], 1)) if weights is None else weights[:, None]
786
+ expected = (X * w, X, Y * w, Y)
787
+ for flags in product([True, False], repeat=4):
788
+ result = fast._get_val_matrices(
789
+ val_indices,
790
+ return_X_val=flags[0],
791
+ return_X_val_unweighted=flags[1],
792
+ return_Y_val=flags[2],
793
+ return_Y_val_unweighted=flags[3],
794
+ )
795
+ for requested, mat_val, mat in zip(flags, result, expected):
796
+ if requested:
797
+ assert_allclose(mat_val, mat[val_indices])
798
+ else:
799
+ assert mat_val is None
800
+ if weights is None:
801
+ if flags[0] and flags[1]:
802
+ assert result[0] is result[1]
803
+ if flags[2] and flags[3]:
804
+ assert result[2] is result[3]
805
+
806
+ def test_pickle(self):
807
+ """
808
+ Tests that a fitted CVMatrix survives a round trip through the standard
809
+ library's pickle and through copy.deepcopy, which uses the same protocol, with
810
+ the same settings and results, although it holds the array namespace module
811
+ `xp`, which cannot be pickled.
812
+ """
813
+ X = self.load_X()[:, :5]
814
+ Y = self.load_Y(["Protein", "Moisture"])
815
+ weights = self.load_weights(random=True)
816
+ val_indices = np.arange(0, X.shape[0], 7)
817
+ fast = CVMatrix(backend=self.backend, compute_XTX=False)
818
+ fast.fit(X, Y, weights)
819
+ XTY, stats = fast.training_XTY(val_indices)
820
+ # Only bytes pickled by this test itself are unpickled, so Bandit's B301
821
+ # concern about deserializing untrusted data does not apply.
822
+ unpickled = pickle.loads(pickle.dumps(fast)) # nosec B301
823
+ for clone in (unpickled, deepcopy(fast)):
824
+ assert type(clone) is CVMatrix
825
+ assert clone.xp is fast.xp
826
+ assert clone.XTX is None and not clone.compute_XTX
827
+ clone_XTY, clone_stats = clone.training_XTY(val_indices)
828
+ assert_allclose(clone_XTY, XTY)
829
+ self.check_equivalent_stats(stats, clone_stats)
830
+
831
+ def test_process_pool(self):
832
+ """
833
+ Tests that a fitted CVMatrix can be used from worker processes of
834
+ concurrent.futures.ProcessPoolExecutor, which pickles with the standard
835
+ library. The workers are started with `spawn`, i.e., as fresh interpreters (the
836
+ default on Windows and macOS). With the jax backend, this also checks that
837
+ float64 data stays float64 in a fresh process, where JAX's 64-bit mode is
838
+ initially disabled.
839
+ """
840
+ X = self.load_X()[:, :5]
841
+ Y = self.load_Y(["Protein", "Moisture"])
842
+ weights = self.load_weights(random=True)
843
+ p = Partitioner(self.load_Y(["split"]).squeeze())
844
+ val_indices = [p.get_validation_indices(fold) for fold in p.folds_dict]
845
+ fast = CVMatrix(backend=self.backend)
846
+ fast.fit(X, Y, weights)
847
+ with ProcessPoolExecutor(
848
+ max_workers=2, mp_context=multiprocessing.get_context("spawn")
849
+ ) as executor:
850
+ results = list(executor.map(fast.training_XTX_XTY, val_indices))
851
+ for indices, ((XTX, XTY), stats) in zip(val_indices, results):
852
+ (expected_XTX, expected_XTY), expected_stats = fast.training_XTX_XTY(
853
+ indices
854
+ )
855
+ assert np.asarray(XTX).dtype == np.asarray(XTY).dtype == np.float64
856
+ assert_allclose(XTX, expected_XTX, rtol=1e-12)
857
+ assert_allclose(XTY, expected_XTY, rtol=1e-12)
858
+ self.check_equivalent_stats(expected_stats, stats, atol=1e-12)
859
+
685
860
  def test_naive_hadamard_vs_matmul(self):
686
861
  """
687
862
  Tests if the NaiveCVMatrix model gives the same result when using Hadamard
@@ -1,5 +1,5 @@
1
1
  version = 1
2
- revision = 3
2
+ revision = 5
3
3
  requires-python = ">=3.10, <3.16"
4
4
  resolution-markers = [
5
5
  "python_full_version >= '3.14' and sys_platform == 'win32'",
@@ -159,7 +159,7 @@ wheels = [
159
159
 
160
160
  [package.optional-dependencies]
161
161
  toml = [
162
- { name = "tomli", marker = "python_full_version < '3.11'" },
162
+ { name = "tomli" },
163
163
  ]
164
164
 
165
165
  [[package]]
@@ -304,7 +304,7 @@ wheels = [
304
304
 
305
305
  [[package]]
306
306
  name = "cvmatrix"
307
- version = "3.2.3"
307
+ version = "3.3.0"
308
308
  source = { editable = "." }
309
309
  dependencies = [
310
310
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -350,7 +350,7 @@ name = "exceptiongroup"
350
350
  version = "1.3.1"
351
351
  source = { registry = "https://pypi.org/simple" }
352
352
  dependencies = [
353
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
353
+ { name = "typing-extensions" },
354
354
  ]
355
355
  sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
356
356
  wheels = [
@@ -374,11 +374,11 @@ resolution-markers = [
374
374
  "python_full_version < '3.11'",
375
375
  ]
376
376
  dependencies = [
377
- { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
378
- { name = "ml-dtypes", marker = "python_full_version < '3.11'" },
379
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
380
- { name = "opt-einsum", marker = "python_full_version < '3.11'" },
381
- { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
377
+ { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" } },
378
+ { name = "ml-dtypes" },
379
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
380
+ { name = "opt-einsum" },
381
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } },
382
382
  ]
383
383
  sdist = { url = "https://files.pythonhosted.org/packages/cf/1e/267f59c8fb7f143c3f778c76cb7ef1389db3fd7e4540f04b9f42ca90764d/jax-0.6.2.tar.gz", hash = "sha256:a437d29038cbc8300334119692744704ca7941490867b9665406b7f90665cd96", size = 2334091, upload-time = "2025-06-17T23:10:27.186Z" }
384
384
  wheels = [
@@ -395,11 +395,11 @@ resolution-markers = [
395
395
  "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
396
396
  ]
397
397
  dependencies = [
398
- { name = "jaxlib", version = "0.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
399
- { name = "ml-dtypes", marker = "python_full_version == '3.11.*'" },
400
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
401
- { name = "opt-einsum", marker = "python_full_version == '3.11.*'" },
402
- { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
398
+ { name = "jaxlib", version = "0.10.2", source = { registry = "https://pypi.org/simple" } },
399
+ { name = "ml-dtypes" },
400
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
401
+ { name = "opt-einsum" },
402
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } },
403
403
  ]
404
404
  sdist = { url = "https://files.pythonhosted.org/packages/d2/73/eb91d98fcadfa2cbcfdd4e417ab116e47eb20882acc5ee678e47c35d6b57/jax-0.10.2.tar.gz", hash = "sha256:bf77428a8c2e6904c4f46d5ab12aa5cfc6cad2179f07f7e4c0fc75ac86ef0639", size = 2775110, upload-time = "2026-06-17T23:44:57.818Z" }
405
405
  wheels = [
@@ -422,11 +422,11 @@ resolution-markers = [
422
422
  "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
423
423
  ]
424
424
  dependencies = [
425
- { name = "jaxlib", version = "0.12.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
426
- { name = "ml-dtypes", marker = "python_full_version >= '3.12'" },
427
- { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
428
- { name = "opt-einsum", marker = "python_full_version >= '3.12'" },
429
- { name = "scipy", version = "1.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
425
+ { name = "jaxlib", version = "0.12.0.1", source = { registry = "https://pypi.org/simple" } },
426
+ { name = "ml-dtypes" },
427
+ { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" } },
428
+ { name = "opt-einsum" },
429
+ { name = "scipy", version = "1.18.1", source = { registry = "https://pypi.org/simple" } },
430
430
  ]
431
431
  sdist = { url = "https://files.pythonhosted.org/packages/2e/5b/5b7f007852809973310ed99eede7db6e9e1cd60914f8fe90b091ae264db4/jax-0.12.0.1.tar.gz", hash = "sha256:f543fb1151c351d328b7dd9e40865c01bf9ae10cdf7e21637196172891d51914", size = 3024361, upload-time = "2026-10-21T21:07:07.969Z" }
432
432
  wheels = [
@@ -441,9 +441,9 @@ resolution-markers = [
441
441
  "python_full_version < '3.11'",
442
442
  ]
443
443
  dependencies = [
444
- { name = "ml-dtypes", marker = "python_full_version < '3.11'" },
445
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
446
- { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
444
+ { name = "ml-dtypes" },
445
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
446
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } },
447
447
  ]
448
448
  wheels = [
449
449
  { url = "https://files.pythonhosted.org/packages/15/c5/41598634c99cbebba46e6777286fb76abc449d33d50aeae5d36128ca8803/jaxlib-0.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4601b2b5dc8c23d6afb293eacfb9aec4e1d1871cb2f29c5a151d103e73b0f8", size = 54298019, upload-time = "2025-06-17T23:10:36.916Z" },
@@ -476,9 +476,9 @@ resolution-markers = [
476
476
  "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
477
477
  ]
478
478
  dependencies = [
479
- { name = "ml-dtypes", marker = "python_full_version == '3.11.*'" },
480
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
481
- { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
479
+ { name = "ml-dtypes" },
480
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
481
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } },
482
482
  ]
483
483
  wheels = [
484
484
  { url = "https://files.pythonhosted.org/packages/b2/2c/7038fc73154307389631b5b2dbe5ac529e1918eecc19a27e6644ad114bbf/jaxlib-0.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5a98873fc867623b81f2bee15d554b8edd6588a183d01fa50d21b1e3db96ff2b", size = 61429039, upload-time = "2026-06-17T23:43:44.858Z" },
@@ -521,9 +521,9 @@ resolution-markers = [
521
521
  "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
522
522
  ]
523
523
  dependencies = [
524
- { name = "ml-dtypes", marker = "python_full_version >= '3.12'" },
525
- { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
526
- { name = "scipy", version = "1.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
524
+ { name = "ml-dtypes" },
525
+ { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" } },
526
+ { name = "scipy", version = "1.18.1", source = { registry = "https://pypi.org/simple" } },
527
527
  ]
528
528
  wheels = [
529
529
  { url = "https://files.pythonhosted.org/packages/17/73/47d66eeab3bb5a48009772f4fa572e46bdf8049ecb180f83d544c933462f/jaxlib-0.12.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db2cc8584d7d0d166293ef2475b726988a10f6f9b5a8e3b8e3712415e5570380", size = 60710018, upload-time = "2026-10-21T21:05:22.443Z" },
@@ -861,10 +861,10 @@ resolution-markers = [
861
861
  "python_full_version < '3.11'",
862
862
  ]
863
863
  dependencies = [
864
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
865
- { name = "python-dateutil", marker = "python_full_version < '3.11'" },
866
- { name = "pytz", marker = "python_full_version < '3.11'" },
867
- { name = "tzdata", marker = "python_full_version < '3.11'" },
864
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
865
+ { name = "python-dateutil" },
866
+ { name = "pytz" },
867
+ { name = "tzdata" },
868
868
  ]
869
869
  sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
870
870
  wheels = [
@@ -936,10 +936,10 @@ resolution-markers = [
936
936
  "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
937
937
  ]
938
938
  dependencies = [
939
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
939
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
940
940
  { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
941
- { name = "python-dateutil", marker = "python_full_version >= '3.11'" },
942
- { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" },
941
+ { name = "python-dateutil" },
942
+ { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
943
943
  ]
944
944
  sdist = { url = "https://files.pythonhosted.org/packages/55/b1/a10c0d1a7e3ce44eb42635771004aa503e3b9ddde0bd39c524dda65f7eff/pandas-3.1.0.tar.gz", hash = "sha256:ecb9b2d8a817b1e1ba1fdb90c0abe9eab31b4fdf2562b7a56b86ac66ace17e49", size = 5933421, upload-time = "2026-10-22T11:57:03.087Z" }
945
945
  wheels = [
@@ -1108,7 +1108,7 @@ resolution-markers = [
1108
1108
  "python_full_version < '3.11'",
1109
1109
  ]
1110
1110
  dependencies = [
1111
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
1111
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
1112
1112
  ]
1113
1113
  sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
1114
1114
  wheels = [
@@ -1169,7 +1169,7 @@ resolution-markers = [
1169
1169
  "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
1170
1170
  ]
1171
1171
  dependencies = [
1172
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
1172
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
1173
1173
  ]
1174
1174
  sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
1175
1175
  wheels = [
@@ -1251,7 +1251,7 @@ resolution-markers = [
1251
1251
  "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
1252
1252
  ]
1253
1253
  dependencies = [
1254
- { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
1254
+ { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" } },
1255
1255
  ]
1256
1256
  sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" }
1257
1257
  wheels = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes