Skip to main content

powerio_prob/solution/
balanced.rs

1//! The balanced solutions: `DcPfSolution`, `AcPfSolution`, `DcOpfSolution`,
2//! and `AcOpfSolution`.
3//!
4//! Values are stored in the shared network's table order and read back by
5//! stable identity: buses by [`BusId`], branches and generators by payload
6//! identity (`uid`, else `{table}:{row}`). Bus injections, bus voltages, and
7//! branch flows are required on the power flow solutions; individual
8//! generator outputs stay optional unless the instance determines them
9//! uniquely or the source records an explicit allocation, and the OPF
10//! solutions require the dispatch they optimized.
11
12use std::sync::Arc;
13
14use powerio_core::Error;
15use powerio_tx::{BalancedNetwork, BusId};
16
17use crate::diagnostics::codes;
18use crate::instance::{AcOpfInstance, AcPfInstance, DcOpfInstance, DcPfInstance};
19use crate::operating::row_identity;
20use crate::solution::{Producer, Residuals, Termination};
21
22/// Optional per generator dispatch, in generator table order.
23#[derive(Clone, Debug, Default, PartialEq)]
24#[non_exhaustive]
25pub struct GeneratorDispatch {
26    /// Active power per generator, MW.
27    pub p_mw: Vec<f64>,
28    /// Reactive power per generator, MVAr; empty for a DC result.
29    pub q_mvar: Vec<f64>,
30}
31
32/// Active and reactive power at the three terminals of one three winding
33/// transformer, in the transformer's declared winding order. Positive values
34/// enter the transformer.
35#[derive(Clone, Copy, Debug, Default, PartialEq)]
36#[non_exhaustive]
37pub struct ThreeWindingTransformerTerminalPower {
38    pub p_mw: [f64; 3],
39    pub q_mvar: [f64; 3],
40}
41
42impl ThreeWindingTransformerTerminalPower {
43    #[must_use]
44    pub const fn new(p_mw: [f64; 3], q_mvar: [f64; 3]) -> Self {
45        Self { p_mw, q_mvar }
46    }
47}
48
49/// Active power at the three terminals of one three winding transformer, in
50/// the transformer's declared winding order. Positive values enter the
51/// transformer.
52#[derive(Clone, Copy, Debug, Default, PartialEq)]
53#[non_exhaustive]
54pub struct ThreeWindingTransformerTerminalActivePower {
55    pub p_mw: [f64; 3],
56}
57
58impl ThreeWindingTransformerTerminalActivePower {
59    #[must_use]
60    pub const fn new(p_mw: [f64; 3]) -> Self {
61        Self { p_mw }
62    }
63}
64
65fn check_length(what: &'static str, got: usize, expected: usize) -> Result<(), Error> {
66    if got == expected {
67        Ok(())
68    } else {
69        Err(Error::new(
70            &codes::BUILD_SOLUTION_SHAPE_MISMATCH,
71            format!("{what} carries {got} values; the instance's table has {expected} rows"),
72        ))
73    }
74}
75
76fn check_nonnegative_multipliers(what: &'static str, values: &[f64]) -> Result<(), Error> {
77    if let Some((row, value)) = values
78        .iter()
79        .copied()
80        .enumerate()
81        .find(|(_, value)| !value.is_finite() || *value < 0.0)
82    {
83        return Err(Error::new(
84            &codes::BUILD_SOLUTION_MULTIPLIER_INVALID,
85            format!("{what} row {row} is {value}; multipliers must be finite and nonnegative"),
86        ));
87    }
88    Ok(())
89}
90
91/// Identity to row position over one network's tables, built once per
92/// solution on first keyed access so repeated reads never rescan a table.
93#[derive(Clone, Debug, Default)]
94struct SolutionIndex {
95    bus: std::collections::BTreeMap<BusId, usize>,
96    branch: std::collections::BTreeMap<String, usize>,
97    generator: std::collections::BTreeMap<String, usize>,
98}
99
100impl SolutionIndex {
101    fn build(network: &BalancedNetwork) -> Result<Self, Error> {
102        let mut index = Self::default();
103        for (row, bus) in network.buses().iter().enumerate() {
104            if index.bus.insert(bus.id, row).is_some() {
105                return Err(duplicate_identity("bus", &bus.id.to_string()));
106            }
107        }
108        for (row, branch) in network.branches().iter().enumerate() {
109            let identity = row_identity(branch.uid.as_deref(), "branches", row);
110            if index.branch.insert(identity.clone(), row).is_some() {
111                return Err(duplicate_identity("branch", &identity));
112            }
113        }
114        for (row, generator) in network.generators().iter().enumerate() {
115            let identity = row_identity(generator.uid.as_deref(), "generators", row);
116            if index.generator.insert(identity.clone(), row).is_some() {
117                return Err(duplicate_identity("generator", &identity));
118            }
119        }
120        Ok(index)
121    }
122}
123
124/// The identity index a solution constructor builds once, refusing a network
125/// whose resolved identities are not all distinct so every keyed accessor
126/// reads its own row.
127fn solution_index<I>(instance: &std::sync::Arc<I>) -> Result<SolutionIndex, Error>
128where
129    I: NetworkCarrier,
130{
131    SolutionIndex::build(instance.network())
132}
133
134/// The one thing solution_index needs from each instance type.
135trait NetworkCarrier {
136    fn network(&self) -> &BalancedNetwork;
137}
138
139impl NetworkCarrier for DcPfInstance {
140    fn network(&self) -> &BalancedNetwork {
141        DcPfInstance::network(self)
142    }
143}
144impl NetworkCarrier for AcPfInstance {
145    fn network(&self) -> &BalancedNetwork {
146        AcPfInstance::network(self)
147    }
148}
149impl NetworkCarrier for DcOpfInstance {
150    fn network(&self) -> &BalancedNetwork {
151        DcOpfInstance::network(self)
152    }
153}
154impl NetworkCarrier for AcOpfInstance {
155    fn network(&self) -> &BalancedNetwork {
156        AcOpfInstance::network(self)
157    }
158}
159
160fn duplicate_identity(kind: &str, identity: &str) -> Error {
161    Error::new(
162        &codes::BUILD_OPERATING_POINT_IDENTITY_UNKNOWN,
163        format!("{kind}: duplicate element identity `{identity}`"),
164    )
165}
166
167fn bus_position(index: &SolutionIndex, bus: BusId) -> Option<usize> {
168    index.bus.get(&bus).copied()
169}
170
171fn branch_position(index: &SolutionIndex, identity: &str) -> Option<usize> {
172    index.branch.get(identity).copied()
173}
174
175fn generator_position(index: &SolutionIndex, identity: &str) -> Option<usize> {
176    index.generator.get(identity).copied()
177}
178
179macro_rules! shared_solution_accessors {
180    ($instance_type:ty) => {
181        /// The immutable instance this solution solves. Borrowed; never a
182        /// copy.
183        #[must_use]
184        pub fn instance(&self) -> &$instance_type {
185            &self.instance
186        }
187
188        /// The shared instance owner, for another solution of the same
189        /// problem.
190        #[must_use]
191        pub fn shared_instance(&self) -> Arc<$instance_type> {
192            Arc::clone(&self.instance)
193        }
194
195        /// The network the solved instance calculates on.
196        #[must_use]
197        pub fn network(&self) -> &BalancedNetwork {
198            self.instance.network()
199        }
200
201        fn row_index(&self) -> &SolutionIndex {
202            &self.index
203        }
204
205        /// Bus IDs in the column order every bulk accessor uses.
206        #[must_use]
207        pub fn bus_order(&self) -> Vec<BusId> {
208            self.network().buses().iter().map(|bus| bus.id).collect()
209        }
210
211        /// Stable branch identities in bulk column order.
212        #[must_use]
213        pub fn branch_order(&self) -> Vec<String> {
214            self.network()
215                .branches()
216                .iter()
217                .enumerate()
218                .map(|(row, branch)| row_identity(branch.uid.as_deref(), "branches", row))
219                .collect()
220        }
221
222        /// Stable generator identities in bulk column order.
223        #[must_use]
224        pub fn generator_order(&self) -> Vec<String> {
225            self.network()
226                .generators()
227                .iter()
228                .enumerate()
229                .map(|(row, generator)| row_identity(generator.uid.as_deref(), "generators", row))
230                .collect()
231        }
232
233        /// How the producing calculation ended.
234        #[must_use]
235        pub fn termination(&self) -> &Termination {
236            &self.termination
237        }
238
239        /// The reported numerical residuals.
240        #[must_use]
241        pub fn residuals(&self) -> &Residuals {
242            &self.residuals
243        }
244
245        /// The producer or solver identity, when recorded.
246        #[must_use]
247        pub fn producer(&self) -> Option<&str> {
248            self.producer.as_deref()
249        }
250
251        /// Record the producer identity.
252        #[must_use]
253        pub fn with_producer(mut self, producer: impl Into<String>) -> Self {
254            self.producer = Some(producer.into());
255            self
256        }
257
258        /// Record the numerical residuals.
259        #[must_use]
260        pub fn with_residuals(mut self, residuals: Residuals) -> Self {
261            self.residuals = residuals;
262            self
263        }
264
265        /// The branch identities the flow columns follow, in table order.
266        pub fn branch_identity_order(&self) -> impl Iterator<Item = String> + '_ {
267            self.network()
268                .branches()
269                .iter()
270                .enumerate()
271                .map(|(row, branch)| row_identity(branch.uid.as_deref(), "branches", row))
272        }
273    };
274}
275
276macro_rules! optional_dispatch_accessors {
277    () => {
278        /// Per generator dispatch, when the instance determines it uniquely
279        /// or the source records an explicit allocation.
280        #[must_use]
281        pub fn generator_dispatch(&self) -> Option<&GeneratorDispatch> {
282            self.generator_dispatch.as_ref()
283        }
284
285        /// Record an explicit per generator allocation.
286        ///
287        /// # Errors
288        /// A dispatch whose length disagrees with the generator table.
289        pub fn with_generator_dispatch(
290            mut self,
291            dispatch: GeneratorDispatch,
292        ) -> Result<Self, Error> {
293            check_length(
294                "generator dispatch",
295                dispatch.p_mw.len(),
296                self.network().generators().len(),
297            )?;
298            if !dispatch.q_mvar.is_empty() {
299                check_length(
300                    "generator reactive dispatch",
301                    dispatch.q_mvar.len(),
302                    self.network().generators().len(),
303                )?;
304            }
305            self.generator_dispatch = Some(dispatch);
306            Ok(self)
307        }
308    };
309}
310
311/// The DC power flow solution: bus angles and injections and branch terminal
312/// active flows over the shared instance.
313#[derive(Clone, Debug)]
314pub struct DcPfSolution {
315    instance: Arc<DcPfInstance>,
316    termination: Termination,
317    residuals: Residuals,
318    producer: Producer,
319    bus_voltage_angle: Vec<f64>,
320    bus_active_injection: Vec<f64>,
321    branch_from_active_flow: Vec<f64>,
322    branch_to_active_flow: Vec<f64>,
323    three_winding_transformer_terminal_active_power:
324        Vec<ThreeWindingTransformerTerminalActivePower>,
325    generator_dispatch: Option<GeneratorDispatch>,
326    index: SolutionIndex,
327}
328
329impl DcPfSolution {
330    /// Assemble the required results: per bus voltage angles (degrees) and
331    /// net active injections (MW) in bus table order, and per branch terminal
332    /// active flows (MW, into the branch at each terminal) in branch table
333    /// order.
334    ///
335    /// # Errors
336    /// A column whose length disagrees with the instance's tables.
337    pub fn new(
338        instance: Arc<DcPfInstance>,
339        termination: Termination,
340        bus_voltage_angle: Vec<f64>,
341        bus_active_injection: Vec<f64>,
342        branch_from_active_flow: Vec<f64>,
343        branch_to_active_flow: Vec<f64>,
344        three_winding_transformer_terminal_active_power: Vec<
345            ThreeWindingTransformerTerminalActivePower,
346        >,
347    ) -> Result<Self, Error> {
348        let buses = instance.network().buses().len();
349        let branches = instance.network().branches().len();
350        check_length("bus voltage angles", bus_voltage_angle.len(), buses)?;
351        check_length("bus active injections", bus_active_injection.len(), buses)?;
352        check_length(
353            "branch from-side flows",
354            branch_from_active_flow.len(),
355            branches,
356        )?;
357        check_length(
358            "branch to-side flows",
359            branch_to_active_flow.len(),
360            branches,
361        )?;
362        check_length(
363            "three winding transformer terminal active powers",
364            three_winding_transformer_terminal_active_power.len(),
365            instance.network().transformers_3w().len(),
366        )?;
367        let index = solution_index(&instance)?;
368        Ok(Self {
369            instance,
370            termination,
371            residuals: Residuals::default(),
372            producer: None,
373            bus_voltage_angle,
374            bus_active_injection,
375            branch_from_active_flow,
376            branch_to_active_flow,
377            three_winding_transformer_terminal_active_power,
378            generator_dispatch: None,
379            index,
380        })
381    }
382
383    shared_solution_accessors!(DcPfInstance);
384    optional_dispatch_accessors!();
385
386    /// Voltage angle at one bus, degrees.
387    #[must_use]
388    pub fn bus_voltage_angle(&self, bus: BusId) -> Option<f64> {
389        Some(self.bus_voltage_angle[bus_position(self.row_index(), bus)?])
390    }
391
392    /// Net active injection at one bus, MW.
393    #[must_use]
394    pub fn bus_active_injection(&self, bus: BusId) -> Option<f64> {
395        Some(self.bus_active_injection[bus_position(self.row_index(), bus)?])
396    }
397
398    /// Active flow into the branch at its from terminal, MW, by stable
399    /// branch identity.
400    #[must_use]
401    pub fn branch_from_active_flow(&self, identity: &str) -> Option<f64> {
402        Some(self.branch_from_active_flow[branch_position(self.row_index(), identity)?])
403    }
404
405    /// Active flow into the branch at its to terminal, MW.
406    #[must_use]
407    pub fn branch_to_active_flow(&self, identity: &str) -> Option<f64> {
408        Some(self.branch_to_active_flow[branch_position(self.row_index(), identity)?])
409    }
410
411    /// Three winding transformer terminal active powers in transformer table
412    /// order.
413    #[must_use]
414    pub fn three_winding_transformer_terminal_active_powers(
415        &self,
416    ) -> &[ThreeWindingTransformerTerminalActivePower] {
417        &self.three_winding_transformer_terminal_active_power
418    }
419
420    /// The complete `bus_voltage_angle` column, in `bus_order`.
421    #[must_use]
422    pub fn bus_voltage_angles(&self) -> &[f64] {
423        &self.bus_voltage_angle
424    }
425    /// The complete `bus_active_injection` column, in `bus_order`.
426    #[must_use]
427    pub fn bus_active_injections(&self) -> &[f64] {
428        &self.bus_active_injection
429    }
430    /// The complete `branch_from_active_flow` column, in `branch_order`.
431    #[must_use]
432    pub fn branch_from_active_flows(&self) -> &[f64] {
433        &self.branch_from_active_flow
434    }
435    /// The complete `branch_to_active_flow` column, in `branch_order`.
436    #[must_use]
437    pub fn branch_to_active_flows(&self) -> &[f64] {
438        &self.branch_to_active_flow
439    }
440}
441
442/// The AC power flow solution: complex bus voltages, active and reactive bus
443/// injections, and terminal branch flows over the shared instance.
444#[derive(Clone, Debug)]
445pub struct AcPfSolution {
446    instance: Arc<AcPfInstance>,
447    termination: Termination,
448    residuals: Residuals,
449    producer: Producer,
450    bus_voltage_magnitude: Vec<f64>,
451    bus_voltage_angle: Vec<f64>,
452    bus_active_injection: Vec<f64>,
453    bus_reactive_injection: Vec<f64>,
454    branch_from_active_flow: Vec<f64>,
455    branch_from_reactive_flow: Vec<f64>,
456    branch_to_active_flow: Vec<f64>,
457    branch_to_reactive_flow: Vec<f64>,
458    three_winding_transformer_terminal_power: Vec<ThreeWindingTransformerTerminalPower>,
459    generator_dispatch: Option<GeneratorDispatch>,
460    index: SolutionIndex,
461}
462
463impl AcPfSolution {
464    /// Assemble the required results: per bus voltage magnitudes (per unit)
465    /// and angles (degrees), net injections (MW, MVAr) in bus table order,
466    /// and per branch terminal flows (MW, MVAr into the branch at each
467    /// terminal) in branch table order.
468    ///
469    /// # Errors
470    /// A column whose length disagrees with the instance's tables.
471    #[allow(clippy::too_many_arguments)] // the required result set is the signature
472    pub fn new(
473        instance: Arc<AcPfInstance>,
474        termination: Termination,
475        bus_voltage_magnitude: Vec<f64>,
476        bus_voltage_angle: Vec<f64>,
477        bus_active_injection: Vec<f64>,
478        bus_reactive_injection: Vec<f64>,
479        branch_from_active_flow: Vec<f64>,
480        branch_from_reactive_flow: Vec<f64>,
481        branch_to_active_flow: Vec<f64>,
482        branch_to_reactive_flow: Vec<f64>,
483        three_winding_transformer_terminal_power: Vec<ThreeWindingTransformerTerminalPower>,
484    ) -> Result<Self, Error> {
485        let buses = instance.network().buses().len();
486        let branches = instance.network().branches().len();
487        check_length("bus voltage magnitudes", bus_voltage_magnitude.len(), buses)?;
488        check_length("bus voltage angles", bus_voltage_angle.len(), buses)?;
489        check_length("bus active injections", bus_active_injection.len(), buses)?;
490        check_length(
491            "bus reactive injections",
492            bus_reactive_injection.len(),
493            buses,
494        )?;
495        check_length(
496            "branch from-side active flows",
497            branch_from_active_flow.len(),
498            branches,
499        )?;
500        check_length(
501            "branch from-side reactive flows",
502            branch_from_reactive_flow.len(),
503            branches,
504        )?;
505        check_length(
506            "branch to-side active flows",
507            branch_to_active_flow.len(),
508            branches,
509        )?;
510        check_length(
511            "branch to-side reactive flows",
512            branch_to_reactive_flow.len(),
513            branches,
514        )?;
515        check_length(
516            "three winding transformer terminal powers",
517            three_winding_transformer_terminal_power.len(),
518            instance.network().transformers_3w().len(),
519        )?;
520        let index = solution_index(&instance)?;
521        Ok(Self {
522            instance,
523            termination,
524            residuals: Residuals::default(),
525            producer: None,
526            bus_voltage_magnitude,
527            bus_voltage_angle,
528            bus_active_injection,
529            bus_reactive_injection,
530            branch_from_active_flow,
531            branch_from_reactive_flow,
532            branch_to_active_flow,
533            branch_to_reactive_flow,
534            three_winding_transformer_terminal_power,
535            generator_dispatch: None,
536            index,
537        })
538    }
539
540    shared_solution_accessors!(AcPfInstance);
541    optional_dispatch_accessors!();
542
543    /// Voltage magnitude at one bus, per unit.
544    #[must_use]
545    pub fn bus_voltage_magnitude(&self, bus: BusId) -> Option<f64> {
546        Some(self.bus_voltage_magnitude[bus_position(self.row_index(), bus)?])
547    }
548
549    /// Voltage angle at one bus, degrees.
550    #[must_use]
551    pub fn bus_voltage_angle(&self, bus: BusId) -> Option<f64> {
552        Some(self.bus_voltage_angle[bus_position(self.row_index(), bus)?])
553    }
554
555    /// Net active injection at one bus, MW.
556    #[must_use]
557    pub fn bus_active_injection(&self, bus: BusId) -> Option<f64> {
558        Some(self.bus_active_injection[bus_position(self.row_index(), bus)?])
559    }
560
561    /// Net reactive injection at one bus, MVAr.
562    #[must_use]
563    pub fn bus_reactive_injection(&self, bus: BusId) -> Option<f64> {
564        Some(self.bus_reactive_injection[bus_position(self.row_index(), bus)?])
565    }
566
567    /// Active flow into the branch at its from terminal, MW, by stable
568    /// branch identity.
569    #[must_use]
570    pub fn branch_from_active_flow(&self, identity: &str) -> Option<f64> {
571        Some(self.branch_from_active_flow[branch_position(self.row_index(), identity)?])
572    }
573
574    /// Reactive flow into the branch at its from terminal, MVAr.
575    #[must_use]
576    pub fn branch_from_reactive_flow(&self, identity: &str) -> Option<f64> {
577        Some(self.branch_from_reactive_flow[branch_position(self.row_index(), identity)?])
578    }
579
580    /// Active flow into the branch at its to terminal, MW.
581    #[must_use]
582    pub fn branch_to_active_flow(&self, identity: &str) -> Option<f64> {
583        Some(self.branch_to_active_flow[branch_position(self.row_index(), identity)?])
584    }
585
586    /// Reactive flow into the branch at its to terminal, MVAr.
587    #[must_use]
588    pub fn branch_to_reactive_flow(&self, identity: &str) -> Option<f64> {
589        Some(self.branch_to_reactive_flow[branch_position(self.row_index(), identity)?])
590    }
591
592    /// Three winding transformer terminal powers in transformer table order.
593    #[must_use]
594    pub fn three_winding_transformer_terminal_powers(
595        &self,
596    ) -> &[ThreeWindingTransformerTerminalPower] {
597        &self.three_winding_transformer_terminal_power
598    }
599}
600
601/// The DC optimal power flow solution: the DC power flow results plus the
602/// optimized generator active dispatch, the objective value, and the
603/// optional economic outputs an optimizing producer can attach.
604#[derive(Clone, Debug)]
605pub struct DcOpfSolution {
606    instance: Arc<DcOpfInstance>,
607    termination: Termination,
608    residuals: Residuals,
609    producer: Producer,
610    bus_voltage_angle: Vec<f64>,
611    bus_active_injection: Vec<f64>,
612    branch_from_active_flow: Vec<f64>,
613    branch_to_active_flow: Vec<f64>,
614    generator_active_power: Vec<f64>,
615    three_winding_transformer_terminal_active_power:
616        Vec<ThreeWindingTransformerTerminalActivePower>,
617    objective: f64,
618    bus_active_power_marginal: Option<Vec<f64>>,
619    branch_from_limit_multiplier: Option<Vec<f64>>,
620    branch_to_limit_multiplier: Option<Vec<f64>>,
621    index: SolutionIndex,
622}
623
624impl DcOpfSolution {
625    /// Assemble the results: the DC power flow columns, the optimized per
626    /// generator active dispatch (MW, generator table order), and the
627    /// objective value.
628    ///
629    /// # Errors
630    /// A column whose length disagrees with the instance's tables.
631    #[allow(clippy::too_many_arguments)] // the required result set is the signature
632    pub fn new(
633        instance: Arc<DcOpfInstance>,
634        termination: Termination,
635        bus_voltage_angle: Vec<f64>,
636        bus_active_injection: Vec<f64>,
637        branch_from_active_flow: Vec<f64>,
638        branch_to_active_flow: Vec<f64>,
639        generator_active_power: Vec<f64>,
640        objective: f64,
641        three_winding_transformer_terminal_active_power: Vec<
642            ThreeWindingTransformerTerminalActivePower,
643        >,
644    ) -> Result<Self, Error> {
645        let buses = instance.network().buses().len();
646        let branches = instance.network().branches().len();
647        check_length("bus voltage angles", bus_voltage_angle.len(), buses)?;
648        check_length("bus active injections", bus_active_injection.len(), buses)?;
649        check_length(
650            "branch from-side flows",
651            branch_from_active_flow.len(),
652            branches,
653        )?;
654        check_length(
655            "branch to-side flows",
656            branch_to_active_flow.len(),
657            branches,
658        )?;
659        check_length(
660            "generator active dispatch",
661            generator_active_power.len(),
662            instance.network().generators().len(),
663        )?;
664        check_length(
665            "three winding transformer terminal active powers",
666            three_winding_transformer_terminal_active_power.len(),
667            instance.network().transformers_3w().len(),
668        )?;
669        let index = solution_index(&instance)?;
670        Ok(Self {
671            instance,
672            termination,
673            residuals: Residuals::default(),
674            producer: None,
675            bus_voltage_angle,
676            bus_active_injection,
677            branch_from_active_flow,
678            branch_to_active_flow,
679            generator_active_power,
680            three_winding_transformer_terminal_active_power,
681            objective,
682            bus_active_power_marginal: None,
683            branch_from_limit_multiplier: None,
684            branch_to_limit_multiplier: None,
685            index,
686        })
687    }
688
689    shared_solution_accessors!(DcOpfInstance);
690
691    /// Attach the derivative of the optimal objective with respect to added
692    /// active demand at each bus, in objective units per MW and bus table
693    /// order. A network generator cost objective gives the usual active power
694    /// locational marginal price; a different objective does not imply money.
695    ///
696    /// # Errors
697    /// A column whose length disagrees with the instance's bus table.
698    pub fn with_bus_active_power_marginals(mut self, marginals: Vec<f64>) -> Result<Self, Error> {
699        check_length(
700            "bus active power marginals",
701            marginals.len(),
702            self.instance.network().buses().len(),
703        )?;
704        self.bus_active_power_marginal = Some(marginals);
705        Ok(self)
706    }
707
708    /// Attach the two nonnegative KKT multipliers for every branch thermal
709    /// bound, in objective units per MW and branch table order. `from` is the
710    /// multiplier on `flow <= rating`; `to` is the multiplier on
711    /// `-flow <= rating`. Increasing one symmetric rating by one MW changes
712    /// the local optimal objective by the negative sum of the two values. A
713    /// branch absent from the DC calculation carries zero in both columns.
714    ///
715    /// # Errors
716    /// A column whose length disagrees with the instance's branch table.
717    pub fn with_branch_thermal_limit_multipliers(
718        mut self,
719        from: Vec<f64>,
720        to: Vec<f64>,
721    ) -> Result<Self, Error> {
722        check_length(
723            "branch from-side thermal limit multipliers",
724            from.len(),
725            self.instance.network().branches().len(),
726        )?;
727        check_length(
728            "branch to-side thermal limit multipliers",
729            to.len(),
730            self.instance.network().branches().len(),
731        )?;
732        check_nonnegative_multipliers("branch from-side thermal limit multipliers", &from)?;
733        check_nonnegative_multipliers("branch to-side thermal limit multipliers", &to)?;
734        self.branch_from_limit_multiplier = Some(from);
735        self.branch_to_limit_multiplier = Some(to);
736        Ok(self)
737    }
738
739    /// The optimized objective value.
740    #[must_use]
741    pub const fn objective(&self) -> f64 {
742        self.objective
743    }
744
745    /// Optimized active power of one generator, MW, by stable identity.
746    #[must_use]
747    pub fn generator_active_power(&self, identity: &str) -> Option<f64> {
748        Some(self.generator_active_power[generator_position(self.row_index(), identity)?])
749    }
750
751    /// Optimal objective derivative per added MW of active demand at one bus.
752    #[must_use]
753    pub fn bus_active_power_marginal(&self, bus: BusId) -> Option<f64> {
754        Some(self.bus_active_power_marginal.as_ref()?[bus_position(self.row_index(), bus)?])
755    }
756
757    /// From-side thermal bound multiplier by stable branch identity.
758    #[must_use]
759    pub fn branch_from_limit_multiplier(&self, identity: &str) -> Option<f64> {
760        Some(
761            self.branch_from_limit_multiplier.as_ref()?
762                [branch_position(self.row_index(), identity)?],
763        )
764    }
765
766    /// To-side thermal bound multiplier by stable branch identity.
767    #[must_use]
768    pub fn branch_to_limit_multiplier(&self, identity: &str) -> Option<f64> {
769        Some(
770            self.branch_to_limit_multiplier.as_ref()?[branch_position(self.row_index(), identity)?],
771        )
772    }
773
774    /// All active demand marginals in bus table order, when attached.
775    #[must_use]
776    pub fn bus_active_power_marginals(&self) -> Option<&[f64]> {
777        self.bus_active_power_marginal.as_deref()
778    }
779
780    /// All from-side thermal bound multipliers in branch table order.
781    #[must_use]
782    pub fn branch_from_limit_multipliers(&self) -> Option<&[f64]> {
783        self.branch_from_limit_multiplier.as_deref()
784    }
785
786    /// All to-side thermal bound multipliers in branch table order.
787    #[must_use]
788    pub fn branch_to_limit_multipliers(&self) -> Option<&[f64]> {
789        self.branch_to_limit_multiplier.as_deref()
790    }
791
792    /// Voltage angle at one bus, degrees.
793    #[must_use]
794    pub fn bus_voltage_angle(&self, bus: BusId) -> Option<f64> {
795        Some(self.bus_voltage_angle[bus_position(self.row_index(), bus)?])
796    }
797
798    /// Net active injection at one bus, MW.
799    #[must_use]
800    pub fn bus_active_injection(&self, bus: BusId) -> Option<f64> {
801        Some(self.bus_active_injection[bus_position(self.row_index(), bus)?])
802    }
803
804    /// Active flow into the branch at its from terminal, MW.
805    #[must_use]
806    pub fn branch_from_active_flow(&self, identity: &str) -> Option<f64> {
807        Some(self.branch_from_active_flow[branch_position(self.row_index(), identity)?])
808    }
809
810    /// Active flow into the branch at its to terminal, MW.
811    #[must_use]
812    pub fn branch_to_active_flow(&self, identity: &str) -> Option<f64> {
813        Some(self.branch_to_active_flow[branch_position(self.row_index(), identity)?])
814    }
815
816    /// Three winding transformer terminal active powers in transformer table
817    /// order.
818    #[must_use]
819    pub fn three_winding_transformer_terminal_active_powers(
820        &self,
821    ) -> &[ThreeWindingTransformerTerminalActivePower] {
822        &self.three_winding_transformer_terminal_active_power
823    }
824}
825
826/// The AC optimal power flow solution: the AC power flow results plus the
827/// optimized generator active and reactive dispatch and the objective value.
828#[derive(Clone, Debug)]
829pub struct AcOpfSolution {
830    instance: Arc<AcOpfInstance>,
831    termination: Termination,
832    residuals: Residuals,
833    producer: Producer,
834    bus_voltage_magnitude: Vec<f64>,
835    bus_voltage_angle: Vec<f64>,
836    bus_active_injection: Vec<f64>,
837    bus_reactive_injection: Vec<f64>,
838    branch_from_active_flow: Vec<f64>,
839    branch_from_reactive_flow: Vec<f64>,
840    branch_to_active_flow: Vec<f64>,
841    branch_to_reactive_flow: Vec<f64>,
842    generator_active_power: Vec<f64>,
843    generator_reactive_power: Vec<f64>,
844    three_winding_transformer_terminal_power: Vec<ThreeWindingTransformerTerminalPower>,
845    objective: f64,
846    bus_active_power_marginal: Option<Vec<f64>>,
847    bus_reactive_power_marginal: Option<Vec<f64>>,
848    branch_from_limit_multiplier: Option<Vec<f64>>,
849    branch_to_limit_multiplier: Option<Vec<f64>>,
850    index: SolutionIndex,
851}
852
853impl AcOpfSolution {
854    /// Assemble the results: the AC power flow columns, the optimized per
855    /// generator dispatch (MW and MVAr, generator table order), and the
856    /// objective value.
857    ///
858    /// # Errors
859    /// A column whose length disagrees with the instance's tables.
860    #[allow(clippy::too_many_arguments)] // the required result set is the signature
861    pub fn new(
862        instance: Arc<AcOpfInstance>,
863        termination: Termination,
864        bus_voltage_magnitude: Vec<f64>,
865        bus_voltage_angle: Vec<f64>,
866        bus_active_injection: Vec<f64>,
867        bus_reactive_injection: Vec<f64>,
868        branch_from_active_flow: Vec<f64>,
869        branch_from_reactive_flow: Vec<f64>,
870        branch_to_active_flow: Vec<f64>,
871        branch_to_reactive_flow: Vec<f64>,
872        generator_active_power: Vec<f64>,
873        generator_reactive_power: Vec<f64>,
874        objective: f64,
875        three_winding_transformer_terminal_power: Vec<ThreeWindingTransformerTerminalPower>,
876    ) -> Result<Self, Error> {
877        let buses = instance.network().buses().len();
878        let branches = instance.network().branches().len();
879        let generators = instance.network().generators().len();
880        check_length("bus voltage magnitudes", bus_voltage_magnitude.len(), buses)?;
881        check_length("bus voltage angles", bus_voltage_angle.len(), buses)?;
882        check_length("bus active injections", bus_active_injection.len(), buses)?;
883        check_length(
884            "bus reactive injections",
885            bus_reactive_injection.len(),
886            buses,
887        )?;
888        check_length(
889            "branch from-side active flows",
890            branch_from_active_flow.len(),
891            branches,
892        )?;
893        check_length(
894            "branch from-side reactive flows",
895            branch_from_reactive_flow.len(),
896            branches,
897        )?;
898        check_length(
899            "branch to-side active flows",
900            branch_to_active_flow.len(),
901            branches,
902        )?;
903        check_length(
904            "branch to-side reactive flows",
905            branch_to_reactive_flow.len(),
906            branches,
907        )?;
908        check_length(
909            "generator active dispatch",
910            generator_active_power.len(),
911            generators,
912        )?;
913        check_length(
914            "generator reactive dispatch",
915            generator_reactive_power.len(),
916            generators,
917        )?;
918        check_length(
919            "three winding transformer terminal powers",
920            three_winding_transformer_terminal_power.len(),
921            instance.network().transformers_3w().len(),
922        )?;
923        let index = solution_index(&instance)?;
924        Ok(Self {
925            instance,
926            termination,
927            residuals: Residuals::default(),
928            producer: None,
929            bus_voltage_magnitude,
930            bus_voltage_angle,
931            bus_active_injection,
932            bus_reactive_injection,
933            branch_from_active_flow,
934            branch_from_reactive_flow,
935            branch_to_active_flow,
936            branch_to_reactive_flow,
937            generator_active_power,
938            generator_reactive_power,
939            three_winding_transformer_terminal_power,
940            objective,
941            bus_active_power_marginal: None,
942            bus_reactive_power_marginal: None,
943            branch_from_limit_multiplier: None,
944            branch_to_limit_multiplier: None,
945            index,
946        })
947    }
948
949    shared_solution_accessors!(AcOpfInstance);
950
951    /// Attach the derivative of the optimal objective with respect to added
952    /// active demand, in objective units per MW and bus table order.
953    ///
954    /// # Errors
955    /// A column whose length disagrees with the instance's bus table.
956    pub fn with_bus_active_power_marginals(mut self, marginals: Vec<f64>) -> Result<Self, Error> {
957        check_length(
958            "bus active power marginals",
959            marginals.len(),
960            self.instance.network().buses().len(),
961        )?;
962        self.bus_active_power_marginal = Some(marginals);
963        Ok(self)
964    }
965
966    /// Attach the derivative of the optimal objective with respect to added
967    /// reactive demand, in objective units per MVAr and bus table order.
968    ///
969    /// # Errors
970    /// A column whose length disagrees with the instance's bus table.
971    pub fn with_bus_reactive_power_marginals(mut self, marginals: Vec<f64>) -> Result<Self, Error> {
972        check_length(
973            "bus reactive power marginals",
974            marginals.len(),
975            self.instance.network().buses().len(),
976        )?;
977        self.bus_reactive_power_marginal = Some(marginals);
978        Ok(self)
979    }
980
981    /// Attach the two nonnegative apparent power limit multipliers, in
982    /// objective units per MVA and branch table order. Increasing the shared
983    /// rating by one MVA changes the local optimal objective by the negative
984    /// sum of the from and to terminal multipliers.
985    pub fn with_branch_thermal_limit_multipliers(
986        mut self,
987        from: Vec<f64>,
988        to: Vec<f64>,
989    ) -> Result<Self, Error> {
990        check_length(
991            "branch from-terminal thermal limit multipliers",
992            from.len(),
993            self.instance.network().branches().len(),
994        )?;
995        check_length(
996            "branch to-terminal thermal limit multipliers",
997            to.len(),
998            self.instance.network().branches().len(),
999        )?;
1000        check_nonnegative_multipliers("branch from-terminal thermal limit multipliers", &from)?;
1001        check_nonnegative_multipliers("branch to-terminal thermal limit multipliers", &to)?;
1002        self.branch_from_limit_multiplier = Some(from);
1003        self.branch_to_limit_multiplier = Some(to);
1004        Ok(self)
1005    }
1006
1007    /// The optimized objective value.
1008    #[must_use]
1009    pub const fn objective(&self) -> f64 {
1010        self.objective
1011    }
1012
1013    /// Optimal objective derivative per added MW of active demand at one bus.
1014    #[must_use]
1015    pub fn bus_active_power_marginal(&self, bus: BusId) -> Option<f64> {
1016        Some(self.bus_active_power_marginal.as_ref()?[bus_position(self.row_index(), bus)?])
1017    }
1018
1019    /// Optimal objective derivative per added MVAr of reactive demand.
1020    #[must_use]
1021    pub fn bus_reactive_power_marginal(&self, bus: BusId) -> Option<f64> {
1022        Some(self.bus_reactive_power_marginal.as_ref()?[bus_position(self.row_index(), bus)?])
1023    }
1024
1025    /// From-terminal apparent power bound multiplier by branch identity.
1026    #[must_use]
1027    pub fn branch_from_limit_multiplier(&self, identity: &str) -> Option<f64> {
1028        Some(
1029            self.branch_from_limit_multiplier.as_ref()?
1030                [branch_position(self.row_index(), identity)?],
1031        )
1032    }
1033
1034    /// To-terminal apparent power bound multiplier by branch identity.
1035    #[must_use]
1036    pub fn branch_to_limit_multiplier(&self, identity: &str) -> Option<f64> {
1037        Some(
1038            self.branch_to_limit_multiplier.as_ref()?[branch_position(self.row_index(), identity)?],
1039        )
1040    }
1041
1042    /// All active demand marginals in bus table order, when attached.
1043    #[must_use]
1044    pub fn bus_active_power_marginals(&self) -> Option<&[f64]> {
1045        self.bus_active_power_marginal.as_deref()
1046    }
1047
1048    /// All reactive demand marginals in bus table order, when attached.
1049    #[must_use]
1050    pub fn bus_reactive_power_marginals(&self) -> Option<&[f64]> {
1051        self.bus_reactive_power_marginal.as_deref()
1052    }
1053
1054    /// All from-terminal thermal bound multipliers in branch table order.
1055    #[must_use]
1056    pub fn branch_from_limit_multipliers(&self) -> Option<&[f64]> {
1057        self.branch_from_limit_multiplier.as_deref()
1058    }
1059
1060    /// All to-terminal thermal bound multipliers in branch table order.
1061    #[must_use]
1062    pub fn branch_to_limit_multipliers(&self) -> Option<&[f64]> {
1063        self.branch_to_limit_multiplier.as_deref()
1064    }
1065
1066    /// Optimized active power of one generator, MW, by stable identity.
1067    #[must_use]
1068    pub fn generator_active_power(&self, identity: &str) -> Option<f64> {
1069        Some(self.generator_active_power[generator_position(self.row_index(), identity)?])
1070    }
1071
1072    /// Optimized reactive power of one generator, MVAr.
1073    #[must_use]
1074    pub fn generator_reactive_power(&self, identity: &str) -> Option<f64> {
1075        Some(self.generator_reactive_power[generator_position(self.row_index(), identity)?])
1076    }
1077
1078    /// Voltage magnitude at one bus, per unit.
1079    #[must_use]
1080    pub fn bus_voltage_magnitude(&self, bus: BusId) -> Option<f64> {
1081        Some(self.bus_voltage_magnitude[bus_position(self.row_index(), bus)?])
1082    }
1083
1084    /// Voltage angle at one bus, degrees.
1085    #[must_use]
1086    pub fn bus_voltage_angle(&self, bus: BusId) -> Option<f64> {
1087        Some(self.bus_voltage_angle[bus_position(self.row_index(), bus)?])
1088    }
1089
1090    /// Net active injection at one bus, MW.
1091    #[must_use]
1092    pub fn bus_active_injection(&self, bus: BusId) -> Option<f64> {
1093        Some(self.bus_active_injection[bus_position(self.row_index(), bus)?])
1094    }
1095
1096    /// Net reactive injection at one bus, MVAr.
1097    #[must_use]
1098    pub fn bus_reactive_injection(&self, bus: BusId) -> Option<f64> {
1099        Some(self.bus_reactive_injection[bus_position(self.row_index(), bus)?])
1100    }
1101
1102    /// Active flow into the branch at its from terminal, MW.
1103    #[must_use]
1104    pub fn branch_from_active_flow(&self, identity: &str) -> Option<f64> {
1105        Some(self.branch_from_active_flow[branch_position(self.row_index(), identity)?])
1106    }
1107
1108    /// Reactive flow into the branch at its from terminal, MVAr.
1109    #[must_use]
1110    pub fn branch_from_reactive_flow(&self, identity: &str) -> Option<f64> {
1111        Some(self.branch_from_reactive_flow[branch_position(self.row_index(), identity)?])
1112    }
1113
1114    /// Active flow into the branch at its to terminal, MW.
1115    #[must_use]
1116    pub fn branch_to_active_flow(&self, identity: &str) -> Option<f64> {
1117        Some(self.branch_to_active_flow[branch_position(self.row_index(), identity)?])
1118    }
1119
1120    /// Reactive flow into the branch at its to terminal, MVAr.
1121    #[must_use]
1122    pub fn branch_to_reactive_flow(&self, identity: &str) -> Option<f64> {
1123        Some(self.branch_to_reactive_flow[branch_position(self.row_index(), identity)?])
1124    }
1125
1126    /// Three winding transformer terminal powers in transformer table order.
1127    #[must_use]
1128    pub fn three_winding_transformer_terminal_powers(
1129        &self,
1130    ) -> &[ThreeWindingTransformerTerminalPower] {
1131        &self.three_winding_transformer_terminal_power
1132    }
1133
1134    /// The complete `bus_voltage_magnitude` column, in `bus_order`.
1135    #[must_use]
1136    pub fn bus_voltage_magnitudes(&self) -> &[f64] {
1137        &self.bus_voltage_magnitude
1138    }
1139    /// The complete `bus_voltage_angle` column, in `bus_order`.
1140    #[must_use]
1141    pub fn bus_voltage_angles(&self) -> &[f64] {
1142        &self.bus_voltage_angle
1143    }
1144    /// The complete `bus_active_injection` column, in `bus_order`.
1145    #[must_use]
1146    pub fn bus_active_injections(&self) -> &[f64] {
1147        &self.bus_active_injection
1148    }
1149    /// The complete `bus_reactive_injection` column, in `bus_order`.
1150    #[must_use]
1151    pub fn bus_reactive_injections(&self) -> &[f64] {
1152        &self.bus_reactive_injection
1153    }
1154    /// The complete `branch_from_active_flow` column, in `branch_order`.
1155    #[must_use]
1156    pub fn branch_from_active_flows(&self) -> &[f64] {
1157        &self.branch_from_active_flow
1158    }
1159    /// The complete `branch_from_reactive_flow` column, in `branch_order`.
1160    #[must_use]
1161    pub fn branch_from_reactive_flows(&self) -> &[f64] {
1162        &self.branch_from_reactive_flow
1163    }
1164    /// The complete `branch_to_active_flow` column, in `branch_order`.
1165    #[must_use]
1166    pub fn branch_to_active_flows(&self) -> &[f64] {
1167        &self.branch_to_active_flow
1168    }
1169    /// The complete `branch_to_reactive_flow` column, in `branch_order`.
1170    #[must_use]
1171    pub fn branch_to_reactive_flows(&self) -> &[f64] {
1172        &self.branch_to_reactive_flow
1173    }
1174    /// The complete `generator_active_power` column, in `generator_order`.
1175    #[must_use]
1176    pub fn generator_active_powers(&self) -> &[f64] {
1177        &self.generator_active_power
1178    }
1179    /// The complete `generator_reactive_power` column, in `generator_order`.
1180    #[must_use]
1181    pub fn generator_reactive_powers(&self) -> &[f64] {
1182        &self.generator_reactive_power
1183    }
1184}