use serde::{Deserialize, Serialize};
use crate::protection::relay::{OcRelay, RelayCharacteristic};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinationPair {
pub primary_idx: usize,
pub backup_idx: usize,
pub i_fault_a: f64,
pub t_primary_s: f64,
pub t_backup_s: f64,
pub margin_s: f64,
pub coordinated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinationViolation {
pub primary_idx: usize,
pub backup_idx: usize,
pub margin_s: f64,
pub required_cti_s: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoordinationStudy {
pub pairs: Vec<CoordinationPair>,
pub violations: Vec<CoordinationViolation>,
pub cti_s: f64,
}
impl CoordinationStudy {
pub fn is_fully_coordinated(&self) -> bool {
self.violations.is_empty()
}
pub fn n_violations(&self) -> usize {
self.violations.len()
}
}
pub fn check_coordination(
relays: &[OcRelay],
pairs: &[(usize, usize, f64)],
cti_s: f64,
) -> CoordinationStudy {
let mut coord_pairs = Vec::new();
let mut violations = Vec::new();
for &(pi, bi, i_fault) in pairs {
let t_p = relays[pi].trip_time(i_fault).unwrap_or(f64::INFINITY);
let t_b = relays[bi].trip_time(i_fault).unwrap_or(f64::INFINITY);
let margin = t_b - t_p;
let coordinated = margin >= cti_s;
if !coordinated {
violations.push(CoordinationViolation {
primary_idx: pi,
backup_idx: bi,
margin_s: margin,
required_cti_s: cti_s,
});
}
coord_pairs.push(CoordinationPair {
primary_idx: pi,
backup_idx: bi,
i_fault_a: i_fault,
t_primary_s: t_p,
t_backup_s: t_b,
margin_s: margin,
coordinated,
});
}
CoordinationStudy {
pairs: coord_pairs,
violations,
cti_s,
}
}
pub fn tcc_curve(relay: &OcRelay, i_min: f64, i_max: f64, n_points: usize) -> Vec<(f64, f64)> {
let log_min = i_min.ln();
let log_max = i_max.ln();
(0..n_points)
.filter_map(|i| {
let frac = i as f64 / (n_points - 1).max(1) as f64;
let current = (log_min + frac * (log_max - log_min)).exp();
relay
.trip_time(current)
.filter(|&t| t.is_finite() && t > 0.0)
.map(|time| (current, time))
})
.collect()
}
pub fn recommend_tms(relay: &OcRelay, t_target_s: f64, i_fault_a: f64) -> f64 {
if relay.i_pickup < 1e-10 || i_fault_a <= relay.i_pickup {
return relay.tms;
}
let m = i_fault_a / relay.i_pickup;
let (k, alpha): (f64, f64) = match relay.curve {
RelayCharacteristic::StandardInverse => (0.14, 0.02),
RelayCharacteristic::VeryInverse => (13.5, 1.0),
RelayCharacteristic::ExtremelyInverse => (80.0, 2.0),
RelayCharacteristic::LongTimeInverse => (120.0, 1.0),
_ => (0.14, 0.02), };
let denom = m.powf(alpha) - 1.0;
if denom < 1e-10 {
return relay.tms;
}
t_target_s * denom / k
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protection::relay::{OcRelay, RelayCharacteristic};
fn make_relay(pickup: f64, tms: f64) -> OcRelay {
OcRelay::new(pickup, tms, RelayCharacteristic::StandardInverse)
}
#[test]
fn test_coordinated_pair() {
let relays = vec![
make_relay(100.0, 0.10), make_relay(100.0, 0.30), ];
let study = check_coordination(&relays, &[(0, 1, 500.0)], 0.20);
assert!(
study.is_fully_coordinated(),
"Backup TMS=0.3 should coordinate with primary TMS=0.1"
);
}
#[test]
fn test_uncoordinated_pair() {
let relays = vec![
make_relay(100.0, 0.30), make_relay(100.0, 0.31), ];
let study = check_coordination(&relays, &[(0, 1, 500.0)], 0.20);
assert!(
!study.is_fully_coordinated(),
"Margin too small → violation expected"
);
assert_eq!(study.n_violations(), 1);
}
#[test]
fn test_margin_calculation() {
let relays = vec![make_relay(100.0, 0.10), make_relay(100.0, 0.40)];
let study = check_coordination(&relays, &[(0, 1, 500.0)], 0.20);
let pair = &study.pairs[0];
assert!((pair.margin_s - (pair.t_backup_s - pair.t_primary_s)).abs() < 1e-10);
assert!(pair.margin_s > 0.0);
}
#[test]
fn test_tcc_curve_length() {
let relay = make_relay(100.0, 0.20);
let curve = tcc_curve(&relay, 150.0, 5000.0, 20);
assert!(!curve.is_empty());
assert!(curve.len() <= 20);
}
#[test]
fn test_tcc_curve_monotone_decreasing() {
let relay = make_relay(100.0, 0.20);
let curve = tcc_curve(&relay, 200.0, 2000.0, 10);
for i in 1..curve.len() {
assert!(curve[i].0 > curve[i - 1].0, "Currents should be increasing");
assert!(
curve[i].1 < curve[i - 1].1 + 1e-6,
"Times should be decreasing"
);
}
}
#[test]
fn test_recommend_tms_positive() {
let relay = make_relay(100.0, 0.20);
let tms_new = recommend_tms(&relay, 0.60, 500.0);
assert!(
tms_new > 0.0,
"Recommended TMS should be positive: {:.4}",
tms_new
);
}
#[test]
fn test_multiple_pairs() {
let relays = vec![
make_relay(100.0, 0.10), make_relay(100.0, 0.35), make_relay(80.0, 0.15), make_relay(80.0, 0.40), ];
let study = check_coordination(&relays, &[(0, 1, 600.0), (2, 3, 400.0)], 0.20);
assert_eq!(study.pairs.len(), 2);
}
}