1use std::path::{Path, PathBuf};
9
10use serde::Serialize;
11use sprs::CsMat;
12
13use crate::Result;
14use crate::indexed::IndexedNetwork;
15use crate::io::mtx::{write_mtx, write_vector_mtx};
16use crate::matrix::incidence::{DcConvention, build_flow_map, build_incidence};
17use crate::matrix::laplacian::{build_weighted_laplacian, ground_at_each, reference_indicator};
18use crate::matrix::opf::{Units, build_opf_instance};
19use crate::matrix::{BuildOptions, ZeroImpedanceRule, ZeroImpedanceSkips};
20use crate::network::Network;
21use crate::{GenCostPatch, MissingGenCostPolicy};
22
23const DCOPF_SCHEMA: &str = "powerio.dcopf";
24const DCOPF_SCHEMA_VERSION: &str = "0.1.0";
25
26#[derive(Debug, Clone)]
27pub struct DcOpfOptions {
28 pub convention: DcConvention,
29 pub units: Units,
30 pub missing_gen_cost: MissingGenCostPolicy,
31 pub gen_cost_patches: Vec<GenCostPatch>,
32}
33
34impl Default for DcOpfOptions {
35 fn default() -> Self {
36 Self {
37 convention: DcConvention::default(),
38 units: Units::default(),
39 missing_gen_cost: MissingGenCostPolicy::Require,
40 gen_cost_patches: Vec::new(),
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
46pub struct DcOpfOutputs {
47 pub dir: PathBuf,
48 pub files: Vec<PathBuf>,
49}
50
51#[derive(Serialize)]
52struct DcOpfMeta {
53 schema: &'static str,
54 schema_version: &'static str,
55 case_name: String,
56 base_mva: f64,
57 dimensions: DcOpfDimensions,
58 index_base: IndexBaseMeta,
59 dc_convention: DcConvention,
60 build_options: BuildOptions,
61 zero_impedance: ZeroImpedanceMeta,
62 grounding: GroundingMeta,
63 operators: Vec<OperatorMeta>,
64 n: usize,
66 m: usize,
68 n_gen: usize,
70 reference_buses: Vec<usize>,
75 convention: DcConvention,
77 units: Units,
78 cost_policy: MissingGenCostPolicy,
79 synthesized_gen_costs: usize,
80 patched_gen_costs: usize,
81 files: Vec<String>,
82 powerio_version: String,
83}
84
85#[derive(Serialize)]
86#[allow(clippy::struct_field_names)]
87struct DcOpfDimensions {
88 n_buses: usize,
89 n_source_branches: usize,
90 n_branch_columns: usize,
91 n_generators: usize,
92 n_reference_buses: usize,
93 n_grounded_buses: usize,
94}
95
96#[derive(Serialize)]
97struct IndexBaseMeta {
98 dense: usize,
99 matrix_market: usize,
100}
101
102#[derive(Serialize)]
103struct ZeroImpedanceMeta {
104 skip: bool,
105 rule: ZeroImpedanceRule,
106 skipped: ZeroImpedanceSkips,
107}
108
109#[derive(Serialize)]
110struct GroundingMeta {
111 reference_buses: Vec<usize>,
112 removed_rows_and_columns: Vec<usize>,
113 grounded_operator: &'static str,
114 reference_selector: &'static str,
115}
116
117#[derive(Serialize)]
118struct OperatorMeta {
119 name: &'static str,
120 file: &'static str,
121 kind: &'static str,
122 rows: usize,
123 cols: usize,
124 index_space: &'static str,
125 units: &'static str,
126}
127
128pub fn write_dcopf_bundle(
130 net: &Network,
131 out_dir: impl AsRef<Path>,
132 opts: &DcOpfOptions,
133) -> Result<DcOpfOutputs> {
134 let mut policy_net = net.clone();
135 let cost_report =
136 policy_net.apply_gen_cost_policy(&opts.gen_cost_patches, opts.missing_gen_cost)?;
137 let view = IndexedNetwork::new(&policy_net);
138
139 let dir = out_dir.as_ref().join(format!("{}_dcopf", view.name()));
140 std::fs::create_dir_all(&dir)?;
141
142 view.check_reference_coverage()?;
143 let refs = view.reference_bus_indices();
144 let build_options = BuildOptions::default();
145 let inc = build_incidence(&view, opts.convention, &build_options)?;
146 let l = build_weighted_laplacian(&inc.a, &inc.b);
147 let l_grounded = ground_at_each(&l, &refs);
148 let flow = build_flow_map(&inc.a, &inc.b);
149 let opf = build_opf_instance(&view, &inc, opts.units)?;
150 let e_r = reference_indicator(view.n(), &refs);
151
152 let mut files = Vec::new();
153
154 put_mat(&dir, "A.mtx", &inc.a, &mut files)?;
156 put_mat(&dir, "L.mtx", &l, &mut files)?;
157 put_mat(&dir, "L_grounded.mtx", &l_grounded, &mut files)?;
158 put_mat(&dir, "BAt.mtx", &flow, &mut files)?;
159 put_mat(&dir, "Cg.mtx", &opf.c_g, &mut files)?;
160
161 put_vec(&dir, "b.mtx", &inc.b, &mut files)?;
163 put_vec(&dir, "p_shift.mtx", &inc.p_shift, &mut files)?;
164 put_vec(&dir, "e_r.mtx", &e_r, &mut files)?;
167 put_vec(&dir, "q.mtx", &opf.bus.q, &mut files)?;
168 put_vec(&dir, "c.mtx", &opf.bus.c, &mut files)?;
169 put_vec(&dir, "pmax.mtx", &opf.bus.pmax, &mut files)?;
170 put_vec(&dir, "pmin.mtx", &opf.bus.pmin, &mut files)?;
171 put_vec(&dir, "fmax.mtx", &opf.f_max, &mut files)?;
172 put_vec(&dir, "pd.mtx", &opf.bus.p_d, &mut files)?;
173
174 put_vec(&dir, "q_gen.mtx", &opf.gen_costs.q, &mut files)?;
176 put_vec(&dir, "c_gen.mtx", &opf.gen_costs.c, &mut files)?;
177 put_vec(&dir, "pmax_gen.mtx", &opf.gen_costs.pmax, &mut files)?;
178 put_vec(&dir, "pmin_gen.mtx", &opf.gen_costs.pmin, &mut files)?;
179
180 let meta = DcOpfMeta {
181 schema: DCOPF_SCHEMA,
182 schema_version: DCOPF_SCHEMA_VERSION,
183 case_name: view.name().to_string(),
184 base_mva: view.base_mva(),
185 dimensions: DcOpfDimensions {
186 n_buses: view.n(),
187 n_source_branches: view.branches().len(),
188 n_branch_columns: inc.m(),
189 n_generators: opf.n_gen(),
190 n_reference_buses: refs.len(),
191 n_grounded_buses: view.n() - refs.len(),
192 },
193 index_base: IndexBaseMeta {
194 dense: 0,
195 matrix_market: 1,
196 },
197 dc_convention: opts.convention,
198 build_options: build_options.clone(),
199 zero_impedance: ZeroImpedanceMeta {
200 skip: build_options.skip_zero_impedance,
201 rule: ZeroImpedanceRule::Reactance,
202 skipped: inc.skipped_zero_impedance.clone(),
203 },
204 grounding: GroundingMeta {
205 reference_buses: refs.clone(),
206 removed_rows_and_columns: refs.clone(),
207 grounded_operator: "L_grounded",
208 reference_selector: "e_r",
209 },
210 operators: operator_meta(view.n(), inc.m(), refs.len(), opf.n_gen()),
211 n: view.n(),
212 m: inc.m(),
213 n_gen: opf.n_gen(),
214 reference_buses: refs.clone(),
215 convention: opts.convention,
216 units: opts.units,
217 cost_policy: opts.missing_gen_cost,
218 synthesized_gen_costs: cost_report.synthesized,
219 patched_gen_costs: cost_report.patched,
220 files: files
221 .iter()
222 .filter_map(|p| p.file_name().and_then(|s| s.to_str()).map(str::to_string))
223 .collect(),
224 powerio_version: env!("CARGO_PKG_VERSION").to_string(),
225 };
226 let meta_path = dir.join("dcopf_meta.json");
227 let json = serde_json::to_string_pretty(&meta).map_err(|e| crate::Error::Mtx(e.to_string()))?;
228 std::fs::write(&meta_path, json)?;
229 files.push(meta_path);
230
231 Ok(DcOpfOutputs { dir, files })
232}
233
234#[allow(clippy::too_many_lines)]
235fn operator_meta(n: usize, m: usize, n_ref: usize, n_gen: usize) -> Vec<OperatorMeta> {
236 let n_grounded = n - n_ref;
237 vec![
238 op(
239 "signed_incidence",
240 "A.mtx",
241 "matrix",
242 n,
243 m,
244 "bus_by_branch",
245 "unitless",
246 ),
247 op(
248 "branch_susceptance",
249 "b.mtx",
250 "vector",
251 m,
252 1,
253 "branch",
254 "per_unit_susceptance",
255 ),
256 op(
257 "weighted_laplacian",
258 "L.mtx",
259 "matrix",
260 n,
261 n,
262 "bus_by_bus",
263 "per_unit_susceptance",
264 ),
265 op(
266 "grounded_laplacian",
267 "L_grounded.mtx",
268 "matrix",
269 n_grounded,
270 n_grounded,
271 "grounded_bus_by_grounded_bus",
272 "per_unit_susceptance",
273 ),
274 op(
275 "flow_map",
276 "BAt.mtx",
277 "matrix",
278 m,
279 n,
280 "branch_by_bus",
281 "per_unit_susceptance",
282 ),
283 op(
284 "generator_to_bus",
285 "Cg.mtx",
286 "matrix",
287 n,
288 n_gen,
289 "bus_by_generator",
290 "unitless",
291 ),
292 op(
293 "phase_shift_injection",
294 "p_shift.mtx",
295 "vector",
296 n,
297 1,
298 "bus",
299 "per_unit_power",
300 ),
301 op(
302 "reference_selector",
303 "e_r.mtx",
304 "vector",
305 n,
306 1,
307 "bus",
308 "indicator",
309 ),
310 op(
311 "bus_cost_quadratic",
312 "q.mtx",
313 "vector",
314 n,
315 1,
316 "bus",
317 "selected_cost_units",
318 ),
319 op(
320 "bus_cost_linear",
321 "c.mtx",
322 "vector",
323 n,
324 1,
325 "bus",
326 "selected_cost_units",
327 ),
328 op(
329 "bus_generation_upper",
330 "pmax.mtx",
331 "vector",
332 n,
333 1,
334 "bus",
335 "selected_power_units",
336 ),
337 op(
338 "bus_generation_lower",
339 "pmin.mtx",
340 "vector",
341 n,
342 1,
343 "bus",
344 "selected_power_units",
345 ),
346 op(
347 "branch_flow_limit",
348 "fmax.mtx",
349 "vector",
350 m,
351 1,
352 "branch",
353 "selected_power_units",
354 ),
355 op(
356 "bus_load",
357 "pd.mtx",
358 "vector",
359 n,
360 1,
361 "bus",
362 "selected_power_units",
363 ),
364 op(
365 "generator_cost_quadratic",
366 "q_gen.mtx",
367 "vector",
368 n_gen,
369 1,
370 "generator",
371 "selected_cost_units",
372 ),
373 op(
374 "generator_cost_linear",
375 "c_gen.mtx",
376 "vector",
377 n_gen,
378 1,
379 "generator",
380 "selected_cost_units",
381 ),
382 op(
383 "generator_upper",
384 "pmax_gen.mtx",
385 "vector",
386 n_gen,
387 1,
388 "generator",
389 "selected_power_units",
390 ),
391 op(
392 "generator_lower",
393 "pmin_gen.mtx",
394 "vector",
395 n_gen,
396 1,
397 "generator",
398 "selected_power_units",
399 ),
400 ]
401}
402
403fn op(
404 name: &'static str,
405 file: &'static str,
406 kind: &'static str,
407 rows: usize,
408 cols: usize,
409 index_space: &'static str,
410 units: &'static str,
411) -> OperatorMeta {
412 OperatorMeta {
413 name,
414 file,
415 kind,
416 rows,
417 cols,
418 index_space,
419 units,
420 }
421}
422
423fn put_mat(dir: &Path, name: &str, m: &CsMat<f64>, files: &mut Vec<PathBuf>) -> Result<()> {
424 let p = dir.join(name);
425 write_mtx(m, &p)?;
426 files.push(p);
427 Ok(())
428}
429
430fn put_vec(dir: &Path, name: &str, v: &[f64], files: &mut Vec<PathBuf>) -> Result<()> {
431 let p = dir.join(name);
432 write_vector_mtx(v, &p)?;
433 files.push(p);
434 Ok(())
435}