use std::collections::BTreeMap;
use powerio_core::Error;
use powerio_tx::{BalancedNetwork, BusId, BusType, DcConvention};
use serde::{Deserialize, Serialize};
use crate::OperatingPoint;
use crate::diagnostics::codes;
use crate::instance::constraints::ActiveConstraints;
use crate::instance::objective::{Objective, ObjectiveTerm};
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
#[non_exhaustive]
pub enum DcBusSpecification {
NetActivePower { p_mw: f64 },
Reference { va_degrees: f64 },
Isolated,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
#[non_exhaustive]
pub enum AcBusSpecification {
Pq { p: f64, q: f64 },
Pv { p: f64, vm: f64 },
Reference { vm: f64, va: f64 },
Isolated,
}
#[derive(Clone, Debug)]
pub struct DcPfInstance {
network: BalancedNetwork,
specifications: Vec<DcBusSpecification>,
approximation: DcConvention,
initial_state: Option<OperatingPoint<BalancedNetwork>>,
}
impl DcPfInstance {
pub fn from_network(network: BalancedNetwork) -> Result<Self, Error> {
require_reference(&network)?;
let totals = aggregate_bus_elements(&network);
let specifications = network
.buses()
.iter()
.map(|bus| match bus.kind {
BusType::Ref => DcBusSpecification::Reference { va_degrees: bus.va },
BusType::Isolated => DcBusSpecification::Isolated,
_ => DcBusSpecification::NetActivePower {
p_mw: net_active_power(&totals, bus.id),
},
})
.collect();
Ok(Self {
network,
specifications,
approximation: DcConvention::default(),
initial_state: None,
})
}
#[must_use]
pub fn with_approximation(mut self, approximation: DcConvention) -> Self {
self.approximation = approximation;
self
}
#[must_use]
pub fn with_initial_state(mut self, state: OperatingPoint<BalancedNetwork>) -> Self {
self.initial_state = Some(state);
self
}
#[must_use]
pub fn network(&self) -> &BalancedNetwork {
&self.network
}
#[must_use]
pub fn specifications(&self) -> &[DcBusSpecification] {
&self.specifications
}
#[must_use]
pub const fn approximation(&self) -> DcConvention {
self.approximation
}
#[must_use]
pub const fn initial_state(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
self.initial_state.as_ref()
}
}
#[derive(Clone, Debug)]
pub struct AcPfInstance {
network: BalancedNetwork,
specifications: Vec<AcBusSpecification>,
initial_state: Option<OperatingPoint<BalancedNetwork>>,
}
impl AcPfInstance {
pub fn from_network(network: BalancedNetwork) -> Result<Self, Error> {
require_reference(&network)?;
let totals = aggregate_bus_elements(&network);
let specifications = network
.buses()
.iter()
.map(|bus| {
let spec = match bus.kind {
BusType::Isolated => AcBusSpecification::Isolated,
BusType::Pv => AcBusSpecification::Pv {
p: net_active_power(&totals, bus.id),
vm: controlled_magnitude(&totals, bus.id, bus.vm)?,
},
BusType::Ref => AcBusSpecification::Reference {
vm: controlled_magnitude(&totals, bus.id, bus.vm)?,
va: bus.va,
},
_ => AcBusSpecification::Pq {
p: net_active_power(&totals, bus.id),
q: net_reactive_power(&totals, bus.id),
},
};
Ok(spec)
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(Self {
network,
specifications,
initial_state: None,
})
}
#[must_use]
pub fn with_initial_state(mut self, state: OperatingPoint<BalancedNetwork>) -> Self {
self.initial_state = Some(state);
self
}
#[must_use]
pub fn network(&self) -> &BalancedNetwork {
&self.network
}
#[must_use]
pub fn specifications(&self) -> &[AcBusSpecification] {
&self.specifications
}
#[must_use]
pub const fn initial_state(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
self.initial_state.as_ref()
}
#[must_use]
pub fn to_dc_pf(&self) -> (DcPfInstance, Vec<powerio_core::Diagnostic>) {
let instance = DcPfInstance {
network: self.network.clone(),
specifications: self
.specifications
.iter()
.map(|specification| match *specification {
AcBusSpecification::Pq { p, .. } | AcBusSpecification::Pv { p, .. } => {
DcBusSpecification::NetActivePower { p_mw: p }
}
AcBusSpecification::Reference { va, .. } => {
DcBusSpecification::Reference { va_degrees: va }
}
AcBusSpecification::Isolated => DcBusSpecification::Isolated,
})
.collect(),
approximation: DcConvention::default(),
initial_state: self.initial_state.clone(),
};
let diagnostics = vec![
transform_discarded("reactive power and voltage magnitude specifications"),
transform_assumption(
"the DC approximation holds every voltage magnitude at one per unit",
),
];
(instance, diagnostics)
}
}
#[derive(Clone, Debug)]
pub struct DcOpfInstance {
network: BalancedNetwork,
objective: Objective,
constraints: ActiveConstraints,
approximation: DcConvention,
initial_state: Option<OperatingPoint<BalancedNetwork>>,
}
impl DcOpfInstance {
pub fn from_network(network: BalancedNetwork) -> Result<Self, Error> {
require_reference(&network)?;
require_dispatchable(&network)?;
Ok(Self {
network,
objective: Objective::network_generator_cost(),
constraints: ActiveConstraints::default(),
approximation: DcConvention::default(),
initial_state: None,
})
}
#[must_use]
pub fn with_objective(mut self, objective: Objective) -> Self {
self.objective = objective;
self
}
#[must_use]
pub fn with_objective_term(mut self, term: ObjectiveTerm) -> Self {
self.objective = std::mem::take(&mut self.objective).with_term(term);
self
}
#[must_use]
pub fn with_constraints(mut self, constraints: ActiveConstraints) -> Self {
self.constraints = constraints;
self
}
#[must_use]
pub fn with_approximation(mut self, approximation: DcConvention) -> Self {
self.approximation = approximation;
self
}
#[must_use]
pub fn with_initial_state(mut self, state: OperatingPoint<BalancedNetwork>) -> Self {
self.initial_state = Some(state);
self
}
#[must_use]
pub fn network(&self) -> &BalancedNetwork {
&self.network
}
#[must_use]
pub const fn objective(&self) -> &Objective {
&self.objective
}
#[must_use]
pub const fn constraints(&self) -> &ActiveConstraints {
&self.constraints
}
#[must_use]
pub const fn approximation(&self) -> DcConvention {
self.approximation
}
#[must_use]
pub const fn initial_state(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
self.initial_state.as_ref()
}
pub fn to_dc_pf(&self) -> Result<(DcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
let instance = DcPfInstance::from_network(self.network.clone())?
.with_approximation(self.approximation);
Ok((
instance,
vec![transform_discarded(
"the objective and the active constraint selections",
)],
))
}
}
#[derive(Clone, Debug)]
pub struct AcOpfInstance {
network: BalancedNetwork,
objective: Objective,
constraints: ActiveConstraints,
initial_state: Option<OperatingPoint<BalancedNetwork>>,
}
impl AcOpfInstance {
pub fn from_network(network: BalancedNetwork) -> Result<Self, Error> {
require_reference(&network)?;
require_dispatchable(&network)?;
Ok(Self {
network,
objective: Objective::network_generator_cost(),
constraints: ActiveConstraints::default(),
initial_state: None,
})
}
#[must_use]
pub fn with_objective(mut self, objective: Objective) -> Self {
self.objective = objective;
self
}
#[must_use]
pub fn with_objective_term(mut self, term: ObjectiveTerm) -> Self {
self.objective = std::mem::take(&mut self.objective).with_term(term);
self
}
#[must_use]
pub fn with_constraints(mut self, constraints: ActiveConstraints) -> Self {
self.constraints = constraints;
self
}
#[must_use]
pub fn with_initial_state(mut self, state: OperatingPoint<BalancedNetwork>) -> Self {
self.initial_state = Some(state);
self
}
#[must_use]
pub fn network(&self) -> &BalancedNetwork {
&self.network
}
#[must_use]
pub const fn objective(&self) -> &Objective {
&self.objective
}
#[must_use]
pub const fn constraints(&self) -> &ActiveConstraints {
&self.constraints
}
#[must_use]
pub const fn initial_state(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
self.initial_state.as_ref()
}
pub fn to_ac_pf(&self) -> Result<(AcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
let instance = AcPfInstance::from_network(self.network.clone())?;
Ok((
instance,
vec![transform_discarded(
"the objective and the active constraint selections",
)],
))
}
#[must_use]
pub fn to_dc_opf(&self) -> (DcOpfInstance, Vec<powerio_core::Diagnostic>) {
let constraints = ActiveConstraints {
generator_capability: self.constraints.generator_capability.clone(),
voltage_bounds: crate::instance::ConstraintSelection::None,
thermal_limits: self.constraints.thermal_limits.clone(),
angle_bounds: self.constraints.angle_bounds.clone(),
};
let instance = DcOpfInstance {
network: self.network.clone(),
objective: self.objective.clone(),
constraints,
approximation: DcConvention::default(),
initial_state: self.initial_state.clone(),
};
let diagnostics = vec![
transform_discarded("the voltage bound constraint selection"),
transform_assumption(
"the DC approximation holds every voltage magnitude at one per unit",
),
];
(instance, diagnostics)
}
}
#[derive(Default)]
struct BusAggregate {
p_gen: f64,
q_gen: f64,
p_load: f64,
q_load: f64,
setpoint: Option<f64>,
conflicting: Option<f64>,
}
fn aggregate_bus_elements(network: &BalancedNetwork) -> BTreeMap<BusId, BusAggregate> {
let mut totals: BTreeMap<BusId, BusAggregate> = BTreeMap::new();
for generator in network
.generators()
.iter()
.filter(|generator| generator.in_service)
{
let entry = totals.entry(generator.bus).or_default();
entry.p_gen += generator.pg;
entry.q_gen += generator.qg;
match entry.setpoint {
None => entry.setpoint = Some(generator.vg),
Some(existing) if existing.to_bits() == generator.vg.to_bits() => {}
Some(_) => {
if entry.conflicting.is_none() {
entry.conflicting = Some(generator.vg);
}
}
}
}
for load in network.loads().iter().filter(|load| load.in_service) {
let entry = totals.entry(load.bus).or_default();
entry.p_load += load.p;
entry.q_load += load.q;
}
totals
}
fn net_active_power(totals: &BTreeMap<BusId, BusAggregate>, bus: BusId) -> f64 {
totals
.get(&bus)
.map_or(0.0, |entry| entry.p_gen - entry.p_load)
}
fn net_reactive_power(totals: &BTreeMap<BusId, BusAggregate>, bus: BusId) -> f64 {
totals
.get(&bus)
.map_or(0.0, |entry| entry.q_gen - entry.q_load)
}
fn controlled_magnitude(
totals: &BTreeMap<BusId, BusAggregate>,
bus: BusId,
stated: f64,
) -> Result<f64, Error> {
let Some(entry) = totals.get(&bus) else {
return Ok(stated);
};
if let (Some(existing), Some(other)) = (entry.setpoint, entry.conflicting) {
return Err(Error::new(
&codes::BUILD_INSTANCE_VOLTAGE_CONTROL_CONFLICT,
format!(
"bus {bus} has in service generators stating voltage setpoints {existing} and {other}; resolve the conflict explicitly before constructing the power flow instance"
),
));
}
Ok(entry.setpoint.unwrap_or(stated))
}
fn require_reference(network: &BalancedNetwork) -> Result<(), Error> {
if network.buses().iter().any(|bus| bus.kind == BusType::Ref) {
Ok(())
} else {
Err(Error::new(
&codes::BUILD_INSTANCE_NO_REFERENCE_BUS,
"the network states no reference (slack) bus",
))
}
}
fn require_dispatchable(network: &BalancedNetwork) -> Result<(), Error> {
if network
.generators()
.iter()
.any(|generator| generator.in_service)
{
Ok(())
} else {
Err(Error::new(
&codes::BUILD_INSTANCE_NO_GENERATORS,
"the network has no in service generator for the problem to dispatch",
))
}
}
pub(crate) fn transform_discarded(what: &str) -> powerio_core::Diagnostic {
powerio_core::Diagnostic::of(
&codes::TRANSFORM_INSTANCE_DATA_DISCARDED,
format!("{what} of the source instance are not part of the derived calculation"),
)
}
pub(crate) fn transform_assumption(what: &str) -> powerio_core::Diagnostic {
powerio_core::Diagnostic::of(&codes::TRANSFORM_INSTANCE_ASSUMPTION, what)
}