use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct GridCurrentController<S: ControlScalar> {
pub kp_d: S,
pub ki_d: S,
pub kp_q: S,
pub ki_q: S,
pub l_filter: S,
pub v_max: S,
int_d: S,
int_q: S,
}
impl<S: ControlScalar> GridCurrentController<S> {
pub fn new(kp: S, ki: S, l_filter: S, v_max: S) -> Self {
Self {
kp_d: kp,
ki_d: ki,
kp_q: kp,
ki_q: ki,
l_filter,
v_max,
int_d: S::ZERO,
int_q: S::ZERO,
}
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
id_ref: S,
iq_ref: S,
id: S,
iq: S,
v_d: S,
v_q: S,
omega: S,
dt: S,
) -> (S, S) {
let e_d = id_ref - id;
let e_q = iq_ref - iq;
self.int_d += e_d * dt;
self.int_q += e_q * dt;
let pi_d = self.kp_d * e_d + self.ki_d * self.int_d;
let pi_q = self.kp_q * e_q + self.ki_q * self.int_q;
let v_d_ref = pi_d - omega * self.l_filter * iq + v_d;
let v_q_ref = pi_q + omega * self.l_filter * id + v_q;
let v_mag = (v_d_ref * v_d_ref + v_q_ref * v_q_ref).sqrt();
if v_mag > self.v_max {
let scale = self.v_max / v_mag;
self.int_d -= e_d * dt;
self.int_q -= e_q * dt;
return (v_d_ref * scale, v_q_ref * scale);
}
(v_d_ref, v_q_ref)
}
pub fn active_power_to_id(&self, p_ref: S, v_d: S) -> S {
let three_halves = S::from_f64(1.5);
if v_d.abs() > S::from_f64(1.0) {
p_ref / (three_halves * v_d)
} else {
S::ZERO
}
}
pub fn reactive_power_to_iq(&self, q_ref: S, v_d: S) -> S {
let three_halves = S::from_f64(1.5);
if v_d.abs() > S::from_f64(1.0) {
-q_ref / (three_halves * v_d)
} else {
S::ZERO
}
}
pub fn reset(&mut self) {
self.int_d = S::ZERO;
self.int_q = S::ZERO;
}
}
#[derive(Debug, Clone, Copy)]
pub struct IslandingDetector<S: ControlScalar> {
pub omega_nominal: S,
pub omega_tolerance: S,
pub v_under_threshold: S,
pub v_over_threshold: S,
islanded: bool,
}
impl<S: ControlScalar> IslandingDetector<S> {
pub fn new(omega_nominal: S, omega_tolerance: S) -> Self {
Self {
omega_nominal,
omega_tolerance,
v_under_threshold: S::from_f64(0.85),
v_over_threshold: S::from_f64(1.10),
islanded: false,
}
}
pub fn check(&mut self, omega_est: S, v_mag: S, v_nominal: S) -> bool {
let freq_fault = (omega_est - self.omega_nominal).abs() > self.omega_tolerance;
let v_ratio = if v_nominal > S::ZERO {
v_mag / v_nominal
} else {
S::ZERO
};
let volt_fault = v_ratio < self.v_under_threshold || v_ratio > self.v_over_threshold;
if freq_fault || volt_fault {
self.islanded = true;
}
self.islanded
}
pub fn is_islanded(&self) -> bool {
self.islanded
}
pub fn reset(&mut self) {
self.islanded = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_controller_drives_error_to_zero() {
let mut ctrl = GridCurrentController::new(10.0_f64, 200.0, 5e-3, 400.0);
let omega = 2.0 * core::f64::consts::PI * 50.0;
let v_d = 230.0_f64 * 1.414; let dt = 1e-4_f64;
let id_ref = 10.0_f64; let iq_ref = 0.0_f64;
let mut id = 0.0_f64;
let mut iq = 0.0_f64;
for _ in 0..5000 {
let (vd, vq) = ctrl.update(id_ref, iq_ref, id, iq, v_d, 0.0, omega, dt);
id += (vd - id) * dt / 5e-3;
iq += (vq - iq) * dt / 5e-3;
}
assert!((id - id_ref).abs() < 1.0, "id={:.2}, ref={:.2}", id, id_ref);
}
#[test]
fn power_to_current_conversion() {
let ctrl = GridCurrentController::new(10.0_f64, 200.0, 5e-3, 400.0);
let v_d = 325.0_f64; let p_ref = 1000.0_f64;
let id = ctrl.active_power_to_id(p_ref, v_d);
let p_actual = 1.5 * v_d * id;
assert!(
(p_actual - p_ref).abs() < 0.1,
"P={:.1}W (ref={:.1}W)",
p_actual,
p_ref
);
}
#[test]
fn islanding_detects_frequency_drift() {
let omega_nom = 2.0 * core::f64::consts::PI * 50.0_f64;
let mut det = IslandingDetector::new(omega_nom, 2.0);
assert!(!det.check(omega_nom + 0.5, 230.0, 230.0));
assert!(det.check(omega_nom + 3.0, 230.0, 230.0));
assert!(det.is_islanded());
}
#[test]
fn islanding_detects_undervoltage() {
let omega_nom = 2.0 * core::f64::consts::PI * 50.0_f64;
let mut det = IslandingDetector::new(omega_nom, 5.0);
assert!(det.check(omega_nom, 184.0, 230.0));
assert!(det.is_islanded());
}
#[test]
fn no_islanding_in_normal_operation() {
let omega_nom = 2.0 * core::f64::consts::PI * 50.0_f64;
let mut det = IslandingDetector::new(omega_nom, 5.0);
assert!(!det.check(omega_nom + 0.1, 228.0, 230.0));
assert!(!det.is_islanded());
}
}