1use std::collections::BTreeSet;
23
24use super::expand::low_voltage_bus;
25use super::lexer::{LexedLine, LineKind, lex};
26use super::sub::{SubsystemSet, decimal};
27use super::{PsseEquipmentIndex, RetainedStatement, field, note_within_budget};
28use crate::diagnostics::{Diagnostic, codes};
29use crate::network::{BalancedNetwork, BusId};
30use crate::{Error, Result};
31
32const FMT: &str = "psse monitored elements";
33
34const KV_TOLERANCE: f64 = 1e-6;
36
37#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40pub struct MonitoredSet {
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub header: Vec<String>,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub statements: Vec<MonitorStatement>,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub retained: Vec<RetainedStatement>,
50}
51
52#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
54#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
55#[serde(tag = "kind", rename_all = "snake_case")]
56#[non_exhaustive]
57pub enum MonitorStatement {
58 BranchesInSubsystem {
60 subsystem: String,
61 low_voltage_3w: bool,
64 },
65 TiesFromSubsystem { subsystem: String },
67 Branches {
69 branches: Vec<BranchRef>,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 retained: Vec<RetainedStatement>,
74 },
75 Interface {
77 name: String,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 rating_mw: Option<f64>,
80 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 branches: Vec<BranchRef>,
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 retained: Vec<RetainedStatement>,
86 },
87 VoltageRange {
89 scope: MonitorScope,
90 vmin: f64,
91 vmax: f64,
92 },
93 VoltageDeviation {
96 scope: MonitorScope,
97 down: f64,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 up: Option<f64>,
100 },
101 BranchesInSubsystemKvRange {
103 subsystem: String,
104 low_voltage_3w: bool,
105 kv_range: (f64, f64),
106 },
107 TiesFromSubsystemKvRange {
109 subsystem: String,
110 kv_range: (f64, f64),
111 },
112 VoltageRangeWithPre {
114 scope: MonitorScope,
115 vmin: f64,
116 vmax: f64,
117 pre_min: f64,
118 pre_max: f64,
119 },
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
124#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
125pub struct BranchRef {
126 pub from: BusId,
127 pub to: BusId,
128 pub circuit: String,
130}
131
132#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
135#[serde(tag = "kind", rename_all = "snake_case")]
136#[non_exhaustive]
137pub enum MonitorScope {
138 AllBuses,
139 Subsystem {
140 name: String,
141 },
142 Bus {
143 bus: BusId,
144 },
145 Area {
146 area: usize,
147 },
148 Zone {
149 zone: usize,
150 },
151 Owner {
152 owner: usize,
153 },
154 Kv {
156 kv: f64,
157 },
158 SubsystemKv {
160 name: String,
161 kv: f64,
162 },
163}
164
165#[derive(Debug, Clone)]
167#[non_exhaustive]
168pub struct MonitoredParsed {
169 pub set: MonitoredSet,
170 pub diagnostics: Vec<Diagnostic>,
172}
173
174impl MonitoredParsed {
175 fn note(&mut self, info: &'static crate::diagnostics::DiagnosticInfo, message: String) {
176 note_within_budget(
177 &mut self.diagnostics,
178 info,
179 &codes::READ_MON_NOTES_TRUNCATED,
180 message,
181 );
182 }
183
184 fn unrecognized(&mut self, number: usize, text: &str) {
185 self.note(
186 &codes::READ_MON_STATEMENT_UNRECOGNIZED,
187 format!("line {number}: statement kept as text: {text}"),
188 );
189 }
190}
191
192fn bad(message: String) -> Error {
193 Error::FormatRead {
194 format: FMT,
195 message,
196 }
197}
198
199impl MonitoredSet {
200 pub fn parse(text: &str) -> Result<MonitoredParsed> {
214 let mut reader = Reader::new();
215 for line in lex(text) {
216 reader.read_line(&line);
217 }
218 reader.finish()
219 }
220
221 #[must_use]
229 pub fn to_mon(&self) -> String {
230 let mut out = String::new();
231 for line in &self.header {
232 out.push_str(line);
233 out.push('\n');
234 }
235 for statement in &self.statements {
236 out.push_str(&write_statement(statement));
237 }
238 for statement in self.retained.iter().filter(|kept| !kept.after_end) {
239 out.push_str(&statement.text);
240 out.push('\n');
241 }
242 out.push_str("END\n");
243 for statement in self.retained.iter().filter(|kept| kept.after_end) {
244 out.push_str(&statement.text);
245 out.push('\n');
246 }
247 out
248 }
249}
250
251struct OpenBlock {
254 opened: usize,
255 interface: Option<(String, Option<f64>)>,
257 branches: Vec<BranchRef>,
258 retained: Vec<RetainedStatement>,
259}
260
261struct Reader {
263 parsed: MonitoredParsed,
264 block: Option<OpenBlock>,
265 seen_statement: bool,
266 ended: bool,
267 noted_text_after_end: bool,
268 band_order_noted_at: Option<usize>,
270}
271
272impl Reader {
273 fn new() -> Self {
274 Reader {
275 parsed: MonitoredParsed {
276 set: MonitoredSet::default(),
277 diagnostics: Vec::new(),
278 },
279 block: None,
280 seen_statement: false,
281 ended: false,
282 noted_text_after_end: false,
283 band_order_noted_at: None,
284 }
285 }
286
287 fn read_line(&mut self, line: &LexedLine<'_>) {
288 if self.block.is_some() {
289 self.read_block_line(line);
290 return;
291 }
292 if self.ended {
293 self.keep_after_end(line);
294 return;
295 }
296 if !self.take_header(line) {
297 return;
298 }
299 let upper = line.keywords();
300 let words = line.words();
301 self.read_statement(line, &upper, &words);
302 }
303
304 fn finish(self) -> Result<MonitoredParsed> {
305 if let Some(open) = self.block {
306 let what = match open.interface {
307 Some((name, _)) => format!("MONITOR INTERFACE '{name}'"),
308 None => "MONITOR BRANCHES".to_owned(),
309 };
310 return Err(bad(format!("line {}: {what} has no END", open.opened)));
311 }
312 Ok(self.parsed)
313 }
314
315 fn take_header(&mut self, line: &LexedLine<'_>) -> bool {
318 if self.seen_statement {
319 return line.kind == LineKind::Statement;
320 }
321 match line.kind {
322 LineKind::Blank => false,
323 LineKind::Comment => {
324 self.parsed.set.header.push(line.text.to_owned());
325 false
326 }
327 LineKind::Statement => {
328 self.seen_statement = true;
329 true
330 }
331 }
332 }
333
334 fn keep_after_end(&mut self, line: &LexedLine<'_>) {
342 if line.kind != LineKind::Statement || is_end(line) {
343 return;
344 }
345 if !self.noted_text_after_end {
346 self.noted_text_after_end = true;
347 self.parsed.note(
348 &codes::READ_MON_TEXT_AFTER_END,
349 format!("line {}: text follows the file END", line.number),
350 );
351 }
352 self.keep_statement(line, true);
353 }
354
355 fn keep_statement(&mut self, line: &LexedLine<'_>, after_end: bool) {
359 self.parsed.set.retained.push(RetainedStatement {
360 line: line.number,
361 text: line.trimmed().to_owned(),
362 after_end,
363 });
364 }
365
366 fn read_block_line(&mut self, line: &LexedLine<'_>) {
368 if line.kind != LineKind::Statement {
369 return;
370 }
371 if is_end(line) {
372 let Some(open) = self.block.take() else {
373 return;
374 };
375 self.parsed.set.statements.push(match open.interface {
376 Some((name, rating_mw)) => MonitorStatement::Interface {
377 name,
378 rating_mw,
379 branches: open.branches,
380 retained: open.retained,
381 },
382 None => MonitorStatement::Branches {
383 branches: open.branches,
384 retained: open.retained,
385 },
386 });
387 return;
388 }
389 let words = line.words();
390 if let Some(branch) = parse_branch_ref(&words) {
391 if let Some(open) = self.block.as_mut() {
392 open.branches.push(branch);
393 }
394 return;
395 }
396 self.parsed.note(
397 &codes::READ_MON_SOURCE_MALFORMED,
398 format!(
399 "line {}: a monitored block line states no branch and was kept as text: {}",
400 line.number,
401 line.trimmed()
402 ),
403 );
404 if let Some(open) = self.block.as_mut() {
405 open.retained.push(RetainedStatement {
406 line: line.number,
407 text: line.trimmed().to_owned(),
408 after_end: false,
409 });
410 }
411 }
412
413 fn read_statement(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
414 if is_end(line) {
415 self.ended = true;
416 return;
417 }
418 if upper[0] == "MONITOR"
419 && let Some(read) = self.read_monitor(line, upper, words)
420 {
421 match read {
422 Read::Statement(statement) => {
423 if matches!(statement, MonitorStatement::VoltageRangeWithPre { .. }) {
424 self.note_band_order(line.number);
425 }
426 self.parsed.set.statements.push(statement);
427 }
428 Read::OpenedBlock => {}
429 }
430 return;
431 }
432 self.parsed.unrecognized(line.number, line.trimmed());
433 self.keep_statement(line, false);
434 }
435
436 fn note_band_order(&mut self, number: usize) {
442 if self.band_order_noted_at.is_some() {
443 return;
444 }
445 self.band_order_noted_at = Some(number);
446 self.parsed.note(
447 &codes::READ_MON_VOLTAGE_BAND_ORDER_ASSUMED,
448 format!(
449 "line {number}: MONITOR VOLTAGE RANGE states four values; every four-value range \
450 in this file is read as the post-contingency low and high, then the \
451 pre-contingency low and high, an order no public PSS/E or TARA source states"
452 ),
453 );
454 }
455
456 fn read_monitor(
459 &mut self,
460 line: &LexedLine<'_>,
461 upper: &[String],
462 words: &[&str],
463 ) -> Option<Read> {
464 match upper.get(1)?.as_str() {
465 "BRANCHES" | "LINES" => {
466 if upper.len() == 2 {
467 self.block = Some(OpenBlock {
468 opened: line.number,
469 interface: None,
470 branches: Vec::new(),
471 retained: Vec::new(),
472 });
473 return Some(Read::OpenedBlock);
474 }
475 let (subsystem, low_voltage_3w, kv_range) = parse_in_subsystem_kv(upper, words, 2)?;
476 Some(Read::Statement(match kv_range {
477 Some(kv_range) => MonitorStatement::BranchesInSubsystemKvRange {
478 subsystem,
479 low_voltage_3w,
480 kv_range,
481 },
482 None => MonitorStatement::BranchesInSubsystem {
483 subsystem,
484 low_voltage_3w,
485 },
486 }))
487 }
488 "TIES" => {
489 let (subsystem, low_voltage_3w, kv_range) = parse_in_subsystem_kv(upper, words, 2)?;
490 (!low_voltage_3w).then_some(Read::Statement(match kv_range {
491 Some(kv_range) => MonitorStatement::TiesFromSubsystemKvRange {
492 subsystem,
493 kv_range,
494 },
495 None => MonitorStatement::TiesFromSubsystem { subsystem },
496 }))
497 }
498 "INTERFACE" => {
499 let name = words.get(2)?.trim().to_owned();
500 let rating_mw = match parse_rating(upper, 3) {
501 Rating::Absent => None,
502 Rating::Stated(value) => Some(value),
503 Rating::Unreadable => return None,
504 };
505 self.block = Some(OpenBlock {
506 opened: line.number,
507 interface: Some((name, rating_mw)),
508 branches: Vec::new(),
509 retained: Vec::new(),
510 });
511 Some(Read::OpenedBlock)
512 }
513 "VOLTAGE" => parse_voltage(upper, words).map(Read::Statement),
514 "BRANCH" | "LINE" => parse_branch_ref(&words[1..]).map(|branch| {
516 Read::Statement(MonitorStatement::Branches {
517 branches: vec![branch],
518 retained: Vec::new(),
519 })
520 }),
521 _ => None,
522 }
523 }
524}
525
526enum Read {
528 Statement(MonitorStatement),
529 OpenedBlock,
530}
531
532fn is_end(line: &LexedLine<'_>) -> bool {
534 line.kind == LineKind::Statement
535 && line.tokens.len() == 1
536 && line.tokens[0].text.eq_ignore_ascii_case("END")
537}
538
539type SubsystemMonitorSpec = (String, bool, Option<(f64, f64)>);
545
546fn parse_in_subsystem_kv(
548 upper: &[String],
549 words: &[&str],
550 at: usize,
551) -> Option<SubsystemMonitorSpec> {
552 let subsystem = words.get(at + 2)?.trim().to_owned();
553 if !matches!(upper.get(at)?.as_str(), "IN" | "FROM") || upper.get(at + 1)? != "SUBSYSTEM" {
554 return None;
555 }
556 let mut next = at + 3;
557 let low_voltage_3w = upper.get(next).is_some_and(|word| word == "3WLOWVOLTAGE");
558 if low_voltage_3w {
559 next += 1;
560 }
561 if upper.get(next).is_some_and(|word| word == "IN")
564 && upper.get(next + 1).is_some_and(|word| word == "KVRANGE")
565 {
566 next += 1;
567 }
568 let kv_range = if upper.get(next).is_some_and(|word| word == "KVRANGE") {
569 let lo = upper.get(next + 1)?.parse::<f64>().ok()?;
570 let hi = upper.get(next + 2)?.parse::<f64>().ok()?;
571 if !lo.is_finite() || !hi.is_finite() || lo > hi {
572 return None;
573 }
574 next += 3;
575 Some((lo, hi))
576 } else {
577 None
578 };
579 (next == upper.len()).then_some((subsystem, low_voltage_3w, kv_range))
580}
581
582enum Rating {
584 Absent,
586 Stated(f64),
587 Unreadable,
589}
590
591fn parse_rating(upper: &[String], at: usize) -> Rating {
593 if at >= upper.len() {
594 return Rating::Absent;
595 }
596 if upper[at] != "RATING" {
597 return Rating::Unreadable;
598 }
599 let Some(value) = upper
600 .get(at + 1)
601 .and_then(|word| word.parse::<f64>().ok())
602 .filter(|value| value.is_finite())
603 else {
604 return Rating::Unreadable;
605 };
606 let mut next = at + 2;
607 if upper.get(next).is_some_and(|word| word == "MW") {
608 next += 1;
609 }
610 if next == upper.len() {
611 Rating::Stated(value)
612 } else {
613 Rating::Unreadable
614 }
615}
616
617fn parse_voltage(upper: &[String], words: &[&str]) -> Option<MonitorStatement> {
620 let deviation = match upper.get(2)?.as_str() {
621 "RANGE" => false,
622 "DEVIATION" => true,
623 _ => return None,
624 };
625 let (scope, at) = parse_scope(upper, words, 3)?;
626 let values: Option<Vec<f64>> = upper[at..]
627 .iter()
628 .map(|word| word.parse::<f64>().ok().filter(|value| value.is_finite()))
629 .collect();
630 let values = values?;
631 match (deviation, values.as_slice()) {
632 (false, [vmin, vmax]) => (vmin <= vmax).then_some(MonitorStatement::VoltageRange {
636 scope,
637 vmin: *vmin,
638 vmax: *vmax,
639 }),
640 (false, [vmin, vmax, pre_min, pre_max]) => {
641 (vmin <= vmax && pre_min <= pre_max).then_some(MonitorStatement::VoltageRangeWithPre {
642 scope,
643 vmin: *vmin,
644 vmax: *vmax,
645 pre_min: *pre_min,
646 pre_max: *pre_max,
647 })
648 }
649 (true, [down]) => Some(MonitorStatement::VoltageDeviation {
650 scope,
651 down: *down,
652 up: None,
653 }),
654 (true, [down, up]) => Some(MonitorStatement::VoltageDeviation {
655 scope,
656 down: *down,
657 up: Some(*up),
658 }),
659 _ => None,
660 }
661}
662
663fn parse_scope(upper: &[String], words: &[&str], at: usize) -> Option<(MonitorScope, usize)> {
665 let integer = |offset: usize| upper.get(at + offset)?.parse::<usize>().ok();
666 match upper.get(at)?.as_str() {
667 "ALL" if upper.get(at + 1).is_some_and(|word| word == "BUSES") => {
668 Some((MonitorScope::AllBuses, at + 2))
669 }
670 "SUBSYSTEM" => {
671 let name = words.get(at + 1)?.trim().to_owned();
672 let mut next = at + 2;
673 let scope = if upper.get(next).is_some_and(|word| word == "KV") {
674 let value = upper.get(next + 1)?.parse::<f64>().ok()?;
675 if !value.is_finite() {
676 return None;
677 }
678 next += 2;
679 MonitorScope::SubsystemKv { name, kv: value }
680 } else {
681 MonitorScope::Subsystem { name }
682 };
683 Some((scope, next))
684 }
685 "BUS" => Some((
686 MonitorScope::Bus {
687 bus: BusId(integer(1)?),
688 },
689 at + 2,
690 )),
691 "AREA" => Some((MonitorScope::Area { area: integer(1)? }, at + 2)),
692 "ZONE" => Some((MonitorScope::Zone { zone: integer(1)? }, at + 2)),
693 "OWNER" => Some((MonitorScope::Owner { owner: integer(1)? }, at + 2)),
694 "KV" => {
695 let kv = upper.get(at + 1)?.parse::<f64>().ok()?;
696 kv.is_finite().then_some((MonitorScope::Kv { kv }, at + 2))
697 }
698 _ => None,
699 }
700}
701
702fn parse_branch_ref(words: &[&str]) -> Option<BranchRef> {
707 let words = match words {
710 [monitor, noun, ..]
711 if monitor.eq_ignore_ascii_case("MONITOR")
712 && (noun.eq_ignore_ascii_case("BRANCH") || noun.eq_ignore_ascii_case("LINE")) =>
713 {
714 &words[1..]
715 }
716 _ => words,
717 };
718 if words.first()?.parse::<usize>().is_err() {
719 let upper: Vec<String> = words.iter().map(|word| word.to_ascii_uppercase()).collect();
720 let mut at = usize::from(matches!(upper[0].as_str(), "BRANCH" | "LINE"));
721 super::take_keyword(&upper, &mut at, "FROM")?;
722 let from = super::take_bus(&upper, &mut at)?;
723 super::take_keyword(&upper, &mut at, "TO")?;
724 let to = super::take_bus(&upper, &mut at)?;
725 let circuit = super::take_circuit(&upper, words, &mut at)?;
726 return Some(BranchRef { from, to, circuit });
727 }
728 if words.len() > 3 {
729 return None;
730 }
731 let from = words.first()?.parse::<usize>().ok()?;
732 let to = words.get(1)?.parse::<usize>().ok()?;
733 let circuit = words.get(2).map_or("1", |word| word.trim());
734 Some(BranchRef {
735 from: BusId(from),
736 to: BusId(to),
737 circuit: if circuit.is_empty() {
738 "1".to_owned()
739 } else {
740 circuit.to_owned()
741 },
742 })
743}
744
745fn write_scope(scope: &MonitorScope) -> String {
750 match scope {
751 MonitorScope::AllBuses => "ALL BUSES".to_owned(),
752 MonitorScope::Subsystem { name } => format!("SUBSYSTEM '{name}'"),
753 MonitorScope::SubsystemKv { name, kv } => format!("SUBSYSTEM '{name}' KV {}", decimal(*kv)),
754 MonitorScope::Bus { bus } => format!("BUS {}", bus.0),
755 MonitorScope::Area { area } => format!("AREA {area}"),
756 MonitorScope::Zone { zone } => format!("ZONE {zone}"),
757 MonitorScope::Owner { owner } => format!("OWNER {owner}"),
758 MonitorScope::Kv { kv } => format!("KV {}", decimal(*kv)),
759 }
760}
761
762fn write_branch_block(
763 head: &str,
764 branches: &[BranchRef],
765 retained: &[RetainedStatement],
766) -> String {
767 use std::fmt::Write as _;
768
769 let mut out = format!("{head}\n");
770 for branch in branches {
771 let _ = writeln!(
772 out,
773 "{:>6} {:>6} {}",
774 branch.from.0,
775 branch.to.0,
776 field(&branch.circuit)
777 );
778 }
779 for statement in retained {
780 out.push_str(&statement.text);
781 out.push('\n');
782 }
783 out.push_str("END\n");
784 out
785}
786
787fn write_statement(statement: &MonitorStatement) -> String {
788 match statement {
789 MonitorStatement::BranchesInSubsystem {
790 subsystem,
791 low_voltage_3w,
792 } => {
793 let low = if *low_voltage_3w { " 3WLOWVOLTAGE" } else { "" };
794 format!("MONITOR BRANCHES IN SUBSYSTEM '{subsystem}'{low}\n")
795 }
796 MonitorStatement::BranchesInSubsystemKvRange {
797 subsystem,
798 low_voltage_3w,
799 kv_range: (lo, hi),
800 } => {
801 let low = if *low_voltage_3w { " 3WLOWVOLTAGE" } else { "" };
802 format!(
803 "MONITOR BRANCHES IN SUBSYSTEM '{subsystem}'{low} KVRANGE {} {}\n",
804 decimal(*lo),
805 decimal(*hi)
806 )
807 }
808 MonitorStatement::TiesFromSubsystem { subsystem } => {
809 format!("MONITOR TIES FROM SUBSYSTEM '{subsystem}'\n")
810 }
811 MonitorStatement::TiesFromSubsystemKvRange {
812 subsystem,
813 kv_range: (lo, hi),
814 } => {
815 format!(
816 "MONITOR TIES FROM SUBSYSTEM '{subsystem}' KVRANGE {} {}\n",
817 decimal(*lo),
818 decimal(*hi)
819 )
820 }
821 MonitorStatement::Branches { branches, retained } => {
822 write_branch_block("MONITOR BRANCHES", branches, retained)
823 }
824 MonitorStatement::Interface {
825 name,
826 rating_mw,
827 branches,
828 retained,
829 } => {
830 let head = match rating_mw {
831 Some(rating) => {
832 format!("MONITOR INTERFACE '{name}' RATING {} MW", decimal(*rating))
833 }
834 None => format!("MONITOR INTERFACE '{name}'"),
835 };
836 write_branch_block(&head, branches, retained)
837 }
838 MonitorStatement::VoltageRange { scope, vmin, vmax } => format!(
839 "MONITOR VOLTAGE RANGE {} {} {}\n",
840 write_scope(scope),
841 decimal(*vmin),
842 decimal(*vmax)
843 ),
844 MonitorStatement::VoltageRangeWithPre {
845 scope,
846 vmin,
847 vmax,
848 pre_min,
849 pre_max,
850 } => format!(
851 "MONITOR VOLTAGE RANGE {} {} {} {} {}\n",
852 write_scope(scope),
853 decimal(*vmin),
854 decimal(*vmax),
855 decimal(*pre_min),
856 decimal(*pre_max)
857 ),
858 MonitorStatement::VoltageDeviation { scope, down, up } => {
859 let tail = match up {
860 Some(up) => format!(" {}", decimal(*up)),
861 None => String::new(),
862 };
863 format!(
864 "MONITOR VOLTAGE DEVIATION {} {}{tail}\n",
865 write_scope(scope),
866 decimal(*down)
867 )
868 }
869 }
870}
871
872#[derive(Debug, Clone, Default, PartialEq)]
879#[non_exhaustive]
880pub struct MonitoredResolution {
881 pub branch_rows: BTreeSet<usize>,
883 pub transformer_3w_rows: BTreeSet<usize>,
885 pub tie_rows: BTreeSet<usize>,
887 pub interfaces: Vec<ResolvedInterface>,
888 pub voltage_ranges: Vec<ResolvedVoltageScope>,
889 pub voltage_ranges_with_pre: Vec<(ResolvedVoltageScope, (f64, f64))>,
891 pub voltage_deviations: Vec<ResolvedVoltageScope>,
892 pub unresolved: Vec<UnresolvedMonitor>,
894}
895
896#[derive(Debug, Clone, Default, PartialEq)]
898pub struct ResolvedInterface {
899 pub name: String,
900 pub rating_mw: Option<f64>,
901 pub members: Vec<InterfaceMember>,
903}
904
905#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
914pub struct InterfaceMember {
915 pub row: usize,
917 pub reversed: bool,
919}
920
921#[derive(Debug, Clone, Default, PartialEq)]
924pub struct ResolvedVoltageScope {
925 pub bus_rows: BTreeSet<usize>,
926 pub low: f64,
927 pub high: Option<f64>,
928}
929
930#[derive(Debug, Clone, PartialEq)]
932pub struct UnresolvedMonitor {
933 pub statement: MonitorStatement,
934 pub reason: UnresolvedMonitorReason,
935}
936
937#[derive(Debug, Clone, PartialEq, Eq)]
939#[non_exhaustive]
940pub enum UnresolvedMonitorReason {
941 NoSuchSubsystem,
943 NoSuchBranch {
944 from: BusId,
945 to: BusId,
946 circuit: String,
947 },
948 AmbiguousBranch {
950 from: BusId,
951 to: BusId,
952 circuit: String,
953 matches: usize,
954 },
955}
956
957impl MonitoredResolution {
958 #[must_use]
960 pub fn diagnostics(&self) -> Vec<Diagnostic> {
961 self.unresolved
962 .iter()
963 .map(|entry| {
964 Diagnostic::of(
965 &codes::BUILD_MON_STATEMENT_UNRESOLVED,
966 describe(&entry.statement, &entry.reason),
967 )
968 })
969 .collect()
970 }
971}
972
973fn describe(statement: &MonitorStatement, reason: &UnresolvedMonitorReason) -> String {
975 let what = match statement {
976 MonitorStatement::BranchesInSubsystem { subsystem, .. }
977 | MonitorStatement::BranchesInSubsystemKvRange { subsystem, .. }
978 | MonitorStatement::TiesFromSubsystem { subsystem }
979 | MonitorStatement::TiesFromSubsystemKvRange { subsystem, .. } => {
980 format!("monitored subsystem '{subsystem}'")
981 }
982 MonitorStatement::Branches { .. } => "monitored branches".to_owned(),
983 MonitorStatement::Interface { name, .. } => format!("monitored interface '{name}'"),
984 MonitorStatement::VoltageRange { .. } | MonitorStatement::VoltageRangeWithPre { .. } => {
985 "monitored voltage range".to_owned()
986 }
987 MonitorStatement::VoltageDeviation { .. } => "monitored voltage deviation".to_owned(),
988 };
989 match reason {
990 UnresolvedMonitorReason::NoSuchSubsystem => {
991 format!("{what}: the subsystem set states no such subsystem")
992 }
993 UnresolvedMonitorReason::NoSuchBranch { from, to, circuit } => {
994 format!("{what}: no branch {from} to {to} circuit {circuit}")
995 }
996 UnresolvedMonitorReason::AmbiguousBranch {
997 from,
998 to,
999 circuit,
1000 matches,
1001 } => format!("{what}: branch {from} to {to} circuit {circuit} names {matches} branches"),
1002 }
1003}
1004
1005impl MonitoredSet {
1006 #[must_use]
1018 pub fn resolve(&self, net: &BalancedNetwork, subsystems: &SubsystemSet) -> MonitoredResolution {
1019 self.resolve_with(&PsseEquipmentIndex::new(net), subsystems)
1020 }
1021
1022 #[must_use]
1028 pub fn resolve_with(
1029 &self,
1030 index: &PsseEquipmentIndex<'_>,
1031 subsystems: &SubsystemSet,
1032 ) -> MonitoredResolution {
1033 let mut out = MonitoredResolution::default();
1034 for statement in &self.statements {
1035 resolve_statement(statement, index.network(), subsystems, index, &mut out);
1036 }
1037 out
1038 }
1039}
1040
1041#[allow(clippy::too_many_lines)]
1042fn resolve_statement(
1043 statement: &MonitorStatement,
1044 net: &BalancedNetwork,
1045 subsystems: &SubsystemSet,
1046 index: &PsseEquipmentIndex,
1047 out: &mut MonitoredResolution,
1048) {
1049 match statement {
1050 MonitorStatement::BranchesInSubsystem {
1051 subsystem,
1052 low_voltage_3w,
1053 } => resolve_branches_in_subsystem(
1054 statement,
1055 subsystem,
1056 *low_voltage_3w,
1057 None,
1058 net,
1059 subsystems,
1060 index,
1061 out,
1062 ),
1063 MonitorStatement::BranchesInSubsystemKvRange {
1064 subsystem,
1065 low_voltage_3w,
1066 kv_range,
1067 } => resolve_branches_in_subsystem(
1068 statement,
1069 subsystem,
1070 *low_voltage_3w,
1071 Some(*kv_range),
1072 net,
1073 subsystems,
1074 index,
1075 out,
1076 ),
1077 MonitorStatement::TiesFromSubsystem { subsystem } => {
1078 resolve_ties_from_subsystem(statement, subsystem, None, net, subsystems, index, out);
1079 }
1080 MonitorStatement::TiesFromSubsystemKvRange {
1081 subsystem,
1082 kv_range,
1083 } => {
1084 resolve_ties_from_subsystem(
1085 statement,
1086 subsystem,
1087 Some(*kv_range),
1088 net,
1089 subsystems,
1090 index,
1091 out,
1092 );
1093 }
1094 MonitorStatement::Branches { branches, .. } => {
1095 for branch in branches {
1096 match bind_branch(index, branch) {
1097 Ok(row) => {
1098 out.branch_rows.insert(row);
1099 }
1100 Err(reason) => out.unresolved.push(UnresolvedMonitor {
1101 statement: statement.clone(),
1102 reason,
1103 }),
1104 }
1105 }
1106 }
1107 MonitorStatement::Interface {
1108 name,
1109 rating_mw,
1110 branches,
1111 ..
1112 } => {
1113 let mut resolved = ResolvedInterface {
1114 name: name.clone(),
1115 rating_mw: *rating_mw,
1116 members: Vec::new(),
1117 };
1118 for branch in branches {
1119 match bind_branch(index, branch) {
1120 Ok(row) => resolved.members.push(member(index, branch, row)),
1121 Err(reason) => out.unresolved.push(UnresolvedMonitor {
1122 statement: statement.clone(),
1123 reason,
1124 }),
1125 }
1126 }
1127 out.interfaces.push(resolved);
1128 }
1129 MonitorStatement::VoltageRange { scope, vmin, vmax } => {
1130 let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
1131 unresolved_subsystem(statement, out);
1132 return;
1133 };
1134 out.voltage_ranges.push(ResolvedVoltageScope {
1135 bus_rows,
1136 low: *vmin,
1137 high: Some(*vmax),
1138 });
1139 }
1140 MonitorStatement::VoltageRangeWithPre {
1141 scope,
1142 vmin,
1143 vmax,
1144 pre_min,
1145 pre_max,
1146 } => {
1147 let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
1148 unresolved_subsystem(statement, out);
1149 return;
1150 };
1151 out.voltage_ranges_with_pre.push((
1152 ResolvedVoltageScope {
1153 bus_rows,
1154 low: *vmin,
1155 high: Some(*vmax),
1156 },
1157 (*pre_min, *pre_max),
1158 ));
1159 }
1160 MonitorStatement::VoltageDeviation { scope, down, up } => {
1161 let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
1162 unresolved_subsystem(statement, out);
1163 return;
1164 };
1165 out.voltage_deviations.push(ResolvedVoltageScope {
1166 bus_rows,
1167 low: *down,
1168 high: *up,
1169 });
1170 }
1171 }
1172}
1173
1174#[allow(clippy::too_many_arguments)]
1175fn resolve_branches_in_subsystem(
1176 statement: &MonitorStatement,
1177 subsystem: &str,
1178 low_voltage_3w: bool,
1179 kv_range: Option<(f64, f64)>,
1180 net: &BalancedNetwork,
1181 subsystems: &SubsystemSet,
1182 index: &PsseEquipmentIndex,
1183 out: &mut MonitoredResolution,
1184) {
1185 let Some(buses) = select(subsystems, subsystem, net) else {
1186 unresolved_subsystem(statement, out);
1187 return;
1188 };
1189 for (row, branch) in net.branches().iter().enumerate() {
1190 if buses.contains(&branch.from)
1191 && buses.contains(&branch.to)
1192 && kv_range.is_none_or(|(lo, hi)| {
1193 bus_in_kv_range(index, branch.from, lo, hi)
1194 && bus_in_kv_range(index, branch.to, lo, hi)
1195 })
1196 {
1197 out.branch_rows.insert(row);
1198 }
1199 }
1200 if low_voltage_3w {
1201 for (row, transformer) in net.transformers_3w().iter().enumerate() {
1202 let low_bus = low_voltage_bus(net, index, transformer);
1203 if buses.contains(&low_bus)
1204 && kv_range.is_none_or(|(lo, hi)| bus_in_kv_range(index, low_bus, lo, hi))
1205 {
1206 out.transformer_3w_rows.insert(row);
1207 }
1208 }
1209 }
1210}
1211
1212fn resolve_ties_from_subsystem(
1213 statement: &MonitorStatement,
1214 subsystem: &str,
1215 kv_range: Option<(f64, f64)>,
1216 net: &BalancedNetwork,
1217 subsystems: &SubsystemSet,
1218 index: &PsseEquipmentIndex,
1219 out: &mut MonitoredResolution,
1220) {
1221 let Some(buses) = select(subsystems, subsystem, net) else {
1222 unresolved_subsystem(statement, out);
1223 return;
1224 };
1225 for (row, branch) in net.branches().iter().enumerate() {
1226 let from_inside = buses.contains(&branch.from);
1227 let to_inside = buses.contains(&branch.to);
1228 let in_scope = match (from_inside, to_inside, kv_range) {
1229 (true, false, Some((lo, hi))) => bus_in_kv_range(index, branch.from, lo, hi),
1230 (false, true, Some((lo, hi))) => bus_in_kv_range(index, branch.to, lo, hi),
1231 (true, false, None) | (false, true, None) => true,
1232 _ => false,
1233 };
1234 if from_inside != to_inside && in_scope {
1235 out.tie_rows.insert(row);
1236 }
1237 }
1238}
1239
1240fn unresolved_subsystem(statement: &MonitorStatement, out: &mut MonitoredResolution) {
1241 out.unresolved.push(UnresolvedMonitor {
1242 statement: statement.clone(),
1243 reason: UnresolvedMonitorReason::NoSuchSubsystem,
1244 });
1245}
1246
1247fn select(subsystems: &SubsystemSet, name: &str, net: &BalancedNetwork) -> Option<BTreeSet<BusId>> {
1248 Some(subsystems.get(name)?.select_buses(net))
1249}
1250
1251fn bus_in_kv_range(index: &PsseEquipmentIndex<'_>, bus: BusId, lo: f64, hi: f64) -> bool {
1252 index.bus_row(bus).is_some_and(|row| {
1253 let kv = index.network().buses()[row].base_kv;
1254 kv >= lo - KV_TOLERANCE && kv <= hi + KV_TOLERANCE
1255 })
1256}
1257
1258fn member(index: &PsseEquipmentIndex<'_>, branch: &BranchRef, row: usize) -> InterfaceMember {
1261 InterfaceMember {
1262 row,
1263 reversed: index.network().branches()[row].from != branch.from,
1264 }
1265}
1266
1267fn bind_branch(
1268 index: &PsseEquipmentIndex,
1269 branch: &BranchRef,
1270) -> std::result::Result<usize, UnresolvedMonitorReason> {
1271 let rows = index.branch_rows(branch.from, branch.to, &branch.circuit);
1272 match rows.as_slice() {
1273 [] => Err(UnresolvedMonitorReason::NoSuchBranch {
1274 from: branch.from,
1275 to: branch.to,
1276 circuit: branch.circuit.clone(),
1277 }),
1278 [row] => Ok(*row),
1279 many => Err(UnresolvedMonitorReason::AmbiguousBranch {
1280 from: branch.from,
1281 to: branch.to,
1282 circuit: branch.circuit.clone(),
1283 matches: many.len(),
1284 }),
1285 }
1286}
1287
1288fn scope_rows(
1292 scope: &MonitorScope,
1293 net: &BalancedNetwork,
1294 subsystems: &SubsystemSet,
1295 index: &PsseEquipmentIndex,
1296) -> Option<BTreeSet<usize>> {
1297 let rows_of = |buses: &BTreeSet<BusId>| -> BTreeSet<usize> {
1298 buses.iter().filter_map(|bus| index.bus_row(*bus)).collect()
1299 };
1300 let matching = |keep: &dyn Fn(&crate::network::Bus) -> bool| -> BTreeSet<usize> {
1301 net.buses()
1302 .iter()
1303 .enumerate()
1304 .filter(|(_, bus)| keep(bus))
1305 .map(|(row, _)| row)
1306 .collect()
1307 };
1308 Some(match scope {
1309 MonitorScope::AllBuses => (0..net.buses().len()).collect(),
1310 MonitorScope::Subsystem { name } => rows_of(&select(subsystems, name, net)?),
1311 MonitorScope::SubsystemKv { name, kv } => select(subsystems, name, net)?
1312 .into_iter()
1313 .filter(|bus| bus_in_kv_range(index, *bus, *kv, *kv))
1314 .filter_map(|bus| index.bus_row(bus))
1315 .collect(),
1316 MonitorScope::Bus { bus } => index.bus_row(*bus).into_iter().collect(),
1317 MonitorScope::Area { area } => matching(&|bus| bus.area == *area),
1318 MonitorScope::Zone { zone } => matching(&|bus| bus.zone == *zone),
1319 MonitorScope::Owner { owner } => matching(&|bus| super::sub::owner_of(bus) == *owner),
1320 MonitorScope::Kv { kv } => matching(&|bus| (bus.base_kv - *kv).abs() <= KV_TOLERANCE),
1321 })
1322}