Source code for protomotions.utils.inference_utils
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Deriving the inference view from the training bundle.
Inference, export and ``--full-eval`` all read the one saved (training) bundle
and derive their view at load through :func:`derive_inference_configs`, in one
order: framework defaults (``apply_inference_defaults`` on each config class),
then overrides, then the recipe's and ``--inference-config``'s hooks. Nothing is
baked at train time; the save path dry-runs this derivation so a broken hook
fails during training, not at inference.
"""
from __future__ import annotations
import argparse
import inspect
import logging
from pathlib import Path
from typing import Callable, List, Mapping, Optional, Sequence
from protomotions.utils.config_builder import load_experiment_module
from protomotions.utils.config_utils import apply_overrides, read_override
from protomotions.utils.experiment_configs import ExperimentConfigs
log = logging.getLogger(__name__)
# A recipe (or --inference-config) hook mutates the inference view in place.
Hook = Callable[[ExperimentConfigs, argparse.Namespace], None]
[docs]
def inference_defaults(configs: ExperimentConfigs) -> None:
"""The framework's inference view, before any override or recipe hook.
A fixed sequence, not a getattr scan: each config class says what inference
changes about it, and the sensors switch to their clean (noise-free) read.
"""
configs.env.apply_inference_defaults()
if configs.env.sensors is not None:
configs.env.sensors = configs.env.sensors.for_inference()
configs.simulator.apply_inference_defaults()
configs.motion_lib.apply_inference_defaults()
if configs.agent is not None:
configs.agent.apply_inference_defaults(configs.env)
[docs]
def derive_inference_configs(
training: ExperimentConfigs,
args: argparse.Namespace,
overrides: Mapping[str, object],
hooks: Sequence[Hook],
) -> ExperimentConfigs:
"""The inference view of a training bundle: defaults, overrides, then hooks.
Works on a deep copy, so the training bundle is never mutated. A hook that
silently undoes an override is refused by name. The derived view is what
inference runs, what export reads and what ``--full-eval`` evaluates.
"""
from protomotions.envs.context_check import run_config_binding_check
view = training.copy()
inference_defaults(view)
applied = apply_overrides(view, overrides)
for hook in hooks:
hook(view, args)
for path, value in applied.items():
current = read_override(view, path)
if current != value:
raise ValueError(
f"inference hook {hook.__module__}.{hook.__qualname__} undid "
f"override {path}={value!r}: the value is now {current!r}"
)
view.motion_lib.validate()
run_config_binding_check(view.env, view.robot, view.terrain, view.agent, log)
return view
def _check_hook_arity(hook: Hook) -> None:
"""A hook takes exactly ``(configs, args)``; the eight-parameter form is gone."""
if len(inspect.signature(hook).parameters) != 2:
raise TypeError(
f"{hook.__module__}.apply_inference_overrides must take (configs, args); "
"the eight-parameter form was retired"
)
[docs]
def recipe_hooks(
checkpoint_dir, inference_config: Optional[Path]
) -> List[Hook]:
"""The inference hooks to run, in order: the recipe's, then ``--inference-config``.
The recipe's hook is loaded from the ``experiment_config.py`` saved beside
the checkpoint (so editing a recipe changes existing checkpoints' inference
view, and a run directory without it is incomplete).
"""
checkpoint_dir = Path(checkpoint_dir)
experiment_path = checkpoint_dir / "experiment_config.py"
if not experiment_path.exists():
raise FileNotFoundError(
f"no experiment_config.py beside the checkpoint in {checkpoint_dir}; "
"the run directory is incomplete, regenerate it with "
"train_agent.py --create-config-only"
)
hooks: List[Hook] = []
recipe_hook = getattr(
load_experiment_module(experiment_path), "apply_inference_overrides", None
)
if recipe_hook is not None:
_check_hook_arity(recipe_hook)
hooks.append(recipe_hook)
if inference_config is not None:
inference_path = Path(inference_config)
inference_hook = getattr(
load_experiment_module(inference_path), "apply_inference_overrides", None
)
if inference_hook is None:
raise ValueError(
f"{inference_path} must define apply_inference_overrides(configs, args)"
)
_check_hook_arity(inference_hook)
hooks.append(inference_hook)
return hooks