Source code for slugpy.run_sim

"""
run_sim.py

Implements run_sim, a convenience function that runs an entire slug
simulation end to end from Python, exactly as the slug command-line
executable does.

:copyright: Copyright (c) 2026 Mark Krumholz
"""

import gc
import pathlib
import warnings

from ._slug import (
    OutputManagerAscii,
    OutputManagerH5,
    SimCluster,
    SimControls,
    SimGalaxy,
)
from .progress import run_with_progress
from .read import read
from .slug_reader import slug_reader


def _run(sim: SimCluster | SimGalaxy, sim_controls: SimControls, progress: bool) -> int:
    """
    Run sim (a SimCluster or SimGalaxy), optionally tracking
    sim.trialsCompleted() against sim_controls.nTrial() via a progress
    bar.

    Returns
    -------
    int
        Whatever sim.run() itself returned: 0 if every trial completed
        normally, or its own sigtermExitCode (143) if a SIGTERM was
        caught and it stopped early instead (see SimCluster.run/
        SimGalaxy.run's own docstrings).
    """
    if progress:
        return run_with_progress(sim.run, sim.trialsCompleted, sim_controls.nTrial, "Running trials")
    return sim.run()


[docs] def run_sim(controls: SimControls | str, progress: bool = True, restart: bool = False) -> slug_reader | None: """ Run a slug simulation end to end, exactly as the slug command-line executable does. Parameters ---------- controls : SimControls or str Either an already-built SimControls object, used directly, or the text of a slug TOML input deck, or a path to one on disk (see SimControls's own constructor for the exact string- parsing rule), used to construct one. Passing a SimControls object directly lets a simulation be driven entirely from Python -- via SimControls's own constructor keyword arguments and setters -- without ever writing an input deck to text. progress : bool, default True Whether to show a progress bar (see slugpy.progress) tracking trials completed while the simulation runs. restart : bool, default False Whether this run should resume a previous, interrupted run of the same model_name/out_dir from its most recent checkpoint, rather than starting a new one from trial 0 (see OutputManagerH5's own docstring for the full detail, and SimCluster.run/SimGalaxy.run's own docstrings for how the starting trial is chosen). Only valid with HDF5 output and a non-zero output.checkpoint_interval in controls -- passing True with ASCII output raises RuntimeError, mirroring the slug command-line executable's own --restart/-R check. Returns ------- slug_reader or None A lazy reader for the HDF5 output file this simulation produced, or None if this simulation's own output mode is ASCII rather than HDF5 -- ASCII output cannot currently be read back this way (a UserWarning is issued in that case; see Details below). Details ------- Mirrors main.cpp's own control flow: obtains a SimControls (either controls itself, or one built from it), an OutputManagerH5 or OutputManagerAscii from it (matching sim_controls.outputMode()), and -- depending on sim_controls.simType() -- a SimCluster or SimGalaxy, which it then runs. The progress bar (when enabled) is driven by run_with_progress: run() itself executes on a background thread, while this calling thread polls the running SimCluster/SimGalaxy's own trialsCompleted() against nTrial() every 0.2s (see run_with_progress's own docstring for why the polling has to happen on this thread rather than a separate watcher thread). This is safe because run() releases the GIL for its own entire duration (see SimCluster.run/SimGalaxy.run's own pybind docstrings), so polling this thread is never blocked by it, and the OpenMP worker threads run() uses internally never touch Python or the GIL at all. Note that when restart is True, trialsCompleted() only counts trials run by this call, not ones a previous, resumed run already completed -- so the progress bar's own denominator (nTrial()) can look larger than what this call alone will actually run. ASCII output is meant for small, human-readable output rather than batch processing, so there is no ASCII counterpart to slug_reader (yet -- this may change in the future). A deck requesting it still runs to completion, and its output files are still written, but this function has nothing to return for them, so it warns and returns None rather than trying to read back a file that was never written. """ if isinstance(controls, SimControls): sim_controls = controls else: sim_controls = SimControls(controls) if restart and sim_controls.outputMode == SimControls.OutputMode.ascii: raise RuntimeError( "run_sim: restart is True, but sim_controls.outputMode() is " "ascii -- restarting is only supported with HDF5 output") if sim_controls.outputMode in (SimControls.OutputMode.h5, SimControls.OutputMode.h5divided): output_manager = OutputManagerH5(sim_controls, restart) else: output_manager = OutputManagerAscii(sim_controls) if sim_controls.simType() == SimControls.SimType.cluster: sim = SimCluster(sim_controls, output_manager, restart) exit_code = _run(sim, sim_controls, progress) del sim elif sim_controls.simType() == SimControls.SimType.galaxy: sim = SimGalaxy(sim_controls, output_manager, restart) exit_code = _run(sim, sim_controls, progress) del sim else: # Unreachable for a SimControls built from a real input deck -- # sim_type is a required key, so simType() can only ever be # SimType.none (a dummy, pre-parse default) here if SimControls # was somehow constructed without going through that parsing at # all. Raised explicitly rather than left for exit_code to be # silently unbound below. raise RuntimeError( f"run_sim: sim_controls.simType() is {sim_controls.simType()!r}, " "which is neither cluster nor galaxy") # exit_code is sim.run()'s own return value: 0 if every trial # completed normally, or its sigtermExitCode (143) if a SIGTERM was # caught partway through (see SimCluster.run/SimGalaxy.run's own # docstrings) -- not exposed to Python as a named constant, so # compared against 0 directly rather than by name if exit_code != 0: warnings.warn( f"run_sim: the simulation was interrupted (exit code {exit_code}) " "before every trial completed, most likely by a caught SIGTERM -- " "the output written so far is still valid, and this run can be " "resumed later by calling run_sim again with restart=True.", stacklevel=2) # Drop the only Python-side reference to output_manager (whether or # not it was actually consumed by a SimCluster/SimGalaxy above) so # its destructor -- which closes the underlying output file(s) -- # runs now, before the read() below tries to reopen the same file # for reading; HDF5 otherwise refuses a second open while the # first (write) handle is still live. del output_manager gc.collect() if sim_controls.outputMode == SimControls.OutputMode.ascii: warnings.warn( "run_sim: ASCII output cannot be returned as a slug_reader; " "returning None. Use HDF5 output (the default) to get a " "slug_reader back.", stacklevel=2) return None # An extension-less model-name path, not a hardcoded modelName + ".h5" # -- slug_reader's own _find_output_files() resolves this to # whichever shape the run's output actually ended up in (a single # consolidated file, per-checkpoint files, or h5divided's own # per-thread files), which a hardcoded ".h5" path would get wrong # whenever checkpointing was enabled. output_path = pathlib.Path(sim_controls.outDir) / sim_controls.modelName return read(str(output_path))