Skip to main content

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}