1use std::collections::HashMap;
5use std::ops::Index;
6
7use faer::Mat;
8use serde::{Deserialize, Serialize};
9use surge_network::AngleReference;
10
11pub(crate) const MIN_REACTANCE: f64 = 1e-20;
21
22#[derive(Debug, thiserror::Error)]
28pub enum DcError {
29 #[error("network has no buses")]
31 EmptyNetwork,
32
33 #[error("network has no slack bus")]
35 NoSlackBus,
36
37 #[error("singular B' matrix -- network may be disconnected")]
39 SingularMatrix,
40
41 #[error("N-2 LODF computation error: {0}")]
43 Infeasible(String),
44
45 #[error("invalid network: {0}")]
47 InvalidNetwork(String),
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DcPfSolution {
57 pub theta: Vec<f64>,
59 pub branch_p_flow: Vec<f64>,
61 pub slack_p_injection: f64,
63 pub solve_time_secs: f64,
65 #[serde(default)]
67 pub total_generation_mw: f64,
68 #[serde(default)]
71 pub slack_distribution: HashMap<usize, f64>,
72 pub p_inject_pu: Vec<f64>,
84 #[serde(default)]
87 pub island_ids: Vec<usize>,
88}
89
90#[derive(Debug, Clone, Default)]
95pub struct DcPfOptions {
96 pub headroom_slack_bus_indices: Option<Vec<usize>>,
103
104 pub participation_factors: Option<Vec<(usize, f64)>>,
114
115 pub angle_reference: AngleReference,
120
121 pub external_p_injections_mw: Vec<(u32, f64)>,
132}
133
134impl DcPfOptions {
135 pub fn with_headroom_slack(participating_bus_indices: &[usize]) -> Self {
140 if participating_bus_indices.is_empty() {
141 return Self::default();
142 }
143 DcPfOptions {
144 headroom_slack_bus_indices: Some(participating_bus_indices.to_vec()),
145 ..Self::default()
146 }
147 }
148
149 pub fn with_participation_factors(weights: &[(usize, f64)]) -> Self {
154 let filtered: Vec<(usize, f64)> = weights
155 .iter()
156 .copied()
157 .filter(|&(_, w)| w > 0.0 && w.is_finite())
158 .collect();
159 if filtered.is_empty() {
160 return Self::default();
161 }
162 DcPfOptions {
163 participation_factors: Some(filtered),
164 ..Self::default()
165 }
166 }
167
168 pub fn with_network_participation(network: &surge_network::Network) -> Self {
174 let weights = network.agc_participation_by_bus();
175 if weights.is_empty() {
176 return Self::default();
177 }
178 DcPfOptions {
179 participation_factors: Some(weights),
180 ..Self::default()
181 }
182 }
183
184 pub fn with_angle_reference(mut self, angle_reference: AngleReference) -> Self {
186 self.angle_reference = angle_reference;
187 self
188 }
189}
190
191#[derive(Debug, Clone, Default)]
201pub struct DcAnalysisRequest {
202 pub monitored_branch_indices: Option<Vec<usize>>,
206 pub ptdf_bus_indices: Option<Vec<usize>>,
211 pub lodf_outage_branch_indices: Vec<usize>,
215 pub otdf_outage_branch_indices: Vec<usize>,
219 pub otdf_bus_indices: Option<Vec<usize>>,
224 pub n2_outage_pairs: Vec<(usize, usize)>,
228 pub pf_options: DcPfOptions,
230 pub sensitivity_options: Option<crate::sensitivity::DcSensitivityOptions>,
236}
237
238impl DcAnalysisRequest {
239 pub fn all_branches() -> Self {
241 Self::default()
242 }
243
244 pub fn with_monitored_branches(monitored_branch_indices: &[usize]) -> Self {
246 Self {
247 monitored_branch_indices: Some(monitored_branch_indices.to_vec()),
248 ..Self::default()
249 }
250 }
251
252 pub fn with_ptdf_buses(mut self, bus_indices: &[usize]) -> Self {
254 self.ptdf_bus_indices = Some(bus_indices.to_vec());
255 self
256 }
257
258 pub fn with_lodf_outages(mut self, outage_branch_indices: &[usize]) -> Self {
260 self.lodf_outage_branch_indices = outage_branch_indices.to_vec();
261 self
262 }
263
264 pub fn with_otdf_outages(mut self, outage_branch_indices: &[usize]) -> Self {
266 self.otdf_outage_branch_indices = outage_branch_indices.to_vec();
267 self
268 }
269
270 pub fn with_otdf_buses(mut self, bus_indices: &[usize]) -> Self {
272 self.otdf_bus_indices = Some(bus_indices.to_vec());
273 self
274 }
275
276 pub fn with_n2_outage_pairs(mut self, outage_pairs: &[(usize, usize)]) -> Self {
278 self.n2_outage_pairs = outage_pairs.to_vec();
279 self
280 }
281
282 pub fn with_pf_options(mut self, pf_options: DcPfOptions) -> Self {
284 self.pf_options = pf_options;
285 self
286 }
287
288 pub fn with_sensitivity_options(
290 mut self,
291 sensitivity_options: crate::sensitivity::DcSensitivityOptions,
292 ) -> Self {
293 self.sensitivity_options = Some(sensitivity_options);
294 self
295 }
296}
297
298#[derive(Debug, Clone)]
300pub struct DcAnalysisResult {
301 pub power_flow: DcPfSolution,
303 pub monitored_branch_indices: Vec<usize>,
305 pub ptdf: PtdfRows,
307 pub ptdf_bus_indices: Vec<usize>,
309 pub otdf: Option<OtdfResult>,
311 pub otdf_outage_branch_indices: Vec<usize>,
313 pub otdf_bus_indices: Vec<usize>,
315 pub lodf: Option<LodfResult>,
317 pub lodf_outage_branch_indices: Vec<usize>,
319 pub n2_lodf: Option<N2LodfBatchResult>,
321 pub n2_outage_pairs: Vec<(usize, usize)>,
323}
324
325#[derive(Debug, Clone, PartialEq, Default)]
331pub struct PtdfRows {
332 monitored_branch_indices: Vec<usize>,
333 row_positions: Vec<Option<usize>>,
334 bus_indices: Vec<usize>,
335 bus_positions: Vec<Option<usize>>,
336 n_values_per_row: usize,
337 values: Vec<f64>,
338}
339
340impl PtdfRows {
341 pub(crate) fn new(
342 monitored_branch_indices: &[usize],
343 bus_indices: &[usize],
344 n_branches: usize,
345 n_total_buses: usize,
346 ) -> Self {
347 let mut row_positions = vec![None; n_branches];
348 for (row_pos, &branch_idx) in monitored_branch_indices.iter().enumerate() {
349 if branch_idx < n_branches && row_positions[branch_idx].is_none() {
350 row_positions[branch_idx] = Some(row_pos);
351 }
352 }
353
354 let mut bus_positions = vec![None; n_total_buses];
355 for (bus_pos, &bus_idx) in bus_indices.iter().enumerate() {
356 if bus_idx < n_total_buses && bus_positions[bus_idx].is_none() {
357 bus_positions[bus_idx] = Some(bus_pos);
358 }
359 }
360 Self {
361 monitored_branch_indices: monitored_branch_indices.to_vec(),
362 row_positions,
363 bus_indices: bus_indices.to_vec(),
364 bus_positions,
365 n_values_per_row: bus_indices.len(),
366 values: vec![0.0; monitored_branch_indices.len() * bus_indices.len()],
367 }
368 }
369
370 #[inline]
372 pub fn monitored_branches(&self) -> &[usize] {
373 &self.monitored_branch_indices
374 }
375
376 #[inline]
378 pub fn bus_indices(&self) -> &[usize] {
379 &self.bus_indices
380 }
381
382 #[inline]
384 pub fn n_rows(&self) -> usize {
385 self.monitored_branch_indices.len()
386 }
387
388 #[inline]
390 pub fn n_cols(&self) -> usize {
391 self.n_values_per_row
392 }
393
394 #[inline]
396 pub fn row(&self, branch_idx: usize) -> Option<&[f64]> {
397 let row_pos = self.row_positions.get(branch_idx).copied().flatten()?;
398 Some(self.row_at(row_pos))
399 }
400
401 #[inline]
403 pub fn row_at(&self, row_pos: usize) -> &[f64] {
404 let start = row_pos * self.n_values_per_row;
405 &self.values[start..start + self.n_values_per_row]
406 }
407
408 #[inline(always)]
409 pub(crate) fn row_at_mut(&mut self, row_pos: usize) -> &mut [f64] {
410 let start = row_pos * self.n_values_per_row;
411 &mut self.values[start..start + self.n_values_per_row]
412 }
413
414 #[inline]
416 pub fn get(&self, branch_idx: usize, bus_idx: usize) -> f64 {
417 let Some(bus_pos) = self.bus_positions.get(bus_idx).copied().flatten() else {
418 return 0.0;
419 };
420 self.row(branch_idx)
421 .and_then(|row| row.get(bus_pos))
422 .copied()
423 .unwrap_or(0.0)
424 }
425
426 pub fn into_parts(self) -> (Vec<usize>, Vec<usize>, Vec<f64>) {
428 (self.monitored_branch_indices, self.bus_indices, self.values)
429 }
430}
431
432#[derive(Debug, Clone, PartialEq)]
438pub struct OtdfResult {
439 monitored_branch_indices: Vec<usize>,
440 outage_branch_indices: Vec<usize>,
441 bus_indices: Vec<usize>,
442 monitored_positions: Vec<Option<usize>>,
443 outage_positions: Vec<Option<usize>>,
444 bus_positions: Vec<Option<usize>>,
445 n_values_per_vector: usize,
446 values: Vec<f64>,
447}
448
449impl OtdfResult {
450 pub(crate) fn new(
451 monitored_branch_indices: &[usize],
452 outage_branch_indices: &[usize],
453 bus_indices: &[usize],
454 n_branches: usize,
455 n_total_buses: usize,
456 ) -> Self {
457 let mut monitored_positions = vec![None; n_branches];
458 for (row_pos, &branch_idx) in monitored_branch_indices.iter().enumerate() {
459 if branch_idx < n_branches && monitored_positions[branch_idx].is_none() {
460 monitored_positions[branch_idx] = Some(row_pos);
461 }
462 }
463
464 let mut outage_positions = vec![None; n_branches];
465 for (col_pos, &branch_idx) in outage_branch_indices.iter().enumerate() {
466 if branch_idx < n_branches && outage_positions[branch_idx].is_none() {
467 outage_positions[branch_idx] = Some(col_pos);
468 }
469 }
470
471 let mut bus_positions = vec![None; n_total_buses];
472 for (bus_pos, &bus_idx) in bus_indices.iter().enumerate() {
473 if bus_idx < n_total_buses && bus_positions[bus_idx].is_none() {
474 bus_positions[bus_idx] = Some(bus_pos);
475 }
476 }
477
478 Self {
479 monitored_branch_indices: monitored_branch_indices.to_vec(),
480 outage_branch_indices: outage_branch_indices.to_vec(),
481 bus_indices: bus_indices.to_vec(),
482 monitored_positions,
483 outage_positions,
484 bus_positions,
485 n_values_per_vector: bus_indices.len(),
486 values: vec![
487 0.0;
488 monitored_branch_indices.len()
489 * outage_branch_indices.len()
490 * bus_indices.len()
491 ],
492 }
493 }
494
495 #[inline]
497 pub fn monitored_branches(&self) -> &[usize] {
498 &self.monitored_branch_indices
499 }
500
501 #[inline]
503 pub fn outage_branches(&self) -> &[usize] {
504 &self.outage_branch_indices
505 }
506
507 #[inline]
509 pub fn bus_indices(&self) -> &[usize] {
510 &self.bus_indices
511 }
512
513 #[inline]
515 pub fn n_monitored(&self) -> usize {
516 self.monitored_branch_indices.len()
517 }
518
519 #[inline]
521 pub fn n_outages(&self) -> usize {
522 self.outage_branch_indices.len()
523 }
524
525 #[inline]
527 pub fn n_buses(&self) -> usize {
528 self.n_values_per_vector
529 }
530
531 #[inline]
533 pub fn vector(&self, monitored_branch_idx: usize, outage_branch_idx: usize) -> Option<&[f64]> {
534 let monitored_pos = self
535 .monitored_positions
536 .get(monitored_branch_idx)
537 .copied()
538 .flatten()?;
539 let outage_pos = self
540 .outage_positions
541 .get(outage_branch_idx)
542 .copied()
543 .flatten()?;
544 Some(self.vector_at(monitored_pos, outage_pos))
545 }
546
547 #[inline]
549 pub fn vector_at(&self, monitored_pos: usize, outage_pos: usize) -> &[f64] {
550 let start = (monitored_pos * self.n_outages() + outage_pos) * self.n_values_per_vector;
551 &self.values[start..start + self.n_values_per_vector]
552 }
553
554 #[inline(always)]
555 pub(crate) fn vector_at_mut(&mut self, monitored_pos: usize, outage_pos: usize) -> &mut [f64] {
556 let start = (monitored_pos * self.n_outages() + outage_pos) * self.n_values_per_vector;
557 &mut self.values[start..start + self.n_values_per_vector]
558 }
559
560 #[inline]
562 pub fn get(
563 &self,
564 monitored_branch_idx: usize,
565 outage_branch_idx: usize,
566 bus_idx: usize,
567 ) -> f64 {
568 let Some(bus_pos) = self.bus_positions.get(bus_idx).copied().flatten() else {
569 return 0.0;
570 };
571 self.vector(monitored_branch_idx, outage_branch_idx)
572 .and_then(|vector| vector.get(bus_pos))
573 .copied()
574 .unwrap_or(0.0)
575 }
576
577 pub fn into_parts(self) -> (Vec<usize>, Vec<usize>, Vec<usize>, Vec<f64>) {
579 (
580 self.monitored_branch_indices,
581 self.outage_branch_indices,
582 self.bus_indices,
583 self.values,
584 )
585 }
586}
587
588#[derive(Debug, Clone, PartialEq)]
594pub struct LodfResult {
595 monitored_branch_indices: Vec<usize>,
596 outage_branch_indices: Vec<usize>,
597 values: Mat<f64>,
598}
599
600impl LodfResult {
601 pub(crate) fn new(
602 monitored_branch_indices: &[usize],
603 outage_branch_indices: &[usize],
604 values: Mat<f64>,
605 ) -> Self {
606 Self {
607 monitored_branch_indices: monitored_branch_indices.to_vec(),
608 outage_branch_indices: outage_branch_indices.to_vec(),
609 values,
610 }
611 }
612
613 #[inline]
615 pub fn monitored_branches(&self) -> &[usize] {
616 &self.monitored_branch_indices
617 }
618
619 #[inline]
621 pub fn outage_branches(&self) -> &[usize] {
622 &self.outage_branch_indices
623 }
624
625 #[inline]
627 pub fn n_rows(&self) -> usize {
628 self.values.nrows()
629 }
630
631 #[inline]
633 pub fn n_cols(&self) -> usize {
634 self.values.ncols()
635 }
636
637 #[inline]
639 pub fn matrix(&self) -> &Mat<f64> {
640 &self.values
641 }
642
643 #[inline]
645 pub fn into_parts(self) -> (Vec<usize>, Vec<usize>, Mat<f64>) {
646 (
647 self.monitored_branch_indices,
648 self.outage_branch_indices,
649 self.values,
650 )
651 }
652}
653
654impl Index<(usize, usize)> for LodfResult {
655 type Output = f64;
656
657 fn index(&self, index: (usize, usize)) -> &Self::Output {
658 &self.values[index]
659 }
660}
661
662#[derive(Debug, Clone, PartialEq)]
664pub struct LodfMatrixResult {
665 branch_indices: Vec<usize>,
666 values: Mat<f64>,
667}
668
669impl LodfMatrixResult {
670 pub(crate) fn new(branch_indices: &[usize], values: Mat<f64>) -> Self {
671 Self {
672 branch_indices: branch_indices.to_vec(),
673 values,
674 }
675 }
676
677 #[inline]
679 pub fn branches(&self) -> &[usize] {
680 &self.branch_indices
681 }
682
683 #[inline]
685 pub fn n_rows(&self) -> usize {
686 self.values.nrows()
687 }
688
689 #[inline]
691 pub fn n_cols(&self) -> usize {
692 self.values.ncols()
693 }
694
695 #[inline]
697 pub fn matrix(&self) -> &Mat<f64> {
698 &self.values
699 }
700
701 #[inline]
703 pub fn into_parts(self) -> (Vec<usize>, Mat<f64>) {
704 (self.branch_indices, self.values)
705 }
706}
707
708impl Index<(usize, usize)> for LodfMatrixResult {
709 type Output = f64;
710
711 fn index(&self, index: (usize, usize)) -> &Self::Output {
712 &self.values[index]
713 }
714}
715
716#[derive(Debug, Clone, PartialEq)]
718pub struct LodfPairs {
719 monitored_branch_indices: Vec<usize>,
720 outage_branch_indices: Vec<usize>,
721 values: HashMap<(usize, usize), f64>,
722}
723
724impl LodfPairs {
725 pub(crate) fn new(
726 monitored_branch_indices: &[usize],
727 outage_branch_indices: &[usize],
728 values: HashMap<(usize, usize), f64>,
729 ) -> Self {
730 Self {
731 monitored_branch_indices: monitored_branch_indices.to_vec(),
732 outage_branch_indices: outage_branch_indices.to_vec(),
733 values,
734 }
735 }
736
737 #[inline]
739 pub fn monitored_branches(&self) -> &[usize] {
740 &self.monitored_branch_indices
741 }
742
743 #[inline]
745 pub fn outage_branches(&self) -> &[usize] {
746 &self.outage_branch_indices
747 }
748
749 #[inline]
751 pub fn len(&self) -> usize {
752 self.values.len()
753 }
754
755 #[inline]
757 pub fn is_empty(&self) -> bool {
758 self.values.is_empty()
759 }
760
761 #[inline]
763 pub fn get(&self, monitored_branch_idx: usize, outage_branch_idx: usize) -> Option<f64> {
764 self.values
765 .get(&(monitored_branch_idx, outage_branch_idx))
766 .copied()
767 }
768
769 #[inline]
771 pub fn entries(&self) -> &HashMap<(usize, usize), f64> {
772 &self.values
773 }
774
775 #[allow(clippy::type_complexity)]
777 #[inline]
778 pub fn into_parts(self) -> (Vec<usize>, Vec<usize>, HashMap<(usize, usize), f64>) {
779 (
780 self.monitored_branch_indices,
781 self.outage_branch_indices,
782 self.values,
783 )
784 }
785}
786
787impl IntoIterator for LodfPairs {
788 type Item = ((usize, usize), f64);
789 type IntoIter = std::collections::hash_map::IntoIter<(usize, usize), f64>;
790
791 fn into_iter(self) -> Self::IntoIter {
792 self.values.into_iter()
793 }
794}
795
796impl<'a> IntoIterator for &'a LodfPairs {
797 type Item = (&'a (usize, usize), &'a f64);
798 type IntoIter = std::collections::hash_map::Iter<'a, (usize, usize), f64>;
799
800 fn into_iter(self) -> Self::IntoIter {
801 self.values.iter()
802 }
803}
804
805#[derive(Debug, Clone, PartialEq)]
811pub struct N2LodfResult {
812 monitored_branch_indices: Vec<usize>,
813 outage_pair: (usize, usize),
814 values: Vec<f64>,
815}
816
817impl N2LodfResult {
818 pub(crate) fn new(
819 monitored_branch_indices: &[usize],
820 outage_pair: (usize, usize),
821 values: Vec<f64>,
822 ) -> Self {
823 Self {
824 monitored_branch_indices: monitored_branch_indices.to_vec(),
825 outage_pair,
826 values,
827 }
828 }
829
830 #[inline]
832 pub fn monitored_branches(&self) -> &[usize] {
833 &self.monitored_branch_indices
834 }
835
836 #[inline]
838 pub fn outage_pair(&self) -> (usize, usize) {
839 self.outage_pair
840 }
841
842 #[inline]
844 pub fn values(&self) -> &[f64] {
845 &self.values
846 }
847
848 #[inline]
850 pub fn len(&self) -> usize {
851 self.values.len()
852 }
853
854 #[inline]
856 pub fn is_empty(&self) -> bool {
857 self.values.is_empty()
858 }
859
860 #[inline]
862 pub fn iter(&self) -> std::slice::Iter<'_, f64> {
863 self.values.iter()
864 }
865
866 #[inline]
868 pub fn into_parts(self) -> (Vec<usize>, (usize, usize), Vec<f64>) {
869 (self.monitored_branch_indices, self.outage_pair, self.values)
870 }
871}
872
873impl Index<usize> for N2LodfResult {
874 type Output = f64;
875
876 fn index(&self, index: usize) -> &Self::Output {
877 &self.values[index]
878 }
879}
880
881#[derive(Debug, Clone, PartialEq)]
883pub struct N2LodfBatchResult {
884 monitored_branch_indices: Vec<usize>,
885 outage_pairs: Vec<(usize, usize)>,
886 values: Mat<f64>,
887}
888
889impl N2LodfBatchResult {
890 pub(crate) fn new(
891 monitored_branch_indices: &[usize],
892 outage_pairs: &[(usize, usize)],
893 values: Mat<f64>,
894 ) -> Self {
895 Self {
896 monitored_branch_indices: monitored_branch_indices.to_vec(),
897 outage_pairs: outage_pairs.to_vec(),
898 values,
899 }
900 }
901
902 #[inline]
904 pub fn monitored_branches(&self) -> &[usize] {
905 &self.monitored_branch_indices
906 }
907
908 #[inline]
910 pub fn outage_pairs(&self) -> &[(usize, usize)] {
911 &self.outage_pairs
912 }
913
914 #[inline]
916 pub fn n_rows(&self) -> usize {
917 self.values.nrows()
918 }
919
920 #[inline]
922 pub fn n_cols(&self) -> usize {
923 self.values.ncols()
924 }
925
926 #[inline]
928 pub fn matrix(&self) -> &Mat<f64> {
929 &self.values
930 }
931
932 #[inline]
934 pub fn into_parts(self) -> (Vec<usize>, Vec<(usize, usize)>, Mat<f64>) {
935 (
936 self.monitored_branch_indices,
937 self.outage_pairs,
938 self.values,
939 )
940 }
941}
942
943impl Index<(usize, usize)> for N2LodfBatchResult {
944 type Output = f64;
945
946 fn index(&self, index: (usize, usize)) -> &Self::Output {
947 &self.values[index]
948 }
949}
950
951#[derive(Debug, Clone, Copy)]
956pub(crate) struct BranchDcMetadata {
957 pub(crate) from_full: usize,
958 pub(crate) to_full: usize,
959 pub(crate) b_dc: f64,
960 pub(crate) in_service: bool,
961 pub(crate) has_reactance: bool,
962}
963
964impl BranchDcMetadata {
965 #[inline(always)]
966 pub(crate) fn is_sensitivity_active(self) -> bool {
967 self.in_service && self.has_reactance
968 }
969}