1mod adjacency;
11mod bdoubleprime;
12mod bprime;
13pub mod incidence;
14mod lacpf;
15pub mod laplacian;
16pub mod sensitivity;
17pub mod triplet;
18mod ybus;
19
20#[cfg(test)]
21mod tests;
22
23pub use adjacency::build_adjacency;
24pub use bdoubleprime::build_bdoubleprime;
25pub use bprime::build_bprime;
26pub use incidence::{
27 DcConvention, IncidenceParts, build_flow_map, build_incidence, susceptance_diag,
28};
29pub use lacpf::build_lacpf;
30pub use laplacian::{
31 GroundedIndexMap, build_weighted_laplacian, ground_at, ground_at_each, reference_indicator,
32 unit_vector,
33};
34pub use sensitivity::{
35 SensitivityMatrices, SensitivityMatrixMetadata, SensitivityMetadata, SensitivityOptions,
36 SensitivitySolver, SensitivitySolverPath, build_lodf, build_ptdf, build_ptdf_lodf,
37 build_ptdf_lodf_with_options,
38};
39pub use ybus::{YbusParts, build_ybus};
40#[cfg(feature = "gridfm")]
43pub(crate) use ybus::{YbusFlags, branch_admittance, branch_flows};
44
45use sprs::CsMat;
46
47pub(crate) use powerio::dc::MIN_DIVISIBLE_MAGNITUDE;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
56#[non_exhaustive]
57pub enum Scheme {
58 #[default]
59 Bx,
60 Xb,
61}
62
63#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
64pub struct BuildOptions {
65 pub scheme: Scheme,
66 pub include_taps: bool,
69 pub include_shifts: bool,
72 pub skip_zero_impedance: bool,
74}
75
76impl Default for BuildOptions {
77 fn default() -> Self {
78 Self {
79 scheme: Scheme::Bx,
80 include_taps: true,
81 include_shifts: true,
82 skip_zero_impedance: true,
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
90#[non_exhaustive]
91pub enum ZeroImpedanceRule {
92 Series,
95 Reactance,
98}
99
100#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
103#[non_exhaustive]
104pub struct ZeroImpedanceSkips {
105 pub count: usize,
106 pub branch_indices: Vec<usize>,
107}
108
109impl ZeroImpedanceSkips {
110 pub fn new(branch_indices: Vec<usize>) -> Self {
111 Self {
112 count: branch_indices.len(),
113 branch_indices,
114 }
115 }
116
117 pub fn is_empty(&self) -> bool {
118 self.count == 0
119 }
120}
121
122pub fn skipped_zero_impedance(
125 case: &crate::indexed::IndexedNetwork,
126 rule: ZeroImpedanceRule,
127) -> ZeroImpedanceSkips {
128 let branch_indices = case
129 .in_service_branches()
130 .filter_map(|(row, br)| {
131 let zero = match rule {
132 ZeroImpedanceRule::Series => br.r * br.r + br.x * br.x == 0.0,
133 ZeroImpedanceRule::Reactance => br.x == 0.0,
134 };
135 zero.then_some(row)
136 })
137 .collect();
138 ZeroImpedanceSkips::new(branch_indices)
139}
140
141#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
143#[non_exhaustive]
144pub struct MatrixStats {
145 pub n: usize,
146 pub nnz: usize,
147 pub min_diag: f64,
148 pub max_diag: f64,
149 pub min_dd_margin: f64,
152 pub m_matrix_sign: bool,
154 pub frobenius_norm: f64,
155 #[serde(default)]
157 pub skipped_zero_impedance: usize,
158 #[serde(default)]
160 pub skipped_zero_impedance_branches: Vec<usize>,
161}
162
163impl MatrixStats {
164 pub fn from_csr(a: &CsMat<f64>) -> Self {
165 let n = a.rows();
166 let mut min_diag = f64::INFINITY;
167 let mut max_diag = f64::NEG_INFINITY;
168 let mut min_dd = f64::INFINITY;
169 let mut m_sign = true;
170 let mut fro_sq = 0.0_f64;
171
172 for (row_idx, row) in a.outer_iterator().enumerate() {
173 let mut diag = 0.0_f64;
174 let mut off_abs = 0.0_f64;
175 for (col, &v) in row.iter() {
176 fro_sq += v * v;
177 if col == row_idx {
178 diag = v;
179 } else {
180 off_abs += v.abs();
181 if v > 0.0 {
182 m_sign = false;
183 }
184 }
185 }
186 min_diag = min_diag.min(diag);
187 max_diag = max_diag.max(diag);
188 min_dd = min_dd.min(diag - off_abs);
189 }
190
191 Self {
192 n,
193 nnz: a.nnz(),
194 min_diag,
195 max_diag,
196 min_dd_margin: min_dd,
197 m_matrix_sign: m_sign,
198 frobenius_norm: fro_sq.sqrt(),
199 skipped_zero_impedance: 0,
200 skipped_zero_impedance_branches: Vec::new(),
201 }
202 }
203
204 #[must_use]
205 pub fn with_zero_impedance_skips(mut self, skips: ZeroImpedanceSkips) -> Self {
206 self.skipped_zero_impedance = skips.count;
207 self.skipped_zero_impedance_branches = skips.branch_indices;
208 self
209 }
210}
211
212pub(crate) fn negate_into(mut a: CsMat<f64>) -> CsMat<f64> {
216 a.data_mut().iter_mut().for_each(|v| *v = -*v);
217 a
218}
219
220pub fn sddm_check(a: &CsMat<f64>) -> bool {
223 if !is_symmetric(a) {
224 return false;
225 }
226 let stats = MatrixStats::from_csr(a);
227 stats.m_matrix_sign && stats.min_dd_margin >= -1e-12 && stats.min_diag > 0.0
228}
229
230fn is_symmetric(a: &CsMat<f64>) -> bool {
231 if a.rows() != a.cols() {
232 return false;
233 }
234 for (&v, (i, j)) in a {
235 let other = a.get(j, i).copied().unwrap_or(0.0);
236 let scale = v.abs().max(other.abs()).max(1.0);
237 if (v - other).abs() > 1e-12 * scale {
238 return false;
239 }
240 }
241 true
242}