FastLSQ 0.4.1__tar.gz → 0.4.2__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.4.1 → fastlsq-0.4.2}/CHANGELOG.md +36 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/FastLSQ.egg-info/PKG-INFO +2 -2
- {fastlsq-0.4.1 → fastlsq-0.4.2}/FastLSQ.egg-info/SOURCES.txt +0 -37
- {fastlsq-0.4.1 → fastlsq-0.4.2}/PKG-INFO +2 -2
- {fastlsq-0.4.1 → fastlsq-0.4.2}/README.md +1 -1
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/__init__.py +1 -1
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/basis.py +124 -15
- {fastlsq-0.4.1 → fastlsq-0.4.2}/pyproject.toml +1 -1
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_integral.py +133 -0
- fastlsq-0.4.1/examples/digital_twins/darcy_heat.py +0 -583
- fastlsq-0.4.1/examples/digital_twins/pendulum.py +0 -242
- fastlsq-0.4.1/examples/digital_twins/pendulum_benchmark.py +0 -281
- fastlsq-0.4.1/examples/digital_twins/plasma_wakefield.py +0 -339
- fastlsq-0.4.1/examples/digital_twins/plasma_wakefield_2D_1.py +0 -863
- fastlsq-0.4.1/examples/digital_twins/plasma_wakefield_2D_2.py +0 -769
- fastlsq-0.4.1/examples/digital_twins/plasma_wakefield_2d_3.py +0 -818
- fastlsq-0.4.1/examples/digital_twins/plasma_wakefield_parameteric.py +0 -475
- fastlsq-0.4.1/examples/digital_twins/plot_utils.py +0 -42
- fastlsq-0.4.1/examples/digital_twins/structural_health_simple.py +0 -343
- fastlsq-0.4.1/examples/digital_twins/turbulence_gravity_cooling.py +0 -387
- fastlsq-0.4.1/examples/inverse/aero_.py +0 -362
- fastlsq-0.4.1/examples/inverse/denoising_parameter_estimation.py +0 -297
- fastlsq-0.4.1/examples/inverse/elastic_wave_animation.py +0 -300
- fastlsq-0.4.1/examples/inverse/heat_from_video.py +0 -394
- fastlsq-0.4.1/examples/inverse/inverse_memory_kernel.py +0 -151
- fastlsq-0.4.1/examples/inverse/inverse_source_integral_sensors.py +0 -130
- fastlsq-0.4.1/examples/inverse/inverse_turbulence.py +0 -811
- fastlsq-0.4.1/examples/inverse/rlc_system_id.py +0 -159
- fastlsq-0.4.1/examples/inverse/shape_ns.py +0 -1426
- fastlsq-0.4.1/examples/inverse/subsurface_imaging.py +0 -547
- fastlsq-0.4.1/examples/inverse/tomography_projection.py +0 -145
- fastlsq-0.4.1/examples/inverse/wing_optimize_simple.py +0 -283
- fastlsq-0.4.1/examples/sindy/compare_sindy_methods.py +0 -760
- fastlsq-0.4.1/examples/sindy/sindy_benchmarks.py +0 -351
- fastlsq-0.4.1/examples/sindy/sindy_differentiable.py +0 -418
- fastlsq-0.4.1/examples/sindy/sindy_minimal_diff.py +0 -716
- fastlsq-0.4.1/misc/fastlsq_teaser.png +0 -0
- fastlsq-0.4.1/misc/ideal_quadrupole.png +0 -0
- fastlsq-0.4.1/misc/inverse_heat_source.gif +0 -0
- fastlsq-0.4.1/misc/inverse_heat_source.png +0 -0
- fastlsq-0.4.1/misc/inverse_magnetostatics.png +0 -0
- fastlsq-0.4.1/misc/inverse_magnetostatics_convergence.png +0 -0
- fastlsq-0.4.1/misc/quadrupole_convergence.png +0 -0
- fastlsq-0.4.1/misc/quadrupole_optimization.png +0 -0
- fastlsq-0.4.1/misc/tutorial_nlpoisson_convergence.png +0 -0
- fastlsq-0.4.1/misc/tutorial_nlpoisson_solution.png +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/FastLSQ.egg-info/dependency_links.txt +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/FastLSQ.egg-info/requires.txt +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/FastLSQ.egg-info/top_level.txt +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/LICENSE +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/MANIFEST.in +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/add_your_own_pde.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/benchmark_comparison.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/custom_features.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/fred_sde.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/fred_sde_fastlsq.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/gaia_potential.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/gaia_potential_fastlsq.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/horizons_ephemeris.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/numerai_alpha.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/numerai_alpha_fastlsq.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/run_all_fastlsq.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/__init__.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/_alsu_lattice.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/_common.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/run_all.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_green_fff.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_passive_loco.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_tides_3months.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s01_visualize.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s04_sunspots.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s05_helioseismology.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s06_tides.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s14_eeg.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/scenarios/s15_circadian.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/extras/spectral_expansion.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/grad_shafranov.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/grid_inverse.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/grid_rl_control.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/grid_swing.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/gs_inverse.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/gs_rl_control.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/integro_differential_demo.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/inverse_heat_source.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/inverse_magnetostatics.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/inverse_source_position.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/learnable_helmholtz.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/memory_diffusion.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/orbit_hill.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/orbit_inverse.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/orbit_rl.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/pde_discovery.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/rlc_integro_differential.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/run_all_extensions.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/run_linear.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/run_nonlinear.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/tutorial_basic.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/tutorial_nonlinear.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/examples/vector_basis_stream_vorticity.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/api.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/benchmark.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/block.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/device.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/diagnostics.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/export.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/geometry.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/learnable.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/lightning.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/linalg.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/newton.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/plotting.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/problems/__init__.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/problems/linear.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/problems/nonlinear.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/problems/regression.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/solvers.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/tuning.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/utils.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/vector.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/fastlsq/viz.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/requirements.txt +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/setup.cfg +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_basic.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_benchmarks_inverse.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_block.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_derivatives.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_device.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_diagnostics_timing.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_grad_shafranov.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_grid_swing.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_learnable.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_orbit_hill.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_projection.py +0 -0
- {fastlsq-0.4.1 → fastlsq-0.4.2}/tests/test_vector_basis.py +0 -0
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to FastLSQ will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.2] - 2026-07-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`IntegralOperator` with `order >= 2` returned wrong values.** The higher-order
|
|
10
|
+
branch evaluated `F_n(hi) − F_n(lo)`, the difference of n-th antiderivatives, which
|
|
11
|
+
is *not* the n-fold iterated (Volterra) integral: it drops the polynomial terms
|
|
12
|
+
`Σ_{j=1}^{n-1} F_j(lo)·Δ^{n−j}/(n−j)!`. For `order=2` on `[0,1]` this was ~380%
|
|
13
|
+
off against a double cumulative-trapezoid reference. `order=1` was always correct
|
|
14
|
+
and is unchanged. The higher-order path now goes through the new
|
|
15
|
+
`SinusoidalBasis.iterated_integral(..., order=n)`, which implements the Cauchy
|
|
16
|
+
repeated-integration formula `∫_lo^x (x−t)^{n−1}/(n−1)! φ(t) dt` and agrees with
|
|
17
|
+
n-fold quadrature to ~1e-9 (grid-limited) and with 60-digit `mpmath` to ~1e-15.
|
|
18
|
+
- **Near-DC features are now consistent across orders.** The `order >= 2` path went
|
|
19
|
+
through the DC-guarded `derivative`, which *zeroes* any feature with
|
|
20
|
+
`|W_dim| <= dc_eps`, while `order=1` handled it exactly via the `sinc` identity. A
|
|
21
|
+
near-DC feature is constant along the integrated axis, so its iterated integral is
|
|
22
|
+
the finite ramp `φ·Δ^n/n!` — the limit-bearing integral does not diverge, unlike the
|
|
23
|
+
standalone `1/W` antiderivative that `dc_eps` exists to guard. `iterated_integral`
|
|
24
|
+
therefore applies **no DC guard** and is exact as `W_dim → 0` at every order, while
|
|
25
|
+
`derivative` with negative `alpha` keeps its guard because that primitive genuinely
|
|
26
|
+
blows up. Documented in both docstrings.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`SinusoidalBasis.iterated_integral(x, dim, lower, upper=None, order=n)`** — the
|
|
31
|
+
n-fold iterated integral in closed form. Evaluated by a cancellation-free Taylor-tail
|
|
32
|
+
series `Δ^n Σ_p θ^p/(n+p)!·Φ_p(Z_lo)` for small `|θ| = |W_dim·Δ|` and by the
|
|
33
|
+
equivalent antiderivative form above it, since the latter's rounding error grows like
|
|
34
|
+
`n!/θ^n`. Holds ~1e-15 (relative to `Δ^n/n!`) for `n ≤ 10`, ~4e-14 by `n = 12`.
|
|
35
|
+
- **`order >= 2` test coverage** in `tests/test_integral.py` (previously every test used
|
|
36
|
+
the default `order=1`): n-fold cumulative-trapezoid and Cauchy-kernel references for
|
|
37
|
+
Volterra and definite integrals, `order=1` agreement between the two code paths,
|
|
38
|
+
near-DC exactness at every order, and an explicit guard that the result is *not* the
|
|
39
|
+
naive difference of n-th antiderivatives.
|
|
40
|
+
|
|
5
41
|
## [0.4.1] - 2026-06-23
|
|
6
42
|
|
|
7
43
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastLSQ
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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,7 +278,7 @@ 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
|
|
281
|
+
| `IntegralOperator` / `IntegroDifferentialOperator` | Closed-form **single-axis** definite / running (Volterra) integrals, including `order=n` **iterated** integrals `∫_lo^x (x−t)^{n−1}/(n−1)! φ dt`; compose with `Op` into one integro-differential design matrix |
|
|
282
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` |
|
|
283
283
|
| `FeatureBasis` | Adapter for non-sinusoidal solvers (e.g. PIELM with tanh) |
|
|
284
284
|
| `FastLSQSolver` | Manages feature blocks; exposes `.basis` for all derivative computations |
|
|
@@ -35,17 +35,6 @@ examples/run_nonlinear.py
|
|
|
35
35
|
examples/tutorial_basic.py
|
|
36
36
|
examples/tutorial_nonlinear.py
|
|
37
37
|
examples/vector_basis_stream_vorticity.py
|
|
38
|
-
examples/digital_twins/darcy_heat.py
|
|
39
|
-
examples/digital_twins/pendulum.py
|
|
40
|
-
examples/digital_twins/pendulum_benchmark.py
|
|
41
|
-
examples/digital_twins/plasma_wakefield.py
|
|
42
|
-
examples/digital_twins/plasma_wakefield_2D_1.py
|
|
43
|
-
examples/digital_twins/plasma_wakefield_2D_2.py
|
|
44
|
-
examples/digital_twins/plasma_wakefield_2d_3.py
|
|
45
|
-
examples/digital_twins/plasma_wakefield_parameteric.py
|
|
46
|
-
examples/digital_twins/plot_utils.py
|
|
47
|
-
examples/digital_twins/structural_health_simple.py
|
|
48
|
-
examples/digital_twins/turbulence_gravity_cooling.py
|
|
49
38
|
examples/extras/fred_sde.py
|
|
50
39
|
examples/extras/fred_sde_fastlsq.py
|
|
51
40
|
examples/extras/gaia_potential.py
|
|
@@ -87,22 +76,6 @@ examples/extras/scenarios/s12_mems_resonator.py
|
|
|
87
76
|
examples/extras/scenarios/s13_variable_stars_kepler.py
|
|
88
77
|
examples/extras/scenarios/s14_eeg.py
|
|
89
78
|
examples/extras/scenarios/s15_circadian.py
|
|
90
|
-
examples/inverse/aero_.py
|
|
91
|
-
examples/inverse/denoising_parameter_estimation.py
|
|
92
|
-
examples/inverse/elastic_wave_animation.py
|
|
93
|
-
examples/inverse/heat_from_video.py
|
|
94
|
-
examples/inverse/inverse_memory_kernel.py
|
|
95
|
-
examples/inverse/inverse_source_integral_sensors.py
|
|
96
|
-
examples/inverse/inverse_turbulence.py
|
|
97
|
-
examples/inverse/rlc_system_id.py
|
|
98
|
-
examples/inverse/shape_ns.py
|
|
99
|
-
examples/inverse/subsurface_imaging.py
|
|
100
|
-
examples/inverse/tomography_projection.py
|
|
101
|
-
examples/inverse/wing_optimize_simple.py
|
|
102
|
-
examples/sindy/compare_sindy_methods.py
|
|
103
|
-
examples/sindy/sindy_benchmarks.py
|
|
104
|
-
examples/sindy/sindy_differentiable.py
|
|
105
|
-
examples/sindy/sindy_minimal_diff.py
|
|
106
79
|
fastlsq/__init__.py
|
|
107
80
|
fastlsq/api.py
|
|
108
81
|
fastlsq/basis.py
|
|
@@ -126,16 +99,6 @@ fastlsq/problems/__init__.py
|
|
|
126
99
|
fastlsq/problems/linear.py
|
|
127
100
|
fastlsq/problems/nonlinear.py
|
|
128
101
|
fastlsq/problems/regression.py
|
|
129
|
-
misc/fastlsq_teaser.png
|
|
130
|
-
misc/ideal_quadrupole.png
|
|
131
|
-
misc/inverse_heat_source.gif
|
|
132
|
-
misc/inverse_heat_source.png
|
|
133
|
-
misc/inverse_magnetostatics.png
|
|
134
|
-
misc/inverse_magnetostatics_convergence.png
|
|
135
|
-
misc/quadrupole_convergence.png
|
|
136
|
-
misc/quadrupole_optimization.png
|
|
137
|
-
misc/tutorial_nlpoisson_convergence.png
|
|
138
|
-
misc/tutorial_nlpoisson_solution.png
|
|
139
102
|
tests/test_basic.py
|
|
140
103
|
tests/test_benchmarks_inverse.py
|
|
141
104
|
tests/test_block.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastLSQ
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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,7 +278,7 @@ 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
|
|
281
|
+
| `IntegralOperator` / `IntegroDifferentialOperator` | Closed-form **single-axis** definite / running (Volterra) integrals, including `order=n` **iterated** integrals `∫_lo^x (x−t)^{n−1}/(n−1)! φ dt`; compose with `Op` into one integro-differential design matrix |
|
|
282
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` |
|
|
283
283
|
| `FeatureBasis` | Adapter for non-sinusoidal solvers (e.g. PIELM with tanh) |
|
|
284
284
|
| `FastLSQSolver` | Manages feature blocks; exposes `.basis` for all derivative computations |
|
|
@@ -237,7 +237,7 @@ 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
|
|
240
|
+
| `IntegralOperator` / `IntegroDifferentialOperator` | Closed-form **single-axis** definite / running (Volterra) integrals, including `order=n` **iterated** integrals `∫_lo^x (x−t)^{n−1}/(n−1)! φ dt`; compose with `Op` into one integro-differential design matrix |
|
|
241
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` |
|
|
242
242
|
| `FeatureBasis` | Adapter for non-sinusoidal solvers (e.g. PIELM with tanh) |
|
|
243
243
|
| `FastLSQSolver` | Manages feature blocks; exposes `.basis` for all derivative computations |
|
|
@@ -61,6 +61,8 @@ Example
|
|
|
61
61
|
|
|
62
62
|
from __future__ import annotations
|
|
63
63
|
|
|
64
|
+
import math
|
|
65
|
+
|
|
64
66
|
import torch
|
|
65
67
|
import numpy as np
|
|
66
68
|
from typing import Optional, Sequence, Union
|
|
@@ -141,8 +143,22 @@ class SinusoidalBasis:
|
|
|
141
143
|
sinusoidal antiderivative (its primitive is a ramp that leaves the basis),
|
|
142
144
|
so its column is zeroed instead of dividing by ~0. Only affects negative
|
|
143
145
|
orders; ordinary derivatives are untouched.
|
|
146
|
+
|
|
147
|
+
It also does **not** affect the limit-bearing integrals
|
|
148
|
+
(:meth:`definite_integral`, :meth:`iterated_integral`): over a finite interval
|
|
149
|
+
the ramp is perfectly well defined -- a DC feature integrates to ``φ·Δ^n/n!``
|
|
150
|
+
-- so those are computed exactly, with no guard, at every order. Only the
|
|
151
|
+
*standalone* primitive genuinely diverges as ``1/W``, and only it is guarded.
|
|
144
152
|
"""
|
|
145
153
|
|
|
154
|
+
# Branch point for :meth:`iterated_integral`. Below |θ| = W_dim·Δ the closed form
|
|
155
|
+
# suffers catastrophic cancellation (its rounding error grows like n!/θ^n), so the
|
|
156
|
+
# equivalent Taylor-tail series is used; above it the series would need many more
|
|
157
|
+
# terms. 40 terms hold both branches to ~1e-15 (relative to Δ^n/n!) for n ≤ 10,
|
|
158
|
+
# degrading gracefully to ~4e-14 by n = 12; verified against 60-digit mpmath.
|
|
159
|
+
_ITER_THETA_MAX = 4.0
|
|
160
|
+
_ITER_TERMS = 40
|
|
161
|
+
|
|
146
162
|
def __init__(
|
|
147
163
|
self,
|
|
148
164
|
W: torch.Tensor,
|
|
@@ -358,6 +374,99 @@ class SinusoidalBasis:
|
|
|
358
374
|
out = delta * torch.sin(half_sum) * torch.sinc(sinc_arg)
|
|
359
375
|
return out * self._inv_norm
|
|
360
376
|
|
|
377
|
+
def iterated_integral(
|
|
378
|
+
self,
|
|
379
|
+
x: torch.Tensor,
|
|
380
|
+
dim: int,
|
|
381
|
+
lower: float,
|
|
382
|
+
upper: Optional[float] = None,
|
|
383
|
+
order: int = 1,
|
|
384
|
+
cache: Optional[BasisCache] = None,
|
|
385
|
+
) -> torch.Tensor:
|
|
386
|
+
"""``order``-fold *iterated* (Cauchy) integral along one axis, shape (M, N).
|
|
387
|
+
|
|
388
|
+
This is the repeated integral with all ``order`` lower limits pinned at
|
|
389
|
+
``lower`` -- the operator a Volterra integral equation actually applies --
|
|
390
|
+
|
|
391
|
+
(I^n φ)(x) = ∫_lo^x ∫_lo^{s_1} … ∫_lo^{s_{n-1}} φ dt … ds_1
|
|
392
|
+
= ∫_lo^x (x − t)^{n−1}/(n−1)! · φ(t) dt,
|
|
393
|
+
|
|
394
|
+
**not** the difference of n-th antiderivatives ``F_n(hi) − F_n(lo)``, which
|
|
395
|
+
omits the polynomial terms ``Σ_{j=1}^{n-1} F_j(lo)·Δ^{n−j}/(n−j)!``.
|
|
396
|
+
|
|
397
|
+
Evaluated by a *cancellation-free* series in the small-``|W·Δ|`` regime and by
|
|
398
|
+
the equivalent closed form otherwise. Writing Δ = hi − lo, θ = W_dim·Δ and
|
|
399
|
+
Φ_k(Z) = sin(Z + kπ/2), the two branches are
|
|
400
|
+
|
|
401
|
+
θ small: (I^n φ)(x) = Δ^n · Σ_{p≥0} θ^p/(n+p)! · Φ_p(Z_lo)
|
|
402
|
+
θ large: (I^n φ)(x) = F_n(Z_hi) − Σ_{j=1}^{n} F_j(Z_lo)·Δ^{n−j}/(n−j)!,
|
|
403
|
+
with F_j(Z) = Φ_{−j}(Z) / W_dim^j.
|
|
404
|
+
|
|
405
|
+
The series is the exact Taylor tail left over after that cancellation, so it is
|
|
406
|
+
finite and exact as ``W_dim → 0`` -- **no DC guard is applied or needed here**.
|
|
407
|
+
A near-DC feature is constant along ``dim`` and correctly integrates to
|
|
408
|
+
``φ · Δ^n/n!``, matching the ``order=1`` sinc identity in
|
|
409
|
+
:meth:`definite_integral`. (This is deliberately unlike :meth:`derivative`
|
|
410
|
+
with negative ``alpha``, whose standalone antiderivative genuinely diverges as
|
|
411
|
+
``1/W`` and so is DC-guarded to zero.)
|
|
412
|
+
|
|
413
|
+
``upper=None`` gives the running (Volterra) integral with variable upper limit
|
|
414
|
+
``x[:, dim]``; a numeric ``upper`` pins it. ``order=1`` reproduces
|
|
415
|
+
:meth:`definite_integral` to machine precision.
|
|
416
|
+
"""
|
|
417
|
+
if order < 1:
|
|
418
|
+
raise ValueError("integration order must be a positive integer")
|
|
419
|
+
if x.dtype != self.W.dtype or x.device != self.W.device:
|
|
420
|
+
x = x.to(dtype=self.W.dtype, device=self.W.device)
|
|
421
|
+
|
|
422
|
+
n = int(order)
|
|
423
|
+
Wd = self.W[dim : dim + 1, :] # (1, N)
|
|
424
|
+
|
|
425
|
+
x_lo = x.clone()
|
|
426
|
+
x_lo[:, dim] = lower
|
|
427
|
+
cache_lo = self.cache(x_lo) # phases at the (common) lower limit
|
|
428
|
+
|
|
429
|
+
if upper is None: # Volterra: upper limit is x_dim itself
|
|
430
|
+
cache_hi = cache if cache is not None else self.cache(x)
|
|
431
|
+
t_hi = x[:, dim : dim + 1] # (M, 1)
|
|
432
|
+
else:
|
|
433
|
+
x_hi = x.clone()
|
|
434
|
+
x_hi[:, dim] = upper
|
|
435
|
+
cache_hi = self.cache(x_hi)
|
|
436
|
+
t_hi = torch.full(
|
|
437
|
+
(x.shape[0], 1), float(upper), device=self.W.device, dtype=self.W.dtype
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
delta = t_hi - float(lower) # (M, 1)
|
|
441
|
+
theta = Wd * delta # (M, N)
|
|
442
|
+
small = theta.abs() <= self._ITER_THETA_MAX
|
|
443
|
+
|
|
444
|
+
# 1/k! as plain floats: torch cannot divide a tensor by math.factorial's
|
|
445
|
+
# arbitrary-precision int, and float() keeps it to a single correct rounding.
|
|
446
|
+
inv_fact = [1.0 / float(math.factorial(k)) for k in range(n + self._ITER_TERMS)]
|
|
447
|
+
|
|
448
|
+
# --- Series branch: Δ^n Σ_p θ^p/(n+p)! Φ_p(Z_lo). θ is zeroed outside the
|
|
449
|
+
# small-|θ| set so the powers cannot overflow (matters in float32).
|
|
450
|
+
th_s = torch.where(small, theta, torch.zeros_like(theta))
|
|
451
|
+
series = torch.zeros_like(theta)
|
|
452
|
+
th_pow = torch.ones_like(theta) # θ^p
|
|
453
|
+
for p in range(self._ITER_TERMS):
|
|
454
|
+
series = series + (th_pow * inv_fact[n + p]) * cache_lo.phase(p)
|
|
455
|
+
th_pow = th_pow * th_s
|
|
456
|
+
series = series * (delta ** n)
|
|
457
|
+
|
|
458
|
+
# --- Closed branch: F_n(Z_hi) − Σ_j F_j(Z_lo) Δ^{n-j}/(n-j)!. W is forced to
|
|
459
|
+
# 1 on the small-|θ| set so the unselected branch cannot produce inf/NaN (which
|
|
460
|
+
# would otherwise poison gradients through torch.where).
|
|
461
|
+
Wd_safe = torch.where(small, torch.ones_like(theta), Wd.expand_as(theta))
|
|
462
|
+
closed = cache_hi.phase(-n) / Wd_safe ** n
|
|
463
|
+
for j in range(1, n + 1):
|
|
464
|
+
closed = closed - (cache_lo.phase(-j) / Wd_safe ** j) * (
|
|
465
|
+
delta ** (n - j) * inv_fact[n - j]
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
return torch.where(small, series, closed) * self._inv_norm
|
|
469
|
+
|
|
361
470
|
# ------------------------------------------------------------------
|
|
362
471
|
# Convenience: 0th order (basis values)
|
|
363
472
|
# ------------------------------------------------------------------
|
|
@@ -874,7 +983,14 @@ class IntegralOperator:
|
|
|
874
983
|
|
|
875
984
|
``apply(basis, x)`` returns an (M, N) design matrix: for Volterra, the running
|
|
876
985
|
integral up to each ``x``; for a partial definite integral in d>1, the matrix
|
|
877
|
-
marginalised along ``dim`` (other coordinates held at ``x``).
|
|
986
|
+
marginalised along ``dim`` (other coordinates held at ``x``).
|
|
987
|
+
|
|
988
|
+
``order=n > 1`` is the n-fold **iterated** integral with every lower limit pinned
|
|
989
|
+
at ``lower`` -- ∫_lo^x ∫_lo^{s_1} … φ, equivalently the Cauchy form
|
|
990
|
+
∫_lo^x (x−t)^{n−1}/(n−1)! φ(t) dt -- which is what a higher-order Volterra equation
|
|
991
|
+
applies. It is *not* the difference of n-th antiderivatives F_n(hi) − F_n(lo);
|
|
992
|
+
see :meth:`SinusoidalBasis.iterated_integral`. Near-DC features are exact and
|
|
993
|
+
finite at every order (no DC guard). Composes with
|
|
878
994
|
:class:`DiffOperator` via ``+ - *`` into an :class:`IntegroDifferentialOperator`,
|
|
879
995
|
so an integro-differential / integral equation is one linear-least-squares block:
|
|
880
996
|
|
|
@@ -931,20 +1047,13 @@ class IntegralOperator:
|
|
|
931
1047
|
return basis.definite_integral(
|
|
932
1048
|
x, self.dim, self.lower, upper=self.upper, cache=cache
|
|
933
1049
|
)
|
|
934
|
-
# order >= 2:
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
return F_hi - F_lo
|
|
942
|
-
|
|
943
|
-
def _shift(self, x: torch.Tensor, value: float) -> torch.Tensor:
|
|
944
|
-
"""Copy of ``x`` with the integration axis pinned to ``value``."""
|
|
945
|
-
xs = x.clone()
|
|
946
|
-
xs[:, self.dim] = value
|
|
947
|
-
return xs
|
|
1050
|
+
# order >= 2: the n-fold *iterated* (Cauchy) integral, all lower limits pinned
|
|
1051
|
+
# at ``lower``. Note this is NOT F_n(hi) − F_n(lo): that difference drops the
|
|
1052
|
+
# polynomial terms Σ_{j<n} F_j(lo)·Δ^{n−j}/(n−j)!. Like the order-1 branch it
|
|
1053
|
+
# is exact and finite for near-DC features (no DC guard).
|
|
1054
|
+
return basis.iterated_integral(
|
|
1055
|
+
x, self.dim, self.lower, upper=self.upper, order=self.order, cache=cache
|
|
1056
|
+
)
|
|
948
1057
|
|
|
949
1058
|
# ------------------------------------------------------------------
|
|
950
1059
|
# Arithmetic composition -> IntegroDifferentialOperator
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "FastLSQ"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "One-shot PDE solving via Fourier features with exact analytical derivatives; rank-revealing solvers, learnable anisotropic bandwidth, and CPU/CUDA/MPS support"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -12,6 +12,11 @@ reciprocal prefactor. These tests make that a *tested* property:
|
|
|
12
12
|
* mixed integro-differential multi-indices match the analytic closed form and autodiff;
|
|
13
13
|
* the DC guard keeps near-zero-frequency antiderivatives finite;
|
|
14
14
|
* definite / running (Volterra) integrals agree with high-resolution quadrature;
|
|
15
|
+
* higher-order (n-fold ITERATED) integrals agree with n-fold cumulative trapezoid and
|
|
16
|
+
with the Cauchy repeated-integration kernel -- and are *not* the naive difference of
|
|
17
|
+
n-th antiderivatives, which drops the polynomial terms;
|
|
18
|
+
* near-DC features integrate to the exact ramp phi*Delta^n/n! at every order, rather
|
|
19
|
+
than being zeroed as the standalone (genuinely divergent) antiderivative is;
|
|
15
20
|
* differential and integral terms compose into one (M, N) design matrix, through which
|
|
16
21
|
gradients flow to learnable coefficients;
|
|
17
22
|
* a Volterra integro-differential boundary-value problem is solved in one LSQ to <1e-5.
|
|
@@ -19,6 +24,8 @@ reciprocal prefactor. These tests make that a *tested* property:
|
|
|
19
24
|
Run with ``pytest`` or directly as a script.
|
|
20
25
|
"""
|
|
21
26
|
|
|
27
|
+
import math
|
|
28
|
+
|
|
22
29
|
import numpy as np
|
|
23
30
|
import torch
|
|
24
31
|
|
|
@@ -126,6 +133,126 @@ def test_definite_matches_quadrature():
|
|
|
126
133
|
assert (rows[0].squeeze() - exact).abs() < 1e-6
|
|
127
134
|
|
|
128
135
|
|
|
136
|
+
def _iterated_trapz(fg, xs, n):
|
|
137
|
+
"""n-fold cumulative trapezoid of ``fg`` on grid ``xs`` (all lower limits at xs[0])."""
|
|
138
|
+
cur = fg
|
|
139
|
+
for _ in range(n):
|
|
140
|
+
nxt = torch.zeros_like(cur)
|
|
141
|
+
nxt[1:] = torch.cumulative_trapezoid(cur, xs)
|
|
142
|
+
cur = nxt
|
|
143
|
+
return cur
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_iterated_volterra_matches_quadrature():
|
|
147
|
+
"""order>=2 is the n-fold ITERATED (Volterra) integral, matching an n-fold cumulative
|
|
148
|
+
trapezoid. Regression guard: the old code returned F_n(hi) - F_n(lo), which drops the
|
|
149
|
+
polynomial terms sum_j F_j(lo)*(x-lo)^(n-j)/(n-j)! and was ~380% off for n=2."""
|
|
150
|
+
torch.manual_seed(3)
|
|
151
|
+
basis = SinusoidalBasis.random(1, 32, sigma=3.0, normalize=False)
|
|
152
|
+
beta = torch.randn(32, 1)
|
|
153
|
+
|
|
154
|
+
xg = torch.linspace(0.0, 1.0, 20001).reshape(-1, 1)
|
|
155
|
+
xs = xg.squeeze()
|
|
156
|
+
fg = (basis.evaluate(xg) @ beta).squeeze()
|
|
157
|
+
|
|
158
|
+
for n in (1, 2, 3, 4):
|
|
159
|
+
ref = _iterated_trapz(fg, xs, n)
|
|
160
|
+
got = (IntegralOperator.volterra(dim=0, lower=0.0, d=1, order=n).apply(basis, xg)
|
|
161
|
+
@ beta).squeeze()
|
|
162
|
+
assert torch.isfinite(got).all()
|
|
163
|
+
rel = (torch.norm(got - ref) / torch.norm(ref)).item()
|
|
164
|
+
assert rel < 1e-6, f"order {n}: volterra vs {n}x-trapz rel-L2 = {rel:.2e}"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_iterated_volterra_matches_cauchy_kernel():
|
|
168
|
+
"""Independent reference: the n-fold iterated integral equals the SINGLE integral
|
|
169
|
+
int_lo^x (x-t)^(n-1)/(n-1)! phi(t) dt (Cauchy formula for repeated integration)."""
|
|
170
|
+
torch.manual_seed(11)
|
|
171
|
+
basis = SinusoidalBasis.random(1, 24, sigma=2.5, normalize=False)
|
|
172
|
+
beta = torch.randn(24, 1)
|
|
173
|
+
|
|
174
|
+
lo, x_eval = -0.4, 0.85
|
|
175
|
+
tg = torch.linspace(lo, x_eval, 40001)
|
|
176
|
+
fg = (basis.evaluate(tg.reshape(-1, 1)) @ beta).squeeze()
|
|
177
|
+
|
|
178
|
+
for n in (2, 3, 5):
|
|
179
|
+
kernel = (x_eval - tg) ** (n - 1) / float(math.factorial(n - 1))
|
|
180
|
+
ref = torch.trapz(kernel * fg, tg)
|
|
181
|
+
got = (IntegralOperator.volterra(dim=0, lower=lo, d=1, order=n)
|
|
182
|
+
.apply(basis, torch.tensor([[x_eval]])) @ beta).squeeze()
|
|
183
|
+
rel = ((got - ref).abs() / ref.abs()).item()
|
|
184
|
+
assert rel < 1e-6, f"order {n}: vs Cauchy kernel rel = {rel:.2e}"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_iterated_definite_matches_quadrature():
|
|
188
|
+
"""The order-n definite integral over [a, b] equals the n-fold running integral
|
|
189
|
+
evaluated at b, and is identical for every row in 1D."""
|
|
190
|
+
torch.manual_seed(4)
|
|
191
|
+
basis = SinusoidalBasis.random(1, 48, sigma=4.0, normalize=False)
|
|
192
|
+
beta = torch.randn(48, 1)
|
|
193
|
+
|
|
194
|
+
a, b = 0.2, 0.9
|
|
195
|
+
xg = torch.linspace(a, b, 20001).reshape(-1, 1)
|
|
196
|
+
fg = (basis.evaluate(xg) @ beta).squeeze()
|
|
197
|
+
|
|
198
|
+
for n in (2, 3):
|
|
199
|
+
ref = _iterated_trapz(fg, xg.squeeze(), n)[-1]
|
|
200
|
+
I = IntegralOperator.definite(dim=0, lower=a, upper=b, d=1, order=n)
|
|
201
|
+
rows = I.apply(basis, torch.rand(5, 1)) @ beta # result independent of x in 1D
|
|
202
|
+
assert torch.allclose(rows, rows[:1].expand_as(rows), atol=1e-10)
|
|
203
|
+
rel = ((rows[0].squeeze() - ref).abs() / ref.abs()).item()
|
|
204
|
+
assert rel < 1e-6, f"order {n}: definite vs {n}x-trapz rel = {rel:.2e}"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_iterated_order_one_matches_definite_integral():
|
|
208
|
+
"""order=1 of the general iterated form reproduces the sinc closed form exactly, so the
|
|
209
|
+
two code paths in IntegralOperator.apply cannot drift apart."""
|
|
210
|
+
torch.manual_seed(12)
|
|
211
|
+
basis = SinusoidalBasis.random(2, 64, sigma=3.0, normalize=False)
|
|
212
|
+
x = sample_box(200, 2)
|
|
213
|
+
for dim, lo, hi in ((0, -0.4, None), (1, 0.1, 0.75)):
|
|
214
|
+
ref = basis.definite_integral(x, dim, lo, upper=hi)
|
|
215
|
+
got = basis.iterated_integral(x, dim, lo, upper=hi, order=1)
|
|
216
|
+
assert torch.allclose(got, ref, atol=1e-14, rtol=1e-11)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_iterated_near_dc_is_exact_not_zeroed():
|
|
220
|
+
"""Documented semantics: unlike the DC-guarded standalone antiderivative (whose 1/W
|
|
221
|
+
primitive genuinely diverges), the *limit-bearing* iterated integral of a near-DC
|
|
222
|
+
feature is finite and exact -- phi * Delta^n / n! -- and must NOT be zeroed. This keeps
|
|
223
|
+
order>=2 consistent with the order=1 sinc path (cf. test_dc_guard_no_blowup)."""
|
|
224
|
+
W = torch.tensor([[0.0, 1e-13, 2.0]])
|
|
225
|
+
b = torch.tensor([[0.7, 0.7, 0.7]])
|
|
226
|
+
basis = SinusoidalBasis(W, b, normalize=False)
|
|
227
|
+
|
|
228
|
+
delta = 1.3
|
|
229
|
+
x = torch.tensor([[delta]])
|
|
230
|
+
for n in (1, 2, 3, 4):
|
|
231
|
+
got = basis.iterated_integral(x, 0, 0.0, order=n)[0]
|
|
232
|
+
assert torch.isfinite(got).all()
|
|
233
|
+
exact_dc = np.sin(0.7) * delta ** n / float(math.factorial(n))
|
|
234
|
+
# both the exactly-DC and the near-DC column hit the ramp value, not zero
|
|
235
|
+
assert abs(got[0].item() - exact_dc) < 1e-13, f"order {n}: DC col = {got[0].item()}"
|
|
236
|
+
assert abs(got[1].item() - exact_dc) < 1e-13
|
|
237
|
+
assert got[0].abs().item() > 1e-3 # explicitly: not zeroed
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_iterated_differs_from_antiderivative_difference():
|
|
241
|
+
"""Pins the actual bug: for n=2 the correct value is F2(x)-F2(lo)-F1(lo)*(x-lo), so it
|
|
242
|
+
must differ from the naive difference F2(x)-F2(lo) by exactly that ramp term."""
|
|
243
|
+
torch.manual_seed(13)
|
|
244
|
+
basis = SinusoidalBasis.random(1, 16, sigma=2.0, normalize=False)
|
|
245
|
+
x = torch.linspace(0.1, 1.0, 9).reshape(-1, 1)
|
|
246
|
+
lo = 0.0
|
|
247
|
+
|
|
248
|
+
naive = basis.derivative(x, (-2,)) - basis.derivative(torch.full_like(x, lo), (-2,))
|
|
249
|
+
got = basis.iterated_integral(x, 0, lo, order=2)
|
|
250
|
+
ramp = basis.derivative(torch.full_like(x, lo), (-1,)) * (x - lo)
|
|
251
|
+
|
|
252
|
+
assert torch.allclose(got, naive - ramp, atol=1e-10, rtol=1e-8)
|
|
253
|
+
assert not torch.allclose(got, naive, atol=1e-6) # the dropped term is not negligible
|
|
254
|
+
|
|
255
|
+
|
|
129
256
|
def test_integro_differential_compose():
|
|
130
257
|
"""Differential + integral terms compose into one (M, N) matrix equal to the sum of parts."""
|
|
131
258
|
torch.manual_seed(5)
|
|
@@ -189,6 +316,12 @@ if __name__ == "__main__":
|
|
|
189
316
|
test_dc_guard_no_blowup()
|
|
190
317
|
test_volterra_matches_quadrature()
|
|
191
318
|
test_definite_matches_quadrature()
|
|
319
|
+
test_iterated_volterra_matches_quadrature()
|
|
320
|
+
test_iterated_volterra_matches_cauchy_kernel()
|
|
321
|
+
test_iterated_definite_matches_quadrature()
|
|
322
|
+
test_iterated_order_one_matches_definite_integral()
|
|
323
|
+
test_iterated_near_dc_is_exact_not_zeroed()
|
|
324
|
+
test_iterated_differs_from_antiderivative_difference()
|
|
192
325
|
test_integro_differential_compose()
|
|
193
326
|
test_learnable_coeff_grad_flows()
|
|
194
327
|
test_volterra_ivp_forward_solve()
|