primat.backend — backend dispatch

primat.backend

Dispatch layer choosing between the compiled C extension (primat._primat_c, wrapping primat-c’s cprimat_run) and the pure-Python solver (primat.main.PRIMAT).

HAS_C_BACKEND is probed once at import time (True iff the extension built successfully – see setup.py’s optional_build_ext, which lets pip install succeed even without a C compiler). run_bbn() is the single dispatch entry point; everything else in this module supports it.

Feature gap (the one remaining C-unsupported PRIMAT.__init__ extension):

  • background= (a custom primat.background.Background object) – an inherently-Python extension point (arbitrary user Python subclassing the background), with no way to cross the C ABI. A non-None background= always forces the Python backend under force_backend in (None, "auto"), and raises ValueError under force_backend="c".

The former extra_rho and decay_era gaps are now closed:

  • extra_rho (extra Friedmann energy-density callables) is supported on the C backend via a tabulated handoff – _tabulate_extra_rho() evaluates the summed rho(Tg) on a dense log-Tg grid and passes the (Tg[], rho[]) arrays to the C extension, which splines them and adds rho(Tg) inside cpr_bg_Hubble (see primat-c/src/background.c and config.h’s extra_rho_* fields). Both backends agree to the cross-backend tolerance.

  • decay_era (the long-lived-isotope Decay-Time era past T_end) is ported: cpr_nuclear_network_decay_era (primat-c/src/nuclear_network.c) mirrors _integrate_decay_era’s matrix-exponential decay propagation (scaling-and-squaring Padé-13). It changes no result-dict observable on either backend (Y_final is the end-of-LT state); its only output is the optional output_decay_evolution TSV, which both backends write in the identical schema.

Set PRIMAT_BACKEND_LOG=1 in the environment (or call with log_backend=True) to print, on every run_bbn()/run_mc() call, which backend actually ran and why – chiefly to catch a silent force_backend="auto" fallback to Python (e.g. because a C-unsupported feature was requested, or the extension failed to build) during development.

custom_network (the GUI “Customise Reactions” override: removed/replaced/ added reactions plus rate-table overrides) is supported on both backends: primat-c’s cprimat_run/cpr_mc_uncertainty take an optional CPRCustomNetwork* (primat-c/include/network_data.h), and primat/_primat_c_src/_wrapper.c parses the same dict shape (UpdateNuclearRates/kept_to_custom_network, see primat/network_data.py/primat/gui/custom_rates.py) into one. It is no longer part of python_only_feature below.

output_time_evolution=True is supported on both backends: the C extension’s cprimat_run populates CPRResults’s evol_* in-memory arrays (primat-c/include/api.h) and primat/_primat_c_src/_wrapper.c hands them back as an "evolution" dict key (plain Python lists, no numpy C-API dependency in the extension); this module assembles the same primat.evolution.EvolutionResult shape the Python backend produces, with no disk I/O on either backend’s part.

data_dir/user_nuclear_dir (see CLAUDE.md’s “Rates directory resolution” section) are supported on both backends: data_dir fully replaces the shipped data tree; user_nuclear_dir is an additive overlay for nuclear networks and rate tables. They are ordinary params dict keys applied generically via cpr_config_set_by_name on the C side, so no special-casing is needed here — except that data_dir must also be forwarded as the data_dir positional argument to _c_ext.run_bbn/ _c_ext.run_mc (the C extension’s cpr_config_init_defaults takes the data folder there rather than via cpr_config_set_by_name).

run_mc() is the MC counterpart of run_bbn(): it dispatches between primat._primat_c’s run_mc (wrapping primat-c/src/mc.c’s threaded cpr_mc_uncertainty) and primat.main.mc_uncertainty (joblib), returning the same primat.main.MCResult shape either way – the “common language” the two backends share for MC results (CLAUDE.md’s backend-parity mandate). The C path uses a pthread/xoshiro256** RNG, not NumPy’s default_rng, so individual samples are not bit-for-bit comparable across backends (only statistically, mean/std convergence – see mc.h).

prev (incremental sample reuse) is supported on the C path, mirroring cpr_mc_uncertainty’s prev_centrals/prev_values parameters (see mc.h): run_mc() checks the same reuse-guard mc_uncertainty does internally (seed/quantities/params/custom_network all matching), plus one more condition the C side cannot check for itself – prev.backend must equal the backend about to compute the extension, since the two backends’ RNG streams are not interchangeable. A prev that fails the guard (e.g. computed by the other backend) is silently ignored, exactly like mc_uncertainty’s own fallback – never an error, and never a forced backend switch. custom_network is supported on both backends, same as run_bbn().

primat.backend.run_bbn(params=None, force_backend=None, extra_rho=None, custom_network=None, background=None, log_backend=False, progress=None)[source]

Run one BBN computation, dispatching to the C or Python backend.

This mirrors PRIMAT(params=params, ...).solve()’s result dict (same keys: YPBBN, DoH, Neff, … – see primat.main.PRIMAT.solve and tests/test_backend_parity.py), so callers can switch backends transparently.

Parameters:
  • params (dict[str, Any] | None) – dict, optional. Same PRIMATConfig overrides accepted by PRIMAT(params=...).

  • force_backend (str | None) – {None, "auto", "c", "python"}. None/"auto" (default) picks the C extension when it is available and the request has no C-unsupported feature (see module docstring), otherwise the Python backend. "c"/"python" force that backend, raising RuntimeError/ValueError respectively if the C backend is unavailable or the request uses a C-unsupported feature.

  • extra_rho (list | None) – forwarded to PRIMAT.__init__ verbatim. extra_rho/background are Python-only (see module docstring), so any non-None value forces the Python backend regardless of force_backend (except force_backend="c", which raises instead). custom_network is supported on both backends and never forces a fallback.

  • custom_network (dict[str, Any] | None) – forwarded to PRIMAT.__init__ verbatim. extra_rho/background are Python-only (see module docstring), so any non-None value forces the Python backend regardless of force_backend (except force_backend="c", which raises instead). custom_network is supported on both backends and never forces a fallback.

  • background – forwarded to PRIMAT.__init__ verbatim. extra_rho/background are Python-only (see module docstring), so any non-None value forces the Python backend regardless of force_backend (except force_backend="c", which raises instead). custom_network is supported on both backends and never forces a fallback.

  • log_backend (bool) – bool, default False. Print which backend actually ran and why (module docstring); also triggered by setting the PRIMAT_BACKEND_LOG environment variable.

  • progress (bool | None) – bool, optional. None (default) defers to params['show_progress'] (DEFAULT_PARAMS default True); pass an explicit True/False to override it for this call. Controls the compact [primat]  HT.  MT.  LT.  done. stderr phase markers on both backends (suppressed when verbose=True).

Returns:

dict – the BBN result dict (YPBBN, DoH, Neff, …, plus a Y_final sub-dict of every tracked nuclide’s final mass fraction).

Return type:

dict[str, Any]

Example

>>> run_bbn({"network": "small"})["YPBBN"]
0.24700...
>>> run_bbn({"network": "small"}, force_backend="python")["YPBBN"]
0.24699...
primat.backend.run_mc(num_mc, quantities=None, params=None, force_backend=None, seed=0, n_jobs=-1, prev=None, custom_network=None, log_backend=False, progress=None)[source]

Run an MC nuclear-rate/tau_n uncertainty propagation, dispatching to the C or Python backend (the MC counterpart of run_bbn()).

This mirrors primat.main.mc_uncertainty()’s return value (an primat.main.MCResult, indexed by quantity name – same .central/.mean/.std/.values per quantity), so callers can switch backends transparently; see this module’s docstring for the RNG caveat (C samples are statistically, not bit-for-bit, comparable to Python’s).

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

  • quantities (str | list[str] | None) – str or list of str, optional. A result-dict key ('YPBBN', 'DoH', …) or nuclide name, or a list of either. None (default) uses every tracked nuclide’s final Y plus the full _DEFAULT_MC_OBSERVABLES set (see _default_mc_quantities()). Regardless of what is passed here, the returned MCResult always additionally contains every tracked nuclide and every _DEFAULT_MC_OBSERVABLES entry this network/custom_network actually produces – at no extra solving cost, since each MC sample already runs a full solve – so a TSV dump (dump_mc_samples()) is always complete even when quantities only asked for one or two values for display.

  • params (dict[str, Any] | None) – forwarded verbatim; see primat.main.mc_uncertainty’s docstring.

  • seed (int | None) – forwarded verbatim; see primat.main.mc_uncertainty’s docstring.

  • n_jobs (int) – forwarded verbatim; see primat.main.mc_uncertainty’s docstring.

  • force_backend (str | None) – {None, "auto", "c", "python"}, same semantics as run_bbn().

  • prev (MCResult | None) – supported on both backends (see module docstring); a previously computed primat.main.MCResult to extend rather than recompute from scratch. Reused only when it is sample-compatible (same seed/quantities/params/custom_network) and came from the same backend that will compute this call (prev.backend); otherwise silently ignored, mirroring mc_uncertainty’s own fallback. Never forces a backend switch or raises.

  • custom_network (dict[str, Any] | None) – supported on both backends (forwarded to cpr_mc_uncertainty’s CPRCustomNetwork*); never forces a fallback.

  • log_backend (bool) – bool, default False. Print which backend actually ran and why (module docstring); also triggered by setting the PRIMAT_BACKEND_LOG environment variable.

  • progress (bool | None) – bool, optional. None (default) defers to params['show_progress'] (DEFAULT_PARAMS default True); pass an explicit True/False to override it for this call. Controls the [MC] Running N samples... banner and the N/total (XX%) counter on both backends.

Returns:

primat.main.MCResult

Return type:

MCResult

Example

>>> run_mc(50, ['YPBBN', 'DoH'], params={'network': 'small'})['YPBBN'].std
>>> run_mc(50, force_backend='python')['DoH'].mean
primat.backend.dump_mc_samples(mc)[source]

Serialise an primat.main.MCResult to TSV text: one column per quantity (header = quantity names, in their original order), one row per MC sample – the on-disk “common language” for MC results shared by both backends (CLAUDE.md’s backend-parity mandate), and the same shape written to <output_mc_file_prefix>_samples.tsv when output_mc_samples=True.

Parameters:

mc (MCResult) – primat.main.MCResult.

Returns:

str – TSV text, with a trailing newline.

Return type:

str

primat.backend.dump_mc_covariance(mc)[source]

Serialise an primat.main.MCResult’s full sample covariance matrix (mc.cov(); ddof=1, all MC quantities in quantity_names order) to the two-header-line TSV written to <output_mc_file_prefix>_covariance.tsv when output_mc_covariance=True.

The joint (off-diagonal) covariance – e.g. between YPBBN and DoH, which are driven by the same MC samples – is exactly what a user needs to build a multi-observable likelihood; the per-observable variances live on the diagonal (C[i, i] == mc[q_i].std**2).

File layout (author spec):

# Covariance matrix of the N=100 primat MC samples (seed=0): ...
quantity        Neff    YPBBN   ...
Neff    <C[0,0]>        <C[0,1]>        ...
...

i.e. line 1 is a single # comment naming the file (with N, seed and the ddof=1 estimator convention); line 2 is the tab-separated quantity names labelling both columns and rows; then one row per quantity, its name first.

Parameters:

mc (MCResult) – primat.main.MCResult.

Returns:

str – TSV text, with a trailing newline.

Return type:

str

primat.backend.dump_mc_correlation(mc)[source]

Serialise an primat.main.MCResult’s full sample correlation matrix (mc.corr(); unit diagonal, ddof=1) to the two-header-line TSV written to <output_mc_file_prefix>_correlation.tsv when output_mc_correlation=True.

Same layout as dump_mc_covariance() (line 1 = a # comment; line 2 = the quantity names; then one labelled row per quantity), with its own header wording and a unit diagonal. A quantity that was identical in every sample (zero variance) has NaN off-diagonal entries – see primat.main.MCResult.corr().

Parameters:

mc (MCResult) – primat.main.MCResult.

Returns:

str – TSV text, with a trailing newline.

Return type:

str

primat.backend.dump_final_with_sigma(names, Y, sigma=None, num_mc=None)[source]

Render the output_final.dat-format final-abundances text.

Two columns (nuclide  Y) when sigma is None – identical to the plain single-run format written by NuclearNetwork._write_final_result. Three columns (nuclide  Y sigma_N<num_mc>) when an MC sigma dict is supplied, so the sample count backing the uncertainty estimate is recorded directly in the header rather than only in the (separate) MC-samples file.

The header row uses the same column widths as the data rows so the column names sit directly above their respective values (no # comment prefix that would shift the label two characters to the right).

Parameters:
  • names (list[str]) – list of str. Nuclide names, in the order to write them.

  • Y (dict[str, float]) – dict, name -> final mass-fraction abundance.

  • sigma (dict[str, float] | None) – dict, name -> 1-sigma MC uncertainty on Y[name], optional.

  • num_mc (int | None) – int, required when sigma is given – the MC sample count, recorded in the header (e.g. sigma_N50).

Returns:

str – the file text, with a trailing newline.

Return type:

str