use phyz_math::DVec;
use phyz_model::{Model, State};
#[derive(Debug, Clone)]
pub struct PiController {
pub tolerance: f64,
pub alpha: f64,
pub beta: f64,
pub dt_min: f64,
pub dt_max: f64,
prev_error: f64,
}
impl PiController {
pub fn new(tolerance: f64, dt_min: f64, dt_max: f64) -> Self {
Self {
tolerance,
alpha: 0.4,
beta: 0.2,
dt_min,
dt_max,
prev_error: tolerance, }
}
pub fn adjust(&mut self, current_dt: f64, error: f64) -> (f64, bool) {
let safety = 0.9;
let p_term = if error > 1e-14 {
(self.tolerance / error).powf(self.alpha)
} else {
2.0 };
let i_term = if self.prev_error > 1e-14 {
(self.tolerance / self.prev_error).powf(self.beta)
} else {
1.0
};
let factor = safety * p_term * i_term;
let factor = factor.clamp(0.1, 5.0);
let new_dt = (current_dt * factor).clamp(self.dt_min, self.dt_max);
let accept = error <= self.tolerance;
self.prev_error = error;
(new_dt, accept)
}
pub fn reset(&mut self) {
self.prev_error = self.tolerance;
}
}
pub struct EmbeddedRkError;
impl EmbeddedRkError {
pub fn estimate(
model: &Model,
state: &State,
derivatives_fn: impl Fn(&Model, &State) -> (DVec, DVec),
) -> f64 {
let dt = model.dt;
let state_rk4 = Self::rk4_step(model, state, dt, &derivatives_fn);
let state_rk5 = Self::rk5_step(model, state, dt, &derivatives_fn);
let mut max_error = 0.0_f64;
for i in 0..state.q.len() {
let err = (state_rk4.q[i] - state_rk5.q[i]).abs();
let scale = state_rk4.q[i].abs().max(1e-6);
max_error = max_error.max(err / scale);
}
for i in 0..state.v.len() {
let err = (state_rk4.v[i] - state_rk5.v[i]).abs();
let scale = state_rk4.v[i].abs().max(1e-6);
max_error = max_error.max(err / scale);
}
max_error
}
fn rk4_step(
model: &Model,
state: &State,
dt: f64,
derivatives_fn: &impl Fn(&Model, &State) -> (DVec, DVec),
) -> State {
let mut result = state.clone();
let (dq1, dv1) = derivatives_fn(model, state);
let mut s2 = state.clone();
s2.q += &dq1 * (dt / 2.0);
s2.v += &dv1 * (dt / 2.0);
let (dq2, dv2) = derivatives_fn(model, &s2);
let mut s3 = state.clone();
s3.q += &dq2 * (dt / 2.0);
s3.v += &dv2 * (dt / 2.0);
let (dq3, dv3) = derivatives_fn(model, &s3);
let mut s4 = state.clone();
s4.q += &dq3 * dt;
s4.v += &dv3 * dt;
let (dq4, dv4) = derivatives_fn(model, &s4);
result.q += &(&dq1 + &dq2 * 2.0 + &dq3 * 2.0 + &dq4) * (dt / 6.0);
result.v += &(&dv1 + &dv2 * 2.0 + &dv3 * 2.0 + &dv4) * (dt / 6.0);
result
}
fn rk5_step(
model: &Model,
state: &State,
dt: f64,
derivatives_fn: &impl Fn(&Model, &State) -> (DVec, DVec),
) -> State {
let mut result = state.clone();
let (dq1, dv1) = derivatives_fn(model, state);
let mut s2 = state.clone();
s2.q += &dq1 * (dt / 5.0);
s2.v += &dv1 * (dt / 5.0);
let (dq2, dv2) = derivatives_fn(model, &s2);
let mut s3 = state.clone();
s3.q += &(&dq1 * (3.0 / 40.0) + &dq2 * (9.0 / 40.0)) * dt;
s3.v += &(&dv1 * (3.0 / 40.0) + &dv2 * (9.0 / 40.0)) * dt;
let (dq3, dv3) = derivatives_fn(model, &s3);
let mut s4 = state.clone();
s4.q += &(&dq1 * (3.0 / 10.0) - &dq2 * (9.0 / 10.0) + &dq3 * (6.0 / 5.0)) * dt;
s4.v += &(&dv1 * (3.0 / 10.0) - &dv2 * (9.0 / 10.0) + &dv3 * (6.0 / 5.0)) * dt;
let (dq4, dv4) = derivatives_fn(model, &s4);
let mut s5 = state.clone();
s5.q += &(&dq1 * (-11.0 / 54.0) + &dq2 * (5.0 / 2.0) - &dq3 * (70.0 / 27.0)
+ &dq4 * (35.0 / 27.0))
* dt;
s5.v += &(&dv1 * (-11.0 / 54.0) + &dv2 * (5.0 / 2.0) - &dv3 * (70.0 / 27.0)
+ &dv4 * (35.0 / 27.0))
* dt;
let (dq5, dv5) = derivatives_fn(model, &s5);
result.q += &(&dq1 * (37.0 / 378.0)
+ &dq3 * (250.0 / 621.0)
+ &dq4 * (125.0 / 594.0)
+ &dq5 * (512.0 / 1771.0))
* dt;
result.v += &(&dv1 * (37.0 / 378.0)
+ &dv3 * (250.0 / 621.0)
+ &dv4 * (125.0 / 594.0)
+ &dv5 * (512.0 / 1771.0))
* dt;
result
}
}
pub struct AdaptiveTimeStep {
pub controller: PiController,
pub rejected_steps: usize,
pub accepted_steps: usize,
}
impl AdaptiveTimeStep {
pub fn new(tolerance: f64, dt_min: f64, dt_max: f64) -> Self {
Self {
controller: PiController::new(tolerance, dt_min, dt_max),
rejected_steps: 0,
accepted_steps: 0,
}
}
pub fn step<F>(&mut self, model: &mut Model, state: &mut State, step_fn: F) -> (bool, f64)
where
F: Fn(&Model, &mut State),
{
let error = EmbeddedRkError::estimate(model, state, |m, s| {
use phyz_rigid::aba;
let qdd = aba(m, s);
(s.v.clone(), qdd)
});
let (new_dt, accept) = self.controller.adjust(model.dt, error);
if accept {
step_fn(model, state);
self.accepted_steps += 1;
} else {
self.rejected_steps += 1;
}
model.dt = new_dt;
(accept, new_dt)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pi_controller() {
let mut controller = PiController::new(1e-4, 1e-6, 1e-2);
let (new_dt, accept) = controller.adjust(0.001, 1e-5);
assert!(accept);
assert!(new_dt > 0.001);
let (new_dt, accept) = controller.adjust(0.001, 1e-3);
assert!(!accept);
assert!(new_dt < 0.001);
}
#[test]
fn test_pi_controller_clamping() {
let mut controller = PiController::new(1e-4, 1e-5, 1e-3);
let (new_dt, _) = controller.adjust(1e-3, 1e-10);
assert!((new_dt - 1e-3).abs() < 1e-10);
let (new_dt, _) = controller.adjust(1e-5, 1.0);
assert!((new_dt - 1e-5).abs() < 1e-10);
}
}