1use powerio_prob::LinDist3FlowOpfInstance;
9
10use crate::matrix::triplet::CooBuilder;
11use crate::{
12 Error, LinDist3FlowCone, LinDist3FlowConeOrigin, LinDist3FlowConicProblem,
13 LinDist3FlowDecisionVariable, LinDist3FlowEqualityOrigin, LinDist3FlowLinearExpression, Result,
14 SparseMatrix, build_lindist3flow_conic_problem, lindist3flow_values_from_primal,
15};
16
17#[derive(Clone, Copy, Debug, PartialEq)]
19#[non_exhaustive]
20pub struct LinDist3FlowStandardFormOptions {
21 pub per_unit: bool,
23 pub apparent_power_base: f64,
25}
26
27impl LinDist3FlowStandardFormOptions {
28 #[must_use]
29 pub const fn si() -> Self {
30 Self {
31 per_unit: false,
32 apparent_power_base: 1_000_000.0,
33 }
34 }
35
36 #[must_use]
37 pub const fn per_unit(apparent_power_base: f64) -> Self {
38 Self {
39 per_unit: true,
40 apparent_power_base,
41 }
42 }
43}
44
45impl Default for LinDist3FlowStandardFormOptions {
46 fn default() -> Self {
47 Self::per_unit(1_000_000.0)
48 }
49}
50
51#[derive(Clone, Debug, PartialEq)]
56#[non_exhaustive]
57pub struct LinDist3FlowScaling {
58 pub apparent_power_base: Option<f64>,
60 pub variable_scale: Vec<f64>,
62 pub row_scale: Vec<f64>,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68#[non_exhaustive]
69pub enum LinDist3FlowStandardCone {
70 Zero { dimension: usize },
71 Nonnegative { dimension: usize },
72 SecondOrder { dimension: usize },
73}
74
75impl LinDist3FlowStandardCone {
76 #[must_use]
77 pub const fn dimension(self) -> usize {
78 match self {
79 Self::Zero { dimension }
80 | Self::Nonnegative { dimension }
81 | Self::SecondOrder { dimension } => dimension,
82 }
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum LinDist3FlowStandardRowOrigin {
90 Equality(LinDist3FlowEqualityOrigin),
91 VariableLowerBound {
92 column: usize,
93 },
94 VariableUpperBound {
95 column: usize,
96 },
97 Cone {
98 origin: LinDist3FlowConeOrigin,
99 row_in_cone: usize,
101 },
102}
103
104#[derive(Clone, Debug)]
106#[non_exhaustive]
107pub struct LinDist3FlowStandardForm {
108 pub p: SparseMatrix,
110 pub q: Vec<f64>,
112 pub a: SparseMatrix,
114 pub b: Vec<f64>,
116 pub cones: Vec<LinDist3FlowStandardCone>,
118 pub row_origins: Vec<LinDist3FlowStandardRowOrigin>,
120 pub scaling: LinDist3FlowScaling,
122 pub canonical: LinDist3FlowConicProblem,
124}
125
126fn invalid(reason: impl Into<String>) -> Error {
127 Error::InvalidLinDist3FlowCoefficients {
128 reason: reason.into(),
129 }
130}
131
132fn bound_count(problem: &LinDist3FlowConicProblem) -> usize {
133 problem
134 .variables
135 .iter()
136 .map(|variable| {
137 usize::from(variable.lower.is_some()) + usize::from(variable.upper.is_some())
138 })
139 .sum()
140}
141
142fn cone_dimension(cone: &LinDist3FlowCone) -> usize {
143 match cone {
144 LinDist3FlowCone::SecondOrder { arguments, .. }
145 | LinDist3FlowCone::RotatedSecondOrder { arguments, .. } => arguments.len(),
146 }
147}
148
149fn add_slack_expression(
150 a: &mut CooBuilder,
151 b: &mut Vec<f64>,
152 row: usize,
153 parts: &[(&LinDist3FlowLinearExpression, f64)],
154) -> Result<()> {
155 let mut constant = 0.0;
156 for (expression, scale) in parts {
157 constant += scale * expression.constant;
158 for term in &expression.terms {
159 let coefficient = -scale * term.coefficient;
160 if !coefficient.is_finite() {
161 return Err(invalid(format!(
162 "standard-form row {row} has a non-finite matrix coefficient"
163 )));
164 }
165 a.add(row, term.column, coefficient);
166 }
167 }
168 if !constant.is_finite() {
169 return Err(invalid(format!(
170 "standard-form row {row} has a non-finite right-hand side"
171 )));
172 }
173 b.push(constant);
174 Ok(())
175}
176
177fn push_cone_origins(
178 origins: &mut Vec<LinDist3FlowStandardRowOrigin>,
179 origin: &LinDist3FlowConeOrigin,
180 dimension: usize,
181) {
182 origins.extend(
183 (0..dimension).map(|row_in_cone| LinDist3FlowStandardRowOrigin::Cone {
184 origin: origin.clone(),
185 row_in_cone,
186 }),
187 );
188}
189
190#[allow(clippy::many_single_char_names, clippy::too_many_lines)]
201fn build_lindist3flow_standard_form_si(
202 instance: &LinDist3FlowOpfInstance,
203) -> Result<LinDist3FlowStandardForm> {
204 let canonical = build_lindist3flow_conic_problem(instance)?;
205 let n = canonical.variables.len();
206 let bounds = bound_count(&canonical);
207 let cone_rows: usize = canonical.cones.iter().map(cone_dimension).sum();
208 let m = canonical
209 .equalities
210 .len()
211 .checked_add(bounds)
212 .and_then(|rows| rows.checked_add(cone_rows))
213 .ok_or_else(|| invalid("LinDist3Flow standard-form row count overflows usize"))?;
214 let estimated_nnz = canonical
215 .equalities
216 .iter()
217 .map(|row| row.expression.terms.len())
218 .sum::<usize>()
219 .saturating_add(bounds)
220 .saturating_add(
221 canonical
222 .cones
223 .iter()
224 .map(|cone| match cone {
225 LinDist3FlowCone::SecondOrder { arguments, .. }
226 | LinDist3FlowCone::RotatedSecondOrder { arguments, .. } => arguments
227 .iter()
228 .map(|argument| argument.terms.len())
229 .sum::<usize>(),
230 })
231 .sum::<usize>(),
232 );
233 let p = CooBuilder::new(n).finish_csc();
234 let q = canonical
235 .variables
236 .iter()
237 .map(|variable| variable.objective_coefficient)
238 .collect();
239 let mut a = CooBuilder::with_capacity_rect(m, n, estimated_nnz);
240 let mut b = Vec::with_capacity(m);
241 let mut cones = Vec::new();
242 let mut row_origins = Vec::with_capacity(m);
243 let mut row = 0;
244
245 if !canonical.equalities.is_empty() {
246 cones.push(LinDist3FlowStandardCone::Zero {
247 dimension: canonical.equalities.len(),
248 });
249 }
250 for equality in &canonical.equalities {
251 for term in &equality.expression.terms {
252 a.add(row, term.column, term.coefficient);
253 }
254 b.push(-equality.expression.constant);
255 row_origins.push(LinDist3FlowStandardRowOrigin::Equality(
256 equality.origin.clone(),
257 ));
258 row += 1;
259 }
260
261 if bounds != 0 {
262 cones.push(LinDist3FlowStandardCone::Nonnegative { dimension: bounds });
263 }
264 for (column, variable) in canonical.variables.iter().enumerate() {
265 if let Some(lower) = variable.lower {
266 a.add(row, column, -1.0);
268 b.push(-lower);
269 row_origins.push(LinDist3FlowStandardRowOrigin::VariableLowerBound { column });
270 row += 1;
271 }
272 if let Some(upper) = variable.upper {
273 a.add(row, column, 1.0);
275 b.push(upper);
276 row_origins.push(LinDist3FlowStandardRowOrigin::VariableUpperBound { column });
277 row += 1;
278 }
279 }
280
281 for cone in &canonical.cones {
282 match cone {
283 LinDist3FlowCone::SecondOrder { origin, arguments } => {
284 if arguments.len() < 2 {
285 return Err(invalid("a second-order cone needs at least two rows"));
286 }
287 cones.push(LinDist3FlowStandardCone::SecondOrder {
288 dimension: arguments.len(),
289 });
290 push_cone_origins(&mut row_origins, origin, arguments.len());
291 for argument in arguments {
292 add_slack_expression(&mut a, &mut b, row, &[(argument, 1.0)])?;
293 row += 1;
294 }
295 }
296 LinDist3FlowCone::RotatedSecondOrder { origin, arguments } => {
297 if arguments.len() < 3 {
298 return Err(invalid(
299 "a rotated second-order cone needs at least three rows",
300 ));
301 }
302 cones.push(LinDist3FlowStandardCone::SecondOrder {
303 dimension: arguments.len(),
304 });
305 push_cone_origins(&mut row_origins, origin, arguments.len());
306 add_slack_expression(
307 &mut a,
308 &mut b,
309 row,
310 &[(&arguments[0], 1.0), (&arguments[1], 1.0)],
311 )?;
312 row += 1;
313 add_slack_expression(
314 &mut a,
315 &mut b,
316 row,
317 &[(&arguments[0], 1.0), (&arguments[1], -1.0)],
318 )?;
319 row += 1;
320 for argument in &arguments[2..] {
321 add_slack_expression(
322 &mut a,
323 &mut b,
324 row,
325 &[(argument, std::f64::consts::SQRT_2)],
326 )?;
327 row += 1;
328 }
329 }
330 }
331 }
332 debug_assert_eq!(row, m);
333 debug_assert_eq!(b.len(), m);
334 debug_assert_eq!(row_origins.len(), m);
335 debug_assert_eq!(cones.iter().map(|cone| cone.dimension()).sum::<usize>(), m);
336 Ok(LinDist3FlowStandardForm {
337 p,
338 q,
339 a: a.finish_csc(),
340 b,
341 cones,
342 row_origins,
343 scaling: LinDist3FlowScaling {
344 apparent_power_base: None,
345 variable_scale: vec![1.0; n],
346 row_scale: vec![1.0; m],
347 },
348 canonical,
349 })
350}
351
352fn per_unit_scales(
353 form: &LinDist3FlowStandardForm,
354 power_base: f64,
355) -> Result<LinDist3FlowScaling> {
356 if !power_base.is_finite() || power_base <= 0.0 {
357 return Err(invalid(
358 "LinDist3Flow apparent-power base must be finite and positive",
359 ));
360 }
361 let variable_scale = form
362 .canonical
363 .variables
364 .iter()
365 .map(|variable| match &variable.variable {
366 LinDist3FlowDecisionVariable::SquaredVoltage { node } => {
367 form.canonical.preparation.network.nodes[*node]
368 .reference_magnitude
369 .powi(2)
370 }
371 LinDist3FlowDecisionVariable::Power(_) => power_base,
372 })
373 .collect::<Vec<_>>();
374 if variable_scale
375 .iter()
376 .any(|scale| !scale.is_finite() || *scale <= 0.0)
377 {
378 return Err(invalid(
379 "LinDist3Flow variable scaling contains a non-finite or nonpositive base",
380 ));
381 }
382 let row_scale = form
383 .row_origins
384 .iter()
385 .map(|origin| match origin {
386 LinDist3FlowStandardRowOrigin::Equality(LinDist3FlowEqualityOrigin::LineDrop {
387 line,
388 conductor,
389 }) => {
390 let node = form.canonical.preparation.network.lines[*line].child_nodes[*conductor];
391 1.0 / form.canonical.preparation.network.nodes[node]
392 .reference_magnitude
393 .powi(2)
394 }
395 LinDist3FlowStandardRowOrigin::Equality(
396 LinDist3FlowEqualityOrigin::ActiveBalance { .. }
397 | LinDist3FlowEqualityOrigin::ReactiveBalance { .. },
398 )
399 | LinDist3FlowStandardRowOrigin::Cone { .. } => 1.0 / power_base,
400 LinDist3FlowStandardRowOrigin::VariableLowerBound { column }
401 | LinDist3FlowStandardRowOrigin::VariableUpperBound { column } => {
402 1.0 / variable_scale[*column]
403 }
404 })
405 .collect::<Vec<_>>();
406 Ok(LinDist3FlowScaling {
407 apparent_power_base: Some(power_base),
408 variable_scale,
409 row_scale,
410 })
411}
412
413fn apply_scaling(form: &mut LinDist3FlowStandardForm, scaling: LinDist3FlowScaling) {
414 for (column, mut entries) in form.a.outer_iterator_mut().enumerate() {
415 for (row, value) in entries.iter_mut() {
416 *value *= scaling.variable_scale[column] * scaling.row_scale[row];
417 }
418 }
419 for (coefficient, scale) in form.q.iter_mut().zip(&scaling.variable_scale) {
420 *coefficient *= scale;
421 }
422 for (right_hand_side, scale) in form.b.iter_mut().zip(&scaling.row_scale) {
423 *right_hand_side *= scale;
424 }
425 form.scaling = scaling;
426}
427
428pub fn build_lindist3flow_standard_form(
435 instance: &LinDist3FlowOpfInstance,
436) -> Result<LinDist3FlowStandardForm> {
437 build_lindist3flow_standard_form_with_options(
438 instance,
439 LinDist3FlowStandardFormOptions::default(),
440 )
441}
442
443pub fn build_lindist3flow_standard_form_with_options(
450 instance: &LinDist3FlowOpfInstance,
451 options: LinDist3FlowStandardFormOptions,
452) -> Result<LinDist3FlowStandardForm> {
453 let mut form = build_lindist3flow_standard_form_si(instance)?;
454 if options.per_unit {
455 let scaling = per_unit_scales(&form, options.apparent_power_base)?;
456 apply_scaling(&mut form, scaling);
457 }
458 Ok(form)
459}
460
461pub fn lindist3flow_values_from_standard_primal(
467 form: &LinDist3FlowStandardForm,
468 primal: &[f64],
469) -> Result<powerio_prob::LinDist3FlowOpfValues> {
470 if primal.len() != form.scaling.variable_scale.len() {
471 return Err(invalid(format!(
472 "LinDist3Flow solver primal has length {}, expected {}",
473 primal.len(),
474 form.scaling.variable_scale.len()
475 )));
476 }
477 let physical = primal
478 .iter()
479 .zip(&form.scaling.variable_scale)
480 .map(|(value, scale)| value * scale)
481 .collect::<Vec<_>>();
482 lindist3flow_values_from_primal(&form.canonical, &physical)
483}
484
485#[cfg(test)]
486mod tests {
487 use approx::assert_relative_eq;
488 use powerio_dist::{
489 Configuration, DistBus, DistGenerator, DistLine, DistLineCode, MulticonductorNetwork,
490 VoltageSource,
491 };
492 use powerio_prob::{LinDist3FlowBuildOptions, LinDist3FlowOpfInstance};
493
494 use super::*;
495
496 fn instance() -> LinDist3FlowOpfInstance {
497 let terminal = vec!["1".to_owned()];
498 let mut network = MulticonductorNetwork::named("standard");
499 let mut source_bus = DistBus::new("source", terminal.clone());
500 source_bus.v_min = Some(220.0);
501 source_bus.v_max = Some(240.0);
502 network.buses_mut().push(source_bus);
503 let mut load_bus = DistBus::new("load", terminal.clone());
504 load_bus.v_min = Some(210.0);
505 load_bus.v_max = Some(240.0);
506 network.buses_mut().push(load_bus);
507 let mut code = DistLineCode::new("one", vec![vec![0.1]], vec![vec![0.2]]);
508 code.i_max = Some(vec![10.0]);
509 code.s_max = Some(vec![2_000.0]);
510 network.line_codes_mut().push(code);
511 network.lines_mut().push(DistLine::new(
512 "line",
513 "source",
514 "load",
515 terminal.clone(),
516 terminal.clone(),
517 "one",
518 1.0,
519 ));
520 let mut source =
521 VoltageSource::new("grid", "source", terminal.clone(), vec![230.0], vec![0.0]);
522 source.energy_cost_rate = Some(vec![0.3]);
523 network.sources_mut().push(source);
524 let mut generator = DistGenerator::new(
525 "pv",
526 "load",
527 terminal,
528 Configuration::Wye,
529 vec![500.0],
530 vec![0.0],
531 );
532 generator.p_min = Some(vec![0.0]);
533 generator.p_max = Some(vec![1_000.0]);
534 generator.q_min = Some(vec![-500.0]);
535 generator.q_max = Some(vec![500.0]);
536 generator.s_max = Some(vec![1_100.0]);
537 generator.i_max = Some(vec![5.0]);
538 generator.cost = Some(vec![0.1]);
539 network.generators_mut().push(generator);
540 LinDist3FlowOpfInstance::from_network(network, LinDist3FlowBuildOptions::default()).unwrap()
541 }
542
543 fn entry(matrix: &SparseMatrix, row: usize, column: usize) -> f64 {
544 matrix.get(row, column).copied().unwrap_or_default()
545 }
546
547 fn si_form() -> LinDist3FlowStandardForm {
548 build_lindist3flow_standard_form_with_options(
549 &instance(),
550 LinDist3FlowStandardFormOptions::si(),
551 )
552 .unwrap()
553 }
554
555 #[test]
556 fn sparse_shapes_and_cone_blocks_match_clarabel_standard_form() {
557 let form = si_form();
558 assert_eq!(form.p.shape(), (8, 8));
559 assert!(form.p.is_csc());
560 assert_eq!(form.p.nnz(), 0);
561 assert_eq!(form.a.shape(), (31, 8));
562 assert!(form.a.is_csc());
563 assert_eq!(form.q.len(), 8);
564 assert_eq!(form.b.len(), 31);
565 assert_eq!(form.row_origins.len(), 31);
566 assert_eq!(
567 form.cones,
568 vec![
569 LinDist3FlowStandardCone::Zero { dimension: 5 },
570 LinDist3FlowStandardCone::Nonnegative { dimension: 8 },
571 LinDist3FlowStandardCone::SecondOrder { dimension: 3 },
572 LinDist3FlowStandardCone::SecondOrder { dimension: 4 },
573 LinDist3FlowStandardCone::SecondOrder { dimension: 4 },
574 LinDist3FlowStandardCone::SecondOrder { dimension: 3 },
575 LinDist3FlowStandardCone::SecondOrder { dimension: 4 },
576 ]
577 );
578 }
579
580 #[test]
581 fn equality_and_bound_rows_have_ax_plus_s_equals_b_signs() {
582 let form = si_form();
583 assert_relative_eq!(entry(&form.a, 0, 0), -1.0);
584 assert_relative_eq!(entry(&form.a, 0, 1), 1.0);
585 assert_relative_eq!(entry(&form.a, 0, 2), 0.2);
586 assert_relative_eq!(entry(&form.a, 0, 3), 0.4);
587 assert_relative_eq!(form.b[0], 0.0);
588
589 assert_relative_eq!(entry(&form.a, 5, 0), -1.0);
590 assert_relative_eq!(form.b[5], -230.0f64.powi(2));
591 assert_relative_eq!(entry(&form.a, 6, 0), 1.0);
592 assert_relative_eq!(form.b[6], 230.0f64.powi(2));
593 }
594
595 #[test]
596 fn rotated_current_cone_is_an_equivalent_ordinary_soc() {
597 let form = si_form();
598 let row = 16;
601 assert_relative_eq!(entry(&form.a, row, 0), -10.0 / 230.0);
602 assert_relative_eq!(form.b[row], 1_150.0);
603 assert_relative_eq!(entry(&form.a, row + 1, 0), -10.0 / 230.0);
604 assert_relative_eq!(form.b[row + 1], -1_150.0);
605 assert_relative_eq!(entry(&form.a, row + 2, 2), -std::f64::consts::SQRT_2);
606 assert_relative_eq!(entry(&form.a, row + 3, 3), -std::f64::consts::SQRT_2);
607 }
608
609 #[test]
610 fn default_per_unit_coordinates_round_trip_to_si_values() {
611 let form = build_lindist3flow_standard_form(&instance()).unwrap();
612 assert_eq!(form.scaling.apparent_power_base, Some(1_000_000.0));
613 assert_relative_eq!(form.scaling.variable_scale[0], 230.0f64.powi(2));
614 assert_relative_eq!(form.scaling.variable_scale[2], 1_000_000.0);
615
616 assert_relative_eq!(form.q[4], 100.0);
618 assert_relative_eq!(form.q[6], 300.0);
619
620 assert_relative_eq!(entry(&form.a, 5, 0), -1.0);
623 assert_relative_eq!(form.b[5], -1.0);
624 assert_relative_eq!(entry(&form.a, 6, 0), 1.0);
625 assert_relative_eq!(form.b[6], 1.0);
626
627 let primal = vec![
630 1.0,
631 228.0f64.powi(2) / 230.0f64.powi(2),
632 0.001,
633 0.0002,
634 0.0005,
635 0.0,
636 0.001,
637 0.0002,
638 ];
639 let values = lindist3flow_values_from_standard_primal(&form, &primal).unwrap();
640 assert_relative_eq!(
641 values.terminal_voltage_magnitude_squared[0],
642 230.0f64.powi(2)
643 );
644 assert_relative_eq!(
645 values.terminal_voltage_magnitude_squared[1],
646 228.0f64.powi(2)
647 );
648 assert_relative_eq!(values.line_active_power[0], 1_000.0);
649 assert_relative_eq!(values.generator_active_power[0], 500.0);
650 assert_relative_eq!(values.source_reactive_power[0], 200.0);
651 }
652
653 #[test]
654 fn per_unit_form_is_an_exact_diagonal_scaling_of_si_form() {
655 let si = si_form();
656 let per_unit = build_lindist3flow_standard_form_with_options(
657 &instance(),
658 LinDist3FlowStandardFormOptions::per_unit(2_000_000.0),
659 )
660 .unwrap();
661
662 assert_eq!(per_unit.a.shape(), si.a.shape());
663 assert_eq!(per_unit.cones, si.cones);
664 assert_eq!(per_unit.row_origins, si.row_origins);
665 for column in 0..si.a.cols() {
666 for row in 0..si.a.rows() {
667 assert_relative_eq!(
668 entry(&per_unit.a, row, column),
669 entry(&si.a, row, column)
670 * per_unit.scaling.variable_scale[column]
671 * per_unit.scaling.row_scale[row],
672 epsilon = 1e-12
673 );
674 }
675 }
676 for column in 0..si.q.len() {
677 assert_relative_eq!(
678 per_unit.q[column],
679 si.q[column] * per_unit.scaling.variable_scale[column],
680 epsilon = 1e-12
681 );
682 }
683 for row in 0..si.b.len() {
684 assert_relative_eq!(
685 per_unit.b[row],
686 si.b[row] * per_unit.scaling.row_scale[row],
687 epsilon = 1e-12
688 );
689 }
690 }
691
692 #[test]
693 fn per_unit_power_base_must_be_positive_and_finite() {
694 for power_base in [0.0, -1.0, f64::INFINITY, f64::NAN] {
695 assert!(
696 build_lindist3flow_standard_form_with_options(
697 &instance(),
698 LinDist3FlowStandardFormOptions::per_unit(power_base),
699 )
700 .is_err()
701 );
702 }
703 }
704}