use powerio_core::Error;
use powerio_dist::MulticonductorNetwork;
use serde::{Deserialize, Serialize};
use crate::OperatingPoint;
use crate::diagnostics::codes;
use crate::instance::balanced::transform_discarded;
use crate::instance::constraints::MulticonductorActiveConstraints;
use crate::instance::objective::{Objective, ObjectiveTerm};
fn require_source_covered_islands(network: &MulticonductorNetwork) -> Result<(), Error> {
let components = crate::update::multiconductor_network_connectivity(network);
let positions: std::collections::BTreeMap<_, _> = network
.buses()
.iter()
.enumerate()
.map(|(index, bus)| (bus.id.to_ascii_lowercase(), index))
.collect();
let mut covered = std::collections::BTreeSet::new();
for source in network.sources() {
if let Some(&index) = positions.get(&source.bus.to_ascii_lowercase()) {
covered.insert(components[index]);
}
}
for (index, bus) in network.buses().iter().enumerate() {
if !covered.contains(&components[index]) {
return Err(Error::new(
&codes::BUILD_INSTANCE_SHAPE_MISMATCH,
format!(
"multiconductor island containing bus `{}` has no voltage source; explicitly resolve de-energized islands before constructing a calculation instance",
bus.id
),
));
}
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PrescribedTerminalPower {
pub load: String,
pub terminals: Vec<String>,
pub p_w: Vec<f64>,
pub q_var: Vec<f64>,
pub voltage_model: powerio_dist::DistLoadVoltageModel,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PrescribedSourceVoltage {
pub source: String,
pub terminals: Vec<String>,
pub v_magnitude: Vec<f64>,
pub v_angle: Vec<f64>,
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct PrescribedSourceBoundary<'a> {
pub source: &'a str,
pub bus: &'a str,
pub terminals: &'a [String],
pub reference_terminal: Option<&'a str>,
pub v_magnitude: &'a [f64],
pub v_angle: &'a [f64],
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
#[non_exhaustive]
pub enum ActiveControlMode {
RegulatorTap { transformer: String },
CapacitorSteps { capacitor: String },
}
#[derive(Clone, Debug)]
pub struct McAcPfInstance {
network: MulticonductorNetwork,
loads: Vec<PrescribedTerminalPower>,
sources: Vec<PrescribedSourceVoltage>,
isolated_terminals: Vec<(String, String)>,
control_modes: Vec<ActiveControlMode>,
initial_point: Option<OperatingPoint<MulticonductorNetwork>>,
}
impl McAcPfInstance {
pub fn from_network(network: MulticonductorNetwork) -> Result<Self, Error> {
powerio_dist::require_electrical_readiness(&network)?;
if network.sources().is_empty() {
return Err(Error::new(
&codes::BUILD_INSTANCE_SHAPE_MISMATCH,
"the multiconductor network states no voltage source to anchor the calculation",
));
}
require_source_covered_islands(&network)?;
let loads = network
.loads()
.iter()
.map(|load| PrescribedTerminalPower {
load: load.name.clone(),
terminals: load.terminal_map.clone(),
p_w: load.p_nom.clone(),
q_var: load.q_nom.clone(),
voltage_model: load.voltage_model.clone(),
})
.collect();
let sources = network
.sources()
.iter()
.map(|source| PrescribedSourceVoltage {
source: source.name.clone(),
terminals: source.terminal_map.clone(),
v_magnitude: source.v_magnitude.clone(),
v_angle: source.v_angle.clone(),
})
.collect();
let controlled_element = |object: &powerio_dist::UntypedObject, key: &str| {
object
.props
.iter()
.find(|(name, _)| name.as_deref() == Some(key))
.map(|(_, value)| value.clone())
};
let mut control_modes = Vec::new();
for object in network.untyped_objects() {
match object.class.to_ascii_lowercase().as_str() {
"regcontrol" => {
if let Some(transformer) = controlled_element(object, "transformer") {
control_modes.push(ActiveControlMode::RegulatorTap { transformer });
}
}
"capcontrol" => {
if let Some(capacitor) = controlled_element(object, "capacitor") {
control_modes.push(ActiveControlMode::CapacitorSteps { capacitor });
}
}
_ => {}
}
}
Ok(Self {
network,
loads,
sources,
isolated_terminals: Vec::new(),
control_modes,
initial_point: None,
})
}
#[must_use]
pub fn with_initial_point(mut self, point: OperatingPoint<MulticonductorNetwork>) -> Self {
self.initial_point = Some(point);
self
}
pub fn with_network(mut self, network: MulticonductorNetwork) -> Result<Self, Error> {
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) -> &MulticonductorNetwork {
&self.network
}
#[must_use]
pub fn loads(&self) -> &[PrescribedTerminalPower] {
&self.loads
}
#[must_use]
pub fn sources(&self) -> &[PrescribedSourceVoltage] {
&self.sources
}
pub fn source_boundary(&self, index: usize) -> Option<PrescribedSourceBoundary<'_>> {
let values = self.sources.get(index)?;
let source = self.network.sources().get(index)?;
Some(PrescribedSourceBoundary {
source: &values.source,
bus: &source.bus,
terminals: &values.terminals,
reference_terminal: source.reference_terminal.as_deref(),
v_magnitude: &values.v_magnitude,
v_angle: &values.v_angle,
})
}
#[must_use]
pub fn isolated_terminals(&self) -> &[(String, String)] {
&self.isolated_terminals
}
#[must_use]
pub fn control_modes(&self) -> &[ActiveControlMode] {
&self.control_modes
}
#[must_use]
pub const fn initial_point(&self) -> Option<&OperatingPoint<MulticonductorNetwork>> {
self.initial_point.as_ref()
}
}
#[derive(Clone, Debug)]
pub struct McAcOpfInstance {
network: MulticonductorNetwork,
objective: Objective,
constraints: MulticonductorActiveConstraints,
initial_point: Option<OperatingPoint<MulticonductorNetwork>>,
}
impl McAcOpfInstance {
pub fn from_network(network: MulticonductorNetwork) -> Result<Self, Error> {
powerio_dist::require_electrical_readiness(&network)?;
if network.sources().is_empty() {
return Err(Error::new(
&codes::BUILD_INSTANCE_SHAPE_MISMATCH,
"the multiconductor network states no voltage source to anchor the calculation",
));
}
require_source_covered_islands(&network)?;
Ok(Self {
network,
objective: Objective::active_power_dispatch_cost(),
constraints: MulticonductorActiveConstraints::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: MulticonductorActiveConstraints) -> Self {
self.constraints = constraints;
self
}
#[must_use]
pub fn with_initial_point(mut self, point: OperatingPoint<MulticonductorNetwork>) -> Self {
self.initial_point = Some(point);
self
}
pub fn with_network(mut self, network: MulticonductorNetwork) -> Result<Self, Error> {
powerio_dist::require_electrical_readiness(&network)?;
if network.sources().is_empty() {
return Err(Error::new(
&codes::BUILD_INSTANCE_SHAPE_MISMATCH,
"the multiconductor network states no voltage source to anchor the calculation",
));
}
require_source_covered_islands(&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) -> &MulticonductorNetwork {
&self.network
}
#[must_use]
pub const fn objective(&self) -> &Objective {
&self.objective
}
#[must_use]
pub const fn constraints(&self) -> &MulticonductorActiveConstraints {
&self.constraints
}
#[must_use]
pub const fn initial_point(&self) -> Option<&OperatingPoint<MulticonductorNetwork>> {
self.initial_point.as_ref()
}
pub fn to_mc_ac_pf(&self) -> Result<(McAcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
let instance = McAcPfInstance::from_network(self.network.clone())?;
Ok((
instance,
vec![transform_discarded(
"the objective and the active constraint selections",
)],
))
}
}