1use std::collections::BTreeMap;
9
10use powerio_prob::{LinDist3FlowOpfInstance, LinDist3FlowOpfValues};
11
12use crate::{
13 Error, LinDist3FlowAffineExpression, LinDist3FlowBalanceEquation, LinDist3FlowPreparation,
14 LinDist3FlowVariable, Result, build_lindist3flow_preparation,
15};
16
17#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
19#[non_exhaustive]
20pub enum LinDist3FlowDecisionVariable {
21 SquaredVoltage { node: usize },
22 Power(LinDist3FlowVariable),
23}
24
25#[derive(Clone, Debug, PartialEq)]
27#[non_exhaustive]
28pub struct LinDist3FlowVariableData {
29 pub variable: LinDist3FlowDecisionVariable,
30 pub lower: Option<f64>,
31 pub upper: Option<f64>,
32 pub objective_coefficient: f64,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq)]
37#[non_exhaustive]
38pub struct LinDist3FlowLinearTerm {
39 pub column: usize,
40 pub coefficient: f64,
41}
42
43#[derive(Clone, Debug, Default, PartialEq)]
45#[non_exhaustive]
46pub struct LinDist3FlowLinearExpression {
47 pub constant: f64,
48 pub terms: Vec<LinDist3FlowLinearTerm>,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum LinDist3FlowEqualityOrigin {
55 LineDrop { line: usize, conductor: usize },
56 ActiveBalance { node: usize },
57 ReactiveBalance { node: usize },
58}
59
60#[derive(Clone, Debug, PartialEq)]
62#[non_exhaustive]
63pub struct LinDist3FlowEquality {
64 pub origin: LinDist3FlowEqualityOrigin,
65 pub expression: LinDist3FlowLinearExpression,
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum LinDist3FlowConeOrigin {
72 LineApparentPower {
73 line: usize,
74 conductor: usize,
75 },
76 LineCurrent {
77 line: usize,
78 conductor: usize,
79 node: usize,
80 },
81 GeneratorApparentPower {
82 generator: usize,
83 channel: usize,
84 },
85 GeneratorCurrent {
86 generator: usize,
87 channel: usize,
88 },
89}
90
91#[derive(Clone, Debug, PartialEq)]
97#[non_exhaustive]
98pub enum LinDist3FlowCone {
99 SecondOrder {
100 origin: LinDist3FlowConeOrigin,
101 arguments: Vec<LinDist3FlowLinearExpression>,
102 },
103 RotatedSecondOrder {
104 origin: LinDist3FlowConeOrigin,
105 arguments: Vec<LinDist3FlowLinearExpression>,
106 },
107}
108
109#[derive(Clone, Debug, PartialEq)]
111#[non_exhaustive]
112pub struct LinDist3FlowConicProblem {
113 pub preparation: LinDist3FlowPreparation,
114 pub variables: Vec<LinDist3FlowVariableData>,
115 pub equalities: Vec<LinDist3FlowEquality>,
116 pub cones: Vec<LinDist3FlowCone>,
117}
118
119fn invalid(reason: impl Into<String>) -> Error {
120 Error::InvalidLinDist3FlowCoefficients {
121 reason: reason.into(),
122 }
123}
124
125fn constant(value: f64) -> LinDist3FlowLinearExpression {
126 LinDist3FlowLinearExpression {
127 constant: value,
128 terms: Vec::new(),
129 }
130}
131
132fn column(column: usize, coefficient: f64) -> LinDist3FlowLinearExpression {
133 LinDist3FlowLinearExpression {
134 constant: 0.0,
135 terms: vec![LinDist3FlowLinearTerm {
136 column,
137 coefficient,
138 }],
139 }
140}
141
142fn scaled_expression(
143 mut expression: LinDist3FlowLinearExpression,
144 scale: f64,
145) -> LinDist3FlowLinearExpression {
146 expression.constant *= scale;
147 for term in &mut expression.terms {
148 term.coefficient *= scale;
149 }
150 expression
151}
152
153fn normalized_expression(
154 constant: f64,
155 terms: impl IntoIterator<Item = (usize, f64)>,
156) -> Result<LinDist3FlowLinearExpression> {
157 if !constant.is_finite() {
158 return Err(invalid(
159 "a conic affine expression has a non-finite constant",
160 ));
161 }
162 let mut combined = BTreeMap::<usize, f64>::new();
163 for (column, coefficient) in terms {
164 if !coefficient.is_finite() {
165 return Err(invalid(format!(
166 "a conic affine expression has a non-finite coefficient at column {column}"
167 )));
168 }
169 *combined.entry(column).or_default() += coefficient;
170 }
171 Ok(LinDist3FlowLinearExpression {
172 constant,
173 terms: combined
174 .into_iter()
175 .filter_map(|(column, coefficient)| {
176 (coefficient.abs() > f64::EPSILON).then_some(LinDist3FlowLinearTerm {
177 column,
178 coefficient,
179 })
180 })
181 .collect(),
182 })
183}
184
185fn voltage_expression(
186 expression: &LinDist3FlowAffineExpression,
187 voltage_columns: &[usize],
188) -> Result<LinDist3FlowLinearExpression> {
189 normalized_expression(
190 expression.constant,
191 expression
192 .voltage_terms
193 .iter()
194 .map(|term| (voltage_columns[term.node], term.coefficient)),
195 )
196}
197
198fn balance_expression(
199 equation: &LinDist3FlowBalanceEquation,
200 voltage_columns: &[usize],
201 power_columns: &BTreeMap<LinDist3FlowVariable, usize>,
202) -> Result<LinDist3FlowLinearExpression> {
203 let voltage_terms = equation
204 .affine
205 .voltage_terms
206 .iter()
207 .map(|term| (voltage_columns[term.node], term.coefficient));
208 let power_terms = equation.variable_terms.iter().map(|term| {
209 power_columns
210 .get(&term.variable)
211 .copied()
212 .map(|column| (column, term.coefficient))
213 .ok_or_else(|| {
214 invalid(format!(
215 "nodal balance references unregistered variable {:?}",
216 term.variable
217 ))
218 })
219 });
220 normalized_expression(
221 equation.affine.constant,
222 voltage_terms.chain(power_terms.collect::<Result<Vec<_>>>()?),
223 )
224}
225
226fn push_power_variable(
227 variables: &mut Vec<LinDist3FlowVariableData>,
228 power_columns: &mut BTreeMap<LinDist3FlowVariable, usize>,
229 variable: LinDist3FlowVariable,
230 lower: Option<f64>,
231 upper: Option<f64>,
232 objective_coefficient: f64,
233) -> Result<()> {
234 if lower.is_some_and(|value| !value.is_finite())
235 || upper.is_some_and(|value| !value.is_finite())
236 || !objective_coefficient.is_finite()
237 {
238 return Err(invalid(format!(
239 "variable {variable:?} has non-finite bound or objective data"
240 )));
241 }
242 if lower.zip(upper).is_some_and(|(lower, upper)| lower > upper) {
243 return Err(invalid(format!(
244 "variable {variable:?} has inverted bounds"
245 )));
246 }
247 let column = variables.len();
248 if power_columns.insert(variable.clone(), column).is_some() {
249 return Err(invalid(format!(
250 "variable {variable:?} was registered more than once"
251 )));
252 }
253 variables.push(LinDist3FlowVariableData {
254 variable: LinDist3FlowDecisionVariable::Power(variable),
255 lower,
256 upper,
257 objective_coefficient,
258 });
259 Ok(())
260}
261
262fn power_column(
263 columns: &BTreeMap<LinDist3FlowVariable, usize>,
264 variable: &LinDist3FlowVariable,
265) -> Result<usize> {
266 columns
267 .get(variable)
268 .copied()
269 .ok_or_else(|| invalid(format!("missing conic variable {variable:?}")))
270}
271
272#[allow(clippy::too_many_lines)]
284pub fn build_lindist3flow_conic_problem(
285 instance: &LinDist3FlowOpfInstance,
286) -> Result<LinDist3FlowConicProblem> {
287 let preparation = build_lindist3flow_preparation(instance)?;
288 let mut variables = Vec::new();
289 let mut voltage_columns = Vec::with_capacity(preparation.network.nodes.len());
290 for (node, data) in preparation.network.nodes.iter().enumerate() {
291 let column = variables.len();
292 voltage_columns.push(column);
293 let (lower, upper) = if let Some(fixed) = data.fixed_squared_voltage {
294 if data
295 .squared_voltage_min
296 .is_some_and(|minimum| fixed < minimum)
297 || data
298 .squared_voltage_max
299 .is_some_and(|maximum| fixed > maximum)
300 {
301 return Err(invalid(format!(
302 "fixed voltage at node {node} is outside its selected voltage bounds"
303 )));
304 }
305 (Some(fixed), Some(fixed))
306 } else {
307 (
309 Some(data.squared_voltage_min.unwrap_or(0.0)),
310 data.squared_voltage_max,
311 )
312 };
313 variables.push(LinDist3FlowVariableData {
314 variable: LinDist3FlowDecisionVariable::SquaredVoltage { node },
315 lower,
316 upper,
317 objective_coefficient: 0.0,
318 });
319 }
320
321 let mut power_columns = BTreeMap::new();
322 for (line, data) in preparation.network.lines.iter().enumerate() {
323 for conductor in 0..data.parent_nodes.len() {
324 push_power_variable(
325 &mut variables,
326 &mut power_columns,
327 LinDist3FlowVariable::LineActive { line, conductor },
328 None,
329 None,
330 0.0,
331 )?;
332 push_power_variable(
333 &mut variables,
334 &mut power_columns,
335 LinDist3FlowVariable::LineReactive { line, conductor },
336 None,
337 None,
338 0.0,
339 )?;
340 }
341 }
342 for (generator, data) in preparation.devices.generators.iter().enumerate() {
343 for (channel, channel_data) in data.channels.iter().enumerate() {
344 push_power_variable(
345 &mut variables,
346 &mut power_columns,
347 LinDist3FlowVariable::GeneratorActive { generator, channel },
348 channel_data.active_min,
349 channel_data.active_max,
350 channel_data.active_objective_coefficient,
351 )?;
352 push_power_variable(
353 &mut variables,
354 &mut power_columns,
355 LinDist3FlowVariable::GeneratorReactive { generator, channel },
356 channel_data.reactive_min,
357 channel_data.reactive_max,
358 0.0,
359 )?;
360 }
361 }
362 for (source, data) in preparation.devices.sources.iter().enumerate() {
363 for (channel, &objective) in data.active_objective_coefficient.iter().enumerate() {
364 push_power_variable(
365 &mut variables,
366 &mut power_columns,
367 LinDist3FlowVariable::SourceActive { source, channel },
368 None,
369 None,
370 objective,
371 )?;
372 push_power_variable(
373 &mut variables,
374 &mut power_columns,
375 LinDist3FlowVariable::SourceReactive { source, channel },
376 None,
377 None,
378 0.0,
379 )?;
380 }
381 }
382
383 let mut equalities = Vec::new();
384 for (line, data) in preparation.network.lines.iter().enumerate() {
385 for conductor in 0..data.parent_nodes.len() {
386 let terms = [
387 (voltage_columns[data.child_nodes[conductor]], 1.0),
388 (voltage_columns[data.parent_nodes[conductor]], -1.0),
389 ]
390 .into_iter()
391 .chain(
392 data.drop.active[conductor]
393 .iter()
394 .enumerate()
395 .map(|(other, &coefficient)| {
396 let column = power_column(
397 &power_columns,
398 &LinDist3FlowVariable::LineActive {
399 line,
400 conductor: other,
401 },
402 );
403 column.map(|column| (column, coefficient))
404 })
405 .collect::<Result<Vec<_>>>()?,
406 )
407 .chain(
408 data.drop.reactive[conductor]
409 .iter()
410 .enumerate()
411 .map(|(other, &coefficient)| {
412 let column = power_column(
413 &power_columns,
414 &LinDist3FlowVariable::LineReactive {
415 line,
416 conductor: other,
417 },
418 );
419 column.map(|column| (column, coefficient))
420 })
421 .collect::<Result<Vec<_>>>()?,
422 );
423 equalities.push(LinDist3FlowEquality {
424 origin: LinDist3FlowEqualityOrigin::LineDrop { line, conductor },
425 expression: normalized_expression(0.0, terms)?,
426 });
427 }
428 }
429 for balance in &preparation.devices.balances {
430 equalities.push(LinDist3FlowEquality {
431 origin: LinDist3FlowEqualityOrigin::ActiveBalance { node: balance.node },
432 expression: balance_expression(&balance.active, &voltage_columns, &power_columns)?,
433 });
434 equalities.push(LinDist3FlowEquality {
435 origin: LinDist3FlowEqualityOrigin::ReactiveBalance { node: balance.node },
436 expression: balance_expression(&balance.reactive, &voltage_columns, &power_columns)?,
437 });
438 }
439
440 let mut cones = Vec::new();
441 for (line, data) in preparation.network.lines.iter().enumerate() {
442 for conductor in 0..data.parent_nodes.len() {
443 let active = power_column(
444 &power_columns,
445 &LinDist3FlowVariable::LineActive { line, conductor },
446 )?;
447 let reactive = power_column(
448 &power_columns,
449 &LinDist3FlowVariable::LineReactive { line, conductor },
450 )?;
451 if let Some(limit) = data.apparent_power_limit[conductor] {
452 cones.push(LinDist3FlowCone::SecondOrder {
453 origin: LinDist3FlowConeOrigin::LineApparentPower { line, conductor },
454 arguments: vec![constant(limit), column(active, 1.0), column(reactive, 1.0)],
455 });
456 }
457 if let Some(limit) = data.current_limit[conductor] {
458 for &node in &[data.parent_nodes[conductor], data.child_nodes[conductor]] {
459 let reference_voltage = preparation.network.nodes[node].reference_magnitude;
460 cones.push(LinDist3FlowCone::RotatedSecondOrder {
461 origin: LinDist3FlowConeOrigin::LineCurrent {
462 line,
463 conductor,
464 node,
465 },
466 arguments: vec![
467 column(voltage_columns[node], limit / reference_voltage),
468 constant(limit * reference_voltage / 2.0),
469 column(active, 1.0),
470 column(reactive, 1.0),
471 ],
472 });
473 }
474 }
475 }
476 }
477 for (generator, data) in preparation.devices.generators.iter().enumerate() {
478 for (channel, channel_data) in data.channels.iter().enumerate() {
479 let active = power_column(
480 &power_columns,
481 &LinDist3FlowVariable::GeneratorActive { generator, channel },
482 )?;
483 let reactive = power_column(
484 &power_columns,
485 &LinDist3FlowVariable::GeneratorReactive { generator, channel },
486 )?;
487 if let Some(limit) = channel_data.apparent_power_limit {
488 cones.push(LinDist3FlowCone::SecondOrder {
489 origin: LinDist3FlowConeOrigin::GeneratorApparentPower { generator, channel },
490 arguments: vec![constant(limit), column(active, 1.0), column(reactive, 1.0)],
491 });
492 }
493 if let Some(limit) = channel_data.current_limit {
494 let reference_voltage = channel_data.reference_winding_voltage;
495 cones.push(LinDist3FlowCone::RotatedSecondOrder {
496 origin: LinDist3FlowConeOrigin::GeneratorCurrent { generator, channel },
497 arguments: vec![
498 scaled_expression(
499 voltage_expression(
500 &channel_data.squared_winding_voltage,
501 &voltage_columns,
502 )?,
503 limit / reference_voltage,
504 ),
505 constant(limit * reference_voltage / 2.0),
506 column(active, 1.0),
507 column(reactive, 1.0),
508 ],
509 });
510 }
511 }
512 }
513
514 Ok(LinDist3FlowConicProblem {
515 preparation,
516 variables,
517 equalities,
518 cones,
519 })
520}
521
522#[allow(clippy::too_many_lines)]
529pub fn lindist3flow_values_from_primal(
530 problem: &LinDist3FlowConicProblem,
531 primal: &[f64],
532) -> Result<LinDist3FlowOpfValues> {
533 if primal.len() != problem.variables.len() {
534 return Err(invalid(format!(
535 "LinDist3Flow primal has length {}, expected {}",
536 primal.len(),
537 problem.variables.len()
538 )));
539 }
540 let line_offsets = problem
541 .preparation
542 .network
543 .lines
544 .iter()
545 .scan(0, |offset, line| {
546 let current = *offset;
547 *offset += line.parent_nodes.len();
548 Some(current)
549 })
550 .collect::<Vec<_>>();
551 let generator_offsets = problem
552 .preparation
553 .devices
554 .generators
555 .iter()
556 .scan(0, |offset, generator| {
557 let current = *offset;
558 *offset += generator.channels.len();
559 Some(current)
560 })
561 .collect::<Vec<_>>();
562 let source_offsets = problem
563 .preparation
564 .devices
565 .sources
566 .iter()
567 .scan(0, |offset, source| {
568 let current = *offset;
569 *offset += source.terminal_nodes.len();
570 Some(current)
571 })
572 .collect::<Vec<_>>();
573 let line_count = problem
574 .preparation
575 .network
576 .lines
577 .iter()
578 .map(|line| line.parent_nodes.len())
579 .sum();
580 let generator_count = problem
581 .preparation
582 .devices
583 .generators
584 .iter()
585 .map(|generator| generator.channels.len())
586 .sum();
587 let source_count = problem
588 .preparation
589 .devices
590 .sources
591 .iter()
592 .map(|source| source.terminal_nodes.len())
593 .sum();
594 let mut values = LinDist3FlowOpfValues::default();
595 values.terminal_voltage_magnitude_squared = vec![0.0; problem.preparation.network.nodes.len()];
596 values.line_active_power = vec![0.0; line_count];
597 values.line_reactive_power = vec![0.0; line_count];
598 values.generator_active_power = vec![0.0; generator_count];
599 values.generator_reactive_power = vec![0.0; generator_count];
600 values.source_active_power = vec![0.0; source_count];
601 values.source_reactive_power = vec![0.0; source_count];
602 for (column, variable) in problem.variables.iter().enumerate() {
603 let value = primal[column];
604 match &variable.variable {
605 LinDist3FlowDecisionVariable::SquaredVoltage { node } => {
606 *values
607 .terminal_voltage_magnitude_squared
608 .get_mut(*node)
609 .ok_or_else(|| invalid(format!("unknown voltage node {node}")))? = value;
610 }
611 LinDist3FlowDecisionVariable::Power(power) => match power {
612 LinDist3FlowVariable::LineActive { line, conductor } => {
613 let position = line_offsets
614 .get(*line)
615 .copied()
616 .and_then(|offset| offset.checked_add(*conductor))
617 .ok_or_else(|| invalid("unknown line active-power index"))?;
618 *values
619 .line_active_power
620 .get_mut(position)
621 .ok_or_else(|| invalid("unknown line active-power conductor"))? = value;
622 }
623 LinDist3FlowVariable::LineReactive { line, conductor } => {
624 let position = line_offsets
625 .get(*line)
626 .copied()
627 .and_then(|offset| offset.checked_add(*conductor))
628 .ok_or_else(|| invalid("unknown line reactive-power index"))?;
629 *values
630 .line_reactive_power
631 .get_mut(position)
632 .ok_or_else(|| invalid("unknown line reactive-power conductor"))? = value;
633 }
634 LinDist3FlowVariable::GeneratorActive { generator, channel } => {
635 let position = generator_offsets
636 .get(*generator)
637 .copied()
638 .and_then(|offset| offset.checked_add(*channel))
639 .ok_or_else(|| invalid("unknown generator active-power index"))?;
640 *values
641 .generator_active_power
642 .get_mut(position)
643 .ok_or_else(|| invalid("unknown generator active-power channel"))? = value;
644 }
645 LinDist3FlowVariable::GeneratorReactive { generator, channel } => {
646 let position = generator_offsets
647 .get(*generator)
648 .copied()
649 .and_then(|offset| offset.checked_add(*channel))
650 .ok_or_else(|| invalid("unknown generator reactive-power index"))?;
651 *values
652 .generator_reactive_power
653 .get_mut(position)
654 .ok_or_else(|| invalid("unknown generator reactive-power channel"))? =
655 value;
656 }
657 LinDist3FlowVariable::SourceActive { source, channel } => {
658 let position = source_offsets
659 .get(*source)
660 .copied()
661 .and_then(|offset| offset.checked_add(*channel))
662 .ok_or_else(|| invalid("unknown source active-power index"))?;
663 *values
664 .source_active_power
665 .get_mut(position)
666 .ok_or_else(|| invalid("unknown source active-power channel"))? = value;
667 }
668 LinDist3FlowVariable::SourceReactive { source, channel } => {
669 let position = source_offsets
670 .get(*source)
671 .copied()
672 .and_then(|offset| offset.checked_add(*channel))
673 .ok_or_else(|| invalid("unknown source reactive-power index"))?;
674 *values
675 .source_reactive_power
676 .get_mut(position)
677 .ok_or_else(|| invalid("unknown source reactive-power channel"))? = value;
678 }
679 },
680 }
681 }
682 Ok(values)
683}
684
685#[cfg(test)]
686mod tests {
687 use approx::assert_relative_eq;
688 use powerio_dist::{
689 Configuration, DistBus, DistGenerator, DistLine, DistLineCode, MulticonductorNetwork,
690 VoltageSource,
691 };
692 use powerio_prob::{LinDist3FlowBuildOptions, LinDist3FlowOpfInstance};
693
694 use super::*;
695
696 fn instance() -> LinDist3FlowOpfInstance {
697 let terminal = vec!["1".to_owned()];
698 let mut network = MulticonductorNetwork::named("conic");
699 let mut source_bus = DistBus::new("source", terminal.clone());
700 source_bus.v_min = Some(220.0);
701 source_bus.v_max = Some(240.0);
702 network.buses_mut().push(source_bus);
703 let mut load_bus = DistBus::new("load", terminal.clone());
704 load_bus.v_min = Some(210.0);
705 load_bus.v_max = Some(240.0);
706 network.buses_mut().push(load_bus);
707 let mut code = DistLineCode::new("one", vec![vec![0.1]], vec![vec![0.2]]);
708 code.i_max = Some(vec![10.0]);
709 code.s_max = Some(vec![2_000.0]);
710 network.line_codes_mut().push(code);
711 network.lines_mut().push(DistLine::new(
712 "line",
713 "source",
714 "load",
715 terminal.clone(),
716 terminal.clone(),
717 "one",
718 1.0,
719 ));
720 let mut source =
721 VoltageSource::new("grid", "source", terminal.clone(), vec![230.0], vec![0.0]);
722 source.energy_cost_rate = Some(vec![0.3]);
723 network.sources_mut().push(source);
724 let mut generator = DistGenerator::new(
725 "pv",
726 "load",
727 terminal,
728 Configuration::Wye,
729 vec![500.0],
730 vec![0.0],
731 );
732 generator.p_min = Some(vec![0.0]);
733 generator.p_max = Some(vec![1_000.0]);
734 generator.q_min = Some(vec![-500.0]);
735 generator.q_max = Some(vec![500.0]);
736 generator.s_max = Some(vec![1_100.0]);
737 generator.i_max = Some(vec![5.0]);
738 generator.cost = Some(vec![0.1]);
739 network.generators_mut().push(generator);
740 LinDist3FlowOpfInstance::from_network(network, LinDist3FlowBuildOptions::default()).unwrap()
741 }
742
743 fn coefficient(expression: &LinDist3FlowLinearExpression, column: usize) -> f64 {
744 expression
745 .terms
746 .iter()
747 .find(|term| term.column == column)
748 .map_or(0.0, |term| term.coefficient)
749 }
750
751 #[test]
752 fn assembly_has_stable_variables_equalities_and_objective() {
753 let problem = build_lindist3flow_conic_problem(&instance()).unwrap();
754 assert_eq!(problem.variables.len(), 8);
755 assert_eq!(problem.equalities.len(), 5);
756 assert_eq!(
757 problem.variables[0].variable,
758 LinDist3FlowDecisionVariable::SquaredVoltage { node: 0 }
759 );
760 assert_eq!(problem.variables[0].lower, Some(230.0f64.powi(2)));
761 assert_eq!(problem.variables[0].upper, Some(230.0f64.powi(2)));
762 assert_relative_eq!(problem.variables[4].objective_coefficient, 0.1 / 1000.0);
763 assert_relative_eq!(problem.variables[6].objective_coefficient, 0.3 / 1000.0);
764 assert_eq!(
765 problem.equalities[0].origin,
766 LinDist3FlowEqualityOrigin::LineDrop {
767 line: 0,
768 conductor: 0
769 }
770 );
771 }
772
773 #[test]
774 fn line_drop_and_balances_reference_canonical_columns() {
775 let problem = build_lindist3flow_conic_problem(&instance()).unwrap();
776 let drop = &problem.equalities[0].expression;
777 assert_relative_eq!(coefficient(drop, 0), -1.0);
778 assert_relative_eq!(coefficient(drop, 1), 1.0);
779 assert_relative_eq!(coefficient(drop, 2), 0.2);
780 assert_relative_eq!(coefficient(drop, 3), 0.4);
781
782 let source_active = &problem.equalities[1].expression;
783 assert_relative_eq!(coefficient(source_active, 2), -1.0);
784 assert_relative_eq!(coefficient(source_active, 6), 1.0);
785 let load_active = &problem.equalities[3].expression;
786 assert_relative_eq!(coefficient(load_active, 2), 1.0);
787 assert_relative_eq!(coefficient(load_active, 4), 1.0);
788 }
789
790 #[test]
791 fn line_and_generator_limits_become_native_cones() {
792 let problem = build_lindist3flow_conic_problem(&instance()).unwrap();
793 assert_eq!(problem.cones.len(), 5);
794 let LinDist3FlowCone::SecondOrder { origin, arguments } = &problem.cones[0] else {
795 panic!("first limit should be a standard SOC")
796 };
797 assert_eq!(
798 origin,
799 &LinDist3FlowConeOrigin::LineApparentPower {
800 line: 0,
801 conductor: 0
802 }
803 );
804 assert_relative_eq!(arguments[0].constant, 2_000.0);
805
806 let LinDist3FlowCone::RotatedSecondOrder { origin, arguments } = &problem.cones[1] else {
807 panic!("line current should use a rotated SOC")
808 };
809 assert!(matches!(
810 origin,
811 LinDist3FlowConeOrigin::LineCurrent { node: 0, .. }
812 ));
813 assert_relative_eq!(arguments[1].constant, 1_150.0);
814 assert_relative_eq!(coefficient(&arguments[0], 0), 10.0 / 230.0);
815
816 let LinDist3FlowCone::RotatedSecondOrder { origin, arguments } = &problem.cones[4] else {
817 panic!("generator current should use a rotated SOC")
818 };
819 assert_eq!(
820 origin,
821 &LinDist3FlowConeOrigin::GeneratorCurrent {
822 generator: 0,
823 channel: 0
824 }
825 );
826 assert_relative_eq!(arguments[1].constant, 575.0);
827 }
828
829 #[test]
830 fn canonical_primal_translates_to_physical_table_order() {
831 let problem = build_lindist3flow_conic_problem(&instance()).unwrap();
832 let primal = (0..problem.variables.len())
833 .map(|column| column as f64 + 10.0)
834 .collect::<Vec<_>>();
835 let values = lindist3flow_values_from_primal(&problem, &primal).unwrap();
836 assert_eq!(values.terminal_voltage_magnitude_squared, vec![10.0, 11.0]);
837 assert_eq!(values.line_active_power, vec![12.0]);
838 assert_eq!(values.line_reactive_power, vec![13.0]);
839 assert_eq!(values.generator_active_power, vec![14.0]);
840 assert_eq!(values.generator_reactive_power, vec![15.0]);
841 assert_eq!(values.source_active_power, vec![16.0]);
842 assert_eq!(values.source_reactive_power, vec![17.0]);
843 assert!(lindist3flow_values_from_primal(&problem, &primal[..7]).is_err());
844 }
845}