1use serde::{Deserialize, Serialize};
2
3use powerio::{BusId, DcConvention, Error, IndexedNetwork, Result};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
7#[non_exhaustive]
8pub enum Units {
9 #[default]
11 PerUnit,
12 Native,
14}
15
16impl std::str::FromStr for Units {
17 type Err = String;
18
19 fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
22 match name.to_ascii_lowercase().replace(['-', '_'], "").as_str() {
23 "perunit" | "pu" => Ok(Units::PerUnit),
24 "native" => Ok(Units::Native),
25 other => Err(format!(
26 "unknown units `{other}`; expected \"per-unit\" or \"native\""
27 )),
28 }
29 }
30}
31
32impl Units {
33 pub(crate) fn power_scales(self, base: f64) -> (f64, f64) {
37 match self {
38 Self::PerUnit => (1.0 / base, 1.0),
39 Self::Native => (1.0, base),
40 }
41 }
42
43 pub(crate) fn cost_scales(self, base: f64) -> (f64, f64) {
46 match self {
47 Self::PerUnit => (base * base, base),
48 Self::Native => (1.0, 1.0),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55pub struct DcOpfOptions {
56 pub convention: DcConvention,
57 pub units: Units,
58 pub skip_zero_impedance: bool,
61}
62
63impl Default for DcOpfOptions {
64 fn default() -> Self {
65 Self {
66 convention: DcConvention::default(),
67 units: Units::default(),
68 skip_zero_impedance: true,
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75#[non_exhaustive]
76pub struct DcGeneratorData {
77 pub bus_of_gen: Vec<usize>,
79 pub source_rows: Vec<usize>,
81 pub q: Vec<f64>,
83 pub c: Vec<f64>,
85 pub c0: Vec<f64>,
89 pub pmax: Vec<f64>,
90 pub pmin: Vec<f64>,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95#[non_exhaustive]
96pub struct DcBranchData {
97 pub from_bus: Vec<usize>,
98 pub to_bus: Vec<usize>,
99 pub b: Vec<f64>,
101 pub shift: Vec<f64>,
103 pub f_max: Vec<f64>,
105 pub angle_min: Vec<f64>,
107 pub angle_max: Vec<f64>,
108 pub source_rows: Vec<usize>,
110 pub skipped_zero_impedance: Vec<usize>,
112}
113
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116#[non_exhaustive]
117pub struct NodalGeneratorData {
118 pub q: Vec<f64>,
119 pub c: Vec<f64>,
120 pub c0: Vec<f64>,
121 pub pmax: Vec<f64>,
122 pub pmin: Vec<f64>,
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131#[non_exhaustive]
132pub struct DcOpfInstance {
133 pub name: String,
134 pub n_buses: usize,
135 pub n_source_generators: usize,
136 pub n_source_branches: usize,
137 pub base_mva: f64,
138 pub units: Units,
139 pub convention: DcConvention,
140 pub skip_zero_impedance: bool,
141 pub bus_ids: Vec<BusId>,
143 pub reference_buses: Vec<usize>,
144 pub p_d: Vec<f64>,
146 pub p_shift: Vec<f64>,
148 pub generators: DcGeneratorData,
149 pub branches: DcBranchData,
150}
151
152impl DcOpfInstance {
153 #[must_use]
154 pub fn n_generators(&self) -> usize {
155 self.generators.q.len()
156 }
157
158 #[must_use]
159 pub fn n_branches(&self) -> usize {
160 self.branches.b.len()
161 }
162
163 pub fn nodal_generator_data(&self) -> Result<NodalGeneratorData> {
168 let mut occupied = vec![false; self.n_buses];
169 let mut q = vec![0.0; self.n_buses];
170 let mut c = vec![0.0; self.n_buses];
171 let mut c0 = vec![0.0; self.n_buses];
172 let mut pmax = vec![0.0; self.n_buses];
173 let mut pmin = vec![0.0; self.n_buses];
174
175 for generator in 0..self.n_generators() {
176 let bus = self.generators.bus_of_gen[generator];
177 if occupied[bus] {
178 return Err(Error::MultipleGeneratorsAtBus {
179 bus_id: self.bus_ids[bus],
180 });
181 }
182 occupied[bus] = true;
183 q[bus] = self.generators.q[generator];
184 c[bus] = self.generators.c[generator];
185 c0[bus] = self.generators.c0[generator];
186 pmax[bus] = self.generators.pmax[generator];
187 pmin[bus] = self.generators.pmin[generator];
188 }
189
190 Ok(NodalGeneratorData {
191 q,
192 c,
193 c0,
194 pmax,
195 pmin,
196 })
197 }
198}
199
200#[allow(clippy::too_many_lines)]
202pub fn build_dc_opf_instance(
203 case: &IndexedNetwork,
204 options: &DcOpfOptions,
205) -> Result<DcOpfInstance> {
206 case.check_reference_coverage()?;
207 case.network().check_base_mva()?;
208
209 let n_buses = case.n();
210 let base = case.per_unit_base();
211 let (p_scale, b_scale) = options.units.power_scales(base);
212 let (q_scale, c_scale) = options.units.cost_scales(base);
213
214 let mut bus_of_gen = Vec::new();
215 let mut generator_rows = Vec::new();
216 let mut q = Vec::new();
217 let mut c = Vec::new();
218 let mut c0 = Vec::new();
219 let mut pmax = Vec::new();
220 let mut pmin = Vec::new();
221
222 for (source_row, generator) in case.in_service_gens() {
223 let bus = case.bus_index(generator.bus).ok_or(Error::UnknownBus {
224 bus_id: generator.bus,
225 element_index: source_row,
226 })?;
227 let cost = generator.cost.as_ref().ok_or(Error::MissingGenCost {
228 gen_index: source_row,
229 })?;
230 let (q_raw, c_raw, c0_raw) =
231 cost.quadratic_with_constant()
232 .ok_or(Error::UnsupportedCostModel {
233 gen_index: source_row,
234 model: cost.model,
235 ncost: cost.ncost,
236 })?;
237 bus_of_gen.push(bus);
238 generator_rows.push(source_row);
239 q.push(q_raw * q_scale);
240 c.push(c_raw * c_scale);
241 c0.push(c0_raw);
242 pmax.push(generator.pmax * p_scale);
243 pmin.push(generator.pmin * p_scale);
244 }
245 if q.is_empty() {
246 return Err(Error::NoGenerators);
247 }
248
249 let mut from_bus = Vec::new();
250 let mut to_bus = Vec::new();
251 let mut b = Vec::new();
252 let mut shift = Vec::new();
253 let mut f_max = Vec::new();
254 let mut angle_min = Vec::new();
255 let mut angle_max = Vec::new();
256 let mut branch_rows = Vec::new();
257 let mut skipped_zero_impedance = Vec::new();
258 let mut p_shift = vec![0.0; n_buses];
259
260 for (source_row, branch) in case.in_service_branches() {
261 let from = case.bus_index(branch.from).ok_or(Error::UnknownBus {
262 bus_id: branch.from,
263 element_index: source_row,
264 })?;
265 let to = case.bus_index(branch.to).ok_or(Error::UnknownBus {
266 bus_id: branch.to,
267 element_index: source_row,
268 })?;
269 if from == to {
270 continue;
273 }
274 if branch.x == 0.0 {
275 if options.skip_zero_impedance {
276 skipped_zero_impedance.push(source_row);
277 continue;
278 }
279 return Err(Error::ZeroImpedance { row: source_row });
280 }
281 let branch_b = options
282 .convention
283 .branch_susceptance(branch.x, branch.effective_tap())
284 * b_scale;
285 if !branch_b.is_finite() {
286 return Err(Error::NonFiniteSusceptance { row: source_row });
287 }
288 let shift_rad = if options.convention.includes_phase_shifts() {
289 case.angle_radians(branch.shift)
290 } else {
291 0.0
292 };
293 if shift_rad != 0.0 {
294 p_shift[from] -= branch_b * shift_rad;
295 p_shift[to] += branch_b * shift_rad;
296 }
297 from_bus.push(from);
298 to_bus.push(to);
299 b.push(branch_b);
300 shift.push(shift_rad);
301 f_max.push(branch.rate_a * p_scale);
302 angle_min.push(case.angle_radians(branch.angmin));
303 angle_max.push(case.angle_radians(branch.angmax));
304 branch_rows.push(source_row);
305 }
306
307 Ok(DcOpfInstance {
308 name: case.name().to_owned(),
309 n_buses,
310 n_source_generators: case.generators().len(),
311 n_source_branches: case.branches().len(),
312 base_mva: case.base_mva(),
313 units: options.units,
314 convention: options.convention,
315 skip_zero_impedance: options.skip_zero_impedance,
316 bus_ids: (0..n_buses).map(|index| case.bus_id(index)).collect(),
317 reference_buses: case.reference_bus_indices(),
318 p_d: case.pd().iter().map(|value| value * p_scale).collect(),
319 p_shift,
320 generators: DcGeneratorData {
321 bus_of_gen,
322 source_rows: generator_rows,
323 q,
324 c,
325 c0,
326 pmax,
327 pmin,
328 },
329 branches: DcBranchData {
330 from_bus,
331 to_bus,
332 b,
333 shift,
334 f_max,
335 angle_min,
336 angle_max,
337 source_rows: branch_rows,
338 skipped_zero_impedance,
339 },
340 })
341}