1use std::f64::consts::{PI, SQRT_2};
14
15#[derive(Debug, Clone, PartialEq)]
21pub enum ConverterTopology {
22 Lcc,
24 TwoLevelVsc,
26 Mmc {
28 n_submodules: usize,
30 },
31 Hybrid,
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub enum DcBreakerType {
38 Mechanical {
40 opening_time_ms: f64,
42 },
43 SolidState {
45 turn_off_time_us: f64,
47 on_state_loss_pct: f64,
49 },
50 HybridBreaker {
52 opening_time_ms: f64,
54 commutation_time_us: f64,
56 },
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub enum DcFaultType {
62 PoleToGround,
64 PoleToPole,
66 ConverterInternal,
68}
69
70#[derive(Debug, Clone)]
72pub struct ConverterStation {
73 pub id: usize,
75 pub name: String,
77 pub topology: ConverterTopology,
79 pub v_dc_rated_kv: f64,
81 pub p_rated_mw: f64,
83 pub x_transformer_pu: f64,
85 pub scr: f64,
87 pub control_angle_deg: f64,
89 pub x_commutation_pu: f64,
91 pub arm_inductance_mh: f64,
93 pub sm_capacitance_uf: f64,
95}
96
97#[derive(Debug, Clone)]
99pub struct DcCable {
100 pub id: usize,
102 pub from_station: usize,
104 pub to_station: usize,
106 pub r_per_km: f64,
108 pub l_per_km: f64,
110 pub c_per_km: f64,
112 pub length_km: f64,
114}
115
116#[derive(Debug, Clone)]
118pub struct DcFaultEvent {
119 pub fault_type: DcFaultType,
121 pub location_cable_id: usize,
123 pub location_fraction: f64,
125 pub fault_resistance: f64,
127 pub inception_time_ms: f64,
129}
130
131#[derive(Debug, Clone)]
133pub struct DcBreaker {
134 pub id: usize,
136 pub station_id: usize,
138 pub breaker_type: DcBreakerType,
140 pub max_breaking_current_ka: f64,
142 pub energy_absorption_mj: f64,
144}
145
146#[derive(Debug, Clone)]
148pub struct DcTransientState {
149 pub time_ms: f64,
151 pub station_voltages_kv: Vec<f64>,
153 pub station_currents_ka: Vec<f64>,
155 pub cable_currents_ka: Vec<f64>,
157 pub fault_current_ka: f64,
159 pub breaker_states: Vec<bool>,
161}
162
163#[derive(Debug, Clone)]
165pub struct DcTransientResult {
166 pub states: Vec<DcTransientState>,
168 pub peak_fault_current_ka: f64,
170 pub fault_clearing_time_ms: f64,
172 pub energy_dissipated_mj: f64,
174 pub voltage_recovery_time_ms: f64,
176 pub max_di_dt: f64,
178 pub fault_cleared: bool,
180}
181
182#[derive(Debug, Clone)]
184pub struct ConverterOperatingPoint {
185 pub station_id: usize,
187 pub p_dc_mw: f64,
189 pub v_dc_kv: f64,
191 pub i_dc_ka: f64,
193 pub firing_angle_deg: f64,
195 pub power_factor: f64,
197 pub losses_mw: f64,
199}
200
201#[derive(Debug, Clone)]
203pub struct DcGridSolution {
204 pub operating_points: Vec<ConverterOperatingPoint>,
206 pub cable_losses_mw: Vec<f64>,
208 pub total_losses_mw: f64,
210 pub converged: bool,
212 pub iterations: usize,
214}
215
216#[derive(Debug, Clone)]
238pub struct DcSwitchingSimulator {
239 pub stations: Vec<ConverterStation>,
241 pub cables: Vec<DcCable>,
243 pub breakers: Vec<DcBreaker>,
245 pub dt_us: f64,
247 pub duration_ms: f64,
249}
250
251impl DcSwitchingSimulator {
256 pub fn new(dt_us: f64, duration_ms: f64) -> Self {
262 Self {
263 stations: Vec::new(),
264 cables: Vec::new(),
265 breakers: Vec::new(),
266 dt_us: dt_us.max(0.1),
267 duration_ms: duration_ms.max(0.001),
268 }
269 }
270
271 pub fn add_station(&mut self, station: ConverterStation) {
273 self.stations.push(station);
274 }
275
276 pub fn add_cable(&mut self, cable: DcCable) {
278 self.cables.push(cable);
279 }
280
281 pub fn add_breaker(&mut self, breaker: DcBreaker) {
283 self.breakers.push(breaker);
284 }
285
286 pub fn solve_dc_power_flow(&self) -> Result<DcGridSolution, String> {
294 let n = self.stations.len();
295 if n == 0 {
296 return Err("No converter stations defined".to_string());
297 }
298
299 let mut g_matrix = vec![0.0_f64; n * n];
301 for cable in &self.cables {
302 let from = cable.from_station;
303 let to = cable.to_station;
304 if from >= n || to >= n {
305 continue;
306 }
307 let r_total = (cable.r_per_km * cable.length_km).max(1e-9);
308 let g = 1.0 / r_total;
309 g_matrix[from * n + from] += g;
310 g_matrix[to * n + to] += g;
311 g_matrix[from * n + to] -= g;
312 g_matrix[to * n + from] -= g;
313 }
314
315 let mut v: Vec<f64> = self.stations.iter().map(|s| s.v_dc_rated_kv).collect();
317
318 let p_spec: Vec<f64> = self
321 .stations
322 .iter()
323 .enumerate()
324 .map(|(i, s)| {
325 if i == 0 {
326 0.0 } else {
328 -s.p_rated_mw * 0.8 }
331 })
332 .collect();
333
334 let max_iter = 50;
335 let tol = 1e-6;
336 let mut converged = false;
337 let mut iterations = 0;
338
339 for iter in 0..max_iter {
340 iterations = iter + 1;
341
342 let mut p_calc = vec![0.0_f64; n];
344 for i in 0..n {
345 let mut sum = 0.0;
346 for j in 0..n {
347 sum += g_matrix[i * n + j] * v[j];
348 }
349 p_calc[i] = v[i] * sum;
350 }
351
352 let mut max_mismatch = 0.0_f64;
354 let mut dp = vec![0.0_f64; n];
355 for i in 1..n {
356 dp[i] = p_spec[i] - p_calc[i];
357 max_mismatch = max_mismatch.max(dp[i].abs());
358 }
359
360 if max_mismatch < tol {
361 converged = true;
362 break;
363 }
364
365 if n == 2 {
369 let j11 = 2.0 * g_matrix[n + 1] * v[1] + g_matrix[n] * v[0];
371 if j11.abs() > 1e-12 {
372 v[1] += dp[1] / j11;
373 }
374 } else {
375 for i in 1..n {
378 let j_ii = 2.0 * g_matrix[i * n + i] * v[i]
379 + (0..n)
380 .filter(|&j| j != i)
381 .map(|j| g_matrix[i * n + j] * v[j])
382 .sum::<f64>();
383 if j_ii.abs() > 1e-12 {
384 v[i] += dp[i] / j_ii;
385 }
386 let v_rated = self.stations[i].v_dc_rated_kv;
388 v[i] = v[i].clamp(v_rated * 0.5, v_rated * 1.5);
389 }
390 }
391 }
392
393 let mut operating_points = Vec::with_capacity(n);
395 for (i, station) in self.stations.iter().enumerate() {
396 let v_kv = v[i];
397 let mut i_sum = 0.0;
399 for j in 0..n {
400 i_sum += g_matrix[i * n + j] * v[j];
401 }
402 let i_ka = i_sum; let p_mw = v_kv * i_ka;
404 let op = match &station.topology {
405 ConverterTopology::Lcc => self.lcc_operating_point(station, i_ka.abs()),
406 _ => self.vsc_operating_point(station, p_mw.abs(), v_kv),
407 };
408 operating_points.push(ConverterOperatingPoint {
409 station_id: station.id,
410 p_dc_mw: p_mw,
411 v_dc_kv: v_kv,
412 i_dc_ka: i_ka,
413 firing_angle_deg: op.firing_angle_deg,
414 power_factor: op.power_factor,
415 losses_mw: op.losses_mw,
416 });
417 }
418
419 let mut cable_losses_mw = Vec::with_capacity(self.cables.len());
420 let mut total_losses = 0.0;
421 for cable in &self.cables {
422 let from = cable.from_station.min(n - 1);
423 let to = cable.to_station.min(n - 1);
424 let r_total = (cable.r_per_km * cable.length_km).max(1e-9);
425 let dv = v[from] - v[to];
426 let i_cable = dv / r_total; let loss = i_cable * i_cable * r_total; cable_losses_mw.push(loss.abs());
429 total_losses += loss.abs();
430 }
431
432 for op in &operating_points {
434 total_losses += op.losses_mw;
435 }
436
437 Ok(DcGridSolution {
438 operating_points,
439 cable_losses_mw,
440 total_losses_mw: total_losses,
441 converged,
442 iterations,
443 })
444 }
445
446 pub fn lcc_operating_point(
455 &self,
456 station: &ConverterStation,
457 i_dc_ka: f64,
458 ) -> ConverterOperatingPoint {
459 let alpha_rad = station.control_angle_deg.to_radians();
460 let v_ac_kv = station.v_dc_rated_kv * PI / (3.0 * SQRT_2);
462
463 let v_d0 = (3.0 * SQRT_2 / PI) * v_ac_kv;
465
466 let z_base = station.v_dc_rated_kv.powi(2) / station.p_rated_mw.max(1e-6);
469 let x_c = station.x_commutation_pu * z_base;
470
471 let v_dc = v_d0 * alpha_rad.cos() - (3.0 / PI) * x_c * i_dc_ka;
472 let p_dc = v_dc * i_dc_ka;
473
474 let cos_alpha = alpha_rad.cos();
476 let mu_arg = (cos_alpha
477 - 2.0 * station.x_commutation_pu * i_dc_ka / station.p_rated_mw.max(1e-6)
478 * station.v_dc_rated_kv)
479 .clamp(-1.0, 1.0);
480 let mu_rad = (alpha_rad.cos() - mu_arg).abs().min(PI / 4.0);
481 let pf = cos_alpha * (1.0 - mu_rad / 2.0);
482
483 let r_conv = 0.001 * z_base; let losses = 0.007 * station.p_rated_mw + i_dc_ka * i_dc_ka * r_conv;
486
487 ConverterOperatingPoint {
488 station_id: station.id,
489 p_dc_mw: p_dc,
490 v_dc_kv: v_dc,
491 i_dc_ka,
492 firing_angle_deg: station.control_angle_deg,
493 power_factor: pf.clamp(0.0, 1.0),
494 losses_mw: losses.abs(),
495 }
496 }
497
498 pub fn vsc_operating_point(
505 &self,
506 station: &ConverterStation,
507 p_mw: f64,
508 v_dc_kv: f64,
509 ) -> ConverterOperatingPoint {
510 let v_dc = v_dc_kv.max(1e-6);
511 let i_dc_ka = p_mw / v_dc;
512
513 let v_ac_kv = v_dc / (1.5_f64).sqrt();
515 let m = if v_ac_kv > 1e-6 {
516 (v_dc / (v_ac_kv * (1.5_f64).sqrt())).clamp(0.0, 1.15)
517 } else {
518 0.85
519 };
520
521 let i_rated = station.p_rated_mw / station.v_dc_rated_kv.max(1e-6);
524 let a = 0.005 * station.p_rated_mw;
525 let b = 0.001 * station.p_rated_mw / i_rated.max(1e-6);
526 let c = 0.005 * station.p_rated_mw / (i_rated * i_rated).max(1e-12);
527 let losses = a + b * i_dc_ka.abs() + c * i_dc_ka * i_dc_ka;
528
529 let loss_factor = match &station.topology {
531 ConverterTopology::Mmc { n_submodules } => {
532 let nsm = (*n_submodules).max(1) as f64;
533 0.5 + 0.5 / nsm.sqrt() }
535 ConverterTopology::TwoLevelVsc => 1.0,
536 ConverterTopology::Hybrid => 0.8,
537 ConverterTopology::Lcc => 0.6, };
539
540 let pf = 1.0; ConverterOperatingPoint {
543 station_id: station.id,
544 p_dc_mw: p_mw,
545 v_dc_kv: v_dc,
546 i_dc_ka,
547 firing_angle_deg: m * 180.0 / PI, power_factor: pf,
549 losses_mw: (losses * loss_factor).abs(),
550 }
551 }
552
553 pub fn simulate_fault(&self, fault: &DcFaultEvent) -> Result<DcTransientResult, String> {
561 if self.stations.is_empty() {
562 return Err("No converter stations defined".to_string());
563 }
564 if self.cables.is_empty() {
565 return Err("No cables defined".to_string());
566 }
567
568 let n_stations = self.stations.len();
569 let n_cables = self.cables.len();
570 let n_breakers = self.breakers.len();
571
572 let dt_ms = self.dt_us / 1000.0; let total_steps = ((self.duration_ms / dt_ms).ceil() as usize).max(1);
574
575 let (l_total_mh, c_total_uf, r_total_ohm, r_fault, v_dc_kv) = self.fault_rlc_params(fault);
577
578 let l_h = l_total_mh * 1e-3;
580 let c_f = c_total_uf * 1e-6;
581 let v_dc_v = v_dc_kv * 1e3;
582
583 let alpha = r_total_ohm / (2.0 * l_h.max(1e-9));
585 let omega0_sq = 1.0 / (l_h.max(1e-9) * c_f.max(1e-12));
586 let is_underdamped = alpha * alpha < omega0_sq;
587
588 let mut state = DcTransientState {
590 time_ms: 0.0,
591 station_voltages_kv: self.stations.iter().map(|s| s.v_dc_rated_kv).collect(),
592 station_currents_ka: vec![0.0; n_stations],
593 cable_currents_ka: vec![0.0; n_cables],
594 fault_current_ka: 0.0,
595 breaker_states: vec![true; n_breakers],
596 };
597
598 let mut states = Vec::with_capacity(total_steps.min(10000));
599 let mut peak_fault_ka = 0.0_f64;
600 let mut max_di_dt = 0.0_f64;
601 let mut energy_mj = 0.0_f64;
602 let mut fault_cleared = false;
603 let mut clearing_time_ms = self.duration_ms;
604 let mut voltage_recovery_time_ms = self.duration_ms;
605 let mut prev_fault_current_ka = 0.0_f64;
606 let mut fault_active = false;
607
608 let breaker_clear_ms = self.fastest_breaker_clearing_ms();
610
611 let record_interval = (total_steps / 5000).max(1);
613
614 for step in 0..total_steps {
615 let t_ms = step as f64 * dt_ms;
616
617 if !fault_active && t_ms >= fault.inception_time_ms {
619 fault_active = true;
620 }
621
622 if fault_active && !fault_cleared {
623 let dt_s = dt_ms * 1e-3;
625 let new_state = self.rk4_step(&state, dt_s, Some(fault));
626
627 let t_fault_s = (t_ms - fault.inception_time_ms).max(0.0) * 1e-3;
629
630 let i_fault_a = if is_underdamped {
631 let omega_d = (omega0_sq - alpha * alpha).max(0.0).sqrt();
632 if omega_d > 1e-12 {
633 (v_dc_v / (omega_d * l_h.max(1e-9)))
634 * (-alpha * t_fault_s).exp()
635 * (omega_d * t_fault_s).sin()
636 } else {
637 0.0
638 }
639 } else {
640 let disc = (alpha * alpha - omega0_sq).max(0.0).sqrt();
642 let s1 = -alpha + disc;
643 let s2 = -alpha - disc;
644 if (s1 - s2).abs() > 1e-12 {
645 let a_coeff = v_dc_v / (l_h.max(1e-9) * (s1 - s2));
646 a_coeff * ((s1 * t_fault_s).exp() - (s2 * t_fault_s).exp())
647 } else {
648 (v_dc_v / l_h.max(1e-9)) * t_fault_s * (-alpha * t_fault_s).exp()
650 }
651 };
652
653 let i_fault_ka = i_fault_a.abs() / 1e3;
654
655 let r_factor = 1.0 / (1.0 + r_fault / (r_total_ohm + 1e-9));
657 let i_fault_ka = i_fault_ka * r_factor;
658
659 let fault_type_factor = match fault.fault_type {
661 DcFaultType::PoleToGround => 0.5,
662 DcFaultType::PoleToPole => 1.0,
663 DcFaultType::ConverterInternal => 0.7,
664 };
665 let i_fault_ka = i_fault_ka * fault_type_factor;
666
667 state.fault_current_ka = i_fault_ka;
668
669 state.cable_currents_ka = new_state.cable_currents_ka;
671 state.station_voltages_kv = new_state.station_voltages_kv;
672 state.station_currents_ka = new_state.station_currents_ka;
673
674 let di_dt = (i_fault_ka - prev_fault_current_ka).abs() / dt_ms.max(1e-12);
676 max_di_dt = max_di_dt.max(di_dt);
677 prev_fault_current_ka = i_fault_ka;
678
679 peak_fault_ka = peak_fault_ka.max(i_fault_ka);
681
682 let i_a = i_fault_ka * 1e3;
684 energy_mj += i_a * i_a * (r_fault + r_total_ohm) * (dt_ms * 1e-3) / 1e6;
685
686 let t_since_inception = t_ms - fault.inception_time_ms;
688 if t_since_inception >= breaker_clear_ms && !fault_cleared {
689 fault_cleared = true;
690 clearing_time_ms = t_ms;
691 for b in state.breaker_states.iter_mut() {
693 *b = false;
694 }
695 }
696 } else if fault_cleared {
697 let t_after_clear_s = (t_ms - clearing_time_ms).max(0.0) * 1e-3;
699 let decay_tau = l_h / r_total_ohm.max(1e-9);
700 state.fault_current_ka =
701 prev_fault_current_ka * (-t_after_clear_s / decay_tau.max(1e-9)).exp();
702 if state.fault_current_ka < 1e-6 {
703 state.fault_current_ka = 0.0;
704 }
705
706 let all_recovered = state
708 .station_voltages_kv
709 .iter()
710 .enumerate()
711 .all(|(i, &v)| v >= 0.9 * self.stations[i].v_dc_rated_kv);
712 if all_recovered && voltage_recovery_time_ms >= self.duration_ms {
713 voltage_recovery_time_ms = t_ms;
714 }
715
716 for (i, v) in state.station_voltages_kv.iter_mut().enumerate() {
718 let v_rated = self.stations[i].v_dc_rated_kv;
719 let recovery_rate = 0.001 * v_rated; if *v < v_rated {
721 *v = (*v + recovery_rate).min(v_rated);
722 }
723 }
724 }
725
726 state.time_ms = t_ms;
727
728 if step % record_interval == 0 || step == total_steps - 1 {
730 states.push(state.clone());
731 }
732 }
733
734 Ok(DcTransientResult {
735 states,
736 peak_fault_current_ka: peak_fault_ka,
737 fault_clearing_time_ms: if fault_cleared {
738 clearing_time_ms
739 } else {
740 self.duration_ms
741 },
742 energy_dissipated_mj: energy_mj,
743 voltage_recovery_time_ms,
744 max_di_dt,
745 fault_cleared,
746 })
747 }
748
749 pub fn simulate_switching(
754 &self,
755 breaker_id: usize,
756 open: bool,
757 ) -> Result<DcTransientResult, String> {
758 if self.stations.is_empty() {
759 return Err("No converter stations defined".to_string());
760 }
761
762 let n_stations = self.stations.len();
763 let _n_cables = self.cables.len();
764 let n_breakers = self.breakers.len();
765
766 let dt_ms = self.dt_us / 1000.0;
767 let total_steps = ((self.duration_ms / dt_ms).ceil() as usize).max(1);
768
769 let mut state = DcTransientState {
770 time_ms: 0.0,
771 station_voltages_kv: self.stations.iter().map(|s| s.v_dc_rated_kv).collect(),
772 station_currents_ka: vec![0.0; n_stations],
773 cable_currents_ka: self
774 .cables
775 .iter()
776 .map(|c| {
777 let r = (c.r_per_km * c.length_km).max(1e-9);
778 let from = c.from_station.min(n_stations.saturating_sub(1));
779 let to = c.to_station.min(n_stations.saturating_sub(1));
780 let v_from = self.stations.get(from).map_or(0.0, |s| s.v_dc_rated_kv);
781 let v_to = self.stations.get(to).map_or(0.0, |s| s.v_dc_rated_kv);
782 (v_from - v_to) / r
783 })
784 .collect(),
785 fault_current_ka: 0.0,
786 breaker_states: vec![!open; n_breakers],
787 };
788
789 let mut states = Vec::with_capacity(total_steps.min(10000));
790 let record_interval = (total_steps / 5000).max(1);
791 let switching_time_ms = self.duration_ms * 0.1; let mut switched = false;
793
794 for step in 0..total_steps {
795 let t_ms = step as f64 * dt_ms;
796 state.time_ms = t_ms;
797
798 if !switched && t_ms >= switching_time_ms {
800 switched = true;
801 for (i, b) in state.breaker_states.iter_mut().enumerate() {
802 if i == breaker_id || breaker_id >= n_breakers {
803 *b = !open;
804 }
805 }
806 if open {
807 for i_c in state.cable_currents_ka.iter_mut() {
809 *i_c *= 0.5; }
811 }
812 }
813
814 let dt_s = dt_ms * 1e-3;
816 let new_state = self.rk4_step(&state, dt_s, None);
817 state.cable_currents_ka = new_state.cable_currents_ka;
818 state.station_voltages_kv = new_state.station_voltages_kv;
819 state.station_currents_ka = new_state.station_currents_ka;
820
821 if step % record_interval == 0 || step == total_steps - 1 {
822 states.push(state.clone());
823 }
824 }
825
826 Ok(DcTransientResult {
827 states,
828 peak_fault_current_ka: 0.0,
829 fault_clearing_time_ms: 0.0,
830 energy_dissipated_mj: 0.0,
831 voltage_recovery_time_ms: 0.0,
832 max_di_dt: 0.0,
833 fault_cleared: true,
834 })
835 }
836
837 fn rk4_step(
847 &self,
848 state: &DcTransientState,
849 dt: f64,
850 fault: Option<&DcFaultEvent>,
851 ) -> DcTransientState {
852 let n_s = self.stations.len();
853 let n_c = self.cables.len();
854
855 let state_len = n_s + n_c;
857 let mut y = vec![0.0_f64; state_len];
858 for (i, &v) in state.station_voltages_kv.iter().enumerate() {
859 if i < n_s {
860 y[i] = v;
861 }
862 }
863 for (i, &ic) in state.cable_currents_ka.iter().enumerate() {
864 if i < n_c {
865 y[n_s + i] = ic;
866 }
867 }
868
869 let k1 = self.state_derivatives(&y, fault);
871 let y2: Vec<f64> = y
872 .iter()
873 .zip(k1.iter())
874 .map(|(&yi, &ki)| yi + 0.5 * dt * ki)
875 .collect();
876 let k2 = self.state_derivatives(&y2, fault);
877 let y3: Vec<f64> = y
878 .iter()
879 .zip(k2.iter())
880 .map(|(&yi, &ki)| yi + 0.5 * dt * ki)
881 .collect();
882 let k3 = self.state_derivatives(&y3, fault);
883 let y4: Vec<f64> = y
884 .iter()
885 .zip(k3.iter())
886 .map(|(&yi, &ki)| yi + dt * ki)
887 .collect();
888 let k4 = self.state_derivatives(&y4, fault);
889
890 let y_new: Vec<f64> = (0..state_len)
892 .map(|i| y[i] + (dt / 6.0) * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]))
893 .collect();
894
895 let mut new_state = state.clone();
897 for (i, v) in y_new.iter().take(n_s).enumerate() {
898 new_state.station_voltages_kv[i] = v.max(0.0);
899 }
900 new_state.cable_currents_ka[..n_c].copy_from_slice(&y_new[n_s..n_s + n_c]);
901
902 for i in 0..n_s {
904 let mut i_net = 0.0;
905 for (ci, cable) in self.cables.iter().enumerate() {
906 let i_cable = if ci < n_c { y_new[n_s + ci] } else { 0.0 };
907 if cable.to_station == i {
908 i_net += i_cable;
909 } else if cable.from_station == i {
910 i_net -= i_cable;
911 }
912 }
913 if i < new_state.station_currents_ka.len() {
914 new_state.station_currents_ka[i] = i_net;
915 }
916 }
917
918 new_state
919 }
920
921 fn state_derivatives(&self, y: &[f64], fault: Option<&DcFaultEvent>) -> Vec<f64> {
925 let n_s = self.stations.len();
926 let n_c = self.cables.len();
927 let mut dydt = vec![0.0_f64; n_s + n_c];
928
929 for (ci, cable) in self.cables.iter().enumerate() {
931 let from = cable.from_station.min(n_s.saturating_sub(1));
932 let to = cable.to_station.min(n_s.saturating_sub(1));
933 let v_from = if from < n_s { y[from] } else { 0.0 };
934 let v_to = if to < n_s { y[to] } else { 0.0 };
935 let r_ohm = (cable.r_per_km * cable.length_km).max(1e-9); let l_h = (cable.l_per_km * cable.length_km * 1e-3).max(1e-9); let i_ka = if (n_s + ci) < y.len() {
938 y[n_s + ci]
939 } else {
940 0.0
941 };
942 dydt[n_s + ci] = (v_from - v_to - r_ohm * i_ka) / l_h;
949 }
950
951 for (si, station) in self.stations.iter().enumerate() {
953 let c_uf = self.station_capacitance_uf(station);
955 let c_f = (c_uf * 1e-6).max(1e-12); let mut i_net_ka = 0.0;
958 for (ci, cable) in self.cables.iter().enumerate() {
959 let i_cable = if (n_s + ci) < y.len() {
960 y[n_s + ci]
961 } else {
962 0.0
963 };
964 if cable.to_station == si {
965 i_net_ka += i_cable;
966 } else if cable.from_station == si {
967 i_net_ka -= i_cable;
968 }
969 }
970
971 if let Some(f) = fault {
973 if f.location_cable_id < self.cables.len() {
974 let cable = &self.cables[f.location_cable_id];
975 if cable.from_station == si || cable.to_station == si {
976 let v_kv = if si < y.len() { y[si] } else { 0.0 };
978 let r_f = f.fault_resistance.max(1e-6);
979 let i_fault_ka = v_kv * 1e3 / (r_f * 1e3); i_net_ka -= i_fault_ka * f.location_fraction;
981 }
982 }
983 }
984
985 dydt[si] = i_net_ka / c_f;
988 }
989
990 dydt
991 }
992
993 #[allow(dead_code)]
998 fn detect_fault(&self, state: &DcTransientState, prev: &DcTransientState) -> bool {
999 let dt_ms = (state.time_ms - prev.time_ms).abs().max(1e-9);
1000
1001 for (i, &i_now) in state.cable_currents_ka.iter().enumerate() {
1003 let i_prev = prev.cable_currents_ka.get(i).copied().unwrap_or(0.0);
1004 let di_dt = (i_now - i_prev).abs() / dt_ms;
1005 if di_dt > 10.0 {
1006 return true;
1008 }
1009 }
1010
1011 for (i, &v) in state.station_voltages_kv.iter().enumerate() {
1013 let v_rated = self.stations.get(i).map_or(500.0, |s| s.v_dc_rated_kv);
1014 if v < 0.8 * v_rated {
1015 return true;
1016 }
1017 }
1018
1019 false
1020 }
1021
1022 fn fault_rlc_params(&self, fault: &DcFaultEvent) -> (f64, f64, f64, f64, f64) {
1028 let cable_id = fault
1029 .location_cable_id
1030 .min(self.cables.len().saturating_sub(1));
1031 let cable = &self.cables[cable_id];
1032 let frac = fault.location_fraction.clamp(0.0, 1.0);
1033 let dist = cable.length_km * frac;
1034
1035 let r_cable = cable.r_per_km * dist;
1037 let l_cable = cable.l_per_km * dist; let c_cable = cable.c_per_km * cable.length_km; let from = cable
1042 .from_station
1043 .min(self.stations.len().saturating_sub(1));
1044 let l_arm = self.stations.get(from).map_or(0.0, |s| s.arm_inductance_mh);
1045
1046 let l_total = l_cable + l_arm + 1.0; let c_total = c_cable
1049 + self
1050 .stations
1051 .iter()
1052 .map(|s| s.sm_capacitance_uf)
1053 .sum::<f64>();
1054 let c_total = c_total.max(0.01); let v_dc = self.stations.get(from).map_or(500.0, |s| s.v_dc_rated_kv);
1057
1058 (
1059 l_total,
1060 c_total,
1061 r_cable.max(0.01),
1062 fault.fault_resistance,
1063 v_dc,
1064 )
1065 }
1066
1067 fn station_capacitance_uf(&self, station: &ConverterStation) -> f64 {
1069 let base = match &station.topology {
1070 ConverterTopology::Mmc { n_submodules } => {
1071 let nsm = (*n_submodules).max(1) as f64;
1073 station.sm_capacitance_uf * nsm / 6.0
1074 }
1075 _ => {
1076 let v = station.v_dc_rated_kv.max(1.0);
1079 let p = station.p_rated_mw.max(1.0);
1080 (p / (v * v * 314.0)) * 1e6 }
1082 };
1083 let cable_c: f64 = self
1085 .cables
1086 .iter()
1087 .filter(|c| c.from_station == station.id || c.to_station == station.id)
1088 .map(|c| c.c_per_km * c.length_km * 0.5) .sum();
1090 (base + cable_c).max(0.01)
1091 }
1092
1093 fn fastest_breaker_clearing_ms(&self) -> f64 {
1095 if self.breakers.is_empty() {
1096 return self.duration_ms;
1097 }
1098 self.breakers
1099 .iter()
1100 .map(|b| breaker_clearing_time_ms(&b.breaker_type))
1101 .fold(f64::MAX, f64::min)
1102 }
1103}
1104
1105fn breaker_clearing_time_ms(bt: &DcBreakerType) -> f64 {
1107 match bt {
1108 DcBreakerType::Mechanical { opening_time_ms } => *opening_time_ms,
1109 DcBreakerType::SolidState {
1110 turn_off_time_us, ..
1111 } => turn_off_time_us / 1000.0,
1112 DcBreakerType::HybridBreaker {
1113 opening_time_ms,
1114 commutation_time_us,
1115 } => {
1116 commutation_time_us / 1000.0 + opening_time_ms * 0.3
1118 }
1119 }
1120}
1121
1122#[cfg(test)]
1127mod tests {
1128 use super::*;
1129
1130 fn lcc_station(id: usize, alpha_deg: f64) -> ConverterStation {
1133 ConverterStation {
1134 id,
1135 name: format!("LCC-{id}"),
1136 topology: ConverterTopology::Lcc,
1137 v_dc_rated_kv: 500.0,
1138 p_rated_mw: 1000.0,
1139 x_transformer_pu: 0.15,
1140 scr: 3.0,
1141 control_angle_deg: alpha_deg,
1142 x_commutation_pu: 0.1,
1143 arm_inductance_mh: 0.0,
1144 sm_capacitance_uf: 0.0,
1145 }
1146 }
1147
1148 fn vsc_station(id: usize) -> ConverterStation {
1149 ConverterStation {
1150 id,
1151 name: format!("VSC-{id}"),
1152 topology: ConverterTopology::TwoLevelVsc,
1153 v_dc_rated_kv: 320.0,
1154 p_rated_mw: 800.0,
1155 x_transformer_pu: 0.12,
1156 scr: 5.0,
1157 control_angle_deg: 0.0,
1158 x_commutation_pu: 0.0,
1159 arm_inductance_mh: 0.0,
1160 sm_capacitance_uf: 0.0,
1161 }
1162 }
1163
1164 fn mmc_station(id: usize) -> ConverterStation {
1165 ConverterStation {
1166 id,
1167 name: format!("MMC-{id}"),
1168 topology: ConverterTopology::Mmc { n_submodules: 400 },
1169 v_dc_rated_kv: 320.0,
1170 p_rated_mw: 1000.0,
1171 x_transformer_pu: 0.12,
1172 scr: 5.0,
1173 control_angle_deg: 0.0,
1174 x_commutation_pu: 0.0,
1175 arm_inductance_mh: 50.0,
1176 sm_capacitance_uf: 10000.0,
1177 }
1178 }
1179
1180 fn standard_cable(id: usize, from: usize, to: usize) -> DcCable {
1181 DcCable {
1182 id,
1183 from_station: from,
1184 to_station: to,
1185 r_per_km: 0.01,
1186 l_per_km: 0.5,
1187 c_per_km: 0.2,
1188 length_km: 100.0,
1189 }
1190 }
1191
1192 #[allow(dead_code)]
1193 fn short_cable(id: usize, from: usize, to: usize) -> DcCable {
1194 DcCable {
1195 id,
1196 from_station: from,
1197 to_station: to,
1198 r_per_km: 0.01,
1199 l_per_km: 0.5,
1200 c_per_km: 0.2,
1201 length_km: 20.0,
1202 }
1203 }
1204
1205 fn mechanical_breaker(id: usize, station: usize) -> DcBreaker {
1206 DcBreaker {
1207 id,
1208 station_id: station,
1209 breaker_type: DcBreakerType::Mechanical {
1210 opening_time_ms: 50.0,
1211 },
1212 max_breaking_current_ka: 10.0,
1213 energy_absorption_mj: 20.0,
1214 }
1215 }
1216
1217 fn solid_state_breaker(id: usize, station: usize) -> DcBreaker {
1218 DcBreaker {
1219 id,
1220 station_id: station,
1221 breaker_type: DcBreakerType::SolidState {
1222 turn_off_time_us: 500.0,
1223 on_state_loss_pct: 0.5,
1224 },
1225 max_breaking_current_ka: 8.0,
1226 energy_absorption_mj: 10.0,
1227 }
1228 }
1229
1230 #[allow(dead_code)]
1231 fn hybrid_breaker(id: usize, station: usize) -> DcBreaker {
1232 DcBreaker {
1233 id,
1234 station_id: station,
1235 breaker_type: DcBreakerType::HybridBreaker {
1236 opening_time_ms: 30.0,
1237 commutation_time_us: 200.0,
1238 },
1239 max_breaking_current_ka: 12.0,
1240 energy_absorption_mj: 15.0,
1241 }
1242 }
1243
1244 fn standard_fault(cable_id: usize) -> DcFaultEvent {
1245 DcFaultEvent {
1246 fault_type: DcFaultType::PoleToPole,
1247 location_cable_id: cable_id,
1248 location_fraction: 0.5,
1249 fault_resistance: 0.1,
1250 inception_time_ms: 5.0,
1251 }
1252 }
1253
1254 fn build_2term_system() -> DcSwitchingSimulator {
1255 let mut sim = DcSwitchingSimulator::new(10.0, 200.0);
1256 sim.add_station(lcc_station(0, 15.0));
1257 sim.add_station(lcc_station(1, 150.0));
1258 sim.add_cable(standard_cable(0, 0, 1));
1259 sim
1260 }
1261
1262 #[test]
1265 fn test_dc_power_flow_converges() {
1266 let sim = build_2term_system();
1267 let sol = sim.solve_dc_power_flow().expect("DC PF should converge");
1268 assert!(sol.converged, "DC power flow should converge");
1269 assert!(sol.iterations <= 50, "Should converge in < 50 iterations");
1270 assert_eq!(sol.operating_points.len(), 2);
1271 }
1272
1273 #[test]
1276 fn test_lcc_vdc_decreases_with_alpha() {
1277 let sim = DcSwitchingSimulator::new(10.0, 100.0);
1278 let s15 = lcc_station(0, 15.0);
1279 let s30 = lcc_station(0, 30.0);
1280 let op15 = sim.lcc_operating_point(&s15, 1.0);
1281 let op30 = sim.lcc_operating_point(&s30, 1.0);
1282 assert!(
1283 op15.v_dc_kv > op30.v_dc_kv,
1284 "V_dc at alpha=15 ({:.1}) should exceed alpha=30 ({:.1})",
1285 op15.v_dc_kv,
1286 op30.v_dc_kv
1287 );
1288 }
1289
1290 #[test]
1293 fn test_vsc_pdc_controllable() {
1294 let sim = DcSwitchingSimulator::new(10.0, 100.0);
1295 let s = vsc_station(0);
1296 let op1 = sim.vsc_operating_point(&s, 100.0, 320.0);
1297 let op2 = sim.vsc_operating_point(&s, 400.0, 320.0);
1298 assert!(
1299 (op1.p_dc_mw - 100.0).abs() < 1.0,
1300 "P should be ~100 MW, got {}",
1301 op1.p_dc_mw
1302 );
1303 assert!(
1304 (op2.p_dc_mw - 400.0).abs() < 1.0,
1305 "P should be ~400 MW, got {}",
1306 op2.p_dc_mw
1307 );
1308 }
1309
1310 #[test]
1313 fn test_vsc_losses_positive() {
1314 let sim = DcSwitchingSimulator::new(10.0, 100.0);
1315 let s = vsc_station(0);
1316 let op = sim.vsc_operating_point(&s, 500.0, 320.0);
1317 assert!(
1318 op.losses_mw > 0.0,
1319 "VSC losses should be positive, got {}",
1320 op.losses_mw
1321 );
1322 }
1323
1324 #[test]
1327 fn test_ptp_fault_peak_positive() {
1328 let mut sim = build_2term_system();
1329 sim.add_breaker(mechanical_breaker(0, 0));
1330 let fault = standard_fault(0);
1331 let res = sim.simulate_fault(&fault).expect("fault sim failed");
1332 assert!(
1333 res.peak_fault_current_ka > 0.0,
1334 "Peak fault current should be > 0, got {}",
1335 res.peak_fault_current_ka
1336 );
1337 }
1338
1339 #[test]
1342 fn test_ptg_lower_peak_than_ptp() {
1343 let mut sim = build_2term_system();
1344 sim.add_breaker(mechanical_breaker(0, 0));
1345
1346 let ptp = DcFaultEvent {
1347 fault_type: DcFaultType::PoleToPole,
1348 location_cable_id: 0,
1349 location_fraction: 0.5,
1350 fault_resistance: 0.1,
1351 inception_time_ms: 5.0,
1352 };
1353 let ptg = DcFaultEvent {
1354 fault_type: DcFaultType::PoleToGround,
1355 location_cable_id: 0,
1356 location_fraction: 0.5,
1357 fault_resistance: 0.1,
1358 inception_time_ms: 5.0,
1359 };
1360
1361 let res_ptp = sim.simulate_fault(&ptp).expect("PtP failed");
1362 let res_ptg = sim.simulate_fault(&ptg).expect("PtG failed");
1363
1364 assert!(
1365 res_ptg.peak_fault_current_ka < res_ptp.peak_fault_current_ka,
1366 "PtG peak ({:.2}) should be < PtP peak ({:.2})",
1367 res_ptg.peak_fault_current_ka,
1368 res_ptp.peak_fault_current_ka
1369 );
1370 }
1371
1372 #[test]
1375 fn test_fault_current_shorter_distance() {
1376 let mut sim = build_2term_system();
1377 sim.add_breaker(mechanical_breaker(0, 0));
1378
1379 let near = DcFaultEvent {
1380 fault_type: DcFaultType::PoleToPole,
1381 location_cable_id: 0,
1382 location_fraction: 0.1,
1383 fault_resistance: 0.1,
1384 inception_time_ms: 5.0,
1385 };
1386 let far = DcFaultEvent {
1387 fault_type: DcFaultType::PoleToPole,
1388 location_cable_id: 0,
1389 location_fraction: 0.9,
1390 fault_resistance: 0.1,
1391 inception_time_ms: 5.0,
1392 };
1393
1394 let res_near = sim.simulate_fault(&near).expect("near failed");
1395 let res_far = sim.simulate_fault(&far).expect("far failed");
1396
1397 assert!(
1398 res_near.peak_fault_current_ka >= res_far.peak_fault_current_ka,
1399 "Near fault ({:.2}) should have >= peak than far fault ({:.2})",
1400 res_near.peak_fault_current_ka,
1401 res_far.peak_fault_current_ka
1402 );
1403 }
1404
1405 #[test]
1408 fn test_fault_current_decreases_with_resistance() {
1409 let mut sim = build_2term_system();
1410 sim.add_breaker(mechanical_breaker(0, 0));
1411
1412 let low_r = DcFaultEvent {
1413 fault_type: DcFaultType::PoleToPole,
1414 location_cable_id: 0,
1415 location_fraction: 0.5,
1416 fault_resistance: 0.1,
1417 inception_time_ms: 5.0,
1418 };
1419 let high_r = DcFaultEvent {
1420 fault_type: DcFaultType::PoleToPole,
1421 location_cable_id: 0,
1422 location_fraction: 0.5,
1423 fault_resistance: 10.0,
1424 inception_time_ms: 5.0,
1425 };
1426
1427 let res_low = sim.simulate_fault(&low_r).expect("low R failed");
1428 let res_high = sim.simulate_fault(&high_r).expect("high R failed");
1429
1430 assert!(
1431 res_low.peak_fault_current_ka >= res_high.peak_fault_current_ka,
1432 "Low R fault ({:.2}) should have >= peak than high R ({:.2})",
1433 res_low.peak_fault_current_ka,
1434 res_high.peak_fault_current_ka
1435 );
1436 }
1437
1438 #[test]
1441 fn test_mechanical_breaker_clearing_time() {
1442 let mut sim = build_2term_system();
1443 sim.add_breaker(DcBreaker {
1444 id: 0,
1445 station_id: 0,
1446 breaker_type: DcBreakerType::Mechanical {
1447 opening_time_ms: 50.0,
1448 },
1449 max_breaking_current_ka: 10.0,
1450 energy_absorption_mj: 20.0,
1451 });
1452
1453 let fault = standard_fault(0);
1454 let res = sim.simulate_fault(&fault).expect("fault failed");
1455
1456 assert!(
1457 res.fault_clearing_time_ms >= 30.0,
1458 "Mechanical breaker should clear >= 30 ms, got {}",
1459 res.fault_clearing_time_ms
1460 );
1461 }
1462
1463 #[test]
1466 fn test_solid_state_breaker_clearing() {
1467 let clearing = breaker_clearing_time_ms(&DcBreakerType::SolidState {
1468 turn_off_time_us: 500.0,
1469 on_state_loss_pct: 0.5,
1470 });
1471 assert!(
1472 clearing < 1.0,
1473 "Solid-state should clear < 1 ms, got {}",
1474 clearing
1475 );
1476 }
1477
1478 #[test]
1481 fn test_hybrid_breaker_intermediate_clearing() {
1482 let mech_time = breaker_clearing_time_ms(&DcBreakerType::Mechanical {
1483 opening_time_ms: 50.0,
1484 });
1485 let ss_time = breaker_clearing_time_ms(&DcBreakerType::SolidState {
1486 turn_off_time_us: 500.0,
1487 on_state_loss_pct: 0.5,
1488 });
1489 let hybrid_time = breaker_clearing_time_ms(&DcBreakerType::HybridBreaker {
1490 opening_time_ms: 30.0,
1491 commutation_time_us: 200.0,
1492 });
1493
1494 assert!(
1495 hybrid_time > ss_time && hybrid_time < mech_time,
1496 "Hybrid ({:.2}) should be between SS ({:.2}) and mech ({:.2})",
1497 hybrid_time,
1498 ss_time,
1499 mech_time
1500 );
1501 }
1502
1503 #[test]
1506 fn test_energy_dissipated_positive() {
1507 let mut sim = build_2term_system();
1508 sim.add_breaker(mechanical_breaker(0, 0));
1509 let fault = standard_fault(0);
1510 let res = sim.simulate_fault(&fault).expect("failed");
1511 assert!(
1512 res.energy_dissipated_mj > 0.0,
1513 "Energy dissipated should be > 0, got {}",
1514 res.energy_dissipated_mj
1515 );
1516 }
1517
1518 #[test]
1521 fn test_fault_cleared_voltage_recovers() {
1522 let mut sim = DcSwitchingSimulator::new(10.0, 300.0);
1523 sim.add_station(lcc_station(0, 15.0));
1524 sim.add_station(lcc_station(1, 150.0));
1525 sim.add_cable(standard_cable(0, 0, 1));
1526 sim.add_breaker(solid_state_breaker(0, 0));
1527
1528 let fault = standard_fault(0);
1529 let res = sim.simulate_fault(&fault).expect("failed");
1530 assert!(res.fault_cleared, "Fault should be cleared with SS breaker");
1531 }
1532
1533 #[test]
1536 fn test_peak_di_dt_positive() {
1537 let mut sim = build_2term_system();
1538 sim.add_breaker(mechanical_breaker(0, 0));
1539 let fault = standard_fault(0);
1540 let res = sim.simulate_fault(&fault).expect("failed");
1541 assert!(
1542 res.max_di_dt > 0.0,
1543 "max di/dt should be > 0, got {}",
1544 res.max_di_dt
1545 );
1546 }
1547
1548 #[test]
1551 fn test_cable_losses_proportional_to_i2r() {
1552 let mut sim = DcSwitchingSimulator::new(10.0, 100.0);
1553 sim.add_station(lcc_station(0, 15.0));
1554 sim.add_station(lcc_station(1, 150.0));
1555 sim.add_cable(DcCable {
1556 id: 0,
1557 from_station: 0,
1558 to_station: 1,
1559 r_per_km: 0.02, l_per_km: 0.5,
1561 c_per_km: 0.2,
1562 length_km: 100.0,
1563 });
1564
1565 let sol = sim.solve_dc_power_flow().expect("PF failed");
1566 assert!(
1567 sol.cable_losses_mw[0] >= 0.0,
1568 "Cable losses should be non-negative"
1569 );
1570 }
1571
1572 #[test]
1575 fn test_mmc_arm_inductance_limits_fault() {
1576 let mut mmc_high_l = mmc_station(0);
1578 mmc_high_l.arm_inductance_mh = 100.0;
1579 mmc_high_l.sm_capacitance_uf = 100.0; let mut mmc_low_l = mmc_station(0);
1582 mmc_low_l.arm_inductance_mh = 1.0;
1583 mmc_low_l.sm_capacitance_uf = 100.0;
1584
1585 let mut sim_high = DcSwitchingSimulator::new(10.0, 200.0);
1586 sim_high.add_station(mmc_high_l.clone());
1587 let mut s1_h = mmc_high_l.clone();
1588 s1_h.id = 1;
1589 sim_high.add_station(s1_h);
1590 sim_high.add_cable(standard_cable(0, 0, 1));
1591 sim_high.add_breaker(mechanical_breaker(0, 0));
1592
1593 let mut sim_low = DcSwitchingSimulator::new(10.0, 200.0);
1594 sim_low.add_station(mmc_low_l.clone());
1595 let mut s1_l = mmc_low_l.clone();
1596 s1_l.id = 1;
1597 sim_low.add_station(s1_l);
1598 sim_low.add_cable(standard_cable(0, 0, 1));
1599 sim_low.add_breaker(mechanical_breaker(0, 0));
1600
1601 let fault = standard_fault(0);
1602 let res_high = sim_high
1603 .simulate_fault(&fault)
1604 .expect("high L fault failed");
1605 let res_low = sim_low.simulate_fault(&fault).expect("low L fault failed");
1606
1607 assert!(
1609 res_high.peak_fault_current_ka <= res_low.peak_fault_current_ka + 0.1,
1610 "High arm L ({:.2}) should limit fault current vs low L ({:.2})",
1611 res_high.peak_fault_current_ka,
1612 res_low.peak_fault_current_ka
1613 );
1614 }
1615
1616 #[test]
1619 fn test_switching_transient_oscillation() {
1620 let mut sim = build_2term_system();
1621 sim.add_breaker(mechanical_breaker(0, 0));
1622
1623 let res = sim
1624 .simulate_switching(0, true)
1625 .expect("switching sim failed");
1626 assert!(
1627 !res.states.is_empty(),
1628 "Switching result should have states"
1629 );
1630
1631 let first_v = res
1633 .states
1634 .first()
1635 .map(|s| s.station_voltages_kv.first().copied().unwrap_or(0.0))
1636 .unwrap_or(0.0);
1637 let mid = res.states.len() / 2;
1638 let mid_v = res
1639 .states
1640 .get(mid)
1641 .map(|s| s.station_voltages_kv.first().copied().unwrap_or(0.0))
1642 .unwrap_or(0.0);
1643 let differs = (first_v - mid_v).abs() > 1e-6 || res.states.len() > 1;
1645 assert!(differs, "Switching should produce transient response");
1646 }
1647
1648 #[test]
1651 fn test_3terminal_power_balance() {
1652 let mut sim = DcSwitchingSimulator::new(10.0, 100.0);
1653 sim.add_station(lcc_station(0, 15.0));
1654 sim.add_station(vsc_station(1));
1655 sim.add_station(lcc_station(2, 150.0));
1656 sim.add_cable(standard_cable(0, 0, 1));
1657 sim.add_cable(standard_cable(1, 1, 2));
1658
1659 let sol = sim.solve_dc_power_flow().expect("3-term PF failed");
1660 assert_eq!(sol.operating_points.len(), 3);
1661 let total_p: f64 = sol.operating_points.iter().map(|op| op.p_dc_mw).sum();
1663 assert!(
1666 total_p.is_finite(),
1667 "Total power should be finite, got {}",
1668 total_p
1669 );
1670 }
1671
1672 #[test]
1675 fn test_underdamped_response_oscillates() {
1676 let mut sim = DcSwitchingSimulator::new(5.0, 100.0);
1678 sim.add_station(vsc_station(0));
1679 sim.add_station(vsc_station(1));
1680 sim.add_cable(DcCable {
1681 id: 0,
1682 from_station: 0,
1683 to_station: 1,
1684 r_per_km: 0.001, l_per_km: 2.0, c_per_km: 1.0, length_km: 50.0,
1688 });
1689 sim.add_breaker(mechanical_breaker(0, 0));
1690
1691 let fault = DcFaultEvent {
1692 fault_type: DcFaultType::PoleToPole,
1693 location_cable_id: 0,
1694 location_fraction: 0.5,
1695 fault_resistance: 0.001,
1696 inception_time_ms: 2.0,
1697 };
1698 let res = sim
1699 .simulate_fault(&fault)
1700 .expect("underdamped fault failed");
1701 assert!(
1702 res.peak_fault_current_ka > 0.0,
1703 "Underdamped should produce fault current"
1704 );
1705 }
1706
1707 #[test]
1710 fn test_overdamped_high_resistance() {
1711 let mut sim = DcSwitchingSimulator::new(10.0, 100.0);
1712 sim.add_station(lcc_station(0, 15.0));
1713 sim.add_station(lcc_station(1, 150.0));
1714 sim.add_cable(DcCable {
1715 id: 0,
1716 from_station: 0,
1717 to_station: 1,
1718 r_per_km: 1.0, l_per_km: 0.1, c_per_km: 0.01, length_km: 100.0,
1722 });
1723 sim.add_breaker(mechanical_breaker(0, 0));
1724
1725 let fault = DcFaultEvent {
1726 fault_type: DcFaultType::PoleToPole,
1727 location_cable_id: 0,
1728 location_fraction: 0.5,
1729 fault_resistance: 50.0,
1730 inception_time_ms: 5.0,
1731 };
1732 let res = sim.simulate_fault(&fault).expect("overdamped fault failed");
1733 assert!(
1735 res.peak_fault_current_ka.is_finite(),
1736 "Overdamped peak should be finite"
1737 );
1738 }
1739
1740 #[test]
1743 fn test_no_fault_stable() {
1744 let mut sim = build_2term_system();
1745 sim.add_breaker(mechanical_breaker(0, 0));
1746
1747 let res = sim.simulate_switching(0, false).expect("stable sim failed");
1749 assert!(!res.states.is_empty(), "Should produce states");
1750 assert!(
1751 res.peak_fault_current_ka.abs() < 1e-6,
1752 "No fault current expected"
1753 );
1754 }
1755
1756 #[test]
1759 fn test_transient_result_states_nonempty() {
1760 let mut sim = build_2term_system();
1761 sim.add_breaker(mechanical_breaker(0, 0));
1762 let fault = standard_fault(0);
1763 let res = sim.simulate_fault(&fault).expect("failed");
1764 assert!(
1765 !res.states.is_empty(),
1766 "Result should contain transient states"
1767 );
1768 assert!(res.states.len() > 1, "Should have multiple state snapshots");
1769 }
1770
1771 #[test]
1774 fn test_operating_point_p_equals_vi() {
1775 let sim = DcSwitchingSimulator::new(10.0, 100.0);
1776 let s = vsc_station(0);
1777 let op = sim.vsc_operating_point(&s, 256.0, 320.0);
1778 let p_check = op.v_dc_kv * op.i_dc_ka;
1779 assert!(
1780 (op.p_dc_mw - p_check).abs() < 1.0,
1781 "P ({:.1}) should ≈ V*I ({:.1})",
1782 op.p_dc_mw,
1783 p_check
1784 );
1785 }
1786
1787 #[test]
1790 fn test_breaker_max_breaking_current() {
1791 let b = mechanical_breaker(0, 0);
1792 assert!(
1793 b.max_breaking_current_ka > 0.0,
1794 "Max breaking current should be positive"
1795 );
1796 assert_eq!(b.max_breaking_current_ka, 10.0);
1797 }
1798
1799 #[test]
1802 fn test_lcc_power_factor() {
1803 let sim = DcSwitchingSimulator::new(10.0, 100.0);
1804 let s = lcc_station(0, 15.0);
1805 let op = sim.lcc_operating_point(&s, 1.0);
1806 assert!(
1807 op.power_factor > 0.0 && op.power_factor <= 1.0,
1808 "PF should be in (0,1], got {}",
1809 op.power_factor
1810 );
1811 }
1812
1813 #[test]
1816 fn test_detect_fault() {
1817 let sim = build_2term_system();
1818
1819 let prev = DcTransientState {
1820 time_ms: 0.0,
1821 station_voltages_kv: vec![500.0, 500.0],
1822 station_currents_ka: vec![1.0, -1.0],
1823 cable_currents_ka: vec![1.0],
1824 fault_current_ka: 0.0,
1825 breaker_states: vec![true],
1826 };
1827 let state_normal = DcTransientState {
1828 time_ms: 0.01,
1829 station_voltages_kv: vec![499.9, 499.9],
1830 station_currents_ka: vec![1.0, -1.0],
1831 cable_currents_ka: vec![1.01],
1832 fault_current_ka: 0.0,
1833 breaker_states: vec![true],
1834 };
1835 let state_fault = DcTransientState {
1836 time_ms: 0.01,
1837 station_voltages_kv: vec![200.0, 200.0], station_currents_ka: vec![20.0, -20.0],
1839 cable_currents_ka: vec![20.0],
1840 fault_current_ka: 15.0,
1841 breaker_states: vec![true],
1842 };
1843
1844 assert!(
1845 !sim.detect_fault(&state_normal, &prev),
1846 "Normal state should not be flagged as fault"
1847 );
1848 assert!(
1849 sim.detect_fault(&state_fault, &prev),
1850 "Faulted state should be detected"
1851 );
1852 }
1853
1854 #[test]
1857 fn test_cable_total_parameters() {
1858 let c = standard_cable(0, 0, 1);
1859 let r_total = c.r_per_km * c.length_km;
1860 let l_total = c.l_per_km * c.length_km;
1861 let c_total = c.c_per_km * c.length_km;
1862 assert!((r_total - 1.0).abs() < 1e-6, "R = 0.01 * 100 = 1.0 Ω");
1863 assert!((l_total - 50.0).abs() < 1e-6, "L = 0.5 * 100 = 50 mH");
1864 assert!((c_total - 20.0).abs() < 1e-6, "C = 0.2 * 100 = 20 μF");
1865 }
1866
1867 #[test]
1870 fn test_empty_system_errors() {
1871 let sim = DcSwitchingSimulator::new(10.0, 100.0);
1872 assert!(sim.solve_dc_power_flow().is_err());
1873 let fault = standard_fault(0);
1874 assert!(sim.simulate_fault(&fault).is_err());
1875 }
1876}