Skip to main content

surge_dc/
types.rs

1// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0
2//! Data types for DC power flow results, requests, and errors.
3
4use std::collections::HashMap;
5use std::ops::Index;
6
7use faer::Mat;
8use serde::{Deserialize, Serialize};
9use surge_network::AngleReference;
10
11// ---------------------------------------------------------------------------
12// Constants
13// ---------------------------------------------------------------------------
14
15/// Minimum absolute reactance for a branch to participate in DC power flow.
16///
17/// Branches with `|x| < MIN_REACTANCE` are treated as zero-impedance ties and
18/// excluded from the B' matrix, sensitivity computations, and flow calculations.
19/// This matches the threshold used by [`Branch::b_dc()`](surge_network::network::Branch::b_dc).
20pub(crate) const MIN_REACTANCE: f64 = 1e-20;
21
22// ---------------------------------------------------------------------------
23// Error type
24// ---------------------------------------------------------------------------
25
26/// Errors arising from DC power flow or sensitivity computation.
27#[derive(Debug, thiserror::Error)]
28pub enum DcError {
29    /// The network contains no buses.
30    #[error("network has no buses")]
31    EmptyNetwork,
32
33    /// No slack (reference) bus found in the network or island.
34    #[error("network has no slack bus")]
35    NoSlackBus,
36
37    /// The B' susceptance matrix is singular, typically due to a disconnected network.
38    #[error("singular B' matrix -- network may be disconnected")]
39    SingularMatrix,
40
41    /// The requested computation is infeasible (e.g., bridge-line N-2 outage).
42    #[error("N-2 LODF computation error: {0}")]
43    Infeasible(String),
44
45    /// The network model is invalid (e.g., out-of-range bus or branch index).
46    #[error("invalid network: {0}")]
47    InvalidNetwork(String),
48}
49
50// ---------------------------------------------------------------------------
51// Power flow solution and options
52// ---------------------------------------------------------------------------
53
54/// Result of a DC power flow computation.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DcPfSolution {
57    /// Bus voltage angles in radians (indexed by internal bus order).
58    pub theta: Vec<f64>,
59    /// Real power flow on each branch in per-unit (positive = from->to).
60    pub branch_p_flow: Vec<f64>,
61    /// Slack bus real power injection in per-unit.
62    pub slack_p_injection: f64,
63    /// Solve time in seconds.
64    pub solve_time_secs: f64,
65    /// Total system generation in MW (only populated for headroom-slack solves).
66    #[serde(default)]
67    pub total_generation_mw: f64,
68    /// Per-bus slack absorption: internal bus index -> MW absorbed.
69    /// Non-empty only when headroom slack is used.
70    #[serde(default)]
71    pub slack_distribution: HashMap<usize, f64>,
72    /// Effective real power injection at each bus in per-unit (full bus order).
73    ///
74    /// For non-slack buses this is the actual RHS used in the solve: scheduled
75    /// Pg − Pd plus Gs shunt losses, PST phantom injections, two-terminal HVDC
76    /// schedules, MTDC converter injections, and any distributed-slack shares.
77    /// For the slack bus it is the back-calculated KCL value from branch flows.
78    ///
79    /// Use this field — not `bus_p_injection_pu()` — when reporting or
80    /// post-processing injection balance, because `bus_p_injection_pu()` omits
81    /// the correction terms above and will not balance against branch flows once
82    /// those features are active.
83    pub p_inject_pu: Vec<f64>,
84    /// Island assignment for each bus (indexed by internal bus index).
85    /// `island_ids[i]` is the island index for bus `i`.
86    #[serde(default)]
87    pub island_ids: Vec<usize>,
88}
89
90/// Options controlling the DC power flow solve.
91///
92/// All fields are optional -- pass `DcPfOptions::default()` for the classic
93/// single-slack behavior.
94#[derive(Debug, Clone, Default)]
95pub struct DcPfOptions {
96    /// Buses participating in headroom-limited slack balancing.
97    ///
98    /// When `Some`, the total power imbalance is redistributed among the listed
99    /// buses in proportion to available generator headroom at those buses.
100    ///
101    /// When `None` (default), the classic single-bus slack formulation is used.
102    pub headroom_slack_bus_indices: Option<Vec<usize>>,
103
104    /// Participation-factor slack weights: `(internal_bus_index, weight)`.
105    ///
106    /// When `Some`, the total power imbalance is redistributed among the
107    /// listed buses in proportion to their weights (normalized internally to
108    /// sum to 1.0). This is the RTO-style distributed slack model where
109    /// weights typically come from generator AGC participation factors
110    /// aggregated by bus.
111    ///
112    /// Takes precedence over `headroom_slack_bus_indices` when both are set.
113    pub participation_factors: Option<Vec<(usize, f64)>>,
114
115    /// Output angle reference convention for reported `theta`.
116    ///
117    /// This affects only the returned bus angles. It does not affect branch
118    /// flows, slack redistribution, or the DC power balance itself.
119    pub angle_reference: AngleReference,
120
121    /// External real-power injections added to the DC power flow RHS.
122    ///
123    /// Each entry is `(bus_number, p_mw)` where `bus_number` is the external
124    /// bus number (matching `Bus::number`) and `p_mw` is the injection in MW
125    /// (positive = generation / injection into AC network, negative = load /
126    /// withdrawal from AC network).
127    ///
128    /// Use this to pass pre-computed MTDC converter injections, external
129    /// schedules, or any other bus-level P corrections that should be folded
130    /// into the DC power flow injection vector.
131    pub external_p_injections_mw: Vec<(u32, f64)>,
132}
133
134impl DcPfOptions {
135    /// Build options with headroom-limited slack balancing on the listed buses.
136    ///
137    /// Indices are *internal* (0-based array position in `Network::buses`),
138    /// not external bus numbers.
139    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    /// Build options with explicit participation-factor slack weights.
150    ///
151    /// Each entry is `(internal_bus_index, weight)`. Weights are normalized
152    /// internally. Zero and negative weights are ignored.
153    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    /// Build options using AGC participation factors from the network's generators.
169    ///
170    /// Aggregates `Generator::agc_participation_factor` by bus, producing
171    /// the standard RTO-style distributed slack model. Falls back to
172    /// single-slack if no generators have participation factors set.
173    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    /// Set the output angle reference convention.
185    pub fn with_angle_reference(mut self, angle_reference: AngleReference) -> Self {
186        self.angle_reference = angle_reference;
187        self
188    }
189}
190
191// ---------------------------------------------------------------------------
192// Analysis request / result
193// ---------------------------------------------------------------------------
194
195/// Canonical request for a one-pass DC sensitivity workflow.
196///
197/// This computes DC power flow plus PTDF for a monitored branch set, and
198/// optionally includes OTDF, subset LODF, and batched N-2 sensitivities over
199/// the same monitored set.
200#[derive(Debug, Clone, Default)]
201pub struct DcAnalysisRequest {
202    /// Branch indices to monitor for PTDF / LODF / N-2 results.
203    ///
204    /// When `None`, all branches in `network.branches` are monitored.
205    pub monitored_branch_indices: Option<Vec<usize>>,
206    /// Optional bus subset for PTDF output vectors.
207    ///
208    /// When `None`, PTDF is returned for all buses. When `Some`, PTDF vectors
209    /// are returned only for the requested internal bus indices in the given order.
210    pub ptdf_bus_indices: Option<Vec<usize>>,
211    /// Explicit outage branches for subset LODF.
212    ///
213    /// When empty, no LODF subset is computed.
214    pub lodf_outage_branch_indices: Vec<usize>,
215    /// Explicit outage branches for subset OTDF.
216    ///
217    /// When empty, no OTDF subset is computed.
218    pub otdf_outage_branch_indices: Vec<usize>,
219    /// Optional bus subset for OTDF output vectors.
220    ///
221    /// When `None`, OTDF is returned for all buses. When `Some`, OTDF vectors
222    /// are returned only for the requested internal bus indices in the given order.
223    pub otdf_bus_indices: Option<Vec<usize>>,
224    /// Explicit outage pairs for batched N-2 sensitivity computation.
225    ///
226    /// When empty, no N-2 batch is computed.
227    pub n2_outage_pairs: Vec<(usize, usize)>,
228    /// DC power flow options applied to the base solve.
229    pub pf_options: DcPfOptions,
230    /// Optional sensitivity options applied to PTDF / OTDF outputs.
231    ///
232    /// When `None`, the sensitivity workflow derives its slack policy from
233    /// `pf_options`: headroom-slack solves use headroom-slack sensitivities,
234    /// otherwise the classic single-slack sensitivity formulation is used.
235    pub sensitivity_options: Option<crate::sensitivity::DcSensitivityOptions>,
236}
237
238impl DcAnalysisRequest {
239    /// Monitor all branches and compute only the base DC power flow + PTDF.
240    pub fn all_branches() -> Self {
241        Self::default()
242    }
243
244    /// Monitor the requested branch set.
245    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    /// Restrict PTDF output to the requested internal bus indices.
253    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    /// Add a subset LODF request for the given outage branches.
259    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    /// Add a subset OTDF request for the given outage branches.
265    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    /// Restrict OTDF output to the requested internal bus indices.
271    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    /// Add a batched N-2 request for the given outage pairs.
277    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    /// Apply DC power flow options to the base solve.
283    pub fn with_pf_options(mut self, pf_options: DcPfOptions) -> Self {
284        self.pf_options = pf_options;
285        self
286    }
287
288    /// Apply explicit sensitivity options to PTDF / OTDF outputs.
289    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/// Result of the canonical one-pass DC sensitivity workflow.
299#[derive(Debug, Clone)]
300pub struct DcAnalysisResult {
301    /// Base DC power flow solution for the requested slack policy.
302    pub power_flow: DcPfSolution,
303    /// Monitored branch set used by PTDF / LODF / N-2 outputs.
304    pub monitored_branch_indices: Vec<usize>,
305    /// PTDF rows in monitored-branch order.
306    pub ptdf: PtdfRows,
307    /// Bus order for `ptdf`.
308    pub ptdf_bus_indices: Vec<usize>,
309    /// Optional monitored-by-outage-by-bus OTDF tensor.
310    pub otdf: Option<OtdfResult>,
311    /// Outage order for `otdf`, when present.
312    pub otdf_outage_branch_indices: Vec<usize>,
313    /// Bus order for `otdf`, when present.
314    pub otdf_bus_indices: Vec<usize>,
315    /// Optional rectangular monitored-by-outage LODF matrix.
316    pub lodf: Option<LodfResult>,
317    /// Column order for `lodf`, when present.
318    pub lodf_outage_branch_indices: Vec<usize>,
319    /// Optional monitored-by-outage-pair N-2 matrix.
320    pub n2_lodf: Option<N2LodfBatchResult>,
321    /// Column order for `n2_lodf`, when present.
322    pub n2_outage_pairs: Vec<(usize, usize)>,
323}
324
325// ---------------------------------------------------------------------------
326// PTDF
327// ---------------------------------------------------------------------------
328
329/// PTDF rows stored in monitored-branch order with dense row-major bus data.
330#[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    /// Returns the monitored branch indices in row order.
371    #[inline]
372    pub fn monitored_branches(&self) -> &[usize] {
373        &self.monitored_branch_indices
374    }
375
376    /// Returns the bus indices in column order.
377    #[inline]
378    pub fn bus_indices(&self) -> &[usize] {
379        &self.bus_indices
380    }
381
382    /// Number of monitored branches (rows).
383    #[inline]
384    pub fn n_rows(&self) -> usize {
385        self.monitored_branch_indices.len()
386    }
387
388    /// Number of buses (columns) per PTDF row.
389    #[inline]
390    pub fn n_cols(&self) -> usize {
391        self.n_values_per_row
392    }
393
394    /// Returns the PTDF row for `branch_idx`, or `None` if not monitored.
395    #[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    /// Returns the PTDF row at positional index `row_pos`.
402    #[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    /// Returns the PTDF value for `(branch_idx, bus_idx)`, or 0.0 if not present.
415    #[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    /// Decompose into `(monitored_branch_indices, bus_indices, values)`.
427    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// ---------------------------------------------------------------------------
433// OTDF
434// ---------------------------------------------------------------------------
435
436/// OTDF vectors stored in monitored-by-outage order with dense row-major bus data.
437#[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    /// Returns the monitored branch indices in row order.
496    #[inline]
497    pub fn monitored_branches(&self) -> &[usize] {
498        &self.monitored_branch_indices
499    }
500
501    /// Returns the outage branch indices in column order.
502    #[inline]
503    pub fn outage_branches(&self) -> &[usize] {
504        &self.outage_branch_indices
505    }
506
507    /// Returns the bus indices for each OTDF vector.
508    #[inline]
509    pub fn bus_indices(&self) -> &[usize] {
510        &self.bus_indices
511    }
512
513    /// Number of monitored branches.
514    #[inline]
515    pub fn n_monitored(&self) -> usize {
516        self.monitored_branch_indices.len()
517    }
518
519    /// Number of outage branches.
520    #[inline]
521    pub fn n_outages(&self) -> usize {
522        self.outage_branch_indices.len()
523    }
524
525    /// Number of buses per OTDF vector.
526    #[inline]
527    pub fn n_buses(&self) -> usize {
528        self.n_values_per_vector
529    }
530
531    /// Returns the OTDF vector for a `(monitored, outage)` pair, or `None` if not present.
532    #[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    /// Returns the OTDF vector at positional indices `(monitored_pos, outage_pos)`.
548    #[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    /// Returns the OTDF value for `(monitored, outage, bus)`, or 0.0 if not present.
561    #[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    /// Decompose into `(monitored_indices, outage_indices, bus_indices, values)`.
578    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// ---------------------------------------------------------------------------
589// LODF
590// ---------------------------------------------------------------------------
591
592/// Rectangular LODF result for explicit monitored and outage branch sets.
593#[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    /// Returns the monitored branch indices (row order).
614    #[inline]
615    pub fn monitored_branches(&self) -> &[usize] {
616        &self.monitored_branch_indices
617    }
618
619    /// Returns the outage branch indices (column order).
620    #[inline]
621    pub fn outage_branches(&self) -> &[usize] {
622        &self.outage_branch_indices
623    }
624
625    /// Number of monitored branches (rows).
626    #[inline]
627    pub fn n_rows(&self) -> usize {
628        self.values.nrows()
629    }
630
631    /// Number of outage branches (columns).
632    #[inline]
633    pub fn n_cols(&self) -> usize {
634        self.values.ncols()
635    }
636
637    /// Returns a reference to the underlying dense LODF matrix.
638    #[inline]
639    pub fn matrix(&self) -> &Mat<f64> {
640        &self.values
641    }
642
643    /// Decompose into `(monitored_indices, outage_indices, values)`.
644    #[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/// Dense all-pairs LODF result for one branch set.
663#[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    /// Returns the branch indices used as both rows and columns.
678    #[inline]
679    pub fn branches(&self) -> &[usize] {
680        &self.branch_indices
681    }
682
683    /// Number of rows (branches).
684    #[inline]
685    pub fn n_rows(&self) -> usize {
686        self.values.nrows()
687    }
688
689    /// Number of columns (branches).
690    #[inline]
691    pub fn n_cols(&self) -> usize {
692        self.values.ncols()
693    }
694
695    /// Returns a reference to the underlying dense all-pairs LODF matrix.
696    #[inline]
697    pub fn matrix(&self) -> &Mat<f64> {
698        &self.values
699    }
700
701    /// Decompose into `(branch_indices, values)`.
702    #[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/// Sparse LODF entries keyed by `(monitored_branch_idx, outage_branch_idx)`.
717#[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    /// Returns the monitored branch indices.
738    #[inline]
739    pub fn monitored_branches(&self) -> &[usize] {
740        &self.monitored_branch_indices
741    }
742
743    /// Returns the outage branch indices.
744    #[inline]
745    pub fn outage_branches(&self) -> &[usize] {
746        &self.outage_branch_indices
747    }
748
749    /// Number of stored LODF entries.
750    #[inline]
751    pub fn len(&self) -> usize {
752        self.values.len()
753    }
754
755    /// Returns `true` if no entries are stored.
756    #[inline]
757    pub fn is_empty(&self) -> bool {
758        self.values.is_empty()
759    }
760
761    /// Returns the LODF value for a `(monitored, outage)` pair, if present.
762    #[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    /// Returns a reference to the underlying `(monitored, outage) -> LODF` map.
770    #[inline]
771    pub fn entries(&self) -> &HashMap<(usize, usize), f64> {
772        &self.values
773    }
774
775    /// Decompose into `(monitored_indices, outage_indices, entries)`.
776    #[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// ---------------------------------------------------------------------------
806// N-2 LODF
807// ---------------------------------------------------------------------------
808
809/// N-2 LODF result for one ordered outage pair.
810#[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    /// Returns the monitored branch indices.
831    #[inline]
832    pub fn monitored_branches(&self) -> &[usize] {
833        &self.monitored_branch_indices
834    }
835
836    /// Returns the `(first, second)` outage branch pair.
837    #[inline]
838    pub fn outage_pair(&self) -> (usize, usize) {
839        self.outage_pair
840    }
841
842    /// Returns the N-2 LODF values in monitored-branch order.
843    #[inline]
844    pub fn values(&self) -> &[f64] {
845        &self.values
846    }
847
848    /// Number of monitored branches.
849    #[inline]
850    pub fn len(&self) -> usize {
851        self.values.len()
852    }
853
854    /// Returns `true` if no monitored branches.
855    #[inline]
856    pub fn is_empty(&self) -> bool {
857        self.values.is_empty()
858    }
859
860    /// Iterates over N-2 LODF values in monitored-branch order.
861    #[inline]
862    pub fn iter(&self) -> std::slice::Iter<'_, f64> {
863        self.values.iter()
864    }
865
866    /// Decompose into `(monitored_indices, outage_pair, values)`.
867    #[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/// Batched N-2 LODF result for ordered outage pairs.
882#[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    /// Returns the monitored branch indices (row order).
903    #[inline]
904    pub fn monitored_branches(&self) -> &[usize] {
905        &self.monitored_branch_indices
906    }
907
908    /// Returns the outage pairs (column order).
909    #[inline]
910    pub fn outage_pairs(&self) -> &[(usize, usize)] {
911        &self.outage_pairs
912    }
913
914    /// Number of monitored branches (rows).
915    #[inline]
916    pub fn n_rows(&self) -> usize {
917        self.values.nrows()
918    }
919
920    /// Number of outage pairs (columns).
921    #[inline]
922    pub fn n_cols(&self) -> usize {
923        self.values.ncols()
924    }
925
926    /// Returns a reference to the underlying dense N-2 LODF matrix.
927    #[inline]
928    pub fn matrix(&self) -> &Mat<f64> {
929        &self.values
930    }
931
932    /// Decompose into `(monitored_indices, outage_pairs, values)`.
933    #[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// ---------------------------------------------------------------------------
952// Internal types
953// ---------------------------------------------------------------------------
954
955#[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}