use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LvrtProfile {
pub name: String,
pub profile_points: Vec<(f64, f64)>,
pub reactive_injection_required: bool,
pub reactive_k_factor: f64,
}
impl LvrtProfile {
pub fn iec61400() -> Self {
Self {
name: "IEC 61400-21".to_string(),
profile_points: vec![
(0.0, 0.0),
(0.15, 0.0),
(0.15, 0.85),
(0.5, 0.85),
(1.5, 0.9),
(3.0, 1.0),
],
reactive_injection_required: true,
reactive_k_factor: 2.0,
}
}
pub fn entso_e() -> Self {
Self {
name: "ENTSO-E RfG".to_string(),
profile_points: vec![
(0.0, 0.0),
(0.14, 0.0),
(0.14, 0.85),
(0.45, 0.85),
(1.5, 0.9),
(3.0, 1.05),
],
reactive_injection_required: true,
reactive_k_factor: 2.0,
}
}
pub fn nerc() -> Self {
Self {
name: "NERC PRC-024-2".to_string(),
profile_points: vec![
(0.0, 0.0),
(0.625, 0.0),
(0.625, 0.15),
(3.0, 0.9),
(10.0, 0.9),
],
reactive_injection_required: false,
reactive_k_factor: 0.0,
}
}
pub fn bdew_mv() -> Self {
Self {
name: "BDEW MV".to_string(),
profile_points: vec![
(0.0, 0.0),
(0.15, 0.0),
(0.15, 0.8),
(0.7, 0.8),
(1.5, 0.9),
(3.0, 1.0),
],
reactive_injection_required: true,
reactive_k_factor: 2.0,
}
}
fn min_voltage_at(&self, time_s: f64) -> f64 {
if self.profile_points.is_empty() {
return 1.0;
}
if time_s <= self.profile_points[0].0 {
return self.profile_points[0].1;
}
let last = self.profile_points[self.profile_points.len() - 1];
if time_s >= last.0 {
return last.1;
}
for window in self.profile_points.windows(2) {
let (t0, v0) = window[0];
let (t1, v1) = window[1];
if time_s >= t0 && time_s <= t1 {
let alpha = if (t1 - t0).abs() < 1e-12 {
1.0
} else {
(time_s - t0) / (t1 - t0)
};
return v0 + alpha * (v1 - v0);
}
}
last.1
}
pub fn passes_lvrt(&self, event: &LvrtEvent) -> LvrtResult {
let mut violated_at: Option<f64> = None;
let mut max_duration_ok = 0.0_f64;
for &(t, v) in &event.time_profile {
let v_min = self.min_voltage_at(t);
if v >= v_min {
max_duration_ok = max_duration_ok.max(t);
} else if violated_at.is_none() {
violated_at = Some(t);
}
}
let compliant = violated_at.is_none();
let required_reactive: Vec<(f64, f64)> = if self.reactive_injection_required {
event
.time_profile
.iter()
.map(|&(t, v)| {
let q = compute_lvrt_reactive_injection(
v,
event.pre_fault_power_pu,
self.reactive_k_factor,
1.0,
);
(t, q)
})
.collect()
} else {
Vec::new()
};
LvrtResult {
compliant,
disconnection_required: !compliant,
max_duration_compliant_s: max_duration_ok,
reactive_support_required: self.reactive_injection_required,
required_reactive_pu: required_reactive,
violated_at_s: violated_at,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LvrtEvent {
pub time_profile: Vec<(f64, f64)>,
pub pre_fault_power_pu: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LvrtResult {
pub compliant: bool,
pub disconnection_required: bool,
pub max_duration_compliant_s: f64,
pub reactive_support_required: bool,
pub required_reactive_pu: Vec<(f64, f64)>,
pub violated_at_s: Option<f64>,
}
pub fn compute_lvrt_reactive_injection(
v_pu: f64,
v_pre_fault: f64,
k_factor: f64,
i_rated_pu: f64,
) -> f64 {
let delta_v = (v_pre_fault - v_pu).max(0.0);
(k_factor * delta_v * i_rated_pu).clamp(0.0, i_rated_pu)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lvrt_iec61400_passes() {
let profile = LvrtProfile::iec61400();
let event = LvrtEvent {
time_profile: vec![
(0.0, 0.0),
(0.05, 0.0),
(0.10, 0.0),
(0.12, 0.5),
(0.20, 0.85),
(0.50, 0.9),
],
pre_fault_power_pu: 1.0,
};
let result = profile.passes_lvrt(&event);
assert!(
result.compliant,
"100 ms zero-voltage event should pass IEC 61400-21 LVRT"
);
}
#[test]
fn test_lvrt_too_long_fails() {
let profile = LvrtProfile::iec61400();
let event = LvrtEvent {
time_profile: vec![
(0.0, 0.0),
(0.10, 0.0),
(0.20, 0.0), (0.30, 0.0),
(0.50, 0.85),
],
pre_fault_power_pu: 1.0,
};
let result = profile.passes_lvrt(&event);
assert!(
!result.compliant,
"200 ms zero-voltage should fail IEC 61400-21"
);
assert!(result.violated_at_s.is_some());
assert!(result.disconnection_required);
}
#[test]
fn test_lvrt_reactive_injection_proportional() {
let q = compute_lvrt_reactive_injection(0.5, 1.0, 2.0, 1.0);
assert!(
(q - 1.0).abs() < 1e-9,
"Reactive injection should be 1.0 pu: got {}",
q
);
let q2 = compute_lvrt_reactive_injection(0.9, 1.0, 2.0, 1.0);
assert!(
(q2 - 0.2).abs() < 1e-9,
"Reactive injection at 0.9 pu should be 0.2: got {}",
q2
);
}
#[test]
fn test_lvrt_nerc_zero_voltage_within_625ms() {
let profile = LvrtProfile::nerc();
let event = LvrtEvent {
time_profile: vec![
(0.0, 0.0),
(0.3, 0.0),
(0.6, 0.0), (1.0, 0.5),
(3.0, 0.9),
],
pre_fault_power_pu: 1.0,
};
let result = profile.passes_lvrt(&event);
assert!(result.compliant, "NERC allows zero voltage for 625 ms");
}
#[test]
fn test_lvrt_reactive_required_in_iec() {
let profile = LvrtProfile::iec61400();
assert!(profile.reactive_injection_required);
}
#[test]
fn test_lvrt_reactive_not_required_in_nerc() {
let profile = LvrtProfile::nerc();
assert!(!profile.reactive_injection_required);
}
}