FastLSQ 0.2.2__tar.gz → 0.2.4__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.2 → fastlsq-0.2.4}/CHANGELOG.md +54 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/FastLSQ.egg-info/PKG-INFO +1 -1
- {fastlsq-0.2.2 → fastlsq-0.2.4}/FastLSQ.egg-info/SOURCES.txt +1 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/PKG-INFO +1 -1
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/__init__.py +1 -1
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/api.py +29 -9
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/linalg.py +46 -7
- {fastlsq-0.2.2 → fastlsq-0.2.4}/pyproject.toml +1 -1
- fastlsq-0.2.4/tests/test_benchmarks_inverse.py +144 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/tests/test_vector_basis.py +1 -1
- {fastlsq-0.2.2 → fastlsq-0.2.4}/FastLSQ.egg-info/dependency_links.txt +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/FastLSQ.egg-info/requires.txt +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/FastLSQ.egg-info/top_level.txt +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/LICENSE +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/MANIFEST.in +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/README.md +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/add_your_own_pde.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/benchmark_comparison.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/custom_features.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/fred_sde.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/fred_sde_fastlsq.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/gaia_potential.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/gaia_potential_fastlsq.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/horizons_ephemeris.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/numerai_alpha.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/numerai_alpha_fastlsq.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/run_all_fastlsq.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/__init__.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/_alsu_lattice.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/_common.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/run_all.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_green_fff.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_passive_loco.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_tides_3months.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s01_visualize.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s04_sunspots.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s05_helioseismology.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s06_tides.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s14_eeg.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/scenarios/s15_circadian.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/extras/spectral_expansion.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/grad_shafranov.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/grid_inverse.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/grid_rl_control.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/grid_swing.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/gs_inverse.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/gs_rl_control.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/inverse_heat_source.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/inverse_magnetostatics.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/inverse_source_position.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/learnable_helmholtz.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/orbit_hill.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/orbit_inverse.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/orbit_rl.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/pde_discovery.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/run_all_extensions.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/run_linear.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/run_nonlinear.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/tutorial_basic.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/tutorial_nonlinear.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/examples/vector_basis_stream_vorticity.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/basis.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/block.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/device.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/diagnostics.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/export.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/geometry.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/learnable.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/lightning.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/newton.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/plotting.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/problems/__init__.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/problems/linear.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/problems/nonlinear.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/problems/regression.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/solvers.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/tuning.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/utils.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/vector.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/fastlsq/viz.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/requirements.txt +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/setup.cfg +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/tests/test_basic.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/tests/test_block.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/tests/test_derivatives.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/tests/test_device.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/tests/test_grad_shafranov.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/tests/test_grid_swing.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/tests/test_learnable.py +0 -0
- {fastlsq-0.2.2 → fastlsq-0.2.4}/tests/test_orbit_hill.py +0 -0
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to FastLSQ will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.4] - 2026-06-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Benchmark + inverse-problem test suite** (`tests/test_benchmarks_inverse.py`):
|
|
10
|
+
12 deterministic smoke tests (~11 s) that solve the linear (`PoissonND`,
|
|
11
|
+
`HeatND`, `Wave1D`, `Helmholtz2D`, `Maxwell2D_TM`) and nonlinear
|
|
12
|
+
(`NLPoisson2D`, `Bratu2D`, `SteadyBurgers1D`, `NLHelmholtz2D`, `AllenCahn1D`)
|
|
13
|
+
benchmark equations through the public `solve_linear` / `solve_nonlinear` API,
|
|
14
|
+
plus two inverse pipelines -- Gaussian source-position recovery (forward solve
|
|
15
|
+
+ L-BFGS) and SINDy-style PDE discovery via analytical derivatives --
|
|
16
|
+
exercising the 0.2.3 QR / N-scaled-collocation solver path end to end.
|
|
17
|
+
|
|
18
|
+
### Known issues
|
|
19
|
+
|
|
20
|
+
- `Wave2D_MS` does not solve via `solve_linear` (relative error 1.0 in every
|
|
21
|
+
configuration tested), and `ElasticWave2D` -- a 2-output vector problem whose
|
|
22
|
+
`exact()` returns `(N, 2)` -- never sets `n_outputs`, so the scalar API cannot
|
|
23
|
+
unpack it. Both are pre-existing problem-definition gaps, independent of the
|
|
24
|
+
solver work, and are excluded from the new smoke test pending a fix.
|
|
25
|
+
|
|
26
|
+
## [0.2.3] - 2026-06-04
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Householder-QR least-squares back-end** `solve_lstsq(..., method="qr")`:
|
|
31
|
+
backward-stable at `cond(A)` (ridge applied via the `[A; sqrt(mu) I]`
|
|
32
|
+
augmentation, not the normal equations), giving SVD-grade accuracy (~1e-14 on
|
|
33
|
+
the Helmholtz random-feature benchmark) at QR cost -- and, on the
|
|
34
|
+
rank-deficient CPU/no-ridge path, faster than the `gelsd` `"svd"` driver too,
|
|
35
|
+
while far more accurate than the normal-equations `"cholesky"` (no `cond(A)`
|
|
36
|
+
squaring, no required ridge). Assumes the system is numerically full column
|
|
37
|
+
rank; `"svd"` remains the rank-deficient-safe reference.
|
|
38
|
+
- **`solve_linear(..., method=...)`**: the linear solve back-end is now
|
|
39
|
+
selectable from the high-level API (`"auto"`, `"qr"`, `"svd"`, `"cholesky"`,
|
|
40
|
+
`"rsvd"`; defaults to `"auto"`).
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **`method="auto"` now tries QR before SVD.** After the Cholesky conditioning
|
|
45
|
+
probe rejects the fast path, `auto` uses the faster, more accurate QR solve and
|
|
46
|
+
falls back to the rank-revealing SVD only when QR's solution blows up
|
|
47
|
+
(`||x|| / (1 + ||b||)` above a generous guard). Real PDE systems measure
|
|
48
|
+
`<= 0.3` and keep QR; genuinely rank-deficient *inconsistent* systems (e.g. a
|
|
49
|
+
random RHS) measure ~3e14 and route to SVD. Net: the default solve is faster
|
|
50
|
+
and at least as accurate on real problems, with minimum-norm SVD preserved
|
|
51
|
+
exactly where it is needed.
|
|
52
|
+
- **N-scaled collocation defaults.** `solve_linear` and `solve_nonlinear` now
|
|
53
|
+
default `n_pde`/`n_bc` to `None` and derive them from the feature count
|
|
54
|
+
(`n_pde = max(3000, 3 * n_blocks * hidden_size)`, `n_bc = max(800, n_pde // 5)`),
|
|
55
|
+
replacing the fixed `10000`/`2000` (and `5000`/`1000`) over-sampling that was
|
|
56
|
+
~6x the default feature count. Faster for the default configuration; passing
|
|
57
|
+
explicit `n_pde`/`n_bc` still overrides.
|
|
58
|
+
|
|
5
59
|
## [0.2.2] - 2026-06-03
|
|
6
60
|
|
|
7
61
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastLSQ
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FastLSQ
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -35,10 +35,11 @@ def solve_linear(
|
|
|
35
35
|
scale: Optional[float] = None,
|
|
36
36
|
n_blocks: int = 3,
|
|
37
37
|
hidden_size: int = 500,
|
|
38
|
-
n_pde: int =
|
|
39
|
-
n_bc: int =
|
|
38
|
+
n_pde: Optional[int] = None,
|
|
39
|
+
n_bc: Optional[int] = None,
|
|
40
40
|
n_test: int = 5000,
|
|
41
41
|
mu: float = 0.0,
|
|
42
|
+
method: str = "auto",
|
|
42
43
|
auto_scale: bool = True,
|
|
43
44
|
auto_scale_trials: int = 5,
|
|
44
45
|
return_solver: bool = False,
|
|
@@ -65,12 +66,17 @@ def solve_linear(
|
|
|
65
66
|
Number of feature blocks.
|
|
66
67
|
hidden_size : int
|
|
67
68
|
Features per block.
|
|
68
|
-
n_pde, n_bc : int
|
|
69
|
-
Number of collocation and boundary points.
|
|
69
|
+
n_pde, n_bc : int, optional
|
|
70
|
+
Number of collocation and boundary points. If None, scaled with the
|
|
71
|
+
feature count: n_pde = max(3000, 3 * n_blocks * hidden_size),
|
|
72
|
+
n_bc = max(800, n_pde // 5).
|
|
70
73
|
n_test : int
|
|
71
74
|
Number of test points for error evaluation.
|
|
72
75
|
mu : float
|
|
73
76
|
Tikhonov regularisation parameter (0 = no regularisation).
|
|
77
|
+
method : str
|
|
78
|
+
Linear solve back-end passed to ``solve_lstsq`` ("auto", "qr", "svd",
|
|
79
|
+
"cholesky", "rsvd"). Default "auto".
|
|
74
80
|
auto_scale : bool
|
|
75
81
|
If True and scale=None, automatically select scale via grid search.
|
|
76
82
|
auto_scale_trials : int
|
|
@@ -93,6 +99,12 @@ def solve_linear(
|
|
|
93
99
|
"""
|
|
94
100
|
t0 = time.time()
|
|
95
101
|
|
|
102
|
+
n_feat = n_blocks * hidden_size
|
|
103
|
+
if n_pde is None:
|
|
104
|
+
n_pde = max(3000, 3 * n_feat) # ~3x oversampling; fixed 10000 was 6x for default N
|
|
105
|
+
if n_bc is None:
|
|
106
|
+
n_bc = max(800, n_pde // 5)
|
|
107
|
+
|
|
96
108
|
# Auto-select scale if needed
|
|
97
109
|
if scale is None and auto_scale:
|
|
98
110
|
if verbose:
|
|
@@ -127,7 +139,7 @@ def solve_linear(
|
|
|
127
139
|
|
|
128
140
|
# Assemble and solve
|
|
129
141
|
A, b = problem.build(solver, x_pde, *build_args)
|
|
130
|
-
beta_raw = solve_lstsq(A, b, mu=mu)
|
|
142
|
+
beta_raw = solve_lstsq(A, b, mu=mu, method=method)
|
|
131
143
|
n_outputs = getattr(problem, "n_outputs", 1)
|
|
132
144
|
solver.beta = unpack_beta(beta_raw, solver.n_features, n_outputs)
|
|
133
145
|
|
|
@@ -170,8 +182,8 @@ def solve_nonlinear(
|
|
|
170
182
|
scale: Optional[float] = None,
|
|
171
183
|
n_blocks: int = 3,
|
|
172
184
|
hidden_size: int = 500,
|
|
173
|
-
n_pde: int =
|
|
174
|
-
n_bc: int =
|
|
185
|
+
n_pde: Optional[int] = None,
|
|
186
|
+
n_bc: Optional[int] = None,
|
|
175
187
|
n_test: int = 5000,
|
|
176
188
|
max_iter: int = 30,
|
|
177
189
|
tol_res: float = 1e-8,
|
|
@@ -202,8 +214,10 @@ def solve_nonlinear(
|
|
|
202
214
|
Number of feature blocks.
|
|
203
215
|
hidden_size : int
|
|
204
216
|
Features per block.
|
|
205
|
-
n_pde, n_bc : int
|
|
206
|
-
Number of collocation and boundary points.
|
|
217
|
+
n_pde, n_bc : int, optional
|
|
218
|
+
Number of collocation and boundary points. If None, scaled with the
|
|
219
|
+
feature count: n_pde = max(3000, 3 * n_blocks * hidden_size),
|
|
220
|
+
n_bc = max(800, n_pde // 5).
|
|
207
221
|
n_test : int
|
|
208
222
|
Number of test points for error evaluation.
|
|
209
223
|
max_iter : int
|
|
@@ -239,6 +253,12 @@ def solve_nonlinear(
|
|
|
239
253
|
"""
|
|
240
254
|
t0 = time.time()
|
|
241
255
|
|
|
256
|
+
n_feat = n_blocks * hidden_size
|
|
257
|
+
if n_pde is None:
|
|
258
|
+
n_pde = max(3000, 3 * n_feat) # ~3x oversampling; fixed 10000 was 6x for default N
|
|
259
|
+
if n_bc is None:
|
|
260
|
+
n_bc = max(800, n_pde // 5)
|
|
261
|
+
|
|
242
262
|
# Auto-select scale if needed
|
|
243
263
|
if scale is None and auto_scale:
|
|
244
264
|
if verbose:
|
|
@@ -11,17 +11,26 @@ condition number -- leaving several orders of magnitude of accuracy on the floor
|
|
|
11
11
|
|
|
12
12
|
``solve_lstsq`` therefore exposes several back-ends via ``method=``:
|
|
13
13
|
|
|
14
|
+
* ``"qr"`` -- Householder-QR least squares (ridge via ``[A; sqrt(mu) I]``
|
|
15
|
+
augmentation). Backward-stable at ``cond(A)`` -- SVD-grade
|
|
16
|
+
accuracy with no normal-equations squaring and no required
|
|
17
|
+
ridge, at ~QR cost (cheaper than SVD). Assumes (numerically)
|
|
18
|
+
full column rank; ``"svd"`` is the rank-deficient-safe choice
|
|
19
|
+
(and ``"auto"``'s ultimate fallback if QR blows up).
|
|
14
20
|
* ``"svd"`` -- rank-revealing truncated SVD of ``A`` (LAPACK ``gelsd`` fast
|
|
15
|
-
path on CPU; explicit SVD elsewhere). The accuracy reference
|
|
21
|
+
path on CPU; explicit SVD elsewhere). The accuracy reference;
|
|
22
|
+
use for a genuinely rank-deficient ``A``.
|
|
16
23
|
* ``"cholesky"`` -- normal-equations ``(A^T A + mu I)`` Cholesky. Fast, but only
|
|
17
24
|
safe when ``A`` is well-conditioned.
|
|
18
25
|
* ``"rsvd"`` -- randomized SVD (range-finder + power iterations). ``O(MNk)``
|
|
19
26
|
for a target ``rank`` k << N -- the cheap option for strongly
|
|
20
27
|
low-rank systems.
|
|
21
28
|
* ``"auto"`` (default) -- try Cholesky; if the system is ill-conditioned (a
|
|
22
|
-
cheap pivot-ratio test)
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
cheap pivot-ratio test) use the faster ``"qr"``, and fall back
|
|
30
|
+
to rank-revealing ``"svd"`` only if QR's solution blows up (the
|
|
31
|
+
feature matrices can be rank-deficient). Fast path when
|
|
32
|
+
well-conditioned, QR speed/accuracy on the rest, SVD as the
|
|
33
|
+
safety net.
|
|
25
34
|
|
|
26
35
|
All back-ends are device/dtype-aware. Apple-MPS lacks a robust ``svd``/``lstsq``,
|
|
27
36
|
so the factorization is run on CPU and the result moved back (one-time warning).
|
|
@@ -33,6 +42,13 @@ import torch
|
|
|
33
42
|
|
|
34
43
|
_MPS_WARNED = False
|
|
35
44
|
|
|
45
|
+
# In ``method="auto"``: above this ``||x|| / (1 + ||b||)`` ratio the unpivoted-QR
|
|
46
|
+
# solve is treated as a rank-deficiency blow-up and handed to the rank-revealing
|
|
47
|
+
# SVD instead. Real PDE systems measure <= 0.3 here; the degenerate inconsistent
|
|
48
|
+
# (random-RHS) rank-deficient case measures ~3e14 -- so the guard is generous and
|
|
49
|
+
# a false positive only costs speed, never correctness.
|
|
50
|
+
_QR_AUTO_NORM_GUARD = 1e6
|
|
51
|
+
|
|
36
52
|
|
|
37
53
|
def _maybe_cpu(A, b):
|
|
38
54
|
"""MPS has no robust svd/lstsq -- factorize on CPU, remember to move back."""
|
|
@@ -86,9 +102,22 @@ def _rsvd_solve(A, b, mu, rcond, rank, oversample, n_iter):
|
|
|
86
102
|
return Vh.transpose(-2, -1) @ (filt.unsqueeze(-1) * (U.transpose(-2, -1) @ b))
|
|
87
103
|
|
|
88
104
|
|
|
105
|
+
def _qr_solve(A, b, mu):
|
|
106
|
+
"""Householder-QR least squares (ridge via [A; sqrt(mu) I] augmentation).
|
|
107
|
+
Backward-stable at cond(A): SVD-grade accuracy with NO normal-equations
|
|
108
|
+
squaring and no required ridge, at ~QR cost (cheaper than SVD). Assumes
|
|
109
|
+
(numerically) full column rank; use method='svd' for a rank-deficient A."""
|
|
110
|
+
if mu:
|
|
111
|
+
n = A.shape[-1]
|
|
112
|
+
A = torch.cat([A, (mu ** 0.5) * torch.eye(n, dtype=A.dtype, device=A.device)], dim=-2)
|
|
113
|
+
b = torch.cat([b, torch.zeros(n, b.shape[-1], dtype=b.dtype, device=b.device)], dim=-2)
|
|
114
|
+
Q, R = torch.linalg.qr(A, mode="reduced")
|
|
115
|
+
return torch.linalg.solve_triangular(R, Q.transpose(-2, -1) @ b, upper=True)
|
|
116
|
+
|
|
117
|
+
|
|
89
118
|
def _auto_solve(A, b, mu, rcond):
|
|
90
119
|
# Cheap conditioning probe: cond(A) ~ max/min Cholesky pivot. If well within
|
|
91
|
-
# float64's reach use the fast Cholesky
|
|
120
|
+
# float64's reach use the fast Cholesky.
|
|
92
121
|
try:
|
|
93
122
|
x, L = _cholesky_solve(A, b, mu)
|
|
94
123
|
d = torch.diagonal(L).abs()
|
|
@@ -96,6 +125,14 @@ def _auto_solve(A, b, mu, rcond):
|
|
|
96
125
|
return x
|
|
97
126
|
except torch.linalg.LinAlgError:
|
|
98
127
|
pass
|
|
128
|
+
# Ill-conditioned: try the faster, backward-stable QR. On a genuinely
|
|
129
|
+
# rank-deficient *inconsistent* A unpivoted QR can return a wildly
|
|
130
|
+
# non-minimum-norm solution, so fall back to the rank-revealing SVD when the
|
|
131
|
+
# QR solution blows up (or is non-finite). See _QR_AUTO_NORM_GUARD.
|
|
132
|
+
x = _qr_solve(A, b, mu)
|
|
133
|
+
nx = torch.linalg.vector_norm(x)
|
|
134
|
+
if torch.isfinite(nx) and nx <= _QR_AUTO_NORM_GUARD * (1.0 + torch.linalg.vector_norm(b)):
|
|
135
|
+
return x
|
|
99
136
|
return _svd_solve(A, b, mu, rcond)
|
|
100
137
|
|
|
101
138
|
|
|
@@ -112,7 +149,7 @@ def solve_lstsq(A, b, mu=0.0, rcond=1e-12, method="auto",
|
|
|
112
149
|
an unstable add-on).
|
|
113
150
|
rcond : float
|
|
114
151
|
Relative singular-value / pivot threshold for rank determination.
|
|
115
|
-
method : {"auto", "svd", "cholesky", "rsvd"}
|
|
152
|
+
method : {"auto", "qr", "svd", "cholesky", "rsvd"}
|
|
116
153
|
Solve back-end (see module docstring). Default "auto".
|
|
117
154
|
rank, oversample, n_iter : int
|
|
118
155
|
Randomized-SVD parameters (``method="rsvd"`` only). Set ``rank`` << N for
|
|
@@ -127,11 +164,13 @@ def solve_lstsq(A, b, mu=0.0, rcond=1e-12, method="auto",
|
|
|
127
164
|
x = _auto_solve(A2, b2, mu, rcond)
|
|
128
165
|
elif method == "svd":
|
|
129
166
|
x = _svd_solve(A2, b2, mu, rcond)
|
|
167
|
+
elif method == "qr":
|
|
168
|
+
x = _qr_solve(A2, b2, mu)
|
|
130
169
|
elif method == "cholesky":
|
|
131
170
|
x = _cholesky_solve(A2, b2, mu)[0]
|
|
132
171
|
elif method == "rsvd":
|
|
133
172
|
x = _rsvd_solve(A2, b2, mu, rcond, rank, oversample, n_iter)
|
|
134
173
|
else:
|
|
135
174
|
raise ValueError(f"Unknown method {method!r}; "
|
|
136
|
-
"choose 'auto', 'svd', 'cholesky', or 'rsvd'.")
|
|
175
|
+
"choose 'auto', 'qr', 'svd', 'cholesky', or 'rsvd'.")
|
|
137
176
|
return x.to(mps_dev) if mps_dev is not None else x
|
|
@@ -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.4"
|
|
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"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Copyright (c) 2026 Antonin Sulc -- MIT.
|
|
2
|
+
"""Smoke tests for the benchmark PDE equations and the inverse-problem workflows.
|
|
3
|
+
|
|
4
|
+
Exercises the forward benchmark problems through the public ``solve_linear`` /
|
|
5
|
+
``solve_nonlinear`` API and two inverse pipelines -- parameter recovery via an
|
|
6
|
+
outer optimiser, and SINDy-style PDE discovery via analytical derivatives -- so
|
|
7
|
+
the v0.2.3 QR / N-scaled-collocation solver path is covered end-to-end, not just
|
|
8
|
+
on the single Poisson problem in ``test_basic``.
|
|
9
|
+
|
|
10
|
+
Scales are fixed (not auto-selected) and the RNG is seeded so the smoke test is
|
|
11
|
+
fast and deterministic; tolerances carry ~10x headroom over measured errors.
|
|
12
|
+
|
|
13
|
+
Excluded (pre-existing, unrelated to the solver work):
|
|
14
|
+
* ``Wave2D_MS`` -- rel-err == 1.0 via ``solve_linear`` in every config
|
|
15
|
+
(old 10000/2000 defaults included), i.e. does not solve.
|
|
16
|
+
* ``ElasticWave2D``-- a 2-output vector problem (``exact()`` returns (N, 2))
|
|
17
|
+
that never sets ``n_outputs``, so the scalar API can't
|
|
18
|
+
unpack it. Needs the vector solver path.
|
|
19
|
+
"""
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pytest
|
|
22
|
+
import torch
|
|
23
|
+
|
|
24
|
+
from fastlsq import (
|
|
25
|
+
solve_linear, solve_nonlinear, solve_lstsq, Op, SinusoidalBasis,
|
|
26
|
+
sample_box, sample_boundary_box,
|
|
27
|
+
)
|
|
28
|
+
from fastlsq.problems import linear as L
|
|
29
|
+
from fastlsq.problems import nonlinear as NL
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# (class, fixed scale, val_err tolerance)
|
|
33
|
+
LINEAR_CASES = [
|
|
34
|
+
(L.PoissonND, 0.5, 5e-3),
|
|
35
|
+
(L.HeatND, 0.5, 1e-1),
|
|
36
|
+
(L.Wave1D, 15.0, 5e-3),
|
|
37
|
+
(L.Helmholtz2D, 10.0, 1e-5),
|
|
38
|
+
(L.Maxwell2D_TM, 2.0, 5e-3),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
NONLINEAR_CASES = [
|
|
42
|
+
(NL.NLPoisson2D, 8.0, 1e-4),
|
|
43
|
+
(NL.Bratu2D, 15.0, 1e-4),
|
|
44
|
+
(NL.SteadyBurgers1D,10.0, 1e-4),
|
|
45
|
+
(NL.NLHelmholtz2D, 5.0, 1e-4),
|
|
46
|
+
(NL.AllenCahn1D, 15.0, 2e-1),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize(
|
|
51
|
+
"cls,scale,tol", LINEAR_CASES, ids=[c[0].__name__ for c in LINEAR_CASES]
|
|
52
|
+
)
|
|
53
|
+
def test_linear_benchmark_solves(cls, scale, tol):
|
|
54
|
+
"""Each linear benchmark equation solves end-to-end via the public API."""
|
|
55
|
+
torch.set_default_dtype(torch.float64)
|
|
56
|
+
torch.manual_seed(0)
|
|
57
|
+
r = solve_linear(cls(), scale=scale, n_blocks=2, hidden_size=300,
|
|
58
|
+
n_test=1500, auto_scale=False, verbose=False)
|
|
59
|
+
ve = r["metrics"]["val_err"]
|
|
60
|
+
assert np.isfinite(ve), f"{cls.__name__}: non-finite val_err"
|
|
61
|
+
assert ve < tol, f"{cls.__name__}: val_err={ve:.2e} exceeds tol {tol:.0e}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize(
|
|
65
|
+
"cls,scale,tol", NONLINEAR_CASES, ids=[c[0].__name__ for c in NONLINEAR_CASES]
|
|
66
|
+
)
|
|
67
|
+
def test_nonlinear_benchmark_solves(cls, scale, tol):
|
|
68
|
+
"""Each nonlinear benchmark equation converges via Newton + the public API."""
|
|
69
|
+
torch.set_default_dtype(torch.float64)
|
|
70
|
+
torch.manual_seed(0)
|
|
71
|
+
r = solve_nonlinear(cls(), scale=scale, n_blocks=2, hidden_size=300,
|
|
72
|
+
n_test=1500, max_iter=15, auto_scale=False, verbose=False)
|
|
73
|
+
ve = r["metrics"]["val_err"]
|
|
74
|
+
assert r["n_iters"] > 0, f"{cls.__name__}: no Newton iterations ran"
|
|
75
|
+
assert np.isfinite(ve), f"{cls.__name__}: non-finite val_err"
|
|
76
|
+
assert ve < tol, f"{cls.__name__}: val_err={ve:.2e} exceeds tol {tol:.0e}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_inverse_source_position():
|
|
80
|
+
"""Recover a Gaussian source position from sensor data (forward solve + L-BFGS)."""
|
|
81
|
+
opt = pytest.importorskip("scipy.optimize")
|
|
82
|
+
torch.set_default_dtype(torch.float64)
|
|
83
|
+
torch.manual_seed(0)
|
|
84
|
+
|
|
85
|
+
pde_op = -Op.laplacian(d=2)
|
|
86
|
+
basis = SinusoidalBasis.random(input_dim=2, n_features=700, sigma=5.0,
|
|
87
|
+
normalize=True)
|
|
88
|
+
x_pde = sample_box(3000, 2)
|
|
89
|
+
x_bc = sample_boundary_box(400, 2)
|
|
90
|
+
n_bc = x_bc.shape[0]
|
|
91
|
+
cache = basis.cache(x_pde)
|
|
92
|
+
A = torch.cat([pde_op.apply(basis, x_pde, cache=cache),
|
|
93
|
+
100.0 * basis.evaluate(x_bc)])
|
|
94
|
+
x_sens = torch.tensor([[0.3, 0.3], [0.7, 0.7], [0.3, 0.7], [0.7, 0.3]])
|
|
95
|
+
|
|
96
|
+
def forward(xs, ys):
|
|
97
|
+
b = torch.exp(-((x_pde[:, 0] - xs) ** 2
|
|
98
|
+
+ (x_pde[:, 1] - ys) ** 2) / 0.1).unsqueeze(1)
|
|
99
|
+
b = torch.cat([b, torch.zeros(n_bc, 1, dtype=b.dtype)])
|
|
100
|
+
beta = solve_lstsq(A, b)
|
|
101
|
+
return (basis.evaluate(x_sens) @ beta).detach().cpu().numpy().ravel()
|
|
102
|
+
|
|
103
|
+
true = np.array([0.4, 0.6])
|
|
104
|
+
rng = np.random.default_rng(0)
|
|
105
|
+
u_obs = forward(*true) + 0.005 * rng.standard_normal(4)
|
|
106
|
+
|
|
107
|
+
res = opt.minimize(
|
|
108
|
+
lambda p: float(np.sum((forward(float(p[0]), float(p[1])) - u_obs) ** 2)),
|
|
109
|
+
x0=[0.5, 0.5], method="L-BFGS-B", bounds=[(0.1, 0.9)] * 2,
|
|
110
|
+
)
|
|
111
|
+
assert np.linalg.norm(res.x - true) < 0.06, f"recovered {res.x} vs {true}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_pde_discovery_recovers_governing_equation():
|
|
115
|
+
"""SINDy-style discovery via analytical derivatives recovers u_xx = a*u + b*u_x.
|
|
116
|
+
|
|
117
|
+
Synthetic damped oscillator u = exp(-x/2) sin(2x) -> u_xx = -4.25 u - 1.0 u_x.
|
|
118
|
+
The dominant restoring term is recovered tightly; the damping term is harder
|
|
119
|
+
from 2% noise, so it is only bounded in sign/magnitude.
|
|
120
|
+
"""
|
|
121
|
+
torch.set_default_dtype(torch.float64)
|
|
122
|
+
torch.manual_seed(42)
|
|
123
|
+
|
|
124
|
+
M = 500
|
|
125
|
+
x = torch.linspace(0, 2 * np.pi, M).reshape(-1, 1)
|
|
126
|
+
u_true = torch.exp(-0.5 * x) * torch.sin(2 * x)
|
|
127
|
+
u_noisy = u_true + 0.02 * torch.randn_like(u_true)
|
|
128
|
+
|
|
129
|
+
basis = SinusoidalBasis.random(input_dim=1, n_features=400, sigma=4.0,
|
|
130
|
+
normalize=True)
|
|
131
|
+
beta = solve_lstsq(basis.evaluate(x), u_noisy, mu=1e-3)
|
|
132
|
+
cache = basis.cache(x)
|
|
133
|
+
u = basis.evaluate(x, cache=cache) @ beta
|
|
134
|
+
u_x = basis.derivative(x, alpha=(1,), cache=cache) @ beta
|
|
135
|
+
u_xx = basis.derivative(x, alpha=(2,), cache=cache) @ beta
|
|
136
|
+
|
|
137
|
+
coef = solve_lstsq(torch.cat([u, u_x], dim=1), u_xx) # u_xx = a*u + b*u_x
|
|
138
|
+
a, b = float(coef[0]), float(coef[1])
|
|
139
|
+
assert abs(a - (-4.25)) < 0.3, f"restoring coeff a={a:.3f} (want -4.25)"
|
|
140
|
+
assert b < 0 and abs(b - (-1.0)) < 0.5, f"damping coeff b={b:.3f} (want -1.0)"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
pytest.main([__file__, "-v"])
|
|
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
|
|
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
|