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.
- {fastlsq-0.2.6 → fastlsq-0.3.0}/CHANGELOG.md +39 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/PKG-INFO +1 -1
- {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/SOURCES.txt +3 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/PKG-INFO +1 -1
- fastlsq-0.3.0/examples/integro_differential_demo.py +112 -0
- fastlsq-0.3.0/examples/inverse/inverse_memory_kernel.py +151 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/__init__.py +12 -2
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/basis.py +326 -11
- {fastlsq-0.2.6 → fastlsq-0.3.0}/pyproject.toml +1 -1
- fastlsq-0.3.0/tests/test_integral.py +195 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_vector_basis.py +1 -1
- {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/dependency_links.txt +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/requires.txt +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/FastLSQ.egg-info/top_level.txt +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/LICENSE +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/MANIFEST.in +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/README.md +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/add_your_own_pde.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/benchmark_comparison.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/custom_features.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/darcy_heat.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/pendulum.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/pendulum_benchmark.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield_2D_1.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield_2D_2.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield_2d_3.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plasma_wakefield_parameteric.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/plot_utils.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/structural_health_simple.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/digital_twins/turbulence_gravity_cooling.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/fred_sde.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/fred_sde_fastlsq.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/gaia_potential.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/gaia_potential_fastlsq.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/horizons_ephemeris.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/numerai_alpha.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/numerai_alpha_fastlsq.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/run_all_fastlsq.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/__init__.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/_alsu_lattice.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/_common.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/run_all.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_green_fff.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_passive_loco.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_tides_3months.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s01_visualize.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s04_sunspots.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s05_helioseismology.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s06_tides.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s14_eeg.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/scenarios/s15_circadian.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/extras/spectral_expansion.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/grad_shafranov.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/grid_inverse.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/grid_rl_control.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/grid_swing.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/gs_inverse.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/gs_rl_control.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/aero_.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/denoising_parameter_estimation.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/elastic_wave_animation.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/heat_from_video.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/inverse_turbulence.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/shape_ns.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/subsurface_imaging.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse/wing_optimize_simple.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse_heat_source.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse_magnetostatics.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/inverse_source_position.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/learnable_helmholtz.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/orbit_hill.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/orbit_inverse.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/orbit_rl.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/pde_discovery.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/run_all_extensions.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/run_linear.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/run_nonlinear.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/sindy/compare_sindy_methods.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/sindy/sindy_benchmarks.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/sindy/sindy_differentiable.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/sindy/sindy_minimal_diff.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/tutorial_basic.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/tutorial_nonlinear.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/examples/vector_basis_stream_vorticity.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/api.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/block.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/device.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/diagnostics.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/export.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/geometry.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/learnable.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/lightning.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/linalg.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/newton.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/plotting.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/problems/__init__.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/problems/linear.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/problems/nonlinear.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/problems/regression.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/solvers.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/tuning.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/utils.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/vector.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/fastlsq/viz.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/fastlsq_teaser.png +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/ideal_quadrupole.png +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/inverse_heat_source.gif +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/inverse_heat_source.png +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/inverse_magnetostatics.png +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/inverse_magnetostatics_convergence.png +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/quadrupole_convergence.png +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/quadrupole_optimization.png +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/tutorial_nlpoisson_convergence.png +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/misc/tutorial_nlpoisson_solution.png +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/requirements.txt +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/setup.cfg +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_basic.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_benchmarks_inverse.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_block.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_derivatives.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_device.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_grad_shafranov.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_grid_swing.py +0 -0
- {fastlsq-0.2.6 → fastlsq-0.3.0}/tests/test_learnable.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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.
|
|
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",
|