[multibody] Prevent multibody/topology from splitting World to break a loop. - #24909
[multibody] Prevent multibody/topology from splitting World to break a loop.#24909sherm1 wants to merge 3 commits into
Conversation
sherm1
left a comment
There was a problem hiding this comment.
+a:@SeanCurtis-TRI for feature review, please (not a rush)
@sherm1 made 1 comment.
Reviewable status: LGTM missing from assignee SeanCurtis-TRI(platform), needs at least two assigned reviewers (waiting on SeanCurtis-TRI).
There was a problem hiding this comment.
Pull request overview
Prevents loop breaking from creating shadow links for World while preserving valid topology construction.
Changes:
- Prioritizes splitting the non-World link during loop closure.
- Reports dynamics failures when this forces splitting a massless link.
- Updates documentation and adds regression coverage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
multibody/topology/spanning_forest.cc |
Implements World-safe loop splitting. |
multibody/topology/spanning_forest.h |
Documents loop-breaking guarantees. |
multibody/topology/link_joint_graph.h |
Clarifies that World cannot have shadows. |
multibody/topology/test/spanning_forest_test.cc |
Updates expectations and adds regression tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
6503f9d to
9205ba9
Compare
|
Here's a review from ChatGPT Overall verdictI reviewed the PR head at commit
Those are not equivalent under the shadow-mass implementation this PR is intended to support. ([GitHub]1) 1. Major:
|
SeanCurtis-TRI
left a comment
There was a problem hiding this comment.
It's basically, but I'm delaying it slightly to see what you think about some of my comments. See below.
@SeanCurtis-TRI reviewed 4 files and all commit messages, and made 7 comments.
Reviewable status: 6 unresolved discussions, LGTM missing from assignee SeanCurtis-TRI(platform), needs at least two assigned reviewers (waiting on sherm1).
multibody/topology/spanning_forest.h line 102 at r2 (raw file):
cutting a Link, and a LoopConstraint is added to reconnect the primary Link to its shadow. World is never the Link we cut so if a loop closes on World we cut the Link at the other end of the loop-closing Joint, even if that Link is
nit: I'm not sure what this term means. It is first introduced in this PR.
My primary concern with the phrase is that it is so ambiguous. IN a loop, arguably every joint is a loop closing joint. However, I'm sure that's not what you mean. What you mean is that when your heuristic has decided on targeting one of those joints, it becomes a "loop-closing joint".
I don't mind the introduction of the term, but it should be defined somewhere. In an ideal world, the user should be able to look at a model with a loop and reliably predict which joint is the closing joint. Know what I mean?
It seems particularly significant because the loop could suddenly make the system incompatible with dynamics. Should I feel confident that that outcome is purely a function of the model? Or if the heuristic were slightly tweaked, could it have classified a different joint as the loop-closing joint and I wouldn't have ended up with a massless leaf?
Code quote:
loop-closing Jointmultibody/topology/spanning_forest.h line 584 at r2 (raw file):
// forward or reversed Mobilizer of the Joint's type. Then we add a Weld // Constraint to attach the shadow to its primary. Some details: // - we never split World, so if one of the Links is World we must split
BTW From this documentation, I infer that the "loop-closing" joint is simply the joint we get that appears to connect the leaves of two trees. The joint that is, in some sense, "equidistant" to the world. If that is wrong, ignore what follows...
As documented above, this can lead to the possibility of splitting a massless body and making the tree unacceptable for dynamics, even if the the loop is otherwise acceptable if only we'd split a different body in the loop.
This seems like a defect in the approach. It converts a valid model into an "invalid" model based on something outside the control of the model author. Surely, it would be better to walk back up the two trees being connected and find the nearest joint that has a massful body for splitting. Sure, we have to surrender how balanced the forest is, but an unbalanced forest that can actually evaluate dynamics is clearly superior to a balanced forest that can't.
multibody/topology/spanning_forest.cc line 871 at r2 (raw file):
const bool parent_is_world = links(parent_ordinal).is_world(); const bool child_is_world = links(child_ordinal).is_world(); DRAKE_DEMAND(!(parent_is_world && child_is_world));
BTW This seems like an incredibly conservative test. I'd posit that this would be better as part of LinkJointGraph::Joint (or earlier) where the Joint class asserts that the parent and child links can't be the same. Making it an enforced invariant on the Joint removes the responsibility of the spanning algorithm to worry about how the graph is specified.
On the other hand, is this the only case in which we care about the property that the links are different? And by deferring the test here, we only pay it if we actually have to handle a loop?
multibody/topology/spanning_forest.cc line 925 at r2 (raw file):
} else { data_.why_no_dynamics = fmt::format( "Loop breaks at joint {} between two massless links {} and {}. "
BTW Both error messages seem to use misleading language: "the loop breaks at joint". Obviously, the model doesn't break any joints. This is Drake apparently doing the breaking. But Drake isn't breaking joints. Given that "breaking a joint" to resolve loops is a strong term of art, it seems inadvisable to use it here when we're not actually breaking joints.
I'm not sure what the preferred language would be, I'd have to ponder that.
multibody/topology/test/spanning_forest_test.cc line 2114 at r2 (raw file):
/* The shadow is of link2, not World. */ const LinkJointGraph::Link& shadow = graph.link_by_index(LinkIndex(3));
BTW This test is about the logic for picking the body to split. The mechanism for creating the shadow has already been well tested. As such, instead of checking for every aspect that suggests the shadow is what it says it is, it's probably enough to check for one property and call it good. Checking the shadow name would be sufficient (all other properties have been tested as correlated with the name).
multibody/topology/test/spanning_forest_test.cc line 2120 at r2 (raw file):
EXPECT_EQ(graph.link_by_index(LinkIndex(2)).num_shadows(), 1); /* World, link1, and link2 all follow the World Mobod; the shadow gets its
nit: Related to the previous note, all of the tests below here are correlated with having not picked any of the other links to split. So, is it necessary to explicitly test everything?
6be0adf to
d346b56
Compare
sherm1
left a comment
There was a problem hiding this comment.
All review comments addressed, PTAL.
I put the response to Claude's comments in one commit and the response to yours (Sean's) in the next commit. Probably easier just to review them together though.
@sherm1 made 7 comments and resolved 6 discussions.
Reviewable status: LGTM missing from assignee SeanCurtis-TRI(platform), needs at least two assigned reviewers, commits need curation (https://drake.mit.edu/reviewable.html#curated-commits) (waiting on SeanCurtis-TRI).
multibody/topology/spanning_forest.h line 102 at r2 (raw file):
Previously, SeanCurtis-TRI (Sean Curtis) wrote…
nit: I'm not sure what this term means. It is first introduced in this PR.
My primary concern with the phrase is that it is so ambiguous. IN a loop, arguably every joint is a loop closing joint. However, I'm sure that's not what you mean. What you mean is that when your heuristic has decided on targeting one of those joints, it becomes a "loop-closing joint".
I don't mind the introduction of the term, but it should be defined somewhere. In an ideal world, the user should be able to look at a model with a loop and reliably predict which joint is the closing joint. Know what I mean?
It seems particularly significant because the loop could suddenly make the system incompatible with dynamics. Should I feel confident that that outcome is purely a function of the model? Or if the heuristic were slightly tweaked, could it have classified a different joint as the loop-closing joint and I wouldn't have ended up with a massless leaf?
Done, PTAL. "Loop joint" might not be the best term but I've defined it here since we do tend to use it. The concept is "the joint we chose for re-targeting to the shadow" but that's too awkward!
I don't think the cutting choice will be easy for users to predict except in limited cases: when there are an odd number of massful links in a loop, the central link will get cut. Even then there are two choices for which joint gets to be the "loop joint". To know which one requires understanding the processing order of the model-building heuristic. It is deterministic, and it's described in the internal documentation, but it is too inside-baseball to expect users to keep it in mind.
multibody/topology/spanning_forest.h line 584 at r2 (raw file):
Previously, SeanCurtis-TRI (Sean Curtis) wrote…
BTW From this documentation, I infer that the "loop-closing" joint is simply the joint we get that appears to connect the leaves of two trees. The joint that is, in some sense, "equidistant" to the world. If that is wrong, ignore what follows...
As documented above, this can lead to the possibility of splitting a massless body and making the tree unacceptable for dynamics, even if the the loop is otherwise acceptable if only we'd split a different body in the loop.
This seems like a defect in the approach. It converts a valid model into an "invalid" model based on something outside the control of the model author. Surely, it would be better to walk back up the two trees being connected and find the nearest joint that has a massful body for splitting. Sure, we have to surrender how balanced the forest is, but an unbalanced forest that can actually evaluate dynamics is clearly superior to a balanced forest that can't.
Agreed! The algorithm already does what you're suggesting. It gobbles up massless links hunting for a massful one to split. It only gives up when it can't find one. It sacrifies branch balancing when necessary to preserve the ability to do dynamics.
multibody/topology/spanning_forest.cc line 871 at r2 (raw file):
Previously, SeanCurtis-TRI (Sean Curtis) wrote…
BTW This seems like an incredibly conservative test. I'd posit that this would be better as part of
LinkJointGraph::Joint(or earlier) where theJointclass asserts that the parent and child links can't be the same. Making it an enforced invariant on theJointremoves the responsibility of the spanning algorithm to worry about how the graph is specified.On the other hand, is this the only case in which we care about the property that the links are different? And by deferring the test here, we only pay it if we actually have to handle a loop?
Right. MbP already prohibits joints where both frames are on the same body. Technically we don't care unless it makes us attempt to split World. A loop that looks like this World -> free joint -> link -> revolute joint -> World is actually vaguely meaningful -- the link is restricted to revolute motion but the free joint q's and v's read out directly as its spatial pose and spatial velocity. (Of course there are better ways to get the same information -- my point is that the topology is still legitimate even if likely ill-advised.)
multibody/topology/spanning_forest.cc line 925 at r2 (raw file):
Previously, SeanCurtis-TRI (Sean Curtis) wrote…
BTW Both error messages seem to use misleading language: "the loop breaks at joint". Obviously, the model doesn't break any joints. This is Drake apparently doing the breaking. But Drake isn't breaking joints. Given that "breaking a joint" to resolve loops is a strong term of art, it seems inadvisable to use it here when we're not actually breaking joints.
I'm not sure what the preferred language would be, I'd have to ponder that.
Done, PTAL (both messages)
multibody/topology/test/spanning_forest_test.cc line 2114 at r2 (raw file):
Previously, SeanCurtis-TRI (Sean Curtis) wrote…
BTW This test is about the logic for picking the body to split. The mechanism for creating the shadow has already been well tested. As such, instead of checking for every aspect that suggests the shadow is what it says it is, it's probably enough to check for one property and call it good. Checking the shadow name would be sufficient (all other properties have been tested as correlated with the name).
Done
multibody/topology/test/spanning_forest_test.cc line 2120 at r2 (raw file):
Previously, SeanCurtis-TRI (Sean Curtis) wrote…
nit: Related to the previous note, all of the tests below here are correlated with having not picked any of the other links to split. So, is it necessary to explicitly test everything?
I like seeing the expected structure laid out. I'm sure you're right that it could be inferred from other information but I like seeing it explicitly laid out here. If nothing else I think it makes it easier to understand the test case. The actual mapping of joints & links to mobods is very obscure and few (human) readers will have a good grasp of what's to be expected.
Previously there were odd circumstances in which the topology code would decide to break a loop by splitting off an arbitrary-mass "shadow" link from World (and welding it back on). That's never a good idea and isn't supported by the implementation in #24864 since shadow links can't have their own mass properties (they get mass from their primary links). @SeanCurtis-TRI and his trusty AI noticed this problem while reviewing #24864.
This PR modifies the loop breaking algorithm slightly to prevent ever choosing World as the to-be-broken link, modifies an existing test case to reflect the new expectation that World won't be broken, and adds a new test case in which fusing links to weld and then connecting that fused assembly back to World would decide to split World in some circumstances.
A few comments are updated to note that World is never split.
There are no user-visible changes since automatic loop breaking isn't yet enabled in Drake. No release notes are required.
This change is