use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DtcLinearError {
InvalidPolePitch,
InvalidResistance,
InvalidHysteresis,
}
impl core::fmt::Display for DtcLinearError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidPolePitch => write!(f, "pole pitch must be > 0"),
Self::InvalidResistance => write!(f, "stator resistance must be >= 0"),
Self::InvalidHysteresis => write!(f, "hysteresis band must be > 0"),
}
}
}
#[derive(Debug, Clone, Copy)]
struct HysteresisComparator<S: ControlScalar> {
upper: S,
lower: S,
last: i8,
}
impl<S: ControlScalar> HysteresisComparator<S> {
fn new(band: S) -> Self {
Self {
upper: band,
lower: -band,
last: 0,
}
}
fn compare(&mut self, error: S) -> i8 {
if error > self.upper {
self.last = 1;
} else if error < self.lower {
self.last = -1;
}
self.last
}
}
#[derive(Debug, Clone, Copy)]
struct FluxHysteresis<S: ControlScalar> {
upper: S,
lower: S,
last: i8,
}
impl<S: ControlScalar> FluxHysteresis<S> {
fn new(band: S) -> Self {
Self {
upper: band,
lower: -band,
last: 1,
}
}
fn compare(&mut self, error: S) -> i8 {
if error > self.upper {
self.last = 1;
} else if error < self.lower {
self.last = 0;
}
self.last
}
}
#[derive(Debug, Clone, Copy)]
pub struct LinearFluxEstimator<S: ControlScalar> {
pub psi_alpha: S,
pub psi_beta: S,
drift_gain: S,
}
impl<S: ControlScalar> LinearFluxEstimator<S> {
pub fn new(drift_gain: S) -> Self {
Self {
psi_alpha: S::ZERO,
psi_beta: S::ZERO,
drift_gain,
}
}
pub fn update(&mut self, v_alpha: S, v_beta: S, i_alpha: S, i_beta: S, r_s: S, dt: S) {
let e_alpha = v_alpha - r_s * i_alpha - self.drift_gain * self.psi_alpha;
let e_beta = v_beta - r_s * i_beta - self.drift_gain * self.psi_beta;
self.psi_alpha += e_alpha * dt;
self.psi_beta += e_beta * dt;
}
pub fn magnitude(&self) -> S {
(self.psi_alpha * self.psi_alpha + self.psi_beta * self.psi_beta).sqrt()
}
pub fn angle(&self) -> S {
let angle = self.psi_beta.atan2(self.psi_alpha);
if angle < S::ZERO {
angle + S::PI * S::TWO
} else {
angle
}
}
pub fn sector(&self) -> usize {
let angle = self.angle();
let normalized = angle / (S::PI * S::TWO);
let sector_f = normalized * S::from_f64(6.0);
(sector_f.to_f64() as usize % 6) + 1
}
}
const SWITCHING_TABLE_LINEAR: [[[u8; 2]; 3]; 6] = [
[[5, 3], [7, 0], [2, 6]],
[[6, 4], [0, 7], [3, 1]],
[[1, 5], [7, 0], [4, 2]],
[[2, 6], [0, 7], [5, 3]],
[[3, 1], [7, 0], [6, 4]],
[[4, 2], [0, 7], [1, 5]],
];
fn select_vector_linear(sector: usize, df: i8, dpsi: i8) -> u8 {
let s = sector.saturating_sub(1).min(5);
let f_idx = match df {
i8::MIN..=-1 => 0,
0 => 1,
_ => 2,
};
let psi_idx = if dpsi <= 0 { 0usize } else { 1usize };
SWITCHING_TABLE_LINEAR[s][f_idx][psi_idx]
}
#[derive(Debug, Clone, Copy)]
pub struct DtcLinearState<S: ControlScalar> {
pub thrust_est: S,
pub flux_magnitude: S,
pub psi_alpha: S,
pub psi_beta: S,
pub voltage_vector: u8,
}
#[derive(Debug, Clone)]
pub struct DirectThrustController<S: ControlScalar> {
flux_est: LinearFluxEstimator<S>,
thrust_comparator: HysteresisComparator<S>,
flux_comparator: FluxHysteresis<S>,
r_s: S,
pole_pitch: S,
force_deadband: S,
thrust_est: S,
voltage_vector: u8,
}
impl<S: ControlScalar> DirectThrustController<S> {
pub fn new(
r_s: S,
pole_pitch: S,
thrust_hysteresis: S,
flux_hysteresis: S,
force_deadband: S,
drift_gain: S,
) -> Result<Self, DtcLinearError> {
if pole_pitch <= S::ZERO {
return Err(DtcLinearError::InvalidPolePitch);
}
if r_s < S::ZERO {
return Err(DtcLinearError::InvalidResistance);
}
if thrust_hysteresis <= S::ZERO || flux_hysteresis <= S::ZERO {
return Err(DtcLinearError::InvalidHysteresis);
}
Ok(Self {
flux_est: LinearFluxEstimator::new(drift_gain),
thrust_comparator: HysteresisComparator::new(thrust_hysteresis),
flux_comparator: FluxHysteresis::new(flux_hysteresis),
r_s,
pole_pitch,
force_deadband,
thrust_est: S::ZERO,
voltage_vector: 0,
})
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
v_alpha: S,
v_beta: S,
i_alpha: S,
i_beta: S,
force_ref: S,
flux_ref: S,
dt: S,
) -> u8 {
self.flux_est
.update(v_alpha, v_beta, i_alpha, i_beta, self.r_s, dt);
let pi_over_tau = S::PI / self.pole_pitch;
let three_half = S::from_f64(1.5);
self.thrust_est = three_half
* pi_over_tau
* (self.flux_est.psi_alpha * i_beta - self.flux_est.psi_beta * i_alpha);
let flux_mag = self.flux_est.magnitude();
let sector = self.flux_est.sector();
let flux_err = flux_ref - flux_mag;
let dpsi = self.flux_comparator.compare(flux_err);
let force_err = force_ref - self.thrust_est;
let force_err_abs = if force_err < S::ZERO {
-force_err
} else {
force_err
};
let vector = if force_err_abs <= self.force_deadband {
7u8
} else {
let df = self.thrust_comparator.compare(force_err);
select_vector_linear(sector, df, dpsi)
};
self.voltage_vector = vector;
vector
}
pub fn state(&self) -> DtcLinearState<S> {
DtcLinearState {
thrust_est: self.thrust_est,
flux_magnitude: self.flux_est.magnitude(),
psi_alpha: self.flux_est.psi_alpha,
psi_beta: self.flux_est.psi_beta,
voltage_vector: self.voltage_vector,
}
}
pub fn thrust_estimate(&self) -> S {
self.thrust_est
}
pub fn reset(&mut self) {
self.flux_est = LinearFluxEstimator::new(self.flux_est.drift_gain);
self.thrust_comparator.last = 0;
self.flux_comparator.last = 1;
self.thrust_est = S::ZERO;
self.voltage_vector = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_controller() -> DirectThrustController<f64> {
DirectThrustController::new(
0.5, 0.06, 5.0, 0.02, 1.0, 0.01, )
.expect("valid params")
}
#[test]
fn construction_succeeds_with_valid_params() {
let ctrl = make_controller();
assert_eq!(ctrl.thrust_estimate(), 0.0);
}
#[test]
fn invalid_pole_pitch_returns_error() {
let result = DirectThrustController::<f64>::new(0.5, 0.0, 5.0, 0.02, 1.0, 0.01);
assert_eq!(result.unwrap_err(), DtcLinearError::InvalidPolePitch);
}
#[test]
fn invalid_hysteresis_returns_error() {
let result = DirectThrustController::<f64>::new(0.5, 0.06, 0.0, 0.02, 1.0, 0.01);
assert_eq!(result.unwrap_err(), DtcLinearError::InvalidHysteresis);
}
#[test]
fn output_vector_is_in_valid_range() {
let mut ctrl = make_controller();
for step in 0..100 {
let t = step as f64 * 1e-4;
let v = 100.0 * libm::sin(2.0 * core::f64::consts::PI * 50.0 * t);
let vec = ctrl.update(v, v * 0.5, 2.0 * libm::cos(100.0 * t), 1.0, 50.0, 0.5, 1e-4);
assert!(vec <= 7, "vector {} out of range", vec);
}
}
#[test]
fn deadband_produces_zero_vector() {
let mut ctrl = DirectThrustController::<f64>::new(
0.1, 0.06, 5.0, 0.02, 1000.0, 0.01,
)
.expect("valid");
for _ in 0..20 {
let v = ctrl.update(1.0, 0.0, 0.1, 0.0, 0.0, 0.5, 1e-4);
assert!(v == 0 || v == 7, "expected zero vector, got {}", v);
}
}
#[test]
fn flux_estimator_integrates_correctly() {
let mut est = LinearFluxEstimator::<f64>::new(0.0);
for _ in 0..10 {
est.update(10.0, 0.0, 0.0, 0.0, 0.0, 1e-3);
}
assert!((est.psi_alpha - 0.1).abs() < 1e-9, "ψα = {}", est.psi_alpha);
}
#[test]
fn reset_clears_flux_estimate() {
let mut ctrl = make_controller();
for _ in 0..50 {
ctrl.update(50.0, 25.0, 2.0, 1.0, 20.0, 0.5, 1e-4);
}
ctrl.reset();
let state = ctrl.state();
assert_eq!(state.psi_alpha, 0.0);
assert_eq!(state.psi_beta, 0.0);
assert_eq!(state.thrust_est, 0.0);
}
}