2core/state_layout.py — Mapping RAG vertices to the flat ODE state vector.
4Host-independent. The reaction admissibility graph defines *which*
5clusters exist; integration needs them packed into a single contiguous
6state vector y in R^{N_eq}. :class:`StateLayout` is the bijection
8 (block, local index) <-> global ODE index
10It is built from a list of named :class:`Block` objects laid out
11contiguously. This formalises — in one place — the index bookkeeping
12that the legacy ``rate_equations.py`` / ``bin_moment_rates.py`` scattered
13across ad-hoc attributes (``i_SIA``, ``i_VAC``, ``i_Q``, ``i_He``,
14``i_J_*``); the graph walker and the C++ bridge both consume a
15StateLayout instead of re-deriving offsets.
19 * ``discrete`` — one ODE per size for a (polarity, population) axis
20 * ``bin_moment`` — discrete prefix + P moments per logarithmic bin
21 * ``scalar`` — a single auxiliary ODE (e.g. free He, Q_tot)
22 * ``aux`` — a fixed-length auxiliary array (e.g. the five
23 cumulative conservation-accounting integrals)
25The layout carries no physics and no host parameters; a host's
26state-space-reduction choice (full discrete vs. bin-moment, He Case 1 vs.
27Case 2) is expressed purely as the *set of blocks* it adds.
29from __future__
import annotations
31from dataclasses
import dataclass, field
32from typing
import Dict, List, Tuple
34_KINDS = (
"discrete",
"bin_moment",
"scalar",
"aux")
37@dataclass(frozen=True)
39 """One contiguous segment of the ODE state vector.
44 Unique identifier of the block within a layout.
46 One of ``discrete``, ``bin_moment``, ``scalar``, ``aux``.
48 Number of ODE entries the block occupies.
50 Block-specific descriptors, e.g. for ``bin_moment``:
51 ``n_discrete``, ``n_bins``, ``moments_per_bin``; for a
52 ``discrete`` size axis: ``population``, ``n_max``.
58 meta: Dict = field(default_factory=dict)
61 if self.
kind not in _KINDS:
63 f
"Block {self.name!r}: kind must be one of {_KINDS} "
64 f
"(got {self.kind!r})")
66 raise ValueError(f
"Block {self.name!r}: length must be >= 0")
70 """An ordered set of :class:`Block` s laid out contiguously in y."""
74 self._offset: Dict[str, int] = {}
79 """Append a block; its global offset is the running total length."""
81 raise RuntimeError(
"cannot add blocks to a frozen StateLayout")
82 if block.name
in self._offset:
83 raise ValueError(f
"duplicate block name {block.name!r}")
84 self._offset[block.name] = self.
N_eq
89 """Add a one-ODE-per-size discrete axis covering sizes 1..n_max."""
91 {
"n_max": n_max, **meta}))
94 moments_per_bin: int, **meta) -> Block:
95 """Add a hybrid discrete-prefix + bin-moment axis."""
96 length = n_discrete + moments_per_bin * n_bins
98 {
"n_discrete": n_discrete,
100 "moments_per_bin": moments_per_bin,
104 """Add a single auxiliary ODE."""
107 def add_aux(self, name: str, length: int, **meta) -> Block:
108 """Add a fixed-length auxiliary array (e.g. accounting integrals)."""
112 """Lock the layout; no further blocks may be added."""
119 """Total number of ODEs across all blocks."""
126 def has(self, name: str) -> bool:
127 return name
in self._offset
129 def block(self, name: str) -> Block:
133 raise KeyError(f
"no block {name!r} in StateLayout")
136 """Global ODE index of the first entry of block ``name``."""
138 return self._offset[name]
140 raise KeyError(f
"no block {name!r} in StateLayout")
from None
142 def slice(self, name: str) -> slice:
143 """Global index ``slice`` spanning block ``name``."""
145 return slice(off, off + self.
block(name).length)
147 def index(self, name: str, local: int) -> int:
148 """Global ODE index of local entry ``local`` within block ``name``.
150 For a ``discrete`` size axis, ``local`` is the 0-based size index
151 (size n maps to local n-1).
153 blk = self.
block(name)
154 if not (0 <= local < blk.length):
156 f
"local index {local} out of range for block {name!r} "
157 f
"(length {blk.length})")
158 return self._offset[name] + local
161 lines = [f
"StateLayout: N_eq = {self.N_eq}"]
163 off = self._offset[b.name]
164 lines.append(f
" [{off:6d}:{off + b.length:6d}] "
165 f
"{b.name:24s} {b.kind:10s} len={b.length}")
166 return "\n".join(lines)
172 return (f
"<StateLayout N_eq={self.N_eq} blocks={len(self._blocks)} "
173 f
"{'frozen' if self._frozen else 'open'}>")
Block block(self, str name)
int index(self, str name, int local)
Block add_block(self, Block block)
"StateLayout" freeze(self)
Block add_scalar(self, str name, **meta)
Block add_aux(self, str name, int length, **meta)
Block add_bin_moment(self, str name, int n_discrete, int n_bins, int moments_per_bin, **meta)
int offset(self, str name)
Block add_discrete(self, str name, int n_max, **meta)
slice slice(self, str name)
Tuple[Block,...] blocks(self)