Scene Objects and Assets
Constructor reference for geometric primitives, YCB objects, and meshes, plus the YCB download cache and bundled simulation asset helpers.
Scene objects define what the robot interacts with. All object types live in so101_nexus and share the abstract SceneObject base class.
from so101_nexus import (
CubeObject,
CylinderObject,
GSOObject,
MeshObject,
PyramidObject,
SphereObject,
YCBObject,
)All concrete object types work on the MuJoCo and MuJoCo Warp backends. Both backends build dynamic-object scenes through so101_nexus.object_slots.build_object_scene_xml.
SceneObject (abstract)
Base class for all scene objects. Subclasses must implement __repr__(), which returns a natural-language description of the object (used in logging and in the task string). SceneObject cannot be instantiated directly; use one of the concrete subclasses below.
CubeObject
A solid-color cube with configurable size and mass. Set side_length_mm as a
float in millimeters. The existing half_size argument remains available for
meter-based code.
from so101_nexus import CubeObject
cube = CubeObject(side_length_mm=25.4, mass=0.015, color="blue")
repr(cube) # "blue cube"| Parameter | Type | Default | Description |
|---|---|---|---|
half_size | float | None | None | Legacy half side length in meters. The default cube has half_size=0.0125. Do not combine with side_length_mm. |
side_length_mm | float | None | None | Full cube side length in millimeters. |
mass | float | 0.01 | Mass of the cube (kg) |
color | ColorName | "red" | Color of the cube. Must be a valid ColorName. |
__repr__() returns "{color} cube", for example "red cube".
CylinderObject, SphereObject, and PyramidObject
These solid-color primitives accept mass and color like CubeObject.
CylinderObject and SphereObject use diameter_mm. PyramidObject uses
side_length_mm. Each primitive keeps its existing half_size argument for
meter-based code.
from so101_nexus import CylinderObject, PyramidObject, SphereObject
cylinder = CylinderObject(diameter_mm=25.4, color="blue")
sphere = SphereObject(diameter_mm=12.7, color="green")
pyramid = PyramidObject(side_length_mm=25.4, color="yellow")The cylinder height equals its diameter. The square pyramid height equals its
base side length. __repr__() returns "{color} cylinder", "{color} sphere", or "{color} pyramid". The valid colors are the same ColorName
values that CubeObject accepts.
YCBObject
A YCB benchmark object loaded from mesh files. Assets are downloaded on first use and cached (see YCB assets).
from so101_nexus import YCBObject
banana = YCBObject("011_banana")
repr(banana) # "banana"| Parameter | Type | Default | Description |
|---|---|---|---|
model_id | str | required | YCB model identifier. Must be a key in YCB_OBJECTS. |
mass_override | float | None | None | Override the default mass. Uses the original mass when None. |
__repr__() returns the human-readable name from the YCB_OBJECTS dictionary.
Model IDs
model_id must be one of the ten supported IDs, which are also the members of the YcbModelId type alias:
| Model ID | YCB_OBJECTS name |
|---|---|
009_gelatin_box | gelatin box |
011_banana | banana |
030_fork | fork |
031_spoon | spoon |
032_knife | knife |
033_spatula | spatula |
037_scissors | scissors |
040_large_marker | large marker |
043_phillips_screwdriver | phillips screwdriver |
058_golf_ball | golf ball |
Every rest pose above is settle-tested against real MuJoCo physics (see
get_mujoco_ycb_rest_pose()); a few needed a
corrected orientation because the default "thinnest axis up" guess tips over
under real dynamics. A handful of objects (both YCB and GSO) also have a
grasp advisory: a geometric screen found no tested cross-section that
fits inside the SO-101 gripper's aperture, so the object is likely too wide
to close on with this arm. This is advisory only, not a construction-time
error - spot-check via teleoperation or a policy rollout before relying on a
flagged object for a real grasp task. The current list is in
so101_nexus.ycb_geometry.GRASP_ADVISORY.
Passing any other model_id raises an error. The same mapping is available at runtime:
from so101_nexus import YCB_OBJECTS
for model_id, name in YCB_OBJECTS.items():
print(f"{model_id}: {name}")GSOObject
A Google Scanned Objects
(GSO) object loaded from mesh files, sharing YCBObject's convex-hull
collision pipeline. Assets are downloaded on first use and cached (see GSO
assets).
from so101_nexus import GSOObject
clamp = GSOObject("Pony_C_Clamp_1440")
repr(clamp) # "C-clamp"| Parameter | Type | Default | Description |
|---|---|---|---|
model_id | str | required | GSO model identifier. Must be a key in GSO_OBJECTS. |
mass_override | float | None | None | Override the default mass. Uses the hand-estimated mass when None. |
__repr__() returns the human-readable name from the GSO_OBJECTS dictionary.
Unlike YCB, GSO ships no benchmark-measured masses. Each model_id's default
mass in GSO_MASSES is a hand estimate: convex-hull volume from the mirrored
scan times an assumed effective density for the filled/packaged object,
rounded to the nearest 5 g. Pass mass_override to replace it with a
measured value.
Model IDs
model_id must be one of the twelve supported IDs, which are also the members of the GsoModelId type alias:
| Model ID | GSO_OBJECTS name |
|---|---|
Pony_C_Clamp_1440 | C-clamp |
Cole_Hardware_Mini_Honey_Dipper | honey dipper |
OXO_Soft_Works_Can_Opener_SnapLock | can opener |
3M_Vinyl_Tape_Green_1_x_36_yd | tape roll |
Shurtape_Gaffers_Tape_Silver_2_x_60_yd | gaffer tape roll |
Big_O_Sponges_Assorted_Cellulose_12_pack | sponge pack |
BIA_Porcelain_Ramekin_With_Glazed_Rim_35_45_oz_cup | ramekin |
CoQ10 | supplement bottle |
Wilton_Pearlized_Sugar_Sprinkles_525_oz_Gold | sprinkles canister |
Marc_Anthony_Strictly_Curls_Curl_Envy_Perfect_Curl_Cream_6_fl_oz_bottle | lotion bottle |
Black_Elderberry_Syrup_54_oz_Gaia_Herbs | syrup bottle |
Nestle_Raisinets_Milk_Chocolate_35_oz_992_g | candy box |
Passing any other model_id raises an error. The same mapping is available at runtime:
from so101_nexus import GSO_OBJECTS
for model_id, name in GSO_OBJECTS.items():
print(f"{model_id}: {name}")See the YCB section above for the grasp advisory note; it applies to GSO objects too.
MeshObject
A custom object defined by your own collision and visual mesh files.
from so101_nexus import MeshObject
obj = MeshObject(
collision_mesh_path="/path/to/collision.obj",
visual_mesh_path="/path/to/visual.obj",
mass=0.05,
name="custom widget",
scale=0.8,
)
repr(obj) # "custom widget"| Parameter | Type | Default | Description |
|---|---|---|---|
collision_mesh_path | str | required | Path to the collision mesh file |
visual_mesh_path | str | required | Path to the visual mesh file |
mass | float | required | Mass of the object (kg) |
name | str | required | Human-readable name for the object |
scale | float | 1.0 | Uniform scale factor applied to the meshes |
__repr__() returns the name provided at construction.
YCB assets
YCB mesh assets are downloaded from the Hugging Face Hub on first use and cached at:
~/.cache/so101_nexus/ycb/{repo_key}/{revision}/{model_id}/Each model directory holds visual.obj, the convex collision parts under
collision_v3/ (collision_000.obj and more), and a manifest.json file. The
directory also holds texture.png when the texture extraction succeeds.
Creating a YCBObject and stepping an environment downloads whatever is missing. You can also trigger the download explicitly:
from so101_nexus import ensure_ycb_assets
path = ensure_ycb_assets("011_banana")ensure_ycb_assets()
def ensure_ycb_assets(model_id: str) -> PathDownloads the mesh assets for model_id if they are not already cached, then returns the cache directory.
visual.obj and the collision_v3/ parts are the required geometry cache. The
generator uses 20,000 deterministic surface samples to measure the convex hull
against the visual scan. It keeps one hull when the p95 error is at most 3 mm
and the maximum error is at most 10 mm. This gate keeps the gelatin box, large
marker, and golf ball as one hull.
The other models use the collision-aware CoACD decomposition. CoACD has no object-level part limit. It limits each part to 128 vertices, which matches the MuJoCo mesh hull limit. The audited models use 3 to 19 parts. Their collision surfaces have 2.07 mm to 3.02 mm of p95 error against the visual scans.
The manifest records the repository commit, source hash, collision-part hashes, CoACD version,
configuration, quality gates, and part sizes. The cache rebuilds after the source,
a collision part, or a generation input changes. An installation without the decomp
extra can use an existing decomposition. Otherwise, it falls back to one convex
hull. See Installation for the extras
matrix.
The function also attempts to extract texture.png from
meshes/{model_id}/google_16k/textured.glb. Texture extraction is best effort.
If no texture image is available, the environment uses the untextured visual
mesh.
Path helpers
These helpers resolve cache paths and never trigger a download. Call ensure_ycb_assets first if the assets may not be present yet; the three collision helpers read the decomposition manifest and raise FileNotFoundError when it is missing.
| Function | Returns |
|---|---|
get_ycb_mesh_dir(model_id) | The model's cache directory |
get_ycb_collision_parts(model_id) | YCBCollisionPart(path, mass_fraction) per convex part |
get_ycb_collision_meshes(model_id) | Paths of the convex collision parts |
get_ycb_collision_mesh(model_id) | Path of the first convex collision part |
get_ycb_visual_mesh(model_id) | Path to visual.obj |
get_ycb_texture_file(model_id) | Expected path to the optional texture.png |
mass_fraction is the part's volume-weighted share of the object's mass; the fractions sum
to 1, so total body mass does not depend on how many parts the decomposition produced.
from so101_nexus import (
get_ycb_collision_meshes,
get_ycb_mesh_dir,
get_ycb_texture_file,
get_ycb_visual_mesh,
)
mesh_dir = get_ycb_mesh_dir("011_banana")
collision_parts = get_ycb_collision_meshes("011_banana")
visual = get_ycb_visual_mesh("011_banana")
texture = get_ycb_texture_file("011_banana")Each helper validates model_id against the supported set and raises on an unknown ID.
Custom source repository
The default source is a pinned commit of ai-habitat/ycb.
Each repository and commit has a separate cache. Legacy caches remain untouched.
For a custom mirror, set both variables before importing the library.
Replace the revision placeholder with the mirror's full 40-character commit SHA:
export SO101_YCB_HF_REPO="your-org/your-ycb-repo"
export SO101_YCB_HF_REVISION="<40-character-commit-sha>"Download troubleshooting
Interrupted download. A partial cache directory is not repaired automatically. Delete it and retry:
rm -rf ~/.cache/so101_nexus/ycb/<repo_key>/<revision>/011_bananaFirewall or proxy. Downloads go through the Hugging Face Hub client. Behind a proxy, set the standard HTTPS_PROXY, HF_ENDPOINT, or HF_HUB_DISABLE_TELEMETRY variables.
GSO assets
GSO mesh assets are downloaded from a Hugging Face mirror on first use and cached at:
~/.cache/so101_nexus/gso/{repo_key}/{revision}/{model_id}/The cache layout, manifest format, CoACD decomposition, and texture
extraction are identical to YCB assets above - both datasets
share the same generator (so101_nexus.mesh_assets), just with a different
source repository and cache subdirectory.
Creating a GSOObject and stepping an environment downloads whatever is missing. You can also trigger the download explicitly:
from so101_nexus import ensure_gso_assets
path = ensure_gso_assets("Pony_C_Clamp_1440")ensure_gso_assets()
def ensure_gso_assets(model_id: str) -> PathDownloads the mesh assets for model_id if they are not already cached, then returns the cache directory.
Path helpers
These mirror the YCB path helpers: they resolve cache paths and never trigger a download. Call ensure_gso_assets first if the assets may not be present yet.
| Function | Returns |
|---|---|
get_gso_mesh_dir(model_id) | The model's cache directory |
get_gso_collision_parts(model_id) | GSOCollisionPart(path, mass_fraction) per convex part |
get_gso_collision_meshes(model_id) | Paths of the convex collision parts |
get_gso_collision_mesh(model_id) | Path of the first convex collision part |
get_gso_visual_mesh(model_id) | Path to visual.obj |
get_gso_texture_file(model_id) | Expected path to the optional texture.png |
from so101_nexus import (
get_gso_collision_meshes,
get_gso_mesh_dir,
get_gso_texture_file,
get_gso_visual_mesh,
)
mesh_dir = get_gso_mesh_dir("Pony_C_Clamp_1440")
collision_parts = get_gso_collision_meshes("Pony_C_Clamp_1440")
visual = get_gso_visual_mesh("Pony_C_Clamp_1440")
texture = get_gso_texture_file("Pony_C_Clamp_1440")Each helper validates model_id against the supported set and raises on an unknown ID.
Custom source repository
The default source is a pinned commit of johnsutor/gso-so101-nexus.
Each repository and commit has a separate cache. Legacy caches remain untouched.
For a custom mirror, set both variables before importing the library.
Replace the revision placeholder with the mirror's full 40-character commit SHA:
export SO101_GSO_HF_REPO="your-org/your-gso-repo"
export SO101_GSO_HF_REVISION="<40-character-commit-sha>"Download troubleshooting
Interrupted download. A partial cache directory is not repaired automatically. Delete it and retry:
rm -rf ~/.cache/so101_nexus/gso/<repo_key>/<revision>/Pony_C_Clamp_1440Firewall or proxy. Downloads go through the Hugging Face Hub client. Behind a proxy, set the standard HTTPS_PROXY, HF_ENDPOINT, or HF_HUB_DISABLE_TELEMETRY variables.
Simulation assets
These helpers resolve paths to the assets bundled with the package.
get_so101_simulation_dir()
def get_so101_simulation_dir() -> PathReturns the SO-101 asset directory (SO101/). It holds the URDF/XML that the teleop tooling reads for calibration metadata. The MuJoCo backend does not load this model.
get_so101_mujoco_model_dir() and get_so101_mujoco_model_path()
def get_so101_mujoco_model_dir() -> Path
def get_so101_mujoco_model_path() -> PathReturn the directory and the so101.xml path of the vendored MuJoCo Menagerie model under SO101_menagerie/. This is the model the MuJoCo backend loads.
get_mujoco_ycb_rest_pose()
def get_mujoco_ycb_rest_pose(
verts: np.ndarray,
margin: float = 0.002,
model_id: str | None = None,
) -> tuple[np.ndarray, float]Computes a stable rest orientation and spawn height for a scanned mesh (YCB or GSO). model_id
looks up a settle-test-validated correction in POSE_OVERRIDES first; without a match, the mesh's
thinnest axis is rotated to point up as a guess.
| Parameter | Type | Default | Description |
|---|---|---|---|
verts | np.ndarray | required | Vertex array of the object mesh |
margin | float | 0.002 | Offset above the surface to avoid interpenetration |
model_id | str | None | None | Dataset identifier to check against POSE_OVERRIDES before falling back to the heuristic |
Returns (quaternion, spawn_z), where quaternion is a NumPy array in wxyz order and spawn_z is the spawn height in meters.