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,310 @@
1
+ """View environment command (Isaac Sim integration)."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+ from typing import Optional
7
+ import click
8
+ from nepher.loader.registry import load_env, load_scene
9
+ from nepher.cli.utils import print_error, print_info, print_success
10
+
11
+ original_argv = None
12
+
13
+
14
+ def _check_isaaclab_installed():
15
+ """Check if isaaclab is installed and raise error if not."""
16
+ try:
17
+ import isaaclab # noqa: F401
18
+ except ImportError:
19
+ try:
20
+ import isaacsim # noqa: F401
21
+ raise ImportError(
22
+ "Isaac Lab is not installed. The 'view' command requires Isaac Lab to be installed.\n"
23
+ "Please install Isaac Lab to use this command. See: https://isaac-sim.github.io/IsaacLab/"
24
+ )
25
+ except ImportError:
26
+ from pathlib import Path
27
+ script_paths = [
28
+ Path(__file__).parent.parent.parent / "scripts" / "nepher_view.py",
29
+ Path("envhub/scripts/nepher_view.py"),
30
+ Path("scripts/nepher_view.py"),
31
+ ]
32
+ script_found = next((p for p in script_paths if p.exists()), None)
33
+ if script_found:
34
+ try:
35
+ script_found = str(script_found.relative_to(Path.cwd()))
36
+ except ValueError:
37
+ script_found = str(script_found)
38
+ help_msg = (
39
+ "Isaac Lab is not available in the current environment.\n\n"
40
+ "The 'view' command must be run through Isaac Lab's Python environment.\n"
41
+ f"Please use the script instead:\n\n"
42
+ f" isaaclab.bat -p {script_found} <env_id> --category <category> [--scene <scene>]\n\n"
43
+ "Or on Linux/Mac:\n"
44
+ f" ./isaaclab.sh -p {script_found} <env_id> --category <category> [--scene <scene>]\n\n"
45
+ "Example:\n"
46
+ f" isaaclab.bat -p {script_found} digital-twin-warehouse-v1 --category navigation --scene small_warehouse"
47
+ )
48
+ else:
49
+ help_msg = (
50
+ "Isaac Lab is not available in the current environment.\n\n"
51
+ "The 'view' command must be run through Isaac Lab's Python environment.\n"
52
+ "Please use the script 'scripts/nepher_view.py' with isaaclab.bat or isaaclab.sh.\n\n"
53
+ "Example:\n"
54
+ " isaaclab.bat -p scripts/nepher_view.py <env_id> --category <category> [--scene <scene>]"
55
+ )
56
+ raise ImportError(help_msg)
57
+
58
+
59
+ def _spawn_usd_scene(scene_cfg, env_cache_path=None):
60
+ """Spawn a USD scene in Isaac Sim."""
61
+ import omni.usd
62
+ from pxr import Gf, UsdGeom
63
+ from pathlib import Path
64
+ import isaaclab.sim as sim_utils
65
+
66
+ if not scene_cfg.usd_path:
67
+ raise ValueError("USD scene config missing usd_path")
68
+
69
+ usd_path = Path(scene_cfg.usd_path)
70
+ if not usd_path.is_absolute():
71
+ usd_path = (env_cache_path / usd_path) if env_cache_path else usd_path.resolve()
72
+ usd_path_str = str(usd_path.resolve())
73
+
74
+ print_info(f"Loading USD scene: {usd_path_str}")
75
+ stage = omni.usd.get_context().get_stage()
76
+
77
+ if not stage.GetPrimAtPath("/World"):
78
+ UsdGeom.Xform.Define(stage, "/World")
79
+
80
+ scene_prim_path = "/World/Scene"
81
+ scene_prim = stage.DefinePrim(scene_prim_path, "Xform")
82
+ scene_prim.GetReferences().AddReference(usd_path_str)
83
+
84
+ try:
85
+ children = scene_prim.GetChildren()
86
+ if children:
87
+ print_info(f"USD scene reference loaded with {len(children)} top-level prims")
88
+ elif scene_prim.IsValid():
89
+ from pxr import Usd
90
+ for child in Usd.PrimRange(scene_prim):
91
+ if child != scene_prim:
92
+ print_info(f"Found prim in scene: {child.GetPath()}")
93
+ break
94
+ except Exception as e:
95
+ print_error(f"Warning: Could not verify USD scene loading: {e}")
96
+
97
+ xform = UsdGeom.Xformable(scene_prim)
98
+
99
+ def _apply_xform_op(attr_name, default_value, op_type):
100
+ """Apply transform op if value differs from default."""
101
+ value = getattr(scene_cfg, attr_name, None)
102
+ if not value or value == default_value:
103
+ return
104
+ existing_ops = xform.GetOrderedXformOps()
105
+ op = next((o for o in existing_ops if o.GetOpType() == op_type), None)
106
+ if op is None:
107
+ op = xform.AddTranslateOp() if op_type == UsdGeom.XformOp.TypeTranslate else xform.AddScaleOp()
108
+ op.Set(Gf.Vec3d(*value))
109
+
110
+ _apply_xform_op('usd_position', (0.0, 0.0, 0.0), UsdGeom.XformOp.TypeTranslate)
111
+ _apply_xform_op('usd_scale', (1.0, 1.0, 1.0), UsdGeom.XformOp.TypeScale)
112
+
113
+ print_success(f"USD scene loaded at {scene_prim_path}")
114
+
115
+ sky_light_path = "/World/SkyLight"
116
+ if not stage.GetPrimAtPath(sky_light_path):
117
+ light_cfg = sim_utils.DomeLightCfg(intensity=1000.0, color=(1.0, 1.0, 1.0))
118
+ light_cfg.func(sky_light_path, light_cfg)
119
+ print_success("Added sky light")
120
+
121
+
122
+ def _spawn_preset_scene(scene_cfg):
123
+ """Spawn a preset scene in Isaac Sim."""
124
+ import isaaclab.sim as sim_utils
125
+ import omni.usd
126
+ from pxr import Gf, UsdGeom
127
+ from isaaclab.terrains import TerrainImporter
128
+
129
+ print_info("Spawning preset scene...")
130
+
131
+ stage = omni.usd.get_context().get_stage()
132
+ if not stage.GetPrimAtPath("/World"):
133
+ UsdGeom.Xform.Define(stage, "/World")
134
+
135
+ terrain_cfg = scene_cfg.get_terrain_cfg()
136
+ if terrain_cfg:
137
+ if not getattr(terrain_cfg, 'prim_path', None):
138
+ terrain_cfg.prim_path = "/World/Terrain"
139
+ TerrainImporter(terrain_cfg)
140
+ print_success(f"Terrain spawned (type: {terrain_cfg.terrain_type})")
141
+
142
+ if hasattr(scene_cfg, 'get_light_cfgs'):
143
+ for name, light_cfg in (scene_cfg.get_light_cfgs() or {}).items():
144
+ prim_path = getattr(light_cfg, 'prim_path', f"/World/Lights/{name}")
145
+ if getattr(light_cfg, 'spawn', None) is not None:
146
+ light_cfg.spawn.func(prim_path, light_cfg.spawn)
147
+ print_success(f"Light '{name}' spawned at {prim_path}")
148
+
149
+ if hasattr(scene_cfg, 'get_obstacle_cfgs'):
150
+ obstacle_cfgs = scene_cfg.get_obstacle_cfgs()
151
+ if obstacle_cfgs:
152
+ env_prim_path = "/World/envs/env_0"
153
+ for path in ["/World/envs", env_prim_path]:
154
+ if not stage.GetPrimAtPath(path):
155
+ UsdGeom.Xform.Define(stage, path)
156
+
157
+ for name, obs_cfg in obstacle_cfgs.items():
158
+ prim_path = getattr(obs_cfg, 'prim_path', f"{env_prim_path}/{name}").replace(
159
+ "{ENV_REGEX_NS}", env_prim_path
160
+ )
161
+ if getattr(obs_cfg, 'spawn', None) is not None:
162
+ obs_cfg.spawn.func(prim_path, obs_cfg.spawn)
163
+ init_state = getattr(obs_cfg, 'init_state', None)
164
+ if init_state is not None:
165
+ prim = stage.GetPrimAtPath(prim_path)
166
+ if prim.IsValid():
167
+ xformable = UsdGeom.Xformable(prim)
168
+ xformable.ClearXformOpOrder()
169
+
170
+ if getattr(init_state, 'pos', None) is not None:
171
+ pos = init_state.pos
172
+ xformable.AddTranslateOp().Set(Gf.Vec3d(pos[0], pos[1], pos[2]))
173
+
174
+ rot = getattr(init_state, 'rot', None)
175
+ if rot is not None:
176
+ import math
177
+ if len(rot) == 4:
178
+ xformable.AddOrientOp(UsdGeom.XformOp.PrecisionDouble).Set(
179
+ Gf.Quatd(rot[0], rot[1], rot[2], rot[3])
180
+ )
181
+ elif len(rot) == 3:
182
+ xformable.AddRotateXYZOp(UsdGeom.XformOp.PrecisionDouble).Set(
183
+ Gf.Vec3d(math.radians(rot[0]), math.radians(rot[1]), math.radians(rot[2]))
184
+ )
185
+ print_success(f"Spawned {len(obstacle_cfgs)} obstacles")
186
+
187
+ sky_light_path = "/World/SkyLight"
188
+ if not stage.GetPrimAtPath(sky_light_path):
189
+ light_cfg = sim_utils.DomeLightCfg(intensity=1000.0, color=(1.0, 1.0, 1.0))
190
+ light_cfg.func(sky_light_path, light_cfg)
191
+ print_success("Added default sky light")
192
+
193
+
194
+ def _setup_app_launcher():
195
+ """Set up AppLauncher and return the simulation app.
196
+
197
+ This function extracts AppLauncher arguments from the original sys.argv
198
+ and creates the AppLauncher instance. It should be called before importing
199
+ any Isaac Lab modules that require the app to be running.
200
+
201
+ Returns:
202
+ The SimulationApp instance from the AppLauncher.
203
+ """
204
+ _check_isaaclab_installed()
205
+
206
+ from isaaclab.app import AppLauncher
207
+
208
+ argv_to_parse = original_argv if original_argv is not None else sys.argv
209
+
210
+ app_launcher_args = []
211
+ i = 1 # Skip script name
212
+ while i < len(argv_to_parse):
213
+ arg = argv_to_parse[i]
214
+ if arg in ("--category", "--scene"):
215
+ i += 2 if i + 1 < len(argv_to_parse) else 1
216
+ continue
217
+ if not arg.startswith("--") and i == 1:
218
+ i += 1
219
+ continue
220
+ app_launcher_args.append(arg)
221
+ i += 1
222
+
223
+ parser = argparse.ArgumentParser()
224
+ AppLauncher.add_app_launcher_args(parser)
225
+ args_cli = parser.parse_args(app_launcher_args)
226
+
227
+ app_launcher = AppLauncher(args_cli)
228
+ return app_launcher.app
229
+
230
+
231
+ @click.command()
232
+ @click.argument("env_id")
233
+ @click.option("--category", required=True, help="Environment category")
234
+ @click.option("--scene", help="Scene name or index")
235
+ def view(env_id: str, category: str, scene: Optional[str]):
236
+ """View environment in Isaac Sim (requires isaaclab)."""
237
+ simulation_app = None
238
+ try:
239
+ simulation_app = _setup_app_launcher()
240
+
241
+ import isaaclab.sim as sim_utils
242
+ from isaaclab.sim import SimulationContext
243
+ import omni.usd
244
+
245
+ env = load_env(env_id, category)
246
+
247
+ if scene:
248
+ scene_cfg = load_scene(env, scene, category)
249
+ print_info(f"Loaded scene: {scene}")
250
+
251
+ usd_path = getattr(scene_cfg, 'usd_path', None)
252
+
253
+ sim_cfg = sim_utils.SimulationCfg(dt=0.01)
254
+ if usd_path:
255
+ _spawn_usd_scene(scene_cfg, env_cache_path=env.cache_path)
256
+ sim = SimulationContext(sim_cfg)
257
+ sim.set_camera_view(eye=[30.0, 30.0, 20.0], target=[0.0, 0.0, 0.0])
258
+ else:
259
+ sim = SimulationContext(sim_cfg)
260
+ _spawn_preset_scene(scene_cfg)
261
+ sim.set_camera_view(eye=[15.0, 15.0, 12.0], target=[0.0, 0.0, 0.0])
262
+
263
+ sim.reset()
264
+
265
+ print_success("Scene loaded successfully!")
266
+ click.echo("\n" + "=" * 60)
267
+ click.echo("Camera Controls:")
268
+ click.echo(" • Left-click + drag: Rotate view")
269
+ click.echo(" • Right-click + drag: Pan view")
270
+ click.echo(" • Scroll: Zoom in/out")
271
+ click.echo(" • F: Focus on selection")
272
+ click.echo("\nPress Ctrl+C in terminal to exit")
273
+ click.echo("=" * 60)
274
+
275
+ try:
276
+ simulation_app = sim.app
277
+ click.echo("\nRunning simulation...\n (Press Ctrl+C to exit)")
278
+ if simulation_app and hasattr(simulation_app, 'is_running'):
279
+ while simulation_app.is_running():
280
+ sim.step()
281
+ else:
282
+ while not sim.is_stopped():
283
+ sim.step()
284
+ except KeyboardInterrupt:
285
+ click.echo("\n\nExiting...")
286
+ except Exception as e:
287
+ if os.getenv("NEPHER_DEBUG"):
288
+ import traceback
289
+ traceback.print_exc()
290
+ click.echo(f"\nWarning: Could not start simulation loop: {e}")
291
+ click.echo(" Scene is loaded. You can interact with it in the Isaac Sim viewport.")
292
+ else:
293
+ print_info(f"Environment: {env.name}")
294
+ click.echo(f" Scenes: {len(env.get_all_scenes())}")
295
+ click.echo("\nAvailable scenes:")
296
+ for i, scene_obj in enumerate(env.get_all_scenes()):
297
+ click.echo(f" [{i}] {scene_obj.name}")
298
+
299
+ except KeyboardInterrupt:
300
+ click.echo("\n\nExiting...")
301
+ except Exception as e:
302
+ import traceback
303
+ print_error(f"Failed to view environment: {str(e)}")
304
+ if __debug__ or os.getenv("NEPHER_DEBUG"):
305
+ click.echo(f"Exception type: {type(e).__name__}")
306
+ traceback.print_exc()
307
+ finally:
308
+ if simulation_app is not None:
309
+ simulation_app.close()
310
+
nepher/cli/main.py ADDED
@@ -0,0 +1,30 @@
1
+ """
2
+ Main CLI entry point.
3
+ """
4
+
5
+ import click
6
+ from nepher.cli.commands import auth, list_cmd, download, upload, cache, view, config
7
+
8
+
9
+ @click.group()
10
+ @click.version_option()
11
+ def main():
12
+ """Nepher: Universal Isaac Lab Environments Platform."""
13
+ pass
14
+
15
+
16
+ # Register commands
17
+ main.add_command(auth.login)
18
+ main.add_command(auth.logout)
19
+ main.add_command(auth.whoami)
20
+ main.add_command(list_cmd.list_cmd)
21
+ main.add_command(download.download)
22
+ main.add_command(upload.upload)
23
+ main.add_command(cache.cache)
24
+ main.add_command(view.view)
25
+ main.add_command(config.config)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()
30
+
nepher/cli/utils.py ADDED
@@ -0,0 +1,28 @@
1
+ """CLI utility functions."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich import print as rprint
6
+
7
+ console = Console()
8
+
9
+
10
+ def print_success(message: str):
11
+ """Print success message."""
12
+ rprint(f"[green]✓[/green] {message}")
13
+
14
+
15
+ def print_error(message: str):
16
+ """Print error message."""
17
+ rprint(f"[red]✗[/red] {message}")
18
+
19
+
20
+ def print_info(message: str):
21
+ """Print info message."""
22
+ rprint(f"[blue]ℹ[/blue] {message}")
23
+
24
+
25
+ def print_warning(message: str):
26
+ """Print warning message."""
27
+ rprint(f"[yellow]⚠[/yellow] {message}")
28
+
nepher/config.py ADDED
@@ -0,0 +1,202 @@
1
+ """
2
+ Configuration management for Nepher.
3
+
4
+ Supports multiple configuration sources with priority:
5
+ 1. CLI arguments
6
+ 2. Environment variables
7
+ 3. Config file
8
+ 4. Category-specific overrides
9
+ 5. Default values
10
+ """
11
+
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Optional
15
+ from functools import lru_cache
16
+
17
+ try:
18
+ import tomllib # Python 3.11+
19
+ except ImportError:
20
+ import tomli as tomllib # Fallback for Python < 3.11
21
+
22
+
23
+ class Config:
24
+ """Centralized configuration manager."""
25
+
26
+ def __init__(self):
27
+ self._config: Dict[str, Any] = {}
28
+ self._config_file: Optional[Path] = None
29
+ self._load_config()
30
+
31
+ def _load_config(self):
32
+ """Load configuration from all sources."""
33
+ self._config = {
34
+ "api_url": "https://envhub.nepher.ai",
35
+ "api_key": None,
36
+ "cache_dir": "~/.nepher/cache",
37
+ "default_category": None,
38
+ "categories": {},
39
+ }
40
+
41
+ config_file = self._find_config_file()
42
+ if config_file and config_file.exists():
43
+ self._config_file = config_file
44
+ try:
45
+ if config_file.suffix == ".toml":
46
+ with open(config_file, "rb") as f:
47
+ file_config = tomllib.load(f)
48
+ self._config.update(file_config)
49
+ elif config_file.suffix == ".json":
50
+ import json
51
+
52
+ with open(config_file, "r") as f:
53
+ file_config = json.load(f)
54
+ self._config.update(file_config)
55
+ except Exception:
56
+ pass
57
+
58
+ if os.getenv("NEPHER_API_URL"):
59
+ self._config["api_url"] = os.getenv("NEPHER_API_URL")
60
+ if os.getenv("NEPHER_API_KEY"):
61
+ self._config["api_key"] = os.getenv("NEPHER_API_KEY")
62
+ if os.getenv("NEPHER_CACHE_DIR"):
63
+ self._config["cache_dir"] = os.getenv("NEPHER_CACHE_DIR")
64
+
65
+ def _find_config_file(self) -> Optional[Path]:
66
+ """Find config file in standard locations."""
67
+ cwd_config = Path.cwd() / ".nepherrc"
68
+ if cwd_config.exists():
69
+ return cwd_config
70
+
71
+ home_config = Path.home() / ".nepher" / "config.toml"
72
+ if home_config.exists():
73
+ return home_config
74
+
75
+ return None
76
+
77
+ def get(self, key: str, default: Any = None) -> Any:
78
+ """
79
+ Get configuration value.
80
+
81
+ Supports dot notation for nested keys (e.g., 'categories.navigation.cache_dir').
82
+ """
83
+ keys = key.split(".")
84
+ value = self._config
85
+
86
+ for k in keys:
87
+ if isinstance(value, dict) and k in value:
88
+ value = value[k]
89
+ else:
90
+ return default
91
+
92
+ return value
93
+
94
+ def set(self, key: str, value: Any, save: bool = True):
95
+ """
96
+ Set configuration value.
97
+
98
+ Supports dot notation for nested keys.
99
+ Creates config file if it doesn't exist.
100
+ """
101
+ keys = key.split(".")
102
+ config = self._config
103
+
104
+ for k in keys[:-1]:
105
+ if k not in config or not isinstance(config[k], dict):
106
+ config[k] = {}
107
+ config = config[k]
108
+
109
+ config[keys[-1]] = value
110
+
111
+ if save:
112
+ self._save_config()
113
+
114
+ def get_cache_dir(self, category: Optional[str] = None, override: Optional[str] = None) -> Path:
115
+ """
116
+ Get cache directory path.
117
+
118
+ Priority:
119
+ 1. override (CLI argument)
120
+ 2. Category-specific cache_dir
121
+ 3. Global cache_dir
122
+ 4. Default
123
+
124
+ Args:
125
+ category: Optional category name for category-specific cache
126
+ override: Optional override path (from CLI flag)
127
+
128
+ Returns:
129
+ Resolved Path object
130
+ """
131
+ if override:
132
+ path_str = override
133
+ elif category:
134
+ cat_config = self.get(f"categories.{category}", {})
135
+ path_str = cat_config.get("cache_dir") or self.get("cache_dir")
136
+ else:
137
+ path_str = self.get("cache_dir")
138
+
139
+ path = Path(path_str).expanduser().resolve()
140
+ path.mkdir(parents=True, exist_ok=True)
141
+
142
+ return path
143
+
144
+ def get_api_url(self) -> str:
145
+ """Get API URL."""
146
+ return self.get("api_url")
147
+
148
+ def get_api_key(self) -> Optional[str]:
149
+ """Get API key (may be None if not set)."""
150
+ return self.get("api_key")
151
+
152
+ def _remove_none_values(self, data: Dict[str, Any]) -> Dict[str, Any]:
153
+ """Recursively remove None values from dictionary for TOML serialization."""
154
+ result = {}
155
+ for key, value in data.items():
156
+ if value is None:
157
+ continue # Skip None values
158
+ elif isinstance(value, dict):
159
+ cleaned = self._remove_none_values(value)
160
+ if cleaned: # Only include non-empty dicts
161
+ result[key] = cleaned
162
+ else:
163
+ result[key] = value
164
+ return result
165
+
166
+ def _save_config(self):
167
+ """Save configuration to file."""
168
+ if not self._config_file:
169
+ config_dir = Path.home() / ".nepher"
170
+ config_dir.mkdir(parents=True, exist_ok=True)
171
+ self._config_file = config_dir / "config.toml"
172
+
173
+ try:
174
+ import tomli_w
175
+
176
+ config_to_save = self._remove_none_values(self._config)
177
+
178
+ with open(self._config_file, "wb") as f:
179
+ tomli_w.dump(config_to_save, f)
180
+ except ImportError:
181
+ import json
182
+
183
+ with open(self._config_file.with_suffix(".json"), "w") as f:
184
+ json.dump(self._config, f, indent=2)
185
+
186
+
187
+ _config_instance: Optional[Config] = None
188
+
189
+
190
+ @lru_cache(maxsize=1)
191
+ def get_config() -> Config:
192
+ """Get global configuration instance."""
193
+ global _config_instance
194
+ if _config_instance is None:
195
+ _config_instance = Config()
196
+ return _config_instance
197
+
198
+
199
+ def set_config(key: str, value: Any, save: bool = True):
200
+ """Set configuration value."""
201
+ get_config().set(key, value, save=save)
202
+
nepher/core.py ADDED
@@ -0,0 +1,67 @@
1
+ """
2
+ Core data structures for Nepher.
3
+
4
+ Defines Environment, Scene, and related types.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import List, Optional, Dict, Any, Union
10
+
11
+
12
+ @dataclass
13
+ class Scene:
14
+ """Represents a single scene within an environment."""
15
+
16
+ name: str
17
+ description: Optional[str] = None
18
+ usd: Optional[Path] = None
19
+ preset: Optional[str] = None
20
+ scene: Optional[str] = None # Python scene file path (for USD scenes with custom config)
21
+ omap_meta: Optional[Path] = None
22
+ metadata: Optional[Dict[str, Any]] = None
23
+
24
+
25
+ @dataclass
26
+ class Environment:
27
+ """Represents an environment bundle."""
28
+
29
+ id: str
30
+ name: str
31
+ description: Optional[str] = None
32
+ category: str = "navigation"
33
+ type: str = "usd" # "usd" or "preset"
34
+ version: Optional[str] = None
35
+ author: Optional[str] = None
36
+ scenes: List[Scene] = field(default_factory=list)
37
+ preset_scenes: List[Scene] = field(default_factory=list)
38
+ benchmark: bool = False
39
+ metadata: Optional[Dict[str, Any]] = None
40
+ cache_path: Optional[Path] = None
41
+
42
+ def get_scene(self, scene: Union[str, int]) -> Optional[Scene]:
43
+ """
44
+ Get a scene by name or index.
45
+
46
+ Args:
47
+ scene: Scene name (str) or index (int)
48
+
49
+ Returns:
50
+ Scene object or None if not found
51
+ """
52
+ all_scenes = self.scenes + self.preset_scenes
53
+
54
+ if isinstance(scene, int):
55
+ if 0 <= scene < len(all_scenes):
56
+ return all_scenes[scene]
57
+ else:
58
+ for s in all_scenes:
59
+ if s.name == scene:
60
+ return s
61
+
62
+ return None
63
+
64
+ def get_all_scenes(self) -> List[Scene]:
65
+ """Get all scenes (USD and preset combined)."""
66
+ return self.scenes + self.preset_scenes
67
+
@@ -0,0 +1,7 @@
1
+ """Environment configuration system."""
2
+
3
+ from nepher.env_cfgs.base import BaseEnvCfg
4
+ from nepher.env_cfgs.registry import get_config_class, register_config_class
5
+
6
+ __all__ = ["BaseEnvCfg", "get_config_class", "register_config_class"]
7
+
@@ -0,0 +1,32 @@
1
+ """
2
+ Base environment configuration interface.
3
+ """
4
+
5
+ from typing import Any
6
+
7
+
8
+ class BaseEnvCfg:
9
+ """Base interface for all environment configs (category-agnostic).
10
+
11
+ Note: Not using ABC to avoid pickling issues with @configclass decorator.
12
+ Subclasses should override methods and raise NotImplementedError if not implemented.
13
+ """
14
+
15
+ name: str = ""
16
+ description: str = ""
17
+ category: str = ""
18
+
19
+ def get_terrain_cfg(self) -> Any:
20
+ """Return terrain configuration.
21
+
22
+ Subclasses must override this method.
23
+ """
24
+ raise NotImplementedError("Subclass must implement get_terrain_cfg()")
25
+
26
+ def get_scene_cfg(self) -> Any:
27
+ """Return scene configuration.
28
+
29
+ Subclasses must override this method.
30
+ """
31
+ raise NotImplementedError("Subclass must implement get_scene_cfg()")
32
+
@@ -0,0 +1,4 @@
1
+ """Manipulation-specific environment configs."""
2
+
3
+ # Placeholder for manipulation configs
4
+