1use 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
22fn require_source_covered_islands(network: &MulticonductorNetwork) -> Result<(), Error> {
26 let components = crate::update::multiconductor_network_connectivity(network);
27 let positions: std::collections::BTreeMap<_, _> = network
28 .buses()
29 .iter()
30 .enumerate()
31 .map(|(index, bus)| (bus.id.to_ascii_lowercase(), index))
32 .collect();
33 let mut covered = std::collections::BTreeSet::new();
34 for source in network.sources() {
35 if let Some(&index) = positions.get(&source.bus.to_ascii_lowercase()) {
36 covered.insert(components[index]);
37 }
38 }
39 for (index, bus) in network.buses().iter().enumerate() {
40 if !covered.contains(&components[index]) {
41 return Err(Error::new(
42 &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
43 format!(
44 "multiconductor island containing bus `{}` has no voltage source; explicitly resolve de-energized islands before constructing a calculation instance",
45 bus.id
46 ),
47 ));
48 }
49 }
50 Ok(())
51}
52
53#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57pub struct PrescribedTerminalPower {
58 pub load: String,
59 pub terminals: Vec<String>,
61 pub p_w: Vec<f64>,
63 pub q_var: Vec<f64>,
65 pub voltage_model: powerio_dist::DistLoadVoltageModel,
67}
68
69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73pub struct PrescribedSourceVoltage {
74 pub source: String,
75 pub terminals: Vec<String>,
77 pub v_magnitude: Vec<f64>,
79 pub v_angle: Vec<f64>,
81}
82
83#[derive(Clone, Copy, Debug)]
88#[non_exhaustive]
89pub struct PrescribedSourceBoundary<'a> {
90 pub source: &'a str,
91 pub bus: &'a str,
92 pub terminals: &'a [String],
93 pub reference_terminal: Option<&'a str>,
94 pub v_magnitude: &'a [f64],
95 pub v_angle: &'a [f64],
96}
97
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case", tag = "kind")]
101#[non_exhaustive]
102pub enum ActiveControlMode {
103 RegulatorTap { transformer: String },
105 CapacitorSteps { capacitor: String },
107}
108
109#[derive(Clone, Debug)]
111pub struct McAcPfInstance {
112 network: MulticonductorNetwork,
113 loads: Vec<PrescribedTerminalPower>,
114 sources: Vec<PrescribedSourceVoltage>,
115 isolated_terminals: Vec<(String, String)>,
116 control_modes: Vec<ActiveControlMode>,
117 initial_point: Option<OperatingPoint<MulticonductorNetwork>>,
118}
119
120impl McAcPfInstance {
121 pub fn from_network(network: MulticonductorNetwork) -> Result<Self, Error> {
131 powerio_dist::require_electrical_readiness(&network)?;
132 if network.sources().is_empty() {
133 return Err(Error::new(
134 &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
135 "the multiconductor network states no voltage source to anchor the calculation",
136 ));
137 }
138 require_source_covered_islands(&network)?;
139 let loads = network
140 .loads()
141 .iter()
142 .map(|load| PrescribedTerminalPower {
143 load: load.name.clone(),
144 terminals: load.terminal_map.clone(),
145 p_w: load.p_nom.clone(),
146 q_var: load.q_nom.clone(),
147 voltage_model: load.voltage_model.clone(),
148 })
149 .collect();
150 let sources = network
151 .sources()
152 .iter()
153 .map(|source| PrescribedSourceVoltage {
154 source: source.name.clone(),
155 terminals: source.terminal_map.clone(),
156 v_magnitude: source.v_magnitude.clone(),
157 v_angle: source.v_angle.clone(),
158 })
159 .collect();
160 let controlled_element = |object: &powerio_dist::UntypedObject, key: &str| {
164 object
165 .props
166 .iter()
167 .find(|(name, _)| name.as_deref() == Some(key))
168 .map(|(_, value)| value.clone())
169 };
170 let mut control_modes = Vec::new();
171 for object in network.untyped_objects() {
172 match object.class.to_ascii_lowercase().as_str() {
173 "regcontrol" => {
174 if let Some(transformer) = controlled_element(object, "transformer") {
175 control_modes.push(ActiveControlMode::RegulatorTap { transformer });
176 }
177 }
178 "capcontrol" => {
179 if let Some(capacitor) = controlled_element(object, "capacitor") {
180 control_modes.push(ActiveControlMode::CapacitorSteps { capacitor });
181 }
182 }
183 _ => {}
184 }
185 }
186 Ok(Self {
187 network,
188 loads,
189 sources,
190 isolated_terminals: Vec::new(),
191 control_modes,
192 initial_point: None,
193 })
194 }
195
196 #[must_use]
198 pub fn with_initial_point(mut self, point: OperatingPoint<MulticonductorNetwork>) -> Self {
199 self.initial_point = Some(point);
200 self
201 }
202
203 pub fn with_network(mut self, network: MulticonductorNetwork) -> Result<Self, Error> {
207 let mut replacement = Self::from_network(network.clone())?;
208 if let Some(initial) = self.initial_point.take() {
209 replacement.initial_point = Some(initial.rebind_network(network)?);
210 }
211 Ok(replacement)
212 }
213
214 #[must_use]
216 pub fn network(&self) -> &MulticonductorNetwork {
217 &self.network
218 }
219
220 #[must_use]
222 pub fn loads(&self) -> &[PrescribedTerminalPower] {
223 &self.loads
224 }
225
226 #[must_use]
230 pub fn sources(&self) -> &[PrescribedSourceVoltage] {
231 &self.sources
232 }
233
234 pub fn source_boundary(&self, index: usize) -> Option<PrescribedSourceBoundary<'_>> {
237 let values = self.sources.get(index)?;
238 let source = self.network.sources().get(index)?;
239 Some(PrescribedSourceBoundary {
240 source: &values.source,
241 bus: &source.bus,
242 terminals: &values.terminals,
243 reference_terminal: source.reference_terminal.as_deref(),
244 v_magnitude: &values.v_magnitude,
245 v_angle: &values.v_angle,
246 })
247 }
248
249 #[must_use]
251 pub fn isolated_terminals(&self) -> &[(String, String)] {
252 &self.isolated_terminals
253 }
254
255 #[must_use]
257 pub fn control_modes(&self) -> &[ActiveControlMode] {
258 &self.control_modes
259 }
260
261 #[must_use]
263 pub const fn initial_point(&self) -> Option<&OperatingPoint<MulticonductorNetwork>> {
264 self.initial_point.as_ref()
265 }
266}
267
268#[derive(Clone, Debug)]
272pub struct McAcOpfInstance {
273 network: MulticonductorNetwork,
274 objective: Objective,
275 constraints: MulticonductorActiveConstraints,
276 initial_point: Option<OperatingPoint<MulticonductorNetwork>>,
277}
278
279impl McAcOpfInstance {
280 pub fn from_network(network: MulticonductorNetwork) -> Result<Self, Error> {
286 powerio_dist::require_electrical_readiness(&network)?;
287 if network.sources().is_empty() {
288 return Err(Error::new(
289 &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
290 "the multiconductor network states no voltage source to anchor the calculation",
291 ));
292 }
293 require_source_covered_islands(&network)?;
294 Ok(Self {
295 network,
296 objective: Objective::active_power_dispatch_cost(),
297 constraints: MulticonductorActiveConstraints::default(),
298 initial_point: None,
299 })
300 }
301
302 #[must_use]
305 pub fn with_objective(mut self, objective: Objective) -> Self {
306 self.objective = objective;
307 self
308 }
309
310 #[must_use]
312 pub fn with_objective_term(mut self, term: ObjectiveTerm) -> Self {
313 self.objective = std::mem::take(&mut self.objective).with_term(term);
314 self
315 }
316
317 #[must_use]
319 pub fn with_constraints(mut self, constraints: MulticonductorActiveConstraints) -> Self {
320 self.constraints = constraints;
321 self
322 }
323
324 #[must_use]
326 pub fn with_initial_point(mut self, point: OperatingPoint<MulticonductorNetwork>) -> Self {
327 self.initial_point = Some(point);
328 self
329 }
330
331 pub fn with_network(mut self, network: MulticonductorNetwork) -> Result<Self, Error> {
334 powerio_dist::require_electrical_readiness(&network)?;
335 if network.sources().is_empty() {
336 return Err(Error::new(
337 &codes::BUILD_INSTANCE_SHAPE_MISMATCH,
338 "the multiconductor network states no voltage source to anchor the calculation",
339 ));
340 }
341 require_source_covered_islands(&network)?;
342 if let Some(initial) = self.initial_point.take() {
343 self.initial_point = Some(initial.rebind_network(network.clone())?);
344 }
345 self.network = network;
346 Ok(self)
347 }
348
349 #[must_use]
351 pub fn network(&self) -> &MulticonductorNetwork {
352 &self.network
353 }
354
355 #[must_use]
357 pub const fn objective(&self) -> &Objective {
358 &self.objective
359 }
360
361 #[must_use]
363 pub const fn constraints(&self) -> &MulticonductorActiveConstraints {
364 &self.constraints
365 }
366
367 #[must_use]
369 pub const fn initial_point(&self) -> Option<&OperatingPoint<MulticonductorNetwork>> {
370 self.initial_point.as_ref()
371 }
372
373 pub fn to_mc_ac_pf(&self) -> Result<(McAcPfInstance, Vec<powerio_core::Diagnostic>), Error> {
380 let instance = McAcPfInstance::from_network(self.network.clone())?;
381 Ok((
382 instance,
383 vec![transform_discarded(
384 "the objective and the active constraint selections",
385 )],
386 ))
387 }
388}