FastLSQ 0.4.2__tar.gz → 0.6.1__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 (130) hide show
  1. {fastlsq-0.4.2 → fastlsq-0.6.1}/CHANGELOG.md +414 -5
  2. {fastlsq-0.4.2 → fastlsq-0.6.1}/FastLSQ.egg-info/PKG-INFO +191 -13
  3. {fastlsq-0.4.2 → fastlsq-0.6.1}/FastLSQ.egg-info/SOURCES.txt +13 -0
  4. {fastlsq-0.4.2 → fastlsq-0.6.1}/FastLSQ.egg-info/requires.txt +1 -0
  5. {fastlsq-0.4.2 → fastlsq-0.6.1}/MANIFEST.in +1 -0
  6. {fastlsq-0.4.2 → fastlsq-0.6.1}/PKG-INFO +191 -13
  7. {fastlsq-0.4.2 → fastlsq-0.6.1}/README.md +189 -12
  8. fastlsq-0.6.1/examples/integral_equations.py +93 -0
  9. fastlsq-0.6.1/examples/sdf_domains.py +126 -0
  10. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/__init__.py +54 -1
  11. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/api.py +5 -1
  12. fastlsq-0.6.1/fastlsq/augment.py +558 -0
  13. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/basis.py +475 -7
  14. fastlsq-0.6.1/fastlsq/geometry.py +1029 -0
  15. fastlsq-0.6.1/fastlsq/kernels.py +330 -0
  16. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/learnable.py +49 -19
  17. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/linalg.py +63 -23
  18. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/newton.py +29 -6
  19. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/problems/__init__.py +7 -0
  20. fastlsq-0.6.1/fastlsq/problems/integral.py +284 -0
  21. fastlsq-0.6.1/fastlsq/py.typed +0 -0
  22. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/solvers.py +7 -5
  23. {fastlsq-0.4.2 → fastlsq-0.6.1}/pyproject.toml +5 -2
  24. {fastlsq-0.4.2 → fastlsq-0.6.1}/requirements.txt +1 -0
  25. fastlsq-0.6.1/tests/test_augment.py +299 -0
  26. fastlsq-0.6.1/tests/test_closed_forms_property.py +323 -0
  27. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_diagnostics_timing.py +4 -1
  28. fastlsq-0.6.1/tests/test_geometry_sdf.py +314 -0
  29. fastlsq-0.6.1/tests/test_kernels.py +208 -0
  30. fastlsq-0.6.1/tests/test_multi_integral.py +146 -0
  31. fastlsq-0.6.1/tests/test_problems_integral.py +184 -0
  32. fastlsq-0.6.1/tests/test_symbol.py +278 -0
  33. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_vector_basis.py +15 -1
  34. fastlsq-0.4.2/fastlsq/geometry.py +0 -243
  35. {fastlsq-0.4.2 → fastlsq-0.6.1}/FastLSQ.egg-info/dependency_links.txt +0 -0
  36. {fastlsq-0.4.2 → fastlsq-0.6.1}/FastLSQ.egg-info/top_level.txt +0 -0
  37. {fastlsq-0.4.2 → fastlsq-0.6.1}/LICENSE +0 -0
  38. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/add_your_own_pde.py +0 -0
  39. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/benchmark_comparison.py +0 -0
  40. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/custom_features.py +0 -0
  41. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/fred_sde.py +0 -0
  42. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/fred_sde_fastlsq.py +0 -0
  43. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/gaia_potential.py +0 -0
  44. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/gaia_potential_fastlsq.py +0 -0
  45. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/horizons_ephemeris.py +0 -0
  46. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/numerai_alpha.py +0 -0
  47. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/numerai_alpha_fastlsq.py +0 -0
  48. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/run_all_fastlsq.py +0 -0
  49. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/__init__.py +0 -0
  50. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/_alsu_lattice.py +0 -0
  51. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/_common.py +0 -0
  52. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/run_all.py +0 -0
  53. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
  54. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
  55. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_green_fff.py +0 -0
  56. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
  57. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
  58. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
  59. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_passive_loco.py +0 -0
  60. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
  61. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
  62. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
  63. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
  64. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_tides_3months.py +0 -0
  65. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
  66. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s01_visualize.py +0 -0
  67. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
  68. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
  69. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s04_sunspots.py +0 -0
  70. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s05_helioseismology.py +0 -0
  71. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s06_tides.py +0 -0
  72. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
  73. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
  74. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
  75. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
  76. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
  77. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
  78. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
  79. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s14_eeg.py +0 -0
  80. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/scenarios/s15_circadian.py +0 -0
  81. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/extras/spectral_expansion.py +0 -0
  82. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/grad_shafranov.py +0 -0
  83. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/grid_inverse.py +0 -0
  84. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/grid_rl_control.py +0 -0
  85. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/grid_swing.py +0 -0
  86. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/gs_inverse.py +0 -0
  87. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/gs_rl_control.py +0 -0
  88. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/integro_differential_demo.py +0 -0
  89. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/inverse_heat_source.py +0 -0
  90. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/inverse_magnetostatics.py +0 -0
  91. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/inverse_source_position.py +0 -0
  92. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/learnable_helmholtz.py +0 -0
  93. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/memory_diffusion.py +0 -0
  94. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/orbit_hill.py +0 -0
  95. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/orbit_inverse.py +0 -0
  96. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/orbit_rl.py +0 -0
  97. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/pde_discovery.py +0 -0
  98. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/rlc_integro_differential.py +0 -0
  99. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/run_all_extensions.py +0 -0
  100. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/run_linear.py +0 -0
  101. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/run_nonlinear.py +0 -0
  102. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/tutorial_basic.py +0 -0
  103. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/tutorial_nonlinear.py +0 -0
  104. {fastlsq-0.4.2 → fastlsq-0.6.1}/examples/vector_basis_stream_vorticity.py +0 -0
  105. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/benchmark.py +0 -0
  106. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/block.py +0 -0
  107. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/device.py +0 -0
  108. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/diagnostics.py +0 -0
  109. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/export.py +0 -0
  110. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/lightning.py +0 -0
  111. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/plotting.py +0 -0
  112. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/problems/linear.py +0 -0
  113. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/problems/nonlinear.py +0 -0
  114. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/problems/regression.py +0 -0
  115. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/tuning.py +0 -0
  116. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/utils.py +0 -0
  117. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/vector.py +0 -0
  118. {fastlsq-0.4.2 → fastlsq-0.6.1}/fastlsq/viz.py +0 -0
  119. {fastlsq-0.4.2 → fastlsq-0.6.1}/setup.cfg +0 -0
  120. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_basic.py +0 -0
  121. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_benchmarks_inverse.py +0 -0
  122. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_block.py +0 -0
  123. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_derivatives.py +0 -0
  124. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_device.py +0 -0
  125. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_grad_shafranov.py +0 -0
  126. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_grid_swing.py +0 -0
  127. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_integral.py +0 -0
  128. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_learnable.py +0 -0
  129. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_orbit_hill.py +0 -0
  130. {fastlsq-0.4.2 → fastlsq-0.6.1}/tests/test_projection.py +0 -0
@@ -2,6 +2,411 @@
2
2
 
3
3
  All notable changes to FastLSQ will be documented in this file.
4
4
 
5
+ ## Release tags
6
+
7
+ Every released version is tagged `vMAJOR.MINOR.PATCH`. The tags for 0.1.0 through
8
+ 0.6.0 were **reconstructed in September 2026**: the project was published to PyPI for
9
+ its first year with only one tag (`v0.2.2`) ever created by hand.
10
+
11
+ Each tag was placed by comparing the `fastlsq/*.py` sources at every commit in the
12
+ repository against the sdist actually published to PyPI, not by guessing from dates.
13
+ Fourteen of the seventeen published releases matched a commit byte for byte. The one
14
+ pre-existing tag, `v0.2.2`, was left untouched and independently resolved to the same
15
+ commit the reconstruction chose, which is the main evidence that the method is sound.
16
+
17
+ Three tags are **approximate**, and their tag messages say so:
18
+
19
+ | Version | What is wrong | Authoritative artefact |
20
+ |---|---|---|
21
+ | 0.1.2 | The published sdist contains `fastlsq/problems/apebench.py`, which exists in no commit in this repository | `fastlsq-0.1.2.tar.gz` on PyPI |
22
+ | 0.1.3 | Same file, same problem | `fastlsq-0.1.3.tar.gz` on PyPI |
23
+ | 0.1.5 | The published `fastlsq/problems/linear.py` is ahead of every committed version of that file | `fastlsq-0.1.5.tar.gz` on PyPI |
24
+
25
+ All three were released from a working tree holding source that was never committed.
26
+ For those versions the sdist on PyPI is the record, not the tag.
27
+
28
+ ### Versions documented here that were never released
29
+
30
+ - **0.2.0** (dated 2026-03-01 below) was never on PyPI and `pyproject.toml` never
31
+ declared it. The published sequence runs 0.1.4 → 0.1.5 → 0.2.1. There is no `v0.2.0`
32
+ tag. The entry is kept for the history of the work, not as a release.
33
+ - **0.4.3** and **0.5.0** are real code milestones on `main` -- `pyproject.toml`
34
+ declares each -- but neither was uploaded. The published sequence runs 0.4.2
35
+ (2026-07-20) straight to 0.6.0 (2026-07-21). Both are tagged, and both tags say
36
+ `NEVER PUBLISHED TO PyPI`.
37
+
38
+ ### Releases missing from this file
39
+
40
+ **0.1.1, 0.1.2, 0.1.3 and 0.1.4 were published to PyPI but have no entry below.**
41
+ They are tagged, and their scope can be read from the tags and the commit history, but
42
+ this file jumps 0.1.0 → 0.2.0 → 0.1.5 and does not describe them. Reconstructing four
43
+ changelog entries a year after the fact would be invention rather than record, so the
44
+ gap is documented here instead of filled in.
45
+
46
+ ## [0.6.1] - 2026-09-18
47
+
48
+ A correctness and packaging pass. No published number changes, and no API is
49
+ removed -- but three of the fixes below were silent wrong answers, not errors.
50
+
51
+ ### Fixed -- silent wrong answers
52
+
53
+ - **kernels** -- `SeparableKernelOperator.inner_products` keyed its cache on
54
+ `(id(basis), n_features)`. A freed basis' address is reused by the next one, and
55
+ `LearnableFastLSQ.basis` builds a new basis object on every access, so a learnable fit
56
+ of a Fredholm problem could train on another basis' operators (measured error of the
57
+ assembled block 0.083 on entries of about 0.05, with no warning). The key is now the
58
+ storage pointer and version counter of `W` and `b`, plus a weak reference to the basis.
59
+ - **linalg** -- `method="qr"` on a rank-deficient matrix returned garbage with no
60
+ exception and no warning (`||x|| = 7e18`, residual `6e4`, where `svd` gives `59` and
61
+ `1e-8`). The diagonal of `R` is now tested against `rcond` and warned about.
62
+ - **learnable** -- `driver="gelsd"` was hard-coded, and torch accepts the rank-revealing
63
+ LAPACK drivers on CPU only, so the learnable path could not run off CPU. Now CPU-only.
64
+
65
+ ### Fixed -- missing operators
66
+
67
+ - **augment** -- `AugmentedBasis` gained `hessian_diag`, `biharmonic`, `advection` and
68
+ `multi_integral`, with the closed forms for the polynomial columns.
69
+ `MultiIntegralOperator.apply(aug, x)` used to raise `AttributeError`, so the `Wave1D`,
70
+ `Wave2D_MS` and `Maxwell2D_TM` problems could not take an augmented basis at all.
71
+ - **basis** -- a 0-d (scalar) symbol is a constant multiplier, not an error.
72
+ - **solvers** -- `add_block(scale=...)` accepts a tuple or tensor of per-axis scales, not
73
+ just a list or ndarray.
74
+ - **newton** -- when no backtracked step satisfied Armijo, the loop restored `beta` and
75
+ then recomputed the identical step until `max_iter`, while `history` recorded the last
76
+ alpha tried as if the step had been taken. It now stops, records the rejection as
77
+ `step_size 0.0`, and tags every exit with a stop reason.
78
+ - **kernels** -- `from_inner_products` objects now raise a clear error from
79
+ `check_quadrature` and `degenerate_eigenvalues` instead of failing inside `leggauss(0)`.
80
+
81
+ ### Changed -- diagnostics and documentation
82
+
83
+ - **linalg** -- `return_info` now carries `method_used`, because `"auto"` picks its
84
+ back-end at run time; on CPU with `mu = 0` it never runs QR (Cholesky probe, then
85
+ `gelsd`), which the module docstring claimed otherwise. The docstring now describes what
86
+ the code does.
87
+ - **linalg/api** -- `rank_used` is documented as what it is: a post-hoc count of singular
88
+ values above `rcond * sigma_max`, not the rank the back-end worked with.
89
+ - **newton** -- the convergence test is relative to the first residual, as the code always
90
+ did; the docstring said absolute.
91
+ - **kernels** -- `degenerate_eigenvalues` no longer runs a full quadrature for inner
92
+ products it discards, and its docstring no longer claims the values depend on the basis.
93
+ - **basis** -- the `1/sqrt(N)` normalisation convention is documented, including the
94
+ differing defaults of `solve_linear` and `solve_nonlinear`.
95
+
96
+ ### Tests
97
+
98
+ - `test_symbol` used `np.trapz`, removed in NumPy 2, so the fractional-Laplacian check
99
+ against the singular-integral definition silently never ran on a current install. With
100
+ `np.trapezoid` the suite goes from 202 passed / 3 failed to 205 passed.
101
+ - New `tests/test_closed_forms_property.py`: 46 property tests checking every closed form
102
+ against an independent reference (autograd, Gauss-Legendre quadrature, the analytic
103
+ characteristic value) at random dimensions, feature counts, bandwidths and orders,
104
+ including the cache-invalidation regression above.
105
+ - `test_version` compared `fastlsq.__version__` against a hard-coded literal, so it had
106
+ to be edited on every release and never caught anything. It now compares
107
+ `__version__` against the version in `pyproject.toml`, which is the drift that can
108
+ actually happen.
109
+ - Full suite: 251 passed.
110
+
111
+ ### Packaging and repository
112
+
113
+ - **`scipy` is declared as a test dependency.** Eight scripts under `examples/` use it for
114
+ their reference solvers, and `tests/test_grad_shafranov.py` and `tests/test_grid_swing.py`
115
+ import those scripts -- but `scipy` appeared only in the `battery` extra, never in `dev`
116
+ or `requirements.txt`. `pip install -e ".[dev]"` into a clean environment produced six
117
+ errors; the suite passed locally only where scipy happened to be installed for other
118
+ reasons. Found by the new CI on its first run.
119
+ - `fastlsq/py.typed` is now actually shipped. `[tool.setuptools.package-data]` had
120
+ declared it since 0.4.0, but the file did not exist, so the package advertised PEP 561
121
+ inline typing that type checkers never saw.
122
+ - The build requirement is `setuptools>=77.0`. `license = "MIT"` is the PEP 639 SPDX
123
+ expression form, which setuptools only understands from 77.0 on; the pin said `>=68.0`.
124
+ - Continuous integration runs the suite on Python 3.9 through 3.12 (`.github/workflows/tests.yml`).
125
+ - The JOSS submission draft (`paper.md`, `paper.bib`) is tracked in the repository.
126
+ - Two 0.5.x changelog entries advertised example scripts under `examples/inverse/` that
127
+ were never shipped; both entries now say so.
128
+
129
+ ### Fixed -- documentation
130
+
131
+ - **The citation in `README.md` had the wrong paper title.** It read *"FastLSQ: A
132
+ Framework for One-Shot PDE Solving"*; the actual title of arXiv:2602.10541 is
133
+ *"FastLSQ: Solving PDEs in One Shot via Fourier Features with Exact Analytical
134
+ Derivatives"*. Anyone who copied that BibTeX block produced a wrong bibliography
135
+ entry. The block now matches the arXiv record and carries the DOI.
136
+ - `paper.bib` cited the same preprint without the `FastLSQ:` title prefix. Corrected to
137
+ the arXiv title, so the JOSS paper and the README agree.
138
+
139
+ ### Added -- release tags and README
140
+
141
+ - Release tags for every version, and the *Release tags* section above documenting how
142
+ they were reconstructed, which three are approximate, and which documented versions
143
+ were never released.
144
+ - `README.md`: status badges (CI, PyPI, Python versions, licence, arXiv); the
145
+ dependency and optional-extras list; a **Development** section covering the test
146
+ suite and CI; a **Releases and versioning** section; and a pointer to the JOSS paper
147
+ draft.
148
+
149
+ ### Changed -- README
150
+
151
+ - `README.md`: the BerkeleyLab ATAP talk link moved from above the project title, where
152
+ it preceded the description, into the **Paper** section with the other references.
153
+
154
+ ## [0.6.0] - 2026-07-21
155
+
156
+ Completes the operator taxonomy: multi-axis integrals, separable kernels, and
157
+ Fredholm equations, with the integral-equation problems promoted into the same
158
+ `solve_linear` harness as the PDEs so they report in one table.
159
+
160
+ ### New features
161
+
162
+ | Feature | API | What it buys |
163
+ |---|---|---|
164
+ | Multi-axis integrals | `MultiIntegralOperator`, `SinusoidalBasis.multi_integral` | Integrate over any subset of axes at once, each independently definite or Volterra — area/volume functionals and mixed "definite in space, running in time" memory terms that a single-axis operator cannot express |
165
+ | Separable (degenerate) kernels | `SeparableKernelOperator` | `K(x,y) = Σ g_m(x) h_m(y)` assembles as a rank-`R` product `G @ C`; the inner products `C` are computed once, independent of the collocation points |
166
+ | Fredholm, second kind | `fredholm_second_kind` | `u − λ∫K u = f` as `I − λK`, solved in the same single least squares as everything else |
167
+ | Kernel diagnostics | `degenerate_eigenvalues`, `check_quadrature` | The `λ` at which the equation is singular, and whether the inner-product quadrature actually resolves the basis — both failure modes that otherwise show up only as a bad fit |
168
+ | Integral Problem classes | `fastlsq.problems.integral` | Four integral / integro-differential problems with **closed-form** solutions, running through `solve_linear` |
169
+
170
+ ### Added
171
+
172
+ - **`MultiIntegralOperator` / `SinusoidalBasis.multi_integral`.** The plane wave
173
+ factorises over axes, `e^{iW·x} = ∏_k e^{iW_k x_k}`, so a multi-axis integral
174
+ is a *product* of the same numerically stable one-axis factors already used by
175
+ `definite_integral`, and the phase collapses to the value at the per-axis
176
+ midpoint:
177
+
178
+ ∫∫ φ_j ∏_{k∈S} dx_k = [∏_{k∈S} Δ_k · sinc(W_k Δ_k / 2π)] · sin(Z_mid)
179
+
180
+ Exact, quadrature-free, and finite for near-DC features on every axis. With
181
+ `|S| = 1` it reproduces `IntegralOperator.definite` bit-for-bit (verified to
182
+ 1e-14); against tensor-product quadrature in 2-D and 3-D it agrees to the
183
+ quadrature's own error. Order is 1 per axis — a repeated integral along one
184
+ axis remains `IntegralOperator`, whose Cauchy/Taylor branches handle the
185
+ small-`W` cancellation a naive repeated product would suffer.
186
+
187
+ - **`SeparableKernelOperator` (`fastlsq/kernels.py`).** A degenerate kernel
188
+ collapses the integral operator to `Σ_m g_m(x) ∫ h_m u`, so acting on the
189
+ basis needs only the `R × N` matrix `C_{mj} = ∫ h_m φ_j`. Assembly is
190
+ `G(x) @ C` — a rank-`R` factorisation instead of an `M × M` kernel evaluation,
191
+ and the assembled block's rank is exactly `R` (verified).
192
+
193
+ Quadrature enters in exactly one place, computing `C`, and it is a
194
+ precomputation rather than a per-row cost: tensor-product Gauss-Legendre,
195
+ spectrally convergent for smooth integrands. Because it must still resolve the
196
+ feature oscillation, `check_quadrature` reports the achieved convergence
197
+ (2e-14 for a typical basis; the test suite also pins that it *flags* an
198
+ under-resolved one), and `from_inner_products` accepts analytic `C` to skip
199
+ quadrature entirely.
200
+
201
+ - **`fredholm_second_kind` and `degenerate_eigenvalues`.** Fredholm equations of
202
+ the second kind assemble as `I − λK`. For a rank-`R` degenerate kernel there
203
+ are at most `R` characteristic values, at which the equation is singular;
204
+ `degenerate_eigenvalues` computes them so a near-singular `λ` is detectable
205
+ instead of silently producing a garbage fit. For `K(x,y)=xy` on `[0,1]` it
206
+ recovers the analytic `λ = 3` as `3.0000000000000093`.
207
+
208
+ Validated against degenerate-kernel theory rather than a reference quadrature:
209
+ the exact solution of `u − λ∫₀¹xy·u = f` is `u = f + λcx` with
210
+ `c = ∫yf/(1−λ/3)`, matched to ~1e-8 for `λ` swept from 0.25 to 2.9 (i.e. right
211
+ up to the singular value). `λ` may be an `nn.Parameter`.
212
+
213
+ - **`fastlsq/problems/integral.py`.** `FredholmProductKernel`,
214
+ `FredholmRank2Kernel`, `VolterraSecondKind` and `IntegroDifferentialODE`, each
215
+ with a closed-form solution, running through `solve_linear` exactly as the PDE
216
+ problems do — so integral equations now produce a results table alongside the
217
+ PDEs rather than living only in example scripts. The module docstring writes
218
+ down the (duck-typed, no base class) Problem contract, which was previously
219
+ implicit and inconsistent.
220
+
221
+ Note the second-kind problems return an **empty** `bcs` list: the identity term
222
+ makes them well posed with no boundary rows at all. Only the
223
+ integro-differential ODE, which has a genuine constant of integration, needs
224
+ one.
225
+
226
+ - **Accuracy regressions (`tests/test_problems_integral.py`).** Each problem is
227
+ solved through the real harness and checked against its analytic solution, and
228
+ separately each closed form is verified to satisfy its own equation by
229
+ independent quadrature — so a closed form and an operator cannot be wrong in
230
+ the same direction without being caught.
231
+
232
+ ### Measured
233
+
234
+ All five integral-equation problems, through `solve_linear` (300 features, 2000
235
+ collocation points), against their closed-form solutions:
236
+
237
+ | Problem | rel L2 | grad rel L2 | boundary rows |
238
+ |---|---|---|---|
239
+ | Fredholm `K=xy`, λ=0.5 | 5.5e-14 | 5.7e-12 | 0 |
240
+ | Fredholm `K=xy`, λ=2.0 | 3.8e-13 | 3.9e-11 | 0 |
241
+ | Fredholm rank-2, λ=0.4 | 9.1e-14 | 9.5e-12 | 0 |
242
+ | Volterra 2nd kind, λ=1.5 | 8.8e-13 | 1.0e-10 | 0 |
243
+ | Integro-differential ODE, λ=4.0 | 6.2e-16 | 1.7e-14 | 1 |
244
+
245
+ Approaching the kernel's singular value `λ = 3` costs only ~2 orders of
246
+ magnitude: 5.5e-14 at `λ = 0.5`, 6.8e-13 at `λ = 2.9`, 3.9e-12 at `λ = 2.99`.
247
+
248
+ ### Documentation
249
+
250
+ - The README now tables the SDF primitives (`sdf_ball`, `sdf_disk`, `sdf_box`,
251
+ `sdf_annulus`, `sdf_lshape`, `sdf_flower`, `sdf_polygon`, `sdf_tokamak`) and
252
+ the CSG combinators (`sdf_union`, `sdf_intersection`, `sdf_difference`,
253
+ `sdf_complement`) by name. They were exported in `__all__` from 0.5.0 but only
254
+ described in prose, so the composable `ψ`-level API was effectively
255
+ undiscoverable — you could find `SDFDomain.disk()` but not `sdf_disk`, and the
256
+ CSG functions not at all.
257
+ - `PolynomialColumns` is likewise named explicitly alongside `AugmentedBasis`.
258
+
259
+ ### Changed
260
+
261
+ - **Operator composition is now duck-typed.** `IntegroDifferentialOperator._as_terms`
262
+ and `DiffOperator.__add__` dispatch on the presence of `.apply(basis, x, cache)`
263
+ rather than a fixed `isinstance` tuple, so operators defined in other modules
264
+ (`SeparableKernelOperator`, and any user-defined operator) compose with the
265
+ built-ins without `basis.py` importing them. No change to existing behaviour.
266
+
267
+ ## [0.5.0] - 2026-07-21
268
+
269
+ Closes three gaps between what the paper describes and what the package shipped:
270
+ SDF geometry (§2.7), Fourier-symbol operators (§3.4), and explicit polynomial
271
+ augmentation columns (§2.3).
272
+
273
+ ### Added
274
+
275
+ - **SDF / membership-oracle geometry (`fastlsq.geometry`).** A domain can now be
276
+ given as any callable `ψ(x)` that is negative inside — no mesh, no analytic
277
+ sampler. `sample_sdf` rejection-samples the interior (unbiased, so the radial
278
+ CDF of a disk is exactly `r²`), `project_to_boundary` lands points on `ψ = 0`,
279
+ and `outward_normal` returns `∇ψ/‖∇ψ‖` via autograd, which is what a Neumann or
280
+ Robin condition needs. `SDFDomain` bundles these with a bounding box and CSG
281
+ composition (`|`, `&`, `-`), plus `neumann_rows` / `robin_rows` that contract
282
+ the analytic basis gradient against the normal into an `(M, N)` block.
283
+
284
+ Shipped domains: `disk`, `ball`, `box`, `annulus` (multiply-connected),
285
+ `lshape` (reentrant corner), `flower` (smooth non-convex), `polygon`, and
286
+ `tokamak` — the D-shaped Miller poloidal cross-section, at the same MAST-U
287
+ scale (`R ∈ [0.6, 1.4]`) as `examples/grad_shafranov.py`, which until now used
288
+ a plain rectangle.
289
+
290
+ Projection is **damped** Newton, `x ← x − ψ∇ψ/‖∇ψ‖²`, with per-point
291
+ backtracking and monotone acceptance. The `1/‖∇ψ‖²` normalisation matters:
292
+ the textbook `x − ψ∇ψ` is valid only for a true distance function, and
293
+ `sdf_flower` has `‖∇ψ‖` spanning 1 to ~10. Undamped, it left
294
+ `|ψ| ≈ 1.5e-1` on the boundary; damped and monotone, 99.9% of interior seeds
295
+ converge to `1e-10`. The residual failures are seeds within `r < 0.2` of the
296
+ centre, where a non-distance `ψ` has no unique nearest boundary point;
297
+ `sample_boundary_sdf` filters them rather than hiding them.
298
+
299
+ - **`SymbolOperator` — Fourier-multiplier (nonlocal) operators.** Every feature
300
+ is a plane wave, so a multiplier `L e^{iξ·x} = m(ξ) e^{iξ·x}` acts *diagonally*
301
+ on the basis: assembling `L` is a per-column rescale, exact to machine
302
+ precision, with no quadrature and no discretisation of the kernel. Operators
303
+ that are hard for mesh methods — the fractional Laplacian is nonlocal with a
304
+ singular kernel, giving a dense ill-conditioned FEM/FD matrix — cost exactly
305
+ what the Laplacian costs here.
306
+
307
+ Factories: `fractional_laplacian(s)`, `riesz_potential(s)`,
308
+ `riesz_transform(k)`, `convolution(k̂)`. The order `s` may be an
309
+ `nn.Parameter`: the symbol is `exp(s·log‖ξ‖²)`, so gradient descent recovers a
310
+ planted order (test recovers `s = 0.65` to `<1e-3`). Composes with `Op` and
311
+ `IntegralOperator` through the usual arithmetic.
312
+
313
+ Verified against independent references, not just self-consistency: `s=1`
314
+ reproduces `−Δ` and `s=2` reproduces `Δ²` bit-exactly; the symbol is checked
315
+ against the **singular-integral definition** including its normalising constant
316
+ `C(1,s)`; and Gaussian convolution matches direct quadrature to `<1e-8`.
317
+
318
+ `ConvolutionOperator` is `SymbolOperator.convolution(k̂)` — this is what
319
+ `examples/memory_diffusion.py:80` was doing by hand with a `(1, N)` broadcast
320
+ coefficient. That hand-rolled version uses `basis.W[0:1, :]`, correct only
321
+ because `d=2` has exactly one spatial axis; a symbol sees all of `W` and
322
+ generalises to any dimension.
323
+
324
+ - **Polynomial / DC augmentation columns (`fastlsq.augment`).** `AugmentedBasis`
325
+ widens a basis with explicit `1, x, x², …` columns carrying **exact** operator
326
+ images — analytic monomial derivatives, antiderivatives (a negative multi-index
327
+ integrates: `x^p → x^{p+1}/(p+1)`), and Cauchy iterated integrals — not a
328
+ zero-derivative stub. It implements the same duck-typed protocol as
329
+ `SinusoidalBasis`, so `Op`, `IntegralOperator` and `SymbolOperator` all work
330
+ unchanged and return `(M, N + n_cols)`.
331
+
332
+ This is what §2.3 means by an integration constant "pinned by an explicit
333
+ polynomial column"; previously the only such column in the repo was hand-rolled
334
+ in `examples/extras/scenarios/s01_beamloss_ode.py`, which had to maintain its
335
+ derivative block by hand.
336
+
337
+ Scope, honestly: a sinusoidal bank with random phases can already approximate a
338
+ constant from near-DC features, so this is an improvement, not a rescue — on
339
+ `u'' = f` with a DC offset it is neutral at `C = 0` and ~12× better at
340
+ `C = 1000`. A non-constant monomial has no function-valued Fourier-multiplier
341
+ image, so `SymbolOperator` on `degree > 0` columns raises rather than inventing
342
+ a value.
343
+
344
+ ### Fixed
345
+
346
+ - **`ProjectionOperator`'s docstring claimed the Fourier-symbol operators were
347
+ "already in the package".** They were not, until this release. It now points at
348
+ `SymbolOperator` and states the actual distinction (a symbol acts diagonally;
349
+ a projection row mixes features).
350
+
351
+ ### Notes
352
+
353
+ - **Which fractional Laplacian.** The symbol calculus applies the multiplier to
354
+ the global plane-wave extension of the trial function, so
355
+ `fractional_laplacian` is the **whole-space (restricted)** `(−Δ)^s` on `R^d`,
356
+ *not* the spectral or regional variant defined by an eigenbasis of a bounded
357
+ domain. These coincide on `R^d` and differ on a bounded domain. Results should
358
+ not be read as the spectral fractional Laplacian.
359
+
360
+ - Boundary points from `sample_boundary_sdf` are **not** uniform with respect to
361
+ surface measure — they are the pushforward of the uniform box measure under the
362
+ projection, which over-weights convex bulges. Fine for collocation; do not use
363
+ them as quadrature nodes for a surface integral without reweighting.
364
+
365
+ ## [0.4.3] - 2026-07-20
366
+
367
+ ### Fixed
368
+
369
+ - **The Σ-learner trained uphill: `train_bandwidth` now uses the exact envelope
370
+ gradient.** The outer loss was backpropagated *through* `torch.linalg.lstsq`,
371
+ whose backward carries a `(AᵀA)⁻¹` and therefore squares `cond(A)`. For a
372
+ routine collocation system (`cond(A) ≈ 2.2e11` on the `AnisoPoisson` benchmark)
373
+ that needs ~5e22 of dynamic range, far past float64. Measured against central
374
+ finite differences at the isotropic init, the resulting "gradient" was **4.3e6×
375
+ too large with `cos(angle) = −0.707`** — an *ascent* direction, which `AdamW`
376
+ duly followed (`clip_grad` rescaled the magnitude but preserved the direction).
377
+ `fit()` consequently returned a solution ~3.6× *worse* than the isotropic
378
+ `solve_linear` baseline it is meant to beat.
379
+
380
+ The inner solve is now performed under `no_grad` and only the assembled `A(L)`
381
+ is differentiated. This is **exact, not an approximation**: for
382
+ `J(L) = ‖A(L)β* − b‖²` the chain-rule contribution through `β*` is
383
+ `2 (Aᵀr)ᵀ dβ*/dL`, and `Aᵀr = 0` at the least-squares optimum (the residual is
384
+ orthogonal to `range(A)`), so the term vanishes identically and
385
+ `dJ/dL = 2 rᵀ (dA/dL) β*` — the envelope (Danskin) theorem. The detached
386
+ gradient matches finite differences to **1.2e-3 relative, `cos(angle) = 1.0000`**,
387
+ and skips the ill-conditioned backward entirely.
388
+
389
+ On the `AnisoPoisson` benchmark the training loss now descends monotonically
390
+ (`1.9e-3` → `9.8e-11`) instead of climbing (`1.9e-3` → `1.2e+05`), an
391
+ independent held-out collocation draw tracks it the whole way (no overfitting),
392
+ and the learned Σ reaches `4.4e-8` against the isotropic `solve_linear`
393
+ baseline's `1.9e-4` — **~4350× better**, where it was previously ~3.6× *worse*.
394
+ Not backpropagating through the solve also makes each step ~6× cheaper
395
+ (`tests/test_learnable.py`: 389s → 57s).
396
+
397
+ Note this is the *third* attempt at this bug (see 0.2.1 and 0.2.2): both earlier
398
+ fixes swapped the inner solve's driver (`svd` → rank-revealing `gelsd`) to stop
399
+ the gradient being `NaN`. That made it **finite but never correct** — the loop
400
+ still differentiated through the solve. Differentiating through an
401
+ ill-conditioned least-squares solve is not stabilisable by choice of driver;
402
+ the envelope identity removes the need to do it at all.
403
+ - **`train_bandwidth` restored the wrong "best" iterate.** `best_params` was
404
+ snapshotted *after* `optimizer.step()`, so it stored `θ_{t+1}` while recording
405
+ `loss(θ_t)` — the best-iterate restore returned the successor of the best
406
+ iterate. The snapshot (and the `history` entry's `sigma` / `cov_diag`, which had
407
+ the same off-by-one) now happens before the step, so all three describe the same
408
+ point.
409
+
5
410
  ## [0.4.2] - 2026-07-20
6
411
 
7
412
  ### Fixed
@@ -105,8 +510,10 @@ All notable changes to FastLSQ will be documented in this file.
105
510
  Gauss--Hermite quadrature of the slice integral to machine precision in d = 2, 3, 4
106
511
  (≤4e-13 observed), that autodiff of the rows wrt `c` matches finite differences, and
107
512
  that a windowed field is recovered from its projections at several directions in one
108
- LSQ. `examples/inverse/tomography_projection.py` demonstrates the full reconstruction
109
- plus the differentiable-optics gradient.
513
+ LSQ. *(Correction, 0.6.1: this entry also announced an
514
+ `examples/inverse/tomography_projection.py` demonstrating the full reconstruction plus
515
+ the differentiable-optics gradient. That example was never shipped; the reconstruction
516
+ and the gradient check live in `tests/test_projection.py` instead.)*
110
517
 
111
518
  ### Scope (honest)
112
519
 
@@ -145,9 +552,11 @@ All notable changes to FastLSQ will be documented in this file.
145
552
  linear-least-squares design matrix; coefficients stay differentiable so learnable
146
553
  integral-term coefficients train through the solve. All three exported from `fastlsq`.
147
554
  - **Examples** `examples/integro_differential_demo.py` (one-shot forward solve of
148
- `u'(x) + ∫_0^x u ds = f`, rel-L2 ~3e-11) and
149
- `examples/inverse/inverse_memory_kernel.py` (recover an unknown memory strength λ from
150
- noisy data via AdamW through the differentiable solve). New `tests/test_integral.py`
555
+ `u'(x) + ∫_0^x u ds = f`, rel-L2 ~3e-11). *(Correction, 0.6.1: this entry also
556
+ announced an `examples/inverse/inverse_memory_kernel.py` recovering an unknown memory
557
+ strength λ from noisy data via AdamW through the differentiable solve. That example was
558
+ never shipped; gradient flow through the solve is covered by `tests/test_integral.py`.)*
559
+ New `tests/test_integral.py`
151
560
  asserts ∫-then-∂ round-trips to identity, Volterra/definite match quadrature, the DC
152
561
  guard stays finite, and gradients flow to learnable coefficients.
153
562