Repository navigation
On PyPI: https://pypi.org/project/FastLSQ/0.7.1/
The library in the published wheel and sdist is byte-identical to fastlsq/ at this tag; the two commits between the build and the tag touched only tests/test_audit_regressions.py.
A correctness and hygiene release from a line-by-line audit of the package.
Every item below was first reproduced on 0.7.0, then fixed, and each fix has a
regression test in tests/test_audit_regressions.py. The closed-form
mathematics -- derivatives, integrals, Fourier symbols, the Radon projection, the
Stokes singularities, every built-in problem's exact solution and Jacobian -- was
checked against autograd and finite differences and found correct; nothing in
that layer changes.
Security
load_checkpointno longer unpickles arbitrary objects. It used
torch.load(weights_only=False)becauseto_dictstored NumPy arrays, which
the safe mode rejects; a crafted.pttherefore executed code on load.
to_dict/save_checkpointnow store CPU tensors andload_checkpointreads
withweights_only=True. Files written by 0.7.0 and earlier need
load_checkpoint(path, allow_pickle=True), which should only be used on files
from a trusted source. Checkpointmetadatamust consist of plain Python
values and tensors.
Fixed
- Solves no longer reset the global RNG.
evaluate_errorseeded
torch.manual_seed(999)for its fixed test set, andauto_select_scaleseeded
every trial, both on the global generator. Anysolve_linear/
solve_nonlinearwith metrics therefore left torch's RNG at a fixed state: two
consecutive solves drew identical random features, and a user's own random
draws afterwards were deterministic regardless of their seed, so repeated-trial
statistics had zero variance. Both now run insidefastlsq.utils.preserve_rng,
which saves and restores the torch (CPU and active CUDA/MPS) and NumPy state.
The fixed test set is unchanged, so reported errors are as before. set_device()is honoured everywhere.problems/linear.py,
problems/integral.py,plotting.pyandlightning.pybound
fastlsq.utils.device, an import-time snapshot, instead of calling
get_device(); afterset_device("mps")(or"cuda") their tensors stayed
on CPU and the solve failed with a device mismatch.from_dict/
load_checkpointalso defaulted to "CUDA if available" rather than the active
device.solve_nonlinearforwardstol_res,tol_duanddampingon the
continuation path. Problems withuse_continuation(SteadyBurgers1D) ran
every stage withnewton_solve's own defaults (1e-12 / 1e-13 / 1.0) whatever
the caller passed.solve_lstsqaccepts a 1-D right-hand side. A(M,)bcrashed in four
back-ends and, in the ridged SVD path, silently broadcast to an(N, N)result.
It is now treated as a single column and the solution returned 1-D; ab
whose row count does not matchAraisesValueError.LearnableFastLSQ:solve_cachedleftbetaflat and_beta_flatunset forn_outputs > 1,
sopredictfailed with a shape mismatch after a cached solve; it now does
the same bookkeeping assolve_inner.- scalar mode built
torch.eye(..., device=get_device())in the default dtype,
so a module moved with.to(torch.float32)or.to("cuda")failed in that
mode only; the identity now follows the parameter's device and dtype. The
parameters are created from Python floats so they take torch's default dtype
(a NumPy scalar pinnedlog_sigmato float64 under a float32 default). train_bandwidthwithmu > 0differentiated the wrong objective: the
detached-beta*(envelope) gradient is exact for the ridge objective the
inner solve minimises, but the outer loss was the plain residual, so the
gradient was off by the cross term-mu beta*(2.6x against finite
differences atmu = 0.1). The outer loss now includes
mu ||beta*||^2 / M. The defaultmu = 0was never affected.train_bandwidthusedAdamWwith its defaultweight_decay = 0.01, a
hidden prior pulling every log-bandwidth towardsigma = 1independent of the
loss (sigma 25 -> 17 over 120 steps with a zero gradient). The new
weight_decayargument defaults to 0.- the log-bandwidth clamp
[-3, 6.5]applied in diagonal and Cholesky mode
but not scalar mode, and was undocumented. It is nowLOG_SCALE_MIN/
LOG_SCALE_MAX, applied in every mode and documented; aninit_scale
outside[0.05, 665]raises at construction instead of being silently capped.
- Boundary projection is scale-free.
project_to_boundary,
sample_boundary_sdfandoutward_normaltested|psi| < toland
||grad psi||^2 > 1e-12as absolute numbers, so a down-scaled implicit function
(1e-7 * (|x| - 1)) was never projected and its "boundary" sample was interior
points. Convergence is now measured by the Newton step length
|psi| / ||grad psi||, a distance, and a gradient counts as degenerate relative
to the largest in the batch.project_to_boundaryandoutward_normalaccept
empty input, andSDFDomain/_as_boundsclone a caller's bounds tensor
instead of aliasing it. check_problemused a forward difference with a fixed1e-5step and a
1e-3threshold, which flagged correct gradients as wrong under float32; it now
uses a central difference with a dtype-chosen step and tolerance. The
"BC consistency" check its docstring promised is implemented (Dirichlet data
againstexact()at the boundary points, reported asbc_check),
gradient_checkisFalsewhenexact()raises, and vector problems
(n_outputs = k) are checked against(n, k)/(n, dim, k)instead of being
reported as shape failures.suggest_scale's docstring now says what it does
(a dimension-only heuristic;n_trialsis ignored).Wave1Dreturned(x_pde, bcs)and tookbuild(solver, x, bcs), so
train_bandwidthand the Newton-mode scale search crashed on it. It now
follows the(x_pde, bcs, f_pde)contract (with a zero forcing and asource
method);buildstill accepts the old call.- Plotting.
plot_solution_1d/plot_solution_2d_slice/ contour /
convergence / spectral saved and closedplt.gcf()rather than the figure they
drew on, so with a caller-suppliedaxthe wrong figure was written and an
unrelated one closed.plot_convergence([])raisesValueErrorinstead of
IndexError;viz.hero_figure_landscape(ncols=1)no longer crashes. PolynomialColumns.symbolwith a precomputed(1, N)tensor took the first
feature's value asm(0); a tensor symbol must now be a single constant
(numel() == 1) or the call raises.MirrorBasislackedsymbol,definite_integral,multi_integral,
iterated_integral,advectionandbiharmonic, soSymbolOperator,
IntegralOperatorandMultiIntegralOperatorfailed on it with
AttributeError; all are implemented bank by bank (exact, since the mirror bank
is itself a bank of plane waves).FastLSQSolver.predict*andPIELMSolver.predict*raise aRuntimeError
namingbetawhen called before a solve, and.basisraises when no block has
been added, instead of aTypeErroronNone.to_dictraisesValueErroron an unsolved solver instead ofAttributeError,
always writesinput_dim/normalize(without themfrom_dictcould not
rebuild the solver even thoughinclude_metadata=Falsewas documented).solve_lstsq(..., return_info=True)documents that undermethod="rsvd"
rank_usedandcond_estimatedescribe the rank-ksketch, not the matrix.
Changed
- Newton line search reuses the accepted trial's Jacobian.
newton_solve
assembled(J, -R)at every backtracking trial, discardedJ, and rebuilt it
at the top of the next iteration; the accepted pair is now carried over, saving
one full assembly per iteration. Results are identical. plot_solution_2d_contouralways returns(fig, (ax_pred, ax_exact)), with
ax_exact = Nonewhenplot_exact=False(it returned a 1-tuple before, against
its docstring).- The import-time
torch.set_default_dtype(torch.float64)is kept -- the
package's accuracy regime and the built-in problems depend on it -- but is now
documented infastlsq.deviceand can be disabled with the environment
variableFASTLSQ_KEEP_DEFAULT_DTYPE=1for processes that must not have their
default dtype changed by an import.
Removed
- The
batteryoptional-dependency group (progpy, pandas, scipy) and the
openpyxlpin inrequirements.txt: both referred to battery-degradation
examples that no longer exist in the repository.
Tests
tests/conftest.pyrestores torch's default dtype after every test. Two
tests switched to float32 without restoring it, so the suite's outcome depended
on execution order (test_solve_linear_phased_metricsfollowed by
test_problems_integral.pyfailed seven accuracy assertions).tests/test_audit_regressions.py: one test per fix above.tests/test_basic.py: the float32 solve must now beat the trivialu = 0
predictor by a margin and return the right shape; the nonlinear solve must
converge and reachval_err < 1e-3;check_problemmust pass its gradient
and boundary checks.
Not changed, deliberately
presentations/ATAP_Sulc_20260324.pptx(28 MB) andmisc/inverse_heat_source.gif
(11 MB) remain tracked becauseREADME.mdlinks to them. Moving them to the
website repository or a release asset would shrink clones; it is a judgement
call left to the maintainer.