use std::f64::consts::PI;
#[inline]
fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
x.max(lo).min(hi)
}
#[cfg(test)]
#[inline]
fn norm3(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[inline]
fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
fn rotate_y(v: [f64; 3], angle: f64) -> [f64; 3] {
let c = angle.cos();
let s = angle.sin();
[c * v[0] + s * v[2], v[1], -s * v[0] + c * v[2]]
}
pub struct PneumaticActuator {
pub length: f64,
pub radius: f64,
pub wall_thickness: f64,
pub modulus: f64,
}
impl PneumaticActuator {
pub fn new(length: f64, radius: f64, wall_thickness: f64, modulus: f64) -> Self {
Self {
length,
radius,
wall_thickness,
modulus,
}
}
pub fn pressure_extension(&self, pressure: f64) -> f64 {
if self.modulus < 1e-9 || self.wall_thickness < 1e-9 {
return 0.0;
}
let area = PI * self.radius * self.radius;
let wall_stiffness = self.modulus * 2.0 * PI * self.radius * self.wall_thickness;
pressure * area * self.length / wall_stiffness
}
pub fn blocking_force(&self, pressure: f64) -> f64 {
pressure * PI * self.radius * self.radius
}
pub fn free_stroke(&self, pressure_max: f64) -> f64 {
self.pressure_extension(pressure_max)
}
pub fn curvature_actuation(&self, pressure: f64) -> f64 {
let dl = self.pressure_extension(pressure);
if self.radius < 1e-15 || self.length < 1e-15 {
return 0.0;
}
dl / (self.radius * self.length)
}
}
pub struct TentacleSegment {
pub length: f64,
pub curvature: f64,
pub twist: f64,
}
impl TentacleSegment {
pub fn new(length: f64, curvature: f64, twist: f64) -> Self {
Self {
length,
curvature,
twist,
}
}
}
pub struct SoftTentacle {
pub segments: Vec<TentacleSegment>,
pub n_seg: usize,
}
impl SoftTentacle {
pub fn new(n: usize, seg_length: f64) -> Self {
let segments = (0..n)
.map(|_| TentacleSegment::new(seg_length, 0.0, 0.0))
.collect();
Self { segments, n_seg: n }
}
pub fn tip_position(&self) -> [f64; 3] {
let pts = self.forward_kinematics_internal();
*pts.last().unwrap_or(&[0.0; 3])
}
pub fn workspace_radius(&self) -> f64 {
self.segments.iter().map(|s| s.length).sum()
}
pub fn forward_kinematics(&self, angles: &[f64]) -> Vec<[f64; 3]> {
let mut pos = [0.0f64; 3];
let mut dir = [0.0f64, 0.0, 1.0]; let mut pts = vec![pos];
for (i, seg) in self.segments.iter().enumerate() {
let angle = angles.get(i).copied().unwrap_or(0.0);
dir = rotate_y(dir, angle);
let step = scale3(dir, seg.length);
pos = add3(pos, step);
pts.push(pos);
}
pts
}
fn forward_kinematics_internal(&self) -> Vec<[f64; 3]> {
let mut pos = [0.0f64; 3];
let mut dir = [0.0f64, 0.0, 1.0];
let mut pts = vec![pos];
for seg in &self.segments {
let angle = seg.curvature * seg.length;
dir = rotate_y(dir, angle);
let step = scale3(dir, seg.length);
pos = add3(pos, step);
pts.push(pos);
}
pts
}
pub fn apply_gravity(&mut self) {
let n = self.n_seg;
for i in 0..n {
let downstream_length: f64 = self.segments[i..].iter().map(|s| s.length).sum();
self.segments[i].curvature += 0.01 * downstream_length;
}
}
pub fn step(&mut self, actuation: &[f64], dt: f64) {
for (i, seg) in self.segments.iter_mut().enumerate() {
let act = actuation.get(i).copied().unwrap_or(0.0);
let target_curvature = act * 10.0; seg.curvature += (target_curvature - seg.curvature) * dt * 5.0;
}
}
}
pub struct PeristalticLocomotion {
pub segments: Vec<f64>,
pub wave_speed: f64,
pub friction_coeff: f64,
}
impl PeristalticLocomotion {
pub fn new(
n_segments: usize,
segment_length: f64,
wave_speed: f64,
friction_coeff: f64,
) -> Self {
Self {
segments: vec![segment_length; n_segments],
wave_speed,
friction_coeff,
}
}
pub fn body_displacement(&self, t: f64) -> f64 {
let l_body: f64 = self.segments.iter().sum();
if l_body < 1e-15 {
return 0.0;
}
let freq = self.wave_speed / l_body;
let amplitude = l_body / (2.0 * PI);
amplitude * (2.0 * PI * freq * t).sin()
}
pub fn locomotion_speed(&self) -> f64 {
let n = self.segments.len() as f64;
let l_body: f64 = self.segments.iter().sum();
if l_body < 1e-15 || n < 1.0 {
return 0.0;
}
let lambda = l_body / n;
let freq = self.wave_speed / l_body;
freq * lambda * (1.0 - self.friction_coeff * 0.1)
}
pub fn efficiency(&self) -> f64 {
if self.wave_speed < 1e-15 {
return 0.0;
}
clamp(self.locomotion_speed() / self.wave_speed, 0.0, 1.0)
}
}
pub struct CephalopodArm {
pub rest_length: f64,
pub muscle_force: f64,
pub passive_stiffness: f64,
}
impl CephalopodArm {
pub fn new(rest_length: f64, muscle_force: f64, passive_stiffness: f64) -> Self {
Self {
rest_length,
muscle_force,
passive_stiffness,
}
}
pub fn elongation(&self, force: f64) -> f64 {
if self.passive_stiffness < 1e-15 {
return 0.0;
}
force / self.passive_stiffness
}
pub fn contraction_ratio(&self) -> f64 {
if self.rest_length < 1e-15 || self.passive_stiffness < 1e-15 {
return 0.0;
}
clamp(
self.muscle_force / (self.passive_stiffness * self.rest_length),
0.0,
1.0,
)
}
}
pub struct CrawlingRobot {
pub body_stiffness: f64,
pub friction_anisotropy: f64,
}
impl CrawlingRobot {
pub fn new(body_stiffness: f64, friction_anisotropy: f64) -> Self {
Self {
body_stiffness,
friction_anisotropy,
}
}
pub fn friction_force(&self, vel: [f64; 2], normal: f64) -> [f64; 2] {
let base_mu = 0.3; let mu_x = if vel[0] < 0.0 {
base_mu * self.friction_anisotropy
} else {
base_mu
};
let mu_y = base_mu;
let fx = if vel[0].abs() > 1e-12 {
-mu_x * normal * vel[0].signum()
} else {
0.0
};
let fy = if vel[1].abs() > 1e-12 {
-mu_y * normal * vel[1].signum()
} else {
0.0
};
[fx, fy]
}
pub fn crawl_speed(&self, actuation_freq: f64) -> f64 {
let l_step = 0.01; let eta = if self.friction_anisotropy > 1.0 {
(self.friction_anisotropy - 1.0) / self.friction_anisotropy
} else {
0.0
};
actuation_freq * l_step * eta
}
}
pub fn cpg_oscillator(phase: f64, freq: f64, amplitude: f64, dt: f64) -> (f64, f64) {
let omega = 2.0 * PI * freq;
let new_phase = phase + omega * dt;
let output = amplitude * new_phase.sin();
(new_phase, output)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pneumatic_extension_zero_at_zero_pressure() {
let a = PneumaticActuator::new(0.1, 0.01, 0.001, 1e6);
assert!((a.pressure_extension(0.0)).abs() < 1e-15);
}
#[test]
fn pneumatic_extension_positive_at_positive_pressure() {
let a = PneumaticActuator::new(0.1, 0.01, 0.001, 1e6);
assert!(a.pressure_extension(100e3) > 0.0);
}
#[test]
fn pneumatic_extension_scales_linearly_with_pressure() {
let a = PneumaticActuator::new(0.1, 0.01, 0.001, 1e6);
let e1 = a.pressure_extension(100e3);
let e2 = a.pressure_extension(200e3);
assert!((e2 - 2.0 * e1).abs() < 1e-12, "e1={e1} e2={e2}");
}
#[test]
fn pneumatic_blocking_force_scales_with_pressure() {
let a = PneumaticActuator::new(0.1, 0.01, 0.001, 1e6);
let f1 = a.blocking_force(100e3);
let f2 = a.blocking_force(200e3);
assert!((f2 - 2.0 * f1).abs() < 1e-8, "f1={f1} f2={f2}");
}
#[test]
fn pneumatic_blocking_force_equals_pressure_times_area() {
let r = 0.01;
let a = PneumaticActuator::new(0.1, r, 0.001, 1e6);
let p = 50e3;
let expected = p * PI * r * r;
let got = a.blocking_force(p);
assert!(
(got - expected).abs() < 1e-8,
"got={got} expected={expected}"
);
}
#[test]
fn pneumatic_free_stroke_equals_extension_at_max_pressure() {
let a = PneumaticActuator::new(0.1, 0.01, 0.001, 1e6);
let p_max = 300e3;
let fs = a.free_stroke(p_max);
let ext = a.pressure_extension(p_max);
assert!((fs - ext).abs() < 1e-15);
}
#[test]
fn pneumatic_curvature_zero_at_zero_pressure() {
let a = PneumaticActuator::new(0.1, 0.01, 0.001, 1e6);
assert!((a.curvature_actuation(0.0)).abs() < 1e-15);
}
#[test]
fn pneumatic_curvature_positive_at_positive_pressure() {
let a = PneumaticActuator::new(0.1, 0.01, 0.001, 1e6);
assert!(a.curvature_actuation(100e3) > 0.0);
}
#[test]
fn tentacle_tip_at_origin_for_zero_segments() {
let t = SoftTentacle::new(0, 0.05);
let tip = t.tip_position();
assert!(norm3(tip) < 1e-12, "tip = {tip:?}");
}
#[test]
fn tentacle_workspace_radius_correct() {
let t = SoftTentacle::new(5, 0.1);
let wr = t.workspace_radius();
assert!((wr - 0.5).abs() < 1e-10);
}
#[test]
fn tentacle_forward_kinematics_length_correct() {
let t = SoftTentacle::new(3, 0.1);
let angles = [0.0; 3];
let pts = t.forward_kinematics(&angles);
assert_eq!(pts.len(), 4, "n+1 points");
let tip = pts[3];
assert!((tip[2] - 0.3).abs() < 1e-10, "tip z = {}", tip[2]);
}
#[test]
fn tentacle_tip_changes_after_step() {
let mut t = SoftTentacle::new(4, 0.1);
let tip_before = t.tip_position();
let actuation = [1.0, 1.0, 1.0, 1.0];
t.step(&actuation, 0.1);
let tip_after = t.tip_position();
let delta = norm3([
tip_after[0] - tip_before[0],
tip_after[1] - tip_before[1],
tip_after[2] - tip_before[2],
]);
assert!(delta > 1e-8, "tip should move after step: delta={delta}");
}
#[test]
fn tentacle_apply_gravity_increases_curvature() {
let mut t = SoftTentacle::new(3, 0.1);
let kappa_before = t.segments[0].curvature;
t.apply_gravity();
let kappa_after = t.segments[0].curvature;
assert!(
kappa_after > kappa_before,
"gravity should increase curvature"
);
}
#[test]
fn tentacle_step_moves_toward_target_curvature() {
let mut t = SoftTentacle::new(2, 0.1);
let kappa_before = t.segments[0].curvature;
let actuation = [1.0, 1.0];
for _ in 0..20 {
t.step(&actuation, 0.05);
}
let kappa_after = t.segments[0].curvature;
assert!(
kappa_after > kappa_before,
"curvature should increase with positive actuation"
);
}
#[test]
fn peristaltic_locomotion_speed_positive() {
let p = PeristalticLocomotion::new(10, 0.05, 0.1, 0.3);
assert!(p.locomotion_speed() > 0.0);
}
#[test]
fn peristaltic_efficiency_between_zero_and_one() {
let p = PeristalticLocomotion::new(10, 0.05, 0.1, 0.3);
let e = p.efficiency();
assert!((0.0..=1.0).contains(&e), "efficiency = {e}");
}
#[test]
fn peristaltic_body_displacement_zero_at_t0() {
let p = PeristalticLocomotion::new(8, 0.05, 0.1, 0.2);
assert!((p.body_displacement(0.0)).abs() < 1e-15);
}
#[test]
fn peristaltic_displacement_periodic() {
let p = PeristalticLocomotion::new(8, 0.05, 0.2, 0.2);
let l_body: f64 = p.segments.iter().sum();
let freq = p.wave_speed / l_body;
let period = 1.0 / freq;
let d0 = p.body_displacement(0.0);
let d1 = p.body_displacement(period);
assert!((d1 - d0).abs() < 1e-10, "d0={d0} d1={d1}");
}
#[test]
fn peristaltic_speed_increases_with_wave_speed() {
let p1 = PeristalticLocomotion::new(5, 0.05, 0.1, 0.2);
let p2 = PeristalticLocomotion::new(5, 0.05, 0.2, 0.2);
assert!(p2.locomotion_speed() > p1.locomotion_speed());
}
#[test]
fn cephalopod_elongation_zero_at_zero_force() {
let arm = CephalopodArm::new(0.3, 5.0, 100.0);
assert!((arm.elongation(0.0)).abs() < 1e-15);
}
#[test]
fn cephalopod_elongation_linear_with_force() {
let arm = CephalopodArm::new(0.3, 5.0, 100.0);
let e1 = arm.elongation(10.0);
let e2 = arm.elongation(20.0);
assert!((e2 - 2.0 * e1).abs() < 1e-12);
}
#[test]
fn cephalopod_contraction_ratio_in_range() {
let arm = CephalopodArm::new(0.3, 5.0, 100.0);
let cr = arm.contraction_ratio();
assert!((0.0..=1.0).contains(&cr), "cr = {cr}");
}
#[test]
fn cephalopod_contraction_ratio_increases_with_muscle_force() {
let arm1 = CephalopodArm::new(0.3, 5.0, 100.0);
let arm2 = CephalopodArm::new(0.3, 20.0, 100.0);
assert!(arm2.contraction_ratio() > arm1.contraction_ratio());
}
#[test]
fn crawling_speed_zero_at_zero_frequency() {
let r = CrawlingRobot::new(1.0, 2.0);
assert!((r.crawl_speed(0.0)).abs() < 1e-15);
}
#[test]
fn crawling_speed_positive_with_anisotropy() {
let r = CrawlingRobot::new(1.0, 3.0);
assert!(r.crawl_speed(1.0) > 0.0);
}
#[test]
fn crawling_speed_scales_with_frequency() {
let r = CrawlingRobot::new(1.0, 2.0);
let v1 = r.crawl_speed(1.0);
let v2 = r.crawl_speed(2.0);
assert!((v2 - 2.0 * v1).abs() < 1e-12);
}
#[test]
fn crawling_speed_zero_with_isotropic_friction() {
let r = CrawlingRobot::new(1.0, 1.0); assert!((r.crawl_speed(5.0)).abs() < 1e-15);
}
#[test]
fn crawling_friction_opposes_velocity() {
let r = CrawlingRobot::new(1.0, 2.0);
let f = r.friction_force([1.0, 0.0], 10.0);
assert!(f[0] < 0.0, "friction should oppose +X motion");
}
#[test]
fn crawling_friction_backward_larger_than_forward() {
let r = CrawlingRobot::new(1.0, 3.0);
let f_fwd = r.friction_force([1.0, 0.0], 10.0);
let f_bwd = r.friction_force([-1.0, 0.0], 10.0);
assert!(
f_bwd[0].abs() > f_fwd[0].abs(),
"backward friction ({}) should exceed forward ({})",
f_bwd[0].abs(),
f_fwd[0].abs()
);
}
#[test]
fn crawling_friction_zero_for_zero_velocity() {
let r = CrawlingRobot::new(1.0, 2.0);
let f = r.friction_force([0.0, 0.0], 10.0);
assert!(f[0].abs() < 1e-15 && f[1].abs() < 1e-15);
}
#[test]
fn cpg_output_bounded_by_amplitude() {
let amplitude = 2.5;
let mut phase = 0.0;
for _ in 0..1000 {
let (new_phase, out) = cpg_oscillator(phase, 1.0, amplitude, 0.001);
phase = new_phase;
assert!(
out.abs() <= amplitude + 1e-10,
"CPG output {out} exceeds amplitude {amplitude}"
);
}
}
#[test]
fn cpg_phase_advances_at_correct_rate() {
let freq = 2.0;
let dt = 0.01;
let (new_phase, _) = cpg_oscillator(0.0, freq, 1.0, dt);
let expected = 2.0 * PI * freq * dt;
assert!(
(new_phase - expected).abs() < 1e-12,
"new_phase = {new_phase}"
);
}
#[test]
fn cpg_zero_amplitude_zero_output() {
let (_, out) = cpg_oscillator(0.5, 1.0, 0.0, 0.01);
assert!(out.abs() < 1e-15, "output = {out}");
}
#[test]
fn cpg_zero_frequency_constant_phase() {
let phase = 0.7;
let (new_phase, _) = cpg_oscillator(phase, 0.0, 1.0, 0.1);
assert!((new_phase - phase).abs() < 1e-12);
}
#[test]
fn cpg_output_oscillates_over_time() {
let mut phase = 0.0;
let freq = 1.0;
let amplitude = 1.0;
let dt = 0.01;
let mut has_positive = false;
let mut has_negative = false;
for _ in 0..300 {
let (new_phase, out) = cpg_oscillator(phase, freq, amplitude, dt);
phase = new_phase;
if out > 0.5 {
has_positive = true;
}
if out < -0.5 {
has_negative = true;
}
}
assert!(has_positive, "CPG should produce positive outputs");
assert!(has_negative, "CPG should produce negative outputs");
}
#[test]
fn clamp_within_bounds() {
assert!((clamp(5.0, 0.0, 10.0) - 5.0).abs() < 1e-15);
assert!((clamp(-1.0, 0.0, 10.0) - 0.0).abs() < 1e-15);
assert!((clamp(15.0, 0.0, 10.0) - 10.0).abs() < 1e-15);
}
#[test]
fn rotate_y_quarter_turn() {
let v = [1.0, 0.0, 0.0];
let rotated = rotate_y(v, PI / 2.0);
assert!(rotated[0].abs() < 1e-10, "x = {}", rotated[0]);
assert!(rotated[1].abs() < 1e-10, "y = {}", rotated[1]);
assert!((rotated[2] + 1.0).abs() < 1e-10, "z = {}", rotated[2]);
}
}