Handle singular Newton matrices in the Anderson solver by milanofthe · Pull Request #241 · pathsim/pathsim · GitHub
Skip to content

Handle singular Newton matrices in the Anderson solver - #241

Merged
milanofthe merged 1 commit into
masterfrom
fix/steadystate-singular-newton
Jul 26, 2026
Merged

milanofthe merged 1 commit into
masterfrom
fix/steadystate-singular-newton

Conversation

@milanofthe

Copy link
Copy Markdown
Member

Summary

steadystate() crashed with LinAlgError: SVD did not converge in Linear Least Squares on a PID plus nonlinear plant loop. The message points at the Anderson least squares, which is not where the problem is.

Root cause: the Newton matrix jac - I is structurally singular whenever a state's derivative does not depend on that state.

  • A PID has dx/dt = u for its integrator state, so that Jacobian row is zero. An Integrator is the same shape.
  • dx/dt = -x**2 + u has a zero Jacobian at x = 0, which is exactly where the first iterate lands.

scipy.linalg.lu_factor does not raise on a singular matrix, it emits a LinAlgWarning and leaves a zero pivot behind, after which lu_solve returns inf/nan. The scalar path divides by jac - 1.0 with no guard at all. Those values propagate through the connections into other blocks and only surface many iterations later, somewhere unrelated, as the SVD failure.

Changes

optim/anderson.py:

  • _factorize() returns None for a singular matrix instead of a factorization with a zero pivot, checking the diagonal of U against a scaled tolerance.
  • _singular_step() applies the pseudo-inverse on the range of the Newton matrix and makes no correction on its null space, where the residual carries no information about the state.
  • The scalar path guards the division the same way.

Recorded in the docstring because it is not obvious: substituting a plain fixed-point step x <- g on the null space was tried and is worse. On a closed loop it injects the full residual every sweep and the iteration diverges (residual 1e11) rather than stalling.

What this does and does not fix

It fixes the crash. The reported PID loop still does not converge, and now says so truthfully with per-block residuals instead of failing with an unrelated exception.

That remaining non-convergence is a property of the block-local steady-state scheme, not of this change: _solve runs a Gauss-Seidel sweep where each block solves only its own equations, and a PID's integrator equilibrium is set by the outer loop, which the local solve cannot see. Worth its own issue.

Test plan

  • Full suite: 1064 passed, 13 skipped
  • New unit tests for _factorize and _singular_step, plus system-level tests that the loop no longer raises LinAlgError and that no inf/nan reaches the block states
  • A solvable system with the same singularity (a stable loop around an Integrator) still converges to its equilibrium
  • Verified against the old implementation: the new tests fail with exactly the original LinAlgError

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

@milanofthe
milanofthe merged commit f8f0229 into master Jul 26, 2026
4 checks passed
@milanofthe
milanofthe deleted the fix/steadystate-singular-newton branch August 12, 2026 13:26
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