Skip to content

Tighten instability detection exception flow and zero-load singular checks#323

Merged
JWock82 merged 5 commits into
mainfrom
copilot/update-instability-detection-implementation
Jun 6, 2026
Merged

Tighten instability detection exception flow and zero-load singular checks#323
JWock82 merged 5 commits into
mainfrom
copilot/update-instability-detection-implementation

Conversation

Copilot AI commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

This PR refines the instability-detection changes in the current analysis path to address review concerns: broad exception masking, over-broad sparse warning suppression, and under-specified handling of homogeneous (rhs == 0) solves. The update keeps scope limited to solver stability detection/error handling and related coverage.

  • Analysis-path error handling

    • Removed broad except: wrappers around the new _solve_unknown_disp call sites in modified first-order/linear paths.
    • Unexpected failures now surface normally; singular/instability cases still map to the existing instability message through intentional handling inside the solve helper.
  • Sparse warning scope

    • Replaced blanket warning suppression around spsolve with targeted handling of scipy.sparse.linalg.MatrixRankWarning only.
    • Captures rank-deficiency signal without suppressing unrelated warnings.
  • rhs == 0 (homogeneous) behavior

    • Added explicit zero-load logic in _solve_unknown_disp: when rhs is zero, treat non-zero solved displacement norm as unstable (x must be ~0 for invertible systems).
    • Preserves existing residual-based check for nonzero rhs.
  • Targeted coverage update

    • Added a focused instability test for the homogeneous edge case to ensure zero-load singular behavior is enforced.
# _solve_unknown_disp (stability check core)
residual = norm(K @ x - rhs)
rhs_norm = norm(rhs)
if rhs_norm > 0:
    unstable = residual > tol * rhs_norm
else:
    unstable = norm(x) > tol  # homogeneous system: unique stable solution is x == 0

if (sparse and singular_warning) or not isfinite(x).all() or unstable:
    raise Exception(_SINGULAR_MSG)
Original prompt

Please update PR #320 in repository JWock82/Pynite by addressing the review concerns in the current changes for the instability-detection implementation.

Context:

  • The current PR introduces _solve_unknown_disp to detect global instability by checking finiteness and the solution residual after solving K11 @ x = rhs.
  • The current implementation still uses broad except: handlers around the call sites and suppresses all warnings around spsolve.
  • The current implementation may also under-handle the rhs == 0 edge case.

Required changes:

  1. Replace broad bare except: handlers in the modified analysis paths with narrower, intentional exception handling that preserves unexpected errors instead of always converting them into the generic singular-matrix message.
  2. Narrow the sparse warning suppression so it suppresses only the intended singular/rank-related warning from SciPy, not all warnings.
  3. Review and improve handling of the rhs == 0 / zero-load edge case in _solve_unknown_disp so the instability detection is more principled and does not silently skip an important singular case.
  4. Update or add tests as needed to cover the changed behavior.
  5. Keep the fix scoped to this PR’s purpose: instability detection and related error handling/readability improvements.

Please make the minimal, reviewable change set needed to address these points and open a pull request with the fixes.

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: Explain these changes.

Assistant: This PR makes PyNite detect a broader class of unstable models by checking the solution residual after solving for displacements, instead of relying only on obvious singular-matrix failures.

In a nutshell

Before this change, analysis could miss some global instability cases: the stiffness matrix might be rank-deficient even though its diagonal entries look fine, and the solver could return NaN or even plausible-looking but wrong numbers. This PR adds a shared helper, _solve_unknown_disp, that solves the system and then verifies the result by checking whether K11 @ x actually matches the right-hand side. If not, it raises the usual “structure is unstable” exception.

What changed

1. New _solve_unknown_disp(...) helper

A new function centralizes solving:

  • supports both sparse (scipy.sparse.linalg.spsolve) and dense (numpy.linalg.solve) paths
  • optionally performs a post-solve stability check
  • raises a consistent instability exception using _SINGULAR_MSG

Its key idea is:

  • solve K11 @ x = rhs
  • compute whether the solution is valid by checking:
    • all values in x are finite
    • residual is small: ||K11 @ x - rhs|| <= tol * ||rhs||

If either fails, the matrix is treated as singular/unstable.

2. Global instability is now detected more reliably

The docstring explains the motivation well:

  • _check_stability(...) only catches cases with zero diagonal stiffness
  • it does not catch mechanisms where the full matrix is singular despite nonzero diagonals
  • examples:
    • internal hinge/release mechanisms
    • insufficient supports / rigid-body motion

This PR addresses those cases using the residual test.

3. Sparse solver warnings are suppressed and replaced with a clearer error path

For sparse solves:

  • spsolve may emit a MatrixRankWarning
  • the code now suppresses that warning with warnings.catch_warnings()
  • instead of letting the warning leak through, it validates the result and raises a clear exception if unstable

That gives users a more consistent API-level failure.

4. _first_order(...) now uses the helper

Previously _first_order solved directly:

  • sparse path: spsolve(...)
  • dense path: solve(...)

Now it:

  • computes rhs first
  • calls _solve_unknown_disp(K11, rhs, sparse, check_stability)

So the first-order analysis path gets the new global-instability detection automatically.

5. Imports updated

New imports support the helper:

  • warnings
  • asarray, isfinite from NumPy
  • norm from numpy.linalg

Why this matters

This is mainly a correctness and safety fix.

Without it, users could get:

  • NaN displacement results, or worse
  • finite but incorrect results from an unstable model

With this change, PyNite is more likely to fail fast with an explicit instability error instead of silently continuing with invalid analysis results.

Behavior impact

Practically:

  • stable models should behave the same
  • unstable or globally singular models are more likely to raise an exception consistently
  • models that previously “solved” but produced garbage may now correctly fail

One subtle detail

The residual check only triggers when:

  • check_stability=True, and
  • either solution entries are non-finite, or
  • residual is too large relative to ||rhs||

So this adds protection without changing the bare-solver path when stability checking ...

This pull request was created from Copilot chat.

gaoflow and others added 3 commits June 6, 2026 04:04
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 #255
Fixes #275
Copilot AI changed the title [WIP] Update instability detection implementation based on review feedback Tighten instability detection exception flow and zero-load singular checks Jun 6, 2026
Copilot AI requested a review from JWock82 June 6, 2026 04:09
Copilot AI requested a review from JWock82 June 6, 2026 04:21
@codecov

codecov Bot commented Jun 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.22222% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 71.45%. Comparing base (147c7aa) to head (25d2bda).

Files with missing lines Patch % Lines
Pynite/FEModel3D.py 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #323      +/-   ##
==========================================
- Coverage   71.46%   71.45%   -0.01%     
==========================================
  Files          23       23              
  Lines        9895     9911      +16     
==========================================
+ Hits         7071     7082      +11     
- Misses       2824     2829       +5     
Flag Coverage Δ
unittests 71.45% <97.22%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JWock82 JWock82 marked this pull request as ready for review June 6, 2026 04:25
@JWock82 JWock82 merged commit 8d634fe into main Jun 6, 2026
4 checks passed
@JWock82 JWock82 deleted the copilot/update-instability-detection-implementation branch June 6, 2026 04: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.

3 participants