#![cfg_attr(not(feature = "std"), no_std)]
use core::mem::MaybeUninit;
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HybridError {
InvalidMode,
DwellTimeViolation,
InvalidParameter,
}
pub type GuardFn<S, const N: usize> = fn(&[S; N]) -> bool;
pub type ResetFn<S, const N: usize> = fn(&[S; N]) -> [S; N];
pub type DynamicsFn<S, const N: usize> = fn(&[S; N], S) -> [S; N];
type GuardRow<S, const N: usize, const M: usize> = [Option<GuardFn<S, N>>; M];
type ResetRow<S, const N: usize, const M: usize> = [Option<ResetFn<S, N>>; M];
unsafe fn init_guards<S, const N: usize, const M: usize>() -> [GuardRow<S, N, M>; M] {
let mut arr: MaybeUninit<[GuardRow<S, N, M>; M]> = MaybeUninit::uninit();
let ptr = arr.as_mut_ptr() as *mut Option<GuardFn<S, N>>;
for i in 0..(M * M) {
ptr.add(i).write(None);
}
arr.assume_init()
}
unsafe fn init_resets<S, const N: usize, const M: usize>() -> [ResetRow<S, N, M>; M] {
let mut arr: MaybeUninit<[ResetRow<S, N, M>; M]> = MaybeUninit::uninit();
let ptr = arr.as_mut_ptr() as *mut Option<ResetFn<S, N>>;
for i in 0..(M * M) {
ptr.add(i).write(None);
}
arr.assume_init()
}
pub struct HybridAutomaton<S, const N: usize, const M: usize> {
state: [S; N],
mode: usize,
dynamics: [DynamicsFn<S, N>; M],
guards: [GuardRow<S, N, M>; M],
resets: [ResetRow<S, N, M>; M],
dwell_counter: usize,
min_dwell: usize,
dt: S,
n_transitions: usize,
}
impl<S: ControlScalar, const N: usize, const M: usize> HybridAutomaton<S, N, M> {
pub fn new(
state0: [S; N],
mode0: usize,
dynamics: [DynamicsFn<S, N>; M],
dt: S,
min_dwell: usize,
) -> Result<Self, HybridError> {
if M == 0 {
return Err(HybridError::InvalidParameter);
}
if mode0 >= M {
return Err(HybridError::InvalidMode);
}
let guards = unsafe { init_guards::<S, N, M>() };
let resets = unsafe { init_resets::<S, N, M>() };
Ok(Self {
state: state0,
mode: mode0,
dynamics,
guards,
resets,
dwell_counter: 0,
min_dwell,
dt,
n_transitions: 0,
})
}
pub fn add_guard(
&mut self,
from: usize,
to: usize,
guard: GuardFn<S, N>,
) -> Result<(), HybridError> {
if from >= M || to >= M {
return Err(HybridError::InvalidMode);
}
self.guards[from][to] = Some(guard);
Ok(())
}
pub fn add_reset(
&mut self,
from: usize,
to: usize,
reset: ResetFn<S, N>,
) -> Result<(), HybridError> {
if from >= M || to >= M {
return Err(HybridError::InvalidMode);
}
self.resets[from][to] = Some(reset);
Ok(())
}
pub fn step(&mut self, u: S) -> Result<(usize, [S; N]), HybridError> {
let dx = (self.dynamics[self.mode])(&self.state, u);
for (x_i, dx_i) in self.state.iter_mut().zip(dx.iter()) {
*x_i += *dx_i * self.dt;
}
let current = self.mode;
for j in 0..M {
if j == current {
continue;
}
if let Some(guard) = self.guards[current][j] {
if guard(&self.state) && self.dwell_counter >= self.min_dwell {
if let Some(reset) = self.resets[current][j] {
self.state = reset(&self.state);
}
self.mode = j;
self.dwell_counter = 0;
self.n_transitions += 1;
break;
}
}
}
self.dwell_counter += 1;
Ok((self.mode, self.state))
}
#[inline]
pub fn state(&self) -> &[S; N] {
&self.state
}
#[inline]
pub fn mode(&self) -> usize {
self.mode
}
#[inline]
pub fn n_transitions(&self) -> usize {
self.n_transitions
}
pub fn force_mode(&mut self, mode: usize) -> Result<(), HybridError> {
if mode >= M {
return Err(HybridError::InvalidMode);
}
self.mode = mode;
self.dwell_counter = 0;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dyn_const_one(x: &[f64; 2], _u: f64) -> [f64; 2] {
let _ = x;
[1.0, 0.0]
}
fn dyn_const_two(x: &[f64; 2], _u: f64) -> [f64; 2] {
let _ = x;
[2.0, 0.0]
}
fn dyn_zero(x: &[f64; 2], _u: f64) -> [f64; 2] {
let _ = x;
[0.0, 0.0]
}
fn guard_x0_gt_half(x: &[f64; 2]) -> bool {
x[0] > 0.5
}
fn guard_always(x: &[f64; 2]) -> bool {
let _ = x;
true
}
fn reset_zero_x0(x: &[f64; 2]) -> [f64; 2] {
[0.0, x[1]]
}
#[test]
fn single_mode_no_transition() {
let dynamics: [DynamicsFn<f64, 2>; 1] = [dyn_const_one];
let mut ha = HybridAutomaton::<f64, 2, 1>::new([0.0, 0.0], 0, dynamics, 0.1, 0).unwrap();
for _ in 0..5 {
ha.step(0.0).unwrap();
}
assert!((ha.state()[0] - 0.5).abs() < 1e-9);
assert_eq!(ha.mode(), 0);
assert_eq!(ha.n_transitions(), 0);
}
#[test]
fn guard_triggers_mode_switch() {
let dynamics: [DynamicsFn<f64, 2>; 2] = [dyn_const_one, dyn_zero];
let mut ha = HybridAutomaton::<f64, 2, 2>::new([0.0, 0.0], 0, dynamics, 0.1, 0).unwrap();
ha.add_guard(0, 1, guard_x0_gt_half).unwrap();
let mut switched = false;
for _ in 0..20 {
let (mode, _) = ha.step(0.0).unwrap();
if mode == 1 {
switched = true;
break;
}
}
assert!(switched, "Should have switched to mode 1");
assert_eq!(ha.n_transitions(), 1);
}
#[test]
fn dwell_prevents_immediate_switch() {
let dynamics: [DynamicsFn<f64, 2>; 2] = [dyn_zero, dyn_zero];
let mut ha = HybridAutomaton::<f64, 2, 2>::new([1.0, 0.0], 0, dynamics, 0.01, 3).unwrap();
ha.add_guard(0, 1, guard_always).unwrap();
let (m1, _) = ha.step(0.0).unwrap(); let (m2, _) = ha.step(0.0).unwrap();
let (m3, _) = ha.step(0.0).unwrap();
assert_eq!(m1, 0, "Should still be mode 0 after step 1");
assert_eq!(m2, 0, "Should still be mode 0 after step 2");
assert_eq!(m3, 0, "Should still be mode 0 after step 3");
let (m4, _) = ha.step(0.0).unwrap();
assert_eq!(m4, 1, "Should have switched to mode 1 at step 4");
assert_eq!(ha.n_transitions(), 1);
}
#[test]
fn reset_applied_on_transition() {
let dynamics: [DynamicsFn<f64, 2>; 2] = [dyn_const_one, dyn_zero];
let mut ha = HybridAutomaton::<f64, 2, 2>::new([0.0, 0.0], 0, dynamics, 0.1, 0).unwrap();
ha.add_guard(0, 1, guard_x0_gt_half).unwrap();
ha.add_reset(0, 1, reset_zero_x0).unwrap();
for _ in 0..20 {
let (mode, _) = ha.step(0.0).unwrap();
if mode == 1 {
break;
}
}
assert_eq!(ha.mode(), 1);
assert!(ha.state()[0].abs() < 1e-9, "Reset should zero x[0]");
}
#[test]
fn invalid_mode_returns_error() {
let dynamics: [DynamicsFn<f64, 2>; 2] = [dyn_zero, dyn_zero];
let result = HybridAutomaton::<f64, 2, 2>::new([0.0, 0.0], 2, dynamics, 0.1, 0);
assert_eq!(result.err(), Some(HybridError::InvalidMode));
}
#[test]
fn n_transitions_counts_correctly() {
let dynamics: [DynamicsFn<f64, 2>; 2] = [dyn_zero, dyn_zero];
let mut ha = HybridAutomaton::<f64, 2, 2>::new([0.0, 0.0], 0, dynamics, 0.01, 0).unwrap();
ha.add_guard(0, 1, guard_always).unwrap();
ha.add_guard(1, 0, guard_always).unwrap();
for _ in 0..3 {
ha.step(0.0).unwrap();
}
assert_eq!(ha.n_transitions(), 3);
}
#[test]
fn euler_step_correct() {
let dynamics: [DynamicsFn<f64, 2>; 1] = [dyn_const_one];
let mut ha = HybridAutomaton::<f64, 2, 1>::new([0.0, 0.0], 0, dynamics, 0.1, 0).unwrap();
let (_, state) = ha.step(0.0).unwrap();
assert!(
(state[0] - 0.1).abs() < 1e-12,
"x[0] should be 0.1 after one step"
);
assert!(state[1].abs() < 1e-12, "x[1] should remain 0.0");
}
#[test]
fn force_mode_and_invalid_force() {
let dynamics: [DynamicsFn<f64, 2>; 2] = [dyn_zero, dyn_zero];
let mut ha = HybridAutomaton::<f64, 2, 2>::new([0.0, 0.0], 0, dynamics, 0.01, 10).unwrap();
ha.force_mode(1).unwrap();
assert_eq!(ha.mode(), 1);
assert_eq!(ha.force_mode(2).err(), Some(HybridError::InvalidMode));
}
#[test]
fn add_guard_invalid_mode_error() {
let dynamics: [DynamicsFn<f64, 2>; 2] = [dyn_zero, dyn_zero];
let mut ha = HybridAutomaton::<f64, 2, 2>::new([0.0, 0.0], 0, dynamics, 0.01, 0).unwrap();
assert_eq!(
ha.add_guard(0, 5, guard_always).err(),
Some(HybridError::InvalidMode)
);
assert_eq!(
ha.add_reset(5, 0, reset_zero_x0).err(),
Some(HybridError::InvalidMode)
);
}
#[test]
fn two_mode_dynamics_correct() {
let dynamics: [DynamicsFn<f64, 2>; 2] = [dyn_const_one, dyn_const_two];
let mut ha = HybridAutomaton::<f64, 2, 2>::new([0.0, 0.0], 0, dynamics, 0.1, 0).unwrap();
ha.add_guard(0, 1, guard_x0_gt_half).unwrap();
loop {
let (mode, _) = ha.step(0.0).unwrap();
if mode == 1 {
break;
}
}
let state_at_switch = *ha.state();
ha.step(0.0).unwrap();
let after = ha.state()[0];
assert!((after - (state_at_switch[0] + 0.2)).abs() < 1e-9);
}
}