Cyberwave Robot Format
Universal robot description schema and format converters for Cyberwave.
Overview
This package provides:
- Universal Schema: A canonical representation for robotic assets (
CommonSchema) - Format Importers: Parse URDF, MJCF, USD into the universal schema
- Format Exporters: Export universal schema to URDF, MJCF, USD
- Validation: Schema validation and consistency checks
Structure
cyberwave_robot_format/
├── schema.py # Core schema definitions (CommonSchema, Link, Joint, etc.)
├── core.py # Base classes for parsers/exporters
├── urdf/ # URDF parser and exporter
├── mjcf/ # MJCF (MuJoCo) parser and exporter
├── usd/ # USD (OpenUSD) parser and exporter
├── mesh/ # Mesh processing utilities
├── math_utils.py # Math utilities (Vector3, Quaternion, etc.)
└── utils.py # General utilities
Schema changes in 0.1.6
Two fixes to CommonSchema itself. Both are backwards compatible to read — an
older JSON document still parses — but they change what the package writes, so any
hash computed over export_universal_schema_json output will differ from 0.1.5.
Materialgained anextensionsfield, matching every other component dataclass.MJCFParseralready wroteextensions["reflectance"]there, so before this any MuJoCo model declaring a material withreflectancefailed to parse with anAttributeError. Serialized materials now carry an"extensions": {}key.Vector3andQuaternioncoerce their components tofloat. The fields were always declaredfloat, butVector3(0, 0, 1)— written inURDFParserand inJoint.axis's own default — leftintcomponents behind, so the same model serialized an axis as"z": 1fresh from the parser and"z": 1.0after anyfrom_dict.export_universal_schema_jsonis now idempotent under a round trip. As a side effect,numpyscalars are normalized too, whichjson.dumpscannot serialize at all.
Installation
pip install cyberwave-robot-format
# USD support needs the OpenUSD Python bindings, which are an optional extra:
pip install "cyberwave-robot-format[usd]"
The rest of the package works without them — importing cyberwave_robot_format
never imports pxr, so only calling the USD parser or exporter requires it.
usd-core publishes no linux-aarch64 wheel; on that platform install
conda-forge's openusd instead.
Usage
Parse URDF
from cyberwave_robot_format import CommonSchema
from cyberwave_robot_format.urdf import URDFParser
# Parse a URDF file
parser = URDFParser()
schema = parser.parse("path/to/robot.urdf")
# Validate the schema
issues = schema.validate()
if issues:
print("Validation issues:", issues)
# Access robot components
for link in schema.links:
print(f"Link: {link.name}, mass: {link.mass}")
for joint in schema.joints:
print(f"Joint: {joint.name}, type: {joint.type}")
Tolerated URDF quirks
Real-world URDFs are often slightly out of spec. The parser recovers from these
rather than failing, and records each one under
schema.extensions["parse_context"] — in warnings, or in errors where the
recovery had to discard something the file asked for:
| In the file | What you get |
|---|---|
<robot> with no name, or a blank/whitespace one |
The name falls back to the URDF file stem |
<joint> with no type |
The joint is kept as fixed, so its child link stays attached to the tree |
<joint> with no type, but with <axis>, <limit>, <mimic> or <safety_controller> |
Also kept as fixed, but recorded in errors — the file described motion that fixed discards, and the spec cannot say which moving type was meant. <dynamics> is not treated as motion evidence, because real files carry it on genuinely fixed joints |
type="Revolute", type=" revolute " |
Normalized to revolute |
An unrecognized joint type (say type="screw") is still an error and the joint
is dropped — recovery covers omissions and formatting, not unsupported
kinematics. Check parse_context["errors"] alongside warnings when a
conversion looks wrong.
Parse MJCF (MuJoCo)
from cyberwave_robot_format.mjcf import MJCFParser
# Parse a MuJoCo XML file
parser = MJCFParser()
schema = parser.parse("path/to/robot.xml")
# Access actuators
for actuator in schema.actuators:
print(f"Actuator: {actuator.name}, joint: {actuator.joint}")
Export to MJCF
from cyberwave_robot_format.mjcf import MJCFExporter
# Export schema to MuJoCo format
exporter = MJCFExporter()
exporter.export(schema, "output/robot.xml")
Continuous joints (a continuous joint type, e.g. a wheel or a spinner)
export as an honest limited="false" free hinge with no positional range —
even if the schema happens to carry limits. Because MuJoCo position actuators
still need a finite ctrlrange, the actuator on a continuous joint is given a
finite band centered on the home pose (home ± π) instead, so the joint stays
truly unlimited while the servo remains usable. Since v0.1.4.
Parse and export USD (OpenUSD)
Requires the usd extra (see Installation). Reads and writes
.usda (text), .usdc (binary), .usd and .usdz.
from cyberwave_robot_format import USDExporter, USDParser
# Schema -> USD. The suffix picks the encoding; .usda is the human-readable one.
USDExporter().export(schema, "output/robot.usda")
# USD -> schema
schema = USDParser().parse("output/robot.usda")
# Or work with the text directly, no files involved
usda_text = USDExporter().export_to_string(schema)
schema = USDParser().parse_string(usda_text)
The export is a complete UsdPhysics articulation — rigid bodies with mass and
inertia, joints with limits and drives, visual and collision geometry, materials,
and collision filtering — so Isaac Sim, Omniverse and usdview can consume it
directly. Alongside each native attribute the exporter also writes a
cyberwave:-namespaced double-precision copy for anything USD stores lossily or
cannot express at all (mimic joints, armature, jerk limits, motor electricals), and
the parser prefers those. The round trip is therefore lossless for every field the
schema can hold, while the stage stays valid USD for everyone else.
Reading foreign USD works too: with no cyberwave: attributes present, links come
from PhysicsRigidBodyAPI, the kinematic tree from physics:body0/body1, joint
axes from physics:axis plus the joint frame rotations, and a
UsdPhysicsDriveAPI becomes an actuator with its gains — so an Isaac Sim robot
imports as an actuated model rather than a passive one.
Because both halves of the schema survive, a USD detour is transparent to the other
converters — URDF -> schema -> MJCF and URDF -> schema -> USD -> schema -> MJCF
produce byte-identical MJCF:
# The format-specific payloads MJCFParser stashes in `extensions` (MuJoCo's
# contype/conaffinity/solref/margin, its <size> and <default> blocks, <contact><pair>
# attributes) are carried through USD untouched, so nothing is lost on the way.
schema = MJCFParser().parse("robot.xml")
MJCFExporter().export(USDParser().parse_string(USDExporter().export_to_string(schema)),
"same_robot.xml")
Conversely, UsdPhysics properties with no schema field of their own —
physics:breakForce, jointEnabled, kinematicEnabled, velocity, a spherical
joint's cone limits, a drive's targetVelocity — are preserved under
extensions["usd"] on import and written back as native physics: attributes on
export, so a foreign stage survives a schema hop too.
Parsing USD you did not author
Opening a USD file composes it: subLayers, references, payloads and
variants are resolved, and their content becomes part of the schema you get back.
A hostile file can name an absolute path and pull that file's prims into the
result:
#usda 1.0
(
subLayers = [@/home/someone/private/robot.usda@]
)
A service that parses an upload and returns or stores the resulting schema would be handing back the content of local USD files it never meant to expose. For untrusted input, restrict composition to the file's own directory:
# Raises ValueError if composition reaches outside the parsed file's directory.
schema = USDParser(allow_external_layers=False).parse(uploaded_path)
Layers beside or beneath the input still resolve normally, so ordinary multi-file
assets keep working; symlinks are resolved before the check, so they cannot step
outside. The default is True because layering is the defining feature of USD and
real assets reference sibling directories — the restriction is opt-in precisely
because it is the caller who knows whether the input is trusted.
Two things you do not need to defend against (verified against usd-core 26.8):
the default asset resolver does not fetch http(s):// references, so there is no
SSRF vector, and .usdz is read in place rather than extracted. Note also that
USDExporter(bake_meshes=True) reads whatever path Geometry.filename holds — do
not enable it for schemas from an untrusted source.
Things worth knowing:
- Angles. USD stores angular joint limits and drive targets in degrees; the
schema uses radians. The conversion is automatic, and the
cyberwave:sidecars are always in schema units. - Units. The stage is always authored
metersPerUnit = 1,kilogramsPerUnit = 1and Z-up, because that is what every number in the schema means. A schema declaringmetadata.unitsas anything but"SI"is written unconverted, with a warning. - Link layout.
UsdPhysicsignores a rigid body nested under another one, so link prims are flat siblings under/<Robot>/Links, each carrying its accumulated world transform at the zero configuration. - Joint axes.
physics:axisonly accepts"X"/"Y"/"Z", so an arbitrary schema axis is baked intophysics:localRot0/localRot1and the token is always"X". - Meshes.
Geometry.filenameis preserved verbatim as a string; USD cannot reference an.stl/.dae/.objas a layer. PassUSDExporter(bake_meshes=True)to additionally resolve and inline the mesh points. A.usdzwhose meshes are unresolvablepackage://URIs is still written, with a warning that those files are not bundled. - World physics.
Physicsmaps to aUsdPhysicsSceneauthored beside the robot prim, so referencing the asset into a larger stage does not drag a second gravity definition along. On import the scene is located by prim type rather than by path, so a foreign stage that names it anything else (/physicsScene,/World/PhysicsScene) still contributes its gravity and solver settings. - One articulation per parse. A stage declaring several
PhysicsArticulationRootAPIprims (a work cell, a robot plus an AMR) parses as the first one, with a warning naming the roots that were skipped. - Materials. A named material is defined once under
/<Robot>/Materialsand shared by every visual using it. If two different materials share a name, the first owns the shared prim and the others are authored inline per visual, with a warning — so both appearances survive the round trip.
Since v0.1.6.
Infer URDF mimic joints (gripper coupling)
When a URDF has coupled finger / gripper joints but no <mimic> tags, infer pairs from
kinematics and write a new {stem}-mimic-joint.urdf (the original file is never modified).
from pathlib import Path
from cyberwave_robot_format.urdf import (
infer_mimic_joints,
infer_and_patch_if_needed,
write_mimic_patched_urdf,
)
result = infer_mimic_joints("robot.urdf")
for m in result.inferred_mimics:
print(m.driver_joint, "→", m.slave_joint, "mult", m.multiplier, "conf", m.confidence)
# Write patched URDF when confidence ≥ 0.85 (default)
patched = infer_and_patch_if_needed(Path("robot.urdf"))
if patched.output_path:
print("Wrote", patched.output_path)
Multiplier defaults
| Context | Default |
|---|---|
URDF <mimic> if multiplier omitted |
1 |
offset omitted |
0 |
| Inference for opposing prismatic jaws | Often -1 when complementary limits validate |
Inference checks opposing axes, complementary joint limits, and samples driver positions so
slave = multiplier × driver + offset stays within slave limits. Pass an optional mjcf_path
to seed coeffs from MuJoCo equality constraints.
Used by Cyberwave backend seed_controllers --infer-mimic-from-urdf and
src/lib/urdf_mimic_utils.py. See cyberwave-backend/docs/mimic-joints.md for the full
platform workflow (autogen, teleop, MQTT).
Cloud-Native Scene Export
Export complete scenes with meshes to ZIP files, supporting cloud storage and in-memory conversion:
from cyberwave_robot_format.mjcf import export_mujoco_zip_cloud
from cyberwave_robot_format.urdf import export_urdf_zip_cloud
# Cloud-safe resolver with in-memory DAE→OBJ conversion
def s3_resolver(filename: str) -> tuple[str, bytes] | None:
"""Download from S3 and convert in memory."""
mesh_bytes = s3.get_object(Bucket='meshes', Key=filename)['Body'].read()
if filename.endswith('.dae'):
obj_bytes = convert_dae_to_obj_in_memory(mesh_bytes)
return (filename.replace('.dae', '.obj'), obj_bytes)
return (Path(filename).name, mesh_bytes)
# Export with cloud resolver (mesh_resolver is required)
mujoco_zip = export_mujoco_zip_cloud(
schema,
s3_resolver,
strict_missing_meshes=True # Fail fast on missing meshes
)
urdf_zip = export_urdf_zip_cloud(schema, s3_resolver)
Development
Install in editable mode:
pip install -e .
Run tests:
pytest
Acknowledgments
This project incorporates portions of code from [https://github.com/thanhndv212/robot_format_converter](Robot Format Converter) (Apache 2.0 licensed).
Original repository: https://github.com/thanhndv212/robot_format_converter
We thank the original authors for their initial work.
@software{robot_format_converter,
author = {Nguyen, Thanh},
title = {Robot Format Converter: Universal Robot Description Format Converter},
year = {2025},
url = {https://github.com/thanhndv212/robot_format_converter},
version = {1.0.0}
}
Release files for cyberwave-robot-format 0.1.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cyberwave_robot_format-0.1.6.tar.gz | 214.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cyberwave_robot_format-0.1.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 357.9 kB
Release files / cyberwave_robot_format-0.1.6.tar.gz
| Download URL | cyberwave_robot_format-0.1.6.tar.gz |
|---|---|
| Size | 214.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
53929997fefaf97da0d413ff8e31cfd42635dd9061201756085ce1079be9ecfb
|
|
BLAKE2b-256 checksum How to use checksums |
cff73cd5372062738b3abbd1d271f1f07a981bc7250220cbe9162f1660c1d2aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.
Transparency logRelease files / cyberwave_robot_format-0.1.6-py3-none-any.whl
| Download URL | cyberwave_robot_format-0.1.6-py3-none-any.whl |
|---|---|
| Size | 143.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b7a6ce04ff0b5e08a086a99fc32b930d0ee1bc60513bab01f39b3078c3d98366
|
|
BLAKE2b-256 checksum How to use checksums |
c2e76dd5eef21d716ef51f4d6528f991deb2bb307036d1c80b1630c2ebdf2e69
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.
Transparency log