# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Discoverable command factory for component micro-benchmarks."""
from __future__ import annotations
import argparse
import sys
from dataclasses import dataclass
from pathlib import Path
from types import MappingProxyType
from isaaclab.cli.utils import run_python_command
[docs]
@dataclass(frozen=True)
class MicrobenchmarkCommand:
"""Resolved micro-benchmark child command.
Attributes:
physics: Exact physics variant selected for the workload.
component: Component workload to benchmark.
script: Benchmark entrypoint to execute.
args: Arguments passed to the benchmark entrypoint.
"""
physics: str
component: str
script: Path
args: list[str]
@dataclass(frozen=True)
class _PhysicsDescriptor:
"""Lightweight workload descriptor for one exact physics variant."""
package: str
variant: str
[docs]
class MicrobenchmarkFactory:
"""Resolve exact physics variants and components to benchmark entrypoints."""
_PHYSICS = MappingProxyType(
{
"physx": _PhysicsDescriptor("isaaclab_physx", "physx"),
"ovphysx": _PhysicsDescriptor("isaaclab_ov", "ovphysx"),
"newton_mjwarp": _PhysicsDescriptor("isaaclab_newton", "newton_mjwarp"),
"newton_kamino": _PhysicsDescriptor("isaaclab_newton", "newton_kamino"),
}
)
_ASSET_COMPONENTS = frozenset({"articulation", "rigid_object", "rigid_object_collection"})
_SENSOR_COMPONENTS = frozenset({"contact_sensor", "frame_transformer", "imu", "pva", "joint_wrench", "ray_caster"})
[docs]
@classmethod
def repository_root(cls) -> Path:
"""Return the Isaac Lab repository root.
Returns:
Repository root containing the backend benchmark entrypoints.
"""
return Path(__file__).parents[4]
[docs]
@classmethod
def physics_variants(cls) -> tuple[str, ...]:
"""Return discoverable exact physics variants.
Returns:
Exact physics selectors accepted by :meth:`build_command`.
"""
return tuple(cls._PHYSICS)
[docs]
@classmethod
def components(cls) -> tuple[str, ...]:
"""Return discoverable component workloads.
Returns:
Sorted component names accepted by :meth:`build_command`.
"""
return tuple(sorted(cls._ASSET_COMPONENTS | cls._SENSOR_COMPONENTS))
[docs]
def build_command(self, physics: str, component: str, passthrough_args: list[str]) -> MicrobenchmarkCommand:
"""Resolve one exact physics/component selection.
Args:
physics: Exact physics variant.
component: Asset or sensor component workload.
passthrough_args: Additional arguments for the selected entrypoint.
Returns:
Child command for the selected workload.
Raises:
ValueError: If the physics variant or component is unknown, or if
:paramref:`passthrough_args` overrides the selected variant.
"""
if any(arg == "--physics_variant" or arg.startswith("--physics_variant=") for arg in passthrough_args):
raise ValueError("--physics_variant is reserved; select the variant with physics=<variant>.")
try:
descriptor = self._PHYSICS[physics]
except KeyError as exc:
available = ", ".join(self.physics_variants())
raise ValueError(f"Unknown physics variant '{physics}'. Available variants: {available}.") from exc
if component not in self._ASSET_COMPONENTS | self._SENSOR_COMPONENTS:
available = ", ".join(self.components())
raise ValueError(f"Unknown component '{component}'. Available components: {available}.")
group = "assets" if component in self._ASSET_COMPONENTS else "sensors"
script_component = "imu_pva" if component in {"imu", "pva"} else component
script = (
self.repository_root()
/ "source"
/ descriptor.package
/ "benchmark"
/ group
/ f"benchmark_{script_component}.py"
)
child_args = ["--physics_variant", descriptor.variant]
if component in {"imu", "pva"}:
child_args.extend(["--sensor", component])
child_args.extend(passthrough_args)
return MicrobenchmarkCommand(descriptor.variant, component, script, child_args)
def _create_parser(factory: MicrobenchmarkFactory, *, add_help: bool = True) -> argparse.ArgumentParser:
"""Create the top-level micro-benchmark parser."""
variants = ", ".join(factory.physics_variants())
components = ", ".join(factory.components())
return argparse.ArgumentParser(
prog="isaaclab microbenchmark",
description="Run one component micro-benchmark with an exact physics variant.",
epilog=f"physics variants: {variants}\ncomponents: {components}",
formatter_class=argparse.RawTextHelpFormatter,
add_help=add_help,
)
[docs]
def run_microbenchmark_cli(args: list[str] | None = None) -> int:
"""Parse and run ``isaaclab microbenchmark``.
Args:
args: Command arguments. Uses :data:`sys.argv` when ``None``.
Returns:
Zero after the child benchmark completes successfully.
Raises:
SystemExit: If command arguments are invalid.
subprocess.CalledProcessError: If the child benchmark fails.
"""
factory = MicrobenchmarkFactory()
raw_args = list(args) if args is not None else sys.argv[1:]
forward_help = any(arg in ("-h", "--help") for arg in raw_args)
if forward_help:
has_component = any(arg == "--component" or arg.startswith("--component=") for arg in raw_args)
has_physics = any(arg.startswith("physics=") for arg in raw_args)
forward_help = has_component and has_physics
parser = _create_parser(factory, add_help=not forward_help)
parser.add_argument("--component", required=True, choices=factory.components())
parser.add_argument("physics", help="Exact selector in the form physics=<variant>.")
parsed, passthrough = parser.parse_known_args(raw_args)
physics = parsed.physics.removeprefix("physics=")
if physics == parsed.physics or not physics:
parser.error("physics must use the form physics=<variant>")
try:
command = factory.build_command(physics, parsed.component, passthrough)
except ValueError as exc:
parser.error(str(exc))
run_python_command(command.script, command.args, check=True)
return 0