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}