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}
102
103#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106pub struct BranchRef {
107 pub from: BusId,
108 pub to: BusId,
109 pub circuit: String,
111}
112
113#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
116#[serde(tag = "kind", rename_all = "snake_case")]
117#[non_exhaustive]
118pub enum MonitorScope {
119 AllBuses,
120 Subsystem {
121 name: String,
122 },
123 Bus {
124 bus: BusId,
125 },
126 Area {
127 area: usize,
128 },
129 Zone {
130 zone: usize,
131 },
132 Owner {
133 owner: usize,
134 },
135 Kv {
137 kv: f64,
138 },
139}
140
141#[derive(Debug, Clone)]
143#[non_exhaustive]
144pub struct MonitoredParsed {
145 pub set: MonitoredSet,
146 pub diagnostics: Vec<Diagnostic>,
148}
149
150impl MonitoredParsed {
151 fn note(&mut self, info: &'static crate::diagnostics::DiagnosticInfo, message: String) {
152 note_within_budget(
153 &mut self.diagnostics,
154 info,
155 &codes::READ_MON_NOTES_TRUNCATED,
156 message,
157 );
158 }
159
160 fn unrecognized(&mut self, number: usize, text: &str) {
161 self.note(
162 &codes::READ_MON_STATEMENT_UNRECOGNIZED,
163 format!("line {number}: statement kept as text: {text}"),
164 );
165 }
166}
167
168fn bad(message: String) -> Error {
169 Error::FormatRead {
170 format: FMT,
171 message,
172 }
173}
174
175impl MonitoredSet {
176 pub fn parse(text: &str) -> Result<MonitoredParsed> {
190 let mut reader = Reader::new();
191 for line in lex(text) {
192 reader.read_line(&line);
193 }
194 reader.finish()
195 }
196
197 #[must_use]
205 pub fn to_mon(&self) -> String {
206 let mut out = String::new();
207 for line in &self.header {
208 out.push_str(line);
209 out.push('\n');
210 }
211 for statement in &self.statements {
212 out.push_str(&write_statement(statement));
213 }
214 for statement in self.retained.iter().filter(|kept| !kept.after_end) {
215 out.push_str(&statement.text);
216 out.push('\n');
217 }
218 out.push_str("END\n");
219 for statement in self.retained.iter().filter(|kept| kept.after_end) {
220 out.push_str(&statement.text);
221 out.push('\n');
222 }
223 out
224 }
225}
226
227struct OpenBlock {
230 opened: usize,
231 interface: Option<(String, Option<f64>)>,
233 branches: Vec<BranchRef>,
234 retained: Vec<RetainedStatement>,
235}
236
237struct Reader {
239 parsed: MonitoredParsed,
240 block: Option<OpenBlock>,
241 seen_statement: bool,
242 ended: bool,
243 noted_text_after_end: bool,
244}
245
246impl Reader {
247 fn new() -> Self {
248 Reader {
249 parsed: MonitoredParsed {
250 set: MonitoredSet::default(),
251 diagnostics: Vec::new(),
252 },
253 block: None,
254 seen_statement: false,
255 ended: false,
256 noted_text_after_end: false,
257 }
258 }
259
260 fn read_line(&mut self, line: &LexedLine<'_>) {
261 if self.block.is_some() {
262 self.read_block_line(line);
263 return;
264 }
265 if self.ended {
266 self.keep_after_end(line);
267 return;
268 }
269 if !self.take_header(line) {
270 return;
271 }
272 let upper = line.keywords();
273 let words = line.words();
274 self.read_statement(line, &upper, &words);
275 }
276
277 fn finish(self) -> Result<MonitoredParsed> {
278 if let Some(open) = self.block {
279 let what = match open.interface {
280 Some((name, _)) => format!("MONITOR INTERFACE '{name}'"),
281 None => "MONITOR BRANCHES".to_owned(),
282 };
283 return Err(bad(format!("line {}: {what} has no END", open.opened)));
284 }
285 Ok(self.parsed)
286 }
287
288 fn take_header(&mut self, line: &LexedLine<'_>) -> bool {
291 if self.seen_statement {
292 return line.kind == LineKind::Statement;
293 }
294 match line.kind {
295 LineKind::Blank => false,
296 LineKind::Comment => {
297 self.parsed.set.header.push(line.text.to_owned());
298 false
299 }
300 LineKind::Statement => {
301 self.seen_statement = true;
302 true
303 }
304 }
305 }
306
307 fn keep_after_end(&mut self, line: &LexedLine<'_>) {
315 if line.kind != LineKind::Statement || is_end(line) {
316 return;
317 }
318 if !self.noted_text_after_end {
319 self.noted_text_after_end = true;
320 self.parsed.note(
321 &codes::READ_MON_TEXT_AFTER_END,
322 format!("line {}: text follows the file END", line.number),
323 );
324 }
325 self.keep_statement(line, true);
326 }
327
328 fn keep_statement(&mut self, line: &LexedLine<'_>, after_end: bool) {
332 self.parsed.set.retained.push(RetainedStatement {
333 line: line.number,
334 text: line.trimmed().to_owned(),
335 after_end,
336 });
337 }
338
339 fn read_block_line(&mut self, line: &LexedLine<'_>) {
341 if line.kind != LineKind::Statement {
342 return;
343 }
344 if is_end(line) {
345 let Some(open) = self.block.take() else {
346 return;
347 };
348 self.parsed.set.statements.push(match open.interface {
349 Some((name, rating_mw)) => MonitorStatement::Interface {
350 name,
351 rating_mw,
352 branches: open.branches,
353 retained: open.retained,
354 },
355 None => MonitorStatement::Branches {
356 branches: open.branches,
357 retained: open.retained,
358 },
359 });
360 return;
361 }
362 let words = line.words();
363 if let Some(branch) = parse_branch_ref(&words) {
364 if let Some(open) = self.block.as_mut() {
365 open.branches.push(branch);
366 }
367 return;
368 }
369 self.parsed.note(
370 &codes::READ_MON_SOURCE_MALFORMED,
371 format!(
372 "line {}: a monitored block line states no branch and was kept as text: {}",
373 line.number,
374 line.trimmed()
375 ),
376 );
377 if let Some(open) = self.block.as_mut() {
378 open.retained.push(RetainedStatement {
379 line: line.number,
380 text: line.trimmed().to_owned(),
381 after_end: false,
382 });
383 }
384 }
385
386 fn read_statement(&mut self, line: &LexedLine<'_>, upper: &[String], words: &[&str]) {
387 if is_end(line) {
388 self.ended = true;
389 return;
390 }
391 if upper[0] == "MONITOR"
392 && let Some(read) = self.read_monitor(line, upper, words)
393 {
394 match read {
395 Read::Statement(statement) => self.parsed.set.statements.push(statement),
396 Read::OpenedBlock => {}
397 }
398 return;
399 }
400 self.parsed.unrecognized(line.number, line.trimmed());
401 self.keep_statement(line, false);
402 }
403
404 fn read_monitor(
407 &mut self,
408 line: &LexedLine<'_>,
409 upper: &[String],
410 words: &[&str],
411 ) -> Option<Read> {
412 match upper.get(1)?.as_str() {
413 "BRANCHES" | "LINES" => {
414 if upper.len() == 2 {
415 self.block = Some(OpenBlock {
416 opened: line.number,
417 interface: None,
418 branches: Vec::new(),
419 retained: Vec::new(),
420 });
421 return Some(Read::OpenedBlock);
422 }
423 let (subsystem, low_voltage_3w) = parse_in_subsystem(upper, words, 2)?;
424 Some(Read::Statement(MonitorStatement::BranchesInSubsystem {
425 subsystem,
426 low_voltage_3w,
427 }))
428 }
429 "TIES" => {
430 let (subsystem, low_voltage_3w) = parse_in_subsystem(upper, words, 2)?;
431 (!low_voltage_3w).then_some(Read::Statement(MonitorStatement::TiesFromSubsystem {
432 subsystem,
433 }))
434 }
435 "INTERFACE" => {
436 let name = words.get(2)?.trim().to_owned();
437 let rating_mw = match parse_rating(upper, 3) {
438 Rating::Absent => None,
439 Rating::Stated(value) => Some(value),
440 Rating::Unreadable => return None,
441 };
442 self.block = Some(OpenBlock {
443 opened: line.number,
444 interface: Some((name, rating_mw)),
445 branches: Vec::new(),
446 retained: Vec::new(),
447 });
448 Some(Read::OpenedBlock)
449 }
450 "VOLTAGE" => parse_voltage(upper, words).map(Read::Statement),
451 _ => None,
452 }
453 }
454}
455
456enum Read {
458 Statement(MonitorStatement),
459 OpenedBlock,
460}
461
462fn is_end(line: &LexedLine<'_>) -> bool {
464 line.kind == LineKind::Statement
465 && line.tokens.len() == 1
466 && line.tokens[0].text.eq_ignore_ascii_case("END")
467}
468
469fn parse_in_subsystem(upper: &[String], words: &[&str], at: usize) -> Option<(String, bool)> {
475 if !matches!(upper.get(at)?.as_str(), "IN" | "FROM") {
476 return None;
477 }
478 if upper.get(at + 1)? != "SUBSYSTEM" {
479 return None;
480 }
481 let subsystem = words.get(at + 2)?.trim().to_owned();
482 let mut next = at + 3;
483 let low_voltage_3w = upper.get(next).is_some_and(|word| word == "3WLOWVOLTAGE");
484 if low_voltage_3w {
485 next += 1;
486 }
487 (next == upper.len()).then_some((subsystem, low_voltage_3w))
488}
489
490enum Rating {
492 Absent,
494 Stated(f64),
495 Unreadable,
497}
498
499fn parse_rating(upper: &[String], at: usize) -> Rating {
501 if at >= upper.len() {
502 return Rating::Absent;
503 }
504 if upper[at] != "RATING" {
505 return Rating::Unreadable;
506 }
507 let Some(value) = upper
508 .get(at + 1)
509 .and_then(|word| word.parse::<f64>().ok())
510 .filter(|value| value.is_finite())
511 else {
512 return Rating::Unreadable;
513 };
514 let mut next = at + 2;
515 if upper.get(next).is_some_and(|word| word == "MW") {
516 next += 1;
517 }
518 if next == upper.len() {
519 Rating::Stated(value)
520 } else {
521 Rating::Unreadable
522 }
523}
524
525fn parse_voltage(upper: &[String], words: &[&str]) -> Option<MonitorStatement> {
528 let deviation = match upper.get(2)?.as_str() {
529 "RANGE" => false,
530 "DEVIATION" => true,
531 _ => return None,
532 };
533 let (scope, at) = parse_scope(upper, words, 3)?;
534 let values: Option<Vec<f64>> = upper[at..]
535 .iter()
536 .map(|word| word.parse::<f64>().ok().filter(|value| value.is_finite()))
537 .collect();
538 let values = values?;
539 match (deviation, values.as_slice()) {
540 (false, [vmin, vmax]) => (vmin <= vmax).then_some(MonitorStatement::VoltageRange {
544 scope,
545 vmin: *vmin,
546 vmax: *vmax,
547 }),
548 (true, [down]) => Some(MonitorStatement::VoltageDeviation {
549 scope,
550 down: *down,
551 up: None,
552 }),
553 (true, [down, up]) => Some(MonitorStatement::VoltageDeviation {
554 scope,
555 down: *down,
556 up: Some(*up),
557 }),
558 _ => None,
559 }
560}
561
562fn parse_scope(upper: &[String], words: &[&str], at: usize) -> Option<(MonitorScope, usize)> {
564 let integer = |offset: usize| upper.get(at + offset)?.parse::<usize>().ok();
565 match upper.get(at)?.as_str() {
566 "ALL" if upper.get(at + 1).is_some_and(|word| word == "BUSES") => {
567 Some((MonitorScope::AllBuses, at + 2))
568 }
569 "SUBSYSTEM" => Some((
570 MonitorScope::Subsystem {
571 name: words.get(at + 1)?.trim().to_owned(),
572 },
573 at + 2,
574 )),
575 "BUS" => Some((
576 MonitorScope::Bus {
577 bus: BusId(integer(1)?),
578 },
579 at + 2,
580 )),
581 "AREA" => Some((MonitorScope::Area { area: integer(1)? }, at + 2)),
582 "ZONE" => Some((MonitorScope::Zone { zone: integer(1)? }, at + 2)),
583 "OWNER" => Some((MonitorScope::Owner { owner: integer(1)? }, at + 2)),
584 "KV" => {
585 let kv = upper.get(at + 1)?.parse::<f64>().ok()?;
586 kv.is_finite().then_some((MonitorScope::Kv { kv }, at + 2))
587 }
588 _ => None,
589 }
590}
591
592fn parse_branch_ref(words: &[&str]) -> Option<BranchRef> {
594 if words.len() > 3 {
595 return None;
596 }
597 let from = words.first()?.parse::<usize>().ok()?;
598 let to = words.get(1)?.parse::<usize>().ok()?;
599 let circuit = words.get(2).map_or("1", |word| word.trim());
600 Some(BranchRef {
601 from: BusId(from),
602 to: BusId(to),
603 circuit: if circuit.is_empty() {
604 "1".to_owned()
605 } else {
606 circuit.to_owned()
607 },
608 })
609}
610
611fn write_scope(scope: &MonitorScope) -> String {
616 match scope {
617 MonitorScope::AllBuses => "ALL BUSES".to_owned(),
618 MonitorScope::Subsystem { name } => format!("SUBSYSTEM '{name}'"),
619 MonitorScope::Bus { bus } => format!("BUS {}", bus.0),
620 MonitorScope::Area { area } => format!("AREA {area}"),
621 MonitorScope::Zone { zone } => format!("ZONE {zone}"),
622 MonitorScope::Owner { owner } => format!("OWNER {owner}"),
623 MonitorScope::Kv { kv } => format!("KV {}", decimal(*kv)),
624 }
625}
626
627fn write_branch_block(
628 head: &str,
629 branches: &[BranchRef],
630 retained: &[RetainedStatement],
631) -> String {
632 use std::fmt::Write as _;
633
634 let mut out = format!("{head}\n");
635 for branch in branches {
636 let _ = writeln!(
637 out,
638 "{:>6} {:>6} {}",
639 branch.from.0,
640 branch.to.0,
641 field(&branch.circuit)
642 );
643 }
644 for statement in retained {
645 out.push_str(&statement.text);
646 out.push('\n');
647 }
648 out.push_str("END\n");
649 out
650}
651
652fn write_statement(statement: &MonitorStatement) -> String {
653 match statement {
654 MonitorStatement::BranchesInSubsystem {
655 subsystem,
656 low_voltage_3w,
657 } => {
658 let tail = if *low_voltage_3w { " 3WLOWVOLTAGE" } else { "" };
659 format!("MONITOR BRANCHES IN SUBSYSTEM '{subsystem}'{tail}\n")
660 }
661 MonitorStatement::TiesFromSubsystem { subsystem } => {
662 format!("MONITOR TIES FROM SUBSYSTEM '{subsystem}'\n")
663 }
664 MonitorStatement::Branches { branches, retained } => {
665 write_branch_block("MONITOR BRANCHES", branches, retained)
666 }
667 MonitorStatement::Interface {
668 name,
669 rating_mw,
670 branches,
671 retained,
672 } => {
673 let head = match rating_mw {
674 Some(rating) => {
675 format!("MONITOR INTERFACE '{name}' RATING {} MW", decimal(*rating))
676 }
677 None => format!("MONITOR INTERFACE '{name}'"),
678 };
679 write_branch_block(&head, branches, retained)
680 }
681 MonitorStatement::VoltageRange { scope, vmin, vmax } => format!(
682 "MONITOR VOLTAGE RANGE {} {} {}\n",
683 write_scope(scope),
684 decimal(*vmin),
685 decimal(*vmax)
686 ),
687 MonitorStatement::VoltageDeviation { scope, down, up } => {
688 let tail = match up {
689 Some(up) => format!(" {}", decimal(*up)),
690 None => String::new(),
691 };
692 format!(
693 "MONITOR VOLTAGE DEVIATION {} {}{tail}\n",
694 write_scope(scope),
695 decimal(*down)
696 )
697 }
698 }
699}
700
701#[derive(Debug, Clone, Default, PartialEq)]
708#[non_exhaustive]
709pub struct MonitoredResolution {
710 pub branch_rows: BTreeSet<usize>,
712 pub transformer_3w_rows: BTreeSet<usize>,
714 pub tie_rows: BTreeSet<usize>,
716 pub interfaces: Vec<ResolvedInterface>,
717 pub voltage_ranges: Vec<ResolvedVoltageScope>,
718 pub voltage_deviations: Vec<ResolvedVoltageScope>,
719 pub unresolved: Vec<UnresolvedMonitor>,
721}
722
723#[derive(Debug, Clone, Default, PartialEq)]
725pub struct ResolvedInterface {
726 pub name: String,
727 pub rating_mw: Option<f64>,
728 pub members: Vec<InterfaceMember>,
730}
731
732#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
741pub struct InterfaceMember {
742 pub row: usize,
744 pub reversed: bool,
746}
747
748#[derive(Debug, Clone, Default, PartialEq)]
751pub struct ResolvedVoltageScope {
752 pub bus_rows: BTreeSet<usize>,
753 pub low: f64,
754 pub high: Option<f64>,
755}
756
757#[derive(Debug, Clone, PartialEq)]
759pub struct UnresolvedMonitor {
760 pub statement: MonitorStatement,
761 pub reason: UnresolvedMonitorReason,
762}
763
764#[derive(Debug, Clone, PartialEq, Eq)]
766#[non_exhaustive]
767pub enum UnresolvedMonitorReason {
768 NoSuchSubsystem,
770 NoSuchBranch {
771 from: BusId,
772 to: BusId,
773 circuit: String,
774 },
775 AmbiguousBranch {
777 from: BusId,
778 to: BusId,
779 circuit: String,
780 matches: usize,
781 },
782}
783
784impl MonitoredResolution {
785 #[must_use]
787 pub fn diagnostics(&self) -> Vec<Diagnostic> {
788 self.unresolved
789 .iter()
790 .map(|entry| {
791 Diagnostic::of(
792 &codes::BUILD_MON_STATEMENT_UNRESOLVED,
793 describe(&entry.statement, &entry.reason),
794 )
795 })
796 .collect()
797 }
798}
799
800fn describe(statement: &MonitorStatement, reason: &UnresolvedMonitorReason) -> String {
802 let what = match statement {
803 MonitorStatement::BranchesInSubsystem { subsystem, .. }
804 | MonitorStatement::TiesFromSubsystem { subsystem } => {
805 format!("monitored subsystem '{subsystem}'")
806 }
807 MonitorStatement::Branches { .. } => "monitored branches".to_owned(),
808 MonitorStatement::Interface { name, .. } => format!("monitored interface '{name}'"),
809 MonitorStatement::VoltageRange { .. } => "monitored voltage range".to_owned(),
810 MonitorStatement::VoltageDeviation { .. } => "monitored voltage deviation".to_owned(),
811 };
812 match reason {
813 UnresolvedMonitorReason::NoSuchSubsystem => {
814 format!("{what}: the subsystem set states no such subsystem")
815 }
816 UnresolvedMonitorReason::NoSuchBranch { from, to, circuit } => {
817 format!("{what}: no branch {from} to {to} circuit {circuit}")
818 }
819 UnresolvedMonitorReason::AmbiguousBranch {
820 from,
821 to,
822 circuit,
823 matches,
824 } => format!("{what}: branch {from} to {to} circuit {circuit} names {matches} branches"),
825 }
826}
827
828impl MonitoredSet {
829 #[must_use]
841 pub fn resolve(&self, net: &BalancedNetwork, subsystems: &SubsystemSet) -> MonitoredResolution {
842 self.resolve_with(&PsseEquipmentIndex::new(net), subsystems)
843 }
844
845 #[must_use]
851 pub fn resolve_with(
852 &self,
853 index: &PsseEquipmentIndex<'_>,
854 subsystems: &SubsystemSet,
855 ) -> MonitoredResolution {
856 let mut out = MonitoredResolution::default();
857 for statement in &self.statements {
858 resolve_statement(statement, index.network(), subsystems, index, &mut out);
859 }
860 out
861 }
862}
863
864fn resolve_statement(
865 statement: &MonitorStatement,
866 net: &BalancedNetwork,
867 subsystems: &SubsystemSet,
868 index: &PsseEquipmentIndex,
869 out: &mut MonitoredResolution,
870) {
871 match statement {
872 MonitorStatement::BranchesInSubsystem {
873 subsystem,
874 low_voltage_3w,
875 } => {
876 let Some(buses) = select(subsystems, subsystem, net) else {
877 unresolved_subsystem(statement, out);
878 return;
879 };
880 for (row, branch) in net.branches().iter().enumerate() {
881 if buses.contains(&branch.from) && buses.contains(&branch.to) {
882 out.branch_rows.insert(row);
883 }
884 }
885 if *low_voltage_3w {
886 for (row, transformer) in net.transformers_3w().iter().enumerate() {
887 if buses.contains(&low_voltage_bus(net, index, transformer)) {
888 out.transformer_3w_rows.insert(row);
889 }
890 }
891 }
892 }
893 MonitorStatement::TiesFromSubsystem { subsystem } => {
894 let Some(buses) = select(subsystems, subsystem, net) else {
895 unresolved_subsystem(statement, out);
896 return;
897 };
898 for (row, branch) in net.branches().iter().enumerate() {
899 if buses.contains(&branch.from) != buses.contains(&branch.to) {
900 out.tie_rows.insert(row);
901 }
902 }
903 }
904 MonitorStatement::Branches { branches, .. } => {
905 for branch in branches {
906 match bind_branch(index, branch) {
907 Ok(row) => {
908 out.branch_rows.insert(row);
909 }
910 Err(reason) => out.unresolved.push(UnresolvedMonitor {
911 statement: statement.clone(),
912 reason,
913 }),
914 }
915 }
916 }
917 MonitorStatement::Interface {
918 name,
919 rating_mw,
920 branches,
921 ..
922 } => {
923 let mut resolved = ResolvedInterface {
924 name: name.clone(),
925 rating_mw: *rating_mw,
926 members: Vec::new(),
927 };
928 for branch in branches {
929 match bind_branch(index, branch) {
930 Ok(row) => resolved.members.push(member(index, branch, row)),
931 Err(reason) => out.unresolved.push(UnresolvedMonitor {
932 statement: statement.clone(),
933 reason,
934 }),
935 }
936 }
937 out.interfaces.push(resolved);
938 }
939 MonitorStatement::VoltageRange { scope, vmin, vmax } => {
940 let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
941 unresolved_subsystem(statement, out);
942 return;
943 };
944 out.voltage_ranges.push(ResolvedVoltageScope {
945 bus_rows,
946 low: *vmin,
947 high: Some(*vmax),
948 });
949 }
950 MonitorStatement::VoltageDeviation { scope, down, up } => {
951 let Some(bus_rows) = scope_rows(scope, net, subsystems, index) else {
952 unresolved_subsystem(statement, out);
953 return;
954 };
955 out.voltage_deviations.push(ResolvedVoltageScope {
956 bus_rows,
957 low: *down,
958 high: *up,
959 });
960 }
961 }
962}
963
964fn unresolved_subsystem(statement: &MonitorStatement, out: &mut MonitoredResolution) {
965 out.unresolved.push(UnresolvedMonitor {
966 statement: statement.clone(),
967 reason: UnresolvedMonitorReason::NoSuchSubsystem,
968 });
969}
970
971fn select(subsystems: &SubsystemSet, name: &str, net: &BalancedNetwork) -> Option<BTreeSet<BusId>> {
972 Some(subsystems.get(name)?.select_buses(net))
973}
974
975fn member(index: &PsseEquipmentIndex<'_>, branch: &BranchRef, row: usize) -> InterfaceMember {
978 InterfaceMember {
979 row,
980 reversed: index.network().branches()[row].from != branch.from,
981 }
982}
983
984fn bind_branch(
985 index: &PsseEquipmentIndex,
986 branch: &BranchRef,
987) -> std::result::Result<usize, UnresolvedMonitorReason> {
988 let rows = index.branch_rows(branch.from, branch.to, &branch.circuit);
989 match rows.as_slice() {
990 [] => Err(UnresolvedMonitorReason::NoSuchBranch {
991 from: branch.from,
992 to: branch.to,
993 circuit: branch.circuit.clone(),
994 }),
995 [row] => Ok(*row),
996 many => Err(UnresolvedMonitorReason::AmbiguousBranch {
997 from: branch.from,
998 to: branch.to,
999 circuit: branch.circuit.clone(),
1000 matches: many.len(),
1001 }),
1002 }
1003}
1004
1005fn scope_rows(
1009 scope: &MonitorScope,
1010 net: &BalancedNetwork,
1011 subsystems: &SubsystemSet,
1012 index: &PsseEquipmentIndex,
1013) -> Option<BTreeSet<usize>> {
1014 let rows_of = |buses: &BTreeSet<BusId>| -> BTreeSet<usize> {
1015 buses.iter().filter_map(|bus| index.bus_row(*bus)).collect()
1016 };
1017 let matching = |keep: &dyn Fn(&crate::network::Bus) -> bool| -> BTreeSet<usize> {
1018 net.buses()
1019 .iter()
1020 .enumerate()
1021 .filter(|(_, bus)| keep(bus))
1022 .map(|(row, _)| row)
1023 .collect()
1024 };
1025 Some(match scope {
1026 MonitorScope::AllBuses => (0..net.buses().len()).collect(),
1027 MonitorScope::Subsystem { name } => rows_of(&select(subsystems, name, net)?),
1028 MonitorScope::Bus { bus } => index.bus_row(*bus).into_iter().collect(),
1029 MonitorScope::Area { area } => matching(&|bus| bus.area == *area),
1030 MonitorScope::Zone { zone } => matching(&|bus| bus.zone == *zone),
1031 MonitorScope::Owner { owner } => matching(&|bus| super::sub::owner_of(bus) == *owner),
1032 MonitorScope::Kv { kv } => matching(&|bus| (bus.base_kv - *kv).abs() <= KV_TOLERANCE),
1033 })
1034}