Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed
^^^^^

* Fixed ``presets=newton_mjwarp`` and ``presets=newton_kamino`` validation so
tasks without active Newton physics presets now raise instead of applying only
non-physics presets.
54 changes: 52 additions & 2 deletions source/isaaclab_tasks/isaaclab_tasks/utils/hydra.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
from .preset_target import PresetTarget

_LITERAL_MAP = {"true": True, "false": False, "none": None, "null": None}
# Canonical Newton solver presets that must resolve through an active PhysicsCfg.
_NEWTON_PHYSICS_PRESETS = {"newton_mjwarp", "newton_kamino"}


def _user_stacklevel() -> int:
Expand Down Expand Up @@ -74,6 +76,11 @@ def _known_preset_names(presets: dict) -> set[str]:
return {name for section in presets.values() for fields in section.values() for name in fields}


def _preset_targets(value) -> set[PresetTarget]:
"""Return typed preset targets matched by a preset alternative value."""
return {target for target in PresetTarget if target.base_classes and isinstance(value, target.base_classes)}


def _normalize_preset_name(name: str, known_names: set[str]) -> str:
"""Map a deprecated preset name to its replacement and emit a warning.

Expand Down Expand Up @@ -279,6 +286,7 @@ def _pick_alternative(
path: str = "",
explicit_name: str | None = None,
consumed_selected: set[str] | None = None,
consumed_selected_targets: dict[str, set[PresetTarget]] | None = None,
):
"""Choose the best alternative from a PresetCfg.

Expand Down Expand Up @@ -310,16 +318,21 @@ def _pick_alternative(
name = _normalize_preset_name(raw_name, field_names)
if name not in fields or name == match_name:
continue
val = fields[name]
if consumed_selected is not None:
consumed_selected.add(raw_name)
consumed_selected.add(name)
if consumed_selected_targets is not None:
targets = _preset_targets(val)
if targets:
consumed_selected_targets.setdefault(raw_name, set()).update(targets)
consumed_selected_targets.setdefault(name, set()).update(targets)
if match_name is not None:
val = fields[name]
if match_value is not val and match_value != val:
raise ValueError(
f"Conflicting global presets: '{match_name}' and '{name}' both define preset for '{path}'"
)
match_name, match_value = name, fields[name]
match_name, match_value = name, val
if match_name is not None:
return match_value
if "default" in fields:
Expand All @@ -338,6 +351,7 @@ def _resolve_active_presets(
*,
strict_explicit: bool = True,
consumed_selected: set[str] | None = None,
consumed_selected_targets: dict[str, set[PresetTarget]] | None = None,
consumed_explicit: set[str] | None = None,
):
"""Resolve presets by walking only the currently active tree.
Expand All @@ -364,6 +378,7 @@ def resolve_chain(preset_obj: PresetCfg, path: str):
path=path,
explicit_name=explicit.get(path),
consumed_selected=consumed_selected,
consumed_selected_targets=consumed_selected_targets,
)
return val

Expand Down Expand Up @@ -535,6 +550,36 @@ def _format_unknown_presets_error(unknown: set[str], name_to_paths: dict[str, li
return "\n".join(lines)


def _selected_newton_physics_presets(selected: list[str]) -> set[str]:
"""Return canonical Newton physics preset names requested by the user."""
aliases = PresetTarget.PHYSICS.legacy_aliases
return {
name
for selected_name in selected
for name in [aliases.get(selected_name, selected_name)]
if name in _NEWTON_PHYSICS_PRESETS
}


def _validate_selected_newton_physics_presets(
selected: list[str],
consumed_selected: set[str],
consumed_selected_targets: dict[str, set[PresetTarget]],
) -> None:
"""Raise when a Newton solver preset did not apply to active physics config."""
missing = sorted(
name
for name in _selected_newton_physics_presets(selected)
if name in consumed_selected and PresetTarget.PHYSICS not in consumed_selected_targets.get(name, set())
)
if missing:
raise ValueError(
f"Selected Newton physics preset(s) {', '.join(missing)} did not match any active physics preset. "
"This task does not declare Newton physics support for the selected solver. "
"Remove the Newton preset or choose a task that declares it on a PhysicsCfg."
)
Comment on lines +570 to +580
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Newton validation fires before unknown-preset check, producing misleading error

_validate_selected_newton_physics_presets runs before the unknown_presets block (line 634). When a task has no newton_mjwarp entry whatsoever — not even a scalar actuator preset — consumed_preset_targets contains no entry for that name, so the Newton validator raises "did not match any active physics preset." The unknown_presets check, which would emit the clearer "Unknown preset(s): newton_mjwarp" message, never runs.

Adding a guard if name in consumed_presets inside the missing comprehension restricts the Newton error to the intended case (preset was applied somewhere but never to a PhysicsCfg), leaving truly-unknown names for the existing unknown-preset error path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6971203. The validator now only raises for Newton physics preset names that were consumed somewhere but did not resolve through a PHYSICS target, so truly unknown names continue to use the existing Unknown preset(s) error path.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice — that distinction (consumed-but-not-resolved via PHYSICS vs truly-unknown) cleanly addresses the concern. 👍



def register_task(task_name: str, agent_entry: str) -> tuple:
"""Load configs, collect presets recursively, register base config to Hydra.

Expand Down Expand Up @@ -566,6 +611,7 @@ def register_task(task_name: str, agent_entry: str) -> tuple:

explicit = {key: val for key, val, _arg in override_items}
consumed_presets: set[str] = set()
consumed_preset_targets: dict[str, set[PresetTarget]] = {}
consumed_explicit: set[str] = set()
env_explicit = {path: name for path, name in explicit.items() if path == "env" or path.startswith("env.")}
agent_explicit = {path: name for path, name in explicit.items() if path == "agent" or path.startswith("agent.")}
Expand All @@ -576,6 +622,7 @@ def register_task(task_name: str, agent_entry: str) -> tuple:
root_path="env",
strict_explicit=False,
consumed_selected=consumed_presets,
consumed_selected_targets=consumed_preset_targets,
consumed_explicit=consumed_explicit,
)
if agent_cfg is not None:
Expand All @@ -586,9 +633,12 @@ def register_task(task_name: str, agent_entry: str) -> tuple:
root_path="agent",
strict_explicit=False,
consumed_selected=consumed_presets,
consumed_selected_targets=consumed_preset_targets,
consumed_explicit=consumed_explicit,
)

_validate_selected_newton_physics_presets(global_presets, consumed_presets, consumed_preset_targets)

unknown_presets = set(global_presets) - consumed_presets
if unknown_presets:
# Build the full discovery table only on the error path, or when a
Expand Down
Loading