1use std::collections::{HashMap, HashSet};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use thiserror::Error;
8
9use surge_network::Network;
10
11#[derive(Debug, Error)]
13pub enum SolutionError {
14 #[error("branch count mismatch: solution has {solution} branches, network has {network}")]
15 BranchCountMismatch { solution: usize, network: usize },
16 #[error(
17 "stored from-end branch flow vectors have mismatched lengths (p_from={p_from}, q_from={q_from})"
18 )]
19 BranchFlowVectorMismatch { p_from: usize, q_from: usize },
20 #[error("stored to-end branch flow vectors have mismatched lengths (p_to={p_to}, q_to={q_to})")]
21 BranchToEndFlowVectorMismatch { p_to: usize, q_to: usize },
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30pub enum AreaDispatchMethod {
31 Apf,
33 SlackBusFallback,
36 Converged,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct AreaInterchangeEntry {
43 pub area: u32,
45 pub scheduled_mw: f64,
47 pub actual_mw: f64,
49 pub error_mw: f64,
51 pub dispatch_method: AreaDispatchMethod,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct AreaInterchangeResult {
58 pub areas: Vec<AreaInterchangeEntry>,
60 pub iterations: usize,
62 pub converged: bool,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
68pub enum SolveStatus {
69 Converged,
71 MaxIterations,
73 Diverged,
75 #[default]
77 Unsolved,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
82#[serde(rename_all = "snake_case")]
83pub enum PfModel {
84 #[default]
86 Ac,
87 Dc,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Default)]
93pub struct PfSolution {
94 pub pf_model: PfModel,
96 pub status: SolveStatus,
98 pub iterations: u32,
100 #[serde(
102 serialize_with = "serialize_max_mismatch",
103 deserialize_with = "deserialize_max_mismatch"
104 )]
105 pub max_mismatch: f64,
106 pub solve_time_secs: f64,
108 pub voltage_magnitude_pu: Vec<f64>,
110 pub voltage_angle_rad: Vec<f64>,
112 pub active_power_injection_pu: Vec<f64>,
114 pub reactive_power_injection_pu: Vec<f64>,
116 pub branch_p_from_mw: Vec<f64>,
118 pub branch_p_to_mw: Vec<f64>,
120 pub branch_q_from_mvar: Vec<f64>,
122 pub branch_q_to_mvar: Vec<f64>,
124 pub bus_numbers: Vec<u32>,
126 #[serde(default, skip_serializing_if = "Vec::is_empty")]
132 pub island_ids: Vec<usize>,
133
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
143 pub q_limited_buses: Vec<u32>,
144
145 #[serde(default)]
150 pub n_q_limit_switches: u32,
151
152 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub gen_slack_contribution_mw: Vec<f64>,
165
166 #[serde(default, skip_serializing_if = "Vec::is_empty")]
173 pub convergence_history: Vec<(u32, f64)>,
174
175 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub worst_mismatch_bus: Option<u32>,
179
180 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub area_interchange: Option<AreaInterchangeResult>,
187}
188
189impl PfSolution {
190 fn validate_from_end_branch_vectors(&self) -> Result<(), SolutionError> {
191 if self.branch_p_from_mw.len() != self.branch_q_from_mvar.len() {
192 return Err(SolutionError::BranchFlowVectorMismatch {
193 p_from: self.branch_p_from_mw.len(),
194 q_from: self.branch_q_from_mvar.len(),
195 });
196 }
197 Ok(())
198 }
199
200 pub fn diverged(n_buses: usize, n_branches: usize, pf_model: PfModel) -> Self {
202 Self {
203 pf_model,
204 status: SolveStatus::Diverged,
205 voltage_magnitude_pu: vec![1.0; n_buses],
206 voltage_angle_rad: vec![0.0; n_buses],
207 active_power_injection_pu: vec![0.0; n_buses],
208 reactive_power_injection_pu: vec![0.0; n_buses],
209 branch_p_from_mw: vec![0.0; n_branches],
210 branch_p_to_mw: vec![0.0; n_branches],
211 branch_q_from_mvar: vec![0.0; n_branches],
212 branch_q_to_mvar: vec![0.0; n_branches],
213 max_mismatch: f64::INFINITY,
214 iterations: 0,
215 solve_time_secs: 0.0,
216 bus_numbers: Vec::new(),
217 island_ids: Vec::new(),
218 q_limited_buses: Vec::new(),
219 n_q_limit_switches: 0,
220 gen_slack_contribution_mw: Vec::new(),
221 convergence_history: Vec::new(),
222 worst_mismatch_bus: None,
223 area_interchange: None,
224 }
225 }
226
227 pub fn flat_start(n_buses: usize, n_branches: usize, pf_model: PfModel) -> Self {
229 let mut sol = Self::diverged(n_buses, n_branches, pf_model);
230 sol.status = SolveStatus::Unsolved;
231 sol.max_mismatch = 0.0;
232 sol
233 }
234
235 pub fn n_islands(&self) -> usize {
237 self.island_ids
238 .iter()
239 .copied()
240 .collect::<HashSet<_>>()
241 .len()
242 }
243
244 pub fn generator_reactive_power_mvar(&self, network: &Network) -> Vec<f64> {
253 let base = network.base_mva;
254 let bus_map = network.bus_index_map();
255 let load_q = network.bus_load_q_mvar_with_map(&bus_map);
256
257 let mut bus_qg: HashMap<u32, f64> = HashMap::new();
258 for (bus_idx, bus) in network.buses.iter().enumerate() {
259 let Some(&solution_bus_idx) = bus_map.get(&bus.number) else {
260 continue;
261 };
262 if solution_bus_idx >= self.reactive_power_injection_pu.len()
263 || solution_bus_idx >= self.voltage_magnitude_pu.len()
264 {
265 continue;
266 }
267 let vm = self.voltage_magnitude_pu[solution_bus_idx];
268 let qd = load_q.get(bus_idx).copied().unwrap_or(0.0);
269 let qg_bus = self.reactive_power_injection_pu[solution_bus_idx] * base + qd
270 - bus.shunt_susceptance_mvar * vm * vm;
271 bus_qg.insert(bus.number, qg_bus);
272 }
273
274 let mut bus_range: HashMap<u32, f64> = HashMap::new();
275 let mut bus_count: HashMap<u32, usize> = HashMap::new();
276 for generator in network
277 .generators
278 .iter()
279 .filter(|generator| generator.in_service)
280 {
281 *bus_range.entry(generator.bus).or_insert(0.0) +=
282 (generator.qmax - generator.qmin).max(0.0);
283 *bus_count.entry(generator.bus).or_insert(0) += 1;
284 }
285
286 network
287 .generators
288 .iter()
289 .map(|generator| {
290 if !generator.in_service {
291 return 0.0;
292 }
293 let total_qg = bus_qg.get(&generator.bus).copied().unwrap_or(0.0);
294 let total_range = bus_range.get(&generator.bus).copied().unwrap_or(0.0);
295 let generator_range = (generator.qmax - generator.qmin).max(0.0);
296 if total_range > 1e-6 {
297 total_qg * generator_range / total_range
298 } else {
299 let units_at_bus = bus_count.get(&generator.bus).copied().unwrap_or(1).max(1);
300 total_qg / units_at_bus as f64
301 }
302 })
303 .collect()
304 }
305
306 pub fn branch_apparent_power(&self) -> Vec<f64> {
310 self.validate_from_end_branch_vectors()
311 .expect("stored from-end branch flow vectors must have matching lengths");
312 self.branch_p_from_mw
313 .iter()
314 .zip(self.branch_q_from_mvar.iter())
315 .map(|(&p, &q)| (p * p + q * q).sqrt())
316 .collect()
317 }
318
319 pub fn branch_loading_pct(&self, network: &Network) -> Result<Vec<f64>, SolutionError> {
324 if self.branch_p_from_mw.len() != network.branches.len() {
325 return Err(SolutionError::BranchCountMismatch {
326 solution: self.branch_p_from_mw.len(),
327 network: network.branches.len(),
328 });
329 }
330 if self.branch_p_to_mw.len() != network.branches.len() {
331 return Err(SolutionError::BranchCountMismatch {
332 solution: self.branch_p_to_mw.len(),
333 network: network.branches.len(),
334 });
335 }
336 self.validate_from_end_branch_vectors()?;
337 if self.branch_p_to_mw.len() != self.branch_q_to_mvar.len() {
338 return Err(SolutionError::BranchToEndFlowVectorMismatch {
339 p_to: self.branch_p_to_mw.len(),
340 q_to: self.branch_q_to_mvar.len(),
341 });
342 }
343 Ok(self
344 .branch_p_from_mw
345 .iter()
346 .zip(self.branch_q_from_mvar.iter())
347 .zip(self.branch_p_to_mw.iter().zip(self.branch_q_to_mvar.iter()))
348 .zip(network.branches.iter())
349 .map(|(((p_from, q_from), (p_to, q_to)), branch)| {
350 let from_s = (*p_from * *p_from + *q_from * *q_from).sqrt();
351 let to_s = (*p_to * *p_to + *q_to * *q_to).sqrt();
352 let flow = from_s.max(to_s);
353 if branch.rating_a_mva > 0.0 {
354 flow / branch.rating_a_mva * 100.0
355 } else {
356 0.0
357 }
358 })
359 .collect())
360 }
361
362 pub fn branch_pq_flows(&self) -> Vec<(f64, f64)> {
364 self.validate_from_end_branch_vectors()
365 .expect("stored from-end branch flow vectors must have matching lengths");
366 self.branch_p_from_mw
367 .iter()
368 .zip(self.branch_q_from_mvar.iter())
369 .map(|(&p, &q)| (p, q))
370 .collect()
371 }
372}
373
374fn serialize_max_mismatch<S>(value: &f64, serializer: S) -> Result<S::Ok, S::Error>
375where
376 S: Serializer,
377{
378 if value.is_finite() {
379 serializer.serialize_some(value)
380 } else {
381 serializer.serialize_none()
382 }
383}
384
385fn deserialize_max_mismatch<'de, D>(deserializer: D) -> Result<f64, D::Error>
386where
387 D: Deserializer<'de>,
388{
389 Ok(Option::<f64>::deserialize(deserializer)?.unwrap_or(f64::INFINITY))
390}
391
392pub fn compute_branch_power_flows(
398 network: &Network,
399 voltage_magnitude_pu: &[f64],
400 voltage_angle_rad: &[f64],
401 base_mva: f64,
402) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
403 assert_eq!(
404 voltage_magnitude_pu.len(),
405 network.n_buses(),
406 "voltage_magnitude_pu has {} entries, expected {}",
407 voltage_magnitude_pu.len(),
408 network.n_buses()
409 );
410 assert_eq!(
411 voltage_angle_rad.len(),
412 network.n_buses(),
413 "voltage_angle_rad has {} entries, expected {}",
414 voltage_angle_rad.len(),
415 network.n_buses()
416 );
417
418 let n_branches = network.n_branches();
419 let bus_map = network.bus_index_map();
420 let mut pf = vec![0.0; n_branches];
421 let mut pt = vec![0.0; n_branches];
422 let mut qf = vec![0.0; n_branches];
423 let mut qt = vec![0.0; n_branches];
424
425 for (branch_idx, branch) in network.branches.iter().enumerate() {
426 if !branch.in_service {
427 continue;
428 }
429
430 let from_idx = bus_map[&branch.from_bus];
431 let to_idx = bus_map[&branch.to_bus];
432
433 let vf = voltage_magnitude_pu[from_idx];
434 let vt = voltage_magnitude_pu[to_idx];
435 let theta_ft = voltage_angle_rad[from_idx] - voltage_angle_rad[to_idx];
436 let flows = branch.power_flows_pu(vf, vt, theta_ft, 1e-40);
437
438 pf[branch_idx] = flows.p_from_pu * base_mva;
439 qf[branch_idx] = flows.q_from_pu * base_mva;
440 pt[branch_idx] = flows.p_to_pu * base_mva;
441 qt[branch_idx] = flows.q_to_pu * base_mva;
442 }
443
444 (pf, pt, qf, qt)
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use serde_json::Value;
451 use surge_network::network::branch::Branch;
452 use surge_network::network::bus::{Bus, BusType};
453 use surge_network::network::generator::Generator;
454 use surge_network::network::load::Load;
455
456 fn two_bus_network(rating_a: f64) -> Network {
457 let mut net = Network::new("test");
458 net.buses.push(Bus::new(1, BusType::Slack, 138.0));
459 net.buses.push(Bus::new(2, BusType::PQ, 138.0));
460 let mut br = Branch::new_line(1, 2, 0.01, 0.1, 0.0);
461 br.rating_a_mva = rating_a;
462 net.branches.push(br);
463 net
464 }
465
466 fn two_bus_solution(
467 pf_model: PfModel,
468 p_from_mw: f64,
469 q_from_mvar: f64,
470 p_to_mw: f64,
471 q_to_mvar: f64,
472 ) -> PfSolution {
473 PfSolution {
474 pf_model,
475 status: SolveStatus::Converged,
476 voltage_magnitude_pu: vec![1.0, 1.0],
477 voltage_angle_rad: vec![0.0, 0.0],
478 active_power_injection_pu: vec![0.0, 0.0],
479 reactive_power_injection_pu: vec![0.0, 0.0],
480 branch_p_from_mw: vec![p_from_mw],
481 branch_p_to_mw: vec![p_to_mw],
482 branch_q_from_mvar: vec![q_from_mvar],
483 branch_q_to_mvar: vec![q_to_mvar],
484 ..Default::default()
485 }
486 }
487
488 fn two_bus_generator_network() -> Network {
489 let mut net = two_bus_network(100.0);
490 let mut gen_a = Generator::with_id("g1", 1, 50.0, 1.0);
491 gen_a.qmin = -10.0;
492 gen_a.qmax = 20.0;
493 let mut gen_b = Generator::with_id("g2", 1, 25.0, 1.0);
494 gen_b.qmin = -5.0;
495 gen_b.qmax = 5.0;
496 net.generators.push(gen_a);
497 net.generators.push(gen_b);
498 net.loads.push(Load::new(2, 60.0, 15.0));
499 net
500 }
501
502 #[test]
503 fn test_branch_apparent_power_uses_stored_values() {
504 let sol = two_bus_solution(PfModel::Ac, 60.0, 80.0, -58.0, -77.0);
506 let flows = sol.branch_apparent_power();
507 assert_eq!(flows.len(), 1);
508 assert!((flows[0] - 100.0).abs() < 1e-9);
509 }
510
511 #[test]
512 fn test_branch_loading_pct_zero_rating() {
513 let net = two_bus_network(0.0);
514 let sol = two_bus_solution(PfModel::Ac, 50.0, 0.0, -50.0, 0.0);
515 let loading = sol.branch_loading_pct(&net).unwrap();
516 assert_eq!(loading, vec![0.0]);
517 }
518
519 #[test]
520 fn test_branch_loading_pct_normal_operation() {
521 let net = two_bus_network(200.0);
522 let sol = two_bus_solution(PfModel::Ac, 60.0, 80.0, -58.0, -77.0);
524 let loading = sol.branch_loading_pct(&net).unwrap();
525 assert_eq!(loading.len(), 1);
526 assert!((loading[0] - 50.0).abs() < 1e-9);
527 }
528
529 #[test]
530 fn test_branch_loading_pct_uses_larger_end() {
531 let net = two_bus_network(200.0);
532 let sol = two_bus_solution(PfModel::Ac, 30.0, 40.0, 80.0, 60.0);
533 let loading = sol.branch_loading_pct(&net).unwrap();
534 assert_eq!(loading.len(), 1);
535 assert!((loading[0] - 50.0).abs() < 1e-9);
536 }
537
538 #[test]
539 fn test_branch_pq_flows_returns_stored_from_end_values() {
540 let sol = two_bus_solution(PfModel::Dc, 42.0, 0.0, -42.0, 0.0);
541 let pq = sol.branch_pq_flows();
542 assert_eq!(pq, vec![(42.0, 0.0)]);
543 }
544
545 #[test]
546 fn test_branch_loading_mismatch_returns_error() {
547 let net = two_bus_network(100.0);
548 let sol = PfSolution {
549 branch_p_from_mw: vec![],
550 ..Default::default()
551 };
552 assert!(sol.branch_loading_pct(&net).is_err());
553 }
554
555 #[test]
556 #[should_panic(expected = "stored from-end branch flow vectors must have matching lengths")]
557 fn test_branch_apparent_power_panics_on_mismatched_vectors() {
558 let sol = PfSolution {
559 branch_p_from_mw: vec![10.0],
560 branch_q_from_mvar: vec![],
561 ..Default::default()
562 };
563 let _ = sol.branch_apparent_power();
564 }
565
566 #[test]
567 #[should_panic(expected = "stored from-end branch flow vectors must have matching lengths")]
568 fn test_branch_pq_flows_panics_on_mismatched_vectors() {
569 let sol = PfSolution {
570 branch_p_from_mw: vec![10.0],
571 branch_q_from_mvar: vec![],
572 ..Default::default()
573 };
574 let _ = sol.branch_pq_flows();
575 }
576
577 #[test]
578 fn test_n_islands_empty() {
579 let sol = PfSolution::default();
580 assert_eq!(sol.n_islands(), 0);
581 }
582
583 #[test]
584 fn test_n_islands_single() {
585 let sol = PfSolution {
586 island_ids: vec![0, 0, 0],
587 ..Default::default()
588 };
589 assert_eq!(sol.n_islands(), 1);
590 }
591
592 #[test]
593 fn test_n_islands_multiple() {
594 let sol = PfSolution {
595 island_ids: vec![0, 1, 2, 0, 1],
596 ..Default::default()
597 };
598 assert_eq!(sol.n_islands(), 3);
599 }
600
601 #[test]
602 fn test_n_islands_non_dense_labels() {
603 let sol = PfSolution {
604 island_ids: vec![2, 4, 2, 9],
605 ..Default::default()
606 };
607 assert_eq!(sol.n_islands(), 3);
608 }
609
610 #[test]
611 fn test_n_islands_single_bus() {
612 let sol = PfSolution {
613 island_ids: vec![0],
614 ..Default::default()
615 };
616 assert_eq!(sol.n_islands(), 1);
617 }
618
619 #[test]
620 fn test_diverged_creates_flat_profile() {
621 let sol = PfSolution::diverged(3, 2, PfModel::Ac);
622 assert_eq!(sol.pf_model, PfModel::Ac);
623 assert_eq!(sol.status, SolveStatus::Diverged);
624 assert_eq!(sol.voltage_magnitude_pu, vec![1.0, 1.0, 1.0]);
625 assert_eq!(sol.voltage_angle_rad, vec![0.0, 0.0, 0.0]);
626 assert_eq!(sol.active_power_injection_pu, vec![0.0, 0.0, 0.0]);
627 assert_eq!(sol.reactive_power_injection_pu, vec![0.0, 0.0, 0.0]);
628 assert_eq!(sol.branch_p_from_mw, vec![0.0, 0.0]);
629 assert_eq!(sol.branch_q_from_mvar, vec![0.0, 0.0]);
630 assert_eq!(sol.max_mismatch, f64::INFINITY);
631 assert_eq!(sol.iterations, 0);
632 }
633
634 #[test]
635 fn test_diverged_serializes_max_mismatch_as_null() {
636 let sol = PfSolution::diverged(1, 0, PfModel::Ac);
637 let json = serde_json::to_value(&sol).unwrap();
638 assert_eq!(json.get("max_mismatch"), Some(&Value::Null));
639
640 let roundtrip: PfSolution = serde_json::from_value(json).unwrap();
641 assert!(roundtrip.max_mismatch.is_infinite());
642 }
643
644 #[test]
645 fn test_flat_start_creates_unsolved_profile() {
646 let sol = PfSolution::flat_start(2, 1, PfModel::Dc);
647 assert_eq!(sol.pf_model, PfModel::Dc);
648 assert_eq!(sol.status, SolveStatus::Unsolved);
649 assert_eq!(sol.voltage_magnitude_pu, vec![1.0, 1.0]);
650 assert_eq!(sol.voltage_angle_rad, vec![0.0, 0.0]);
651 assert_eq!(sol.branch_p_from_mw, vec![0.0]);
652 assert_eq!(sol.max_mismatch, 0.0);
653 }
654
655 #[test]
656 fn test_generator_reactive_power_mvar_apportions_by_capability() {
657 let net = two_bus_generator_network();
658 let sol = PfSolution {
659 status: SolveStatus::Converged,
660 voltage_magnitude_pu: vec![1.0, 1.0],
661 voltage_angle_rad: vec![0.0, 0.0],
662 active_power_injection_pu: vec![0.0, 0.0],
663 reactive_power_injection_pu: vec![0.40, -0.15],
664 bus_numbers: vec![1, 2],
665 ..Default::default()
666 };
667
668 let qg = sol.generator_reactive_power_mvar(&net);
669 assert_eq!(qg.len(), 2);
670 assert!(
671 (qg[0] - 30.0).abs() < 1e-9,
672 "first generator should absorb 30 MVAr-equivalent share"
673 );
674 assert!(
675 (qg[1] - 10.0).abs() < 1e-9,
676 "second generator should absorb 10 MVAr-equivalent share"
677 );
678 }
679
680 #[test]
681 fn test_generator_reactive_power_mvar_splits_evenly_without_capability_range() {
682 let mut net = two_bus_network(100.0);
683 let mut gen_a = Generator::with_id("g1", 1, 50.0, 1.0);
684 gen_a.qmin = 0.0;
685 gen_a.qmax = 0.0;
686 let mut gen_b = Generator::with_id("g2", 1, 25.0, 1.0);
687 gen_b.qmin = 0.0;
688 gen_b.qmax = 0.0;
689 net.generators.push(gen_a);
690 net.generators.push(gen_b);
691 let sol = PfSolution {
692 status: SolveStatus::Converged,
693 voltage_magnitude_pu: vec![1.0, 1.0],
694 voltage_angle_rad: vec![0.0, 0.0],
695 active_power_injection_pu: vec![0.0, 0.0],
696 reactive_power_injection_pu: vec![0.12, 0.0],
697 bus_numbers: vec![1, 2],
698 ..Default::default()
699 };
700
701 let qg = sol.generator_reactive_power_mvar(&net);
702 assert_eq!(qg, vec![6.0, 6.0]);
703 }
704
705 #[test]
706 fn test_generator_reactive_power_mvar_accounts_for_bus_shunt_without_extra_base_factor() {
707 let mut net = two_bus_network(100.0);
708 net.buses[0].shunt_susceptance_mvar = -100.0;
709 net.generators.push(Generator::with_id("g1", 1, 50.0, 1.0));
710 let sol = PfSolution {
711 status: SolveStatus::Converged,
712 voltage_magnitude_pu: vec![1.0, 1.0],
713 voltage_angle_rad: vec![0.0, 0.0],
714 active_power_injection_pu: vec![0.0, 0.0],
715 reactive_power_injection_pu: vec![0.0, 0.0],
716 bus_numbers: vec![1, 2],
717 ..Default::default()
718 };
719
720 let qg = sol.generator_reactive_power_mvar(&net);
721 assert_eq!(qg, vec![100.0]);
722 }
723
724 #[test]
725 fn test_convergence_history_length_matches_iterations() {
726 let sol = PfSolution {
727 status: SolveStatus::Converged,
728 iterations: 4,
729 convergence_history: vec![(0, 1.0), (1, 0.5), (2, 0.1), (3, 0.01), (4, 1e-8)],
730 ..Default::default()
731 };
732 assert_eq!(sol.convergence_history.len(), (sol.iterations + 1) as usize);
733 }
734
735 #[test]
736 fn test_worst_mismatch_bus_populated_on_divergence() {
737 let sol = PfSolution {
738 status: SolveStatus::Diverged,
739 worst_mismatch_bus: Some(42),
740 max_mismatch: 1e3,
741 ..Default::default()
742 };
743 assert_eq!(sol.worst_mismatch_bus, Some(42));
744 assert_eq!(sol.status, SolveStatus::Diverged);
745 }
746
747 #[test]
748 fn test_worst_mismatch_bus_none_on_convergence() {
749 let sol = PfSolution {
750 status: SolveStatus::Converged,
751 worst_mismatch_bus: None,
752 max_mismatch: 1e-10,
753 ..Default::default()
754 };
755 assert!(sol.worst_mismatch_bus.is_none());
756 }
757}