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