1use std::collections::BTreeMap;
4
5use num_complex::Complex64;
6use powerio_dist::{Configuration, DistLoad, DistLoadVoltageModel};
7use powerio_prob::{LinDist3FlowOpfInstance, ObjectiveTerm};
8
9use crate::{
10 ConnectionPowerMap, Error, LinDist3FlowNetworkData, Result, build_lindist3flow_network_data,
11 connection_power_map, cross_voltage_coefficients, winding_voltage_coefficients,
12};
13
14#[derive(Clone, Copy, Debug, PartialEq)]
16#[non_exhaustive]
17pub struct LinDist3FlowVoltageTerm {
18 pub node: usize,
19 pub coefficient: f64,
20}
21
22#[derive(Clone, Debug, Default, PartialEq)]
24#[non_exhaustive]
25pub struct LinDist3FlowAffineExpression {
26 pub constant: f64,
27 pub voltage_terms: Vec<LinDist3FlowVoltageTerm>,
28}
29
30#[derive(Clone, Debug, Default, PartialEq)]
32#[non_exhaustive]
33pub struct LinDist3FlowComplexAffinePower {
34 pub active: LinDist3FlowAffineExpression,
35 pub reactive: LinDist3FlowAffineExpression,
36}
37
38#[derive(Clone, Debug, PartialEq)]
40#[non_exhaustive]
41pub struct LinDist3FlowLoadData {
42 pub load: String,
43 pub source_load_row: usize,
44 pub terminal_nodes: Vec<usize>,
45 pub incidence: Vec<Vec<f64>>,
46 pub terminal_power_map: ConnectionPowerMap,
47 pub channel_power: Vec<LinDist3FlowComplexAffinePower>,
48}
49
50#[derive(Clone, Debug, PartialEq)]
52#[non_exhaustive]
53pub struct LinDist3FlowShuntData {
54 pub shunt: String,
55 pub source_shunt_row: usize,
56 pub terminal_nodes: Vec<usize>,
57 pub terminal_power: Vec<LinDist3FlowComplexAffinePower>,
58}
59
60#[derive(Clone, Debug, PartialEq)]
62#[non_exhaustive]
63pub struct LinDist3FlowDispatchChannel {
64 pub active_min: Option<f64>,
65 pub active_max: Option<f64>,
66 pub reactive_min: Option<f64>,
67 pub reactive_max: Option<f64>,
68 pub apparent_power_limit: Option<f64>,
69 pub current_limit: Option<f64>,
70 pub squared_winding_voltage: LinDist3FlowAffineExpression,
72 pub reference_winding_voltage: f64,
73 pub active_objective_coefficient: f64,
75}
76
77#[derive(Clone, Debug, PartialEq)]
79#[non_exhaustive]
80pub struct LinDist3FlowGeneratorData {
81 pub generator: String,
82 pub source_generator_row: usize,
83 pub terminal_nodes: Vec<usize>,
84 pub incidence: Vec<Vec<f64>>,
85 pub terminal_power_map: ConnectionPowerMap,
86 pub channels: Vec<LinDist3FlowDispatchChannel>,
87}
88
89#[derive(Clone, Debug, PartialEq)]
92#[non_exhaustive]
93pub struct LinDist3FlowSourceData {
94 pub source: String,
95 pub source_source_row: usize,
96 pub terminal_nodes: Vec<usize>,
97 pub active_objective_coefficient: Vec<f64>,
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
102#[non_exhaustive]
103pub enum LinDist3FlowVariable {
104 LineActive { line: usize, conductor: usize },
105 LineReactive { line: usize, conductor: usize },
106 GeneratorActive { generator: usize, channel: usize },
107 GeneratorReactive { generator: usize, channel: usize },
108 SourceActive { source: usize, channel: usize },
109 SourceReactive { source: usize, channel: usize },
110}
111
112#[derive(Clone, Debug, PartialEq)]
114#[non_exhaustive]
115pub struct LinDist3FlowBalanceTerm {
116 pub variable: LinDist3FlowVariable,
117 pub coefficient: f64,
118}
119
120#[derive(Clone, Debug, Default, PartialEq)]
122#[non_exhaustive]
123pub struct LinDist3FlowBalanceEquation {
124 pub affine: LinDist3FlowAffineExpression,
125 pub variable_terms: Vec<LinDist3FlowBalanceTerm>,
126}
127
128#[derive(Clone, Debug, PartialEq)]
130#[non_exhaustive]
131pub struct LinDist3FlowBalanceRow {
132 pub node: usize,
133 pub active: LinDist3FlowBalanceEquation,
134 pub reactive: LinDist3FlowBalanceEquation,
135}
136
137#[derive(Clone, Debug, PartialEq)]
139#[non_exhaustive]
140pub struct LinDist3FlowDeviceData {
141 pub loads: Vec<LinDist3FlowLoadData>,
142 pub shunts: Vec<LinDist3FlowShuntData>,
143 pub generators: Vec<LinDist3FlowGeneratorData>,
144 pub sources: Vec<LinDist3FlowSourceData>,
145 pub balances: Vec<LinDist3FlowBalanceRow>,
146}
147
148#[derive(Clone, Debug, PartialEq)]
150#[non_exhaustive]
151pub struct LinDist3FlowPreparation {
152 pub network: LinDist3FlowNetworkData,
153 pub devices: LinDist3FlowDeviceData,
154}
155
156fn invalid(reason: impl Into<String>) -> Error {
157 Error::InvalidLinDist3FlowCoefficients {
158 reason: reason.into(),
159 }
160}
161
162fn node_key(bus: &str, terminal: &str) -> (String, String) {
163 (bus.to_ascii_lowercase(), terminal.to_owned())
164}
165
166pub fn lindist3flow_connection_incidence(
172 configuration: Configuration,
173 terminal_count: usize,
174 channel_count: usize,
175) -> Result<Vec<Vec<f64>>> {
176 match configuration {
177 Configuration::SinglePhase | Configuration::Delta
178 if terminal_count == 2 && channel_count == 1 =>
179 {
180 Ok(vec![vec![1.0, -1.0]])
181 }
182 Configuration::Wye | Configuration::SinglePhase if terminal_count == channel_count => {
183 Ok((0..terminal_count)
184 .map(|row| {
185 (0..terminal_count)
186 .map(|column| f64::from(row == column))
187 .collect()
188 })
189 .collect())
190 }
191 Configuration::Delta if terminal_count == 3 && channel_count == 3 => Ok(vec![
192 vec![1.0, -1.0, 0.0],
193 vec![0.0, 1.0, -1.0],
194 vec![-1.0, 0.0, 1.0],
195 ]),
196 _ => Err(invalid(format!(
197 "{configuration:?} connection with {terminal_count} terminals and {channel_count} channels is unsupported"
198 ))),
199 }
200}
201
202fn resolve_nodes(
203 positions: &BTreeMap<(String, String), usize>,
204 bus: &str,
205 terminals: &[String],
206 family: &str,
207 name: &str,
208) -> Result<Vec<usize>> {
209 terminals
210 .iter()
211 .map(|terminal| {
212 positions
213 .get(&node_key(bus, terminal))
214 .copied()
215 .ok_or_else(|| {
216 invalid(format!(
217 "{family} `{name}` names unknown terminal `{bus}/{terminal}`"
218 ))
219 })
220 })
221 .collect()
222}
223
224fn references(instance: &LinDist3FlowOpfInstance, nodes: &[usize]) -> Vec<Complex64> {
225 nodes
226 .iter()
227 .map(|&node| {
228 let reference = &instance.reference().voltages[node];
229 Complex64::from_polar(reference.magnitude, reference.angle)
230 })
231 .collect()
232}
233
234fn global_affine(
235 constant: f64,
236 scale: f64,
237 local: &crate::AffineScalarCoefficients,
238 nodes: &[usize],
239) -> LinDist3FlowAffineExpression {
240 LinDist3FlowAffineExpression {
241 constant: constant + scale * local.constant,
242 voltage_terms: local
243 .coefficients
244 .iter()
245 .zip(nodes)
246 .filter_map(|(coefficient, node)| {
247 let coefficient = scale * coefficient;
248 (coefficient.abs() > f64::EPSILON).then_some(LinDist3FlowVoltageTerm {
249 node: *node,
250 coefficient,
251 })
252 })
253 .collect(),
254 }
255}
256
257fn channel_value(values: &[f64], channel: usize, channels: usize, label: &str) -> Result<f64> {
258 let value = match values.len() {
259 1 => values[0],
260 length if length == channels => values[channel],
261 length => {
262 return Err(invalid(format!(
263 "{label} has length {length}, expected 1 or {channels}"
264 )));
265 }
266 };
267 if !value.is_finite() {
268 return Err(invalid(format!("{label} entry {channel} is non-finite")));
269 }
270 Ok(value)
271}
272
273fn load_channel_power(
274 load: &DistLoad,
275 incidence: &[Vec<f64>],
276 reference: &[Complex64],
277 terminal_nodes: &[usize],
278 channel: usize,
279) -> Result<LinDist3FlowComplexAffinePower> {
280 let channels = load.p_nom.len();
281 let p_nom = load.p_nom[channel];
282 let q_nom = load.q_nom[channel];
283 if !p_nom.is_finite() || !q_nom.is_finite() {
284 return Err(invalid(format!(
285 "load `{}` channel {channel} nominal power is non-finite",
286 load.name
287 )));
288 }
289 if matches!(
290 load.voltage_model,
291 DistLoadVoltageModel::ConstantPower { .. }
292 ) {
293 return Ok(LinDist3FlowComplexAffinePower {
294 active: LinDist3FlowAffineExpression {
295 constant: p_nom,
296 voltage_terms: Vec::new(),
297 },
298 reactive: LinDist3FlowAffineExpression {
299 constant: q_nom,
300 voltage_terms: Vec::new(),
301 },
302 });
303 }
304 let winding = winding_voltage_coefficients(&incidence[channel], reference)?;
305 let (v_nom, alpha_z, alpha_p, beta_z, beta_p) = match &load.voltage_model {
306 DistLoadVoltageModel::ConstantImpedance { v_nom } => (
307 channel_value(v_nom, channel, channels, "load v_nom")?,
308 1.0,
309 0.0,
310 1.0,
311 0.0,
312 ),
313 DistLoadVoltageModel::Zip {
314 v_nom,
315 alpha_z,
316 alpha_i,
317 alpha_p,
318 beta_z,
319 beta_i,
320 beta_p,
321 } => {
322 let alpha_i = channel_value(alpha_i, channel, channels, "load alpha_i")?;
323 let beta_i = channel_value(beta_i, channel, channels, "load beta_i")?;
324 if alpha_i.abs() > f64::EPSILON || beta_i.abs() > f64::EPSILON {
325 return Err(invalid(format!(
326 "load `{}` channel {channel} has a nonzero current fraction",
327 load.name
328 )));
329 }
330 (
331 channel_value(v_nom, channel, channels, "load v_nom")?,
332 channel_value(alpha_z, channel, channels, "load alpha_z")?,
333 channel_value(alpha_p, channel, channels, "load alpha_p")?,
334 channel_value(beta_z, channel, channels, "load beta_z")?,
335 channel_value(beta_p, channel, channels, "load beta_p")?,
336 )
337 }
338 _ => {
339 return Err(invalid(format!(
340 "load `{}` voltage model is outside the supported ZP slice",
341 load.name
342 )));
343 }
344 };
345 if !v_nom.is_finite() || v_nom <= 0.0 {
346 return Err(invalid(format!(
347 "load `{}` channel {channel} v_nom must be finite and positive",
348 load.name
349 )));
350 }
351 Ok(LinDist3FlowComplexAffinePower {
352 active: global_affine(
353 p_nom * alpha_p,
354 p_nom * alpha_z / v_nom.powi(2),
355 &winding,
356 terminal_nodes,
357 ),
358 reactive: global_affine(
359 q_nom * beta_p,
360 q_nom * beta_z / v_nom.powi(2),
361 &winding,
362 terminal_nodes,
363 ),
364 })
365}
366
367fn shunt_terminal_power(
368 name: &str,
369 g: &[Vec<f64>],
370 b: &[Vec<f64>],
371 reference: &[Complex64],
372 terminal_nodes: &[usize],
373 terminal: usize,
374) -> Result<LinDist3FlowComplexAffinePower> {
375 let n = terminal_nodes.len();
376 if g.len() != n || b.len() != n {
377 return Err(invalid(format!(
378 "shunt `{name}` admittance does not have {n} rows"
379 )));
380 }
381 let mut active = LinDist3FlowAffineExpression::default();
382 let mut reactive = LinDist3FlowAffineExpression::default();
383 for other in 0..n {
384 if g[terminal].len() != n || b[terminal].len() != n {
385 return Err(invalid(format!(
386 "shunt `{name}` admittance row {terminal} does not have {n} entries"
387 )));
388 }
389 let admittance = Complex64::new(g[terminal][other], b[terminal][other]);
390 if !admittance.re.is_finite() || !admittance.im.is_finite() {
391 return Err(invalid(format!(
392 "shunt `{name}` admittance entry ({terminal}, {other}) is non-finite"
393 )));
394 }
395 let cross = cross_voltage_coefficients(reference[terminal], reference[other])?;
396 let scale = admittance.conj();
397 let constant = scale * cross.constant;
398 let own = scale * cross.coefficient_phi;
399 let other_coefficient = scale * cross.coefficient_psi;
400 active.constant += constant.re;
401 reactive.constant += constant.im;
402 active.voltage_terms.push(LinDist3FlowVoltageTerm {
403 node: terminal_nodes[terminal],
404 coefficient: own.re,
405 });
406 reactive.voltage_terms.push(LinDist3FlowVoltageTerm {
407 node: terminal_nodes[terminal],
408 coefficient: own.im,
409 });
410 active.voltage_terms.push(LinDist3FlowVoltageTerm {
411 node: terminal_nodes[other],
412 coefficient: other_coefficient.re,
413 });
414 reactive.voltage_terms.push(LinDist3FlowVoltageTerm {
415 node: terminal_nodes[other],
416 coefficient: other_coefficient.im,
417 });
418 }
419 Ok(LinDist3FlowComplexAffinePower { active, reactive })
420}
421
422fn checked_vector_value(
423 values: Option<&[f64]>,
424 channel: usize,
425 channels: usize,
426 label: &str,
427) -> Result<Option<f64>> {
428 let Some(values) = values else {
429 return Ok(None);
430 };
431 if values.len() != channels {
432 return Err(invalid(format!(
433 "{label} has length {}, expected {channels}",
434 values.len()
435 )));
436 }
437 let value = values[channel];
438 if !value.is_finite() {
439 return Err(invalid(format!("{label} entry {channel} is non-finite")));
440 }
441 Ok(Some(value))
442}
443
444fn positive_limit(
445 values: Option<&[f64]>,
446 channel: usize,
447 channels: usize,
448 label: &str,
449) -> Result<Option<f64>> {
450 let value = checked_vector_value(values, channel, channels, label)?;
451 if value.is_some_and(|value| value <= 0.0) {
452 return Err(invalid(format!("{label} entry {channel} must be positive")));
453 }
454 Ok(value)
455}
456
457fn generator_bounds(
458 nominal: f64,
459 lower: Option<&[f64]>,
460 upper: Option<&[f64]>,
461 channel: usize,
462 channels: usize,
463 label: &str,
464 selected: bool,
465) -> Result<(Option<f64>, Option<f64>)> {
466 if !selected {
467 return Ok((None, None));
468 }
469 let lower = checked_vector_value(lower, channel, channels, &format!("{label} minimum"))?;
470 let upper = checked_vector_value(upper, channel, channels, &format!("{label} maximum"))?;
471 let (lower, upper) = if lower.is_none() && upper.is_none() {
472 if !nominal.is_finite() {
473 return Err(invalid(format!("{label} nominal value is non-finite")));
474 }
475 (Some(nominal), Some(nominal))
476 } else {
477 (lower, upper)
478 };
479 if lower.zip(upper).is_some_and(|(lower, upper)| lower > upper) {
480 return Err(invalid(format!("{label} bounds are inverted")));
481 }
482 Ok((lower, upper))
483}
484
485fn cost_value(values: Option<&[f64]>, channel: usize, channels: usize, label: &str) -> Result<f64> {
486 let Some(values) = values else {
487 return Ok(0.0);
488 };
489 Ok(channel_value(values, channel, channels, label)? / 1000.0)
490}
491
492fn uses_dispatch_cost(instance: &LinDist3FlowOpfInstance) -> Result<bool> {
493 let mut enabled = false;
494 for term in instance.base_instance().objective().terms() {
495 match term {
496 ObjectiveTerm::ActivePowerDispatchCost => enabled = true,
497 ObjectiveTerm::NetworkGeneratorCost => {
498 return Err(invalid(
499 "LinDist3Flow does not compile balanced-network generator cost curves",
500 ));
501 }
502 _ => {
503 return Err(invalid(
504 "the LinDist3Flow objective contains an unknown term",
505 ));
506 }
507 }
508 }
509 Ok(enabled)
510}
511
512fn add_affine(
513 target: &mut LinDist3FlowAffineExpression,
514 source: &LinDist3FlowAffineExpression,
515 scale: f64,
516) {
517 target.constant += scale * source.constant;
518 target
519 .voltage_terms
520 .extend(source.voltage_terms.iter().filter_map(|term| {
521 let coefficient = scale * term.coefficient;
522 (coefficient.abs() > f64::EPSILON).then_some(LinDist3FlowVoltageTerm {
523 node: term.node,
524 coefficient,
525 })
526 }));
527}
528
529fn add_variable(
530 equation: &mut LinDist3FlowBalanceEquation,
531 variable: LinDist3FlowVariable,
532 coefficient: f64,
533) {
534 if coefficient.abs() > f64::EPSILON {
535 equation.variable_terms.push(LinDist3FlowBalanceTerm {
536 variable,
537 coefficient,
538 });
539 }
540}
541
542#[allow(clippy::too_many_lines)]
553pub fn build_lindist3flow_device_data(
554 instance: &LinDist3FlowOpfInstance,
555 network_data: &LinDist3FlowNetworkData,
556) -> Result<LinDist3FlowDeviceData> {
557 let network = instance.network();
558 let positions = network_data
559 .nodes
560 .iter()
561 .enumerate()
562 .map(|(position, node)| (node_key(&node.node.bus, &node.node.terminal), position))
563 .collect::<BTreeMap<_, _>>();
564 let dispatch_cost = uses_dispatch_cost(instance)?;
565
566 let mut loads = Vec::with_capacity(network.loads().len());
567 for (row, load) in network.loads().iter().enumerate() {
568 if load.p_nom.len() != load.q_nom.len() || load.p_nom.is_empty() {
569 return Err(invalid(format!(
570 "load `{}` active/reactive channel counts are empty or unequal",
571 load.name
572 )));
573 }
574 let terminal_nodes = resolve_nodes(
575 &positions,
576 &load.bus,
577 &load.terminal_map,
578 "load",
579 &load.name,
580 )?;
581 let incidence = lindist3flow_connection_incidence(
582 load.configuration,
583 terminal_nodes.len(),
584 load.p_nom.len(),
585 )?;
586 let reference = references(instance, &terminal_nodes);
587 let terminal_power_map = connection_power_map(&incidence, &reference)?;
588 let channel_power = (0..load.p_nom.len())
589 .map(|channel| {
590 load_channel_power(load, &incidence, &reference, &terminal_nodes, channel)
591 })
592 .collect::<Result<Vec<_>>>()?;
593 loads.push(LinDist3FlowLoadData {
594 load: load.name.clone(),
595 source_load_row: row,
596 terminal_nodes,
597 incidence,
598 terminal_power_map,
599 channel_power,
600 });
601 }
602
603 let mut shunts = Vec::with_capacity(network.shunts().len());
604 for (row, shunt) in network.shunts().iter().enumerate() {
605 let terminal_nodes = resolve_nodes(
606 &positions,
607 &shunt.bus,
608 &shunt.terminal_map,
609 "shunt",
610 &shunt.name,
611 )?;
612 let reference = references(instance, &terminal_nodes);
613 let terminal_power = (0..terminal_nodes.len())
614 .map(|terminal| {
615 shunt_terminal_power(
616 &shunt.name,
617 &shunt.g,
618 &shunt.b,
619 &reference,
620 &terminal_nodes,
621 terminal,
622 )
623 })
624 .collect::<Result<Vec<_>>>()?;
625 shunts.push(LinDist3FlowShuntData {
626 shunt: shunt.name.clone(),
627 source_shunt_row: row,
628 terminal_nodes,
629 terminal_power,
630 });
631 }
632
633 let mut generators = Vec::with_capacity(network.generators().len());
634 for (row, generator) in network.generators().iter().enumerate() {
635 let channels = generator.p_nom.len();
636 if channels == 0 || generator.q_nom.len() != channels {
637 return Err(invalid(format!(
638 "generator `{}` active/reactive channel counts are empty or unequal",
639 generator.name
640 )));
641 }
642 let terminal_nodes = resolve_nodes(
643 &positions,
644 &generator.bus,
645 &generator.terminal_map,
646 "generator",
647 &generator.name,
648 )?;
649 let incidence = lindist3flow_connection_incidence(
650 generator.configuration,
651 terminal_nodes.len(),
652 channels,
653 )?;
654 let reference = references(instance, &terminal_nodes);
655 let terminal_power_map = connection_power_map(&incidence, &reference)?;
656 let capability_selected = instance
657 .base_instance()
658 .constraints()
659 .generator_capability
660 .selects(&generator.name);
661 let mut prepared_channels = Vec::with_capacity(channels);
662 for (channel, incidence_row) in incidence.iter().enumerate() {
663 let winding = winding_voltage_coefficients(incidence_row, &reference)?;
664 let squared_winding_voltage = global_affine(0.0, 1.0, &winding, &terminal_nodes);
665 let reference_winding_voltage =
666 terminal_power_map.reference_winding_voltage[channel].norm();
667 let (active_min, active_max) = generator_bounds(
668 generator.p_nom[channel],
669 generator.p_min.as_deref(),
670 generator.p_max.as_deref(),
671 channel,
672 channels,
673 "generator active power",
674 capability_selected,
675 )?;
676 let (reactive_min, reactive_max) = generator_bounds(
677 generator.q_nom[channel],
678 generator.q_min.as_deref(),
679 generator.q_max.as_deref(),
680 channel,
681 channels,
682 "generator reactive power",
683 capability_selected,
684 )?;
685 prepared_channels.push(LinDist3FlowDispatchChannel {
686 active_min,
687 active_max,
688 reactive_min,
689 reactive_max,
690 apparent_power_limit: capability_selected
691 .then(|| {
692 positive_limit(
693 generator.s_max.as_deref(),
694 channel,
695 channels,
696 "generator apparent-power limit",
697 )
698 })
699 .transpose()?
700 .flatten(),
701 current_limit: capability_selected
702 .then(|| {
703 positive_limit(
704 generator.i_max.as_deref(),
705 channel,
706 channels,
707 "generator current limit",
708 )
709 })
710 .transpose()?
711 .flatten(),
712 squared_winding_voltage,
713 reference_winding_voltage,
714 active_objective_coefficient: if dispatch_cost {
715 cost_value(
716 generator.cost.as_deref(),
717 channel,
718 channels,
719 "generator cost",
720 )?
721 } else {
722 0.0
723 },
724 });
725 }
726 generators.push(LinDist3FlowGeneratorData {
727 generator: generator.name.clone(),
728 source_generator_row: row,
729 terminal_nodes,
730 incidence,
731 terminal_power_map,
732 channels: prepared_channels,
733 });
734 }
735
736 let mut sources = Vec::with_capacity(network.sources().len());
737 for (row, source) in network.sources().iter().enumerate() {
738 let terminal_nodes = resolve_nodes(
739 &positions,
740 &source.bus,
741 &source.terminal_map,
742 "voltage source",
743 &source.name,
744 )?;
745 let channels = terminal_nodes.len();
746 let active_objective_coefficient = (0..channels)
747 .map(|channel| {
748 if dispatch_cost {
749 cost_value(
750 source.energy_cost_rate.as_deref(),
751 channel,
752 channels,
753 "voltage source energy cost",
754 )
755 } else {
756 Ok(0.0)
757 }
758 })
759 .collect::<Result<Vec<_>>>()?;
760 sources.push(LinDist3FlowSourceData {
761 source: source.name.clone(),
762 source_source_row: row,
763 terminal_nodes,
764 active_objective_coefficient,
765 });
766 }
767
768 let mut balances = (0..network_data.nodes.len())
769 .map(|node| LinDist3FlowBalanceRow {
770 node,
771 active: LinDist3FlowBalanceEquation::default(),
772 reactive: LinDist3FlowBalanceEquation::default(),
773 })
774 .collect::<Vec<_>>();
775 for (line_index, line) in network_data.lines.iter().enumerate() {
776 for conductor in 0..line.parent_nodes.len() {
777 add_variable(
778 &mut balances[line.parent_nodes[conductor]].active,
779 LinDist3FlowVariable::LineActive {
780 line: line_index,
781 conductor,
782 },
783 -1.0,
784 );
785 add_variable(
786 &mut balances[line.parent_nodes[conductor]].reactive,
787 LinDist3FlowVariable::LineReactive {
788 line: line_index,
789 conductor,
790 },
791 -1.0,
792 );
793 add_variable(
794 &mut balances[line.child_nodes[conductor]].active,
795 LinDist3FlowVariable::LineActive {
796 line: line_index,
797 conductor,
798 },
799 1.0,
800 );
801 add_variable(
802 &mut balances[line.child_nodes[conductor]].reactive,
803 LinDist3FlowVariable::LineReactive {
804 line: line_index,
805 conductor,
806 },
807 1.0,
808 );
809 }
810 }
811 for load in &loads {
812 for terminal in 0..load.terminal_nodes.len() {
813 let balance = &mut balances[load.terminal_nodes[terminal]];
814 for channel in 0..load.channel_power.len() {
815 let power = &load.channel_power[channel];
816 let real = load.terminal_power_map.real_part[terminal][channel];
817 let imag = load.terminal_power_map.imag_part[terminal][channel];
818 add_affine(&mut balance.active.affine, &power.active, -real);
819 add_affine(&mut balance.active.affine, &power.reactive, imag);
820 add_affine(&mut balance.reactive.affine, &power.active, -imag);
821 add_affine(&mut balance.reactive.affine, &power.reactive, -real);
822 }
823 }
824 }
825 for shunt in &shunts {
826 for (terminal, &node) in shunt.terminal_nodes.iter().enumerate() {
827 add_affine(
828 &mut balances[node].active.affine,
829 &shunt.terminal_power[terminal].active,
830 -1.0,
831 );
832 add_affine(
833 &mut balances[node].reactive.affine,
834 &shunt.terminal_power[terminal].reactive,
835 -1.0,
836 );
837 }
838 }
839 for (generator_index, generator) in generators.iter().enumerate() {
840 for terminal in 0..generator.terminal_nodes.len() {
841 let balance = &mut balances[generator.terminal_nodes[terminal]];
842 for channel in 0..generator.channels.len() {
843 let real = generator.terminal_power_map.real_part[terminal][channel];
844 let imag = generator.terminal_power_map.imag_part[terminal][channel];
845 add_variable(
846 &mut balance.active,
847 LinDist3FlowVariable::GeneratorActive {
848 generator: generator_index,
849 channel,
850 },
851 real,
852 );
853 add_variable(
854 &mut balance.active,
855 LinDist3FlowVariable::GeneratorReactive {
856 generator: generator_index,
857 channel,
858 },
859 -imag,
860 );
861 add_variable(
862 &mut balance.reactive,
863 LinDist3FlowVariable::GeneratorActive {
864 generator: generator_index,
865 channel,
866 },
867 imag,
868 );
869 add_variable(
870 &mut balance.reactive,
871 LinDist3FlowVariable::GeneratorReactive {
872 generator: generator_index,
873 channel,
874 },
875 real,
876 );
877 }
878 }
879 }
880 for (source_index, source) in sources.iter().enumerate() {
881 for (channel, &node) in source.terminal_nodes.iter().enumerate() {
882 add_variable(
883 &mut balances[node].active,
884 LinDist3FlowVariable::SourceActive {
885 source: source_index,
886 channel,
887 },
888 1.0,
889 );
890 add_variable(
891 &mut balances[node].reactive,
892 LinDist3FlowVariable::SourceReactive {
893 source: source_index,
894 channel,
895 },
896 1.0,
897 );
898 }
899 }
900
901 Ok(LinDist3FlowDeviceData {
902 loads,
903 shunts,
904 generators,
905 sources,
906 balances,
907 })
908}
909
910pub fn build_lindist3flow_preparation(
916 instance: &LinDist3FlowOpfInstance,
917) -> Result<LinDist3FlowPreparation> {
918 let network = build_lindist3flow_network_data(instance)?;
919 let devices = build_lindist3flow_device_data(instance, &network)?;
920 Ok(LinDist3FlowPreparation { network, devices })
921}
922
923#[cfg(test)]
924mod tests {
925 use std::f64::consts::PI;
926
927 use approx::assert_relative_eq;
928 use powerio_dist::{
929 Configuration, DistBus, DistGenerator, DistLine, DistLineCode, DistLoad, DistShunt,
930 MulticonductorNetwork, VoltageSource,
931 };
932 use powerio_prob::{
933 ConstraintSelection, LinDist3FlowBuildOptions, LinDist3FlowOpfInstance, McAcOpfInstance,
934 MulticonductorActiveConstraints,
935 };
936
937 use super::*;
938
939 fn one_phase_network() -> MulticonductorNetwork {
940 let terminal = vec!["1".to_owned()];
941 let mut network = MulticonductorNetwork::named("one-phase");
942 network
943 .buses_mut()
944 .push(DistBus::new("source", terminal.clone()));
945 network
946 .buses_mut()
947 .push(DistBus::new("load", terminal.clone()));
948 network
949 .line_codes_mut()
950 .push(DistLineCode::new("one", vec![vec![0.1]], vec![vec![0.2]]));
951 network.lines_mut().push(DistLine::new(
952 "line",
953 "source",
954 "load",
955 terminal.clone(),
956 terminal.clone(),
957 "one",
958 1.0,
959 ));
960 network.sources_mut().push(VoltageSource::new(
961 "grid",
962 "source",
963 terminal,
964 vec![230.0],
965 vec![0.0],
966 ));
967 network
968 }
969
970 fn three_phase_network() -> MulticonductorNetwork {
971 let terminals = vec!["1".to_owned(), "2".to_owned(), "3".to_owned()];
972 let mut network = MulticonductorNetwork::named("three-phase");
973 network
974 .buses_mut()
975 .push(DistBus::new("source", terminals.clone()));
976 network
977 .buses_mut()
978 .push(DistBus::new("load", terminals.clone()));
979 network.line_codes_mut().push(DistLineCode::new(
980 "three",
981 vec![
982 vec![0.1, 0.0, 0.0],
983 vec![0.0, 0.1, 0.0],
984 vec![0.0, 0.0, 0.1],
985 ],
986 vec![
987 vec![0.2, 0.0, 0.0],
988 vec![0.0, 0.2, 0.0],
989 vec![0.0, 0.0, 0.2],
990 ],
991 ));
992 network.lines_mut().push(DistLine::new(
993 "line",
994 "source",
995 "load",
996 terminals.clone(),
997 terminals.clone(),
998 "three",
999 1.0,
1000 ));
1001 network.sources_mut().push(VoltageSource::new(
1002 "grid",
1003 "source",
1004 terminals,
1005 vec![230.0; 3],
1006 vec![0.0, -2.0 * PI / 3.0, 2.0 * PI / 3.0],
1007 ));
1008 network
1009 }
1010
1011 fn evaluate(expression: &LinDist3FlowAffineExpression, squared_voltage: &[f64]) -> f64 {
1012 expression.constant
1013 + expression
1014 .voltage_terms
1015 .iter()
1016 .map(|term| term.coefficient * squared_voltage[term.node])
1017 .sum::<f64>()
1018 }
1019
1020 #[test]
1021 fn connection_incidence_matches_supported_physical_channels() {
1022 assert_eq!(
1023 lindist3flow_connection_incidence(Configuration::SinglePhase, 2, 1).unwrap(),
1024 [vec![1.0, -1.0]]
1025 );
1026 assert_eq!(
1027 lindist3flow_connection_incidence(Configuration::Wye, 2, 2).unwrap(),
1028 [vec![1.0, 0.0], vec![0.0, 1.0]]
1029 );
1030 assert_eq!(
1031 lindist3flow_connection_incidence(Configuration::Delta, 3, 3).unwrap()[2],
1032 [-1.0, 0.0, 1.0]
1033 );
1034 assert!(lindist3flow_connection_incidence(Configuration::Delta, 3, 1).is_err());
1035 }
1036
1037 #[test]
1038 fn constant_power_load_and_line_flows_have_the_reference_kcl_signs() {
1039 let mut network = one_phase_network();
1040 network.loads_mut().push(DistLoad::new(
1041 "demand",
1042 "load",
1043 vec!["1".to_owned()],
1044 Configuration::Wye,
1045 vec![1_000.0],
1046 vec![200.0],
1047 ));
1048 let instance =
1049 LinDist3FlowOpfInstance::from_network(network, LinDist3FlowBuildOptions::default())
1050 .unwrap();
1051 let preparation = build_lindist3flow_preparation(&instance).unwrap();
1052
1053 let source = &preparation.devices.balances[0];
1054 assert!(
1055 source
1056 .active
1057 .variable_terms
1058 .contains(&LinDist3FlowBalanceTerm {
1059 variable: LinDist3FlowVariable::SourceActive {
1060 source: 0,
1061 channel: 0,
1062 },
1063 coefficient: 1.0,
1064 })
1065 );
1066 assert!(
1067 source
1068 .active
1069 .variable_terms
1070 .contains(&LinDist3FlowBalanceTerm {
1071 variable: LinDist3FlowVariable::LineActive {
1072 line: 0,
1073 conductor: 0,
1074 },
1075 coefficient: -1.0,
1076 })
1077 );
1078 let load = &preparation.devices.balances[1];
1079 assert_relative_eq!(load.active.affine.constant, -1_000.0, epsilon = 1e-12);
1080 assert_relative_eq!(load.reactive.affine.constant, -200.0, epsilon = 1e-12);
1081 assert!(
1082 load.active
1083 .variable_terms
1084 .contains(&LinDist3FlowBalanceTerm {
1085 variable: LinDist3FlowVariable::LineActive {
1086 line: 0,
1087 conductor: 0,
1088 },
1089 coefficient: 1.0,
1090 })
1091 );
1092 }
1093
1094 #[test]
1095 fn delta_zp_load_is_affine_and_exact_at_the_reference() {
1096 let mut network = three_phase_network();
1097 let mut load = DistLoad::new(
1098 "delta",
1099 "load",
1100 vec!["1".to_owned(), "2".to_owned(), "3".to_owned()],
1101 Configuration::Delta,
1102 vec![1_000.0, 2_000.0, 3_000.0],
1103 vec![100.0, 200.0, 300.0],
1104 );
1105 load.voltage_model = DistLoadVoltageModel::Zip {
1106 v_nom: vec![230.0 * 3.0_f64.sqrt()],
1107 alpha_z: vec![0.4],
1108 alpha_i: vec![0.0],
1109 alpha_p: vec![0.6],
1110 beta_z: vec![0.25],
1111 beta_i: vec![0.0],
1112 beta_p: vec![0.75],
1113 };
1114 network.loads_mut().push(load);
1115 let instance =
1116 LinDist3FlowOpfInstance::from_network(network, LinDist3FlowBuildOptions::default())
1117 .unwrap();
1118 let preparation = build_lindist3flow_preparation(&instance).unwrap();
1119 let squared_voltage = preparation
1120 .network
1121 .nodes
1122 .iter()
1123 .map(|node| node.reference_magnitude.powi(2))
1124 .collect::<Vec<_>>();
1125 let load = &preparation.devices.loads[0];
1126
1127 for channel in 0..3 {
1128 assert_relative_eq!(
1129 evaluate(&load.channel_power[channel].active, &squared_voltage),
1130 [1_000.0, 2_000.0, 3_000.0][channel],
1131 epsilon = 1e-9
1132 );
1133 let allocation_sum = (0..3)
1134 .map(|terminal| load.terminal_power_map.matrix[terminal][channel])
1135 .sum::<Complex64>();
1136 assert_relative_eq!(allocation_sum.re, 1.0, epsilon = 1e-12);
1137 assert_relative_eq!(allocation_sum.im, 0.0, epsilon = 1e-12);
1138 }
1139 }
1140
1141 #[test]
1142 fn generator_rows_keep_bounds_cost_soc_voltage_and_terminal_map() {
1143 let mut network = one_phase_network();
1144 let mut generator = DistGenerator::new(
1145 "der",
1146 "load",
1147 vec!["1".to_owned()],
1148 Configuration::Wye,
1149 vec![500.0],
1150 vec![0.0],
1151 );
1152 generator.p_min = Some(vec![0.0]);
1153 generator.p_max = Some(vec![1_000.0]);
1154 generator.q_min = Some(vec![-300.0]);
1155 generator.q_max = Some(vec![300.0]);
1156 generator.cost = Some(vec![0.2]);
1157 generator.s_max = Some(vec![1_100.0]);
1158 generator.i_max = Some(vec![5.0]);
1159 network.generators_mut().push(generator);
1160 let instance =
1161 LinDist3FlowOpfInstance::from_network(network, LinDist3FlowBuildOptions::default())
1162 .unwrap();
1163 let preparation = build_lindist3flow_preparation(&instance).unwrap();
1164 let channel = &preparation.devices.generators[0].channels[0];
1165
1166 assert_eq!(channel.active_min, Some(0.0));
1167 assert_eq!(channel.active_max, Some(1_000.0));
1168 assert_eq!(channel.reactive_min, Some(-300.0));
1169 assert_eq!(channel.reactive_max, Some(300.0));
1170 assert_eq!(channel.apparent_power_limit, Some(1_100.0));
1171 assert_eq!(channel.current_limit, Some(5.0));
1172 assert_relative_eq!(
1173 channel.active_objective_coefficient,
1174 0.0002,
1175 epsilon = 1e-15
1176 );
1177 assert_relative_eq!(channel.reference_winding_voltage, 230.0, epsilon = 1e-12);
1178 assert_relative_eq!(
1179 evaluate(
1180 &channel.squared_winding_voltage,
1181 &[230.0_f64.powi(2), 230.0_f64.powi(2)]
1182 ),
1183 230.0_f64.powi(2),
1184 epsilon = 1e-9
1185 );
1186 }
1187
1188 #[test]
1189 fn shunt_power_enters_balance_as_an_affine_absorption() {
1190 let mut network = one_phase_network();
1191 network.shunts_mut().push(DistShunt::new(
1192 "capacitive",
1193 "load",
1194 vec!["1".to_owned()],
1195 vec![vec![0.0]],
1196 vec![vec![0.01]],
1197 ));
1198 let instance =
1199 LinDist3FlowOpfInstance::from_network(network, LinDist3FlowBuildOptions::default())
1200 .unwrap();
1201 let preparation = build_lindist3flow_preparation(&instance).unwrap();
1202 let squared_voltage = vec![230.0_f64.powi(2); 2];
1203 let shunt = &preparation.devices.shunts[0].terminal_power[0];
1204
1205 assert_relative_eq!(
1206 evaluate(&shunt.active, &squared_voltage),
1207 0.0,
1208 epsilon = 1e-9
1209 );
1210 assert_relative_eq!(
1211 evaluate(&shunt.reactive, &squared_voltage),
1212 -0.01 * 230.0_f64.powi(2),
1213 epsilon = 1e-9
1214 );
1215 assert_relative_eq!(
1216 evaluate(
1217 &preparation.devices.balances[1].reactive.affine,
1218 &squared_voltage
1219 ),
1220 0.01 * 230.0_f64.powi(2),
1221 epsilon = 1e-9
1222 );
1223 }
1224
1225 #[test]
1226 fn inactive_constraint_families_remove_only_their_numerical_limits() {
1227 let mut network = one_phase_network();
1228 network.buses_mut()[1].v_min = Some(210.0);
1229 network.lines_mut()[0].i_max = Some(vec![100.0]);
1230 let mut generator = DistGenerator::new(
1231 "der",
1232 "load",
1233 vec!["1".to_owned()],
1234 Configuration::Wye,
1235 vec![500.0],
1236 vec![0.0],
1237 );
1238 generator.p_min = Some(vec![0.0]);
1239 generator.p_max = Some(vec![1_000.0]);
1240 network.generators_mut().push(generator);
1241 let mut constraints = MulticonductorActiveConstraints::default();
1242 constraints.terminal_voltage_bounds = ConstraintSelection::None;
1243 constraints.conductor_limits = ConstraintSelection::None;
1244 constraints.generator_capability = ConstraintSelection::None;
1245 let base = McAcOpfInstance::from_network(network)
1246 .unwrap()
1247 .with_constraints(constraints);
1248 let instance =
1249 LinDist3FlowOpfInstance::from_mc_ac(base, LinDist3FlowBuildOptions::default()).unwrap();
1250 let preparation = build_lindist3flow_preparation(&instance).unwrap();
1251
1252 assert!(preparation.network.nodes[1].squared_voltage_min.is_none());
1253 assert_eq!(preparation.network.lines[0].current_limit, [None]);
1254 assert!(
1255 preparation.devices.generators[0].channels[0]
1256 .active_min
1257 .is_none()
1258 );
1259 }
1260}