1use serde::{Deserialize, Serialize};
2
3use powerio::{BusId, DcConvention, IndexedNetwork};
4
5use crate::{Error, Result};
6
7use crate::{ReferenceBuses, limits, nodal};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
11#[non_exhaustive]
12pub enum Units {
13 #[default]
15 PerUnit,
16 Native,
18}
19
20impl std::str::FromStr for Units {
21 type Err = String;
22
23 fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
26 match name.to_ascii_lowercase().replace(['-', '_'], "").as_str() {
27 "perunit" | "pu" => Ok(Units::PerUnit),
28 "native" => Ok(Units::Native),
29 other => Err(format!(
30 "unknown units `{other}`; expected \"per-unit\" or \"native\""
31 )),
32 }
33 }
34}
35
36impl Units {
37 pub(crate) fn power_scales(self, base: f64) -> (f64, f64) {
41 match self {
42 Self::PerUnit => (1.0 / base, 1.0),
43 Self::Native => (1.0, base),
44 }
45 }
46
47 pub(crate) fn cost_scales(self, base: f64) -> (f64, f64) {
50 match self {
51 Self::PerUnit => (base * base, base),
52 Self::Native => (1.0, 1.0),
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub struct DcOpfOptions {
60 pub convention: DcConvention,
61 pub units: Units,
62 pub skip_zero_impedance: bool,
65 pub synthesize_unrated_limits: bool,
70}
71
72impl Default for DcOpfOptions {
73 fn default() -> Self {
74 Self {
75 convention: DcConvention::default(),
76 units: Units::default(),
77 skip_zero_impedance: true,
78 synthesize_unrated_limits: false,
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[non_exhaustive]
86pub struct DcGeneratorData {
87 pub bus_of_gen: Vec<usize>,
89 pub source_rows: Vec<usize>,
91 pub q: Vec<f64>,
93 pub c: Vec<f64>,
95 pub c0: Vec<f64>,
99 pub pmax: Vec<f64>,
100 pub pmin: Vec<f64>,
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105#[non_exhaustive]
106pub struct DcBranchData {
107 pub from_bus: Vec<usize>,
108 pub to_bus: Vec<usize>,
109 pub b: Vec<f64>,
112 pub shift: Vec<f64>,
115 pub f_max: Vec<f64>,
117 pub angle_min: Vec<f64>,
119 pub angle_max: Vec<f64>,
120 pub source_rows: Vec<usize>,
122 pub skipped_zero_impedance: Vec<usize>,
124}
125
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129#[non_exhaustive]
130pub struct NodalGeneratorData {
131 pub q: Vec<f64>,
132 pub c: Vec<f64>,
133 pub c0: Vec<f64>,
134 pub pmax: Vec<f64>,
135 pub pmin: Vec<f64>,
136 pub has_gen: Vec<bool>,
139}
140
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147#[non_exhaustive]
148pub struct DcOpfInstance {
149 pub name: String,
150 pub n_buses: usize,
151 pub n_source_generators: usize,
152 pub n_source_branches: usize,
153 pub base_mva: f64,
154 pub units: Units,
155 pub convention: DcConvention,
156 pub skip_zero_impedance: bool,
157 pub bus_ids: Vec<BusId>,
159 pub reference_buses: ReferenceBuses,
160 pub p_d: Vec<f64>,
162 pub g_s: Vec<f64>,
170 pub p_shift: Vec<f64>,
172 pub generators: DcGeneratorData,
173 pub branches: DcBranchData,
174}
175
176impl DcOpfInstance {
177 #[must_use]
178 pub fn n_generators(&self) -> usize {
179 self.generators.q.len()
180 }
181
182 #[must_use]
183 pub fn n_branches(&self) -> usize {
184 self.branches.b.len()
185 }
186
187 #[must_use]
197 pub fn nodal_generator_data(&self) -> NodalGeneratorData {
198 let n = self.n_buses;
199 let generators = &self.generators;
200 let bus_of_gen = &generators.bus_of_gen;
201 let costs =
202 nodal::combine_costs(n, bus_of_gen, &generators.q, &generators.c, &generators.c0);
203 NodalGeneratorData {
204 q: costs.q,
205 c: costs.c,
206 c0: costs.c0,
207 pmax: nodal::sum_by_bus(n, bus_of_gen, &generators.pmax),
208 pmin: nodal::sum_by_bus(n, bus_of_gen, &generators.pmin),
209 has_gen: nodal::buses_with_generators(n, bus_of_gen),
210 }
211 }
212}
213
214#[allow(clippy::too_many_lines)]
216pub fn build_dc_opf_instance(
217 case: &IndexedNetwork,
218 options: &DcOpfOptions,
219) -> Result<DcOpfInstance> {
220 case.check_reference_coverage()?;
221 case.network().check_base_mva()?;
222
223 let n_buses = case.n();
224 let base = case.per_unit_base();
225 let (p_scale, b_scale) = options.units.power_scales(base);
226 let thermal = limits::ThermalLimits {
227 synthesize_unrated: options.synthesize_unrated_limits,
228 power_scale: p_scale,
229 admittance_scale: b_scale,
230 };
231 let (q_scale, c_scale) = options.units.cost_scales(base);
232
233 let mut bus_of_gen = Vec::new();
234 let mut generator_rows = Vec::new();
235 let mut q = Vec::new();
236 let mut c = Vec::new();
237 let mut c0 = Vec::new();
238 let mut pmax = Vec::new();
239 let mut pmin = Vec::new();
240
241 for (source_row, generator) in case.in_service_gens() {
242 let bus = case
243 .bus_index(generator.bus)
244 .ok_or(powerio::Error::UnknownBus {
245 bus_id: generator.bus,
246 element_index: source_row,
247 })?;
248 let cost = generator
249 .cost
250 .as_ref()
251 .ok_or(powerio::Error::MissingGenCost {
252 gen_index: source_row,
253 })?;
254 let (q_raw, c_raw, c0_raw) = nodal::quadratic_terms(cost, source_row)?;
255 bus_of_gen.push(bus);
256 generator_rows.push(source_row);
257 q.push(q_raw * q_scale);
258 c.push(c_raw * c_scale);
259 c0.push(c0_raw);
260 pmax.push(generator.pmax * p_scale);
261 pmin.push(generator.pmin * p_scale);
262 }
263 if q.is_empty() {
264 return Err(Error::NoGenerators);
265 }
266
267 let mut from_bus = Vec::new();
268 let mut to_bus = Vec::new();
269 let mut b = Vec::new();
270 let mut shift = Vec::new();
271 let mut f_max = Vec::new();
272 let mut angle_min = Vec::new();
273 let mut angle_max = Vec::new();
274 let mut branch_rows = Vec::new();
275 let mut skipped_zero_impedance = Vec::new();
276 let mut p_shift = vec![0.0; n_buses];
277 let buses = &case.network().buses;
279
280 for (source_row, branch) in case.in_service_branches() {
281 let from = case
282 .bus_index(branch.from)
283 .ok_or(powerio::Error::UnknownBus {
284 bus_id: branch.from,
285 element_index: source_row,
286 })?;
287 let to = case
288 .bus_index(branch.to)
289 .ok_or(powerio::Error::UnknownBus {
290 bus_id: branch.to,
291 element_index: source_row,
292 })?;
293 if from == to {
294 continue;
297 }
298 if branch.x.abs() < powerio::dc::MIN_DIVISIBLE_MAGNITUDE {
302 if options.skip_zero_impedance {
303 skipped_zero_impedance.push(source_row);
304 continue;
305 }
306 return Err(powerio::Error::ZeroImpedance { row: source_row }.into());
307 }
308 let branch_b = options.convention.branch_susceptance(
309 branch.r,
310 branch.x,
311 branch.divisible_tap(source_row)?,
312 ) * b_scale;
313 if !branch_b.is_finite() {
314 return Err(powerio::Error::NonFiniteSusceptance { row: source_row }.into());
315 }
316 let shift_rad = if options.convention.includes_phase_shifts() {
317 case.angle_radians(branch.shift)
318 } else {
319 0.0
320 };
321 if shift_rad != 0.0 {
322 p_shift[from] -= branch_b * shift_rad;
323 p_shift[to] += branch_b * shift_rad;
324 }
325 let amin = case.angle_radians(branch.angmin);
326 let amax = case.angle_radians(branch.angmax);
327 from_bus.push(from);
328 to_bus.push(to);
329 b.push(branch_b);
330 shift.push(shift_rad);
331 f_max.push(thermal.of(branch, amin, amax, buses[from].vmax, buses[to].vmax));
332 angle_min.push(amin);
333 angle_max.push(amax);
334 branch_rows.push(source_row);
335 }
336
337 Ok(DcOpfInstance {
338 name: case.name().to_owned(),
339 n_buses,
340 n_source_generators: case.generators().len(),
341 n_source_branches: case.branches().len(),
342 base_mva: case.base_mva(),
343 units: options.units,
344 convention: options.convention,
345 skip_zero_impedance: options.skip_zero_impedance,
346 bus_ids: (0..n_buses).map(|index| case.bus_id(index)).collect(),
347 reference_buses: ReferenceBuses::new(case.reference_bus_indices()),
348 p_d: case.pd().iter().map(|value| value * p_scale).collect(),
349 g_s: case.gs().iter().map(|value| value * p_scale).collect(),
350 p_shift,
351 generators: DcGeneratorData {
352 bus_of_gen,
353 source_rows: generator_rows,
354 q,
355 c,
356 c0,
357 pmax,
358 pmin,
359 },
360 branches: DcBranchData {
361 from_bus,
362 to_bus,
363 b,
364 shift,
365 f_max,
366 angle_min,
367 angle_max,
368 source_rows: branch_rows,
369 skipped_zero_impedance,
370 },
371 })
372}