Skip to content

[Assets] Add SimReady catalogue objects and a lift environment that uses them - #3

Draft
hujc7 wants to merge 32 commits into
developfrom
jichuanh/simready-multiobject
Draft

[Assets] Add SimReady catalogue objects and a lift environment that uses them#3
hujc7 wants to merge 32 commits into
developfrom
jichuanh/simready-multiobject

Conversation

@hujc7

@hujc7 hujc7 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Adds SimReady catalogue assets to Isaac Lab: a configurable library that turns USD-Search results
into task-ready rigid objects, and a lift environment that spawns 96 distinct catalogue objects
instead of primitive shapes.

Most of this PR exists to work around what the search client cannot answer. That is worth stating
first, because if the client closes those gaps, a large part of this code should be deleted.


1. What the search client would need for Lab to use it well

simready-search sets 13 of the 54 fields its own HTTP endpoint accepts, so Lab downloads and
opens 3.1 GB of assets to recover facts the service already holds.

Reachable over HTTP today, missing from the client

Missing What Lab does instead Measured cost
min/max_bbox_x, min/max_bbox_z SearchFilterHeight maps only to bbox_y, so two axes are checked after download filtering all three server-side cuts 196 candidates to 123
return_usd_dimensions the bounding box comes back free over HTTP; the client returns none, so every asset is opened to measure it 44 of 210 candidates are rejected purely on size, after being fetched
return_usd_properties physics and semantic attributes likewise unavailable on results forces the audit to exist
filter_by_properties_numeric no numeric comparison at all no mass or polygon filtering, even where indexed
similarity_threshold / deduplicate_by_hash Lab groups near-identical variants by parsing filenames for variant codes a naming heuristic stands in for embedding-distance dedup
/search/stats/usd_properties one of 20 endpoints the client does not wrap; it enumerates every indexed property and value the vocabulary question looks unanswerable from the client alone

Not available at either level — service-side asks

Gap Evidence
Validation filtering matches any date. passed=~True matches assets that passed once, so regressed assets still match. This alone admitted all 61 no-rigid-body candidates. Lab re-reads the newest verdict from each asset to compensate.
Physics properties are indexed at root-prim depth only. SimReady props author physics:mass 4–5 levels down in a sublayer. mass is indexed for 182 of 4411 assets; physics:approximation for 1; __polygon_count reports 0–2 polygons for 25% of the catalogue because only the wrapper layer is counted.
A missing property excludes the asset rather than being reported. physics:mass<1000000 returns 4 results out of 261. Filtering on mass would silently reduce this task's object set from 96 to about 4.

If validation filtering used the newest verdict and physics attributes were indexed at their authored
depth, require_rigid_body, require_declared_collision and the validation re-read in this PR would
all become unnecessary, and the audit would shrink to mass alone.


2. Lab-side decisions, given those gaps

Open every candidate once, and record the result

The audit is the only place these facts exist, so it opens each asset once and caches the outcome.
The resolved asset list is written to simready_objects.json beside the task config and committed,
so later runs contact no service: a cold resolve costs ~60 s, a replay 0.1 s.

Filter on asset properties, not on where the answer comes from

SimReadyObjectFilterCfg names properties; unset fields are unconstrained. Whether a property is
answered by the index or by opening the asset is an implementation detail.

Trust what the asset declares

Authored mass and scale are used as-is. An object is dropped because this robot could not handle it,
never because the number looks surprising. Measured on the live catalogue, 210 candidates yield 96:

Dropped Reason
61 no rigid body — collision geometry but nothing physics can move
44 too large for the workspace
6 a collider declares no approximation, so its mesh would become a convex hull
3 too thin for stable contact

Aggregate mass rather than reading one attribute

UsdPhysicsMassAPI may sit on the body or on the colliders beneath it, and catalogue assets use the
second form — a coffee cup declares body and lid separately. Reading a single attribute returned a
fragment: a drinks can was recorded at exactly half its mass.

Reject silently-degraded collision

A mesh collider applying no UsdPhysicsMeshCollisionAPI still collides, but USD defaults its
approximation to none, which is legal for static geometry only, so PhysX substitutes a convex hull
and a cup loses its interior. Of 211 candidates, 199 declare an approximation (almost all sdf) and
12 declare none; none declares an unusable one, so this is a pipeline omission rather than a
choice, and the three objects it removes are exactly the three PhysX reported substituting at load.

Change one thing about each asset

The rigid body is extracted to a uniform /Object root, because assets nest it under
differently-named paths and a per-environment view cannot be built across varying paths. Collider,
mass and origin are left as published.

Keep the pipeline Kit-free

UsdUtils.LocalizeAsset cannot resolve an https:// path without a resolver plugin, and
ComputeAllDependencies reports zero dependencies rather than failing, so the layer closure is
mirrored explicitly. Fetching is parallel at two levels — across assets, and across each asset's
layers — because the cost is per-file round-trips rather than bandwidth: 25.4x over the
sequential form, with an identical result.


Validation

Live service and Isaac Sim PhysX on a 4xL40 box.

Check Result
Search 210 candidates in ~46 s
Audit → select → prepare 96 objects, all USDs valid: one rigid body at /Object
Heterogeneous PhysX clone env built with 4096 environments
Colliders survive extraction 0 objects fell through the table, 600 steps under gravity
Record replay 0.1 s, identical set, with the search path made to raise
Tests 37, no simulation app and no network

Known gaps in this PR

  • The Kit-free fetch drops physics material bindings, so friction and restitution fall back to
    defaults. Friction decides whether an object slips from the fingers, so this is the most
    consequential remaining gap for a grasping task.
  • Units are assumed to be metres; usd_dimensions exposes scene_mpu, which is not checked.
  • Heterogeneous spawning is PhysX-only. Newton degrades all collision to convex hulls to satisfy
    MuJoCo's homogeneous-worlds requirement, and sdf — used by 157 of 159 colliders here — is a PhysX
    token absent from Newton's approximation mapping.
  • Unrelated to this branch: the dexsuite tasks fail Hydra config serialization
    (UnsupportedValueType: ResolvableString), so they cannot be launched through
    scripts/reinforcement_learning/train.py. Reproduces on the stock Isaac-Lift-KukaAllegro.

Review guide

Read commit by commit. utils/simready is moved in one commit and rewritten in the next, so the
squashed view shows a large delete-plus-add; per commit, git reports the move as a rename.

# Commit
1 Add SimReady USD-Search asset resolution to spawners
2 Cover simready extra in install CLI constant tests
3 Move SimReady search resolver to utils.assets
4 Cut the SimReady integration back to an MVP
5 Resolve SimReady objects by robot capability
6 Add height and path-exclusion filters to SimReady search
7 Turn utils.simready into a sub-module
8 Drive SimReady object resolution from a configuration class
9 Test capability-based SimReady object selection
10 Document the SimReady asset sub-module
11 Re-audit SimReady assets when the cache layout changes
12 Name the SimReady modules after their contents
13 Import configclass from its own module
14 Expose the whole SimReady search surface in the filter config
15 Register lift environments backed by the SimReady catalogue
16 Point the SimReady references at the sub-module
17 Remove task defaults from the SimReady object filter
18 Replace the heavy-object reservation with a filter hook
19 Record which assets a query resolved to
20 Report progress while auditing SimReady candidates
21 Audit SimReady candidates concurrently
22 Fetch an asset's layers level by level rather than one at a time
23 Test that a cached launch reaches neither the service nor the search package
24 Model the SimReady filter on asset properties
25 Record the resolved object set for the lift task
26 Spawn the full usable object set instead of one per product family
27 Drop the product-family cap
28 Record the 99 catalogue objects the lift task spawns
29 Keep the product-family cap available but unused by the task
30 Sum the mass a catalogue asset actually declares
31 Drop catalogue objects whose colliders declare no approximation
32 Record the 96 objects that remain after the collision check

@github-actions github-actions Bot added documentation Improvements or additions to documentation isaac-lab labels Jul 28, 2026
@hujc7
hujc7 changed the base branch from jichuanh/simready-usd-search to develop July 28, 2026 05:19
@hujc7
hujc7 force-pushed the jichuanh/simready-multiobject branch from 2773365 to 8162e22 Compare July 28, 2026 06:23
@hujc7
hujc7 changed the base branch from develop to jichuanh/simready-usd-search July 28, 2026 06:24
@hujc7
hujc7 force-pushed the jichuanh/simready-usd-search branch from e216ed3 to 2cb8260 Compare July 28, 2026 06:37
@hujc7
hujc7 force-pushed the jichuanh/simready-multiobject branch from 8162e22 to c48d003 Compare July 28, 2026 06:37
hujc7 added 22 commits July 29, 2026 15:20
Spawn configurations previously required hardcoded USD file paths. This
adds the isaaclab.sim.spawners.simready sub-module, which resolves the
usd_path of a spawner configuration from a SimReady USD-Search semantic
query at configuration construction time: SimReadyUsdFileCfg uses the
top-ranked result and SimReadyMultiUsdFileCfg freezes the top-k results
for heterogeneous cloning. No spawner changes are needed since the
resolved paths feed the existing UsdFileCfg/MultiUsdFileCfg machinery.

The required simready-search package is optional and lazy-imported,
available through the new 'simready' extra (./isaaclab.sh -i simready).
Search failures raise typed errors, results are tie-broken
deterministically for multi-process runs, and already-resolved
configurations skip the query on copy()/replace().
The constant tests assert the literal contents of VALID_EXTRA_FEATURES
and MANUAL_EXTRA_FEATURES, so the new 'simready' extra must be added to
their expected sets. Also mirror the manual-extra dispatch test used by
'ov' for the new selector-less 'simready' token.
The search function is spawner-agnostic: it takes a query and returns
USD asset paths. Move it and the endpoint constant to
isaaclab.utils.assets, next to the other asset retrieval helpers, so
non-spawner consumers can use it without importing spawner modules.
The simready spawner sub-module keeps only the two cfg subclasses.

Split the tests to mirror the layout: resolver tests move to the
utils asset tests together with the fake simready.search module; the
spawner cfg tests stub the resolver directly and gain a
parameter-forwarding test.
Per SimReady team guidance, treat semantic search as an authoring-time
discovery step and consume the resolved USD files directly for
reproducible training. Drop the spawner cfg subclasses (query-per-run
ergonomics, import-time network calls, silent Hydra override no-op)
in favor of the plain resolver plus pin-first guidance and the search
API contract in its docstring.

Add scripts/demos/simready_lift.py integrating the search with the
Franka lift task: resolved assets replace the cube one variant per
environment, with zero-action smoke, PPO training, and checkpoint
playback modes. Pinned --usd_path runs need no service credentials.
The demo previously took a fixed list of asset paths and rescaled them to
fit the gripper. That does not extend to a large object set: the search
service returns only asset paths, so nothing in a query says whether a
given robot can actually pick an object up.

Add SimReadyObjectLibrary, which searches the catalogue, opens each
candidate once to record the facts the service does not expose (rigid-body
presence, authored mass, bounding box, latest validation verdict), caches
those audits, and selects objects the robot can handle. Assets are left as
published apart from extracting their rigid body into a uniform /Object
root, which heterogeneous cloning requires because assets nest that body
under differently-named paths.

Asset access needs no Isaac Sim: each asset's layer closure is mirrored
over HTTPS and opened as local files.

Point the demo at Isaac-Lift-KukaAllegro, which already supplies
point-cloud observations, contact-shaped rewards and a per-environment
table, so the demo only supplies objects.
The search service filters server-side on bounding-box height and on path
substrings, but the helper exposed neither, so callers had to re-implement
the search to use them. Excluding locally also wastes the top_k budget on
results that are thrown away.

Sweeping many phrases additionally needs an empty result set to be a
non-event rather than an error, since one barren phrase must not sink a
whole sweep.

Point the default endpoint at production. The dev deployment's asset index
lags and returns stale results, so it was never a usable default.
Make room for a configuration class alongside the implementation, matching
how the other utils sub-modules (noise, modifiers) are laid out: a lazily
exported package whose overview lives in __init__ and whose symbols are
declared in the type stub.

No behaviour change.
Selection knobs were spread across four method signatures, so a caller had
to know which argument each method wanted and repeat the ones that had to
agree between calls. Collect them into SimReadyObjectFilterCfg and
SimReadyObjectLibraryCfg and have the implementation read only from those,
matching how the rest of the repository separates configuration from
behaviour.

Every filter bound is documented as a statement about robot capability,
which is what the values actually encode: an object is dropped because the
gripper could not hold it, never because its authored mass looks
surprising.

Delegate the service call to search_simready_usd_paths rather than
re-implementing it, add resolve() for the common search-select-prepare
sequence, and report progress through the module logger instead of print.
Selection now reads from cached audits, so the whole filter, family
grouping, and mass stratification can be exercised without the simulation
app or the search service by pre-populating the cache.

Cover each rejection reason, the mass band reserved for objects at the edge
of the gripper's capability, family grouping, and the cache round trip,
where JSON has no tuple and the bounding-box extents could silently come
back as a list.
Add isaaclab.utils.simready to the utils API reference and record the new
configuration classes, the extended search filters, and the endpoint change
in the changelog fragment.

Also re-check the path exclusions locally after the search returns. They
are applied by the service, and the redundant check costs nothing.
The audit cache persists ObjectSpec field-by-field, which couples the
on-disk format to the field names: a rename turned every existing entry
into a TypeError on load. That is the worst place to fail, because the
cache is what keeps resolution off the slow path in the first place.

Cache contents are regenerable by definition, so treat an entry whose
layout no longer matches as a miss and audit the asset again.
simready/simready.py was the only module in utils/ sharing its package's
name. Every other sub-module is named for what it holds -- noise_model.py,
modifier_base.py, circular_buffer.py -- and a package/package.py pair also
makes attribute access on the package ambiguous between the exported symbol
and the sub-module.

No behaviour change.
Importing it through the lazily exported isaaclab.utils package left every
public symbol in this sub-module undocumented: while autodoc walks
isaaclab.utils, resolving a member re-enters that package's lazy lookup,
which raises, so autodoc records the member as unavailable and skips it.
The API page rendered the module docstring and nothing else.

Every other configuration module imports the symbol directly from
isaaclab.utils.configclass. Do the same.
The config exposed three of the service's eleven filters and hardcoded a
fourth, so anything else the service could answer had to be reached by
bypassing the library. Worse, the workspace size bound was silently reused
as the service's height query, mixing a robot-capability number into a
catalogue request.

Expose every filter the service supports, and keep the ones it does not --
mass, the two bounding-box axes beyond height, rigid-body presence, the
newest-dated validation verdict, product-family collapsing -- as fields
that the implementation completes after opening the asset. Each field
records which of the two it is, so the cost of a filter is visible where it
is set rather than buried in the query builder.

Also check validation against the configured features instead of a
hardcoded one, and document the four-stage workflow with its per-stage cost
on the library itself.
The catalogue objects were only reachable through a demo script that
rebuilt the task configuration by hand, so they could not be trained with
the standard workflow and the overrides were invisible to anyone reading
the task.

Add a mixin that changes the one thing that differs from the stock lift
task -- which objects are spawned -- and register Isaac-Lift-KukaAllegro-
SimReady and its play variant alongside the existing entries, matching how
the camera variants are configured. Training now runs through the usual
train.py with no bespoke script.

Spawn through MultiUsdFileCfg rather than assembling a list of per-asset
spawners, since that wrapper already exists for exactly this case.
The registered Isaac-Lift-KukaAllegro-SimReady environment covers what the
script did, through the standard training workflow, so keeping the script
would leave two entry points that have to agree about how objects are
resolved.

Repoint the two references that named it: the search helper's docstring now
points at the sub-module, and the changelog records the removal with the
command that replaces it.
The filter shipped a table-top phrase list, workspace size bounds, gripper
mass bounds and a heavy-object share, none of which describe the catalogue
or the search service -- they describe one task on one robot. No other core
configuration embeds a domain value list as a default, and must-choose
fields elsewhere use MISSING.

Make the fields that define a task MISSING, give the optional ones a
neutral disabled default, and move the table-top values to the environment
that wants them.

Also drop the play variant, leaving a single registered environment.
heavy_from and heavy_fraction encoded one opinion about what a good object
set looks like -- that some share should be too heavy to lift -- as two
coupled knobs on a general library. The pair only meant anything together,
neither described the catalogue or the robot, and no other criterion could
be expressed that way.

Drop both and add filter_func, an optional predicate over the audited
spec, so a caller can express what the named fields cannot: an aspect-ratio
bound, a density, a naming convention. The task keeps its mass range and
lets the catalogue's own spread decide how often lifting is easy.

The spec passed to the predicate carries what the built-in filters need and
no more, since the audit is cached in that form; the field documents the
limit.
Only the per-asset audits were cached, so every construction still issued
one search request per phrase. Under distributed training that is once per
rank, and a catalogue update between ranks would hand them different object
sets -- a correctness problem, not just a slow start.

Record the resolved asset list against a fingerprint of the effective
query, so a repeat resolve issues no search at all and changing any filter
resolves afresh. The file is JSON, sorted and indented, small enough to
commit when a run has to be reproducible.

Keep it separate from the audit cache: that one is keyed by asset, grows
with the catalogue and is disposable, while this is keyed by query and is
worth keeping.
Auditing is the one slow stage -- on a cold cache every candidate is an
HTTPS fetch of a whole layer closure plus a stage open -- and it ran
silently, so a resolve looked hung for twenty minutes with no way to tell
progress from a stall short of counting files on disk.

The earlier implementation logged every 25 candidates; that was lost when
printing was replaced with the module logger.
Auditing 210 candidates took 32 minutes, and profiling put 100% of that in
the network: mirroring an asset's layers costs ~20 s while opening the
stage and measuring it costs ~0.01 s. The layers are fetched one request at
a time, so the cost is per-file round-trips rather than bandwidth.

Audit candidates through a thread pool. The work is independent per asset
and bound by latency, so it scales close to linearly.

Assets share material and texture layers, so concurrent audits can race for
the same path; download to a private name and rename, which is atomic on
the same filesystem.
Nothing about the asset host requires the layers of a single asset to be
mirrored in sequence -- they are plain HTTPS objects. The sequencing was an
artefact of walking the closure one URL per iteration, which made an asset
cost the sum of its layers' round-trips: about thirteen requests at a
second and a half each.

A layer's own references are only known once it has been parsed, so the
closure still has to be walked breadth-first. That ordering is per level,
not per file, so fetch every layer of a level at once.
…package

The task resolves its objects while its configuration is built, so the
first launch legitimately needs the network. Every later one must not: it
has to work on a machine with no connectivity and without the optional
simready-search package installed.

Assert it by making any search attempt raise, the way an uninstalled
package would, and requiring the resolve to succeed regardless.
@hujc7
hujc7 force-pushed the jichuanh/simready-usd-search branch from 2cb8260 to 80608db Compare July 29, 2026 22:21
@hujc7
hujc7 force-pushed the jichuanh/simready-multiobject branch from 3d5ca98 to c107314 Compare July 29, 2026 22:21
@hujc7
hujc7 changed the base branch from jichuanh/simready-usd-search to develop July 29, 2026 22:23
@hujc7 hujc7 changed the title [Assets] Resolve SimReady objects by robot capability via a configurable library [Assets] Add SimReady catalogue objects and a lift environment that uses them Jul 29, 2026
hujc7 added 9 commits July 30, 2026 00:34
The configuration described where each filter ran -- at the search service
or after opening the asset -- which is an implementation detail that can
change if the service starts returning more. From a caller's side there is
one idea: assets have properties, and you constrain the ones you care
about.

Drop the service/local framing, make every field optional so an unset
property is simply not constrained, and remove the fields nobody could
populate: the catalogue vocabulary filters (classes, tags, profiles,
countries, scene locations) work only if you already know a valid value,
and the service publishes no list and returns none on results.

Record the resolved asset set to a file meant to be committed beside the
task. A configuration then resolves to a fixed set of objects that everyone
fetches without contacting the service, rather than each machine -- or each
rank -- querying a catalogue that may have moved in between.

Also: num_objects belongs to the query rather than the scene;
distinct_families becomes max_per_product_family, which says what it does;
and _filter becomes _search_filter, since it returns a filter object.
Generated against the live catalogue: 210 candidates, of which 61 carry no
rigid body, 44 exceed the workspace and 6 are too thin, leaving 99 usable
assets across 39 product families.

The record is used as-is on a later run -- 39 objects in 0.1 s with the
search path made to raise -- so the task spawns the same objects on any
machine without contacting the service.

Warn when fewer objects satisfy the filter than were asked for. The task
requests 100 and the catalogue holds 39 distinct table-top families, which
was previously visible only as an INFO line.
Capping variants at one per product left 39 objects from 99 usable assets.
The wider set is worth more to training than the visual repetition costs,
and 99 is what the catalogue holds under these bounds.
Capping variants at one per product left 39 objects where 99 are usable.
The wider set matters more to training than the visual repetition costs, so
the task takes all of them and the field -- along with the name-derived
family grouping behind it -- goes with it.

num_objects stays at 100 as an upper bound; the catalogue yields 99 under
these filters, and the warning added alongside says so rather than leaving
the shortfall to be noticed.
Generated against the live service: 210 candidates, of which 61 carry no
rigid body, 44 exceed the workspace and 6 are too thin, leaving 99.

Replaying the record resolves those 99 in 0.1 s with the search path made
to raise, so a run reproduces the same objects with no service call.

Doubling the phrase list returns the same 210 candidates, so 99 is what the
catalogue holds under these bounds rather than a limit of the query.
Capping variants is worth having when the objects are seen together -- a
demo grid or a recorded video, where four golf balls read as one object --
and costs object count when they are not. The lift task wants the wider set,
so it leaves the field unset and takes all 99.

Restore the field, the grouping behind it, and its tests; the docstring now
states the tradeoff with the measured numbers (99 uncapped, 39 at a cap of
one) so the choice is visible where it is made.
UsdPhysicsMassAPI may be applied to the rigid body or to the collision
prims beneath it, and the two are not alternatives: a value on the body is
the body's mass, while values on descendants are contributions that sum.
Catalogue assets use the second form -- a coffee cup declares body and lid
separately, which is what places its centre of mass correctly.

Reading a single attribute therefore recorded a fragment of the real mass.
Measured across 120 downloaded assets, 52 declare mass on more than one
prim, so the mass filter has been comparing against one part of the object
for roughly two in five. A coffee cup weighing 0.0398 + 0.1216 kg was
recorded as whichever prim the traversal reached last.

Aggregate instead, preferring a value on the body when one exists so
assets that state the same mass on both body and mesh are not counted
twice.
A mesh collider that applies no UsdPhysicsMeshCollisionAPI still collides,
but USD defaults its approximation to none -- a raw triangle mesh, legal
for static geometry only -- so a simulator asked to move the body
substitutes a convex hull. A cup loses its interior and a mug its handle
gap, with nothing to show for it but a log line: the asset passes
validation and the run succeeds.

The catalogue is well authored here, so rejecting them is cheap. Of 211
candidates, 199 declare an approximation and 12 do not; none declares an
unusable one, so this is an omission in a pipeline rather than a choice.
Three of the ninety-nine objects the lift task spawns are affected, and
they are exactly the three PhysX reported substituting at load.

Record the effective approximation on the spec as well, using none where
the schema is absent, so what the simulator will do is visible rather than
inferred.
Regenerated against the live catalogue: 210 candidates, of which 61 carry
no rigid body, 44 exceed the workspace, 6 declare no collider
approximation and 3 are too thin, leaving 96.

The three objects this removes from the previous record -- a mug, a cup and
a chips bag -- are exactly the three PhysX reported substituting a convex
hull for at load, so every object the task now spawns collides as its
author authored it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation infrastructure isaac-lab

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant