use std::collections::HashMap;
use surge_network::Network;
use surge_network::network::protection::{
CurrentRelaySettings, DistanceRelaySettings, ProtectionData, RecloseSequenceData, RecloseShot,
SynchrocheckSettings,
};
use super::indices::CgmesIndices;
use super::types::ObjMap;
fn resolve_bus(objects: &ObjMap, idx: &CgmesIndices, eq_id: &str) -> Option<u32> {
idx.terminals(eq_id).iter().find_map(|tid| {
let tn = idx.terminal_tn(objects, tid)?;
idx.tn_bus(tn)
})
}
pub(crate) fn build_protection_data(objects: &ObjMap, idx: &CgmesIndices, network: &mut Network) {
let mut data = ProtectionData::default();
for (id, obj) in objects.iter().filter(|(_, o)| o.class == "CurrentRelay") {
let name = obj.get_text("name").unwrap_or_default().to_string();
let bus = resolve_bus(objects, idx, id);
let protected_switch_mrid = obj.get_ref("ProtectedSwitch").map(|s| s.to_string());
let inverse_time = obj
.get_text("inverseTimeFlag")
.map(|s| s == "true")
.unwrap_or(false);
let directional = obj
.get_text("powerDirectionFlag")
.map(|s| s == "true")
.unwrap_or(false);
data.current_relays.push(CurrentRelaySettings {
mrid: id.clone(),
name,
phase_pickup_a: obj.parse_f64("currentLimit1"),
ground_pickup_a: obj.parse_f64("currentLimit2"),
neg_seq_pickup_a: obj.parse_f64("currentLimit3"),
phase_time_dial_s: obj.parse_f64("timeDelay1"),
ground_time_dial_s: obj.parse_f64("timeDelay2"),
neg_seq_time_dial_s: obj.parse_f64("timeDelay3"),
inverse_time,
directional,
bus,
protected_switch_mrid,
});
}
for (id, obj) in objects.iter().filter(|(_, o)| o.class == "DistanceRelay") {
let name = obj.get_text("name").unwrap_or_default().to_string();
let bus = resolve_bus(objects, idx, id);
let protected_switch_mrid = obj.get_ref("ProtectedSwitch").map(|s| s.to_string());
data.distance_relays.push(DistanceRelaySettings {
mrid: id.clone(),
name,
forward_reach_ohm: obj.parse_f64("forwardReach"),
forward_blind_ohm: obj.parse_f64("forwardBlind"),
backward_reach_ohm: obj.parse_f64("backwardReach"),
backward_blind_ohm: obj.parse_f64("backwardBlind"),
mho_angle_deg: obj.parse_f64("operationPhaseAngle"),
zero_seq_rx_ratio: obj.parse_f64("zeroSeqRXRatio"),
zero_seq_reach_ohm: obj.parse_f64("zeroSeqReach"),
bus,
protected_switch_mrid,
});
}
let mut reclose_map: HashMap<String, Vec<RecloseShot>> = HashMap::new();
for (_id, obj) in objects.iter().filter(|(_, o)| o.class == "RecloseSequence") {
let switch_mrid = match obj.get_ref("ProtectedSwitch") {
Some(s) => s.to_string(),
None => continue,
};
let step = obj.parse_f64("recloseStep").unwrap_or(1.0) as u32;
let delay_s = obj.parse_f64("recloseDelay").unwrap_or(0.0);
reclose_map
.entry(switch_mrid)
.or_default()
.push(RecloseShot { step, delay_s });
}
for (switch_mrid, mut shots) in reclose_map {
shots.sort_by_key(|s| s.step);
data.reclose_sequences.push(RecloseSequenceData {
protected_switch_mrid: switch_mrid,
shots,
});
}
for (id, obj) in objects
.iter()
.filter(|(_, o)| o.class == "SynchrocheckRelay")
{
let name = obj.get_text("name").unwrap_or_default().to_string();
let bus = resolve_bus(objects, idx, id);
let protected_switch_mrid = obj.get_ref("ProtectedSwitch").map(|s| s.to_string());
data.synchrocheck_relays.push(SynchrocheckSettings {
mrid: id.clone(),
name,
max_angle_diff_deg: obj.parse_f64("maxAngleDiff"),
max_freq_diff_hz: obj.parse_f64("maxFreqDiff"),
max_volt_diff_pu: obj.parse_f64("maxVoltDiff"),
bus,
protected_switch_mrid,
});
}
for (id, obj) in objects
.iter()
.filter(|(_, o)| o.class == "ProtectionEquipment")
{
let name = obj.get_text("name").unwrap_or_default().to_string();
let bus = resolve_bus(objects, idx, id);
let protected_switch_mrid = obj.get_ref("ProtectedSwitch").map(|s| s.to_string());
let directional = obj
.get_text("powerDirectionFlag")
.map(|s| s == "true")
.unwrap_or(false);
data.current_relays.push(CurrentRelaySettings {
mrid: id.clone(),
name,
phase_pickup_a: obj.parse_f64("highLimit"),
ground_pickup_a: obj.parse_f64("lowLimit"),
neg_seq_pickup_a: None,
phase_time_dial_s: obj.parse_f64("relayDelayTime"),
ground_time_dial_s: None,
neg_seq_time_dial_s: None,
inverse_time: false,
directional,
bus,
protected_switch_mrid,
});
}
if !data.is_empty() {
tracing::info!(
current_relays = data.current_relays.len(),
distance_relays = data.distance_relays.len(),
reclose_sequences = data.reclose_sequences.len(),
synchrocheck_relays = data.synchrocheck_relays.len(),
"Protection equipment parsed from CGMES"
);
network.cim.protection_data = data;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cgmes::types::{CimObj, CimVal};
fn text(s: &str) -> CimVal {
CimVal::Text(s.to_string())
}
fn refval(s: &str) -> CimVal {
CimVal::Ref(s.to_string())
}
fn make_test_env() -> (ObjMap, CgmesIndices) {
let mut objects: ObjMap = HashMap::new();
let mut tn = CimObj::new("TopologicalNode");
tn.attrs.insert("name".into(), text("TN1"));
objects.insert("tn1".into(), tn);
let mut term = CimObj::new("Terminal");
term.attrs.insert("TopologicalNode".into(), refval("tn1"));
term.attrs
.insert("ConductingEquipment".into(), refval("relay1"));
term.attrs.insert("sequenceNumber".into(), text("1"));
objects.insert("term1".into(), term);
let mut idx = CgmesIndices::build(&objects);
idx.tn_bus.insert("tn1".into(), 1);
(objects, idx)
}
#[test]
fn test_parse_current_relay() {
let (mut objects, idx) = make_test_env();
let mut relay = CimObj::new("CurrentRelay");
relay.attrs.insert("name".into(), text("OC_Relay_1"));
relay.attrs.insert("currentLimit1".into(), text("400.0"));
relay.attrs.insert("currentLimit2".into(), text("100.0"));
relay.attrs.insert("timeDelay1".into(), text("0.5"));
relay.attrs.insert("timeDelay2".into(), text("1.0"));
relay.attrs.insert("inverseTimeFlag".into(), text("true"));
relay.attrs.insert("ProtectedSwitch".into(), refval("brk1"));
objects.insert("relay1".into(), relay);
let mut network = Network::default();
build_protection_data(&objects, &idx, &mut network);
assert_eq!(network.cim.protection_data.current_relays.len(), 1);
let r = &network.cim.protection_data.current_relays[0];
assert_eq!(r.name, "OC_Relay_1");
assert_eq!(r.phase_pickup_a, Some(400.0));
assert_eq!(r.ground_pickup_a, Some(100.0));
assert_eq!(r.phase_time_dial_s, Some(0.5));
assert!(r.inverse_time);
assert_eq!(r.bus, Some(1));
assert_eq!(r.protected_switch_mrid.as_deref(), Some("brk1"));
}
#[test]
fn test_parse_distance_relay() {
let (mut objects, idx) = make_test_env();
let mut relay = CimObj::new("DistanceRelay");
relay.attrs.insert("name".into(), text("Z_Relay_1"));
relay.attrs.insert("forwardReach".into(), text("12.5"));
relay.attrs.insert("backwardReach".into(), text("3.0"));
relay
.attrs
.insert("operationPhaseAngle".into(), text("75.0"));
relay.attrs.insert("zeroSeqRXRatio".into(), text("2.5"));
objects.insert("relay1".into(), relay);
let mut network = Network::default();
build_protection_data(&objects, &idx, &mut network);
assert_eq!(network.cim.protection_data.distance_relays.len(), 1);
let d = &network.cim.protection_data.distance_relays[0];
assert_eq!(d.forward_reach_ohm, Some(12.5));
assert_eq!(d.backward_reach_ohm, Some(3.0));
assert_eq!(d.mho_angle_deg, Some(75.0));
assert_eq!(d.zero_seq_rx_ratio, Some(2.5));
assert_eq!(d.bus, Some(1));
}
#[test]
fn test_parse_reclose_sequence() {
let (mut objects, idx) = make_test_env();
let mut rs1 = CimObj::new("RecloseSequence");
rs1.attrs.insert("ProtectedSwitch".into(), refval("brk1"));
rs1.attrs.insert("recloseStep".into(), text("1"));
rs1.attrs.insert("recloseDelay".into(), text("0.3"));
objects.insert("rs1".into(), rs1);
let mut rs2 = CimObj::new("RecloseSequence");
rs2.attrs.insert("ProtectedSwitch".into(), refval("brk1"));
rs2.attrs.insert("recloseStep".into(), text("2"));
rs2.attrs.insert("recloseDelay".into(), text("15.0"));
objects.insert("rs2".into(), rs2);
let mut network = Network::default();
build_protection_data(&objects, &idx, &mut network);
assert_eq!(network.cim.protection_data.reclose_sequences.len(), 1);
let seq = &network.cim.protection_data.reclose_sequences[0];
assert_eq!(seq.protected_switch_mrid, "brk1");
assert_eq!(seq.shots.len(), 2);
assert_eq!(seq.shots[0].step, 1);
assert!((seq.shots[0].delay_s - 0.3).abs() < 1e-12);
assert_eq!(seq.shots[1].step, 2);
assert!((seq.shots[1].delay_s - 15.0).abs() < 1e-12);
}
#[test]
fn test_parse_synchrocheck_relay() {
let (mut objects, idx) = make_test_env();
let mut relay = CimObj::new("SynchrocheckRelay");
relay.attrs.insert("name".into(), text("SC_Relay_1"));
relay.attrs.insert("maxAngleDiff".into(), text("20.0"));
relay.attrs.insert("maxFreqDiff".into(), text("0.2"));
relay.attrs.insert("maxVoltDiff".into(), text("0.1"));
relay.attrs.insert("ProtectedSwitch".into(), refval("brk2"));
objects.insert("relay1".into(), relay);
let mut network = Network::default();
build_protection_data(&objects, &idx, &mut network);
assert_eq!(network.cim.protection_data.synchrocheck_relays.len(), 1);
let sc = &network.cim.protection_data.synchrocheck_relays[0];
assert_eq!(sc.name, "SC_Relay_1");
assert_eq!(sc.max_angle_diff_deg, Some(20.0));
assert_eq!(sc.max_freq_diff_hz, Some(0.2));
assert_eq!(sc.max_volt_diff_pu, Some(0.1));
assert_eq!(sc.bus, Some(1));
}
#[test]
fn test_parse_protection_equipment_generic() {
let (mut objects, idx) = make_test_env();
let mut pe = CimObj::new("ProtectionEquipment");
pe.attrs.insert("name".into(), text("GenericRelay"));
pe.attrs.insert("highLimit".into(), text("500.0"));
pe.attrs.insert("lowLimit".into(), text("50.0"));
pe.attrs.insert("relayDelayTime".into(), text("0.1"));
pe.attrs.insert("powerDirectionFlag".into(), text("true"));
objects.insert("relay1".into(), pe);
let mut network = Network::default();
build_protection_data(&objects, &idx, &mut network);
assert_eq!(network.cim.protection_data.current_relays.len(), 1);
let r = &network.cim.protection_data.current_relays[0];
assert_eq!(r.name, "GenericRelay");
assert_eq!(r.phase_pickup_a, Some(500.0));
assert_eq!(r.ground_pickup_a, Some(50.0));
assert_eq!(r.phase_time_dial_s, Some(0.1));
assert!(r.directional);
}
#[test]
fn test_empty_protection_data() {
let (objects, idx) = make_test_env();
let mut network = Network::default();
build_protection_data(&objects, &idx, &mut network);
assert!(network.cim.protection_data.is_empty());
}
#[test]
fn test_reclose_no_switch_skipped() {
let (mut objects, idx) = make_test_env();
let mut rs = CimObj::new("RecloseSequence");
rs.attrs.insert("recloseStep".into(), text("1"));
rs.attrs.insert("recloseDelay".into(), text("0.5"));
objects.insert("rs_orphan".into(), rs);
let mut network = Network::default();
build_protection_data(&objects, &idx, &mut network);
assert!(network.cim.protection_data.reclose_sequences.is_empty());
}
}