primat — top-level facade

main.py

Main class for primat.

Design

  • PRIMAT.__init__(params) accepts an optional dict of parameters, builds a PRIMATConfig, loads all data files (thermodynamics tables and nuclear rate tables), and pre-computes the thermal background.

  • PRIMAT.solve() runs the full nuclear network ODE integration and returns the BBN predictions.

  • PRIMAT.primat_results() calls solve() and returns the result dict (for backwards compatibility).

class primat.main.PRIMAT(params=None, extra_rho=None, custom_network=None, background=None)[source]

Bases: object

Main primat class.

Parameters:
  • params (dict, optional) – Run-time parameters overriding defaults (see config.DEFAULT_PARAMS).

  • extra_rho (list of callable, optional) –

    Extra contributions to the total energy density entering the Friedmann equation. Each element is a function rho(Tg) -> MeV^4 of the photon temperature Tg [MeV], summed into rho_tot by primat.background.StandardBackground.Hubble(). This is the generic plug-in point for “dark sector” components; Early Dark Energy (cfg.fEDE > 0) is implemented as the first such plug-in (see primat.background.StandardBackground._setup_EDE()) and is appended automatically – callers do not need to include it here.

    Example: a constant extra radiation density of dRho [MeV^4]:

    >>> PRIMAT({"network": "small"}, extra_rho=[lambda Tg: dRho])
    

    Ignored (with a warning) if background is supplied: a caller providing a full Background instance is expected to fold any extra energy density into it directly.

  • background (primat.background.Background, optional) –

    A pre-built background instance to drive the nuclear network with, in place of the standard cfg.custom_background-driven dispatch below. This is the seam for a fully custom expansion history (e.g. a non-standard cosmology that needs more than extra_rho can express): subclass primat.background.Background – whose docstring lists the compulsory (T_of_t, t_of_T, rhoB_BBN, weak_nTOp_frwrd/weak_nTOp_bkwrd) and optional methods – and pass an instance here. Since Background.__init__ already takes (cfg, plasma, extra_rho), PRIMAT takes self.cfg/self.plasma from the supplied instance (background.cfg/background.plasma) rather than building its own, so the nuclear network and the background always agree on which config/plasma drove them – build the instance with your own PRIMATConfig/Plasma first, then hand it to PRIMAT. None (default) preserves today’s cfg.custom_background-based dispatch (StandardBackground or CustomBackground) using params/extra_rho as usual. Mutually exclusive with params/extra_rho/cfg.custom_background, which only make sense for the default dispatch; supplying background together with params or extra_rho emits a warning, and the supplied background instance wins.

    Example: drive the network with a hand-built background:

    >>> from primat import Background
    >>> from primat.config import PRIMATConfig
    >>> from primat.plasma import Plasma
    >>> cfg = PRIMATConfig({"network": "small"})
    >>> plasma = Plasma(cfg)
    >>> class MyBackground(Background):
    ...     ...  # implement T_of_t, t_of_T, rhoB_BBN, weak_nTOp_*
    >>> PRIMAT(background=MyBackground(cfg, plasma))
    

  • custom_network (dict, optional) –

    GUI/scripting “Customise Reactions” override, forwarded verbatim to primat.network_data.UpdateNuclearRates (see its docstring for the {"removed": [...], "replaced": {...}, "added": {...}} schema). None (default) uses the standard cfg.network reaction list unchanged. Not a PRIMATConfig field: it carries bulk table data rather than a fingerprintable scalar, so it does not participate in any rate cache fingerprint.

    Example: drop one reaction, override another’s rate table, and add a brand-new reaction (its stoichiometry is read from the name):

    >>> PRIMAT({"network": "small"}, custom_network={
    ...     "removed": ["d_d__t_p"],
    ...     "replaced": {"n_p__d_g": "0.001 1.2e3\n10.0 4.5e1\n"},
    ...     "added": {"t_t__He4_n_n": "0.001 1.0e2\n10.0 1.0e2\n"},
    ... })
    

solve(progress=None)[source]

Integrate the nuclear network over the three temperature eras and return a dict of BBN observables.

Delegates the ODE integration to primat.nuclear_network.NuclearNetwork.solve() (Class 2), which is driven by self.background (Class 1, see primat.background) and populates self.nuclear.Y_final, self.nuclear.abundance_names and self.nuclear.Y_of_t. The “final observables” – light-element ratios from Y_final, plus Neff/Omeganurel/OneOverOmeganunr from the background’s optional neutrino-sector hooks (Background.rho_nu_total_final(), Background.N_eff(), Background.Omeganuh2_relnu()/ Background.Omeganuh2_nrnu()) – are assembled here, into self.results, which is what get_quantity/__getitem__/ Neff()/YPBBN()/… and primat_results() read.

The neutrino-sector keys (Neff, Omeganurel, OneOverOmeganunr) are only added to the dict if the background actually provides that information (None returned from the corresponding hook) – a minimal background with no neutrino-sector model simply omits them.

Parameters:

progress (bool | None) – bool, optional. None (default) defers to cfg.show_progress (a DEFAULT_PARAMS key, default True). Pass an explicit True/False to override the config for this call regardless of show_progress (e.g. False inside MC workers to avoid per-sample spam).

Return type:

dict[str, Any]

property T_of_t: Callable[[Any], Any]

T_γ(t) interpolator [MeV], available after initialisation.

property t_of_T: Callable[[Any], Any]

t(T_γ) interpolator [s], available after initialisation.

property a_of_T: Callable[[Any], Any]

Scale factor a(T_γ), available after initialisation.

a follows the same normalisation as the internal a(T) ODE (primat.background.StandardBackground._setup_background_and_cosmo()): entropy conservation a^3 * spl(T) = const fixed so that a * T -> T0CMB [MeV] as T -> 0, i.e. a = 1 today up to the small entropy-injection correction from e+e- annihilation encoded in spl(T).

Example

>>> p.a_of_T(1.0)   # scale factor at T_gamma = 1 MeV
property T_of_a: Callable[[Any], Any]

T_γ(a) interpolator [MeV], available after initialisation.

Inverse of a_of_T; same normalisation convention for a.

property a_of_t: Callable[[Any], Any]

Scale factor a(t), available after initialisation.

Same normalisation as a_of_T; t is the cosmic time [s] used by T_of_t/t_of_T.

property t_of_a: Callable[[Any], Any]

Cosmic time t(a) [s], available after initialisation.

Inverse of a_of_t; same normalisation convention for a.

primat_results()[source]

Return the BBN result dict, running solve() first if needed.

Return type:

dict[str, Any]

property abundance_names: list[str]

Tracked nuclide names, in abundance-vector order (solves if needed).

For the large network this is the full ~59-nuclide list; accessing it also guarantees self.A/N/Z cover every species (handy for plotting A_i Y_i for all nuclides).

property evolution: EvolutionResult | None

The unified time-evolution result (primat.evolution.EvolutionResult, None unless cfg.output_time_evolution=True), solving first if needed. Thin alias for self.nuclear.evolution so callers that don’t care whether they hold a live PRIMAT or a backend-agnostic primat.gui.run_view.GuiRun (which has no .nuclear at all) can read run.evolution uniformly either way.

Neff()[source]
Return type:

float

Omeganurel()[source]
Return type:

float

Omeganunonrel()[source]
Return type:

float

YPCMB()[source]
Return type:

float

YPBBN()[source]
Return type:

float

DoH()[source]
Return type:

float

He3oH()[source]
Return type:

float

Li7oH()[source]
Return type:

float

get_quantity(quantity)[source]

Return a scalar BBN quantity by name.

Accepts any key from the result dict (‘YPBBN’, ‘DoH’, ‘He3oH’, ‘Li7oH’, ‘Neff’, ‘YPCMB’, …) or a nuclide name from cfg.Nuclides (‘H2’, ‘He4’, ‘Li7’, …) for the final mass fraction Y.

Parameters:

quantity (str)

Return type:

float

primat.main.mc_uncertainty(num_mc, quantity, params=None, n_jobs=-1, seed=0, prev=None, custom_network=None, progress=None)[source]

Estimate nuclear-rate and neutron-lifetime uncertainties on BBN observables via Monte Carlo.

Each MC sample draws all active nuclear rate offsets p_* independently from N(0,1), plus the neutron lifetime tau_n ~ N(cfg.tau_n, cfg.std_tau_n) (used when cfg.tau_n_flag=True, the default), and runs a full primat solve. By default all reactions in the selected network are varied.

Parameters:
  • num_mc (int) – Number of MC samples.

  • quantity (str or list of str) – A key from the result dict (‘YPBBN’, ‘DoH’, ‘He3oH’, ‘Li7oH’, ‘Neff’, ‘YPCMB’, …) or a nuclide name (‘H2’, ‘He4’, ‘Li7’, …) for the final mass fraction Y. Pass a list to evaluate multiple quantities in one MC pass (more efficient than separate calls). This only controls which quantities are guaranteed present and validated strictly (an unknown name raises); the returned MCResult always additionally contains every tracked nuclide’s final Y and every standard observable in _DEFAULT_MC_OBSERVABLES that this network/custom_network actually produces (Neff, YPBBN, YPCMB, DoH, He3oH, He3oHe4, Li7oH, Li6oLi7, YCNO), at no extra solving cost (each MC sample already runs a full solve). This keeps a TSV dump (primat.backend.dump_mc_samples) complete even when the caller only asked for one or two quantities for display purposes.

  • params (dict, optional) – Base parameters for PRIMAT (e.g. Omegabh2, is_small, network).

  • n_jobs (int) – Number of parallel workers passed to joblib.Parallel (-1 = all CPUs). n_jobs=1 runs the samples serially in-process and does not import joblib at all, so a lean core install (pip install primat, no mc/recommended extra) can still do Monte-Carlo this way; any other value needs joblib and raises an actionable ImportError if it is missing.

  • seed (int) – Base random seed; sample i uses seed + i for reproducibility. When evaluating on a parameter grid (e.g. scanning Ω_b h²), use the same seed at every grid point so that sample i draws the same rate vector p_* everywhere. This correlates the MC noise across the grid, making any finite-sample bias cancel when comparing predictions at different parameter values.

  • prev (MCResult, optional) – A previously computed result to extend rather than recompute from scratch. Because sample i is fully determined by seed + i, the first min(len(prev), num_mc) samples are identical to prev as long as the seed, the set/order of quantities, params and custom_network all match, so only the missing samples (seed + n_prev .. seed + num_mc - 1) are actually solved. This makes it cheap to refine an estimate – e.g. going from 30 to 50 samples only runs the 20 new ones. prev is silently ignored (full recompute) if its seed, quantities, params, custom_network or backend (this function only reuses a prev whose backend is "python" or unset – a C-backend result has incompatible RNG samples, see primat.backend.run_mc) differ from this call; if num_mc is smaller than len(prev), the result is just prev truncated to num_mc samples (nothing is solved).

  • custom_network (dict, optional) – “Customise Reactions” override, forwarded to every PRIMAT instance built here (see PRIMAT’s docstring for the {"removed": [...], "replaced": {...}} schema). Reactions listed under "removed" are also excluded from the set of varied rate offsets (rate_keys) below, since they no longer exist in the network; reactions listed under "replaced" stay in rate_keys and are varied using the replacement table’s own error column (UpdateNuclearRates builds expsigma from it), so a custom rate’s uncertainty is honoured automatically. None (default) uses the standard, uncustomised network.

  • progress (bool, optional) – When truthy, print a running N/total (XX%) counter to stderr as samples complete, so the user can track advancement during long MC runs. One update per parallel worker chunk – for n_jobs=-1 (all CPUs) this gives roughly cpu_count updates over the full run. None (default) defers to params['show_progress'] (DEFAULT_PARAMS default True); pass an explicit True/False to override it for this call.

Returns:

MCResult – Dict-like object indexed by quantity name. Each value is an MCQuantityResult with attributes central, mean, std, and values.

Return type:

MCResult

Example

>>> mc = mc_uncertainty(100, ['YPBBN', 'DoH'], params={'Omegabh2': 0.022})
>>> mc['YPBBN'].central
>>> mc['YPBBN'].std
>>> mc['DoH'].values   # full sample array