1use std::path::{Path, PathBuf};
2
3use powerio_tx::{GenCostPolicyReport, MissingGenCostPolicy};
4
5use crate::Result;
6use crate::SparseMatrix;
7use serde::Serialize;
8
9use super::prep::{DcOpfPreparation, Units};
10use super::{DcOpfAssemblyOptions, matrices_from_preparation};
11
12const DCOPF_SCHEMA: &str = "powerio.dcopf";
13
14#[derive(Debug, Clone)]
16pub struct DcOpfBundleMetadata {
17 pub cost_policy: MissingGenCostPolicy,
18 pub cost_report: GenCostPolicyReport,
19}
20
21impl Default for DcOpfBundleMetadata {
22 fn default() -> Self {
23 Self {
24 cost_policy: MissingGenCostPolicy::Require,
25 cost_report: GenCostPolicyReport::default(),
26 }
27 }
28}
29
30#[derive(Debug, Clone, Default)]
32pub struct DcOpfBundleOptions {
33 pub assembly: DcOpfAssemblyOptions,
35 pub metadata: DcOpfBundleMetadata,
36}
37
38#[derive(Debug, Clone)]
39#[non_exhaustive]
40pub struct DcOpfOutputs {
41 pub dir: PathBuf,
42 pub files: Vec<PathBuf>,
43}
44
45#[derive(Serialize)]
46struct DcOpfMeta<'a> {
47 schema: &'static str,
48 case_name: &'a str,
49 base_mva: f64,
50 dimensions: DcOpfDimensions,
51 index_base: IndexBaseMeta,
52 branch_susceptance_formula: &'static str,
53 build_options: BuildOptionsMeta,
54 zero_impedance: ZeroImpedanceMeta<'a>,
55 grounding: GroundingMeta<'a>,
56 operators: Vec<OperatorMeta>,
57 units: Units,
58 cost_policy: MissingGenCostPolicy,
59 synthesized_gen_costs: usize,
60 patched_gen_costs: usize,
61 files: Vec<String>,
62 powerio_version: &'static str,
63}
64
65#[derive(Serialize)]
66#[allow(clippy::struct_field_names)]
67struct DcOpfDimensions {
68 n_buses: usize,
69 n_source_branches: usize,
70 n_branch_columns: usize,
71 n_generators: usize,
72 n_reference_buses: usize,
73 n_grounded_buses: usize,
74}
75
76#[derive(Serialize)]
77struct IndexBaseMeta {
78 dense: usize,
79 matrix_market: usize,
80}
81
82#[derive(Serialize)]
83struct BuildOptionsMeta {
84 skip_zero_impedance: bool,
85 synthesize_unrated_limits: bool,
86}
87
88#[derive(Serialize)]
89struct ZeroImpedanceMeta<'a> {
90 skip: bool,
91 rule: &'static str,
92 skipped: ZeroImpedanceSkips<'a>,
93}
94
95#[derive(Serialize)]
96struct ZeroImpedanceSkips<'a> {
97 count: usize,
98 branch_indices: &'a [usize],
99}
100
101#[derive(Serialize)]
102struct GroundingMeta<'a> {
103 reference_buses: &'a [usize],
104 removed_rows_and_columns: &'a [usize],
105 grounded_operator: &'static str,
106 reference_selector: &'static str,
107}
108
109#[derive(Serialize)]
110struct OperatorMeta {
111 name: &'static str,
112 file: &'static str,
113 kind: &'static str,
114 rows: usize,
115 cols: usize,
116 index_space: &'static str,
117 units: &'static str,
118}
119
120pub fn emit_dcopf_bundle(
130 instance: &powerio_prob::DcOpfInstance,
131 out_dir: impl AsRef<Path>,
132 options: &DcOpfBundleOptions,
133) -> Result<DcOpfOutputs> {
134 emit_prepared(
135 &super::build_dc_opf_preparation(instance, &options.assembly)?,
136 out_dir,
137 options,
138 )
139}
140
141#[allow(clippy::too_many_lines)]
143fn emit_prepared(
144 instance: &DcOpfPreparation,
145 out_dir: impl AsRef<Path>,
146 options: &DcOpfBundleOptions,
147) -> Result<DcOpfOutputs> {
148 let matrices = matrices_from_preparation(instance);
149 let nodal = instance.calc_nodal_generator_data()?;
150 let fixed_withdrawal = instance.calc_fixed_nodal_withdrawal();
151 let flow_offset = instance.calc_branch_flow_offset();
152 let bundle_root = out_dir
157 .as_ref()
158 .join(format!("{}_dcopf", crate::sanitize_stem(&instance.name)));
159
160 let mut inventory: Vec<(&'static str, Vec<u8>)> = Vec::new();
161 put_mat(&mut inventory, "A.mtx", &matrices.bus_branch_incidence)?;
162 put_mat(&mut inventory, "L.mtx", &matrices.laplacian)?;
163 put_mat(
164 &mut inventory,
165 "L_grounded.mtx",
166 &matrices.grounded_laplacian,
167 )?;
168 put_mat(&mut inventory, "BAt.mtx", &matrices.branch_flow_matrix)?;
169 put_mat(&mut inventory, "Cg.mtx", &matrices.generator_bus)?;
170
171 put_vec(
172 &mut inventory,
173 "b.mtx",
174 &instance.branches.susceptance_magnitude,
175 )?;
176 put_vec(&mut inventory, "shift.mtx", &instance.branches.shift)?;
177 put_vec(&mut inventory, "flow_offset.mtx", &flow_offset)?;
178 put_vec(&mut inventory, "p_shift.mtx", &instance.p_shift)?;
179 put_vec(&mut inventory, "fixed_withdrawal.mtx", &fixed_withdrawal)?;
180 put_vec(&mut inventory, "e_r.mtx", &matrices.reference_selector)?;
181 put_vec(&mut inventory, "q.mtx", &nodal.q)?;
182 put_vec(&mut inventory, "c.mtx", &nodal.c)?;
183 put_vec(&mut inventory, "c0.mtx", &nodal.c0)?;
184 put_vec(&mut inventory, "pmax.mtx", &nodal.pmax)?;
185 put_vec(&mut inventory, "pmin.mtx", &nodal.pmin)?;
186 put_vec(&mut inventory, "fmax.mtx", &instance.branches.f_max)?;
187 put_vec(&mut inventory, "pd.mtx", &instance.p_d)?;
188 put_vec(&mut inventory, "gs.mtx", &instance.g_s)?;
189 put_vec(
190 &mut inventory,
191 "angle_min.mtx",
192 &instance.branches.angle_min,
193 )?;
194 put_vec(
195 &mut inventory,
196 "angle_max.mtx",
197 &instance.branches.angle_max,
198 )?;
199
200 put_vec(&mut inventory, "q_gen.mtx", &instance.generators.q)?;
201 put_vec(&mut inventory, "c_gen.mtx", &instance.generators.c)?;
202 put_vec(&mut inventory, "c0_gen.mtx", &instance.generators.c0)?;
203 put_vec(&mut inventory, "pmax_gen.mtx", &instance.generators.pmax)?;
204 put_vec(&mut inventory, "pmin_gen.mtx", &instance.generators.pmin)?;
205
206 let power_units = match instance.units {
207 Units::PerUnit => "per_unit_power",
208 Units::Native => "native_power",
209 };
210 let meta = DcOpfMeta {
211 schema: DCOPF_SCHEMA,
212 case_name: &instance.name,
213 base_mva: instance.base_mva,
214 dimensions: DcOpfDimensions {
215 n_buses: instance.n_buses,
216 n_source_branches: instance.n_source_branches,
217 n_branch_columns: instance.n_branches(),
218 n_generators: instance.n_generators(),
219 n_reference_buses: instance.reference_buses.len(),
220 n_grounded_buses: instance.n_buses - instance.reference_buses.len(),
221 },
222 index_base: IndexBaseMeta {
223 dense: 0,
224 matrix_market: 1,
225 },
226 branch_susceptance_formula: instance.formula.formula_name(),
227 build_options: BuildOptionsMeta {
228 skip_zero_impedance: instance.skip_zero_impedance,
229 synthesize_unrated_limits: instance.synthesize_unrated_limits,
230 },
231 zero_impedance: ZeroImpedanceMeta {
232 skip: instance.skip_zero_impedance,
233 rule: "Reactance",
234 skipped: ZeroImpedanceSkips {
235 count: instance.branches.skipped_zero_impedance.len(),
236 branch_indices: &instance.branches.skipped_zero_impedance,
237 },
238 },
239 grounding: GroundingMeta {
240 reference_buses: instance.reference_buses.as_ref(),
241 removed_rows_and_columns: instance.reference_buses.as_ref(),
242 grounded_operator: "L_grounded",
243 reference_selector: "e_r",
244 },
245 operators: operator_meta(
246 instance.n_buses,
247 instance.n_branches(),
248 instance.reference_buses.len(),
249 instance.n_generators(),
250 power_units,
251 ),
252 units: instance.units,
253 cost_policy: options.metadata.cost_policy,
254 synthesized_gen_costs: options.metadata.cost_report.synthesized,
255 patched_gen_costs: options.metadata.cost_report.patched,
256 files: inventory
259 .iter()
260 .map(|(name, _)| (*name).to_string())
261 .collect(),
262 powerio_version: powerio_tx::VERSION,
263 };
264 let json = serde_json::to_string_pretty(&meta)
265 .map_err(|error| crate::Error::Mtx(error.to_string()))?;
266 inventory.push(("dcopf_meta.json", json.into_bytes()));
267
268 let artifacts = inventory
271 .into_iter()
272 .map(|(name, bytes)| {
273 Ok(powerio_core::MemoryArtifact::new(
274 powerio_core::ArtifactPath::new(name)?,
275 bytes,
276 ))
277 })
278 .collect::<std::result::Result<Vec<_>, powerio_core::Error>>()
279 .map_err(crate::Error::from)?;
280 let committed = powerio_core::Destination::path(&bundle_root)
281 .__commit_artifacts(
282 true,
283 powerio_core::Fidelity::Canonical,
284 artifacts,
285 Vec::new(),
286 )
287 .map_err(crate::Error::from)?;
288 let powerio_core::EmittedOutput::Path { root, artifacts } = committed.into_output() else {
289 unreachable!("a path destination returns a path output")
290 };
291
292 Ok(DcOpfOutputs {
293 dir: root,
294 files: artifacts,
295 })
296}
297
298#[allow(clippy::too_many_lines)]
299fn operator_meta(
300 n: usize,
301 m: usize,
302 n_ref: usize,
303 n_gen: usize,
304 power_units: &'static str,
305) -> Vec<OperatorMeta> {
306 let n_grounded = n - n_ref;
307 vec![
308 op(
309 "signed_incidence",
310 "A.mtx",
311 "matrix",
312 n,
313 m,
314 "bus_by_branch",
315 "unitless",
316 ),
317 op(
318 "branch_susceptance",
319 "b.mtx",
320 "vector",
321 m,
322 1,
323 "branch",
324 power_units,
325 ),
326 op(
327 "branch_phase_shift",
328 "shift.mtx",
329 "vector",
330 m,
331 1,
332 "branch",
333 "radian",
334 ),
335 op(
336 "branch_flow_offset",
337 "flow_offset.mtx",
338 "vector",
339 m,
340 1,
341 "branch",
342 power_units,
343 ),
344 op(
345 "weighted_laplacian",
346 "L.mtx",
347 "matrix",
348 n,
349 n,
350 "bus_by_bus",
351 power_units,
352 ),
353 op(
354 "grounded_laplacian",
355 "L_grounded.mtx",
356 "matrix",
357 n_grounded,
358 n_grounded,
359 "grounded_bus_by_grounded_bus",
360 power_units,
361 ),
362 op(
363 "branch_flow_matrix",
364 "BAt.mtx",
365 "matrix",
366 m,
367 n,
368 "branch_by_bus",
369 power_units,
370 ),
371 op(
372 "generator_to_bus",
373 "Cg.mtx",
374 "matrix",
375 n,
376 n_gen,
377 "bus_by_generator",
378 "unitless",
379 ),
380 op(
381 "phase_shift_injection",
382 "p_shift.mtx",
383 "vector",
384 n,
385 1,
386 "bus",
387 power_units,
388 ),
389 op(
390 "fixed_nodal_withdrawal",
391 "fixed_withdrawal.mtx",
392 "vector",
393 n,
394 1,
395 "bus",
396 power_units,
397 ),
398 op(
399 "reference_selector",
400 "e_r.mtx",
401 "vector",
402 n,
403 1,
404 "bus",
405 "indicator",
406 ),
407 op(
408 "bus_cost_quadratic",
409 "q.mtx",
410 "vector",
411 n,
412 1,
413 "bus",
414 "selected_cost_units",
415 ),
416 op(
417 "bus_cost_linear",
418 "c.mtx",
419 "vector",
420 n,
421 1,
422 "bus",
423 "selected_cost_units",
424 ),
425 op(
426 "bus_cost_constant",
427 "c0.mtx",
428 "vector",
429 n,
430 1,
431 "bus",
432 "selected_cost_units",
433 ),
434 op(
435 "bus_generation_upper",
436 "pmax.mtx",
437 "vector",
438 n,
439 1,
440 "bus",
441 power_units,
442 ),
443 op(
444 "bus_generation_lower",
445 "pmin.mtx",
446 "vector",
447 n,
448 1,
449 "bus",
450 power_units,
451 ),
452 op(
453 "branch_flow_limit",
454 "fmax.mtx",
455 "vector",
456 m,
457 1,
458 "branch",
459 power_units,
460 ),
461 op("bus_load", "pd.mtx", "vector", n, 1, "bus", power_units),
462 op(
463 "bus_shunt_conductance",
464 "gs.mtx",
465 "vector",
466 n,
467 1,
468 "bus",
469 power_units,
470 ),
471 op(
472 "branch_angle_minimum",
473 "angle_min.mtx",
474 "vector",
475 m,
476 1,
477 "branch",
478 "radian",
479 ),
480 op(
481 "branch_angle_maximum",
482 "angle_max.mtx",
483 "vector",
484 m,
485 1,
486 "branch",
487 "radian",
488 ),
489 op(
490 "generator_cost_quadratic",
491 "q_gen.mtx",
492 "vector",
493 n_gen,
494 1,
495 "generator",
496 "selected_cost_units",
497 ),
498 op(
499 "generator_cost_linear",
500 "c_gen.mtx",
501 "vector",
502 n_gen,
503 1,
504 "generator",
505 "selected_cost_units",
506 ),
507 op(
508 "generator_cost_constant",
509 "c0_gen.mtx",
510 "vector",
511 n_gen,
512 1,
513 "generator",
514 "selected_cost_units",
515 ),
516 op(
517 "generator_upper",
518 "pmax_gen.mtx",
519 "vector",
520 n_gen,
521 1,
522 "generator",
523 power_units,
524 ),
525 op(
526 "generator_lower",
527 "pmin_gen.mtx",
528 "vector",
529 n_gen,
530 1,
531 "generator",
532 power_units,
533 ),
534 ]
535}
536
537fn op(
538 name: &'static str,
539 file: &'static str,
540 kind: &'static str,
541 rows: usize,
542 cols: usize,
543 index_space: &'static str,
544 units: &'static str,
545) -> OperatorMeta {
546 OperatorMeta {
547 name,
548 file,
549 kind,
550 rows,
551 cols,
552 index_space,
553 units,
554 }
555}
556
557fn put_mat(
558 inventory: &mut Vec<(&'static str, Vec<u8>)>,
559 name: &'static str,
560 matrix: &SparseMatrix,
561) -> Result<()> {
562 inventory.push((name, crate::io::to_mtx_bytes(matrix)?));
563 Ok(())
564}
565
566fn put_vec(
567 inventory: &mut Vec<(&'static str, Vec<u8>)>,
568 name: &'static str,
569 values: &[f64],
570) -> Result<()> {
571 inventory.push((name, crate::io::to_vector_mtx_bytes(values)?));
572 Ok(())
573}