cyberwave-robot-format 0.1.2__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.
@@ -0,0 +1,80 @@
1
+ """Cyberwave Robot Format - Universal robot description schema and format converters.
2
+
3
+ This package provides a unified schema for representing robotic assets and tools
4
+ for converting between different robot description formats (URDF, MJCF, SDF, etc.).
5
+ """
6
+
7
+ from cyberwave_robot_format.schema import (
8
+ Actuator,
9
+ ActuatorType,
10
+ Collision,
11
+ CollisionConfig,
12
+ CollisionExclude,
13
+ CollisionGroup,
14
+ CollisionPair,
15
+ CommonSchema,
16
+ Contact,
17
+ ContactSurface,
18
+ Geometry,
19
+ GeometryType,
20
+ Inertia,
21
+ Joint,
22
+ JointDynamics,
23
+ JointLimits,
24
+ JointType,
25
+ Link,
26
+ Material,
27
+ Metadata,
28
+ Physics,
29
+ PhysicsSolver,
30
+ Pose,
31
+ Quaternion,
32
+ Scene,
33
+ Sensor,
34
+ Vector3,
35
+ Visual,
36
+ )
37
+ from cyberwave_robot_format.urdf import URDFParser, URDFExporter, export_urdf_zip_cloud, export_urdf_scene_xml
38
+ from cyberwave_robot_format.mjcf import MJCFParser, MJCFExporter, export_mujoco_zip_cloud, export_mujoco_scene_xml
39
+ from cyberwave_robot_format.schema_export import export_universal_schema_json
40
+
41
+ __version__ = "0.1.0"
42
+ __all__ = [
43
+ "CommonSchema",
44
+ "Link",
45
+ "Joint",
46
+ "Visual",
47
+ "Collision",
48
+ "CollisionConfig",
49
+ "CollisionExclude",
50
+ "CollisionGroup",
51
+ "CollisionPair",
52
+ "Geometry",
53
+ "Material",
54
+ "Pose",
55
+ "Vector3",
56
+ "Quaternion",
57
+ "Inertia",
58
+ "JointType",
59
+ "GeometryType",
60
+ "JointLimits",
61
+ "JointDynamics",
62
+ "Actuator",
63
+ "ActuatorType",
64
+ "Sensor",
65
+ "Contact",
66
+ "ContactSurface",
67
+ "Physics",
68
+ "PhysicsSolver",
69
+ "Scene",
70
+ "Metadata",
71
+ "URDFParser",
72
+ "URDFExporter",
73
+ "MJCFParser",
74
+ "MJCFExporter",
75
+ "export_mujoco_zip_cloud",
76
+ "export_mujoco_scene_xml",
77
+ "export_urdf_zip_cloud",
78
+ "export_urdf_scene_xml",
79
+ "export_universal_schema_json",
80
+ ]
@@ -0,0 +1,370 @@
1
+ """
2
+ Core format conversion engine and orchestration classes.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from .schema import CommonSchema, Material
13
+ from .utils import detect_format, validate_schema
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ParseError(Exception):
19
+ """Exception raised during parsing errors."""
20
+
21
+ pass
22
+
23
+
24
+ class ValidationError(Exception):
25
+ """Exception raised during validation errors."""
26
+
27
+ pass
28
+
29
+
30
+ @dataclass
31
+ class ParseContext:
32
+ """Context information for parsing operations with enhanced error tracking."""
33
+
34
+ file_path: Path
35
+ base_dir: Path
36
+ materials: dict[str, Material] = field(default_factory=dict)
37
+ meshes: dict[str, str] = field(default_factory=dict)
38
+ warnings: list[str] = field(default_factory=list)
39
+ errors: list[str] = field(default_factory=list)
40
+
41
+ def add_warning(self, message: str, element: str | None = None) -> None:
42
+ """Add warning message with optional element context."""
43
+ if element:
44
+ message = f"{element}: {message}"
45
+ self.warnings.append(message)
46
+ logger.warning(message)
47
+
48
+ def add_error(self, message: str, element: str | None = None) -> None:
49
+ """Add error message with optional element context."""
50
+ if element:
51
+ message = f"{element}: {message}"
52
+ self.errors.append(message)
53
+ logger.error(message)
54
+
55
+
56
+ class BaseParser(ABC):
57
+ """Base class for format parsers."""
58
+
59
+ @abstractmethod
60
+ def parse(self, input_path: str | Path) -> CommonSchema:
61
+ """Parse input file and return common schema representation."""
62
+
63
+ @abstractmethod
64
+ def can_parse(self, file_path: str | Path) -> bool:
65
+ """Check if this parser can handle the given file."""
66
+
67
+
68
+ class BaseExporter(ABC):
69
+ """Base class for format exporters."""
70
+
71
+ @abstractmethod
72
+ def export(self, schema: CommonSchema, output_path: str | Path) -> None:
73
+ """Export common schema to target format."""
74
+
75
+ def get_extension(self) -> str:
76
+ """Return the file extension for this format."""
77
+ raise NotImplementedError("Subclasses must implement get_extension()")
78
+
79
+
80
+ class ConversionEngine:
81
+ """Core conversion engine that orchestrates format conversions.
82
+
83
+ The engine maintains a registry of parsers and exporters for different
84
+ formats and handles the conversion workflow between them via the common
85
+ schema intermediate representation.
86
+
87
+ Example:
88
+ >>> engine = ConversionEngine()
89
+ >>> engine.register_parser('urdf', URDFParser())
90
+ >>> engine.register_exporter('sdf', SDFExporter())
91
+ >>> engine.convert('robot.urdf', 'robot.sdf')
92
+ """
93
+
94
+ def __init__(self):
95
+ self.parsers: dict[str, BaseParser] = {}
96
+ self.exporters: dict[str, BaseExporter] = {}
97
+
98
+ def register_parser(self, format_name: str, parser: BaseParser) -> None:
99
+ """Register a parser for a specific format."""
100
+ self.parsers[format_name.lower()] = parser
101
+ logger.debug("Registered parser for format: %s.", format_name)
102
+
103
+ def register_exporter(self, format_name: str, exporter: BaseExporter) -> None:
104
+ """Register an exporter for a specific format."""
105
+ self.exporters[format_name.lower()] = exporter
106
+ logger.debug("Registered exporter for format: %s.", format_name)
107
+
108
+ def get_parser(self, format_name: str) -> BaseParser | None:
109
+ """Get parser for a specific format."""
110
+ return self.parsers.get(format_name.lower())
111
+
112
+ def get_exporter(self, format_name: str) -> BaseExporter | None:
113
+ """Get exporter for a specific format."""
114
+ return self.exporters.get(format_name.lower())
115
+
116
+ def detect_format(self, file_path: str | Path) -> str | None:
117
+ """Detect format of a file using registered parsers."""
118
+ file_path = Path(file_path)
119
+
120
+ # Try each parser to see if it can parse the file
121
+ for format_name, parser in self.parsers.items():
122
+ if parser.can_parse(file_path):
123
+ return format_name
124
+
125
+ # Fall back to utility function
126
+ return detect_format(file_path)
127
+
128
+ def get_supported_formats(self) -> dict[str, list[str]]:
129
+ """Return supported formats for parsing and exporting."""
130
+ return {
131
+ "parsers": list(self.parsers.keys()),
132
+ "exporters": list(self.exporters.keys()),
133
+ }
134
+
135
+ def convert(
136
+ self,
137
+ input_path: str | Path,
138
+ output_path: str | Path,
139
+ source_format: str | None = None,
140
+ target_format: str | None = None,
141
+ validation: bool = True,
142
+ ) -> CommonSchema:
143
+ """Convert between robot description formats.
144
+
145
+ Args:
146
+ input_path: Path to input file
147
+ output_path: Path to output file
148
+ source_format: Source format (auto-detected if None)
149
+ target_format: Target format (inferred from extension if None)
150
+ validation: Whether to validate schema during conversion
151
+
152
+ Returns:
153
+ CommonSchema representation of the robot model.
154
+
155
+ Raises:
156
+ ValueError: If formats are unsupported or conversion fails.
157
+ FileNotFoundError: If input file doesn't exist.
158
+ """
159
+ input_path = Path(input_path)
160
+ output_path = Path(output_path)
161
+
162
+ if not input_path.exists():
163
+ raise FileNotFoundError(f"Input file not found: {input_path}")
164
+
165
+ # Auto-detect source format if not specified
166
+ if source_format is None:
167
+ source_format = self.detect_format(input_path)
168
+ if source_format is None:
169
+ raise ValueError(f"Cannot detect format for: {input_path}")
170
+
171
+ # Infer target format from extension if not specified
172
+ if target_format is None:
173
+ target_format = output_path.suffix.lstrip(".").lower()
174
+
175
+ # Get appropriate parser and exporter
176
+ parser = self.parsers.get(source_format.lower())
177
+ if parser is None:
178
+ raise ValueError(f"No parser found for format: {source_format}")
179
+
180
+ exporter = self.exporters.get(target_format.lower())
181
+ if exporter is None:
182
+ raise ValueError(f"No exporter found for format: {target_format}")
183
+
184
+ # Parse input to common schema
185
+ logger.info("Parsing %s file: %s.", source_format.upper(), input_path)
186
+ schema = parser.parse(input_path)
187
+
188
+ # Validate schema if requested
189
+ if validation:
190
+ logger.debug("Validating intermediate schema.")
191
+ validate_schema(schema)
192
+
193
+ # Export to target format
194
+ logger.info("Exporting to %s: %s.", target_format.upper(), output_path)
195
+ output_path.parent.mkdir(parents=True, exist_ok=True)
196
+ exporter.export(schema, output_path)
197
+
198
+ logger.info("Conversion complete: %s -> %s.", input_path, output_path)
199
+ return schema
200
+
201
+
202
+ class FormatConverter:
203
+ """High-level interface for robot format conversions.
204
+
205
+ This class provides a simplified API for common conversion tasks,
206
+ with built-in parsers and exporters for standard formats.
207
+
208
+ Example:
209
+ >>> converter = FormatConverter()
210
+ >>> converter.urdf_to_sdf('robot.urdf', 'robot.sdf')
211
+ >>> converter.batch_convert('models/', 'output/', 'urdf', 'mjcf')
212
+ """
213
+
214
+ def __init__(self):
215
+ self.engine = ConversionEngine()
216
+ self._register_default_processors()
217
+
218
+ def _register_default_processors(self) -> None:
219
+ """Register default parsers and exporters for standard formats."""
220
+ # Import from submodules
221
+ from .mjcf import MJCFExporter, MJCFParser
222
+ from .obj import OBJMTLExporter, OBJMTLParser
223
+ from .schema_io import SchemaExporter, SchemaParser
224
+ from .sdf import SDFExporter, SDFParser
225
+ from .urdf import URDFExporter, URDFParser
226
+
227
+ # Register parsers
228
+ self.engine.register_parser("urdf", URDFParser())
229
+ self.engine.register_parser("sdf", SDFParser())
230
+ self.engine.register_parser("mjcf", MJCFParser())
231
+ self.engine.register_parser("xml", MJCFParser()) # MJCF is XML
232
+ self.engine.register_parser("schema", SchemaParser())
233
+ self.engine.register_parser("yaml", SchemaParser())
234
+ self.engine.register_parser("json", SchemaParser())
235
+ self.engine.register_parser("obj", OBJMTLParser())
236
+
237
+ # Register exporters
238
+ self.engine.register_exporter("urdf", URDFExporter())
239
+ self.engine.register_exporter("sdf", SDFExporter())
240
+ self.engine.register_exporter("mjcf", MJCFExporter())
241
+ self.engine.register_exporter("xml", MJCFExporter())
242
+ self.engine.register_exporter("schema", SchemaExporter())
243
+ self.engine.register_exporter("yaml", SchemaExporter())
244
+ self.engine.register_exporter("json", SchemaExporter())
245
+ self.engine.register_exporter("obj", OBJMTLExporter())
246
+
247
+ # Try to register USD support if available
248
+ try:
249
+ from .usd import USDExporter, USDParser
250
+
251
+ self.engine.register_parser("usd", USDParser())
252
+ self.engine.register_parser("usda", USDParser())
253
+ self.engine.register_exporter("usd", USDExporter())
254
+ self.engine.register_exporter("usda", USDExporter())
255
+ except ImportError:
256
+ logger.debug("USD support not available (missing pxr module).")
257
+
258
+ def convert(
259
+ self, input_path: str | Path, output_path: str | Path, **kwargs
260
+ ) -> CommonSchema:
261
+ """Convert between formats using the conversion engine."""
262
+ return self.engine.convert(input_path, output_path, **kwargs)
263
+
264
+ def urdf_to_sdf(self, urdf_path: str, sdf_path: str) -> CommonSchema:
265
+ """Convert URDF to SDF format."""
266
+ return self.convert(
267
+ urdf_path, sdf_path, source_format="urdf", target_format="sdf"
268
+ )
269
+
270
+ def urdf_to_mjcf(self, urdf_path: str, mjcf_path: str) -> CommonSchema:
271
+ """Convert URDF to MJCF format."""
272
+ return self.convert(
273
+ urdf_path, mjcf_path, source_format="urdf", target_format="mjcf"
274
+ )
275
+
276
+ def sdf_to_urdf(self, sdf_path: str, urdf_path: str) -> CommonSchema:
277
+ """Convert SDF to URDF format."""
278
+ return self.convert(
279
+ sdf_path, urdf_path, source_format="sdf", target_format="urdf"
280
+ )
281
+
282
+ def to_schema(self, input_path: str, schema_path: str) -> CommonSchema:
283
+ """Convert any supported format to common schema."""
284
+ return self.convert(input_path, schema_path, target_format="schema")
285
+
286
+ def from_schema(
287
+ self, schema_path: str, output_path: str, target_format: str | None = None
288
+ ) -> CommonSchema:
289
+ """Convert from common schema to any supported format."""
290
+ return self.convert(
291
+ schema_path,
292
+ output_path,
293
+ source_format="schema",
294
+ target_format=target_format,
295
+ )
296
+
297
+ def batch_convert(
298
+ self,
299
+ input_dir: str | Path,
300
+ output_dir: str | Path,
301
+ source_format: str,
302
+ target_format: str,
303
+ pattern: str = "*",
304
+ ) -> list[Path]:
305
+ """Batch convert files in a directory.
306
+
307
+ Args:
308
+ input_dir: Input directory path
309
+ output_dir: Output directory path
310
+ source_format: Source file format
311
+ target_format: Target file format
312
+ pattern: File pattern to match (default: "*")
313
+
314
+ Returns:
315
+ List of successfully converted output files.
316
+ """
317
+ input_dir = Path(input_dir)
318
+ output_dir = Path(output_dir)
319
+
320
+ if not input_dir.exists():
321
+ raise FileNotFoundError(f"Input directory not found: {input_dir}")
322
+
323
+ output_dir.mkdir(parents=True, exist_ok=True)
324
+
325
+ # Find matching files
326
+ # If pattern already includes extension, use it directly
327
+ # Otherwise, append the source format extension
328
+ if pattern.endswith(f".{source_format}") or "*" in pattern:
329
+ # Pattern already specifies the extension
330
+ input_files = list(input_dir.glob(pattern))
331
+ else:
332
+ ext = f".{source_format}"
333
+ input_files = list(input_dir.glob(f"{pattern}{ext}"))
334
+
335
+ if not input_files:
336
+ logger.warning("No %s files found in %s.", source_format, input_dir)
337
+ return []
338
+
339
+ converted_files = []
340
+ target_ext = f".{target_format}"
341
+
342
+ for input_file in input_files:
343
+ output_file = output_dir / (input_file.stem + target_ext)
344
+ try:
345
+ self.convert(
346
+ input_file,
347
+ output_file,
348
+ source_format=source_format,
349
+ target_format=target_format,
350
+ )
351
+ converted_files.append(output_file)
352
+ except Exception as e:
353
+ logger.error("Failed to convert %s: %s.", input_file, e)
354
+
355
+ logger.info(
356
+ "Batch conversion complete: %s/%s files converted.",
357
+ len(converted_files),
358
+ len(input_files),
359
+ )
360
+ return converted_files
361
+
362
+ def get_conversion_matrix(self) -> dict[str, list[str]]:
363
+ """Get matrix of supported conversion paths."""
364
+ formats = self.engine.get_supported_formats()
365
+ matrix = {}
366
+
367
+ for source_fmt in formats["parsers"]:
368
+ matrix[source_fmt] = formats["exporters"]
369
+
370
+ return matrix
@@ -0,0 +1,110 @@
1
+ """
2
+ Math utilities for robot format conversion.
3
+
4
+ This module provides common mathematical types used across the format conversion
5
+ pipeline: vectors, quaternions, poses, and inertia tensors.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from dataclasses import dataclass
12
+
13
+ import numpy as np
14
+
15
+
16
+ @dataclass
17
+ class Vector3:
18
+ """3D vector representation."""
19
+
20
+ x: float = 0.0
21
+ y: float = 0.0
22
+ z: float = 0.0
23
+
24
+ def to_list(self) -> list[float]:
25
+ """Convert to list format."""
26
+ return [self.x, self.y, self.z]
27
+
28
+ def to_array(self) -> np.ndarray:
29
+ """Convert to numpy array."""
30
+ return np.array([self.x, self.y, self.z])
31
+
32
+ @classmethod
33
+ def from_list(cls, values: list[float]) -> Vector3:
34
+ """Create from list of values."""
35
+ return cls(values[0], values[1], values[2])
36
+
37
+
38
+ @dataclass
39
+ class Quaternion:
40
+ """Quaternion representation for rotations."""
41
+
42
+ x: float = 0.0
43
+ y: float = 0.0
44
+ z: float = 0.0
45
+ w: float = 1.0
46
+
47
+ def to_list(self) -> list[float]:
48
+ """Convert to list format [x, y, z, w]."""
49
+ return [self.x, self.y, self.z, self.w]
50
+
51
+ @classmethod
52
+ def from_rpy(cls, roll: float, pitch: float, yaw: float) -> Quaternion:
53
+ """Create quaternion from roll-pitch-yaw angles (Fixed XYZ convention)."""
54
+ cy = math.cos(yaw * 0.5)
55
+ sy = math.sin(yaw * 0.5)
56
+ cp = math.cos(pitch * 0.5)
57
+ sp = math.sin(pitch * 0.5)
58
+ cr = math.cos(roll * 0.5)
59
+ sr = math.sin(roll * 0.5)
60
+
61
+ w = cr * cp * cy + sr * sp * sy
62
+ x = sr * cp * cy - cr * sp * sy
63
+ y = cr * sp * cy + sr * cp * sy
64
+ z = cr * cp * sy - sr * sp * cy
65
+
66
+ return cls(x, y, z, w)
67
+
68
+
69
+ @dataclass
70
+ class Pose:
71
+ """6DOF pose representation."""
72
+
73
+ position: Vector3 = None # type: ignore[assignment]
74
+ orientation: Quaternion = None # type: ignore[assignment]
75
+
76
+ def __post_init__(self) -> None:
77
+ if self.position is None:
78
+ self.position = Vector3()
79
+ if self.orientation is None:
80
+ self.orientation = Quaternion()
81
+
82
+ @classmethod
83
+ def from_xyzrpy(cls, xyz: list[float], rpy: list[float]) -> Pose:
84
+ """Create pose from position and RPY orientation."""
85
+ return cls(
86
+ position=Vector3.from_list(xyz),
87
+ orientation=Quaternion.from_rpy(rpy[0], rpy[1], rpy[2]),
88
+ )
89
+
90
+
91
+ @dataclass
92
+ class Inertia:
93
+ """Inertia tensor representation."""
94
+
95
+ ixx: float = 0.0
96
+ iyy: float = 0.0
97
+ izz: float = 0.0
98
+ ixy: float = 0.0
99
+ ixz: float = 0.0
100
+ iyz: float = 0.0
101
+
102
+ def to_matrix(self) -> np.ndarray:
103
+ """Convert to 3x3 inertia matrix."""
104
+ return np.array(
105
+ [
106
+ [self.ixx, self.ixy, self.ixz],
107
+ [self.ixy, self.iyy, self.iyz],
108
+ [self.ixz, self.iyz, self.izz],
109
+ ]
110
+ )
@@ -0,0 +1,13 @@
1
+ from cyberwave_robot_format.mesh.processing import (
2
+ convert_dae_to_obj,
3
+ convert_mesh_bytes_to_obj,
4
+ get_mesh_lookup_key,
5
+ )
6
+ from cyberwave_robot_format.mesh.resolution import resolve_mesh_uri
7
+
8
+ __all__ = [
9
+ "convert_dae_to_obj",
10
+ "convert_mesh_bytes_to_obj",
11
+ "get_mesh_lookup_key",
12
+ "resolve_mesh_uri",
13
+ ]