use crate::core::matrix::{vec_norm, vec_sub};
use crate::core::scalar::ControlScalar;
use crate::networked::NetworkedError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StaticTriggerConfig<S: ControlScalar> {
pub sigma: S,
pub epsilon: S,
pub min_inter_event_time_ms: S,
}
impl<S: ControlScalar> StaticTriggerConfig<S> {
pub fn new(sigma: S, epsilon: S, min_inter_event_time_ms: S) -> Result<Self, NetworkedError> {
if sigma >= S::ONE || sigma < S::ZERO {
return Err(NetworkedError::InvalidTopology);
}
if epsilon <= S::ZERO {
return Err(NetworkedError::InvalidTopology);
}
if min_inter_event_time_ms < S::ZERO {
return Err(NetworkedError::InvalidTopology);
}
Ok(Self {
sigma,
epsilon,
min_inter_event_time_ms,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct StaticTrigger<S: ControlScalar, const N: usize> {
config: StaticTriggerConfig<S>,
last_event_time_ms: S,
}
impl<S: ControlScalar, const N: usize> StaticTrigger<S, N> {
pub fn new(config: StaticTriggerConfig<S>) -> Self {
Self {
config,
last_event_time_ms: -S::from_f64(1e9), }
}
pub fn check(&mut self, x_current: &[S; N], x_last: &[S; N], t_now_ms: S) -> bool {
let elapsed = t_now_ms - self.last_event_time_ms;
if elapsed < self.config.min_inter_event_time_ms {
return false;
}
let e = vec_sub(x_last, x_current);
let norm_e = vec_norm(&e);
let norm_x = vec_norm(x_current);
let threshold = self.config.sigma * norm_x + self.config.epsilon;
if norm_e >= threshold {
self.last_event_time_ms = t_now_ms;
true
} else {
false
}
}
pub fn force_trigger(&mut self, t_now_ms: S) {
self.last_event_time_ms = t_now_ms;
}
pub fn time_since_last_ms(&self, t_now_ms: S) -> S {
t_now_ms - self.last_event_time_ms
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DynamicTriggerConfig<S: ControlScalar> {
pub beta: S,
pub gamma: S,
pub eta_init: S,
pub min_inter_event_time_ms: S,
}
impl<S: ControlScalar> DynamicTriggerConfig<S> {
pub fn new(
beta: S,
gamma: S,
eta_init: S,
min_inter_event_time_ms: S,
) -> Result<Self, NetworkedError> {
if beta <= S::ZERO || gamma <= S::ZERO {
return Err(NetworkedError::InvalidTopology);
}
if eta_init < S::ZERO {
return Err(NetworkedError::InvalidTopology);
}
if min_inter_event_time_ms < S::ZERO {
return Err(NetworkedError::InvalidTopology);
}
Ok(Self {
beta,
gamma,
eta_init,
min_inter_event_time_ms,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct DynamicTrigger<S: ControlScalar, const N: usize> {
config: DynamicTriggerConfig<S>,
eta: S,
last_event_time_ms: S,
}
impl<S: ControlScalar, const N: usize> DynamicTrigger<S, N> {
pub fn new(config: DynamicTriggerConfig<S>) -> Self {
let eta = config.eta_init;
Self {
config,
eta,
last_event_time_ms: -S::from_f64(1e9),
}
}
pub fn check(&mut self, x_current: &[S; N], x_last: &[S; N], dt: S, t_now_ms: S) -> bool {
let e = vec_sub(x_last, x_current);
let norm_e_sq = {
let mut s = S::ZERO;
for v in e.iter() {
s += *v * *v;
}
s
};
let norm_x_sq = {
let mut s = S::ZERO;
for v in x_current.iter() {
s += *v * *v;
}
s
};
let eta_dot = -self.config.beta * self.eta + self.config.gamma * norm_x_sq - norm_e_sq;
let eta_new = self.eta + dt * eta_dot;
self.eta = if eta_new < S::ZERO { S::ZERO } else { eta_new };
let elapsed = t_now_ms - self.last_event_time_ms;
if elapsed < self.config.min_inter_event_time_ms {
return false;
}
if norm_e_sq >= self.eta {
self.last_event_time_ms = t_now_ms;
self.eta = self.config.eta_init;
true
} else {
false
}
}
pub fn eta(&self) -> S {
self.eta
}
pub fn reset_eta(&mut self) {
self.eta = self.config.eta_init;
}
}
#[derive(Debug, Clone, Copy)]
pub struct InterEventTimer<S: ControlScalar> {
count: u64,
min_iet_ms: S,
max_iet_ms: S,
sum_iet_ms: S,
last_event_ms: Option<S>,
}
impl<S: ControlScalar> InterEventTimer<S> {
pub fn new() -> Self {
Self {
count: 0,
min_iet_ms: S::from_f64(f64::MAX),
max_iet_ms: S::ZERO,
sum_iet_ms: S::ZERO,
last_event_ms: None,
}
}
pub fn record_event(&mut self, t_ms: S) {
if let Some(prev) = self.last_event_ms {
let iet = t_ms - prev;
if iet > S::ZERO {
self.count += 1;
if iet < self.min_iet_ms {
self.min_iet_ms = iet;
}
if iet > self.max_iet_ms {
self.max_iet_ms = iet;
}
self.sum_iet_ms += iet;
}
}
self.last_event_ms = Some(t_ms);
}
pub fn event_count(&self) -> u64 {
self.count
}
pub fn min_iet_ms(&self) -> Option<S> {
if self.count == 0 {
None
} else {
Some(self.min_iet_ms)
}
}
pub fn max_iet_ms(&self) -> Option<S> {
if self.count == 0 {
None
} else {
Some(self.max_iet_ms)
}
}
pub fn mean_iet_ms(&self) -> Option<S> {
if self.count == 0 {
None
} else {
Some(self.sum_iet_ms / S::from_f64(self.count as f64))
}
}
}
impl<S: ControlScalar> Default for InterEventTimer<S> {
fn default() -> Self {
Self::new()
}
}
pub struct EventTriggeredController<S: ControlScalar, const N: usize, F>
where
F: Fn(&[S; N]) -> S,
{
trigger: StaticTrigger<S, N>,
control_fn: F,
x_last: [S; N],
u_hold: S,
timer: InterEventTimer<S>,
}
impl<S: ControlScalar, const N: usize, F> EventTriggeredController<S, N, F>
where
F: Fn(&[S; N]) -> S,
{
pub fn new(
config: StaticTriggerConfig<S>,
control_fn: F,
x_init: [S; N],
) -> Result<Self, NetworkedError> {
let u_init = control_fn(&x_init);
Ok(Self {
trigger: StaticTrigger::new(config),
control_fn,
x_last: x_init,
u_hold: u_init,
timer: InterEventTimer::new(),
})
}
pub fn update(&mut self, x: &[S; N], t_now_ms: S) -> (S, bool) {
let triggered = self.trigger.check(x, &self.x_last, t_now_ms);
if triggered {
self.x_last = *x;
self.u_hold = (self.control_fn)(x);
self.timer.record_event(t_now_ms);
}
(self.u_hold, triggered)
}
pub fn timer(&self) -> &InterEventTimer<S> {
&self.timer
}
pub fn u_hold(&self) -> S {
self.u_hold
}
}
#[cfg(test)]
mod tests {
use super::*;
fn prop_law<const N: usize>(k: f64) -> impl Fn(&[f64; N]) -> f64 {
move |x| -k * x[0]
}
#[test]
fn static_trigger_config_validation() {
assert_eq!(
StaticTriggerConfig::<f64>::new(1.0, 0.01, 0.0),
Err(NetworkedError::InvalidTopology)
);
assert_eq!(
StaticTriggerConfig::<f64>::new(0.5, 0.0, 0.0),
Err(NetworkedError::InvalidTopology)
);
assert!(StaticTriggerConfig::<f64>::new(0.5, 0.01, 0.0).is_ok());
}
#[test]
fn static_trigger_fires_on_large_error() {
let cfg = StaticTriggerConfig::<f64>::new(0.5, 0.01, 0.0).expect("valid config");
let mut trigger = StaticTrigger::<f64, 2>::new(cfg);
let x_current = [1.0_f64, 0.0];
let x_last = [10.0_f64, 0.0]; assert!(trigger.check(&x_current, &x_last, 1.0));
}
#[test]
fn static_trigger_does_not_fire_on_small_error() {
let cfg = StaticTriggerConfig::<f64>::new(0.5, 0.01, 0.0).expect("valid config");
let mut trigger = StaticTrigger::<f64, 2>::new(cfg);
let x_current = [1.0_f64, 0.0];
let x_last = [1.001_f64, 0.0]; assert!(!trigger.check(&x_current, &x_last, 1.0));
}
#[test]
fn static_trigger_respects_min_iet() {
let cfg = StaticTriggerConfig::<f64>::new(0.1, 0.01, 100.0).expect("valid config");
let mut trigger = StaticTrigger::<f64, 2>::new(cfg);
let x_current = [1.0_f64, 0.0];
let x_last = [10.0_f64, 0.0];
assert!(trigger.check(&x_current, &x_last, 0.0));
assert!(!trigger.check(&x_current, &x_last, 50.0));
assert!(trigger.check(&x_current, &x_last, 100.0));
}
#[test]
fn static_trigger_rate_reduces_constant_ref() {
let cfg = StaticTriggerConfig::<f64>::new(0.1, 0.01, 0.0).expect("valid config");
let mut trigger = StaticTrigger::<f64, 2>::new(cfg);
let mut early_triggers = 0u32;
let mut late_triggers = 0u32;
let mut x_last = [0.0_f64, 0.0];
for step in 0..200 {
let alpha = 0.95_f64;
let x_current = [1.0 - alpha.powi(step + 1), 0.0];
let t_ms = step as f64 * 10.0;
if trigger.check(&x_current, &x_last, t_ms) {
x_last = x_current;
if step < 50 {
early_triggers += 1;
} else if step >= 150 {
late_triggers += 1;
}
}
}
assert!(
early_triggers >= late_triggers,
"early={early_triggers} late={late_triggers}"
);
}
#[test]
fn dynamic_trigger_config_validation() {
assert_eq!(
DynamicTriggerConfig::<f64>::new(0.0, 1.0, 0.0, 0.0),
Err(NetworkedError::InvalidTopology)
);
assert!(DynamicTriggerConfig::<f64>::new(1.0, 0.5, 0.1, 0.0).is_ok());
}
#[test]
fn dynamic_trigger_fires_when_error_exceeds_eta() {
let cfg = DynamicTriggerConfig::<f64>::new(1.0, 0.5, 1.0, 0.0).expect("valid config");
let mut trigger = DynamicTrigger::<f64, 2>::new(cfg);
let x_current = [1.0_f64, 0.0];
let x_last = [100.0_f64, 0.0];
assert!(trigger.check(&x_current, &x_last, 0.001, 10.0));
}
#[test]
fn dynamic_trigger_fewer_events_than_static() {
let static_cfg = StaticTriggerConfig::<f64>::new(0.1, 0.01, 0.0).expect("valid config");
let dynamic_cfg =
DynamicTriggerConfig::<f64>::new(2.0, 0.5, 0.01, 0.0).expect("valid config");
let mut st = StaticTrigger::<f64, 2>::new(static_cfg);
let mut dt = DynamicTrigger::<f64, 2>::new(dynamic_cfg);
let mut x_last_s = [5.0_f64, 5.0];
let mut x_last_d = [5.0_f64, 5.0];
let mut static_count = 0u32;
let mut dynamic_count = 0u32;
let alpha = 0.9_f64;
for step in 0..200 {
let x_current = [5.0 * alpha.powi(step + 1), 5.0 * alpha.powi(step + 1)];
let t_ms = step as f64;
if st.check(&x_current, &x_last_s, t_ms) {
x_last_s = x_current;
static_count += 1;
}
if dt.check(&x_current, &x_last_d, 0.001, t_ms) {
x_last_d = x_current;
dynamic_count += 1;
}
}
assert!(
dynamic_count <= static_count + 5,
"dynamic={dynamic_count}, static={static_count}"
);
}
#[test]
fn event_triggered_controller_returns_u() {
let cfg = StaticTriggerConfig::<f64>::new(0.5, 0.01, 0.0).expect("valid");
let x_init = [0.0_f64, 0.0];
let mut ctrl = EventTriggeredController::<f64, 2, _>::new(cfg, prop_law(2.0), x_init)
.expect("valid controller");
let x = [1.0_f64, 0.0];
let (u, _tx) = ctrl.update(&x, 0.0);
assert!((u - (-2.0)).abs() < 1e-10);
}
#[test]
fn event_triggered_differs_from_time_triggered() {
let cfg = StaticTriggerConfig::<f64>::new(0.1, 0.01, 10.0).expect("valid");
let x_init = [0.0_f64, 0.0];
let mut ctrl =
EventTriggeredController::<f64, 2, _>::new(cfg, prop_law(1.0), x_init).expect("valid");
let x_const = [0.5_f64, 0.0]; let mut transmissions = 0u32;
for step in 0..100 {
let (_, tx) = ctrl.update(&x_const, step as f64);
if tx {
transmissions += 1;
}
}
assert!(transmissions <= 15, "transmissions={transmissions}");
assert!(transmissions >= 1);
}
#[test]
fn inter_event_timer_records_correctly() {
let mut timer = InterEventTimer::<f64>::new();
assert!(timer.min_iet_ms().is_none());
timer.record_event(0.0);
timer.record_event(10.0);
timer.record_event(30.0);
assert_eq!(timer.event_count(), 2);
assert!((timer.min_iet_ms().expect("some") - 10.0).abs() < 1e-10);
assert!((timer.max_iet_ms().expect("some") - 20.0).abs() < 1e-10);
assert!((timer.mean_iet_ms().expect("some") - 15.0).abs() < 1e-10);
}
}