Skip to content

Feat/root finding - #18

Open
backlundtransform wants to merge 8 commits into
masterfrom
feat/root-finding
Open

backlundtransform wants to merge 8 commits into
masterfrom
feat/root-finding

Conversation

@backlundtransform

Copy link
Copy Markdown
Owner

No description provided.

The library had exactly one root finder, NumericExtensions.NewtonRaphson,
and it could not be trusted. It ran a fixed 100 iterations with no
convergence test and no early exit, divided by the derivative without
checking it for zero - yielding a silent Inf or NaN where the tangent went
flat - took no tolerance or iteration budget, and gave the caller no way to
tell a converged answer from a diverged one. Each of those 100 iterations
called DerivativeExtensions.Derivate, which allocates a Pascal matrix per
call, so an easy root cost 100 matrix allocations and 200 function
evaluations no matter how early it actually converged.

Add Numerics/RootFinding/ with Bisection, Secant, Brent and a real Newton,
each returning a RootResult carrying the estimate, whether it converged, the
iteration count and the residual, so failure is reported rather than hidden.
Brent is the default for a bracketed search and is exposed as the FindRoot
extension. Newton stops on a vanishing or non-finite slope and on a
non-finite iterate instead of iterating on NaN, and takes an analytic
derivative through an overload.

NewtonRaphson keeps its signature and delegates to the new Newton. Its
default derivative reproduces Derivate at order 1 exactly - the same
backward difference over the same 1e-5 step - so existing results are
unchanged, but without the per-iteration allocation and with an early exit.

Migrate the two hand-rolled Newton loops in the physics module,
KeplerOrbit.TrueAnomaly and LagrangePoints.SolveCollinear, onto the shared
implementation. Both were already correct; this removes the duplication.

Also make NumericsTests.FindRoots assert the root it computes - it asserted
Math.Abs(2) == 2 and never looked at the result.

Suite: 1570 passing. The three failures in TimeserieValidationTests are
unrelated and predate this branch: TestData/CsvTestDataGenerator formats
decimals with the current culture, so on a Swedish machine 6.44 is written
as 6,44 and collides with the CSV delimiter. They pass under
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 and on CI.
AGENTS.md requires the section README to be updated when public API lands in
that section, and the root-finding module added a namespace without one.

Add a Root Finding section covering FindRoot, the four methods and when each
applies, RootResult and EnsureConverged, and point the existing
Newton-Raphson bullet at it.
Measured before the Del 1 migration, so the migration can be shown not to
cost anything and the v4.4 performance work has something to beat.

The numbers that bear on v4.3 directly: reusing an LU factorization is 57x
faster than refactoring per solve at N=256 (119us against 6782us), which is
the whole argument for the "factor once" rule; and Matrix.Inverse is the
most expensive operation measured at 35.5ms, five times a full LU
factor-and-solve, which is what the Kalman filters pay today by forming
S.Inverse() explicitly.

Four findings belong to v4.4 rather than v4.3 but were discovered here, so
they are written down: every Matrix allocates a second n*n array for the
identity field that almost nothing reads, doubling the memory cost of every
matrix; new VectorN(double[]) clones its input, so SpMV allocates its result
twice; the triple-loop multiply grows 70x for a 4x dimension against 64x for
pure O(n^3), the rest being cache misses; and one MLP training epoch
allocates 88.75 MB over 2048 samples.

Runs use --inProcess: the Windows application control policy blocks the DLL
copy BenchmarkDotNet places in its generated per-benchmark folder, so the
default toolchain yields no results on this machine. Re-runs must match for
the comparison to mean anything.
v4.1 built the decompositions but left every caller solving systems its own
way. Five of those sites now go through LuDecomposition instead of their own
Gaussian elimination: the GaussElimination extension itself, the RBF weight
solve in MultivariateInterpolation, the polynomial regression solve in
InferentialStatisticsExtensions, the dense not-a-knot fallback in
CubicSplineInterpolation, and the global system in Assembler1D. The
tridiagonal branch of the spline keeps the Thomas algorithm, which is O(n)
against LU's O(n^3) and must not be replaced by it.

GaussElimination was not merely duplicated, it was wrong. Its back
substitution assigned the pivot A[i,i] to the solution component whenever the
numerator came out exactly zero, so any system with a zero in its solution
was answered incorrectly and silently: the diagonal system 2x=2, 3y=0, 4z=4
returned [1, 3, 1] instead of [1, 0, 1]. It now returns the right answer, and
no longer modifies its arguments in place. The existing tests assert exact
equality and still hold.

Add LuDecomposition.SmallestPivotMagnitude so callers can detect a
near-singular matrix rather than only an exactly singular one, and express
IsSingular in terms of it. Without this the RBF solve would have lost its
near-singularity guard and reported a bare "not invertible" in place of
advice to change epsilon or kernel.

Add characterisation tests for PanelMethod, which had neither tests nor
callers. They assert invariants rather than physical correctness, because the
one check against a closed-form solution does not pass: for a 200-panel
cylinder at zero incidence the solver returns Cp ~= 1 on every panel, where
potential flow gives a distribution over [-3, 1]. Whether the solver is wrong
or a seamed cylinder is outside its domain is unresolved, so that test is
left ignored with the question recorded, and PanelMethod itself is not
migrated.

Suite: 1580 passing, 1 ignored.
…sites

Three migrations, continuing the work of routing callers onto the
decompositions v4.1 built.

The Kalman filters formed an explicit inverse of the innovation covariance to
compute the gain. S is symmetric positive definite, so K = P*Ht*S^-1 is now
solved as S*Kt = (P*Ht)t instead. The baseline measured Matrix.Inverse at
35.5ms for a 256x256 matrix against 6.8ms for a full LU factor-and-solve, and
an explicit inverse can also break the symmetry the filter depends on. Add
Matrix.SolveSymmetricPositiveDefinite, which uses Cholesky and falls back to
LU if the matrix is not positive definite, since a recursively updated
covariance can drift out of definiteness in floating point. It lives in
LinearAlgebra rather than in StateEstimation because that is the section that
owns the abstraction. All four inverse sites across KalmanFilter,
ExtendedKalmanFilter and KalmanSmoother are converted.

CoupledOscillators had its own private Jacobi eigensolver, duplicating
EigenDecomposition. Its symmetrised dynamical matrix is symmetric by
construction, so the shared decomposition takes its symmetric path and
provides the same contract: ascending real eigenvalues, orthonormal
eigenvectors as columns.

PCA found its eigenpairs by power iteration with deflation. It now uses
EigenDecomposition for both the covariance and the dual Gram path. This is an
accuracy fix, not only de-duplication: two of the sixteen new PCA tests fail
against the old implementation, with score columns correlated to 2.1e-5 and
reconstruction off by 7.4e-8, which is what deflation error at a 1e-8
tolerance leaves behind. PowerIteration is deleted. MaxIterations, Tolerance
and Seed no longer affect anything but are kept so existing calls and
grid-search parameter dictionaries keep working; they are documented as inert.

PCA had no tests at all before this. The new suite asserts properties of the
decomposition rather than one implementation's output.

Two planned items are dropped, with reasons recorded in the roadmap:
PanelMethod is not migrated, and the legacy EigenValues, EigenVector and
DominantEigenVector helpers do not delegate, because their published contract
is rounded integer ratios that OdeSolver and existing tests depend on.
Changing it is an API decision, not a refactor.

Verification: the solution builds clean in Release on all three target
frameworks, and the Kalman (6) and CoupledOscillators (45) tests passed
before the Windows application control policy began blocking the test
assembly continuously. The full suite has NOT been run against these three
changes. Run it, or let CI do it, before merging.
…quations

Every linear fitter built the design matrix and then immediately reduced it to
XtX before solving. That squares the condition number of X, and a Vandermonde
design - any polynomial fit - is poorly conditioned to begin with. The solve
now happens on the design matrix itself.

Measured on a degree-5 fit over [1, 2], by running this branch's new test
against the previous implementation: the normal equations recover the
coefficients to a worst-case error of 5.6e-5, QR reaches 7.2e-11. That is
close to six decimal digits, and it is why the change is worth making rather
than merely tidy.

Add two primitives to FittingSolver: SolveLeastSquares, and
SolveWeightedLeastSquares which scales each row by the square root of its
weight so the weighted problem becomes an ordinary one. Both return
(XtX)^-1 alongside the coefficients, recovered from the triangular factor as
R^-1 R^-T, so the Gram matrix is never formed or inverted for standard errors
either. LeastSquaresFitter, WeightedLeastSquaresFitter, RobustFitter (initial
OLS, every IRLS step, and its covariance) and ParameterEstimation all move
over, as does the covariance estimate in NonlinearLeastSquaresFitter.

The Levenberg-Marquardt iteration itself does not move. Marquardt damping is
applied to JtJ's diagonal, which has no design-matrix equivalent short of the
augmented formulation [J; sqrt(lambda*D)] - a change to the algorithm, not to
the solver, and so a separate decision. It is noted in the code, the roadmap
and the section README.

Rank deficiency is now detected by QrDecomposition.IsFullRank, which uses a
relative threshold, rather than by an absolute 1e-14 pivot test. The exception
type is unchanged, so NonlinearLeastSquaresFitter's catch still breaks its
loop as before.

Add FittingConditioningTests, which pin the accuracy down with a tolerance of
1e-9 - comfortably met by QR, far out of reach of the normal equations - so a
revert to XtX fails loudly instead of quietly losing digits.

Suite: 1602 passing, 1 ignored, verified in Release configuration.
Four items that had been on the list since v4.1 and never done.

NaiveBayes.NumClasses threw NotImplementedException - the only such member
left in the library, and the only IClassificationModel that did not provide
it. It is now set during Fit as the highest label plus one, matching
DecisionTree, RandomForest, KernelSVC, KNearestNeighbors and LinearSVC, so
callers can use it to size label-indexed arrays. The model had no tests at
all, so add a suite covering that, separation of two Gaussian blobs,
classification of unseen points, the unfitted-model guard, and the
hyperparameters-only Clone convention.

Remove the csproj reference excluding NumericExtensions.cs~RF43b9e4.TMP. The
file has not existed for a long time.

Remove the unused SARSA._pendingNextAction field, which produced a CS0169
warning on every target framework. Library warnings drop from three to zero;
the two that remain are unused locals in the test project and were not in
scope.

Consolidate four roadmaps that existed in both docs/ and docs/completed/.
Three resolved straightforwardly, but the pairs were not simply duplicates
and the choice mattered: the docs/ copy of AdvancedGameEngineRoadMap had
become entirely unchecked while the completed/ copy was checked, and the
completed/ copy is the one that matches the code - the Aerodynamics models
are present, and AircraftState and FlightDynamicsEngine are absent only
because Engines.Game was extracted into its own package. Multiphysics was the
reverse: the docs/ copy recorded the 1-D FEM primitives as done, which they
are, while the completed/ copy still called them deferred. That copy is
replaced by the accurate one, with its Assembler1D line corrected to say
LuDecomposition rather than Gaussian elimination, which this branch changed.

Suite: 1608 passing, 1 ignored, verified in Release configuration.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant