nepher 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nepher/__init__.py +36 -0
- nepher/api/__init__.py +6 -0
- nepher/api/client.py +384 -0
- nepher/api/endpoints.py +97 -0
- nepher/auth.py +150 -0
- nepher/cli/__init__.py +2 -0
- nepher/cli/commands/__init__.py +6 -0
- nepher/cli/commands/auth.py +37 -0
- nepher/cli/commands/cache.py +85 -0
- nepher/cli/commands/config.py +77 -0
- nepher/cli/commands/download.py +72 -0
- nepher/cli/commands/list.py +75 -0
- nepher/cli/commands/upload.py +69 -0
- nepher/cli/commands/view.py +310 -0
- nepher/cli/main.py +30 -0
- nepher/cli/utils.py +28 -0
- nepher/config.py +202 -0
- nepher/core.py +67 -0
- nepher/env_cfgs/__init__.py +7 -0
- nepher/env_cfgs/base.py +32 -0
- nepher/env_cfgs/manipulation/__init__.py +4 -0
- nepher/env_cfgs/navigation/__init__.py +45 -0
- nepher/env_cfgs/navigation/abstract_nav_cfg.py +159 -0
- nepher/env_cfgs/navigation/preset_nav_cfg.py +590 -0
- nepher/env_cfgs/navigation/usd_nav_cfg.py +644 -0
- nepher/env_cfgs/registry.py +31 -0
- nepher/loader/__init__.py +9 -0
- nepher/loader/base.py +27 -0
- nepher/loader/category_loaders/__init__.py +2 -0
- nepher/loader/preset_loader.py +80 -0
- nepher/loader/registry.py +63 -0
- nepher/loader/usd_loader.py +49 -0
- nepher/storage/__init__.py +8 -0
- nepher/storage/bundle.py +78 -0
- nepher/storage/cache.py +145 -0
- nepher/storage/manifest.py +80 -0
- nepher/utils/__init__.py +12 -0
- nepher/utils/fast_spawn_sampler.py +334 -0
- nepher/utils/free_zone_finder.py +239 -0
- nepher-0.1.0.dist-info/METADATA +235 -0
- nepher-0.1.0.dist-info/RECORD +45 -0
- nepher-0.1.0.dist-info/WHEEL +5 -0
- nepher-0.1.0.dist-info/entry_points.txt +2 -0
- nepher-0.1.0.dist-info/licenses/LICENSE +97 -0
- nepher-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
# Copyright (c) 2025, Nepher Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
|
|
6
|
+
"""Base environment preset configuration with manually defined obstacles.
|
|
7
|
+
|
|
8
|
+
This preset supports environments with obstacles defined either as:
|
|
9
|
+
- Geometric primitives (cuboids) with configurable size, position, and color
|
|
10
|
+
- USD file assets with configurable position and scale
|
|
11
|
+
|
|
12
|
+
The preset uses free zone computation to ensure safe robot and goal positioning
|
|
13
|
+
in obstacle-free areas.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
|
|
21
|
+
import torch
|
|
22
|
+
|
|
23
|
+
import isaaclab.sim as sim_utils
|
|
24
|
+
from isaaclab.assets import AssetBaseCfg
|
|
25
|
+
from isaaclab.terrains import TerrainImporterCfg
|
|
26
|
+
from isaaclab.utils import configclass
|
|
27
|
+
|
|
28
|
+
from nepher.env_cfgs.navigation.abstract_nav_cfg import AbstractNavigationEnvCfg
|
|
29
|
+
from nepher.utils.free_zone_finder import FreeZone, Rectangle, find_free_zones
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ObstacleConfig:
|
|
34
|
+
"""Configuration for a single obstacle.
|
|
35
|
+
|
|
36
|
+
Obstacles can be defined as either:
|
|
37
|
+
- A cuboid primitive with size and color (when usd_path is None)
|
|
38
|
+
- A USD file asset (when usd_path is provided)
|
|
39
|
+
|
|
40
|
+
Obstacles can be static (default) or dynamic. Dynamic obstacles can be moved
|
|
41
|
+
programmatically during simulation and respond to physics forces.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Position in world coordinates (x, y, z)
|
|
45
|
+
position: tuple[float, float, float] = (2.0, 0.0, 0.25)
|
|
46
|
+
"""World position of the obstacle center (x, y, z)."""
|
|
47
|
+
|
|
48
|
+
# Cuboid configuration (used when usd_path is None)
|
|
49
|
+
size: tuple[float, float, float] = (0.5, 0.5, 0.5)
|
|
50
|
+
"""Size of cuboid obstacle (width, depth, height). Used when usd_path is None."""
|
|
51
|
+
|
|
52
|
+
color: tuple[float, float, float] = (0.8, 0.2, 0.2)
|
|
53
|
+
"""RGB color for cuboid obstacle. Used when usd_path is None."""
|
|
54
|
+
|
|
55
|
+
# USD asset configuration (used when usd_path is provided)
|
|
56
|
+
usd_path: str | None = None
|
|
57
|
+
"""Path to USD file for obstacle asset. If None, uses cuboid primitive."""
|
|
58
|
+
|
|
59
|
+
usd_scale: tuple[float, float, float] = (1.0, 1.0, 1.0)
|
|
60
|
+
"""Scale factor for USD asset (x, y, z). Used when usd_path is provided."""
|
|
61
|
+
|
|
62
|
+
# Dynamic obstacle configuration
|
|
63
|
+
is_dynamic: bool = False
|
|
64
|
+
"""Whether this obstacle is dynamic (can move). If True, kinematic_enabled=False."""
|
|
65
|
+
|
|
66
|
+
include_in_static_layout: bool = True
|
|
67
|
+
"""Whether to include this obstacle in static layout calculations (free zones, collision checking).
|
|
68
|
+
Set to False for fully dynamic obstacles that move unpredictably. Default True includes
|
|
69
|
+
the obstacle at its initial position for planning purposes."""
|
|
70
|
+
|
|
71
|
+
# Path-based movement configuration (for dynamic obstacles)
|
|
72
|
+
path_waypoints: list[tuple[float, float]] | None = None
|
|
73
|
+
"""List of (x, y) waypoints defining the path for dynamic obstacles.
|
|
74
|
+
If None and is_dynamic=True, obstacle stays at initial position."""
|
|
75
|
+
|
|
76
|
+
movement_speed: float = 0.5
|
|
77
|
+
"""Movement speed along path in m/s. Only used if path_waypoints is provided."""
|
|
78
|
+
|
|
79
|
+
path_loop: bool = True
|
|
80
|
+
"""If True, obstacle loops back to start after reaching end. If False, reverses direction (ping-pong)."""
|
|
81
|
+
|
|
82
|
+
initial_path_progress: float = 0.0
|
|
83
|
+
"""Initial progress along path (0.0 = start, 1.0 = end). Used for randomization/staggering."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@configclass
|
|
87
|
+
class PresetNavigationEnvCfg(AbstractNavigationEnvCfg):
|
|
88
|
+
"""Base configuration for environment presets with manually defined obstacles.
|
|
89
|
+
|
|
90
|
+
This preset is designed for environments where obstacles are explicitly defined
|
|
91
|
+
in the configuration. It supports both geometric primitives (cuboids) and USD
|
|
92
|
+
file assets as obstacles.
|
|
93
|
+
|
|
94
|
+
The preset automatically computes free zones from obstacles and uses them for
|
|
95
|
+
safe robot and goal positioning. It validates positions to ensure they don't
|
|
96
|
+
overlap with obstacles or go out of playground bounds.
|
|
97
|
+
|
|
98
|
+
Performance: Free zones are cached after first computation to avoid expensive
|
|
99
|
+
recomputation on every reset. Position sampling is vectorized for batch efficiency.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
# Preset identification
|
|
103
|
+
name: str = "obstacle_preset"
|
|
104
|
+
description: str = "Base environment preset with manually defined obstacles"
|
|
105
|
+
category: str = "navigation"
|
|
106
|
+
|
|
107
|
+
# ========== Cached Data (computed lazily, not serialized) ==========
|
|
108
|
+
# These are runtime caches, not configuration fields
|
|
109
|
+
_cached_free_zones: list[FreeZone] | None = None
|
|
110
|
+
_cached_zone_bounds: torch.Tensor | None = None # Shape: (num_zones, 4) for x_min, y_min, x_max, y_max
|
|
111
|
+
_cached_zone_probs: torch.Tensor | None = None # Shape: (num_zones,) for area-weighted sampling
|
|
112
|
+
|
|
113
|
+
# Terrain configuration
|
|
114
|
+
terrain_type: str = "plane"
|
|
115
|
+
"""Type of terrain: 'plane' for flat terrain, 'generator' for generated terrain."""
|
|
116
|
+
|
|
117
|
+
terrain_friction: float = 1.0
|
|
118
|
+
"""Static and dynamic friction coefficient for terrain."""
|
|
119
|
+
|
|
120
|
+
terrain_restitution: float = 0.0
|
|
121
|
+
"""Restitution coefficient for terrain collisions."""
|
|
122
|
+
|
|
123
|
+
terrain_usd_path: str | None = None
|
|
124
|
+
"""Path to USD file for terrain mesh. If None, uses terrain_type."""
|
|
125
|
+
|
|
126
|
+
env_spacing: float = 20.0
|
|
127
|
+
"""Environment spacing for grid-like origins (meters). Used for multi-environment layouts."""
|
|
128
|
+
|
|
129
|
+
# Obstacles configuration
|
|
130
|
+
obstacles: list[ObstacleConfig] = field(default_factory=list)
|
|
131
|
+
"""List of obstacle configurations. Can include cuboids and USD assets."""
|
|
132
|
+
|
|
133
|
+
# Playground bounds (x_min, y_min, x_max, y_max) in world coordinates
|
|
134
|
+
# If None, playground is auto-computed from obstacles with margin
|
|
135
|
+
playground: tuple[float, float, float, float] | None = None
|
|
136
|
+
"""Playground boundary (x_min, y_min, x_max, y_max). If None, auto-computed."""
|
|
137
|
+
|
|
138
|
+
# Free zone computation parameters
|
|
139
|
+
playground_margin: float = 2.0
|
|
140
|
+
"""Margin for auto-computed playground (meters). Used when playground is None."""
|
|
141
|
+
|
|
142
|
+
min_zone_size: float = 0.7
|
|
143
|
+
"""Minimum dimension (width and height) of a free zone (meters)."""
|
|
144
|
+
|
|
145
|
+
max_zones: int | None = None
|
|
146
|
+
"""Maximum number of free zones to compute. If None, no limit."""
|
|
147
|
+
|
|
148
|
+
clearance: float = 0.05
|
|
149
|
+
"""Clearance margin to shrink free zones by for safety (meters)."""
|
|
150
|
+
|
|
151
|
+
robot_safety_margin: float = 0.25
|
|
152
|
+
"""Additional safety margin around robot for position generation (meters)."""
|
|
153
|
+
|
|
154
|
+
# Robot initial position ranges (used as fallback when free zones unavailable)
|
|
155
|
+
robot_init_pos_x_range: tuple[float, float] = (-0.5, 0.5)
|
|
156
|
+
"""Range for robot initial x position. Used as fallback."""
|
|
157
|
+
|
|
158
|
+
robot_init_pos_y_range: tuple[float, float] = (-0.5, 0.5)
|
|
159
|
+
"""Range for robot initial y position. Used as fallback."""
|
|
160
|
+
|
|
161
|
+
robot_init_yaw_range: tuple[float, float] = (-math.pi, math.pi)
|
|
162
|
+
"""Range for robot initial yaw angle in radians."""
|
|
163
|
+
|
|
164
|
+
# ========== Lighting & Background Configuration ==========
|
|
165
|
+
|
|
166
|
+
sky_light_intensity: float = 750.0
|
|
167
|
+
"""Intensity of the dome/sky light."""
|
|
168
|
+
|
|
169
|
+
sky_texture: str | None = None
|
|
170
|
+
"""Path to HDRI texture for sky background. If None, uses uniform white color."""
|
|
171
|
+
|
|
172
|
+
sky_color: tuple[float, float, float] = (1.0, 1.0, 1.0)
|
|
173
|
+
"""RGB color for sky light (used when sky_texture is None)."""
|
|
174
|
+
|
|
175
|
+
sky_visible: bool = True
|
|
176
|
+
"""Whether the sky is visible. If False, sky appears black (indoor scenes)."""
|
|
177
|
+
|
|
178
|
+
# ========== Abstract Method Implementations ==========
|
|
179
|
+
|
|
180
|
+
def get_terrain_cfg(self) -> TerrainImporterCfg:
|
|
181
|
+
"""Generate terrain configuration."""
|
|
182
|
+
return TerrainImporterCfg(
|
|
183
|
+
prim_path="/World/ground",
|
|
184
|
+
terrain_type=self.terrain_type,
|
|
185
|
+
terrain_generator=None,
|
|
186
|
+
usd_path=self.terrain_usd_path,
|
|
187
|
+
env_spacing=self.env_spacing,
|
|
188
|
+
max_init_terrain_level=None,
|
|
189
|
+
collision_group=-1,
|
|
190
|
+
physics_material=sim_utils.RigidBodyMaterialCfg(
|
|
191
|
+
friction_combine_mode="multiply",
|
|
192
|
+
restitution_combine_mode="multiply",
|
|
193
|
+
static_friction=self.terrain_friction,
|
|
194
|
+
dynamic_friction=self.terrain_friction,
|
|
195
|
+
restitution=self.terrain_restitution,
|
|
196
|
+
),
|
|
197
|
+
debug_vis=False,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def get_obstacle_cfgs(self) -> dict[str, AssetBaseCfg]:
|
|
201
|
+
"""Generate obstacle asset configurations.
|
|
202
|
+
|
|
203
|
+
Converts ObstacleConfig entries into AssetBaseCfg objects suitable for
|
|
204
|
+
spawning in the simulation. Supports both cuboid primitives and USD assets.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Dictionary mapping obstacle names to their asset configurations.
|
|
208
|
+
"""
|
|
209
|
+
obstacle_cfgs = {}
|
|
210
|
+
collision_props = sim_utils.CollisionPropertiesCfg(collision_enabled=True)
|
|
211
|
+
|
|
212
|
+
for i, obs in enumerate(self.obstacles):
|
|
213
|
+
name = f"obstacle_{i + 1}"
|
|
214
|
+
# Configure rigid body properties based on whether obstacle is dynamic
|
|
215
|
+
rigid_props = sim_utils.RigidBodyPropertiesCfg(
|
|
216
|
+
rigid_body_enabled=True,
|
|
217
|
+
kinematic_enabled=not obs.is_dynamic # Dynamic obstacles are not kinematic
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if obs.usd_path is not None:
|
|
221
|
+
# Use USD asset
|
|
222
|
+
spawn_cfg = sim_utils.UsdFileCfg(
|
|
223
|
+
usd_path=obs.usd_path,
|
|
224
|
+
scale=obs.usd_scale,
|
|
225
|
+
rigid_props=rigid_props,
|
|
226
|
+
collision_props=collision_props,
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
# Use cuboid primitive
|
|
230
|
+
spawn_cfg = sim_utils.CuboidCfg(
|
|
231
|
+
size=obs.size,
|
|
232
|
+
rigid_props=rigid_props,
|
|
233
|
+
collision_props=collision_props,
|
|
234
|
+
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=obs.color),
|
|
235
|
+
)
|
|
236
|
+
obstacle_cfgs[name] = AssetBaseCfg(
|
|
237
|
+
prim_path="{ENV_REGEX_NS}/" + name.capitalize().replace("_", ""),
|
|
238
|
+
spawn=spawn_cfg,
|
|
239
|
+
init_state=AssetBaseCfg.InitialStateCfg(pos=obs.position),
|
|
240
|
+
)
|
|
241
|
+
return obstacle_cfgs
|
|
242
|
+
|
|
243
|
+
def get_light_cfgs(self) -> dict[str, AssetBaseCfg]:
|
|
244
|
+
"""Generate light asset configurations (dome/sky light).
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Dictionary with sky_light configuration.
|
|
248
|
+
"""
|
|
249
|
+
return {
|
|
250
|
+
"sky_light": AssetBaseCfg(
|
|
251
|
+
prim_path="/World/skyLight",
|
|
252
|
+
spawn=sim_utils.DomeLightCfg(
|
|
253
|
+
intensity=self.sky_light_intensity,
|
|
254
|
+
color=self.sky_color,
|
|
255
|
+
texture_file=self.sky_texture,
|
|
256
|
+
visible_in_primary_ray=self.sky_visible,
|
|
257
|
+
),
|
|
258
|
+
),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
def gen_goal_random_pos(
|
|
262
|
+
self,
|
|
263
|
+
env_ids: torch.Tensor,
|
|
264
|
+
env_origins: torch.Tensor,
|
|
265
|
+
device: str | torch.device = "cpu",
|
|
266
|
+
**kwargs,
|
|
267
|
+
) -> torch.Tensor:
|
|
268
|
+
"""Generate goal positions inside free zones (vectorized, cached).
|
|
269
|
+
|
|
270
|
+
This method uses cached free zones and vectorized sampling for high performance.
|
|
271
|
+
Goals are guaranteed to be in obstacle-free areas safe for navigation.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
env_ids: Tensor of environment indices to generate goals for.
|
|
275
|
+
env_origins: Tensor of shape (num_envs, 3) containing environment origins.
|
|
276
|
+
device: Device to create tensors on.
|
|
277
|
+
**kwargs: Additional keyword arguments (unused, for interface compatibility).
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Tensor of shape (len(env_ids), 3) containing goal positions (x, y, z).
|
|
281
|
+
Z-coordinate is set to goal_z_offset from the abstract base.
|
|
282
|
+
"""
|
|
283
|
+
num_goals = len(env_ids)
|
|
284
|
+
goal_pos = torch.zeros((num_goals, 3), device=device)
|
|
285
|
+
|
|
286
|
+
# Get cached zone data (computed once, reused)
|
|
287
|
+
zone_bounds, zone_probs = self._get_zone_sampling_data(device)
|
|
288
|
+
|
|
289
|
+
if zone_bounds is None:
|
|
290
|
+
# Fallback: use playground bounds if no free zones found
|
|
291
|
+
playground = self._get_playground()
|
|
292
|
+
if playground is None:
|
|
293
|
+
x_min, y_min, x_max, y_max = -2.0, -2.0, 2.0, 2.0
|
|
294
|
+
else:
|
|
295
|
+
x_min, y_min = playground.x_min, playground.y_min
|
|
296
|
+
x_max, y_max = playground.x_max, playground.y_max
|
|
297
|
+
|
|
298
|
+
# Vectorized sampling from playground
|
|
299
|
+
goal_pos[:, 0] = torch.rand(num_goals, device=device) * (x_max - x_min) + x_min
|
|
300
|
+
goal_pos[:, 1] = torch.rand(num_goals, device=device) * (y_max - y_min) + y_min
|
|
301
|
+
else:
|
|
302
|
+
# Vectorized zone selection (all environments at once)
|
|
303
|
+
assert zone_probs is not None # Both are always set together
|
|
304
|
+
zone_indices = torch.multinomial(zone_probs, num_goals, replacement=True)
|
|
305
|
+
|
|
306
|
+
# Get bounds for selected zones: (num_goals, 4)
|
|
307
|
+
selected_bounds = zone_bounds[zone_indices]
|
|
308
|
+
|
|
309
|
+
# Vectorized random sampling within selected zone bounds
|
|
310
|
+
rand_xy = torch.rand((num_goals, 2), device=device)
|
|
311
|
+
goal_pos[:, 0] = rand_xy[:, 0] * (selected_bounds[:, 2] - selected_bounds[:, 0]) + selected_bounds[:, 0]
|
|
312
|
+
goal_pos[:, 1] = rand_xy[:, 1] * (selected_bounds[:, 3] - selected_bounds[:, 1]) + selected_bounds[:, 1]
|
|
313
|
+
|
|
314
|
+
# Set z-coordinate to goal offset
|
|
315
|
+
goal_pos[:, 2] = self.goal_z_offset
|
|
316
|
+
|
|
317
|
+
# Add environment origins
|
|
318
|
+
goal_pos[:, :2] += env_origins[env_ids, :2]
|
|
319
|
+
return goal_pos
|
|
320
|
+
|
|
321
|
+
def gen_bot_random_pos(
|
|
322
|
+
self,
|
|
323
|
+
env_ids: torch.Tensor,
|
|
324
|
+
env_origins: torch.Tensor,
|
|
325
|
+
device: str | torch.device = "cpu",
|
|
326
|
+
**kwargs,
|
|
327
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
328
|
+
"""Generate robot initial positions and yaws inside free zones (vectorized, cached).
|
|
329
|
+
|
|
330
|
+
This method uses cached free zones and vectorized sampling for high performance.
|
|
331
|
+
Positions are guaranteed to be in obstacle-free areas safe for robot spawning.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
env_ids: Tensor of environment indices to generate positions for.
|
|
335
|
+
env_origins: Tensor of shape (num_envs, 3) containing environment origins.
|
|
336
|
+
device: Device to create tensors on.
|
|
337
|
+
**kwargs: Additional keyword arguments (unused, for interface compatibility).
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Tuple of (positions, yaws):
|
|
341
|
+
- positions: Tensor of shape (len(env_ids), 3) containing (x, y, z).
|
|
342
|
+
- yaws: Tensor of shape (len(env_ids),) containing yaw angles.
|
|
343
|
+
"""
|
|
344
|
+
num_positions = len(env_ids)
|
|
345
|
+
positions = torch.zeros((num_positions, 3), device=device)
|
|
346
|
+
|
|
347
|
+
# Get cached zone data (computed once, reused)
|
|
348
|
+
zone_bounds, zone_probs = self._get_zone_sampling_data(device)
|
|
349
|
+
|
|
350
|
+
# Vectorized yaw sampling (always needed)
|
|
351
|
+
yaw_range = self.robot_init_yaw_range[1] - self.robot_init_yaw_range[0]
|
|
352
|
+
yaws = torch.rand(num_positions, device=device) * yaw_range + self.robot_init_yaw_range[0]
|
|
353
|
+
|
|
354
|
+
if zone_bounds is None:
|
|
355
|
+
# Fallback: vectorized sampling from configured ranges
|
|
356
|
+
x_range = self.robot_init_pos_x_range[1] - self.robot_init_pos_x_range[0]
|
|
357
|
+
y_range = self.robot_init_pos_y_range[1] - self.robot_init_pos_y_range[0]
|
|
358
|
+
positions[:, 0] = torch.rand(num_positions, device=device) * x_range + self.robot_init_pos_x_range[0]
|
|
359
|
+
positions[:, 1] = torch.rand(num_positions, device=device) * y_range + self.robot_init_pos_y_range[0]
|
|
360
|
+
else:
|
|
361
|
+
# Vectorized zone selection (all environments at once)
|
|
362
|
+
assert zone_probs is not None # Both are always set together
|
|
363
|
+
zone_indices = torch.multinomial(zone_probs, num_positions, replacement=True)
|
|
364
|
+
|
|
365
|
+
# Get bounds for selected zones: (num_positions, 4)
|
|
366
|
+
selected_bounds = zone_bounds[zone_indices]
|
|
367
|
+
|
|
368
|
+
# Vectorized random sampling within selected zone bounds
|
|
369
|
+
rand_xy = torch.rand((num_positions, 2), device=device)
|
|
370
|
+
positions[:, 0] = rand_xy[:, 0] * (selected_bounds[:, 2] - selected_bounds[:, 0]) + selected_bounds[:, 0]
|
|
371
|
+
positions[:, 1] = rand_xy[:, 1] * (selected_bounds[:, 3] - selected_bounds[:, 1]) + selected_bounds[:, 1]
|
|
372
|
+
|
|
373
|
+
# Add environment origins
|
|
374
|
+
positions[:, :2] += env_origins[env_ids, :2]
|
|
375
|
+
return positions, yaws
|
|
376
|
+
|
|
377
|
+
def validate_positions(
|
|
378
|
+
self,
|
|
379
|
+
positions: torch.Tensor,
|
|
380
|
+
env_origins: torch.Tensor,
|
|
381
|
+
device: str | torch.device = "cpu",
|
|
382
|
+
robot_radius: float | None = None,
|
|
383
|
+
**kwargs,
|
|
384
|
+
) -> torch.Tensor:
|
|
385
|
+
"""Validate that positions are safe (not in obstacles, within bounds).
|
|
386
|
+
|
|
387
|
+
Checks if positions are:
|
|
388
|
+
1. Within playground bounds (if playground is configured)
|
|
389
|
+
2. Not overlapping with any obstacles (with robot radius clearance)
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
positions: Tensor of shape (N, 2) or (N, 3) with positions (x, y) or (x, y, z).
|
|
393
|
+
env_origins: Tensor of shape (num_envs, 3) containing environment origins.
|
|
394
|
+
device: Device to create tensors on.
|
|
395
|
+
robot_radius: Safety radius around robot for collision checking.
|
|
396
|
+
If None, uses self.robot_radius.
|
|
397
|
+
**kwargs: Additional keyword arguments (unused, for interface compatibility).
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Boolean tensor of shape (N,) indicating which positions are valid.
|
|
401
|
+
True means position is valid/safe, False means invalid/unsafe.
|
|
402
|
+
"""
|
|
403
|
+
if robot_radius is None:
|
|
404
|
+
robot_radius = self.robot_radius
|
|
405
|
+
|
|
406
|
+
# Extract 2D positions (x, y only)
|
|
407
|
+
if positions.shape[1] == 3:
|
|
408
|
+
pos_2d = positions[:, :2]
|
|
409
|
+
else:
|
|
410
|
+
pos_2d = positions
|
|
411
|
+
|
|
412
|
+
num_positions = pos_2d.shape[0]
|
|
413
|
+
valid = torch.ones(num_positions, dtype=torch.bool, device=device)
|
|
414
|
+
|
|
415
|
+
# Get obstacle layout with robot radius clearance
|
|
416
|
+
obstacle_boxes = self._get_obstacle_layout(clearance=robot_radius)
|
|
417
|
+
playground = self._get_playground()
|
|
418
|
+
|
|
419
|
+
# Check each position
|
|
420
|
+
for i in range(num_positions):
|
|
421
|
+
x, y = pos_2d[i, 0].item(), pos_2d[i, 1].item()
|
|
422
|
+
|
|
423
|
+
# Check playground bounds
|
|
424
|
+
if playground is not None:
|
|
425
|
+
if (x < playground.x_min or x > playground.x_max or
|
|
426
|
+
y < playground.y_min or y > playground.y_max):
|
|
427
|
+
valid[i] = False
|
|
428
|
+
continue
|
|
429
|
+
|
|
430
|
+
# Check obstacle collisions
|
|
431
|
+
for obs_box in obstacle_boxes:
|
|
432
|
+
if (obs_box.x_min <= x <= obs_box.x_max and
|
|
433
|
+
obs_box.y_min <= y <= obs_box.y_max):
|
|
434
|
+
valid[i] = False
|
|
435
|
+
break
|
|
436
|
+
|
|
437
|
+
return valid
|
|
438
|
+
|
|
439
|
+
# ========== Helper Methods ==========
|
|
440
|
+
|
|
441
|
+
def _get_zone_sampling_data(
|
|
442
|
+
self, device: str | torch.device = "cpu"
|
|
443
|
+
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
|
|
444
|
+
"""Get cached zone bounds and probabilities for vectorized sampling.
|
|
445
|
+
|
|
446
|
+
Computes free zones once and caches the results. Returns pre-computed
|
|
447
|
+
tensors optimized for batch position sampling.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
device: Device to create/move tensors to.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Tuple of (zone_bounds, zone_probs):
|
|
454
|
+
- zone_bounds: Tensor of shape (num_zones, 4) with [x_min, y_min, x_max, y_max]
|
|
455
|
+
per zone (with safety margin applied). None if no zones.
|
|
456
|
+
- zone_probs: Tensor of shape (num_zones,) with area-weighted probabilities.
|
|
457
|
+
None if no zones.
|
|
458
|
+
"""
|
|
459
|
+
# Compute free zones if not cached
|
|
460
|
+
if self._cached_free_zones is None:
|
|
461
|
+
self._cached_free_zones, _, _ = self._compute_free_zones()
|
|
462
|
+
|
|
463
|
+
free_zones = self._cached_free_zones
|
|
464
|
+
if not free_zones:
|
|
465
|
+
return None, None
|
|
466
|
+
|
|
467
|
+
# Compute zone bounds tensor if not cached (or move to correct device)
|
|
468
|
+
if self._cached_zone_bounds is None or self._cached_zone_bounds.device != torch.device(device):
|
|
469
|
+
num_zones = len(free_zones)
|
|
470
|
+
zone_bounds = torch.zeros((num_zones, 4), device=device)
|
|
471
|
+
zone_areas = torch.zeros(num_zones, device=device)
|
|
472
|
+
|
|
473
|
+
for i, zone in enumerate(free_zones):
|
|
474
|
+
# Get zone bounds with safety margin
|
|
475
|
+
x_min = min(zone.x1, zone.x2) + self.robot_safety_margin
|
|
476
|
+
x_max = max(zone.x1, zone.x2) - self.robot_safety_margin
|
|
477
|
+
y_min = min(zone.y1, zone.y2) + self.robot_safety_margin
|
|
478
|
+
y_max = max(zone.y1, zone.y2) - self.robot_safety_margin
|
|
479
|
+
|
|
480
|
+
# Ensure valid bounds (fall back to original if margin makes it invalid)
|
|
481
|
+
if x_max <= x_min:
|
|
482
|
+
x_min, x_max = min(zone.x1, zone.x2), max(zone.x1, zone.x2)
|
|
483
|
+
if y_max <= y_min:
|
|
484
|
+
y_min, y_max = min(zone.y1, zone.y2), max(zone.y1, zone.y2)
|
|
485
|
+
|
|
486
|
+
zone_bounds[i] = torch.tensor([x_min, y_min, x_max, y_max], device=device)
|
|
487
|
+
zone_areas[i] = zone.area
|
|
488
|
+
|
|
489
|
+
# Compute area-weighted probabilities
|
|
490
|
+
zone_probs = zone_areas / zone_areas.sum()
|
|
491
|
+
|
|
492
|
+
self._cached_zone_bounds = zone_bounds
|
|
493
|
+
self._cached_zone_probs = zone_probs
|
|
494
|
+
|
|
495
|
+
return self._cached_zone_bounds, self._cached_zone_probs
|
|
496
|
+
|
|
497
|
+
def clear_zone_cache(self) -> None:
|
|
498
|
+
"""Clear cached free zone data.
|
|
499
|
+
|
|
500
|
+
Call this if obstacles are modified after initialization to force
|
|
501
|
+
recomputation of free zones on next position generation.
|
|
502
|
+
"""
|
|
503
|
+
self._cached_free_zones = None
|
|
504
|
+
self._cached_zone_bounds = None
|
|
505
|
+
self._cached_zone_probs = None
|
|
506
|
+
|
|
507
|
+
def _get_obstacle_layout(self, clearance: float = 0.0) -> list[Rectangle]:
|
|
508
|
+
"""Generate bounding boxes for all obstacles.
|
|
509
|
+
|
|
510
|
+
Only includes obstacles that have include_in_static_layout=True (excludes
|
|
511
|
+
fully dynamic obstacles that move unpredictably).
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
clearance: Optional clearance margin around each obstacle.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
List of Rectangle bounding boxes for each obstacle that should be
|
|
518
|
+
included in static layout calculations.
|
|
519
|
+
"""
|
|
520
|
+
boxes = []
|
|
521
|
+
for obs in self.obstacles:
|
|
522
|
+
# Skip obstacles that should not be included in static layout
|
|
523
|
+
if not obs.include_in_static_layout:
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
# For USD assets, use a default size if not specified
|
|
527
|
+
# In practice, you might want to read bounding box from USD metadata
|
|
528
|
+
if obs.usd_path is not None:
|
|
529
|
+
# Use a reasonable default size for USD assets
|
|
530
|
+
# This could be improved by reading actual bounding box
|
|
531
|
+
half_x = (obs.usd_scale[0] * 0.5) + clearance
|
|
532
|
+
half_y = (obs.usd_scale[1] * 0.5) + clearance
|
|
533
|
+
else:
|
|
534
|
+
half_x = obs.size[0] / 2.0 + clearance
|
|
535
|
+
half_y = obs.size[1] / 2.0 + clearance
|
|
536
|
+
|
|
537
|
+
boxes.append(Rectangle(
|
|
538
|
+
x_min=obs.position[0] - half_x,
|
|
539
|
+
y_min=obs.position[1] - half_y,
|
|
540
|
+
x_max=obs.position[0] + half_x,
|
|
541
|
+
y_max=obs.position[1] + half_y,
|
|
542
|
+
))
|
|
543
|
+
return boxes
|
|
544
|
+
|
|
545
|
+
def _get_playground(self) -> Rectangle | None:
|
|
546
|
+
"""Get playground bounds as a Rectangle.
|
|
547
|
+
|
|
548
|
+
Returns:
|
|
549
|
+
Rectangle if playground is configured, None otherwise.
|
|
550
|
+
"""
|
|
551
|
+
if self.playground is None:
|
|
552
|
+
return None
|
|
553
|
+
return Rectangle(
|
|
554
|
+
x_min=self.playground[0],
|
|
555
|
+
y_min=self.playground[1],
|
|
556
|
+
x_max=self.playground[2],
|
|
557
|
+
y_max=self.playground[3],
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
def _compute_free_zones(
|
|
561
|
+
self,
|
|
562
|
+
) -> tuple[list[FreeZone], list[Rectangle], Rectangle]:
|
|
563
|
+
"""Compute free zones that are safe for robot navigation.
|
|
564
|
+
|
|
565
|
+
Uses class configuration attributes for all parameters.
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
Tuple of (free_zones, obstacle_boxes, playground).
|
|
569
|
+
"""
|
|
570
|
+
playground = self._get_playground()
|
|
571
|
+
|
|
572
|
+
# Get obstacle layout (no clearance expansion for free zone computation)
|
|
573
|
+
obstacle_layout = self._get_obstacle_layout(clearance=0.0)
|
|
574
|
+
|
|
575
|
+
# Use utility function to find free zones
|
|
576
|
+
# The clearance parameter shrinks free zones by that amount for safety
|
|
577
|
+
free_zones, playground = find_free_zones(
|
|
578
|
+
obstacle_boxes=obstacle_layout,
|
|
579
|
+
playground=playground,
|
|
580
|
+
playground_margin=self.playground_margin,
|
|
581
|
+
min_zone_size=self.min_zone_size,
|
|
582
|
+
max_zones=self.max_zones,
|
|
583
|
+
clearance=self.clearance,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
return free_zones, obstacle_layout, playground
|
|
587
|
+
|
|
588
|
+
# Alias for backward compatibility
|
|
589
|
+
ObstacleEnvironmentPresetCfg = PresetNavigationEnvCfg
|
|
590
|
+
|