Skip to main content

powerio_prob/solution/
lindist3flow.rs

1//! Solver-independent LinDist3Flow OPF results.
2//!
3//! Values follow formulation identities and physical table order, never a
4//! solver adapter's column order. This lets a native or WASM solver return the
5//! same public solution type after translating its primal vector.
6
7use 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, LinDist3FlowPfInstance};
15use crate::solution::{Producer, Residuals, Termination};
16
17/// Physical primal values from a LinDist3Flow solve.
18///
19/// Node values follow `instance.topology().nodes`. Line values follow
20/// `instance.topology().conductors`. Generator channels are flattened in
21/// generator table order, then channel order. Source channels are flattened
22/// in source table order, then terminal-map order. Voltage is squared volts;
23/// all power quantities are watts or vars.
24#[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
117fn validate_values(
118    instance: &LinDist3FlowOpfInstance,
119    values: &LinDist3FlowOpfValues,
120) -> Result<SolutionIndex, Error> {
121    let network = instance.network();
122    let nodes = instance.topology().nodes.len();
123    let conductors = instance.topology().conductors.len();
124    let generators = network
125        .generators()
126        .iter()
127        .map(|generator| generator.p_nom.len())
128        .sum();
129    let sources = network
130        .sources()
131        .iter()
132        .map(|source| source.terminal_map.len())
133        .sum();
134    shape(
135        "terminal squared voltage",
136        values.terminal_voltage_magnitude_squared.len(),
137        nodes,
138    )?;
139    shape(
140        "line active power",
141        values.line_active_power.len(),
142        conductors,
143    )?;
144    shape(
145        "line reactive power",
146        values.line_reactive_power.len(),
147        conductors,
148    )?;
149    shape(
150        "generator active power",
151        values.generator_active_power.len(),
152        generators,
153    )?;
154    shape(
155        "generator reactive power",
156        values.generator_reactive_power.len(),
157        generators,
158    )?;
159    shape(
160        "source active power",
161        values.source_active_power.len(),
162        sources,
163    )?;
164    shape(
165        "source reactive power",
166        values.source_reactive_power.len(),
167        sources,
168    )?;
169    SolutionIndex::new(instance)
170}
171
172/// A solution of the fixed-reference LinDist3Flow OPF approximation.
173#[derive(Clone, Debug)]
174pub struct LinDist3FlowOpfSolution {
175    instance: Arc<LinDist3FlowOpfInstance>,
176    termination: Termination,
177    residuals: Residuals,
178    producer: Producer,
179    values: LinDist3FlowOpfValues,
180    objective: f64,
181    index: SolutionIndex,
182}
183
184impl LinDist3FlowOpfSolution {
185    pub const FORMULATION: &'static str = "lindist3flow";
186
187    /// Construct a result in the formulation's documented physical order.
188    ///
189    /// # Errors
190    /// Any primal column disagrees with the instance's node or device axes.
191    pub fn new(
192        instance: Arc<LinDist3FlowOpfInstance>,
193        termination: Termination,
194        values: LinDist3FlowOpfValues,
195        objective: f64,
196    ) -> Result<Self, Error> {
197        let index = validate_values(&instance, &values)?;
198        Ok(Self {
199            instance,
200            termination,
201            residuals: Residuals::default(),
202            producer: None,
203            values,
204            objective,
205            index,
206        })
207    }
208
209    #[must_use]
210    pub const fn formulation(&self) -> &'static str {
211        Self::FORMULATION
212    }
213
214    #[must_use]
215    pub fn instance(&self) -> &LinDist3FlowOpfInstance {
216        &self.instance
217    }
218
219    #[must_use]
220    pub fn shared_instance(&self) -> Arc<LinDist3FlowOpfInstance> {
221        Arc::clone(&self.instance)
222    }
223
224    #[must_use]
225    pub fn network(&self) -> &MulticonductorNetwork {
226        self.instance.network()
227    }
228
229    #[must_use]
230    pub const fn termination(&self) -> &Termination {
231        &self.termination
232    }
233
234    #[must_use]
235    pub const fn residuals(&self) -> &Residuals {
236        &self.residuals
237    }
238
239    #[must_use]
240    pub fn producer(&self) -> Option<&str> {
241        self.producer.as_deref()
242    }
243
244    #[must_use]
245    pub const fn values(&self) -> &LinDist3FlowOpfValues {
246        &self.values
247    }
248
249    #[must_use]
250    pub const fn objective(&self) -> f64 {
251        self.objective
252    }
253
254    #[must_use]
255    pub fn with_producer(mut self, producer: impl Into<String>) -> Self {
256        self.producer = Some(producer.into());
257        self
258    }
259
260    #[must_use]
261    pub const fn with_residuals(mut self, residuals: Residuals) -> Self {
262        self.residuals = residuals;
263        self
264    }
265
266    pub fn node_order(&self) -> impl ExactSizeIterator<Item = &LinDist3FlowNode> {
267        self.instance.topology().nodes.iter()
268    }
269
270    #[must_use]
271    pub fn terminal_voltage_magnitude_squared(&self, bus: &str, terminal: &str) -> Option<f64> {
272        let position = self
273            .index
274            .nodes
275            .get(&(bus.to_ascii_lowercase(), terminal.to_owned()))?;
276        Some(self.values.terminal_voltage_magnitude_squared[*position])
277    }
278
279    #[must_use]
280    pub fn terminal_voltage_magnitude(&self, bus: &str, terminal: &str) -> Option<f64> {
281        let squared = self.terminal_voltage_magnitude_squared(bus, terminal)?;
282        (squared >= 0.0).then(|| squared.sqrt())
283    }
284
285    #[must_use]
286    pub fn line_active_power(&self, line: &str, conductor: usize) -> Option<f64> {
287        Some(
288            self.values.line_active_power[*self
289                .index
290                .line_conductors
291                .get(&(line.to_owned(), conductor))?],
292        )
293    }
294
295    #[must_use]
296    pub fn line_reactive_power(&self, line: &str, conductor: usize) -> Option<f64> {
297        Some(
298            self.values.line_reactive_power[*self
299                .index
300                .line_conductors
301                .get(&(line.to_owned(), conductor))?],
302        )
303    }
304
305    fn channel_position(
306        index: &BTreeMap<String, (usize, usize)>,
307        identity: &str,
308        channel: usize,
309    ) -> Option<usize> {
310        let (offset, count) = *index.get(identity)?;
311        (channel < count).then_some(offset + channel)
312    }
313
314    #[must_use]
315    pub fn generator_active_power(&self, generator: &str, channel: usize) -> Option<f64> {
316        Some(
317            self.values.generator_active_power
318                [Self::channel_position(&self.index.generators, generator, channel)?],
319        )
320    }
321
322    #[must_use]
323    pub fn generator_reactive_power(&self, generator: &str, channel: usize) -> Option<f64> {
324        Some(
325            self.values.generator_reactive_power
326                [Self::channel_position(&self.index.generators, generator, channel)?],
327        )
328    }
329
330    #[must_use]
331    pub fn source_active_power(&self, source: &str, channel: usize) -> Option<f64> {
332        Some(
333            self.values.source_active_power
334                [Self::channel_position(&self.index.sources, source, channel)?],
335        )
336    }
337
338    #[must_use]
339    pub fn source_reactive_power(&self, source: &str, channel: usize) -> Option<f64> {
340        Some(
341            self.values.source_reactive_power
342                [Self::channel_position(&self.index.sources, source, channel)?],
343        )
344    }
345}
346
347/// Physical quantity evaluated against a monitored LinDist3Flow limit.
348#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
349#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
350#[serde(rename_all = "snake_case")]
351#[non_exhaustive]
352pub enum LinDist3FlowLimitKind {
353    LineApparentPower,
354    LineCurrentFrom,
355    LineCurrentTo,
356}
357
358impl LinDist3FlowLimitKind {
359    #[must_use]
360    pub const fn unit(self) -> &'static str {
361        match self {
362            Self::LineApparentPower => "VA",
363            Self::LineCurrentFrom | Self::LineCurrentTo => "A",
364        }
365    }
366}
367
368/// One semantic SI limit evaluation from a converged fixed-dispatch point.
369#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
370#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
371#[non_exhaustive]
372pub struct LinDist3FlowLimitCheck {
373    pub line: String,
374    pub conductor: usize,
375    pub kind: LinDist3FlowLimitKind,
376    pub value: f64,
377    pub limit: f64,
378    pub loading_ratio: f64,
379    pub overloaded: bool,
380    /// Fixed-dispatch thermal limits are monitored, not conic constraints.
381    pub constraint_enforced: bool,
382}
383
384impl LinDist3FlowLimitCheck {
385    /// Construct a monitored (not enforced) SI limit check.
386    #[must_use]
387    pub fn monitored(
388        line: impl Into<String>,
389        conductor: usize,
390        kind: LinDist3FlowLimitKind,
391        value: f64,
392        limit: f64,
393    ) -> Self {
394        let loading_ratio = value / limit;
395        Self {
396            line: line.into(),
397            conductor,
398            kind,
399            value,
400            limit,
401            loading_ratio,
402            overloaded: loading_ratio > 1.0,
403            constraint_enforced: false,
404        }
405    }
406}
407
408fn invalid_limit(reason: impl Into<String>) -> Error {
409    Error::new(&codes::BUILD_SOLUTION_LIMIT_CHECK_INVALID, reason.into())
410}
411
412fn ratings(
413    line: Option<&[f64]>,
414    code: Option<&[f64]>,
415    conductors: usize,
416    label: &str,
417) -> Result<Vec<Option<f64>>, Error> {
418    let Some(values) = line.or(code) else {
419        return Ok(vec![None; conductors]);
420    };
421    if values.len() != conductors {
422        return Err(invalid_limit(format!(
423            "{label} has {} entries, expected {conductors}",
424            values.len()
425        )));
426    }
427    values
428        .iter()
429        .enumerate()
430        .map(|(position, value)| {
431            if value.is_finite() && *value > 0.0 {
432                Ok(Some(*value))
433            } else {
434                Err(invalid_limit(format!(
435                    "{label} entry {position} must be finite and positive"
436                )))
437            }
438        })
439        .collect()
440}
441
442/// Evaluate every retained line thermal rating at a fixed-dispatch point.
443/// Values and limits are SI volt-amperes or amperes. Endpoint current uses
444/// `hypot(P,Q) / |V|`; parallel contacts remain separate line identities.
445///
446/// # Errors
447/// The values do not match the formulation axes, a rating is invalid, or an
448/// endpoint squared voltage is nonpositive or non-finite.
449#[allow(clippy::too_many_lines)] // Keep the three related SI checks in one auditable pass.
450pub fn evaluate_lindist3flow_pf_limits(
451    instance: &LinDist3FlowPfInstance,
452    values: &LinDist3FlowOpfValues,
453) -> Result<Vec<LinDist3FlowLimitCheck>, Error> {
454    let formulation = instance.formulation();
455    if values.line_active_power.len() != formulation.topology().conductors.len()
456        || values.line_reactive_power.len() != formulation.topology().conductors.len()
457        || values.terminal_voltage_magnitude_squared.len() != formulation.topology().nodes.len()
458    {
459        return Err(invalid_limit(
460            "fixed-dispatch values do not match the topology node/conductor axes",
461        ));
462    }
463    let nodes = formulation
464        .topology()
465        .nodes
466        .iter()
467        .enumerate()
468        .map(|(position, node)| {
469            (
470                (node.bus.to_ascii_lowercase(), node.terminal.clone()),
471                position,
472            )
473        })
474        .collect::<BTreeMap<_, _>>();
475    let mut by_line = BTreeMap::<usize, Vec<(usize, usize)>>::new();
476    for (value_position, conductor) in formulation.topology().conductors.iter().enumerate() {
477        by_line
478            .entry(conductor.source_line_row)
479            .or_default()
480            .push((conductor.conductor_position, value_position));
481    }
482
483    let mut checks = Vec::new();
484    for (line_row, mut positions) in by_line {
485        let line =
486            formulation.network().lines().get(line_row).ok_or_else(|| {
487                invalid_limit(format!("topology names missing line row {line_row}"))
488            })?;
489        positions.sort_by_key(|(conductor, _)| *conductor);
490        let code = formulation
491            .network()
492            .linecode(&line.linecode)
493            .ok_or_else(|| {
494                invalid_limit(format!(
495                    "line `{}` names missing linecode `{}`",
496                    line.name, line.linecode
497                ))
498            })?;
499        let conductor_count = positions.len();
500        let current_limits = ratings(
501            line.i_max.as_deref(),
502            code.i_max.as_deref(),
503            conductor_count,
504            &format!("line `{}` current limit", line.name),
505        )?;
506        let apparent_limits = ratings(
507            line.s_max.as_deref(),
508            code.s_max.as_deref(),
509            conductor_count,
510            &format!("line `{}` apparent-power limit", line.name),
511        )?;
512        for (conductor, value_position) in positions {
513            let topology = &formulation.topology().conductors[value_position];
514            let p = values.line_active_power[value_position];
515            let q = values.line_reactive_power[value_position];
516            if !p.is_finite() || !q.is_finite() {
517                return Err(invalid_limit(format!(
518                    "line `{}` conductor {conductor} has non-finite power",
519                    line.name
520                )));
521            }
522            let apparent = p.hypot(q);
523            if let Some(limit) = apparent_limits[conductor] {
524                checks.push(LinDist3FlowLimitCheck::monitored(
525                    &line.name,
526                    conductor,
527                    LinDist3FlowLimitKind::LineApparentPower,
528                    apparent,
529                    limit,
530                ));
531            }
532            let Some(limit) = current_limits[conductor] else {
533                continue;
534            };
535            let (from, to) = if topology.reversed {
536                (&topology.child, &topology.parent)
537            } else {
538                (&topology.parent, &topology.child)
539            };
540            for (node, kind) in [
541                (from, LinDist3FlowLimitKind::LineCurrentFrom),
542                (to, LinDist3FlowLimitKind::LineCurrentTo),
543            ] {
544                let position = nodes
545                    .get(&(node.bus.to_ascii_lowercase(), node.terminal.clone()))
546                    .copied()
547                    .ok_or_else(|| {
548                        invalid_limit(format!(
549                            "line `{}` endpoint `{}/{}` is absent from the node axis",
550                            line.name, node.bus, node.terminal
551                        ))
552                    })?;
553                let squared_voltage = values.terminal_voltage_magnitude_squared[position];
554                if !squared_voltage.is_finite() || squared_voltage <= 0.0 {
555                    return Err(invalid_limit(format!(
556                        "line `{}` endpoint `{}/{}` has nonpositive or non-finite squared voltage",
557                        line.name, node.bus, node.terminal
558                    )));
559                }
560                checks.push(LinDist3FlowLimitCheck::monitored(
561                    &line.name,
562                    conductor,
563                    kind,
564                    apparent / squared_voltage.sqrt(),
565                    limit,
566                ));
567            }
568        }
569    }
570    Ok(checks)
571}
572
573/// Result of a fixed-dispatch approximate LinDist3Flow calculation.
574#[derive(Clone, Debug)]
575pub struct LinDist3FlowPfSolution {
576    instance: Arc<LinDist3FlowPfInstance>,
577    termination: Termination,
578    residuals: Residuals,
579    producer: Producer,
580    values: LinDist3FlowOpfValues,
581    limit_checks: Vec<LinDist3FlowLimitCheck>,
582    index: SolutionIndex,
583}
584
585impl LinDist3FlowPfSolution {
586    pub const FORMULATION: &'static str = "lindist3flow_fixed_dispatch";
587
588    /// Construct a fixed-dispatch result. A converged point automatically
589    /// evaluates every retained supported line rating; other terminations
590    /// carry no operating-limit claims.
591    ///
592    /// # Errors
593    /// A primal column has the wrong shape, or a retained rating or converged
594    /// endpoint voltage cannot be evaluated.
595    pub fn new(
596        instance: Arc<LinDist3FlowPfInstance>,
597        termination: Termination,
598        values: LinDist3FlowOpfValues,
599    ) -> Result<Self, Error> {
600        let index = validate_values(instance.formulation(), &values)?;
601        let limit_checks = if matches!(termination, Termination::Converged) {
602            evaluate_lindist3flow_pf_limits(&instance, &values)?
603        } else {
604            Vec::new()
605        };
606        Ok(Self {
607            instance,
608            termination,
609            residuals: Residuals::default(),
610            producer: None,
611            values,
612            limit_checks,
613            index,
614        })
615    }
616
617    #[must_use]
618    pub const fn formulation(&self) -> &'static str {
619        Self::FORMULATION
620    }
621
622    #[must_use]
623    pub fn instance(&self) -> &LinDist3FlowPfInstance {
624        &self.instance
625    }
626
627    #[must_use]
628    pub fn shared_instance(&self) -> Arc<LinDist3FlowPfInstance> {
629        Arc::clone(&self.instance)
630    }
631
632    #[must_use]
633    pub fn network(&self) -> &MulticonductorNetwork {
634        self.instance.network()
635    }
636
637    #[must_use]
638    pub const fn termination(&self) -> &Termination {
639        &self.termination
640    }
641
642    #[must_use]
643    pub const fn residuals(&self) -> &Residuals {
644        &self.residuals
645    }
646
647    #[must_use]
648    pub fn producer(&self) -> Option<&str> {
649        self.producer.as_deref()
650    }
651
652    #[must_use]
653    pub const fn values(&self) -> &LinDist3FlowOpfValues {
654        &self.values
655    }
656
657    #[must_use]
658    pub fn limit_checks(&self) -> &[LinDist3FlowLimitCheck] {
659        &self.limit_checks
660    }
661
662    #[must_use]
663    pub fn with_producer(mut self, producer: impl Into<String>) -> Self {
664        self.producer = Some(producer.into());
665        self
666    }
667
668    #[must_use]
669    pub const fn with_residuals(mut self, residuals: Residuals) -> Self {
670        self.residuals = residuals;
671        self
672    }
673
674    #[must_use]
675    pub fn terminal_voltage_magnitude_squared(&self, bus: &str, terminal: &str) -> Option<f64> {
676        let position = self
677            .index
678            .nodes
679            .get(&(bus.to_ascii_lowercase(), terminal.to_owned()))?;
680        Some(self.values.terminal_voltage_magnitude_squared[*position])
681    }
682
683    #[must_use]
684    pub fn line_active_power(&self, line: &str, conductor: usize) -> Option<f64> {
685        let position = self
686            .index
687            .line_conductors
688            .get(&(line.to_owned(), conductor))?;
689        Some(self.values.line_active_power[*position])
690    }
691
692    #[must_use]
693    pub fn line_reactive_power(&self, line: &str, conductor: usize) -> Option<f64> {
694        let position = self
695            .index
696            .line_conductors
697            .get(&(line.to_owned(), conductor))?;
698        Some(self.values.line_reactive_power[*position])
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use crate::{LinDist3FlowBuildOptions, LinDist3FlowOpfInstance};
705    use powerio_dist::{DistBus, DistLine, DistLineCode, MulticonductorNetwork, VoltageSource};
706
707    use super::*;
708
709    fn instance() -> Arc<LinDist3FlowOpfInstance> {
710        let terminal = vec!["1".to_owned()];
711        let mut network = MulticonductorNetwork::named("solution");
712        network
713            .buses_mut()
714            .push(DistBus::new("source", terminal.clone()));
715        network
716            .buses_mut()
717            .push(DistBus::new("load", terminal.clone()));
718        network
719            .line_codes_mut()
720            .push(DistLineCode::new("one", vec![vec![0.1]], vec![vec![0.2]]));
721        network.lines_mut().push(DistLine::new(
722            "line",
723            "source",
724            "load",
725            terminal.clone(),
726            terminal.clone(),
727            "one",
728            1.0,
729        ));
730        network.sources_mut().push(VoltageSource::new(
731            "grid",
732            "source",
733            terminal,
734            vec![230.0],
735            vec![0.0],
736        ));
737        Arc::new(
738            LinDist3FlowOpfInstance::from_network(network, LinDist3FlowBuildOptions::default())
739                .unwrap(),
740        )
741    }
742
743    fn values() -> LinDist3FlowOpfValues {
744        LinDist3FlowOpfValues {
745            terminal_voltage_magnitude_squared: vec![230.0f64.powi(2), 228.0f64.powi(2)],
746            line_active_power: vec![1_000.0],
747            line_reactive_power: vec![200.0],
748            generator_active_power: Vec::new(),
749            generator_reactive_power: Vec::new(),
750            source_active_power: vec![1_000.0],
751            source_reactive_power: vec![200.0],
752        }
753    }
754
755    #[test]
756    fn result_is_keyed_by_formulation_identities() {
757        let instance = instance();
758        let solution = LinDist3FlowOpfSolution::new(
759            Arc::clone(&instance),
760            Termination::Converged,
761            values(),
762            0.3,
763        )
764        .unwrap()
765        .with_producer("test-solver");
766        assert_eq!(solution.formulation(), "lindist3flow");
767        assert_eq!(solution.producer(), Some("test-solver"));
768        assert!((solution.terminal_voltage_magnitude("load", "1").unwrap() - 228.0).abs() < 1e-12);
769        assert!((solution.line_active_power("line", 0).unwrap() - 1_000.0).abs() < 1e-12);
770        assert!((solution.source_reactive_power("grid", 0).unwrap() - 200.0).abs() < 1e-12);
771        assert!(std::ptr::eq(solution.instance(), instance.as_ref()));
772    }
773
774    #[test]
775    fn every_physical_axis_is_shape_checked() {
776        let instance = instance();
777        let mut wrong = values();
778        wrong.line_active_power.push(0.0);
779        assert!(
780            LinDist3FlowOpfSolution::new(instance, Termination::Converged, wrong, 0.0).is_err()
781        );
782    }
783}