use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct VsiConfig<S: ControlScalar> {
pub l1: S,
pub c: S,
pub l2: S,
pub rf: S,
pub v_grid: S,
pub omega_grid: S,
}
impl<S: ControlScalar> VsiConfig<S> {
pub fn new(l1: S, c: S, l2: S, rf: S, v_grid: S, omega_grid: S) -> Option<Self> {
if l1 <= S::ZERO || c <= S::ZERO || l2 <= S::ZERO {
return None;
}
Some(Self {
l1,
c,
l2,
rf,
v_grid,
omega_grid,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct VsiPlant<S: ControlScalar> {
pub config: VsiConfig<S>,
state: [S; 6],
t: S,
}
impl<S: ControlScalar> VsiPlant<S> {
pub fn new(config: VsiConfig<S>) -> Self {
Self {
config,
state: [S::ZERO; 6],
t: S::ZERO,
}
}
fn derivatives(&self, x: &[S; 6], v_inv_a: S, v_inv_b: S, theta: S) -> [S; 6] {
let cfg = &self.config;
let vg_a = cfg.v_grid * theta.cos();
let vg_b = cfg.v_grid * theta.sin();
let dil1_a = (v_inv_a - cfg.rf * x[0] - x[2]) / cfg.l1;
let dvc_a = (x[0] - x[4]) / cfg.c;
let dil2_a = (x[2] - vg_a) / cfg.l2;
let dil1_b = (v_inv_b - cfg.rf * x[1] - x[3]) / cfg.l1;
let dvc_b = (x[1] - x[5]) / cfg.c;
let dil2_b = (x[3] - vg_b) / cfg.l2;
[dil1_a, dil1_b, dvc_a, dvc_b, dil2_a, dil2_b]
}
pub fn step(&mut self, v_inv_alpha: S, v_inv_beta: S, dt: S) {
let theta0 = self.config.omega_grid * self.t;
let theta_half = self.config.omega_grid * (self.t + dt * S::HALF);
let theta_end = self.config.omega_grid * (self.t + dt);
let x = self.state;
let k1 = self.derivatives(&x, v_inv_alpha, v_inv_beta, theta0);
let x2 = add_scaled(&x, &k1, dt * S::HALF);
let k2 = self.derivatives(&x2, v_inv_alpha, v_inv_beta, theta_half);
let x3 = add_scaled(&x, &k2, dt * S::HALF);
let k3 = self.derivatives(&x3, v_inv_alpha, v_inv_beta, theta_half);
let x4 = add_scaled(&x, &k3, dt);
let k4 = self.derivatives(&x4, v_inv_alpha, v_inv_beta, theta_end);
let sixth = S::from_f64(1.0 / 6.0);
for i in 0..6 {
self.state[i] = x[i] + dt * sixth * (k1[i] + S::TWO * k2[i] + S::TWO * k3[i] + k4[i]);
}
self.t += dt;
}
pub fn i_grid_alpha(&self) -> S {
self.state[4]
}
pub fn i_grid_beta(&self) -> S {
self.state[5]
}
pub fn i_l1_alpha(&self) -> S {
self.state[0]
}
pub fn i_l1_beta(&self) -> S {
self.state[1]
}
pub fn vc_alpha(&self) -> S {
self.state[2]
}
pub fn vc_beta(&self) -> S {
self.state[3]
}
pub fn time(&self) -> S {
self.t
}
pub fn reset(&mut self) {
self.state = [S::ZERO; 6];
self.t = S::ZERO;
}
pub fn state(&self) -> &[S; 6] {
&self.state
}
}
#[derive(Debug, Clone, Copy)]
pub struct VsiCurrentController<S: ControlScalar> {
pub kp: S,
pub ki: S,
pub omega: S,
pub l2: S,
int_d: S,
int_q: S,
pub v_limit: S,
}
impl<S: ControlScalar> VsiCurrentController<S> {
pub fn new(kp: S, ki: S, omega: S, l2: S, v_limit: S) -> Self {
Self {
kp,
ki,
omega,
l2,
int_d: S::ZERO,
int_q: S::ZERO,
v_limit,
}
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
id_ref: S,
iq_ref: S,
id: S,
iq: S,
v_gd: S,
v_gq: S,
theta: S,
dt: S,
) -> (S, S) {
let e_d = id_ref - id;
let e_q = iq_ref - iq;
let int_d_new = self.int_d + e_d * dt;
let int_q_new = self.int_q + e_q * dt;
let cross_d = -self.omega * self.l2 * iq;
let cross_q = self.omega * self.l2 * id;
let vd = self.kp * e_d + self.ki * int_d_new + cross_d + v_gd;
let vq = self.kp * e_q + self.ki * int_q_new + cross_q + v_gq;
let vd_clamped = vd.clamp_val(-self.v_limit, self.v_limit);
let vq_clamped = vq.clamp_val(-self.v_limit, self.v_limit);
self.int_d = if vd_clamped == vd {
int_d_new
} else {
self.int_d
};
self.int_q = if vq_clamped == vq {
int_q_new
} else {
self.int_q
};
let cos_t = theta.cos();
let sin_t = theta.sin();
let v_alpha = vd_clamped * cos_t - vq_clamped * sin_t;
let v_beta = vd_clamped * sin_t + vq_clamped * cos_t;
(v_alpha, v_beta)
}
pub fn reset(&mut self) {
self.int_d = S::ZERO;
self.int_q = S::ZERO;
}
pub fn integrator_d(&self) -> S {
self.int_d
}
pub fn integrator_q(&self) -> S {
self.int_q
}
}
#[inline]
fn add_scaled<S: ControlScalar>(a: &[S; 6], b: &[S; 6], scale: S) -> [S; 6] {
[
a[0] + scale * b[0],
a[1] + scale * b[1],
a[2] + scale * b[2],
a[3] + scale * b[3],
a[4] + scale * b[4],
a[5] + scale * b[5],
]
}
pub fn park_transform<S: ControlScalar>(i_alpha: S, i_beta: S, theta: S) -> (S, S) {
let cos_t = theta.cos();
let sin_t = theta.sin();
let id = i_alpha * cos_t + i_beta * sin_t;
let iq = -i_alpha * sin_t + i_beta * cos_t;
(id, iq)
}
#[cfg(test)]
mod tests {
use super::*;
use core::f64::consts::PI;
fn default_config() -> VsiConfig<f64> {
VsiConfig::new(
3e-3, 10e-6, 1e-3, 0.1, 230.0, 2.0 * PI * 50.0,
)
.expect("valid config")
}
#[test]
fn vsi_plant_bounded_under_grid_excitation() {
let cfg = default_config();
let mut plant = VsiPlant::new(cfg);
let dt = 1e-5_f64;
let omega = 2.0 * PI * 50.0;
let v_amp = 230.0_f64;
for k in 0..10_000 {
let t = k as f64 * dt;
let v_a = v_amp * (omega * t).cos();
let v_b = v_amp * (omega * t).sin();
plant.step(v_a, v_b, dt);
}
let i_a = plant.i_grid_alpha();
let i_b = plant.i_grid_beta();
assert!(i_a.is_finite(), "i_grid_alpha not finite: {i_a}");
assert!(i_b.is_finite(), "i_grid_beta not finite: {i_b}");
assert!(i_a.abs() < 200.0, "i_grid_alpha={i_a:.3} A exceeds bound");
}
#[test]
fn vsi_current_tracking_step_response() {
let cfg = VsiConfig::new(
3e-3, 10e-6, 1e-3, 0.1, 0.0, 2.0 * PI * 50.0,
)
.expect("valid config");
let mut plant = VsiPlant::new(cfg);
let dt = 1e-6_f64;
let v_dc = 100.0_f64;
for _ in 0..1000 {
plant.step(v_dc, 0.0, dt);
}
let i_final = plant.i_grid_alpha();
assert!(
i_final > 0.0,
"i_grid_alpha={i_final:.4} A should be positive with positive DC drive"
);
assert!(
i_final < 200.0,
"i_grid_alpha={i_final:.4} A exceeds physical bound"
);
}
#[test]
fn vsi_reactive_power_sign() {
let cfg = VsiConfig::new(
3e-3,
10e-6,
1e-3,
10.0, 0.0, 2.0 * PI * 50.0,
)
.expect("valid config");
let mut plant = VsiPlant::new(cfg);
let dt = 1e-5_f64;
let omega = 2.0 * PI * 50.0;
let v_amp = 50.0_f64;
let steps_prime = (5.0 / (50.0 * dt)) as usize;
for k in 0..steps_prime {
let t = k as f64 * dt;
plant.step(0.0, v_amp * (omega * t).sin(), dt);
}
let steps_measure = (2.0 / (50.0 * dt)) as usize;
let mut i_beta_sum = 0.0_f64;
let mut count = 0usize;
for k in 0..steps_measure {
let t = (steps_prime + k) as f64 * dt;
plant.step(0.0, v_amp * (omega * t).sin(), dt);
i_beta_sum += plant.i_grid_beta() * (omega * t).sin();
count += 1;
}
let i_beta_fund = i_beta_sum / count as f64;
assert!(
i_beta_fund.abs() > 0.001,
"fundamental β current = {i_beta_fund:.4} A should be non-zero under β drive"
);
}
#[test]
fn vsi_config_rejects_invalid() {
assert!(VsiConfig::<f64>::new(0.0, 10e-6, 1e-3, 0.1, 230.0, 314.16).is_none());
assert!(VsiConfig::<f64>::new(3e-3, 0.0, 1e-3, 0.1, 230.0, 314.16).is_none());
assert!(VsiConfig::<f64>::new(3e-3, 10e-6, 0.0, 0.1, 230.0, 314.16).is_none());
}
#[test]
fn vsi_controller_reset() {
let mut ctrl = VsiCurrentController::<f64>::new(10.0, 500.0, 314.16, 1e-3, 400.0);
let theta = 0.5_f64;
ctrl.update(10.0, 0.0, 0.0, 0.0, 230.0, 0.0, theta, 1e-4);
ctrl.reset();
assert_eq!(ctrl.integrator_d(), 0.0);
assert_eq!(ctrl.integrator_q(), 0.0);
}
}