FastLSQ 0.2.6__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. {fastlsq-0.2.6 → fastlsq-0.3.0}/CHANGELOG.md +39 -0
  2. {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/PKG-INFO +1 -1
  3. {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/SOURCES.txt +3 -0
  4. {fastlsq-0.2.6 → fastlsq-0.3.0}/PKG-INFO +1 -1
  5. fastlsq-0.3.0/examples/integro_differential_demo.py +112 -0
  6. fastlsq-0.3.0/examples/inverse/inverse_memory_kernel.py +151 -0
  7. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/__init__.py +12 -2
  8. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/basis.py +326 -11
  9. {fastlsq-0.2.6 → fastlsq-0.3.0}/pyproject.toml +1 -1
  10. fastlsq-0.3.0/tests/test_integral.py +195 -0
  11. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_vector_basis.py +1 -1
  12. {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/dependency_links.txt +0 -0
  13. {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/requires.txt +0 -0
  14. {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/top_level.txt +0 -0
  15. {fastlsq-0.2.6 → fastlsq-0.3.0}/LICENSE +0 -0
  16. {fastlsq-0.2.6 → fastlsq-0.3.0}/MANIFEST.in +0 -0
  17. {fastlsq-0.2.6 → fastlsq-0.3.0}/README.md +0 -0
  18. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/add_your_own_pde.py +0 -0
  19. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/benchmark_comparison.py +0 -0
  20. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/custom_features.py +0 -0
  21. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/darcy_heat.py +0 -0
  22. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/pendulum.py +0 -0
  23. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/pendulum_benchmark.py +0 -0
  24. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield.py +0 -0
  25. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield_2D_1.py +0 -0
  26. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield_2D_2.py +0 -0
  27. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield_2d_3.py +0 -0
  28. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield_parameteric.py +0 -0
  29. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plot_utils.py +0 -0
  30. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/structural_health_simple.py +0 -0
  31. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/turbulence_gravity_cooling.py +0 -0
  32. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/fred_sde.py +0 -0
  33. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/fred_sde_fastlsq.py +0 -0
  34. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/gaia_potential.py +0 -0
  35. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/gaia_potential_fastlsq.py +0 -0
  36. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/horizons_ephemeris.py +0 -0
  37. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/numerai_alpha.py +0 -0
  38. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/numerai_alpha_fastlsq.py +0 -0
  39. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/run_all_fastlsq.py +0 -0
  40. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/__init__.py +0 -0
  41. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/_alsu_lattice.py +0 -0
  42. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/_common.py +0 -0
  43. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/run_all.py +0 -0
  44. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
  45. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
  46. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_green_fff.py +0 -0
  47. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
  48. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
  49. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
  50. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_passive_loco.py +0 -0
  51. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
  52. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
  53. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
  54. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
  55. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_tides_3months.py +0 -0
  56. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
  57. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_visualize.py +0 -0
  58. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
  59. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
  60. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s04_sunspots.py +0 -0
  61. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s05_helioseismology.py +0 -0
  62. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s06_tides.py +0 -0
  63. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
  64. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
  65. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
  66. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
  67. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
  68. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
  69. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
  70. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s14_eeg.py +0 -0
  71. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s15_circadian.py +0 -0
  72. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/spectral_expansion.py +0 -0
  73. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/grad_shafranov.py +0 -0
  74. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/grid_inverse.py +0 -0
  75. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/grid_rl_control.py +0 -0
  76. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/grid_swing.py +0 -0
  77. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/gs_inverse.py +0 -0
  78. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/gs_rl_control.py +0 -0
  79. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/aero_.py +0 -0
  80. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/denoising_parameter_estimation.py +0 -0
  81. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/elastic_wave_animation.py +0 -0
  82. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/heat_from_video.py +0 -0
  83. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/inverse_turbulence.py +0 -0
  84. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/shape_ns.py +0 -0
  85. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/subsurface_imaging.py +0 -0
  86. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/wing_optimize_simple.py +0 -0
  87. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse_heat_source.py +0 -0
  88. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse_magnetostatics.py +0 -0
  89. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse_source_position.py +0 -0
  90. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/learnable_helmholtz.py +0 -0
  91. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/orbit_hill.py +0 -0
  92. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/orbit_inverse.py +0 -0
  93. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/orbit_rl.py +0 -0
  94. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/pde_discovery.py +0 -0
  95. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/run_all_extensions.py +0 -0
  96. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/run_linear.py +0 -0
  97. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/run_nonlinear.py +0 -0
  98. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/sindy/compare_sindy_methods.py +0 -0
  99. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/sindy/sindy_benchmarks.py +0 -0
  100. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/sindy/sindy_differentiable.py +0 -0
  101. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/sindy/sindy_minimal_diff.py +0 -0
  102. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/tutorial_basic.py +0 -0
  103. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/tutorial_nonlinear.py +0 -0
  104. {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/vector_basis_stream_vorticity.py +0 -0
  105. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/api.py +0 -0
  106. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/block.py +0 -0
  107. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/device.py +0 -0
  108. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/diagnostics.py +0 -0
  109. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/export.py +0 -0
  110. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/geometry.py +0 -0
  111. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/learnable.py +0 -0
  112. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/lightning.py +0 -0
  113. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/linalg.py +0 -0
  114. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/newton.py +0 -0
  115. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/plotting.py +0 -0
  116. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/problems/__init__.py +0 -0
  117. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/problems/linear.py +0 -0
  118. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/problems/nonlinear.py +0 -0
  119. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/problems/regression.py +0 -0
  120. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/solvers.py +0 -0
  121. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/tuning.py +0 -0
  122. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/utils.py +0 -0
  123. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/vector.py +0 -0
  124. {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/viz.py +0 -0
  125. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/fastlsq_teaser.png +0 -0
  126. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/ideal_quadrupole.png +0 -0
  127. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/inverse_heat_source.gif +0 -0
  128. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/inverse_heat_source.png +0 -0
  129. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/inverse_magnetostatics.png +0 -0
  130. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/inverse_magnetostatics_convergence.png +0 -0
  131. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/quadrupole_convergence.png +0 -0
  132. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/quadrupole_optimization.png +0 -0
  133. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/tutorial_nlpoisson_convergence.png +0 -0
  134. {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/tutorial_nlpoisson_solution.png +0 -0
  135. {fastlsq-0.2.6 → fastlsq-0.3.0}/requirements.txt +0 -0
  136. {fastlsq-0.2.6 → fastlsq-0.3.0}/setup.cfg +0 -0
  137. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_basic.py +0 -0
  138. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_benchmarks_inverse.py +0 -0
  139. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_block.py +0 -0
  140. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_derivatives.py +0 -0
  141. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_device.py +0 -0
  142. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_grad_shafranov.py +0 -0
  143. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_grid_swing.py +0 -0
  144. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_learnable.py +0 -0
  145. {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_orbit_hill.py +0 -0
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to FastLSQ will be documented in this file.
4
4
 
5
+ ## [0.3.0] - 2026-06-21
6
+
7
+ ### Added
8
+
9
+ - **Exact closed-form integral & integro-differential operators.** The cyclic
10
+ derivative identity `D^α sin(W·x+b) = (∏ W^{α_k}) Φ_{|α| mod 4}` runs *backwards* --
11
+ integration is differentiation of *negative* order, `∫ sin(wx+b) dx = −(1/w)cos(wx+b)`.
12
+ `SinusoidalBasis.derivative` now accepts **signed** multi-indices (negative entries =
13
+ indefinite integration), with a DC guard (`dc_eps`) that zeros features whose frequency
14
+ along an integrated axis is ~0 (their primitive is a ramp that leaves the sinusoidal
15
+ family). `DiffOperator.antiderivative(dim, order, d)` exposes this as a factory, so
16
+ `Op.partial(0,1,1) - k*Op.antiderivative(0,1,1)` composes integro-differential operators.
17
+ - **`IntegralOperator`** -- definite and running (Volterra) integrals with limits,
18
+ via the new `SinusoidalBasis.definite_integral`. Evaluated with a numerically stable
19
+ `sinc` identity (no `1/w` division), so the running integral of a near-DC feature is
20
+ exact rather than singular. Factories `IntegralOperator.volterra(...)` /
21
+ `IntegralOperator.definite(...)`.
22
+ - **`IntegroDifferentialOperator`** -- the common roof under which differential and
23
+ integral terms compose (`+`, `−`, scalar/`nn.Parameter` `*`) into one `(M, N)`
24
+ linear-least-squares design matrix; coefficients stay differentiable so learnable
25
+ integral-term coefficients train through the solve. All three exported from `fastlsq`.
26
+ - **Examples** `examples/integro_differential_demo.py` (one-shot forward solve of
27
+ `u'(x) + ∫_0^x u ds = f`, rel-L2 ~3e-11) and
28
+ `examples/inverse/inverse_memory_kernel.py` (recover an unknown memory strength λ from
29
+ noisy data via AdamW through the differentiable solve). New `tests/test_integral.py`
30
+ asserts ∫-then-∂ round-trips to identity, Volterra/definite match quadrature, the DC
31
+ guard stays finite, and gradients flow to learnable coefficients.
32
+
33
+ ## [0.2.6] - 2026-06-09
34
+
35
+ ### Changed
36
+
37
+ - **Faster `_auto_solve` fallback for ill-conditioned systems.** When the
38
+ Cholesky path fails on CPU with no ridge (`mu = 0`), `_auto_solve` now goes
39
+ straight to the rank-deficient-safe SVD solve (LAPACK `gelsd`) instead of
40
+ first attempting Householder QR with the blow-up guard -- on CPU `gelsd` is
41
+ faster than QR, so the detour only added a full extra factorization. The
42
+ QR-then-SVD path is unchanged for ridge solves and non-CPU devices.
43
+
5
44
  ## [0.2.5] - 2026-06-04
6
45
 
7
46
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FastLSQ
3
- Version: 0.2.6
3
+ Version: 0.3.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
@@ -18,6 +18,7 @@ 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
@@ -88,6 +89,7 @@ examples/inverse/aero_.py
88
89
  examples/inverse/denoising_parameter_estimation.py
89
90
  examples/inverse/elastic_wave_animation.py
90
91
  examples/inverse/heat_from_video.py
92
+ examples/inverse/inverse_memory_kernel.py
91
93
  examples/inverse/inverse_turbulence.py
92
94
  examples/inverse/shape_ns.py
93
95
  examples/inverse/subsurface_imaging.py
@@ -135,6 +137,7 @@ tests/test_derivatives.py
135
137
  tests/test_device.py
136
138
  tests/test_grad_shafranov.py
137
139
  tests/test_grid_swing.py
140
+ tests/test_integral.py
138
141
  tests/test_learnable.py
139
142
  tests/test_orbit_hill.py
140
143
  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.3.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
@@ -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()
@@ -11,7 +11,15 @@ Reference:
11
11
  """
12
12
 
13
13
  from fastlsq.device import resolve_device, set_device, get_device, device_info
14
- from fastlsq.basis import SinusoidalBasis, BasisCache, DiffOperator, Op, FeatureBasis
14
+ from fastlsq.basis import (
15
+ SinusoidalBasis,
16
+ BasisCache,
17
+ DiffOperator,
18
+ Op,
19
+ IntegralOperator,
20
+ IntegroDifferentialOperator,
21
+ FeatureBasis,
22
+ )
15
23
  from fastlsq.solvers import FastLSQSolver, PIELMSolver
16
24
  from fastlsq.vector import VectorBasis, VectorFastLSQSolver
17
25
  from fastlsq.linalg import solve_lstsq
@@ -44,7 +52,7 @@ from fastlsq.export import (
44
52
  )
45
53
  from fastlsq import viz
46
54
 
47
- __version__ = "0.2.5"
55
+ __version__ = "0.3.0"
48
56
  __all__ = [
49
57
  # Device selection (CPU / CUDA / Apple-MPS, dtype-aware)
50
58
  "resolve_device",
@@ -56,6 +64,8 @@ __all__ = [
56
64
  "BasisCache",
57
65
  "DiffOperator",
58
66
  "Op",
67
+ "IntegralOperator",
68
+ "IntegroDifferentialOperator",
59
69
  "FeatureBasis",
60
70
  # Vector-valued basis (0.1.5)
61
71
  "VectorBasis",