Skip to main content

powerio_prob/solution/
multiconductor.rs

1//! The multiconductor solutions: `McAcPfSolution` and `McAcOpfSolution`.
2//!
3//! Terminal values are stored in the shared network's bus table order, each
4//! bus's terminals in its stated order — the layout the operating point builders use —
5//! and read back by `(bus, terminal)`. Source injections are per source
6//! terminal in source table order.
7
8use std::sync::Arc;
9
10use powerio_core::Error;
11use powerio_dist::MulticonductorNetwork;
12
13use crate::diagnostics::codes;
14use crate::instance::{McAcOpfInstance, McAcPfInstance};
15use crate::solution::{Producer, Residuals, Termination};
16
17fn check_length(what: &'static str, got: usize, expected: usize) -> Result<(), Error> {
18    if got == expected {
19        Ok(())
20    } else {
21        Err(Error::new(
22            &codes::BUILD_SOLUTION_SHAPE_MISMATCH,
23            format!("{what} carries {got} values; the instance resolves {expected} terminals"),
24        ))
25    }
26}
27
28fn terminal_count(network: &MulticonductorNetwork) -> usize {
29    network.buses().iter().map(|bus| bus.terminals.len()).sum()
30}
31
32fn source_terminal_count(network: &MulticonductorNetwork) -> usize {
33    network
34        .sources()
35        .iter()
36        .map(|source| source.terminal_map.len())
37        .sum()
38}
39
40/// Bus-and-terminal to flat column position, built once per solution on
41/// first keyed access so repeated reads never rescan the bus table.
42#[derive(Clone, Debug, Default)]
43struct TerminalIndex {
44    position: std::collections::BTreeMap<(String, String), usize>,
45}
46
47impl TerminalIndex {
48    fn build(network: &MulticonductorNetwork) -> Result<Self, Error> {
49        let mut position = std::collections::BTreeMap::new();
50        let mut offset = 0usize;
51        for row in network.buses() {
52            for (column, terminal) in row.terminals.iter().enumerate() {
53                if position
54                    .insert((row.id.clone(), terminal.clone()), offset + column)
55                    .is_some()
56                {
57                    return Err(Error::new(
58                        &codes::BUILD_OPERATING_POINT_IDENTITY_UNKNOWN,
59                        format!("bus `{}`: duplicate terminal identity `{terminal}`", row.id),
60                    ));
61                }
62            }
63            offset += row.terminals.len();
64        }
65        Ok(Self { position })
66    }
67}
68
69fn terminal_position(index: &TerminalIndex, bus: &str, terminal: &str) -> Option<usize> {
70    index
71        .position
72        .get(&(bus.to_owned(), terminal.to_owned()))
73        .copied()
74}
75
76macro_rules! shared_mc_solution_accessors {
77    ($instance_type:ty) => {
78        /// The immutable instance this solution solves. Borrowed; never a
79        /// copy.
80        #[must_use]
81        pub fn instance(&self) -> &$instance_type {
82            &self.instance
83        }
84
85        /// The shared instance owner, for another solution of the same
86        /// problem.
87        #[must_use]
88        pub fn shared_instance(&self) -> Arc<$instance_type> {
89            Arc::clone(&self.instance)
90        }
91
92        fn terminal_index(&self) -> &TerminalIndex {
93            &self.index
94        }
95
96        /// The network the solved instance calculates on.
97        #[must_use]
98        pub fn network(&self) -> &MulticonductorNetwork {
99            self.instance.network()
100        }
101
102        /// How the producing calculation ended.
103        #[must_use]
104        pub fn termination(&self) -> &Termination {
105            &self.termination
106        }
107
108        /// The reported numerical residuals.
109        #[must_use]
110        pub fn residuals(&self) -> &Residuals {
111            &self.residuals
112        }
113
114        /// The producer or solver identity, when recorded.
115        #[must_use]
116        pub fn producer(&self) -> Option<&str> {
117            self.producer.as_deref()
118        }
119
120        /// Record the producer identity.
121        #[must_use]
122        pub fn with_producer(mut self, producer: impl Into<String>) -> Self {
123            self.producer = Some(producer.into());
124            self
125        }
126
127        /// Record the numerical residuals.
128        #[must_use]
129        pub fn with_residuals(mut self, residuals: Residuals) -> Self {
130            self.residuals = residuals;
131            self
132        }
133
134        /// Voltage magnitude at one bus terminal, volts.
135        #[must_use]
136        pub fn terminal_voltage_magnitude(&self, bus: &str, terminal: &str) -> Option<f64> {
137            Some(
138                self.terminal_voltage_magnitude
139                    [terminal_position(self.terminal_index(), bus, terminal)?],
140            )
141        }
142
143        /// Voltage angle at one bus terminal, radians.
144        #[must_use]
145        pub fn terminal_voltage_angle(&self, bus: &str, terminal: &str) -> Option<f64> {
146            Some(
147                self.terminal_voltage_angle
148                    [terminal_position(self.terminal_index(), bus, terminal)?],
149            )
150        }
151
152        /// Current into the network at one bus terminal, amperes, when the
153        /// producer reported currents.
154        #[must_use]
155        pub fn terminal_current_magnitude(&self, bus: &str, terminal: &str) -> Option<f64> {
156            let values = self.terminal_current_magnitude.as_ref()?;
157            Some(values[terminal_position(self.terminal_index(), bus, terminal)?])
158        }
159
160        /// Active power into the network at one bus terminal, watts, when
161        /// the producer reported terminal powers.
162        #[must_use]
163        pub fn terminal_active_power(&self, bus: &str, terminal: &str) -> Option<f64> {
164            let values = self.terminal_active_power.as_ref()?;
165            Some(values[terminal_position(self.terminal_index(), bus, terminal)?])
166        }
167
168        /// Record per terminal current magnitudes, amperes.
169        ///
170        /// # Errors
171        /// A column whose length disagrees with the resolved terminals.
172        pub fn with_terminal_currents(mut self, values: Vec<f64>) -> Result<Self, Error> {
173            check_length(
174                "terminal current magnitudes",
175                values.len(),
176                terminal_count(self.network()),
177            )?;
178            self.terminal_current_magnitude = Some(values);
179            Ok(self)
180        }
181
182        /// Record per terminal active powers, watts.
183        ///
184        /// # Errors
185        /// A column whose length disagrees with the resolved terminals.
186        pub fn with_terminal_powers(mut self, values: Vec<f64>) -> Result<Self, Error> {
187            check_length(
188                "terminal active powers",
189                values.len(),
190                terminal_count(self.network()),
191            )?;
192            self.terminal_active_power = Some(values);
193            Ok(self)
194        }
195
196        /// Per source terminal active injections, watts, in source table
197        /// order with each source's terminal map order.
198        #[must_use]
199        pub fn source_active_injections(&self) -> &[f64] {
200            &self.source_active_injection
201        }
202    };
203}
204
205/// The multiconductor AC power flow solution: terminal complex voltages,
206/// optional terminal currents and powers, and source injections over the
207/// shared instance.
208#[derive(Clone, Debug)]
209pub struct McAcPfSolution {
210    instance: Arc<McAcPfInstance>,
211    termination: Termination,
212    residuals: Residuals,
213    producer: Producer,
214    terminal_voltage_magnitude: Vec<f64>,
215    terminal_voltage_angle: Vec<f64>,
216    terminal_current_magnitude: Option<Vec<f64>>,
217    terminal_active_power: Option<Vec<f64>>,
218    source_active_injection: Vec<f64>,
219    index: TerminalIndex,
220}
221
222impl McAcPfSolution {
223    /// Assemble the required results: per terminal voltage magnitudes
224    /// (volts) and angles (radians) in bus table and stated terminal order,
225    /// and per source terminal active injections (watts) in source table
226    /// order.
227    ///
228    /// # Errors
229    /// A column whose length disagrees with the instance's resolved
230    /// terminals.
231    pub fn new(
232        instance: Arc<McAcPfInstance>,
233        termination: Termination,
234        terminal_voltage_magnitude: Vec<f64>,
235        terminal_voltage_angle: Vec<f64>,
236        source_active_injection: Vec<f64>,
237    ) -> Result<Self, Error> {
238        let terminals = terminal_count(instance.network());
239        check_length(
240            "terminal voltage magnitudes",
241            terminal_voltage_magnitude.len(),
242            terminals,
243        )?;
244        check_length(
245            "terminal voltage angles",
246            terminal_voltage_angle.len(),
247            terminals,
248        )?;
249        check_length(
250            "source active injections",
251            source_active_injection.len(),
252            source_terminal_count(instance.network()),
253        )?;
254        let index = TerminalIndex::build(instance.network())?;
255        Ok(Self {
256            instance,
257            termination,
258            residuals: Residuals::default(),
259            producer: None,
260            terminal_voltage_magnitude,
261            terminal_voltage_angle,
262            terminal_current_magnitude: None,
263            terminal_active_power: None,
264            source_active_injection,
265            index,
266        })
267    }
268
269    shared_mc_solution_accessors!(McAcPfInstance);
270}
271
272/// The multiconductor AC optimal power flow solution: the power flow results
273/// plus per phase generator dispatch and the objective value.
274#[derive(Clone, Debug)]
275pub struct McAcOpfSolution {
276    instance: Arc<McAcOpfInstance>,
277    termination: Termination,
278    residuals: Residuals,
279    producer: Producer,
280    terminal_voltage_magnitude: Vec<f64>,
281    terminal_voltage_angle: Vec<f64>,
282    terminal_current_magnitude: Option<Vec<f64>>,
283    terminal_active_power: Option<Vec<f64>>,
284    source_active_injection: Vec<f64>,
285    /// Per generator, per phase active dispatch, generator table order with
286    /// each generator's terminal map order, watts.
287    generator_active_power: Vec<f64>,
288    objective: f64,
289    index: TerminalIndex,
290}
291
292impl McAcOpfSolution {
293    /// Assemble the results: the power flow columns plus the optimized per
294    /// phase generator dispatch (watts, generator table order with each
295    /// generator's terminal map order) and the objective value.
296    ///
297    /// # Errors
298    /// A column whose length disagrees with the instance's resolved
299    /// terminals or generator conductors.
300    pub fn new(
301        instance: Arc<McAcOpfInstance>,
302        termination: Termination,
303        terminal_voltage_magnitude: Vec<f64>,
304        terminal_voltage_angle: Vec<f64>,
305        source_active_injection: Vec<f64>,
306        generator_active_power: Vec<f64>,
307        objective: f64,
308    ) -> Result<Self, Error> {
309        let terminals = terminal_count(instance.network());
310        check_length(
311            "terminal voltage magnitudes",
312            terminal_voltage_magnitude.len(),
313            terminals,
314        )?;
315        check_length(
316            "terminal voltage angles",
317            terminal_voltage_angle.len(),
318            terminals,
319        )?;
320        check_length(
321            "source active injections",
322            source_active_injection.len(),
323            source_terminal_count(instance.network()),
324        )?;
325        let generator_conductors: usize = instance
326            .network()
327            .generators()
328            .iter()
329            .map(|generator| generator.terminal_map.len())
330            .sum();
331        check_length(
332            "per phase generator dispatch",
333            generator_active_power.len(),
334            generator_conductors,
335        )?;
336        let index = TerminalIndex::build(instance.network())?;
337        Ok(Self {
338            instance,
339            termination,
340            residuals: Residuals::default(),
341            producer: None,
342            terminal_voltage_magnitude,
343            terminal_voltage_angle,
344            terminal_current_magnitude: None,
345            terminal_active_power: None,
346            source_active_injection,
347            generator_active_power,
348            objective,
349            index,
350        })
351    }
352
353    shared_mc_solution_accessors!(McAcOpfInstance);
354
355    /// The optimized objective value.
356    #[must_use]
357    pub const fn objective(&self) -> f64 {
358        self.objective
359    }
360
361    /// The optimized per phase generator dispatch, watts, generator table
362    /// order with each generator's terminal map order.
363    #[must_use]
364    pub fn generator_active_powers(&self) -> &[f64] {
365        &self.generator_active_power
366    }
367}