Skip to main content

phyz_model/
model.rs

1//! Model definition — static description of a physical system.
2
3use crate::{Body, Joint, JointType, State};
4use phyz_math::{GRAVITY, SpatialInertia, SpatialTransform, Vec3};
5
6/// An actuator attached to a joint.
7///
8/// Follows MuJoCo's affine actuator model: the generalized force applied to the
9/// joint's first DOF is
10///
11/// ```text
12/// f = gear * (gain * ctrl + bias_q * q + bias_v * v)
13/// ```
14///
15/// which expresses `<motor>`, `<position>`, `<velocity>` and the affine subset
16/// of `<general>` with one parameter set, so downstream code never branches on
17/// actuator type. A plain motor is `gain = 1, bias_q = bias_v = 0`, for which
18/// this reduces to `gear * clamp(ctrl)`.
19#[derive(Debug, Clone)]
20pub struct Actuator {
21    /// Actuator name, as declared in the source model.
22    pub name: String,
23    /// Name of the joint this actuator drives.
24    pub joint_name: String,
25    /// Index into [`Model::joints`] of the driven joint.
26    pub joint_idx: usize,
27    /// Gear ratio applied to the control signal to produce joint torque.
28    pub gear: f64,
29    /// Optional `[lo, hi]` clamp on the control signal, before `gear`.
30    pub ctrl_range: Option<[f64; 2]>,
31    /// Coefficient on `ctrl`. 1 for a motor, `kp` for a position servo.
32    pub gain: f64,
33    /// Coefficient on joint position. `-kp` for a position servo, else 0.
34    pub bias_q: f64,
35    /// Coefficient on joint velocity. `-kv` for a servo, else 0.
36    pub bias_v: f64,
37    /// Force limits applied after the affine law, or `None` if unlimited.
38    pub force_range: Option<[f64; 2]>,
39}
40
41impl Default for Actuator {
42    fn default() -> Self {
43        Self {
44            name: String::new(),
45            joint_name: String::new(),
46            joint_idx: 0,
47            gear: 1.0,
48            ctrl_range: None,
49            gain: 1.0,
50            bias_q: 0.0,
51            bias_v: 0.0,
52            force_range: None,
53        }
54    }
55}
56
57impl Actuator {
58    /// Clamp a raw control value to this actuator's control range.
59    pub fn clamp_ctrl(&self, ctrl: f64) -> f64 {
60        match self.ctrl_range {
61            Some([lo, hi]) => ctrl.clamp(lo.min(hi), hi.max(lo)),
62            None => ctrl,
63        }
64    }
65
66    /// Generalized force produced by this actuator for a raw control value,
67    /// ignoring any state feedback.
68    ///
69    /// Correct for motors. Position and velocity servos additionally depend on
70    /// `q` and `v`; use [`Self::force_at`] for those.
71    pub fn force(&self, ctrl: f64) -> f64 {
72        self.force_at(ctrl, 0.0, 0.0)
73    }
74
75    /// Evaluate the full affine actuator law at the joint's current state.
76    pub fn force_at(&self, ctrl: f64, q: f64, v: f64) -> f64 {
77        let f = self.gear * (self.gain * self.clamp_ctrl(ctrl) + self.bias_q * q + self.bias_v * v);
78        match self.force_range {
79            Some([lo, hi]) => f.clamp(lo.min(hi), hi.max(lo)),
80            None => f,
81        }
82    }
83
84    /// A direct-torque motor: `f = gear * ctrl`.
85    pub fn motor(name: &str, joint_name: &str, joint_idx: usize, gear: f64) -> Self {
86        Self {
87            name: name.to_string(),
88            joint_name: joint_name.to_string(),
89            joint_idx,
90            gear,
91            ..Default::default()
92        }
93    }
94
95    /// A position servo: `f = gear * (kp * (ctrl - q) - kv * v)`.
96    pub fn position(
97        name: &str,
98        joint_name: &str,
99        joint_idx: usize,
100        gear: f64,
101        kp: f64,
102        kv: f64,
103    ) -> Self {
104        Self {
105            gain: kp,
106            bias_q: -kp,
107            bias_v: -kv,
108            ..Self::motor(name, joint_name, joint_idx, gear)
109        }
110    }
111
112    /// A velocity servo: `f = gear * kv * (ctrl - v)`.
113    pub fn velocity(name: &str, joint_name: &str, joint_idx: usize, gear: f64, kv: f64) -> Self {
114        Self {
115            gain: kv,
116            bias_v: -kv,
117            ..Self::motor(name, joint_name, joint_idx, gear)
118        }
119    }
120}
121
122/// Static model describing the topology and parameters of a physical system.
123#[derive(Debug, Clone)]
124pub struct Model {
125    /// Bodies in the kinematic tree (index 0 = first body, no world body).
126    pub bodies: Vec<Body>,
127    /// Joints connecting bodies.
128    pub joints: Vec<Joint>,
129    /// Gravity vector in world frame.
130    pub gravity: Vec3,
131    /// Integration timestep.
132    pub dt: f64,
133    /// Total number of position DOFs.
134    pub nq: usize,
135    /// Total number of velocity DOFs.
136    pub nv: usize,
137    /// Position DOF offset for each joint.
138    pub q_offsets: Vec<usize>,
139    /// Velocity DOF offset for each joint.
140    pub v_offsets: Vec<usize>,
141    /// Actuators (motors) acting on joints.
142    pub actuators: Vec<Actuator>,
143    /// Body pairs that must never produce a contact, beyond the structural
144    /// exclusions [`Model::may_collide`] applies on its own.
145    ///
146    /// The escape hatch for authored geometry that overlaps across a *moving*
147    /// joint — common in URDFs, where a shoulder capsule is drawn long enough
148    /// to intersect the trunk box at some arm angles. Such a pair is a
149    /// modelling artifact, not an event, but no rule derivable from the tree
150    /// can tell it apart from a genuine self-touch, so it is data.
151    ///
152    /// Order within a pair does not matter.
153    pub contact_exclude: Vec<(usize, usize)>,
154}
155
156impl Model {
157    /// Create a default empty state for this model.
158    pub fn default_state(&self) -> State {
159        State::new_with_nu(self.nq, self.nv, self.nu(), self.bodies.len())
160    }
161
162    /// Number of actuators (MuJoCo's `nu`).
163    pub fn nu(&self) -> usize {
164        self.actuators.len()
165    }
166
167    /// Map actuator-space controls to generalized forces of dimension `nv`.
168    ///
169    /// Each actuator's control is clamped to its `ctrl_range`, scaled by `gear`,
170    /// and added to the first velocity DOF of the joint it drives.
171    ///
172    /// When the model has no actuators, `ctrl` is interpreted as a raw per-DOF
173    /// generalized force and passed through unchanged — this keeps hand-built
174    /// models (which set `state.ctrl` per DOF) working.
175    pub fn actuator_forces(&self, ctrl: &phyz_math::DVec) -> phyz_math::DVec {
176        let mut qfrc = phyz_math::DVec::zeros(self.nv);
177        if self.actuators.is_empty() {
178            for i in 0..self.nv.min(ctrl.len()) {
179                qfrc[i] = ctrl[i];
180            }
181            return qfrc;
182        }
183        for (a, act) in self.actuators.iter().enumerate() {
184            if a >= ctrl.len() {
185                break;
186            }
187            let Some(&v_idx) = self.v_offsets.get(act.joint_idx) else {
188                continue;
189            };
190            if v_idx < self.nv {
191                qfrc[v_idx] += act.force(ctrl[a]);
192            }
193        }
194        qfrc
195    }
196
197    /// Map actuator-space controls to generalized forces, evaluating the full
198    /// affine actuator law against the joint's current state.
199    ///
200    /// Identical to [`Self::actuator_forces`] for plain motors. Position and
201    /// velocity servos need `q` and `v`, so this is the variant the dynamics
202    /// call; the state-free one remains for callers that only have controls.
203    pub fn actuator_forces_at(
204        &self,
205        ctrl: &phyz_math::DVec,
206        q: &phyz_math::DVec,
207        v: &phyz_math::DVec,
208    ) -> phyz_math::DVec {
209        let mut qfrc = phyz_math::DVec::zeros(self.nv);
210        if self.actuators.is_empty() {
211            for i in 0..self.nv.min(ctrl.len()) {
212                qfrc[i] = ctrl[i];
213            }
214            return qfrc;
215        }
216        for (a, act) in self.actuators.iter().enumerate() {
217            if a >= ctrl.len() {
218                break;
219            }
220            let (Some(&v_idx), Some(&q_idx)) = (
221                self.v_offsets.get(act.joint_idx),
222                self.q_offsets.get(act.joint_idx),
223            ) else {
224                continue;
225            };
226            if v_idx < self.nv && q_idx < q.len() {
227                qfrc[v_idx] += act.force_at(ctrl[a], q[q_idx], v[v_idx]);
228            }
229        }
230        qfrc
231    }
232
233    /// Number of bodies.
234    pub fn nbodies(&self) -> usize {
235        self.bodies.len()
236    }
237
238    /// One [`crate::ContactMaterial`] per body: each body's own material where
239    /// it has one, and `scene` everywhere else.
240    ///
241    /// This is what `phyz_contact::assemble` wants for its `materials`
242    /// argument. Callers used to write `vec![material.clone(); n_bodies]`
243    /// because there was no other way to fill the slice; that expression is
244    /// exactly `model.contact_materials(&material)` on a model whose bodies
245    /// all carry `None`, so switching to this call changes nothing until a
246    /// body is actually given a material.
247    ///
248    /// The result is always at least one element long, since `assemble` treats
249    /// an empty slice as "defaults everywhere".
250    ///
251    /// **Which body carries the material matters.** Friction combines by
252    /// `max` — see `ContactMaterial::combine` — so a grippy body grips
253    /// everything it touches, including surfaces you wanted it to slide on.
254    pub fn contact_materials(&self, scene: &crate::ContactMaterial) -> Vec<crate::ContactMaterial> {
255        if self.bodies.is_empty() {
256            return vec![scene.clone()];
257        }
258        self.bodies
259            .iter()
260            .map(|b| b.material.clone().unwrap_or_else(|| scene.clone()))
261            .collect()
262    }
263
264    /// Attach `material` to the body named `name`, returning whether it
265    /// existed. The ergonomic form of `model.bodies[i].material = Some(..)`
266    /// when you have a name rather than an index — which is the usual case
267    /// after importing a URDF or MJCF.
268    pub fn set_body_material(&mut self, name: &str, material: crate::ContactMaterial) -> bool {
269        match self.body_index(name) {
270            Some(i) => {
271                self.bodies[i].material = Some(material);
272                true
273            }
274            None => false,
275        }
276    }
277
278    /// Look up a body index by name.
279    pub fn body_index(&self, name: &str) -> Option<usize> {
280        self.bodies.iter().position(|b| b.name == name)
281    }
282
283    /// Look up a joint index by name.
284    pub fn joint_index(&self, name: &str) -> Option<usize> {
285        self.joints.iter().position(|j| j.name == name)
286    }
287
288    /// Weld-group id per body: bodies joined only by [`JointType::Fixed`]
289    /// joints share an id, because no configuration can ever move them
290    /// relative to one another.
291    ///
292    /// Used by contact filtering — see [`Model::may_collide`]. A welded pair
293    /// overlaps by construction (that is what a fixed joint usually
294    /// *expresses*: one physical part split across two links), and the
295    /// overlap is constant, so a contact between them is a permanent
296    /// unresolvable penetration rather than an event.
297    ///
298    /// Transitive, so a chain `A —fixed— B —fixed— C` puts all three in one
299    /// group even though `A` and `C` are not parent and child.
300    pub fn weld_groups(&self) -> Vec<usize> {
301        let n = self.bodies.len();
302        let mut group: Vec<usize> = (0..n).collect();
303
304        // Union-find with path halving; the tree is shallow so this is
305        // effectively linear.
306        fn find(group: &mut [usize], mut i: usize) -> usize {
307            while group[i] != i {
308                group[i] = group[group[i]];
309                i = group[i];
310            }
311            i
312        }
313
314        for (i, body) in self.bodies.iter().enumerate() {
315            if body.parent < 0 {
316                continue;
317            }
318            let Some(joint) = self.joints.get(body.joint_idx) else {
319                continue;
320            };
321            if joint.joint_type != JointType::Fixed {
322                continue;
323            }
324            let p = body.parent as usize;
325            if p >= n {
326                continue;
327            }
328            let (a, b) = (find(&mut group, i), find(&mut group, p));
329            if a != b {
330                group[a] = b;
331            }
332        }
333
334        (0..n).map(|i| find(&mut group, i)).collect()
335    }
336
337    /// May bodies `i` and `j` produce a contact with each other?
338    ///
339    /// `welds` is [`Model::weld_groups`], passed in so a detection loop
340    /// computes it once rather than per pair.
341    ///
342    /// Three structural exclusions, none of them tunable, because each one
343    /// describes a pair that is in contact in *every* configuration and so
344    /// carries no information:
345    ///
346    /// 1. **A body with itself.** Two shapes on one link cannot move relative
347    ///    to each other.
348    /// 2. **Parent and child.** The two links a joint connects overlap at the
349    ///    joint by construction — that is what a joint *is*. A humanoid has
350    ///    one such pair per joint, and left unfiltered they are the only
351    ///    contacts the narrow phase ever reports.
352    /// 3. **Welded bodies.** As (2), for links a fixed joint chain has made
353    ///    rigid with respect to each other.
354    ///
355    /// Anything else — a hand reaching its own thigh, an arm crossing the
356    /// trunk — is a real event and is allowed through. Models needing a
357    /// further exclusion (geometry that overlaps across a *moving* joint, a
358    /// common URDF authoring artifact) list it in [`Model::contact_exclude`].
359    pub fn may_collide(&self, i: usize, j: usize, welds: &[usize]) -> bool {
360        if i == j {
361            return false;
362        }
363        if welds.get(i).is_some() && welds.get(i) == welds.get(j) {
364            return false;
365        }
366        let parent_child = self.bodies.get(i).map(|b| b.parent) == Some(j as i32)
367            || self.bodies.get(j).map(|b| b.parent) == Some(i as i32);
368        if parent_child {
369            return false;
370        }
371        let (lo, hi) = if i < j { (i, j) } else { (j, i) };
372        !self
373            .contact_exclude
374            .iter()
375            .any(|&(a, b)| (a.min(b), a.max(b)) == (lo, hi))
376    }
377}
378
379/// Builder for constructing models.
380pub struct ModelBuilder {
381    bodies: Vec<Body>,
382    joints: Vec<Joint>,
383    gravity: Vec3,
384    dt: f64,
385}
386
387impl ModelBuilder {
388    /// Start building a new model.
389    pub fn new() -> Self {
390        Self {
391            bodies: Vec::new(),
392            joints: Vec::new(),
393            gravity: Vec3::new(0.0, 0.0, -GRAVITY),
394            dt: 0.001,
395        }
396    }
397
398    /// Set the gravity vector.
399    pub fn gravity(mut self, g: Vec3) -> Self {
400        self.gravity = g;
401        self
402    }
403
404    /// Set the timestep.
405    pub fn dt(mut self, dt: f64) -> Self {
406        self.dt = dt;
407        self
408    }
409
410    /// Add a body with a revolute joint attached to the given parent.
411    ///
412    /// `parent` is the index of the parent body, or -1 for world.
413    /// `parent_to_joint` is the transform from parent body frame to joint frame.
414    /// `inertia` is the body's spatial inertia in its own frame.
415    pub fn add_revolute_body(
416        mut self,
417        name: &str,
418        parent: i32,
419        parent_to_joint: SpatialTransform,
420        inertia: SpatialInertia,
421    ) -> Self {
422        let joint_idx = self.joints.len();
423        self.joints.push(Joint::revolute(parent_to_joint));
424        self.bodies
425            .push(Body::new(name, inertia, parent, joint_idx));
426        self
427    }
428
429    /// Add a body with a prismatic joint attached to the given parent.
430    pub fn add_prismatic_body(
431        mut self,
432        name: &str,
433        parent: i32,
434        parent_to_joint: SpatialTransform,
435        axis: Vec3,
436        inertia: SpatialInertia,
437    ) -> Self {
438        let joint_idx = self.joints.len();
439        self.joints.push(Joint::prismatic(parent_to_joint, axis));
440        self.bodies
441            .push(Body::new(name, inertia, parent, joint_idx));
442        self
443    }
444
445    /// Add a body with a spherical (ball) joint attached to the given parent.
446    pub fn add_spherical_body(
447        mut self,
448        name: &str,
449        parent: i32,
450        parent_to_joint: SpatialTransform,
451        inertia: SpatialInertia,
452    ) -> Self {
453        let joint_idx = self.joints.len();
454        self.joints.push(Joint::spherical(parent_to_joint));
455        self.bodies
456            .push(Body::new(name, inertia, parent, joint_idx));
457        self
458    }
459
460    /// Add a body with a free joint (6 DOF) attached to the given parent.
461    pub fn add_free_body(
462        mut self,
463        name: &str,
464        parent: i32,
465        parent_to_joint: SpatialTransform,
466        inertia: SpatialInertia,
467    ) -> Self {
468        let joint_idx = self.joints.len();
469        self.joints.push(Joint::free(parent_to_joint));
470        self.bodies
471            .push(Body::new(name, inertia, parent, joint_idx));
472        self
473    }
474
475    /// Add a body with a fixed joint (0 DOF) attached to the given parent.
476    pub fn add_fixed_body(
477        mut self,
478        name: &str,
479        parent: i32,
480        parent_to_joint: SpatialTransform,
481        inertia: SpatialInertia,
482    ) -> Self {
483        let joint_idx = self.joints.len();
484        self.joints.push(Joint::fixed(parent_to_joint));
485        self.bodies
486            .push(Body::new(name, inertia, parent, joint_idx));
487        self
488    }
489
490    /// Add a generic joint and body.
491    pub fn add_body(
492        mut self,
493        name: &str,
494        parent: i32,
495        joint: Joint,
496        inertia: SpatialInertia,
497    ) -> Self {
498        let joint_idx = self.joints.len();
499        self.joints.push(joint);
500        self.bodies
501            .push(Body::new(name, inertia, parent, joint_idx));
502        self
503    }
504
505    /// Add a free body with collision geometry (useful for dropping objects).
506    pub fn add_free_body_with_geometry(
507        mut self,
508        name: &str,
509        parent: i32,
510        parent_to_joint: SpatialTransform,
511        inertia: SpatialInertia,
512        geometry: crate::Body,
513    ) -> Self {
514        let joint_idx = self.joints.len();
515        self.joints.push(Joint::free(parent_to_joint));
516        let mut body = Body::new(name, inertia, parent, joint_idx);
517        body.geometry = geometry.geometry;
518        body.collisions = geometry.collisions;
519        body.visuals = geometry.visuals;
520        self.bodies.push(body);
521        self
522    }
523
524    /// Build the model.
525    pub fn build(self) -> Model {
526        let mut nq = 0;
527        let mut nv = 0;
528        let mut q_offsets = Vec::new();
529        let mut v_offsets = Vec::new();
530
531        for joint in &self.joints {
532            q_offsets.push(nq);
533            v_offsets.push(nv);
534            nq += joint.ndof();
535            nv += joint.ndof();
536        }
537
538        Model {
539            bodies: self.bodies,
540            joints: self.joints,
541            gravity: self.gravity,
542            dt: self.dt,
543            nq,
544            nv,
545            q_offsets,
546            v_offsets,
547            actuators: Vec::new(),
548            contact_exclude: Vec::new(),
549        }
550    }
551}
552
553impl Default for ModelBuilder {
554    fn default() -> Self {
555        Self::new()
556    }
557}