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,644 @@
|
|
|
1
|
+
# Copyright (c) 2025, Nepher Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
|
|
6
|
+
"""Base USD environment configuration for loading complete scenes from USD files.
|
|
7
|
+
|
|
8
|
+
This preset supports environments loaded entirely from USD files, where:
|
|
9
|
+
- The terrain, obstacles, and scene elements are defined in the USD file
|
|
10
|
+
- Spawn areas (free zones) are explicitly defined for safe robot/goal positioning
|
|
11
|
+
- Collision geometry comes from the USD file's collision meshes
|
|
12
|
+
|
|
13
|
+
Unlike PresetNavigationEnvCfg which manually defines obstacles, this preset
|
|
14
|
+
treats the USD file as a complete scene and focuses on defining navigation-relevant
|
|
15
|
+
areas within that scene.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
import isaaclab.sim as sim_utils
|
|
26
|
+
from isaaclab.assets import AssetBaseCfg
|
|
27
|
+
from isaaclab.terrains import TerrainImporterCfg
|
|
28
|
+
from isaaclab.utils import configclass
|
|
29
|
+
|
|
30
|
+
from nepher.env_cfgs.navigation.abstract_nav_cfg import AbstractNavigationEnvCfg
|
|
31
|
+
from nepher.utils.free_zone_finder import FreeZone, Rectangle
|
|
32
|
+
from nepher.utils.fast_spawn_sampler import FastSpawnSampler, OccupancyMapConfig
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class SpawnAreaConfig:
|
|
37
|
+
"""Configuration for a spawn area (free zone) where robots/goals can be placed.
|
|
38
|
+
|
|
39
|
+
Spawn areas define rectangular regions that are safe for robot and goal spawning.
|
|
40
|
+
These should be placed in collision-free areas of the USD scene.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Bounds in world coordinates (x_min, y_min, x_max, y_max)
|
|
44
|
+
bounds: tuple[float, float, float, float] = (-1.0, -1.0, 1.0, 1.0)
|
|
45
|
+
"""Rectangular bounds (x_min, y_min, x_max, y_max) in world coordinates."""
|
|
46
|
+
|
|
47
|
+
weight: float = 1.0
|
|
48
|
+
"""Relative weight for random selection. Higher weight = more likely to be selected."""
|
|
49
|
+
|
|
50
|
+
allow_robot_spawn: bool = True
|
|
51
|
+
"""Whether robots can spawn in this area."""
|
|
52
|
+
|
|
53
|
+
allow_goal_spawn: bool = True
|
|
54
|
+
"""Whether goals can spawn in this area."""
|
|
55
|
+
|
|
56
|
+
name: str = ""
|
|
57
|
+
"""Optional name for this spawn area (e.g., 'room_1', 'hallway')."""
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def x_min(self) -> float:
|
|
61
|
+
return self.bounds[0]
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def y_min(self) -> float:
|
|
65
|
+
return self.bounds[1]
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def x_max(self) -> float:
|
|
69
|
+
return self.bounds[2]
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def y_max(self) -> float:
|
|
73
|
+
return self.bounds[3]
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def width(self) -> float:
|
|
77
|
+
return self.x_max - self.x_min
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def height(self) -> float:
|
|
81
|
+
return self.y_max - self.y_min
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def area(self) -> float:
|
|
85
|
+
return self.width * self.height
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def center(self) -> tuple[float, float]:
|
|
89
|
+
return ((self.x_min + self.x_max) / 2.0, (self.y_min + self.y_max) / 2.0)
|
|
90
|
+
|
|
91
|
+
def to_rectangle(self) -> Rectangle:
|
|
92
|
+
"""Convert to Rectangle object for compatibility with free zone utilities."""
|
|
93
|
+
return Rectangle(
|
|
94
|
+
x_min=self.x_min,
|
|
95
|
+
y_min=self.y_min,
|
|
96
|
+
x_max=self.x_max,
|
|
97
|
+
y_max=self.y_max,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class ExclusionZoneConfig:
|
|
103
|
+
"""Configuration for an exclusion zone where robots/goals cannot be placed.
|
|
104
|
+
|
|
105
|
+
Exclusion zones define rectangular regions that should be avoided for spawning,
|
|
106
|
+
even if they fall within a spawn area. Useful for marking static obstacles,
|
|
107
|
+
furniture, or other hazards within the USD scene.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# Bounds in world coordinates (x_min, y_min, x_max, y_max)
|
|
111
|
+
bounds: tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0)
|
|
112
|
+
"""Rectangular bounds (x_min, y_min, x_max, y_max) in world coordinates."""
|
|
113
|
+
|
|
114
|
+
name: str = ""
|
|
115
|
+
"""Optional name for this exclusion zone (e.g., 'table_1', 'pillar')."""
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def x_min(self) -> float:
|
|
119
|
+
return self.bounds[0]
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def y_min(self) -> float:
|
|
123
|
+
return self.bounds[1]
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def x_max(self) -> float:
|
|
127
|
+
return self.bounds[2]
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def y_max(self) -> float:
|
|
131
|
+
return self.bounds[3]
|
|
132
|
+
|
|
133
|
+
def to_rectangle(self) -> Rectangle:
|
|
134
|
+
"""Convert to Rectangle object for compatibility with free zone utilities."""
|
|
135
|
+
return Rectangle(
|
|
136
|
+
x_min=self.x_min,
|
|
137
|
+
y_min=self.y_min,
|
|
138
|
+
x_max=self.x_max,
|
|
139
|
+
y_max=self.y_max,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def contains_point(self, x: float, y: float) -> bool:
|
|
143
|
+
"""Check if a point is inside this exclusion zone."""
|
|
144
|
+
return self.x_min <= x <= self.x_max and self.y_min <= y <= self.y_max
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@configclass
|
|
148
|
+
class UsdNavigationEnvCfg(AbstractNavigationEnvCfg):
|
|
149
|
+
"""Base configuration for USD-based navigation environments.
|
|
150
|
+
|
|
151
|
+
This preset loads a complete environment from a USD file and provides
|
|
152
|
+
mechanisms for defining spawn areas and exclusion zones for safe
|
|
153
|
+
robot and goal positioning.
|
|
154
|
+
|
|
155
|
+
The USD file should contain:
|
|
156
|
+
- Terrain/ground geometry
|
|
157
|
+
- Static obstacles and scene elements
|
|
158
|
+
- Collision meshes for physics interaction
|
|
159
|
+
|
|
160
|
+
Spawn areas and exclusion zones are defined separately to allow flexible
|
|
161
|
+
configuration of navigation-safe regions within the scene.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
# Preset identification
|
|
165
|
+
name: str = "usd_env"
|
|
166
|
+
description: str = "Base USD environment loaded from a scene file"
|
|
167
|
+
category: str = "navigation"
|
|
168
|
+
|
|
169
|
+
# ========== USD Scene Configuration ==========
|
|
170
|
+
|
|
171
|
+
usd_path: str = ""
|
|
172
|
+
"""Path to the main USD file containing the environment scene."""
|
|
173
|
+
|
|
174
|
+
usd_scale: tuple[float, float, float] = (1.0, 1.0, 1.0)
|
|
175
|
+
"""Scale factor for the USD scene (x, y, z)."""
|
|
176
|
+
|
|
177
|
+
usd_position: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
|
178
|
+
"""Position offset for the USD scene (x, y, z)."""
|
|
179
|
+
|
|
180
|
+
usd_rotation: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0)
|
|
181
|
+
"""Rotation quaternion for the USD scene (w, x, y, z)."""
|
|
182
|
+
|
|
183
|
+
# ========== Terrain Configuration ==========
|
|
184
|
+
|
|
185
|
+
terrain_type: str = "usd"
|
|
186
|
+
"""Type of terrain: 'usd' to load terrain from usd_path, 'plane' for flat ground."""
|
|
187
|
+
|
|
188
|
+
terrain_friction: float = 1.0
|
|
189
|
+
"""Static and dynamic friction coefficient for terrain."""
|
|
190
|
+
|
|
191
|
+
terrain_restitution: float = 0.0
|
|
192
|
+
"""Restitution coefficient for terrain collisions."""
|
|
193
|
+
|
|
194
|
+
env_spacing: float = 50.0
|
|
195
|
+
"""Environment spacing for grid-like origins (meters). Should be large enough to contain the USD scene."""
|
|
196
|
+
|
|
197
|
+
# ========== Spawn Area Configuration ==========
|
|
198
|
+
|
|
199
|
+
spawn_areas: list[SpawnAreaConfig] = field(default_factory=list)
|
|
200
|
+
"""List of spawn area configurations. Defines where robots/goals can be placed."""
|
|
201
|
+
|
|
202
|
+
exclusion_zones: list[ExclusionZoneConfig] = field(default_factory=list)
|
|
203
|
+
"""List of exclusion zone configurations. Defines areas to avoid for spawning."""
|
|
204
|
+
|
|
205
|
+
# Playground bounds (x_min, y_min, x_max, y_max) in world coordinates
|
|
206
|
+
# If None, playground is auto-computed from spawn areas
|
|
207
|
+
playground: tuple[float, float, float, float] | None = None
|
|
208
|
+
"""Playground boundary (x_min, y_min, x_max, y_max). If None, auto-computed from spawn areas."""
|
|
209
|
+
|
|
210
|
+
# Safety margins
|
|
211
|
+
robot_safety_margin: float = 0.25
|
|
212
|
+
"""Additional safety margin around robot for position generation (meters)."""
|
|
213
|
+
|
|
214
|
+
spawn_area_margin: float = 0.1
|
|
215
|
+
"""Margin to shrink spawn areas by for safety (meters)."""
|
|
216
|
+
|
|
217
|
+
# Robot initial position/yaw ranges (used as fallback when no spawn areas defined)
|
|
218
|
+
robot_init_pos_x_range: tuple[float, float] = (-1.0, 1.0)
|
|
219
|
+
"""Range for robot initial x position. Used as fallback."""
|
|
220
|
+
|
|
221
|
+
robot_init_pos_y_range: tuple[float, float] = (-1.0, 1.0)
|
|
222
|
+
"""Range for robot initial y position. Used as fallback."""
|
|
223
|
+
|
|
224
|
+
robot_init_yaw_range: tuple[float, float] = (-math.pi, math.pi)
|
|
225
|
+
"""Range for robot initial yaw angle in radians."""
|
|
226
|
+
|
|
227
|
+
# ========== Fast Spawn Sampler Configuration ==========
|
|
228
|
+
|
|
229
|
+
occupancy_map_yaml: str | None = None
|
|
230
|
+
"""Path to occupancy map YAML file for fast collision-free sampling."""
|
|
231
|
+
|
|
232
|
+
spawn_grid_resolution: float = 0.1
|
|
233
|
+
"""Grid resolution (meters) for pre-computing valid spawn positions."""
|
|
234
|
+
|
|
235
|
+
min_robot_goal_distance: float = 1.0
|
|
236
|
+
"""Minimum distance between robot spawn and goal positions."""
|
|
237
|
+
|
|
238
|
+
# ========== Lighting & Background Configuration ==========
|
|
239
|
+
|
|
240
|
+
use_usd_lighting: bool = True
|
|
241
|
+
"""Whether to use lighting defined in the USD file. If False, uses custom lighting."""
|
|
242
|
+
|
|
243
|
+
sky_light_intensity: float = 750.0
|
|
244
|
+
"""Intensity of the dome/sky light (used when use_usd_lighting=False)."""
|
|
245
|
+
|
|
246
|
+
sky_texture: str | None = None
|
|
247
|
+
"""Path to HDRI texture for sky background. If None, uses uniform color."""
|
|
248
|
+
|
|
249
|
+
sky_color: tuple[float, float, float] = (1.0, 1.0, 1.0)
|
|
250
|
+
"""RGB color for sky light (used when sky_texture is None)."""
|
|
251
|
+
|
|
252
|
+
sky_visible: bool = True
|
|
253
|
+
"""Whether the sky is visible. If False, sky appears black (indoor scenes)."""
|
|
254
|
+
|
|
255
|
+
# ========== Abstract Method Implementations ==========
|
|
256
|
+
|
|
257
|
+
def get_terrain_cfg(self) -> TerrainImporterCfg:
|
|
258
|
+
"""Generate terrain configuration.
|
|
259
|
+
|
|
260
|
+
If terrain_type is 'usd', uses the usd_path as terrain.
|
|
261
|
+
Otherwise, creates a flat plane terrain.
|
|
262
|
+
"""
|
|
263
|
+
if self.terrain_type == "usd" and self.usd_path:
|
|
264
|
+
return TerrainImporterCfg(
|
|
265
|
+
prim_path="/World/ground",
|
|
266
|
+
terrain_type="usd",
|
|
267
|
+
terrain_generator=None,
|
|
268
|
+
usd_path=self.usd_path,
|
|
269
|
+
env_spacing=self.env_spacing,
|
|
270
|
+
max_init_terrain_level=None,
|
|
271
|
+
collision_group=-1,
|
|
272
|
+
physics_material=sim_utils.RigidBodyMaterialCfg(
|
|
273
|
+
friction_combine_mode="multiply",
|
|
274
|
+
restitution_combine_mode="multiply",
|
|
275
|
+
static_friction=self.terrain_friction,
|
|
276
|
+
dynamic_friction=self.terrain_friction,
|
|
277
|
+
restitution=self.terrain_restitution,
|
|
278
|
+
),
|
|
279
|
+
debug_vis=False,
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
return TerrainImporterCfg(
|
|
283
|
+
prim_path="/World/ground",
|
|
284
|
+
terrain_type="plane",
|
|
285
|
+
terrain_generator=None,
|
|
286
|
+
usd_path=None,
|
|
287
|
+
env_spacing=self.env_spacing,
|
|
288
|
+
max_init_terrain_level=None,
|
|
289
|
+
collision_group=-1,
|
|
290
|
+
physics_material=sim_utils.RigidBodyMaterialCfg(
|
|
291
|
+
friction_combine_mode="multiply",
|
|
292
|
+
restitution_combine_mode="multiply",
|
|
293
|
+
static_friction=self.terrain_friction,
|
|
294
|
+
dynamic_friction=self.terrain_friction,
|
|
295
|
+
restitution=self.terrain_restitution,
|
|
296
|
+
),
|
|
297
|
+
debug_vis=False,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def get_obstacle_cfgs(self) -> dict[str, AssetBaseCfg]:
|
|
301
|
+
"""Generate obstacle asset configurations.
|
|
302
|
+
|
|
303
|
+
For USD environments, obstacles are typically included in the USD file.
|
|
304
|
+
This method returns an empty dict by default. Override if you need
|
|
305
|
+
to add additional obstacles programmatically.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Empty dictionary (obstacles are in the USD file).
|
|
309
|
+
"""
|
|
310
|
+
return {}
|
|
311
|
+
|
|
312
|
+
def get_light_cfgs(self) -> dict[str, AssetBaseCfg]:
|
|
313
|
+
"""Generate light asset configurations.
|
|
314
|
+
|
|
315
|
+
If use_usd_lighting is True, returns empty dict (lighting from USD).
|
|
316
|
+
Otherwise, returns a dome/sky light configuration.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Dictionary with sky_light configuration, or empty if using USD lighting.
|
|
320
|
+
"""
|
|
321
|
+
if self.use_usd_lighting:
|
|
322
|
+
return {}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
"sky_light": AssetBaseCfg(
|
|
326
|
+
prim_path="/World/skyLight",
|
|
327
|
+
spawn=sim_utils.DomeLightCfg(
|
|
328
|
+
intensity=self.sky_light_intensity,
|
|
329
|
+
color=self.sky_color,
|
|
330
|
+
texture_file=self.sky_texture,
|
|
331
|
+
visible_in_primary_ray=self.sky_visible,
|
|
332
|
+
),
|
|
333
|
+
),
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# ========== Fast Sampler (lazy initialized) ==========
|
|
337
|
+
_spawn_sampler: FastSpawnSampler | None = None
|
|
338
|
+
|
|
339
|
+
def _get_spawn_sampler(self, device: str | torch.device = "cpu") -> FastSpawnSampler:
|
|
340
|
+
"""Get or create the fast spawn sampler (lazy init)."""
|
|
341
|
+
if self._spawn_sampler is not None:
|
|
342
|
+
return self._spawn_sampler
|
|
343
|
+
|
|
344
|
+
# Build occupancy map config if yaml path provided
|
|
345
|
+
omap_cfg = None
|
|
346
|
+
if self.occupancy_map_yaml:
|
|
347
|
+
omap_cfg = OccupancyMapConfig(
|
|
348
|
+
yaml_path=self.occupancy_map_yaml,
|
|
349
|
+
safety_margin=self.robot_safety_margin,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Build spawn bounds from spawn areas or fallback ranges
|
|
353
|
+
spawn_bounds = None
|
|
354
|
+
if self.spawn_areas:
|
|
355
|
+
x_min = min(a.x_min for a in self.spawn_areas)
|
|
356
|
+
y_min = min(a.y_min for a in self.spawn_areas)
|
|
357
|
+
x_max = max(a.x_max for a in self.spawn_areas)
|
|
358
|
+
y_max = max(a.y_max for a in self.spawn_areas)
|
|
359
|
+
spawn_bounds = (x_min, y_min, x_max, y_max)
|
|
360
|
+
else:
|
|
361
|
+
spawn_bounds = (
|
|
362
|
+
self.robot_init_pos_x_range[0],
|
|
363
|
+
self.robot_init_pos_y_range[0],
|
|
364
|
+
self.robot_init_pos_x_range[1],
|
|
365
|
+
self.robot_init_pos_y_range[1],
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Build exclusion zones list
|
|
369
|
+
exclusions = [(z.x_min, z.y_min, z.x_max, z.y_max) for z in self.exclusion_zones]
|
|
370
|
+
|
|
371
|
+
self._spawn_sampler = FastSpawnSampler(
|
|
372
|
+
device=device,
|
|
373
|
+
omap_config=omap_cfg,
|
|
374
|
+
spawn_bounds=spawn_bounds,
|
|
375
|
+
exclusion_rects=exclusions,
|
|
376
|
+
grid_resolution=self.spawn_grid_resolution,
|
|
377
|
+
safety_margin=self.robot_safety_margin,
|
|
378
|
+
)
|
|
379
|
+
return self._spawn_sampler
|
|
380
|
+
|
|
381
|
+
def gen_goal_random_pos(
|
|
382
|
+
self,
|
|
383
|
+
env_ids: torch.Tensor,
|
|
384
|
+
env_origins: torch.Tensor,
|
|
385
|
+
device: str | torch.device = "cpu",
|
|
386
|
+
**kwargs,
|
|
387
|
+
) -> torch.Tensor:
|
|
388
|
+
"""Generate goal positions - O(1) per sample using pre-computed valid cells.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
env_ids: Tensor of environment indices to generate goals for.
|
|
392
|
+
env_origins: Tensor of shape (num_envs, 3) containing environment origins.
|
|
393
|
+
device: Device to create tensors on.
|
|
394
|
+
**kwargs: robot_positions (Tensor) - if provided, ensures min distance.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Tensor of shape (len(env_ids), 3) containing goal positions (x, y, z).
|
|
398
|
+
"""
|
|
399
|
+
n = len(env_ids)
|
|
400
|
+
sampler = self._get_spawn_sampler(device)
|
|
401
|
+
|
|
402
|
+
robot_positions = kwargs.get("robot_positions")
|
|
403
|
+
if robot_positions is not None and self.min_robot_goal_distance > 0:
|
|
404
|
+
xy = sampler.sample_with_min_distance(
|
|
405
|
+
n,
|
|
406
|
+
existing_positions=robot_positions[:, :2],
|
|
407
|
+
min_distance=self.min_robot_goal_distance,
|
|
408
|
+
)
|
|
409
|
+
else:
|
|
410
|
+
xy = sampler.sample(n)
|
|
411
|
+
|
|
412
|
+
# Build (x, y, 0) positions relative to env origins
|
|
413
|
+
goal_pos = torch.zeros((n, 3), device=device)
|
|
414
|
+
goal_pos[:, :2] = xy + env_origins[env_ids, :2]
|
|
415
|
+
return goal_pos
|
|
416
|
+
|
|
417
|
+
def gen_bot_random_pos(
|
|
418
|
+
self,
|
|
419
|
+
env_ids: torch.Tensor,
|
|
420
|
+
env_origins: torch.Tensor,
|
|
421
|
+
device: str | torch.device = "cpu",
|
|
422
|
+
**kwargs,
|
|
423
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
424
|
+
"""Generate robot positions - O(1) per sample using pre-computed valid cells.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
env_ids: Tensor of environment indices to generate positions for.
|
|
428
|
+
env_origins: Tensor of shape (num_envs, 3) containing environment origins.
|
|
429
|
+
device: Device to create tensors on.
|
|
430
|
+
**kwargs: Unused.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Tuple of (positions, yaws):
|
|
434
|
+
- positions: Tensor of shape (len(env_ids), 3) containing (x, y, z).
|
|
435
|
+
- yaws: Tensor of shape (len(env_ids),) containing yaw angles.
|
|
436
|
+
"""
|
|
437
|
+
n = len(env_ids)
|
|
438
|
+
sampler = self._get_spawn_sampler(device)
|
|
439
|
+
|
|
440
|
+
xy = sampler.sample(n)
|
|
441
|
+
|
|
442
|
+
# Build positions relative to env origins
|
|
443
|
+
positions = torch.zeros((n, 3), device=device)
|
|
444
|
+
positions[:, :2] = xy + env_origins[env_ids, :2]
|
|
445
|
+
|
|
446
|
+
# Random yaw
|
|
447
|
+
yaw_min, yaw_max = self.robot_init_yaw_range
|
|
448
|
+
yaws = torch.rand(n, device=device) * (yaw_max - yaw_min) + yaw_min
|
|
449
|
+
|
|
450
|
+
return positions, yaws
|
|
451
|
+
|
|
452
|
+
def validate_positions(
|
|
453
|
+
self,
|
|
454
|
+
positions: torch.Tensor,
|
|
455
|
+
env_origins: torch.Tensor,
|
|
456
|
+
device: str | torch.device = "cpu",
|
|
457
|
+
robot_radius: float | None = None,
|
|
458
|
+
**kwargs,
|
|
459
|
+
) -> torch.Tensor:
|
|
460
|
+
"""Validate positions against pre-computed valid cells - O(N) batch check.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
positions: Tensor of shape (N, 2) or (N, 3) with positions (x, y) or (x, y, z).
|
|
464
|
+
env_origins: Tensor of shape (num_envs, 3) containing environment origins.
|
|
465
|
+
device: Device to create tensors on.
|
|
466
|
+
robot_radius: Unused (safety margin is baked into sampler).
|
|
467
|
+
**kwargs: env_ids (Tensor) - if provided, subtracts corresponding origins.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Boolean tensor of shape (N,) - True if position is valid/safe.
|
|
471
|
+
"""
|
|
472
|
+
sampler = self._get_spawn_sampler(device)
|
|
473
|
+
|
|
474
|
+
# Extract xy only
|
|
475
|
+
xy = positions[:, :2] if positions.shape[1] >= 2 else positions
|
|
476
|
+
|
|
477
|
+
# Subtract env origins if env_ids provided
|
|
478
|
+
env_ids = kwargs.get("env_ids")
|
|
479
|
+
if env_ids is not None:
|
|
480
|
+
xy = xy - env_origins[env_ids, :2]
|
|
481
|
+
|
|
482
|
+
return sampler.validate(xy.to(device))
|
|
483
|
+
|
|
484
|
+
# ========== Helper Methods ==========
|
|
485
|
+
|
|
486
|
+
def _get_playground(self) -> Rectangle | None:
|
|
487
|
+
"""Get playground bounds as a Rectangle.
|
|
488
|
+
|
|
489
|
+
If playground is not configured, computes bounds from spawn areas.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Rectangle if playground is configured or can be computed, None otherwise.
|
|
493
|
+
"""
|
|
494
|
+
if self.playground is not None:
|
|
495
|
+
return Rectangle(
|
|
496
|
+
x_min=self.playground[0],
|
|
497
|
+
y_min=self.playground[1],
|
|
498
|
+
x_max=self.playground[2],
|
|
499
|
+
y_max=self.playground[3],
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Compute from spawn areas
|
|
503
|
+
if not self.spawn_areas:
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
x_min = min(area.x_min for area in self.spawn_areas)
|
|
507
|
+
y_min = min(area.y_min for area in self.spawn_areas)
|
|
508
|
+
x_max = max(area.x_max for area in self.spawn_areas)
|
|
509
|
+
y_max = max(area.y_max for area in self.spawn_areas)
|
|
510
|
+
|
|
511
|
+
return Rectangle(x_min=x_min, y_min=y_min, x_max=x_max, y_max=y_max)
|
|
512
|
+
|
|
513
|
+
def _sample_position_from_areas(
|
|
514
|
+
self,
|
|
515
|
+
areas: list[SpawnAreaConfig],
|
|
516
|
+
device: str | torch.device = "cpu",
|
|
517
|
+
safety_margin: float = 0.0,
|
|
518
|
+
max_attempts: int = 100,
|
|
519
|
+
) -> tuple[float, float]:
|
|
520
|
+
"""Sample a random position from the given spawn areas.
|
|
521
|
+
|
|
522
|
+
Uses weighted random selection based on area size and weight.
|
|
523
|
+
Avoids exclusion zones.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
areas: List of spawn areas to sample from.
|
|
527
|
+
device: Device for tensor operations.
|
|
528
|
+
safety_margin: Additional margin to shrink spawn areas by.
|
|
529
|
+
max_attempts: Maximum number of attempts to find valid position.
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
Tuple of (x, y) position.
|
|
533
|
+
"""
|
|
534
|
+
if not areas:
|
|
535
|
+
return (0.0, 0.0)
|
|
536
|
+
|
|
537
|
+
# Compute weighted areas for selection
|
|
538
|
+
weighted_areas = torch.tensor(
|
|
539
|
+
[area.area * area.weight for area in areas],
|
|
540
|
+
device=device
|
|
541
|
+
)
|
|
542
|
+
area_probs = weighted_areas / weighted_areas.sum()
|
|
543
|
+
|
|
544
|
+
for _ in range(max_attempts):
|
|
545
|
+
# Select area based on weighted probability
|
|
546
|
+
area_idx: int = int(torch.multinomial(area_probs, 1).item())
|
|
547
|
+
area = areas[area_idx]
|
|
548
|
+
|
|
549
|
+
# Get area bounds with safety margin
|
|
550
|
+
margin = self.spawn_area_margin + safety_margin
|
|
551
|
+
x_min = area.x_min + margin
|
|
552
|
+
x_max = area.x_max - margin
|
|
553
|
+
y_min = area.y_min + margin
|
|
554
|
+
y_max = area.y_max - margin
|
|
555
|
+
|
|
556
|
+
# Ensure valid bounds
|
|
557
|
+
if x_max <= x_min:
|
|
558
|
+
x_min = area.x_min
|
|
559
|
+
x_max = area.x_max
|
|
560
|
+
if y_max <= y_min:
|
|
561
|
+
y_min = area.y_min
|
|
562
|
+
y_max = area.y_max
|
|
563
|
+
|
|
564
|
+
# Sample random position
|
|
565
|
+
x = torch.rand(1, device=device).item() * (x_max - x_min) + x_min
|
|
566
|
+
y = torch.rand(1, device=device).item() * (y_max - y_min) + y_min
|
|
567
|
+
|
|
568
|
+
# Check exclusion zones
|
|
569
|
+
in_exclusion = False
|
|
570
|
+
for zone in self.exclusion_zones:
|
|
571
|
+
if zone.contains_point(x, y):
|
|
572
|
+
in_exclusion = True
|
|
573
|
+
break
|
|
574
|
+
|
|
575
|
+
if not in_exclusion:
|
|
576
|
+
return (x, y)
|
|
577
|
+
|
|
578
|
+
# Fallback: return center of first area
|
|
579
|
+
return areas[0].center
|
|
580
|
+
|
|
581
|
+
def get_spawn_areas_as_free_zones(self) -> list[FreeZone]:
|
|
582
|
+
"""Convert spawn areas to FreeZone objects for compatibility.
|
|
583
|
+
|
|
584
|
+
Useful for integration with existing free zone-based algorithms.
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
List of FreeZone objects corresponding to spawn areas.
|
|
588
|
+
"""
|
|
589
|
+
free_zones = []
|
|
590
|
+
for area in self.spawn_areas:
|
|
591
|
+
if area.allow_robot_spawn or area.allow_goal_spawn:
|
|
592
|
+
free_zones.append(FreeZone(
|
|
593
|
+
x1=area.x_min + self.spawn_area_margin,
|
|
594
|
+
y1=area.y_min + self.spawn_area_margin,
|
|
595
|
+
x2=area.x_max - self.spawn_area_margin,
|
|
596
|
+
y2=area.y_max - self.spawn_area_margin,
|
|
597
|
+
))
|
|
598
|
+
return free_zones
|
|
599
|
+
|
|
600
|
+
def get_exclusion_zones_as_rectangles(self) -> list[Rectangle]:
|
|
601
|
+
"""Convert exclusion zones to Rectangle objects.
|
|
602
|
+
|
|
603
|
+
Useful for integration with obstacle avoidance algorithms.
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
List of Rectangle objects corresponding to exclusion zones.
|
|
607
|
+
"""
|
|
608
|
+
return [zone.to_rectangle() for zone in self.exclusion_zones]
|
|
609
|
+
|
|
610
|
+
def get_scene_asset_cfg(self) -> AssetBaseCfg | None:
|
|
611
|
+
"""Get asset configuration for the USD scene.
|
|
612
|
+
|
|
613
|
+
Returns an AssetBaseCfg for loading the USD scene as a static asset,
|
|
614
|
+
separate from the terrain. Useful when you need the scene as an asset
|
|
615
|
+
rather than terrain.
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
AssetBaseCfg for the USD scene, or None if usd_path is not set.
|
|
619
|
+
"""
|
|
620
|
+
if not self.usd_path:
|
|
621
|
+
return None
|
|
622
|
+
|
|
623
|
+
return AssetBaseCfg(
|
|
624
|
+
prim_path="{ENV_REGEX_NS}/Scene",
|
|
625
|
+
spawn=sim_utils.UsdFileCfg(
|
|
626
|
+
usd_path=self.usd_path,
|
|
627
|
+
scale=self.usd_scale,
|
|
628
|
+
rigid_props=sim_utils.RigidBodyPropertiesCfg(
|
|
629
|
+
rigid_body_enabled=True,
|
|
630
|
+
kinematic_enabled=True,
|
|
631
|
+
),
|
|
632
|
+
collision_props=sim_utils.CollisionPropertiesCfg(
|
|
633
|
+
collision_enabled=True,
|
|
634
|
+
),
|
|
635
|
+
),
|
|
636
|
+
init_state=AssetBaseCfg.InitialStateCfg(
|
|
637
|
+
pos=self.usd_position,
|
|
638
|
+
rot=self.usd_rotation,
|
|
639
|
+
),
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
# Alias for backward compatibility
|
|
643
|
+
UsdEnvironmentCfg = UsdNavigationEnvCfg
|
|
644
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Config class registry for category-specific configs."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Type
|
|
4
|
+
from nepher.env_cfgs.base import BaseEnvCfg
|
|
5
|
+
|
|
6
|
+
# Registry: (category, type) -> config class
|
|
7
|
+
_CONFIG_REGISTRY: Dict[tuple[str, str], Type[BaseEnvCfg]] = {} # type: ignore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_config_class(category: str, type: str) -> Type[BaseEnvCfg]:
|
|
11
|
+
"""Get config class for category and type."""
|
|
12
|
+
key = (category, type)
|
|
13
|
+
if key not in _CONFIG_REGISTRY:
|
|
14
|
+
try:
|
|
15
|
+
if category == "navigation":
|
|
16
|
+
import nepher.env_cfgs.navigation # noqa: F401
|
|
17
|
+
except ImportError:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
if key not in _CONFIG_REGISTRY:
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"No config class registered for category={category}, type={type}. "
|
|
23
|
+
f"Available registrations: {list(_CONFIG_REGISTRY.keys())}"
|
|
24
|
+
)
|
|
25
|
+
return _CONFIG_REGISTRY[key]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def register_config_class(category: str, type: str, config_class: Type[BaseEnvCfg]):
|
|
29
|
+
"""Register a custom config class."""
|
|
30
|
+
_CONFIG_REGISTRY[(category, type)] = config_class
|
|
31
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Loader system for environments."""
|
|
2
|
+
|
|
3
|
+
from nepher.loader.base import BaseLoader
|
|
4
|
+
from nepher.loader.usd_loader import UsdLoader
|
|
5
|
+
from nepher.loader.preset_loader import PresetLoader
|
|
6
|
+
from nepher.loader.registry import load_env, load_scene
|
|
7
|
+
|
|
8
|
+
__all__ = ["BaseLoader", "UsdLoader", "PresetLoader", "load_env", "load_scene"]
|
|
9
|
+
|