• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

California-Planet-Search / radvel / 25467290701

06 May 2026 11:39PM UTC coverage: 90.15% (-0.07%) from 90.224%
25467290701

Pull #427

github

web-flow
UI: taller plots, full plot/table picker, persistent run.log, run- prefix (#434)

Bundle of small follow-ups against the run page based on user feedback:

- Plots panel now scales to ~85vh for the RV multipanel (the tallest
  plot) and ~70vh for the corner, so the embed never inner-scrolls.
  RV gets its own row; corner / autocorr / trend / derived lay out
  2-up as before.
- Plots and Tables steps now open a type-picker modal listing every
  type the API supports (plots: rv, corner, auto, trend, derived;
  tables: params, priors, rv, ic_compare, derived, crit). Types whose
  prerequisite isn't met (e.g. "corner needs MCMC/NS") are shown
  greyed out with the reason inline.
- IC compare and Tables steps were stuck in 'ready' even after
  running because the [ic_compare] / [table] sections don't write
  ``run = True``. Detect by section presence instead.
- Report step now writes a [report] section to .stat on success so
  the step flips to '✓ done'. Failures are translated from the
  driver's ``shutil.copy(<pdfname>, current)`` FileNotFoundError to
  a clearer ``report_pdf_missing`` message that points at the
  ``<run_id>_results.tex`` source for debugging.
- ``_capture`` now appends every step's stdout/stderr to
  ``<rundir>/run.log`` with a step header. The UI's terminal panel
  refetches that file on every render so the log survives page
  reloads (was previously in-memory only).
- New runs use the ``run-`` prefix instead of ``r-``. The validator
  still accepts ``r-`` for backward compatibility so existing run
  directories keep working.

Tests updated for the new prefix; default suite + 29 API tests pass.
Pull Request #427: Release v1.6.0: HTTP API + Docker service

84 of 97 new or added lines in 3 files covered. (86.6%)

314 existing lines in 14 files now uncovered.

4320 of 4792 relevant lines covered (90.15%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

36.67
/radvel/nested_sampling.py
1
import os
1✔
2
import shutil
1✔
3
from typing import Optional
1✔
4

5
import h5py
1✔
6
import numpy as np
1✔
7
from pandas import DataFrame, read_hdf
1✔
8

9

10
from radvel.posterior import Posterior
1✔
11

12

13
def _run_dynesty(
1✔
14
    post: Posterior,
15
    output_dir: Optional[str] = None,
16
    sampler_type: str = "static",
17
    proceed: bool = False,
18
    sampler_kwargs: Optional[dict] = None,
19
    run_kwargs: Optional[dict] = None,
20
) -> dict:
21
    """Run nested sampling with `Dynesty <https://dynesty.readthedocs.io/>`_
22

23
    Args:
24
        post: radvel posterior object
25
        output_dir: Output directory where the sampler checkpoints and results will be stored. Nothing is stored by default.
26
            **Note**: This replaces the sampler's built-in "checkpoint_file" argument. A ``dynesty.save`` file is created automatically.
27
            When sampling is finished, the final state of the sampler is stored.
28
        proceed: Continue from previous run in output_dir if available.
29
        sampler_kwargs: Dictionary of keyword arguments passed to the 'sampler' object from the underlying nested sampling package at initialization.
30
            See each package's documentation to learn more on the available arguments. This is not available for ``sampler='multinest'``.
31
            Defaults to ``None``.
32
        run_kwargs: Dictionary of keyword arguments passed to the 'run' methods from the underlying nested sampling package.
33
            See each package's documentation to learn more on the available aruments.
34
    Returns:
35
        Dictionary of results with the following keys:
36
            - ``samples``: Samples array with shape ``(nsamples, nparams)``
37
            - ``lnZ``: Log of the Bayesian evidence
38
            - ``lnZ``: Statistical uncertainty on the evidence
39
            - ``sampler``: Sampler object used by the nested sampling library. Provides more fine-grained access to the results.
40

41
    """
UNCOV
42
    from dynesty import DynamicNestedSampler, NestedSampler
×
43

UNCOV
44
    run_kwargs = run_kwargs or {}
×
45
    sampler_kwargs = sampler_kwargs or {}
×
46

47
    if sampler_type == "static":
×
48
        sampler_class = NestedSampler
×
UNCOV
49
    elif sampler_type == "dynamic":
×
50
        sampler_class = DynamicNestedSampler
×
51
    else:
52
        raise ValueError(
×
53
            f"Expected 'dynamic' or 'static' as sampler_type. Got {sampler_type}"
54
        )
55

UNCOV
56
    if "resume" in run_kwargs:
×
UNCOV
57
        raise ValueError("'resume' not supported for dynesty. Use radlvel's 'proceed' instead'")
×
UNCOV
58
    run_kwargs["resume"] = proceed
×
59

60
    if "checkpoint_file" in run_kwargs:
×
61
        raise ValueError(
×
62
            "checkpoint_file not supported for dynesty. Use radvel's output_dir instead."
63
        )
64
    if output_dir is not None:
×
UNCOV
65
        checkpoint_file = os.path.join(output_dir, "sampler.save")
×
UNCOV
66
        run_kwargs["checkpoint_file"] = checkpoint_file
×
67
    checkpoint_file = run_kwargs.get("checkpoint_file", None)
×
68

69
    if proceed and checkpoint_file is not None and os.path.exists(checkpoint_file):
×
70
        sampler = sampler_class.restore(checkpoint_file)
×
71
    else:
72
        sampler = sampler_class(
×
73
            post.likelihood_ns_array,
74
            post.prior_transform,
75
            len(post.name_vary_params()),
76
            **sampler_kwargs,
77
        )
78
        # Dynesty cannot resume when the file does not exist
UNCOV
79
        if (
×
80
            proceed
81
            and checkpoint_file is not None
82
            and not os.path.exists(checkpoint_file)
83
        ):
UNCOV
84
            run_kwargs["resume"] = False
×
85

UNCOV
86
    if checkpoint_file is not None and not os.path.exists(checkpoint_file):
×
87
        outdir = os.path.dirname(checkpoint_file)
×
UNCOV
88
        os.makedirs(outdir)
×
89

90
    sampler.run_nested(**run_kwargs)
×
91

UNCOV
92
    if checkpoint_file is not None:
×
93
        sampler.save(checkpoint_file)
×
94

95
    results = {
×
96
        "samples": sampler.results.samples_equal(),
97
        "lnZ": sampler.results["logz"][-1],
98
        "lnZerr": sampler.results["logzerr"][-1],
99
        "sampler": sampler,
100
    }
101

UNCOV
102
    return results
×
103

104

105
def _run_ultranest(
1✔
106
    post: Posterior,
107
    output_dir: Optional[str] = None,
108
    proceed: bool = False,
109
    sampler_kwargs: Optional[dict] = None,
110
    run_kwargs: Optional[dict] = None,
111
) -> dict:
112
    """Run nested sampling with `Ultranest <https://johannesbuchner.github.io/UltraNest/ultranest.html#ultranest.integrator.ReactiveNestedSampler>`_
113

114
    The SliceSampler will be used automatically for more than 7 parameters.
115
    See `the Ultranest docs <https://johannesbuchner.github.io/UltraNest/example-sine-highd.html#Step-samplers-in-UltraNest>`_ for more information
116

117
    Parameters starting with ``tc`` or ``tp`` are assumed to by time of conjunction or time of periastron and are marked as ``wrapped_params`` automatically.
118

119
    Args:
120
        post: radvel posterior object
121
        output_dir: Output directory where the sampler checkpoints and results will be stored. Nothing is stored by default.
122
            **Note**: This replaces the sampler's built-in "log_dir" argument.
123
            The ultranest ``log_dir`` is automatically set to ``output_dir``.
124
        proceed: Continue from previous run in output_dir if available.
125
        sampler_kwargs: Dictionary of keyword arguments passed to the 'sampler' object from the underlying nested sampling package at initialization.
126
            See each package's documentation to learn more on the available arguments.
127
            Defaults to ``None``.
128
        run_kwargs: Dictionary of keyword arguments passed to the 'run' methods from the underlying nested sampling package.
129
            See each package's documentation to learn more on the available aruments.
130
    Returns:
131
        Dictionary of results with the following keys:
132
            - ``samples``: Samples array with shape ``(nsamples, nparams)``
133
            - ``lnZ``: Log of the Bayesian evidence
134
            - ``lnZ``: Statistical uncertainty on the evidence
135
            - ``sampler``: Sampler object used by the nested sampling library. Provides more fine-grained access to the results.
136
    """
137
    from ultranest import ReactiveNestedSampler
1✔
138
    from ultranest.stepsampler import SliceSampler, generate_mixture_random_direction
1✔
139

140
    run_kwargs = run_kwargs or {}
1✔
141
    sampler_kwargs = sampler_kwargs or {}
1✔
142

143
    if "log_dir" in sampler_kwargs:
1✔
UNCOV
144
        raise ValueError(
×
145
            "log_dir not supported for ultranest. Use radvel's output_dir instead."
146
        )
147
    if "resume" in sampler_kwargs:
1✔
148
        raise ValueError(
1✔
149
            "'resume' not supported for ultranest. Use radvel's 'proceed' instead."
150
        )
151
    sampler_kwargs["resume"] = proceed or 'overwrite'
1✔
152
    sampler_kwargs["log_dir"] = output_dir
1✔
153

154
    param_names = post.name_vary_params()
1✔
155
    wrapped_params = [pn.startswith(("tc", "tp")) for pn in param_names]
1✔
156
    # I guess simplest is to use overwrite or re-run
157
    sampler = ReactiveNestedSampler(
1✔
158
        param_names,
159
        post.likelihood_ns_array,
160
        post.prior_transform,
161
        wrapped_params=wrapped_params,
162
        **sampler_kwargs,
163
    )
164

165
    num_params = len(param_names)
1✔
166
    if num_params > 7:
1✔
UNCOV
167
        nsteps = len(param_names) * 2
×
UNCOV
168
        sampler.stepsampler = SliceSampler(
×
169
            nsteps=nsteps, generate_direction=generate_mixture_random_direction,
170
        )
171

172
    sampler.run(**run_kwargs)
1✔
173

174
    results = {
1✔
175
        "samples": sampler.results["samples"],
176
        "lnZ": sampler.results["logz"],
177
        "lnZerr": sampler.results["logzerr"],
178
        "sampler": sampler,
179
    }
180

181
    return results
1✔
182

183

184
def _run_multinest(
1✔
185
    post: Posterior,
186
    output_dir: Optional[str] = None,
187
    overwrite: bool = False,
188
    proceed: bool = False,
189
    run_kwargs: Optional[dict] = None,
190
) -> dict:
191
    """Run nested sampling with `PyMultiNest <https://johannesbuchner.github.io/PyMultiNest/pymultinest.html#>`_
192

193
    Args:
194
        post: radvel posterior object
195
        output_dir: Output directory where the sampler checkpoints and results will be stored. Nothing is stored by default.
196
            **Note**: This replaces the sampler's built-in "outputfiles_basename" argument.
197
            If ``output_dir`` is specified, sets ``outputfiles_basename`` to ``<output_dir>/out``
198
        proceed: Continue from previous run in output_dir if available.
199
        overwrite: Overwrite the output files if they exist. Defaults to ``False``.
200
        run_kwargs: Dictionary of keyword arguments passed to the 'run' methods from the underlying nested sampling package.
201
            See each package's documentation to learn more on the available aruments.
202
    Returns:
203
        Dictionary of results with the following keys:
204
            - ``samples``: Samples array with shape ``(nsamples, nparams)``
205
            - ``lnZ``: Log of the Bayesian evidence
206
            - ``lnZ``: Statistical uncertainty on the evidence
207
    """
UNCOV
208
    import pymultinest
×
209

UNCOV
210
    run_kwargs = run_kwargs or {}
×
211

212
    # By default, assume we want a temporary output dir
213
    tmp = True
×
214

UNCOV
215
    if output_dir is None:
×
216
        output_dir = "tmpdir"
×
217
    else:
218
        # if an actual outupt dir was specified, it is not temporary
219
        tmp = False
×
220

UNCOV
221
    if "outputfiles_basename" in run_kwargs:
×
222
        raise ValueError(
×
223
            "outputfiles_basename not supported for multinest. Use radvel's output_dir instead."
224
        )
225
    run_kwargs["outputfiles_basename"] = os.path.join(output_dir, "out")
×
226

UNCOV
227
    if "resume" in run_kwargs:
×
228
        raise ValueError(
×
229
            "'resume' not supported for multinest. Use radvel's 'proceed' instead."
230
        )
231
    run_kwargs["resume"] = proceed
×
232

UNCOV
233
    os.makedirs(output_dir, exist_ok=tmp or overwrite or proceed)
×
234

UNCOV
235
    def loglike(p, ndim, nparams):
×
236
        """Log-likelihood for multinest
237
        Must support ndim and nparams arguments
238
        and create a list-copy of the object to avoid segfault.
239
        """
240
        # This is required to avoid segfault
241
        # See here: https://github.com/JohannesBuchner/PyMultiNest/issues/41, which I semi-understand
UNCOV
242
        p = [p[i] for i in range(ndim)]
×
UNCOV
243
        return post.likelihood_ns_array(p)
×
244

245
    def prior_transform(u, ndim, nparams):
×
246
        """Prior transform for multinest
247

248
        Multinest requires the prior transform to handle ndim, nparams arguments
249
        and to modify the array in-place
250
        """
UNCOV
251
        post.prior_transform(u, inplace=True)
×
252

UNCOV
253
    ndim = len(post.name_vary_params())
×
254

UNCOV
255
    pymultinest.run(loglike, prior_transform, ndim, **run_kwargs)
×
256

UNCOV
257
    a = pymultinest.Analyzer(
×
258
        outputfiles_basename=run_kwargs["outputfiles_basename"], n_params=ndim
259
    )
260

UNCOV
261
    results = {}
×
UNCOV
262
    results["samples"] = a.get_equal_weighted_posterior()[:, :-1]
×
UNCOV
263
    results["lnZ"] = a.get_stats()["global evidence"]
×
264
    results["lnZerr"] = a.get_stats()["global evidence error"]
×
265

266
    if tmp:
×
267
        shutil.rmtree(output_dir)
×
268

269
    return results
×
270

271

272
def _run_nautilus(
1✔
273
    post: Posterior,
274
    output_dir: Optional[str] = None,
275
    proceed: bool = False,
276
    sampler_kwargs: Optional[dict] = None,
277
    run_kwargs: Optional[dict] = None,
278
) -> dict:
279
    """Run nested sampling with `Nautilus <https://nautilus-sampler.readthedocs.io/en/latest/api_high.html>`_
280

281
    Args:
282
        post: radvel posterior object
283
        output_dir: Output directory where the sampler checkpoints and results will be stored. Nothing is stored by default.
284
            **Note**: This replaces the sampler's built-in "filepath argument.
285
            The nautilus output is automatically stored in ``nautilus_output.hdf5`` under that location.
286
        proceed: Continue from previous run in output_dir if available.
287
        sampler_kwargs: Dictionary of keyword arguments passed to the 'sampler' object from the underlying nested sampling package at initialization.
288
            See each package's documentation to learn more on the available arguments.
289
            Defaults to ``None``.
290
        run_kwargs: Dictionary of keyword arguments passed to the 'run' methods from the underlying nested sampling package.
291
            See each package's documentation to learn more on the available aruments.
292
    Returns:
293
        Dictionary of results with the following keys:
294
            - ``samples``: Samples array with shape ``(nsamples, nparams)``
295
            - ``lnZ``: Log of the Bayesian evidence
296
            - ``lnZ``: Statistical uncertainty on the evidence
297
            - ``sampler``: Sampler object used by the nested sampling library. Provides more fine-grained access to the results.
298
    """
UNCOV
299
    from nautilus import Sampler
×
300

UNCOV
301
    sampler_kwargs = sampler_kwargs or {}
×
302
    run_kwargs = run_kwargs or {}
×
UNCOV
303
    run_kwargs.setdefault("verbose", True)
×
304

305
    if "filepath" in sampler_kwargs:
×
306
        raise ValueError(
×
307
            "filepath not supported for nautilus. Use radvel's output_dir instead."
308
        )
309
    if output_dir is not None:
×
UNCOV
310
        sampler_kwargs["filepath"] = os.path.join(output_dir, "nautilus_output.hdf5")
×
UNCOV
311
    if "resume" in sampler_kwargs:
×
312
        raise ValueError(
×
313
            "'resume' not supported for ultranest. Use radvel's 'proceed' instead."
314
        )
315
    sampler_kwargs["resume"] = proceed
×
316

UNCOV
317
    ndim = len(post.name_vary_params())
×
318
    sampler = Sampler(
×
319
        post.prior_transform, post.likelihood_ns_array, n_dim=ndim, **sampler_kwargs
320
    )
321
    sampler.run(**run_kwargs)
×
UNCOV
322
    results = {
×
323
        "samples": sampler.posterior(equal_weight=True)[0],
324
        "lnZ": sampler.log_z,
325
        "lnZerr": sampler.n_eff**-0.5,
326
        "sampler": sampler,
327
    }
UNCOV
328
    return results
×
329

330

331
BACKENDS = {
1✔
332
    "dynesty-static": _run_dynesty,
333
    "dynesty-dynamic": _run_dynesty,
334
    "multinest": _run_multinest,
335
    "ultranest": _run_ultranest,
336
    "nautilus": _run_nautilus,
337
}
338

339

340
def run(
1✔
341
    post: Posterior,
342
    output_dir: Optional[str] = None,
343
    overwrite: bool = False,
344
    proceed: bool = False,
345
    sampler: str = "ultranest",
346
    sampler_kwargs: Optional[dict] = None,
347
    run_kwargs: Optional[dict] = None,
348
) -> dict:
349
    """Run nested sampling
350

351
    Args:
352
        post: radvel posterior object
353
        output_dir: Output directory where the sampler checkpoints and results will be stored.
354
            Nothing is stored by default.
355
            **Note**: This replaces the sampler's built-in "checkpoint_file", "log_dir", or "outputfiles_basename" argument.
356
            Once you specify output there, everything is saved there automatically.
357
            A ``results.hdf5`` file will also be saved with the results dict, except for the sampler.
358
        overwrite: Overwrite the results.hdf5 if True. Will be enabled automatically when proceed=True.
359
        proceed: Resume from a previous run in the same output_dir if available. Also automatically enables overwrite.
360
        sampler: name of the sampler to use. Should be one of 'ultranest', 'dynesty-static', 'dynesty-dynamic', 'nautilus', or 'multinest'.
361
            Defaults to 'ultranest'.
362
        sampler_kwargs: Dictionary of keyword arguments passed to the 'sampler' object from the underlying nested sampling package at initialization.
363
            See each package's documentation to learn more on the available arguments. This is not available for ``sampler='multinest'``.
364
            Defaults to ``None``.
365
        run_kwargs: Dictionary of keyword arguments passed to the 'run' methods from the underlying nested sampling package.
366
            See each package's documentation to learn more on the available aruments.
367
    Returns:
368
        Dictionary of results with the keys below.
369

370
        - ``samples``: Samples dataframe with one column per parameters and lnprobability (log-posterior) for each set.
371
                       The samples are equally weighted, meaning they are equivalent to MCMC samples.
372
        - ``lnZ``: Log of the Bayesian evidence
373
        - ``lnZ``: Statistical uncertainty on the evidence
374
        - ``sampler``: Sampler object used by the nested sampling library. Provides more fine-grained access to the results.
375

376
    Link to each package's API documentation:
377

378
    - `Ultranest <https://johannesbuchner.github.io/UltraNest/ultranest.html#ultranest.integrator.ReactiveNestedSampler>`_
379
    - `Nautilus <https://nautilus-sampler.readthedocs.io/en/latest/api_high.html>`_
380
    - `Dynesty <https://dynesty.readthedocs.io>`_
381
    - `PyMultiNest <https://johannesbuchner.github.io/PyMultiNest/pymultinest.html#>`_
382
    """
383
    post.check_proper_priors()
1✔
384

385
    # Proceed automatically enables overwrite.
386
    overwrite = overwrite or proceed
1✔
387

388
    if output_dir is not None:
1✔
389
        results_file = os.path.join(output_dir, "results.hdf5")
1✔
390
        if os.path.exists(results_file) and not overwrite:
1✔
391
            raise FileExistsError(
1✔
392
                f"Results file {results_file} exists and 'overwrite' is False."
393
            )
394

395
    sampler = sampler.lower()
1✔
396
    if sampler == "pymultinest":
1✔
UNCOV
397
        sampler = "multinest"
×
398

399
    # fmt: off
400
    if sampler == "ultranest":
1✔
401
        results = _run_ultranest(post, output_dir=output_dir, proceed=proceed, sampler_kwargs=sampler_kwargs, run_kwargs=run_kwargs)
1✔
UNCOV
402
    elif sampler == "dynesty-static":
×
UNCOV
403
        results = _run_dynesty(post, sampler_type="static", proceed=proceed, output_dir=output_dir, sampler_kwargs=sampler_kwargs, run_kwargs=run_kwargs)
×
UNCOV
404
    elif sampler == "dynesty-dynamic":
×
405
        results = _run_dynesty(post, sampler_type="dynamic", proceed=proceed, output_dir=output_dir, sampler_kwargs=sampler_kwargs, run_kwargs=run_kwargs)
×
406
    elif sampler == "multinest":
×
407
        if sampler_kwargs is not None and len(sampler_kwargs) > 0:
×
408
            raise TypeError("Argument sampler_kwargs is invalid for sampler 'multinest', only run_kwargs is supported")
×
409
        results = _run_multinest(post, output_dir=output_dir, proceed=proceed, overwrite=overwrite, run_kwargs=run_kwargs)
×
410
    elif sampler == "nautilus":
×
411
        results = _run_nautilus(post, output_dir=output_dir, proceed=proceed, sampler_kwargs=sampler_kwargs, run_kwargs=run_kwargs)
×
412
    else:
413
        raise ValueError(f"Unknown sampler '{sampler}'. Available options are {list(BACKENDS.keys())}")
×
414
    # fmt: on
415
    
416
    df = DataFrame(results["samples"], columns=post.name_vary_params())
1✔
417
    lnprob_arr = np.empty(len(df))
1✔
418
    for i, row in df.iterrows():
1✔
419
        lnprob_arr[i] = post.logprob_array(row.values)
1✔
420
    df["lnprobability"] = lnprob_arr
1✔
421

422

423
    results["samples"] = df
1✔
424

425
    if output_dir is not None:
1✔
426
        with h5py.File(results_file, mode="w") as h5f:
1✔
427
            for key, val in results.items():
1✔
428
                if key == "sampler" or key == "samples":
1✔
429
                    continue
1✔
430
                h5f.create_dataset(key, data=val)
1✔
431
        results["samples"].to_hdf(results_file, key="samples", mode="a")
1✔
432

433
    return results
1✔
434

435

436
def load_results(results_file: str) -> dict:
1✔
437
    """Load nested sampling results dictionary
438

439
    Args:
440
        results_file: Path to hdf5 file containing the results.
441
    Returns:
442
        Dictionary with nested sampling results.
443
        Note that the ``sampler`` key is not saved, so it is not in the dictionary returned by this function.
444
    """
UNCOV
445
    results = {}
×
UNCOV
446
    with h5py.File(results_file) as h5f:
×
UNCOV
447
        results["lnZ"] = np.array(h5f["lnZ"]).item()
×
448
        results["lnZerr"] = np.array(h5f["lnZerr"]).item()
×
449
    results["samples"] = read_hdf(results_file, key="samples")
×
450
    return results
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc