Skip to main content

geop_ops_assembly/urdf/
tree.rs

1//! [`tree`]: a part's mechanism as a URDF robot's tree of links — which
2//! placed parts move as one link, and which joint carries each link on the
3//! one before it — or why it is none.
4//!
5//! Parts held together by mates alone — constraints and fastened joints —
6//! move as one, so they make one link; so do the parts the mates hold to
7//! the ground (the fixed parts), which make the root, and a pattern's
8//! copies with the part each copies. That only holds if those mates hold them rigidly: where they leave
9//! a part free to move, the motion is one no URDF joint names, and the
10//! export is refused. The revolute and slider joints between links must
11//! then form a tree hanging from the root: a joint closing a loop, or a
12//! link no joint reaches, is refused too, by name.
13
14use std::collections::{BTreeMap, HashMap, HashSet};
15
16use crate::{Joint, JointKind, Motion};
17use geop_core_math::{
18    geop_error::{GeopError, GeopResult},
19    solvers::walk::{Take, flood},
20    union_find::UnionFind,
21};
22
23use crate::urdf::Mechanism;
24
25/// The links of a mechanism and the joints between them.
26#[derive(Clone, Debug, PartialEq)]
27pub struct Tree {
28    /// The root first — the ground, and every body held to it — then each
29    /// link after the one its joint carries it on.
30    pub links: Vec<LinkBodies>,
31    /// The joint carrying each link but the root, in the order of
32    /// [`Tree::links`].
33    pub joints: Vec<TreeJoint>,
34}
35
36/// The bodies of one link, by index in the mechanism, and its name: the
37/// first of its parts — for the root, the first fixed one, or `base`.
38#[derive(Clone, Debug, PartialEq)]
39pub struct LinkBodies {
40    pub name: String,
41    pub bodies: Vec<usize>,
42}
43
44/// A joint of the mechanism (by index) carrying the link `child` on the
45/// link `parent` (by index in [`Tree::links`]); `forward` if its first end
46/// is on the parent.
47#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct TreeJoint {
49    pub joint: usize,
50    pub parent: usize,
51    pub child: usize,
52    pub forward: bool,
53}
54
55/// The groups of `mechanism`'s bodies that move as one: those held to the
56/// ground, and those its constraints and fastened joints hold together —
57/// of the ground (`0`) and of every body (`b + 1`), the smallest of its
58/// group, so the ground's group is `0`.
59fn groups(mechanism: &Mechanism) -> Vec<usize> {
60    let count = mechanism.bodies.len();
61    let mut groups = UnionFind::new(count + 1);
62    for (b, body) in mechanism.bodies.iter().enumerate() {
63        match body.carrier {
64            Some(carrier) => groups.union(carrier + 1, b + 1),
65            None if !body.free => groups.union(0, b + 1),
66            None => {}
67        }
68    }
69    for (_, [a, b]) in &mechanism.constraints {
70        groups.union(node(*a), node(*b));
71    }
72    for joint in &mechanism.joints {
73        if joint.kind.joint == Joint::Fastened {
74            groups.union(node(joint.a.body), node(joint.b.body));
75        }
76    }
77    (0..=count).map(|node| groups.find(node)).collect()
78}
79
80/// The node of an end attached to `body`, or to the ground.
81fn node(body: Option<usize>) -> usize {
82    body.map_or(0, |b| b + 1)
83}
84
85/// The tree of links `mechanism` makes (see the module docs) — not yet
86/// checked for links mates hold loosely (see [`check_rigid`]).
87pub fn tree(mechanism: &Mechanism) -> GeopResult<Tree> {
88    let count = mechanism.bodies.len();
89    let groups = groups(mechanism);
90    for joint in &mechanism.joints {
91        check_joint(&joint.name, &joint.kind)?;
92    }
93
94    let part = |b: usize| mechanism.bodies[b].name.as_str();
95    let members =
96        |group: usize| -> Vec<usize> { (0..count).filter(|&b| groups[b + 1] == group).collect() };
97    let link_named = |group: usize| -> LinkBodies {
98        let bodies = members(group);
99        let fixed = (bodies.iter()).find(|&&b| {
100            let body = &mechanism.bodies[b];
101            !body.free && body.carrier.is_none()
102        });
103        let name = match fixed.or(bodies.first()) {
104            Some(&b) => part(b).to_string(),
105            None => "base".to_string(),
106        };
107        LinkBodies { name, bodies }
108    };
109
110    let moving: Vec<usize> = (0..mechanism.joints.len())
111        .filter(|&j| mechanism.joints[j].kind.joint != Joint::Fastened)
112        .collect();
113    let ends = |j: usize| {
114        let joint = &mechanism.joints[j];
115        (groups[node(joint.a.body)], groups[node(joint.b.body)])
116    };
117    let mut tree = Tree {
118        links: vec![link_named(0)],
119        joints: Vec::new(),
120    };
121    let mut link_of_group = HashMap::from([(0, 0)]);
122    let mut group_of_link = vec![0];
123    // Per link, the joint carrying it, by index into `tree.joints`.
124    let mut carried_by: Vec<Option<usize>> = vec![None];
125    let mut used = HashSet::new();
126    // Out from the root, level by level: each link carries the links its
127    // joints not yet used lead to.
128    flood([0], Take::Oldest, |&link| {
129        let group = group_of_link[link];
130        let mut children = Vec::new();
131        for &j in &moving {
132            let (a, b) = ends(j);
133            if used.contains(&j) || (a != group && b != group) {
134                continue;
135            }
136            used.insert(j);
137            if a == b {
138                return Err(GeopError::new(format!(
139                    "the joint {:?} joins {}, which mates already hold rigidly together: a closed loop, which URDF — a tree of links — cannot express; remove the joint or the mates",
140                    mechanism.joints[j].name, tree.links[link].name
141                )));
142            }
143            let (other, forward) = if a == group { (b, true) } else { (a, false) };
144            if let Some(&reached) = link_of_group.get(&other) {
145                return Err(loop_error(mechanism, &tree, &carried_by, link, reached, j));
146            }
147            let child = tree.links.len();
148            link_of_group.insert(other, child);
149            group_of_link.push(other);
150            tree.links.push(link_named(other));
151            carried_by.push(Some(tree.joints.len()));
152            tree.joints.push(TreeJoint {
153                joint: j,
154                parent: link,
155                child,
156                forward,
157            });
158            children.push(child);
159        }
160        Ok(children)
161    })?;
162
163    let unreached: Vec<&str> = (0..count)
164        .filter(|&b| !link_of_group.contains_key(&groups[b + 1]))
165        .map(part)
166        .collect();
167    if !unreached.is_empty() {
168        let root = &tree.links[0];
169        let one = unreached.len() == 1;
170        let parts = unreached.join(", ");
171        return Err(GeopError::new(if root.bodies.is_empty() {
172            format!(
173                "no part is fixed, so {parts} {} from nothing: a URDF robot is a tree of links from one root, the fixed part — fix the part the robot stands on",
174                if one { "hangs" } else { "hang" }
175            )
176        } else {
177            format!(
178                "{parts} {} joined to the fixed part {} by neither joints nor mates: a URDF robot is one tree of links from one root — join {} to it, or fix {}",
179                if one { "is" } else { "are" },
180                root.name,
181                if one { "it" } else { "them" },
182                if one { "it" } else { "them" },
183            )
184        }));
185    }
186    Ok(tree)
187}
188
189/// Refuses a joint no URDF joint is — one that turns and slides, or turns
190/// more ways than one — or limits URDF cannot write: a revolute joint
191/// limited one way only, a slider not limited both ways.
192fn check_joint(name: &str, kind: &JointKind) -> GeopResult<()> {
193    let problem = match kind.joint {
194        Joint::Revolute | Joint::Slider | Joint::Fastened => None,
195        other => Some(format!(
196            "is {}, and a URDF joint turns about one axis or slides along one, or neither: use a revolute, a slider or a fastened joint",
197            other.label().to_lowercase(),
198        )),
199    };
200    let [min, max] = match kind.joint {
201        Joint::Slider => kind.limits(Motion::Slide),
202        _ => kind.limits(Motion::Turn),
203    };
204    let problem = problem.or_else(|| match kind.joint {
205        Joint::Revolute if min.is_some() != max.is_some() => Some(format!(
206            "is limited one way only ({}), and a URDF revolute joint is limited both ways or not at all (continuous): give it both limits or neither",
207            if min.is_some() { "a min" } else { "a max" }
208        )),
209        Joint::Slider if min.is_none() || max.is_none() => Some(
210            "slides without a min and a max, and a URDF prismatic joint needs both: give the joint its travel"
211                .to_string(),
212        ),
213        _ => None,
214    });
215    match problem {
216        Some(problem) => Err(GeopError::new(format!("the joint {name:?} {problem}"))),
217        None => Ok(()),
218    }
219}
220
221/// Refuses parts that mates alone hold together but not rigidly: those of
222/// `mechanism` that `freedom` — how free each placed part is, by name,
223/// with every joint held at its coordinates — says can still move. Of the
224/// links of `tree`, which hold every part.
225pub fn check_rigid(
226    mechanism: &Mechanism,
227    tree: &Tree,
228    freedom: &BTreeMap<String, usize>,
229) -> GeopResult<()> {
230    let loose: Vec<usize> = (0..mechanism.bodies.len())
231        .filter(|&b| {
232            freedom
233                .get(&mechanism.bodies[b].name)
234                .is_some_and(|&dof| dof > 0)
235        })
236        .collect();
237    if loose.is_empty() {
238        return Ok(());
239    }
240    // The links of the loose parts, and the mates holding those together.
241    // The assembly itself is the root's.
242    let link_of = |body: Option<usize>| match body {
243        Some(b) => tree.links.iter().position(|l| l.bodies.contains(&b)),
244        None => Some(0),
245    };
246    let links: HashSet<Option<usize>> = loose.iter().map(|&b| link_of(Some(b))).collect();
247    let in_links = |ends: [Option<usize>; 2]| ends.iter().any(|&e| links.contains(&link_of(e)));
248    let parts: Vec<&str> = (loose.iter())
249        .map(|&b| mechanism.bodies[b].name.as_str())
250        .collect();
251    let mut mates: Vec<&str> = (mechanism.constraints.iter())
252        .filter(|(_, ends)| in_links(*ends))
253        .map(|(name, _)| name.as_str())
254        .collect();
255    mates.extend(
256        (mechanism.joints.iter())
257            .filter(|j| j.kind.joint == Joint::Fastened && in_links([j.a.body, j.b.body]))
258            .map(|j| j.name.as_str()),
259    );
260    Err(GeopError::new(format!(
261        "{} {} held by the mates {} alone, which leave {} free to move: a URDF robot moves only at its joints — join the parts by revolute or slider joints, or hold them fully",
262        parts.join(", "),
263        if parts.len() == 1 { "is" } else { "are" },
264        mates.join(", "),
265        if parts.len() == 1 { "it" } else { "them" },
266    )))
267}
268
269/// The error for the joint `closing`, which joins the link `from` to the
270/// link `reached`, already in the tree: the loop it closes, by its joints
271/// and its links.
272fn loop_error(
273    mechanism: &Mechanism,
274    tree: &Tree,
275    carried_by: &[Option<usize>],
276    from: usize,
277    reached: usize,
278    closing: usize,
279) -> GeopError {
280    // Each link's way back to the root: the links, from it.
281    let up = |link: usize| -> Vec<usize> {
282        std::iter::successors(Some(link), |&l| {
283            carried_by[l].map(|j| tree.joints[j].parent)
284        })
285        .collect()
286    };
287    let (a, b) = (up(from), up(reached));
288    let common = a.iter().find(|l| b.contains(l)).copied().unwrap_or(0);
289    let before = |path: &[usize]| -> Vec<usize> {
290        path.iter().copied().take_while(|&l| l != common).collect()
291    };
292    let (a, b) = (before(&a), before(&b));
293    let mut links: Vec<usize> = a.clone();
294    links.push(common);
295    links.extend(b.iter().rev());
296    let mut joints: Vec<&str> = a
297        .iter()
298        .chain(&b)
299        .filter_map(|&l| carried_by[l])
300        .map(|j| mechanism.joints[tree.joints[j].joint].name.as_str())
301        .collect();
302    joints.push(mechanism.joints[closing].name.as_str());
303    joints.sort();
304    let links: Vec<&str> = links.iter().map(|&l| tree.links[l].name.as_str()).collect();
305    GeopError::new(format!(
306        "the joints {} close a loop through {}: URDF describes a tree of links, and cannot express a closed kinematic chain — remove one of these joints to export the rest",
307        joints.join(", "),
308        links.join(", ")
309    ))
310}