Source code for femorph_solver.exceptions
"""Typed exception hierarchy for femorph-solver.
A small, audit-flagged set of exception classes — every error a
caller might want to catch by *kind* (mesh shape, BC mismatch,
solver unavailable, didn't converge) is its own subclass of
:class:`FemorphSolverError`. Bare ``ValueError`` / ``RuntimeError``
remain reserved for genuinely-unexpected internal-invariant
violations that the caller can't recover from.
Why this hierarchy exists
-------------------------
The audit (issue #750 + the second-pass audit) flagged that AI
agents and downstream services calling ``femorph_solver`` from
LLM-orchestrated workflows must currently catch bare ``ValueError``
and string-match the message to know what failed. That's fragile
and breaks when the message wording is polished.
After this hierarchy lands, the contract is:
* Caller wants "any femorph problem" → ``except FemorphSolverError``.
* Caller wants "user-fixable mesh / BC issue" → ``except (MeshError,
ConstraintError, MaterialError)``.
* Caller wants "I'll retry on a different backend" →
``except SolverUnavailableError``.
* Caller wants "I'll retry with looser tolerance / more modes" →
``except ConvergenceError``.
The hierarchy is intentionally shallow (one level deep) — each
concrete class is its own intent, not an inheritance ladder.
``SolverUnavailableError`` still subclasses ``RuntimeError`` for
back-compat with code that catches it that way today.
"""
from __future__ import annotations
__all__ = [
"ConstraintError",
"ConvergenceError",
"FemorphSolverError",
"MaterialError",
"MeshError",
"SolverUnavailableError",
"ValidationError",
]
class FemorphSolverError(Exception):
"""Root exception for every error femorph-solver itself raises.
Catching this catches every typed error the library raises but
nothing else — it does not subsume bare ``ValueError`` from
third-party libraries (numpy, scipy, pyvista) that propagate
through. For "anything that went wrong inside a solve," catch
``FemorphSolverError``; for "anything at all," catch the broader
``Exception``.
"""
class MeshError(FemorphSolverError):
"""Mesh topology / connectivity problem.
Examples: an element type is not registered, a cell has the
wrong vertex count for its declared element type, the mesh has
no points, or the cyclic-pair detection couldn't pair the low /
high faces.
"""
class MaterialError(FemorphSolverError):
"""Material assignment or property-table problem.
Examples: an element has no material assigned, a material lacks
a required property (e.g. ``EX``), or a value is non-physical
(negative density).
"""
class ConstraintError(FemorphSolverError):
"""Boundary-condition / DOF / coupling problem.
Examples: a Dirichlet constraint refers to a node that doesn't
exist, a coupling chain forms a cycle, an invalid DOF label,
or all DOFs are constrained leaving an empty free system.
"""
class ConvergenceError(FemorphSolverError):
"""Iterative solver failed to converge within tolerance.
Carries optional ``residual`` and ``n_iter`` attributes so the
caller can decide whether to retry with looser tolerance, more
iterations, or a different backend.
"""
def __init__(
self,
message: str,
*,
residual: float | None = None,
n_iter: int | None = None,
) -> None:
super().__init__(message)
self.residual = residual
self.n_iter = n_iter
[docs]
class SolverUnavailableError(FemorphSolverError, RuntimeError):
"""A registered solver backend isn't available on this install.
Typical case: a CHOLMOD / Pardiso / MUMPS optional dependency
isn't pip-installed. Carries an ``install_hint`` attribute the
caller can show the user to recover.
Subclasses ``RuntimeError`` as well as ``FemorphSolverError`` so
pre-PR-N code that does ``except RuntimeError`` keeps working.
"""
def __init__(self, message: str, *, install_hint: str = "") -> None:
super().__init__(message)
self.install_hint = install_hint
class ValidationError(FemorphSolverError):
"""Aggregated validation failure raised by ``Model.validate(strict=True)``.
Carries the full ``findings`` list so callers can inspect each
individual problem. When ``strict=False`` the validator returns
findings instead of raising.
"""
def __init__(self, findings: list[str]) -> None:
n = len(findings)
super().__init__(f"Model has {n} validation issue(s):\n - " + "\n - ".join(findings))
self.findings = list(findings)