mod read;
mod write;
mod xml;
use std::f64::consts::PI;
use crate::model::ConductorMatrix;
pub(crate) use read::parse_cim_collecting;
pub(crate) use write::emit_cim_dist_text;
const DISTRIBUTION_MARKERS: [&[u8]; 10] = [
b"ACLineSegmentPhase",
b"EnergyConsumerPhase",
b"EnergySourcePhase",
b"SwitchPhase",
b"ShuntCompensatorPhase",
b"PerLengthPhaseImpedance",
b"PhaseImpedanceData",
b"TransformerTank",
b"PowerElectronicsConnectionPhase",
b"ConcentricNeutralCableInfo",
];
pub const CIM_DIST_HEAD_BYTES: usize = 16 * 1024;
#[must_use]
pub fn looks_like_cim_dist_header(head: &[u8]) -> bool {
let head = &head[..head.len().min(CIM_DIST_HEAD_BYTES)];
has(head, b"22-rdf-syntax-ns#")
&& (has(head, b"CIM100#") || has(head, b"CIM-schema-cim"))
&& !has(head, b"FullModel")
}
#[must_use]
pub fn looks_like_cim_dist(bytes: &[u8]) -> bool {
looks_like_cim_dist_header(bytes)
&& DISTRIBUTION_MARKERS.iter().any(|marker| has(bytes, marker))
}
fn has(haystack: &[u8], needle: &[u8]) -> bool {
memchr::memmem::find(haystack, needle).is_some()
}
fn is_case_document(entry: &str) -> bool {
!entry.contains('/')
&& std::path::Path::new(entry)
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("xml"))
}
#[must_use]
pub fn source_looks_like_cim_dist(source: &powerio_core::Source) -> bool {
if !source.is_directory() {
return source
.primary_buffer()
.is_ok_and(|buffer| looks_like_cim_dist(buffer.content_bytes()));
}
let Ok(entries) = source.entry_names() else {
return false;
};
entries.iter().any(|entry| {
is_case_document(entry.as_str())
&& source
.root_buffer(entry.as_str())
.is_ok_and(|buffer| looks_like_cim_dist(buffer.content_bytes()))
})
}
fn z_base(v_ref: f64, s_rating: f64) -> Option<f64> {
(v_ref > 0.0 && s_rating > 0.0).then_some(v_ref * v_ref / s_rating)
}
fn sequence_to_phase(z0: f64, z1: f64, n: usize) -> ConductorMatrix {
let self_z = (z0 + 2.0 * z1) / 3.0;
let mutual = (z0 - z1) / 3.0;
let mut m = vec![vec![mutual; n]; n];
for (i, row) in m.iter_mut().enumerate() {
row[i] = self_z;
}
m
}
fn wrap_angle(angle: f64) -> f64 {
let mut a = angle % (2.0 * PI);
if a <= -PI {
a += 2.0 * PI;
} else if a > PI {
a -= 2.0 * PI;
}
a
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::{is_case_document, looks_like_cim_dist, sequence_to_phase, wrap_angle};
#[test]
#[allow(clippy::float_cmp)]
fn sequence_expands_to_a_symmetric_phase_matrix() {
let m = sequence_to_phase(6.0, 3.0, 3);
assert_eq!(m[0][0], 4.0);
assert_eq!(m[0][1], 1.0);
assert_eq!(m[1][0], 1.0);
}
#[test]
fn angles_wrap_into_the_half_open_interval() {
assert!((wrap_angle(-4.0 * PI / 3.0) - 2.0 * PI / 3.0).abs() < 1e-12);
assert!((wrap_angle(PI) - PI).abs() < 1e-12);
assert!((wrap_angle(-PI) - PI).abs() < 1e-12);
}
#[test]
fn a_case_reads_only_the_top_level_xml_documents() {
assert!(is_case_document("feeder.xml"));
assert!(is_case_document("feeder.XML"));
assert!(!is_case_document("archive/feeder.xml"));
assert!(!is_case_document("notes.txt"));
}
const RDF: &str = r#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cim="http://iec.ch/TC57/CIM100#">"#;
#[test]
fn a_distribution_document_is_recognized_by_its_per_phase_classes() {
let doc = format!("{RDF}<cim:ACLineSegmentPhase rdf:ID=\"_p\"/></rdf:RDF>");
assert!(looks_like_cim_dist(doc.as_bytes()));
}
#[test]
fn a_cgmes_profile_or_a_document_without_markers_is_not() {
let header = format!(
"{RDF}<md:FullModel rdf:about=\"urn:uuid:1\"/><cim:ACLineSegmentPhase rdf:ID=\"_p\"/></rdf:RDF>"
);
assert!(!looks_like_cim_dist(header.as_bytes()));
let balanced = format!("{RDF}<cim:ACLineSegment rdf:ID=\"_l\"/></rdf:RDF>");
assert!(!looks_like_cim_dist(balanced.as_bytes()));
assert!(!looks_like_cim_dist(
b"<network xmlns=\"http://www.powsybl.org/schema/iidm/1_0\"/>"
));
}
}