core/state_layout.py — Mapping RAG vertices to the flat ODE state vector.
Host-independent. The reaction admissibility graph defines *which*
clusters exist; integration needs them packed into a single contiguous
state vector y in R^{N_eq}. :class:`StateLayout` is the bijection
(block, local index) <-> global ODE index
It is built from a list of named :class:`Block` objects laid out
contiguously. This formalises — in one place — the index bookkeeping
that the legacy ``rate_equations.py`` / ``bin_moment_rates.py`` scattered
across ad-hoc attributes (``i_SIA``, ``i_VAC``, ``i_Q``, ``i_He``,
``i_J_*``); the graph walker and the C++ bridge both consume a
StateLayout instead of re-deriving offsets.
A block is one of:
* ``discrete`` — one ODE per size for a (polarity, population) axis
* ``bin_moment`` — discrete prefix + P moments per logarithmic bin
* ``scalar`` — a single auxiliary ODE (e.g. free He, Q_tot)
* ``aux`` — a fixed-length auxiliary array (e.g. the five
cumulative conservation-accounting integrals)
The layout carries no physics and no host parameters; a host's
state-space-reduction choice (full discrete vs. bin-moment, He Case 1 vs.
Case 2) is expressed purely as the *set of blocks* it adds.