1use std::collections::BTreeMap;
19
20use powerio_core::ComponentId;
21
22use crate::diagnostics::{Diagnostic, codes};
23use crate::format::psse::{
24 detailed_source_property, quoted_circuit_id, quoted_device_id, transformer_3w_id,
25};
26use crate::network::{BalancedNetwork, BusId, BusType};
27
28use super::{ContingencyAction, ContingencySet, MAX_READER_NOTES};
29
30const BUS: &str = "bus";
34const LOAD: &str = "load";
35const SHUNT: &str = "shunt";
36const GENERATOR: &str = "generator";
37const BRANCH: &str = "branch";
38const TRANSFORMER_3W: &str = "transformer_3w";
39
40#[derive(Debug, Clone)]
54pub struct PsseEquipmentIndex<'n> {
55 net: &'n BalancedNetwork,
56 machine_ids: Vec<String>,
57 circuit_ids: Vec<String>,
58 transformer_3w_ids: Vec<String>,
59 bus_rows: BTreeMap<BusId, usize>,
60 branch_rows: BranchRows,
63 machine_rows: MachineRows,
64 fixed_shunt_rows: DeviceRows,
65 switched_shunt_rows: BTreeMap<BusId, Vec<usize>>,
66 load_rows: DeviceRows,
67 transformer_3w_rows: Transformer3wRows,
70}
71
72fn sorted_triple(buses: [BusId; 3]) -> [BusId; 3] {
73 let mut sorted = buses;
74 sorted.sort_unstable();
75 sorted
76}
77
78type DeviceRows = BTreeMap<BusId, Vec<(String, usize)>>;
83
84type BranchRows = BTreeMap<(BusId, BusId, bool, String), Vec<usize>>;
93
94type MachineRows = BTreeMap<(BusId, String), usize>;
96
97type Transformer3wRows = BTreeMap<[BusId; 3], Vec<(String, usize)>>;
100
101fn machine_index(net: &BalancedNetwork, sanitized: &mut usize) -> (Vec<String>, MachineRows) {
109 let mut ids = Vec::with_capacity(net.generators().len());
110 let mut rows = BTreeMap::new();
111 let mut used = BTreeMap::new();
112 for (row, generator) in net.generators().iter().enumerate() {
113 let preferred =
114 detailed_source_property(net, GENERATOR, generator.uid.as_deref(), "psse_eqid")
115 .filter(|id| !id.is_empty());
116 let id = quoted_circuit_id(preferred, generator.bus, &mut used, sanitized);
117 rows.insert((generator.bus, id.trim().to_owned()), row);
118 ids.push(id);
119 }
120 (ids, rows)
121}
122
123fn branch_index(net: &BalancedNetwork, sanitized: &mut usize) -> (Vec<String>, BranchRows) {
131 let mut ids = vec![String::new(); net.branches().len()];
132 let mut rows: BranchRows = BTreeMap::new();
133 let mut line_ids = BTreeMap::new();
134 let mut transformer_ids = BTreeMap::new();
135 for transformers in [false, true] {
136 for (row, branch) in net.branches().iter().enumerate() {
137 if branch.is_transformer() != transformers {
138 continue;
139 }
140 let retained = transformers
141 .then(|| {
142 detailed_source_property(net, "transformer", branch.uid.as_deref(), "psse_eqid")
143 })
144 .flatten();
145 let preferred = branch
146 .extras
147 .get("id")
148 .and_then(serde_json::Value::as_str)
149 .or(retained);
150 let used = if transformers {
151 &mut transformer_ids
152 } else {
153 &mut line_ids
154 };
155 let id = quoted_circuit_id(preferred, (branch.from, branch.to), used, sanitized);
156 rows.entry((branch.from, branch.to, transformers, id.trim().to_owned()))
157 .or_default()
158 .push(row);
159 ids[row] = id;
160 }
161 }
162 (ids, rows)
163}
164
165fn load_index(net: &BalancedNetwork, sanitized: &mut usize) -> DeviceRows {
167 let mut rows: DeviceRows = BTreeMap::new();
168 let mut used = BTreeMap::new();
169 for (row, load) in net.loads().iter().enumerate() {
170 let id = quoted_device_id(&load.extras, load.bus, &mut used, sanitized);
171 rows.entry(load.bus)
172 .or_default()
173 .push((id.trim().to_owned(), row));
174 }
175 rows
176}
177
178fn shunt_index(
184 net: &BalancedNetwork,
185 sanitized: &mut usize,
186) -> (DeviceRows, BTreeMap<BusId, Vec<usize>>) {
187 let mut fixed: DeviceRows = BTreeMap::new();
188 let mut switched: BTreeMap<BusId, Vec<usize>> = BTreeMap::new();
189 let mut used = BTreeMap::new();
190 for (row, shunt) in net.shunts().iter().enumerate() {
191 if shunt.control.is_some() {
192 switched.entry(shunt.bus).or_default().push(row);
193 continue;
194 }
195 let id = quoted_device_id(&shunt.extras, shunt.bus, &mut used, sanitized);
196 fixed
197 .entry(shunt.bus)
198 .or_default()
199 .push((id.trim().to_owned(), row));
200 }
201 (fixed, switched)
202}
203
204fn transformer_3w_index(
207 net: &BalancedNetwork,
208 sanitized: &mut usize,
209) -> (Vec<String>, Transformer3wRows) {
210 let mut ids = Vec::with_capacity(net.transformers_3w().len());
211 let mut rows: Transformer3wRows = BTreeMap::new();
212 let mut used = BTreeMap::new();
213 for (row, transformer) in net.transformers_3w().iter().enumerate() {
214 let id = transformer_3w_id(net, transformer, &mut used, sanitized);
215 let buses = sorted_triple([
216 transformer.windings[0].bus,
217 transformer.windings[1].bus,
218 transformer.windings[2].bus,
219 ]);
220 rows.entry(buses)
221 .or_default()
222 .push((id.trim().to_owned(), row));
223 ids.push(id);
224 }
225 (ids, rows)
226}
227
228impl<'n> PsseEquipmentIndex<'n> {
229 #[must_use]
231 pub fn new(net: &'n BalancedNetwork) -> Self {
232 let mut sanitized = 0usize;
235 let (machine_ids, machine_rows) = machine_index(net, &mut sanitized);
236 let (circuit_ids, branch_rows) = branch_index(net, &mut sanitized);
237 let (fixed_shunt_rows, switched_shunt_rows) = shunt_index(net, &mut sanitized);
238 let (transformer_3w_ids, transformer_3w_rows) = transformer_3w_index(net, &mut sanitized);
239 Self {
240 net,
241 machine_ids,
242 circuit_ids,
243 transformer_3w_ids,
244 bus_rows: net
245 .buses()
246 .iter()
247 .enumerate()
248 .map(|(row, bus)| (bus.id, row))
249 .collect(),
250 branch_rows,
251 machine_rows,
252 fixed_shunt_rows,
253 switched_shunt_rows,
254 load_rows: load_index(net, &mut sanitized),
255 transformer_3w_rows,
256 }
257 }
258
259 #[must_use]
262 pub fn network(&self) -> &'n BalancedNetwork {
263 self.net
264 }
265
266 #[must_use]
268 pub fn machine_ids(&self) -> &[String] {
269 &self.machine_ids
270 }
271
272 #[must_use]
274 pub fn circuit_ids(&self) -> &[String] {
275 &self.circuit_ids
276 }
277
278 #[must_use]
281 pub fn transformer_3w_ids(&self) -> &[String] {
282 &self.transformer_3w_ids
283 }
284
285 #[must_use]
287 pub fn bus_row(&self, bus: BusId) -> Option<usize> {
288 self.bus_rows.get(&bus).copied()
289 }
290
291 #[must_use]
300 pub fn branch_rows(&self, from: BusId, to: BusId, circuit: &str) -> Vec<usize> {
301 let lines = self.family_rows(from, to, circuit, false);
302 if lines.is_empty() {
303 self.family_rows(from, to, circuit, true)
304 } else {
305 lines
306 }
307 }
308
309 fn family_rows(&self, from: BusId, to: BusId, circuit: &str, transformer: bool) -> Vec<usize> {
312 let circuit = circuit.trim();
313 let mut rows = Vec::new();
314 if let Some(forward) = self
315 .branch_rows
316 .get(&(from, to, transformer, circuit.to_owned()))
317 {
318 rows.extend_from_slice(forward);
319 }
320 if from != to
321 && let Some(reverse) =
322 self.branch_rows
323 .get(&(to, from, transformer, circuit.to_owned()))
324 {
325 rows.extend_from_slice(reverse);
326 }
327 rows
328 }
329
330 #[must_use]
335 pub fn machine_row(&self, bus: BusId, id: &str) -> Option<usize> {
336 self.machine_rows.get(&(bus, id.trim().to_owned())).copied()
337 }
338
339 #[must_use]
342 pub fn fixed_shunt_rows(&self, bus: BusId, id: Option<&str>) -> Vec<usize> {
343 select_rows(self.fixed_shunt_rows.get(&bus), id)
344 }
345
346 #[must_use]
351 pub fn switched_shunt_rows(&self, bus: BusId) -> Vec<usize> {
352 self.switched_shunt_rows
353 .get(&bus)
354 .cloned()
355 .unwrap_or_default()
356 }
357
358 #[must_use]
361 pub fn load_rows(&self, bus: BusId, id: Option<&str>) -> Vec<usize> {
362 select_rows(self.load_rows.get(&bus), id)
363 }
364
365 #[must_use]
371 pub fn transformer_3w_row(&self, buses: [BusId; 3], circuit: &str) -> Option<usize> {
372 self.transformer_3w_rows(buses, circuit).first().copied()
373 }
374
375 #[must_use]
381 pub fn transformer_3w_rows(&self, buses: [BusId; 3], circuit: &str) -> Vec<usize> {
382 let circuit = circuit.trim();
383 self.transformer_3w_rows
384 .get(&sorted_triple(buses))
385 .into_iter()
386 .flatten()
387 .filter(|(id, _)| id == circuit)
388 .map(|(_, row)| *row)
389 .collect()
390 }
391}
392
393fn select_rows(at_bus: Option<&Vec<(String, usize)>>, id: Option<&str>) -> Vec<usize> {
397 let Some(devices) = at_bus else {
398 return Vec::new();
399 };
400 match id {
401 None => devices.iter().map(|(_, row)| *row).collect(),
402 Some(wanted) => {
403 let wanted = wanted.trim();
404 devices
405 .iter()
406 .filter(|(id, _)| id == wanted)
407 .map(|(_, row)| *row)
408 .collect()
409 }
410 }
411}
412
413#[derive(Debug, Clone, Default, PartialEq)]
415#[non_exhaustive]
416pub struct ContingencyResolution {
417 pub cases: Vec<ResolvedCase>,
419 pub resolved: usize,
421 pub unresolved: usize,
423 pub unrecognized_statements: usize,
425}
426
427impl ContingencyResolution {
428 #[must_use]
436 pub fn diagnostics(&self) -> Vec<Diagnostic> {
437 let mut notes = Vec::new();
438 for case in self.cases.iter().filter(|case| !case.is_resolved()) {
439 if notes.len() == MAX_READER_NOTES {
440 notes.push(Diagnostic::of(
441 &codes::BUILD_CON_NOTES_TRUNCATED,
442 "further resolution notes suppressed",
443 ));
444 break;
445 }
446 let first = &case.unresolved[0];
447 notes.push(Diagnostic::of(
448 &codes::BUILD_CON_CASE_UNRESOLVED,
449 format!(
450 "contingency '{}': {}",
451 case.name,
452 describe(&first.action, first.reason)
453 ),
454 ));
455 }
456 notes
457 }
458}
459
460#[derive(Debug, Clone, Default, PartialEq)]
462#[non_exhaustive]
463pub struct ResolvedCase {
464 pub name: String,
465 pub components: Vec<ResolvedComponent>,
468 pub unresolved: Vec<UnresolvedAction>,
470}
471
472impl ResolvedCase {
473 #[must_use]
475 pub fn is_resolved(&self) -> bool {
476 self.unresolved.is_empty()
477 }
478}
479
480#[derive(Debug, Clone, PartialEq, Eq)]
482#[non_exhaustive]
483pub struct ResolvedComponent {
484 pub component_type: &'static str,
488 pub id: Option<ComponentId>,
494 pub row: usize,
496 pub in_service: bool,
500}
501
502#[derive(Debug, Clone, PartialEq)]
504#[non_exhaustive]
505pub struct UnresolvedAction {
506 pub action: ContingencyAction,
507 pub reason: UnresolvedReason,
508}
509
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
512#[non_exhaustive]
513pub enum UnresolvedReason {
514 NoSuchBus,
515 NoSuchBranch,
516 AmbiguousBranch {
519 matches: usize,
520 },
521 AmbiguousTransformer3w {
524 matches: usize,
525 },
526 NoSuchMachine,
527 NoSuchShunt,
528 NoSuchLoad,
529 NoSuchTransformer3w,
530 Unrecognized,
532}
533
534impl UnresolvedReason {
535 #[must_use]
537 pub fn name(&self) -> &'static str {
538 match self {
539 Self::NoSuchBus => "no_such_bus",
540 Self::NoSuchBranch => "no_such_branch",
541 Self::AmbiguousBranch { .. } => "ambiguous_branch",
542 Self::AmbiguousTransformer3w { .. } => "ambiguous_transformer_3w",
543 Self::NoSuchMachine => "no_such_machine",
544 Self::NoSuchShunt => "no_such_shunt",
545 Self::NoSuchLoad => "no_such_load",
546 Self::NoSuchTransformer3w => "no_such_transformer_3w",
547 Self::Unrecognized => "unrecognized",
548 }
549 }
550}
551
552fn describe(action: &ContingencyAction, reason: UnresolvedReason) -> String {
554 match (action, reason) {
555 (ContingencyAction::OpenBranch { from, to, circuit }, UnresolvedReason::NoSuchBranch) => {
556 format!("no branch {from} to {to} circuit {circuit}")
557 }
558 (
559 ContingencyAction::OpenBranch { from, to, circuit },
560 UnresolvedReason::AmbiguousBranch { matches },
561 ) => format!("branch {from} to {to} circuit {circuit} names {matches} branches"),
562 (
563 ContingencyAction::OpenThreeWinding { buses, circuit },
564 UnresolvedReason::AmbiguousTransformer3w { matches },
565 ) => format!(
566 "three winding transformer on buses {} {} {} circuit {circuit} names {matches} transformers",
567 buses[0], buses[1], buses[2]
568 ),
569 (ContingencyAction::OpenThreeWinding { buses, circuit }, _) => format!(
570 "no three winding transformer on buses {} {} {} circuit {circuit}",
571 buses[0], buses[1], buses[2]
572 ),
573 (
574 ContingencyAction::RemoveMachine { bus, id }
575 | ContingencyAction::AddMachine { bus, id },
576 _,
577 ) => format!("no machine {id} at bus {bus}"),
578 (ContingencyAction::RemoveShunt { bus, id }, _) => match id {
579 Some(id) => format!("no fixed shunt {id} at bus {bus}"),
580 None => format!("no fixed shunt at bus {bus}"),
581 },
582 (ContingencyAction::RemoveSwitchedShunt { bus }, _) => {
583 format!("no switched shunt at bus {bus}")
584 }
585 (ContingencyAction::RemoveLoad { bus, id }, _) => match id {
586 Some(id) => format!("no load {id} at bus {bus}"),
587 None => format!("no load at bus {bus}"),
588 },
589 (ContingencyAction::Unrecognized { text }, _) => {
590 format!("statement kept as text: {text}")
591 }
592 (
593 ContingencyAction::DisconnectBus { bus }
594 | ContingencyAction::ChangeLoad { bus, .. }
595 | ContingencyAction::ChangeGeneration { bus, .. },
596 _,
597 ) => format!("no bus {bus}"),
598 (ContingencyAction::OpenBranch { from, to, circuit }, _) => {
599 format!("branch {from} to {to} circuit {circuit} did not bind")
600 }
601 }
602}
603
604impl ContingencySet {
605 #[must_use]
616 pub fn resolve(&self, net: &BalancedNetwork) -> ContingencyResolution {
617 self.resolve_with(&PsseEquipmentIndex::new(net))
618 }
619
620 #[must_use]
626 pub fn resolve_with(&self, index: &PsseEquipmentIndex<'_>) -> ContingencyResolution {
627 let mut out = ContingencyResolution::default();
628 for case in &self.cases {
629 let mut resolved = ResolvedCase {
630 name: case.name.clone(),
631 ..ResolvedCase::default()
632 };
633 for action in &case.actions {
634 if matches!(action, ContingencyAction::Unrecognized { .. }) {
635 out.unrecognized_statements += 1;
636 }
637 match bind(index, action) {
638 Ok(components) => resolved.components.extend(components),
639 Err(reason) => resolved.unresolved.push(UnresolvedAction {
640 action: action.clone(),
641 reason,
642 }),
643 }
644 }
645 if resolved.is_resolved() {
646 out.resolved += 1;
647 } else {
648 out.unresolved += 1;
649 }
650 out.cases.push(resolved);
651 }
652 out
653 }
654}
655
656fn bind(
665 index: &PsseEquipmentIndex<'_>,
666 action: &ContingencyAction,
667) -> Result<Vec<ResolvedComponent>, UnresolvedReason> {
668 let net = index.network();
669 match action {
670 ContingencyAction::OpenBranch { from, to, circuit } => {
671 let rows = index.branch_rows(*from, *to, circuit);
672 match rows.as_slice() {
673 [] => Err(UnresolvedReason::NoSuchBranch),
674 [row] => Ok(vec![branch_component(net, *row)]),
675 many => Err(UnresolvedReason::AmbiguousBranch {
676 matches: many.len(),
677 }),
678 }
679 }
680 ContingencyAction::OpenThreeWinding { buses, circuit } => {
681 let rows = index.transformer_3w_rows(*buses, circuit);
682 match rows.as_slice() {
683 [] => Err(UnresolvedReason::NoSuchTransformer3w),
684 [row] => Ok(vec![transformer_3w_component(net, *row)]),
685 many => Err(UnresolvedReason::AmbiguousTransformer3w {
686 matches: many.len(),
687 }),
688 }
689 }
690 ContingencyAction::RemoveMachine { bus, id }
691 | ContingencyAction::AddMachine { bus, id } => index
692 .machine_row(*bus, id)
693 .map(|row| vec![generator_component(net, row)])
694 .ok_or(UnresolvedReason::NoSuchMachine),
695 ContingencyAction::RemoveShunt { bus, id } => {
696 let rows = index.fixed_shunt_rows(*bus, id.as_deref());
697 non_empty(rows, UnresolvedReason::NoSuchShunt).map(|rows| {
698 rows.into_iter()
699 .map(|row| shunt_component(net, row))
700 .collect()
701 })
702 }
703 ContingencyAction::RemoveSwitchedShunt { bus } => {
704 let rows = index.switched_shunt_rows(*bus);
705 non_empty(rows, UnresolvedReason::NoSuchShunt).map(|rows| {
706 rows.into_iter()
707 .map(|row| shunt_component(net, row))
708 .collect()
709 })
710 }
711 ContingencyAction::RemoveLoad { bus, id } => {
712 let rows = index.load_rows(*bus, id.as_deref());
713 non_empty(rows, UnresolvedReason::NoSuchLoad).map(|rows| {
714 rows.into_iter()
715 .map(|row| load_component(net, row))
716 .collect()
717 })
718 }
719 ContingencyAction::DisconnectBus { bus }
720 | ContingencyAction::ChangeLoad { bus, .. }
721 | ContingencyAction::ChangeGeneration { bus, .. } => index
722 .bus_row(*bus)
723 .map(|row| vec![bus_component(net, row)])
724 .ok_or(UnresolvedReason::NoSuchBus),
725 ContingencyAction::Unrecognized { .. } => Err(UnresolvedReason::Unrecognized),
726 }
727}
728
729fn non_empty(rows: Vec<usize>, reason: UnresolvedReason) -> Result<Vec<usize>, UnresolvedReason> {
730 if rows.is_empty() {
731 Err(reason)
732 } else {
733 Ok(rows)
734 }
735}
736
737fn component_id(component_type: &str, uid: Option<&str>) -> Option<ComponentId> {
744 let local = uid.filter(|uid| !uid.is_empty())?;
745 ComponentId::new(component_type, local).ok()
746}
747
748fn bus_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
749 let bus = &net.buses()[row];
750 ResolvedComponent {
751 component_type: BUS,
752 id: component_id(BUS, bus.uid.as_deref()),
753 row,
754 in_service: bus.kind != BusType::Isolated,
755 }
756}
757
758fn load_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
759 let load = &net.loads()[row];
760 ResolvedComponent {
761 component_type: LOAD,
762 id: component_id(LOAD, load.uid.as_deref()),
763 row,
764 in_service: load.in_service,
765 }
766}
767
768fn shunt_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
769 let shunt = &net.shunts()[row];
770 ResolvedComponent {
771 component_type: SHUNT,
772 id: component_id(SHUNT, shunt.uid.as_deref()),
773 row,
774 in_service: shunt.in_service,
775 }
776}
777
778fn generator_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
779 let generator = &net.generators()[row];
780 ResolvedComponent {
781 component_type: GENERATOR,
782 id: component_id(GENERATOR, generator.uid.as_deref()),
783 row,
784 in_service: generator.in_service,
785 }
786}
787
788fn branch_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
789 let branch = &net.branches()[row];
790 ResolvedComponent {
791 component_type: BRANCH,
792 id: component_id(BRANCH, branch.uid.as_deref()),
793 row,
794 in_service: branch.in_service,
795 }
796}
797
798fn transformer_3w_component(net: &BalancedNetwork, row: usize) -> ResolvedComponent {
799 let transformer = &net.transformers_3w()[row];
800 ResolvedComponent {
801 component_type: TRANSFORMER_3W,
802 id: component_id(TRANSFORMER_3W, transformer.uid.as_deref()),
803 row,
804 in_service: transformer.in_service,
805 }
806}