use super::functions::*;
#[derive(Debug, Clone)]
pub struct ActivationDynamics {
pub tau_act: f64,
pub tau_deact: f64,
pub activation_min: f64,
pub activation: f64,
pub shape_factor: f64,
}
impl ActivationDynamics {
pub fn default_params() -> Self {
Self {
tau_act: 0.010,
tau_deact: 0.040,
activation_min: 0.01,
activation: 0.0,
shape_factor: 0.1,
}
}
pub fn fast_twitch() -> Self {
Self {
tau_act: 0.005,
tau_deact: 0.020,
activation_min: 0.01,
activation: 0.0,
shape_factor: 0.1,
}
}
pub fn step(&mut self, u: f64, dt: f64) {
let u_clamped = clamp(u, 0.0, 1.0);
let tau = if u_clamped >= self.activation {
self.tau_act * (0.5 + 1.5 * self.activation)
} else {
self.tau_deact / (0.5 + 1.5 * self.activation)
};
let da = (u_clamped - self.activation) / tau;
self.activation = clamp(self.activation + da * dt, self.activation_min, 1.0);
}
pub fn steady_state(&self, u: f64) -> f64 {
clamp(u, self.activation_min, 1.0)
}
pub fn da_dt(&self, u: f64) -> f64 {
let u_clamped = clamp(u, 0.0, 1.0);
let tau = if u_clamped >= self.activation {
self.tau_act * (0.5 + 1.5 * self.activation)
} else {
self.tau_deact / (0.5 + 1.5 * self.activation)
};
(u_clamped - self.activation) / tau
}
}
#[derive(Debug, Clone)]
pub struct MuscleRecruitment {
pub motor_units: Vec<MotorUnit>,
pub neural_drive: f64,
pub total_peak_force: f64,
}
impl MuscleRecruitment {
pub fn new(n: usize, max_force: f64) -> Self {
let mut units = Vec::with_capacity(n);
let log_range = 3.0_f64;
for i in 0..n {
let t = i as f64 / (n as f64 - 1.0).max(1.0);
let threshold = 10.0_f64.powf(-log_range + log_range * t)
/ 10.0_f64.powf(-log_range + log_range * 0.0);
let threshold_norm = clamp(t, 0.0, 1.0);
let twitch_force = max_force / (n as f64) * 10.0_f64.powf(3.0 * t);
let twitch_force_norm = twitch_force / (max_force / (n as f64) * 10.0_f64.powf(3.0));
let contraction_time = if t > 0.5 { 0.030 } else { 0.080 };
let is_fast = t > 0.5;
let _ = threshold;
units.push(MotorUnit::new(
threshold_norm,
twitch_force_norm * max_force,
contraction_time,
is_fast,
));
}
let total_peak = units.iter().map(|u| u.peak_force()).sum();
Self {
motor_units: units,
neural_drive: 0.0,
total_peak_force: total_peak,
}
}
pub fn update(&mut self, u: f64) {
self.neural_drive = clamp(u, 0.0, 1.0);
for mu in &mut self.motor_units {
if u >= mu.recruitment_threshold {
let excess =
(u - mu.recruitment_threshold) / (1.0 - mu.recruitment_threshold).max(1e-6);
let ff = clamp(excess, 0.0, 1.0);
mu.firing_rate = mu.required_firing_rate(0.1 + 0.89 * ff).max(0.0);
let sigmoid = 1.0 / (1.0 + (-12.0 * (ff - 0.5)).exp());
mu.force = mu.peak_force() * sigmoid;
} else {
mu.firing_rate = 0.0;
mu.force = 0.0;
}
}
}
pub fn total_force(&self) -> f64 {
self.motor_units.iter().map(|u| u.force).sum()
}
pub fn recruited_count(&self) -> usize {
if self.neural_drive <= 0.0 {
return 0;
}
self.motor_units
.iter()
.filter(|u| u.recruitment_threshold <= self.neural_drive)
.count()
}
pub fn force_fraction(&self) -> f64 {
if self.total_peak_force < 1e-10 {
return 0.0;
}
self.total_force() / self.total_peak_force
}
}
#[derive(Debug, Clone)]
pub struct MuscleOptimization {
pub muscles: Vec<OptMuscle>,
}
impl MuscleOptimization {
pub fn new(muscles: Vec<OptMuscle>) -> Self {
Self { muscles }
}
pub fn static_optimization(&mut self, required_torque: f64) -> Vec<f64> {
let n = self.muscles.len();
if n == 0 {
return Vec::new();
}
let total_max: f64 = self.muscles.iter().map(|m| m.max_torque()).sum();
if total_max < 1e-12 {
return vec![0.0; n];
}
let fraction = (required_torque / total_max).clamp(0.0, 1.0);
let activations: Vec<f64> = self
.muscles
.iter()
.map(|m| {
if m.max_torque() > 1e-12 {
(fraction * total_max / (m.max_torque() * n as f64)).clamp(0.0, 1.0)
} else {
0.0
}
})
.collect();
for (m, &a) in self.muscles.iter_mut().zip(activations.iter()) {
m.activation = a;
}
activations
}
pub fn emg_driven_step(&mut self, dt: f64, emg: &[f64]) -> Vec<f64> {
let n = self.muscles.len();
let mut activations = Vec::with_capacity(n);
for (i, m) in self.muscles.iter_mut().enumerate() {
let emg_val = if i < emg.len() {
emg[i].clamp(0.0, 1.0)
} else {
0.0
};
let tau_act = 0.01;
let tau_deact = 0.04;
let tau = if emg_val > m.activation {
tau_act
} else {
tau_deact
};
let da = (emg_val - m.activation) / tau * dt;
m.activation = clamp(m.activation + da, 0.01, 1.0);
activations.push(m.activation);
}
activations
}
pub fn stress_objective(&self) -> f64 {
self.muscles
.iter()
.map(|m| m.activation * m.activation)
.sum()
}
}
#[derive(Debug, Clone)]
pub struct ForceVelocityRelation {
pub v_max: f64,
pub a_over_f0: f64,
pub eccentric_force_max: f64,
pub eccentric_shape: f64,
}
impl ForceVelocityRelation {
pub fn fast_twitch() -> Self {
Self {
v_max: 10.0,
a_over_f0: 0.25,
eccentric_force_max: 1.5,
eccentric_shape: 0.1,
}
}
pub fn slow_twitch() -> Self {
Self {
v_max: 4.0,
a_over_f0: 0.25,
eccentric_force_max: 1.4,
eccentric_shape: 0.08,
}
}
pub fn fv(&self, v_norm: f64) -> f64 {
if v_norm <= 0.0 {
let a = self.a_over_f0;
let vn = (-v_norm).min(1.0);
(1.0 - vn) / (1.0 + vn / a)
} else {
let vn = v_norm.min(1.0);
let fmax = self.eccentric_force_max;
let b = self.eccentric_shape;
fmax - (fmax - 1.0) * (1.0 - vn) / (1.0 + vn / b)
}
}
pub fn optimal_velocity_fraction(&self) -> f64 {
let a = self.a_over_f0;
let sqrt_term = (a + 1.0).sqrt();
(sqrt_term - a.sqrt()) / sqrt_term
}
}
#[derive(Debug, Clone)]
pub struct MuscleGeometry {
pub origin: [f64; 3],
pub insertion: [f64; 3],
pub fiber_direction: [f64; 3],
pub pennation_angle: f64,
pub optimal_fiber_length: f64,
pub pcsa: f64,
pub volume: f64,
}
impl MuscleGeometry {
pub fn new(
origin: [f64; 3],
insertion: [f64; 3],
pennation_angle: f64,
optimal_fiber_length: f64,
pcsa: f64,
) -> Self {
let dir = normalize3(sub3(insertion, origin));
let volume = pcsa * optimal_fiber_length;
Self {
origin,
insertion,
fiber_direction: dir,
pennation_angle,
optimal_fiber_length,
pcsa,
volume,
}
}
pub fn soleus() -> Self {
Self::new([0.0, -0.20, 0.0], [0.0, -0.45, 0.0], 0.349, 0.030, 25.0e-4)
}
pub fn rectus_femoris() -> Self {
Self::new([0.0, 0.10, 0.0], [0.0, -0.30, 0.0], 0.087, 0.084, 16.0e-4)
}
pub fn musculotendon_length(&self) -> f64 {
norm3(sub3(self.insertion, self.origin))
}
pub fn fiber_length(&self, musculotendon_len: f64, tendon_slack_length: f64) -> f64 {
let l_mt = musculotendon_len;
let l_ts = tendon_slack_length;
let cos_alpha = self.pennation_angle.cos();
if cos_alpha < 1e-10 {
return self.optimal_fiber_length;
}
(l_mt - l_ts).max(1e-6) / cos_alpha
}
pub fn fiber_force_projection(&self, fiber_force: f64) -> f64 {
fiber_force * self.pennation_angle.cos()
}
}
#[derive(Debug, Clone)]
pub struct ForceLengthRelation {
pub optimal_fiber_length: f64,
pub active_width: f64,
pub passive_stiffness: f64,
pub passive_toe_strain: f64,
pub titin_stiffness: f64,
pub titin_engagement: f64,
}
impl ForceLengthRelation {
pub fn gastrocnemius() -> Self {
Self {
optimal_fiber_length: 0.051,
active_width: 0.56,
passive_stiffness: 3.0,
passive_toe_strain: 0.1,
titin_stiffness: 10.0,
titin_engagement: 1.4,
}
}
pub fn default_params() -> Self {
Self {
optimal_fiber_length: 0.10,
active_width: 0.56,
passive_stiffness: 3.0,
passive_toe_strain: 0.10,
titin_stiffness: 8.0,
titin_engagement: 1.4,
}
}
pub fn fl_active(&self, l_norm: f64) -> f64 {
let w = self.active_width;
(-(l_norm - 1.0).powi(2) / (2.0 * w * w)).exp()
}
pub fn fl_passive(&self, l_norm: f64) -> f64 {
let eps0 = self.passive_toe_strain;
if l_norm <= 1.0 + eps0 {
0.0
} else {
let e = l_norm - 1.0 - eps0;
self.passive_stiffness * e * e
}
}
pub fn titin_force(&self, l_fiber: f64) -> f64 {
let l_eng = self.titin_engagement * self.optimal_fiber_length;
if l_fiber <= l_eng {
0.0
} else {
self.titin_stiffness * (l_fiber - l_eng)
}
}
pub fn fl_total_passive(&self, l_norm: f64) -> f64 {
let titin_l = l_norm * self.optimal_fiber_length;
self.fl_passive(l_norm)
+ self.titin_force(titin_l) / (self.titin_stiffness * self.optimal_fiber_length)
}
}
#[derive(Debug, Clone)]
pub struct WholeBodyMuscle {
pub muscles: Vec<MuscleEntry>,
}
impl WholeBodyMuscle {
pub fn new(muscles: Vec<MuscleEntry>) -> Self {
Self { muscles }
}
pub fn total_joint_moment(&self) -> f64 {
self.muscles.iter().map(|m| m.joint_moment()).sum()
}
pub fn max_joint_moment(&self) -> f64 {
self.muscles
.iter()
.map(|m| m.mtu.muscle.f_max * m.moment_arm)
.sum()
}
pub fn static_optimisation(&self, target_moment: f64) -> Vec<f64> {
let n = self.muscles.len();
if n == 0 {
return Vec::new();
}
let capacities: Vec<f64> = self
.muscles
.iter()
.map(|m| m.mtu.muscle.f_max * m.moment_arm)
.collect();
let sum_c2: f64 = capacities.iter().map(|c| c * c).sum();
if sum_c2 < 1e-20 {
return vec![0.0; n];
}
let lambda = target_moment / sum_c2;
capacities
.iter()
.zip(self.muscles.iter())
.map(|(c, m)| clamp(c * lambda, 0.0, m.max_activation))
.collect()
}
pub fn redundancy_index(&self) -> usize {
self.muscles.len().saturating_sub(1)
}
pub fn step(&mut self, dt: f64, activations: &[f64], l_mts: &[f64]) {
for ((m, &a), &l) in self
.muscles
.iter_mut()
.zip(activations.iter())
.zip(l_mts.iter())
{
m.mtu.step(dt, l, a);
}
}
}
#[derive(Debug, Clone)]
pub struct MusculotendonUnit {
pub muscle: HillMuscleModel,
pub geometry: MuscleGeometry,
pub lmt: f64,
pub v_mt: f64,
}
impl MusculotendonUnit {
pub fn new(muscle: HillMuscleModel, geometry: MuscleGeometry) -> Self {
let lmt = geometry.musculotendon_length();
Self {
muscle,
geometry,
lmt,
v_mt: 0.0,
}
}
pub fn soleus() -> Self {
let muscle = HillMuscleModel::soleus();
let geometry = MuscleGeometry::soleus();
Self::new(muscle, geometry)
}
pub fn tendon_strain(&self) -> f64 {
let l_ts = self.muscle.tendon_slack_length;
let cos_a = self.muscle.pennation_angle.cos();
let l_ce = self.muscle.l_ce_norm * self.muscle.l_opt;
let l_t = self.lmt - l_ce * cos_a;
if l_t <= l_ts {
0.0
} else {
(l_t - l_ts) / l_ts
}
}
pub fn fiber_length(&self) -> f64 {
self.muscle.l_ce_norm * self.muscle.l_opt
}
pub fn current_pennation_angle(&self) -> f64 {
let sin_a0 = self.muscle.pennation_angle.sin();
let l_ce = self.fiber_length();
let sin_a = (self.muscle.l_opt * sin_a0 / l_ce).min(1.0);
sin_a.asin()
}
pub fn mtu_force(&self) -> f64 {
let cos_a = self.current_pennation_angle().cos();
self.muscle.ce_force_total() * cos_a
}
pub fn step(&mut self, dt: f64, l_mt: f64, a_target: f64) {
self.v_mt = (l_mt - self.lmt) / dt;
self.lmt = l_mt;
self.muscle.step(dt, l_mt, a_target);
}
}
#[derive(Debug, Clone)]
pub struct MotorUnit {
pub recruitment_threshold: f64,
pub twitch_force: f64,
pub contraction_time: f64,
pub is_fast_twitch: bool,
pub firing_rate: f64,
pub force: f64,
}
impl MotorUnit {
pub fn new(
recruitment_threshold: f64,
twitch_force: f64,
contraction_time: f64,
is_fast_twitch: bool,
) -> Self {
Self {
recruitment_threshold,
twitch_force,
contraction_time,
is_fast_twitch,
firing_rate: 0.0,
force: 0.0,
}
}
pub fn peak_force(&self) -> f64 {
self.twitch_force * 7.0
}
pub fn required_firing_rate(&self, force_fraction: f64) -> f64 {
let ff = clamp(force_fraction, 0.01, 0.99);
let f_char = 1.0 / (2.0 * self.contraction_time);
f_char * (ff / (1.0 - ff)).ln() + f_char
}
}
#[derive(Debug, Clone)]
pub struct MuscleEntry {
pub name: String,
pub mtu: MusculotendonUnit,
pub moment_arm: f64,
pub max_activation: f64,
}
impl MuscleEntry {
pub fn new(name: &str, mtu: MusculotendonUnit, moment_arm: f64, max_activation: f64) -> Self {
Self {
name: name.to_string(),
mtu,
moment_arm,
max_activation,
}
}
pub fn joint_moment(&self) -> f64 {
self.mtu.mtu_force() * self.moment_arm
}
}
#[derive(Debug, Clone)]
pub struct JointConfig {
pub name: String,
pub position: [f64; 3],
pub angle: f64,
pub min_angle: f64,
pub max_angle: f64,
}
impl JointConfig {
pub fn new(name: &str, position: [f64; 3], angle: f64, min_angle: f64, max_angle: f64) -> Self {
Self {
name: name.to_string(),
position,
angle: clamp(angle, min_angle, max_angle),
min_angle,
max_angle,
}
}
pub fn set_angle(&mut self, angle: f64) {
self.angle = clamp(angle, self.min_angle, self.max_angle);
}
}
#[derive(Debug, Clone)]
pub struct MuscleAttachment {
pub name: String,
pub mtu: MusculotendonUnit,
pub joint_idx: usize,
pub moment_arm: f64,
pub origin: [f64; 3],
pub insertion: [f64; 3],
}
impl MuscleAttachment {
pub fn new(
name: &str,
mtu: MusculotendonUnit,
joint_idx: usize,
moment_arm: f64,
origin: [f64; 3],
insertion: [f64; 3],
) -> Self {
Self {
name: name.to_string(),
mtu,
joint_idx,
moment_arm: moment_arm.abs(),
origin,
insertion,
}
}
pub fn line_of_action(&self) -> [f64; 3] {
normalize3(sub3(self.insertion, self.origin))
}
}
#[derive(Debug, Clone)]
pub struct MusculoskeletalSystem {
pub joints: Vec<JointConfig>,
pub muscles: Vec<MuscleAttachment>,
}
impl MusculoskeletalSystem {
pub fn new() -> Self {
Self {
joints: Vec::new(),
muscles: Vec::new(),
}
}
pub fn add_joint(&mut self, cfg: JointConfig) -> usize {
let idx = self.joints.len();
self.joints.push(cfg);
idx
}
pub fn add_muscle(&mut self, att: MuscleAttachment) {
self.muscles.push(att);
}
pub fn compute_joint_torques(&self) -> Vec<f64> {
let mut torques = vec![0.0f64; self.joints.len()];
for att in &self.muscles {
if att.joint_idx >= self.joints.len() {
continue;
}
let muscle = &att.mtu.muscle;
let f_active = muscle.ce_force_active();
let f_passive = muscle.pee_force();
let f_total = f_active + f_passive;
torques[att.joint_idx] += f_total * att.moment_arm;
}
torques
}
pub fn step(&mut self, dt: f64, activations: &[f64], lmt_values: &[f64]) {
for (i, att) in self.muscles.iter_mut().enumerate() {
let a = if i < activations.len() {
activations[i]
} else {
0.0
};
let lmt = if i < lmt_values.len() {
lmt_values[i]
} else {
att.mtu.lmt
};
att.mtu.step(dt, lmt, a);
}
}
}
#[derive(Debug, Clone)]
pub struct FatigueDynamics {
pub central_fatigue: f64,
pub peripheral_fatigue: f64,
pub central_fatigue_rate: f64,
pub peripheral_fatigue_rate: f64,
pub central_recovery_rate: f64,
pub peripheral_recovery_rate: f64,
pub effort_threshold: f64,
}
impl FatigueDynamics {
pub fn new() -> Self {
Self {
central_fatigue: 0.0,
peripheral_fatigue: 0.0,
central_fatigue_rate: 0.005,
peripheral_fatigue_rate: 0.015,
central_recovery_rate: 0.002,
peripheral_recovery_rate: 0.004,
effort_threshold: 0.2,
}
}
pub fn step(&mut self, dt: f64, effort: f64) {
let e = effort.clamp(0.0, 1.0);
if e > self.effort_threshold {
let excess = e - self.effort_threshold;
self.central_fatigue += self.central_fatigue_rate * excess * dt;
self.peripheral_fatigue += self.peripheral_fatigue_rate * excess * dt;
} else {
self.central_fatigue -= self.central_recovery_rate * dt;
self.peripheral_fatigue -= self.peripheral_recovery_rate * dt;
}
self.central_fatigue = clamp(self.central_fatigue, 0.0, 1.0);
self.peripheral_fatigue = clamp(self.peripheral_fatigue, 0.0, 1.0);
}
pub fn fatigue_index(&self) -> f64 {
0.3 * self.central_fatigue + 0.7 * self.peripheral_fatigue
}
pub fn effective_force_scale(&self, voluntary_effort: f64) -> f64 {
let e = voluntary_effort.clamp(0.0, 1.0);
let fi = self.fatigue_index();
(e * (1.0 - fi)).max(0.0)
}
pub fn time_to_recovery(&self, threshold: f64) -> f64 {
let fi = self.fatigue_index();
if fi <= threshold {
return 0.0;
}
let decay_rate = 0.3 * self.central_recovery_rate + 0.7 * self.peripheral_recovery_rate;
if decay_rate < 1e-12 {
return f64::INFINITY;
}
-(fi - threshold).ln() / decay_rate
}
}
#[derive(Debug, Clone)]
pub struct HillMuscleModel {
pub f_max: f64,
pub l_opt: f64,
pub tendon_slack_length: f64,
pub tendon_stiffness: f64,
pub pennation_angle: f64,
pub fv: ForceVelocityRelation,
pub fl: ForceLengthRelation,
pub activation: f64,
pub l_ce_norm: f64,
pub v_ce_norm: f64,
}
impl HillMuscleModel {
pub fn new(f_max: f64, l_opt: f64, tendon_slack_length: f64, pennation_angle: f64) -> Self {
Self {
f_max,
l_opt,
tendon_slack_length,
tendon_stiffness: f_max / (0.04 * tendon_slack_length),
pennation_angle,
fv: ForceVelocityRelation::fast_twitch(),
fl: ForceLengthRelation::default_params(),
activation: 0.0,
l_ce_norm: 1.0,
v_ce_norm: 0.0,
}
}
pub fn tibialis_anterior() -> Self {
Self::new(600.0, 0.098, 0.223, 0.105)
}
pub fn soleus() -> Self {
Self::new(3550.0, 0.030, 0.268, 0.349)
}
pub fn ce_force_active(&self) -> f64 {
let fl = self.fl.fl_active(self.l_ce_norm);
let fv = self.fv.fv(self.v_ce_norm);
self.f_max * self.activation * fl * fv
}
pub fn pee_force(&self) -> f64 {
let fp = self.fl.fl_passive(self.l_ce_norm);
self.f_max * fp
}
pub fn ce_force_total(&self) -> f64 {
self.ce_force_active() + self.pee_force()
}
pub fn see_force(&self, l_mt: f64) -> f64 {
let l_ts = self.tendon_slack_length;
let cos_alpha = self.pennation_angle.cos();
let l_ce = self.l_ce_norm * self.l_opt;
let l_tendon = l_mt - l_ce * cos_alpha;
if l_tendon <= l_ts {
0.0
} else {
self.tendon_stiffness * (l_tendon - l_ts)
}
}
pub fn compute_v_ce(&self, l_mt: f64) -> f64 {
let f_see = self.see_force(l_mt);
let f_ce_passive = self.pee_force();
let fl = self.fl.fl_active(self.l_ce_norm);
if fl < 1e-10 || self.activation < 1e-10 {
return 0.0;
}
let f_active_target = (f_see - f_ce_passive).max(0.0);
let f_iso = self.f_max * self.activation * fl;
if f_iso < 1e-10 {
return 0.0;
}
let fv_target = f_active_target / f_iso;
let a = self.fv.a_over_f0;
let fv_clamped = clamp(fv_target, 0.0, 1.0);
let vn = (1.0 - fv_clamped) / (1.0 + fv_clamped / a);
-vn
}
pub fn step(&mut self, dt: f64, l_mt: f64, a_target: f64) {
let tau = if a_target > self.activation {
0.01
} else {
0.04
};
self.activation += (a_target - self.activation) * dt / tau;
self.activation = clamp(self.activation, 0.0, 1.0);
self.v_ce_norm = self.compute_v_ce(l_mt);
let dl = self.v_ce_norm * self.fv.v_max * dt / self.l_opt;
self.l_ce_norm += dl;
self.l_ce_norm = clamp(self.l_ce_norm, 0.2, 1.8);
}
}
#[derive(Debug, Clone)]
pub struct OptMuscle {
pub name: String,
pub mtu: MusculotendonUnit,
pub moment_arm: f64,
pub activation: f64,
}
impl OptMuscle {
pub fn new(name: &str, mtu: MusculotendonUnit, moment_arm: f64) -> Self {
Self {
name: name.to_string(),
mtu,
moment_arm: moment_arm.abs(),
activation: 0.0,
}
}
pub fn max_torque(&self) -> f64 {
self.mtu.muscle.f_max * self.moment_arm
}
}
#[derive(Debug, Clone)]
pub struct SarcomereModel {
pub attached_fraction: f64,
pub sarcomere_length: f64,
pub optimal_sarcomere_length: f64,
pub attach_rate: f64,
pub detach_rate_shortening: f64,
pub detach_rate_lengthening: f64,
pub max_isometric_stress: f64,
pub length_plateau_width: f64,
}
impl SarcomereModel {
pub fn step(&mut self, dt: f64, activation: f64, shortening_velocity: f64) {
let f = self.attach_rate * activation.clamp(0.0, 1.0);
let g = if shortening_velocity >= 0.0 {
self.detach_rate_shortening * (1.0 + shortening_velocity.abs())
} else {
self.detach_rate_lengthening * (1.0 + shortening_velocity.abs() * 0.5)
};
let dn = f * (1.0 - self.attached_fraction) - g * self.attached_fraction;
self.attached_fraction = clamp(self.attached_fraction + dn * dt, 0.0, 1.0);
}
pub fn active_force(&self) -> f64 {
let lf = self.length_dependent_force(self.sarcomere_length);
lf * self.attached_fraction * self.max_isometric_stress
}
pub fn length_dependent_force(&self, length: f64) -> f64 {
let l_norm = length / self.optimal_sarcomere_length;
let w = self.length_plateau_width;
if (l_norm - 1.0).abs() <= w {
1.0
} else if l_norm < 1.0 - w {
((l_norm - 0.5) / (0.5 - w)).clamp(0.0, 1.0)
} else {
((1.8 - l_norm) / (0.8 - w)).clamp(0.0, 1.0)
}
}
pub fn passive_force(&self) -> f64 {
let l_norm = self.sarcomere_length / self.optimal_sarcomere_length;
if l_norm > 1.0 {
(l_norm - 1.0).powi(2) * 0.5 * self.max_isometric_stress
} else {
0.0
}
}
}