FastLSQ 0.4.0__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.0 → fastlsq-0.4.2}/CHANGELOG.md +65 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/FastLSQ.egg-info/PKG-INFO +2 -2
- {fastlsq-0.4.0 → fastlsq-0.4.2}/FastLSQ.egg-info/SOURCES.txt +2 -37
- {fastlsq-0.4.0 → fastlsq-0.4.2}/PKG-INFO +2 -2
- {fastlsq-0.4.0 → fastlsq-0.4.2}/README.md +1 -1
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/__init__.py +2 -1
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/api.py +31 -3
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/basis.py +161 -15
- fastlsq-0.4.2/fastlsq/benchmark.py +100 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/export.py +34 -3
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/learnable.py +14 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/linalg.py +84 -19
- {fastlsq-0.4.0 → fastlsq-0.4.2}/pyproject.toml +1 -1
- fastlsq-0.4.2/tests/test_diagnostics_timing.py +197 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_integral.py +133 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_vector_basis.py +1 -1
- fastlsq-0.4.0/examples/digital_twins/darcy_heat.py +0 -583
- fastlsq-0.4.0/examples/digital_twins/pendulum.py +0 -242
- fastlsq-0.4.0/examples/digital_twins/pendulum_benchmark.py +0 -281
- fastlsq-0.4.0/examples/digital_twins/plasma_wakefield.py +0 -339
- fastlsq-0.4.0/examples/digital_twins/plasma_wakefield_2D_1.py +0 -863
- fastlsq-0.4.0/examples/digital_twins/plasma_wakefield_2D_2.py +0 -769
- fastlsq-0.4.0/examples/digital_twins/plasma_wakefield_2d_3.py +0 -818
- fastlsq-0.4.0/examples/digital_twins/plasma_wakefield_parameteric.py +0 -475
- fastlsq-0.4.0/examples/digital_twins/plot_utils.py +0 -42
- fastlsq-0.4.0/examples/digital_twins/structural_health_simple.py +0 -343
- fastlsq-0.4.0/examples/digital_twins/turbulence_gravity_cooling.py +0 -387
- fastlsq-0.4.0/examples/inverse/aero_.py +0 -362
- fastlsq-0.4.0/examples/inverse/denoising_parameter_estimation.py +0 -297
- fastlsq-0.4.0/examples/inverse/elastic_wave_animation.py +0 -300
- fastlsq-0.4.0/examples/inverse/heat_from_video.py +0 -394
- fastlsq-0.4.0/examples/inverse/inverse_memory_kernel.py +0 -151
- fastlsq-0.4.0/examples/inverse/inverse_source_integral_sensors.py +0 -130
- fastlsq-0.4.0/examples/inverse/inverse_turbulence.py +0 -811
- fastlsq-0.4.0/examples/inverse/rlc_system_id.py +0 -159
- fastlsq-0.4.0/examples/inverse/shape_ns.py +0 -1426
- fastlsq-0.4.0/examples/inverse/subsurface_imaging.py +0 -547
- fastlsq-0.4.0/examples/inverse/tomography_projection.py +0 -145
- fastlsq-0.4.0/examples/inverse/wing_optimize_simple.py +0 -283
- fastlsq-0.4.0/examples/sindy/compare_sindy_methods.py +0 -760
- fastlsq-0.4.0/examples/sindy/sindy_benchmarks.py +0 -351
- fastlsq-0.4.0/examples/sindy/sindy_differentiable.py +0 -418
- fastlsq-0.4.0/examples/sindy/sindy_minimal_diff.py +0 -716
- fastlsq-0.4.0/misc/fastlsq_teaser.png +0 -0
- fastlsq-0.4.0/misc/ideal_quadrupole.png +0 -0
- fastlsq-0.4.0/misc/inverse_heat_source.gif +0 -0
- fastlsq-0.4.0/misc/inverse_heat_source.png +0 -0
- fastlsq-0.4.0/misc/inverse_magnetostatics.png +0 -0
- fastlsq-0.4.0/misc/inverse_magnetostatics_convergence.png +0 -0
- fastlsq-0.4.0/misc/quadrupole_convergence.png +0 -0
- fastlsq-0.4.0/misc/quadrupole_optimization.png +0 -0
- fastlsq-0.4.0/misc/tutorial_nlpoisson_convergence.png +0 -0
- fastlsq-0.4.0/misc/tutorial_nlpoisson_solution.png +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/FastLSQ.egg-info/dependency_links.txt +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/FastLSQ.egg-info/requires.txt +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/FastLSQ.egg-info/top_level.txt +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/LICENSE +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/MANIFEST.in +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/add_your_own_pde.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/benchmark_comparison.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/custom_features.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/fred_sde.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/fred_sde_fastlsq.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/gaia_potential.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/gaia_potential_fastlsq.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/horizons_ephemeris.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/numerai_alpha.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/numerai_alpha_fastlsq.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/run_all_fastlsq.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/__init__.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/_alsu_lattice.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/_common.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/run_all.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_green_fff.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_passive_loco.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_tides_3months.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s01_visualize.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s04_sunspots.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s05_helioseismology.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s06_tides.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s14_eeg.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/scenarios/s15_circadian.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/extras/spectral_expansion.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/grad_shafranov.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/grid_inverse.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/grid_rl_control.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/grid_swing.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/gs_inverse.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/gs_rl_control.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/integro_differential_demo.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/inverse_heat_source.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/inverse_magnetostatics.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/inverse_source_position.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/learnable_helmholtz.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/memory_diffusion.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/orbit_hill.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/orbit_inverse.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/orbit_rl.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/pde_discovery.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/rlc_integro_differential.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/run_all_extensions.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/run_linear.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/run_nonlinear.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/tutorial_basic.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/tutorial_nonlinear.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/examples/vector_basis_stream_vorticity.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/block.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/device.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/diagnostics.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/geometry.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/lightning.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/newton.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/plotting.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/problems/__init__.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/problems/linear.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/problems/nonlinear.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/problems/regression.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/solvers.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/tuning.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/utils.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/vector.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/fastlsq/viz.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/requirements.txt +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/setup.cfg +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_basic.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_benchmarks_inverse.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_block.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_derivatives.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_device.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_grad_shafranov.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_grid_swing.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_learnable.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_orbit_hill.py +0 -0
- {fastlsq-0.4.0 → fastlsq-0.4.2}/tests/test_projection.py +0 -0
|
@@ -2,6 +2,71 @@
|
|
|
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
|
+
|
|
41
|
+
## [0.4.1] - 2026-06-23
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **Solve-time diagnostics — `solve_lstsq(..., return_info=True)`.** Returns
|
|
46
|
+
`(x, info)` with `info = {t_solve, rank_used, residual, cond_estimate}`. `t_solve`
|
|
47
|
+
is the **device-synced wall-time of the solve step alone** (singular values for
|
|
48
|
+
the rank/cond diagnostics are computed *outside* the timed region), `rank_used` is
|
|
49
|
+
the rank-revealing effective numerical rank, and `cond_estimate` is `s_max/s_min`
|
|
50
|
+
over the retained subspace. The default `return_info=False` path is unchanged.
|
|
51
|
+
- **Phased breakdown in `solve_linear` metrics.** `metrics` now reports
|
|
52
|
+
`scale_search_s`, `assemble_s`, `solve_s` (plus `rank_used`, `residual`,
|
|
53
|
+
`cond_estimate`) so the headline time is no longer dominated by the auto-scale
|
|
54
|
+
search — the reported solve time is a number the library hands you, not one a
|
|
55
|
+
benchmark reconstructs by hand.
|
|
56
|
+
- **`fastlsq.benchmark.time_solve(fn, reps, warmup, device)`** — a device-correct
|
|
57
|
+
timing primitive (`synchronize` bracketing + warm-up + min-of-reps) returning the
|
|
58
|
+
reproducible solve-time *floor* in seconds (`return_all=True` for full stats). Also
|
|
59
|
+
exposes `fastlsq.benchmark.synchronize`.
|
|
60
|
+
- **`SinusoidalBasis.random_covariance(d, N, Sigma=… | L=…)`** — fixed full-`Sigma`
|
|
61
|
+
constructor (`W = L @ W_hat`, `Sigma = L Lᵀ`), the symmetric counterpart to
|
|
62
|
+
`random` / `random_anisotropic`.
|
|
63
|
+
- **`LearnableFastLSQ.freeze() -> SinusoidalBasis`** — freezes the learned bandwidth
|
|
64
|
+
into a plain, detached basis for one clean, timed one-shot deployment solve (the
|
|
65
|
+
`.basis` property still reconstructs-with-grad each access).
|
|
66
|
+
- **Checkpoint provenance.** `save_checkpoint` auto-records a `provenance` block
|
|
67
|
+
(library version, device, dtype, timestamp, and the realized scale / `Sigma`) into
|
|
68
|
+
the metadata.
|
|
69
|
+
|
|
5
70
|
## [0.4.0] - 2026-06-22
|
|
6
71
|
|
|
7
72
|
### 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,25 +76,10 @@ 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
|
|
82
|
+
fastlsq/benchmark.py
|
|
109
83
|
fastlsq/block.py
|
|
110
84
|
fastlsq/device.py
|
|
111
85
|
fastlsq/diagnostics.py
|
|
@@ -125,21 +99,12 @@ fastlsq/problems/__init__.py
|
|
|
125
99
|
fastlsq/problems/linear.py
|
|
126
100
|
fastlsq/problems/nonlinear.py
|
|
127
101
|
fastlsq/problems/regression.py
|
|
128
|
-
misc/fastlsq_teaser.png
|
|
129
|
-
misc/ideal_quadrupole.png
|
|
130
|
-
misc/inverse_heat_source.gif
|
|
131
|
-
misc/inverse_heat_source.png
|
|
132
|
-
misc/inverse_magnetostatics.png
|
|
133
|
-
misc/inverse_magnetostatics_convergence.png
|
|
134
|
-
misc/quadrupole_convergence.png
|
|
135
|
-
misc/quadrupole_optimization.png
|
|
136
|
-
misc/tutorial_nlpoisson_convergence.png
|
|
137
|
-
misc/tutorial_nlpoisson_solution.png
|
|
138
102
|
tests/test_basic.py
|
|
139
103
|
tests/test_benchmarks_inverse.py
|
|
140
104
|
tests/test_block.py
|
|
141
105
|
tests/test_derivatives.py
|
|
142
106
|
tests/test_device.py
|
|
107
|
+
tests/test_diagnostics_timing.py
|
|
143
108
|
tests/test_grad_shafranov.py
|
|
144
109
|
tests/test_grid_swing.py
|
|
145
110
|
tests/test_integral.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 |
|
|
@@ -53,8 +53,9 @@ from fastlsq.export import (
|
|
|
53
53
|
load_checkpoint,
|
|
54
54
|
)
|
|
55
55
|
from fastlsq import viz
|
|
56
|
+
from fastlsq import benchmark
|
|
56
57
|
|
|
57
|
-
__version__ = "0.4.
|
|
58
|
+
__version__ = "0.4.2"
|
|
58
59
|
__all__ = [
|
|
59
60
|
# Device selection (CPU / CUDA / Apple-MPS, dtype-aware)
|
|
60
61
|
"resolve_device",
|
|
@@ -105,16 +105,20 @@ def solve_linear(
|
|
|
105
105
|
if n_bc is None:
|
|
106
106
|
n_bc = max(800, n_pde // 5)
|
|
107
107
|
|
|
108
|
-
# Auto-select scale if needed
|
|
108
|
+
# Auto-select scale if needed (timed as its own phase: it is a search that can
|
|
109
|
+
# be amortised / skipped at deployment, so it must not inflate the solve time).
|
|
110
|
+
scale_search_s = 0.0
|
|
109
111
|
if scale is None and auto_scale:
|
|
110
112
|
if verbose:
|
|
111
113
|
print("Auto-selecting optimal scale...")
|
|
114
|
+
t_phase = time.perf_counter()
|
|
112
115
|
scale = auto_select_scale(
|
|
113
116
|
problem, solver_class=FastLSQSolver,
|
|
114
117
|
n_blocks=n_blocks, hidden_size=hidden_size,
|
|
115
118
|
n_pde=n_pde, n_bc=n_bc, n_trials=auto_scale_trials,
|
|
116
119
|
verbose=verbose,
|
|
117
120
|
)
|
|
121
|
+
scale_search_s = time.perf_counter() - t_phase
|
|
118
122
|
if verbose:
|
|
119
123
|
print(f"Selected scale: {scale:.3f}")
|
|
120
124
|
|
|
@@ -137,10 +141,25 @@ def solve_linear(
|
|
|
137
141
|
x_pde, bcs = data
|
|
138
142
|
build_args = (bcs,)
|
|
139
143
|
|
|
140
|
-
# Assemble
|
|
144
|
+
# Assemble (timed phase)
|
|
145
|
+
t_phase = time.perf_counter()
|
|
141
146
|
A, b = problem.build(solver, x_pde, *build_args)
|
|
142
|
-
|
|
147
|
+
assemble_s = time.perf_counter() - t_phase
|
|
148
|
+
|
|
149
|
+
# Solve (timed phase). With metrics on, ``solve_lstsq`` hands back the
|
|
150
|
+
# device-synced solve time plus rank/residual/cond diagnostics directly; with
|
|
151
|
+
# metrics off, keep the plain fast solve and just time it externally.
|
|
143
152
|
n_outputs = getattr(problem, "n_outputs", 1)
|
|
153
|
+
if return_metrics:
|
|
154
|
+
beta_raw, solve_info = solve_lstsq(
|
|
155
|
+
A, b, mu=mu, method=method, return_info=True
|
|
156
|
+
)
|
|
157
|
+
solve_s = solve_info["t_solve"]
|
|
158
|
+
else:
|
|
159
|
+
t_phase = time.perf_counter()
|
|
160
|
+
beta_raw = solve_lstsq(A, b, mu=mu, method=method)
|
|
161
|
+
solve_s = time.perf_counter() - t_phase
|
|
162
|
+
solve_info = None
|
|
144
163
|
solver.beta = unpack_beta(beta_raw, solver.n_features, n_outputs)
|
|
145
164
|
|
|
146
165
|
runtime = time.time() - t0
|
|
@@ -169,6 +188,15 @@ def solve_linear(
|
|
|
169
188
|
"val_err": val_err,
|
|
170
189
|
"grad_err": grad_err,
|
|
171
190
|
"runtime": runtime,
|
|
191
|
+
# Phased breakdown: the conflated ``runtime`` split into its parts so
|
|
192
|
+
# the headline solve time is no longer dominated by the scale search.
|
|
193
|
+
"scale_search_s": scale_search_s,
|
|
194
|
+
"assemble_s": assemble_s,
|
|
195
|
+
"solve_s": solve_s,
|
|
196
|
+
# Rank-revealing solve diagnostics (the paper's central mechanism).
|
|
197
|
+
"rank_used": solve_info["rank_used"],
|
|
198
|
+
"residual": solve_info["residual"],
|
|
199
|
+
"cond_estimate": solve_info["cond_estimate"],
|
|
172
200
|
}
|
|
173
201
|
if verbose:
|
|
174
202
|
print(f"Value error: {val_err:.2e}, Gradient error: {grad_err:.2e}")
|
|
@@ -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,
|
|
@@ -197,6 +213,43 @@ class SinusoidalBasis:
|
|
|
197
213
|
b = torch.rand(1, n_features, device=get_device()) * 2 * np.pi
|
|
198
214
|
return cls(W, b, normalize=normalize, dc_eps=dc_eps)
|
|
199
215
|
|
|
216
|
+
@classmethod
|
|
217
|
+
def random_covariance(
|
|
218
|
+
cls,
|
|
219
|
+
input_dim: int,
|
|
220
|
+
n_features: int,
|
|
221
|
+
Sigma: Optional[Union[list, np.ndarray, torch.Tensor]] = None,
|
|
222
|
+
L: Optional[Union[list, np.ndarray, torch.Tensor]] = None,
|
|
223
|
+
normalize: bool = True,
|
|
224
|
+
dc_eps: float = 1e-8,
|
|
225
|
+
) -> SinusoidalBasis:
|
|
226
|
+
"""Create a basis with full-covariance Gaussian frequencies.
|
|
227
|
+
|
|
228
|
+
Frequencies are drawn ``W[:, j] ~ N(0, Sigma)`` via ``W = L @ W_hat`` with
|
|
229
|
+
``W_hat ~ N(0, I_d)`` and ``Sigma = L L^T``. Provide **exactly one** of
|
|
230
|
+
``Sigma`` (the ``d x d`` covariance) or its lower-triangular Cholesky factor
|
|
231
|
+
``L``. This is the fixed full-``Sigma`` analogue of :meth:`random`
|
|
232
|
+
(isotropic) and :meth:`random_anisotropic` (axis-aligned), and uses the same
|
|
233
|
+
``W = L @ W_hat`` reparameterisation as
|
|
234
|
+
:class:`~fastlsq.learnable.LearnableFastLSQ` -- so a swept covariance no
|
|
235
|
+
longer needs a hand-rolled ``chol(Sigma) @ randn``.
|
|
236
|
+
"""
|
|
237
|
+
if (Sigma is None) == (L is None):
|
|
238
|
+
raise ValueError(
|
|
239
|
+
"random_covariance: provide exactly one of Sigma or L."
|
|
240
|
+
)
|
|
241
|
+
dev = get_device()
|
|
242
|
+
dt = torch.get_default_dtype()
|
|
243
|
+
if L is None:
|
|
244
|
+
Sigma_t = torch.as_tensor(Sigma, device=dev, dtype=dt)
|
|
245
|
+
L_t = torch.linalg.cholesky(Sigma_t)
|
|
246
|
+
else:
|
|
247
|
+
L_t = torch.as_tensor(L, device=dev, dtype=dt)
|
|
248
|
+
W_hat = torch.randn(input_dim, n_features, device=dev, dtype=L_t.dtype)
|
|
249
|
+
W = L_t @ W_hat
|
|
250
|
+
b = torch.rand(1, n_features, device=dev, dtype=L_t.dtype) * 2 * np.pi
|
|
251
|
+
return cls(W, b, normalize=normalize, dc_eps=dc_eps)
|
|
252
|
+
|
|
200
253
|
# ------------------------------------------------------------------
|
|
201
254
|
# Caching
|
|
202
255
|
# ------------------------------------------------------------------
|
|
@@ -321,6 +374,99 @@ class SinusoidalBasis:
|
|
|
321
374
|
out = delta * torch.sin(half_sum) * torch.sinc(sinc_arg)
|
|
322
375
|
return out * self._inv_norm
|
|
323
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
|
+
|
|
324
470
|
# ------------------------------------------------------------------
|
|
325
471
|
# Convenience: 0th order (basis values)
|
|
326
472
|
# ------------------------------------------------------------------
|
|
@@ -837,7 +983,14 @@ class IntegralOperator:
|
|
|
837
983
|
|
|
838
984
|
``apply(basis, x)`` returns an (M, N) design matrix: for Volterra, the running
|
|
839
985
|
integral up to each ``x``; for a partial definite integral in d>1, the matrix
|
|
840
|
-
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
|
|
841
994
|
:class:`DiffOperator` via ``+ - *`` into an :class:`IntegroDifferentialOperator`,
|
|
842
995
|
so an integro-differential / integral equation is one linear-least-squares block:
|
|
843
996
|
|
|
@@ -894,20 +1047,13 @@ class IntegralOperator:
|
|
|
894
1047
|
return basis.definite_integral(
|
|
895
1048
|
x, self.dim, self.lower, upper=self.upper, cache=cache
|
|
896
1049
|
)
|
|
897
|
-
# order >= 2:
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
return F_hi - F_lo
|
|
905
|
-
|
|
906
|
-
def _shift(self, x: torch.Tensor, value: float) -> torch.Tensor:
|
|
907
|
-
"""Copy of ``x`` with the integration axis pinned to ``value``."""
|
|
908
|
-
xs = x.clone()
|
|
909
|
-
xs[:, self.dim] = value
|
|
910
|
-
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
|
+
)
|
|
911
1057
|
|
|
912
1058
|
# ------------------------------------------------------------------
|
|
913
1059
|
# Arithmetic composition -> IntegroDifferentialOperator
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Copyright (c) 2026 Antonin Sulc
|
|
2
|
+
# Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
"""Device-correct timing primitives for FastLSQ.
|
|
5
|
+
|
|
6
|
+
Naively wall-clock-timing a CUDA solve is wrong: kernels are launched
|
|
7
|
+
asynchronously, so ``t1 - t0`` measures launch overhead, not compute. Without a
|
|
8
|
+
``synchronize`` bracket every GPU number is meaningless (and irreproducible).
|
|
9
|
+
:func:`time_solve` adds the missing primitive -- ``synchronize`` bracketing plus
|
|
10
|
+
warm-up and a min-of-reps reduction -- so the reported solve time is the
|
|
11
|
+
reproducible compute *floor* on any device (CPU / CUDA / Apple-MPS).
|
|
12
|
+
|
|
13
|
+
import fastlsq as fl
|
|
14
|
+
t = fl.benchmark.time_solve(lambda: fl.solve_lstsq(A, b)) # seconds, floor
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
|
|
21
|
+
from fastlsq.device import get_device
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def synchronize(device=None):
|
|
25
|
+
"""Block until all queued work on ``device`` has finished.
|
|
26
|
+
|
|
27
|
+
No-op on CPU; calls ``torch.cuda.synchronize`` on CUDA and
|
|
28
|
+
``torch.mps.synchronize`` on Apple-MPS (when available). ``device=None``
|
|
29
|
+
uses the active FastLSQ device.
|
|
30
|
+
"""
|
|
31
|
+
device = get_device() if device is None else torch.device(device)
|
|
32
|
+
if device.type == "cuda":
|
|
33
|
+
torch.cuda.synchronize(device)
|
|
34
|
+
elif device.type == "mps":
|
|
35
|
+
sync = getattr(getattr(torch, "mps", None), "synchronize", None)
|
|
36
|
+
if sync is not None:
|
|
37
|
+
sync()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def time_solve(fn, reps=10, warmup=2, device=None, return_all=False):
|
|
41
|
+
"""Device-correct timing floor for a callable.
|
|
42
|
+
|
|
43
|
+
Runs ``warmup`` unmeasured calls (allocator / autotune warm-up), then ``reps``
|
|
44
|
+
measured calls, each bracketed by :func:`synchronize` so CUDA/MPS asynchronous
|
|
45
|
+
execution is fully accounted for. Returns the **minimum** elapsed time in
|
|
46
|
+
seconds -- the reproducible solve-time floor -- which is the right summary for
|
|
47
|
+
a deterministic op whose variance is pure system noise.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
fn : callable
|
|
52
|
+
Zero-argument callable performing the work to time, e.g.
|
|
53
|
+
``lambda: solve_lstsq(A, b)``.
|
|
54
|
+
reps : int
|
|
55
|
+
Number of measured repetitions (>= 1).
|
|
56
|
+
warmup : int
|
|
57
|
+
Number of unmeasured warm-up repetitions.
|
|
58
|
+
device : str | torch.device | None
|
|
59
|
+
Device to synchronize around each call. ``None`` uses the active FastLSQ
|
|
60
|
+
device.
|
|
61
|
+
return_all : bool
|
|
62
|
+
If True, return a stats dict
|
|
63
|
+
``{"min", "median", "mean", "std", "times", "reps", "warmup", "device"}``
|
|
64
|
+
instead of the bare floor.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
float
|
|
69
|
+
The minimum solve time in seconds (``return_all=False``).
|
|
70
|
+
dict
|
|
71
|
+
Full timing statistics (``return_all=True``).
|
|
72
|
+
"""
|
|
73
|
+
device = get_device() if device is None else torch.device(device)
|
|
74
|
+
|
|
75
|
+
for _ in range(max(0, warmup)):
|
|
76
|
+
fn()
|
|
77
|
+
synchronize(device)
|
|
78
|
+
|
|
79
|
+
times = []
|
|
80
|
+
for _ in range(max(1, reps)):
|
|
81
|
+
synchronize(device)
|
|
82
|
+
t0 = time.perf_counter()
|
|
83
|
+
fn()
|
|
84
|
+
synchronize(device)
|
|
85
|
+
times.append(time.perf_counter() - t0)
|
|
86
|
+
|
|
87
|
+
t = torch.tensor(times, dtype=torch.float64)
|
|
88
|
+
floor = float(t.min())
|
|
89
|
+
if not return_all:
|
|
90
|
+
return floor
|
|
91
|
+
return {
|
|
92
|
+
"min": floor,
|
|
93
|
+
"median": float(t.median()),
|
|
94
|
+
"mean": float(t.mean()),
|
|
95
|
+
"std": float(t.std(unbiased=False)),
|
|
96
|
+
"times": times,
|
|
97
|
+
"reps": len(times),
|
|
98
|
+
"warmup": max(0, warmup),
|
|
99
|
+
"device": str(device),
|
|
100
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
"""Export utilities for FastLSQ solutions (NumPy, VTK, etc.)."""
|
|
5
5
|
|
|
6
|
+
import time
|
|
7
|
+
|
|
6
8
|
import torch
|
|
7
9
|
import numpy as np
|
|
8
10
|
from typing import Optional, Union, Dict, Any
|
|
@@ -10,6 +12,32 @@ from typing import Optional, Union, Dict, Any
|
|
|
10
12
|
from fastlsq.solvers import FastLSQSolver
|
|
11
13
|
|
|
12
14
|
|
|
15
|
+
def _provenance(solver: FastLSQSolver) -> Dict[str, Any]:
|
|
16
|
+
"""Auto-recorded provenance for a saved model: library version, device, dtype,
|
|
17
|
+
and the realized frequency bandwidth (scale / Sigma) the model actually uses."""
|
|
18
|
+
import fastlsq # lazy: avoids a circular import at module load
|
|
19
|
+
|
|
20
|
+
prov: Dict[str, Any] = {
|
|
21
|
+
"fastlsq_version": getattr(fastlsq, "__version__", None),
|
|
22
|
+
"created": time.time(),
|
|
23
|
+
"input_dim": solver.input_dim,
|
|
24
|
+
"n_features": solver.n_features,
|
|
25
|
+
}
|
|
26
|
+
W = torch.cat(solver.W_list, dim=1) if solver.W_list else None
|
|
27
|
+
ref = W if W is not None else solver.beta
|
|
28
|
+
if ref is not None:
|
|
29
|
+
prov["device"] = str(ref.device)
|
|
30
|
+
prov["dtype"] = str(ref.dtype).replace("torch.", "")
|
|
31
|
+
if W is not None:
|
|
32
|
+
# Realized frequency second moment Sigma = (W Wᵀ)/N and per-axis scale.
|
|
33
|
+
freq_cov = (W @ W.transpose(-2, -1)) / W.shape[1]
|
|
34
|
+
prov["freq_cov"] = freq_cov.cpu().numpy()
|
|
35
|
+
prov["freq_std"] = torch.sqrt(
|
|
36
|
+
torch.diagonal(freq_cov).clamp_min(0.0)
|
|
37
|
+
).cpu().numpy()
|
|
38
|
+
return prov
|
|
39
|
+
|
|
40
|
+
|
|
13
41
|
def to_numpy(
|
|
14
42
|
solver: FastLSQSolver,
|
|
15
43
|
x: Union[torch.Tensor, np.ndarray],
|
|
@@ -138,11 +166,14 @@ def save_checkpoint(
|
|
|
138
166
|
path : str
|
|
139
167
|
File path (.pt or .pth extension recommended).
|
|
140
168
|
metadata : dict, optional
|
|
141
|
-
Additional metadata to save.
|
|
169
|
+
Additional metadata to save. A ``provenance`` block (library version,
|
|
170
|
+
device, dtype, timestamp, and realized scale / Sigma) is auto-recorded
|
|
171
|
+
unless the caller supplies its own ``provenance`` key.
|
|
142
172
|
"""
|
|
143
173
|
state = to_dict(solver, include_weights=True, include_metadata=True)
|
|
144
|
-
if metadata
|
|
145
|
-
|
|
174
|
+
meta = dict(metadata) if metadata else {}
|
|
175
|
+
meta.setdefault("provenance", _provenance(solver))
|
|
176
|
+
state["metadata"] = meta
|
|
146
177
|
torch.save(state, path)
|
|
147
178
|
|
|
148
179
|
|
|
@@ -153,6 +153,20 @@ class LearnableFastLSQ(nn.Module):
|
|
|
153
153
|
"""
|
|
154
154
|
return SinusoidalBasis(self._W(), self.b, normalize=self._normalize)
|
|
155
155
|
|
|
156
|
+
def freeze(self) -> SinusoidalBasis:
|
|
157
|
+
"""Freeze the learned bandwidth into a plain, detached ``SinusoidalBasis``.
|
|
158
|
+
|
|
159
|
+
Unlike the :attr:`basis` property -- which reconstructs *with grad* on every
|
|
160
|
+
access -- this returns a fixed basis built from the current
|
|
161
|
+
``W = L @ W_hat`` with the weights detached and cloned. Run the (untimed)
|
|
162
|
+
bandwidth search, then ``freeze()`` the learned ``Sigma`` into this plain
|
|
163
|
+
basis for one clean, timed one-shot solve: the deployment artifact.
|
|
164
|
+
"""
|
|
165
|
+
with torch.no_grad():
|
|
166
|
+
W = self._W().detach().clone()
|
|
167
|
+
b = self.b.detach().clone()
|
|
168
|
+
return SinusoidalBasis(W, b, normalize=self._normalize)
|
|
169
|
+
|
|
156
170
|
# ------------------------------------------------------------------
|
|
157
171
|
# Prediction
|
|
158
172
|
# ------------------------------------------------------------------
|