1use std::path::{Path, PathBuf};
9
10use rand::SeedableRng;
11use rand::distr::{Distribution, StandardUniform};
12use rand_chacha::ChaCha8Rng;
13use serde::{Deserialize, Serialize};
14
15use crate::Result;
16use crate::indexed::IndexedNetwork;
17use crate::io::meta::{CaseMetadata, MatrixMetadata, write_meta_json};
18use crate::io::mtx::{write_mtx, write_vector_mtx};
19use crate::matrix::{
20 BuildOptions, MatrixStats, ZeroImpedanceRule, ZeroImpedanceSkips, build_adjacency,
21 build_bdoubleprime, build_bprime, build_lacpf, build_ybus, negate_into, sddm_check,
22 skipped_zero_impedance,
23};
24use crate::network::Network;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[non_exhaustive]
28pub enum MatrixKind {
29 BPrime,
31 BDoublePrime,
33 YbusG,
35 YbusB,
37 Lacpf,
39 Adjacency,
41}
42
43impl MatrixKind {
44 pub const ALL: &'static [MatrixKind] = &[
45 Self::BPrime,
46 Self::BDoublePrime,
47 Self::YbusG,
48 Self::YbusB,
49 Self::Lacpf,
50 Self::Adjacency,
51 ];
52
53 pub fn slug(self) -> &'static str {
54 match self {
55 Self::BPrime => "bprime",
56 Self::BDoublePrime => "bdoubleprime",
57 Self::YbusG => "ybus_real",
58 Self::YbusB => "ybus_imag",
59 Self::Lacpf => "lacpf",
60 Self::Adjacency => "adjacency",
61 }
62 }
63
64 pub fn label(self) -> &'static str {
65 match self {
66 Self::BPrime => "B' (FDPF, shuntless)",
67 Self::BDoublePrime => "B'' (FDPF, with shunts)",
68 Self::YbusG => "Re(Y_bus)",
69 Self::YbusB => "-Im(Y_bus)",
70 Self::Lacpf => "LACPF block (2n×2n)",
71 Self::Adjacency => "adjacency (0/1)",
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
78pub enum RhsKind {
79 #[default]
80 None,
81 Random,
83 Injection,
85}
86
87#[derive(Debug, Clone)]
88pub struct Pipeline {
89 pub matrices: Vec<MatrixKind>,
90 pub options: BuildOptions,
91 pub rhs: RhsKind,
92 pub rng_seed: u64,
93 pub source_file: Option<PathBuf>,
94}
95
96impl Default for Pipeline {
97 fn default() -> Self {
98 Self {
99 matrices: vec![MatrixKind::BPrime],
100 options: BuildOptions::default(),
101 rhs: RhsKind::None,
102 rng_seed: 0x00C0_FFEE,
103 source_file: None,
104 }
105 }
106}
107
108#[derive(Debug, Clone)]
109pub struct PipelineOutputs {
110 pub case_name: String,
111 pub files: Vec<PathBuf>,
112 pub metadata: CaseMetadata,
113}
114
115impl Pipeline {
116 pub fn run(&self, net: &Network, out_dir: impl AsRef<Path>) -> Result<PipelineOutputs> {
117 let out_dir = out_dir.as_ref();
118 std::fs::create_dir_all(out_dir)?;
119
120 let view = IndexedNetwork::new(net);
121
122 let mut files = Vec::new();
123 let mut matrices_meta = Vec::new();
124 let mut ybus_cache = None;
125
126 for &kind in &self.matrices {
127 let matrix_path = out_dir.join(format!("{}_{}.mtx", view.name(), kind.slug()));
128 let matrix = self.build_for_run(&view, kind, &mut ybus_cache)?;
129 write_mtx(&matrix, &matrix_path)?;
130 let stats = matrix_stats_for_kind(&matrix, &view, kind, &self.options);
131 let sddm = sddm_check(&matrix);
132 matrices_meta.push(MatrixMetadata {
133 kind: kind.slug().to_string(),
134 file: matrix_path
135 .file_name()
136 .and_then(|s| s.to_str())
137 .unwrap_or("")
138 .to_string(),
139 stats,
140 sddm,
141 });
142 files.push(matrix_path);
143
144 if let Some(rhs) = self.build_rhs(&view, kind) {
146 let rhs_path = out_dir.join(format!("{}_{}_rhs.mtx", view.name(), kind.slug()));
147 write_vector_mtx(&rhs, &rhs_path)?;
148 files.push(rhs_path);
149 }
150 }
151
152 let shunt_path = out_dir.join(format!("{}_shunt.mtx", view.name()));
154 let base = view.per_unit_base();
155 let shunt: Vec<f64> = view.bs().iter().map(|&b| b / base).collect();
156 write_vector_mtx(&shunt, &shunt_path)?;
157 files.push(shunt_path);
158
159 let metadata = CaseMetadata {
160 case_name: view.name().to_string(),
161 source_file: self
162 .source_file
163 .as_ref()
164 .and_then(|p| p.to_str())
165 .map(str::to_string),
166 source_sha256: self
167 .source_file
168 .as_ref()
169 .and_then(|p| std::fs::read(p).ok())
170 .map(|b| sha256_hex(&b)),
171 base_mva: view.base_mva(),
172 n_buses: view.n(),
173 n_branches: view.branches().len(),
174 build_options: self.options.clone(),
175 matrices: matrices_meta,
176 powerio_version: env!("CARGO_PKG_VERSION").to_string(),
177 };
178 let meta_path = out_dir.join(format!("{}_meta.json", view.name()));
179 write_meta_json(&metadata, &meta_path)?;
180 files.push(meta_path);
181
182 Ok(PipelineOutputs {
183 case_name: view.name().to_string(),
184 files,
185 metadata,
186 })
187 }
188
189 fn build_for_run(
190 &self,
191 case: &IndexedNetwork,
192 kind: MatrixKind,
193 ybus_cache: &mut Option<YbusCache>,
194 ) -> Result<sprs::CsMat<f64>> {
195 match kind {
196 MatrixKind::YbusG => take_ybus_g(case, &self.options, ybus_cache),
197 MatrixKind::YbusB => take_ybus_b(case, &self.options, ybus_cache),
198 _ => build_kind(case, kind, &self.options),
199 }
200 }
201
202 fn build_rhs(&self, case: &IndexedNetwork, kind: MatrixKind) -> Option<Vec<f64>> {
203 if matches!(self.rhs, RhsKind::None)
205 || matches!(kind, MatrixKind::Lacpf | MatrixKind::Adjacency)
206 {
207 return None;
208 }
209 let n = case.n();
210 let v = match self.rhs {
211 RhsKind::Random => {
212 let mut rng = ChaCha8Rng::seed_from_u64(self.rng_seed.wrapping_add(kind as u64));
213 let dist = StandardUniform;
214 let mut v: Vec<f64> = (0..n)
215 .map(|_| {
216 let u: f64 = dist.sample(&mut rng);
217 u - 0.5
218 })
219 .collect();
220 let mean = v.iter().sum::<f64>() / n as f64;
221 for x in &mut v {
222 *x -= mean; }
224 v
225 }
226 RhsKind::Injection => {
227 let base = case.per_unit_base();
228 match kind {
229 MatrixKind::BPrime | MatrixKind::YbusG | MatrixKind::YbusB => {
230 case.pd().iter().map(|&p| -p / base).collect()
231 }
232 MatrixKind::BDoublePrime => case.qd().iter().map(|&q| -q / base).collect(),
233 MatrixKind::Lacpf | MatrixKind::Adjacency => unreachable!(),
234 }
235 }
236 RhsKind::None => unreachable!(),
237 };
238 Some(v)
239 }
240}
241
242struct YbusCache {
243 g: Option<sprs::CsMat<f64>>,
244 b: Option<sprs::CsMat<f64>>,
245}
246
247fn fill_ybus_cache(
248 view: &IndexedNetwork,
249 opts: &BuildOptions,
250 ybus_cache: &mut Option<YbusCache>,
251) -> Result<()> {
252 let parts = build_ybus(view, opts)?;
253 *ybus_cache = Some(YbusCache {
254 g: Some(parts.g),
255 b: Some(parts.b),
256 });
257 Ok(())
258}
259
260fn take_ybus_g(
261 view: &IndexedNetwork,
262 opts: &BuildOptions,
263 ybus_cache: &mut Option<YbusCache>,
264) -> Result<sprs::CsMat<f64>> {
265 if ybus_cache.as_ref().is_none_or(|c| c.g.is_none()) {
266 fill_ybus_cache(view, opts, ybus_cache)?;
267 }
268 Ok(ybus_cache
269 .as_mut()
270 .and_then(|c| c.g.take())
271 .expect("Ybus cache was just filled, so the real part must be present"))
272}
273
274fn take_ybus_b(
275 view: &IndexedNetwork,
276 opts: &BuildOptions,
277 ybus_cache: &mut Option<YbusCache>,
278) -> Result<sprs::CsMat<f64>> {
279 if ybus_cache.as_ref().is_none_or(|c| c.b.is_none()) {
280 fill_ybus_cache(view, opts, ybus_cache)?;
281 }
282 let b = ybus_cache
283 .as_mut()
284 .and_then(|c| c.b.take())
285 .expect("Ybus cache was just filled, so the imaginary part must be present");
286 Ok(negate_into(b))
287}
288
289pub fn build_kind(
293 view: &IndexedNetwork,
294 kind: MatrixKind,
295 opts: &BuildOptions,
296) -> Result<sprs::CsMat<f64>> {
297 match kind {
298 MatrixKind::BPrime => build_bprime(view, opts),
299 MatrixKind::BDoublePrime => build_bdoubleprime(view, opts),
300 MatrixKind::YbusG => build_ybus(view, opts).map(|p| p.g),
301 MatrixKind::YbusB => build_ybus(view, opts).map(|p| negate_into(p.b)),
302 MatrixKind::Lacpf => build_lacpf(view, opts),
303 MatrixKind::Adjacency => build_adjacency(view),
304 }
305}
306
307pub fn zero_impedance_rule_for_kind(
308 kind: MatrixKind,
309 opts: &BuildOptions,
310) -> Option<ZeroImpedanceRule> {
311 match kind {
312 MatrixKind::BPrime => Some(match opts.scheme {
313 crate::matrix::Scheme::Bx => ZeroImpedanceRule::Series,
314 crate::matrix::Scheme::Xb => ZeroImpedanceRule::Reactance,
315 }),
316 MatrixKind::BDoublePrime => Some(match opts.scheme {
317 crate::matrix::Scheme::Bx => ZeroImpedanceRule::Reactance,
318 crate::matrix::Scheme::Xb => ZeroImpedanceRule::Series,
319 }),
320 MatrixKind::YbusG | MatrixKind::YbusB | MatrixKind::Lacpf => {
321 Some(ZeroImpedanceRule::Series)
322 }
323 MatrixKind::Adjacency => None,
324 }
325}
326
327pub fn zero_impedance_skips_for_kind(
328 view: &IndexedNetwork,
329 kind: MatrixKind,
330 opts: &BuildOptions,
331) -> ZeroImpedanceSkips {
332 if !opts.skip_zero_impedance {
333 return ZeroImpedanceSkips::default();
334 }
335 zero_impedance_rule_for_kind(kind, opts).map_or_else(ZeroImpedanceSkips::default, |rule| {
336 skipped_zero_impedance(view, rule)
337 })
338}
339
340pub fn matrix_stats_for_kind(
341 matrix: &sprs::CsMat<f64>,
342 view: &IndexedNetwork,
343 kind: MatrixKind,
344 opts: &BuildOptions,
345) -> MatrixStats {
346 MatrixStats::from_csr(matrix)
347 .with_zero_impedance_skips(zero_impedance_skips_for_kind(view, kind, opts))
348}
349
350fn sha256_hex(bytes: &[u8]) -> String {
351 use sha2::{Digest, Sha256};
352 use std::fmt::Write as _;
353 let mut hasher = Sha256::new();
354 hasher.update(bytes);
355 let digest = hasher.finalize();
356 let mut out = String::with_capacity(digest.len() * 2);
357 for byte in digest {
358 let _ = write!(out, "{byte:02x}");
359 }
360 out
361}