primat.sensitivity — logarithmic sensitivity tables¶
primat.sensitivity — logarithmic sensitivity of BBN observables to parameters.
This module promotes the ad-hoc finite-difference loop that used to live in
notebooks/Sensitivity.ipynb into a reusable public API. Referees of BBN
papers routinely ask for a sensitivity table
the dimensionless “if parameter \(p\) rises by 1 %, observable \(O\)
rises by \(S\) %”. sensitivity_table() computes the whole matrix by
symmetric finite-differencing full BBN solves and returns a
SensitivityTable dataclass with to_markdown() / to_dataframe()
views ready to paste into a paper or notebook.
Why symmetric finite differences? For a fractional step \(\delta\) the central estimate
is accurate to \(O(\delta^2)\) (the linear error term cancels), so the
default 1 % step (rel_step=0.01) already gives ~4 correct digits without
paying for tiny-step round-off. The denominator 2 ln(1+δ) — rather than the
naive 2δ — makes the result an exact logarithmic derivative: the two runs
sit at \(\ln p \pm \ln(1+\delta)\) in log-parameter space.
Three flavours of parameter need three variation recipes, all expressed through
the SensTarget helper (a plain string is auto-classified):
Nuclear reaction rates (e.g.
"n_p__d_g"): varied through primat’srescale_nuclear_rates+delta_<rxn>mechanism, which multiplies the median rate by(1 + delta)— see the Rate variation how-to. The resulting number is a genuine \(\partial\ln O/\partial\ln(\text{rate})\).Multiplicative physical parameters with a non-zero fiducial value (
tau_n,GN,Omegabh2, …): scaled by(1 ± δ)about their effective value, giving \(\partial\ln O/\partial\ln p\) directly.Additive parameters whose fiducial value is zero, so a multiplicative step is degenerate (
DeltaNeffat its SM value 0): varied by an absolute±stepabout the base value. The denominator is user-controlled (default2 ln(1+rel_step)) because the “natural” normalisation of an additive knob is a modelling choice, not a mathematical given.
Example
>>> from primat.sensitivity import sensitivity_table, SensTarget
>>> tab = sensitivity_table(
... params={"network": "small"},
... observables=["YPBBN", "DoH"],
... targets=[
... "n_p__d_g", # a nuclear rate (auto -> rate)
... "tau_n", # neutron lifetime (auto -> mult.)
... "Omegabh2", # baryon density (auto -> mult.)
... SensTarget("DeltaNeff", kind="additive", step=1.0),
... ],
... )
>>> print(tab.to_markdown())
| Parameter | $Y_P$ | D/H |
| --- | --- | --- |
| n_p__d_g | +0.024 | +0.207 |
| ...
- class primat.sensitivity.SensTarget(param, label=None, kind='auto', step=None, denom=None)[source]¶
Bases:
objectOne row of a sensitivity table: what parameter to vary and how.
A
SensTargetknows how to turn a fractional steprel_stepinto the two bracketing parameter overrides (plus/minus) fed toprimat.backend.run_bbn(), plus the finite-difference denominator. Most callers never build one explicitly: passing a bare string tosensitivity_table()constructsSensTarget(param)withkind="auto", which classifies the string as a reaction rate (if it names a reaction in the effective config’sp_rxntable) or a multiplicative physical parameter otherwise.- Variables:
param (str) – config-parameter name (e.g.
"tau_n","Omegabh2","DeltaNeff") or a nuclear reaction name (e.g."n_p__d_g").label (str | None) – display name for this row in tables. Defaults to
param.kind (str) –
"auto"(default; classify at resolve time),"rate"(force thedelta_<rxn>rescaling mechanism),"param"(force multiplicativep(1±δ)) or"additive"(absolute±step).step (float | None) – for
kind="additive"only, the absolute half-step (e.g.1.0forDeltaNeff). Ignored otherwise; defaults torel_stepwhenNone.denom (float | None) – override the finite-difference denominator.
None(default) uses2 ln(1+rel_step), which makesrate/paramrows exact logarithmic derivatives. Handy for additive rows where you want a different normalisation.
- Parameters:
Example
>>> SensTarget("Omegabh2", label=r"$\Omega_b h^2$") SensTarget(param='Omegabh2', ...) >>> SensTarget("DeltaNeff", kind="additive", step=1.0) # per unit ΔNeff SensTarget(param='DeltaNeff', ...)
- display_label()[source]¶
Row label to print — the explicit
labelor the parameter name.- Return type:
- resolve(cfg, rel_step)[source]¶
Return
(plus_params, minus_params, denom)for this target.cfgis the effectivePRIMATConfigbuilt from the caller’sparams(so fiducial values honour user overrides, e.g. a non-defaulttau_n).rel_stepis the fractional step \(\delta\). The two returned dicts are merged onto the base params for the+and-runs;denomdividesln O+ - ln O-.
- class primat.sensitivity.SensitivityTable(row_labels, observables, obs_labels, values, fiducial, rel_step=0.01)[source]¶
Bases:
objectResult of
sensitivity_table(): a (targets × observables) matrix.Holds the logarithmic-sensitivity matrix plus enough context to render it.
values[i, j]is \(\partial \ln O_j / \partial \ln p_i\) (or the additive analogue for additive targets).- Variables:
row_labels (list[str]) – display label of each varied parameter (table rows).
observables (list[str]) – result-dict keys of each observable (table columns).
obs_labels (list[str]) – pretty column headers, aligned with
observables.values (numpy.ndarray) –
(n_targets, n_observables)float array of sensitivities.fiducial (dict[str, float]) – observable-key → fiducial (unperturbed) value, from the single shared central solve.
rel_step (float) – the fractional step used for the finite differences.
- Parameters:
Example
>>> tab.to_dataframe() >>> print(tab.to_markdown()) >>> tab.values[0, 1] # dln(D/H)/dln(first param)
- to_dataframe()[source]¶
Return the sensitivity matrix as a pandas
DataFrame.Rows are indexed by parameter label, columns by observable label. Requires pandas (an optional dependency); raised as an
ImportErrorwith an actionable message if it is missing.Example
>>> df = tab.to_dataframe() >>> df.loc[r"$\tau_n$", "D/H"]
- to_markdown(fmt='+.4f')[source]¶
Render the table as a GitHub-flavoured Markdown string.
Hand-rolled (no pandas/tabulate dependency) so it always works.
fmtis the per-cell format spec (default"+.4f"— signed, 4 decimals, which resolves the ~1e-2–1e-3 sensitivities BBN cares about).Example
>>> print(tab.to_markdown()) | Parameter | $Y_P$ | D/H | | --- | --- | --- | | $\tau_n$ | +0.7290 | +0.4130 |
- primat.sensitivity.sensitivity_table(params, observables, targets, rel_step=0.01, *, obs_labels=None, force_backend=None, progress=False)[source]¶
Compute the logarithmic-sensitivity matrix of BBN observables.
For each target parameter this runs two full BBN solves bracketing the fiducial point (
p(1±δ)for multiplicative targets,±deltafor rate targets,base±stepfor additive targets) and forms the symmetric finite-difference logarithmic derivative \(\partial\ln O/\partial\ln p\). A single shared central solve at the fiducial parameters is run once and stored in the result for reference (the symmetric estimate itself uses only the two bracketing runs).Cost is
2 * len(targets) + 1solves; with the default C backend asmall-network solve is ~sub-second, so a full 12-rate + 4-parameter table is a few tens of seconds.- Parameters:
params (dict[str, Any] | None) – base (“fiducial”) parameter overrides, exactly as accepted by
primat.backend.run_bbn()/PRIMAT(params=...).None== all defaults. Fiducial values of multiplicative targets are read from the config built from these params, so overriding e.g.tau_nhere shifts the point about which it is differentiated.observables (list[str]) – result-dict keys to differentiate, e.g.
["YPBBN", "DoH", "He3oH", "Li7oH"].targets (list[Any]) – parameters to vary. Each item is either a plain string (auto-classified into a rate or multiplicative parameter — see
SensTarget) or an explicitSensTargetfor full control (additive knobs, custom labels, custom denominators).rel_step (float) – fractional finite-difference step \(\delta\) (default 0.01 = 1 %). Small enough for ~4-digit accuracy, large enough to stay clear of solver round-off.
obs_labels (list[str] | None) – optional pretty column headers aligned with
observables; defaults toDEFAULT_OBS_LABELS(falling back to the raw key).force_backend (str | None) – forwarded to
primat.backend.run_bbn()(None/"auto"prefers the fast C backend;"c"/"python"force one).progress (bool) – forwarded to
run_bbn— leaveFalseto silence the per-solve phase markers during the (many) sensitivity runs.
- Returns:
SensitivityTable – with
.valuesthe(len(targets), len(observables))matrix,.to_markdown()/.to_dataframe()views, and.fiducialthe shared central observable values.- Return type:
Example
>>> tab = sensitivity_table( ... {"network": "small"}, ... observables=["YPBBN", "DoH"], ... targets=["tau_n", "Omegabh2", "n_p__d_g"], ... ) >>> tab.values.shape (3, 2)