use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PqDiagram {
pub p_points: Vec<f64>,
pub q_min: Vec<f64>,
pub q_max: Vec<f64>,
}
impl PqDiagram {
pub fn iec61400_wind() -> Self {
Self {
p_points: vec![0.0, 0.2, 0.5, 0.75, 1.0],
q_min: vec![-0.33, -0.33, -0.33, -0.33, -0.33],
q_max: vec![0.33, 0.33, 0.33, 0.33, 0.33],
}
}
pub fn entso_e_generator() -> Self {
Self {
p_points: vec![0.0, 0.2, 0.5, 0.8, 1.0],
q_min: vec![-0.20, -0.25, -0.30, -0.33, -0.33],
q_max: vec![0.20, 0.25, 0.30, 0.33, 0.40],
}
}
fn interpolate_q(p_points: &[f64], q_vals: &[f64], p_pu: f64) -> f64 {
let n = p_points.len();
if n == 0 {
return 0.0;
}
let p = p_pu.clamp(p_points[0], p_points[n - 1]);
if n == 1 {
return q_vals[0];
}
for i in 0..n - 1 {
let p0 = p_points[i];
let p1 = p_points[i + 1];
if p >= p0 && p <= p1 {
let alpha = if (p1 - p0).abs() < 1e-12 {
1.0
} else {
(p - p0) / (p1 - p0)
};
return q_vals[i] + alpha * (q_vals[i + 1] - q_vals[i]);
}
}
q_vals[n - 1]
}
pub fn q_max_at_p(&self, p_pu: f64) -> f64 {
Self::interpolate_q(&self.p_points, &self.q_max, p_pu)
}
pub fn q_min_at_p(&self, p_pu: f64) -> f64 {
Self::interpolate_q(&self.p_points, &self.q_min, p_pu)
}
pub fn within_capability(&self, p_pu: f64, q_pu: f64) -> bool {
q_pu >= self.q_min_at_p(p_pu) && q_pu <= self.q_max_at_p(p_pu)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReactiveRequirement {
pub voltage_range: (f64, f64),
pub power_factor_at_pcc: f64,
pub q_priority: bool,
}
impl ReactiveRequirement {
pub fn entso_e() -> Self {
Self {
voltage_range: (0.90, 1.10),
power_factor_at_pcc: 0.95,
q_priority: true,
}
}
pub fn check_compliance(&self, p_pu: f64, q_pu: f64, v_pu: f64) -> bool {
if v_pu < self.voltage_range.0 || v_pu > self.voltage_range.1 {
return false;
}
let s = (p_pu * p_pu + q_pu * q_pu).sqrt();
if s < 1e-9 {
return true; }
let pf = (p_pu / s).abs();
pf >= self.power_factor_at_pcc
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pq_diagram_within_capability() {
let pq = PqDiagram::iec61400_wind();
assert!(pq.within_capability(0.8, 0.2));
assert!(pq.within_capability(0.5, -0.33));
assert!(pq.within_capability(1.0, 0.33));
}
#[test]
fn test_pq_diagram_outside_capability() {
let pq = PqDiagram::iec61400_wind();
assert!(!pq.within_capability(0.8, 0.5));
assert!(!pq.within_capability(0.5, -0.5));
}
#[test]
fn test_q_max_at_full_power() {
let pq = PqDiagram::iec61400_wind();
let q_max = pq.q_max_at_p(1.0);
assert!((q_max - 0.33).abs() < 1e-9);
}
#[test]
fn test_q_min_at_zero_power() {
let pq = PqDiagram::iec61400_wind();
let q_min = pq.q_min_at_p(0.0);
assert!((q_min - (-0.33)).abs() < 1e-9);
}
#[test]
fn test_entso_e_generator_q_max_varies_with_p() {
let pq = PqDiagram::entso_e_generator();
let q_max_half = pq.q_max_at_p(0.5);
let q_max_full = pq.q_max_at_p(1.0);
assert!(
q_max_full > q_max_half,
"Q_max should increase with P for ENTSO-E generator"
);
}
#[test]
fn test_reactive_requirement_entso_e_compliant() {
let req = ReactiveRequirement::entso_e();
assert!(req.check_compliance(0.8, 0.1, 1.0));
}
#[test]
fn test_reactive_requirement_low_pf_fails() {
let req = ReactiveRequirement::entso_e();
assert!(!req.check_compliance(0.5, 0.5, 1.0));
}
#[test]
fn test_reactive_requirement_voltage_out_of_range() {
let req = ReactiveRequirement::entso_e();
assert!(!req.check_compliance(0.9, 0.1, 0.85));
assert!(!req.check_compliance(0.9, 0.1, 1.15));
}
}