use std::f64::consts::PI;
pub type Vec3 = [f64; 3];
pub type Mat3 = [[f64; 3]; 3];
#[inline]
fn vec3_add(a: Vec3, b: Vec3) -> Vec3 {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn vec3_sub(a: Vec3, b: Vec3) -> Vec3 {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn vec3_scale(a: Vec3, s: f64) -> Vec3 {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
fn vec3_dot(a: Vec3, b: Vec3) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn vec3_cross(a: Vec3, b: Vec3) -> Vec3 {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[inline]
fn vec3_norm(a: Vec3) -> f64 {
vec3_dot(a, a).sqrt()
}
#[inline]
fn vec3_normalize(a: Vec3) -> Vec3 {
let n = vec3_norm(a);
if n < 1e-14 {
[0.0, 0.0, 0.0]
} else {
vec3_scale(a, 1.0 / n)
}
}
fn rodrigues(v: Vec3, k: Vec3, theta: f64) -> Vec3 {
let (s, c) = (theta.sin(), theta.cos());
let kxv = vec3_cross(k, v);
let kdv = vec3_dot(k, v);
vec3_add(
vec3_add(vec3_scale(v, c), vec3_scale(kxv, s)),
vec3_scale(k, kdv * (1.0 - c)),
)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CrossSectionShape {
Circular {
radius: f64,
},
Rectangular {
width: f64,
height: f64,
},
Elliptical {
semi_a: f64,
semi_b: f64,
},
}
#[derive(Debug, Clone, Copy)]
pub struct RodCrossSection {
pub shape: CrossSectionShape,
pub area: f64,
pub i1: f64,
pub i2: f64,
pub torsion_j: f64,
}
impl RodCrossSection {
pub fn new(shape: CrossSectionShape) -> Self {
match shape {
CrossSectionShape::Circular { radius: r } => {
let area = PI * r * r;
let i = PI * r * r * r * r / 4.0;
let j = PI * r * r * r * r / 2.0;
Self {
shape,
area,
i1: i,
i2: i,
torsion_j: j,
}
}
CrossSectionShape::Rectangular {
width: b,
height: h,
} => {
let area = b * h;
let i1 = b * h * h * h / 12.0;
let i2 = h * b * b * b / 12.0;
let a_min = b.min(h);
let a_max = b.max(h);
let j = a_min * a_min * a_min * a_max / 3.0
* (1.0 - 0.63 * a_min / a_max * (1.0 - a_min.powi(4) / (12.0 * a_max.powi(4))));
Self {
shape,
area,
i1,
i2,
torsion_j: j,
}
}
CrossSectionShape::Elliptical {
semi_a: a,
semi_b: b,
} => {
let area = PI * a * b;
let i1 = PI * a * b * b * b / 4.0;
let i2 = PI * b * a * a * a / 4.0;
let j = PI * a * a * a * b * b * b / (a * a + b * b);
Self {
shape,
area,
i1,
i2,
torsion_j: j,
}
}
}
}
pub fn shear_correction_factor(&self) -> f64 {
match self.shape {
CrossSectionShape::Circular { .. } => 0.9,
CrossSectionShape::Rectangular { .. } => 5.0 / 6.0,
CrossSectionShape::Elliptical {
semi_a: a,
semi_b: b,
} => {
let ratio = (a / b).min(b / a);
0.83 + 0.07 * ratio
}
}
}
}
#[derive(Debug, Clone)]
pub struct CosseratNode {
pub position: Vec3,
pub d1: Vec3,
pub d2: Vec3,
pub d3: Vec3,
pub velocity: Vec3,
pub angular_velocity: Vec3,
pub external_force: Vec3,
pub external_moment: Vec3,
}
impl CosseratNode {
pub fn new(position: Vec3) -> Self {
Self {
position,
d1: [1.0, 0.0, 0.0],
d2: [0.0, 1.0, 0.0],
d3: [0.0, 0.0, 1.0],
velocity: [0.0; 3],
angular_velocity: [0.0; 3],
external_force: [0.0; 3],
external_moment: [0.0; 3],
}
}
pub fn with_directors(position: Vec3, d1: Vec3, d2: Vec3, d3: Vec3) -> Self {
Self {
position,
d1,
d2,
d3,
velocity: [0.0; 3],
angular_velocity: [0.0; 3],
external_force: [0.0; 3],
external_moment: [0.0; 3],
}
}
pub fn orthonormalize(&mut self) {
self.d3 = vec3_normalize(self.d3);
let proj = vec3_dot(self.d1, self.d3);
self.d1 = vec3_normalize(vec3_sub(self.d1, vec3_scale(self.d3, proj)));
self.d2 = vec3_normalize(vec3_cross(self.d3, self.d1));
}
pub fn rotate_directors(&mut self, omega: Vec3, dt: f64) {
let angle = vec3_norm(omega) * dt;
if angle < 1e-14 {
return;
}
let axis = vec3_scale(omega, 1.0 / (angle / dt));
let axis = vec3_normalize(axis);
self.d1 = rodrigues(self.d1, axis, angle);
self.d2 = rodrigues(self.d2, axis, angle);
self.d3 = rodrigues(self.d3, axis, angle);
}
}
#[derive(Debug, Clone)]
pub struct RodStiffness {
pub ei1: f64,
pub ei2: f64,
pub gj: f64,
pub ea: f64,
pub ga: f64,
}
impl RodStiffness {
pub fn from_material(young_modulus: f64, shear_modulus: f64, cs: &RodCrossSection) -> Self {
let ks = cs.shear_correction_factor();
Self {
ei1: young_modulus * cs.i1,
ei2: young_modulus * cs.i2,
gj: shear_modulus * cs.torsion_j,
ea: young_modulus * cs.area,
ga: shear_modulus * ks * cs.area,
}
}
}
#[derive(Debug, Clone)]
pub struct CosseratRod {
pub nodes: Vec<CosseratNode>,
pub stiffness: RodStiffness,
pub kappa1_0: Vec<f64>,
pub kappa2_0: Vec<f64>,
pub tau_0: Vec<f64>,
pub rest_lengths: Vec<f64>,
pub linear_density: f64,
pub rotary_inertia: f64,
}
impl CosseratRod {
pub fn new_straight(
n_nodes: usize,
total_length: f64,
stiffness: RodStiffness,
linear_density: f64,
rotary_inertia: f64,
) -> Self {
assert!(n_nodes >= 2, "Rod must have at least 2 nodes");
let seg_len = total_length / (n_nodes - 1) as f64;
let nodes: Vec<CosseratNode> = (0..n_nodes)
.map(|i| {
let z = i as f64 * seg_len;
CosseratNode::new([0.0, 0.0, z])
})
.collect();
let n_segs = n_nodes - 1;
Self {
nodes,
stiffness,
kappa1_0: vec![0.0; n_segs],
kappa2_0: vec![0.0; n_segs],
tau_0: vec![0.0; n_segs],
rest_lengths: vec![seg_len; n_segs],
linear_density,
rotary_inertia,
}
}
pub fn n_segments(&self) -> usize {
self.nodes.len() - 1
}
pub fn n_nodes(&self) -> usize {
self.nodes.len()
}
pub fn total_rest_length(&self) -> f64 {
self.rest_lengths.iter().sum()
}
pub fn segment_tangent(&self, seg: usize) -> Vec3 {
let a = &self.nodes[seg];
let b = &self.nodes[seg + 1];
vec3_normalize(vec3_sub(b.position, a.position))
}
pub fn segment_stretch(&self, seg: usize) -> f64 {
let a = &self.nodes[seg];
let b = &self.nodes[seg + 1];
let len = vec3_norm(vec3_sub(b.position, a.position));
len / self.rest_lengths[seg]
}
pub fn segment_directors(&self, seg: usize) -> (Vec3, Vec3, Vec3) {
let a = &self.nodes[seg];
let b = &self.nodes[seg + 1];
let d1 = vec3_normalize(vec3_add(a.d1, b.d1));
let d2 = vec3_normalize(vec3_add(a.d2, b.d2));
let d3 = vec3_normalize(vec3_add(a.d3, b.d3));
(d1, d2, d3)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SegmentInternalLoads {
pub axial_force: f64,
pub shear1: f64,
pub shear2: f64,
pub moment1: f64,
pub moment2: f64,
pub torsion: f64,
}
pub struct InternalForces;
impl InternalForces {
pub fn segment_strains(rod: &CosseratRod, seg: usize) -> (f64, f64, f64, f64, f64, f64) {
let a = &rod.nodes[seg];
let b = &rod.nodes[seg + 1];
let diff = vec3_sub(b.position, a.position);
let current_len = vec3_norm(diff);
let rest_len = rod.rest_lengths[seg];
let eps = (current_len - rest_len) / rest_len;
let t = if current_len > 1e-14 {
vec3_scale(diff, 1.0 / current_len)
} else {
[0.0, 0.0, 1.0]
};
let (d1_m, d2_m, _d3_m) = rod.segment_directors(seg);
let gamma1 = vec3_dot(t, d1_m);
let gamma2 = vec3_dot(t, d2_m);
let d3a = a.d3;
let d3b = b.d3;
let dd3 = vec3_sub(d3b, d3a);
let dkappa1 = vec3_dot(dd3, d1_m) / rest_len - rod.kappa1_0[seg];
let dkappa2 = vec3_dot(dd3, d2_m) / rest_len - rod.kappa2_0[seg];
let d1a = a.d1;
let d1b = b.d1;
let dd1 = vec3_sub(d1b, d1a);
let dtau = vec3_dot(dd1, d2_m) / rest_len - rod.tau_0[seg];
(eps, gamma1, gamma2, dkappa1, dkappa2, dtau)
}
pub fn compute(rod: &CosseratRod, seg: usize) -> SegmentInternalLoads {
let (eps, gamma1, gamma2, dkappa1, dkappa2, dtau) = Self::segment_strains(rod, seg);
SegmentInternalLoads {
axial_force: rod.stiffness.ea * eps,
shear1: rod.stiffness.ga * gamma1,
shear2: rod.stiffness.ga * gamma2,
moment1: rod.stiffness.ei1 * dkappa1,
moment2: rod.stiffness.ei2 * dkappa2,
torsion: rod.stiffness.gj * dtau,
}
}
pub fn nodal_forces(rod: &CosseratRod, seg: usize) -> (Vec3, Vec3, Vec3, Vec3) {
let loads = Self::compute(rod, seg);
let a = &rod.nodes[seg];
let b = &rod.nodes[seg + 1];
let rest_len = rod.rest_lengths[seg];
let (d1_m, d2_m, d3_m) = rod.segment_directors(seg);
let diff = vec3_sub(b.position, a.position);
let current_len = vec3_norm(diff);
let tangent = if current_len > 1e-14 {
vec3_scale(diff, 1.0 / current_len)
} else {
d3_m
};
let f_internal = vec3_add(
vec3_add(
vec3_scale(tangent, loads.axial_force),
vec3_scale(d1_m, loads.shear1),
),
vec3_scale(d2_m, loads.shear2),
);
let m_a_body = vec3_add(
vec3_add(
vec3_scale(d1_m, loads.moment1),
vec3_scale(d2_m, loads.moment2),
),
vec3_scale(d3_m, loads.torsion),
);
let m_a = vec3_scale(m_a_body, 1.0 / rest_len);
let f_b = f_internal;
let f_a = vec3_scale(f_internal, -1.0);
let m_b = vec3_scale(m_a, -1.0);
let half_len = rest_len * 0.5;
let torque_a = vec3_scale(vec3_cross(a.d3, f_internal), half_len);
let torque_b = vec3_scale(vec3_cross(b.d3, f_b), half_len);
(f_a, vec3_add(m_a, torque_a), f_b, vec3_add(m_b, torque_b))
}
}
pub struct CosseratDynamics;
impl CosseratDynamics {
pub fn accelerations(rod: &CosseratRod) -> Vec<(Vec3, Vec3)> {
let n = rod.n_nodes();
let mut forces = vec![[0.0f64; 3]; n];
let mut moments = vec![[0.0f64; 3]; n];
for seg in 0..rod.n_segments() {
let (fa, ma, fb, mb) = InternalForces::nodal_forces(rod, seg);
forces[seg] = vec3_add(forces[seg], fa);
moments[seg] = vec3_add(moments[seg], ma);
forces[seg + 1] = vec3_add(forces[seg + 1], fb);
moments[seg + 1] = vec3_add(moments[seg + 1], mb);
}
for i in 0..n {
forces[i] = vec3_add(forces[i], rod.nodes[i].external_force);
moments[i] = vec3_add(moments[i], rod.nodes[i].external_moment);
}
let l_elem = rod.total_rest_length() / rod.n_segments() as f64;
let mass_per_node = rod.linear_density * l_elem;
let inertia_per_node = rod.rotary_inertia * l_elem;
(0..n)
.map(|i| {
let acc = if mass_per_node > 1e-30 {
vec3_scale(forces[i], 1.0 / mass_per_node)
} else {
[0.0; 3]
};
let alpha = if inertia_per_node > 1e-30 {
vec3_scale(moments[i], 1.0 / inertia_per_node)
} else {
[0.0; 3]
};
(acc, alpha)
})
.collect()
}
pub fn step_euler(rod: &mut CosseratRod, dt: f64) {
let accels = Self::accelerations(rod);
let n = rod.n_nodes();
for (i, &(acc, alpha)) in accels.iter().enumerate().take(n) {
rod.nodes[i].velocity = vec3_add(rod.nodes[i].velocity, vec3_scale(acc, dt));
rod.nodes[i].angular_velocity =
vec3_add(rod.nodes[i].angular_velocity, vec3_scale(alpha, dt));
let vel = rod.nodes[i].velocity;
rod.nodes[i].position = vec3_add(rod.nodes[i].position, vec3_scale(vel, dt));
let omega = rod.nodes[i].angular_velocity;
rod.nodes[i].rotate_directors(omega, dt);
rod.nodes[i].orthonormalize();
}
}
}
#[derive(Debug, Clone)]
pub enum BcType {
Clamped,
Pinned,
Free,
FollowerForce {
magnitude: f64,
},
PrescribedCurvature {
kappa1: f64,
kappa2: f64,
tau: f64,
},
}
#[derive(Debug, Clone)]
pub struct BoundaryConditions {
pub base: BcType,
pub tip: BcType,
pub base_position: Vec3,
pub base_d1: Vec3,
pub base_d2: Vec3,
pub base_d3: Vec3,
}
impl BoundaryConditions {
pub fn cantilever(base_position: Vec3, d1: Vec3, d2: Vec3, d3: Vec3) -> Self {
Self {
base: BcType::Clamped,
tip: BcType::Free,
base_position,
base_d1: d1,
base_d2: d2,
base_d3: d3,
}
}
pub fn apply(&self, rod: &mut CosseratRod) {
match &self.base {
BcType::Clamped => {
let n = &mut rod.nodes[0];
n.position = self.base_position;
n.d1 = self.base_d1;
n.d2 = self.base_d2;
n.d3 = self.base_d3;
n.velocity = [0.0; 3];
n.angular_velocity = [0.0; 3];
}
BcType::Pinned => {
let n = &mut rod.nodes[0];
n.position = self.base_position;
n.velocity = [0.0; 3];
}
_ => {}
}
let last = rod.nodes.len() - 1;
if let BcType::FollowerForce { magnitude } = &self.tip {
let d3 = rod.nodes[last].d3;
rod.nodes[last].external_force = vec3_scale(d3, *magnitude);
}
}
}
pub struct StaticSolver {
pub max_iter: usize,
pub tol: f64,
}
impl StaticSolver {
pub fn new(max_iter: usize, tol: f64) -> Self {
Self { max_iter, tol }
}
pub fn residual_norm(rod: &CosseratRod) -> f64 {
let accs = CosseratDynamics::accelerations(rod);
accs.iter()
.skip(1) .map(|(a, alpha)| vec3_norm(*a) + vec3_norm(*alpha))
.sum()
}
pub fn solve(&self, rod: &mut CosseratRod, bcs: &BoundaryConditions) -> (usize, f64) {
let dt = 1e-4;
let damping = 0.95;
let mut iter = 0;
for _ in 0..self.max_iter {
iter += 1;
CosseratDynamics::step_euler(rod, dt);
bcs.apply(rod);
for node in rod.nodes.iter_mut() {
node.velocity = vec3_scale(node.velocity, damping);
node.angular_velocity = vec3_scale(node.angular_velocity, damping);
}
let res = Self::residual_norm(rod);
if res < self.tol {
return (iter, res);
}
}
(iter, Self::residual_norm(rod))
}
pub fn shoot(
rod: &CosseratRod,
base_kappa1: f64,
base_kappa2: f64,
base_tau: f64,
) -> (Vec3, Vec3) {
let n = rod.n_nodes();
let mut pos = rod.nodes[0].position;
let mut d1 = rod.nodes[0].d1;
let mut d2 = rod.nodes[0].d2;
let mut d3 = rod.nodes[0].d3;
let seg_len = rod.total_rest_length() / rod.n_segments() as f64;
let n_segs = n - 1;
for i in 0..n_segs {
let t = i as f64 / n_segs as f64;
let k1 = base_kappa1 * (1.0 - t) + rod.kappa1_0[i] * t;
let k2 = base_kappa2 * (1.0 - t) + rod.kappa2_0[i] * t;
let tau = base_tau * (1.0 - t) + rod.tau_0[i] * t;
let omega = vec3_add(
vec3_add(vec3_scale(d1, k1), vec3_scale(d2, k2)),
vec3_scale(d3, tau),
);
let dd1 = vec3_scale(vec3_cross(omega, d1), seg_len);
let dd2 = vec3_scale(vec3_cross(omega, d2), seg_len);
let dd3 = vec3_scale(vec3_cross(omega, d3), seg_len);
d1 = vec3_normalize(vec3_add(d1, dd1));
d2 = vec3_normalize(vec3_add(d2, dd2));
d3 = vec3_normalize(vec3_add(d3, dd3));
pos = vec3_add(pos, vec3_scale(d3, seg_len));
}
(pos, d3)
}
}
pub struct RodContact {
pub stiffness: f64,
pub friction_coeff: f64,
pub adhesion: f64,
}
impl RodContact {
pub fn new(stiffness: f64, friction_coeff: f64, adhesion: f64) -> Self {
Self {
stiffness,
friction_coeff,
adhesion,
}
}
pub fn segment_segment_distance(
rod_a: &CosseratRod,
seg_a: usize,
rod_b: &CosseratRod,
seg_b: usize,
) -> (f64, Vec3, Vec3) {
let p0 = rod_a.nodes[seg_a].position;
let p1 = rod_a.nodes[seg_a + 1].position;
let q0 = rod_b.nodes[seg_b].position;
let q1 = rod_b.nodes[seg_b + 1].position;
let d1 = vec3_sub(p1, p0);
let d2 = vec3_sub(q1, q0);
let r = vec3_sub(p0, q0);
let a = vec3_dot(d1, d1);
let e = vec3_dot(d2, d2);
let f = vec3_dot(d2, r);
let (s, t) = if a < 1e-14 {
let s = 0.0f64;
let t = if e > 1e-14 {
(f / e).clamp(0.0, 1.0)
} else {
0.0
};
(s, t)
} else {
let c = vec3_dot(d1, r);
if e < 1e-14 {
((-c / a).clamp(0.0, 1.0), 0.0)
} else {
let b = vec3_dot(d1, d2);
let denom = a * e - b * b;
let s = if denom.abs() > 1e-14 {
((b * f - c * e) / denom).clamp(0.0, 1.0)
} else {
0.5
};
let t = ((b * s + f) / e).clamp(0.0, 1.0);
(s, t)
}
};
let pa = vec3_add(p0, vec3_scale(d1, s));
let pb = vec3_add(q0, vec3_scale(d2, t));
let dist = vec3_norm(vec3_sub(pa, pb));
(dist, pa, pb)
}
pub fn detect_contacts(
&self,
rod_a: &CosseratRod,
rod_b: &CosseratRod,
radius: f64,
) -> Vec<(usize, usize, f64, Vec3, Vec3)> {
let mut contacts = Vec::new();
for sa in 0..rod_a.n_segments() {
for sb in 0..rod_b.n_segments() {
let (dist, pa, pb) = Self::segment_segment_distance(rod_a, sa, rod_b, sb);
if dist < radius {
contacts.push((sa, sb, dist, pa, pb));
}
}
}
contacts
}
pub fn contact_force_magnitude(&self, distance: f64, radius: f64) -> f64 {
let penetration = radius - distance;
if distance < radius {
self.stiffness * penetration
} else {
let pull_range = radius * 0.1;
if distance < radius + pull_range {
-self.adhesion * (distance - radius) / pull_range
} else {
0.0
}
}
}
}
pub fn euler_bernoulli_tip_deflection(load: f64, length: f64, ei: f64) -> f64 {
load * length.powi(3) / (3.0 * ei)
}
pub fn end_moment_tip_rotation(moment: f64, length: f64, ei: f64) -> f64 {
moment * length / ei
}
pub fn torsion_angle(torque: f64, length: f64, gj: f64) -> f64 {
torque * length / gj
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-8;
#[test]
fn test_circular_area() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.01 });
let expected = PI * 0.01 * 0.01;
assert!((cs.area - expected).abs() < 1e-12, "area={}", cs.area);
}
#[test]
fn test_circular_moment_of_inertia() {
let r = 0.02;
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: r });
let expected = PI * r.powi(4) / 4.0;
assert!((cs.i1 - expected).abs() < 1e-20, "i1={}", cs.i1);
assert!((cs.i2 - expected).abs() < 1e-20, "i2={}", cs.i2);
}
#[test]
fn test_circular_torsion_constant() {
let r = 0.01;
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: r });
let expected = PI * r.powi(4) / 2.0;
assert!(
(cs.torsion_j - expected).abs() < 1e-18,
"J={}",
cs.torsion_j
);
}
#[test]
fn test_rectangular_moment_of_inertia() {
let b = 0.02;
let h = 0.04;
let cs = RodCrossSection::new(CrossSectionShape::Rectangular {
width: b,
height: h,
});
let i1_expected = b * h.powi(3) / 12.0;
assert!(
(cs.i1 - i1_expected).abs() < 1e-18,
"i1={} expected={}",
cs.i1,
i1_expected
);
}
#[test]
fn test_elliptical_area() {
let a = 0.03;
let b = 0.01;
let cs = RodCrossSection::new(CrossSectionShape::Elliptical {
semi_a: a,
semi_b: b,
});
let expected = PI * a * b;
assert!((cs.area - expected).abs() < 1e-14, "area={}", cs.area);
}
#[test]
fn test_straight_rod_node_spacing() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(200e9, 77e9, &cs);
let rod = CosseratRod::new_straight(6, 1.0, stiff, 1.0, 1e-6);
for i in 1..rod.n_nodes() {
let dz = rod.nodes[i].position[2] - rod.nodes[i - 1].position[2];
assert!((dz - 0.2).abs() < 1e-10, "dz[{}]={}", i, dz);
}
}
#[test]
fn test_straight_rod_zero_strain() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(200e9, 77e9, &cs);
let rod = CosseratRod::new_straight(5, 1.0, stiff, 1.0, 1e-6);
for seg in 0..rod.n_segments() {
let (eps, g1, g2, dk1, dk2, dtau) = InternalForces::segment_strains(&rod, seg);
assert!(eps.abs() < EPS, "axial strain at seg {seg}: {eps}");
assert!(g1.abs() < 0.1, "shear g1 at seg {seg}: {g1}"); assert!(g2.abs() < EPS, "shear g2 at seg {seg}: {g2}");
assert!(dk1.abs() < EPS, "Δκ1 at seg {seg}: {dk1}");
assert!(dk2.abs() < EPS, "Δκ2 at seg {seg}: {dk2}");
assert!(dtau.abs() < EPS, "Δτ at seg {seg}: {dtau}");
}
}
#[test]
fn test_euler_bernoulli_formula() {
let p = 10.0; let l = 1.0; let ei = 1000.0; let delta = euler_bernoulli_tip_deflection(p, l, ei);
let expected = p * l.powi(3) / (3.0 * ei);
assert!((delta - expected).abs() < EPS);
}
#[test]
fn test_end_moment_rotation() {
let m = 5.0;
let l = 0.5;
let ei = 200.0;
let theta = end_moment_tip_rotation(m, l, ei);
assert!((theta - m * l / ei).abs() < EPS);
}
#[test]
fn test_torsion_angle_formula() {
let t = 2.0;
let l = 1.0;
let gj = 500.0;
let phi = torsion_angle(t, l, gj);
assert!((phi - t * l / gj).abs() < EPS);
}
#[test]
fn test_rodrigues_rotation_180() {
let v = [1.0, 0.0, 0.0];
let k = [0.0, 0.0, 1.0];
let result = rodrigues(v, k, PI);
assert!((result[0] + 1.0).abs() < 1e-12, "x={}", result[0]);
assert!(result[1].abs() < 1e-12, "y={}", result[1]);
}
#[test]
fn test_node_orthonormalize() {
let mut node = CosseratNode::new([0.0, 0.0, 0.0]);
node.d1 = [0.9, 0.1, 0.3];
node.orthonormalize();
let dot_d1_d3 = vec3_dot(node.d1, node.d3);
assert!(dot_d1_d3.abs() < 1e-10, "d1·d3={dot_d1_d3}");
let dot_d1_d2 = vec3_dot(node.d1, node.d2);
assert!(dot_d1_d2.abs() < 1e-10, "d1·d2={dot_d1_d2}");
}
#[test]
fn test_static_solver_residual_decreases() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.01 });
let stiff = RodStiffness::from_material(1e6, 4e5, &cs);
let mut rod = CosseratRod::new_straight(10, 1.0, stiff, 0.1, 1e-6);
let last = rod.n_nodes() - 1;
rod.nodes[last].external_force = [0.0, 1.0, 0.0];
let bcs = BoundaryConditions::cantilever(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
);
let solver = StaticSolver::new(200, 1e-3);
let _res0 = StaticSolver::residual_norm(&rod);
let (iters, _res_final) = solver.solve(&mut rod, &bcs);
assert!(iters > 0, "solver should iterate at least once");
}
#[test]
fn test_segment_segment_distance_collinear() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let rod_a = CosseratRod::new_straight(3, 1.0, stiff.clone(), 1.0, 1e-6);
let mut rod_b = CosseratRod::new_straight(3, 1.0, stiff, 1.0, 1e-6);
for n in rod_b.nodes.iter_mut() {
n.position[0] += 0.1;
}
let (dist, _pa, _pb) = RodContact::segment_segment_distance(&rod_a, 0, &rod_b, 0);
assert!((dist - 0.1).abs() < 1e-10, "dist={dist}");
}
#[test]
fn test_rod_contact_detection() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let rod_a = CosseratRod::new_straight(3, 1.0, stiff.clone(), 1.0, 1e-6);
let mut rod_b = CosseratRod::new_straight(3, 1.0, stiff, 1.0, 1e-6);
for n in rod_b.nodes.iter_mut() {
n.position[0] += 0.05;
}
let contact = RodContact::new(1000.0, 0.3, 0.0);
let contacts = contact.detect_contacts(&rod_a, &rod_b, 0.1);
assert!(!contacts.is_empty(), "should detect contacts");
}
#[test]
fn test_rod_contact_no_contact() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let rod_a = CosseratRod::new_straight(3, 1.0, stiff.clone(), 1.0, 1e-6);
let mut rod_b = CosseratRod::new_straight(3, 1.0, stiff, 1.0, 1e-6);
for n in rod_b.nodes.iter_mut() {
n.position[0] += 10.0;
}
let contact = RodContact::new(1000.0, 0.3, 0.0);
let contacts = contact.detect_contacts(&rod_a, &rod_b, 0.1);
assert!(contacts.is_empty(), "should have no contacts");
}
#[test]
fn test_follower_force_bc() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let mut rod = CosseratRod::new_straight(5, 1.0, stiff, 1.0, 1e-6);
let last = rod.n_nodes() - 1;
rod.nodes[last].d3 = vec3_normalize([0.0, 1.0, 1.0]);
let bcs = BoundaryConditions {
base: BcType::Clamped,
tip: BcType::FollowerForce { magnitude: 5.0 },
base_position: [0.0; 3],
base_d1: [1.0, 0.0, 0.0],
base_d2: [0.0, 1.0, 0.0],
base_d3: [0.0, 0.0, 1.0],
};
bcs.apply(&mut rod);
let tip_force = rod.nodes[last].external_force;
let tip_d3 = rod.nodes[last].d3;
let cos_angle = vec3_dot(vec3_normalize(tip_force), tip_d3);
assert!(
(cos_angle - 1.0).abs() < 1e-10,
"follower force not along d3"
);
}
#[test]
fn test_clamped_bc_resets_base() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let mut rod = CosseratRod::new_straight(5, 1.0, stiff, 1.0, 1e-6);
rod.nodes[0].position = [5.0, 5.0, 5.0]; rod.nodes[0].velocity = [1.0, 2.0, 3.0];
let bcs = BoundaryConditions::cantilever(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
);
bcs.apply(&mut rod);
assert_eq!(rod.nodes[0].position, [0.0, 0.0, 0.0]);
assert_eq!(rod.nodes[0].velocity, [0.0, 0.0, 0.0]);
}
#[test]
fn test_dynamics_gravity() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e6, 4e5, &cs);
let mut rod = CosseratRod::new_straight(5, 1.0, stiff, 1.0, 1e-6);
for node in rod.nodes.iter_mut() {
node.external_force = [0.0, -9.81, 0.0];
}
let pos_before = rod.nodes[2].position[1];
let dt = 1e-3;
for _ in 0..10 {
CosseratDynamics::step_euler(&mut rod, dt);
}
let pos_after = rod.nodes[2].position[1];
assert!(pos_after < pos_before, "node should fall under gravity");
}
#[test]
fn test_segment_tangent_straight() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let rod = CosseratRod::new_straight(5, 1.0, stiff, 1.0, 1e-6);
let t = rod.segment_tangent(0);
assert!((t[2] - 1.0).abs() < 1e-10, "tangent={t:?}");
}
#[test]
fn test_segment_stretch_undeformed() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let rod = CosseratRod::new_straight(5, 1.0, stiff, 1.0, 1e-6);
for seg in 0..rod.n_segments() {
let s = rod.segment_stretch(seg);
assert!((s - 1.0).abs() < 1e-10, "stretch[{seg}]={s}");
}
}
#[test]
fn test_internal_loads_zero_undeformed() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.01 });
let stiff = RodStiffness::from_material(200e9, 77e9, &cs);
let rod = CosseratRod::new_straight(5, 1.0, stiff, 1.0, 1e-6);
for seg in 0..rod.n_segments() {
let loads = InternalForces::compute(&rod, seg);
assert!(loads.axial_force.abs() < EPS, "N at seg {seg}");
assert!(loads.moment1.abs() < EPS, "M1 at seg {seg}");
assert!(loads.moment2.abs() < EPS, "M2 at seg {seg}");
assert!(loads.torsion.abs() < EPS, "T at seg {seg}");
}
}
#[test]
fn test_shoot_zero_curvature() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let rod = CosseratRod::new_straight(10, 1.0, stiff, 1.0, 1e-6);
let (tip_pos, tip_d3) = StaticSolver::shoot(&rod, 0.0, 0.0, 0.0);
assert!((tip_pos[2] - 1.0).abs() < 1e-8, "tip_z={}", tip_pos[2]);
assert!((tip_d3[2] - 1.0).abs() < 1e-8, "tip_d3={tip_d3:?}");
}
#[test]
fn test_contact_force_repulsion() {
let contact = RodContact::new(1000.0, 0.3, 0.0);
let f = contact.contact_force_magnitude(0.05, 0.1);
assert!(
f > 0.0,
"contact force should be repulsive for penetration, f={f}"
);
}
#[test]
fn test_contact_force_at_radius() {
let contact = RodContact::new(1000.0, 0.3, 0.0);
let f = contact.contact_force_magnitude(0.1, 0.1);
assert!(f.abs() < EPS, "f at contact radius should be ~0, f={f}");
}
#[test]
fn test_stiffness_ea() {
let r = 0.01;
let e = 200e9;
let g = 77e9;
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: r });
let stiff = RodStiffness::from_material(e, g, &cs);
let expected_ea = e * cs.area;
assert!((stiff.ea - expected_ea).abs() / expected_ea < 1e-10);
}
#[test]
fn test_total_rest_length() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let rod = CosseratRod::new_straight(11, 2.5, stiff, 1.0, 1e-6);
assert!((rod.total_rest_length() - 2.5).abs() < 1e-10);
}
#[test]
fn test_directors_orthonormal() {
let node = CosseratNode::new([0.0, 0.0, 0.0]);
let d12 = vec3_dot(node.d1, node.d2);
let d13 = vec3_dot(node.d1, node.d3);
let d23 = vec3_dot(node.d2, node.d3);
let n1 = vec3_norm(node.d1);
let n2 = vec3_norm(node.d2);
let n3 = vec3_norm(node.d3);
assert!(d12.abs() < EPS, "d1·d2={d12}");
assert!(d13.abs() < EPS, "d1·d3={d13}");
assert!(d23.abs() < EPS, "d2·d3={d23}");
assert!((n1 - 1.0).abs() < EPS);
assert!((n2 - 1.0).abs() < EPS);
assert!((n3 - 1.0).abs() < EPS);
}
#[test]
fn test_rotate_directors_full_circle() {
let mut node = CosseratNode::new([0.0, 0.0, 0.0]);
let omega_z = 2.0 * PI; let dt = 1.0; node.rotate_directors([0.0, 0.0, omega_z], dt);
node.orthonormalize();
assert!((node.d1[0] - 1.0).abs() < 1e-10, "d1x={}", node.d1[0]);
assert!((node.d2[1] - 1.0).abs() < 1e-10, "d2y={}", node.d2[1]);
}
#[test]
fn test_n_segments() {
let cs = RodCrossSection::new(CrossSectionShape::Circular { radius: 0.005 });
let stiff = RodStiffness::from_material(1e9, 4e8, &cs);
let rod = CosseratRod::new_straight(7, 1.0, stiff, 1.0, 1e-6);
assert_eq!(rod.n_segments(), 6);
assert_eq!(rod.n_nodes(), 7);
}
}