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.
Files changed (118) hide show
  1. {fastlsq-0.2.1 → fastlsq-0.2.2}/CHANGELOG.md +47 -0
  2. {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/PKG-INFO +8 -8
  3. {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/SOURCES.txt +0 -10
  4. {fastlsq-0.2.1 → fastlsq-0.2.2}/MANIFEST.in +0 -1
  5. {fastlsq-0.2.1 → fastlsq-0.2.2}/PKG-INFO +8 -8
  6. {fastlsq-0.2.1 → fastlsq-0.2.2}/README.md +3 -3
  7. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/orbit_hill.py +7 -5
  8. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/__init__.py +1 -1
  9. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/api.py +7 -5
  10. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/basis.py +5 -0
  11. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/export.py +4 -1
  12. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/learnable.py +15 -8
  13. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/linalg.py +1 -1
  14. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/newton.py +8 -2
  15. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/problems/linear.py +6 -6
  16. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/problems/nonlinear.py +20 -20
  17. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/problems/regression.py +38 -38
  18. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/tuning.py +9 -1
  19. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/vector.py +2 -2
  20. {fastlsq-0.2.1 → fastlsq-0.2.2}/pyproject.toml +5 -5
  21. fastlsq-0.2.1/misc/fastlsq_teaser.png +0 -0
  22. fastlsq-0.2.1/misc/ideal_quadrupole.png +0 -0
  23. fastlsq-0.2.1/misc/inverse_heat_source.gif +0 -0
  24. fastlsq-0.2.1/misc/inverse_heat_source.png +0 -0
  25. fastlsq-0.2.1/misc/inverse_magnetostatics.png +0 -0
  26. fastlsq-0.2.1/misc/inverse_magnetostatics_convergence.png +0 -0
  27. fastlsq-0.2.1/misc/quadrupole_convergence.png +0 -0
  28. fastlsq-0.2.1/misc/quadrupole_optimization.png +0 -0
  29. fastlsq-0.2.1/misc/tutorial_nlpoisson_convergence.png +0 -0
  30. fastlsq-0.2.1/misc/tutorial_nlpoisson_solution.png +0 -0
  31. {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/dependency_links.txt +0 -0
  32. {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/requires.txt +0 -0
  33. {fastlsq-0.2.1 → fastlsq-0.2.2}/FastLSQ.egg-info/top_level.txt +0 -0
  34. {fastlsq-0.2.1 → fastlsq-0.2.2}/LICENSE +0 -0
  35. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/add_your_own_pde.py +0 -0
  36. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/benchmark_comparison.py +0 -0
  37. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/custom_features.py +0 -0
  38. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/fred_sde.py +0 -0
  39. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/fred_sde_fastlsq.py +0 -0
  40. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/gaia_potential.py +0 -0
  41. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/gaia_potential_fastlsq.py +0 -0
  42. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/horizons_ephemeris.py +0 -0
  43. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/numerai_alpha.py +0 -0
  44. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/numerai_alpha_fastlsq.py +0 -0
  45. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/run_all_fastlsq.py +0 -0
  46. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/__init__.py +0 -0
  47. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/_alsu_lattice.py +0 -0
  48. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/_common.py +0 -0
  49. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/run_all.py +0 -0
  50. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_beamloss_ode.py +0 -0
  51. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_betatron_tune.py +0 -0
  52. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_green_fff.py +0 -0
  53. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_hill_ivp.py +0 -0
  54. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_observe_fit_act_simulator.py +0 -0
  55. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_orbit_inverse.py +0 -0
  56. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_passive_loco.py +0 -0
  57. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_perturbed_hill.py +0 -0
  58. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_sofb_observe_fit_act.py +0 -0
  59. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_streaming_archive_growth.py +0 -0
  60. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_synchrotron_ode.py +0 -0
  61. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_tides_3months.py +0 -0
  62. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_topoff_impulse.py +0 -0
  63. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s01_visualize.py +0 -0
  64. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s02_plasma_wakefield.py +0 -0
  65. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s03_synchrobetatron.py +0 -0
  66. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s04_sunspots.py +0 -0
  67. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s05_helioseismology.py +0 -0
  68. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s06_tides.py +0 -0
  69. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s07_iers_earth_rotation.py +0 -0
  70. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s08_mauna_loa_co2.py +0 -0
  71. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s09_enso_qbo.py +0 -0
  72. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s10_pulsar_timing.py +0 -0
  73. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s11_modal_analysis.py +0 -0
  74. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s12_mems_resonator.py +0 -0
  75. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s13_variable_stars_kepler.py +0 -0
  76. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s14_eeg.py +0 -0
  77. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/scenarios/s15_circadian.py +0 -0
  78. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/extras/spectral_expansion.py +0 -0
  79. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/grad_shafranov.py +0 -0
  80. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/grid_inverse.py +0 -0
  81. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/grid_rl_control.py +0 -0
  82. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/grid_swing.py +0 -0
  83. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/gs_inverse.py +0 -0
  84. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/gs_rl_control.py +0 -0
  85. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/inverse_heat_source.py +0 -0
  86. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/inverse_magnetostatics.py +0 -0
  87. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/inverse_source_position.py +0 -0
  88. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/learnable_helmholtz.py +0 -0
  89. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/orbit_inverse.py +0 -0
  90. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/orbit_rl.py +0 -0
  91. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/pde_discovery.py +0 -0
  92. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/run_all_extensions.py +0 -0
  93. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/run_linear.py +0 -0
  94. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/run_nonlinear.py +0 -0
  95. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/tutorial_basic.py +0 -0
  96. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/tutorial_nonlinear.py +0 -0
  97. {fastlsq-0.2.1 → fastlsq-0.2.2}/examples/vector_basis_stream_vorticity.py +0 -0
  98. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/block.py +0 -0
  99. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/device.py +0 -0
  100. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/diagnostics.py +0 -0
  101. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/geometry.py +0 -0
  102. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/lightning.py +0 -0
  103. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/plotting.py +0 -0
  104. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/problems/__init__.py +0 -0
  105. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/solvers.py +0 -0
  106. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/utils.py +0 -0
  107. {fastlsq-0.2.1 → fastlsq-0.2.2}/fastlsq/viz.py +0 -0
  108. {fastlsq-0.2.1 → fastlsq-0.2.2}/requirements.txt +0 -0
  109. {fastlsq-0.2.1 → fastlsq-0.2.2}/setup.cfg +0 -0
  110. {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_basic.py +0 -0
  111. {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_block.py +0 -0
  112. {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_derivatives.py +0 -0
  113. {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_device.py +0 -0
  114. {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_grad_shafranov.py +0 -0
  115. {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_grid_swing.py +0 -0
  116. {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_learnable.py +0 -0
  117. {fastlsq-0.2.1 → fastlsq-0.2.2}/tests/test_orbit_hill.py +0 -0
  118. {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.1
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/asulc/FastLSQ
8
- Project-URL: Repository, https://github.com/asulc/FastLSQ
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/asulc/FastLSQ/issues
11
- Project-URL: Changelog, https://github.com/asulc/FastLSQ/blob/main/CHANGELOG.md
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
@@ -2,7 +2,6 @@ include LICENSE
2
2
  include README.md
3
3
  include CHANGELOG.md
4
4
  include requirements.txt
5
- recursive-include misc *.png *.gif
6
5
  recursive-include examples *.py
7
6
  recursive-include tests *.py
8
7
  recursive-exclude * __pycache__
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FastLSQ
3
- Version: 0.2.1
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/asulc/FastLSQ
8
- Project-URL: Repository, https://github.com/asulc/FastLSQ
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/asulc/FastLSQ/issues
11
- Project-URL: Changelog, https://github.com/asulc/FastLSQ/blob/main/CHANGELOG.md
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
- AtA = A64.T @ A64 + MU_REG * np.eye(A64.shape[1])
170
- Atb = A64.T @ b64
171
- cho = cho_factor(AtA)
172
- return cho_solve(cho, Atb)
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
  # ---------------------------------------------------------------------------
@@ -44,7 +44,7 @@ from fastlsq.export import (
44
44
  )
45
45
  from fastlsq import viz
46
46
 
47
- __version__ = "0.2.1"
47
+ __version__ = "0.2.2"
48
48
  __all__ = [
49
49
  # Device selection (CPU / CUDA / Apple-MPS, dtype-aware)
50
50
  "resolve_device",
@@ -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-12,
178
- tol_du: float = 1e-13,
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
- if schedule[-1] != getattr(problem, "nu_target", None):
268
- schedule.append(getattr(problem, "nu_target", None))
269
- schedule = [v for v in schedule if v >= getattr(problem, "nu_target", 0.0)]
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
- state = torch.load(path, map_location=device)
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 a
184
- rank-revealing truncated SVD of ``A``, so gradients still flow back to
185
- ``L`` *and* the solve is stable when ``A`` is rank-deficient. (The plain
186
- ``torch.linalg.lstsq`` used previously amplifies the near-null space and
187
- makes the outer AdamW loop diverge.)
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
- U, S, Vh = torch.linalg.svd(A, full_matrices=False)
194
- filt = torch.where(S > rcond * S[0], S / (S * S + mu), torch.zeros_like(S))
195
- beta_flat = Vh.transpose(-2, -1) @ (filt.unsqueeze(-1) * (U.transpose(-2, -1) @ b))
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.5) * d.max():
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
- solver.beta = beta_old + alpha * delta_beta
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 and rel_du < tol_du:
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: u_x_tt - c_p² u_x_xx - c_s² u_x_yy - (c_p² - c_s²) u_y_xy = 0
396
- A1_x = t_scale * u_tt - self.c_p2 * u_xx - self.c_s2 * u_yy
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: u_y_tt - c_p² u_y_yy - c_s² u_y_xx - (c_p² - c_s²) u_x_xy = 0
400
- A2_x = -self.c_cross * u_xy
401
- A2_y = t_scale * u_tt - self.c_p2 * u_yy - self.c_s2 * u_xx
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.utils import device
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=device)
31
- z = lambda n: torch.zeros(n, 1, device=device)
32
- o = lambda n: torch.ones(n, 1, device=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=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=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=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=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=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=device),
214
- torch.ones(n_bc // 2, 1, device=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=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=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=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=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=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=device),
359
- torch.ones(n_bc // 2, 1, device=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=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=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.utils import device
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, n_samples=5000):
64
- x_pde = torch.rand(n_samples, 2, device=device)
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=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, n_samples=5000):
110
- x_space = torch.rand(n_samples, 1, device=device) * 4 - 2
111
- t_time = torch.rand(n_samples, 1, device=device) * 0.1
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=device) * 4 - 2
121
- t_time = torch.rand(n, 1, device=device) * 0.1
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, n_samples=5000):
157
- x_space = torch.rand(n_samples, 1, device=device) * 20 - 10
158
- t_time = torch.rand(n_samples, 1, device=device)
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=device) * 20 - 10
168
- t_time = torch.rand(n, 1, device=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, n_samples=5000):
209
- x_space = torch.rand(n_samples, 1, device=device) * 20 - 10
210
- t_time = torch.rand(n_samples, 1, device=device) * 20
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=device) * 20 - 10
220
- t_time = torch.rand(n, 1, device=device) * 20
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, n_samples=5000):
249
- x_space = torch.rand(n_samples, 1, device=device) * 2 - 1
250
- t_time = torch.rand(n_samples, 1, device=device)
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=device) * 2 - 1
260
- t_time = torch.rand(n, 1, device=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, n_samples=5000):
294
- x_space = torch.rand(n_samples, 1, device=device) * 1.5 - 0.5
295
- y_space = torch.rand(n_samples, 1, device=device) * 2.0 - 0.5
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=device) * 1.5 - 0.5
305
- y_space = torch.rand(n, 1, device=device) * 2.0 - 0.5
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, n_samples=5000):
336
- x_pde = torch.rand(n_samples, 2, device=device)
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=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, n_samples=5000):
362
- x_pde = torch.rand(n_samples, 2, device=device)
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=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, n_samples=5000):
389
- x_pde = torch.rand(n_samples, 2, device=device)
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=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.1"
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/asulc/FastLSQ"
59
- Repository = "https://github.com/asulc/FastLSQ"
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/asulc/FastLSQ/issues"
62
- Changelog = "https://github.com/asulc/FastLSQ/blob/main/CHANGELOG.md"
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes