1use phyz_math::{SpatialVec, Vec3};
4use phyz_model::{Model, State};
5
6#[derive(Debug, Clone)]
10pub struct Tendon {
11 pub path: Vec<usize>,
13 pub stiffness: f64,
15 pub rest_length: f64,
17 pub max_force: f64,
19 pub damping: f64,
21}
22
23impl Tendon {
24 pub fn new(path: Vec<usize>, stiffness: f64, rest_length: f64, max_force: f64) -> Self {
26 Self {
27 path,
28 stiffness,
29 rest_length,
30 max_force,
31 damping: 0.0,
32 }
33 }
34
35 pub fn with_damping(mut self, damping: f64) -> Self {
37 self.damping = damping;
38 self
39 }
40
41 pub fn current_length(&self, state: &State) -> f64 {
45 let mut length = 0.0;
46
47 for i in 0..self.path.len().saturating_sub(1) {
48 let body_a = self.path[i];
49 let body_b = self.path[i + 1];
50
51 if body_a < state.body_xform.len() && body_b < state.body_xform.len() {
52 let pos_a = state.body_xform[body_a].pos;
53 let pos_b = state.body_xform[body_b].pos;
54 length += (pos_b - pos_a).norm();
55 }
56 }
57
58 length
59 }
60
61 pub fn current_length_rate(&self, _state: &State) -> f64 {
66 0.0 }
68
69 pub fn compute_forces(&self, state: &State) -> Vec<(usize, SpatialVec)> {
74 let current_len = self.current_length(state);
75 let stretch = current_len - self.rest_length;
76
77 let length_rate = self.current_length_rate(state);
79 let mut force_magnitude = -self.stiffness * stretch - self.damping * length_rate;
80
81 if force_magnitude.abs() > self.max_force {
83 force_magnitude = force_magnitude.signum() * self.max_force;
84 }
85
86 let mut forces = Vec::new();
88
89 for i in 0..self.path.len().saturating_sub(1) {
90 let body_a = self.path[i];
91 let body_b = self.path[i + 1];
92
93 if body_a < state.body_xform.len() && body_b < state.body_xform.len() {
94 let pos_a = state.body_xform[body_a].pos;
95 let pos_b = state.body_xform[body_b].pos;
96
97 let direction = (pos_b - pos_a).normalize();
98 let force_vec = direction * force_magnitude;
99
100 forces.push((body_a, SpatialVec::new(Vec3::zeros(), force_vec)));
102
103 forces.push((body_b, SpatialVec::new(Vec3::zeros(), -force_vec)));
105 }
106 }
107
108 forces
109 }
110
111 pub fn is_valid(&self, model: &Model) -> bool {
113 if self.path.len() < 2 {
114 return false;
115 }
116
117 for &body_idx in &self.path {
118 if body_idx >= model.nbodies() {
119 return false;
120 }
121 }
122
123 true
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use phyz_math::{SpatialInertia, SpatialTransform, Vec3};
131 use phyz_model::ModelBuilder;
132
133 #[test]
134 fn test_tendon_length() {
135 let model = ModelBuilder::new()
137 .add_revolute_body(
138 "link1",
139 -1,
140 SpatialTransform::identity(),
141 SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
142 )
143 .add_revolute_body(
144 "link2",
145 0,
146 SpatialTransform::translation(Vec3::new(0.0, 0.0, -1.0)),
147 SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
148 )
149 .build();
150
151 let mut state = model.default_state();
152
153 state.body_xform[0] = SpatialTransform::translation(Vec3::new(0.0, 0.0, 0.0));
155 state.body_xform[1] = SpatialTransform::translation(Vec3::new(0.0, 0.0, -1.0));
156
157 let tendon = Tendon::new(vec![0, 1], 100.0, 0.5, 10.0);
158 let length = tendon.current_length(&state);
159
160 assert!((length - 1.0).abs() < 1e-10);
162 }
163
164 #[test]
165 fn test_tendon_forces() {
166 let model = ModelBuilder::new()
167 .add_revolute_body(
168 "link1",
169 -1,
170 SpatialTransform::identity(),
171 SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
172 )
173 .add_revolute_body(
174 "link2",
175 0,
176 SpatialTransform::translation(Vec3::new(0.0, 0.0, -1.0)),
177 SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
178 )
179 .build();
180
181 let mut state = model.default_state();
182 state.body_xform[0] = SpatialTransform::translation(Vec3::new(0.0, 0.0, 0.0));
183 state.body_xform[1] = SpatialTransform::translation(Vec3::new(0.0, 0.0, -1.5));
184
185 let tendon = Tendon::new(vec![0, 1], 100.0, 1.0, 1000.0);
187 let forces = tendon.compute_forces(&state);
188
189 assert_eq!(forces.len(), 2);
191
192 let force_mag = forces[0].1.linear().norm();
194 assert!((force_mag - 50.0).abs() < 1e-6);
195 }
196
197 #[test]
198 fn test_tendon_force_saturation() {
199 let model = ModelBuilder::new()
200 .add_revolute_body(
201 "link1",
202 -1,
203 SpatialTransform::identity(),
204 SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
205 )
206 .add_revolute_body(
207 "link2",
208 0,
209 SpatialTransform::translation(Vec3::new(0.0, 0.0, -1.0)),
210 SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
211 )
212 .build();
213
214 let mut state = model.default_state();
215 state.body_xform[0] = SpatialTransform::translation(Vec3::new(0.0, 0.0, 0.0));
216 state.body_xform[1] = SpatialTransform::translation(Vec3::new(0.0, 0.0, -2.0));
217
218 let tendon = Tendon::new(vec![0, 1], 100.0, 1.0, 10.0);
221 let forces = tendon.compute_forces(&state);
222
223 let force_mag = forces[0].1.linear().norm();
224 assert!((force_mag - 10.0).abs() < 1e-6);
225 }
226
227 #[test]
228 fn test_tendon_validation() {
229 let model = ModelBuilder::new()
230 .add_revolute_body(
231 "link",
232 -1,
233 SpatialTransform::identity(),
234 SpatialInertia::point_mass(1.0, Vec3::new(0.0, 0.0, -0.5)),
235 )
236 .build();
237
238 let valid_tendon = Tendon::new(vec![0], 100.0, 1.0, 10.0);
239 assert!(!valid_tendon.is_valid(&model)); let invalid_tendon = Tendon::new(vec![0, 99], 100.0, 1.0, 10.0);
242 assert!(!invalid_tendon.is_valid(&model)); }
244}