FastLSQ 0.2.3__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.3 → fastlsq-0.2.4}/CHANGELOG.md +21 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/FastLSQ.egg-info/PKG-INFO +1 -1
- {fastlsq-0.2.3 → fastlsq-0.2.4}/FastLSQ.egg-info/SOURCES.txt +1 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/PKG-INFO +1 -1
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/__init__.py +1 -1
- {fastlsq-0.2.3 → fastlsq-0.2.4}/pyproject.toml +1 -1
- fastlsq-0.2.4/tests/test_benchmarks_inverse.py +144 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/tests/test_vector_basis.py +1 -1
- {fastlsq-0.2.3 → fastlsq-0.2.4}/FastLSQ.egg-info/dependency_links.txt +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/FastLSQ.egg-info/requires.txt +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/FastLSQ.egg-info/top_level.txt +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/LICENSE +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/MANIFEST.in +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/README.md +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/add_your_own_pde.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/benchmark_comparison.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/custom_features.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/fred_sde.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/fred_sde_fastlsq.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/gaia_potential.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/gaia_potential_fastlsq.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/horizons_ephemeris.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/numerai_alpha.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/numerai_alpha_fastlsq.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/run_all_fastlsq.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/__init__.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/_alsu_lattice.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/_common.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/run_all.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_green_fff.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_passive_loco.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_tides_3months.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s01_visualize.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s04_sunspots.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s05_helioseismology.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s06_tides.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s14_eeg.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/scenarios/s15_circadian.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/extras/spectral_expansion.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/grad_shafranov.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/grid_inverse.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/grid_rl_control.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/grid_swing.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/gs_inverse.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/gs_rl_control.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/inverse_heat_source.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/inverse_magnetostatics.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/inverse_source_position.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/learnable_helmholtz.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/orbit_hill.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/orbit_inverse.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/orbit_rl.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/pde_discovery.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/run_all_extensions.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/run_linear.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/run_nonlinear.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/tutorial_basic.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/tutorial_nonlinear.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/examples/vector_basis_stream_vorticity.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/api.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/basis.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/block.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/device.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/diagnostics.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/export.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/geometry.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/learnable.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/lightning.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/linalg.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/newton.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/plotting.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/problems/__init__.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/problems/linear.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/problems/nonlinear.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/problems/regression.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/solvers.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/tuning.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/utils.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/vector.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/fastlsq/viz.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/requirements.txt +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/setup.cfg +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/tests/test_basic.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/tests/test_block.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/tests/test_derivatives.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/tests/test_device.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/tests/test_grad_shafranov.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/tests/test_grid_swing.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/tests/test_learnable.py +0 -0
- {fastlsq-0.2.3 → fastlsq-0.2.4}/tests/test_orbit_hill.py +0 -0
|
@@ -2,6 +2,27 @@
|
|
|
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
|
+
|
|
5
26
|
## [0.2.3] - 2026-06-04
|
|
6
27
|
|
|
7
28
|
### Added
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|