primat.evolution — time-evolution schema

evolution.py

Unified time-evolution schema: the shared columns both the Python and C backends populate, so a notebook can plot nuclide evolution from either backend’s output without caring which one ran.

This module docstring is the authoritative contract for that schema (column names, order, and semantics): both backends’ writers must conform to it, and any new column must be added to both before it is considered part of the schema. It supersedes the now-removed PRIMAT.md design note (whose §7 is still recoverable for historical context via git show a140bab^:PRIMAT.md).

EvolutionResult is the in-memory primary artifact – populated by NuclearNetwork.solve() as self.evolution (and surfaced as PRIMAT.solve()’s returned dict’s "evolution" key) whenever cfg.output_time_evolution=True, with no disk I/O required to get it. Disk output (the cfg.output_file TSV) is a derived convenience: it is produced by calling dump_evolution() on that same object, not something the solver opens a file for directly.

Columns (tab-separated, #-free header line – see _CORE_COLUMNS): t_s  a  T_gamma_MeV  T_nue_MeV  T_numu_MeV  T_nutau_MeV  Y_<nuclide> ... The Y_<nuclide> block is network-dependent (small/large have different nuclide lists), so the header line is the source of truth – load_evolution() reads it dynamically rather than assuming a fixed column count.

Per-reaction forward-rate columns (<reaction>_frwrd) are an optional trailing block, appended after the Y_<nuclide> block when cfg.output_rates_time_evolution=True – one column per reaction in the active LT network (~12 for small/small_parthenope, 68 for large``+``amax=8, ~429 for full large). They carry the forward reaction-rate interpolant [same table units as the shipped nuclear rates] evaluated at each row’s photon temperature, and live in EvolutionResult.rates (None when the flag is off). Both backends emit the identical column names in the identical (lexicographically sorted) order – see the CLAUDE.md schema-parity mandate. The a/T_nu columns are np.nan when the active background has no scale-factor/neutrino-sector tracking (e.g. a minimal custom background).

class primat.evolution.EvolutionResult(t, a, T_gamma, T_nu, Y=<factory>, rates=None)[source]

Bases: object

In-memory unified time-evolution result.

Variables:
  • t (np.ndarray) – Cosmic time [s].

  • a (np.ndarray) – Scale factor (np.nan everywhere if the background has no scale-factor relation, e.g. a minimal custom background).

  • T_gamma (np.ndarray) – Photon temperature [MeV].

  • T_nu (dict of str -> np.ndarray) – Per-flavour neutrino temperature [MeV], keyed "e"/"mu"/ "tau" (np.nan arrays if the background tracks no neutrino sector).

  • Y (dict of str -> np.ndarray) – Per-nuclide mass-fraction abundance, keyed by nuclide name, in network order (n/p first).

  • rates (dict of str -> np.ndarray, optional) – Optional per-reaction forward-rate columns (populated when cfg.output_rates_time_evolution=True – one per reaction in the active LT network; None otherwise). Keyed by column name <reaction>_frwrd (canonical rate syntax, e.g. n_p__d_g_frwrd), each value an array aligned with t, holding the forward reaction-rate interpolant at that step’s photon temperature. Serialised as the trailing column block after Y_ (see the module docstring); both backends emit the identical names in the identical sorted order.

Parameters:
  • t (NDArray[float64])

  • a (NDArray[float64])

  • T_gamma (NDArray[float64])

  • T_nu (dict[str, NDArray[float64]])

  • Y (dict[str, NDArray[float64]])

  • rates (dict[str, NDArray[float64]] | None)

t: NDArray[float64]
a: NDArray[float64]
T_gamma: NDArray[float64]
T_nu: dict[str, NDArray[float64]]
Y: dict[str, NDArray[float64]]
rates: dict[str, NDArray[float64]] | None = None
primat.evolution.dump_evolution(result, path=None)[source]

Serialise result to the shared TSV schema (module docstring).

Always returns the TSV text; additionally writes it to path if given (relative paths resolve against the current working directory, like cfg.output_file elsewhere in this package). Called by: NuclearNetwork._write_time_evolution (the cfg.output_file convenience path); and primat-gui’s download buttons, which call this lazily on run.evolution to produce the file text for st.download_button(data=...) – never via a tempfile.

Parameters:
Returns:

str – The TSV text (header line + one row per time step).

Return type:

str

primat.evolution.Y_interpolator(result, name)[source]

Build a Y(t) callable for nuclide name from result.t/ result.Y[name] alone – the backend-agnostic counterpart of primat.main.PRIMAT.__getitem__’s live SciPy interpolator, usable on a plain EvolutionResult from either backend’s primat.backend.run_bbn() (no live PRIMAT/NuclearNetwork object required).

Same convention as the live interpolator built by NuclearNetwork.solve() (nuclear_network.py’s self.Y_of_t): piecewise-linear, with fill_value=(0, Y[-1]) so a query before result.t[0] reads as zero abundance (not yet produced) and a query after result.t[-1] holds at the final value (no decay beyond the integrated era – see run_bbn’s decay_era gap, module docstring of primat.backend, for the one case this does not cover).

Parameters:
Returns:

callablet -> Y (accepts a scalar or array t [s]).

Return type:

Callable[[float | NDArray[float64]], Any]

primat.evolution.T_gamma_interpolator(result)[source]

Build a T_gamma(t) [MeV] callable from result.t/result.T_gamma alone – the backend-agnostic counterpart of primat.main.PRIMAT.T_of_t, usable on a plain EvolutionResult from either backend.

Piecewise-linear, clamped to the first/last sampled temperature outside [result.t[0], result.t[-1]] (T_gamma decreases monotonically with t, so this is the high-T/low-T extrapolation respectively).

Parameters:

result (EvolutionResult) – EvolutionResult.

Returns:

callablet -> T_gamma [MeV] (accepts a scalar or array t [s]).

Return type:

Callable[[float | NDArray[float64]], Any]

primat.evolution.t_of_T_interpolator(result)[source]

Build a T_gamma -> t [s] inverse-lookup callable, backend-agnostic.

The counterpart of T_gamma_interpolator(), useful e.g. to add a secondary x-axis labelled in T_gamma on a plot whose primary axis is cosmic time t (see notebooks/AbundanceEvolution.ipynb). Backend- agnostic replacement for primat.main.PRIMAT.t_of_T, the live-instance, Python-only method this used to require.

T_gamma decreases monotonically with t over a BBN run, so the (T_gamma, t) pairs are reversed into ascending order and interpolated with numpy.interp() (clamped at the ends, matching the monotone extrapolation behaviour of T_gamma_interpolator()).

Parameters:

result (EvolutionResult) – EvolutionResult.

Returns:

callableT_gamma [MeV] -> t [s] (accepts a scalar or array).

Return type:

Callable[[float | NDArray[float64]], Any]

primat.evolution.load_evolution(path)[source]

Parse the shared TSV schema written by either backend’s dump_evolution/equivalent writer, returning the same EvolutionResult structure as solve()’s in-memory run.evolution – for the case of reloading a previously-saved run without re-solving.

Trailing per-reaction rate columns (*_frwrd/*_bkwrd, written when output_rates_time_evolution=True) are collected into EvolutionResult.rates (None if the file has none). Any other column beyond the core + Y_<nuclide> + rate blocks (e.g. a backend-specific bonus column) is simply ignored, so a file containing extra columns is still loadable.

Parameters:

path (str)

Returns:

EvolutionResult

Return type:

EvolutionResult