Repository navigation
UrdfManager: two of the same arm, iterative mounts, and per-mount frame names - #5105
Open
jeff-hykin wants to merge 108 commits into
Open
jeff-hykin wants to merge 108 commits into
jeff-hykin wants to merge 108 commits into
Conversation
ModuleBase.frame_id already puts a module's own frame behind frame_id_prefix, but a module that stamps or looks up frames by other names of its own (a URDF link, a sensor mount) had nowhere to go. ConnectionModule had grown exactly that helper privately; lift it to ModuleBase so the next module needing it does not grow a third copy. Blueprint.namespace() sets frame_id_prefix, so this is what keeps two of the same robot, or two of the same arm, from colliding on tf: they share a description and so share link names. Behaviour is unchanged for ConnectionModule beyond an empty name now staying empty rather than becoming "prefix/".
…ink frames
Two of the same arm, or two of the same robot, were not expressible. Both halves of
the problem were that a URDF-derived name is shared by every copy of that URDF:
- physical_connections addressed a module by its class, and RPCClient.remote turns a
class into an RPC topic by class name, so two instances of one class resolved to
the same topic. Values are now normalized to a PhysicalConnection, which carries
the deployed instance name alongside the class. A class still works and still means
"the only instance of itself"; the useful new form is a namespaced blueprint,
XArm.blueprint(...).namespace("left"), which names the exact instance it deploys.
An instance name on its own is accepted too, for a module whose class is not
importable here, at the cost of the fail-fast get_usd check.
The subtree client cache is keyed by remote name for the same reason.
- the joint tf went out under the raw URDF link names. It now goes through frame(),
which Blueprint.namespace() has already pointed at the right prefix, and
UrdfManagerPointClearing looks its root link and multi-DOF joint edges up the same
way so it still finds what the manager published.
Both are no-ops without a namespace: frame_id_prefix is None by default, and nothing
in the tree sets physical_connections yet.
A blueprint value is resolved to the one atom answering get_usd, so passing an arm's
whole blueprint (arm plus its driver) works; a blueprint with none of them, or more
than one, is rejected with the names it found.
Contributor
There was a problem hiding this comment.
go2 replay realtime (arm64)
Details
| Benchmark suite | Current: 6c3076d | Previous: 5bc57a0 | Ratio |
|---|---|---|---|
peak memory |
1742.41 MB |
1776.91 MB |
0.98 |
peak threads |
369 threads |
376 threads |
0.98 |
network (transport) |
2733.227 MB |
2730.972 MB |
1.00 |
disk write |
1.824 MB |
1.59 MB |
1.15 |
instructions |
138.679 G |
138.52 G |
1.00 |
This comment was automatically generated by workflow using github-action-benchmark.
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## jeff/feat/usd_manager #5105 +/- ##
=========================================================
- Coverage 82.60% 82.46% -0.15%
=========================================================
Files 1870 1872 +2
Lines 183606 184719 +1113
Branches 15445 15505 +60
=========================================================
+ Hits 151674 152333 +659
- Misses 27956 28400 +444
- Partials 3976 3986 +10
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 6 files with indirect coverage changes 🚀 New features to boost your workflow:
|
This reverts commit 0375047. The prefix is going into the description at graft time instead, so neither UrdfManager nor UrdfManagerPointClearing reads frame_id_prefix any more, and with no new caller the helper has nothing to earn its place with. ConnectionModule gets its private copy back.
… frame_id_prefix
Two of the same arm still need one of them renamed, but the rename belongs in the
description rather than in a runtime attribute. A module holds one description and does
not know which copy of itself it is; the name it was deployed under says exactly that.
So UrdfManager asks a namespaced module for its USD and moves every frame in the answer
under that namespace itself.
prefix_usd_frames(usd, prefix) rewrites the dimos:link and dimos:joint attributes, which
are the names the description travels under: a link name is a tf frame_id, and a joint
name is what a JointState keys on, so leaving the joints behind would be the same
collision one layer down. Prim names cannot hold a "/" and prim paths are already unique
by nesting, so neither has to move, and the joints' body relationships keep working.
The prefix comes from PhysicalConnection.namespace: everything before the last "/" of the
instance name, the same rule the coordinator's _atom_namespace uses, so a nested
namespace ("fleet/robot0/xarm") prefixes whole.
The root's own links are never renamed. It is one URDF, so its names are already unique,
and the physical_connections keys stay the spelling the user wrote them in.
This drops the frame_id_prefix reading added earlier on this branch: joint tf goes out
under the URDF's link names again, and UrdfManagerPointClearing looks them up unchanged.
An empty prefix returns the same string object, so an un-namespaced subtree is not even
re-exported.
… name namespace() sets instance_name and frame_id_prefix together, so deriving one from the other usually agrees -- but it keeps a frame_id_prefix the user set by hand, and that override is precisely the answer to "what are this module's frames called". So PhysicalConnection carries a frame_prefix read straight off the atom's kwargs. An instance name passed on its own still falls back to the namespace in the name: there is no object there to read anything off.
Resolved in favour of this branch's shape, with physical_connections -> mounts and PhysicalConnection -> Mount applied across it. Mount is what a mount holds: the module, the name it answers RPC under, and the prefix its frames take.
Same reason as the one on the base branch: test_no_sections forbids a comment wrapped in separator dashes, and the test names already say what each group covers.
Follows the base branch off custom attributes: prefix_usd_frames now rewrites displayName, and gives a prim one from its prim name when it has none, because a prefixed name like "left/arm" is exactly what a prim name cannot hold. It walks the prims that stand for a link (UsdPhysics.RigidBodyAPI) or a joint (UsdPhysics.Joint) rather than the ones carrying an attribute of ours, so the geometry prims and the defaultPrim are left alone as before. The stand-in subtree in the tests now speaks the same standard dialect urdf_to_usd emits, which is what it was always meant to stand in for.
A USD layer names its meshes relative to something, and only the module that answered for it knows what. Without that root travelling alongside, a subtree's mesh paths are unresolvable the moment they leave the process that produced them: the answering module's cwd is not the asker's, and two subtrees need not share a root at all. So get_usd and get_scene now answer with [asset_root, usd], and get_scene rebases each subtree onto the root its own module reported before grafting, which is what lets it answer with a single root for the whole scene. resolve_usd_assets(usd, root) does the rebasing, on the standard reference arcs at the layer level. An absolute path is left alone, and an empty root means "nothing to resolve". A malformed answer -- not two strings, or an empty layer -- is treated like a module that is not up yet and asked again rather than grafted blindly. get_scene has no deadline any more. A mount is a module the blueprint says is physically there, so not answering is a thing to wait out, not to give up on; half a scene would hand a renderer a robot with no arm. scene_timeout_s is gone with the TimeoutError. Since waiting forever silently is its own failure, it now warns every 20 asks.
A vendor description names its frames whatever it names them, which is rarely what the robot
they are bolted to calls them. ``Mount.of(arm).renaming(pre=..., post=...)`` says so, on
either side of the namespace prefix:
.renaming(pre={"base_link": "arm_base"}) -> left/arm_base
.renaming(post={"left/base_link": "left_arm"}) -> left_arm
Both sides exist because either can be the easier one to write. ``pre`` is in the subtree's
own names, as its own description spells them, which is what you have in front of you when
reading a vendor URDF. ``post`` is in the prefixed names, for when the prefix is not the shape
you wanted at all -- it is the only way to get a name that does not start with the namespace.
So _adopt now goes rename, prefix, rename. Both maps reach joints as well as links, since a
joint name is a JointState key and needs renaming for the same reasons.
rename_usd_frames reads every name before writing any, so a -> b alongside b -> a is a swap
rather than both collapsing onto one. A key matching no link or joint is an error naming what
the subtree does have: a misspelt frame would otherwise be a silent no-op that only shows up
later as a scene whose frames are not the ones asked for.
renaming() merges rather than replaces, like the chainable edits on RobotModel, so two callers
can each add one.
Both branches had dropped it; resolved in favour of this one, which carries the asset-root answer and the per-mount renames on top.
…pace # Conflicts: # dimos/robot/urdf_manager.py
…exist before stop can run Merging the static-tf publisher up: a namespaced manager must put its fixed edges through frame() like its moving ones, or the office scene would publish un-prefixed frames. The thread's fields are initialised before _check_mounts, so the dispose() that a rejected mount triggers does not read them first -- the same trap as the publisher that stop() touched before __init__ had made it.
Nabla7
reviewed
Nov 11, 2026
|
|
||
| SUBTREE_RPC = "get_usd" | ||
| SCENE_RPC = "get_scene" | ||
| ASSET_RPC = "get_asset" |
Nabla7
reviewed
Nov 11, 2026
| if mount.subtree_rpc == SCENE_RPC: | ||
| answer = getattr(client, SCENE_RPC)(chain) | ||
| else: | ||
| answer = getattr(client, SUBTREE_RPC)() |
Collaborator
There was a problem hiding this comment.
todo: make sure this isn't recursion on UsdManager
Nabla7
reviewed
Nov 11, 2026
| root = self._asset_root() | ||
| return found if found.is_absolute() or not root else Path(root) / found | ||
|
|
||
| def static_transforms(self) -> list[Transform]: |
Collaborator
There was a problem hiding this comment.
todo: this needs to happen on a per-robot basis.
Pim, on #5102: "todo: don't delete prexisting comments/docstrings". An earlier pass in this stack stripped comments and docstrings from every file it touched, including the ones it only edited in passing. model.py is pre-existing code, so 31 docstrings and 9 comments that main wrote went with it. Restored from main by qualified name, so only the ones whose code is still here came back; the ones belonging to code this PR replaced stayed out. The same stripping hit pre-existing files on five other branches in the stack and each is being restored on the branch that did it.
Four of Pim's todos on #5105. "this needs to happen on a per-robot basis" (on static_transforms), which Jeff widened to: make tooling that a module can be handed its USD, a joint-state stream and a tf stream, and it publishes on tf by itself. That is `dimos/robot/joint_tf.py`. `JointTf(model, joint_state, tf).start()` and it runs: the moved-edge dedupe, the keepalive repeat a tf lookup needs, the rate limit, and the fixed edges on their own when no joint state ever arrives (the office scene has no controller, so nothing calls add_joint_state for it). `frame=` is how three robots in one world get three sets of frame names. Nothing in it knows about modules, so a robot that is not a UsdManager can use it as easily. UsdManager now owns one instead of being one. `handle_coordinator_joint_state` is gone because JointTf subscribes to the stream itself, which is the point; `static_transforms`, `joint_transforms` and `add_joint_state` stay as one-liners because other code and the point-clearing subclass call them. `MULTI_DOF_JOINT_TYPES` moved with the code that reads it. "fix unused var": ASSET_RPC. It is genuinely unused until `sim2/sim.py` asks for assets, so it is deleted here and declared on the branch that first calls it. "make sure this isn't recursion on UsdManager": it is, deliberately -- a mounted manager is asked for its whole scene, not just its own layer, because `_tree_rpc` picks get_scene over get_usd when the module answers both. What bounds it is the visited chain and `_refuse_a_cycle`. Every test of that faked the chain by calling get_scene with one already in it, so there is now a real one: two managers mounted on each other through a client that really calls the other's get_scene, asserting the error names `left_scene -> right_scene -> left_scene` and that each manager was asked exactly once. "don't delete prexisting comments/docstrings": restored in urdf_manager.py, urdf_manager_point_clearing.py and test_urdf_manager.py from what main says. 12 new tests in test_joint_tf.py; the 107 in test_urdf_manager.py still pass.
The old static-tf thread only started `if publish_joint_tf and static_transforms()`, and JointTf dropped the second half: a manager standing on a bare world link -- no joints, no controller wired to it -- got a thread that woke four times a second to publish an empty list. There are ten of these in office_555. Nothing else changes: a manager with a joint stream still starts, and so does one with fixed edges and no stream, which is the case the thread exists for.
jeff-hykin
added a commit
that referenced
this pull request
Nov 11, 2026
Pim, on #5102: "PR1 shouldn't edit urdf manager (utils only)". This reverts e51f020 (the static-tf thread) here; the file is byte-identical to main again, and the first PR touches nothing but dimos/robot/assets. The fixed-edge publishing it added is still needed -- the office scene has no controller, so nothing ever calls add_joint_state for it -- so it moves up to #5105, which is the PR that actually owns UrdfManager's behaviour.
jeff-hykin
added a commit
that referenced
this pull request
Nov 11, 2026
…t called Pim, on #5105: "fix unused var". ASSET_RPC was declared four PRs before anything called it, so it was deleted there and lands here instead -- `sim2/sim.py` is the first thing that asks a manager for an asset.
jeff-hykin
added a commit
that referenced
this pull request
Nov 11, 2026
The conflict was this branch's joint_renames work against the per-robot joint->tf tooling from #5105, which is where joint_transforms now lives. So joint_state_name and the warn-once go into JointTf as a `joint_renames` option, and UsdManager hands it `self.config.joint_renames` -- which is the shape the tooling wanted anyway: everything about posing one description's joints is in one place, and a module that is not a UsdManager gets the renaming for free. UsdManager.joint_state_name stays as a one-liner onto it.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets the same module be mounted twice by resolving mounts iteratively (with cycle detection) against post-namespace frame names, applying per-subtree frame prefixes and
tf_renames, and republishing every mesh asset under a conflict-free/dimos/assets/path.