Skip to content
Merged
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,62 @@
Added
^^^^^

* Added :attr:`~isaaclab.cloner.CloneCfg.clone_template` for the replicated environment prim path,
with ``{}`` marking the environment index. It replaces ``CloneCfg.clone_regex``, whose value is
now ``clone_template.format("[^/]+")``.
* Added an ``env_template`` argument to :func:`~isaaclab.cloner.make_clone_plan` and
:class:`~isaaclab.cloner.ReplicateSession`.
* Added :func:`~isaaclab.sim.utils.path_expr_to_glob` and
:func:`~isaaclab.sim.utils.split_path_expr`, for converting a prim path expression to the glob
the physics engines accept and for splitting one without cutting a character class in half.
* Added :func:`~isaaclab.cloner.expand_env_regex_ns`, and applied it when an asset or a sensor is
constructed. ``{ENV_REGEX_NS}`` previously only resolved for assets a
:class:`~isaaclab.scene.InteractiveScene` collected, so a direct environment -- which builds its
own -- had to spell the namespace out. Either kind may now use the macro, and no configuration
has to name the wildcard that selects one environment.

Changed
^^^^^^^

* **Breaking:** Changed :func:`~isaaclab.sim.utils.find_matching_prims` to match the whole prim
path as a plain regular expression instead of one token per path segment. ``.`` now matches
``/``, so ``/World/Robot/.*`` selects descendants at any depth; use ``[^/]+`` for a single
segment. Unscoped queries test every authored prim, including inactive and undefined prims and
instance proxies, without inferring a traversal root or depth limit from the expression.
Clone-aware discovery instead rebases the expression through the active clone plan and searches
only its concrete source subtree, never every cloned destination environment.
* Changed :func:`~isaaclab.sim.utils.find_first_matching_prim` to delegate to
:func:`~isaaclab.sim.utils.find_matching_prims`, so both read an expression the same way.
* Changed the environment namespace to spell its slot ``[^/]+`` rather than ``.*``, so
``{ENV_REGEX_NS}/Robot`` no longer also selects a ``Robot`` nested deeper under an environment.
* Changed :func:`~isaaclab.cloner.path.match` to accept a character class in the clone slot, so a
segment-safe namespace resolves against a destination template.

* Changed prim path expressions throughout the repository to spell a single path segment
``[^/]`` rather than ``.``, so each pattern selects what it selected before now that ``.``
matches ``/``.

Removed
^^^^^^^

* Removed the legacy glob-wildcard rewrite from prim path expressions. A bare ``*`` is a regular
expression quantifier and is no longer rewritten to ``.*``; the rewrite could not tell a glob
star from a quantifier and corrupted ``[^/]*`` into ``[^/].*``. Patterns relying on ``*`` as a
standalone wildcard should spell it ``.*`` (any depth) or ``[^/]*`` (one path segment).

Fixed
^^^^^

* Fixed :func:`~isaaclab.cloner.make_clone_plan` raising ``IndexError`` for a prim path holding
more than one wildcard, and ignoring a non-default environment namespace.
* Fixed :class:`~isaaclab.sensors.MultiMeshRayCaster` expanding ``{ENV_REGEX_NS}`` with a
hardcoded namespace instead of the shared default.
* Fixed callers that split a prim path expression on ``/`` cutting a ``[^/]`` character class in
half, which raised ``re.error: unterminated character set`` or produced a truncated body name.
* Fixed :func:`~isaaclab.sim.spawn_multi_asset` rejecting an index slot spelled ``[^/]*``; the
slot is now any segment wildcard rather than a literal ``.*``.
* Fixed callers that substituted a concrete environment index into a path expression by matching
one spelling of the environment slot, so a namespace written with a different quantifier was
left unresolved: the visualizer camera view, and the deformable render bindings.
* Fixed :func:`~isaaclab.cloner.query.path_to_source` reporting its destination as a glob, which
matched nothing when a caller used it as the path expression its name promises.
5 changes: 5 additions & 0 deletions source/isaaclab/isaaclab/assets/asset_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import isaaclab.sim as sim_utils
from isaaclab.cloner import queue_replication
from isaaclab.cloner.cloner_cfg import expand_env_regex_ns
from isaaclab.physics import PhysicsEvent, PhysicsManager
from isaaclab.sim.simulation_context import SimulationContext
from isaaclab.sim.utils.stage import get_current_stage
Expand Down Expand Up @@ -97,6 +98,10 @@ def __init__(self, cfg: AssetBaseCfg):
"""
# check that the config is valid
cfg.validate()
# expand the namespace macro before the cfg is queued, so the clone plan keys its rows
# by a real path expression. The scene has already done this for the assets it collects;
# this covers the ones a direct environment builds itself.
cfg.prim_path = expand_env_regex_ns(cfg.prim_path)
# register the original cfg object for cloning: the clone plan keys rows by the
# cfg identity the scene collected; contexts and policy resolve at replication time
queue_replication(cfg)
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/isaaclab/assets/asset_base_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class InitialStateCfg:
The expression can contain the environment namespace regex ``{ENV_REGEX_NS}`` which
will be replaced with the environment namespace.

Example: ``{ENV_REGEX_NS}/Robot`` will be replaced with ``/World/envs/env_.*/Robot``.
Example: ``{ENV_REGEX_NS}/Robot`` will be replaced with ``/World/envs/env_[^/]+/Robot``.
"""

spawn: SpawnerCfg | None = None
Expand Down
3 changes: 2 additions & 1 deletion source/isaaclab/isaaclab/cloner/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ __all__ = [
"add",
"clone_plan_from_env_0",
"disabled_fabric_change_notifies",
"expand_env_regex_ns",
"filter_collisions",
"grid_transforms",
"make_clone_plan",
Expand Down Expand Up @@ -37,7 +38,7 @@ from .clone_plan import (
make_valid_clone_combinations,
num_spawn_variants,
)
from .cloner_cfg import CloneCfg, InclusionSet, add
from .cloner_cfg import CloneCfg, InclusionSet, add, expand_env_regex_ns
from .cloner_strategies import random, sequential
from .collision_filter import filter_collisions
from .replicate_session import (
Expand Down
16 changes: 6 additions & 10 deletions source/isaaclab/isaaclab/cloner/clone_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@

import isaaclab.sim as sim_utils

from .cloner_cfg import InclusionSet
from .cloner_cfg import DEFAULT_ENV_TEMPLATE, InclusionSet
from .cloner_strategies import sequential
from .path import split
from .path import match


@dataclass(frozen=True, eq=False)
Expand Down Expand Up @@ -224,6 +224,7 @@ def make_clone_plan(
*,
clone_strategy: Callable = sequential,
valid_set: torch.Tensor | None = None,
env_template: str = DEFAULT_ENV_TEMPLATE,
) -> ClonePlan:
"""Build a :class:`ClonePlan` from asset cfgs.

Expand Down Expand Up @@ -266,22 +267,18 @@ def set_spawn_paths(spawn_cfg: Any, paths: list[str | None]) -> None:
raise ValueError("Single spawner expects exactly one planned source path.")
spawn_cfg.spawn_path = active[0]

env_root_marker = "/World/envs/"
env_template = "/World/envs/env_{}"

# 1) Build per-group records: (cfg, spawn_cfg, destination_template, num_variants).
groups: list[tuple[Any, Any, str, int]] = []
for cfg in cfgs:
if not hasattr(cfg, "prim_path") or not hasattr(cfg, "spawn") or cfg.spawn is None:
continue
prim_path = cfg.prim_path
if env_root_marker not in prim_path:
if (matched := match(prim_path, env_template)) is None:
continue
count = num_spawn_variants(cfg.spawn)
if count <= 0:
raise ValueError(f"Spawner at '{prim_path}' must have at least one variant.")
destination = prim_path.replace(".*", "{}")
groups.append((cfg, cfg.spawn, destination, count))
groups.append((cfg, cfg.spawn, env_template + matched.suffix, count))

env_ids = torch.arange(num_clones, dtype=torch.long, device=device)
positions, _ = grid_transforms(num_clones, env_spacing, device=device)
Expand Down Expand Up @@ -408,9 +405,8 @@ def clone_plan_from_env_0(
"""
from .replicate_session import REPLICATION_QUEUE # noqa: PLC0415

prefix, _ = split(destination)
cfg_rows: dict[int, tuple[int, ...]] = {
id(cfg): (0,) for cfg in REPLICATION_QUEUE if cfg.prim_path.startswith(prefix)
id(cfg): (0,) for cfg in REPLICATION_QUEUE if match(cfg.prim_path, destination) is not None
}
return ClonePlan(
sources=(source,),
Expand Down
30 changes: 28 additions & 2 deletions source/isaaclab/isaaclab/cloner/cloner_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@

from .cloner_strategies import sequential

DEFAULT_ENV_TEMPLATE = "/World/envs/env_{}"
"""Default path template for a replicated env prim; ``{}`` marks the environment index."""


def expand_env_regex_ns(path_expr: str, env_template: str = DEFAULT_ENV_TEMPLATE) -> str:
"""Replace the ``{ENV_REGEX_NS}`` macro with the environment namespace it stands for.

The macro spares a configuration from spelling the namespace, and with it the segment
wildcard that names one environment. :class:`~isaaclab.scene.InteractiveScene` expands it
against its own template for the assets it collects; assets built outside the scene (a
direct environment builds its own) go through here instead.

Args:
path_expr: Prim path expression, with or without the macro.
env_template: Environment path template whose ``{}`` marks the environment index.

Returns:
``path_expr`` with the macro replaced, unchanged when it holds no macro.
"""
# a plain replace, not str.format: the rest of the expression may hold braces of its own
return path_expr.replace("{ENV_REGEX_NS}", env_template.format("[^/]+"))


@configclass
class InclusionSet:
Expand Down Expand Up @@ -46,8 +68,12 @@ class CloneCfg:
device: str = "cpu"
"""Torch device on which mapping buffers are allocated."""

clone_regex: str = "/World/envs/env_.*"
"""Regex matching every replicated env prim. Used to expand ``{ENV_REGEX_NS}`` cfg macros."""
clone_template: str = DEFAULT_ENV_TEMPLATE
Comment thread
ooctipus marked this conversation as resolved.
"""Path template for every replicated env prim, where ``{}`` is the environment index.

The regex form used to expand ``{ENV_REGEX_NS}`` cfg macros is
``clone_template.format("[^/]+")``, which confines the slot to one path segment.
"""

replicate_physics: bool = True
"""Whether physics replication clones each environment. Default is True.
Expand Down
9 changes: 6 additions & 3 deletions source/isaaclab/isaaclab/cloner/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ def split(template: str) -> tuple[str, str]:
def match(path_expr: str, template: str) -> TemplateMatch | None:
"""Match ``path_expr`` against a destination template, capturing the instance slot.

The ``"{}"`` slot matches one path segment's worth of text, whether a concrete id (``3``)
or a wildcard (``.*``). Recovering that text is the only way to tell which instance a
The ``"{}"`` slot matches one path segment's worth of text: a concrete id (``3``) or a
wildcard standing for one segment (``.*``, ``[^/]+``). Recovering that text is the only way to tell which instance a
concrete clone path belongs to without slicing the string by hand.

Args:
Expand All @@ -73,7 +73,10 @@ def match(path_expr: str, template: str) -> TemplateMatch | None:
TemplateMatch(instance='3', suffix='/base')
"""
prefix, template_suffix = split(template)
pattern = re.compile(re.escape(prefix) + r"([^/]+)" + re.escape(template_suffix))
# the slot holds one segment's worth of text: a concrete id, or a wildcard standing for one.
# A segment-safe wildcard is written as a character class, whose text contains a '/' that is
# not a separator, so it is matched as a class rather than by the one-segment alternative.
pattern = re.compile(re.escape(prefix) + r"(\[\^?[^]]*\][*+?]?|[^/]+)" + re.escape(template_suffix))
matched = pattern.match(path_expr)
if matched is None:
return None
Expand Down
21 changes: 11 additions & 10 deletions source/isaaclab/isaaclab/cloner/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,25 +162,26 @@ def path_to_source(plan: ClonePlan, path_expr: str, env_id: int | None = None) -
A *concrete* clone path names its environment in the template's clone slot, and that
environment selects which variant to report — which is what lets this undo
:func:`path_to_clone` for a heterogeneous asset. A *wildcard* expression
(``.../env_.*/...``) names no environment and stands for all of them, so it resolves to
(``.../env_[^/]+/...``) names no environment and stands for all of them, so it resolves to
the first populated variant unless ``env_id`` says which one to take.

Args:
plan: Active clone plan.
path_expr: Clone-side path expression (e.g. a sensor's ``prim_path``, with ``.*`` env
wildcard) or a concrete clone path.
path_expr: Clone-side path expression (e.g. a sensor's ``prim_path``, with a segment
wildcard in the env slot) or a concrete clone path.
env_id: Environment whose variant to resolve. Defaults to the one ``path_expr`` names
when it is concrete, and to no particular environment otherwise.

Returns:
A ``(source_path, destination_glob, asset_suffix)`` tuple, where ``asset_suffix`` is
the part of ``path_expr`` below the owning template. ``None`` when ``path_expr``
matches no row, or no matching row populates the requested environment, letting
callers fall back to direct stage resolution.
A ``(source_path, destination_expr, asset_suffix)`` tuple, where ``destination_expr``
spells the clone slot ``[^/]+`` so it reads as a path expression like every other one,
and ``asset_suffix`` is the part of ``path_expr`` below the owning template. ``None``
when ``path_expr`` matches no row, or no matching row populates the requested
environment, letting callers fall back to direct stage resolution.

Partial-env coverage is supported: when the matching rows cover only a subset of envs
(an asset present in some envs but not others, as in heterogeneous scenes), the
returned glob resolves to just those envs.
returned expression resolves to just those envs.

Raises:
ValueError: When ``path_expr`` is owned by multiple distinct, equally near templates.
Expand All @@ -202,7 +203,7 @@ def path_to_source(plan: ClonePlan, path_expr: str, env_id: int | None = None) -
rows = [row for row in rows if bool(plan.clone_mask[row][column])]
if not rows:
return None
return plan.sources[rows[0]], template.replace("{}", "*"), matched.suffix
return plan.sources[rows[0]], template.format("[^/]+"), matched.suffix


def iter_sources(plan: ClonePlan, path_expr: str) -> Iterator[tuple[str, str, str, tuple[int, ...]]]:
Expand All @@ -214,7 +215,7 @@ def iter_sources(plan: ClonePlan, path_expr: str) -> Iterator[tuple[str, str, st
Example:
For a row with prototype root ``"/World/source/Robot"``, destination template
``"/World/scenes/{}/Robot"`` and env ids ``(0, 2)``, querying
``"/World/scenes/.*/Robot/base"`` yields ``("/World/source/Robot",
``"/World/scenes/[^/]+/Robot/base"`` yields ``("/World/source/Robot",
"/World/scenes/{}/Robot", "/World/source/Robot/base", (0, 2))``.

Args:
Expand Down
4 changes: 4 additions & 0 deletions source/isaaclab/isaaclab/cloner/replicate_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from isaaclab.utils.version import has_kit

from .clone_plan import make_clone_plan
from .cloner_cfg import DEFAULT_ENV_TEMPLATE
from .cloner_strategies import sequential
from .usd import UsdReplicateContext

Expand Down Expand Up @@ -141,6 +142,7 @@ def __init__(
clone_strategy: Callable = sequential,
valid_set: torch.Tensor | None = None,
replicate_physics: bool = True,
env_template: str = DEFAULT_ENV_TEMPLATE,
):
"""Capture arguments for :func:`make_clone_plan` and :func:`replicate`.

Expand All @@ -155,6 +157,7 @@ def __init__(
prototype combinations; ``None`` uses the full cartesian product.
replicate_physics: Whether physics replication clones each environment;
forwarded to :func:`replicate`.
env_template: Path template for a replicated env prim, ``{}`` marking the env index.
"""
self._cfgs = cfgs
self._stage = stage
Expand All @@ -165,6 +168,7 @@ def __init__(
device=device,
clone_strategy=clone_strategy,
valid_set=valid_set,
env_template=env_template,
)
self._plan: ClonePlan | None = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ def has_rigid_body_api(prim) -> bool:
if not rigid_matches:
raise ValueError(f"No descendant rigid body found under the expression: '{self._asset.cfg.prim_path}'.")
_, root_rigidbody_path = rigid_matches[0]
task_frame_transformer_path = "/World/envs/env_.*/" + self.cfg.task_frame_rel_path
task_frame_transformer_path = f"{self._env.scene.env_regex_ns}/{self.cfg.task_frame_rel_path}"
task_frame_transformer_cfg = FrameTransformerCfg(
prim_path=root_rigidbody_path,
target_frames=[
Expand Down
20 changes: 6 additions & 14 deletions source/isaaclab/isaaclab/envs/mdp/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2560,20 +2560,16 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv):

# join all bodies in the asset
body_names = asset_cfg.body_names
if isinstance(body_names, str):
body_names_regex = body_names
elif isinstance(body_names, list):
body_names_regex = "|".join(body_names)
else:
body_names_regex = ".*"
body_names_regex = "|".join(body_names) if isinstance(body_names, list) else body_names
body_names_regex = f"(?:{body_names_regex})" if isinstance(body_names_regex, str) else ".*"

# create the affected prim path
# Check if the pattern with '/visuals' yields results when matching `body_names_regex`.
# If not, fall back to a broader pattern without '/visuals'.
asset_main_prim_path = asset.cfg.prim_path
pattern_with_visuals = f"{asset_main_prim_path}/{body_names_regex}/visuals"
# Use sim_utils to check if any prims currently match this pattern
matching_prims = sim_utils.find_matching_prim_paths(pattern_with_visuals)
matching_prims = sim_utils.resolve_matching_prims_from_source(pattern_with_visuals, raise_if_no_matches=False)
if matching_prims:
# If matches are found, use the pattern with /visuals
prim_path = pattern_with_visuals
Expand Down Expand Up @@ -2751,14 +2747,10 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv):
else:
# default: the configured bodies' visual meshes
body_names = asset_cfg.body_names
if isinstance(body_names, str):
body_names_regex = body_names
elif isinstance(body_names, list):
body_names_regex = "|".join(body_names)
else:
body_names_regex = ".*"
body_names_regex = "|".join(body_names) if isinstance(body_names, list) else body_names
body_names_regex = f"(?:{body_names_regex})" if isinstance(body_names_regex, str) else ".*"
pattern_with_visuals = f"{asset.cfg.prim_path}/{body_names_regex}/visuals"
if sim_utils.find_matching_prim_paths(pattern_with_visuals):
if sim_utils.resolve_matching_prims_from_source(pattern_with_visuals, raise_if_no_matches=False):
mesh_prim_path = pattern_with_visuals
else:
# fall back to any descendant if the asset has no ".../visuals" layout
Expand Down
Loading
Loading