jax-nufft 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. jax_nufft-0.2.0/.gitignore +50 -0
  2. jax_nufft-0.2.0/CHANGELOG.md +411 -0
  3. jax_nufft-0.2.0/LICENSE +201 -0
  4. jax_nufft-0.2.0/PKG-INFO +895 -0
  5. jax_nufft-0.2.0/README.md +660 -0
  6. jax_nufft-0.2.0/pyproject.toml +124 -0
  7. jax_nufft-0.2.0/src/jax_nufft/__init__.py +13 -0
  8. jax_nufft-0.2.0/src/jax_nufft/_types.py +34 -0
  9. jax_nufft-0.2.0/src/jax_nufft/_utils.py +5 -0
  10. jax_nufft-0.2.0/src/jax_nufft/_version.py +11 -0
  11. jax_nufft-0.2.0/src/jax_nufft/kernel.py +343 -0
  12. jax_nufft-0.2.0/src/jax_nufft/planning.py +1772 -0
  13. jax_nufft-0.2.0/src/jax_nufft/wgridder.py +3168 -0
  14. jax_nufft-0.2.0/tests/__init__.py +0 -0
  15. jax_nufft-0.2.0/tests/bench_harness.py +311 -0
  16. jax_nufft-0.2.0/tests/conftest.py +700 -0
  17. jax_nufft-0.2.0/tests/jax_floor_probe.py +722 -0
  18. jax_nufft-0.2.0/tests/test_accuracy_sweep.py +376 -0
  19. jax_nufft-0.2.0/tests/test_adjoint.py +313 -0
  20. jax_nufft-0.2.0/tests/test_against_dft.py +758 -0
  21. jax_nufft-0.2.0/tests/test_against_ducc.py +645 -0
  22. jax_nufft-0.2.0/tests/test_auto_strategy.py +717 -0
  23. jax_nufft-0.2.0/tests/test_auto_strategy_acceptance.py +122 -0
  24. jax_nufft-0.2.0/tests/test_bench_harness.py +208 -0
  25. jax_nufft-0.2.0/tests/test_benchmark_against_ducc.py +559 -0
  26. jax_nufft-0.2.0/tests/test_benchmark_claims.py +1964 -0
  27. jax_nufft-0.2.0/tests/test_benchmark_gpu.py +270 -0
  28. jax_nufft-0.2.0/tests/test_boundary_planes.py +796 -0
  29. jax_nufft-0.2.0/tests/test_chunked_strategy.py +1562 -0
  30. jax_nufft-0.2.0/tests/test_clumped_track.py +1024 -0
  31. jax_nufft-0.2.0/tests/test_constant_w.py +122 -0
  32. jax_nufft-0.2.0/tests/test_custom_vjp.py +1859 -0
  33. jax_nufft-0.2.0/tests/test_default_w_strategy.py +1325 -0
  34. jax_nufft-0.2.0/tests/test_divide_by_n.py +2186 -0
  35. jax_nufft-0.2.0/tests/test_dtype.py +1017 -0
  36. jax_nufft-0.2.0/tests/test_hermitian.py +1658 -0
  37. jax_nufft-0.2.0/tests/test_jax_floor.py +704 -0
  38. jax_nufft-0.2.0/tests/test_jax_integration.py +563 -0
  39. jax_nufft-0.2.0/tests/test_kernel.py +370 -0
  40. jax_nufft-0.2.0/tests/test_nshift.py +645 -0
  41. jax_nufft-0.2.0/tests/test_nthreads_resolution.py +457 -0
  42. jax_nufft-0.2.0/tests/test_padding_overhead.py +1399 -0
  43. jax_nufft-0.2.0/tests/test_planning.py +2899 -0
  44. jax_nufft-0.2.0/tests/test_smoke.py +15 -0
  45. jax_nufft-0.2.0/tests/test_strategies_equivalent.py +330 -0
  46. jax_nufft-0.2.0/tests/test_timing_nthreads.py +98 -0
  47. jax_nufft-0.2.0/tests/test_version.py +85 -0
  48. jax_nufft-0.2.0/tests/test_window_bucketing.py +1558 -0
@@ -0,0 +1,50 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ .eggs/
12
+ pip-wheel-metadata/
13
+
14
+ # Virtual environments
15
+ .venv/
16
+ .env/
17
+ env/
18
+ venv/
19
+
20
+ # Pixi
21
+ .pixi/
22
+ # ...and as a bare symlink: a worktree may link .pixi at another checkout's
23
+ # environments, which `.pixi/` does not match (it matches a directory only),
24
+ # so `git add -A` would otherwise commit a path valid on exactly one machine.
25
+ .pixi
26
+
27
+ # Testing / tooling
28
+ .pytest_cache/
29
+ .coverage
30
+ .coverage.*
31
+ htmlcov/
32
+ .tox/
33
+ .mypy_cache/
34
+ .ruff_cache/
35
+
36
+ # Editors
37
+ .vscode/
38
+ .idea/
39
+ *.swp
40
+ *.swo
41
+ .DS_Store
42
+
43
+ # Jupyter
44
+ .ipynb_checkpoints/
45
+
46
+ # Build artefacts
47
+ *.log
48
+
49
+ # Repo-local scratch (ephemeral probes, ad-hoc benches, notes)
50
+ tmp/
@@ -0,0 +1,411 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
6
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ Figures quoted below are recomputed from the committed benchmark JSON by
9
+ `tests/test_benchmark_claims.py` where a JSON exists; the rest cite the pull request that measured
10
+ them.
11
+
12
+ ## [Unreleased]
13
+
14
+ ## [0.2.0] — 2026-09-10
15
+
16
+ This release covers everything since **v0.1.1**. A v0.1.2 series was developed and merged but never
17
+ tagged or released, so its changes appear here for the first time; they are marked *(v0.1.2 series)*.
18
+
19
+ ### Removed
20
+
21
+ - **Python 3.10 is no longer supported.** The floor is 3.11. *(v0.1.2 series)*
22
+
23
+ ### Changed — breaking
24
+
25
+ - **The default `w_strategy` is now `"auto"`, not `"dense_scan"`.**
26
+ `"auto"` resolves per call from the plan and the device platform, so the strategy a given call
27
+ runs may differ from v0.1.1. Pass `w_strategy="dense_scan"` explicitly to keep the old behaviour.
28
+ On one GH200, against ducc0 on the 72 Grace cores of the same node, the old default ran 1.4–5.6×
29
+ *slower* than ducc0 on five of six measured cells, where `dense_vmap` runs 1.4–6.3× faster in
30
+ all six. (That range is the `dense_vmap` column; the heuristic picks `windowed_vmap` on the
31
+ sixth cell, at 3.5× faster, which is inside the same range.)
32
+ ([#46](https://github.com/chrisfinlay/jax-nufft/issues/46),
33
+ [PR #48](https://github.com/chrisfinlay/jax-nufft/pull/48))
34
+
35
+ - **The minimum supported JAX is now 0.6.0**, raised from the declared 0.5.0. The old floor was
36
+ never correct: `src/` calls `jax.typeof`, which was exported in 0.6.0, so on the declared minimum
37
+ every operator invoked through `jax.disable_jit()` raised `AttributeError`.
38
+ ([#21](https://github.com/chrisfinlay/jax-nufft/issues/21),
39
+ [PR #52](https://github.com/chrisfinlay/jax-nufft/pull/52))
40
+
41
+ - **Running with `jax_enable_x64` disabled is now explicit rather than silent.** Previously a
42
+ float64 plan built with x64 off silently produced float32 results with a ~3.4e-5 error floor and
43
+ no warning. Plans now carry an explicit dtype, mixed dtypes are cast or rejected, and the
44
+ x64-off path is guarded.
45
+ ([#11](https://github.com/chrisfinlay/jax-nufft/issues/11),
46
+ [PR #37](https://github.com/chrisfinlay/jax-nufft/pull/37))
47
+
48
+ - **The default `nthreads` is now strategy-aware.** `nthreads=0` previously made the default
49
+ strategy several times slower than single-threaded.
50
+ ([#24](https://github.com/chrisfinlay/jax-nufft/issues/24),
51
+ [PR #40](https://github.com/chrisfinlay/jax-nufft/pull/40))
52
+
53
+ - **Plan internals changed shape.** `uvw` is stored once in metres with per-channel coordinates
54
+ derived inside JIT, and leaves that were never read have been dropped. Code that reached into
55
+ `WGridderPlan` fields rather than using the public operators may need updating; the plan is not
56
+ a stable public interface.
57
+ ([#23](https://github.com/chrisfinlay/jax-nufft/issues/23),
58
+ [PR #42](https://github.com/chrisfinlay/jax-nufft/pull/42))
59
+
60
+ - **The `auto` selector's padding branch reads a different field on the adjoint.**
61
+ `window_padding_overhead` is unchanged — still `n_chan × n_w × max_window_size /
62
+ live_row_count`, and now explicitly the *forward's* ratio — but the windowed adjoint buckets its
63
+ plane slices (below), so its padded work is smaller and it is gated on a new
64
+ `window_padding_overhead_adjoint`. Neither cutoff moved (`_CPU_PADDING_CUTOFF` 6.0,
65
+ `_GPU_PADDING_CUTOFF` 3.0), but on the adjoint leg neither is reached by any repository fixture
66
+ at any epsilon in either geometry (the adjoint maxima over the forty-cell calibration grid are
67
+ 1.6206 unfolded and 1.4133 folded), so an adjoint that used to fall back to a dense strategy on a
68
+ high padding figure now stays windowed. The forward leg is untouched. `WGridderPlan` also gains
69
+ the static `window_buckets` and `max_window_size_per_chan` and a tenth leaf `window_plane_order`,
70
+ appended after `flip_sign` in the flatten order.
71
+ ([#26](https://github.com/chrisfinlay/jax-nufft/issues/26))
72
+
73
+ ### Added
74
+
75
+ - **`divide_by_n` on both operators.** `dirty2vis` and `vis2dirty` each take a keyword-only
76
+ `divide_by_n` flag applying the measurement equation's image-side `1/n`. With **equal** flags the
77
+ pair is an exact adjoint; with the previous mixed defaults the dot-product residual was 0.63,
78
+ against 1.3e-15 when matched.
79
+ ([#20](https://github.com/chrisfinlay/jax-nufft/issues/20),
80
+ [PR #51](https://github.com/chrisfinlay/jax-nufft/pull/51))
81
+
82
+ - **`w_strategy="auto"`**, a platform-aware heuristic choosing among `dense_scan`, `dense_vmap`,
83
+ `windowed_scan` and `windowed_vmap`. Opt-in when introduced, and the default since
84
+ [#46](https://github.com/chrisfinlay/jax-nufft/issues/46). *(v0.1.2 series)*
85
+
86
+ - **`w_strategy="chunked"` / `"windowed_chunked"` with a static `w_chunk` (default 32)**, making the
87
+ w-plane loop a memory/compute curve instead of a choice between two points. The loop scans over
88
+ chunks of at most `w_chunk` planes with a `vmap` inside each, so transient memory follows
89
+ `w_chunk` rather than `n_w`. The four older names *are* points on that curve — `dense_scan` and
90
+ `windowed_scan` are `w_chunk=1`, `dense_vmap` and `windowed_vmap` are `w_chunk=n_w` — and share
91
+ its code, so a call at either end is bit-identical to the old name for it **at equal
92
+ `nthreads`**. `w_chunk` is part of the JIT key and of the primitives' static configuration, so
93
+ reverse mode chunks the way its forward did.
94
+
95
+ The default `w_chunk = 32` exceeds `n_w` on most, not all, of this repository's fixtures.
96
+ Measured over every telescope in `tests/conftest.py` at both pointings (seed 0, eps 1e-6,
97
+ float64, hermitian, one channel): EDA2 11 / **56**, GH200_large 9 / 26, MWA_compact 8 / 12,
98
+ MWA_extended 11 / **134**, MeerKAT 8 / 13 (zenith / off30). Two of the ten run a real chunk
99
+ loop at the default, with padding; the other eight clamp to `dense_vmap`.
100
+
101
+ Measured on MWA_extended off30 (256², 600 rows, `n_w = 134`, float64, eps 1e-6, `nthreads=1`,
102
+ single channel, `memory_analysis().temp_size_in_bytes`), in units of one complex image:
103
+
104
+ | | `dense_scan` | `chunked(8)` | `chunked(16)` | `chunked(32)` | `dense_vmap` |
105
+ |---|---:|---:|---:|---:|---:|
106
+ | forward | 2.01× | 9.08× | 16.15× | **28.26×** | 135.23× |
107
+ | adjoint | 2.01× | 9.01× | 16.01× | **28.01×** | 268.00× |
108
+
109
+ `chunked(32)` there runs 1.01–1.12× (forward) and 0.94–1.26× (adjoint) of `dense_vmap`'s time
110
+ over **seven** interleaved passes on a 10-core Apple M-series. Read those to one significant
111
+ figure: the suite's own control — `chunked(1)`, the same compiled program as `dense_scan` —
112
+ spans 0.92–1.00× against it over the same passes, which is the instrument's resolution at this
113
+ problem size. The memory rows are exact and reproduce to the byte.
114
+
115
+ **On a GH200, where the issue was opened.** MWA_extended off30 at 3600² / 1 219 200 rows
116
+ (`n_w = 140`, complex image 197.8 MB), eps 1e-6, float64, single channel, defaults for
117
+ `hermitian`/`nthreads`; median of 5 with warm-up outside the timer:
118
+
119
+ | | temp | vs `dense_vmap` | forward | adjoint |
120
+ |---|---:|---:|---:|---:|
121
+ | `dense_scan` | 414 MB | **73.1× less** | 2.35× | 1.94× |
122
+ | `chunked(8)` | 1 929 MB | 15.7× less | 1.73× | 1.53× |
123
+ | `chunked(16)` | 3 659 MB | 8.3× less | 1.35× | 1.24× |
124
+ | `chunked(32)` | 6 256 MB | 4.8× less | **1.27×** | 1.17× |
125
+ | `chunked(64)` | 10 367 MB | 2.9× less | 1.10× | 1.06× |
126
+ | `dense_vmap` | 30 290 MB | 1.0× | 1.00× | 1.00× |
127
+
128
+ The 30 GB that made this cell need a 96 GB device becomes **6.3 GB** at the default `w_chunk`.
129
+ Note that the definition of done's GPU gate — "`chunked(32)` within 1.2× of `dense_vmap` on
130
+ every cell" — is **breached on that forward cell at 1.27×**; its adjoint and every other cell
131
+ measured pass. Two of the four GPU fixtures cannot inform the gate at all (MWA_extended zenith
132
+ `n_w = 13`, MeerKAT off30 `n_w = 14`: `w_chunk = 32` clamps and reads 1.00× by construction);
133
+ EDA2 off30 (150² / 4 896 000 rows, `n_w = 60`) is the other real cell and passes at 1.07× /
134
+ 1.01×. Full tables in `README.md` §`w_chunk`.
135
+
136
+ Existing strategies are untouched **on every plan with `n_w > 1`**: the optimised HLO for all
137
+ four older names, plus `auto`, is byte-identical before and after over both operators and four
138
+ fixtures. The exception is the constant-w fast path (`n_w == 1`), where the plane loop tests
139
+ `w_chunk >= n_w` before `w_chunk == 1` and so sends the *scan* names down the single-plane vmap
140
+ branch instead of `lax.scan` — 24 of 82 optimised-HLO keys differ there, with bit-identical
141
+ results on every cell and a strictly smaller program.
142
+
143
+ **Not done:** implementation-plan item 4 — `chunked` cells at `w_chunk ∈ {8, 32, 128}` in the
144
+ CPU and GPU benchmark suites. `tests/test_benchmark_claims.py` recovers `w_strategy` from the
145
+ benchmark test *name* and classifies it with `rsplit("_", 1)[1]`, which raises on `"chunked"`
146
+ and yields a third family on `"windowed_chunked"`, against pair counts and spreads pinned to
147
+ three decimals from committed v0.1.2 JSONs. Teaching that layer about a family that is neither
148
+ scan nor vmap is its own change; until then the curve is measured out-of-band and published
149
+ above. Issue #25 is therefore **not fully closed** by this entry.
150
+ ([#25](https://github.com/chrisfinlay/jax-nufft/issues/25))
151
+
152
+ - **The windowed adjoint buckets its w-planes by window size.** Each channel's planes are
153
+ sorted into at most four size classes, placed by an exact dynamic program over that channel's
154
+ own padded window lengths, and each class is a sub-loop with its own static slice length — so a
155
+ plane whose window holds 8 rows no longer reads 155. The class table is per channel, so a
156
+ high-frequency channel (narrower windows) is no longer charged the plan-wide maximum.
157
+
158
+ **The windowed forward is unchanged, deliberately.** Bucketing it as well was implemented,
159
+ measured and reverted: on one GH200 (Daint, eps 1e-6, float64, `n_chan = 1`, realistic sizes,
160
+ the `auto` path) it ran 20.3x slower on GH200_large zenith (2048²/50k, `n_w = 9`; 16.7 → 339.7
161
+ ms), 58.4x slower on MeerKAT off30 (2700²/302400, `n_w = 14`; 32.9 → 1918.7 ms), and timed out
162
+ at 420 s on MWA_extended zenith (3600²/1.22M) against 48.6 ms — while the adjoint over the same
163
+ runs was 1.08-1.24x. `auto` resolves the forward to `windowed_vmap` on most realistic GPU plans,
164
+ so this reached defaulting users. **The cause is not understood** and is left open as
165
+ #65; two diagnosis-and-fix rounds did not move the GPU numbers. The forward therefore
166
+ keeps the previous release's code, its `max_window_size` slice and its
167
+ `window_padding_overhead` figure, and its optimised HLO is that code's: over 7 `w_strategy` x
168
+ 10 fixtures, all 70 forward modules differ from the previous revision in nothing but one
169
+ appended unused parameter (the `window_plane_order` leaf) and the renumbering of the operand
170
+ after it. No instruction differs. (The four GPU timings are the maintainer's, on hardware this
171
+ repository's test suite does not have.)
172
+
173
+ Padded row-work over irreducible row-work on the ten review cells (eps 1e-6, float64, seed 0,
174
+ `hermitian=True`, `n_chan = 1`, CI fixture sizes) — the forward column is
175
+ `window_padding_overhead`, unchanged, and the adjoint column is the new
176
+ `window_padding_overhead_adjoint`:
177
+
178
+ | fixture | forward | adjoint | classes |
179
+ |---|---:|---:|---:|
180
+ | EDA2 zenith | 1.5709 | 1.0368 | 4 |
181
+ | EDA2 off30 | 2.5191 | 1.2424 | 4 |
182
+ | MWA_compact zenith | 1.1431 | 1.0007 | 2 |
183
+ | MWA_compact off30 | 1.7139 | 1.0467 | 4 |
184
+ | MWA_extended zenith | 1.5714 | 1.0371 | 4 |
185
+ | MWA_extended off30 | 4.9441 | **1.3768** | 4 |
186
+ | MeerKAT zenith | 1.1429 | 1.0005 | 2 |
187
+ | MeerKAT off30 | 1.8571 | 1.0690 | 4 |
188
+ | GH200_large zenith | 1.2857 | 1.0000 | 4 |
189
+ | GH200_large off30 | 2.7389 | 1.2861 | 4 |
190
+
191
+ Adjoint runtime, CPU `nthreads=1`, macOS arm64 10-core, median of 11 calls with the strategies
192
+ interleaved inside one process and one warm-up outside the timer (AGENTS.md §6), before →
193
+ after. MWA_extended off30 (`n_w = 134`): `windowed_scan` 167.4 → 147.4 ms, `windowed_vmap`
194
+ 137.0 → 120.2 ms, `windowed_chunked(32)` 138.4 → 110.0 ms. MeerKAT off30 (`n_w = 13`):
195
+ `windowed_scan` 16.71 → 16.36 ms, `windowed_vmap` 13.37 → 13.72 ms, `windowed_chunked(32)`
196
+ 13.15 → 14.14 ms — i.e. flat to 7.5% *slower* on that fixture, whose padding was only 1.86x to
197
+ start with, so the sub-loops cost about what they save. Forward runtime is unchanged to within
198
+ 1% on every strategy of both fixtures, as it must be.
199
+
200
+ Adjoint transient memory falls with the padding. Measured `vis2dirty` `temp_size_in_bytes` on a
201
+ 4000-row, 16², 138-plane fixture (float64, eps 1e-6, `max_window_size` 1072 of 4000 rows),
202
+ before → after: `windowed_vmap` 2,932,224 → 1,038,464 B, `windowed_chunked(32)` 1,143,392 →
203
+ 787,616 B, `windowed_scan` 106,568 → 106,760 B (a scan holds one class's slice at a time, so
204
+ its peak is the widest class and does not fall). Every `dirty2vis` transient on that fixture is
205
+ byte-identical before and after, on all five strategies.
206
+
207
+ **The definition of done's two CPU timing gates, stated as measured rather than as met.**
208
+
209
+ *Gate 1 — "windowed strategies within 1.2x of the best dense strategy on CPU single-thread for
210
+ MWA_extended off30 and MeerKAT off30".* Measured under the protocol above: `windowed_vmap` and
211
+ `windowed_chunked` clear it on all four (fixture, operator) cells, at 0.85-1.16x, but
212
+ `windowed_scan` reads 1.51x / 1.14x (MWA_extended off30 forward / adjoint) and 1.44x / 1.25x
213
+ (MeerKAT off30), so it is **above 1.2x on three of the four**. That is the scan family's own
214
+ gap and not windowing: `dense_scan` alone is 1.29-1.54x of the best dense strategy on the same
215
+ runs, and `windowed_scan` is 0.88-0.98x of `dense_scan`. Three of those four readings are the
216
+ *forward*, which this change does not touch, so on those the gate is measuring the previous
217
+ release. Read on the pick a caller actually gets, it is met: CPU `auto` resolves to a windowed
218
+ strategy on exactly one of the four cells (MWA_extended off30 **adjoint**; the two forwards go
219
+ to `dense_scan`, and MeerKAT off30's adjoint fails the `n_w / W > 2` gate at 1.86), and that
220
+ cell reads **1.14x**.
221
+
222
+ *Gate 2 — "faster than dense on the adjoint for a 20k-row problem" (`synthetic_uvw` with
223
+ `GH200_LARGE`'s baseline parameters, `n_rows = 20_000`, `n_pix = 512`, off30).* Measured, same
224
+ protocol, two independent rounds per revision, eps 1e-6, float64, seed 0, `hermitian=True`:
225
+ `n_w = 25`, `max_window_size` 15,067 of 20,000, buckets `((2568, 11), (5567, 4), (10266, 5),
226
+ (15067, 5))`, adjoint overhead 1.2656 against a forward 2.6905. Adjoint, best dense 162.8 /
227
+ 164.5 ms: `windowed_vmap` **111.0 / 110.1 ms (0.68x)**, `windowed_chunked(32)` 110.5 / 111.5 ms
228
+ (0.68x), `windowed_scan` 174.4 / 177.1 ms (1.07x). The same three strategies at the previous
229
+ revision read 165.9 / 165.2, 166.8 / 166.3 and 180.8 / 180.1 ms against a best dense of 163.0 /
230
+ 162.5 — i.e. **bucketing is what takes this cell from 1.02x to 0.68x**. Gate met, by 1.47x.
231
+
232
+ **Not done:** the definition of done's GPU gate, which needs a CUDA jax-finufft build; and the
233
+ windowed forward, which is #65's.
234
+
235
+ **Known defect, inherited rather than introduced.** The windowed forward's `windowed_chunked`
236
+ branch accumulates a whole chunk into one shared `(n_rows,)` carry through a single scatter-add
237
+ over its `(w_chunk, max_window_size)` index block. The windows in a chunk overlap physically —
238
+ every visibility is inside the w-kernel's support on 7.00 planes on every fixture here — so
239
+ those updates collide and XLA has to assume they can. Whether that costs anything on a GPU is
240
+ the question the failed diagnosis above leaves open; the GH200 A/B does not answer it, because
241
+ `w_chunk = 32` exceeds `n_w` on both of those fixtures and `windowed_chunked` degenerates to
242
+ `windowed_vmap` there. It is declared by an `xfail(strict=True)` cell rather than fixed here;
243
+ #65.
244
+
245
+ **Not measured by any gate:** the multi-channel path. Every figure above is `n_chan = 1`, which
246
+ is one bucket-table group and emits the previous release's program. A plan whose channels
247
+ bucket differently compiles one adjoint body per group, and at a realistic ±5% frequency spread
248
+ the group count *equals* `n_chan`. Measured on EDA2 off30 at `n_chan` 1 / 4 / 8 / 16 / 32, the
249
+ compile time of a jitted `windowed_scan` `vis2dirty` is 0.083 / 0.224 / 0.397 / 0.656 / 1.231 s
250
+ against a flat 0.047-0.073 s for `dense_scan` and 0.056-0.083 s at the previous revision. The
251
+ windowed adjoint's transient is below both baselines to eight channels and above them past that
252
+ — 2,035,712 B at `n_chan = 16` against 1,237,384 B before (+64.5%) and `dense_scan`'s
253
+ 1,202,376 B (+69.3%) — because it concatenates one image cube per group. `dirty2vis` does not
254
+ group and is byte-identical at every channel count. Retuning `auto` for channel count is issue
255
+ [#34](https://github.com/chrisfinlay/jax-nufft/issues/34).
256
+ ([#26](https://github.com/chrisfinlay/jax-nufft/issues/26))
257
+
258
+ - **A constant-w fast path.** When every row shares one `w` in wavelengths, the plan collapses to a
259
+ single plane (`plan.n_w == 1`, `plan.is_constant_w`). *(v0.1.2 series)*
260
+
261
+ - **A GPU benchmark suite** (`--runbench-gpu`) with HBM capture, plus committed GH200 baselines
262
+ under `docs/benchmarks/`. *(v0.1.2 series)*
263
+
264
+ - **CI covers Python 3.13 and 3.14.** *(v0.1.2 series)*
265
+
266
+ - **A CI job that runs the declared JAX floor.** Every other job runs the single `jax` the pixi
267
+ lockfile resolves, so the `jax>=` bound in `pyproject.toml` was exercised by nothing — which is
268
+ how it came to say `0.5.0` while `src/` called `jax.typeof` (see the floor bump above). The
269
+ `jax floor probe` job installs **jax alone** at the declared version and runs
270
+ `tests/jax_floor_probe.py`, which reads the floor out of `pyproject.toml`, derives every
271
+ module-level `jax.*` attribute chain `src/` and `tests/` touch by walking their ASTs (84 of
272
+ them, measured on this branch; issue #53 records the earlier #21 review as having checked 18 by
273
+ hand), imports and `getattr`s each, and then drives a 3×3-matmul miniature of the wgridder's
274
+ primitive pattern through `jit`, `grad`, `jvp`, `linear_transpose`, `vmap` inside `grad`,
275
+ `grad(grad(...))` and `disable_jit`. A `Call` terminates an attribute chain, so methods of a
276
+ *returned* object — `jax.typeof(...).to_tangent_aval()`, `jax.jit(...).lower()`,
277
+ `jnp.zeros(...).at[...]`, `.astype`, `.real`, `.reshape` — are outside the derived set and
278
+ cannot be in it; the miniature calls all six for real instead, and which six is itself derived.
279
+ Neither the version, the symbol list nor the method list is maintained by hand. With the floor
280
+ reverted to `>=0.5.0` the job fails, naming `jax.typeof` as the one missing symbol of the 84.
281
+ ([#53](https://github.com/chrisfinlay/jax-nufft/issues/53))
282
+
283
+ ### Fixed
284
+
285
+ - **`jax.grad` through either operator no longer costs `O(n_w · image)` memory.** Both operators are
286
+ bound as linear primitives with `ad.primitive_transposes`, so gradients cost
287
+ `O(image + n_rows)`. Measured peak XLA temp for `grad`:
288
+
289
+ | fixture | `n_w` | forward | before | after |
290
+ |---|---:|---:|---:|---:|
291
+ | MWA_extended off30, 256² | 134 | 2.11 MB | 285.47 MB | **3.16 MB** |
292
+ | 1024² / 20k rows | 25 | 33.87 MB | 897.83 MB | **50.65 MB** |
293
+
294
+ Gradients also got **1.15× faster**, and the forward is bit-identical.
295
+ ([#21](https://github.com/chrisfinlay/jax-nufft/issues/21),
296
+ [PR #52](https://github.com/chrisfinlay/jax-nufft/pull/52))
297
+
298
+ - **The accuracy contract now holds.** The achieved error was 3–4.5× the requested epsilon at
299
+ 1e-6…1e-8 and 26–550× at 1e-10…1e-12. Adopting FINUFFT's w-kernel width rule brought this within
300
+ the documented multiple, and the test tolerances that had hidden it were tightened to measured
301
+ bounds.
302
+ ([#9](https://github.com/chrisfinlay/jax-nufft/issues/9),
303
+ [#10](https://github.com/chrisfinlay/jax-nufft/issues/10),
304
+ [PR #38](https://github.com/chrisfinlay/jax-nufft/pull/38),
305
+ [PR #39](https://github.com/chrisfinlay/jax-nufft/pull/39))
306
+
307
+ - **Benchmark figures quoted in prose are now recomputed from the committed JSON by a test**, after
308
+ five hand-written citations were found to disagree with the data they cited.
309
+ ([#49](https://github.com/chrisfinlay/jax-nufft/issues/49))
310
+
311
+ ### Performance
312
+
313
+ - **The w-plane count is roughly halved, twice.** `nshift` centres the `n−1` range
314
+ ([#16](https://github.com/chrisfinlay/jax-nufft/issues/16)), and negative-w rows are folded onto
315
+ their Hermitian image for real skies
316
+ ([#17](https://github.com/chrisfinlay/jax-nufft/issues/17)) — on MWA_extended off30, `n_w` 251 →
317
+ 134. Every measured GPU cell got faster.
318
+ ([PR #41](https://github.com/chrisfinlay/jax-nufft/pull/41),
319
+ [PR #50](https://github.com/chrisfinlay/jax-nufft/pull/50))
320
+
321
+ - **`window_padding_overhead` measures the right denominator**, so the reported figure is no longer
322
+ inflated by counting zero-weight padding rows as irreducible work. Values on this scale are not
323
+ comparable with those from v0.1.2 or earlier.
324
+ ([#43](https://github.com/chrisfinlay/jax-nufft/issues/43),
325
+ [PR #47](https://github.com/chrisfinlay/jax-nufft/pull/47))
326
+
327
+ ### Internal
328
+
329
+ - Test coverage for odd, non-square and anisotropic images and per-channel geometry
330
+ ([#14](https://github.com/chrisfinlay/jax-nufft/issues/14)); clumped and constant-w w-distributions
331
+ against an external oracle, and the x64-off leg's first off-zenith numerical oracle
332
+ ([#15](https://github.com/chrisfinlay/jax-nufft/issues/15)); exact-identity gradient tests plus an
333
+ independent derivative reference built from `jax.vjp` of an exact DFT
334
+ ([#22](https://github.com/chrisfinlay/jax-nufft/issues/22)).
335
+ - Native aarch64 CPU environments, split `gpu`/`gpu-dev` features, and pixi-based CI.
336
+ *(v0.1.2 series)*
337
+
338
+ ### Documentation
339
+
340
+ - **The GPU-versus-ducc0 and memory comparisons are now measured data in the tree**
341
+ ([#33](https://github.com/chrisfinlay/jax-nufft/issues/33)). Both were previously hand-written
342
+ markdown tables; `tests/test_benchmark_claims.py` recorded them as figures it could not
343
+ recompute. `docs/benchmarks/v0.2.0-vs-ducc0-gh200.json` and `v0.2.0-memory-gh200.json` now hold
344
+ the sweeps, and every figure the README prints from them is recomputed by a test. The ducc0
345
+ memory side is the median of three runs per cell: the interpreter baseline it subtracts is
346
+ itself a high-water mark and varied 36–144 MB across 48 runs of an identical program, which is
347
+ comparable with the whole working set of the smaller fixtures. Ten of the sixteen cells clear a
348
+ 200 MB resolvability bar; the README marks the other six rather than quoting them.
349
+ - Problem sizes are derived from instrument parameters (`pixsize = lambda / (3 B_max)`,
350
+ `n_pix` the next even 5-smooth integer covering the field of view, `n_rows = 150 N_bl`)
351
+ rather than taken from the CI fixtures, which are 400–600 rows at 64–256 pixels and measure
352
+ overhead rather than the algorithm. The test recomputes all eight sizes from the rule.
353
+ - ducc0 is compared at its own best measured thread count per cell. All 288 hardware threads
354
+ is never its best setting, and costs it 1.9–6.0× against that best.
355
+ - **The accuracy sweep now runs in CI** ([#33](https://github.com/chrisfinlay/jax-nufft/issues/33)).
356
+ Nothing in `.github/workflows` passed `--runsweep`, so the only evidence for the headline
357
+ `2 * epsilon` contract ran solely when someone typed the flag locally. It costs 22 s.
358
+ - **The accuracy contract is scoped to the grid that measures it.** The sweep holds `w_strategy`
359
+ at `dense_scan`, which is not the shipped default; cross-strategy agreement is pinned at
360
+ `1e-11`, wider than the contract itself below `epsilon = 5e-12`. The `3 * epsilon` bound
361
+ against ducc0 holds `epsilon` at {`1e-4`, `1e-6`}.
362
+ - **Corrected claims that the repository's own code or data falsified**
363
+ ([#33](https://github.com/chrisfinlay/jax-nufft/issues/33)): the README reported the package
364
+ version as v0.1.2, which it had not been since #59, and attributed shipped work to a "v0.1.3"
365
+ that will
366
+ never be tagged; the memory table reported 0 MB for three rows, an artefact of measuring two
367
+ operators against one monotonic high-water mark; the padding-overhead ranges quoted an
368
+ `epsilon = 1e-6` slice while naming a four-epsilon grid; "all four `w_strategy` choices"
369
+ survived #25 adding two more; `_plane_chunk_grid(56, 32)` pads nothing where two docstrings
370
+ said it pads a plane; and float32 was said to halve plan memory, which holds for
371
+ image-dominated plans (0.501×) but not row-dominated ones (0.586×).
372
+ - `[tool.mypy]` and `[tool.ruff]` now target Python 3.11, matching `requires-python`.
373
+ - **README rewritten as an overview** (1850 lines to ~600), with badges, a table of contents,
374
+ LaTeX-rendered mathematics in place of ASCII code fences, and parameter tables in place of
375
+ wall-of-prose API descriptions. The reference material it carried is moved unchanged — not
376
+ dropped — into [`docs/algorithm.md`](docs/algorithm.md),
377
+ [`docs/strategies.md`](docs/strategies.md), [`docs/accuracy.md`](docs/accuracy.md) and
378
+ [`docs/benchmarking.md`](docs/benchmarking.md); every figure in those files was verified during
379
+ the #33 pass, so moving beat rewriting. Installation now leads with `pip install jax-nufft`,
380
+ which 0.2.0 is the first release to make true, and records that there is no `jax-nufft[gpu]`
381
+ extra — the CPU/CUDA choice belongs to `jax-finufft`, and the extra the previous README
382
+ documented never existed.
383
+ - **GPU installation is documented as its own step, before `pip install`.** `jax-finufft`'s PyPI
384
+ wheels carry no CUDA, so installing `jax-nufft` from PyPI on a GPU machine yields a CPU backend
385
+ that runs quietly and slowly. The CUDA builds are on conda-forge (linux-64 and linux-aarch64
386
+ only) and must be installed first; the README now says so, gives the command, and shows how to
387
+ check which backend is actually loaded, since `jax.devices()` reporting a CUDA device does not
388
+ imply `jax-finufft` is the CUDA build.
389
+
390
+ ### Packaging
391
+
392
+ - **A publish workflow** ([`.github/workflows/publish.yml`](.github/workflows/publish.yml)) builds
393
+ the sdist and wheel and uploads them to PyPI via Trusted Publishing when a GitHub Release is
394
+ published. A tag alone does not trigger it, and `workflow_dispatch` targets TestPyPI only, so a
395
+ version number cannot be spent by accident.
396
+ - The upload is gated on **the full test suite running against the released commit**, not on
397
+ main having been green at some earlier point: `test.yml` gained a `workflow_call` trigger and
398
+ `publish.yml` calls it, so a release cut from an untested commit cannot reach PyPI. It is also
399
+ gated on the tag agreeing with the built artifacts
400
+ ([`.github/scripts/check_release_version.py`](.github/scripts/check_release_version.py)) and on
401
+ `twine check --strict`. Publishing a `.dev` or pre-release version is refused. This is the
402
+ automated form of the check that #59 was filed for.
403
+
404
+ ## [0.1.1] and earlier
405
+
406
+ Not itemised here: this file starts at 0.2.0. See the git history, and the
407
+ historical plan documents under `docs/`.
408
+
409
+ [Unreleased]: https://github.com/chrisfinlay/jax-nufft/compare/v0.2.0...HEAD
410
+ [0.2.0]: https://github.com/chrisfinlay/jax-nufft/compare/v0.1.1...v0.2.0
411
+ [0.1.1]: https://github.com/chrisfinlay/jax-nufft/releases/tag/v0.1.1
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for describing the origin of the Work and
141
+ reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Support. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or support.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 jax-nufft contributors
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.