use std::collections::HashMap;
use cimdecoder::CimDataset;
use crate::Violation;
pub fn validate(dataset: &CimDataset) -> Vec<Violation> {
let mut v = Vec::new();
v.extend(check_ac_line_segment_base_voltage(dataset));
v.extend(check_regulating_control_target_value_tap_changer(dataset));
v.extend(check_ac_line_segment_base_voltage_diff(dataset));
v.extend(check_boundary_point_bppl(dataset));
v.extend(check_equivalent_injection_regulation_capability_not_hvdc(dataset));
v
}
fn terminal_nominal_voltage(dataset: &CimDataset, term: &cimstructs::Terminal) -> Option<f64> {
let tn_id = term.topological_node.as_ref()?.mrid.trim_start_matches('#').to_string();
let tn = dataset.entries.get(&tn_id)?.element.as_any().downcast_ref::<cimstructs::TopologicalNode>()?;
let bv_id = tn.base_voltage.as_ref()?.mrid.trim_start_matches('#').to_string();
let bv = dataset.entries.get(&bv_id)?.element.as_any().downcast_ref::<cimstructs::BaseVoltage>()?;
bv.nominal_voltage
}
fn build_terminals_by_equipment_seq(dataset: &CimDataset) -> HashMap<String, HashMap<i64, String>> {
let mut map: HashMap<String, HashMap<i64, String>> = HashMap::new();
for mrid in dataset.by_type.get("Terminal").into_iter().flatten() {
let entry = &dataset.entries[mrid];
if let Some(term) = entry.element.as_any().downcast_ref::<cimstructs::Terminal>() {
if let Some(ce) = &term.conducting_equipment {
let eq_id = ce.mrid.trim_start_matches('#').to_string();
let seq = term.base.sequence_number.unwrap_or(0);
map.entry(eq_id).or_default().insert(seq, mrid.clone());
}
}
}
map
}
fn check_ac_line_segment_base_voltage(dataset: &CimDataset) -> Vec<Violation> {
let by_eq = build_terminals_by_equipment_seq(dataset);
let mut v = Vec::new();
for mrid in dataset.by_type.get("ACLineSegment").into_iter().flatten() {
let terms = match by_eq.get(mrid) { Some(t) => t, None => continue };
let t1_id = match terms.get(&1) { Some(id) => id, None => continue };
let t2_id = match terms.get(&2) { Some(id) => id, None => continue };
let t1 = match dataset.entries.get(t1_id).and_then(|e| e.element.as_any().downcast_ref::<cimstructs::Terminal>()) { Some(t) => t, None => continue };
let t2 = match dataset.entries.get(t2_id).and_then(|e| e.element.as_any().downcast_ref::<cimstructs::Terminal>()) { Some(t) => t, None => continue };
let v1 = match terminal_nominal_voltage(dataset, t1) { Some(v) => v, None => continue };
let v2 = match terminal_nominal_voltage(dataset, t2) { Some(v) => v, None => continue };
if t1.topological_node.as_ref().map(|r| r.mrid.as_str()) == t2.topological_node.as_ref().map(|r| r.mrid.as_str()) { continue; }
if v1 != v2 {
v.push(Violation {
object_id: mrid.clone(), rule_id: "eqn301:ACLineSegment-baseVoltage".into(),
name: "C:301:EQ:ACLineSegment:baseVoltage".into(), class: "ACLineSegment".into(),
property: "ACLineSegment.BaseVoltage".into(),
message: format!("The ACLineSegment has different BaseVoltage.nominalVoltage at the two ends. Voltage at end 1 is: {}. Voltage at end 2 is: {}.", v1, v2),
severity: "sh:Warning".into(), description: String::new(),
});
}
}
v
}
fn check_regulating_control_target_value_tap_changer(dataset: &CimDataset) -> Vec<Violation> {
let mut v = Vec::new();
let voltage_suffix = "voltage";
let mut rtc_by_tcc: HashMap<String, Vec<String>> = HashMap::new();
for rtc_mrid in dataset.by_type.get("RatioTapChanger").into_iter().flatten() {
let rtc_entry = &dataset.entries[rtc_mrid];
if let Some(rtc) = rtc_entry.element.as_any().downcast_ref::<cimstructs::RatioTapChanger>() {
if let Some(r) = &rtc.base.tap_changer_control {
let tcc_id = r.mrid.trim_start_matches('#').to_string();
rtc_by_tcc.entry(tcc_id).or_default().push(rtc_mrid.clone());
}
}
}
for rc_mrid in dataset.by_type.get("RegulatingControl").into_iter().chain(dataset.by_type.get("TapChangerControl").into_iter()).flatten() {
let rc_entry = &dataset.entries[rc_mrid];
let (mode_ok, enabled, target_value, terminal_ref) =
if let Some(rc) = rc_entry.element.as_any().downcast_ref::<cimstructs::RegulatingControl>() {
(rc.mode.as_ref().map_or(false, |r| r.uri.ends_with(voltage_suffix)), rc.enabled.unwrap_or(false), rc.target_value.unwrap_or(0.0), rc.terminal.as_ref())
} else if let Some(tcc) = rc_entry.element.as_any().downcast_ref::<cimstructs::TapChangerControl>() {
(tcc.base.mode.as_ref().map_or(false, |r| r.uri.ends_with(voltage_suffix)), tcc.base.enabled.unwrap_or(false), tcc.base.target_value.unwrap_or(0.0), tcc.base.terminal.as_ref())
} else {
continue
};
if !mode_ok || !enabled { continue; }
let term_id = match terminal_ref { Some(r) => r.mrid.trim_start_matches('#'), None => continue };
for rtc_mrid in rtc_by_tcc.get(rc_mrid).into_iter().flatten() {
let rtc_entry = &dataset.entries[rtc_mrid];
let rtc = match rtc_entry.element.as_any().downcast_ref::<cimstructs::RatioTapChanger>() { Some(r) => r, None => continue };
if !rtc.base.control_enabled.unwrap_or(false) { continue; }
let nominal_u = (|| -> Option<f64> {
let term = dataset.entries.get(term_id)?.element.as_any().downcast_ref::<cimstructs::Terminal>()?;
let cn_id = term.connectivity_node.as_ref()?.mrid.trim_start_matches('#');
let cn = dataset.entries.get(cn_id)?.element.as_any().downcast_ref::<cimstructs::ConnectivityNode>()?;
let cnc_id = cn.connectivity_node_container.as_ref()?.mrid.trim_start_matches('#');
let vl = dataset.entries.get(cnc_id)?.element.as_any().downcast_ref::<cimstructs::VoltageLevel>()?;
let bv_id = vl.base_voltage.as_ref()?.mrid.trim_start_matches('#');
dataset.entries.get(bv_id)?.element.as_any().downcast_ref::<cimstructs::BaseVoltage>()?.nominal_voltage
})();
let nominal_u = match nominal_u { Some(u) if u != 0.0 => u, _ => continue };
let target_pu = target_value / nominal_u;
let step_pct = rtc.step_voltage_increment.unwrap_or(0.0) / 100.0;
let high = rtc.base.high_step.unwrap_or(0);
let neutral = rtc.base.neutral_step.unwrap_or(0);
let low = rtc.base.low_step.unwrap_or(0);
let upper_limit = 1.0 + step_pct * (high - neutral) as f64;
let lower_limit = 1.0 - step_pct * (neutral - low) as f64;
if target_pu < lower_limit || target_pu > upper_limit {
v.push(Violation {
object_id: rc_mrid.clone(), rule_id: "eqn452:RegulatingControl.targetValue-tapChanger".into(),
name: "C:452:EQ:RegulatingControl.targetValue:tapChanger".into(), class: "RegulatingControl".into(),
property: "RegulatingControl.targetValue".into(),
message: format!("Target value PU ({}) is outside TapChanger capability limits [{}, {}].", target_pu, lower_limit, upper_limit),
severity: "sh:Violation".into(), description: String::new(),
});
}
}
}
v
}
fn check_ac_line_segment_base_voltage_diff(dataset: &CimDataset) -> Vec<Violation> {
let by_eq = build_terminals_by_equipment_seq(dataset);
let mut v = Vec::new();
for mrid in dataset.by_type.get("ACLineSegment").into_iter().flatten() {
let terms = match by_eq.get(mrid) { Some(t) => t, None => continue };
let t1_id = match terms.get(&1) { Some(id) => id, None => continue };
let t2_id = match terms.get(&2) { Some(id) => id, None => continue };
let t1 = match dataset.entries.get(t1_id).and_then(|e| e.element.as_any().downcast_ref::<cimstructs::Terminal>()) { Some(t) => t, None => continue };
let t2 = match dataset.entries.get(t2_id).and_then(|e| e.element.as_any().downcast_ref::<cimstructs::Terminal>()) { Some(t) => t, None => continue };
let v1 = match terminal_nominal_voltage(dataset, t1) { Some(v) => v, None => continue };
let v2 = match terminal_nominal_voltage(dataset, t2) { Some(v) => v, None => continue };
let diff = if v1 < v2 { (v2 - v1) / v1 } else { (v1 - v2) / v2 };
if diff > 0.1 {
v.push(Violation {
object_id: mrid.clone(), rule_id: "eqn600:ACLineSegment-BaseVoltageDiff".into(),
name: "C:600:EQ:ACLineSegment:BaseVoltageDiff".into(), class: "ACLineSegment".into(),
property: "rdf:type".into(),
message: format!("More than 10% difference of BaseVoltage.nominalVoltage at the two ends (V1: {}, V2: {}).", v1, v2),
severity: "sh:Violation".into(), description: String::new(),
});
}
}
v
}
fn check_boundary_point_bppl(dataset: &CimDataset) -> Vec<Violation> {
let mut bp_to_cn: HashMap<String, String> = HashMap::new();
for mrid in dataset.by_type.get("BoundaryPoint").into_iter().flatten() {
let entry = &dataset.entries[mrid];
if let Some(bp) = entry.element.as_any().downcast_ref::<cimstructs::BoundaryPoint>() {
if let Some(r) = &bp.connectivity_node {
bp_to_cn.insert(mrid.clone(), r.mrid.trim_start_matches('#').to_string());
}
}
}
let mut cn_terminals: HashMap<String, Vec<String>> = HashMap::new();
for mrid in dataset.by_type.get("Terminal").into_iter().flatten() {
let entry = &dataset.entries[mrid];
if let Some(term) = entry.element.as_any().downcast_ref::<cimstructs::Terminal>() {
if let Some(r) = &term.connectivity_node {
cn_terminals.entry(r.mrid.trim_start_matches('#').to_string()).or_default().push(mrid.clone());
}
}
}
let mut v = Vec::new();
for (_, cn_id) in &bp_to_cn {
let mut has_eq_injection = false;
let mut has_two_terminal = false;
for term_mrid in cn_terminals.get(cn_id).into_iter().flatten() {
let term = match dataset.entries.get(term_mrid).and_then(|e| e.element.as_any().downcast_ref::<cimstructs::Terminal>()) { Some(t) => t, None => continue };
let eq_id = match &term.conducting_equipment { Some(r) => r.mrid.trim_start_matches('#').to_string(), None => continue };
let eq_entry = match dataset.entries.get(&eq_id) { Some(e) => e, None => continue };
if eq_entry.element.as_any().downcast_ref::<cimstructs::EquivalentInjection>().is_some() { has_eq_injection = true; }
if eq_entry.element.as_any().downcast_ref::<cimstructs::ACLineSegment>().is_some()
|| eq_entry.element.as_any().downcast_ref::<cimstructs::PowerTransformer>().is_some()
|| eq_entry.element.as_any().downcast_ref::<cimstructs::Breaker>().is_some()
|| eq_entry.element.as_any().downcast_ref::<cimstructs::Disconnector>().is_some() { has_two_terminal = true; }
}
if !has_eq_injection {
v.push(Violation {
object_id: cn_id.clone(), rule_id: "eqn600:BoundaryPoint-bppl1Bppl2".into(),
name: "C:600:EQ:BoundaryPoint:bppl1Bppl2".into(), class: "ConnectivityNode".into(),
property: "rdf:type".into(),
message: "Boundary Point ConnectivityNode does not have an EquivalentInjection connected.".into(),
severity: "sh:Violation".into(), description: String::new(),
});
}
if !has_two_terminal {
v.push(Violation {
object_id: cn_id.clone(), rule_id: "eqn600:BoundaryPoint-bppl3".into(),
name: "C:600:EQ:BoundaryPoint:bppl3".into(), class: "ConnectivityNode".into(),
property: "rdf:type".into(),
message: "Boundary Point ConnectivityNode does not have a two-terminal ConductingEquipment connected.".into(),
severity: "sh:Info".into(), description: String::new(),
});
}
}
v
}
fn check_equivalent_injection_regulation_capability_not_hvdc(dataset: &CimDataset) -> Vec<Violation> {
let mut cn_is_dc: HashMap<String, bool> = HashMap::new();
for mrid in dataset.by_type.get("BoundaryPoint").into_iter().flatten() {
let entry = &dataset.entries[mrid];
if let Some(bp) = entry.element.as_any().downcast_ref::<cimstructs::BoundaryPoint>() {
if let Some(r) = &bp.connectivity_node {
cn_is_dc.insert(r.mrid.trim_start_matches('#').to_string(), bp.is_direct_current.unwrap_or(false));
}
}
}
let mut equip_non_hvdc_bp: HashMap<String, bool> = HashMap::new();
for term_mrid in dataset.by_type.get("Terminal").into_iter().flatten() {
let term = match dataset.entries.get(term_mrid).and_then(|e| e.element.as_any().downcast_ref::<cimstructs::Terminal>()) { Some(t) => t, None => continue };
let eq_id = match &term.conducting_equipment { Some(r) => r.mrid.trim_start_matches('#').to_string(), None => continue };
if let Some(cn) = &term.connectivity_node {
let cn_id = cn.mrid.trim_start_matches('#');
if let Some(&is_dc) = cn_is_dc.get(cn_id) {
if !is_dc {
equip_non_hvdc_bp.insert(eq_id, true);
}
}
}
}
let mut v = Vec::new();
for ei_mrid in dataset.by_type.get("EquivalentInjection").into_iter().flatten() {
let ei_entry = &dataset.entries[ei_mrid];
let ei = match ei_entry.element.as_any().downcast_ref::<cimstructs::EquivalentInjection>() { Some(e) => e, None => continue };
let is_non_hvdc_bp = equip_non_hvdc_bp.contains_key(ei_mrid.as_str());
if is_non_hvdc_bp {
if ei.regulation_capability.unwrap_or(false) || ei.reactive_capability_curve.is_some() {
v.push(Violation {
object_id: ei_mrid.clone(), rule_id: "eqn600:EquivalentInjection.regulationCapability-notHVDC".into(),
name: "C:600:EQ:EquivalentInjection.regulationCapability:notHvdc".into(), class: "EquivalentInjection".into(),
property: "EquivalentInjection.regulationCapability".into(),
message: "EquivalentInjection at non-HVDC BoundaryPoint has regulationCapability=true or a ReactiveCapabilityCurve.".into(),
severity: "sh:Violation".into(), description: String::new(),
});
}
}
}
v
}