1use std::collections::BTreeMap;
8use std::sync::Arc;
9
10use powerio_core::Error;
11use powerio_dist::MulticonductorNetwork;
12
13use crate::diagnostics::codes;
14use crate::instance::{LinDist3FlowNode, LinDist3FlowOpfInstance};
15use crate::solution::{Producer, Residuals, Termination};
16
17#[derive(Clone, Debug, Default, PartialEq)]
25#[non_exhaustive]
26pub struct LinDist3FlowOpfValues {
27 pub terminal_voltage_magnitude_squared: Vec<f64>,
28 pub line_active_power: Vec<f64>,
29 pub line_reactive_power: Vec<f64>,
30 pub generator_active_power: Vec<f64>,
31 pub generator_reactive_power: Vec<f64>,
32 pub source_active_power: Vec<f64>,
33 pub source_reactive_power: Vec<f64>,
34}
35
36fn shape(what: &str, actual: usize, expected: usize) -> Result<(), Error> {
37 if actual == expected {
38 Ok(())
39 } else {
40 Err(Error::new(
41 &codes::BUILD_SOLUTION_SHAPE_MISMATCH,
42 format!("{what} carries {actual} values; expected {expected}"),
43 ))
44 }
45}
46
47fn duplicate(family: &str, identity: &str) -> Error {
48 Error::new(
49 &codes::BUILD_OPERATING_POINT_IDENTITY_UNKNOWN,
50 format!("duplicate {family} identity `{identity}`"),
51 )
52}
53
54#[derive(Clone, Debug)]
55struct SolutionIndex {
56 nodes: BTreeMap<(String, String), usize>,
57 line_conductors: BTreeMap<(String, usize), usize>,
58 generators: BTreeMap<String, (usize, usize)>,
59 sources: BTreeMap<String, (usize, usize)>,
60}
61
62impl SolutionIndex {
63 fn new(instance: &LinDist3FlowOpfInstance) -> Result<Self, Error> {
64 let mut nodes = BTreeMap::new();
65 for (position, node) in instance.topology().nodes.iter().enumerate() {
66 let key = (node.bus.to_ascii_lowercase(), node.terminal.clone());
67 if nodes.insert(key, position).is_some() {
68 return Err(duplicate(
69 "terminal",
70 &format!("{}/{}", node.bus, node.terminal),
71 ));
72 }
73 }
74
75 let mut line_conductors = BTreeMap::new();
76 for (position, conductor) in instance.topology().conductors.iter().enumerate() {
77 let key = (conductor.line.clone(), conductor.conductor_position);
78 if line_conductors.insert(key, position).is_some() {
79 return Err(duplicate("line conductor", &conductor.line));
80 }
81 }
82
83 let mut generators = BTreeMap::new();
84 let mut offset = 0;
85 for generator in instance.network().generators() {
86 let channels = generator.p_nom.len();
87 if generators
88 .insert(generator.name.clone(), (offset, channels))
89 .is_some()
90 {
91 return Err(duplicate("generator", &generator.name));
92 }
93 offset += channels;
94 }
95
96 let mut sources = BTreeMap::new();
97 let mut offset = 0;
98 for source in instance.network().sources() {
99 let channels = source.terminal_map.len();
100 if sources
101 .insert(source.name.clone(), (offset, channels))
102 .is_some()
103 {
104 return Err(duplicate("voltage source", &source.name));
105 }
106 offset += channels;
107 }
108 Ok(Self {
109 nodes,
110 line_conductors,
111 generators,
112 sources,
113 })
114 }
115}
116
117#[derive(Clone, Debug)]
119pub struct LinDist3FlowOpfSolution {
120 instance: Arc<LinDist3FlowOpfInstance>,
121 termination: Termination,
122 residuals: Residuals,
123 producer: Producer,
124 values: LinDist3FlowOpfValues,
125 objective: f64,
126 index: SolutionIndex,
127}
128
129impl LinDist3FlowOpfSolution {
130 pub const FORMULATION: &'static str = "lindist3flow";
131
132 pub fn new(
137 instance: Arc<LinDist3FlowOpfInstance>,
138 termination: Termination,
139 values: LinDist3FlowOpfValues,
140 objective: f64,
141 ) -> Result<Self, Error> {
142 let network = instance.network();
143 let nodes = instance.topology().nodes.len();
144 let conductors = instance.topology().conductors.len();
145 let generators = network
146 .generators()
147 .iter()
148 .map(|generator| generator.p_nom.len())
149 .sum();
150 let sources = network
151 .sources()
152 .iter()
153 .map(|source| source.terminal_map.len())
154 .sum();
155 shape(
156 "terminal squared voltage",
157 values.terminal_voltage_magnitude_squared.len(),
158 nodes,
159 )?;
160 shape(
161 "line active power",
162 values.line_active_power.len(),
163 conductors,
164 )?;
165 shape(
166 "line reactive power",
167 values.line_reactive_power.len(),
168 conductors,
169 )?;
170 shape(
171 "generator active power",
172 values.generator_active_power.len(),
173 generators,
174 )?;
175 shape(
176 "generator reactive power",
177 values.generator_reactive_power.len(),
178 generators,
179 )?;
180 shape(
181 "source active power",
182 values.source_active_power.len(),
183 sources,
184 )?;
185 shape(
186 "source reactive power",
187 values.source_reactive_power.len(),
188 sources,
189 )?;
190 let index = SolutionIndex::new(&instance)?;
191 Ok(Self {
192 instance,
193 termination,
194 residuals: Residuals::default(),
195 producer: None,
196 values,
197 objective,
198 index,
199 })
200 }
201
202 #[must_use]
203 pub const fn formulation(&self) -> &'static str {
204 Self::FORMULATION
205 }
206
207 #[must_use]
208 pub fn instance(&self) -> &LinDist3FlowOpfInstance {
209 &self.instance
210 }
211
212 #[must_use]
213 pub fn shared_instance(&self) -> Arc<LinDist3FlowOpfInstance> {
214 Arc::clone(&self.instance)
215 }
216
217 #[must_use]
218 pub fn network(&self) -> &MulticonductorNetwork {
219 self.instance.network()
220 }
221
222 #[must_use]
223 pub const fn termination(&self) -> &Termination {
224 &self.termination
225 }
226
227 #[must_use]
228 pub const fn residuals(&self) -> &Residuals {
229 &self.residuals
230 }
231
232 #[must_use]
233 pub fn producer(&self) -> Option<&str> {
234 self.producer.as_deref()
235 }
236
237 #[must_use]
238 pub const fn values(&self) -> &LinDist3FlowOpfValues {
239 &self.values
240 }
241
242 #[must_use]
243 pub const fn objective(&self) -> f64 {
244 self.objective
245 }
246
247 #[must_use]
248 pub fn with_producer(mut self, producer: impl Into<String>) -> Self {
249 self.producer = Some(producer.into());
250 self
251 }
252
253 #[must_use]
254 pub const fn with_residuals(mut self, residuals: Residuals) -> Self {
255 self.residuals = residuals;
256 self
257 }
258
259 pub fn node_order(&self) -> impl ExactSizeIterator<Item = &LinDist3FlowNode> {
260 self.instance.topology().nodes.iter()
261 }
262
263 #[must_use]
264 pub fn terminal_voltage_magnitude_squared(&self, bus: &str, terminal: &str) -> Option<f64> {
265 let position = self
266 .index
267 .nodes
268 .get(&(bus.to_ascii_lowercase(), terminal.to_owned()))?;
269 Some(self.values.terminal_voltage_magnitude_squared[*position])
270 }
271
272 #[must_use]
273 pub fn terminal_voltage_magnitude(&self, bus: &str, terminal: &str) -> Option<f64> {
274 let squared = self.terminal_voltage_magnitude_squared(bus, terminal)?;
275 (squared >= 0.0).then(|| squared.sqrt())
276 }
277
278 #[must_use]
279 pub fn line_active_power(&self, line: &str, conductor: usize) -> Option<f64> {
280 Some(
281 self.values.line_active_power[*self
282 .index
283 .line_conductors
284 .get(&(line.to_owned(), conductor))?],
285 )
286 }
287
288 #[must_use]
289 pub fn line_reactive_power(&self, line: &str, conductor: usize) -> Option<f64> {
290 Some(
291 self.values.line_reactive_power[*self
292 .index
293 .line_conductors
294 .get(&(line.to_owned(), conductor))?],
295 )
296 }
297
298 fn channel_position(
299 index: &BTreeMap<String, (usize, usize)>,
300 identity: &str,
301 channel: usize,
302 ) -> Option<usize> {
303 let (offset, count) = *index.get(identity)?;
304 (channel < count).then_some(offset + channel)
305 }
306
307 #[must_use]
308 pub fn generator_active_power(&self, generator: &str, channel: usize) -> Option<f64> {
309 Some(
310 self.values.generator_active_power
311 [Self::channel_position(&self.index.generators, generator, channel)?],
312 )
313 }
314
315 #[must_use]
316 pub fn generator_reactive_power(&self, generator: &str, channel: usize) -> Option<f64> {
317 Some(
318 self.values.generator_reactive_power
319 [Self::channel_position(&self.index.generators, generator, channel)?],
320 )
321 }
322
323 #[must_use]
324 pub fn source_active_power(&self, source: &str, channel: usize) -> Option<f64> {
325 Some(
326 self.values.source_active_power
327 [Self::channel_position(&self.index.sources, source, channel)?],
328 )
329 }
330
331 #[must_use]
332 pub fn source_reactive_power(&self, source: &str, channel: usize) -> Option<f64> {
333 Some(
334 self.values.source_reactive_power
335 [Self::channel_position(&self.index.sources, source, channel)?],
336 )
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use crate::{LinDist3FlowBuildOptions, LinDist3FlowOpfInstance};
343 use powerio_dist::{DistBus, DistLine, DistLineCode, MulticonductorNetwork, VoltageSource};
344
345 use super::*;
346
347 fn instance() -> Arc<LinDist3FlowOpfInstance> {
348 let terminal = vec!["1".to_owned()];
349 let mut network = MulticonductorNetwork::named("solution");
350 network
351 .buses_mut()
352 .push(DistBus::new("source", terminal.clone()));
353 network
354 .buses_mut()
355 .push(DistBus::new("load", terminal.clone()));
356 network
357 .line_codes_mut()
358 .push(DistLineCode::new("one", vec![vec![0.1]], vec![vec![0.2]]));
359 network.lines_mut().push(DistLine::new(
360 "line",
361 "source",
362 "load",
363 terminal.clone(),
364 terminal.clone(),
365 "one",
366 1.0,
367 ));
368 network.sources_mut().push(VoltageSource::new(
369 "grid",
370 "source",
371 terminal,
372 vec![230.0],
373 vec![0.0],
374 ));
375 Arc::new(
376 LinDist3FlowOpfInstance::from_network(network, LinDist3FlowBuildOptions::default())
377 .unwrap(),
378 )
379 }
380
381 fn values() -> LinDist3FlowOpfValues {
382 LinDist3FlowOpfValues {
383 terminal_voltage_magnitude_squared: vec![230.0f64.powi(2), 228.0f64.powi(2)],
384 line_active_power: vec![1_000.0],
385 line_reactive_power: vec![200.0],
386 generator_active_power: Vec::new(),
387 generator_reactive_power: Vec::new(),
388 source_active_power: vec![1_000.0],
389 source_reactive_power: vec![200.0],
390 }
391 }
392
393 #[test]
394 fn result_is_keyed_by_formulation_identities() {
395 let instance = instance();
396 let solution = LinDist3FlowOpfSolution::new(
397 Arc::clone(&instance),
398 Termination::Converged,
399 values(),
400 0.3,
401 )
402 .unwrap()
403 .with_producer("test-solver");
404 assert_eq!(solution.formulation(), "lindist3flow");
405 assert_eq!(solution.producer(), Some("test-solver"));
406 assert!((solution.terminal_voltage_magnitude("load", "1").unwrap() - 228.0).abs() < 1e-12);
407 assert!((solution.line_active_power("line", 0).unwrap() - 1_000.0).abs() < 1e-12);
408 assert!((solution.source_reactive_power("grid", 0).unwrap() - 200.0).abs() < 1e-12);
409 assert!(std::ptr::eq(solution.instance(), instance.as_ref()));
410 }
411
412 #[test]
413 fn every_physical_axis_is_shape_checked() {
414 let instance = instance();
415 let mut wrong = values();
416 wrong.line_active_power.push(0.0);
417 assert!(
418 LinDist3FlowOpfSolution::new(instance, Termination::Converged, wrong, 0.0).is_err()
419 );
420 }
421}