Python samplers

Gravity can use sampling algorithms implemented in Python while keeping the model, prior transform, and likelihood calculation in Julia. Python support is provided by PythonCall.jl.

The implemented backends are UltraNest, Nautilus, and dynesty.

Installation

PythonCall is a dependency of Gravity, but Python sampling packages are optional. To install UltraNest into the Python environment managed by PythonCall, run from the same Julia project used for Gravity:

julia> import CondaPkg

julia> CondaPkg.add("ultranest")

julia> CondaPkg.add_pip("nautilus-sampler"; extras=["checkpointing"])

julia> CondaPkg.add_pip("dynesty")

Alternatively, create a CondaPkg.toml beside the active Project.toml:

[deps]
ultranest = ""

[pip.deps]
dynesty = ""

[pip.deps.nautilus-sampler]
extras = ["checkpointing"]

The Python environment is resolved the next time PythonCall is loaded. Verify the installation with:

julia> import PythonCall

julia> PythonCall.pyimport("ultranest").__version__
Python: '...'

julia> PythonCall.pyimport("nautilus").__version__
Python: '...'

julia> PythonCall.pyimport("dynesty").__version__
Python: '...'

When a generated model selects Nautilus or dynesty, Gravity checks its PythonCall environment and automatically installs a missing distribution with CondaPkg. If Nautilus has a non-null filepath, Gravity also installs its checkpointing extra, which provides h5py. UltraNest remains manually installed. The commands above are also useful for preparing an environment before a run or diagnosing installation issues.

YAML configuration

Select the backend under sampling.algorithm:

sampling:
    scheme: sourceplane
    algorithm: ultranest
    algorithm-options:
        points: 400
        dlogz: 0.5
        min-ess: 400
        max-ncalls: 100000
        log-dir: results/ultranest
        resume: resume
        ndraw-min: 128
        ndraw-max: 4096
        show-visualization: false
        include-logdensity: true
    threads: auto
    random-seed: 741

For Nautilus:

sampling:
    scheme: sourceplane
    algorithm: nautilus
    algorithm-options:
        points: 400
        min-ess: 1000
        max-ncalls: 100000
        n-batch: 200
        n-networks: 4
        filepath: results/nautilus.hdf5
        resume: true
        equal-weight-boost: 2.0
        include-logdensity: true
    threads: auto
    random-seed: 741

For dynesty:

sampling:
    scheme: sourceplane
    algorithm: dynesty
    algorithm-options:
        dynamic: true
        points: 500
        bounds: multi
        proposal: rwalk
        dlogz: 0.1
        min-ess: 1000
        max-ncalls: 100000
        show-status: true
        include-logdensity: true
    threads: auto
    random-seed: 741

Then use the standard Gravity workflow:

using Gravity

model = gravitymodel("model.yml")
chain = runmodel(model)

The generated prior transform is quantile(model.prior, u). All backends sample the generated model.loglikelihood(θ, model.options). This distinction is important: applying the prior transform and then sampling logposterior would count the prior twice and produce an incorrect evidence.

Options

Nested samplers maintain a set of live points and progressively replace the point with the lowest likelihood. The options fall into four groups:

  • Exploration quality (points, bounds, and proposal controls) determines how reliably the sampler finds and follows separate or narrow modes.
  • Convergence targets (dlogz, min-ess, and f-live) determine when a normally progressing run is considered complete.
  • Safety limits (max-ncalls, max-iters, and timeout) stop a run even if its convergence targets have not been met.
  • Throughput and persistence (batch, display, and checkpoint options) affect speed and recoverability, not the statistical target.

Increasing points is usually the first response to missed modes or unstable evidence. Tightening dlogz primarily asks for a more complete evidence integral, while increasing min-ess primarily asks for more information in the posterior sample. These changes all increase the number of likelihood evaluations, sometimes substantially.

max-ncalls and max-iters are therefore best treated as emergency budgets, not convergence settings. If a run reaches one of them, inspect the backend status and diagnostics before treating its evidence or posterior as final. The backends count work differently: a likelihood-call limit is the most comparable budget across them.

UltraNest options

UltraNest uses a reactive strategy: points is a minimum rather than necessarily the final live-point count, and the run continues until all requested convergence criteria are satisfied.

YAML optionPython argumentMeaning and use
pointsmin_num_live_pointsMinimum live-point population. More points improve resolution of modes and evidence reliability, at greater cost.
dlogzdlogzStop when the estimated remaining evidence is sufficiently small. Smaller values are stricter; 0.5 is a useful exploratory value, while evidence-sensitive work should test convergence with smaller values.
min-essmin_essMinimum effective sample size (ESS) of the weighted posterior. Increase it when posterior summaries are noisy even though the evidence criterion is satisfied.
max-ncallsmax_ncallsHard ceiling on likelihood evaluations. Reaching it does not imply convergence.
max-itersmax_itersHard ceiling on nested-sampling iterations. Prefer max-ncalls when budgeting expensive likelihoods.
ndraw-minndraw_minSmallest vectorized proposal batch. Larger batches reduce Julia–Python overhead and expose more Julia threading, but can evaluate more rejected points and use more memory.
ndraw-maxndraw_maxLargest batch UltraNest may use when draw-multiple is enabled. It must be at least ndraw-min; avoid very large values unless batched likelihood evaluation is demonstrably cheap.
draw-multipledraw_multipleAllow UltraNest to enlarge proposal batches when sampling efficiency falls. Usually keep this enabled for Gravity's vectorized likelihood.
num-test-samplesnum_test_samplesNumber of startup calls used to catch invalid prior transforms, shapes, or likelihood values. This is a correctness check, not an accuracy control.
num-bootstrapsnum_bootstrapsNumber of bootstrap evidence estimators and region-construction rounds. More can make uncertainty/region estimates more robust, but increases region-update work.
log-dirlog_dirDirectory for checkpoints, results, and diagnostic files. Use a distinct directory for each materially different model.
resumeresumeoverwrite starts fresh in log-dir; subfolder creates a new numbered run; resume continues an identical likelihood/prior; resume-similar may reuse points after a changed likelihood.
storage-backendstorage_backendUltraNest point-store format. The upstream-recommended default is hdf5; change it mainly for interoperability or debugging.
warmstart-max-tauwarmstart_max_tauFor resume-similar, maximum tolerated change in live-point ordering. Values from 0 (conservative) to 1 (permissive); the default -1 disables this warm-start path.
show-statusJulia callback / show_statusShow progress. With visualization disabled, Gravity uses a compact Julia progress meter.
show-visualizationviz_callbackEnable UltraNest's multi-line live-point display. Useful for interactive diagnosis, noisy in logs.
include-logdensityEvaluate the generated log-posterior for the final equal-weight sample and store it as chain variable :lp. Disable to avoid this extra batched pass.

With show-status: true and show-visualization: false, Gravity disables UltraNest's Python printer and renders a Julia-native progress meter containing the evidence, remaining evidence, iteration count, and likelihood-call count. The meter is active only when Julia's stderr is a real TTY, so redirected output remains quiet. show-visualization opts back into UltraNest's verbose multi-line live-point diagrams. Set both options to false for a completely quiet sampler.

If top-level sampling.iterations is explicitly present, it is used as max_iters unless algorithm-options.max-iters is also specified.

For a first serious run, set points, dlogz, and min-ess, enable a checkpoint directory, and leave the region-construction controls at their UltraNest defaults. Tune ndraw-min and ndraw-max only after timing the batched likelihood.

Nautilus options

Nautilus first explores the likelihood and constructs neural-network bounds, then samples shells to reach its posterior ESS target. Options controlling bounds can affect correctness and efficiency; change them only when diagnostics or repeated runs indicate that the defaults are inadequate.

YAML optionPython argumentMeaning and use
pointsn_liveLive points used to construct bounds. More points help resolve complicated or separated modes but make exploration and bound fitting more expensive.
n-updaten_updateMaximum additions to the live set before constructing a new bound. Smaller values update bounds more often; larger values reduce fitting overhead but may use stale, inefficient bounds.
enlarge-per-dimenlarge_per_dimLinear enlargement of outer ellipsoids in each dimension. Larger values are safer but less efficient; values too close to one can exclude valid volume.
n-points-minn_points_minMinimum points assigned to an ellipsoid, limiting how finely a multi-ellipsoid bound can split. Smaller values permit finer clusters but make their estimates noisier.
split-thresholdsplit_thresholdVolume-ratio threshold for splitting a multi-ellipsoid bound. Lower values split more readily; use the default unless bound construction is known to be inefficient.
n-networksn_networksNeural networks combined in each Nautilus bound. More networks can stabilize the learned boundary at extra fitting cost.
n-batchn_batchLikelihood evaluations requested per batch. Larger values improve Julia threading and reduce callback overhead, but can overshoot stopping limits and consume more memory.
n-like-new-boundn_like_new_boundMaximum likelihood calls before attempting a new bound. Lower values refresh bounds more frequently.
f-livef_liveExploration stops after the estimated fraction of evidence still in the live set falls below this value. Smaller values explore farther and cost more. This is distinct from the later min-ess target.
n-shelln_shellMinimum retained points per shell. Increasing it improves shell representation but adds likelihood calls.
min-essn_effTarget posterior ESS. Raise it for more precise posterior expectations; it does not by itself make the initial exploration more reliable.
max-ncallsn_like_maxHard total likelihood-call ceiling, including calls restored from a checkpoint. A run stopped here reports chain.info.success == false.
timeouttimeoutWall-clock limit in seconds for the current run call. Like max-ncalls, it is a safety limit rather than convergence evidence.
discard-explorationdiscard_explorationDiscard exploration-phase points. Nautilus documents this as required for a fully unbiased posterior and evidence estimate, at the cost of throwing away useful samples.
filepathfilepath.h5/.hdf5 checkpoint file, or null for no checkpoint. Gravity installs Nautilus's checkpointing extra when this is non-null.
resumeresumeWith a checkpoint file, continue it when true; start from scratch and overwrite it when false. Do not resume after changing the model or prior.
equal-weight-boostequal_weight_boostControls conversion of the weighted posterior to the equal-weight chain. Values above 1 return more points and allow duplicates, approximating the weighted posterior more closely; they do not create new likelihood information.
show-statusverbosePrint Nautilus progress information.
include-logdensityStore a final generated log-posterior evaluation as chain variable :lp.

An explicit top-level sampling.iterations becomes n_like_max unless max-ncalls is set. random-seed initializes Nautilus directly.

equal-weight-boost only affects the returned MCMCChains.Chains, after the sampler has finished. Increasing it can reduce resampling granularity but cannot increase the ESS of the underlying weighted sample; use min-ess for that.

dynesty options

Dynamic nested sampling (the default) can allocate additional samples where they most improve the evidence or posterior. Static sampling keeps a fixed live-point population and has simpler, more predictable behavior. Dynamic sampling is normally preferable for posterior estimation; static sampling is useful when comparing against a conventional fixed-live-point run.

YAML optionPython argumentMeaning and use
dynamicsampler classtrue selects DynamicNestedSampler; false selects the static NestedSampler.
pointsnliveInitial live-point count (or fixed count for static sampling). Increase it for complex multimodal or strongly curved likelihoods.
boundsboundShape used to approximate the constrained prior. Gravity accepts its generic names (nobounds, ellipsoid, multiellipsoid) and dynesty names such as none, single, or multi; multi is a robust general choice.
proposalsampleMethod for drawing within the bound. Common choices are auto, unif/rejection, rwalk, slice, and rslice. Rejection is effective for tight, accurate bounds; random-walk or slice methods are often better in higher dimensions or difficult geometries but require tuning.
update-intervalupdate_intervalBound-update frequency. More frequent updates can improve proposal efficiency but spend more time rebuilding bounds. Leave automatic unless profiling shows a problem.
enlargeenlargeMultiplicative bound enlargement. Larger bounds are safer but reduce rejection efficiency; too-small bounds risk under-covering the constrained prior.
num-bootstrapsbootstrapBootstrap realizations used to estimate bound enlargement. More is more conservative and expensive; it interacts with enlarge.
walkswalksNumber of accepted random-walk steps before proposing a new live point. Applies to random-walk proposals; too few can leave correlated proposals, too many waste likelihood calls.
acceptance-fractionfaccTarget acceptance fraction used to tune random walks. Only relevant to proposal methods that use it.
slicesslicesNumber of slice updates per proposal. Larger values mix more thoroughly at higher cost.
cluster-dimensionsncdimNumber of leading dimensions used when clustering bounds. Useful when only part of a high-dimensional parameter space is strongly multimodal.
dlogzdlogz_init / dlogzEvidence-based stopping tolerance (dlogz_init for the initial dynamic run, dlogz for static). Smaller is stricter and more expensive.
min-essn_effectiveDynamic-only target ESS. Gravity ignores it for static sampling. Increase it for smoother posterior summaries.
max-ncallsmaxcallHard likelihood-call ceiling; reaching it need not mean convergence.
max-itersmaxiterHard iteration ceiling. Top-level sampling.iterations supplies this value unless max-iters is set.
checkpoint-filecheckpoint_fileFile used to save dynesty's sampler state. Use a new file after changing the model, prior, or relevant sampler setup.
checkpoint-everycheckpoint_everyApproximate seconds between checkpoints. Short intervals improve recoverability but add I/O overhead.
resumeresumeResume from checkpoint-file when possible. This is dynesty checkpoint resumption, not extension from a Gravity initial_state.
show-statusprint_progressDisplay dynesty's progress output.
include-logdensityStore a final generated log-posterior evaluation as chain variable :lp.

When tuning proposal, change one family-specific control at a time: walks and acceptance-fraction for random walks, or slices for slice sampling. A low acceptance rate alone does not prove that the posterior is wrong; compare evidence, posterior summaries, and repeated seeded runs before changing bound enlargement or clustering.

Parallel and vectorized evaluation

UltraNest and Nautilus configure the Python likelihood with vectorized=true. Python passes a matrix with shape (number_of_points, number_of_parameters) to Julia. Generated Gravity functions already support this convention:

values = model.loglikelihood(points, model.options)

Each matrix row is one parameter vector. Gravity evaluates these rows with its threaded tmap implementation. Larger ndraw-min and ndraw-max values reduce Julia–Python callback overhead and expose more parallel work, but also increase memory usage.

Start Julia with multiple threads to benefit from batched evaluation:

julia --project=. --threads=auto

The optimal batch size depends on the model. Benchmark model.loglikelihood(points, model.options) using representative batch sizes before a long run.

Dynesty does not provide a general vectorized-likelihood interface. Gravity batches direct pool calls used for live-point initialization and dynamic batches. Dynesty's internal proposal evolution uses the scalar Julia callback to avoid unsafe concurrent Python-to-Julia calls.

Result and evidence

runmodel returns an MCMCChains.Chains containing the backend's equally weighted posterior samples. The evidence is available as:

chain.logevidence

UltraNest diagnostics are stored in:

chain.info.logzerr
chain.info.niter
chain.info.ncall
chain.info.ess

For Nautilus, chain.info contains success, ess, ncall (when the installed version exposes it), log_weights, and log_likelihood. The latter two preserve Nautilus's values even though the chain itself is equal-weighted.

The native result remains accessible through chain.info.state:

result = chain.info.state
result.result["weighted_samples"]
result.sampler

With include-logdensity: true (the default), Gravity performs one final batched evaluation of model.logposterior and stores it as :lp. This makes bestlp(chain) and report generation work as for other Gravity samplers. Set the option to false if this final evaluation is too expensive.

Low-level interface

The bridge can also be used without a generated Gravity model:

problem = Gravity.PythonNestedSamplingProblem(
    loglikelihood,
    prior_transform,
    ndim;
    batch_logdensity,
    parameter_names=[:a, :b],
)

sampler = Gravity.UltraNestSampler(
    ndraw_min=128,
    run_kwargs=(min_num_live_points=400, dlogz=0.5),
    julia_progress=true,
)

result = Gravity.python_sample(sampler, problem)
chain = MCMCChains.Chains(result)

The corresponding Nautilus configuration is:

sampler = Gravity.NautilusSampler(
    n_live=400,
    n_batch=200,
    run_kwargs=(n_eff=1000, f_live=0.01),
    posterior_kwargs=(equal_weight_boost=2.0,),
)
result = Gravity.python_sample(sampler, problem)
chain = Gravity.nautilus_chains(result)

The corresponding dynesty configuration is:

sampler = Gravity.DynestySampler(
    nlive=500,
    bound="multi",
    sample="rwalk",
    run_kwargs=(dlogz_init=0.1, n_effective=1000),
)
result = Gravity.python_sample(sampler, problem)
chain = Gravity.dynesty_chains(result)

Density functions requiring static data can pass it without a closure:

problem = Gravity.PythonNestedSamplingProblem(
    loglikelihood,
    prior_transform,
    ndim;
    options,
    batch_logdensity=loglikelihood,
)

Troubleshooting

  • Automatic installation fails: check network and package-index access, or manually install ultranest or nautilus-sampler in the same project and PythonCall environment from which Gravity is run.
  • UltraNest was installed but cannot be imported: inspect PythonCall.python_executable_path() and ensure installation and execution use the same Julia project.
  • NameError: name 'h5py' is not defined: the installed Nautilus lacks its checkpointing extra. Run CondaPkg.add_pip("nautilus-sampler"; extras=["checkpointing"]); current Gravity versions do this automatically when filepath is non-null.
  • Sampling is not parallel: start Julia with --threads=auto, keep parameters.use_threads configured to include sampling, and use batch sizes larger than one.
  • Evidence differs unexpectedly: ensure the sampler receives loglikelihood, not logposterior, when using a prior transform.
  • A resumed run rejects the likelihood: the model, parameter order, prior, or static likelihood options changed. Use a new log-dir or resume: overwrite.