use crate::core::matrix::vec_norm;
use crate::core::scalar::ControlScalar;
use crate::networked::NetworkedError;
use libm::log as libm_log;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SelfTrigger<S: ControlScalar, const N: usize> {
trigger_sigma: S,
min_next_trigger_s: S,
max_next_trigger_s: S,
}
impl<S: ControlScalar, const N: usize> SelfTrigger<S, N> {
pub fn new(
trigger_sigma: S,
min_next_trigger_s: S,
max_next_trigger_s: S,
) -> Result<Self, NetworkedError> {
if trigger_sigma <= S::ZERO || trigger_sigma >= S::ONE {
return Err(NetworkedError::InvalidTopology);
}
if min_next_trigger_s <= S::ZERO {
return Err(NetworkedError::InvalidTopology);
}
if max_next_trigger_s <= min_next_trigger_s {
return Err(NetworkedError::InvalidTopology);
}
Ok(Self {
trigger_sigma,
min_next_trigger_s,
max_next_trigger_s,
})
}
pub fn next_trigger_time(&self, x: &[S; N], a_norm: S) -> Result<S, NetworkedError> {
if a_norm <= S::ZERO {
return Err(NetworkedError::NumericalError);
}
let norm_x = vec_norm(x);
if norm_x <= S::ZERO {
return Ok(self.min_next_trigger_s);
}
let sigma_f64 = self.trigger_sigma.to_f64();
let a_norm_f64 = a_norm.to_f64();
let tau_f64 = libm_log(1.0 + sigma_f64) / a_norm_f64;
let tau = S::from_f64(tau_f64);
let clamped = if tau < self.min_next_trigger_s {
self.min_next_trigger_s
} else if tau > self.max_next_trigger_s {
self.max_next_trigger_s
} else {
tau
};
Ok(clamped)
}
pub fn trigger_sigma(&self) -> S {
self.trigger_sigma
}
}
#[derive(Debug, Clone, Copy)]
pub struct LqrGain<S: ControlScalar, const N: usize> {
pub k: [S; N],
}
impl<S: ControlScalar, const N: usize> LqrGain<S, N> {
pub fn compute(&self, x: &[S; N]) -> S {
let mut u = S::ZERO;
for (&k_i, &x_i) in self.k.iter().zip(x.iter()) {
u -= k_i * x_i;
}
u
}
}
#[derive(Debug, Clone, Copy)]
pub struct SelfTriggeredLqr<S: ControlScalar, const N: usize> {
gain: LqrGain<S, N>,
trigger: SelfTrigger<S, N>,
a_cl_norm: S,
x_last: [S; N],
u_hold: S,
next_trigger_ms: S,
update_count: u64,
}
impl<S: ControlScalar, const N: usize> SelfTriggeredLqr<S, N> {
pub fn new(
gain: LqrGain<S, N>,
trigger: SelfTrigger<S, N>,
a_cl_norm: S,
x_init: [S; N],
) -> Result<Self, NetworkedError> {
if a_cl_norm <= S::ZERO {
return Err(NetworkedError::NumericalError);
}
let u_hold = gain.compute(&x_init);
Ok(Self {
gain,
trigger,
a_cl_norm,
x_last: x_init,
u_hold,
next_trigger_ms: S::ZERO, update_count: 0,
})
}
pub fn update(&mut self, x: &[S; N], t_now_ms: S) -> Result<(S, Option<S>), NetworkedError> {
if t_now_ms >= self.next_trigger_ms {
self.x_last = *x;
self.u_hold = self.gain.compute(x);
self.update_count += 1;
let tau_s = self.trigger.next_trigger_time(x, self.a_cl_norm)?;
let tau_ms = tau_s * S::from_f64(1000.0);
let next_ms = t_now_ms + tau_ms;
self.next_trigger_ms = next_ms;
Ok((self.u_hold, Some(next_ms)))
} else {
Ok((self.u_hold, None))
}
}
pub fn update_count(&self) -> u64 {
self.update_count
}
pub fn next_trigger_ms(&self) -> S {
self.next_trigger_ms
}
pub fn u_hold(&self) -> S {
self.u_hold
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn self_trigger_config_validation() {
assert_eq!(
SelfTrigger::<f64, 2>::new(0.0, 0.001, 1.0),
Err(NetworkedError::InvalidTopology)
);
assert_eq!(
SelfTrigger::<f64, 2>::new(0.5, 0.0, 1.0),
Err(NetworkedError::InvalidTopology)
);
assert_eq!(
SelfTrigger::<f64, 2>::new(0.5, 1.0, 0.5),
Err(NetworkedError::InvalidTopology)
);
assert!(SelfTrigger::<f64, 2>::new(0.5, 0.001, 10.0).is_ok());
}
#[test]
fn next_trigger_time_positive() {
let trigger = SelfTrigger::<f64, 2>::new(0.3, 0.001, 10.0).expect("valid");
let x = [1.0_f64, 0.5];
let tau = trigger
.next_trigger_time(&x, 2.0)
.expect("no numerical error");
assert!(tau > 0.0, "next trigger time must be positive, got {tau}");
}
#[test]
fn next_trigger_time_clamp_to_min() {
let trigger = SelfTrigger::<f64, 2>::new(0.01, 0.1, 10.0).expect("valid");
let x = [1.0_f64, 0.0];
let tau = trigger
.next_trigger_time(&x, 1000.0) .expect("no numerical error");
assert!(tau >= 0.1, "should be clamped to min=0.1, got {tau}");
}
#[test]
fn next_trigger_time_monotone_in_sigma() {
let t1 = SelfTrigger::<f64, 2>::new(0.1, 0.001, 100.0)
.expect("valid")
.next_trigger_time(&[1.0, 0.0], 1.0)
.expect("ok");
let t2 = SelfTrigger::<f64, 2>::new(0.5, 0.001, 100.0)
.expect("valid")
.next_trigger_time(&[1.0, 0.0], 1.0)
.expect("ok");
assert!(
t2 >= t1,
"larger sigma should give longer next-trigger time: t1={t1}, t2={t2}"
);
}
#[test]
fn next_trigger_time_zero_x_returns_min() {
let trigger = SelfTrigger::<f64, 2>::new(0.3, 0.05, 10.0).expect("valid");
let x = [0.0_f64, 0.0]; let tau = trigger.next_trigger_time(&x, 2.0).expect("no error");
assert!(
(tau - 0.05).abs() < 1e-10,
"zero state should return min={}, got {tau}",
0.05
);
}
#[test]
fn next_trigger_satisfies_trigger_condition() {
let sigma = 0.3_f64;
let trigger = SelfTrigger::<f64, 1>::new(sigma, 0.001, 100.0).expect("valid");
let x = [2.0_f64];
let a_norm = 1.5_f64;
let tau = trigger.next_trigger_time(&x, a_norm).expect("ok");
let ratio = 1.0 - (-a_norm * tau).exp();
assert!(
ratio <= sigma + 1e-9,
"ratio={ratio} should be ≤ sigma={sigma}"
);
}
#[test]
fn self_triggered_lqr_initial_update() {
let gain = LqrGain::<f64, 2> { k: [1.0, 0.5] };
let trigger = SelfTrigger::<f64, 2>::new(0.3, 0.001, 10.0).expect("valid");
let x_init = [1.0_f64, 0.5];
let mut ctrl = SelfTriggeredLqr::new(gain, trigger, 1.0, x_init).expect("valid");
let x = [1.0_f64, 0.5];
let (u, next) = ctrl.update(&x, 0.0).expect("ok");
assert!((u - (-1.25)).abs() < 1e-10, "u={u}");
assert!(next.is_some(), "first call should always transmit");
assert!(next.expect("some") > 0.0);
}
#[test]
fn self_triggered_lqr_holds_between_events() {
let gain = LqrGain::<f64, 2> { k: [1.0, 0.0] };
let trigger = SelfTrigger::<f64, 2>::new(0.5, 0.001, 100.0).expect("valid");
let x_init = [1.0_f64, 0.0];
let mut ctrl = SelfTriggeredLqr::new(gain, trigger, 0.5, x_init).expect("valid");
let x = [1.0_f64, 0.0];
let (u0, next0) = ctrl.update(&x, 0.0).expect("ok");
assert!(next0.is_some());
let next_ms = next0.expect("some");
let (u1, next1) = ctrl.update(&x, next_ms * 0.5).expect("ok");
assert!(next1.is_none(), "should hold between events");
assert!((u1 - u0).abs() < 1e-10, "held value unchanged");
}
#[test]
fn self_triggered_lqr_update_count() {
let gain = LqrGain::<f64, 2> { k: [2.0, 1.0] };
let trigger = SelfTrigger::<f64, 2>::new(0.3, 0.001, 1000.0).expect("valid");
let x_init = [1.0_f64, 0.0];
let mut ctrl = SelfTriggeredLqr::new(gain, trigger, 1.0, x_init).expect("valid");
ctrl.update(&x_init, 0.0).expect("ok");
let count_after_first = ctrl.update_count();
assert_eq!(count_after_first, 1);
ctrl.update(&x_init, 10_000.0).expect("ok");
assert_eq!(ctrl.update_count(), 2);
}
}