use std::collections::{BTreeMap, BTreeSet};
use powerio_core::Error;
use powerio_tx::{BalancedNetwork, BranchSusceptanceFormula, BusId, BusType};
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)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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>,
branch_susceptance_formula: BranchSusceptanceFormula,
initial_point: Option<OperatingPoint<BalancedNetwork>>,
}
impl DcPfInstance {
pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
network.assign_missing_component_ids();
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,
branch_susceptance_formula: BranchSusceptanceFormula::default(),
initial_point: None,
})
}
#[must_use]
pub fn with_branch_susceptance_formula(mut self, formula: BranchSusceptanceFormula) -> Self {
self.branch_susceptance_formula = formula;
self
}
#[must_use]
pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
self.initial_point = Some(point);
self
}
pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
network.assign_missing_component_ids();
let mut replacement = Self::from_network(network.clone())?
.with_branch_susceptance_formula(self.branch_susceptance_formula);
if let Some(initial) = self.initial_point.take() {
replacement.initial_point = Some(initial.rebind_network(network)?);
}
Ok(replacement)
}
#[must_use]
pub fn network(&self) -> &BalancedNetwork {
&self.network
}
#[must_use]
pub fn specifications(&self) -> &[DcBusSpecification] {
&self.specifications
}
#[must_use]
pub const fn branch_susceptance_formula(&self) -> BranchSusceptanceFormula {
self.branch_susceptance_formula
}
#[must_use]
pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
self.initial_point.as_ref()
}
}
#[derive(Clone, Debug)]
pub struct AcPfInstance {
network: BalancedNetwork,
specifications: Vec<AcBusSpecification>,
initial_point: Option<OperatingPoint<BalancedNetwork>>,
}
impl AcPfInstance {
pub fn new(
mut network: BalancedNetwork,
specifications: Vec<AcBusSpecification>,
) -> Result<Self, Error> {
network.assign_missing_component_ids();
if specifications.len() != network.buses().len() {
return Err(Error::new(
&codes::BUILD_INSTANCE_SHAPE_MISMATCH,
format!(
"AC power flow specifications carry {} rows; the network has {} buses",
specifications.len(),
network.buses().len()
),
));
}
if !specifications
.iter()
.any(|specification| matches!(specification, AcBusSpecification::Reference { .. }))
{
return Err(Error::new(
&codes::BUILD_INSTANCE_NO_REFERENCE_BUS,
"the AC power flow specifications state no reference (slack) bus",
));
}
Ok(Self {
network,
specifications,
initial_point: None,
})
}
pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
network.assign_missing_component_ids();
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>>()?;
Self::new(network, specifications)
}
#[must_use]
pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
self.initial_point = Some(point);
self
}
pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
network.assign_missing_component_ids();
let mut replacement = Self::from_network(network.clone())?;
if let Some(initial) = self.initial_point.take() {
replacement.initial_point = Some(initial.rebind_network(network)?);
}
Ok(replacement)
}
#[must_use]
pub fn network(&self) -> &BalancedNetwork {
&self.network
}
#[must_use]
pub fn specifications(&self) -> &[AcBusSpecification] {
&self.specifications
}
#[must_use]
pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
self.initial_point.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(),
branch_susceptance_formula: BranchSusceptanceFormula::default(),
initial_point: self.initial_point.clone(),
};
let diagnostics = vec![
transform_discarded("reactive power and voltage magnitude specifications"),
transform_assumption(
"the DC power flow model holds every voltage magnitude at one per unit",
),
];
(instance, diagnostics)
}
}
#[derive(Clone, Debug)]
pub struct DcOpfInstance {
network: BalancedNetwork,
objective: Objective,
constraints: ActiveConstraints,
branch_susceptance_formula: BranchSusceptanceFormula,
initial_point: Option<OperatingPoint<BalancedNetwork>>,
}
impl DcOpfInstance {
pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
network.assign_missing_component_ids();
require_reference(&network)?;
require_dispatchable(&network)?;
let objective = default_opf_objective(&network);
Ok(Self {
network,
objective,
constraints: ActiveConstraints::default(),
branch_susceptance_formula: BranchSusceptanceFormula::default(),
initial_point: 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_branch_susceptance_formula(mut self, formula: BranchSusceptanceFormula) -> Self {
self.branch_susceptance_formula = formula;
self
}
#[must_use]
pub fn with_initial_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
self.initial_point = Some(point);
self
}
pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
network.assign_missing_component_ids();
require_reference(&network)?;
require_dispatchable(&network)?;
if let Some(initial) = self.initial_point.take() {
self.initial_point = Some(initial.rebind_network(network.clone())?);
}
self.network = network;
Ok(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 branch_susceptance_formula(&self) -> BranchSusceptanceFormula {
self.branch_susceptance_formula
}
#[must_use]
pub const fn initial_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
self.initial_point.as_ref()
}
pub fn to_dc_pf(&self) -> Result<(DcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
let instance = DcPfInstance::from_network(self.network.clone())?
.with_branch_susceptance_formula(self.branch_susceptance_formula);
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_point: Option<OperatingPoint<BalancedNetwork>>,
}
impl AcOpfInstance {
pub fn from_network(mut network: BalancedNetwork) -> Result<Self, Error> {
network.assign_missing_component_ids();
require_reference(&network)?;
require_dispatchable(&network)?;
let objective = default_opf_objective(&network);
Ok(Self {
network,
objective,
constraints: ActiveConstraints::default(),
initial_point: 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_point(mut self, point: OperatingPoint<BalancedNetwork>) -> Self {
self.initial_point = Some(point);
self
}
pub fn with_network(mut self, mut network: BalancedNetwork) -> Result<Self, Error> {
network.assign_missing_component_ids();
require_reference(&network)?;
require_dispatchable(&network)?;
if let Some(initial) = self.initial_point.take() {
self.initial_point = Some(initial.rebind_network(network.clone())?);
}
self.network = network;
Ok(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_point(&self) -> Option<&OperatingPoint<BalancedNetwork>> {
self.initial_point.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,
branch_susceptance_formula: BranchSusceptanceFormula::default(),
initial_point: self.initial_point.clone(),
};
let diagnostics = vec![
transform_discarded("the voltage bound constraint selection"),
transform_assumption(
"the DC power flow model 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> {
let active_buses = active_bus_ids(network);
if network
.generators()
.iter()
.any(|generator| generator.in_service && active_buses.contains(&generator.bus))
{
Ok(())
} else {
Err(Error::new(
&codes::BUILD_INSTANCE_NO_GENERATORS,
"the network has no in service generator for the problem to dispatch",
))
}
}
fn default_opf_objective(network: &BalancedNetwork) -> Objective {
let active_buses = active_bus_ids(network);
if network.generators().iter().any(|generator| {
generator.in_service && active_buses.contains(&generator.bus) && generator.cost.is_some()
}) {
Objective::network_generator_cost()
} else {
Objective::none()
}
}
fn active_bus_ids(network: &BalancedNetwork) -> BTreeSet<BusId> {
network
.buses()
.iter()
.filter(|bus| bus.kind != BusType::Isolated)
.map(|bus| bus.id)
.collect()
}
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)
}