use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrescribedTimeError {
InvalidTime,
InvalidGain,
TimeExpired,
}
impl core::fmt::Display for PrescribedTimeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
PrescribedTimeError::InvalidTime => {
f.write_str("target_time, epsilon and dt must all be strictly positive")
}
PrescribedTimeError::InvalidGain => {
f.write_str("k must be positive and lambda must be >= 1")
}
PrescribedTimeError::TimeExpired => {
f.write_str("prescribed time has expired (t >= target_time - epsilon)")
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PrescribedTimeController<S: ControlScalar> {
target_time: S,
epsilon: S,
k: S,
lambda: S,
t: S,
dt: S,
}
impl<S: ControlScalar> PrescribedTimeController<S> {
pub fn new(
target_time: S,
epsilon: S,
k: S,
lambda: S,
dt: S,
) -> Result<Self, PrescribedTimeError> {
if target_time <= S::ZERO || epsilon <= S::ZERO || dt <= S::ZERO {
return Err(PrescribedTimeError::InvalidTime);
}
if k <= S::ZERO || lambda < S::ONE {
return Err(PrescribedTimeError::InvalidGain);
}
Ok(Self {
target_time,
epsilon,
k,
lambda,
t: S::ZERO,
dt,
})
}
pub fn update(&mut self, x: S) -> Result<S, PrescribedTimeError> {
if self.t >= self.target_time - self.epsilon {
return Err(PrescribedTimeError::TimeExpired);
}
let remaining = self.target_time - self.t + self.epsilon;
let mu = S::ONE / remaining;
let u = -self.k * mu.powf(self.lambda) * x;
self.t += self.dt;
Ok(u)
}
pub fn time_remaining(&self) -> S {
let remaining = self.target_time - self.t;
if remaining < S::ZERO {
S::ZERO
} else {
remaining
}
}
pub fn reset(&mut self) {
self.t = S::ZERO;
}
pub fn current_time(&self) -> S {
self.t
}
pub fn target_time(&self) -> S {
self.target_time
}
}
#[cfg(test)]
mod tests {
use super::*;
const DT: f64 = 0.001;
#[test]
fn zero_state_zero_control() {
let mut ctrl =
PrescribedTimeController::<f64>::new(1.0, 0.01, 2.0, 1.5, DT).expect("valid params");
let u = ctrl.update(0.0).expect("update ok");
assert!(u.abs() < 1e-14, "u should be zero for x=0, got {}", u);
}
#[test]
fn control_grows_near_target_time() {
let mut ctrl =
PrescribedTimeController::<f64>::new(1.0, 0.01, 1.0, 2.0, DT).expect("valid params");
let u0 = ctrl.update(1.0).expect("first update").abs();
for _ in 0..899 {
ctrl.update(1.0).expect("update ok");
}
let u_late = ctrl.update(1.0).expect("late update").abs();
assert!(
u_late > u0,
"control magnitude should grow near deadline: u0={:.6}, u_late={:.6}",
u0,
u_late
);
}
#[test]
fn time_expiry_returns_error() {
let mut ctrl =
PrescribedTimeController::<f64>::new(0.1, 0.01, 1.0, 1.0, DT).expect("valid params");
let mut expired = false;
for _ in 0..200 {
match ctrl.update(0.5) {
Ok(_) => {}
Err(PrescribedTimeError::TimeExpired) => {
expired = true;
break;
}
Err(e) => panic!("unexpected error: {}", e),
}
}
assert!(expired, "controller should have expired");
}
#[test]
fn known_gain_formula() {
let mut ctrl =
PrescribedTimeController::<f64>::new(1.0, 0.01, 1.0, 1.0, DT).expect("valid params");
let x = 2.0_f64;
let u = ctrl.update(x).expect("update ok");
let expected = -2.0_f64 / 1.01_f64;
assert!(
(u - expected).abs() < 1e-10,
"expected u={:.8}, got u={:.8}",
expected,
u
);
}
#[test]
fn controller_reduces_state() {
let mut ctrl =
PrescribedTimeController::<f64>::new(2.0, 0.01, 2.0, 1.5, DT).expect("valid params");
let mut x = 1.0_f64;
let x_init = x;
for _ in 0..500 {
match ctrl.update(x) {
Ok(u) => {
x += DT * u;
}
Err(_) => break,
}
}
assert!(
x.abs() < x_init.abs(),
"state should decrease: x_init={:.4}, x_final={:.4}",
x_init,
x
);
}
#[test]
fn invalid_target_time_returns_error() {
let res = PrescribedTimeController::<f64>::new(0.0, 0.01, 1.0, 1.0, DT);
assert!(
matches!(res, Err(PrescribedTimeError::InvalidTime)),
"expected InvalidTime, got {:?}",
res.err()
);
}
#[test]
fn time_remaining_decreases() {
let mut ctrl =
PrescribedTimeController::<f64>::new(1.0, 0.01, 1.0, 1.0, DT).expect("valid params");
let r0 = ctrl.time_remaining();
ctrl.update(1.0).expect("update ok");
let r1 = ctrl.time_remaining();
assert!(
r1 < r0,
"time_remaining should decrease: r0={:.6}, r1={:.6}",
r0,
r1
);
}
#[test]
fn reset_restores_clock() {
let mut ctrl =
PrescribedTimeController::<f64>::new(1.0, 0.01, 1.0, 1.0, DT).expect("valid params");
for _ in 0..100 {
ctrl.update(0.5).expect("update ok");
}
ctrl.reset();
assert!(
ctrl.current_time().abs() < 1e-14,
"time should be zero after reset: t={}",
ctrl.current_time()
);
}
#[test]
fn invalid_lambda_returns_error() {
let res = PrescribedTimeController::<f64>::new(1.0, 0.01, 1.0, 0.5, DT);
assert!(
matches!(res, Err(PrescribedTimeError::InvalidGain)),
"expected InvalidGain, got {:?}",
res.err()
);
}
}