#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use num_traits::Float;
#[derive(Debug, Clone, PartialEq)]
pub(super) struct SpeedState {
pub(super) speed: f64,
pub(super) speed_efficiency: f64,
}
impl Default for SpeedState {
#[inline]
fn default() -> Self {
Self { speed: 1.0, speed_efficiency: 1.0 }
}
}
pub(super) fn accumulate_totals(
masses: &[f64],
old_forces: &[[f64; 2]],
forces: &[[f64; 2]],
) -> (f64, f64) {
let mut swinging = 0.0;
let mut traction = 0.0;
for ((mass, old), new) in masses.iter().zip(old_forces).zip(forces) {
let diff_x = old[0] - new[0];
let diff_y = old[1] - new[1];
let sum_x = old[0] + new[0];
let sum_y = old[1] + new[1];
let node_swinging = mass * (diff_x * diff_x + diff_y * diff_y).sqrt();
let node_traction = mass * 0.5 * (sum_x * sum_x + sum_y * sum_y).sqrt();
if node_swinging.is_finite() && node_traction.is_finite() {
swinging += node_swinging;
traction += node_traction;
}
}
(swinging, traction)
}
pub(super) fn update_global_speed(
state: &mut SpeedState,
swinging: f64,
traction: f64,
number_of_nodes: usize,
jitter_tolerance: f64,
) {
if swinging <= 0.0 && traction <= 0.0 {
return;
}
#[allow(clippy::cast_precision_loss)]
let n = number_of_nodes as f64;
let estimated_optimal_jitter_tolerance = 0.05 * n.sqrt();
let min_jt = estimated_optimal_jitter_tolerance.sqrt();
let max_jt = 10.0_f64;
let mut jt = jitter_tolerance
* min_jt.max(max_jt.min(estimated_optimal_jitter_tolerance * traction / (n * n)));
let min_speed_efficiency = 0.05;
if swinging / traction > 2.0 {
if state.speed_efficiency > min_speed_efficiency {
state.speed_efficiency *= 0.5;
}
jt = jt.max(jitter_tolerance);
}
let target_speed = jt * state.speed_efficiency * traction / swinging;
if swinging > jt * traction {
if state.speed_efficiency > min_speed_efficiency {
state.speed_efficiency *= 0.7;
}
} else if state.speed < 1000.0 {
state.speed_efficiency *= 1.3;
}
let max_rise = 0.5;
state.speed += (target_speed - state.speed).min(max_rise * state.speed);
}
pub(super) fn per_node_factor(speed: f64, mass: f64, local_swinging: f64) -> f64 {
speed / (1.0 + (speed * mass * local_swinging).sqrt())
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::{SpeedState, accumulate_totals, per_node_factor, update_global_speed};
#[test]
fn totals_closed_form() {
let masses = [2.0, 3.0];
let old_forces = [[1.0, 0.0], [0.0, 2.0]];
let forces = [[0.0, 1.0], [0.0, 0.0]];
let (swinging, traction) = accumulate_totals(&masses, &old_forces, &forces);
assert!((swinging - (2.0 * core::f64::consts::SQRT_2 + 6.0)).abs() < 1e-12);
assert!((traction - (core::f64::consts::SQRT_2 + 3.0)).abs() < 1e-12);
}
#[test]
fn totals_steady_course() {
let masses = [1.0];
let old_forces = [[3.0, 4.0]];
let forces = [[3.0, 4.0]];
let (swinging, traction) = accumulate_totals(&masses, &old_forces, &forces);
assert_eq!(swinging, 0.0);
assert_eq!(traction, 5.0);
}
#[test]
fn totals_perfect_oscillation() {
let masses = [1.0];
let old_forces = [[3.0, 4.0]];
let forces = [[-3.0, -4.0]];
let (swinging, traction) = accumulate_totals(&masses, &old_forces, &forces);
assert_eq!(swinging, 10.0);
assert_eq!(traction, 0.0);
}
#[test]
fn totals_skip_non_finite_forces() {
let masses = [1.0, 1.0];
let old_forces = [[f64::INFINITY, 0.0], [3.0, 4.0]];
let forces = [[f64::NEG_INFINITY, 0.0], [3.0, 4.0]];
let (swinging, traction) = accumulate_totals(&masses, &old_forces, &forces);
assert_eq!(swinging, 0.0);
assert_eq!(traction, 5.0);
}
#[test]
fn speed_growth_branch_with_max_rise() {
let mut state = SpeedState::default();
update_global_speed(&mut state, 1.0, 10000.0, 100, 1.0);
assert!((state.speed - 1.5).abs() < 1e-12);
assert!((state.speed_efficiency - 1.3).abs() < 1e-12);
}
#[test]
fn speed_overswing_branch() {
let mut state = SpeedState::default();
update_global_speed(&mut state, 150.0, 100.0, 100, 1.0);
let jt = 0.5_f64.sqrt();
assert!((state.speed - jt * 100.0 / 150.0).abs() < 1e-12);
assert!((state.speed_efficiency - 0.7).abs() < 1e-12);
}
#[test]
fn speed_erratic_branch() {
let mut state = SpeedState::default();
update_global_speed(&mut state, 300.0, 100.0, 100, 1.0);
assert!((state.speed - 1.0 / 6.0).abs() < 1e-12);
assert!((state.speed_efficiency - 0.35).abs() < 1e-12);
}
#[test]
fn speed_jitter_clamped_at_max() {
let mut state = SpeedState { speed: 8.0, speed_efficiency: 1.0 };
update_global_speed(&mut state, 1e9, 1e9, 100, 1.0);
assert!((state.speed - 10.0).abs() < 1e-12);
assert!((state.speed_efficiency - 1.3).abs() < 1e-12);
}
#[test]
fn speed_growth_gated_above_1000() {
let mut state = SpeedState { speed: 1000.0, speed_efficiency: 1.0 };
update_global_speed(&mut state, 1.0, 10000.0, 100, 1.0);
assert!((state.speed - 1500.0).abs() < 1e-12);
assert!((state.speed_efficiency - 1.0).abs() < 1e-12);
}
#[test]
fn speed_efficiency_floor() {
let mut state = SpeedState { speed: 1.0, speed_efficiency: 0.05 };
update_global_speed(&mut state, 300.0, 100.0, 100, 1.0);
assert!((state.speed_efficiency - 0.05).abs() < 1e-12);
assert!((state.speed - 0.05 / 3.0).abs() < 1e-12);
}
#[test]
fn speed_zero_swinging_rises_by_cap() {
let mut state = SpeedState::default();
update_global_speed(&mut state, 0.0, 10.0, 4, 1.0);
assert!((state.speed - 1.5).abs() < 1e-12);
assert!((state.speed_efficiency - 1.3).abs() < 1e-12);
}
#[test]
fn speed_all_zero_is_noop() {
let mut state = SpeedState { speed: 3.0, speed_efficiency: 0.8 };
update_global_speed(&mut state, 0.0, 0.0, 4, 1.0);
assert_eq!(state, SpeedState { speed: 3.0, speed_efficiency: 0.8 });
}
#[test]
fn per_node_factor_closed_form() {
assert!((per_node_factor(4.0, 2.0, 2.0) - 0.8).abs() < 1e-12);
}
#[test]
fn per_node_factor_no_swinging() {
assert_eq!(per_node_factor(2.5, 7.0, 0.0), 2.5);
}
}