Skip to content

settings

Global user settings for mujoco_mojo, persisted to ~/.mujoco-mojo/settings.toml.

VisualizationSettings

Bases: BaseModel

Colors for force, torque, contact, and proximity overlays rendered during simulation.

SensAIModelEntry

Bases: BaseModel

One entry in SensAISettings.models - a provider paired with that provider's own model identifier.

SensAISettings

Bases: BaseModel

Settings for the SensAI assistant embedded in the Dojo dashboard.

DojoSettings

Bases: BaseModel

Settings for the Dojo dashboard.

SlurmExtraSettings

Bases: RootModel[dict[str, SlurmScalar]]

Flat key-value pairs used to extend a SLURM submission. sbatch.-prefixed keys become #SBATCH lines; everything else becomes an exported environment variable. Values must be scalars.

GeneralSettings

Bases: BaseModel

General-purpose defaults shared across mujoco-mojo's CLI commands, plus asset-bundling behavior.

ReloadedSettings

Bases: BaseModel

Settings for mujoco-mojo reloaded.

MonteCarloRunSettings

Bases: _RunDefaults

Settings for mujoco-mojo run monte-carlo.

SingleRunSettings

Bases: _RunDefaults

Settings for mujoco-mojo run single.

OptimizeRunSettings

Bases: _RunDefaults

Settings for mujoco-mojo run optimization.

RunSettings

Bases: BaseModel

Settings for mujoco-mojo run's subcommands.

GenerateJsonSchemaWithDefaults

Bases: GenerateJsonSchema

Appends each field's default value to its description, so editors that only surface description on hover (e.g. VS Code's Even Better TOML) still show it, without hand-duplicating every Field(default=...) into its own description text.

Also attaches an x-enum-descriptions extension (value -> that member's own attribute-docstring, via _enum_value_descriptions above) to every enum schema - generic to any StrEnum with attribute-docstring'd members, not specific to whichever model happens to use this generator, so a consumer like the Dojo settings panel and PlotConfig's Plot Editor/JSON editor tooltips (dojo/plot_config.py) both get per-dropdown-option descriptions for free from the same one generator, rather than each redefining this behavior in its own subclass.

get_default_value

Python
get_default_value(schema: WithDefaultSchema) -> Any

Pydantic's own default implementation only ever surfaces a static default=, never a default_factory= result, since a factory could be expensive or side-effecting. Every default_factory actually used across this codebase (dict, list, lambda: SlurmExtraSettings({}), and the nested settings-group constructors) is cheap and side-effect-free, so calling it here is safe, and it's what lets _append_defaults below show a real "Default: ..." for fields like PlotConfig.y_axes/annotations/shapes that would otherwise silently show none.

Source code in src/mujoco_mojo/settings.py
Python
def get_default_value(self, schema: core_schema.WithDefaultSchema) -> Any:
    """
    Pydantic's own default implementation only ever surfaces a static `default=`, never a `default_factory=` result, since a factory could be expensive or side-effecting. Every `default_factory` actually used across this codebase (`dict`, `list`, `lambda: SlurmExtraSettings({})`, and the nested settings-group constructors) is cheap and side-effect-free, so calling it here is safe, and it's what lets `_append_defaults` below show a real "Default: ..." for fields like `PlotConfig.y_axes`/`annotations`/`shapes` that would otherwise silently show none.
    """
    default = super().get_default_value(schema)
    if default is not NoDefault:
        return default
    factory = schema.get("default_factory")
    if factory is None or schema.get("default_factory_takes_data"):
        return NoDefault
    # `default_factory_takes_data` being falsy is what actually
    # guarantees the zero-argument overload at runtime; the stub's
    # `Callable[[], Any] | Callable[[dict[str, Any]], Any]` union
    # can't express that correlation itself.
    no_arg_factory = cast("Callable[[], Any]", factory)
    try:
        return no_arg_factory()
    except Exception:
        return NoDefault

MujocoMojoSettings

Bases: BaseSettings

Global user-level settings persisted to ~/.mujoco-mojo/settings.toml, layered with an optional project-local override file - the same User-settings-vs-Workspace-settings model VS Code uses.

Instantiate to load. Sources are checked in priority order: constructor kwargs > environment variables > .env file (env_file in model_config) > project settings file (project_settings_file(), <cwd>/.mujoco-mojo/settings.toml) > global settings file (~/.mujoco-mojo/settings.toml) > defaults. Every field can be overridden at the project level, not just a specific subset - a project file is expected to hold only the handful of keys that genuinely differ from the global defaults (e.g. per-project SLURM extras or force-scaling), not a full copy. Environment variables (real ones and .env entries alike) use the prefix MUJOCO_MOJO_ with __ as the nested delimiter, e.g. MUJOCO_MOJO_DOJO__SENSAI__MODEL_NAME=llama3.2:3b.

defaults classmethod

Python
defaults() -> MujocoMojoSettings

Builds a fresh instance from pure code-level defaults, ignoring environment variables and any existing settings.toml.

A constructor kwarg left unset resolves against this class's other settings sources (env vars, then the TOML file) rather than the field's own default - that layering is the entire reason BaseSettings exists - so every field's default must be passed explicitly here to get a true reset. Iterating model_fields rather than hand-listing field names means a newly added settings field is automatically covered, with nothing to remember to update.

Returns:

Type Description
MujocoMojoSettings

A MujocoMojoSettings instance with every field at its code-level default.

Source code in src/mujoco_mojo/settings.py
Python
@classmethod
def defaults(cls) -> MujocoMojoSettings:
    """
    Builds a fresh instance from pure code-level defaults, ignoring environment variables and any existing settings.toml.

    A constructor kwarg left unset resolves against this class's other settings sources (env vars, then the TOML file) rather than the field's own default - that layering is the entire reason `BaseSettings` exists - so every field's default must be passed explicitly here to get a true reset. Iterating `model_fields` rather than hand-listing field names means a newly added settings field is automatically covered, with nothing to remember to update.

    Returns:
        A `MujocoMojoSettings` instance with every field at its code-level default.

    """
    field_defaults = {
        name: info.get_default(call_default_factory=True)
        for name, info in cls.model_fields.items()
    }
    return cls(**field_defaults)

set_project_value classmethod

Python
set_project_value(key: str, value: Any) -> None

Sets a single dotted-path setting (e.g. "assets.symlink") in the project-local settings file, creating any intermediate tables as needed, without touching anything else already there.

key.split(".") is ambiguous for a free-form group like slurm (a RootModel[dict[str, ...]]), whose own keys legitimately contain literal dots (e.g. "sbatch.account") - "slurm.sbatch.account" must set that one flat key, not descend two more table levels. Resolved by walking the declared field types (not the current data) until a RootModel field is reached; everything after that point is rejoined into a single literal key instead of split further.

Validated by overlaying the new value onto the full currently-effective settings (project + global + env + defaults) and calling model_validate on the result - model_config forbids extra fields on every fixed-shape settings group, so a typo'd leaf and a wrong-typed value both raise pydantic.ValidationError before anything is written. The validated and re-dumped leaf value is what actually gets persisted, not the raw value argument, so e.g. the CLI's loosely-typed input string "true" for a bool field is written to TOML as a real boolean, not a quoted string. Only that one changed leaf is written into the project file, keeping it a small, deliberate diff rather than a full mirror of every setting - contrast with save(), which always persists every field (correct for the global file, wrong here).

Parameters:

Name Type Description Default
key str

Dotted path, e.g. "dojo.sensai.base_url", "assets.symlink", or "slurm.sbatch.account".

required
value Any

The value to set, already parsed to its target Python type.

required

Raises:

Type Description
KeyError

If an intermediate segment of key isn't a real settings table.

ValidationError

If the resulting settings are invalid (unknown leaf, wrong type, etc.).

Source code in src/mujoco_mojo/settings.py
Python
@classmethod
def set_project_value(cls, key: str, value: Any) -> None:
    """
    Sets a single dotted-path setting (e.g. "assets.symlink") in the project-local settings file, creating any intermediate tables as needed, without touching anything else already there.

    `key.split(".")` is ambiguous for a free-form group like `slurm` (a `RootModel[dict[str, ...]]`), whose own keys legitimately contain literal dots (e.g. `"sbatch.account"`) - `"slurm.sbatch.account"` must set that one flat key, not descend two more table levels. Resolved by walking the *declared field types* (not the current data) until a `RootModel` field is reached; everything after that point is rejoined into a single literal key instead of split further.

    Validated by overlaying the new value onto the full currently-effective settings (project + global + env + defaults) and calling `model_validate` on the result - `model_config` forbids extra fields on every fixed-shape settings group, so a typo'd leaf and a wrong-typed value both raise `pydantic.ValidationError` before anything is written. The *validated and re-dumped* leaf value is what actually gets persisted, not the raw `value` argument, so e.g. the CLI's loosely-typed input string `"true"` for a `bool` field is written to TOML as a real boolean, not a quoted string. Only that one changed leaf is written into the project file, keeping it a small, deliberate diff rather than a full mirror of every setting - contrast with `save()`, which always persists every field (correct for the global file, wrong here).

    Args:
        key: Dotted path, e.g. "dojo.sensai.base_url", "assets.symlink", or "slurm.sbatch.account".
        value: The value to set, already parsed to its target Python type.

    Raises:
        KeyError: If an intermediate segment of `key` isn't a real settings table.
        pydantic.ValidationError: If the resulting settings are invalid (unknown leaf, wrong type, etc.).

    """
    parts = key.split(".")

    # stop splitting once a free-form (RootModel) group is reached - the
    # rest of the path is that group's own literal key, dots and all
    model: type[BaseModel] = cls
    for i, part in enumerate(parts[:-1]):
        field_info = model.model_fields.get(part)
        annotation = field_info.annotation if field_info else None
        if not (isinstance(annotation, type) and issubclass(annotation, BaseModel)):
            msg = f"Unknown settings path: {key}"
            raise KeyError(msg)
        if issubclass(annotation, RootModel):
            parts = [*parts[: i + 1], ".".join(parts[i + 1 :])]
            break
        model = annotation

    # mode="json" turns any SecretStr field into its masked string rather
    # than a raw object tomlkit can't write, and exclude_none=True omits
    # an unset Optional field entirely - see save()
    effective = cls().model_dump(mode="json", exclude_none=True)
    overlay = effective
    for part in parts[:-1]:
        nxt = overlay.get(part)
        if not isinstance(nxt, dict):
            msg = f"Unknown settings path: {key}"
            raise KeyError(msg)
        overlay = nxt
    overlay[parts[-1]] = value
    validated = cls.model_validate(effective).model_dump(
        mode="json", exclude_none=True
    )

    coerced = validated
    for part in parts[:-1]:
        coerced = coerced[part]
    coerced_leaf = coerced[parts[-1]]

    path = project_settings_file()
    if path.exists():
        doc = tomlkit.parse(path.read_text(encoding="utf-8"))
    else:
        # model_construct() bypasses BaseSettings' usual env/TOML-reading
        # __init__ entirely - safe here since save(project=True) never
        # reads the instance's field values, and this path must not
        # require the *global* settings file (a wholly separate file) to
        # currently be valid just to bootstrap a fresh project file
        cls.model_construct().save(path.parent, project=True)
        doc = tomlkit.parse(path.read_text(encoding="utf-8"))

    node = doc
    for part in parts[:-1]:
        if part not in node or not isinstance(node[part], (Table, dict)):
            node[part] = tomlkit.table()
        node = node[part]
    node[parts[-1]] = coerced_leaf

    path.write_text(tomlkit.dumps(doc), encoding="utf-8")

save

Python
save(
    directory: Path | None = None, *, project: bool = False
) -> Path

Persist settings to directory (defaults to the global settings directory, ~/.mujoco-mojo), alongside a freshly (re)generated settings.schema.json/.taplo.toml pair (see write_schema_files) - one method covers both the global settings file and a project-local override file, so there's no separate init_project_file-style method to keep in sync with this one.

project=False (the default - the global file): every field's current value is merged into settings.toml with tomlkit, preserving any comments or formatting added by hand. A key that exists in the file but no longer has a corresponding settings value (e.g. a removed slurm entry) is left as-is rather than deleted.

project=True (for <cwd>/.mujoco-mojo/settings.toml): self's field values are never written - a project file is meant to hold only a small, deliberate diff from the global defaults, not a full mirror of every setting (see set_project_value) - so an existing file's contents are left completely untouched, and a fresh one gets just the #:schema header. Also drops a .gitignore (*) next to it if one isn't already there, so a project's local overrides - which may be machine-specific - don't get committed by accident.

Either way, a brand-new file starts with a #:schema header pointing at the colocated settings.schema.json - a bare relative filename, resolved by taplo against the TOML file's own directory, rather than a cross-directory reference to another directory's schema. The latter is what broke under SSH/SSHFS-style setups where only one of the two directories was visible to the editor - every settings directory is now self-contained. An existing file that's missing that header (written before schema support existed, or hand-edited to drop the line) gets it prepended on the next save too, except for the already-exists project-file case just above, which skips reading the file at all.

Parameters:

Name Type Description Default
directory Path | None

Where to write settings.toml, settings.schema.json, and .taplo.toml. Defaults to SETTINGS_DIR (looked up at call time, not import time, so tests can monkeypatch it).

None
project bool

Write the sparse project-local form described above instead of the full global one.

False

Returns:

Type Description
Path

The settings.toml path written to.

Source code in src/mujoco_mojo/settings.py
Python
def save(self, directory: Path | None = None, *, project: bool = False) -> Path:
    """
    Persist settings to `directory` (defaults to the global settings directory, `~/.mujoco-mojo`), alongside a freshly (re)generated `settings.schema.json`/`.taplo.toml` pair (see `write_schema_files`) - one method covers both the global settings file and a project-local override file, so there's no separate `init_project_file`-style method to keep in sync with this one.

    `project=False` (the default - the global file): every field's current value is merged into `settings.toml` with `tomlkit`, preserving any comments or formatting added by hand. A key that exists in the file but no longer has a corresponding settings value (e.g. a removed `slurm` entry) is left as-is rather than deleted.

    `project=True` (for `<cwd>/.mujoco-mojo/settings.toml`): `self`'s field values are never written - a project file is meant to hold only a small, deliberate diff from the global defaults, not a full mirror of every setting (see `set_project_value`) - so an existing file's contents are left completely untouched, and a fresh one gets just the `#:schema` header. Also drops a `.gitignore` (`*`) next to it if one isn't already there, so a project's local overrides - which may be machine-specific - don't get committed by accident.

    Either way, a brand-new file starts with a `#:schema` header pointing at the colocated settings.schema.json - a bare relative filename, resolved by taplo against the TOML file's own directory, rather than a cross-directory reference to another directory's schema. The latter is what broke under SSH/SSHFS-style setups where only one of the two directories was visible to the editor - every settings directory is now self-contained. An existing file that's missing that header (written before schema support existed, or hand-edited to drop the line) gets it prepended on the next save too, except for the already-exists project-file case just above, which skips reading the file at all.

    Args:
        directory: Where to write `settings.toml`, `settings.schema.json`, and `.taplo.toml`. Defaults to `SETTINGS_DIR` (looked up at call time, not import time, so tests can monkeypatch it).
        project: Write the sparse project-local form described above instead of the full global one.

    Returns:
        The `settings.toml` path written to.

    """
    if directory is None:
        directory = SETTINGS_DIR
    directory.mkdir(parents=True, exist_ok=True)
    toml_path = directory / "settings.toml"

    if project:
        gitignore = directory / ".gitignore"
        if not gitignore.exists():
            gitignore.write_text("*\n", encoding="utf-8")

    if not (project and toml_path.exists()):
        # guards the read-modify-write below against concurrent writers -
        # the CLI, the Dojo settings panel, and multiple browser tabs can
        # all call save() around the same time
        lock_path = toml_path.with_suffix(toml_path.suffix + ".lock")
        with FileLock(lock_path):
            if toml_path.exists():
                text = _ensure_schema_header(toml_path.read_text(encoding="utf-8"))
                doc = tomlkit.parse(text)
            else:
                doc = tomlkit.parse("#:schema settings.schema.json\n")

            if not project:
                # mode="json" turns any SecretStr field (e.g. dojo.password)
                # into its masked "**********" string rather than a raw object
                # tomlkit can't write at all - the real value is never persisted
                # by this method. exclude_none=True omits an unset Optional field
                # (e.g. dojo.chime) entirely, since TOML has no null literal to write.
                _merge_into_toml(
                    doc, self.model_dump(mode="json", exclude_none=True), type(self)
                )

            toml_path.write_text(tomlkit.dumps(doc), encoding="utf-8")

    self.write_schema_files(directory)
    return toml_path

write_schema_files classmethod

Python
write_schema_files(directory: Path | None = None) -> None

Writes settings.schema.json and a companion .taplo.toml into directory, so taplo-based editors (VS Code's Even Better TOML, Neovim, etc.) get hover hints and validation for whatever settings.toml lives there. Safe to re-run any time, e.g. after upgrading mujoco-mojo changes the settings shape.

The schema and taplo config are always colocated with the settings.toml they describe, rather than one directory's file referencing another's schema by absolute path - that cross-directory reference is exactly what broke under SSH/SSHFS-style setups where only one of the two directories (typically just the project one) is visible to the editor.

Parameters:

Name Type Description Default
directory Path | None

Where to write settings.schema.json and .taplo.toml. Defaults to SETTINGS_DIR (looked up at call time, not import time, so tests can monkeypatch it).

None
Source code in src/mujoco_mojo/settings.py
Python
@classmethod
def write_schema_files(cls, directory: Path | None = None) -> None:
    """
    Writes settings.schema.json and a companion .taplo.toml into `directory`, so taplo-based editors (VS Code's Even Better TOML, Neovim, etc.) get hover hints and validation for whatever settings.toml lives there. Safe to re-run any time, e.g. after upgrading mujoco-mojo changes the settings shape.

    The schema and taplo config are always colocated with the settings.toml they describe, rather than one directory's file referencing another's schema by absolute path - that cross-directory reference is exactly what broke under SSH/SSHFS-style setups where only one of the two directories (typically just the project one) is visible to the editor.

    Args:
        directory: Where to write `settings.schema.json` and `.taplo.toml`. Defaults to `SETTINGS_DIR` (looked up at call time, not import time, so tests can monkeypatch it).

    """
    if directory is None:
        directory = SETTINGS_DIR
    directory.mkdir(parents=True, exist_ok=True)

    schema_file = directory / "settings.schema.json"
    taplo_file = directory / ".taplo.toml"

    schema = cls.model_json_schema(schema_generator=GenerateJsonSchemaWithDefaults)
    schema_file.write_text(json.dumps(schema), encoding="utf-8")

    # taplo requires a file:// URI for the schema url - a relative path is not supported
    schema_uri = schema_file.as_uri()
    taplo_file.write_text(
        f'[[rule]]\ninclude = ["settings.toml"]\n\n[rule.schema]\nurl = "{schema_uri}"\n',
        encoding="utf-8",
    )

project_settings_file

Python
project_settings_file() -> Path

Path to the project-local settings file, <cwd>/.mujoco-mojo/settings.toml.

Computed fresh on every call rather than cached as a module-level constant, since Path.cwd() can legitimately differ across calls within one process (e.g. a long-running dashboard server, or tests).

Returns:

Type Description
Path

Path.cwd() / ".mujoco-mojo" / "settings.toml".

Source code in src/mujoco_mojo/settings.py
Python
def project_settings_file() -> Path:
    """
    Path to the project-local settings file, `<cwd>/.mujoco-mojo/settings.toml`.

    Computed fresh on every call rather than cached as a module-level constant, since `Path.cwd()` can legitimately differ across calls within one process (e.g. a long-running dashboard server, or tests).

    Returns:
        `Path.cwd() / ".mujoco-mojo" / "settings.toml"`.

    """
    return Path.cwd() / ".mujoco-mojo" / "settings.toml"