FastLSQ 0.2.1__tar.gz → 0.2.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.2.1 → fastlsq-0.2.2}/CHANGELOG.md +47 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/PKG-INFO +8 -8
- {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/SOURCES.txt +0 -10
- {fastlsq-0.2.1 → fastlsq-0.2.2}/MANIFEST.in +0 -1
- {fastlsq-0.2.1 → fastlsq-0.2.2}/PKG-INFO +8 -8
- {fastlsq-0.2.1 → fastlsq-0.2.2}/README.md +3 -3
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/orbit_hill.py +7 -5
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/__init__.py +1 -1
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/api.py +7 -5
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/basis.py +5 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/export.py +4 -1
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/learnable.py +15 -8
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/linalg.py +1 -1
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/newton.py +8 -2
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/problems/linear.py +6 -6
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/problems/nonlinear.py +20 -20
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/problems/regression.py +38 -38
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/tuning.py +9 -1
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/vector.py +2 -2
- {fastlsq-0.2.1 → fastlsq-0.2.2}/pyproject.toml +5 -5
- fastlsq-0.2.1/misc/fastlsq_teaser.png +0 -0
- fastlsq-0.2.1/misc/ideal_quadrupole.png +0 -0
- fastlsq-0.2.1/misc/inverse_heat_source.gif +0 -0
- fastlsq-0.2.1/misc/inverse_heat_source.png +0 -0
- fastlsq-0.2.1/misc/inverse_magnetostatics.png +0 -0
- fastlsq-0.2.1/misc/inverse_magnetostatics_convergence.png +0 -0
- fastlsq-0.2.1/misc/quadrupole_convergence.png +0 -0
- fastlsq-0.2.1/misc/quadrupole_optimization.png +0 -0
- fastlsq-0.2.1/misc/tutorial_nlpoisson_convergence.png +0 -0
- fastlsq-0.2.1/misc/tutorial_nlpoisson_solution.png +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/dependency_links.txt +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/requires.txt +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/top_level.txt +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/LICENSE +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/add_your_own_pde.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/benchmark_comparison.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/custom_features.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/fred_sde.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/fred_sde_fastlsq.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/gaia_potential.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/gaia_potential_fastlsq.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/horizons_ephemeris.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/numerai_alpha.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/numerai_alpha_fastlsq.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/run_all_fastlsq.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/__init__.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/_alsu_lattice.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/_common.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/run_all.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_green_fff.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_passive_loco.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_tides_3months.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_visualize.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s04_sunspots.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s05_helioseismology.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s06_tides.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s14_eeg.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s15_circadian.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/spectral_expansion.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/grad_shafranov.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/grid_inverse.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/grid_rl_control.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/grid_swing.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/gs_inverse.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/gs_rl_control.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/inverse_heat_source.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/inverse_magnetostatics.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/inverse_source_position.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/learnable_helmholtz.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/orbit_inverse.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/orbit_rl.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/pde_discovery.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/run_all_extensions.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/run_linear.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/run_nonlinear.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/tutorial_basic.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/tutorial_nonlinear.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/vector_basis_stream_vorticity.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/block.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/device.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/diagnostics.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/geometry.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/lightning.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/plotting.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/problems/__init__.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/solvers.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/utils.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/viz.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/requirements.txt +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/setup.cfg +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_basic.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_block.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_derivatives.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_device.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_grad_shafranov.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_grid_swing.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_learnable.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_orbit_hill.py +0 -0
- {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_vector_basis.py +0 -0
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to FastLSQ will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.2] - 2026-06-03
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Learnable bandwidth now trains.** `LearnableFastLSQ.solve_inner` replaced the
|
|
10
|
+
backprop-through-`torch.linalg.svd` inner solve (which returned NaN gradients
|
|
11
|
+
w.r.t. the bandwidth on the clustered singular values of random-feature
|
|
12
|
+
matrices) with the SVD-based `gelsd` rank-revealing least-squares driver, so
|
|
13
|
+
`train_bandwidth` / `fit` no longer stall at step 0.
|
|
14
|
+
- **Default-solve accuracy.** Tightened the `_auto_solve` Cholesky-acceptance
|
|
15
|
+
probe from `rcond**0.5` to `rcond**0.25`, so `method="auto"` falls back to SVD
|
|
16
|
+
before the normal-equations Cholesky loses half its float64 digits
|
|
17
|
+
(cond(A) ~ 1e7 previously returned a ~1e-3-accurate answer).
|
|
18
|
+
- **Newton convergence and robustness.** The stop test now combines a *relative*
|
|
19
|
+
residual criterion (`res_norm < tol_res * R0`) with the relative solution
|
|
20
|
+
change (`||Δu||/||u|| < tol_du`); the previous unreachable absolute residual
|
|
21
|
+
tolerance forced every nonlinear solve to run the full `max_iter`. The
|
|
22
|
+
backtracking line search keeps the previous iterate when no step satisfies
|
|
23
|
+
Armijo instead of committing a worse point. `solve_nonlinear` default
|
|
24
|
+
tolerances loosened to `tol_res=1e-8`, `tol_du=1e-10`.
|
|
25
|
+
- **Continuation guard.** `solve_nonlinear` no longer raises `TypeError` when a
|
|
26
|
+
problem sets `use_continuation=True` without a `nu_target`.
|
|
27
|
+
- **Regression problems solvable via the public API.** Their `get_train_data`
|
|
28
|
+
now accepts the `n_pde`/`n_bc` signature used by `solve_linear`,
|
|
29
|
+
`auto_select_scale`, and `check_problem` (was `n_samples`, raising
|
|
30
|
+
`TypeError`); `auto_select_scale` now raises when every trial fails instead of
|
|
31
|
+
silently returning the first scale.
|
|
32
|
+
- **Float32 inputs.** `SinusoidalBasis.cache` promotes inputs to the basis
|
|
33
|
+
dtype/device, so float32 collocation points no longer raise `float != double`.
|
|
34
|
+
- **Checkpoint reload.** `load_checkpoint` passes `weights_only=False`, fixing
|
|
35
|
+
`UnpicklingError` on torch >= 2.6 (checkpoints store NumPy arrays).
|
|
36
|
+
- **Vector per-component scale.** `VectorFastLSQSolver.add_block` accepts a NumPy
|
|
37
|
+
array of per-component bandwidths (previously list/tuple only, silently
|
|
38
|
+
misread as per-dimension).
|
|
39
|
+
- **ElasticWave2D operator.** Scaled the spatial and cross terms by `t_max²`
|
|
40
|
+
(time normalisation), consistent with `Wave2D_MS`.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Problem modules (`nonlinear.py`, `regression.py`) resolve the device via the
|
|
45
|
+
live `get_device()` rather than an import-time snapshot.
|
|
46
|
+
- Packaging: the source distribution no longer ships the `misc/` images (the
|
|
47
|
+
sdist was ~14 MB); project URLs point to `github.com/sulcantonin/FastLSQ`;
|
|
48
|
+
README images use absolute URLs so they render on PyPI.
|
|
49
|
+
`examples/orbit_hill.py` solves via rank-revealing `lstsq` rather than a
|
|
50
|
+
normal-equations Cholesky.
|
|
51
|
+
|
|
5
52
|
## [0.2.1] - 2026-06-02
|
|
6
53
|
|
|
7
54
|
### Added
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastLSQ
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.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
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/sulcantonin/FastLSQ
|
|
8
|
+
Project-URL: Repository, https://github.com/sulcantonin/FastLSQ
|
|
9
9
|
Project-URL: Paper, https://arxiv.org/abs/2602.10541
|
|
10
|
-
Project-URL: Bug Tracker, https://github.com/
|
|
11
|
-
Project-URL: Changelog, https://github.com/
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/sulcantonin/FastLSQ/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/sulcantonin/FastLSQ/blob/main/CHANGELOG.md
|
|
12
12
|
Keywords: pde,partial-differential-equations,fourier-features,least-squares,scientific-computing,neural-network,physics-informed,newton-raphson
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
<p align="center">
|
|
48
|
-
<img src="misc/fastlsq_teaser.png" alt="FastLSQ method overview" width="400"/>
|
|
48
|
+
<img src="https://raw.githubusercontent.com/sulcantonin/FastLSQ/main/misc/fastlsq_teaser.png" alt="FastLSQ method overview" width="400"/>
|
|
49
49
|
</p>
|
|
50
50
|
|
|
51
51
|
**Solving PDEs in one shot via Fourier features with exact analytical derivatives.**
|
|
@@ -235,8 +235,8 @@ python examples/learnable_helmholtz.py
|
|
|
235
235
|
The analytical derivatives enable gradients through the pre-factored solve, making inverse problems tractable. Example: recovering 4 anisotropic Gaussian heat sources (24 parameters) from 4 sparse sensors. The heat equation is solved in space-time; L-BFGS-B optimises source positions and shapes to match sensor time-series. *(Click image for animation.)*
|
|
236
236
|
|
|
237
237
|
<p align="center">
|
|
238
|
-
<a href="misc/inverse_heat_source.gif">
|
|
239
|
-
<img src="misc/inverse_heat_source.png" alt="Inverse heat source localisation" width="700"/>
|
|
238
|
+
<a href="https://raw.githubusercontent.com/sulcantonin/FastLSQ/main/misc/inverse_heat_source.gif">
|
|
239
|
+
<img src="https://raw.githubusercontent.com/sulcantonin/FastLSQ/main/misc/inverse_heat_source.png" alt="Inverse heat source localisation" width="700"/>
|
|
240
240
|
</a>
|
|
241
241
|
</p>
|
|
242
242
|
|
|
@@ -95,16 +95,6 @@ fastlsq/problems/__init__.py
|
|
|
95
95
|
fastlsq/problems/linear.py
|
|
96
96
|
fastlsq/problems/nonlinear.py
|
|
97
97
|
fastlsq/problems/regression.py
|
|
98
|
-
misc/fastlsq_teaser.png
|
|
99
|
-
misc/ideal_quadrupole.png
|
|
100
|
-
misc/inverse_heat_source.gif
|
|
101
|
-
misc/inverse_heat_source.png
|
|
102
|
-
misc/inverse_magnetostatics.png
|
|
103
|
-
misc/inverse_magnetostatics_convergence.png
|
|
104
|
-
misc/quadrupole_convergence.png
|
|
105
|
-
misc/quadrupole_optimization.png
|
|
106
|
-
misc/tutorial_nlpoisson_convergence.png
|
|
107
|
-
misc/tutorial_nlpoisson_solution.png
|
|
108
98
|
tests/test_basic.py
|
|
109
99
|
tests/test_block.py
|
|
110
100
|
tests/test_derivatives.py
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastLSQ
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.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
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://github.com/sulcantonin/FastLSQ
|
|
8
|
+
Project-URL: Repository, https://github.com/sulcantonin/FastLSQ
|
|
9
9
|
Project-URL: Paper, https://arxiv.org/abs/2602.10541
|
|
10
|
-
Project-URL: Bug Tracker, https://github.com/
|
|
11
|
-
Project-URL: Changelog, https://github.com/
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/sulcantonin/FastLSQ/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/sulcantonin/FastLSQ/blob/main/CHANGELOG.md
|
|
12
12
|
Keywords: pde,partial-differential-equations,fourier-features,least-squares,scientific-computing,neural-network,physics-informed,newton-raphson
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
<p align="center">
|
|
48
|
-
<img src="misc/fastlsq_teaser.png" alt="FastLSQ method overview" width="400"/>
|
|
48
|
+
<img src="https://raw.githubusercontent.com/sulcantonin/FastLSQ/main/misc/fastlsq_teaser.png" alt="FastLSQ method overview" width="400"/>
|
|
49
49
|
</p>
|
|
50
50
|
|
|
51
51
|
**Solving PDEs in one shot via Fourier features with exact analytical derivatives.**
|
|
@@ -235,8 +235,8 @@ python examples/learnable_helmholtz.py
|
|
|
235
235
|
The analytical derivatives enable gradients through the pre-factored solve, making inverse problems tractable. Example: recovering 4 anisotropic Gaussian heat sources (24 parameters) from 4 sparse sensors. The heat equation is solved in space-time; L-BFGS-B optimises source positions and shapes to match sensor time-series. *(Click image for animation.)*
|
|
236
236
|
|
|
237
237
|
<p align="center">
|
|
238
|
-
<a href="misc/inverse_heat_source.gif">
|
|
239
|
-
<img src="misc/inverse_heat_source.png" alt="Inverse heat source localisation" width="700"/>
|
|
238
|
+
<a href="https://raw.githubusercontent.com/sulcantonin/FastLSQ/main/misc/inverse_heat_source.gif">
|
|
239
|
+
<img src="https://raw.githubusercontent.com/sulcantonin/FastLSQ/main/misc/inverse_heat_source.png" alt="Inverse heat source localisation" width="700"/>
|
|
240
240
|
</a>
|
|
241
241
|
</p>
|
|
242
242
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="misc/fastlsq_teaser.png" alt="FastLSQ method overview" width="400"/>
|
|
7
|
+
<img src="https://raw.githubusercontent.com/sulcantonin/FastLSQ/main/misc/fastlsq_teaser.png" alt="FastLSQ method overview" width="400"/>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
**Solving PDEs in one shot via Fourier features with exact analytical derivatives.**
|
|
@@ -194,8 +194,8 @@ python examples/learnable_helmholtz.py
|
|
|
194
194
|
The analytical derivatives enable gradients through the pre-factored solve, making inverse problems tractable. Example: recovering 4 anisotropic Gaussian heat sources (24 parameters) from 4 sparse sensors. The heat equation is solved in space-time; L-BFGS-B optimises source positions and shapes to match sensor time-series. *(Click image for animation.)*
|
|
195
195
|
|
|
196
196
|
<p align="center">
|
|
197
|
-
<a href="misc/inverse_heat_source.gif">
|
|
198
|
-
<img src="misc/inverse_heat_source.png" alt="Inverse heat source localisation" width="700"/>
|
|
197
|
+
<a href="https://raw.githubusercontent.com/sulcantonin/FastLSQ/main/misc/inverse_heat_source.gif">
|
|
198
|
+
<img src="https://raw.githubusercontent.com/sulcantonin/FastLSQ/main/misc/inverse_heat_source.png" alt="Inverse heat source localisation" width="700"/>
|
|
199
199
|
</a>
|
|
200
200
|
</p>
|
|
201
201
|
|
|
@@ -31,7 +31,6 @@ import sys
|
|
|
31
31
|
import time
|
|
32
32
|
import numpy as np
|
|
33
33
|
import torch
|
|
34
|
-
from scipy.linalg import cho_factor, cho_solve
|
|
35
34
|
|
|
36
35
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
37
36
|
from fastlsq.basis import SinusoidalBasis # noqa: E402
|
|
@@ -166,10 +165,13 @@ def assemble(basis: SinusoidalBasis, pts_int: torch.Tensor):
|
|
|
166
165
|
def solve(A, b):
|
|
167
166
|
A64 = A.astype(np.float64, copy=False)
|
|
168
167
|
b64 = b.astype(np.float64, copy=False)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
# Rank-revealing least squares. Forming the normal equations A^T A (+ridge)
|
|
169
|
+
# and Cholesky-factoring them squares the condition number of this
|
|
170
|
+
# random-feature system, which made cho_factor fail ("not positive
|
|
171
|
+
# definite"); lstsq solves min ||A x - b|| directly via SVD and needs no
|
|
172
|
+
# positive-definiteness.
|
|
173
|
+
beta, *_ = np.linalg.lstsq(A64, b64, rcond=None)
|
|
174
|
+
return beta
|
|
173
175
|
|
|
174
176
|
|
|
175
177
|
# ---------------------------------------------------------------------------
|
|
@@ -174,8 +174,8 @@ def solve_nonlinear(
|
|
|
174
174
|
n_bc: int = 1000,
|
|
175
175
|
n_test: int = 5000,
|
|
176
176
|
max_iter: int = 30,
|
|
177
|
-
tol_res: float = 1e-
|
|
178
|
-
tol_du: float = 1e-
|
|
177
|
+
tol_res: float = 1e-8,
|
|
178
|
+
tol_du: float = 1e-10,
|
|
179
179
|
damping: float = 1.0,
|
|
180
180
|
mu: float = 1e-10,
|
|
181
181
|
auto_scale: bool = True,
|
|
@@ -264,9 +264,11 @@ def solve_nonlinear(
|
|
|
264
264
|
# Check for continuation
|
|
265
265
|
if getattr(problem, "use_continuation", False):
|
|
266
266
|
schedule = list(problem.continuation_schedule)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
nu_target = getattr(problem, "nu_target", None)
|
|
268
|
+
if nu_target is not None:
|
|
269
|
+
if schedule[-1] != nu_target:
|
|
270
|
+
schedule.append(nu_target)
|
|
271
|
+
schedule = [v for v in schedule if v >= nu_target]
|
|
270
272
|
|
|
271
273
|
history = continuation_solve(
|
|
272
274
|
solver, problem, x_pde, bcs, f_pde,
|
|
@@ -172,6 +172,11 @@ class SinusoidalBasis:
|
|
|
172
172
|
|
|
173
173
|
def cache(self, x: torch.Tensor) -> BasisCache:
|
|
174
174
|
"""Create a cache for the given collocation points."""
|
|
175
|
+
# Accept inputs in any dtype/device (e.g. float32 from user code) and
|
|
176
|
+
# promote to the basis's own dtype/device so ``x @ self.W`` never trips
|
|
177
|
+
# a float32-vs-float64 mismatch.
|
|
178
|
+
if x.dtype != self.W.dtype or x.device != self.W.device:
|
|
179
|
+
x = x.to(dtype=self.W.dtype, device=self.W.device)
|
|
175
180
|
return BasisCache(x @ self.W + self.b)
|
|
176
181
|
|
|
177
182
|
# ------------------------------------------------------------------
|
|
@@ -164,7 +164,10 @@ def load_checkpoint(
|
|
|
164
164
|
solver : FastLSQSolver
|
|
165
165
|
metadata : dict, optional
|
|
166
166
|
"""
|
|
167
|
-
|
|
167
|
+
# weights_only=False: save_checkpoint writes NumPy arrays (see to_dict),
|
|
168
|
+
# which torch>=2.6's default weights_only=True refuses to unpickle. The
|
|
169
|
+
# file is produced by this library, so it is trusted.
|
|
170
|
+
state = torch.load(path, map_location=device, weights_only=False)
|
|
168
171
|
metadata = state.pop("metadata", None)
|
|
169
172
|
solver = from_dict(state, device=device)
|
|
170
173
|
return solver, metadata
|
|
@@ -180,19 +180,26 @@ class LearnableFastLSQ(nn.Module):
|
|
|
180
180
|
rcond: float = 1e-12):
|
|
181
181
|
"""Differentiable rank-revealing inner solve.
|
|
182
182
|
|
|
183
|
-
Solves ``beta* = argmin ||A beta - b||^2 + mu ||beta||^2`` through
|
|
184
|
-
|
|
185
|
-
``L`` *and* the solve is stable when ``A``
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
Solves ``beta* = argmin ||A beta - b||^2 + mu ||beta||^2`` through the
|
|
184
|
+
SVD-based ``gelsd`` least-squares driver with ``rcond`` truncation, so
|
|
185
|
+
gradients still flow back to ``L`` *and* the solve is stable when ``A``
|
|
186
|
+
is rank-deficient. (The ``rcond`` cut suppresses the near-null space,
|
|
187
|
+
and ``gelsd``'s backward uses the stable pseudoinverse formula rather
|
|
188
|
+
than per-singular-vector derivatives -- which is what keeps the outer
|
|
189
|
+
AdamW loop's gradients finite. A plain ``torch.linalg.lstsq`` *without*
|
|
190
|
+
``rcond`` is what amplifies the null space.)
|
|
188
191
|
|
|
189
192
|
For ``n_outputs > 1`` the system is block-stacked: the flat solution is
|
|
190
193
|
kept as ``self._beta_flat`` (shape-compatible with ``A``) for residual
|
|
191
194
|
losses, while ``self.beta`` is reshaped to ``(N, k)`` for prediction.
|
|
192
195
|
"""
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
if mu and mu > 0.0:
|
|
197
|
+
n = A.shape[-1]
|
|
198
|
+
A_aug = torch.cat([A, (mu ** 0.5) * torch.eye(n, dtype=A.dtype, device=A.device)], dim=0)
|
|
199
|
+
b_aug = torch.cat([b, torch.zeros(n, b.shape[-1], dtype=b.dtype, device=b.device)], dim=0)
|
|
200
|
+
beta_flat = torch.linalg.lstsq(A_aug, b_aug, rcond=rcond, driver="gelsd").solution
|
|
201
|
+
else:
|
|
202
|
+
beta_flat = torch.linalg.lstsq(A, b, rcond=rcond, driver="gelsd").solution
|
|
196
203
|
self._beta_flat = beta_flat
|
|
197
204
|
if self.n_outputs > 1:
|
|
198
205
|
self.beta = unpack_beta(beta_flat, self.n_features, self.n_outputs)
|
|
@@ -92,7 +92,7 @@ def _auto_solve(A, b, mu, rcond):
|
|
|
92
92
|
try:
|
|
93
93
|
x, L = _cholesky_solve(A, b, mu)
|
|
94
94
|
d = torch.diagonal(L).abs()
|
|
95
|
-
if torch.isfinite(d).all() and d.min() > (rcond ** 0.
|
|
95
|
+
if torch.isfinite(d).all() and d.min() > (rcond ** 0.25) * d.max():
|
|
96
96
|
return x
|
|
97
97
|
except torch.linalg.LinAlgError:
|
|
98
98
|
pass
|
|
@@ -87,10 +87,13 @@ def newton_solve(solver, problem, x_pde, bcs, f_pde,
|
|
|
87
87
|
history = []
|
|
88
88
|
n_outputs = getattr(problem, "n_outputs", 1)
|
|
89
89
|
N = solver.n_features
|
|
90
|
+
R0 = None
|
|
90
91
|
|
|
91
92
|
for it in range(max_iter):
|
|
92
93
|
J, neg_R = problem.build_newton_step(solver, x_pde, bcs, f_pde)
|
|
93
94
|
res_norm = torch.norm(neg_R).item()
|
|
95
|
+
if R0 is None:
|
|
96
|
+
R0 = max(res_norm, 1e-30)
|
|
94
97
|
|
|
95
98
|
delta_beta_raw = solve_lstsq(J, neg_R, mu=mu)
|
|
96
99
|
delta_beta = unpack_beta(delta_beta_raw, N, n_outputs)
|
|
@@ -116,7 +119,10 @@ def newton_solve(solver, problem, x_pde, bcs, f_pde,
|
|
|
116
119
|
break
|
|
117
120
|
alpha *= 0.5
|
|
118
121
|
else:
|
|
119
|
-
|
|
122
|
+
# No backtracked step satisfied the Armijo condition; reject the
|
|
123
|
+
# step and keep the previous iterate rather than committing a
|
|
124
|
+
# point that may be worse than where we started.
|
|
125
|
+
solver.beta = beta_old
|
|
120
126
|
|
|
121
127
|
history.append({
|
|
122
128
|
"iter": it, "residual": res_norm,
|
|
@@ -128,7 +134,7 @@ def newton_solve(solver, problem, x_pde, bcs, f_pde,
|
|
|
128
134
|
print(f" Newton {it:2d}: |R|={res_norm:.2e} "
|
|
129
135
|
f"|du|/|u|={rel_du:.2e} alpha={alpha:.3f}")
|
|
130
136
|
|
|
131
|
-
if res_norm < tol_res
|
|
137
|
+
if res_norm < tol_res * R0 or rel_du < tol_du:
|
|
132
138
|
if verbose:
|
|
133
139
|
print(f" Converged in {it + 1} iterations "
|
|
134
140
|
f"(|R|={res_norm:.1e}, |du|/|u|={rel_du:.1e})")
|
|
@@ -392,13 +392,13 @@ class ElasticWave2D:
|
|
|
392
392
|
# t is normalised to [0,1]; physical d²/dt² = (1/t_max)² d²/dτ²
|
|
393
393
|
t_scale = self.t_max ** 2
|
|
394
394
|
|
|
395
|
-
# PDE1:
|
|
396
|
-
A1_x = t_scale *
|
|
397
|
-
A1_y = -self.c_cross * u_xy
|
|
395
|
+
# PDE1: u_x_ττ = t_max²·(c_p² u_x_xx + c_s² u_x_yy + (c_p²-c_s²) u_y_xy)
|
|
396
|
+
A1_x = u_tt - t_scale * (self.c_p2 * u_xx + self.c_s2 * u_yy)
|
|
397
|
+
A1_y = -t_scale * self.c_cross * u_xy
|
|
398
398
|
|
|
399
|
-
# PDE2:
|
|
400
|
-
A2_x = -self.c_cross * u_xy
|
|
401
|
-
A2_y = t_scale *
|
|
399
|
+
# PDE2: u_y_ττ = t_max²·(c_p² u_y_yy + c_s² u_y_xx + (c_p²-c_s²) u_x_xy)
|
|
400
|
+
A2_x = -t_scale * self.c_cross * u_xy
|
|
401
|
+
A2_y = u_tt - t_scale * (self.c_p2 * u_yy + self.c_s2 * u_xx)
|
|
402
402
|
|
|
403
403
|
A_pde = torch.cat([
|
|
404
404
|
torch.cat([A1_x, A1_y], dim=1),
|
|
@@ -17,7 +17,7 @@ Each class provides:
|
|
|
17
17
|
import torch
|
|
18
18
|
import numpy as np
|
|
19
19
|
|
|
20
|
-
from fastlsq.
|
|
20
|
+
from fastlsq.device import get_device
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
# ======================================================================
|
|
@@ -27,9 +27,9 @@ from fastlsq.utils import device
|
|
|
27
27
|
def _unit_square_boundary(n_bc):
|
|
28
28
|
"""Generate n_bc random points on the boundary of [0,1]^2."""
|
|
29
29
|
n_side = n_bc // 4
|
|
30
|
-
r = lambda n: torch.rand(n, 1, device=
|
|
31
|
-
z = lambda n: torch.zeros(n, 1, device=
|
|
32
|
-
o = lambda n: torch.ones(n, 1, device=
|
|
30
|
+
r = lambda n: torch.rand(n, 1, device=get_device())
|
|
31
|
+
z = lambda n: torch.zeros(n, 1, device=get_device())
|
|
32
|
+
o = lambda n: torch.ones(n, 1, device=get_device())
|
|
33
33
|
return torch.cat([
|
|
34
34
|
torch.cat([z(n_side), r(n_side)], 1),
|
|
35
35
|
torch.cat([o(n_side), r(n_side)], 1),
|
|
@@ -68,7 +68,7 @@ class NLPoisson2D:
|
|
|
68
68
|
return 2 * np.pi ** 2 * u + u ** 3
|
|
69
69
|
|
|
70
70
|
def get_train_data(self, n_pde=5000, n_bc=1000):
|
|
71
|
-
x_pde = torch.rand(n_pde, 2, device=
|
|
71
|
+
x_pde = torch.rand(n_pde, 2, device=get_device())
|
|
72
72
|
f_pde = self.source(x_pde)
|
|
73
73
|
x_bc = _unit_square_boundary(n_bc)
|
|
74
74
|
u_bc = self.exact(x_bc)
|
|
@@ -102,7 +102,7 @@ class NLPoisson2D:
|
|
|
102
102
|
return torch.cat(rows_A, 0), torch.cat(rows_b, 0)
|
|
103
103
|
|
|
104
104
|
def get_test_points(self, n=5000):
|
|
105
|
-
return torch.rand(n, 2, device=
|
|
105
|
+
return torch.rand(n, 2, device=get_device())
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
# ======================================================================
|
|
@@ -136,7 +136,7 @@ class Bratu2D:
|
|
|
136
136
|
return 2 * np.pi ** 2 * u - self.lam * torch.exp(u)
|
|
137
137
|
|
|
138
138
|
def get_train_data(self, n_pde=5000, n_bc=1000):
|
|
139
|
-
x_pde = torch.rand(n_pde, 2, device=
|
|
139
|
+
x_pde = torch.rand(n_pde, 2, device=get_device())
|
|
140
140
|
f_pde = self.source(x_pde)
|
|
141
141
|
x_bc = _unit_square_boundary(n_bc)
|
|
142
142
|
u_bc = self.exact(x_bc)
|
|
@@ -171,7 +171,7 @@ class Bratu2D:
|
|
|
171
171
|
return torch.cat(rows_A, 0), torch.cat(rows_b, 0)
|
|
172
172
|
|
|
173
173
|
def get_test_points(self, n=5000):
|
|
174
|
-
return torch.rand(n, 2, device=
|
|
174
|
+
return torch.rand(n, 2, device=get_device())
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
# ======================================================================
|
|
@@ -207,13 +207,13 @@ class SteadyBurgers1D:
|
|
|
207
207
|
return u * ux - self.nu * uxx
|
|
208
208
|
|
|
209
209
|
def get_train_data(self, n_pde=3000, n_bc=200):
|
|
210
|
-
x_pde = torch.rand(n_pde, 1, device=
|
|
210
|
+
x_pde = torch.rand(n_pde, 1, device=get_device())
|
|
211
211
|
f_pde = self.source(x_pde)
|
|
212
212
|
x_bc = torch.cat([
|
|
213
|
-
torch.zeros(n_bc // 2, 1, device=
|
|
214
|
-
torch.ones(n_bc // 2, 1, device=
|
|
213
|
+
torch.zeros(n_bc // 2, 1, device=get_device()),
|
|
214
|
+
torch.ones(n_bc // 2, 1, device=get_device()),
|
|
215
215
|
], 0)
|
|
216
|
-
u_bc = torch.zeros(n_bc, 1, device=
|
|
216
|
+
u_bc = torch.zeros(n_bc, 1, device=get_device())
|
|
217
217
|
return x_pde, [(x_bc, u_bc)], f_pde
|
|
218
218
|
|
|
219
219
|
def build_newton_step(self, solver, x_pde, bcs, f_pde):
|
|
@@ -249,7 +249,7 @@ class SteadyBurgers1D:
|
|
|
249
249
|
return torch.cat(rows_A, 0), torch.cat(rows_b, 0)
|
|
250
250
|
|
|
251
251
|
def get_test_points(self, n=5000):
|
|
252
|
-
return torch.rand(n, 1, device=
|
|
252
|
+
return torch.rand(n, 1, device=get_device())
|
|
253
253
|
|
|
254
254
|
|
|
255
255
|
# ======================================================================
|
|
@@ -285,7 +285,7 @@ class NLHelmholtz2D:
|
|
|
285
285
|
return -self.k ** 2 * u + self.alpha * u ** 3
|
|
286
286
|
|
|
287
287
|
def get_train_data(self, n_pde=5000, n_bc=1000):
|
|
288
|
-
x_pde = torch.rand(n_pde, 2, device=
|
|
288
|
+
x_pde = torch.rand(n_pde, 2, device=get_device())
|
|
289
289
|
f_pde = self.source(x_pde)
|
|
290
290
|
x_bc = _unit_square_boundary(n_bc)
|
|
291
291
|
u_bc = self.exact(x_bc)
|
|
@@ -321,7 +321,7 @@ class NLHelmholtz2D:
|
|
|
321
321
|
return torch.cat(rows_A, 0), torch.cat(rows_b, 0)
|
|
322
322
|
|
|
323
323
|
def get_test_points(self, n=5000):
|
|
324
|
-
return torch.rand(n, 2, device=
|
|
324
|
+
return torch.rand(n, 2, device=get_device())
|
|
325
325
|
|
|
326
326
|
|
|
327
327
|
# ======================================================================
|
|
@@ -352,13 +352,13 @@ class AllenCahn1D:
|
|
|
352
352
|
return self.eps * uxx + u - u ** 3
|
|
353
353
|
|
|
354
354
|
def get_train_data(self, n_pde=3000, n_bc=200):
|
|
355
|
-
x_pde = torch.rand(n_pde, 1, device=
|
|
355
|
+
x_pde = torch.rand(n_pde, 1, device=get_device())
|
|
356
356
|
f_pde = self.source(x_pde)
|
|
357
357
|
x_bc = torch.cat([
|
|
358
|
-
torch.zeros(n_bc // 2, 1, device=
|
|
359
|
-
torch.ones(n_bc // 2, 1, device=
|
|
358
|
+
torch.zeros(n_bc // 2, 1, device=get_device()),
|
|
359
|
+
torch.ones(n_bc // 2, 1, device=get_device()),
|
|
360
360
|
], 0)
|
|
361
|
-
u_bc = torch.zeros(n_bc, 1, device=
|
|
361
|
+
u_bc = torch.zeros(n_bc, 1, device=get_device())
|
|
362
362
|
return x_pde, [(x_bc, u_bc)], f_pde
|
|
363
363
|
|
|
364
364
|
def build_newton_step(self, solver, x_pde, bcs, f_pde):
|
|
@@ -393,4 +393,4 @@ class AllenCahn1D:
|
|
|
393
393
|
return torch.cat(rows_A, 0), torch.cat(rows_b, 0)
|
|
394
394
|
|
|
395
395
|
def get_test_points(self, n=5000):
|
|
396
|
-
return torch.rand(n, 1, device=
|
|
396
|
+
return torch.rand(n, 1, device=get_device())
|
|
@@ -14,7 +14,7 @@ avoid code duplication.
|
|
|
14
14
|
import torch
|
|
15
15
|
import numpy as np
|
|
16
16
|
|
|
17
|
-
from fastlsq.
|
|
17
|
+
from fastlsq.device import get_device
|
|
18
18
|
from fastlsq.problems.nonlinear import Bratu2D, NLHelmholtz2D
|
|
19
19
|
|
|
20
20
|
|
|
@@ -60,8 +60,8 @@ class Burgers1D_Regression:
|
|
|
60
60
|
dz_dt = -0.5 / (4 * self.nu)
|
|
61
61
|
return torch.cat([du_dz * dz_dx, du_dz * dz_dt], dim=1)
|
|
62
62
|
|
|
63
|
-
def get_train_data(self,
|
|
64
|
-
x_pde = torch.rand(
|
|
63
|
+
def get_train_data(self, n_pde=5000, n_bc=0):
|
|
64
|
+
x_pde = torch.rand(n_pde, 2, device=get_device())
|
|
65
65
|
u_true = self.exact(x_pde)
|
|
66
66
|
return x_pde, [(x_pde, u_true, "data_fit")]
|
|
67
67
|
|
|
@@ -69,7 +69,7 @@ class Burgers1D_Regression:
|
|
|
69
69
|
return _regression_build(slv, x_pde, bcs)
|
|
70
70
|
|
|
71
71
|
def get_test_points(self, n=10000):
|
|
72
|
-
return torch.rand(n, self.dim, device=
|
|
72
|
+
return torch.rand(n, self.dim, device=get_device())
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
# ======================================================================
|
|
@@ -106,9 +106,9 @@ class KdV_Regression:
|
|
|
106
106
|
k = sqrt_c / 2.0
|
|
107
107
|
return torch.cat([du_dz * k, du_dz * (-k * self.c)], dim=1)
|
|
108
108
|
|
|
109
|
-
def get_train_data(self,
|
|
110
|
-
x_space = torch.rand(
|
|
111
|
-
t_time = torch.rand(
|
|
109
|
+
def get_train_data(self, n_pde=5000, n_bc=0):
|
|
110
|
+
x_space = torch.rand(n_pde, 1, device=get_device()) * 4 - 2
|
|
111
|
+
t_time = torch.rand(n_pde, 1, device=get_device()) * 0.1
|
|
112
112
|
x_pde = torch.cat([x_space, t_time], dim=1)
|
|
113
113
|
u_true = self.exact(x_pde)
|
|
114
114
|
return x_pde, [(x_pde, u_true, "data_fit")]
|
|
@@ -117,8 +117,8 @@ class KdV_Regression:
|
|
|
117
117
|
return _regression_build(slv, x_pde, bcs)
|
|
118
118
|
|
|
119
119
|
def get_test_points(self, n=10000):
|
|
120
|
-
x_space = torch.rand(n, 1, device=
|
|
121
|
-
t_time = torch.rand(n, 1, device=
|
|
120
|
+
x_space = torch.rand(n, 1, device=get_device()) * 4 - 2
|
|
121
|
+
t_time = torch.rand(n, 1, device=get_device()) * 0.1
|
|
122
122
|
return torch.cat([x_space, t_time], dim=1)
|
|
123
123
|
|
|
124
124
|
|
|
@@ -153,9 +153,9 @@ class ReactionDiffusion_Regression:
|
|
|
153
153
|
du_dz = -2.0 * ((1.0 + E).pow(-3)) * E
|
|
154
154
|
return torch.cat([du_dz * alpha, du_dz * (-alpha * c)], dim=1)
|
|
155
155
|
|
|
156
|
-
def get_train_data(self,
|
|
157
|
-
x_space = torch.rand(
|
|
158
|
-
t_time = torch.rand(
|
|
156
|
+
def get_train_data(self, n_pde=5000, n_bc=0):
|
|
157
|
+
x_space = torch.rand(n_pde, 1, device=get_device()) * 20 - 10
|
|
158
|
+
t_time = torch.rand(n_pde, 1, device=get_device())
|
|
159
159
|
x_pde = torch.cat([x_space, t_time], dim=1)
|
|
160
160
|
u_true = self.exact(x_pde)
|
|
161
161
|
return x_pde, [(x_pde, u_true, "data_fit")]
|
|
@@ -164,8 +164,8 @@ class ReactionDiffusion_Regression:
|
|
|
164
164
|
return _regression_build(slv, x_pde, bcs)
|
|
165
165
|
|
|
166
166
|
def get_test_points(self, n=10000):
|
|
167
|
-
x_space = torch.rand(n, 1, device=
|
|
168
|
-
t_time = torch.rand(n, 1, device=
|
|
167
|
+
x_space = torch.rand(n, 1, device=get_device()) * 20 - 10
|
|
168
|
+
t_time = torch.rand(n, 1, device=get_device())
|
|
169
169
|
return torch.cat([x_space, t_time], dim=1)
|
|
170
170
|
|
|
171
171
|
|
|
@@ -205,9 +205,9 @@ class SineGordon_Regression:
|
|
|
205
205
|
dA_dt = (1.0 / denom) * (k * w * cos_wt)
|
|
206
206
|
return torch.cat([du_dA * dA_dx, du_dA * dA_dt], dim=1)
|
|
207
207
|
|
|
208
|
-
def get_train_data(self,
|
|
209
|
-
x_space = torch.rand(
|
|
210
|
-
t_time = torch.rand(
|
|
208
|
+
def get_train_data(self, n_pde=5000, n_bc=0):
|
|
209
|
+
x_space = torch.rand(n_pde, 1, device=get_device()) * 20 - 10
|
|
210
|
+
t_time = torch.rand(n_pde, 1, device=get_device()) * 20
|
|
211
211
|
x_pde = torch.cat([x_space, t_time], dim=1)
|
|
212
212
|
u_true = self.exact(x_pde)
|
|
213
213
|
return x_pde, [(x_pde, u_true, "data_fit")]
|
|
@@ -216,8 +216,8 @@ class SineGordon_Regression:
|
|
|
216
216
|
return _regression_build(slv, x_pde, bcs)
|
|
217
217
|
|
|
218
218
|
def get_test_points(self, n=2000):
|
|
219
|
-
x_space = torch.rand(n, 1, device=
|
|
220
|
-
t_time = torch.rand(n, 1, device=
|
|
219
|
+
x_space = torch.rand(n, 1, device=get_device()) * 20 - 10
|
|
220
|
+
t_time = torch.rand(n, 1, device=get_device()) * 20
|
|
221
221
|
return torch.cat([x_space, t_time], dim=1)
|
|
222
222
|
|
|
223
223
|
|
|
@@ -245,9 +245,9 @@ class KleinGordon_Regression:
|
|
|
245
245
|
du_dt = -2 * np.pi * torch.sin(np.pi * xv) * torch.sin(2 * np.pi * tv)
|
|
246
246
|
return torch.cat([du_dx, du_dt], dim=1)
|
|
247
247
|
|
|
248
|
-
def get_train_data(self,
|
|
249
|
-
x_space = torch.rand(
|
|
250
|
-
t_time = torch.rand(
|
|
248
|
+
def get_train_data(self, n_pde=5000, n_bc=0):
|
|
249
|
+
x_space = torch.rand(n_pde, 1, device=get_device()) * 2 - 1
|
|
250
|
+
t_time = torch.rand(n_pde, 1, device=get_device())
|
|
251
251
|
x_pde = torch.cat([x_space, t_time], dim=1)
|
|
252
252
|
u_true = self.exact(x_pde)
|
|
253
253
|
return x_pde, [(x_pde, u_true, "data_fit")]
|
|
@@ -256,8 +256,8 @@ class KleinGordon_Regression:
|
|
|
256
256
|
return _regression_build(slv, x_pde, bcs)
|
|
257
257
|
|
|
258
258
|
def get_test_points(self, n=2000):
|
|
259
|
-
x_space = torch.rand(n, 1, device=
|
|
260
|
-
t_time = torch.rand(n, 1, device=
|
|
259
|
+
x_space = torch.rand(n, 1, device=get_device()) * 2 - 1
|
|
260
|
+
t_time = torch.rand(n, 1, device=get_device())
|
|
261
261
|
return torch.cat([x_space, t_time], dim=1)
|
|
262
262
|
|
|
263
263
|
|
|
@@ -290,9 +290,9 @@ class NavierStokes2D_Kovasznay:
|
|
|
290
290
|
du_dy = 2 * np.pi * exp_term * sin_term
|
|
291
291
|
return torch.cat([du_dx, du_dy], dim=1)
|
|
292
292
|
|
|
293
|
-
def get_train_data(self,
|
|
294
|
-
x_space = torch.rand(
|
|
295
|
-
y_space = torch.rand(
|
|
293
|
+
def get_train_data(self, n_pde=5000, n_bc=0):
|
|
294
|
+
x_space = torch.rand(n_pde, 1, device=get_device()) * 1.5 - 0.5
|
|
295
|
+
y_space = torch.rand(n_pde, 1, device=get_device()) * 2.0 - 0.5
|
|
296
296
|
x_pde = torch.cat([x_space, y_space], dim=1)
|
|
297
297
|
u_true = self.exact(x_pde)
|
|
298
298
|
return x_pde, [(x_pde, u_true, "data_fit")]
|
|
@@ -301,8 +301,8 @@ class NavierStokes2D_Kovasznay:
|
|
|
301
301
|
return _regression_build(slv, x_pde, bcs)
|
|
302
302
|
|
|
303
303
|
def get_test_points(self, n=2000):
|
|
304
|
-
x_space = torch.rand(n, 1, device=
|
|
305
|
-
y_space = torch.rand(n, 1, device=
|
|
304
|
+
x_space = torch.rand(n, 1, device=get_device()) * 1.5 - 0.5
|
|
305
|
+
y_space = torch.rand(n, 1, device=get_device()) * 2.0 - 0.5
|
|
306
306
|
return torch.cat([x_space, y_space], dim=1)
|
|
307
307
|
|
|
308
308
|
|
|
@@ -332,8 +332,8 @@ class GrayScott_Pulse:
|
|
|
332
332
|
darg_dt = 2 * self.c * (xv - self.c * tv) / self.sigma
|
|
333
333
|
return torch.cat([u * darg_dx, u * darg_dt], dim=1)
|
|
334
334
|
|
|
335
|
-
def get_train_data(self,
|
|
336
|
-
x_pde = torch.rand(
|
|
335
|
+
def get_train_data(self, n_pde=5000, n_bc=0):
|
|
336
|
+
x_pde = torch.rand(n_pde, 2, device=get_device())
|
|
337
337
|
u_true = self.exact(x_pde)
|
|
338
338
|
return x_pde, [(x_pde, u_true, "data_fit")]
|
|
339
339
|
|
|
@@ -341,7 +341,7 @@ class GrayScott_Pulse:
|
|
|
341
341
|
return _regression_build(slv, x_pde, bcs)
|
|
342
342
|
|
|
343
343
|
def get_test_points(self, n=2000):
|
|
344
|
-
return torch.rand(n, 2, device=
|
|
344
|
+
return torch.rand(n, 2, device=get_device())
|
|
345
345
|
|
|
346
346
|
|
|
347
347
|
# ======================================================================
|
|
@@ -358,8 +358,8 @@ class Bratu2D_Regression(Bratu2D):
|
|
|
358
358
|
super().__init__(lam=1.0)
|
|
359
359
|
self.name = "Bratu 2D (Reg)"
|
|
360
360
|
|
|
361
|
-
def get_train_data(self,
|
|
362
|
-
x_pde = torch.rand(
|
|
361
|
+
def get_train_data(self, n_pde=5000, n_bc=0):
|
|
362
|
+
x_pde = torch.rand(n_pde, 2, device=get_device())
|
|
363
363
|
u_true = self.exact(x_pde)
|
|
364
364
|
return x_pde, [(x_pde, u_true, "data_fit")]
|
|
365
365
|
|
|
@@ -367,7 +367,7 @@ class Bratu2D_Regression(Bratu2D):
|
|
|
367
367
|
return _regression_build(slv, x_pde, bcs)
|
|
368
368
|
|
|
369
369
|
def get_test_points(self, n=5000):
|
|
370
|
-
return torch.rand(n, 2, device=
|
|
370
|
+
return torch.rand(n, 2, device=get_device())
|
|
371
371
|
|
|
372
372
|
|
|
373
373
|
# ======================================================================
|
|
@@ -385,8 +385,8 @@ class NLHelmholtz2D_Regression(NLHelmholtz2D):
|
|
|
385
385
|
super().__init__(k=3.0, alpha=0.5)
|
|
386
386
|
self.name = "NL-Helmholtz (Reg)"
|
|
387
387
|
|
|
388
|
-
def get_train_data(self,
|
|
389
|
-
x_pde = torch.rand(
|
|
388
|
+
def get_train_data(self, n_pde=5000, n_bc=0):
|
|
389
|
+
x_pde = torch.rand(n_pde, 2, device=get_device())
|
|
390
390
|
u_true = self.exact(x_pde)
|
|
391
391
|
return x_pde, [(x_pde, u_true, "data_fit")]
|
|
392
392
|
|
|
@@ -394,4 +394,4 @@ class NLHelmholtz2D_Regression(NLHelmholtz2D):
|
|
|
394
394
|
return _regression_build(slv, x_pde, bcs)
|
|
395
395
|
|
|
396
396
|
def get_test_points(self, n=5000):
|
|
397
|
-
return torch.rand(n, 2, device=
|
|
397
|
+
return torch.rand(n, 2, device=get_device())
|
|
@@ -58,6 +58,7 @@ def auto_select_scale(
|
|
|
58
58
|
|
|
59
59
|
best_scale = scales[0]
|
|
60
60
|
best_error = float("inf")
|
|
61
|
+
last_exc = None
|
|
61
62
|
n_outputs = getattr(problem, "n_outputs", 1)
|
|
62
63
|
|
|
63
64
|
for scale in scales:
|
|
@@ -108,7 +109,8 @@ def auto_select_scale(
|
|
|
108
109
|
if np.isnan(val_err) or np.isinf(val_err):
|
|
109
110
|
val_err = 1e10
|
|
110
111
|
errors.append(val_err)
|
|
111
|
-
except Exception:
|
|
112
|
+
except Exception as e:
|
|
113
|
+
last_exc = e
|
|
112
114
|
errors.append(1e10)
|
|
113
115
|
|
|
114
116
|
mean_error = np.mean(errors)
|
|
@@ -119,4 +121,10 @@ def auto_select_scale(
|
|
|
119
121
|
best_error = mean_error
|
|
120
122
|
best_scale = scale
|
|
121
123
|
|
|
124
|
+
if best_error >= 1e10:
|
|
125
|
+
msg = ("auto_select_scale: no scale produced a finite error; every "
|
|
126
|
+
"trial failed or diverged")
|
|
127
|
+
if last_exc is not None:
|
|
128
|
+
msg += f" (last exception: {last_exc!r})"
|
|
129
|
+
raise RuntimeError(msg)
|
|
122
130
|
return best_scale
|
|
@@ -356,11 +356,11 @@ class VectorFastLSQSolver:
|
|
|
356
356
|
):
|
|
357
357
|
"""Append a feature block to every component.
|
|
358
358
|
|
|
359
|
-
If `scale` is a list/tuple of length ``n_components``, each
|
|
359
|
+
If `scale` is a list/tuple/ndarray of length ``n_components``, each
|
|
360
360
|
component is scaled independently. Otherwise the same scale
|
|
361
361
|
is shared.
|
|
362
362
|
"""
|
|
363
|
-
if isinstance(scale, (list, tuple)) and \
|
|
363
|
+
if isinstance(scale, (list, tuple, np.ndarray)) and \
|
|
364
364
|
len(scale) == self._n_components and \
|
|
365
365
|
not isinstance(scale[0], (list, tuple, np.ndarray)):
|
|
366
366
|
scales = list(scale)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "FastLSQ"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.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"
|
|
@@ -55,11 +55,11 @@ lightning = [
|
|
|
55
55
|
]
|
|
56
56
|
|
|
57
57
|
[project.urls]
|
|
58
|
-
Homepage = "https://github.com/
|
|
59
|
-
Repository = "https://github.com/
|
|
58
|
+
Homepage = "https://github.com/sulcantonin/FastLSQ"
|
|
59
|
+
Repository = "https://github.com/sulcantonin/FastLSQ"
|
|
60
60
|
Paper = "https://arxiv.org/abs/2602.10541"
|
|
61
|
-
"Bug Tracker" = "https://github.com/
|
|
62
|
-
Changelog = "https://github.com/
|
|
61
|
+
"Bug Tracker" = "https://github.com/sulcantonin/FastLSQ/issues"
|
|
62
|
+
Changelog = "https://github.com/sulcantonin/FastLSQ/blob/main/CHANGELOG.md"
|
|
63
63
|
|
|
64
64
|
[tool.setuptools.packages.find]
|
|
65
65
|
include = ["fastlsq*"]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|