use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SrmError {
InvalidStatorPoles,
InvalidRotorPoles,
InvalidInductance,
InvalidPhaseIndex,
InvalidSaturation,
}
impl core::fmt::Display for SrmError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidStatorPoles => write!(f, "stator poles must be even and >= 4"),
Self::InvalidRotorPoles => write!(f, "rotor poles must be >= 2"),
Self::InvalidInductance => write!(f, "L_min < L_max, both > 0 required"),
Self::InvalidPhaseIndex => write!(f, "phase index out of range"),
Self::InvalidSaturation => write!(f, "saturation coefficient must be >= 0"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SrmPhaseParams<S: ControlScalar> {
pub l_min: S,
pub l_max: S,
pub k_sat: S,
pub r_phase: S,
pub i_rated: S,
}
#[derive(Debug, Clone, Copy)]
pub struct CommutationAngles<S: ControlScalar> {
pub theta_on: S,
pub theta_off: S,
}
#[derive(Debug, Clone, Copy)]
pub struct SrmPhaseState<S: ControlScalar> {
pub current: S,
pub voltage: S,
pub active: bool,
}
impl<S: ControlScalar> Default for SrmPhaseState<S> {
fn default() -> Self {
Self {
current: S::ZERO,
voltage: S::ZERO,
active: false,
}
}
}
#[derive(Debug, Clone, Copy)]
struct RippleEstimator<S: ControlScalar> {
alpha: S,
t_min: S,
t_max: S,
t_avg: S,
ripple: S,
count: u32,
}
impl<S: ControlScalar> RippleEstimator<S> {
fn new(alpha: S) -> Self {
Self {
alpha,
t_min: S::from_f64(f64::MAX / 2.0),
t_max: S::from_f64(f64::MIN / 2.0),
t_avg: S::ZERO,
ripple: S::ZERO,
count: 0,
}
}
fn update(&mut self, torque: S) {
if torque < self.t_min {
self.t_min = torque;
}
if torque > self.t_max {
self.t_max = torque;
}
self.t_avg = self.alpha * torque + (S::ONE - self.alpha) * self.t_avg;
self.count += 1;
if self.count % 32 == 0 && self.t_avg.abs() > S::from_f64(1e-9) {
let raw_ripple = (self.t_max - self.t_min) / self.t_avg.abs();
self.ripple = self.alpha * raw_ripple + (S::ONE - self.alpha) * self.ripple;
self.t_min = torque;
self.t_max = torque;
}
}
}
#[derive(Debug, Clone)]
pub struct SrmModel<S: ControlScalar, const N: usize> {
phase_params: [SrmPhaseParams<S>; N],
n_stator_poles: u32,
n_rotor_poles: u32,
n_phases: usize,
commutation: [CommutationAngles<S>; N],
phase_states: [SrmPhaseState<S>; N],
theta_mech: S,
omega_mech: S,
inertia: S,
b_friction: S,
torque_total: S,
ripple_est: RippleEstimator<S>,
rotor_pole_pitch: S,
}
impl<S: ControlScalar, const N: usize> SrmModel<S, N> {
pub fn new(
n_stator_poles: u32,
n_rotor_poles: u32,
phase_params: [SrmPhaseParams<S>; N],
commutation: [CommutationAngles<S>; N],
inertia: S,
b_friction: S,
) -> Result<Self, SrmError> {
if n_stator_poles < 4 || n_stator_poles % 2 != 0 {
return Err(SrmError::InvalidStatorPoles);
}
if n_rotor_poles < 2 {
return Err(SrmError::InvalidRotorPoles);
}
for p in &phase_params {
if p.l_min <= S::ZERO || p.l_max <= p.l_min {
return Err(SrmError::InvalidInductance);
}
if p.k_sat < S::ZERO {
return Err(SrmError::InvalidSaturation);
}
}
let n_phases = N;
let rotor_pole_pitch = S::PI * S::TWO / S::from_f64(n_rotor_poles as f64);
let phase_states = core::array::from_fn(|_| SrmPhaseState::default());
Ok(Self {
phase_params,
n_stator_poles,
n_rotor_poles,
n_phases,
commutation,
phase_states,
theta_mech: S::ZERO,
omega_mech: S::ZERO,
inertia,
b_friction,
torque_total: S::ZERO,
ripple_est: RippleEstimator::new(S::from_f64(0.05)),
rotor_pole_pitch,
})
}
pub fn inductance(&self, phase: usize, theta: S, current: S) -> Result<S, SrmError> {
if phase >= self.n_phases {
return Err(SrmError::InvalidPhaseIndex);
}
let params = &self.phase_params[phase];
let l_min = params.l_min;
let l_max = params.l_max;
let k_sat = params.k_sat;
let phase_offset =
self.rotor_pole_pitch * S::from_f64(phase as f64) / S::from_f64(self.n_phases as f64);
let theta_rel = theta - phase_offset;
let wrapped = self.wrap_angle(theta_rel, self.rotor_pole_pitch);
let phi = wrapped / self.rotor_pole_pitch;
let two_pi_phi = S::TWO * S::PI * phi;
let f_theta = S::HALF * (S::ONE - two_pi_phi.cos());
let i_sq = current * current;
let g_current = S::ONE / (S::ONE + k_sat * i_sq);
let l = l_min + (l_max - l_min) * f_theta * g_current;
Ok(l)
}
fn inductance_gradient(&self, phase: usize, theta: S, current: S) -> S {
let h = S::from_f64(1e-4); let l_plus = self
.inductance(phase, theta + h, current)
.unwrap_or(S::ZERO);
let l_minus = self
.inductance(phase, theta - h, current)
.unwrap_or(S::ZERO);
(l_plus - l_minus) / (S::TWO * h)
}
pub fn phase_torque(&self, phase: usize, theta: S, current: S) -> Result<S, SrmError> {
if phase >= self.n_phases {
return Err(SrmError::InvalidPhaseIndex);
}
let dl_dtheta = self.inductance_gradient(phase, theta, current);
Ok(S::HALF * current * current * dl_dtheta)
}
fn is_phase_active(&self, phase: usize, theta: S) -> bool {
if phase >= self.n_phases {
return false;
}
let ca = &self.commutation[phase];
let phase_offset =
self.rotor_pole_pitch * S::from_f64(phase as f64) / S::from_f64(self.n_phases as f64);
let theta_rel = theta - phase_offset;
let wrapped = self.wrap_angle(theta_rel, self.rotor_pole_pitch);
wrapped >= ca.theta_on && wrapped < ca.theta_off
}
pub fn step(&mut self, voltages: &[S; N], tau_load: S, dt: S) {
let mut torque_sum = S::ZERO;
let theta = self.theta_mech;
let omega = self.omega_mech;
#[allow(clippy::needless_range_loop)]
for ph in 0..self.n_phases {
let active = self.is_phase_active(ph, theta);
self.phase_states[ph].active = active;
let v = if active { voltages[ph] } else { S::ZERO };
self.phase_states[ph].voltage = v;
let i = self.phase_states[ph].current;
let r = self.phase_params[ph].r_phase;
let l = self
.inductance(ph, theta, i)
.unwrap_or(self.phase_params[ph].l_min);
let dl = self.inductance_gradient(ph, theta, i);
let back_emf = i * omega * dl;
let l_safe = if l > S::from_f64(1e-12) {
l
} else {
S::from_f64(1e-12)
};
let di_dt = (v - r * i - back_emf) / l_safe;
self.phase_states[ph].current += di_dt * dt;
if self.phase_states[ph].current < S::ZERO {
self.phase_states[ph].current = S::ZERO;
}
let t_ph = self
.phase_torque(ph, theta, self.phase_states[ph].current)
.unwrap_or(S::ZERO);
torque_sum += t_ph;
}
self.torque_total = torque_sum;
let j_safe = if self.inertia > S::from_f64(1e-15) {
self.inertia
} else {
S::from_f64(1e-15)
};
let domega = (torque_sum - self.b_friction * omega - tau_load) / j_safe;
self.omega_mech += domega * dt;
self.theta_mech += self.omega_mech * dt;
self.theta_mech = self.wrap_angle(self.theta_mech, S::PI * S::TWO);
self.ripple_est.update(torque_sum);
}
pub fn torque_total(&self) -> S {
self.torque_total
}
pub fn omega_mech(&self) -> S {
self.omega_mech
}
pub fn theta_mech(&self) -> S {
self.theta_mech
}
pub fn phase_current(&self, phase: usize) -> S {
if phase < self.n_phases {
self.phase_states[phase].current
} else {
S::ZERO
}
}
pub fn phase_active(&self, phase: usize) -> bool {
if phase < self.n_phases {
self.phase_states[phase].active
} else {
false
}
}
pub fn torque_ripple(&self) -> S {
self.ripple_est.ripple
}
pub fn n_stator_poles(&self) -> u32 {
self.n_stator_poles
}
pub fn n_rotor_poles(&self) -> u32 {
self.n_rotor_poles
}
pub fn reset(&mut self) {
for ph in 0..self.n_phases {
self.phase_states[ph] = SrmPhaseState::default();
}
self.theta_mech = S::ZERO;
self.omega_mech = S::ZERO;
self.torque_total = S::ZERO;
self.ripple_est = RippleEstimator::new(S::from_f64(0.05));
}
fn wrap_angle(&self, angle: S, period: S) -> S {
if period <= S::ZERO {
return S::ZERO;
}
let mut a = angle;
while a >= period {
a -= period;
}
while a < S::ZERO {
a += period;
}
a
}
}
pub fn srm_6_4_default<S: ControlScalar>() -> Result<SrmModel<S, 3>, SrmError> {
let params = core::array::from_fn(|_| SrmPhaseParams {
l_min: S::from_f64(5e-3),
l_max: S::from_f64(30e-3),
k_sat: S::from_f64(0.05),
r_phase: S::from_f64(1.2),
i_rated: S::from_f64(5.0),
});
let rpp = core::f64::consts::PI / 2.0; let commutation = core::array::from_fn(|_| CommutationAngles {
theta_on: S::from_f64(rpp * 0.1),
theta_off: S::from_f64(rpp * 0.6),
});
SrmModel::new(
6, 4, params,
commutation,
S::from_f64(5e-5), S::from_f64(1e-4), )
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn srm_64_construction_succeeds() {
let srm = srm_6_4_default::<f64>();
assert!(srm.is_ok());
}
#[test]
fn invalid_stator_poles_returns_error() {
let params: [SrmPhaseParams<f64>; 3] = core::array::from_fn(|_| SrmPhaseParams {
l_min: 5e-3,
l_max: 30e-3,
k_sat: 0.05,
r_phase: 1.2,
i_rated: 5.0,
});
let comm: [CommutationAngles<f64>; 3] = core::array::from_fn(|_| CommutationAngles {
theta_on: 0.1,
theta_off: 0.9,
});
let result: Result<SrmModel<f64, 3>, _> = SrmModel::new(3, 4, params, comm, 5e-5, 1e-4); assert_eq!(result.unwrap_err(), SrmError::InvalidStatorPoles);
}
#[test]
fn invalid_inductance_returns_error() {
let mut params: [SrmPhaseParams<f64>; 3] = core::array::from_fn(|_| SrmPhaseParams {
l_min: 5e-3,
l_max: 30e-3,
k_sat: 0.05,
r_phase: 1.2,
i_rated: 5.0,
});
params[0].l_min = 40e-3; let comm: [CommutationAngles<f64>; 3] = core::array::from_fn(|_| CommutationAngles {
theta_on: 0.1,
theta_off: 0.9,
});
let result: Result<SrmModel<f64, 3>, _> = SrmModel::new(6, 4, params, comm, 5e-5, 1e-4);
assert_eq!(result.unwrap_err(), SrmError::InvalidInductance);
}
#[test]
fn inductance_in_bounds() {
let srm = srm_6_4_default::<f64>().expect("construction ok");
for i_ph in 0..3 {
for angle_step in 0..100 {
let theta = angle_step as f64 * core::f64::consts::PI / 50.0;
let l = srm.inductance(i_ph, theta, 2.0).expect("valid phase");
let lmin = srm.phase_params[i_ph].l_min;
let lmax = srm.phase_params[i_ph].l_max;
assert!(l >= lmin * 0.99, "L={} < L_min={}", l, lmin);
assert!(l <= lmax * 1.01, "L={} > L_max={}", l, lmax);
}
}
}
#[test]
fn step_accelerates_motor_with_applied_voltage() {
let mut srm = srm_6_4_default::<f64>().expect("ok");
let voltages = [100.0_f64; 3];
for _ in 0..1000 {
srm.step(&voltages, 0.0, 1e-5);
}
assert!(
srm.omega_mech() > 0.0,
"motor should accelerate, got ω={}",
srm.omega_mech()
);
}
#[test]
fn phase_current_non_negative() {
let mut srm = srm_6_4_default::<f64>().expect("ok");
let voltages = [50.0_f64; 3];
for _ in 0..500 {
srm.step(&voltages, 0.0, 1e-5);
for ph in 0..3 {
assert!(
srm.phase_current(ph) >= 0.0,
"phase {} current negative: {}",
ph,
srm.phase_current(ph)
);
}
}
}
#[test]
fn reset_restores_zero_state() {
let mut srm = srm_6_4_default::<f64>().expect("ok");
let voltages = [100.0_f64; 3];
for _ in 0..500 {
srm.step(&voltages, 0.0, 1e-5);
}
srm.reset();
assert_eq!(srm.omega_mech(), 0.0);
assert_eq!(srm.theta_mech(), 0.0);
for ph in 0..3 {
assert_eq!(srm.phase_current(ph), 0.0);
}
}
#[test]
fn out_of_range_phase_returns_error() {
let srm = srm_6_4_default::<f64>().expect("ok");
let result = srm.inductance(10, 0.0, 1.0);
assert_eq!(result.unwrap_err(), SrmError::InvalidPhaseIndex);
}
}