"""bngsim exception hierarchy.
All bngsim exceptions inherit from :class:`BngsimError`, which inherits from
``RuntimeError``. This allows ``except BngsimError`` to catch all bngsim-specific
errors while still being caught by ``except RuntimeError``.
Hierarchy::
BngsimError (RuntimeError)
├── ModelError — .net parse failures, invalid model state
├── SimulationError — solver failures (convergence, NaN, etc.)
├── SimulationTimeout — wall-clock budget exceeded during run()
├── ParameterError — unknown parameter name, type mismatch
├── SsaValidationError — SBML construct incompatible with SSA
└── StopConditionMet — stop condition triggered (carries partial result)
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bngsim._ssa_validation import SsaIssue
[docs]
class BngsimError(RuntimeError):
"""Base exception for all bngsim errors."""
[docs]
class ModelError(BngsimError):
"""Error loading or manipulating a model.
Raised when:
- A .net file cannot be parsed
- A model is in an invalid state for simulation
- A reserved name conflict is detected
"""
[docs]
class SimulationError(BngsimError):
"""Error during simulation.
Raised when:
- CVODE fails to converge
- NaN/Inf detected in species concentrations
- Maximum number of steps exceeded
"""
[docs]
class ParameterError(BngsimError):
"""Error with parameter access.
Raised when:
- A parameter name is not found in the model
- A parameter value is invalid (NaN, Inf, wrong type)
"""
[docs]
class SsaValidationError(BngsimError):
"""An SBML model contains constructs that cannot be simulated under SSA.
Raised by :class:`bngsim.Simulator` when ``method="ssa"`` is requested
on a model whose loader-captured SSA issues include any with
``severity="error"``. The full issue list (errors and warnings) is
available as :attr:`issues`.
The error message advertises ``strict_ssa=False`` as a workaround when
at least one of the raised codes is overridable. ``override_attempted``
flips the message to call out that the user did pass
``strict_ssa=False`` but the listed codes are non-overridable.
"""
# Codes that can NEVER be downgraded to a warning, even with
# ``strict_ssa=False``. Kept here (not in _simulator.py) so that the
# error envelope can format its hint without a circular import.
NON_OVERRIDABLE_CODES = frozenset({"non_integer_stoichiometry", "fast_reaction"})
def __init__(
self,
issues: list[SsaIssue],
*,
override_attempted: bool = False,
) -> None:
self.issues = list(issues)
self.override_attempted = override_attempted
super().__init__(self._format(self.issues, override_attempted))
@classmethod
def _format(cls, issues: list[SsaIssue], override_attempted: bool = False) -> str:
errors = [i for i in issues if i.severity == "error"]
if not errors:
return "Model has SSA-validation issues but none are errors."
n = len(errors)
plural = "s" if n != 1 else ""
if override_attempted:
lead = (
f"Model has SSA-validation error{plural} that strict_ssa=False "
f"cannot override ({n} error{plural}). The codes below sit "
"in the non-overridable set because they violate SSA's "
"discrete-fire kernel rather than just the kineticLaw shape:"
)
else:
lead = f"Model is incompatible with SSA simulation ({n} error{plural}):"
lines = [lead]
for iss in errors:
loc = f" [{iss.location}]" if iss.location else ""
lines.append(f" - {iss.code}{loc}: {iss.message}")
# Advertise the strict_ssa=False lever ONLY when (a) we're not
# already inside a non-overridable raise, and (b) at least one of
# the listed codes is in fact overridable. Otherwise the hint
# would mislead the user into thinking the flag will help.
if not override_attempted:
overridable = [i for i in errors if i.code not in cls.NON_OVERRIDABLE_CODES]
if overridable:
lines.append("")
lines.append(
"To bypass this gate and accept approximate SSA "
"dynamics on the overridable codes, pass "
"strict_ssa=False to bngsim.Simulator(...). Note: "
+ ", ".join(sorted(cls.NON_OVERRIDABLE_CODES))
+ " cannot be overridden — those need a model fix."
)
return "\n".join(lines)
[docs]
class SimulationTimeout(BngsimError):
"""Raised when a simulation exceeds its wall-clock budget.
Distinct from :class:`SimulationError` so callers (e.g. PyBNF's
``wall_time_sim``) can classify wall-clock terminations separately from
solver/convergence failures. Inherits from :class:`BngsimError` (and
therefore ``RuntimeError``).
Attributes
----------
timeout : float
The configured wall-clock budget, in seconds.
elapsed : float
Actual elapsed wall-clock time at the point the timeout fired, in
seconds. ``elapsed >= timeout`` always holds.
partial_result : Result | None
Reserved for future use. Currently always ``None`` — bngsim does not
yet salvage a partial Result from a timed-out run.
"""
def __init__(
self,
message: str,
*,
timeout: float = 0.0,
elapsed: float = 0.0,
partial_result: object = None,
) -> None:
super().__init__(message)
self.timeout = float(timeout)
self.elapsed = float(elapsed)
self.partial_result = partial_result
[docs]
class StopConditionMet(BngsimError):
"""A stop condition was triggered during simulation.
The partial result up to the trigger point is attached as ``self.result``.
Attributes
----------
result : Result
Partial simulation result truncated at the stop point.
condition : str
Description of the condition that triggered.
"""
def __init__(self, message: str, *, result: object = None, condition: str = "") -> None:
super().__init__(message)
self.result = result
self.condition = condition
[docs]
class ConversionError(BngsimError):
"""A format conversion (e.g. SBML→.net) cannot be completed faithfully.
Raised by :func:`bngsim.convert.sbml_to_net` when the source model uses a
construct that the target format cannot represent without changing the
model's meaning — for example an ``hasOnlySubstanceUnits`` species in a
compartment whose volume is not 1, a cross-compartment reaction that needs
per-species volume scaling, a rate-rule ODE, a Michaelis–Menten rate-law
type, or a table (interpolation) function. The message names the offending
construct and the model so the cause is actionable.
Pass ``strict=False`` (API) / ``--allow-lossy`` (CLI) to downgrade these to
a :class:`ConversionWarning` and emit a best-effort ``.net`` anyway.
"""
[docs]
class ConversionWarning(UserWarning):
"""A format conversion dropped or approximated part of the source model.
Emitted when the network channel is preserved but something outside it was
left behind — most commonly SBML ``<event>`` elements, which belong to the
simulation-protocol channel (a SED-ML sidecar), not the ``.net`` network.
Also used when ``strict=False`` downgrades an otherwise-fatal
:class:`ConversionError`. Filter or escalate it like any ``UserWarning``::
import warnings, bngsim
warnings.simplefilter("error", bngsim.ConversionWarning) # promote
warnings.simplefilter("ignore", bngsim.ConversionWarning) # silence
"""
[docs]
class SsaBoundaryWarning(UserWarning):
"""An SSA run hit a literal-rate-law boundary condition (GH #110).
Emitted once per run when the exact SSA either drove a species count
negative (no non-negativity floor is applied — non-negativity is the rate
law's responsibility, matching the CVODE path) or fired a reaction in
reverse because its rate law evaluated negative. Both keep the SSA mean
consistent with the ODE; the warning surfaces what would otherwise be a
silent boundary behavior. Filter or escalate it like any ``UserWarning``::
import warnings, bngsim
warnings.simplefilter("error", bngsim.SsaBoundaryWarning) # promote
warnings.simplefilter("ignore", bngsim.SsaBoundaryWarning) # silence
The structured counts are always available on ``result.ssa_diagnostics``
regardless of warning filters.
"""
[docs]
class DenseSolverFallbackWarning(UserWarning):
"""A large ODE model is running on the dense solver for lack of KLU (GH #209).
Emitted once per process when ``Simulator.run()`` integrates a large model
(``n_species`` past the warn threshold) on the **dense** linear solver only
because this bngsim install was built **without** SuiteSparse/KLU — not
because the user asked for ``force_dense_linear_solver`` or
``jacobian="jax"`` (both legitimately dense). Dense LU factorizes the full
N×N Jacobian at O(N³); for a sparse genome-scale network that is the
difference between minutes and hours. The fix is to rebuild bngsim with KLU
(see :func:`bngsim.capabilities` and GH #209). Filter or escalate it like any
``UserWarning``::
import warnings, bngsim
warnings.simplefilter("ignore", bngsim.DenseSolverFallbackWarning) # silence
warnings.simplefilter("error", bngsim.DenseSolverFallbackWarning) # promote
"""