Detect global instability via the solution residual (fixes #255, #275)#320
Closed
gaoflow wants to merge 1 commit into
Closed
Detect global instability via the solution residual (fixes #255, #275)#320gaoflow wants to merge 1 commit into
gaoflow wants to merge 1 commit into
Conversation
A globally unstable model -- a mechanism (for example a beam with an internal hinge and no support there) or a structure with insufficient supports -- has a singular global stiffness matrix even though every diagonal term is non-zero. The existing nodal stability check only inspects the diagonal for unsupported zero-stiffness degrees of freedom, so it cannot see this kind of global rank deficiency. The underlying solvers do not reliably report it either: scipy's spsolve returns NaN (with only a MatrixRankWarning) or even finite-but-meaningless displacements, and numpy.linalg.solve can miss the singularity due to round-off. The model then silently returns erroneous reactions and displacements. This adds Analysis._solve_unknown_disp, which solves for the unknown displacements and, when check_stability is enabled, verifies the solution against equilibrium using the relative residual ||K11 @ x - rhs|| / ||rhs||. A stable structure -- even a numerically ill-conditioned one -- solves to within round-off because the direct solvers are backward stable, whereas a singular system produces a non-finite or large-residual result. Unlike a pivot-magnitude/rank threshold, the residual test does not misclassify legitimately ill-conditioned but stable models (verified against the existing suite, which includes stiff mat-foundation and plate meshes whose smallest LU pivots are already near machine epsilon while their solves remain accurate). The first-order elastic/TC path (Analysis._first_order) and the linear path (FEModel3D.analyze_linear) now route through this helper. The check is gated on the existing check_stability flag, so the historical fast path is preserved. Added Testing/test_instability.py covering the hinge mechanism and the unsupported-model scenarios (sparse and dense), that a stable propped cantilever still solves to the correct reaction, and that check_stability=False bypasses the check. Fixes JWock82#255 Fixes JWock82#275
Owner
|
Thanks for this excellent contribution. I made a few improvements to this with the help of GitHub Copilot and merged it all in PR #323. |
Contributor
Author
|
Thanks for folding it into #323 — glad the global-instability detection was useful, and the Copilot polish on top sounds great. Happy to see it land. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #255 and #275. A globally unstable model — a mechanism (e.g. a beam with an internal hinge and no support at the hinge) or a structure with insufficient supports (rigid-body motion) — has a singular global stiffness matrix even though every diagonal term is non-zero.
analyze()currently solves it anyway and silently returns erroneous reactions, shears, moments and displacements instead of raising.The existing nodal stability check (
Analysis._check_stability) only inspects the diagonal for unsupported zero-stiffness DOFs, so it cannot see this kind of global rank deficiency. The underlying solvers don't reliably report it either:scipy.sparse.linalg.spsolvereturnsNaN(with only aMatrixRankWarning), or in some cases finite-but-meaningless displacements with no warning at all (No error is thrown on unstable model in some scenarios #255 scenario 2).numpy.linalg.solvecan miss the singularity due to round-off (No error is thrown on unstable model in some scenarios #255 scenario 3), and neither solver flags the hinge mechanism of Model produces results when unstable with a hinge #275.Approach
I add
Analysis._solve_unknown_disp(K11, rhs, sparse, check_stability), which solves for the unknown displacements and, whencheck_stabilityis enabled, verifies the solution against equilibrium using the relative residual‖K11·x − rhs‖ / ‖rhs‖. A stable structure — even a numerically ill-conditioned one — solves to within round-off because the direct solvers are backward-stable, whereas a singular system yields a non-finite or O(1)-residual result.I first tried the natural pivot-magnitude / rank approach (factorize and compare the smallest LU pivot to the largest). It is not sound for this codebase: several stable models in the existing test suite (e.g. the Kassimali 2D frame, and the mat-foundation / plate meshes) have a smallest-to-largest pivot ratio already down at ≈1e-17, indistinguishable from a genuine mechanism, so any pivot threshold misclassifies them. Measured across the suite, the worst stable relative residual is ≈2.4e-13, while the unstable models from #255/#275 give residuals of order 1 — a clean ~13-order-of-magnitude separation. The default tolerance is
1e-6.The check is cheap (one matrix–vector product plus two norms, no extra factorization) and gated on the existing
check_stabilityflag, so the historical fast path is preserved when it is disabled.Scope
The first-order elastic / tension-compression path (
Analysis._first_order, used byanalyze()) and the linear path (FEModel3D.analyze_linear) now route through the helper. The P-Delta and pushover paths are intentionally left unchanged — they deliberately drive the structure toward buckling (where the tangent stiffness approaches singular by design), so a hard instability raise there would need separate, buckling-aware handling.Reproduction & verification
All four reported scenarios now raise
The stiffness matrix is singular ...instead of returning results, for both the sparse and dense solvers:sparse=False.Added
Testing/test_instability.pycovering the hinge mechanism and the unsupported-model scenarios (sparse and dense), that a stable propped cantilever still solves to the correct reaction (5wL/8), and thatcheck_stability=Falsebypasses the check.The full core test suite passes (the optional-dependency test files for the visualization/VTK backends fail to collect locally for unrelated reasons and were excluded): 121 passed (115 existing + 6 new), with no regressions.