Skip to content

Commit 8c00387

Browse files
committed
Adds misc algorithms over graph and forest
1 parent dd7e816 commit 8c00387

6 files changed

+572
-76
lines changed

multibody/topology/link_joint_graph.cc

+148
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,154 @@ bool LinkJointGraph::link_is_static(const Link& link) const {
704704
ForestBuildingOptions::kStatic);
705705
}
706706

707+
/* Runs through the Mobods in the model but records the (active) Link
708+
indexes rather than the Mobod indexes. */
709+
std::vector<BodyIndex> LinkJointGraph::FindPathFromWorld(
710+
BodyIndex link_index) const {
711+
ThrowIfForestNotBuiltYet(__func__);
712+
const SpanningForest::Mobod* mobod =
713+
&forest().mobods()[link_to_mobod(link_index)];
714+
std::vector<BodyIndex> path(mobod->level() + 1);
715+
while (mobod->inboard().is_valid()) {
716+
const Link& link = links(mobod->link_ordinal());
717+
path[mobod->level()] = link.index(); // Active Link if composite.
718+
mobod = &forest().mobods(mobod->inboard());
719+
}
720+
DRAKE_DEMAND(mobod->is_world());
721+
path[0] = BodyIndex(0);
722+
return path;
723+
}
724+
725+
BodyIndex LinkJointGraph::FindFirstCommonAncestor(BodyIndex link1_index,
726+
BodyIndex link2_index) const {
727+
ThrowIfForestNotBuiltYet(__func__);
728+
const MobodIndex mobod_ancestor = forest().FindFirstCommonAncestor(
729+
link_to_mobod(link1_index), link_to_mobod(link2_index));
730+
const Link& ancestor_link =
731+
links(forest().mobod_to_link_ordinal(mobod_ancestor));
732+
return ancestor_link.index();
733+
}
734+
735+
std::vector<BodyIndex> LinkJointGraph::FindSubtreeLinks(
736+
BodyIndex link_index) const {
737+
ThrowIfForestNotBuiltYet(__func__);
738+
const MobodIndex root_mobod_index = link_to_mobod(link_index);
739+
return forest().FindSubtreeLinks(root_mobod_index);
740+
}
741+
742+
// Our link_composites collection doesn't include lone Links that aren't welded
743+
// to anything. The return from this function must include every Link, with
744+
// the World link in the first set (even if nothing is welded to it).
745+
std::vector<std::set<BodyIndex>> LinkJointGraph::GetSubgraphsOfWeldedLinks()
746+
const {
747+
ThrowIfForestNotBuiltYet(__func__);
748+
749+
std::vector<std::set<BodyIndex>> subgraphs;
750+
751+
// First, collect all the precomputed Link Composites. World is always
752+
// the first one, even if nothing is welded to it.
753+
for (const LinkComposite& composite : link_composites()) {
754+
subgraphs.emplace_back(
755+
std::set<BodyIndex>(composite.links.cbegin(), composite.links.cend()));
756+
}
757+
758+
// Finally, make one-Link subgraphs for Links that aren't in any composite.
759+
for (const Link& link : links()) {
760+
if (link.composite().has_value()) continue;
761+
subgraphs.emplace_back(std::set<BodyIndex>{link.index()});
762+
}
763+
764+
return subgraphs;
765+
}
766+
767+
// Strategy here is to make repeated use of CalcLinksWeldedTo(), separating
768+
// the singleton sets from the actually-welded sets, and then move the
769+
// singletons to the end to match what GetSubgraphsOfWeldedLinks() does.
770+
std::vector<std::set<BodyIndex>> LinkJointGraph::CalcSubgraphsOfWeldedLinks()
771+
const {
772+
// Work with ordinals rather than indexes.
773+
std::vector<bool> visited(num_user_links(), false);
774+
775+
// World always comes first, even if it is alone.
776+
std::vector<std::set<BodyIndex>> subgraphs{CalcLinksWeldedTo(BodyIndex(0))};
777+
for (BodyIndex index : subgraphs.back())
778+
visited[index_to_ordinal(index)] = true;
779+
780+
std::vector<std::set<BodyIndex>> singletons;
781+
// If a Forest was already built, there may be shadow links added to
782+
// the graph -- don't process those here.
783+
for (LinkOrdinal link_ordinal(1); link_ordinal < num_user_links();
784+
++link_ordinal) {
785+
const Link& link = links(link_ordinal);
786+
if (link.is_shadow() || visited[link_ordinal]) continue;
787+
std::set<BodyIndex> welded_links = CalcLinksWeldedTo(link.index());
788+
for (BodyIndex index : welded_links)
789+
visited[index_to_ordinal(index)] = true;
790+
auto* which_list = ssize(welded_links) == 1 ? &singletons : &subgraphs;
791+
which_list->emplace_back(std::move(welded_links));
792+
}
793+
794+
// Now move all the singletons onto the end of the subgraphs list.
795+
for (auto& singleton : singletons)
796+
subgraphs.emplace_back(std::move(singleton));
797+
798+
return subgraphs;
799+
}
800+
801+
// If the Link isn't part of a LinkComposite just return the Link. Otherwise,
802+
// return all the Links in its LinkComposite.
803+
std::set<BodyIndex> LinkJointGraph::GetLinksWeldedTo(
804+
BodyIndex link_index) const {
805+
ThrowIfForestNotBuiltYet(__func__);
806+
DRAKE_DEMAND(link_index.is_valid());
807+
DRAKE_THROW_UNLESS(has_link(link_index));
808+
const Link& link = link_by_index(link_index);
809+
const std::optional<LinkCompositeIndex> composite_index = link.composite();
810+
if (!composite_index.has_value()) return std::set<BodyIndex>{link_index};
811+
const std::vector<BodyIndex>& welded_links =
812+
link_composites(*composite_index).links;
813+
return std::set<BodyIndex>(welded_links.cbegin(), welded_links.cend());
814+
}
815+
816+
// Without a Forest we don't have LinkComposites available so recursively
817+
// chase Weld joints instead.
818+
std::set<BodyIndex> LinkJointGraph::CalcLinksWeldedTo(
819+
BodyIndex link_index) const {
820+
std::set<BodyIndex> result;
821+
AppendLinksWeldedTo(link_index, &result);
822+
return result;
823+
}
824+
825+
void LinkJointGraph::AppendLinksWeldedTo(BodyIndex link_index,
826+
std::set<BodyIndex>* result) const {
827+
DRAKE_DEMAND(result != nullptr);
828+
DRAKE_DEMAND(link_index.is_valid());
829+
DRAKE_THROW_UNLESS(has_link(link_index));
830+
DRAKE_DEMAND(result->count(link_index) == 0);
831+
832+
const Link& link = link_by_index(link_index);
833+
834+
// A Link is always considered welded to itself.
835+
result->insert(link_index);
836+
for (auto joint_index : link.joints()) {
837+
const Joint& joint = joint_by_index(joint_index);
838+
if (joint.traits_index() != weld_joint_traits_index()) continue;
839+
const BodyIndex welded_link_index = joint.other_link_index(link_index);
840+
// Don't reprocess if we already did the other end.
841+
if (result->count(welded_link_index) == 0)
842+
AppendLinksWeldedTo(welded_link_index, &*result);
843+
}
844+
}
845+
846+
void LinkJointGraph::ThrowIfForestNotBuiltYet(const char* func) const {
847+
if (!forest_is_valid()) {
848+
throw std::logic_error(
849+
fmt::format("{}(): no SpanningForest available. Call BuildForest() "
850+
"before calling this function.",
851+
func));
852+
}
853+
}
854+
707855
void LinkJointGraph::ThrowLinkWasRemoved(const char* func,
708856
BodyIndex link_index) const {
709857
throw std::logic_error(fmt::format(

multibody/topology/link_joint_graph.h

+82-2
Original file line numberDiff line numberDiff line change
@@ -407,11 +407,11 @@ class LinkJointGraph {
407407
[[nodiscard]] inline const LoopConstraint& loop_constraints(
408408
LoopConstraintIndex constraint_index) const;
409409

410-
/** Links with this index or higher are "ephemeral" (added during
410+
/** Links with this ordinal or higher are "ephemeral" (added during
411411
forest-building). See the class comment for more information. */
412412
[[nodiscard]] int num_user_links() const { return data_.num_user_links; }
413413

414-
/** Joints with this index or higher are "ephemeral" (added during
414+
/** Joints with this ordinal or higher are "ephemeral" (added during
415415
forest-building). See the class comment for more information. */
416416
[[nodiscard]] int num_user_joints() const { return data_.num_user_joints; }
417417

@@ -499,6 +499,79 @@ class LinkJointGraph {
499499
void ChangeJointType(JointIndex existing_joint_index,
500500
const std::string& name_of_new_type);
501501

502+
/** After the Forest is built, returns the sequence of Links from World to the
503+
given Link L in the Forest. In case the Forest was built with a single Mobod
504+
for each Link Composite (Links connected by Weld joints), we only report the
505+
"active Link" for each Link Composite so that there is only one Link returned
506+
for each level in Link L's tree. World is always the active Link for its
507+
composite so will always be the first entry in the result. However, if L is
508+
part of a Link Composite the final entry will be L's composite's active link,
509+
which might not be L. Cost is O(ℓ) where ℓ is Link L's level in the
510+
SpanningForest.
511+
@throws std::exception if called prior to modeling
512+
@see link_composites(), SpanningForest::FindPathFromWorld() */
513+
std::vector<BodyIndex> FindPathFromWorld(BodyIndex link_index) const;
514+
515+
/** After the Forest is built, finds the first Link common to the paths
516+
towards World from each of two Links in the SpanningForest. Returns World
517+
immediately if the Links are on different trees of the forest. Otherwise the
518+
cost is O(ℓ) where ℓ is the length of the longer path from one of the Links
519+
to the ancestor. */
520+
BodyIndex FindFirstCommonAncestor(BodyIndex link1_index,
521+
BodyIndex link2_index) const;
522+
523+
/** After the Forest is built, finds all the Links following the Forest
524+
subtree whose root mobilized body is the one followed by the given Link. That
525+
is, we look up the given Link's Mobod B and return all the Links that follow
526+
B or any other Mobod in the subtree rooted at B. The Links following B
527+
come first, and the rest follow the depth-first ordering of the Mobods.
528+
In particular, the result is _not_ sorted by BodyIndex. Computational cost
529+
is O(ℓ) where ℓ is the number of Links following the subtree. */
530+
std::vector<BodyIndex> FindSubtreeLinks(BodyIndex link_index) const;
531+
532+
/** After the Forest is built, this method can be called to return a
533+
partitioning of the LinkJointGraph into subgraphs such that (a) every Link is
534+
in one and only one subgraph, and (b) two Links are in the same subgraph iff
535+
there is a path between them which consists only of Weld joints.
536+
Each subgraph of welded Links is represented as a set of
537+
Link indexes (using BodyIndex). By definition, these subgraphs will be
538+
disconnected by any non-Weld joint between two Links. A few notes:
539+
- The maximum number of returned subgraphs equals the number of Links in
540+
the graph. This corresponds to a graph with no Weld joints.
541+
- The World Link is included in a set of welded Links, and this set is
542+
element zero in the returned vector. The other subgraphs are in no
543+
particular order.
544+
- The minimum number of subgraphs is one. This corresponds to a graph with
545+
all Links welded to World.
546+
547+
@throws std::exception if the SpanningForest hasn't been built yet.
548+
@see CalcSubgraphsOfWeldedLinks() if you haven't built a Forest yet */
549+
std::vector<std::set<BodyIndex>> GetSubgraphsOfWeldedLinks() const;
550+
551+
/** This much-slower method does not depend on the SpanningForest having
552+
already been built. It is a fallback for when there is no Forest.
553+
@see GetSubgraphsOfWeldedLinks() if you already have a Forest built */
554+
std::vector<std::set<BodyIndex>> CalcSubgraphsOfWeldedLinks() const;
555+
556+
/** After the Forest is built, returns all Links that are transitively welded,
557+
or rigidly affixed, to `link_index`, per these two definitions:
558+
1. A Link is always considered welded to itself.
559+
2. Two unique Links are considered welded together exclusively by the
560+
presence of weld joints, not by other constructs such as constraints.
561+
562+
Therefore, if `link_index` is a valid index to a Link in this graph, then the
563+
return vector will always contain at least one entry storing `link_index`.
564+
This is fast because we just need to sort the already-calculated
565+
LinkComposite the given `link_index` is part of (if any).
566+
567+
@throws std::exception if the SpanningForest hasn't been built yet or
568+
`link_index` is out of range */
569+
std::set<BodyIndex> GetLinksWeldedTo(BodyIndex link_index) const;
570+
571+
/** This slower method does not depend on the SpanningForest having
572+
already been built. It is a fallback for when there is no Forest. */
573+
std::set<BodyIndex> CalcLinksWeldedTo(BodyIndex link_index) const;
574+
502575
// FYI Debugging APIs (including Graphviz-related) are defined in
503576
// link_joint_graph_debug.cc.
504577

@@ -508,6 +581,7 @@ class LinkJointGraph {
508581
The result is in the "dot" language, see https://graphviz.org. If you
509582
write it to some file foo.dot, you can generate a viewable png (for
510583
example) using the command `dot -Tpng foo.dot >foo.png`.
584+
@see SpanningForest::GenerateGraphvizString()
511585
@see MakeGraphvizFiles() for an easier way to get pngs. */
512586
std::string GenerateGraphvizString(
513587
std::string_view label, bool include_ephemeral_elements = true) const;
@@ -690,6 +764,12 @@ class LinkJointGraph {
690764
void AddUnmodeledJointToComposite(JointOrdinal unmodeled_joint_ordinal,
691765
LinkCompositeIndex which);
692766

767+
// This is the implementation for CalcLinksWeldedTo().
768+
void AppendLinksWeldedTo(BodyIndex link_index,
769+
std::set<BodyIndex>* result) const;
770+
771+
void ThrowIfForestNotBuiltYet(const char* func) const;
772+
693773
[[noreturn]] void ThrowLinkWasRemoved(const char* func,
694774
BodyIndex link_index) const;
695775

0 commit comments

Comments
 (0)