1use std::collections::{BTreeMap, BTreeSet};
22
23use powerio_core::Error;
24use powerio_tx::{BalancedNetwork, BranchSusceptanceFormula, BusId, BusType};
25use serde::{Deserialize, Serialize};
26
27use crate::OperatingPoint;
28use crate::diagnostics::codes;
29use crate::instance::constraints::ActiveConstraints;
30use crate::instance::objective::{Objective, ObjectiveTerm};
31
32#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case", tag = "kind")]
36#[non_exhaustive]
37pub enum DcBusSpecification {
38 NetActivePower { p_mw: f64 },
41 Reference { va_degrees: f64 },
43 Isolated,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[serde(rename_all = "snake_case", tag = "kind")]
53#[non_exhaustive]
54pub enum AcBusSpecification {
55 Pq { p: f64, q: f64 },
57 Pv { p: f64, vm: f64 },
59 Reference { vm: f64, va: f64 },
61 Isolated,
63}
64
65#[derive(Clone, Debug)]
68pub struct DcPfInstance {
69 network: BalancedNetwork,
70 specifications: Vec<DcBusSpecification>,
71 branch_susceptance_formula: BranchSusceptanceFormula,
72 initial_point: Option<OperatingPoint<BalancedNetwork>>,
73}
74
75impl DcPfInstance {
76 pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
84 network.assign_missing_component_ids();
85 require_reference(&network)?;
86 let totals = aggregate_bus_elements(&network);
87 let specifications = network
88 .buses()
89 .iter()
90 .map(|bus| match bus.kind {
91 BusType::Ref => DcBusSpecification::Reference { va_degrees: bus.va },
92 BusType::Isolated => DcBusSpecification::Isolated,
93 _ => DcBusSpecification::NetActivePower {
95 p_mw: net_active_power(&totals, bus.id),
96 },
97 })
98 .collect();
99 Ok(Self {
100 network,
101 specifications,
102 branch_susceptance_formula: BranchSusceptanceFormula::default(),
103 initial_point: None,
104 })
105 }
106
107 #[must_use]
110 pub fn with_branch_susceptance_formula(mut self, formula: BranchSusceptanceFormula) -> Self {
111 self.branch_susceptance_formula = formula;
112 self
113 }
114
115 #[must_use]
117 pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
118 self.initial_point = Some(point);
119 self
120 }
121
122 pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
130 network.assign_missing_component_ids();
131 let mut replacement = Self::from_network(network.clone())?
132 .with_branch_susceptance_formula(self.branch_susceptance_formula);
133 if let Some(initial) = self.initial_point.take() {
134 replacement.initial_point = Some(initial.rebind_network(network)?);
135 }
136 Ok(replacement)
137 }
138
139 #[must_use]
141 pub fn network(&self) -> &BalancedNetwork {
142 &self.network
143 }
144
145 #[must_use]
147 pub fn specifications(&self) -> &[DcBusSpecification] {
148 &self.specifications
149 }
150
151 #[must_use]
153 pub const fn branch_susceptance_formula(&self) -> BranchSusceptanceFormula {
154 self.branch_susceptance_formula
155 }
156
157 #[must_use]
159 pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
160 self.initial_point.as_ref()
161 }
162}
163
164#[derive(Clone, Debug)]
167pub struct AcPfInstance {
168 network: BalancedNetwork,
169 specifications: Vec<AcBusSpecification>,
170 initial_point: Option<OperatingPoint<BalancedNetwork>>,
171}
172
173impl AcPfInstance {
174 pub fn new(
183 mut network: BalancedNetwork,
184 specifications: Vec<AcBusSpecification>,
185 ) -> Result<Self, Error> {
186 network.assign_missing_component_ids();
187 if specifications.len() != network.buses().len() {
188 return Err(Error::new(
189 &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
190 format!(
191 "AC power flow specifications carry {} rows; the network has {} buses",
192 specifications.len(),
193 network.buses().len()
194 ),
195 ));
196 }
197 if !specifications
198 .iter()
199 .any(|specification| matches!(specification, AcBusSpecification::Reference { .. }))
200 {
201 return Err(Error::new(
202 &codes::BUILD_INSTANCE_NO_REFERENCE_BUS,
203 "the AC power flow specifications state no reference (slack) bus",
204 ));
205 }
206 Ok(Self {
207 network,
208 specifications,
209 initial_point: None,
210 })
211 }
212
213 pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
223 network.assign_missing_component_ids();
224 require_reference(&network)?;
225 let totals = aggregate_bus_elements(&network);
226 let specifications = network
227 .buses()
228 .iter()
229 .map(|bus| {
230 let spec = match bus.kind {
231 BusType::Isolated => AcBusSpecification::Isolated,
232 BusType::Pv => AcBusSpecification::Pv {
233 p: net_active_power(&totals, bus.id),
234 vm: controlled_magnitude(&totals, bus.id, bus.vm)?,
235 },
236 BusType::Ref => AcBusSpecification::Reference {
237 vm: controlled_magnitude(&totals, bus.id, bus.vm)?,
238 va: bus.va,
239 },
240 _ => AcBusSpecification::Pq {
242 p: net_active_power(&totals, bus.id),
243 q: net_reactive_power(&totals, bus.id),
244 },
245 };
246 Ok(spec)
247 })
248 .collect::<Result<Vec<_>, Error>>()?;
249 Self::new(network, specifications)
250 }
251
252 #[must_use]
254 pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
255 self.initial_point = Some(point);
256 self
257 }
258
259 pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
266 network.assign_missing_component_ids();
267 let mut replacement = Self::from_network(network.clone())?;
268 if let Some(initial) = self.initial_point.take() {
269 replacement.initial_point = Some(initial.rebind_network(network)?);
270 }
271 Ok(replacement)
272 }
273
274 #[must_use]
276 pub fn network(&self) -> &BalancedNetwork {
277 &self.network
278 }
279
280 #[must_use]
282 pub fn specifications(&self) -> &[AcBusSpecification] {
283 &self.specifications
284 }
285
286 #[must_use]
288 pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
289 self.initial_point.as_ref()
290 }
291
292 #[must_use]
296 pub fn to_dc_pf(&self) -> (DcPfInstance, Vec<powerio_core::Diagnostic>) {
297 let instance = DcPfInstance {
298 network: self.network.clone(),
299 specifications: self
300 .specifications
301 .iter()
302 .map(|specification| match *specification {
303 AcBusSpecification::Pq { p, .. } | AcBusSpecification::Pv { p, .. } => {
304 DcBusSpecification::NetActivePower { p_mw: p }
305 }
306 AcBusSpecification::Reference { va, .. } => {
307 DcBusSpecification::Reference { va_degrees: va }
308 }
309 AcBusSpecification::Isolated => DcBusSpecification::Isolated,
310 })
311 .collect(),
312 branch_susceptance_formula: BranchSusceptanceFormula::default(),
313 initial_point: self.initial_point.clone(),
314 };
315 let diagnostics = vec![
316 transform_discarded("reactive power and voltage magnitude specifications"),
317 transform_assumption(
318 "the DC power flow model holds every voltage magnitude at one per unit",
319 ),
320 ];
321 (instance, diagnostics)
322 }
323}
324
325#[derive(Clone, Debug)]
329pub struct DcOpfInstance {
330 network: BalancedNetwork,
331 objective: Objective,
332 constraints: ActiveConstraints,
333 branch_susceptance_formula: BranchSusceptanceFormula,
334 initial_point: Option<OperatingPoint<BalancedNetwork>>,
335}
336
337impl DcOpfInstance {
338 pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
349 network.assign_missing_component_ids();
350 require_reference(&network)?;
351 require_dispatchable(&network)?;
352 let objective = default_opf_objective(&network);
353 Ok(Self {
354 network,
355 objective,
356 constraints: ActiveConstraints::default(),
357 branch_susceptance_formula: BranchSusceptanceFormula::default(),
358 initial_point: None,
359 })
360 }
361
362 #[must_use]
365 pub fn with_objective(mut self, objective: Objective) -> Self {
366 self.objective = objective;
367 self
368 }
369
370 #[must_use]
372 pub fn with_objective_term(mut self, term: ObjectiveTerm) -> Self {
373 self.objective = std::mem::take(&mut self.objective).with_term(term);
374 self
375 }
376
377 #[must_use]
379 pub fn with_constraints(mut self, constraints: ActiveConstraints) -> Self {
380 self.constraints = constraints;
381 self
382 }
383
384 #[must_use]
386 pub fn with_branch_susceptance_formula(mut self, formula: BranchSusceptanceFormula) -> Self {
387 self.branch_susceptance_formula = formula;
388 self
389 }
390
391 #[must_use]
393 pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
394 self.initial_point = Some(point);
395 self
396 }
397
398 pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
408 network.assign_missing_component_ids();
409 require_reference(&network)?;
410 require_dispatchable(&network)?;
411 if let Some(initial) = self.initial_point.take() {
412 self.initial_point = Some(initial.rebind_network(network.clone())?);
413 }
414 self.network = network;
415 Ok(self)
416 }
417
418 #[must_use]
420 pub fn network(&self) -> &BalancedNetwork {
421 &self.network
422 }
423
424 #[must_use]
426 pub const fn objective(&self) -> &Objective {
427 &self.objective
428 }
429
430 #[must_use]
432 pub const fn constraints(&self) -> &ActiveConstraints {
433 &self.constraints
434 }
435
436 #[must_use]
438 pub const fn branch_susceptance_formula(&self) -> BranchSusceptanceFormula {
439 self.branch_susceptance_formula
440 }
441
442 #[must_use]
444 pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
445 self.initial_point.as_ref()
446 }
447
448 pub fn to_dc_pf(&self) -> Result<(DcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
455 let instance = DcPfInstance::from_network(self.network.clone())?
456 .with_branch_susceptance_formula(self.branch_susceptance_formula);
457 Ok((
458 instance,
459 vec![transform_discarded(
460 "the objective and the active constraint selections",
461 )],
462 ))
463 }
464}
465
466#[derive(Clone, Debug)]
470pub struct AcOpfInstance {
471 network: BalancedNetwork,
472 objective: Objective,
473 constraints: ActiveConstraints,
474 initial_point: Option<OperatingPoint<BalancedNetwork>>,
475}
476
477impl AcOpfInstance {
478 pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
489 network.assign_missing_component_ids();
490 require_reference(&network)?;
491 require_dispatchable(&network)?;
492 let objective = default_opf_objective(&network);
493 Ok(Self {
494 network,
495 objective,
496 constraints: ActiveConstraints::default(),
497 initial_point: None,
498 })
499 }
500
501 #[must_use]
504 pub fn with_objective(mut self, objective: Objective) -> Self {
505 self.objective = objective;
506 self
507 }
508
509 #[must_use]
511 pub fn with_objective_term(mut self, term: ObjectiveTerm) -> Self {
512 self.objective = std::mem::take(&mut self.objective).with_term(term);
513 self
514 }
515
516 #[must_use]
518 pub fn with_constraints(mut self, constraints: ActiveConstraints) -> Self {
519 self.constraints = constraints;
520 self
521 }
522
523 #[must_use]
525 pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
526 self.initial_point = Some(point);
527 self
528 }
529
530 pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
537 network.assign_missing_component_ids();
538 require_reference(&network)?;
539 require_dispatchable(&network)?;
540 if let Some(initial) = self.initial_point.take() {
541 self.initial_point = Some(initial.rebind_network(network.clone())?);
542 }
543 self.network = network;
544 Ok(self)
545 }
546
547 #[must_use]
549 pub fn network(&self) -> &BalancedNetwork {
550 &self.network
551 }
552
553 #[must_use]
555 pub const fn objective(&self) -> &Objective {
556 &self.objective
557 }
558
559 #[must_use]
561 pub const fn constraints(&self) -> &ActiveConstraints {
562 &self.constraints
563 }
564
565 #[must_use]
567 pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
568 self.initial_point.as_ref()
569 }
570
571 pub fn to_ac_pf(&self) -> Result<(AcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
578 let instance = AcPfInstance::from_network(self.network.clone())?;
579 Ok((
580 instance,
581 vec![transform_discarded(
582 "the objective and the active constraint selections",
583 )],
584 ))
585 }
586
587 #[must_use]
592 pub fn to_dc_opf(&self) -> (DcOpfInstance, Vec<powerio_core::Diagnostic>) {
593 let constraints = ActiveConstraints {
594 generator_capability: self.constraints.generator_capability.clone(),
595 voltage_bounds: crate::instance::ConstraintSelection::None,
596 thermal_limits: self.constraints.thermal_limits.clone(),
597 angle_bounds: self.constraints.angle_bounds.clone(),
598 };
599 let instance = DcOpfInstance {
600 network: self.network.clone(),
601 objective: self.objective.clone(),
602 constraints,
603 branch_susceptance_formula: BranchSusceptanceFormula::default(),
604 initial_point: self.initial_point.clone(),
605 };
606 let diagnostics = vec![
607 transform_discarded("the voltage bound constraint selection"),
608 transform_assumption(
609 "the DC power flow model holds every voltage magnitude at one per unit",
610 ),
611 ];
612 (instance, diagnostics)
613 }
614}
615
616#[derive(Default)]
621struct BusAggregate {
622 p_gen: f64,
623 q_gen: f64,
624 p_load: f64,
625 q_load: f64,
626 setpoint: Option<f64>,
627 conflicting: Option<f64>,
628}
629
630fn aggregate_bus_elements(network: &BalancedNetwork) -> BTreeMap<BusId, BusAggregate> {
631 let mut totals: BTreeMap<BusId, BusAggregate> = BTreeMap::new();
632 for generator in network
633 .generators()
634 .iter()
635 .filter(|generator| generator.in_service)
636 {
637 let entry = totals.entry(generator.bus).or_default();
638 entry.p_gen += generator.pg;
639 entry.q_gen += generator.qg;
640 match entry.setpoint {
641 None => entry.setpoint = Some(generator.vg),
642 Some(existing) if existing.to_bits() == generator.vg.to_bits() => {}
643 Some(_) => {
644 if entry.conflicting.is_none() {
645 entry.conflicting = Some(generator.vg);
646 }
647 }
648 }
649 }
650 for load in network.loads().iter().filter(|load| load.in_service) {
651 let entry = totals.entry(load.bus).or_default();
652 entry.p_load += load.p;
653 entry.q_load += load.q;
654 }
655 totals
656}
657
658fn net_active_power(totals: &BTreeMap<BusId, BusAggregate>, bus: BusId) -> f64 {
659 totals
660 .get(&bus)
661 .map_or(0.0, |entry| entry.p_gen - entry.p_load)
662}
663
664fn net_reactive_power(totals: &BTreeMap<BusId, BusAggregate>, bus: BusId) -> f64 {
666 totals
667 .get(&bus)
668 .map_or(0.0, |entry| entry.q_gen - entry.q_load)
669}
670
671fn controlled_magnitude(
676 totals: &BTreeMap<BusId, BusAggregate>,
677 bus: BusId,
678 stated: f64,
679) -> Result<f64, Error> {
680 let Some(entry) = totals.get(&bus) else {
681 return Ok(stated);
682 };
683 if let (Some(existing), Some(other)) = (entry.setpoint, entry.conflicting) {
684 return Err(Error::new(
685 &codes::BUILD_INSTANCE_VOLTAGE_CONTROL_CONFLICT,
686 format!(
687 "bus {bus} has in service generators stating voltage setpoints {existing} and {other}; resolve the conflict explicitly before constructing the power flow instance"
688 ),
689 ));
690 }
691 Ok(entry.setpoint.unwrap_or(stated))
692}
693
694fn require_reference(network: &BalancedNetwork) -> Result<(), Error> {
695 if network.buses().iter().any(|bus| bus.kind == BusType::Ref) {
696 Ok(())
697 } else {
698 Err(Error::new(
699 &codes::BUILD_INSTANCE_NO_REFERENCE_BUS,
700 "the network states no reference (slack) bus",
701 ))
702 }
703}
704
705fn require_dispatchable(network: &BalancedNetwork) -> Result<(), Error> {
706 let active_buses = active_bus_ids(network);
707 if network
708 .generators()
709 .iter()
710 .any(|generator| generator.in_service && active_buses.contains(&generator.bus))
711 {
712 Ok(())
713 } else {
714 Err(Error::new(
715 &codes::BUILD_INSTANCE_NO_GENERATORS,
716 "the network has no in service generator for the problem to dispatch",
717 ))
718 }
719}
720
721fn default_opf_objective(network: &BalancedNetwork) -> Objective {
722 let active_buses = active_bus_ids(network);
723 if network.generators().iter().any(|generator| {
724 generator.in_service && active_buses.contains(&generator.bus) && generator.cost.is_some()
725 }) {
726 Objective::network_generator_cost()
727 } else {
728 Objective::none()
729 }
730}
731
732fn active_bus_ids(network: &BalancedNetwork) -> BTreeSet<BusId> {
733 network
734 .buses()
735 .iter()
736 .filter(|bus| bus.kind != BusType::Isolated)
737 .map(|bus| bus.id)
738 .collect()
739}
740
741pub(crate) fn transform_discarded(what: &str) -> powerio_core::Diagnostic {
742 powerio_core::Diagnostic::of(
743 &codes::TRANSFORM_INSTANCE_DATA_DISCARDED,
744 format!("{what} of the source instance are not part of the derived calculation"),
745 )
746}
747
748pub(crate) fn transform_assumption(what: &str) -> powerio_core::Diagnostic {
749 powerio_core::Diagnostic::of(&codes::TRANSFORM_INSTANCE_ASSUMPTION, what)
750}