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.
Files changed (45) hide show
  1. nepher/__init__.py +36 -0
  2. nepher/api/__init__.py +6 -0
  3. nepher/api/client.py +384 -0
  4. nepher/api/endpoints.py +97 -0
  5. nepher/auth.py +150 -0
  6. nepher/cli/__init__.py +2 -0
  7. nepher/cli/commands/__init__.py +6 -0
  8. nepher/cli/commands/auth.py +37 -0
  9. nepher/cli/commands/cache.py +85 -0
  10. nepher/cli/commands/config.py +77 -0
  11. nepher/cli/commands/download.py +72 -0
  12. nepher/cli/commands/list.py +75 -0
  13. nepher/cli/commands/upload.py +69 -0
  14. nepher/cli/commands/view.py +310 -0
  15. nepher/cli/main.py +30 -0
  16. nepher/cli/utils.py +28 -0
  17. nepher/config.py +202 -0
  18. nepher/core.py +67 -0
  19. nepher/env_cfgs/__init__.py +7 -0
  20. nepher/env_cfgs/base.py +32 -0
  21. nepher/env_cfgs/manipulation/__init__.py +4 -0
  22. nepher/env_cfgs/navigation/__init__.py +45 -0
  23. nepher/env_cfgs/navigation/abstract_nav_cfg.py +159 -0
  24. nepher/env_cfgs/navigation/preset_nav_cfg.py +590 -0
  25. nepher/env_cfgs/navigation/usd_nav_cfg.py +644 -0
  26. nepher/env_cfgs/registry.py +31 -0
  27. nepher/loader/__init__.py +9 -0
  28. nepher/loader/base.py +27 -0
  29. nepher/loader/category_loaders/__init__.py +2 -0
  30. nepher/loader/preset_loader.py +80 -0
  31. nepher/loader/registry.py +63 -0
  32. nepher/loader/usd_loader.py +49 -0
  33. nepher/storage/__init__.py +8 -0
  34. nepher/storage/bundle.py +78 -0
  35. nepher/storage/cache.py +145 -0
  36. nepher/storage/manifest.py +80 -0
  37. nepher/utils/__init__.py +12 -0
  38. nepher/utils/fast_spawn_sampler.py +334 -0
  39. nepher/utils/free_zone_finder.py +239 -0
  40. nepher-0.1.0.dist-info/METADATA +235 -0
  41. nepher-0.1.0.dist-info/RECORD +45 -0
  42. nepher-0.1.0.dist-info/WHEEL +5 -0
  43. nepher-0.1.0.dist-info/entry_points.txt +2 -0
  44. nepher-0.1.0.dist-info/licenses/LICENSE +97 -0
  45. 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
+