Skip to main content

powerio_prob/instance/
multiconductor.rs

1//! The multiconductor calculation instances: `McAcPfInstance` and
2//! `McAcOpfInstance`.
3//!
4//! Both share the reusable [`MulticonductorNetwork`] handle. The power flow
5//! instance contains the partial boundary specification a distribution power
6//! flow needs — prescribed terminal powers, prescribed source terminal
7//! voltages, isolated terminals, load voltage models, and the active
8//! equipment control modes — never a required complete operating point. The
9//! OPF instance adds the typed per phase objective and the active constraint
10//! selections.
11
12use powerio_core::Error;
13use powerio_dist::MulticonductorNetwork;
14use serde::{Deserialize, Serialize};
15
16use crate::OperatingPoint;
17use crate::diagnostics::codes;
18use crate::instance::balanced::transform_discarded;
19use crate::instance::constraints::MulticonductorActiveConstraints;
20use crate::instance::objective::{Objective, ObjectiveTerm};
21
22/// One load's prescribed terminal power: the load's stated per phase complex
23/// power, keyed by the load's name and its terminal map. Watts and vars, as
24/// the network states them.
25#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
26pub struct PrescribedTerminalPower {
27    pub load: String,
28    /// The load's terminals, in its stated terminal map order.
29    pub terminals: Vec<String>,
30    /// Watts per terminal, aligned to `terminals`.
31    pub p_w: Vec<f64>,
32    /// Vars per terminal, aligned to `terminals`.
33    pub q_var: Vec<f64>,
34    /// The load's stated voltage dependence.
35    pub voltage_model: powerio_dist::DistLoadVoltageModel,
36}
37
38/// One source's prescribed terminal complex voltage: volts and radians per
39/// terminal, as the network states them.
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct PrescribedSourceVoltage {
42    pub source: String,
43    /// The source's terminals, in its stated terminal map order.
44    pub terminals: Vec<String>,
45    /// Volts per terminal (0.0 on grounded terminals).
46    pub v_magnitude: Vec<f64>,
47    /// Radians per terminal.
48    pub v_angle: Vec<f64>,
49}
50
51/// One equipment control that is active for the calculation, by element name.
52#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case", tag = "kind")]
54#[non_exhaustive]
55pub enum ActiveControlMode {
56    /// A regulating transformer's tap control.
57    RegulatorTap { transformer: String },
58    /// A switched capacitor's step control.
59    CapacitorSteps { capacitor: String },
60}
61
62/// The multiconductor AC power flow instance.
63#[derive(Clone, Debug)]
64pub struct McAcPfInstance {
65    network: MulticonductorNetwork,
66    loads: Vec<PrescribedTerminalPower>,
67    sources: Vec<PrescribedSourceVoltage>,
68    isolated_terminals: Vec<(String, String)>,
69    control_modes: Vec<ActiveControlMode>,
70    initial_point: Option<OperatingPoint<MulticonductorNetwork>>,
71}
72
73impl McAcPfInstance {
74    /// Build the instance from the network's stated data: every in service
75    /// load contributes its prescribed per phase powers and voltage model,
76    /// every voltage source its prescribed terminal complex voltage, and
77    /// every stated regulator and capacitor control an active control mode.
78    ///
79    /// # Errors
80    /// A network with no voltage source: a distribution power flow has no
81    /// boundary condition without one.
82    pub fn from_network(network: MulticonductorNetwork) -> Result<Self, Error> {
83        powerio_dist::require_electrical_readiness(&network)?;
84        if network.sources().is_empty() {
85            return Err(Error::new(
86                &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
87                "the multiconductor network states no voltage source to anchor the calculation",
88            ));
89        }
90        let loads = network
91            .loads()
92            .iter()
93            .map(|load| PrescribedTerminalPower {
94                load: load.name.clone(),
95                terminals: load.terminal_map.clone(),
96                p_w: load.p_nom.clone(),
97                q_var: load.q_nom.clone(),
98                voltage_model: load.voltage_model.clone(),
99            })
100            .collect();
101        let sources = network
102            .sources()
103            .iter()
104            .map(|source| PrescribedSourceVoltage {
105                source: source.name.clone(),
106                terminals: source.terminal_map.clone(),
107                v_magnitude: source.v_magnitude.clone(),
108                v_angle: source.v_angle.clone(),
109            })
110            .collect();
111        // Controllers live in the retained untyped objects: a `regcontrol`
112        // regulates the transformer its `transformer` property names, and a
113        // `capcontrol` switches the capacitor its `capacitor` property names.
114        let controlled_element = |object: &powerio_dist::UntypedObject, key: &str| {
115            object
116                .props
117                .iter()
118                .find(|(name, _)| name.as_deref() == Some(key))
119                .map(|(_, value)| value.clone())
120        };
121        let mut control_modes = Vec::new();
122        for object in network.untyped_objects() {
123            match object.class.to_ascii_lowercase().as_str() {
124                "regcontrol" => {
125                    if let Some(transformer) = controlled_element(object, "transformer") {
126                        control_modes.push(ActiveControlMode::RegulatorTap { transformer });
127                    }
128                }
129                "capcontrol" => {
130                    if let Some(capacitor) = controlled_element(object, "capacitor") {
131                        control_modes.push(ActiveControlMode::CapacitorSteps { capacitor });
132                    }
133                }
134                _ => {}
135            }
136        }
137        Ok(Self {
138            network,
139            loads,
140            sources,
141            isolated_terminals: Vec::new(),
142            control_modes,
143            initial_point: None,
144        })
145    }
146
147    /// Supply an optional solver initial point.
148    #[must_use]
149    pub fn with_initial_point(mut self, point: OperatingPoint<MulticonductorNetwork>) -> Self {
150        self.initial_point = Some(point);
151        self
152    }
153
154    /// Replace the network and recalculate its prescribed powers, source
155    /// voltages, and active controls while preserving a compatible initial
156    /// point.
157    pub fn with_network(mut self, network: MulticonductorNetwork) -> Result<Self, Error> {
158        let mut replacement = Self::from_network(network.clone())?;
159        if let Some(initial) = self.initial_point.take() {
160            replacement.initial_point = Some(initial.rebind_network(network)?);
161        }
162        Ok(replacement)
163    }
164
165    /// The network this instance calculates on. Borrowed; never a copy.
166    #[must_use]
167    pub fn network(&self) -> &MulticonductorNetwork {
168        &self.network
169    }
170
171    /// The prescribed load terminal powers, in load table order.
172    #[must_use]
173    pub fn loads(&self) -> &[PrescribedTerminalPower] {
174        &self.loads
175    }
176
177    /// The prescribed source terminal voltages, in source table order.
178    #[must_use]
179    pub fn sources(&self) -> &[PrescribedSourceVoltage] {
180        &self.sources
181    }
182
183    /// Terminals with no equation, as `(bus, terminal)` pairs.
184    #[must_use]
185    pub fn isolated_terminals(&self) -> &[(String, String)] {
186        &self.isolated_terminals
187    }
188
189    /// The equipment controls active for the calculation.
190    #[must_use]
191    pub fn control_modes(&self) -> &[ActiveControlMode] {
192        &self.control_modes
193    }
194
195    /// The optional solver initial point.
196    #[must_use]
197    pub const fn initial_point(&self) -> Option<&OperatingPoint<MulticonductorNetwork>> {
198        self.initial_point.as_ref()
199    }
200}
201
202/// The multiconductor AC optimal power flow instance: the shared network,
203/// the typed per phase objective, and the active terminal voltage, conductor,
204/// and per phase generator constraint selections.
205#[derive(Clone, Debug)]
206pub struct McAcOpfInstance {
207    network: MulticonductorNetwork,
208    objective: Objective,
209    constraints: MulticonductorActiveConstraints,
210    initial_point: Option<OperatingPoint<MulticonductorNetwork>>,
211}
212
213impl McAcOpfInstance {
214    /// Build the instance with the default per phase objective and every
215    /// stated limit active.
216    ///
217    /// # Errors
218    /// A network with no voltage source.
219    pub fn from_network(network: MulticonductorNetwork) -> Result<Self, Error> {
220        powerio_dist::require_electrical_readiness(&network)?;
221        if network.sources().is_empty() {
222            return Err(Error::new(
223                &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
224                "the multiconductor network states no voltage source to anchor the calculation",
225            ));
226        }
227        Ok(Self {
228            network,
229            objective: Objective::active_power_dispatch_cost(),
230            constraints: MulticonductorActiveConstraints::default(),
231            initial_point: None,
232        })
233    }
234
235    /// Replace the objective, consuming the instance. The shared network
236    /// moves; no table is copied.
237    #[must_use]
238    pub fn with_objective(mut self, objective: Objective) -> Self {
239        self.objective = objective;
240        self
241    }
242
243    /// Append one objective term, consuming the instance.
244    #[must_use]
245    pub fn with_objective_term(mut self, term: ObjectiveTerm) -> Self {
246        self.objective = std::mem::take(&mut self.objective).with_term(term);
247        self
248    }
249
250    /// Replace the active constraint selections, consuming the instance.
251    #[must_use]
252    pub fn with_constraints(mut self, constraints: MulticonductorActiveConstraints) -> Self {
253        self.constraints = constraints;
254        self
255    }
256
257    /// Supply an optional solver initial point.
258    #[must_use]
259    pub fn with_initial_point(mut self, point: OperatingPoint<MulticonductorNetwork>) -> Self {
260        self.initial_point = Some(point);
261        self
262    }
263
264    /// Replace the network while preserving this instance's objective,
265    /// constraint selections, and a compatible initial point.
266    pub fn with_network(mut self, network: MulticonductorNetwork) -> Result<Self, Error> {
267        powerio_dist::require_electrical_readiness(&network)?;
268        if network.sources().is_empty() {
269            return Err(Error::new(
270                &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
271                "the multiconductor network states no voltage source to anchor the calculation",
272            ));
273        }
274        if let Some(initial) = self.initial_point.take() {
275            self.initial_point = Some(initial.rebind_network(network.clone())?);
276        }
277        self.network = network;
278        Ok(self)
279    }
280
281    /// The network this instance calculates on. Borrowed; never a copy.
282    #[must_use]
283    pub fn network(&self) -> &MulticonductorNetwork {
284        &self.network
285    }
286
287    /// The typed objective.
288    #[must_use]
289    pub const fn objective(&self) -> &Objective {
290        &self.objective
291    }
292
293    /// The active constraint selections.
294    #[must_use]
295    pub const fn constraints(&self) -> &MulticonductorActiveConstraints {
296        &self.constraints
297    }
298
299    /// The optional solver initial point.
300    #[must_use]
301    pub const fn initial_point(&self) -> Option<&OperatingPoint<MulticonductorNetwork>> {
302        self.initial_point.as_ref()
303    }
304
305    /// The multiconductor power flow instance for this problem's network at
306    /// its stated injections: the objective and constraint selections are
307    /// discarded, and the discard is recorded.
308    ///
309    /// # Errors
310    /// As [`McAcPfInstance::from_network`].
311    pub fn to_mc_ac_pf(&self) -> Result<(McAcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
312        let instance = McAcPfInstance::from_network(self.network.clone())?;
313        Ok((
314            instance,
315            vec![transform_discarded(
316                "the objective and the active constraint selections",
317            )],
318        ))
319    }
320}