1use std::path::{Path, PathBuf};
2
3use powerio::{GenCostPolicyReport, MissingGenCostPolicy};
4
5use crate::Result;
6use powerio_matrix::SparseMatrix;
7use powerio_matrix::io::{write_mtx, write_vector_mtx};
8use serde::Serialize;
9
10use crate::{DcOpfInstance, Units};
11
12use super::build_dc_opf_matrices;
13
14const DCOPF_SCHEMA: &str = "powerio.dcopf";
15
16#[derive(Debug, Clone)]
18pub struct DcOpfBundleMetadata {
19 pub cost_policy: MissingGenCostPolicy,
20 pub cost_report: GenCostPolicyReport,
21}
22
23impl Default for DcOpfBundleMetadata {
24 fn default() -> Self {
25 Self {
26 cost_policy: MissingGenCostPolicy::Require,
27 cost_report: GenCostPolicyReport::default(),
28 }
29 }
30}
31
32#[derive(Debug, Clone, Default)]
34pub struct DcOpfBundleOptions {
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 dc_convention: powerio::DcConvention,
53 build_options: BuildOptionsMeta,
54 zero_impedance: ZeroImpedanceMeta<'a>,
55 grounding: GroundingMeta<'a>,
56 operators: Vec<OperatorMeta>,
57 n: usize,
58 m: usize,
59 n_gen: usize,
60 reference_buses: &'a [usize],
61 convention: powerio::DcConvention,
62 units: Units,
63 cost_policy: MissingGenCostPolicy,
64 synthesized_gen_costs: usize,
65 patched_gen_costs: usize,
66 files: Vec<String>,
67 powerio_version: &'static str,
68}
69
70#[derive(Serialize)]
71#[allow(clippy::struct_field_names)]
72struct DcOpfDimensions {
73 n_buses: usize,
74 n_source_branches: usize,
75 n_branch_columns: usize,
76 n_generators: usize,
77 n_reference_buses: usize,
78 n_grounded_buses: usize,
79}
80
81#[derive(Serialize)]
82struct IndexBaseMeta {
83 dense: usize,
84 matrix_market: usize,
85}
86
87#[derive(Serialize)]
88struct BuildOptionsMeta {
89 skip_zero_impedance: bool,
90}
91
92#[derive(Serialize)]
93struct ZeroImpedanceMeta<'a> {
94 skip: bool,
95 rule: &'static str,
96 skipped: ZeroImpedanceSkips<'a>,
97}
98
99#[derive(Serialize)]
100struct ZeroImpedanceSkips<'a> {
101 count: usize,
102 branch_indices: &'a [usize],
103}
104
105#[derive(Serialize)]
106struct GroundingMeta<'a> {
107 reference_buses: &'a [usize],
108 removed_rows_and_columns: &'a [usize],
109 grounded_operator: &'static str,
110 reference_selector: &'static str,
111}
112
113#[derive(Serialize)]
114struct OperatorMeta {
115 name: &'static str,
116 file: &'static str,
117 kind: &'static str,
118 rows: usize,
119 cols: usize,
120 index_space: &'static str,
121 units: &'static str,
122}
123
124#[allow(clippy::too_many_lines)]
129pub fn write_dcopf_bundle(
130 instance: &DcOpfInstance,
131 out_dir: impl AsRef<Path>,
132 options: &DcOpfBundleOptions,
133) -> Result<DcOpfOutputs> {
134 let matrices = build_dc_opf_matrices(instance);
135 let nodal = instance.nodal_generator_data();
136 let dir = out_dir.as_ref().join(format!(
141 "{}_dcopf",
142 powerio_matrix::sanitize_stem(&instance.name)
143 ));
144 std::fs::create_dir_all(&dir)?;
145
146 let mut files = Vec::new();
147 put_mat(&dir, "A.mtx", &matrices.incidence, &mut files)?;
148 put_mat(&dir, "L.mtx", &matrices.laplacian, &mut files)?;
149 put_mat(
150 &dir,
151 "L_grounded.mtx",
152 &matrices.grounded_laplacian,
153 &mut files,
154 )?;
155 put_mat(&dir, "BAt.mtx", &matrices.flow_map, &mut files)?;
156 put_mat(&dir, "Cg.mtx", &matrices.generator_bus, &mut files)?;
157
158 put_vec(&dir, "b.mtx", &instance.branches.b, &mut files)?;
159 put_vec(&dir, "p_shift.mtx", &instance.p_shift, &mut files)?;
160 put_vec(&dir, "e_r.mtx", &matrices.reference_selector, &mut files)?;
161 put_vec(&dir, "q.mtx", &nodal.q, &mut files)?;
162 put_vec(&dir, "c.mtx", &nodal.c, &mut files)?;
163 put_vec(&dir, "c0.mtx", &nodal.c0, &mut files)?;
164 put_vec(&dir, "pmax.mtx", &nodal.pmax, &mut files)?;
165 put_vec(&dir, "pmin.mtx", &nodal.pmin, &mut files)?;
166 put_vec(&dir, "fmax.mtx", &instance.branches.f_max, &mut files)?;
167 put_vec(&dir, "pd.mtx", &instance.p_d, &mut files)?;
168 put_vec(&dir, "gs.mtx", &instance.g_s, &mut files)?;
169 put_vec(
170 &dir,
171 "angle_min.mtx",
172 &instance.branches.angle_min,
173 &mut files,
174 )?;
175 put_vec(
176 &dir,
177 "angle_max.mtx",
178 &instance.branches.angle_max,
179 &mut files,
180 )?;
181
182 put_vec(&dir, "q_gen.mtx", &instance.generators.q, &mut files)?;
183 put_vec(&dir, "c_gen.mtx", &instance.generators.c, &mut files)?;
184 put_vec(&dir, "c0_gen.mtx", &instance.generators.c0, &mut files)?;
185 put_vec(&dir, "pmax_gen.mtx", &instance.generators.pmax, &mut files)?;
186 put_vec(&dir, "pmin_gen.mtx", &instance.generators.pmin, &mut files)?;
187
188 let power_units = match instance.units {
189 Units::PerUnit => "per_unit_power",
190 Units::Native => "native_power",
191 };
192 let meta = DcOpfMeta {
193 schema: DCOPF_SCHEMA,
194 case_name: &instance.name,
195 base_mva: instance.base_mva,
196 dimensions: DcOpfDimensions {
197 n_buses: instance.n_buses,
198 n_source_branches: instance.n_source_branches,
199 n_branch_columns: instance.n_branches(),
200 n_generators: instance.n_generators(),
201 n_reference_buses: instance.reference_buses.len(),
202 n_grounded_buses: instance.n_buses - instance.reference_buses.len(),
203 },
204 index_base: IndexBaseMeta {
205 dense: 0,
206 matrix_market: 1,
207 },
208 dc_convention: instance.convention,
209 build_options: BuildOptionsMeta {
210 skip_zero_impedance: instance.skip_zero_impedance,
211 },
212 zero_impedance: ZeroImpedanceMeta {
213 skip: instance.skip_zero_impedance,
214 rule: "Reactance",
215 skipped: ZeroImpedanceSkips {
216 count: instance.branches.skipped_zero_impedance.len(),
217 branch_indices: &instance.branches.skipped_zero_impedance,
218 },
219 },
220 grounding: GroundingMeta {
221 reference_buses: instance.reference_buses.as_ref(),
222 removed_rows_and_columns: instance.reference_buses.as_ref(),
223 grounded_operator: "L_grounded",
224 reference_selector: "e_r",
225 },
226 operators: operator_meta(
227 instance.n_buses,
228 instance.n_branches(),
229 instance.reference_buses.len(),
230 instance.n_generators(),
231 power_units,
232 ),
233 n: instance.n_buses,
234 m: instance.n_branches(),
235 n_gen: instance.n_generators(),
236 reference_buses: instance.reference_buses.as_ref(),
237 convention: instance.convention,
238 units: instance.units,
239 cost_policy: options.metadata.cost_policy,
240 synthesized_gen_costs: options.metadata.cost_report.synthesized,
241 patched_gen_costs: options.metadata.cost_report.patched,
242 files: files
243 .iter()
244 .filter_map(|path| path.file_name()?.to_str().map(str::to_owned))
245 .collect(),
246 powerio_version: powerio::VERSION,
247 };
248 let meta_path = dir.join("dcopf_meta.json");
249 let json = serde_json::to_string_pretty(&meta)
250 .map_err(|error| powerio_matrix::Error::Mtx(error.to_string()))?;
251 std::fs::write(&meta_path, json)?;
252 files.push(meta_path);
253
254 Ok(DcOpfOutputs { dir, files })
255}
256
257#[allow(clippy::too_many_lines)]
258fn operator_meta(
259 n: usize,
260 m: usize,
261 n_ref: usize,
262 n_gen: usize,
263 power_units: &'static str,
264) -> Vec<OperatorMeta> {
265 let n_grounded = n - n_ref;
266 vec![
267 op(
268 "signed_incidence",
269 "A.mtx",
270 "matrix",
271 n,
272 m,
273 "bus_by_branch",
274 "unitless",
275 ),
276 op(
277 "branch_susceptance",
278 "b.mtx",
279 "vector",
280 m,
281 1,
282 "branch",
283 power_units,
284 ),
285 op(
286 "weighted_laplacian",
287 "L.mtx",
288 "matrix",
289 n,
290 n,
291 "bus_by_bus",
292 power_units,
293 ),
294 op(
295 "grounded_laplacian",
296 "L_grounded.mtx",
297 "matrix",
298 n_grounded,
299 n_grounded,
300 "grounded_bus_by_grounded_bus",
301 power_units,
302 ),
303 op(
304 "flow_map",
305 "BAt.mtx",
306 "matrix",
307 m,
308 n,
309 "branch_by_bus",
310 power_units,
311 ),
312 op(
313 "generator_to_bus",
314 "Cg.mtx",
315 "matrix",
316 n,
317 n_gen,
318 "bus_by_generator",
319 "unitless",
320 ),
321 op(
322 "phase_shift_injection",
323 "p_shift.mtx",
324 "vector",
325 n,
326 1,
327 "bus",
328 power_units,
329 ),
330 op(
331 "reference_selector",
332 "e_r.mtx",
333 "vector",
334 n,
335 1,
336 "bus",
337 "indicator",
338 ),
339 op(
340 "bus_cost_quadratic",
341 "q.mtx",
342 "vector",
343 n,
344 1,
345 "bus",
346 "selected_cost_units",
347 ),
348 op(
349 "bus_cost_linear",
350 "c.mtx",
351 "vector",
352 n,
353 1,
354 "bus",
355 "selected_cost_units",
356 ),
357 op(
358 "bus_generation_upper",
359 "pmax.mtx",
360 "vector",
361 n,
362 1,
363 "bus",
364 power_units,
365 ),
366 op(
367 "bus_generation_lower",
368 "pmin.mtx",
369 "vector",
370 n,
371 1,
372 "bus",
373 power_units,
374 ),
375 op(
376 "branch_flow_limit",
377 "fmax.mtx",
378 "vector",
379 m,
380 1,
381 "branch",
382 power_units,
383 ),
384 op("bus_load", "pd.mtx", "vector", n, 1, "bus", power_units),
385 op(
386 "branch_angle_minimum",
387 "angle_min.mtx",
388 "vector",
389 m,
390 1,
391 "branch",
392 "radian",
393 ),
394 op(
395 "branch_angle_maximum",
396 "angle_max.mtx",
397 "vector",
398 m,
399 1,
400 "branch",
401 "radian",
402 ),
403 op(
404 "generator_cost_quadratic",
405 "q_gen.mtx",
406 "vector",
407 n_gen,
408 1,
409 "generator",
410 "selected_cost_units",
411 ),
412 op(
413 "generator_cost_linear",
414 "c_gen.mtx",
415 "vector",
416 n_gen,
417 1,
418 "generator",
419 "selected_cost_units",
420 ),
421 op(
422 "generator_upper",
423 "pmax_gen.mtx",
424 "vector",
425 n_gen,
426 1,
427 "generator",
428 power_units,
429 ),
430 op(
431 "generator_lower",
432 "pmin_gen.mtx",
433 "vector",
434 n_gen,
435 1,
436 "generator",
437 power_units,
438 ),
439 ]
440}
441
442fn op(
443 name: &'static str,
444 file: &'static str,
445 kind: &'static str,
446 rows: usize,
447 cols: usize,
448 index_space: &'static str,
449 units: &'static str,
450) -> OperatorMeta {
451 OperatorMeta {
452 name,
453 file,
454 kind,
455 rows,
456 cols,
457 index_space,
458 units,
459 }
460}
461
462fn put_mat(dir: &Path, name: &str, matrix: &SparseMatrix, files: &mut Vec<PathBuf>) -> Result<()> {
463 let path = dir.join(name);
464 write_mtx(matrix, &path)?;
465 files.push(path);
466 Ok(())
467}
468
469fn put_vec(dir: &Path, name: &str, values: &[f64], files: &mut Vec<PathBuf>) -> Result<()> {
470 let path = dir.join(name);
471 write_vector_mtx(values, &path)?;
472 files.push(path);
473 Ok(())
474}