Source code for bngsim._named_array
"""bngsim.NamedArray — RoadRunner-compatible labeled ndarray.
Mirrors the shape of :class:`roadrunner.NamedArray`: a 2-D
:class:`numpy.ndarray` subclass that carries column names and
supports column lookup by string. Used by
:meth:`bngsim.Result.as_roadrunner` to provide a drop-in replacement
for ``rr.simulate(...)`` output in PyBNF-style stochastic-fitting
workflows.
See ``dev/plans/SBML_SSA_SUPPORT_PLAN.md`` Phase 4.
"""
from __future__ import annotations
from typing import Any
import numpy as np
from numpy.typing import NDArray
[docs]
class NamedArray(np.ndarray):
"""A 2-D ndarray with named columns (RoadRunner-compatible).
Constructed via :meth:`bngsim.Result.as_roadrunner`. End users
rarely instantiate it directly; subclass primarily exists so that
callers can identify the array shape (``arr.colnames``) and look
up columns by name (``arr["[X]"]``, ``arr[:, "[X]"]``).
Attributes
----------
colnames : list[str]
One entry per column, in column order. Always a fresh list —
slicing / arithmetic that drops columns invalidates the
original mapping, so the inherited list is left untouched on
non-trivial transforms.
Examples
--------
>>> result = sim.run(t_span=(0, 10), n_points=11)
>>> arr = result.as_roadrunner()
>>> arr.colnames
['time', '[X]', '[Y]']
>>> arr["[X]"] # 1-D column view
array([ ... ])
>>> arr[:, "[X]"] # equivalent
array([ ... ])
"""
# __slots__ omitted: ndarray subclasses must not declare __slots__
# (numpy stores subclass attributes via __array_finalize__).
def __new__(cls, data: NDArray[np.float64], colnames: list[str]) -> NamedArray:
arr = np.asarray(data, dtype=np.float64).view(cls)
if arr.ndim != 2:
raise ValueError(f"NamedArray must be 2-D, got shape {arr.shape}")
if arr.shape[1] != len(colnames):
raise ValueError(
f"colnames length {len(colnames)} does not match number of columns {arr.shape[1]}"
)
arr.colnames = list(colnames)
return arr
def __array_finalize__(self, obj: NDArray[Any] | None) -> None:
if obj is None:
return
self.colnames = list(getattr(obj, "colnames", []))
def __getitem__(self, key: Any) -> NDArray[np.float64]: # type: ignore[override]
# Forms supported:
# arr["name"] → 1-D column (RR convention)
# arr[:, "name"] → 1-D column
# arr[i, "name"] → scalar
# arr[i:j, ["a","b"]] → 2-D NamedArray slice
# Anything else falls through to ndarray.__getitem__.
if isinstance(key, str):
return self._col_by_name(key)
if isinstance(key, tuple) and len(key) == 2:
row_key, col_key = key
if isinstance(col_key, str):
idx = self._col_index(col_key)
return np.asarray(self).__getitem__((row_key, idx))
if isinstance(col_key, list) and col_key and isinstance(col_key[0], str):
idxs = [self._col_index(name) for name in col_key]
sub = np.asarray(self).__getitem__((row_key, idxs))
if sub.ndim == 2:
return NamedArray(sub, [self.colnames[i] for i in idxs])
return sub
return super().__getitem__(key)
def _col_index(self, name: str) -> int:
try:
return self.colnames.index(name)
except ValueError:
raise KeyError(self._unknown_selector_message(name)) from None
def _col_by_name(self, name: str) -> NDArray[np.float64]:
return np.asarray(self)[:, self._col_index(name)]
def _unknown_selector_message(self, name: str) -> str:
# Match RoadRunner's selector-not-found text closely enough that
# PyBNF code that catches RR errors keeps working.
return f"Invalid selection '{name}'. Valid selections: {self.colnames}"
def __repr__(self) -> str:
body = np.array2string(np.asarray(self), separator=", ")
return f"NamedArray(\n{body},\ncolnames={self.colnames})"