use cimmodel::base::FastMap as HashMap;
use cimmodel::CimDataset;
use crate::Violation;
use super::Fields;
pub fn validate(dataset: &CimDataset) -> Vec<Violation> {
let mut v = Vec::new();
v.extend(check_terminal_phases_consistency_topological_node(dataset));
v.extend(check_switch_same_topological_node(dataset));
v.extend(check_terminal_exch8_topological_node(dataset));
v
}
fn check_terminal_phases_consistency_topological_node(dataset: &CimDataset) -> Vec<Violation> {
const ABCN: &str = "PhaseCode.ABCN";
const N: &str = "PhaseCode.N";
const ABC: &str = "PhaseCode.ABC";
let mut node_terminals: HashMap<String, Vec<(String, String)>> = HashMap::default();
for mrid in dataset.by_type.get("Terminal").into_iter().flatten() {
let entry = &dataset.entries[mrid];
let term = match Fields::of_class(entry, "Terminal") {
Some(t) => t, None => continue,
};
let tn_id = match term.reference("Terminal.TopologicalNode") {
Some(r) => r.trim_start_matches('#').to_string(), None => continue,
};
let phase = term.enumeration("Terminal.phases").map_or(String::new(), str::to_string);
node_terminals.entry(tn_id).or_default().push((mrid.clone(), phase));
}
let mut v = Vec::new();
'outer: for (node_id, terms) in &node_terminals {
if terms.len() < 2 { continue; }
for i in 0..terms.len() {
for j in (0..terms.len()).filter(|&j| j != i) {
let val1 = &terms[i].1;
let val2 = &terms[j].1;
let failed = if !val1.is_empty() && !val2.is_empty() {
if (val1 == ABCN || val1 == N) && val2 != ABCN && val2 != N { true }
else { val1 == ABC && val2 != ABC }
} else if !val1.is_empty() && val2.is_empty() {
val1 == ABCN || val1 == N
} else {
false
};
if failed {
v.push(Violation {
object_id: node_id.clone(),
rule_id: "tpn301:Terminal.phases-consistencyTopologicalNode".into(),
name: "C:301:TP:Terminal.phases:consistencyTopologicalNode".into(),
class: "TopologicalNode".into(),
property: "Terminal.phases".into(),
message: format!("The phase codes for the connected terminals are not consistent. Terminal {} code: {}, Terminal {} code: {}.",
terms[i].0, val1, terms[j].0, val2),
severity: "sh:Violation".into(),
description: String::new(),
});
continue 'outer;
}
}
}
}
v
}
fn get_tn_for_terminal(term: &Fields, dataset: &CimDataset) -> Option<String> {
if let Some(tn_ref) = term.reference("Terminal.TopologicalNode") {
return Some(tn_ref.trim_start_matches('#').to_string());
}
let cn_id = term.reference("Terminal.ConnectivityNode")?.trim_start_matches('#').to_string();
let cn = Fields::get(dataset, &cn_id, "ConnectivityNode")?;
Some(cn.reference("ConnectivityNode.TopologicalNode")?.trim_start_matches('#').to_string())
}
fn check_switch_same_topological_node(dataset: &CimDataset) -> Vec<Violation> {
let mut eq_terminals: HashMap<String, Vec<String>> = HashMap::default();
for mrid in dataset.by_type.get("Terminal").into_iter().flatten() {
let entry = &dataset.entries[mrid];
if let Some(term) = Fields::of_class(entry, "Terminal")
&& let Some(ce_ref) = term.reference("Terminal.ConductingEquipment") {
eq_terminals.entry(ce_ref.trim_start_matches('#').to_string())
.or_default().push(mrid.clone());
}
}
const SWITCHES: &[&str] = &[
"Switch", "Disconnector", "Fuse", "Jumper", "Cut", "GroundDisconnector",
"LoadBreakSwitch", "Breaker", "DisconnectingCircuitBreaker",
];
let mut v = Vec::new();
for class in SWITCHES {
for mrid in dataset.by_type.get(*class).into_iter().flatten() {
let Some(obj) = Fields::of_class(&dataset.entries[mrid], class) else { continue };
if obj.bool("Switch.retained") != Some(true) { continue; }
let terms = match eq_terminals.get(mrid) { Some(t) => t, None => continue };
let mut t1_tn: Option<String> = None;
let mut t2_tn: Option<String> = None;
for t_mrid in terms {
if let Some(term) = Fields::get(dataset, t_mrid, "Terminal") {
match term.i64("ACDCTerminal.sequenceNumber") {
Some(1) => t1_tn = get_tn_for_terminal(&term, dataset),
Some(2) => t2_tn = get_tn_for_terminal(&term, dataset),
_ => {}
}
}
}
if let (Some(tn1), Some(tn2)) = (t1_tn, t2_tn)
&& !tn1.is_empty() && tn1 == tn2 {
v.push(Violation {
object_id: mrid.clone(),
rule_id: "tpn456:Switch-sameTopologicalNode".into(),
name: "C:456:TP:Terminal:switch".into(),
class: class.to_string(),
property: "retained".into(),
message: "Terminals of retained Switch connect to the same TopologicalNode.".into(),
severity: "sh:Violation".into(),
description: String::new(),
});
}
}
}
v
}
fn check_terminal_exch8_topological_node(dataset: &CimDataset) -> Vec<Violation> {
let mut rc_terminals: cimmodel::base::FastSet<String> = cimmodel::base::FastSet::default();
for type_name in &["RegulatingControl", "TapChangerControl"] {
for mrid in dataset.by_type.get(*type_name).into_iter().flatten() {
let entry = &dataset.entries[mrid];
let rc_term = Fields::of_class(entry, type_name)
.and_then(|rc| rc.reference("RegulatingControl.Terminal").map(|r| r.trim_start_matches('#').to_string()));
if let Some(t_id) = rc_term { rc_terminals.insert(t_id); }
}
}
let mut v = Vec::new();
for mrid in dataset.by_type.get("Terminal").into_iter().flatten() {
if !rc_terminals.contains(mrid) { continue; }
let entry = &dataset.entries[mrid];
let term = match Fields::of_class(entry, "Terminal") {
Some(t) => t, None => continue,
};
if term.reference("Terminal.TopologicalNode").is_some() { continue; }
let has_tn = term.reference("Terminal.ConnectivityNode").is_some_and(|cn_ref| {
Fields::get(dataset, cn_ref.trim_start_matches('#'), "ConnectivityNode")
.is_some_and(|cn| cn.reference("ConnectivityNode.TopologicalNode").is_some())
});
if !has_tn {
v.push(Violation {
object_id: mrid.clone(),
rule_id: "tpn600:Terminal-EXCH8TopologicalNode".into(),
name: "C:600:EQ:Terminal:EXCH8TopologicalNode".into(),
class: "Terminal".into(),
property: "TopologicalNode".into(),
message: "The Terminal is referenced by a RegulatingControl but is not associated with a TopologicalNode.".into(),
severity: "sh:Violation".into(),
description: String::new(),
});
}
}
v
}