sagari 1.0.0__tar.gz
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.
- sagari-1.0.0/CHANGELOG.md +27 -0
- sagari-1.0.0/CITATION.cff +40 -0
- sagari-1.0.0/LICENSE +21 -0
- sagari-1.0.0/MANIFEST.in +6 -0
- sagari-1.0.0/PKG-INFO +189 -0
- sagari-1.0.0/README.md +123 -0
- sagari-1.0.0/benchmarks/two_basin.py +129 -0
- sagari-1.0.0/docs/analysis.md +77 -0
- sagari-1.0.0/docs/api.rst +32 -0
- sagari-1.0.0/docs/changelog.md +2 -0
- sagari-1.0.0/docs/citing.md +8 -0
- sagari-1.0.0/docs/cluster.md +109 -0
- sagari-1.0.0/docs/concepts.md +74 -0
- sagari-1.0.0/docs/conf.py +29 -0
- sagari-1.0.0/docs/configuration.md +72 -0
- sagari-1.0.0/docs/faq.md +43 -0
- sagari-1.0.0/docs/index.md +53 -0
- sagari-1.0.0/docs/installation.md +31 -0
- sagari-1.0.0/docs/quickstart.md +51 -0
- sagari-1.0.0/docs/reproducibility.md +42 -0
- sagari-1.0.0/examples/README.md +20 -0
- sagari-1.0.0/examples/quickstart.py +47 -0
- sagari-1.0.0/examples/sco_runalls.py +106 -0
- sagari-1.0.0/pyproject.toml +88 -0
- sagari-1.0.0/setup.cfg +4 -0
- sagari-1.0.0/src/sagari/__init__.py +38 -0
- sagari-1.0.0/src/sagari/_version.py +3 -0
- sagari-1.0.0/src/sagari/acquisition.py +264 -0
- sagari-1.0.0/src/sagari/benchmark.py +80 -0
- sagari-1.0.0/src/sagari/campaign.py +445 -0
- sagari-1.0.0/src/sagari/cli.py +146 -0
- sagari-1.0.0/src/sagari/errorbars.py +297 -0
- sagari-1.0.0/src/sagari/gp.py +139 -0
- sagari-1.0.0/src/sagari/monitor.py +151 -0
- sagari-1.0.0/src/sagari/posterior.py +375 -0
- sagari-1.0.0/src/sagari/replay.py +34 -0
- sagari-1.0.0/src/sagari/sobol.py +72 -0
- sagari-1.0.0/src/sagari/space.py +184 -0
- sagari-1.0.0/src/sagari.egg-info/PKG-INFO +189 -0
- sagari-1.0.0/src/sagari.egg-info/SOURCES.txt +58 -0
- sagari-1.0.0/src/sagari.egg-info/dependency_links.txt +1 -0
- sagari-1.0.0/src/sagari.egg-info/entry_points.txt +2 -0
- sagari-1.0.0/src/sagari.egg-info/requires.txt +24 -0
- sagari-1.0.0/src/sagari.egg-info/top_level.txt +1 -0
- sagari-1.0.0/tests/conftest.py +5 -0
- sagari-1.0.0/tests/fixtures/golden.json +1 -0
- sagari-1.0.0/tests/fixtures/make_golden.py +48 -0
- sagari-1.0.0/tests/helpers.py +24 -0
- sagari-1.0.0/tests/test_acquisition.py +59 -0
- sagari-1.0.0/tests/test_benchmark.py +25 -0
- sagari-1.0.0/tests/test_campaign.py +129 -0
- sagari-1.0.0/tests/test_errorbars.py +32 -0
- sagari-1.0.0/tests/test_examples.py +36 -0
- sagari-1.0.0/tests/test_golden.py +76 -0
- sagari-1.0.0/tests/test_monitor.py +29 -0
- sagari-1.0.0/tests/test_parity.py +192 -0
- sagari-1.0.0/tests/test_posterior.py +49 -0
- sagari-1.0.0/tests/test_replay.py +32 -0
- sagari-1.0.0/tests/test_sobol.py +26 -0
- sagari-1.0.0/tests/test_space.py +55 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 (2026-10-15)
|
|
4
|
+
|
|
5
|
+
First public release of SAGARI (Schwarzschild Adaptive Gaussian-process Acquisition, Refinement, and
|
|
6
|
+
Inference), accompanying Waters & Gültekin (2026).
|
|
7
|
+
|
|
8
|
+
- `ParameterSpace`: bounds, linear and logarithmic axes, fixed parameters; YAML and dict input.
|
|
9
|
+
- `SobolGlobalDesign` / `initial_design`: persistent scrambled-Sobol global design.
|
|
10
|
+
- `propose_round`: refinement rounds by feasibility-weighted Expected Improvement with a nugget for the
|
|
11
|
+
model-to-model scatter, a training window in units of that scatter, a denoised incumbent, a two-scale
|
|
12
|
+
candidate pool, a minimum separation, verification of isolated low models, and a global Sobol share.
|
|
13
|
+
- `smoothed_estimate`, `estimate_converged`: monitoring estimate and stopping rule; `trusted_best_point`.
|
|
14
|
+
- `errorbar_round`, `inference_gp`, `gp_profiles`, `profile_verdict`, `out_of_sample`: error-bar rounds,
|
|
15
|
+
the inference surrogate (optionally with an automatically chosen training window) and its checks.
|
|
16
|
+
- `posterior`: the ensemble-MCMC posterior on the supported surrogate, surrogate-marginalized quantiles and
|
|
17
|
+
mode shares, Δχ² profiles and their parametric-bootstrap widening.
|
|
18
|
+
- `Campaign` and the `sagari` command line: file-based campaigns for clusters, and an in-process loop.
|
|
19
|
+
- `replay`, `benchmark`: archive replays and the two-basin objective of the paper; `benchmarks/two_basin.py`.
|
|
20
|
+
- Parity tests against the original campaign code of the paper, and a golden fixture written by it.
|
|
21
|
+
- Four executed example notebooks (a first campaign; the cluster workflow; the final analysis on a two-solution
|
|
22
|
+
problem; the two-basin benchmark and an archive replay), a documentation site (concepts, cluster workflow,
|
|
23
|
+
configuration reference, analysis, reproducibility, FAQ), and example space and settings files.
|
|
24
|
+
- `posterior.surface_draws` raises the covariance jitter when the factorization fails (posterior samples much
|
|
25
|
+
closer together than the kernel's length scales); the first attempt uses the paper's value.
|
|
26
|
+
- Tested on Python 3.10–3.13, down to NumPy 1.26, SciPy 1.11, scikit-learn 1.3 and pandas 2.0, and up to
|
|
27
|
+
pandas 3.0.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
title: "SAGARI: Schwarzschild Adaptive Gaussian-process Acquisition, Refinement, and Inference"
|
|
3
|
+
message: "If you use this software, please cite the paper below and the software."
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Waters
|
|
7
|
+
given-names: Thomas K.
|
|
8
|
+
affiliation: University of Michigan
|
|
9
|
+
orcid: "https://orcid.org/0000-0002-5231-7240"
|
|
10
|
+
- family-names: Gültekin
|
|
11
|
+
given-names: Kayhan
|
|
12
|
+
affiliation: University of Michigan
|
|
13
|
+
orcid: "https://orcid.org/0000-0002-1146-0198"
|
|
14
|
+
version: 1.0.0
|
|
15
|
+
date-released: "2026-10-15"
|
|
16
|
+
url: "https://github.com/thomas-k-waters/sagari"
|
|
17
|
+
license: MIT
|
|
18
|
+
keywords:
|
|
19
|
+
- astronomy
|
|
20
|
+
- supermassive black holes
|
|
21
|
+
- Schwarzschild modeling
|
|
22
|
+
- Bayesian optimization
|
|
23
|
+
- Gaussian processes
|
|
24
|
+
abstract: >
|
|
25
|
+
SAGARI replaces the nested parameter grids of orbit-superposition modeling campaigns with adaptive
|
|
26
|
+
rounds: a scrambled Sobol initial design, refinement by feasibility-weighted Expected Improvement under a
|
|
27
|
+
Gaussian-process surrogate with an explicit term for the model-to-model scatter of chi2, a stopping rule on
|
|
28
|
+
smoothed parameter ranges, error-bar rounds that test the surrogate out of sample, and a posterior sampled
|
|
29
|
+
on the final surrogate. It needs only the parameters and chi2 of each model.
|
|
30
|
+
preferred-citation:
|
|
31
|
+
type: article
|
|
32
|
+
authors:
|
|
33
|
+
- family-names: Waters
|
|
34
|
+
given-names: Thomas K.
|
|
35
|
+
- family-names: Gültekin
|
|
36
|
+
given-names: Kayhan
|
|
37
|
+
title: "Schwarzschild Orbit-library Modeling with an Order of Magnitude Fewer Models: Adaptive Gaussian-process Sampling and a Case Study of M 86"
|
|
38
|
+
journal: "The Astrophysical Journal"
|
|
39
|
+
year: 2026
|
|
40
|
+
notes: "submitted"
|
sagari-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas K. Waters
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sagari-1.0.0/MANIFEST.in
ADDED
sagari-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sagari
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: SAGARI: Schwarzschild Adaptive Gaussian-process Acquisition, Refinement, and Inference -- adaptive sampling for orbit-superposition modeling and other expensive, noisy chi2 surfaces
|
|
5
|
+
Author-email: "Thomas K. Waters" <waterstk@umich.edu>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Thomas K. Waters
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/thomas-k-waters/sagari
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/thomas-k-waters/sagari/issues
|
|
30
|
+
Project-URL: Documentation, https://sagari.readthedocs.io
|
|
31
|
+
Keywords: astronomy,black holes,Schwarzschild modeling,orbit superposition,Bayesian optimization,Gaussian process,expected improvement,Sobol sequence
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Science/Research
|
|
34
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Requires-Python: >=3.10
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
License-File: LICENSE
|
|
44
|
+
Requires-Dist: numpy>=1.26
|
|
45
|
+
Requires-Dist: scipy>=1.11
|
|
46
|
+
Requires-Dist: pandas>=2.0
|
|
47
|
+
Requires-Dist: scikit-learn>=1.3
|
|
48
|
+
Requires-Dist: emcee>=3.1
|
|
49
|
+
Requires-Dist: pyyaml>=6.0
|
|
50
|
+
Provides-Extra: plot
|
|
51
|
+
Requires-Dist: matplotlib>=3.7; extra == "plot"
|
|
52
|
+
Requires-Dist: corner>=2.2; extra == "plot"
|
|
53
|
+
Provides-Extra: docs
|
|
54
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
55
|
+
Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
|
|
56
|
+
Requires-Dist: myst-nb>=1.0; extra == "docs"
|
|
57
|
+
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
58
|
+
Requires-Dist: matplotlib>=3.7; extra == "docs"
|
|
59
|
+
Provides-Extra: dev
|
|
60
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
61
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
62
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
63
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
64
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
65
|
+
Dynamic: license-file
|
|
66
|
+
|
|
67
|
+
# SAGARI
|
|
68
|
+
|
|
69
|
+
**Schwarzschild Adaptive Gaussian-process Acquisition, Refinement, and Inference**
|
|
70
|
+
|
|
71
|
+
[](https://github.com/thomas-k-waters/sagari/actions/workflows/tests.yml)
|
|
72
|
+
[](https://pypi.org/project/sagari/)
|
|
73
|
+
[](https://sagari.readthedocs.io)
|
|
74
|
+
[](LICENSE)
|
|
75
|
+
|
|
76
|
+
Adaptive Gaussian-process sampling for Schwarzschild orbit-superposition modeling, and for any other model
|
|
77
|
+
whose goodness of fit is an expensive, noisy χ².
|
|
78
|
+
|
|
79
|
+
An orbit-superposition model costs hours of CPU time, and a nested parameter grid needs ~10⁴ of them.
|
|
80
|
+
SAGARI replaces the grid with a sequence of rounds, each placed where a surrogate of the χ² surface predicts
|
|
81
|
+
the largest gain, and stops the campaign when the answer stops moving. On the archived models of two published
|
|
82
|
+
black-hole mass measurements it recovered the published mass in every replayed trial with **9 to 30 times fewer
|
|
83
|
+
models** than the original campaigns.
|
|
84
|
+
|
|
85
|
+
It is the method of
|
|
86
|
+
|
|
87
|
+
> Waters & Gültekin (2026), *Schwarzschild Orbit-library Modeling with an Order of Magnitude Fewer Models:
|
|
88
|
+
> Adaptive Gaussian-process Sampling and a Case Study of M 86*
|
|
89
|
+
|
|
90
|
+
and reproduces the code used there exactly ([details](#fidelity-to-the-paper)).
|
|
91
|
+
|
|
92
|
+
The package never runs a model. It proposes parameter sets and reads back their χ², so it can drive any code: an
|
|
93
|
+
orbit-superposition pipeline on a cluster, a Jeans model, or a lookup in an archive.
|
|
94
|
+
|
|
95
|
+
📖 **Documentation:** https://sagari.readthedocs.io
|
|
96
|
+
|
|
97
|
+
## Installation
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install sagari # add [plot] for the example notebooks
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Python ≥ 3.10. Depends on NumPy, SciPy, pandas, scikit-learn, emcee and PyYAML.
|
|
104
|
+
|
|
105
|
+
## Quick start
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import sagari as sg
|
|
109
|
+
|
|
110
|
+
space = sg.ParameterSpace.from_dict({"parameters": {
|
|
111
|
+
"mbh": {"bounds": [1e6, 1e10], "scale": "log"},
|
|
112
|
+
"upsilon": {"bounds": [1.0, 10.0]},
|
|
113
|
+
"v_c": {"bounds": [0.0, 500.0]},
|
|
114
|
+
"inclination": {"value": 60.0}, # fixed
|
|
115
|
+
}})
|
|
116
|
+
|
|
117
|
+
def my_model(design): # a DataFrame of parameter sets -> one chi2 per row
|
|
118
|
+
... # (NaN for a model that failed)
|
|
119
|
+
|
|
120
|
+
c = sg.Campaign.create("my_campaign", space, converge_on=["mbh", "upsilon"])
|
|
121
|
+
results = c.run(my_model) # Sobol design, search rounds, stopping rule, error-bar rounds
|
|
122
|
+
|
|
123
|
+
gp, _, _ = c.surrogate(results) # the final surrogate of the chi2 surface
|
|
124
|
+
post = sg.posterior.sample_posterior(gp, space)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## On a cluster
|
|
128
|
+
|
|
129
|
+
When each model is a batch job, the campaign lives in a directory and moves one round at a time:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
sagari init my_campaign --space space.yaml --converge-on mbh,upsilon
|
|
133
|
+
sagari next my_campaign # writes my_campaign/rounds/round_000.csv
|
|
134
|
+
# ... run the models of the round with your own code; add model_id,chi2 rows to results.csv ...
|
|
135
|
+
sagari next my_campaign --results results.csv # the next round (search or error bars), or "done"
|
|
136
|
+
sagari status my_campaign --results results.csv # ranges per round, stopping rule, out-of-sample checks
|
|
137
|
+
sagari posterior my_campaign --results results.csv --out posterior --profiles
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Failed models are written as `chi2 = nan` and kept: the search learns to avoid where models fail.
|
|
141
|
+
`examples/sco_runalls.py` connects a campaign to the Fortran pipeline of the paper (`runallt` commands file in,
|
|
142
|
+
`cres` files out).
|
|
143
|
+
|
|
144
|
+
## How it works
|
|
145
|
+
|
|
146
|
+
1. **Initial design**: a scrambled Sobol sequence over the box, continued by every later round.
|
|
147
|
+
2. **Search rounds**: a Gaussian process with a Matérn 5/2 kernel and an explicit nugget for the model-to-model
|
|
148
|
+
scatter of χ², fitted near the minimum. A second GP fitted to every model keeps the search out of regions that
|
|
149
|
+
are hopeless or where models fail. Each round places 65% of its models by feasibility-weighted Expected
|
|
150
|
+
Improvement, with a denoised incumbent, two-scale candidates and a minimum separation, and verifies isolated
|
|
151
|
+
low models by their neighbours. The other 35% continues the Sobol sequence.
|
|
152
|
+
3. **Stopping rule**: smoothed 16–84% ranges of the parameters you care about. The search stops when they move
|
|
153
|
+
by less than 0.2 of their width, after at least two refinement rounds.
|
|
154
|
+
4. **Error-bar rounds**: models fill the acceptable region and the parameter profiles. Each model's χ² is
|
|
155
|
+
predicted before it runs, so the surrogate is tested out of sample.
|
|
156
|
+
5. **Posterior**: MCMC on the final surrogate where models constrain it, averaged over the surrogate's own
|
|
157
|
+
uncertainty, plus Δχ² profile intervals.
|
|
158
|
+
|
|
159
|
+
The defaults are the settings adopted and validated in the paper.
|
|
160
|
+
|
|
161
|
+
## Example notebooks
|
|
162
|
+
|
|
163
|
+
| notebook | |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| [01 Your first campaign](examples/notebooks/01_quickstart.ipynb) | a complete campaign in Python on a noisy analytic χ² |
|
|
166
|
+
| [02 A campaign on a cluster](examples/notebooks/02_cluster_workflow.ipynb) | round files, results tables, failures, narrowing the box, the CLI |
|
|
167
|
+
| [03 From the surrogate to a measurement](examples/notebooks/03_analysis.ipynb) | profiles, posteriors, two solutions, surrogate uncertainty |
|
|
168
|
+
| [04 Benchmark and replay](examples/notebooks/04_benchmark_and_replay.ipynb) | the paper's two-basin benchmark and a replay on an archive |
|
|
169
|
+
|
|
170
|
+
## Fidelity to the paper
|
|
171
|
+
|
|
172
|
+
The package is a port of the campaign code used in the paper, not a reimplementation:
|
|
173
|
+
|
|
174
|
+
- `tests/test_parity.py` runs the original code and the package side by side and requires identical output at
|
|
175
|
+
every stage (needs the original code; see the docs).
|
|
176
|
+
- `tests/test_golden.py` checks the same outputs against a fixture written by the original code, and runs anywhere.
|
|
177
|
+
- Replays of the paper's NGC 5102 and NGC 3258 campaigns, and its two-basin benchmark
|
|
178
|
+
(`benchmarks/two_basin.py`), are reproduced model for model.
|
|
179
|
+
|
|
180
|
+
Exact repetition from a seed also needs the same library versions and the same number of linear-algebra threads
|
|
181
|
+
(`OMP_NUM_THREADS=1`).
|
|
182
|
+
|
|
183
|
+
## Citing
|
|
184
|
+
|
|
185
|
+
Please cite Waters & Gültekin (2026) and the software (`CITATION.cff`).
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT. See [LICENSE](LICENSE).
|
sagari-1.0.0/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# SAGARI
|
|
2
|
+
|
|
3
|
+
**Schwarzschild Adaptive Gaussian-process Acquisition, Refinement, and Inference**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/thomas-k-waters/sagari/actions/workflows/tests.yml)
|
|
6
|
+
[](https://pypi.org/project/sagari/)
|
|
7
|
+
[](https://sagari.readthedocs.io)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
Adaptive Gaussian-process sampling for Schwarzschild orbit-superposition modeling, and for any other model
|
|
11
|
+
whose goodness of fit is an expensive, noisy χ².
|
|
12
|
+
|
|
13
|
+
An orbit-superposition model costs hours of CPU time, and a nested parameter grid needs ~10⁴ of them.
|
|
14
|
+
SAGARI replaces the grid with a sequence of rounds, each placed where a surrogate of the χ² surface predicts
|
|
15
|
+
the largest gain, and stops the campaign when the answer stops moving. On the archived models of two published
|
|
16
|
+
black-hole mass measurements it recovered the published mass in every replayed trial with **9 to 30 times fewer
|
|
17
|
+
models** than the original campaigns.
|
|
18
|
+
|
|
19
|
+
It is the method of
|
|
20
|
+
|
|
21
|
+
> Waters & Gültekin (2026), *Schwarzschild Orbit-library Modeling with an Order of Magnitude Fewer Models:
|
|
22
|
+
> Adaptive Gaussian-process Sampling and a Case Study of M 86*
|
|
23
|
+
|
|
24
|
+
and reproduces the code used there exactly ([details](#fidelity-to-the-paper)).
|
|
25
|
+
|
|
26
|
+
The package never runs a model. It proposes parameter sets and reads back their χ², so it can drive any code: an
|
|
27
|
+
orbit-superposition pipeline on a cluster, a Jeans model, or a lookup in an archive.
|
|
28
|
+
|
|
29
|
+
📖 **Documentation:** https://sagari.readthedocs.io
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install sagari # add [plot] for the example notebooks
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Python ≥ 3.10. Depends on NumPy, SciPy, pandas, scikit-learn, emcee and PyYAML.
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import sagari as sg
|
|
43
|
+
|
|
44
|
+
space = sg.ParameterSpace.from_dict({"parameters": {
|
|
45
|
+
"mbh": {"bounds": [1e6, 1e10], "scale": "log"},
|
|
46
|
+
"upsilon": {"bounds": [1.0, 10.0]},
|
|
47
|
+
"v_c": {"bounds": [0.0, 500.0]},
|
|
48
|
+
"inclination": {"value": 60.0}, # fixed
|
|
49
|
+
}})
|
|
50
|
+
|
|
51
|
+
def my_model(design): # a DataFrame of parameter sets -> one chi2 per row
|
|
52
|
+
... # (NaN for a model that failed)
|
|
53
|
+
|
|
54
|
+
c = sg.Campaign.create("my_campaign", space, converge_on=["mbh", "upsilon"])
|
|
55
|
+
results = c.run(my_model) # Sobol design, search rounds, stopping rule, error-bar rounds
|
|
56
|
+
|
|
57
|
+
gp, _, _ = c.surrogate(results) # the final surrogate of the chi2 surface
|
|
58
|
+
post = sg.posterior.sample_posterior(gp, space)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## On a cluster
|
|
62
|
+
|
|
63
|
+
When each model is a batch job, the campaign lives in a directory and moves one round at a time:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
sagari init my_campaign --space space.yaml --converge-on mbh,upsilon
|
|
67
|
+
sagari next my_campaign # writes my_campaign/rounds/round_000.csv
|
|
68
|
+
# ... run the models of the round with your own code; add model_id,chi2 rows to results.csv ...
|
|
69
|
+
sagari next my_campaign --results results.csv # the next round (search or error bars), or "done"
|
|
70
|
+
sagari status my_campaign --results results.csv # ranges per round, stopping rule, out-of-sample checks
|
|
71
|
+
sagari posterior my_campaign --results results.csv --out posterior --profiles
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Failed models are written as `chi2 = nan` and kept: the search learns to avoid where models fail.
|
|
75
|
+
`examples/sco_runalls.py` connects a campaign to the Fortran pipeline of the paper (`runallt` commands file in,
|
|
76
|
+
`cres` files out).
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
1. **Initial design**: a scrambled Sobol sequence over the box, continued by every later round.
|
|
81
|
+
2. **Search rounds**: a Gaussian process with a Matérn 5/2 kernel and an explicit nugget for the model-to-model
|
|
82
|
+
scatter of χ², fitted near the minimum. A second GP fitted to every model keeps the search out of regions that
|
|
83
|
+
are hopeless or where models fail. Each round places 65% of its models by feasibility-weighted Expected
|
|
84
|
+
Improvement, with a denoised incumbent, two-scale candidates and a minimum separation, and verifies isolated
|
|
85
|
+
low models by their neighbours. The other 35% continues the Sobol sequence.
|
|
86
|
+
3. **Stopping rule**: smoothed 16–84% ranges of the parameters you care about. The search stops when they move
|
|
87
|
+
by less than 0.2 of their width, after at least two refinement rounds.
|
|
88
|
+
4. **Error-bar rounds**: models fill the acceptable region and the parameter profiles. Each model's χ² is
|
|
89
|
+
predicted before it runs, so the surrogate is tested out of sample.
|
|
90
|
+
5. **Posterior**: MCMC on the final surrogate where models constrain it, averaged over the surrogate's own
|
|
91
|
+
uncertainty, plus Δχ² profile intervals.
|
|
92
|
+
|
|
93
|
+
The defaults are the settings adopted and validated in the paper.
|
|
94
|
+
|
|
95
|
+
## Example notebooks
|
|
96
|
+
|
|
97
|
+
| notebook | |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| [01 Your first campaign](examples/notebooks/01_quickstart.ipynb) | a complete campaign in Python on a noisy analytic χ² |
|
|
100
|
+
| [02 A campaign on a cluster](examples/notebooks/02_cluster_workflow.ipynb) | round files, results tables, failures, narrowing the box, the CLI |
|
|
101
|
+
| [03 From the surrogate to a measurement](examples/notebooks/03_analysis.ipynb) | profiles, posteriors, two solutions, surrogate uncertainty |
|
|
102
|
+
| [04 Benchmark and replay](examples/notebooks/04_benchmark_and_replay.ipynb) | the paper's two-basin benchmark and a replay on an archive |
|
|
103
|
+
|
|
104
|
+
## Fidelity to the paper
|
|
105
|
+
|
|
106
|
+
The package is a port of the campaign code used in the paper, not a reimplementation:
|
|
107
|
+
|
|
108
|
+
- `tests/test_parity.py` runs the original code and the package side by side and requires identical output at
|
|
109
|
+
every stage (needs the original code; see the docs).
|
|
110
|
+
- `tests/test_golden.py` checks the same outputs against a fixture written by the original code, and runs anywhere.
|
|
111
|
+
- Replays of the paper's NGC 5102 and NGC 3258 campaigns, and its two-basin benchmark
|
|
112
|
+
(`benchmarks/two_basin.py`), are reproduced model for model.
|
|
113
|
+
|
|
114
|
+
Exact repetition from a seed also needs the same library versions and the same number of linear-algebra threads
|
|
115
|
+
(`OMP_NUM_THREADS=1`).
|
|
116
|
+
|
|
117
|
+
## Citing
|
|
118
|
+
|
|
119
|
+
Please cite Waters & Gültekin (2026) and the software (`CITATION.cff`).
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The two-basin benchmark of the paper (Section 4.3 and Appendix B), run with sagari.
|
|
2
|
+
|
|
3
|
+
Each trial is a campaign of 256 Sobol points and five rounds of 128 (896 models) on the synthetic objective
|
|
4
|
+
of :mod:`sagari.benchmark`, for three samplers:
|
|
5
|
+
|
|
6
|
+
full the method of the paper (Matern 5/2 kernel, 30 sigma training window, 35% global share, plug-in
|
|
7
|
+
incumbent, two-scale candidates, minimum separation, verification of isolated models)
|
|
8
|
+
ablated the same search without its robustness measures: squared-exponential kernel, no training window,
|
|
9
|
+
15% global share, the lowest model as the incumbent, no local candidates, no minimum separation
|
|
10
|
+
or verification, one candidate pool for the whole campaign (the feasibility weighting is kept)
|
|
11
|
+
sobol Sobol sampling alone
|
|
12
|
+
|
|
13
|
+
After every round it records whether the best point of the smoothed estimate lies in the deeper basin, its
|
|
14
|
+
M_BH error, the models within 2 sigma of the deeper minimum, and the share of local points in the failure
|
|
15
|
+
region. Trials use seeds 1000, 1001, ... as in the paper (100 trials: Figure 1).
|
|
16
|
+
|
|
17
|
+
python two_basin.py [n_trials] [--hopeless] [--out results/two_basin.csv] [--procs N]
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
from multiprocessing import Pool
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
for _v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS", "VECLIB_MAXIMUM_THREADS"):
|
|
28
|
+
os.environ.setdefault(_v, "1")
|
|
29
|
+
|
|
30
|
+
import numpy as np # noqa: E402
|
|
31
|
+
import pandas as pd # noqa: E402
|
|
32
|
+
|
|
33
|
+
import sagari as sg # noqa: E402
|
|
34
|
+
from sagari.benchmark import NOISE, PILOT_SPACE, TwoBasin # noqa: E402
|
|
35
|
+
|
|
36
|
+
N0, NR, N_ROUNDS = 256, 128, 5
|
|
37
|
+
WASTE_ABOVE = 2000.0 # a local point with true Delta chi2 above this is wasted
|
|
38
|
+
VARIANTS = {
|
|
39
|
+
"full": dict(kernel="matern52", train_window=30.0, global_fraction=0.35),
|
|
40
|
+
"ablated": dict(kernel="rbf", train_window=None, global_fraction=0.15, incumbent="raw", n_local_candidates=0,
|
|
41
|
+
min_separation=0.0, verify=False, redraw_candidates=False),
|
|
42
|
+
"sobol": None,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def trial(args):
|
|
47
|
+
variant, seed, hopeless = args
|
|
48
|
+
obj = TwoBasin(hopeless=hopeless)
|
|
49
|
+
space = PILOT_SPACE
|
|
50
|
+
rng = np.random.default_rng(seed)
|
|
51
|
+
sob = sg.SobolGlobalDesign(space.dim, seed)
|
|
52
|
+
d = sg.initial_design(space, N0, sob)
|
|
53
|
+
table = d[space.free_params].copy()
|
|
54
|
+
table["chi2"] = obj(d, rng)
|
|
55
|
+
rows, waste = [], []
|
|
56
|
+
for rnd in range(N_ROUNDS + 1):
|
|
57
|
+
if rnd > 0:
|
|
58
|
+
if VARIANTS[variant] is None:
|
|
59
|
+
d = sg.initial_design(space, NR, sob)
|
|
60
|
+
else:
|
|
61
|
+
# noise_diagnostic=False: the band for verifying isolated models is twice the sd of the 20 lowest
|
|
62
|
+
# chi2, as in the benchmark runs of the paper
|
|
63
|
+
d = sg.propose_round(table, space, NR, sob, seed=seed, noise_diagnostic=False, **VARIANTS[variant])
|
|
64
|
+
loc = (d["stage"] != "sobol_global_continuation").values
|
|
65
|
+
if loc.any():
|
|
66
|
+
waste.append(float(((obj.true(space.to_unit(d[loc]))[0] - 1000) > WASTE_ABOVE).mean()))
|
|
67
|
+
new = d[space.free_params].copy()
|
|
68
|
+
new["chi2"] = obj(d, rng)
|
|
69
|
+
table = pd.concat([table, new], ignore_index=True)
|
|
70
|
+
# The runs of the paper drew a search pool for a diagnostic not reported here from the generator that also
|
|
71
|
+
# draws the chi2 scatter; draw the same numbers so that every later round sees identical scatter.
|
|
72
|
+
rng.random((20000, space.dim))
|
|
73
|
+
for _ in range(50):
|
|
74
|
+
rng.standard_normal((20, space.dim))
|
|
75
|
+
f, in_n = obj.true(space.to_unit(table))
|
|
76
|
+
row = dict(variant=variant, seed=seed, round=rnd, n_models=len(table), n_failed=int(table["chi2"].isna().sum()),
|
|
77
|
+
waste=float(np.mean(waste)) if waste else np.nan, n_in_N=int((((f - 1000) < 2 * NOISE) & in_n).sum()))
|
|
78
|
+
est = sg.smoothed_estimate(table, space, seed=seed, kernel="matern52", train_window=30.0)
|
|
79
|
+
if est is not None:
|
|
80
|
+
row.update(est_found=obj.found(est), est_mbh_err=obj.mbh_error(est),
|
|
81
|
+
est_mbh_covers=bool(est["mbh"][1] <= 8.0e8 <= est["mbh"][2]))
|
|
82
|
+
rows.append(row)
|
|
83
|
+
return rows
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def wilson(k, n, z=1.96):
|
|
87
|
+
p = k / n
|
|
88
|
+
c = (p + z * z / (2 * n)) / (1 + z * z / n)
|
|
89
|
+
h = z * np.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / (1 + z * z / n)
|
|
90
|
+
return c - h, c + h
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main():
|
|
94
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
95
|
+
ap.add_argument("n_trials", type=int, nargs="?", default=20)
|
|
96
|
+
ap.add_argument("--hopeless", action="store_true", help="add the region of failed and very-high-chi2 models")
|
|
97
|
+
ap.add_argument("--variants", default="full,ablated,sobol")
|
|
98
|
+
ap.add_argument("--out", default=None)
|
|
99
|
+
ap.add_argument("--procs", type=int, default=None)
|
|
100
|
+
a = ap.parse_args()
|
|
101
|
+
out = Path(a.out or f"results/two_basin_{'hopeless' if a.hopeless else 'plain'}.csv")
|
|
102
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
part = out.with_suffix(".jsonl")
|
|
104
|
+
done = set()
|
|
105
|
+
if part.exists(): # finished trials are kept, so an interrupted run loses nothing
|
|
106
|
+
done = {(r[0]["variant"], r[0]["seed"]) for r in map(json.loads, part.read_text().splitlines())}
|
|
107
|
+
jobs = [(v, 1000 + s, a.hopeless) for v in a.variants.split(",") for s in range(a.n_trials)
|
|
108
|
+
if (v, 1000 + s) not in done]
|
|
109
|
+
print(f"{len(jobs)} trials to run ({len(done)} already in {part})", flush=True)
|
|
110
|
+
with Pool(a.procs) as pool:
|
|
111
|
+
for rs in pool.imap_unordered(trial, jobs):
|
|
112
|
+
with open(part, "a") as f:
|
|
113
|
+
f.write(json.dumps(rs, default=float) + "\n")
|
|
114
|
+
print(f"{rs[0]['variant']} {rs[0]['seed']}: found={rs[-1].get('est_found')}", flush=True)
|
|
115
|
+
rows = [r for rs in map(json.loads, part.read_text().splitlines()) for r in rs if r["seed"] < 1000 + a.n_trials]
|
|
116
|
+
df = pd.DataFrame(rows)
|
|
117
|
+
df.to_csv(out, index=False)
|
|
118
|
+
fin = df[df["n_models"] == N0 + N_ROUNDS * NR]
|
|
119
|
+
print(f"\nat {N0 + N_ROUNDS * NR} models ({'with' if a.hopeless else 'without'} the failure region):")
|
|
120
|
+
for v, g in fin.groupby("variant"):
|
|
121
|
+
k, n = int(g["est_found"].sum()), len(g)
|
|
122
|
+
lo, hi = wilson(k, n)
|
|
123
|
+
print(f" {v:8s} deeper basin found in {k}/{n} ({100 * lo:.0f}-{100 * hi:.0f}%), median M_BH error "
|
|
124
|
+
f"{g['est_mbh_err'].median():.2f} dex, median models within 2 sigma of it {g['n_in_N'].median():.0f}, "
|
|
125
|
+
f"local points wasted {100 * g['waste'].mean():.0f}%")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# From the final surrogate to a measurement
|
|
2
|
+
|
|
3
|
+
The constraints come from the **inference surrogate**: a GP fitted to every model inside the training window,
|
|
4
|
+
with a nugget for the model-to-model scatter. Do not use the lowest model. With scatter, the lowest of many
|
|
5
|
+
models lies well below the smooth minimum (in M86, 17 below).
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import sagari as sg
|
|
9
|
+
from sagari import posterior as po
|
|
10
|
+
|
|
11
|
+
c = sg.Campaign("my_campaign")
|
|
12
|
+
gp, chi2_min, threshold = c.surrogate("results.csv")
|
|
13
|
+
print(gp.scatter) # sigma_chi2, the fitted model-to-model scatter
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Outside a campaign directory, use `sg.inference_gp(table, space)` on any table of models.
|
|
17
|
+
|
|
18
|
+
## Profiles: Δχ² intervals
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
prof = po.profiles(gp, space, seed=c.seed) # every free parameter
|
|
22
|
+
iv = po.profile_intervals(prof, space, levels=(1.0, 4.0, 10.0)) # {param: {level: [(lo, hi), ...]}}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
A profile minimizes the surrogate mean over all other parameters at each value of one. Points are trusted only
|
|
26
|
+
where models constrain the surrogate (latent sd ≤ 2σ). Intervals can have several pieces when the region is
|
|
27
|
+
disconnected. The ends move when the scatter is redrawn; `profile_interval_bootstrap` refits the surrogate to
|
|
28
|
+
parametric-bootstrap copies and gives their spread.
|
|
29
|
+
|
|
30
|
+
## The posterior
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
post = po.sample_posterior(gp, space) # emcee: 32 walkers, 25000 steps, 5000 burn-in
|
|
34
|
+
post["mbh"].quantile([0.1585, 0.5, 0.8415])
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This samples $p\propto\exp(-\mu/2)$ with priors uniform in the unit cube (log-uniform on log axes). The posterior
|
|
38
|
+
is zero wherever the surrogate's latent sd exceeds $2\sigma_{\chi^2}$: there no models constrain it, and a sampler
|
|
39
|
+
would settle in any low value the surrogate extrapolates. The samples in the unit cube are in
|
|
40
|
+
`post.attrs["unit"]`.
|
|
41
|
+
|
|
42
|
+
## Averaging over the surrogate's uncertainty
|
|
43
|
+
|
|
44
|
+
The surrogate is fitted to noisy models, so its mean is uncertain too. This matters most when two solutions
|
|
45
|
+
differ by less than the scatter:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
idx, D, D_refit = po.surface_draws(gp, post.attrs["unit"]) # 2000 latent draws at 3000 samples (+300 refits)
|
|
49
|
+
lo, med, hi = po.pooled_quantiles(post["mbh"].values[idx], D) # the surrogate-marginalized posterior
|
|
50
|
+
shares = po.mode_shares(D, post["upsilon"].values[idx] > 7.0) # how uncertain the split between two solutions is
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`D` holds joint draws of the surrogate's latent error (the scatter removed, so it is not counted twice).
|
|
54
|
+
`D_refit` comes from refits to the models with their χ² redrawn from the scatter. Reweighting the samples by each
|
|
55
|
+
draw gives the posterior of that realization of the surface. Pooling the reweighted posteriors gives the
|
|
56
|
+
surrogate-marginalized posterior that the paper adopts.
|
|
57
|
+
|
|
58
|
+
## Other priors
|
|
59
|
+
|
|
60
|
+
Every prior is a reweighting of the samples. For a prior uniform in $r_c$ instead of $\log r_c$:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
w = post["r_c"].values[idx]
|
|
64
|
+
po.pooled_quantiles(post["mbh"].values[idx], D, prior_w=w)
|
|
65
|
+
po.mode_shares(D, select, prior_w=w)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Checking the surrogate
|
|
69
|
+
|
|
70
|
+
- **Out of sample**: every error-bar round stores the prediction made before each model ran;
|
|
71
|
+
`sagari status` reports the rms of $z=(\chi^2-\mu)/s$ (≈1 for a calibrated surrogate) and the fractions
|
|
72
|
+
beyond ±2. `sg.out_of_sample(round_table, chi2)` computes it directly.
|
|
73
|
+
- **Leave-one-out**: `po.loo(gp)` returns the closed-form leave-one-out residuals of the training models.
|
|
74
|
+
- **The scatter**: `gp.scatter` should be stable once the region of the minimum is sampled.
|
|
75
|
+
`sg.errorbars.window_check(gp)` compares it with the leave-one-out rms near the floor.
|
|
76
|
+
|
|
77
|
+
The [analysis notebook](notebooks/03_analysis.ipynb) runs all of this on a two-solution problem.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
API
|
|
2
|
+
===
|
|
3
|
+
|
|
4
|
+
.. automodule:: sagari.space
|
|
5
|
+
:members:
|
|
6
|
+
|
|
7
|
+
.. automodule:: sagari.sobol
|
|
8
|
+
:members:
|
|
9
|
+
|
|
10
|
+
.. automodule:: sagari.acquisition
|
|
11
|
+
:members:
|
|
12
|
+
|
|
13
|
+
.. automodule:: sagari.monitor
|
|
14
|
+
:members:
|
|
15
|
+
|
|
16
|
+
.. automodule:: sagari.errorbars
|
|
17
|
+
:members:
|
|
18
|
+
|
|
19
|
+
.. automodule:: sagari.posterior
|
|
20
|
+
:members:
|
|
21
|
+
|
|
22
|
+
.. automodule:: sagari.campaign
|
|
23
|
+
:members:
|
|
24
|
+
|
|
25
|
+
.. automodule:: sagari.gp
|
|
26
|
+
:members:
|
|
27
|
+
|
|
28
|
+
.. automodule:: sagari.replay
|
|
29
|
+
:members:
|
|
30
|
+
|
|
31
|
+
.. automodule:: sagari.benchmark
|
|
32
|
+
:members:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Citing
|
|
2
|
+
|
|
3
|
+
If you use SAGARI, please cite the paper that describes and validates the method:
|
|
4
|
+
|
|
5
|
+
> Waters, T. K., & Gültekin, K. 2026, *Schwarzschild Orbit-library Modeling with an Order of Magnitude Fewer
|
|
6
|
+
> Models: Adaptive Gaussian-process Sampling and a Case Study of M 86*, ApJ, submitted
|
|
7
|
+
|
|
8
|
+
and the software itself (`CITATION.cff` in the repository; the archived version on Zenodo).
|