1use std::collections::{BTreeMap, BTreeSet};
8
9use serde::{Deserialize, Serialize};
10use serde_json::{Value, json};
11
12use crate::{
13 Configuration, DistCapacitor, DistGenerator, DistIbr, DistLine, DistLineCode,
14 DistLoadVoltageModel, DistShunt, Error, IbrPrimeMover, IbrTopology, MulticonductorNetwork,
15 NeutralKronOptions, NeutralKronReport, Result, neutral_kron_reduce,
16};
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[serde(rename_all = "snake_case")]
23#[non_exhaustive]
24pub enum LinDist3FlowPreparationPolicy {
25 #[default]
27 Reject,
28 Lower,
30 Approximate,
32 Permissive,
34}
35
36impl LinDist3FlowPreparationPolicy {
37 const fn level(self) -> u8 {
38 match self {
39 Self::Reject => 0,
40 Self::Lower => 1,
41 Self::Approximate => 2,
42 Self::Permissive => 3,
43 }
44 }
45
46 const fn at_least(self, required: Self) -> bool {
47 self.level() >= required.level()
48 }
49
50 #[must_use]
51 pub const fn as_str(self) -> &'static str {
52 match self {
53 Self::Reject => "reject",
54 Self::Lower => "lower",
55 Self::Approximate => "approximate",
56 Self::Permissive => "permissive",
57 }
58 }
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
63#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
64#[serde(rename_all = "snake_case")]
65#[non_exhaustive]
66pub enum LinDist3FlowPreparationActionKind {
67 NeutralKronReduced,
68 OpenSwitchOmitted,
69 ClosedSwitchLowered,
70 LineShuntLowered,
71 CapacitorLowered,
72 LoadApproximated,
73 IbrApproximated,
74 StaticControlFrozen,
75 UntypedObjectOmitted,
76 InitialPointOmitted,
77}
78
79impl LinDist3FlowPreparationActionKind {
80 #[must_use]
81 pub const fn as_str(self) -> &'static str {
82 match self {
83 Self::NeutralKronReduced => "neutral_kron_reduced",
84 Self::OpenSwitchOmitted => "open_switch_omitted",
85 Self::ClosedSwitchLowered => "closed_switch_lowered",
86 Self::LineShuntLowered => "line_shunt_lowered",
87 Self::CapacitorLowered => "capacitor_lowered",
88 Self::LoadApproximated => "load_approximated",
89 Self::IbrApproximated => "ibr_approximated",
90 Self::StaticControlFrozen => "static_control_frozen",
91 Self::UntypedObjectOmitted => "untyped_object_omitted",
92 Self::InitialPointOmitted => "initial_point_omitted",
93 }
94 }
95
96 #[must_use]
99 pub const fn is_approximation(self) -> bool {
100 matches!(
101 self,
102 Self::LoadApproximated
103 | Self::IbrApproximated
104 | Self::StaticControlFrozen
105 | Self::UntypedObjectOmitted
106 | Self::InitialPointOmitted
107 )
108 }
109
110 #[must_use]
113 pub const fn is_omission(self) -> bool {
114 matches!(
115 self,
116 Self::OpenSwitchOmitted
117 | Self::StaticControlFrozen
118 | Self::UntypedObjectOmitted
119 | Self::InitialPointOmitted
120 )
121 }
122}
123
124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
126#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
127#[non_exhaustive]
128pub struct LinDist3FlowPreparationAction {
129 pub kind: LinDist3FlowPreparationActionKind,
130 pub source: String,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub target: Option<String>,
133 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
134 pub details: BTreeMap<String, Value>,
135}
136
137impl LinDist3FlowPreparationAction {
138 #[must_use]
141 pub fn new(
142 kind: LinDist3FlowPreparationActionKind,
143 source: impl Into<String>,
144 target: Option<String>,
145 ) -> Self {
146 Self {
147 kind,
148 source: source.into(),
149 target,
150 details: BTreeMap::new(),
151 }
152 }
153}
154
155#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
157#[non_exhaustive]
158pub struct LinDist3FlowPreparationReport {
159 pub policy: LinDist3FlowPreparationPolicy,
160 pub actions: Vec<LinDist3FlowPreparationAction>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub neutral_kron: Option<NeutralKronReport>,
163}
164
165#[derive(Clone, Debug)]
167pub struct LinDist3FlowPreparedNetwork {
168 network: MulticonductorNetwork,
169 report: LinDist3FlowPreparationReport,
170}
171
172impl LinDist3FlowPreparedNetwork {
173 #[must_use]
174 pub fn network(&self) -> &MulticonductorNetwork {
175 &self.network
176 }
177
178 #[must_use]
179 pub const fn report(&self) -> &LinDist3FlowPreparationReport {
180 &self.report
181 }
182
183 #[must_use]
184 pub fn into_parts(self) -> (MulticonductorNetwork, LinDist3FlowPreparationReport) {
185 (self.network, self.report)
186 }
187}
188
189fn fail(message: impl Into<String>) -> Error {
190 Error::LinDist3FlowPreparation {
191 message: message.into(),
192 }
193}
194
195fn unique_name(prefix: &str, names: &mut BTreeSet<String>) -> String {
196 let mut candidate = prefix.to_owned();
197 let mut suffix = 2usize;
198 while !names.insert(candidate.to_ascii_lowercase()) {
199 candidate = format!("{prefix}-{suffix}");
200 suffix += 1;
201 }
202 candidate
203}
204
205fn zero_matrix(n: usize) -> Vec<Vec<f64>> {
206 vec![vec![0.0; n]; n]
207}
208
209fn scaled(matrix: &[Vec<f64>], scale: f64) -> Vec<Vec<f64>> {
210 matrix
211 .iter()
212 .map(|row| row.iter().map(|value| value * scale).collect())
213 .collect()
214}
215
216fn nonzero(matrix: &[Vec<f64>]) -> bool {
217 matrix.iter().flatten().any(|value| *value != 0.0)
218}
219
220fn lower_line_shunts(
221 network: &mut MulticonductorNetwork,
222 report: &mut LinDist3FlowPreparationReport,
223) -> Result<()> {
224 let lines = network.lines().clone();
225 let mut names = network
226 .shunts()
227 .iter()
228 .map(|shunt| shunt.name.to_ascii_lowercase())
229 .collect::<BTreeSet<_>>();
230 let mut additions = Vec::new();
231 for line in &lines {
232 let code = network.linecode(&line.linecode).ok_or_else(|| {
233 fail(format!(
234 "line `{}` names missing linecode `{}`",
235 line.name, line.linecode
236 ))
237 })?;
238 let n = line.terminal_map_from.len();
239 if line.terminal_map_to.len() != n
240 || [&code.g_from, &code.b_from, &code.g_to, &code.b_to]
241 .into_iter()
242 .any(|matrix| matrix.len() != n || matrix.iter().any(|row| row.len() != n))
243 {
244 return Err(fail(format!(
245 "line `{}` shunt matrices do not align with its terminal maps",
246 line.name
247 )));
248 }
249 for (side, bus, terminals, g, b) in [
250 (
251 "from",
252 &line.bus_from,
253 &line.terminal_map_from,
254 &code.g_from,
255 &code.b_from,
256 ),
257 (
258 "to",
259 &line.bus_to,
260 &line.terminal_map_to,
261 &code.g_to,
262 &code.b_to,
263 ),
264 ] {
265 if !nonzero(g) && !nonzero(b) {
266 continue;
267 }
268 let name = unique_name(
269 &format!("__l3f-line-{}-{side}-shunt", line.name),
270 &mut names,
271 );
272 additions.push(DistShunt::new(
273 &name,
274 bus,
275 terminals.clone(),
276 scaled(g, line.length),
277 scaled(b, line.length),
278 ));
279 report.actions.push(LinDist3FlowPreparationAction::new(
280 LinDist3FlowPreparationActionKind::LineShuntLowered,
281 format!("line.{}", line.name),
282 Some(format!("shunt.{name}")),
283 ));
284 }
285 }
286 network.shunts_mut().extend(additions);
287 for code in network.line_codes_mut() {
288 code.g_from = zero_matrix(code.n_conductors);
289 code.b_from = zero_matrix(code.n_conductors);
290 code.g_to = zero_matrix(code.n_conductors);
291 code.b_to = zero_matrix(code.n_conductors);
292 }
293 Ok(())
294}
295
296fn lower_switches(
297 network: &mut MulticonductorNetwork,
298 report: &mut LinDist3FlowPreparationReport,
299) -> Result<()> {
300 let switches = std::mem::take(network.switches_mut());
301 let mut line_names = network
302 .lines()
303 .iter()
304 .map(|line| line.name.to_ascii_lowercase())
305 .collect::<BTreeSet<_>>();
306 let mut code_names = network
307 .line_codes()
308 .iter()
309 .map(|code| code.name.to_ascii_lowercase())
310 .collect::<BTreeSet<_>>();
311 for switch in switches {
312 let source = format!("switch.{}", switch.name);
313 if switch.open {
314 report.actions.push(LinDist3FlowPreparationAction::new(
315 LinDist3FlowPreparationActionKind::OpenSwitchOmitted,
316 source,
317 None,
318 ));
319 continue;
320 }
321 let n = switch.terminal_map_from.len();
322 if n == 0 || switch.terminal_map_to.len() != n {
323 return Err(fail(format!(
324 "closed switch `{}` has empty or unequal terminal maps",
325 switch.name
326 )));
327 }
328 if switch
329 .i_max
330 .as_ref()
331 .is_some_and(|limits| limits.len() != n)
332 {
333 return Err(fail(format!(
334 "closed switch `{}` current limits do not align with its terminal maps",
335 switch.name
336 )));
337 }
338 let line_name = unique_name(&format!("__l3f-switch-{}", switch.name), &mut line_names);
339 let code_name = unique_name(
340 &format!("__l3f-switch-{}-ideal", switch.name),
341 &mut code_names,
342 );
343 network.line_codes_mut().push(DistLineCode::new(
344 &code_name,
345 zero_matrix(n),
346 zero_matrix(n),
347 ));
348 let mut line = DistLine::new(
349 &line_name,
350 switch.bus_from,
351 switch.bus_to,
352 switch.terminal_map_from,
353 switch.terminal_map_to,
354 code_name,
355 1.0,
356 );
357 line.i_max = switch.i_max;
358 network.lines_mut().push(line);
359 let mut action = LinDist3FlowPreparationAction::new(
360 LinDist3FlowPreparationActionKind::ClosedSwitchLowered,
361 source,
362 Some(format!("line.{line_name}")),
363 );
364 action.details.insert(
365 "flow_identity".to_owned(),
366 json!("independent_contact_flow"),
367 );
368 report.actions.push(action);
369 }
370 Ok(())
371}
372
373fn capacitor_matrix(capacitor: &DistCapacitor) -> Result<Vec<Vec<f64>>> {
374 if !capacitor.q_rated.is_finite()
375 || !capacitor.v_nom.is_finite()
376 || capacitor.v_nom <= 0.0
377 || capacitor.terminal_map.is_empty()
378 {
379 return Err(fail(format!(
380 "capacitor `{}` has invalid rating, nominal voltage, or terminal map",
381 capacitor.name
382 )));
383 }
384 let n = capacitor.terminal_map.len();
385 let mut matrix = zero_matrix(n);
386 let mut couple = |a: usize, b: usize, value: f64| {
387 matrix[a][a] += value;
388 matrix[b][b] += value;
389 matrix[a][b] -= value;
390 matrix[b][a] -= value;
391 };
392 match capacitor.configuration {
393 Configuration::Wye if n == 1 => {
395 matrix[0][0] = capacitor.q_rated / capacitor.v_nom.powi(2);
396 }
397 Configuration::Wye => {
400 let phases = n - 1;
401 let v_ln = capacitor.v_nom / 3f64.sqrt();
402 #[allow(clippy::cast_precision_loss)]
403 let value = capacitor.q_rated / (phases as f64) / v_ln.powi(2);
404 for phase in 0..phases {
405 couple(phase, phases, value);
406 }
407 }
408 Configuration::SinglePhase if n == 2 => {
409 couple(0, 1, capacitor.q_rated / capacitor.v_nom.powi(2));
410 }
411 Configuration::Delta if n == 3 => {
412 let value = capacitor.q_rated / (3.0 * capacitor.v_nom.powi(2));
413 for pair in 0..3 {
414 couple(pair, (pair + 1) % 3, value);
415 }
416 }
417 _ => {
418 return Err(fail(format!(
419 "capacitor `{}` {:?} connection with {n} terminals is not supported",
420 capacitor.name, capacitor.configuration
421 )));
422 }
423 }
424 Ok(matrix)
425}
426
427fn lower_capacitors(
428 network: &mut MulticonductorNetwork,
429 report: &mut LinDist3FlowPreparationReport,
430) -> Result<()> {
431 let capacitors = std::mem::take(network.capacitors_mut());
432 let mut names = network
433 .shunts()
434 .iter()
435 .map(|shunt| shunt.name.to_ascii_lowercase())
436 .collect::<BTreeSet<_>>();
437 for capacitor in capacitors {
438 let matrix = capacitor_matrix(&capacitor)?;
439 let name = unique_name(&format!("__l3f-capacitor-{}", capacitor.name), &mut names);
440 network.shunts_mut().push(DistShunt::new(
441 &name,
442 &capacitor.bus,
443 capacitor.terminal_map.clone(),
444 zero_matrix(matrix.len()),
445 matrix,
446 ));
447 report.actions.push(LinDist3FlowPreparationAction::new(
448 LinDist3FlowPreparationActionKind::CapacitorLowered,
449 format!("capacitor.{}", capacitor.name),
450 Some(format!("shunt.{name}")),
451 ));
452 }
453 Ok(())
454}
455
456fn approximate_loads(
457 network: &mut MulticonductorNetwork,
458 report: &mut LinDist3FlowPreparationReport,
459) -> Result<()> {
460 let expand = |values: &[f64], channels: usize, label: &str| -> Result<Vec<f64>> {
461 match values.len() {
462 1 => Ok(vec![values[0]; channels]),
463 length if length == channels => Ok(values.to_vec()),
464 length => Err(fail(format!(
465 "{label} has length {length}, expected 1 or {channels}"
466 ))),
467 }
468 };
469 for load in network.loads_mut() {
470 let approximation = match &load.voltage_model {
471 DistLoadVoltageModel::ConstantCurrent { v_nom } => Some(DistLoadVoltageModel::Zip {
472 v_nom: v_nom.clone(),
473 alpha_z: vec![0.5; load.p_nom.len()],
474 alpha_i: vec![0.0; load.p_nom.len()],
475 alpha_p: vec![0.5; load.p_nom.len()],
476 beta_z: vec![0.5; load.q_nom.len()],
477 beta_i: vec![0.0; load.q_nom.len()],
478 beta_p: vec![0.5; load.q_nom.len()],
479 }),
480 DistLoadVoltageModel::Zip {
481 v_nom,
482 alpha_z,
483 alpha_i,
484 alpha_p,
485 beta_z,
486 beta_i,
487 beta_p,
488 } if alpha_i.iter().any(|value| value.abs() > f64::EPSILON)
489 || beta_i.iter().any(|value| value.abs() > f64::EPSILON) =>
490 {
491 let combine = |z: &[f64],
492 i: &[f64],
493 p: &[f64],
494 channels: usize|
495 -> Result<(Vec<f64>, Vec<f64>)> {
496 let z = expand(z, channels, "ZIP impedance fraction")?;
497 let i = expand(i, channels, "ZIP current fraction")?;
498 let p = expand(p, channels, "ZIP power fraction")?;
499 Ok((
500 z.iter().zip(&i).map(|(z, i)| z + 0.5 * i).collect(),
501 p.iter().zip(&i).map(|(p, i)| p + 0.5 * i).collect(),
502 ))
503 };
504 let (alpha_z, alpha_p) = combine(alpha_z, alpha_i, alpha_p, load.p_nom.len())?;
505 let (beta_z, beta_p) = combine(beta_z, beta_i, beta_p, load.q_nom.len())?;
506 Some(DistLoadVoltageModel::Zip {
507 v_nom: v_nom.clone(),
508 alpha_z,
509 alpha_i: vec![0.0; load.p_nom.len()],
510 alpha_p,
511 beta_z,
512 beta_i: vec![0.0; load.q_nom.len()],
513 beta_p,
514 })
515 }
516 DistLoadVoltageModel::Exponential {
517 v_nom,
518 gamma_p,
519 gamma_q,
520 } => {
521 let gamma_p = expand(gamma_p, load.p_nom.len(), "active exponent")?;
522 let gamma_q = expand(gamma_q, load.q_nom.len(), "reactive exponent")?;
523 Some(DistLoadVoltageModel::Zip {
524 v_nom: v_nom.clone(),
525 alpha_z: gamma_p.iter().map(|gamma| 0.5 * gamma).collect(),
526 alpha_i: vec![0.0; load.p_nom.len()],
527 alpha_p: gamma_p.iter().map(|gamma| 1.0 - 0.5 * gamma).collect(),
528 beta_z: gamma_q.iter().map(|gamma| 0.5 * gamma).collect(),
529 beta_i: vec![0.0; load.q_nom.len()],
530 beta_p: gamma_q.iter().map(|gamma| 1.0 - 0.5 * gamma).collect(),
531 })
532 }
533 _ => None,
534 };
535 if let Some(approximation) = approximation {
536 load.voltage_model = approximation;
537 let mut action = LinDist3FlowPreparationAction::new(
538 LinDist3FlowPreparationActionKind::LoadApproximated,
539 format!("load.{}", load.name),
540 Some(format!("load.{}", load.name)),
541 );
542 action.details.insert(
543 "method".to_owned(),
544 json!("first_order_squared_voltage_at_nominal"),
545 );
546 report.actions.push(action);
547 }
548 }
549 Ok(())
550}
551
552fn ibr_configuration(ibr: &DistIbr) -> Result<Configuration> {
553 match (ibr.topology, ibr.terminal_map.len(), ibr.s_max.len()) {
554 (IbrTopology::SinglePhase, 1, 1) => Ok(Configuration::Wye),
555 (IbrTopology::SinglePhase, 2, 1) => Ok(Configuration::SinglePhase),
556 (IbrTopology::ThreeLeg | IbrTopology::FourLeg, terminals, phases)
557 if terminals == phases =>
558 {
559 Ok(Configuration::Wye)
560 }
561 _ => Err(fail(format!(
562 "IBR `{}` topology and terminal/rating dimensions cannot be mapped to a static generator",
563 ibr.name
564 ))),
565 }
566}
567
568fn approximate_ibrs(
569 network: &mut MulticonductorNetwork,
570 report: &mut LinDist3FlowPreparationReport,
571) -> Result<()> {
572 let ibrs = std::mem::take(network.ibrs_mut());
573 let mut names = network
574 .generators()
575 .iter()
576 .map(|generator| generator.name.to_ascii_lowercase())
577 .collect::<BTreeSet<_>>();
578 for ibr in ibrs {
579 let channels = ibr.s_max.len();
580 if channels == 0
581 || ibr
582 .s_max
583 .iter()
584 .any(|rating| !rating.is_finite() || *rating <= 0.0)
585 {
586 return Err(fail(format!(
587 "IBR `{}` has invalid phase ratings",
588 ibr.name
589 )));
590 }
591 let configuration = ibr_configuration(&ibr)?;
592 let name = unique_name(&format!("__l3f-ibr-{}", ibr.name), &mut names);
593 let total_rating = ibr.s_max.iter().sum::<f64>();
594 let p_nom = ibr.p_avail.map_or_else(
595 || vec![0.0; channels],
596 |available| {
597 ibr.s_max
598 .iter()
599 .map(|rating| available * rating / total_rating)
600 .collect()
601 },
602 );
603 let mut generator = DistGenerator::new(
604 &name,
605 &ibr.bus,
606 ibr.terminal_map.clone(),
607 configuration,
608 p_nom.clone(),
609 vec![0.0; channels],
610 );
611 generator.p_min = ibr.p_min.clone().or_else(|| {
612 (ibr.p_avail.is_some()
613 && matches!(ibr.prime_mover, IbrPrimeMover::Pv | IbrPrimeMover::Generic))
614 .then(|| vec![0.0; channels])
615 });
616 generator.p_max = ibr.p_max.clone().or_else(|| ibr.p_avail.map(|_| p_nom));
617 if generator.p_min.is_some() != generator.p_max.is_some() {
618 return Err(fail(format!(
619 "IBR `{}` supplies only one side of its active-power bounds",
620 ibr.name
621 )));
622 }
623 generator.q_min.clone_from(&ibr.q_min);
624 generator.q_max.clone_from(&ibr.q_max);
625 if generator.q_min.is_some() != generator.q_max.is_some() {
626 return Err(fail(format!(
627 "IBR `{}` supplies only one side of its reactive-power bounds",
628 ibr.name
629 )));
630 }
631 generator.s_max = Some(ibr.s_max.clone());
632 generator.i_max.clone_from(&ibr.i_max);
633 network.generators_mut().push(generator);
634 let mut action = LinDist3FlowPreparationAction::new(
635 LinDist3FlowPreparationActionKind::IbrApproximated,
636 format!("ibr.{}", ibr.name),
637 Some(format!("generator.{name}")),
638 );
639 action
640 .details
641 .insert("method".to_owned(), json!("static_dispatchable_generator"));
642 action
643 .details
644 .insert("control_profile".to_owned(), json!(&ibr.control_profile));
645 report.actions.push(action);
646 if let Some(profile) = &ibr.control_profile {
647 report.actions.push(LinDist3FlowPreparationAction::new(
648 LinDist3FlowPreparationActionKind::StaticControlFrozen,
649 format!("control_profile.{profile}"),
650 Some(format!("generator.{name}")),
651 ));
652 }
653 }
654 Ok(())
655}
656
657fn omit_controls_and_untyped(
658 network: &mut MulticonductorNetwork,
659 policy: LinDist3FlowPreparationPolicy,
660 report: &mut LinDist3FlowPreparationReport,
661) {
662 let objects = std::mem::take(network.untyped_objects_mut());
663 let mut retained = Vec::new();
664 for object in objects {
665 let control = matches!(
666 object.class.to_ascii_lowercase().as_str(),
667 "regcontrol" | "capcontrol" | "invcontrol" | "swtcontrol"
668 );
669 if control && policy.at_least(LinDist3FlowPreparationPolicy::Approximate) {
670 report.actions.push(LinDist3FlowPreparationAction::new(
671 LinDist3FlowPreparationActionKind::StaticControlFrozen,
672 format!("{}.{}", object.class, object.name),
673 None,
674 ));
675 } else if policy.at_least(LinDist3FlowPreparationPolicy::Permissive) {
676 report.actions.push(LinDist3FlowPreparationAction::new(
677 LinDist3FlowPreparationActionKind::UntypedObjectOmitted,
678 format!("{}.{}", object.class, object.name),
679 None,
680 ));
681 } else {
682 retained.push(object);
683 }
684 }
685 *network.untyped_objects_mut() = retained;
686 if policy.at_least(LinDist3FlowPreparationPolicy::Approximate) {
687 for profile in network.control_profiles() {
688 let source = format!("control_profile.{}", profile.name);
689 if !report.actions.iter().any(|action| action.source == source) {
690 report.actions.push(LinDist3FlowPreparationAction::new(
691 LinDist3FlowPreparationActionKind::StaticControlFrozen,
692 source,
693 None,
694 ));
695 }
696 }
697 network.control_profiles_mut().clear();
698 }
699}
700
701pub fn prepare_lindist3flow_network(
714 network: &MulticonductorNetwork,
715 policy: LinDist3FlowPreparationPolicy,
716) -> Result<LinDist3FlowPreparedNetwork> {
717 let mut prepared = network.clone();
718 let mut report = LinDist3FlowPreparationReport {
719 policy,
720 ..LinDist3FlowPreparationReport::default()
721 };
722 if policy.at_least(LinDist3FlowPreparationPolicy::Lower) {
723 lower_capacitors(&mut prepared, &mut report)?;
726 let reduction = neutral_kron_reduce(&prepared, &NeutralKronOptions::default())?;
727 let (network, neutral_report) = reduction.into_parts();
728 prepared = network;
729 if !neutral_report.buses.is_empty() {
730 let mut action = LinDist3FlowPreparationAction::new(
731 LinDist3FlowPreparationActionKind::NeutralKronReduced,
732 "network",
733 Some("network".to_owned()),
734 );
735 action
736 .details
737 .insert("bus_count".to_owned(), json!(neutral_report.buses.len()));
738 report.actions.push(action);
739 report.neutral_kron = Some(neutral_report);
740 }
741 lower_switches(&mut prepared, &mut report)?;
742 lower_line_shunts(&mut prepared, &mut report)?;
743 }
744 if policy.at_least(LinDist3FlowPreparationPolicy::Approximate) {
745 approximate_loads(&mut prepared, &mut report)?;
746 approximate_ibrs(&mut prepared, &mut report)?;
747 }
748 omit_controls_and_untyped(&mut prepared, policy, &mut report);
749 prepared.extras_mut().insert(
750 "powerio_lindist3flow_preparation".to_owned(),
751 json!({
752 "policy": report.policy,
753 "actions": report.actions,
754 }),
755 );
756 Ok(LinDist3FlowPreparedNetwork {
757 network: prepared,
758 report,
759 })
760}