Source code for bngsim._ssa_validation
"""SSA-compatibility validation for SBML-loaded models.
The SBML loader (``_sbml_loader.py``) records SSA-relevant constructs at
load time and stashes the resulting :class:`SsaIssue` records on
``Model._ssa_issues``. :func:`validate_for_ssa` returns that list, plus
runtime warnings for fractional initial SSA populations that the simulator
will round at setup.
:class:`bngsim.Simulator` calls :func:`validate_for_ssa` when
``method="ssa"`` is requested and raises
:class:`bngsim.SsaValidationError` if any issue has
``severity == "error"``. Warnings are emitted via the ``bngsim`` logger.
See ``dev/plans/SBML_SSA_SUPPORT_PLAN.md`` Phase 3.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal
if TYPE_CHECKING:
from bngsim._model import Model
[docs]
@dataclass(frozen=True)
class SsaIssue:
"""A single SSA-compatibility finding from an SBML model.
Attributes
----------
severity
``"error"`` (Simulator init will raise) or ``"warning"`` (logged).
code
Stable machine-readable identifier for the issue category.
message
Human-readable description, ideally pointing the user at a fix.
location
Optional locator string (e.g. ``"reaction:R1"``,
``"compartment:cell"``, ``"species:S"``).
"""
severity: Literal["error", "warning"]
code: str
message: str
location: str | None = None
[docs]
def validate_for_ssa(model: Model) -> list[SsaIssue]:
"""Return SSA-compatibility issues recorded for *model*.
The list is populated by the SBML loader at load time, with an
additional model-agnostic warning when the current species state
contains non-integer molecule populations. Fractional populations are
rounded by the C++ SSA setup path before simulation.
PyBNF and other consumers can call this before constructing a
``Simulator`` to pre-flight a model — the same list will be re-checked
by ``Simulator(model, method="ssa")``.
"""
issues = list(getattr(model, "_ssa_issues", None) or [])
try:
species_meta = list(model._core.codegen_data()["species"])
except Exception:
species_meta = []
for sp in species_meta:
name = str(sp.get("name", ""))
volume_factor = float(sp.get("volume_factor", 1.0))
if not name or not math.isfinite(volume_factor) or volume_factor <= 0.0:
continue
try:
storage_value = float(model.get_concentration(name))
except Exception:
continue
amount = storage_value * volume_factor
if not math.isfinite(amount):
continue
rounded_amount = math.floor(amount + 0.5) if amount >= 0.0 else math.ceil(amount - 0.5)
if abs(amount - rounded_amount) <= 1e-9 * max(1.0, abs(amount)):
continue
issues.append(
SsaIssue(
severity="warning",
code="non_integer_initial_population",
message=(
"Initial SSA population is non-integer; bngsim will round it "
f"to {rounded_amount:g} before simulation."
),
location=f"species:{name}",
)
)
return issues