RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
state_layout.py
Go to the documentation of this file.
1"""
2core/state_layout.py — Mapping RAG vertices to the flat ODE state vector.
3
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
7
8 (block, local index) <-> global ODE index
9
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.
16
17A block is one of:
18
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)
24
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.
28"""
29from __future__ import annotations
30
31from dataclasses import dataclass, field
32from typing import Dict, List, Tuple
33
34_KINDS = ("discrete", "bin_moment", "scalar", "aux")
35
36
37@dataclass(frozen=True)
38class Block:
39 """One contiguous segment of the ODE state vector.
40
41 Attributes
42 ----------
43 name : str
44 Unique identifier of the block within a layout.
45 kind : str
46 One of ``discrete``, ``bin_moment``, ``scalar``, ``aux``.
47 length : int
48 Number of ODE entries the block occupies.
49 meta : dict
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``.
53 """
54
55 name: str
56 kind: str
57 length: int
58 meta: Dict = field(default_factory=dict)
59
60 def __post_init__(self) -> None:
61 if self.kind not in _KINDS:
62 raise ValueError(
63 f"Block {self.name!r}: kind must be one of {_KINDS} "
64 f"(got {self.kind!r})")
65 if self.length < 0:
66 raise ValueError(f"Block {self.name!r}: length must be >= 0")
67
68
70 """An ordered set of :class:`Block` s laid out contiguously in y."""
71
72 def __init__(self) -> None:
73 self._blocks: List[Block] = []
74 self._offset: Dict[str, int] = {}
75 self._frozen = False
76
77 # ── construction ──────────────────────────────────────────────────────
78 def add_block(self, block: Block) -> Block:
79 """Append a block; its global offset is the running total length."""
80 if self._frozen:
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
85 self._blocks.append(block)
86 return block
87
88 def add_discrete(self, name: str, n_max: int, **meta) -> Block:
89 """Add a one-ODE-per-size discrete axis covering sizes 1..n_max."""
90 return self.add_block(Block(name, "discrete", n_max,
91 {"n_max": n_max, **meta}))
92
93 def add_bin_moment(self, name: str, n_discrete: int, n_bins: int,
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
97 return self.add_block(Block(name, "bin_moment", length,
98 {"n_discrete": n_discrete,
99 "n_bins": n_bins,
100 "moments_per_bin": moments_per_bin,
101 **meta}))
102
103 def add_scalar(self, name: str, **meta) -> Block:
104 """Add a single auxiliary ODE."""
105 return self.add_block(Block(name, "scalar", 1, dict(meta)))
106
107 def add_aux(self, name: str, length: int, **meta) -> Block:
108 """Add a fixed-length auxiliary array (e.g. accounting integrals)."""
109 return self.add_block(Block(name, "aux", length, dict(meta)))
110
111 def freeze(self) -> "StateLayout":
112 """Lock the layout; no further blocks may be added."""
113 self._frozen = True
114 return self
115
116 # ── queries ───────────────────────────────────────────────────────────
117 @property
118 def N_eq(self) -> int:
119 """Total number of ODEs across all blocks."""
120 return sum(b.length for b in self._blocks)
121
122 @property
123 def blocks(self) -> Tuple[Block, ...]:
124 return tuple(self._blocks)
125
126 def has(self, name: str) -> bool:
127 return name in self._offset
128
129 def block(self, name: str) -> Block:
130 for b in self._blocks:
131 if b.name == name:
132 return b
133 raise KeyError(f"no block {name!r} in StateLayout")
134
135 def offset(self, name: str) -> int:
136 """Global ODE index of the first entry of block ``name``."""
137 try:
138 return self._offset[name]
139 except KeyError:
140 raise KeyError(f"no block {name!r} in StateLayout") from None
141
142 def slice(self, name: str) -> slice:
143 """Global index ``slice`` spanning block ``name``."""
144 off = self.offset(name)
145 return slice(off, off + self.block(name).length)
146
147 def index(self, name: str, local: int) -> int:
148 """Global ODE index of local entry ``local`` within block ``name``.
149
150 For a ``discrete`` size axis, ``local`` is the 0-based size index
151 (size n maps to local n-1).
152 """
153 blk = self.block(name)
154 if not (0 <= local < blk.length):
155 raise IndexError(
156 f"local index {local} out of range for block {name!r} "
157 f"(length {blk.length})")
158 return self._offset[name] + local
159
160 def describe(self) -> str:
161 lines = [f"StateLayout: N_eq = {self.N_eq}"]
162 for b in self._blocks:
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)
167
168 def __len__(self) -> int:
169 return self.N_eq
170
171 def __repr__(self) -> str:
172 return (f"<StateLayout N_eq={self.N_eq} blocks={len(self._blocks)} "
173 f"{'frozen' if self._frozen else 'open'}>")
int index(self, str name, int local)
Block add_block(self, Block block)
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)
Block add_discrete(self, str name, int n_max, **meta)