1use std::collections::{BTreeMap, BTreeSet};
22
23use powerio_core::Error;
24use powerio_tx::{BalancedNetwork, BranchSusceptanceFormula, BusId, BusType, HvdcTreatment};
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 },
42 Reference { va_degrees: f64 },
44 Isolated,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53#[serde(rename_all = "snake_case", tag = "kind")]
54#[non_exhaustive]
55pub enum AcBusSpecification {
56 Pq { p: f64, q: f64 },
58 Pv { p: f64, vm: f64 },
60 Reference { vm: f64, va: f64 },
62 Isolated,
64}
65
66#[derive(Clone, Debug)]
69pub struct DcPfInstance {
70 network: BalancedNetwork,
71 specifications: Vec<DcBusSpecification>,
72 branch_susceptance_formula: BranchSusceptanceFormula,
73 hvdc_treatment: HvdcTreatment,
74 initial_point: Option<OperatingPoint<BalancedNetwork>>,
75}
76
77impl DcPfInstance {
78 pub fn from_network(network: BalancedNetwork) -> Result<Self, Error> {
87 Self::from_network_with_hvdc(network, HvdcTreatment::default())
88 }
89
90 pub fn from_network_with_hvdc(
98 mut network: BalancedNetwork,
99 hvdc_treatment: HvdcTreatment,
100 ) -> Result<Self, Error> {
101 network.assign_missing_component_ids();
102 require_reference(&network)?;
103 let totals = aggregate_bus_elements(&network, hvdc_treatment);
104 let specifications = network
105 .buses()
106 .iter()
107 .map(|bus| match bus.kind {
108 BusType::Ref => DcBusSpecification::Reference { va_degrees: bus.va },
109 BusType::Isolated => DcBusSpecification::Isolated,
110 _ => DcBusSpecification::NetActivePower {
112 p_mw: net_active_power(&totals, bus.id),
113 },
114 })
115 .collect();
116 Ok(Self {
117 network,
118 specifications,
119 branch_susceptance_formula: BranchSusceptanceFormula::default(),
120 hvdc_treatment,
121 initial_point: None,
122 })
123 }
124
125 #[must_use]
128 pub fn with_branch_susceptance_formula(mut self, formula: BranchSusceptanceFormula) -> Self {
129 self.branch_susceptance_formula = formula;
130 self
131 }
132
133 #[must_use]
135 pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
136 self.initial_point = Some(point);
137 self
138 }
139
140 pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
148 network.assign_missing_component_ids();
149 let mut replacement = Self::from_network_with_hvdc(network.clone(), self.hvdc_treatment)?
150 .with_branch_susceptance_formula(self.branch_susceptance_formula);
151 if let Some(initial) = self.initial_point.take() {
152 replacement.initial_point = Some(initial.rebind_network(network)?);
153 }
154 Ok(replacement)
155 }
156
157 #[must_use]
159 pub fn network(&self) -> &BalancedNetwork {
160 &self.network
161 }
162
163 #[must_use]
165 pub fn specifications(&self) -> &[DcBusSpecification] {
166 &self.specifications
167 }
168
169 #[must_use]
171 pub const fn branch_susceptance_formula(&self) -> BranchSusceptanceFormula {
172 self.branch_susceptance_formula
173 }
174
175 #[must_use]
177 pub const fn hvdc_treatment(&self) -> HvdcTreatment {
178 self.hvdc_treatment
179 }
180
181 #[must_use]
185 pub fn hvdc_diagnostics(&self) -> Vec<powerio_core::Diagnostic> {
186 self.network
187 .calc_hvdc_injections()
188 .to_diagnostics(self.hvdc_treatment)
189 }
190
191 #[must_use]
193 pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
194 self.initial_point.as_ref()
195 }
196}
197
198#[derive(Clone, Debug)]
201pub struct AcPfInstance {
202 network: BalancedNetwork,
203 specifications: Vec<AcBusSpecification>,
204 hvdc_treatment: HvdcTreatment,
205 initial_point: Option<OperatingPoint<BalancedNetwork>>,
206}
207
208impl AcPfInstance {
209 pub fn new(
219 network: BalancedNetwork,
220 specifications: Vec<AcBusSpecification>,
221 ) -> Result<Self, Error> {
222 Self::new_with_hvdc(network, specifications, HvdcTreatment::default())
223 }
224
225 pub fn new_with_hvdc(
234 mut network: BalancedNetwork,
235 specifications: Vec<AcBusSpecification>,
236 hvdc_treatment: HvdcTreatment,
237 ) -> Result<Self, Error> {
238 network.assign_missing_component_ids();
239 if specifications.len() != network.buses().len() {
240 return Err(Error::new(
241 &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
242 format!(
243 "AC power flow specifications carry {} rows; the network has {} buses",
244 specifications.len(),
245 network.buses().len()
246 ),
247 ));
248 }
249 if !specifications
250 .iter()
251 .any(|specification| matches!(specification, AcBusSpecification::Reference { .. }))
252 {
253 return Err(Error::new(
254 &codes::BUILD_INSTANCE_NO_REFERENCE_BUS,
255 "the AC power flow specifications state no reference (slack) bus",
256 ));
257 }
258 Ok(Self {
259 network,
260 specifications,
261 hvdc_treatment,
262 initial_point: None,
263 })
264 }
265
266 pub fn from_network(network: BalancedNetwork) -> Result<Self, Error> {
279 Self::from_network_with_hvdc(network, HvdcTreatment::default())
280 }
281
282 pub fn from_network_with_hvdc(
290 mut network: BalancedNetwork,
291 hvdc_treatment: HvdcTreatment,
292 ) -> Result<Self, Error> {
293 network.assign_missing_component_ids();
294 require_reference(&network)?;
295 let totals = aggregate_bus_elements(&network, hvdc_treatment);
296 let specifications = network
297 .buses()
298 .iter()
299 .map(|bus| {
300 let spec = match bus.kind {
301 BusType::Isolated => AcBusSpecification::Isolated,
302 BusType::Pv => AcBusSpecification::Pv {
303 p: net_active_power(&totals, bus.id),
304 vm: controlled_magnitude(&totals, bus.id, bus.vm)?,
305 },
306 BusType::Ref => AcBusSpecification::Reference {
307 vm: controlled_magnitude(&totals, bus.id, bus.vm)?,
308 va: bus.va,
309 },
310 _ => AcBusSpecification::Pq {
312 p: net_active_power(&totals, bus.id),
313 q: net_reactive_power(&totals, bus.id),
314 },
315 };
316 Ok(spec)
317 })
318 .collect::<Result<Vec<_>, Error>>()?;
319 Self::new_with_hvdc(network, specifications, hvdc_treatment)
320 }
321
322 #[must_use]
324 pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
325 self.initial_point = Some(point);
326 self
327 }
328
329 pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
336 network.assign_missing_component_ids();
337 let mut replacement = Self::from_network_with_hvdc(network.clone(), self.hvdc_treatment)?;
338 if let Some(initial) = self.initial_point.take() {
339 replacement.initial_point = Some(initial.rebind_network(network)?);
340 }
341 Ok(replacement)
342 }
343
344 #[must_use]
346 pub fn network(&self) -> &BalancedNetwork {
347 &self.network
348 }
349
350 #[must_use]
352 pub fn specifications(&self) -> &[AcBusSpecification] {
353 &self.specifications
354 }
355
356 #[must_use]
359 pub const fn hvdc_treatment(&self) -> HvdcTreatment {
360 self.hvdc_treatment
361 }
362
363 #[must_use]
366 pub fn hvdc_diagnostics(&self) -> Vec<powerio_core::Diagnostic> {
367 self.network
368 .calc_hvdc_injections()
369 .to_diagnostics(self.hvdc_treatment)
370 }
371
372 #[must_use]
374 pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
375 self.initial_point.as_ref()
376 }
377
378 #[must_use]
382 pub fn to_dc_pf(&self) -> (DcPfInstance, Vec<powerio_core::Diagnostic>) {
383 let instance = DcPfInstance {
384 network: self.network.clone(),
385 specifications: self
386 .specifications
387 .iter()
388 .map(|specification| match *specification {
389 AcBusSpecification::Pq { p, .. } | AcBusSpecification::Pv { p, .. } => {
390 DcBusSpecification::NetActivePower { p_mw: p }
391 }
392 AcBusSpecification::Reference { va, .. } => {
393 DcBusSpecification::Reference { va_degrees: va }
394 }
395 AcBusSpecification::Isolated => DcBusSpecification::Isolated,
396 })
397 .collect(),
398 branch_susceptance_formula: BranchSusceptanceFormula::default(),
399 hvdc_treatment: self.hvdc_treatment,
400 initial_point: self.initial_point.clone(),
401 };
402 let diagnostics = vec![
403 transform_discarded("reactive power and voltage magnitude specifications"),
404 transform_assumption(
405 "the DC power flow model holds every voltage magnitude at one per unit",
406 ),
407 ];
408 (instance, diagnostics)
409 }
410}
411
412#[derive(Clone, Debug)]
416pub struct DcOpfInstance {
417 network: BalancedNetwork,
418 objective: Objective,
419 constraints: ActiveConstraints,
420 branch_susceptance_formula: BranchSusceptanceFormula,
421 initial_point: Option<OperatingPoint<BalancedNetwork>>,
422}
423
424impl DcOpfInstance {
425 pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
436 network.assign_missing_component_ids();
437 require_reference(&network)?;
438 require_dispatchable(&network)?;
439 let objective = default_opf_objective(&network);
440 Ok(Self {
441 network,
442 objective,
443 constraints: ActiveConstraints::default(),
444 branch_susceptance_formula: BranchSusceptanceFormula::default(),
445 initial_point: None,
446 })
447 }
448
449 #[must_use]
452 pub fn with_objective(mut self, objective: Objective) -> Self {
453 self.objective = objective;
454 self
455 }
456
457 #[must_use]
459 pub fn with_objective_term(mut self, term: ObjectiveTerm) -> Self {
460 self.objective = std::mem::take(&mut self.objective).with_term(term);
461 self
462 }
463
464 #[must_use]
466 pub fn with_constraints(mut self, constraints: ActiveConstraints) -> Self {
467 self.constraints = constraints;
468 self
469 }
470
471 #[must_use]
473 pub fn with_branch_susceptance_formula(mut self, formula: BranchSusceptanceFormula) -> Self {
474 self.branch_susceptance_formula = formula;
475 self
476 }
477
478 #[must_use]
480 pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
481 self.initial_point = Some(point);
482 self
483 }
484
485 pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
495 network.assign_missing_component_ids();
496 require_reference(&network)?;
497 require_dispatchable(&network)?;
498 if let Some(initial) = self.initial_point.take() {
499 self.initial_point = Some(initial.rebind_network(network.clone())?);
500 }
501 self.network = network;
502 Ok(self)
503 }
504
505 #[must_use]
507 pub fn network(&self) -> &BalancedNetwork {
508 &self.network
509 }
510
511 #[must_use]
513 pub const fn objective(&self) -> &Objective {
514 &self.objective
515 }
516
517 #[must_use]
519 pub const fn constraints(&self) -> &ActiveConstraints {
520 &self.constraints
521 }
522
523 #[must_use]
525 pub const fn branch_susceptance_formula(&self) -> BranchSusceptanceFormula {
526 self.branch_susceptance_formula
527 }
528
529 #[must_use]
531 pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
532 self.initial_point.as_ref()
533 }
534
535 pub fn to_dc_pf(&self) -> Result<(DcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
542 let instance = DcPfInstance::from_network(self.network.clone())?
543 .with_branch_susceptance_formula(self.branch_susceptance_formula);
544 Ok((
545 instance,
546 vec![transform_discarded(
547 "the objective and the active constraint selections",
548 )],
549 ))
550 }
551}
552
553#[derive(Clone, Debug)]
557pub struct AcOpfInstance {
558 network: BalancedNetwork,
559 objective: Objective,
560 constraints: ActiveConstraints,
561 initial_point: Option<OperatingPoint<BalancedNetwork>>,
562}
563
564impl AcOpfInstance {
565 pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
576 network.assign_missing_component_ids();
577 require_reference(&network)?;
578 require_dispatchable(&network)?;
579 let objective = default_opf_objective(&network);
580 Ok(Self {
581 network,
582 objective,
583 constraints: ActiveConstraints::default(),
584 initial_point: None,
585 })
586 }
587
588 #[must_use]
591 pub fn with_objective(mut self, objective: Objective) -> Self {
592 self.objective = objective;
593 self
594 }
595
596 #[must_use]
598 pub fn with_objective_term(mut self, term: ObjectiveTerm) -> Self {
599 self.objective = std::mem::take(&mut self.objective).with_term(term);
600 self
601 }
602
603 #[must_use]
605 pub fn with_constraints(mut self, constraints: ActiveConstraints) -> Self {
606 self.constraints = constraints;
607 self
608 }
609
610 #[must_use]
612 pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
613 self.initial_point = Some(point);
614 self
615 }
616
617 pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
624 network.assign_missing_component_ids();
625 require_reference(&network)?;
626 require_dispatchable(&network)?;
627 if let Some(initial) = self.initial_point.take() {
628 self.initial_point = Some(initial.rebind_network(network.clone())?);
629 }
630 self.network = network;
631 Ok(self)
632 }
633
634 #[must_use]
636 pub fn network(&self) -> &BalancedNetwork {
637 &self.network
638 }
639
640 #[must_use]
642 pub const fn objective(&self) -> &Objective {
643 &self.objective
644 }
645
646 #[must_use]
648 pub const fn constraints(&self) -> &ActiveConstraints {
649 &self.constraints
650 }
651
652 #[must_use]
654 pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
655 self.initial_point.as_ref()
656 }
657
658 pub fn to_ac_pf(&self) -> Result<(AcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
665 let instance = AcPfInstance::from_network(self.network.clone())?;
666 Ok((
667 instance,
668 vec![transform_discarded(
669 "the objective and the active constraint selections",
670 )],
671 ))
672 }
673
674 #[must_use]
679 pub fn to_dc_opf(&self) -> (DcOpfInstance, Vec<powerio_core::Diagnostic>) {
680 let constraints = ActiveConstraints {
681 generator_capability: self.constraints.generator_capability.clone(),
682 voltage_bounds: crate::instance::ConstraintSelection::None,
683 thermal_limits: self.constraints.thermal_limits.clone(),
684 angle_bounds: self.constraints.angle_bounds.clone(),
685 };
686 let instance = DcOpfInstance {
687 network: self.network.clone(),
688 objective: self.objective.clone(),
689 constraints,
690 branch_susceptance_formula: BranchSusceptanceFormula::default(),
691 initial_point: self.initial_point.clone(),
692 };
693 let diagnostics = vec![
694 transform_discarded("the voltage bound constraint selection"),
695 transform_assumption(
696 "the DC power flow model holds every voltage magnitude at one per unit",
697 ),
698 ];
699 (instance, diagnostics)
700 }
701}
702
703#[derive(Default)]
708struct BusAggregate {
709 p_gen: f64,
710 q_gen: f64,
711 p_load: f64,
712 q_load: f64,
713 p_hvdc: f64,
714 q_hvdc: f64,
715 setpoint: Option<f64>,
716 conflicting: Option<f64>,
717}
718
719fn aggregate_bus_elements(
720 network: &BalancedNetwork,
721 hvdc_treatment: HvdcTreatment,
722) -> BTreeMap<BusId, BusAggregate> {
723 let mut totals: BTreeMap<BusId, BusAggregate> = BTreeMap::new();
724 for generator in network
725 .generators()
726 .iter()
727 .filter(|generator| generator.in_service)
728 {
729 let entry = totals.entry(generator.bus).or_default();
730 entry.p_gen += generator.pg;
731 entry.q_gen += generator.qg;
732 match entry.setpoint {
733 None => entry.setpoint = Some(generator.vg),
734 Some(existing) if existing.to_bits() == generator.vg.to_bits() => {}
735 Some(_) => {
736 if entry.conflicting.is_none() {
737 entry.conflicting = Some(generator.vg);
738 }
739 }
740 }
741 }
742 for load in network.loads().iter().filter(|load| load.in_service) {
743 let entry = totals.entry(load.bus).or_default();
744 entry.p_load += load.p;
745 entry.q_load += load.q;
746 }
747 if hvdc_treatment == HvdcTreatment::FixedInjection {
748 for terminal in network.calc_hvdc_injections().terminals {
749 let entry = totals.entry(terminal.bus).or_default();
750 entry.p_hvdc += terminal.p;
751 entry.q_hvdc += terminal.q;
752 }
753 }
754 totals
755}
756
757fn net_active_power(totals: &BTreeMap<BusId, BusAggregate>, bus: BusId) -> f64 {
758 totals
759 .get(&bus)
760 .map_or(0.0, |entry| entry.p_gen - entry.p_load + entry.p_hvdc)
761}
762
763fn net_reactive_power(totals: &BTreeMap<BusId, BusAggregate>, bus: BusId) -> f64 {
765 totals
766 .get(&bus)
767 .map_or(0.0, |entry| entry.q_gen - entry.q_load + entry.q_hvdc)
768}
769
770fn controlled_magnitude(
775 totals: &BTreeMap<BusId, BusAggregate>,
776 bus: BusId,
777 stated: f64,
778) -> Result<f64, Error> {
779 let Some(entry) = totals.get(&bus) else {
780 return Ok(stated);
781 };
782 if let (Some(existing), Some(other)) = (entry.setpoint, entry.conflicting) {
783 return Err(Error::new(
784 &codes::BUILD_INSTANCE_VOLTAGE_CONTROL_CONFLICT,
785 format!(
786 "bus {bus} has in service generators stating voltage setpoints {existing} and {other}; resolve the conflict explicitly before constructing the power flow instance"
787 ),
788 ));
789 }
790 Ok(entry.setpoint.unwrap_or(stated))
791}
792
793fn require_reference(network: &BalancedNetwork) -> Result<(), Error> {
794 if network.buses().iter().any(|bus| bus.kind == BusType::Ref) {
795 Ok(())
796 } else {
797 Err(Error::new(
798 &codes::BUILD_INSTANCE_NO_REFERENCE_BUS,
799 "the network states no reference (slack) bus",
800 ))
801 }
802}
803
804fn require_dispatchable(network: &BalancedNetwork) -> Result<(), Error> {
805 let active_buses = active_bus_ids(network);
806 if network
807 .generators()
808 .iter()
809 .any(|generator| generator.in_service && active_buses.contains(&generator.bus))
810 {
811 Ok(())
812 } else {
813 Err(Error::new(
814 &codes::BUILD_INSTANCE_NO_GENERATORS,
815 "the network has no in service generator for the problem to dispatch",
816 ))
817 }
818}
819
820fn default_opf_objective(network: &BalancedNetwork) -> Objective {
821 let active_buses = active_bus_ids(network);
822 if network.generators().iter().any(|generator| {
823 generator.in_service && active_buses.contains(&generator.bus) && generator.cost.is_some()
824 }) {
825 Objective::network_generator_cost()
826 } else {
827 Objective::none()
828 }
829}
830
831fn active_bus_ids(network: &BalancedNetwork) -> BTreeSet<BusId> {
832 network
833 .buses()
834 .iter()
835 .filter(|bus| bus.kind != BusType::Isolated)
836 .map(|bus| bus.id)
837 .collect()
838}
839
840pub(crate) fn transform_discarded(what: &str) -> powerio_core::Diagnostic {
841 powerio_core::Diagnostic::of(
842 &codes::TRANSFORM_INSTANCE_DATA_DISCARDED,
843 format!("{what} of the source instance are not part of the derived calculation"),
844 )
845}
846
847pub(crate) fn transform_assumption(what: &str) -> powerio_core::Diagnostic {
848 powerio_core::Diagnostic::of(&codes::TRANSFORM_INSTANCE_ASSUMPTION, what)
849}