FastLSQ 0.2.6__tar.gz → 0.4.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 (152) hide show
  1. {fastlsq-0.2.6 → fastlsq-0.4.0}/CHANGELOG.md +95 -0
  2. {fastlsq-0.2.6 → fastlsq-0.4.0}/FastLSQ.egg-info/PKG-INFO +4 -1
  3. {fastlsq-0.2.6 → fastlsq-0.4.0}/FastLSQ.egg-info/SOURCES.txt +9 -0
  4. {fastlsq-0.2.6 → fastlsq-0.4.0}/PKG-INFO +4 -1
  5. {fastlsq-0.2.6 → fastlsq-0.4.0}/README.md +3 -0
  6. fastlsq-0.4.0/examples/integro_differential_demo.py +112 -0
  7. fastlsq-0.4.0/examples/inverse/inverse_memory_kernel.py +151 -0
  8. fastlsq-0.4.0/examples/inverse/inverse_source_integral_sensors.py +130 -0
  9. fastlsq-0.4.0/examples/inverse/rlc_system_id.py +159 -0
  10. fastlsq-0.4.0/examples/inverse/tomography_projection.py +145 -0
  11. fastlsq-0.4.0/examples/memory_diffusion.py +144 -0
  12. fastlsq-0.4.0/examples/rlc_integro_differential.py +153 -0
  13. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/__init__.py +16 -2
  14. fastlsq-0.4.0/fastlsq/basis.py +1231 -0
  15. {fastlsq-0.2.6 → fastlsq-0.4.0}/pyproject.toml +1 -1
  16. fastlsq-0.4.0/tests/test_integral.py +195 -0
  17. fastlsq-0.4.0/tests/test_projection.py +263 -0
  18. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_vector_basis.py +1 -1
  19. fastlsq-0.2.6/fastlsq/basis.py +0 -635
  20. {fastlsq-0.2.6 → fastlsq-0.4.0}/FastLSQ.egg-info/dependency_links.txt +0 -0
  21. {fastlsq-0.2.6 → fastlsq-0.4.0}/FastLSQ.egg-info/requires.txt +0 -0
  22. {fastlsq-0.2.6 → fastlsq-0.4.0}/FastLSQ.egg-info/top_level.txt +0 -0
  23. {fastlsq-0.2.6 → fastlsq-0.4.0}/LICENSE +0 -0
  24. {fastlsq-0.2.6 → fastlsq-0.4.0}/MANIFEST.in +0 -0
  25. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/add_your_own_pde.py +0 -0
  26. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/benchmark_comparison.py +0 -0
  27. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/custom_features.py +0 -0
  28. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/darcy_heat.py +0 -0
  29. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/pendulum.py +0 -0
  30. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/pendulum_benchmark.py +0 -0
  31. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/plasma_wakefield.py +0 -0
  32. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/plasma_wakefield_2D_1.py +0 -0
  33. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/plasma_wakefield_2D_2.py +0 -0
  34. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/plasma_wakefield_2d_3.py +0 -0
  35. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/plasma_wakefield_parameteric.py +0 -0
  36. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/plot_utils.py +0 -0
  37. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/structural_health_simple.py +0 -0
  38. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/digital_twins/turbulence_gravity_cooling.py +0 -0
  39. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/fred_sde.py +0 -0
  40. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/fred_sde_fastlsq.py +0 -0
  41. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/gaia_potential.py +0 -0
  42. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/gaia_potential_fastlsq.py +0 -0
  43. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/horizons_ephemeris.py +0 -0
  44. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/numerai_alpha.py +0 -0
  45. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/numerai_alpha_fastlsq.py +0 -0
  46. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/run_all_fastlsq.py +0 -0
  47. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/__init__.py +0 -0
  48. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/_alsu_lattice.py +0 -0
  49. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/_common.py +0 -0
  50. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/run_all.py +0 -0
  51. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
  52. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
  53. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_green_fff.py +0 -0
  54. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
  55. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
  56. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
  57. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_passive_loco.py +0 -0
  58. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
  59. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
  60. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
  61. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
  62. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_tides_3months.py +0 -0
  63. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
  64. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s01_visualize.py +0 -0
  65. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
  66. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
  67. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s04_sunspots.py +0 -0
  68. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s05_helioseismology.py +0 -0
  69. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s06_tides.py +0 -0
  70. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
  71. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
  72. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
  73. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
  74. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
  75. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
  76. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
  77. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s14_eeg.py +0 -0
  78. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/scenarios/s15_circadian.py +0 -0
  79. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/extras/spectral_expansion.py +0 -0
  80. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/grad_shafranov.py +0 -0
  81. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/grid_inverse.py +0 -0
  82. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/grid_rl_control.py +0 -0
  83. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/grid_swing.py +0 -0
  84. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/gs_inverse.py +0 -0
  85. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/gs_rl_control.py +0 -0
  86. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse/aero_.py +0 -0
  87. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse/denoising_parameter_estimation.py +0 -0
  88. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse/elastic_wave_animation.py +0 -0
  89. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse/heat_from_video.py +0 -0
  90. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse/inverse_turbulence.py +0 -0
  91. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse/shape_ns.py +0 -0
  92. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse/subsurface_imaging.py +0 -0
  93. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse/wing_optimize_simple.py +0 -0
  94. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse_heat_source.py +0 -0
  95. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse_magnetostatics.py +0 -0
  96. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/inverse_source_position.py +0 -0
  97. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/learnable_helmholtz.py +0 -0
  98. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/orbit_hill.py +0 -0
  99. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/orbit_inverse.py +0 -0
  100. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/orbit_rl.py +0 -0
  101. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/pde_discovery.py +0 -0
  102. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/run_all_extensions.py +0 -0
  103. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/run_linear.py +0 -0
  104. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/run_nonlinear.py +0 -0
  105. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/sindy/compare_sindy_methods.py +0 -0
  106. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/sindy/sindy_benchmarks.py +0 -0
  107. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/sindy/sindy_differentiable.py +0 -0
  108. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/sindy/sindy_minimal_diff.py +0 -0
  109. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/tutorial_basic.py +0 -0
  110. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/tutorial_nonlinear.py +0 -0
  111. {fastlsq-0.2.6 → fastlsq-0.4.0}/examples/vector_basis_stream_vorticity.py +0 -0
  112. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/api.py +0 -0
  113. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/block.py +0 -0
  114. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/device.py +0 -0
  115. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/diagnostics.py +0 -0
  116. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/export.py +0 -0
  117. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/geometry.py +0 -0
  118. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/learnable.py +0 -0
  119. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/lightning.py +0 -0
  120. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/linalg.py +0 -0
  121. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/newton.py +0 -0
  122. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/plotting.py +0 -0
  123. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/problems/__init__.py +0 -0
  124. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/problems/linear.py +0 -0
  125. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/problems/nonlinear.py +0 -0
  126. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/problems/regression.py +0 -0
  127. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/solvers.py +0 -0
  128. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/tuning.py +0 -0
  129. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/utils.py +0 -0
  130. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/vector.py +0 -0
  131. {fastlsq-0.2.6 → fastlsq-0.4.0}/fastlsq/viz.py +0 -0
  132. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/fastlsq_teaser.png +0 -0
  133. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/ideal_quadrupole.png +0 -0
  134. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/inverse_heat_source.gif +0 -0
  135. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/inverse_heat_source.png +0 -0
  136. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/inverse_magnetostatics.png +0 -0
  137. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/inverse_magnetostatics_convergence.png +0 -0
  138. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/quadrupole_convergence.png +0 -0
  139. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/quadrupole_optimization.png +0 -0
  140. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/tutorial_nlpoisson_convergence.png +0 -0
  141. {fastlsq-0.2.6 → fastlsq-0.4.0}/misc/tutorial_nlpoisson_solution.png +0 -0
  142. {fastlsq-0.2.6 → fastlsq-0.4.0}/requirements.txt +0 -0
  143. {fastlsq-0.2.6 → fastlsq-0.4.0}/setup.cfg +0 -0
  144. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_basic.py +0 -0
  145. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_benchmarks_inverse.py +0 -0
  146. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_block.py +0 -0
  147. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_derivatives.py +0 -0
  148. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_device.py +0 -0
  149. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_grad_shafranov.py +0 -0
  150. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_grid_swing.py +0 -0
  151. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_learnable.py +0 -0
  152. {fastlsq-0.2.6 → fastlsq-0.4.0}/tests/test_orbit_hill.py +0 -0
@@ -2,6 +2,101 @@
2
2
 
3
3
  All notable changes to FastLSQ will be documented in this file.
4
4
 
5
+ ## [0.4.0] - 2026-06-22
6
+
7
+ ### Added
8
+
9
+ - **Closed-form projection (Radon) operator for windowed bases.** The integral
10
+ operator class now covers the *projection / Radon* (line/hyperplane-integral)
11
+ family, not just the single-axis integrals of 0.3.0. A tomographic measurement is
12
+ a projection onto a generally **non-axis-aligned** hyperplane,
13
+ `p(u) = ∫ f(z) δ(c·z − u) dz` (a Fredholm equation of the first kind) — beam
14
+ phase-space tomography, CT, Abel inversion — which the single-axis `IntegralOperator`
15
+ cannot express.
16
+ - **`GaussianWindowedBasis`** — the windowed-Fourier (Gabor) member of the basis
17
+ family, `ψ_j(z) = exp(−‖ζ‖²/2)·sin(W_j·ζ + b_j)` with `ζ = T⁻¹(z − mean)`. The
18
+ Gaussian window is a **fixed prior** (set once from the data's second moments via
19
+ `GaussianWindowedBasis.from_data`, *not* trained): the projection of a bare unbounded
20
+ sinusoid over an infinite hyperplane diverges, and the window makes the hyperplane
21
+ integral integrable and analytic. Coefficients stay linear, so a fit is still one
22
+ linear least squares. The Gaussian envelope changes the derivative algebra, so this
23
+ class is deliberately scoped to **value** (`evaluate`) and **projection** — it does
24
+ not claim the full `DiffOperator` calculus of the bare `SinusoidalBasis`.
25
+ - **`ProjectionOperator`** — assembles the `(M, N)` projection design matrix in
26
+ **closed form, with no quadrature**: in the whitened frame, with `q = Tᵀc`,
27
+ `σ_u² = ‖q‖²`, `u₀ = c·mean`, `jac = |det T|/‖q‖`,
28
+ `(P ψ_j)(u) = jac·(2π)^((d−1)/2)·exp(−‖ω_j‖²/2)·exp(−(u−u₀)²/(2σ_u²))·sin(α_j u + φ_j)`,
29
+ with `α_j = (W_j·q)/‖q‖²`, `‖ω_j‖² = ‖W_j‖² − (W_j·q̂)²`, `φ_j = b_j − α_j u₀`. The
30
+ rows are **differentiable in the direction `c` (the optics)**, for differentiable
31
+ experiment design (autodiff `d(posterior)/d(optics)`): the across-slice energy uses
32
+ the rotation-invariant `‖W‖²−(W·q̂)²` (no QR complement) and every quantity stays a
33
+ tensor (no `float()`/`.item()` casts), so autograd flows to `c` (verified
34
+ `autodiff == finite-difference` to ~5e-9). `from_transport(M, e)` builds the
35
+ tomography convention `c = Mᵀe` (transport by optics `M`, read on axis `e`). The
36
+ operator mirrors the `apply(basis, x, cache)` signature but is standalone (it needs
37
+ the windowed basis, so it does not compose into `IntegroDifferentialOperator`). Both
38
+ classes are exported from `fastlsq`.
39
+ - **Tests / example.** `tests/test_projection.py` asserts the closed form equals a
40
+ Gauss--Hermite quadrature of the slice integral to machine precision in d = 2, 3, 4
41
+ (≤4e-13 observed), that autodiff of the rows wrt `c` matches finite differences, and
42
+ that a windowed field is recovered from its projections at several directions in one
43
+ LSQ. `examples/inverse/tomography_projection.py` demonstrates the full reconstruction
44
+ plus the differentiable-optics gradient.
45
+
46
+ ### Scope (honest)
47
+
48
+ - The closed-form projection assembly works **only** for the Gaussian-windowed basis
49
+ (the Gaussian × plane-wave hyperplane integral is analytic); other windows
50
+ (compact / polynomial) generally are **not** closed form. Scope = *Gaussian-windowed*
51
+ tomographic / line-integral operators, not "any projection".
52
+ - The window is a *fixed prior* (set from data moments), required for convergence — not
53
+ a tuned hyperparameter.
54
+ - This is a **different** analytic-kernel mechanism from the Fourier-symbol
55
+ (convolution / fractional) class — it is the projection / Radon (line/hyperplane
56
+ integral) class.
57
+ - No novelty is claimed over ELM / RBF-for-integral-equations prior art; the distinctive
58
+ parts are quadrature-free closed-form projection rows, differentiability in the optics,
59
+ and the unified operator algebra.
60
+
61
+ ## [0.3.0] - 2026-06-21
62
+
63
+ ### Added
64
+
65
+ - **Exact closed-form integral & integro-differential operators.** The cyclic
66
+ derivative identity `D^α sin(W·x+b) = (∏ W^{α_k}) Φ_{|α| mod 4}` runs *backwards* --
67
+ integration is differentiation of *negative* order, `∫ sin(wx+b) dx = −(1/w)cos(wx+b)`.
68
+ `SinusoidalBasis.derivative` now accepts **signed** multi-indices (negative entries =
69
+ indefinite integration), with a DC guard (`dc_eps`) that zeros features whose frequency
70
+ along an integrated axis is ~0 (their primitive is a ramp that leaves the sinusoidal
71
+ family). `DiffOperator.antiderivative(dim, order, d)` exposes this as a factory, so
72
+ `Op.partial(0,1,1) - k*Op.antiderivative(0,1,1)` composes integro-differential operators.
73
+ - **`IntegralOperator`** -- definite and running (Volterra) integrals with limits,
74
+ via the new `SinusoidalBasis.definite_integral`. Evaluated with a numerically stable
75
+ `sinc` identity (no `1/w` division), so the running integral of a near-DC feature is
76
+ exact rather than singular. Factories `IntegralOperator.volterra(...)` /
77
+ `IntegralOperator.definite(...)`.
78
+ - **`IntegroDifferentialOperator`** -- the common roof under which differential and
79
+ integral terms compose (`+`, `−`, scalar/`nn.Parameter` `*`) into one `(M, N)`
80
+ linear-least-squares design matrix; coefficients stay differentiable so learnable
81
+ integral-term coefficients train through the solve. All three exported from `fastlsq`.
82
+ - **Examples** `examples/integro_differential_demo.py` (one-shot forward solve of
83
+ `u'(x) + ∫_0^x u ds = f`, rel-L2 ~3e-11) and
84
+ `examples/inverse/inverse_memory_kernel.py` (recover an unknown memory strength λ from
85
+ noisy data via AdamW through the differentiable solve). New `tests/test_integral.py`
86
+ asserts ∫-then-∂ round-trips to identity, Volterra/definite match quadrature, the DC
87
+ guard stays finite, and gradients flow to learnable coefficients.
88
+
89
+ ## [0.2.6] - 2026-06-09
90
+
91
+ ### Changed
92
+
93
+ - **Faster `_auto_solve` fallback for ill-conditioned systems.** When the
94
+ Cholesky path fails on CPU with no ridge (`mu = 0`), `_auto_solve` now goes
95
+ straight to the rank-deficient-safe SVD solve (LAPACK `gelsd`) instead of
96
+ first attempting Householder QR with the blow-up guard -- on CPU `gelsd` is
97
+ faster than QR, so the detour only added a full extra factorization. The
98
+ QR-then-SVD path is unchanged for ridge solves and non-CPU devices.
99
+
5
100
  ## [0.2.5] - 2026-06-04
6
101
 
7
102
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FastLSQ
3
- Version: 0.2.6
3
+ Version: 0.4.0
4
4
  Summary: One-shot PDE solving via Fourier features with exact analytical derivatives; rank-revealing solvers, learnable anisotropic bandwidth, and CPU/CUDA/MPS support
5
5
  Author: Antonin Sulc
6
6
  License-Expression: MIT
@@ -278,6 +278,8 @@ derivative engine:
278
278
  | `SinusoidalBasis` | Evaluates basis functions and arbitrary-order derivatives in O(1) via the cyclic identity |
279
279
  | `BasisCache` | Pre-computes sin(Z)/cos(Z) once, reuses across multiple derivative evaluations |
280
280
  | `DiffOperator` / `Op` | Symbolic linear differential operators that compose via +, -, scalar *; coefficients can be `nn.Parameter` for learnable PDEs |
281
+ | `IntegralOperator` / `IntegroDifferentialOperator` | Closed-form **single-axis** definite / running (Volterra) integrals; compose with `Op` into one integro-differential design matrix |
282
+ | `GaussianWindowedBasis` / `ProjectionOperator` | Windowed-Fourier (Gabor) basis + closed-form **projection (Radon)** operator `∫ f δ(c·z−u) dz` for tomographic / line-integral inverse problems; quadrature-free and differentiable in the optics `c` |
281
283
  | `FeatureBasis` | Adapter for non-sinusoidal solvers (e.g. PIELM with tanh) |
282
284
  | `FastLSQSolver` | Manages feature blocks; exposes `.basis` for all derivative computations |
283
285
  | `LearnableFastLSQ` | Differentiable solver with learnable bandwidth via reparameterisation trick |
@@ -367,6 +369,7 @@ See `examples/add_your_own_pde.py` for the complete tutorial.
367
369
 
368
370
  - **Analytical derivative engine**: `SinusoidalBasis` computes arbitrary-order derivatives exactly in O(1) -- the foundation of the entire framework
369
371
  - **Symbolic PDE operators**: Compose differential operators with `Op` (Laplacian, wave, Helmholtz, biharmonic, custom) via intuitive arithmetic; coefficients can be `nn.Parameter` for AdamW optimisation
372
+ - **Closed-form integral operators**: `IntegralOperator` (single-axis definite / Volterra integrals) composes with `Op` into one integro-differential least-squares block. The integral class now also includes the **projection (Radon) operator** (`ProjectionOperator` on a `GaussianWindowedBasis`) -- quadrature-free `∫ f δ(c·z−u) dz` line/hyperplane integrals for tomographic inverse problems, differentiable in the optics `c` for experiment design
370
373
  - **Vector-valued solutions**: First-class support for **u**: ℝᵈ → ℝᵏ (elasticity, Stokes, Maxwell). Problems declare `n_outputs = k`; `block_concat` assembles coupled block systems; `solver.predict(x)` returns shape `(M, k)`. Scalar problems are the `k=1` case
371
374
  - **High-level API**: Solve PDEs in one line with `solve_linear()` and `solve_nonlinear()`
372
375
  - **Robust linear solver**: Pluggable least-squares back-ends; the default `auto` routes Cholesky -> QR -> SVD, and backward-stable QR delivers SVD-grade accuracy at QR cost on the rank-deficient random-feature system
@@ -18,14 +18,17 @@ examples/grid_rl_control.py
18
18
  examples/grid_swing.py
19
19
  examples/gs_inverse.py
20
20
  examples/gs_rl_control.py
21
+ examples/integro_differential_demo.py
21
22
  examples/inverse_heat_source.py
22
23
  examples/inverse_magnetostatics.py
23
24
  examples/inverse_source_position.py
24
25
  examples/learnable_helmholtz.py
26
+ examples/memory_diffusion.py
25
27
  examples/orbit_hill.py
26
28
  examples/orbit_inverse.py
27
29
  examples/orbit_rl.py
28
30
  examples/pde_discovery.py
31
+ examples/rlc_integro_differential.py
29
32
  examples/run_all_extensions.py
30
33
  examples/run_linear.py
31
34
  examples/run_nonlinear.py
@@ -88,9 +91,13 @@ examples/inverse/aero_.py
88
91
  examples/inverse/denoising_parameter_estimation.py
89
92
  examples/inverse/elastic_wave_animation.py
90
93
  examples/inverse/heat_from_video.py
94
+ examples/inverse/inverse_memory_kernel.py
95
+ examples/inverse/inverse_source_integral_sensors.py
91
96
  examples/inverse/inverse_turbulence.py
97
+ examples/inverse/rlc_system_id.py
92
98
  examples/inverse/shape_ns.py
93
99
  examples/inverse/subsurface_imaging.py
100
+ examples/inverse/tomography_projection.py
94
101
  examples/inverse/wing_optimize_simple.py
95
102
  examples/sindy/compare_sindy_methods.py
96
103
  examples/sindy/sindy_benchmarks.py
@@ -135,6 +142,8 @@ tests/test_derivatives.py
135
142
  tests/test_device.py
136
143
  tests/test_grad_shafranov.py
137
144
  tests/test_grid_swing.py
145
+ tests/test_integral.py
138
146
  tests/test_learnable.py
139
147
  tests/test_orbit_hill.py
148
+ tests/test_projection.py
140
149
  tests/test_vector_basis.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FastLSQ
3
- Version: 0.2.6
3
+ Version: 0.4.0
4
4
  Summary: One-shot PDE solving via Fourier features with exact analytical derivatives; rank-revealing solvers, learnable anisotropic bandwidth, and CPU/CUDA/MPS support
5
5
  Author: Antonin Sulc
6
6
  License-Expression: MIT
@@ -278,6 +278,8 @@ derivative engine:
278
278
  | `SinusoidalBasis` | Evaluates basis functions and arbitrary-order derivatives in O(1) via the cyclic identity |
279
279
  | `BasisCache` | Pre-computes sin(Z)/cos(Z) once, reuses across multiple derivative evaluations |
280
280
  | `DiffOperator` / `Op` | Symbolic linear differential operators that compose via +, -, scalar *; coefficients can be `nn.Parameter` for learnable PDEs |
281
+ | `IntegralOperator` / `IntegroDifferentialOperator` | Closed-form **single-axis** definite / running (Volterra) integrals; compose with `Op` into one integro-differential design matrix |
282
+ | `GaussianWindowedBasis` / `ProjectionOperator` | Windowed-Fourier (Gabor) basis + closed-form **projection (Radon)** operator `∫ f δ(c·z−u) dz` for tomographic / line-integral inverse problems; quadrature-free and differentiable in the optics `c` |
281
283
  | `FeatureBasis` | Adapter for non-sinusoidal solvers (e.g. PIELM with tanh) |
282
284
  | `FastLSQSolver` | Manages feature blocks; exposes `.basis` for all derivative computations |
283
285
  | `LearnableFastLSQ` | Differentiable solver with learnable bandwidth via reparameterisation trick |
@@ -367,6 +369,7 @@ See `examples/add_your_own_pde.py` for the complete tutorial.
367
369
 
368
370
  - **Analytical derivative engine**: `SinusoidalBasis` computes arbitrary-order derivatives exactly in O(1) -- the foundation of the entire framework
369
371
  - **Symbolic PDE operators**: Compose differential operators with `Op` (Laplacian, wave, Helmholtz, biharmonic, custom) via intuitive arithmetic; coefficients can be `nn.Parameter` for AdamW optimisation
372
+ - **Closed-form integral operators**: `IntegralOperator` (single-axis definite / Volterra integrals) composes with `Op` into one integro-differential least-squares block. The integral class now also includes the **projection (Radon) operator** (`ProjectionOperator` on a `GaussianWindowedBasis`) -- quadrature-free `∫ f δ(c·z−u) dz` line/hyperplane integrals for tomographic inverse problems, differentiable in the optics `c` for experiment design
370
373
  - **Vector-valued solutions**: First-class support for **u**: ℝᵈ → ℝᵏ (elasticity, Stokes, Maxwell). Problems declare `n_outputs = k`; `block_concat` assembles coupled block systems; `solver.predict(x)` returns shape `(M, k)`. Scalar problems are the `k=1` case
371
374
  - **High-level API**: Solve PDEs in one line with `solve_linear()` and `solve_nonlinear()`
372
375
  - **Robust linear solver**: Pluggable least-squares back-ends; the default `auto` routes Cholesky -> QR -> SVD, and backward-stable QR delivers SVD-grade accuracy at QR cost on the rank-deficient random-feature system
@@ -237,6 +237,8 @@ derivative engine:
237
237
  | `SinusoidalBasis` | Evaluates basis functions and arbitrary-order derivatives in O(1) via the cyclic identity |
238
238
  | `BasisCache` | Pre-computes sin(Z)/cos(Z) once, reuses across multiple derivative evaluations |
239
239
  | `DiffOperator` / `Op` | Symbolic linear differential operators that compose via +, -, scalar *; coefficients can be `nn.Parameter` for learnable PDEs |
240
+ | `IntegralOperator` / `IntegroDifferentialOperator` | Closed-form **single-axis** definite / running (Volterra) integrals; compose with `Op` into one integro-differential design matrix |
241
+ | `GaussianWindowedBasis` / `ProjectionOperator` | Windowed-Fourier (Gabor) basis + closed-form **projection (Radon)** operator `∫ f δ(c·z−u) dz` for tomographic / line-integral inverse problems; quadrature-free and differentiable in the optics `c` |
240
242
  | `FeatureBasis` | Adapter for non-sinusoidal solvers (e.g. PIELM with tanh) |
241
243
  | `FastLSQSolver` | Manages feature blocks; exposes `.basis` for all derivative computations |
242
244
  | `LearnableFastLSQ` | Differentiable solver with learnable bandwidth via reparameterisation trick |
@@ -326,6 +328,7 @@ See `examples/add_your_own_pde.py` for the complete tutorial.
326
328
 
327
329
  - **Analytical derivative engine**: `SinusoidalBasis` computes arbitrary-order derivatives exactly in O(1) -- the foundation of the entire framework
328
330
  - **Symbolic PDE operators**: Compose differential operators with `Op` (Laplacian, wave, Helmholtz, biharmonic, custom) via intuitive arithmetic; coefficients can be `nn.Parameter` for AdamW optimisation
331
+ - **Closed-form integral operators**: `IntegralOperator` (single-axis definite / Volterra integrals) composes with `Op` into one integro-differential least-squares block. The integral class now also includes the **projection (Radon) operator** (`ProjectionOperator` on a `GaussianWindowedBasis`) -- quadrature-free `∫ f δ(c·z−u) dz` line/hyperplane integrals for tomographic inverse problems, differentiable in the optics `c` for experiment design
329
332
  - **Vector-valued solutions**: First-class support for **u**: ℝᵈ → ℝᵏ (elasticity, Stokes, Maxwell). Problems declare `n_outputs = k`; `block_concat` assembles coupled block systems; `solver.predict(x)` returns shape `(M, k)`. Scalar problems are the `k=1` case
330
333
  - **High-level API**: Solve PDEs in one line with `solve_linear()` and `solve_nonlinear()`
331
334
  - **Robust linear solver**: Pluggable least-squares back-ends; the default `auto` routes Cholesky -> QR -> SVD, and backward-stable QR delivers SVD-grade accuracy at QR cost on the rank-deficient random-feature system
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 Antonin Sulc
3
+ # Licensed under the MIT License. See LICENSE file for details.
4
+
5
+ """
6
+ Integro-differential equation in one shot: exact closed-form *calculus* -> linear LSQ.
7
+
8
+ FastLSQ solves PDEs by exploiting that every *derivative* of a sinusoidal feature is
9
+ closed-form. The same identity runs backwards -- integration is differentiation of
10
+ negative order -- so *integral* terms are closed-form too. Differential and integral
11
+ terms then compose into a single linear-least-squares design matrix.
12
+
13
+ Here we solve the Volterra integro-differential boundary-value problem
14
+
15
+ u'(x) + ∫_0^x u(s) ds = f(x), u(0) = 0, x in [0, L],
16
+
17
+ whose exact solution is u*(x) = sin(w x), giving the (known) forcing
18
+
19
+ f(x) = w cos(w x) + (1 - cos(w x)) / w.
20
+
21
+ The operator is assembled as one object,
22
+
23
+ L = Op.partial(0, 1, d=1) + IntegralOperator.volterra(dim=0, lower=0.0, d=1),
24
+
25
+ stacked with the initial condition, and solved in a single ``solve_lstsq`` call -- no
26
+ time stepping, no quadrature, no autodiff. We then verify a global conservation
27
+ identity ∫_0^L u with ``IntegralOperator.definite``.
28
+
29
+ Usage: python integro_differential_demo.py
30
+ """
31
+
32
+ import numpy as np
33
+ import torch
34
+ import matplotlib
35
+ matplotlib.use("Agg")
36
+ import matplotlib.pyplot as plt
37
+
38
+ from fastlsq import SinusoidalBasis, Op, IntegralOperator, solve_lstsq
39
+ from fastlsq.geometry import sample_box
40
+
41
+
42
+ def main():
43
+ torch.set_default_dtype(torch.float64)
44
+ torch.manual_seed(0)
45
+
46
+ # ------------------------------------------------------------------
47
+ # Problem definition (analytic ground truth -> exact forcing)
48
+ # ------------------------------------------------------------------
49
+ w, L = 3.0, 1.0
50
+ u_star = lambda x: torch.sin(w * x) # exact solution
51
+ f_rhs = lambda x: w * torch.cos(w * x) + (1.0 - torch.cos(w * x)) / w # known forcing
52
+ u0 = 0.0 # initial condition
53
+
54
+ # ------------------------------------------------------------------
55
+ # Basis + collocation
56
+ # ------------------------------------------------------------------
57
+ basis = SinusoidalBasis.random(input_dim=1, n_features=800, sigma=5.0, normalize=False)
58
+ x_col = sample_box(4000, 1) * L # interior collocation points in [0, L]
59
+ x_ic = torch.zeros(1, 1) # x = 0 for the initial condition
60
+
61
+ # ------------------------------------------------------------------
62
+ # One unified integro-differential operator: u'(x) + ∫_0^x u(s) ds
63
+ # ------------------------------------------------------------------
64
+ op = Op.partial(0, 1, d=1) + IntegralOperator.volterra(dim=0, lower=0.0, d=1)
65
+ print("Operator:", op)
66
+
67
+ # Assemble [PDE rows ; weighted IC row] = [f ; weighted u0] and solve once.
68
+ W_IC = 100.0
69
+ A = torch.cat([op.apply(basis, x_col), W_IC * basis.evaluate(x_ic)])
70
+ b = torch.cat([f_rhs(x_col), W_IC * torch.full((1, 1), u0)])
71
+ beta = solve_lstsq(A, b, mu=1e-10)
72
+
73
+ # ------------------------------------------------------------------
74
+ # Report accuracy on held-out points
75
+ # ------------------------------------------------------------------
76
+ x_test = sample_box(3000, 1) * L
77
+ u_pred = basis.evaluate(x_test) @ beta
78
+ u_true = u_star(x_test)
79
+ val_err = (torch.norm(u_pred - u_true) / (torch.norm(u_true) + 1e-15)).item()
80
+
81
+ # Global conservation identity: ∫_0^L u (closed form, single functional row)
82
+ int_num = float(IntegralOperator.definite(0, 0.0, L, d=1).apply(basis, x_test[:1]) @ beta)
83
+ int_exact = float((1.0 - np.cos(w * L)) / w)
84
+
85
+ print(f" solution rel-L2 error : {val_err:.2e}")
86
+ print(f" ∫_0^L u (closed form) : {int_num:.6f} exact {int_exact:.6f}")
87
+
88
+ # ------------------------------------------------------------------
89
+ # Plot: solution vs exact, and pointwise residual
90
+ # ------------------------------------------------------------------
91
+ xs = torch.linspace(0, L, 400).reshape(-1, 1)
92
+ us = (basis.evaluate(xs) @ beta).squeeze().numpy()
93
+ ue = u_star(xs).squeeze().numpy()
94
+ xg = xs.squeeze().numpy()
95
+
96
+ fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(11, 4))
97
+ ax0.plot(xg, ue, "k-", lw=2.5, label="exact sin(w x)")
98
+ ax0.plot(xg, us, "C1--", lw=1.8, label="FastLSQ")
99
+ ax0.set_title("u'(x) + ∫₀ˣ u ds = f(x), u(0)=0")
100
+ ax0.set_xlabel("x"); ax0.set_ylabel("u(x)"); ax0.legend()
101
+ ax1.semilogy(xg, np.abs(us - ue) + 1e-18, "C3-")
102
+ ax1.set_title(f"pointwise |error| (rel-L2 = {val_err:.1e})")
103
+ ax1.set_xlabel("x"); ax1.set_ylabel("|u_pred - u*|")
104
+ plt.tight_layout()
105
+ out = "integro_differential_demo.pdf"
106
+ plt.savefig(out, dpi=150, bbox_inches="tight")
107
+ plt.close()
108
+ print(f" -> Saved: {out}")
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 Antonin Sulc
3
+ # Licensed under the MIT License. See LICENSE file for details.
4
+
5
+ """
6
+ Inverse problem: recover a memory-kernel strength from data through an INTEGRAL term.
7
+
8
+ Many physical systems have *memory*: the rate of change depends not only on the present
9
+ state but on its accumulated history. A minimal model is the Volterra integro-differential
10
+ equation
11
+
12
+ u'(x) + λ ∫_0^x u(s) ds = f(x), u(0) = 0,
13
+
14
+ where the scalar λ is the (unknown) memory / relaxation strength. Given the known forcing
15
+ f and noisy observations of u, we recover λ.
16
+
17
+ This mirrors ``examples/learnable_helmholtz.py`` (learnable operator coefficient + AdamW),
18
+ but the learnable coefficient now multiplies an ``IntegralOperator``: the operator is rebuilt
19
+ each forward() as
20
+
21
+ L(λ) = Op.partial(0, 1, d=1) + λ · IntegralOperator.volterra(dim=0, lower=0.0, d=1),
22
+
23
+ solved in one differentiable least-squares call, and gradients flow loss -> u(λ) -> β ->
24
+ lstsq -> A(λ) -> λ. The inner solve uses Tikhonov regularisation + the SVD-based
25
+ ``gelsd`` driver (as in ``fastlsq/learnable.py``); a plain ``lstsq`` on the rank-deficient
26
+ feature matrix yields an unreliable gradient.
27
+
28
+ A closed-form / SINDy alternative (fit u, then linearly regress u' against [u, ∫u, ...] in one
29
+ LSQ -- no optimiser) is shown in ``examples/inverse/denoising_parameter_estimation.py``.
30
+
31
+ Usage: python inverse_memory_kernel.py
32
+ """
33
+
34
+ import numpy as np
35
+ import torch
36
+ import torch.nn as nn
37
+ import matplotlib
38
+ matplotlib.use("Agg")
39
+ import matplotlib.pyplot as plt
40
+
41
+ from fastlsq import SinusoidalBasis, Op, IntegralOperator
42
+ from fastlsq.geometry import sample_box
43
+
44
+
45
+ class MemoryKernelInverse(nn.Module):
46
+ """Recover λ in u'(x) + λ ∫_0^x u ds = f(x), u(0)=0.
47
+
48
+ The operator is rebuilt each forward() so λ is always current. The inner solve is
49
+ differentiable and rank-revealing (Tikhonov + gelsd), so AdamW can descend in λ.
50
+ """
51
+
52
+ def __init__(self, n_features: int = 600, sigma: float = 5.0, init_lam: float = 1.0,
53
+ mu: float = 1e-8, rcond: float = 1e-12, w_ic: float = 100.0):
54
+ super().__init__()
55
+ self.basis = SinusoidalBasis.random(
56
+ input_dim=1, n_features=n_features, sigma=sigma, normalize=True
57
+ )
58
+ self.lam = nn.Parameter(torch.tensor(float(init_lam)))
59
+ self.mu, self.rcond, self.w_ic = mu, rcond, w_ic
60
+ self.beta: torch.Tensor | None = None
61
+
62
+ def solve(self, x_col, f_col, u0):
63
+ """Differentiable physics-constrained solve β(λ) for the current λ."""
64
+ op = Op.partial(0, 1, d=1) + self.lam * IntegralOperator.volterra(0, 0.0, d=1)
65
+ x_ic = torch.zeros(1, 1, dtype=x_col.dtype)
66
+ A = torch.cat([op.apply(self.basis, x_col), self.w_ic * self.basis.evaluate(x_ic)])
67
+ b = torch.cat([f_col, self.w_ic * torch.full((1, 1), float(u0))])
68
+ # Tikhonov-augmented, SVD-based solve -> stable gradient w.r.t. λ.
69
+ N = self.basis.n_features
70
+ A_aug = torch.cat([A, (self.mu ** 0.5) * torch.eye(N, dtype=A.dtype)])
71
+ b_aug = torch.cat([b, torch.zeros(N, 1, dtype=A.dtype)])
72
+ self.beta = torch.linalg.lstsq(A_aug, b_aug, rcond=self.rcond, driver="gelsd").solution
73
+ return self.beta
74
+
75
+ def predict(self, x):
76
+ return self.basis.evaluate(x) @ self.beta
77
+
78
+
79
+ def main():
80
+ torch.set_default_dtype(torch.float64)
81
+ torch.manual_seed(0)
82
+
83
+ # ------------------------------------------------------------------
84
+ # Synthetic data: u* = sin(w x); forcing f encodes the TRUE memory λ*
85
+ # ------------------------------------------------------------------
86
+ w, L = 3.0, 1.0
87
+ lam_true = 5.0
88
+ u_star = lambda x: torch.sin(w * x)
89
+ f_rhs = lambda x: w * torch.cos(w * x) + lam_true * (1.0 - torch.cos(w * x)) / w
90
+ u0 = 0.0
91
+
92
+ x_col = sample_box(3000, 1) * L # physics collocation points
93
+ f_col = f_rhs(x_col) # known forcing
94
+ x_obs = sample_box(300, 1) * L # where u is observed
95
+ noise = 1e-3
96
+ u_obs = u_star(x_obs) + noise * torch.randn_like(x_obs) # noisy observations
97
+
98
+ # ------------------------------------------------------------------
99
+ # Recover λ by gradient descent through the differentiable solve
100
+ # ------------------------------------------------------------------
101
+ model = MemoryKernelInverse(init_lam=1.0)
102
+ optimizer = torch.optim.AdamW(model.parameters(), lr=0.2)
103
+
104
+ print(f"Recovering memory strength λ (true λ = {lam_true})")
105
+ print("-" * 52)
106
+ lam_hist, loss_hist = [], []
107
+ for step in range(150):
108
+ optimizer.zero_grad()
109
+ model.solve(x_col, f_col, u0)
110
+ loss = torch.mean((model.predict(x_obs) - u_obs) ** 2)
111
+ loss.backward()
112
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 10.0)
113
+ optimizer.step()
114
+ lam_hist.append(model.lam.item()); loss_hist.append(loss.item())
115
+ if step % 25 == 0 or step == 149:
116
+ print(f" step {step:3d}: λ = {model.lam.item():.4f}, data-misfit = {loss.item():.2e}")
117
+
118
+ lam_hat = model.lam.item()
119
+ print("-" * 52)
120
+ print(f" recovered λ = {lam_hat:.4f} (true {lam_true}, |error| = {abs(lam_hat - lam_true):.2e})")
121
+ print(f" noise floor ~ {noise:.0e}; recovery is within the noise level")
122
+
123
+ # ------------------------------------------------------------------
124
+ # Plots: λ convergence & fitted solution vs observations
125
+ # ------------------------------------------------------------------
126
+ with torch.no_grad():
127
+ xs = torch.linspace(0, L, 400).reshape(-1, 1)
128
+ us = model.predict(xs).squeeze().numpy()
129
+ ue = u_star(xs).squeeze().numpy()
130
+
131
+ fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(11, 4))
132
+ ax0.axhline(lam_true, color="k", ls="--", lw=1.5, label=f"true λ = {lam_true}")
133
+ ax0.plot(lam_hist, "C0-", lw=1.8, label="AdamW iterate")
134
+ ax0.set_title("memory strength λ convergence")
135
+ ax0.set_xlabel("step"); ax0.set_ylabel("λ"); ax0.legend()
136
+
137
+ ax1.scatter(x_obs.squeeze().numpy(), u_obs.squeeze().numpy(), s=8, c="0.6",
138
+ label="noisy data", zorder=1)
139
+ ax1.plot(xs.squeeze().numpy(), ue, "k-", lw=2.2, label="true u*", zorder=2)
140
+ ax1.plot(xs.squeeze().numpy(), us, "C1--", lw=1.8, label="recovered u(λ̂)", zorder=3)
141
+ ax1.set_title(f"fit with recovered λ̂ = {lam_hat:.3f}")
142
+ ax1.set_xlabel("x"); ax1.set_ylabel("u(x)"); ax1.legend()
143
+ plt.tight_layout()
144
+ out = "inverse_memory_kernel.pdf"
145
+ plt.savefig(out, dpi=150, bbox_inches="tight")
146
+ plt.close()
147
+ print(f" -> Saved: {out}")
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 Antonin Sulc
3
+ # Licensed under the MIT License. See LICENSE file for details.
4
+
5
+ """
6
+ Inverse problem with the definite integral as a *measurement operator*: recover a source from
7
+ a bank of finite-aperture detectors.
8
+
9
+ Real sensors rarely sample a field pointwise -- a photodiode, a thermocouple bead, a gravimeter
10
+ all report the field *integrated over their aperture*. So the data are window integrals
11
+
12
+ d_i = ∫_{a_i}^{b_i} u(x) dx, i = 1 … N_det,
13
+
14
+ of a field u that is itself generated by an unknown source f through the steady diffusion law
15
+
16
+ -u''(x) = f(x), u(0) = u(1) = 0.
17
+
18
+ We recover both the field u and the source f = -u'' from the (noisy) integrated readings alone.
19
+ Each detector is one row of an ``IntegralOperator.definite`` design block -- the *exact* integral
20
+ of every Fourier feature over the detector window -- so the forward measurement operator is
21
+ assembled in closed form and the inverse is one regularised least-squares solve:
22
+
23
+ [ ∫_{a_i}^{b_i} φ ds ] [ d_i ]
24
+ [ w · φ(0) ] · β = [ 0 ] -> u(x)=φ(x)·β, f(x)=-φ''(x)·β.
25
+ [ w · φ(1) ] [ 0 ]
26
+
27
+ Recovering a function from finitely many integral functionals is ill-posed, so the rank-revealing
28
+ solver + a light Tikhonov ridge return the smoothest field consistent with the apertures and the
29
+ boundary conditions. The field u is recovered sharply; the source f = -u'' is harder (a second
30
+ derivative amplifies measurement noise), which the error plot makes honest.
31
+
32
+ Usage: python inverse_source_integral_sensors.py
33
+ """
34
+
35
+ import numpy as np
36
+ import torch
37
+ import matplotlib
38
+ matplotlib.use("Agg")
39
+ import matplotlib.pyplot as plt
40
+
41
+ from fastlsq import SinusoidalBasis, Op, IntegralOperator, solve_lstsq
42
+
43
+ PI = np.pi
44
+
45
+
46
+ def main():
47
+ torch.set_default_dtype(torch.float64)
48
+ torch.manual_seed(0)
49
+ rng = np.random.default_rng(0)
50
+
51
+ # ------------------------------------------------------------------
52
+ # Ground truth: -u'' = f, u(0)=u(1)=0, multi-scale source
53
+ # ------------------------------------------------------------------
54
+ amps = [(1, 1.0), (3, 0.6), (5, 0.3)] # (mode k, amplitude)
55
+ f_true = lambda x: sum(a * torch.sin(k * PI * x) for k, a in amps)
56
+ u_true = lambda x: sum(a / (k * PI) ** 2 * torch.sin(k * PI * x) for k, a in amps)
57
+
58
+ # ------------------------------------------------------------------
59
+ # Detector bank: contiguous windows tiling [0, 1]; reading = ∫ window u
60
+ # ------------------------------------------------------------------
61
+ n_det = 32
62
+ edges = torch.linspace(0.0, 1.0, n_det + 1)
63
+ basis = SinusoidalBasis.random(input_dim=1, n_features=400, sigma=5.0, normalize=False)
64
+
65
+ det_rows, d_clean = [], []
66
+ for i in range(n_det):
67
+ a_i, b_i = float(edges[i]), float(edges[i + 1])
68
+ # one detector = exact closed-form integral of every feature over [a_i, b_i]
69
+ det_rows.append(IntegralOperator.definite(0, a_i, b_i, d=1).apply(basis, torch.zeros(1, 1)))
70
+ # true reading via fine quadrature of the exact field
71
+ xs = torch.linspace(a_i, b_i, 400)
72
+ d_clean.append(float(torch.trapz(u_true(xs), xs)))
73
+ A_det = torch.cat(det_rows) # (n_det, N)
74
+ d_clean = torch.tensor(d_clean).reshape(-1, 1)
75
+
76
+ noise_pct = 0.005 # 0.5 % detector noise
77
+ d_meas = d_clean + noise_pct * torch.std(d_clean) * torch.from_numpy(
78
+ rng.normal(0.0, 1.0, d_clean.shape))
79
+
80
+ # ------------------------------------------------------------------
81
+ # Inverse solve: detectors + Dirichlet BCs, rank-revealing + light ridge
82
+ # ------------------------------------------------------------------
83
+ W_BC = 30.0
84
+ x_bc = torch.tensor([[0.0], [1.0]])
85
+ A = torch.cat([A_det, W_BC * basis.evaluate(x_bc)])
86
+ b = torch.cat([d_meas, torch.zeros(2, 1)])
87
+ beta = solve_lstsq(A, b, mu=1e-7)
88
+
89
+ # ------------------------------------------------------------------
90
+ # Recover field u and source f = -u'' ; report accuracy
91
+ # ------------------------------------------------------------------
92
+ xt = torch.linspace(0, 1, 400).reshape(-1, 1)
93
+ u_rec = (basis.evaluate(xt) @ beta).squeeze()
94
+ f_rec = (-Op.partial(0, 2, d=1).apply(basis, xt) @ beta).squeeze()
95
+ u_ref, f_ref = u_true(xt).squeeze(), f_true(xt).squeeze()
96
+ u_err = (torch.norm(u_rec - u_ref) / torch.norm(u_ref)).item()
97
+ f_err = (torch.norm(f_rec - f_ref) / torch.norm(f_ref)).item()
98
+ print(f"detectors: {n_det} windows, {noise_pct*100:.1f}% noise")
99
+ print(f" field u(x) rel-L2 : {u_err:.2e}")
100
+ print(f" source f(x)=-u'' rel-L2 : {f_err:.2e}")
101
+
102
+ # ------------------------------------------------------------------
103
+ # Plot: recovered source | recovered field + detector readings
104
+ # ------------------------------------------------------------------
105
+ xg = xt.squeeze().numpy()
106
+ fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(12, 4.2))
107
+ ax0.plot(xg, f_ref.numpy(), "k-", lw=2.5, label="true source f")
108
+ ax0.plot(xg, f_rec.numpy(), "C1--", lw=1.6, label="recovered -u''")
109
+ ax0.set_title(f"source f(x) (rel-L2 = {f_err:.1e})")
110
+ ax0.set_xlabel("x"); ax0.set_ylabel("f(x)"); ax0.legend()
111
+
112
+ ax1.plot(xg, u_ref.numpy(), "k-", lw=2.5, label="true field u")
113
+ ax1.plot(xg, u_rec.numpy(), "C1--", lw=1.6, label="recovered u")
114
+ centers = (0.5 * (edges[:-1] + edges[1:])).numpy()
115
+ widths = (edges[1:] - edges[:-1]).numpy()
116
+ ax1.bar(centers, (d_meas.squeeze() / torch.from_numpy(widths)).numpy(), width=widths * 0.9,
117
+ color="C0", alpha=0.25, label="detector readings (÷ width)")
118
+ ax1.set_title(f"field u(x) + integrating detectors (rel-L2 = {u_err:.1e})")
119
+ ax1.set_xlabel("x"); ax1.set_ylabel("u(x)"); ax1.legend(fontsize=8)
120
+
121
+ fig.suptitle("Inverse source from finite-aperture detectors: d_i = ∫ u -> recover u, f", y=1.02)
122
+ plt.tight_layout()
123
+ out = "inverse_source_integral_sensors.pdf"
124
+ plt.savefig(out, dpi=150, bbox_inches="tight")
125
+ plt.close()
126
+ print(f" -> Saved: {out}")
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()