1use serde::{Deserialize, Serialize};
2
3use powerio_tx::{BalancedNetwork, BranchSusceptanceFormula, BusId, IndexedNetwork};
4
5use crate::{AnalysisBranchSource, Error, Result};
6use powerio_prob::ReferenceBuses;
7
8use super::{limits, nodal};
9use crate::{PiecewiseLinearCost, PreparedObjective};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13#[non_exhaustive]
14pub enum Units {
15 #[default]
17 PerUnit,
18 Native,
20}
21
22impl std::str::FromStr for Units {
23 type Err = String;
24
25 fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
28 match name.to_ascii_lowercase().replace(['-', '_'], "").as_str() {
29 "perunit" | "pu" => Ok(Units::PerUnit),
30 "native" => Ok(Units::Native),
31 other => Err(format!(
32 "unknown units `{other}`; expected \"per-unit\" or \"native\""
33 )),
34 }
35 }
36}
37
38impl Units {
39 pub(crate) fn power_scales(self, base: f64) -> (f64, f64) {
43 match self {
44 Self::PerUnit => (1.0 / base, 1.0),
45 Self::Native => (1.0, base),
46 }
47 }
48
49 pub(crate) fn cost_scales(self, base: f64) -> (f64, f64) {
52 match self {
53 Self::PerUnit => (base * base, base),
54 Self::Native => (1.0, 1.0),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61pub struct DcOpfOptions {
62 pub formula: BranchSusceptanceFormula,
64 pub units: Units,
65 pub skip_zero_impedance: bool,
71 #[serde(default)]
77 pub synthesize_unrated_limits: bool,
78 #[serde(default = "default_true")]
81 pub correct_angle_difference_bounds: bool,
82 pub objective: PreparedObjective,
84}
85
86const fn default_true() -> bool {
87 true
88}
89
90impl Default for DcOpfOptions {
91 fn default() -> Self {
92 Self {
93 formula: BranchSusceptanceFormula::default(),
94 units: Units::default(),
95 skip_zero_impedance: false,
96 synthesize_unrated_limits: false,
97 correct_angle_difference_bounds: true,
98 objective: PreparedObjective::default(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105#[non_exhaustive]
106pub struct DcGeneratorParameters {
107 pub identities: Vec<String>,
109 pub bus_of_gen: Vec<usize>,
111 pub analysis_rows: Vec<usize>,
113 pub source_rows: Vec<Option<usize>>,
116 pub q: Vec<f64>,
118 pub c: Vec<f64>,
120 pub c0: Vec<f64>,
124 pub piecewise_linear: Vec<Option<PiecewiseLinearCost>>,
130 pub pmax: Vec<f64>,
131 pub pmin: Vec<f64>,
132 pub capability_active: Vec<bool>,
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138#[non_exhaustive]
139pub struct DcBranchParameters {
140 pub identities: Vec<String>,
142 pub from_bus: Vec<usize>,
143 pub to_bus: Vec<usize>,
144 pub susceptance_magnitude: Vec<f64>,
147 pub shift: Vec<f64>,
150 pub f_max: Vec<f64>,
152 pub angle_min: Vec<f64>,
154 pub angle_max: Vec<f64>,
155 pub analysis_rows: Vec<usize>,
157 pub analysis_sources: Vec<AnalysisBranchSource>,
160 pub skipped_zero_impedance: Vec<usize>,
162 pub thermal_limit_active: Vec<bool>,
164 pub angle_bound_active: Vec<bool>,
166}
167
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[non_exhaustive]
172pub struct NodalGeneratorParameters {
173 pub q: Vec<f64>,
174 pub c: Vec<f64>,
175 pub c0: Vec<f64>,
176 pub pmax: Vec<f64>,
177 pub pmin: Vec<f64>,
178 pub has_gen: Vec<bool>,
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189#[non_exhaustive]
190pub struct DcOpfPreparation {
191 pub name: String,
192 pub n_buses: usize,
193 pub n_source_generators: usize,
194 pub n_source_branches: usize,
195 pub base_mva: f64,
196 pub units: Units,
197 pub formula: BranchSusceptanceFormula,
199 pub objective: PreparedObjective,
201 pub skip_zero_impedance: bool,
202 #[serde(default)]
208 pub synthesize_unrated_limits: bool,
209 pub correct_angle_difference_bounds: bool,
211 pub bus_ids: Vec<BusId>,
213 pub bus_analysis_rows: Vec<usize>,
215 pub bus_source_rows: Vec<Option<usize>>,
218 pub reference_buses: ReferenceBuses,
219 pub p_d: Vec<f64>,
221 pub g_s: Vec<f64>,
229 pub p_shift: Vec<f64>,
232 pub generators: DcGeneratorParameters,
233 pub branches: DcBranchParameters,
234}
235
236impl DcOpfPreparation {
237 #[must_use]
238 pub fn n_generators(&self) -> usize {
239 self.generators.q.len()
240 }
241
242 #[must_use]
243 pub fn n_branches(&self) -> usize {
244 self.branches.susceptance_magnitude.len()
245 }
246
247 #[must_use]
253 pub fn calc_fixed_nodal_withdrawal(&self) -> Vec<f64> {
254 (0..self.n_buses)
255 .map(|bus| self.p_d[bus] + self.g_s[bus] + self.p_shift[bus])
256 .collect()
257 }
258
259 #[must_use]
269 pub fn calc_branch_flow_offset(&self) -> Vec<f64> {
270 (0..self.n_branches())
271 .map(|branch| {
272 -self.branches.susceptance_magnitude[branch] * self.branches.shift[branch]
273 })
274 .collect()
275 }
276
277 pub fn calc_nodal_generator_data(&self) -> Result<NodalGeneratorParameters> {
287 let n = self.n_buses;
288 let generators = &self.generators;
289 if let Some(gen_index) = generators.piecewise_linear.iter().position(Option::is_some) {
290 return Err(Error::PiecewiseNodalCost { gen_index });
291 }
292 let bus_of_gen = &generators.bus_of_gen;
293 let costs =
294 nodal::combine_costs(n, bus_of_gen, &generators.q, &generators.c, &generators.c0);
295 Ok(NodalGeneratorParameters {
296 q: costs.q,
297 c: costs.c,
298 c0: costs.c0,
299 pmax: nodal::sum_by_bus(n, bus_of_gen, &generators.pmax),
300 pmin: nodal::sum_by_bus(n, bus_of_gen, &generators.pmin),
301 has_gen: nodal::buses_with_generators(n, bus_of_gen),
302 })
303 }
304}
305
306#[allow(clippy::too_many_lines)]
312pub(crate) fn preparation_from_view(
313 case: &IndexedNetwork,
314 options: DcOpfOptions,
315) -> Result<DcOpfPreparation> {
316 case.network().check_base_mva()?;
317
318 let active_buses = crate::opf::active_bus_index(case)?;
319 let n_buses = active_buses.analysis_rows.len();
320 let base = case.per_unit_base();
321 let (p_scale, b_scale) = options.units.power_scales(base);
322 let thermal = limits::ThermalLimits {
323 synthesize_unrated: options.synthesize_unrated_limits,
324 power_scale: p_scale,
325 admittance_scale: b_scale,
326 };
327 let (q_scale, c_scale) = options.units.cost_scales(base);
328
329 let mut bus_of_gen = Vec::new();
330 let mut generator_identities = Vec::new();
331 let mut generator_rows = Vec::new();
332 let mut q = Vec::new();
333 let mut c = Vec::new();
334 let mut c0 = Vec::new();
335 let mut piecewise_linear = Vec::new();
336 let mut pmax = Vec::new();
337 let mut pmin = Vec::new();
338
339 for (source_row, generator) in case.in_service_gens() {
340 let analysis_bus = case
341 .bus_index(generator.bus)
342 .ok_or(powerio_tx::Error::UnknownBus {
343 bus_id: generator.bus,
344 element_index: source_row,
345 })?;
346 let Some(bus) = active_buses.dense_by_analysis[analysis_bus] else {
347 continue;
348 };
349 let terms = match options.objective {
350 PreparedObjective::Feasibility => nodal::GeneratorCostTerms {
351 q: 0.0,
352 c: 0.0,
353 c0: 0.0,
354 piecewise_linear: None,
355 },
356 PreparedObjective::NetworkGeneratorCost => {
357 let cost = generator
358 .cost
359 .as_ref()
360 .ok_or(powerio_tx::Error::MissingGenCost {
361 gen_index: source_row,
362 })?;
363 nodal::generator_cost_terms(cost, source_row, p_scale)?
364 }
365 };
366 generator_identities.push(crate::opf::row_identity(
367 generator.uid.as_deref(),
368 "generators",
369 source_row,
370 ));
371 bus_of_gen.push(bus);
372 generator_rows.push(source_row);
373 q.push(terms.q * q_scale);
374 c.push(terms.c * c_scale);
375 c0.push(terms.c0);
376 piecewise_linear.push(terms.piecewise_linear);
377 pmax.push(generator.pmax * p_scale);
378 pmin.push(generator.pmin * p_scale);
379 }
380 if q.is_empty() {
381 return Err(Error::NoGenerators);
382 }
383
384 let mut from_bus = Vec::new();
385 let mut branch_identities = Vec::new();
386 let mut to_bus = Vec::new();
387 let mut b = Vec::new();
388 let mut shift = Vec::new();
389 let mut f_max = Vec::new();
390 let mut angle_min = Vec::new();
391 let mut angle_max = Vec::new();
392 let mut branch_rows = Vec::new();
393 let mut skipped_zero_impedance = Vec::new();
394 let mut p_shift = vec![0.0; n_buses];
395 let buses = &case.network().buses();
397
398 for (source_row, branch) in case.in_service_branches() {
399 let from_analysis = case
400 .bus_index(branch.from)
401 .ok_or(powerio_tx::Error::UnknownBus {
402 bus_id: branch.from,
403 element_index: source_row,
404 })?;
405 let to_analysis = case
406 .bus_index(branch.to)
407 .ok_or(powerio_tx::Error::UnknownBus {
408 bus_id: branch.to,
409 element_index: source_row,
410 })?;
411 let (Some(from), Some(to)) = (
412 active_buses.dense_by_analysis[from_analysis],
413 active_buses.dense_by_analysis[to_analysis],
414 ) else {
415 continue;
416 };
417 if from == to {
418 continue;
421 }
422 if branch.x.abs() < powerio_tx::dc::MIN_DIVISIBLE_MAGNITUDE {
426 if options.skip_zero_impedance {
427 skipped_zero_impedance.push(source_row);
428 continue;
429 }
430 return Err(powerio_tx::Error::ZeroImpedance { row: source_row }.into());
431 }
432 let tap = if options.formula.reads_tap() {
434 branch.calc_divisible_tap(source_row)?
435 } else {
436 1.0
437 };
438 let branch_b = options
439 .formula
440 .calc_solver_edge_weight(branch.r, branch.x, tap)
441 * b_scale;
442 if !branch_b.is_finite() {
443 return Err(powerio_tx::Error::NonFiniteSusceptance { row: source_row }.into());
444 }
445 let shift_rad = if options.formula.includes_phase_shifts() {
446 case.to_radians(branch.shift)
447 } else {
448 0.0
449 };
450 if shift_rad != 0.0 {
451 p_shift[from] -= branch_b * shift_rad;
452 p_shift[to] += branch_b * shift_rad;
453 }
454 let source_amin = case.to_radians(branch.angmin);
455 let source_amax = case.to_radians(branch.angmax);
456 let (amin, amax) = if options.correct_angle_difference_bounds {
457 powerio_tx::correct_angle_difference_bounds(source_amin, source_amax)
458 } else {
459 (source_amin, source_amax)
460 };
461 from_bus.push(from);
462 branch_identities.push(crate::opf::row_identity(
463 branch.uid.as_deref(),
464 "branches",
465 source_row,
466 ));
467 to_bus.push(to);
468 b.push(branch_b);
469 shift.push(shift_rad);
470 f_max.push(thermal.of(
471 branch,
472 source_amin,
473 source_amax,
474 &buses[from_analysis],
475 &buses[to_analysis],
476 ));
477 angle_min.push(amin);
478 angle_max.push(amax);
479 branch_rows.push(source_row);
480 }
481
482 let n_active_generators = q.len();
483 let n_active_branches = b.len();
484 let bus_analysis_rows = active_buses.analysis_rows;
485 let p_d = bus_analysis_rows
486 .iter()
487 .map(|&row| case.pd()[row] * p_scale)
488 .collect();
489 let g_s = bus_analysis_rows
490 .iter()
491 .map(|&row| case.gs()[row] * p_scale)
492 .collect();
493 let bus_source_rows = bus_analysis_rows.iter().copied().map(Some).collect();
494 Ok(DcOpfPreparation {
495 name: case.name().to_owned(),
496 n_buses,
497 n_source_generators: case.generators().len(),
498 n_source_branches: case.branches().len(),
499 base_mva: case.base_mva(),
500 units: options.units,
501 formula: options.formula,
502 objective: options.objective,
503 skip_zero_impedance: options.skip_zero_impedance,
504 synthesize_unrated_limits: options.synthesize_unrated_limits,
505 correct_angle_difference_bounds: options.correct_angle_difference_bounds,
506 bus_ids: active_buses.bus_ids,
507 bus_analysis_rows,
508 bus_source_rows,
509 reference_buses: active_buses.reference_buses,
510 p_d,
511 g_s,
512 p_shift,
513 generators: DcGeneratorParameters {
514 identities: generator_identities,
515 bus_of_gen,
516 analysis_rows: generator_rows.clone(),
517 source_rows: generator_rows.into_iter().map(Some).collect(),
518 q,
519 c,
520 c0,
521 piecewise_linear,
522 pmax,
523 pmin,
524 capability_active: vec![true; n_active_generators],
525 },
526 branches: DcBranchParameters {
527 identities: branch_identities,
528 from_bus,
529 to_bus,
530 susceptance_magnitude: b,
531 shift,
532 f_max,
533 angle_min,
534 angle_max,
535 analysis_rows: branch_rows.clone(),
536 analysis_sources: branch_rows
537 .iter()
538 .copied()
539 .map(|row| AnalysisBranchSource::Branch { row })
540 .collect(),
541 skipped_zero_impedance,
542 thermal_limit_active: vec![true; n_active_branches],
543 angle_bound_active: vec![true; n_active_branches],
544 },
545 })
546}
547
548pub(crate) fn apply_instance_semantics(
551 preparation: &mut DcOpfPreparation,
552 source: &BalancedNetwork,
553 constraints: &powerio_prob::ActiveConstraints,
554) -> Result<()> {
555 let source_generator_ids: Vec<String> = source
556 .generators()
557 .iter()
558 .enumerate()
559 .map(|(row, generator)| {
560 crate::opf::row_identity(generator.uid.as_deref(), "generators", row)
561 })
562 .collect();
563 let source_branch_ids: Vec<String> = source
564 .branches()
565 .iter()
566 .enumerate()
567 .map(|(row, branch)| crate::opf::row_identity(branch.uid.as_deref(), "branches", row))
568 .collect();
569
570 preparation.generators.capability_active = crate::opf::constraint_mask(
571 "generator capability",
572 &constraints.generator_capability,
573 &source_generator_ids,
574 &preparation.generators.identities,
575 )?;
576
577 let source_bus_ids: Vec<String> = source
582 .buses()
583 .iter()
584 .map(|bus| bus.id.to_string())
585 .collect();
586 let _ = crate::opf::constraint_mask(
587 "bus voltage bounds",
588 &constraints.voltage_bounds,
589 &source_bus_ids,
590 &[],
591 )?;
592
593 let mut analysis_branch_ids = source_branch_ids;
596 analysis_branch_ids.extend(
597 preparation
598 .branches
599 .identities
600 .iter()
601 .zip(&preparation.branches.analysis_rows)
602 .filter(|(_, row)| **row >= source.branches().len())
603 .map(|(identity, _)| identity.clone()),
604 );
605 preparation.branches.thermal_limit_active = crate::opf::constraint_mask(
606 "branch thermal limits",
607 &constraints.thermal_limits,
608 &analysis_branch_ids,
609 &preparation.branches.identities,
610 )?;
611 for (active, limit) in preparation
612 .branches
613 .thermal_limit_active
614 .iter_mut()
615 .zip(&preparation.branches.f_max)
616 {
617 *active &= *limit > 0.0;
618 }
619 preparation.branches.angle_bound_active = crate::opf::constraint_mask(
620 "branch angle bounds",
621 &constraints.angle_bounds,
622 &analysis_branch_ids,
623 &preparation.branches.identities,
624 )?;
625
626 preparation.n_source_generators = source.generators().len();
627 preparation.n_source_branches = source.branches().len();
628 preparation.bus_source_rows = preparation
629 .bus_analysis_rows
630 .iter()
631 .map(|&row| (row < source.buses().len()).then_some(row))
632 .collect();
633 preparation.generators.source_rows = preparation
634 .generators
635 .analysis_rows
636 .iter()
637 .map(|&row| (row < source.generators().len()).then_some(row))
638 .collect();
639 let analysis_sources = crate::opf::analysis_branch_sources(source);
640 preparation.branches.analysis_sources = preparation
641 .branches
642 .analysis_rows
643 .iter()
644 .map(|&row| analysis_sources[row])
645 .collect();
646 Ok(())
647}