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,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
+