powerio/format/powerworld/objects.rs
1//! Typed views over aux object types the transmission core does not model.
2//!
3//! Contingencies, limit sets, and rating set names matter to transmission
4//! studies even though [`crate::network::Network`] does not model them. They
5//! are retained losslessly by the generic layer ([`super::AuxFile`]); the
6//! views here give them names and structure. Everything stays read only: the
7//! data round trips through the retained source, untouched.
8
9use super::auxiliary::AuxFile;
10
11/// One contingency from a `Contingency` DATA section, with the actions of its
12/// `CTGElement` SUBDATA.
13#[derive(Debug, Clone, PartialEq)]
14pub struct Contingency {
15 /// `CTGLabel`, the contingency's unique name.
16 pub label: String,
17 /// One entry per CTGElement line: the action string PowerWorld calls
18 /// "WhoAmI + action", e.g. `BRANCH 2 1 1 OPEN`.
19 pub actions: Vec<String>,
20}
21
22/// The contingencies of a parsed aux file, in file order.
23///
24/// Empty when the file carries no `Contingency` sections. Rows with no
25/// `CTGLabel` field are skipped (a contingency without a name is not
26/// addressable).
27#[must_use]
28pub fn contingencies(aux: &AuxFile) -> Vec<Contingency> {
29 let mut out = Vec::new();
30 for blk in aux.data_of("Contingency") {
31 let Some(label_at) = blk.field_index("CTGLabel") else {
32 continue;
33 };
34 for row in &blk.rows {
35 let Some(label) = row.values.get(label_at) else {
36 continue;
37 };
38 let actions = row
39 .subdata
40 .iter()
41 .filter(|s| s.name.eq_ignore_ascii_case("CTGElement"))
42 .flat_map(|s| s.lines.iter())
43 .filter_map(|line| first_quoted(line))
44 .map(str::to_string)
45 .collect();
46 out.push(Contingency {
47 label: label.clone(),
48 actions,
49 });
50 }
51 }
52 out
53}
54
55/// Name → row lookup for the per object-type rating set names
56/// (`RatingSetNameBus`, `RatingSetNameBranch`, `RatingSetNameInterface`).
57/// Returns `(set_number, name)` pairs in file order.
58#[must_use]
59pub fn rating_set_names(aux: &AuxFile, object_type: &str) -> Vec<(usize, String)> {
60 let mut out = Vec::new();
61 for blk in aux.data_of(object_type) {
62 let (Some(num_at), Some(name_at)) = (
63 blk.field_index("RatingSetNum"),
64 blk.field_index("RatingSetName"),
65 ) else {
66 continue;
67 };
68 for row in &blk.rows {
69 if let (Some(num), Some(name)) = (row.values.get(num_at), row.values.get(name_at)) {
70 if let Ok(n) = num.trim().parse() {
71 out.push((n, name.clone()));
72 }
73 }
74 }
75 }
76 out
77}
78
79/// The interior of the first `"..."` on a CTGElement line: its action string.
80fn first_quoted(line: &str) -> Option<&str> {
81 let start = line.find('"')? + 1;
82 let end = start + line[start..].find('"')?;
83 Some(line[start..end].trim())
84}