orblet 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- orblet/__init__.py +207 -0
- orblet/chain_stats.py +194 -0
- orblet/constants.py +307 -0
- orblet/design/__init__.py +53 -0
- orblet/design/builder.py +461 -0
- orblet/design/columns.py +453 -0
- orblet/elements.py +934 -0
- orblet/interpret/__init__.py +20 -0
- orblet/interpret/amrf.py +1178 -0
- orblet/interpret/astrometric_upper_limit.py +262 -0
- orblet/interpret/companion_mass.py +758 -0
- orblet/interpret/flux_ratio.py +724 -0
- orblet/kepler.py +222 -0
- orblet/likelihood.py +335 -0
- orblet/model.py +1082 -0
- orblet/parallax.py +258 -0
- orblet/periodogram.py +875 -0
- orblet/plotting.py +1315 -0
- orblet/prepare.py +370 -0
- orblet/priors.py +715 -0
- orblet/residuals.py +225 -0
- orblet/rv_chain.py +346 -0
- orblet/sampling.py +544 -0
- orblet/search.py +686 -0
- orblet/simulate/__init__.py +19 -0
- orblet/simulate/bundles.py +277 -0
- orblet/simulate/cadence.py +331 -0
- orblet/simulate/orbit.py +470 -0
- orblet/solve/__init__.py +24 -0
- orblet/solve/astrometry.py +1020 -0
- orblet/solve/rv.py +799 -0
- orblet-0.1.0.dist-info/METADATA +140 -0
- orblet-0.1.0.dist-info/RECORD +36 -0
- orblet-0.1.0.dist-info/WHEEL +5 -0
- orblet-0.1.0.dist-info/licenses/LICENSE +21 -0
- orblet-0.1.0.dist-info/top_level.txt +1 -0
orblet/__init__.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""orblet — a standalone orbit fitter: arrays in, orbital elements out.
|
|
2
|
+
|
|
3
|
+
No mission, no data files, no network. orblet is a CONSUMER of data and never
|
|
4
|
+
a producer: you hand it epochs, measurements and uncertainties, and it hands
|
|
5
|
+
back a model, a likelihood, a solution. Where those arrays came from, and
|
|
6
|
+
whether you are allowed to have them, is not orblet's business.
|
|
7
|
+
|
|
8
|
+
Work the open way
|
|
9
|
+
-----------------
|
|
10
|
+
orblet ships ATOMS, not a black box. A Kepler solver, forward models, design
|
|
11
|
+
matrices, the two linear cores, proposed likelihoods, default priors, a
|
|
12
|
+
frequency search, the element conversions, the plots. You assemble the
|
|
13
|
+
optimiser or sampler you actually want and plug your own target function into
|
|
14
|
+
it; the default recipes are written out in the manuals and the example
|
|
15
|
+
notebooks rather than hidden inside a wrapper.
|
|
16
|
+
|
|
17
|
+
That is a deliberate trade. A one-line `fit()` is quicker to call and harder
|
|
18
|
+
to trust: when it disagrees with your expectation you cannot see which step
|
|
19
|
+
disagreed. Here every step is a function you can call, print and plot on its
|
|
20
|
+
own.
|
|
21
|
+
|
|
22
|
+
The shape of the thing
|
|
23
|
+
----------------------
|
|
24
|
+
model the forward models: elements and epochs -> observables
|
|
25
|
+
design the design matrices, one column per linear parameter
|
|
26
|
+
solve the linear cores — fix the geometry, solve the rest exactly
|
|
27
|
+
search the frequency scan and the periodograms
|
|
28
|
+
likelihood proposed likelihoods (Gaussian with jitter), not mandates
|
|
29
|
+
priors prior classes, defaults, and the spec parser
|
|
30
|
+
prepare epoch and time-scale handling
|
|
31
|
+
sampling MCMC atoms: chains, stacking, R-hat, ESS, dead chains
|
|
32
|
+
elements conversions between parameter conventions
|
|
33
|
+
residuals the 5-parameter residual routine
|
|
34
|
+
parallax observer position -> on-sky parallax factors
|
|
35
|
+
interpret what a fitted orbit implies: masses, limits, flux ratios
|
|
36
|
+
plotting the orbit, the corner, the sky plane
|
|
37
|
+
simulate synthetic data with known truth, for testing and teaching
|
|
38
|
+
|
|
39
|
+
Why the split matters: the geometry of a Keplerian orbit is non-linear, but
|
|
40
|
+
once it is fixed the amplitudes enter LINEARLY and have a closed-form best
|
|
41
|
+
fit. `solve` is that closed form. A sampler over a handful of non-linear
|
|
42
|
+
parameters, with `solve` as its inner step, beats one over all of them.
|
|
43
|
+
|
|
44
|
+
Import cost
|
|
45
|
+
-----------
|
|
46
|
+
Nothing heavy is imported when you `import orblet`. Every name below is
|
|
47
|
+
resolved on first attribute access (PEP 562), so scipy, emcee and matplotlib
|
|
48
|
+
arrive only if you touch something that needs them.
|
|
49
|
+
|
|
50
|
+
**This package is under construction.** It is being assembled in place inside
|
|
51
|
+
the gaia-orbits monorepo, one dependency layer at a time, before being
|
|
52
|
+
extracted to its own repository. The modules arrive by relocation, unchanged.
|
|
53
|
+
|
|
54
|
+
State of the move: `docs/plans/2026-09-23-orblet-b1-chunks.md`
|
|
55
|
+
Plan of record: `docs/plans/2026-09-23-orblet-extraction-plan.md`
|
|
56
|
+
Machine-checked: `tests/_orblet_move_map.py`
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import importlib
|
|
62
|
+
|
|
63
|
+
# public name -> (module, attribute). One mechanism, one table, grouped by
|
|
64
|
+
# what the name is FOR rather than by which file it happens to live in.
|
|
65
|
+
#: The package version. The ONLY place it is written: the build reads it from
|
|
66
|
+
#: here (``[tool.setuptools.dynamic]`` in orblet's pyproject), and the packaging
|
|
67
|
+
#: test pins that the installed metadata agrees. A plain literal on purpose —
|
|
68
|
+
#: setuptools parses it from the source without importing the package.
|
|
69
|
+
__version__ = "0.1.0"
|
|
70
|
+
|
|
71
|
+
_LAZY_EXPORTS: dict[str, tuple[str, str]] = {
|
|
72
|
+
# ── Design matrices: one column per linear parameter ───────────────
|
|
73
|
+
"rv_design_matrix": ("orblet.design.columns", "rv_design_matrix"),
|
|
74
|
+
"ti_design_matrix": ("orblet.design.columns", "ti_design_matrix"),
|
|
75
|
+
"acceleration_columns": ("orblet.design.columns", "acceleration_columns"),
|
|
76
|
+
"astrometric_5param_design_matrix": (
|
|
77
|
+
"orblet.solve.astrometry",
|
|
78
|
+
"astrometric_5param_design_matrix",
|
|
79
|
+
),
|
|
80
|
+
|
|
81
|
+
# ── Linear cores: geometry fixed, amplitudes solved exactly ────────
|
|
82
|
+
"linear_solve_rv": ("orblet.solve.rv", "linear_solve_rv"),
|
|
83
|
+
"linear_solve_ti": ("orblet.solve.astrometry", "linear_solve_ti"),
|
|
84
|
+
"best_linear_params_rv": ("orblet.solve.rv", "best_linear_params_rv"),
|
|
85
|
+
"best_linear_params_ti": (
|
|
86
|
+
"orblet.solve.astrometry",
|
|
87
|
+
"best_linear_params_ti",
|
|
88
|
+
),
|
|
89
|
+
"fit_astrometric_5param": (
|
|
90
|
+
"orblet.solve.astrometry",
|
|
91
|
+
"fit_astrometric_5param",
|
|
92
|
+
),
|
|
93
|
+
"recover_K": ("orblet.solve.rv", "recover_K"),
|
|
94
|
+
"recover_omega": ("orblet.solve.rv", "recover_omega"),
|
|
95
|
+
"semi_amplitude_kms": ("orblet.model", "semi_amplitude_kms"),
|
|
96
|
+
|
|
97
|
+
# ── Searching for the period ───────────────────────────────────────
|
|
98
|
+
"scan_ti_frequency": ("orblet.search", "scan_ti_frequency"),
|
|
99
|
+
|
|
100
|
+
# ── Seeds: turning one solution into a starting point for the next ─
|
|
101
|
+
"compose_rv_seed": ("orblet.solve.rv", "compose_rv_seed"),
|
|
102
|
+
"compose_ti_seed": ("orblet.search", "compose_ti_seed"),
|
|
103
|
+
"ti_amplitude_chains": ("orblet.solve.astrometry", "ti_amplitude_chains"),
|
|
104
|
+
"mirror_inclination": ("orblet.elements", "mirror_inclination"),
|
|
105
|
+
|
|
106
|
+
# ── Epochs and time scales ─────────────────────────────────────────
|
|
107
|
+
"prepare_rv_for_orbit": ("orblet.prepare", "prepare_rv_for_orbit"),
|
|
108
|
+
"resolve_epochs_mjd": ("orblet.prepare", "resolve_epochs_mjd"),
|
|
109
|
+
|
|
110
|
+
# ── Where the observer is, as on-sky parallax factors ──────────────
|
|
111
|
+
"per_direction_parallax_factors": (
|
|
112
|
+
"orblet.parallax",
|
|
113
|
+
"per_direction_parallax_factors",
|
|
114
|
+
),
|
|
115
|
+
"OBSERVER_GEOCENTRE": ("orblet.parallax", "OBSERVER_GEOCENTRE"),
|
|
116
|
+
"OBSERVER_L2": ("orblet.parallax", "OBSERVER_L2"),
|
|
117
|
+
"L2_OFFSET_AU": ("orblet.parallax", "L2_OFFSET_AU"),
|
|
118
|
+
|
|
119
|
+
# ── Element conversions and derived curves ─────────────────────────
|
|
120
|
+
"extract_orbital_elements": (
|
|
121
|
+
"orblet.elements",
|
|
122
|
+
"extract_orbital_elements",
|
|
123
|
+
),
|
|
124
|
+
"ti_to_kepler": ("orblet.elements", "ti_to_kepler"),
|
|
125
|
+
"to_nss_convention": ("orblet.elements", "to_nss_convention"),
|
|
126
|
+
"compute_rv_model_curve": ("orblet.elements", "compute_rv_model_curve"),
|
|
127
|
+
"compute_residuals": ("orblet.elements", "compute_residuals"),
|
|
128
|
+
|
|
129
|
+
# ── Carrying one channel's chain into the other's prior ────────────
|
|
130
|
+
"a1_sini_from_rv_chain": ("orblet.rv_chain", "a1_sini_from_rv_chain"),
|
|
131
|
+
"astrometric_priors_from_rv_chain": (
|
|
132
|
+
"orblet.rv_chain",
|
|
133
|
+
"astrometric_priors_from_rv_chain",
|
|
134
|
+
),
|
|
135
|
+
"predicted_k_kms_from_astrometric_chain": (
|
|
136
|
+
"orblet.rv_chain",
|
|
137
|
+
"predicted_k_kms_from_astrometric_chain",
|
|
138
|
+
),
|
|
139
|
+
|
|
140
|
+
# ── Plots (matplotlib arrives here, not before) ────────────────────
|
|
141
|
+
"plot_orbit_fit": ("orblet.plotting", "plot_orbit_fit"),
|
|
142
|
+
"plot_orbit_corner": ("orblet.plotting", "plot_orbit_corner"),
|
|
143
|
+
"plot_astrometric_orbit": ("orblet.plotting", "plot_astrometric_orbit"),
|
|
144
|
+
"plot_astrometric_sky_overlay": (
|
|
145
|
+
"orblet.plotting",
|
|
146
|
+
"plot_astrometric_sky_overlay",
|
|
147
|
+
),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def __getattr__(name: str):
|
|
152
|
+
"""Resolve a public name on first access (PEP 562).
|
|
153
|
+
|
|
154
|
+
Keeps ``import orblet`` cheap: nothing below the front door is executed
|
|
155
|
+
until something is actually asked for.
|
|
156
|
+
"""
|
|
157
|
+
target = _LAZY_EXPORTS.get(name)
|
|
158
|
+
if target is None:
|
|
159
|
+
raise AttributeError(
|
|
160
|
+
f"module {__name__!r} has no attribute {name!r}"
|
|
161
|
+
)
|
|
162
|
+
module_name, attr = target
|
|
163
|
+
return getattr(importlib.import_module(module_name), attr)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def __dir__() -> list[str]:
|
|
167
|
+
return sorted(__all__)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Kept as a literal, same names as the table above.
|
|
171
|
+
__all__ = [
|
|
172
|
+
"rv_design_matrix",
|
|
173
|
+
"ti_design_matrix",
|
|
174
|
+
"acceleration_columns",
|
|
175
|
+
"astrometric_5param_design_matrix",
|
|
176
|
+
"linear_solve_rv",
|
|
177
|
+
"linear_solve_ti",
|
|
178
|
+
"best_linear_params_rv",
|
|
179
|
+
"best_linear_params_ti",
|
|
180
|
+
"fit_astrometric_5param",
|
|
181
|
+
"recover_K",
|
|
182
|
+
"recover_omega",
|
|
183
|
+
"semi_amplitude_kms",
|
|
184
|
+
"scan_ti_frequency",
|
|
185
|
+
"compose_rv_seed",
|
|
186
|
+
"compose_ti_seed",
|
|
187
|
+
"ti_amplitude_chains",
|
|
188
|
+
"mirror_inclination",
|
|
189
|
+
"prepare_rv_for_orbit",
|
|
190
|
+
"resolve_epochs_mjd",
|
|
191
|
+
"per_direction_parallax_factors",
|
|
192
|
+
"OBSERVER_GEOCENTRE",
|
|
193
|
+
"OBSERVER_L2",
|
|
194
|
+
"L2_OFFSET_AU",
|
|
195
|
+
"extract_orbital_elements",
|
|
196
|
+
"ti_to_kepler",
|
|
197
|
+
"to_nss_convention",
|
|
198
|
+
"compute_rv_model_curve",
|
|
199
|
+
"compute_residuals",
|
|
200
|
+
"a1_sini_from_rv_chain",
|
|
201
|
+
"astrometric_priors_from_rv_chain",
|
|
202
|
+
"predicted_k_kms_from_astrometric_chain",
|
|
203
|
+
"plot_orbit_fit",
|
|
204
|
+
"plot_orbit_corner",
|
|
205
|
+
"plot_astrometric_orbit",
|
|
206
|
+
"plot_astrometric_sky_overlay",
|
|
207
|
+
]
|
orblet/chain_stats.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chain-summary helpers for posterior samples.
|
|
3
|
+
|
|
4
|
+
Linear-quantity helpers (:func:`chain_quantiles`,
|
|
5
|
+
:func:`chain_credible_interval`, :func:`chain_summary_table`) are
|
|
6
|
+
straightforward wrappers over :func:`numpy.quantile` with strict
|
|
7
|
+
input validation.
|
|
8
|
+
|
|
9
|
+
Angular-quantity helper (:func:`chain_circular_summary`) computes the
|
|
10
|
+
circular mean and circular standard deviation of an angle chain. Two
|
|
11
|
+
implementation notes worth preserving:
|
|
12
|
+
|
|
13
|
+
- Inputs are wrapped to ``[0, 2π)`` before the circular-std call
|
|
14
|
+
because :func:`scipy.stats.circstd` does NOT internally fold values
|
|
15
|
+
outside ``[low, high]``.
|
|
16
|
+
- For known-multimodal posteriors (e.g., the ``(ω + π, Ω + π)`` 180°
|
|
17
|
+
degeneracy in retrograde-inclination astrometric fits), the circular
|
|
18
|
+
mean collapses to a meaningless midpoint between modes; the docstring
|
|
19
|
+
flags this so the helper is not used naively for such cases.
|
|
20
|
+
|
|
21
|
+
*Dependencies.* Three of the four helpers need only numpy and scipy.
|
|
22
|
+
:func:`chain_summary_table` returns a :class:`pandas.DataFrame`, and pandas is
|
|
23
|
+
an OPTIONAL dependency (``pip install orblet[tables]``) imported inside that
|
|
24
|
+
function rather than at module scope — otherwise one unused table helper would
|
|
25
|
+
make the whole module unimportable for anyone without pandas. The numbers are
|
|
26
|
+
available without it: :func:`chain_quantiles` returns the same values as a
|
|
27
|
+
plain dict, and :func:`chain_summary_table` only arranges them.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from typing import TYPE_CHECKING
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
from scipy.stats import circstd
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING: # pragma: no cover - for type checkers only
|
|
38
|
+
import pandas as pd
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _quantile_key(level: float) -> str:
|
|
42
|
+
"""Encode a quantile level as a 3-digit-millis key (e.g., ``q025``)."""
|
|
43
|
+
return f"q{int(round(level * 1000)):03d}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def chain_quantiles(
|
|
47
|
+
samples: np.ndarray,
|
|
48
|
+
levels: tuple[float, ...] = (0.025, 0.5, 0.975),
|
|
49
|
+
) -> dict[str, float]:
|
|
50
|
+
"""
|
|
51
|
+
Compute quantiles of a 1-D chain.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
samples : np.ndarray, shape (n,)
|
|
56
|
+
1-D array of posterior samples. Empty arrays raise
|
|
57
|
+
:class:`ValueError`. NaN samples raise :class:`ValueError`.
|
|
58
|
+
levels : tuple of floats in (0, 1), default ``(0.025, 0.5, 0.975)``
|
|
59
|
+
Quantile levels. Returned dict keys are integer-encoded:
|
|
60
|
+
``f"q{int(round(lvl*1000)):03d}"`` → ``"q025"``, ``"q500"``,
|
|
61
|
+
``"q975"``.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
dict[str, float]
|
|
66
|
+
Mapping of quantile-key to value.
|
|
67
|
+
"""
|
|
68
|
+
samples = np.asarray(samples, dtype=np.float64)
|
|
69
|
+
if samples.size == 0:
|
|
70
|
+
raise ValueError("samples is empty")
|
|
71
|
+
if np.any(np.isnan(samples)):
|
|
72
|
+
raise ValueError("samples contains NaN")
|
|
73
|
+
qs = np.quantile(samples, list(levels))
|
|
74
|
+
return {_quantile_key(lvl): float(qs[i]) for i, lvl in enumerate(levels)}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def chain_credible_interval(
|
|
78
|
+
samples: np.ndarray,
|
|
79
|
+
level: float = 0.99,
|
|
80
|
+
) -> dict[str, float]:
|
|
81
|
+
"""
|
|
82
|
+
Compute the central credible interval at the given level.
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
samples : np.ndarray, shape (n,)
|
|
87
|
+
1-D array of posterior samples. Empty arrays raise
|
|
88
|
+
:class:`ValueError`.
|
|
89
|
+
level : float in (0, 1), default 0.99
|
|
90
|
+
Credible-interval fraction (e.g., 0.68, 0.95, 0.99).
|
|
91
|
+
Computes quantiles at ``(1-level)/2`` and ``(1+level)/2``.
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
dict[str, float]
|
|
96
|
+
``{"low": float, "high": float, "level": float}``.
|
|
97
|
+
"""
|
|
98
|
+
samples = np.asarray(samples, dtype=np.float64)
|
|
99
|
+
if samples.size == 0:
|
|
100
|
+
raise ValueError("samples is empty")
|
|
101
|
+
if np.any(np.isnan(samples)):
|
|
102
|
+
raise ValueError("samples contains NaN")
|
|
103
|
+
lo_q = (1.0 - level) / 2.0
|
|
104
|
+
hi_q = (1.0 + level) / 2.0
|
|
105
|
+
lo, hi = np.quantile(samples, [lo_q, hi_q])
|
|
106
|
+
return {"low": float(lo), "high": float(hi), "level": float(level)}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def chain_circular_summary(samples: np.ndarray) -> dict[str, float]:
|
|
110
|
+
"""
|
|
111
|
+
Compute circular-mean and circular-std of an angular chain.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
samples : np.ndarray, shape (n,)
|
|
116
|
+
1-D array of angle samples in radians. Convention may be
|
|
117
|
+
``[0, 2π)`` or ``[-π, π)``; the function pre-wraps to
|
|
118
|
+
``[0, 2π)`` before computing.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
dict[str, float]
|
|
123
|
+
``{"circmean_rad": float, "circstd_rad": float}``. Boundary
|
|
124
|
+
convention: ``circmean_rad`` is in ``[0, 2π)``.
|
|
125
|
+
|
|
126
|
+
Notes
|
|
127
|
+
-----
|
|
128
|
+
Pre-wrap to ``[0, 2π)`` is required because
|
|
129
|
+
:func:`scipy.stats.circstd` does NOT internally re-wrap; values
|
|
130
|
+
outside ``[low, high]`` are not folded.
|
|
131
|
+
|
|
132
|
+
For posteriors known to be multi-modal (e.g., ω with the
|
|
133
|
+
``(ω + π, Ω + π)`` 180° degeneracy in retrograde-inclination
|
|
134
|
+
astrometric fits), the circular mean collapses bimodal
|
|
135
|
+
posteriors to a meaningless midpoint. Report per-mode summaries
|
|
136
|
+
instead in those cases.
|
|
137
|
+
"""
|
|
138
|
+
samples = np.asarray(samples, dtype=np.float64)
|
|
139
|
+
if samples.size == 0:
|
|
140
|
+
raise ValueError("samples is empty")
|
|
141
|
+
if np.any(np.isnan(samples)):
|
|
142
|
+
raise ValueError("samples contains NaN")
|
|
143
|
+
# Pre-wrap to [0, 2π) — scipy.stats.circstd does not re-wrap.
|
|
144
|
+
samples = samples % (2.0 * np.pi)
|
|
145
|
+
# Circular mean via complex-exponential trick: arg(mean(exp(i·θ)))
|
|
146
|
+
# is the canonical wrap-aware estimator and ignores additive 2π
|
|
147
|
+
# shifts in any individual sample.
|
|
148
|
+
circmean = float(np.angle(np.mean(np.exp(1j * samples))) % (2.0 * np.pi))
|
|
149
|
+
circstd_val = float(circstd(samples, low=0.0, high=2.0 * np.pi))
|
|
150
|
+
return {"circmean_rad": circmean, "circstd_rad": circstd_val}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def chain_summary_table(
|
|
154
|
+
chain_dict: dict[str, np.ndarray],
|
|
155
|
+
levels: tuple[float, ...] = (0.025, 0.5, 0.975),
|
|
156
|
+
) -> pd.DataFrame:
|
|
157
|
+
"""
|
|
158
|
+
Tabular summary across multiple chain keys.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
chain_dict : dict[str, np.ndarray]
|
|
163
|
+
Mapping of parameter name → 1-D sample array.
|
|
164
|
+
levels : tuple of floats, default ``(0.025, 0.5, 0.975)``
|
|
165
|
+
Same as :func:`chain_quantiles`.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
pd.DataFrame
|
|
170
|
+
Columns: ``parameter``, plus one column per level (integer-
|
|
171
|
+
encoded keys: ``q025``, ``q500``, ``q975``).
|
|
172
|
+
"""
|
|
173
|
+
# pandas is imported HERE, not at module scope, so that the three helpers
|
|
174
|
+
# above — which need only numpy and scipy — stay usable when pandas is not
|
|
175
|
+
# installed. A top-of-file import would take all four down over this one
|
|
176
|
+
# call. See the module docstring for the install story.
|
|
177
|
+
try:
|
|
178
|
+
import pandas as pd
|
|
179
|
+
except ImportError as exc: # pragma: no cover - depends on the install
|
|
180
|
+
raise ImportError(
|
|
181
|
+
"chain_summary_table returns a pandas DataFrame, and orblet does "
|
|
182
|
+
"not install pandas by default. Either `pip install orblet[tables]`, "
|
|
183
|
+
"or call chain_quantiles per parameter — it returns the same "
|
|
184
|
+
"numbers as a plain dict and needs only numpy."
|
|
185
|
+
) from exc
|
|
186
|
+
|
|
187
|
+
rows = []
|
|
188
|
+
for name, samples in chain_dict.items():
|
|
189
|
+
qs = chain_quantiles(samples, levels=levels)
|
|
190
|
+
row = {"parameter": name}
|
|
191
|
+
row.update(qs)
|
|
192
|
+
rows.append(row)
|
|
193
|
+
columns = ["parameter"] + [_quantile_key(lvl) for lvl in levels]
|
|
194
|
+
return pd.DataFrame(rows, columns=columns)
|