Skip to content

statusing

Step module-attribute

Python
Step = Literal['pending', 'generating', 'solving', 'done']

Steps a trial can have

TRIAL_STATUS_FNAME module-attribute

Python
TRIAL_STATUS_FNAME = 'trial_status.json'

Filename of trial status files.

JOB_STATUS_FNAME module-attribute

Python
JOB_STATUS_FNAME = 'job_status.json'

Filename of job status files.

REQUIREMENTS_FNAME module-attribute

Python
REQUIREMENTS_FNAME = 'requirements.json'

Filename of per-trial requirement result files.

JobType

Bases: StrEnum

Which mujoco-mojo run subcommand (or the reloaded viewer) produced a job.

MONTE_CARLO class-attribute instance-attribute

Python
MONTE_CARLO = 'monte_carlo'

A mujoco-mojo run monte_carlo job: many trials sampled from randomized parameters.

OPTIMIZE class-attribute instance-attribute

Python
OPTIMIZE = 'optimize'

A mujoco-mojo run optimize job: trials driven by a parameter search/optimizer.

RELOADED class-attribute instance-attribute

Python
RELOADED = 'reloaded'

A mujoco-mojo reloaded job: interactive live-reload viewer, not a batch of trials.

ExecutionMode

Bases: StrEnum

Where a job's trials actually run.

LOCAL class-attribute instance-attribute

Python
LOCAL = 'local'

Trials run as local processes/threads on the machine that launched the job.

SLURM class-attribute instance-attribute

Python
SLURM = 'slurm'

Trials are submitted as SLURM jobs to a cluster scheduler.

Completion

Bases: StrEnum

How a trial ended, from "still running" through the different ways it can finish.

INCOMPLETE class-attribute instance-attribute

Python
INCOMPLETE = 'incomplete'

Neither completed nor failed. Indicates the process is ongoing.

SUCCESS class-attribute instance-attribute

Python
SUCCESS = 'success'

Completed successfully; all requirements passed (or none were registered).

FAILURE class-attribute instance-attribute

Python
FAILURE = 'failure'

Completed without exceptions but one or more requirements failed.

TERMINATED class-attribute instance-attribute

Python
TERMINATED = 'terminated'

Stopped early via SimulationStopped (user request or terminating requirement). Counted as a requirement failure in job statistics, not an error.

ERROR class-attribute instance-attribute

Python
ERROR = 'error'

An unhandled exception was raised while processing the trial. Distinct from FAILURE: the run broke, it did not merely miss its requirements.

RequirementResult

Bases: MojoBaseModel

Result of a single requirement check evaluated at the end of a trial.

name instance-attribute

Python
name: str

Name of the requirement.

passed instance-attribute

Python
passed: bool

Whether the requirement was satisfied.

message instance-attribute

Python
message: str

Human-readable message from the requirement function.

decided_at class-attribute instance-attribute

Python
decided_at: float = 0.0

Simulation time at which the verdict was determined: the time of the first live failure, the time it latched, or the trial's final sim time for a plain end-of-trial verdict.

every class-attribute instance-attribute

Python
every: int | None = None

Live-check cadence the requirement was registered with (steps), or None if it only ran once at end-of-trial.

terminate_on_fail class-attribute instance-attribute

Python
terminate_on_fail: bool = False

Whether a failing live check was configured to end the simulation early.

terminate_on_pass class-attribute instance-attribute

Python
terminate_on_pass: bool = False

Whether a passing live check was configured to end the simulation early.

latch_on_fail class-attribute instance-attribute

Python
latch_on_fail: bool = False

Whether the check stopped being re-evaluated once it first failed.

latch_on_pass class-attribute instance-attribute

Python
latch_on_pass: bool = False

Whether the check stopped being re-evaluated once it first passed.

StepStatus

Bases: MojoBaseModel

started class-attribute instance-attribute

Python
started: datetime | None = None

Time the step started.

A None value indicates the step has not begun.

elapsed class-attribute instance-attribute

Python
elapsed: float | None = None

Time the step took to run.

A None value indicates the step has not completed. This value is not updated throughout the step's execution (i.e., if the step is in progress, this value will still report None).

td property

Python
td: timedelta | None

Time delta object provided by datetime.

TrialStatus

Bases: MojoBaseModel

Persistant state of a single trial, saved to disk.

trial_num instance-attribute

Python
trial_num: int

Trial number identifier.

step class-attribute instance-attribute

Python
step: Step = 'pending'

The current step of the trial.

completion class-attribute instance-attribute

Python
completion: Completion = INCOMPLETE

The overall completion type.

Included here so that the MojoRunner can set the bulk status of the job with a try-except block.

pending class-attribute instance-attribute

Python
pending: StepStatus = Field(
    default=StepStatus(started=now(UTC))
)

Pending step.

This is not really a real "step", it is really just an indicator the MuJoCo Mojo is waiting to run this trial.

generating class-attribute instance-attribute

Python
generating: StepStatus = Field(default_factory=StepStatus)

Generation step.

  • Information on times during which the job sequencer is generating XML (not yet running MuJoCo).
  • This step must be completed before moving to solving.
  • It is the first step in the sequence.

solving class-attribute instance-attribute

Python
solving: StepStatus = Field(default_factory=StepStatus)

Solving step.

  • Information on times during which the job sequencer is running MuJoCo.
  • This step is the second step in the sequence.
  • Completion of the step is the end of the trial.

requirements class-attribute instance-attribute

Python
requirements: list[RequirementResult] = Field(
    default_factory=list
)

Results of all requirement checks evaluated at the end of this trial.

requirements_passed property

Python
requirements_passed: bool | None

True if all requirements passed, False if any failed, None if no requirements were registered.

td property

Python
td: timedelta

The current timedelta for the trial to be completed. Incomplete steps are assumed to have 0 runtime.

JobStatus

Bases: MojoBaseModel

Orchestrates the global state of a job.

This class acts as a cache and aggregator for the individual TrialStatus files on disk. It provides high-level metrics needed for dashboards and job resumption.

id class-attribute instance-attribute

Python
id: str = Field(default_factory=lambda: hex)

Unique identifier generated each time a job is created.

Lets the Dojo detect that job_status.json now belongs to a different run than the one it has cached in memory (e.g. the user deleted the workdir and reran their analysis), so it can discard its stale JobStatus instead of overwriting the new file with old data.

started_by class-attribute instance-attribute

Python
started_by: str = Field(default_factory=getuser)

Who owns the job.

job_type instance-attribute

Python
job_type: JobType

What type of job it is.

execution_mode instance-attribute

Python
execution_mode: ExecutionMode

What execution mode the job was run with.

workdir instance-attribute

Python
workdir: Path

Job workdir.

n_proc instance-attribute

Python
n_proc: int

Number of processors used to define the job.

padding_style instance-attribute

Python
padding_style: str

Trial folder number padding style.

seed instance-attribute

Python
seed: int | None

Seed used to generate the job.

start_time class-attribute instance-attribute

Python
start_time: datetime = Field(
    default_factory=lambda: now(UTC)
)

Initial start time of the job.

elapsed class-attribute instance-attribute

Python
elapsed: timedelta = Field(default=timedelta(0))

Time elapsed running the job.

generator instance-attribute

Python
generator: tuple[str, Path | None, int | None]

What generator was used. Name of runtime, Path to the runtime, and linenumber.

runtime instance-attribute

Python
runtime: tuple[str, Path | None, int | None]

What runtime was used. Name of runtime, Path to the runtime, and linenumber.

objective instance-attribute

Python
objective: tuple[str, Path | None, int | None]

What objective function was used. Name of function, Path to the function, and linenumber.

gen_args_used instance-attribute

Python
gen_args_used: bool

Whether or not *args were used for the generator.

gen_kwargs_used instance-attribute

Python
gen_kwargs_used: bool

Whether or not **kwargs were used for the generator.

run_args_used instance-attribute

Python
run_args_used: bool

Whether or not *args were used for the runtime.

run_kwargs_used instance-attribute

Python
run_kwargs_used: bool

Whether or not **kwargs were used for the runtime.

trial_nums instance-attribute

Python
trial_nums: list[int]

The exhaustive list of trial identifiers expected for this job.

n_trial property

Python
n_trial: int

Number of trials used to define the job.

pending_trial_nums property

Python
pending_trial_nums: list[int]

Returns trial numbers which are either missing or marked as incomplete.

time_remaining_wall_clock property

Python
time_remaining_wall_clock: timedelta

Calculates the estimated time ramianing based on elapsed wall-clock time.

For simulations which were not resumed, this property is more accurate than the time_remaining_average_success property.

time_remaining_average_success property

Python
time_remaining_average_success: timedelta

Calculates the estimated time remaining based on the average completed-trial duration (successes and requirement failures; errored trials are excluded).

For simulations which were resumed, this property is more accurate than the time_remaining_wall_clock property.

failure_rate property

Python
failure_rate: float

Fraction of done trials that failed one or more requirements.

error_rate property

Python
error_rate: float

Fraction of done trials that raised an unhandled exception.

progress_bar property

Python
progress_bar: str

Visual progress bar for text-based monitoring.

n_failed property

Python
n_failed: int

Number of done trials that failed one or more requirements (FAILURE or TERMINATED). Errored trials are counted separately in n_error.

n_error property

Python
n_error: int

Number of trials that raised an unhandled exception (ERROR).

failed_trial_nums property

Python
failed_trial_nums: list[int]

Trials that completed but failed one or more requirements (FAILURE or TERMINATED).

error_trial_nums property

Python
error_trial_nums: list[int]

Trials that raised an unhandled exception (ERROR).

unsuccessful_trial_nums property

Python
unsuccessful_trial_nums: list[int]

Union of failed and errored trials, for anything-went-wrong checks like process exit codes.

requirement_summary property

Python
requirement_summary: list[RequirementSummary]

Per-requirement pass/fail counts aggregated across all cached trial statuses.

n_requirements_passed property

Python
n_requirements_passed: int

Total number of individual requirement checks that passed across all trials.

n_requirements_failed property

Python
n_requirements_failed: int

Total number of individual requirement checks that failed across all trials.

clear_cache

Python
clear_cache() -> None

Wipes the internal trial cache to force a full disk re-scan.

Source code in src/mujoco_mojo/utils/statusing.py
Python
def clear_cache(self) -> None:
    """Wipes the internal trial cache to force a full disk re-scan."""
    self._cache.clear()

reload_if_new_run

Python
reload_if_new_run() -> JobStatus | None

Checks workdir/JOB_STATUS_FNAME for a run_id different from this instance's.

If the user reran their analysis (e.g. after deleting the workdir), the file on disk now belongs to a different run and this instance's cached state (trial cache, trial_nums, generator, etc.) is stale. Returns a freshly-loaded JobStatus for the new run if so, or None if the file is missing, unreadable, or still belongs to this run.

Source code in src/mujoco_mojo/utils/statusing.py
Python
def reload_if_new_run(self) -> JobStatus | None:
    """
    Checks `workdir/JOB_STATUS_FNAME` for a `run_id` different from this instance's.

    If the user reran their analysis (e.g. after deleting the workdir), the file on disk now belongs to a different run and this instance's cached state (trial cache, trial_nums, generator, etc.) is stale. Returns a freshly-loaded `JobStatus` for the new run if so, or `None` if the file is missing, unreadable, or still belongs to this run.
    """
    path = self.workdir / JOB_STATUS_FNAME
    if not path.exists():
        return None

    try:
        on_disk = JobStatus.model_validate_json(path.read_text())
    except Exception:
        return None

    if on_disk.id == self.id:
        return None

    logger.info(f"Detected a new job run at {path} (run_id changed). Reloading.")
    return on_disk

get_trial_status

Python
get_trial_status(trial_num: int) -> TrialStatus | None

Returns the cached status for a trial, or None if not yet loaded.

Source code in src/mujoco_mojo/utils/statusing.py
Python
def get_trial_status(self, trial_num: int) -> TrialStatus | None:
    """Returns the cached status for a trial, or None if not yet loaded."""
    return self._cache.get(trial_num)

refresh_from_disk

Python
refresh_from_disk(
    n_proc: int = 1,
    progress_callback: Callable[[float], None]
    | None = None,
    force_refetch: bool = False,
    persist: bool = True,
) -> None

Scans the workdir to identify which trials still need execution, but only for runs not already in the cache (i.e., completed).

Parameters:

Name Type Description Default
n_proc int

Number of worker threads used to scan trial status files.

1
progress_callback Callable[[float], None] | None

Optional callback invoked with scan progress (0-100).

None
force_refetch bool

If True, clears the cache before scanning so every trial is re-read from disk.

False
persist bool

If True (default), writes the refreshed status back to workdir/JOB_STATUS_FNAME. Read-only consumers (e.g. the Dojo monitor) should pass False so they don't overwrite the file the run owner manages.

True
Source code in src/mujoco_mojo/utils/statusing.py
Python
def refresh_from_disk(
    self,
    n_proc: int = 1,
    progress_callback: Callable[[float], None] | None = None,
    force_refetch: bool = False,
    persist: bool = True,
) -> None:
    """
    Scans the workdir to identify which trials still need execution, but only for runs not already in the cache (i.e., completed).

    Args:
        n_proc: Number of worker threads used to scan trial status files.
        progress_callback: Optional callback invoked with scan progress (0-100).
        force_refetch: If `True`, clears the cache before scanning so every trial is re-read from disk.
        persist: If `True` (default), writes the refreshed status back to `workdir/JOB_STATUS_FNAME`. Read-only consumers (e.g. the Dojo monitor) should pass `False` so they don't overwrite the file the run owner manages.

    """
    if force_refetch:
        self.clear_cache()

    max_workers = max(1, n_proc)

    # identify work
    needed_tns = [
        tn
        for tn in self.trial_nums
        if tn not in self._cache
        or self._cache[tn].completion == Completion.INCOMPLETE
    ]

    if not needed_tns:
        if progress_callback:
            return progress_callback(100.0)
        return

    def _load_status(tn: int) -> tuple[int, TrialStatus | None]:
        path = self.trial_num_to_path(tn) / TRIAL_STATUS_FNAME
        if not path.exists():
            return tn, None
        try:
            return tn, TrialStatus.model_validate_json(path.read_text())
        except Exception:
            return tn, None

    # execute with real-time tracking
    total_tasks = len(needed_tns)
    completed_count = 0

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_tn = {executor.submit(_load_status, tn): tn for tn in needed_tns}

        # as completed yields futures as soon as done
        for future in as_completed(future_to_tn):
            tn, status = future.result()

            # update the cache immediately
            if status is not None:
                with self._lock:
                    self._cache[tn] = status

            # broadcast status
            completed_count += 1
            if progress_callback:
                pct = (completed_count / total_tasks) * 100
                progress_callback(pct)

    # post calculations
    with self._lock:
        timed_durations = [
            s.td.total_seconds()
            for s in self._cache.values()
            if s.completion in _TIMED_COMPLETIONS
        ]
        if timed_durations:
            avg_sec = sum(timed_durations) / len(timed_durations)
            self.average_trial_duration = timedelta(seconds=avg_sec)

        if not self.is_done:
            self.elapsed = datetime.now(UTC) - self.start_time
    if persist:
        self.dump_to_path(self.workdir / JOB_STATUS_FNAME)

get_trial_nums_with_status

Python
get_trial_nums_with_status(
    *status: Completion, invert: bool = False
) -> list[int]

Returns a list of trial numbers which have the provided status. If invert is True it will provide trial numbers which do not match.

Source code in src/mujoco_mojo/utils/statusing.py
Python
def get_trial_nums_with_status(
    self, *status: Completion, invert: bool = False
) -> list[int]:
    """Returns a list of trial numbers which have the provided status. If invert is True it will provide trial numbers which do not match."""
    to_check = [m for m in Completion if m not in status] if invert else status

    return sorted([tn for tn, comp in self._registry.items() if comp in to_check])

update_trial

Python
update_trial(
    status: TrialStatus, save: bool = True
) -> None

Updates the internal registry and average trial duration and optionally persists the global status.

Source code in src/mujoco_mojo/utils/statusing.py
Python
def update_trial(self, status: TrialStatus, save: bool = True) -> None:
    """Updates the internal registry and average trial duration and optionally persists the global status."""
    with self._lock:
        self._cache[status.trial_num] = status

        timed_durations = [
            s.td.total_seconds()
            for s in self._cache.values()
            if s.completion in _TIMED_COMPLETIONS and s.td is not None
        ]

        if timed_durations:
            self.average_trial_duration = timedelta(
                seconds=sum(timed_durations) / len(timed_durations)
            )

        self.elapsed = datetime.now(UTC) - self.start_time

        if save:
            self.dump_to_path(self.workdir / JOB_STATUS_FNAME)

completion_timeline

Python
completion_timeline(n_bins: int = 40) -> list[TimelineBin]

Reconstructs cumulative outcome counts over the job's wall-clock life, for the monitor's stacked progress histogram.

Each trial's completion time is taken as the latest recorded step end (started + elapsed). A trial that is still INCOMPLETE but has begun generating or solving counts as "running" from that start time onward rather than "pending"; claimed-but-not-yet-started trials (only pending.started set) still count as pending. The axis runs from job start to now if any trial is currently running, or to the last completion event if the job is idle/abandoned (so a stalled job doesn't render a flat tail stretching to now). For each of n_bins evenly spaced timestamps in that range, the bin reports how many trials had succeeded, failed requirements, errored, or were running by that time; everything else is pending.

Source code in src/mujoco_mojo/utils/statusing.py
Python
def completion_timeline(self, n_bins: int = 40) -> list[TimelineBin]:
    """
    Reconstructs cumulative outcome counts over the job's wall-clock life, for the monitor's stacked progress histogram.

    Each trial's completion time is taken as the latest recorded step end (`started + elapsed`). A trial that is still `INCOMPLETE` but has begun generating or solving counts as "running" from that start time onward rather than "pending"; claimed-but-not-yet-started trials (only `pending.started` set) still count as pending. The axis runs from job start to now if any trial is currently running, or to the last completion event if the job is idle/abandoned (so a stalled job doesn't render a flat tail stretching to now). For each of `n_bins` evenly spaced timestamps in that range, the bin reports how many trials had succeeded, failed requirements, errored, or were running by that time; everything else is pending.
    """
    with self._lock:
        statuses = list(self._cache.values())

    events: list[tuple[datetime, Completion]] = []
    running_starts: list[datetime] = []
    for s in statuses:
        if s.completion == Completion.INCOMPLETE:
            run_start = s.generating.started or s.solving.started
            if run_start is not None:
                running_starts.append(run_start)
            continue
        step_ends = [
            step.started + timedelta(seconds=step.elapsed)
            for step in (s.pending, s.generating, s.solving)
            if step.started is not None and step.elapsed is not None
        ]
        if not step_ends:
            continue
        events.append((max(step_ends), s.completion))

    if not events and not running_starts:
        return []

    start = self.start_time
    if running_starts:
        # the job is actively progressing right now: let the axis track
        # "now" rather than freezing at the last completion event
        end = datetime.now(UTC)
    else:
        # nothing is running: cap the axis at the last completion event so
        # a job that stopped (or was abandoned) long ago doesn't render a
        # flat tail stretching to now; self.elapsed can't be used here
        # since refresh_from_disk keeps bumping it to now-start while the
        # job is not done
        end = min(datetime.now(UTC), max(ts for ts, _ in events))
    total_span = (end - start).total_seconds()
    if total_span <= 0:
        return []

    bins: list[TimelineBin] = []
    for i in range(1, n_bins + 1):
        t = start + timedelta(seconds=total_span * i / n_bins)
        n_s = sum(1 for ts, c in events if ts <= t and c == Completion.SUCCESS)
        n_f = sum(1 for ts, c in events if ts <= t and c in _FAILED_COMPLETIONS)
        n_e = sum(1 for ts, c in events if ts <= t and c == Completion.ERROR)
        n_r = sum(1 for rs in running_starts if rs <= t)
        bins.append(
            {
                "t": (t - start).total_seconds(),
                # microsecond precision: Plotly plots this as a categorical
                # x-axis, so two bins that round to the same whole second
                # (common with many bins over a short span) would collide
                # onto a single x position and appear summed/overlaid
                # instead of as separate bars
                "label": self._utc_to_local(t).strftime("%Y-%m-%d %H:%M:%S.%f"),
                "n_success": n_s,
                "n_failed": n_f,
                "n_error": n_e,
                "n_running": n_r,
                "n_pending": max(0, self.n_trial - n_s - n_f - n_e - n_r),
            }
        )
    return bins

to_monitor_json

Python
to_monitor_json(n_proc: int | None = None) -> dict

Returns a lightweight summary optimized for the Alpine.js dashboard.

Source code in src/mujoco_mojo/utils/statusing.py
Python
def to_monitor_json(self, n_proc: int | None = None) -> dict:
    """Returns a lightweight summary optimized for the Alpine.js dashboard."""
    from mujoco_mojo.runtime.signal_manager import SignalManager

    # We trigger the disk refresh here so the data is fresh
    # obj = self.model_validate_json((self.workdir / JOB_STATUS_FNAME).read_text())
    # obj.refresh_from_disk(n_proc=obj.n_proc if n_proc is None else n_proc)

    avg_seconds = self.average_trial_duration.total_seconds()

    # Avoid divide by zero if no trials have finished yet
    throughput = 0
    if avg_seconds > 0:
        throughput = (60.0 / avg_seconds) * self.n_proc

    success_tns = self.success_trial_nums
    failed_tns = self.failed_trial_nums
    error_tns = self.error_trial_nums

    def _with_db(tns: list[int]) -> list[int]:
        return [
            tn
            for tn in tns
            if (
                self.trial_num_to_path(tn) / SignalManager.default_output_name()
            ).exists()
        ]

    return {
        "progress": self.progress * 100,
        "n_success": self.n_success,
        "n_failed": self.n_failed,
        "n_error": self.n_error,
        "n_trial": self.n_trial,
        "n_done": self.n_done,
        "n_remaining": self.n_remaining,
        "failure_rate": self.failure_rate,
        "error_rate": self.error_rate,
        "throughput": round(throughput, 1),
        "avg_duration": str(self.average_trial_duration).split(".")[0],
        "time_remaining": str(self.time_remaining_average_success).split(".")[0],
        "elapsed": str(self.elapsed).split(".")[0],
        "is_complete": self.progress >= 1.0,
        "success_tns": success_tns,
        "failure_tns": failed_tns,
        "error_tns": error_tns,
        "last_success_tn": max(success_tns) if success_tns else None,
        "last_failure_tn": max(failed_tns) if failed_tns else None,
        "last_error_tn": max(error_tns) if error_tns else None,
        "start_time": f"{self._utc_to_local(self.start_time).strftime('%Y-%m-%d %H:%M:%S')} {self.local_tzabbr}",
        "end_time": f"{self._utc_to_local(self.end_time).strftime('%Y-%m-%d %H:%M:%S')} {self.local_tzabbr}",
        "version": f"mujoco-mojo v{version('mujoco-mojo')}",
        "padding_style": self.padding_style,
        "failure_tns_with_db": _with_db(failed_tns),
        "error_tns_with_db": _with_db(error_tns),
        "timeline": self.completion_timeline(),
    }