1#![allow(clippy::needless_range_loop)]
2use std::collections::HashMap;
35use std::path::Path;
36
37use surge_network::Network;
38use surge_network::network::AreaSchedule;
39use surge_network::network::Owner;
40use surge_network::network::Region;
41use surge_network::network::facts::FactsType;
42use surge_network::network::impedance_correction::ImpedanceCorrectionTable;
43use surge_network::network::multi_section_line::MultiSectionLineGroup;
44use surge_network::network::scheduled_area_transfer::ScheduledAreaTransfer;
45use surge_network::network::topology::{
46 BusbarSection, ConnectivityNode, Substation as SubstationData, TerminalConnection,
47 TopologyMapping, VoltageLevel,
48};
49use surge_network::network::{Branch, BranchOpfControl, BranchType, Bus, BusType, Generator};
50use surge_network::network::{
51 DcBranch, DcBus, DcConverter, LccConverterTerminal, LccDcConverter, LccDcConverterRole,
52 LccHvdcControlMode, LccHvdcLink,
53};
54use surge_network::network::{FactsDevice, FactsMode};
55use surge_network::network::{NodeBreakerTopology, SwitchDevice, SwitchType};
56use surge_network::network::{OltcSpec, ParSpec};
57use surge_network::network::{
58 VscConverterAcControlMode, VscConverterTerminal, VscHvdcControlMode, VscHvdcLink,
59};
60use thiserror::Error;
61
62use super::multi_terminal_dc::{RawMtdcBus, RawMtdcConverter, RawMtdcLink, RawMtdcSystem};
63
64#[derive(Error, Debug)]
65pub enum PsseError {
66 #[error("I/O error: {0}")]
67 Io(#[from] std::io::Error),
68
69 #[error("parse error on line {line}: {message}")]
70 Parse { line: usize, message: String },
71
72 #[error("missing section: {0}")]
73 MissingSection(String),
74
75 #[error("unsupported PSS/E version: {0}")]
76 UnsupportedVersion(u32),
77
78 #[error("unexpected end of file in {0} section")]
79 UnexpectedEof(String),
80
81 #[error("non-finite float value on line {line}: {message}")]
83 NonFiniteValue { line: usize, message: String },
84}
85
86pub fn parse_file(path: &Path) -> Result<Network, PsseError> {
88 let content = std::fs::read_to_string(path)?;
89 let name = path
90 .file_stem()
91 .and_then(|s| s.to_str())
92 .unwrap_or("unknown")
93 .to_string();
94 parse_string_with_name(&content, &name)
95}
96
97pub fn parse_str(content: &str) -> Result<Network, PsseError> {
99 parse_string_with_name(content, "unknown")
100}
101
102fn parse_string_with_name(content: &str, name: &str) -> Result<Network, PsseError> {
103 let lines: Vec<&str> = content.lines().collect();
104 if lines.len() < 3 {
105 return Err(PsseError::Parse {
106 line: 1,
107 message: "PSS/E RAW file must have at least 3 header lines".into(),
108 });
109 }
110
111 let header_line_idx = if lines[0].starts_with("@!") { 1 } else { 0 };
115
116 let (sbase, version, freq_hz) = parse_header(lines[header_line_idx], header_line_idx + 1)?;
118
119 let mut network = Network::new(name);
120 network.base_mva = sbase;
121 network.freq_hz = freq_hz;
122
123 let mut pos = header_line_idx + 3;
125
126 while pos < lines.len() {
132 let line = lines[pos].trim();
133 if line.is_empty() || line.starts_with("@!") {
134 pos += 1;
135 continue;
136 }
137 if is_section_end(line) {
138 pos += 1;
140 continue;
141 }
142 let first = line
144 .split(|c: char| c == ',' || c.is_ascii_whitespace())
145 .next()
146 .unwrap_or("");
147 if first.parse::<f64>().is_err() {
148 pos += 1;
149 continue;
150 }
151 break; }
153
154 let buses;
156 (buses, pos) = parse_bus_section(&lines, pos)?;
157
158 let bus_basekv: HashMap<u32, f64> = buses.iter().map(|b| (b.number, b.base_kv)).collect();
160
161 network.buses = buses;
162
163 sanitize_voltage_limits(&mut network);
165
166 let loads;
168 (loads, pos) = parse_load_section(&lines, pos)?;
169 apply_loads(&mut network, &loads).map_err(|err| PsseError::Parse {
170 line: 1,
171 message: err.to_string(),
172 })?;
173
174 let shunts;
176 (shunts, pos) = parse_fixed_shunt_section(&lines, pos)?;
177 if version >= 36 {
179 let droop_controls;
180 (droop_controls, pos) = parse_voltage_droop_control_section(&lines, pos);
181 network.metadata.voltage_droop_controls = droop_controls;
182 }
183 pos = skip_to_section(&lines, pos, "generator");
184 apply_shunts(&mut network, &shunts);
185
186 let generators;
188 (generators, pos) = parse_generator_section(&lines, pos)?;
189 if version >= 36 {
191 let rating_sets;
192 (rating_sets, pos) = parse_switching_device_rating_set_section(&lines, pos);
193 network.metadata.switching_device_rating_sets = rating_sets;
194 }
195 pos = skip_to_section(&lines, pos, "branch");
196 network.generators = generators;
197
198 let branches;
200 let branch_terminal_shunts;
201 (branches, branch_terminal_shunts, pos) = parse_branch_section(&lines, pos, sbase)?;
202 network.branches = branches;
203 apply_shunts(&mut network, &branch_terminal_shunts);
205
206 let sys_switch_devices;
209 if version >= 34 {
210 if let Some(sys_pos) =
211 seek_section(&lines, pos.saturating_sub(1), "system switching device")
212 {
213 (sys_switch_devices, pos) = parse_system_switching_device_section(&lines, sys_pos);
214 pos = skip_to_section(&lines, pos, "transformer");
215 } else {
216 sys_switch_devices = Vec::new();
217 }
218 } else {
219 sys_switch_devices = Vec::new();
220 }
221
222 let (transformers, star_buses, oltc_specs, par_specs, pos) =
224 parse_transformer_section(&lines, pos, sbase, version, &bus_basekv)?;
225 network.buses.extend(star_buses);
228 network.branches.extend(transformers);
229 network.controls.oltc_specs.extend(oltc_specs);
230 network.controls.par_specs.extend(par_specs);
231
232 let seek_start = if pos > 0 { pos - 1 } else { pos };
242
243 if let Some(ai_pos) = seek_section(&lines, seek_start, "area interchange") {
245 let (areas, _) = parse_area_schedule_section(&lines, ai_pos)?;
246 network.area_schedules = areas;
247 }
248
249 if let Some(dc_pos) = seek_section(&lines, seek_start, "two-terminal dc") {
251 let (lcc_links, _) = parse_dc_line_section(&lines, dc_pos)?;
252 network.hvdc.links = lcc_links
253 .into_iter()
254 .map(surge_network::network::HvdcLink::Lcc)
255 .collect();
256 }
257
258 if let Some(vsc_pos) = seek_section(&lines, seek_start, "vsc dc line") {
260 let (vsc_lines, _) = parse_vsc_dc_section(&lines, vsc_pos)?;
261 network.hvdc.links.extend(
262 vsc_lines
263 .into_iter()
264 .map(surge_network::network::HvdcLink::Vsc),
265 );
266 }
267
268 if let Some(ic_pos) = seek_section(&lines, seek_start, "impedance correction") {
270 let (tables, _) = parse_impedance_correction_section(&lines, ic_pos);
271 network.metadata.impedance_corrections = tables;
272 }
273
274 if let Some(mt_pos) = seek_section(&lines, seek_start, "multi-terminal dc") {
276 let (mt_lines, _) = parse_multi_terminal_dc_section(&lines, mt_pos);
277 normalize_dc_grids(&mut network, &mt_lines);
278 }
279
280 if let Some(ms_pos) = seek_section(&lines, seek_start, "multi-section line") {
282 let (ms_lines, _) = parse_multi_section_line_section(&lines, ms_pos);
283 network.metadata.multi_section_line_groups = ms_lines;
284 }
285
286 if let Some(z_pos) = seek_section(&lines, seek_start, "zone") {
288 let (zones, _) = parse_zone_section(&lines, z_pos);
289 network.metadata.regions = zones;
290 }
291
292 if let Some(ia_pos) = seek_section(&lines, seek_start, "inter-area transfer") {
294 let (transfers, _) = parse_inter_area_transfer_section(&lines, ia_pos);
295 network.metadata.scheduled_area_transfers = transfers;
296 }
297
298 if let Some(ow_pos) = seek_section(&lines, seek_start, "owner") {
300 let (owners, _) = parse_owner_section(&lines, ow_pos);
301 network.metadata.owners = owners;
302 }
303
304 if let Some(facts_pos) = seek_section(&lines, seek_start, "facts") {
308 let (facts, _) = parse_facts_section(&lines, facts_pos);
309 network.facts_devices = facts;
310 }
311
312 if let Some(sw_pos) = seek_section(&lines, seek_start, "switched shunt") {
316 let (sw_shunts, _) = parse_switched_shunt_section(&lines, sw_pos)?;
317 let base_mva = network.base_mva;
318 apply_switched_shunts(&mut network, &sw_shunts, base_mva);
319 }
320
321 if let Some(im_pos) = seek_section(&lines, seek_start, "induction machine") {
323 let machines = parse_induction_machine_section(&lines, im_pos);
324 network.induction_machines = machines;
325 }
326
327 if let Some(sub_pos) = seek_section(&lines, seek_start, "substation") {
331 let sm = parse_substation_data_section(&lines, sub_pos, &bus_basekv, &sys_switch_devices);
332 if !sm.connectivity_nodes.is_empty() {
333 network.topology = Some(sm);
334 }
335 } else if !sys_switch_devices.is_empty() {
336 network.topology = Some(build_sys_switch_model(
339 &sys_switch_devices,
340 &network,
341 &bus_basekv,
342 ));
343 }
344 Ok(network)
345}
346
347fn parse_header(line: &str, line_num: usize) -> Result<(f64, u32, f64), PsseError> {
352 let fields = tokenize_record(line);
353 if fields.is_empty() {
354 return Err(PsseError::Parse {
355 line: line_num,
356 message: "empty header line".into(),
357 });
358 }
359
360 let sbase = if fields.len() > 1 {
362 parse_f64(&fields[1], line_num, "SBASE")?
363 } else {
364 100.0
365 };
366
367 let version = if fields.len() > 2 {
368 parse_f64(&fields[2], line_num, "REV")? as u32
369 } else {
370 33 };
372
373 let freq_hz = if fields.len() > 5 {
376 let f = parse_f64(&fields[5], line_num, "BASFRQ").unwrap_or(60.0);
377 if f > 0.0 { f } else { 60.0 }
378 } else {
379 60.0
380 };
381
382 Ok((sbase, version, freq_hz))
383}
384
385fn skip_to_section(lines: &[&str], pos: usize, target: &str) -> usize {
404 let target_lc = target.to_ascii_lowercase();
405 let mut i = pos;
406 while i < lines.len() {
407 let line = lines[i].trim();
408 if line.is_empty() || line.starts_with("@!") {
409 i += 1;
410 continue;
411 }
412 if is_section_end(line) {
413 let lc = line.to_ascii_lowercase();
414 if let Some(begin_pos) = lc.find("begin")
421 && lc[begin_pos..].contains(&target_lc)
422 {
423 return i + 1;
424 }
425 i += 1;
428 continue;
429 }
430 break;
433 }
434 pos }
436
437fn seek_section(lines: &[&str], start: usize, target: &str) -> Option<usize> {
450 let target_lc = target.to_ascii_lowercase();
451 for i in start..lines.len() {
452 let line = lines[i].trim();
453 if is_section_end(line) {
454 let lc = line.to_ascii_lowercase();
455 if let Some(begin_pos) = lc.find("begin")
456 && lc[begin_pos..].contains(&target_lc)
457 {
458 return Some(i + 1);
459 }
460 }
461 }
462 None
463}
464
465use crate::parse_utils::{
470 RawLoad, RawShunt, RawSwitchedShunt, apply_loads, apply_shunts, apply_switched_shunts,
471 sanitize_voltage_limits, unquote,
472};
473
474fn parse_bus_section(lines: &[&str], start: usize) -> Result<(Vec<Bus>, usize), PsseError> {
475 let mut buses = Vec::new();
476 let mut pos = start;
477
478 while pos < lines.len() {
479 let line = lines[pos].trim();
480 if is_section_end(line) {
481 return Ok((buses, pos + 1));
482 }
483 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
484 return Ok((buses, pos + 1));
485 }
486 if line.starts_with("@!") {
487 pos += 1;
488 continue;
489 }
490
491 let fields = tokenize_record(line);
492 if fields.is_empty() {
493 pos += 1;
494 continue;
495 }
496
497 let line_num = pos + 1;
498 if fields.len() < 4 {
501 pos += 1;
502 continue;
503 }
504
505 let number = parse_f64(&fields[0], line_num, "bus number")? as u32;
506 let name = unquote(&fields[1]);
507 let base_kv = parse_f64(&fields[2], line_num, "BASKV")?;
508 let ide = parse_f64(&fields[3], line_num, "IDE")? as u32;
509
510 let area = if fields.len() > 4 {
511 parse_f64(&fields[4], line_num, "AREA")? as u32
512 } else {
513 1
514 };
515 let zone = if fields.len() > 5 {
516 parse_f64(&fields[5], line_num, "ZONE")? as u32
517 } else {
518 1
519 };
520 let owner = if fields.len() > 6 {
521 parse_f64(&fields[6], line_num, "OWNER").unwrap_or(1.0) as u32
522 } else {
523 1
524 };
525 let vm = if fields.len() > 7 {
526 parse_f64(&fields[7], line_num, "VM")?
527 } else {
528 1.0
529 };
530 let va_deg = if fields.len() > 8 {
531 parse_f64(&fields[8], line_num, "VA")?
532 } else {
533 0.0
534 };
535
536 let bus_type = match ide {
537 2 => BusType::PV,
538 3 => BusType::Slack,
539 4 => BusType::Isolated,
540 _ => BusType::PQ,
541 };
542
543 let vmax = if fields.len() > 9 {
545 parse_f64(&fields[9], line_num, "NVHI").unwrap_or(1.1)
546 } else {
547 1.1
548 };
549 let vmin = if fields.len() > 10 {
550 parse_f64(&fields[10], line_num, "NVLO").unwrap_or(0.9)
551 } else {
552 0.9
553 };
554
555 let latitude = if fields.len() > 13 {
558 parse_f64(&fields[13], line_num, "LATITUDE")
559 .ok()
560 .filter(|&v| v.abs() > 1e-10)
561 } else {
562 None
563 };
564 let longitude = if fields.len() > 14 {
565 parse_f64(&fields[14], line_num, "LONGITUDE")
566 .ok()
567 .filter(|&v| v.abs() > 1e-10)
568 } else {
569 None
570 };
571
572 buses.push(Bus {
573 number,
574 name,
575 bus_type,
576 shunt_conductance_mw: 0.0, shunt_susceptance_mvar: 0.0,
578 area,
579 voltage_magnitude_pu: vm,
580 voltage_angle_rad: va_deg.to_radians(),
581 base_kv,
582 zone,
583 voltage_max_pu: vmax,
584 voltage_min_pu: vmin,
585 island_id: 0,
586 latitude,
587 longitude,
588 owners: if owner > 0 {
589 vec![surge_network::network::OwnershipEntry {
590 owner,
591 fraction: 1.0,
592 }]
593 } else {
594 Vec::new()
595 },
596 ..Bus::new(0, BusType::PQ, 0.0)
597 });
598
599 pos += 1;
600 }
601
602 Err(PsseError::UnexpectedEof("Bus Data".into()))
603}
604
605fn parse_load_section(lines: &[&str], start: usize) -> Result<(Vec<RawLoad>, usize), PsseError> {
610 let mut loads = Vec::new();
611 let mut pos = start;
612
613 while pos < lines.len() {
614 let line = lines[pos].trim();
615 if is_section_end(line) {
616 return Ok((loads, pos + 1));
617 }
618 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
619 return Ok((loads, pos + 1));
620 }
621 if line.starts_with("@!") {
622 pos += 1;
623 continue;
624 }
625
626 let fields = tokenize_record(line);
627 if fields.is_empty() {
628 pos += 1;
629 continue;
630 }
631
632 let line_num = pos + 1;
633 if fields.len() < 6 {
635 pos += 1;
636 continue;
637 }
638
639 let bus = parse_f64(&fields[0], line_num, "bus")? as u32;
640 let id = unquote(&fields[1]);
641 let status = parse_status(&fields[2], line_num, "STATUS")?;
643 let pl = parse_f64(&fields[5], line_num, "PL")?;
645 let ql = if fields.len() > 6 {
646 parse_f64(&fields[6], line_num, "QL")?
647 } else {
648 0.0
649 };
650 let ip = if fields.len() > 7 {
654 parse_f64(&fields[7], line_num, "IP").unwrap_or(0.0)
655 } else {
656 0.0
657 };
658 let iq = if fields.len() > 8 {
659 parse_f64(&fields[8], line_num, "IQ").unwrap_or(0.0)
660 } else {
661 0.0
662 };
663 let yp = if fields.len() > 9 {
664 parse_f64(&fields[9], line_num, "YP").unwrap_or(0.0)
665 } else {
666 0.0
667 };
668 let yq = if fields.len() > 10 {
669 parse_f64(&fields[10], line_num, "YQ").unwrap_or(0.0)
670 } else {
671 0.0
672 };
673 let p_total = pl + ip + yp;
674 let q_total = ql + iq + yq;
675
676 let (zip_pz, zip_pi, zip_pp) = if p_total.abs() > 1e-10 {
678 (yp / p_total, ip / p_total, pl / p_total)
679 } else {
680 (0.0, 0.0, 1.0)
681 };
682 let (zip_qz, zip_qi, zip_qp) = if q_total.abs() > 1e-10 {
683 (yq / q_total, iq / q_total, ql / q_total)
684 } else {
685 (0.0, 0.0, 1.0)
686 };
687
688 if ip.abs() > 1e-10 || iq.abs() > 1e-10 || yp.abs() > 1e-10 || yq.abs() > 1e-10 {
689 tracing::debug!(
690 bus,
691 ip,
692 iq,
693 yp,
694 yq,
695 "PSS/E load at bus {bus}: ZIP fractions preserved \
696 (Z={zip_pz:.4}/{zip_qz:.4}, I={zip_pi:.4}/{zip_qi:.4}, P={zip_pp:.4}/{zip_qp:.4})"
697 );
698 }
699
700 let owner = if fields.len() > 11 {
701 Some(parse_f64(&fields[11], line_num, "OWNER").unwrap_or(1.0) as u32)
702 } else {
703 None
704 };
705 let conforming = if fields.len() > 12 {
707 let scale_val = parse_f64(&fields[12], line_num, "SCALE").unwrap_or(1.0);
708 scale_val.abs() > 0.5
709 } else {
710 true
711 };
712
713 loads.push(RawLoad {
714 bus,
715 id,
716 status,
717 owner,
718 pl: p_total,
719 ql: q_total,
720 conforming,
721 zip_p_impedance_frac: zip_pz,
722 zip_p_current_frac: zip_pi,
723 zip_p_power_frac: zip_pp,
724 zip_q_impedance_frac: zip_qz,
725 zip_q_current_frac: zip_qi,
726 zip_q_power_frac: zip_qp,
727 });
728
729 pos += 1;
730 }
731
732 Err(PsseError::UnexpectedEof("Load Data".into()))
733}
734
735fn parse_fixed_shunt_section(
740 lines: &[&str],
741 start: usize,
742) -> Result<(Vec<RawShunt>, usize), PsseError> {
743 let mut shunts = Vec::new();
744 let mut pos = start;
745
746 while pos < lines.len() {
747 let line = lines[pos].trim();
748 if is_section_end(line) {
749 return Ok((shunts, pos + 1));
750 }
751 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
752 return Ok((shunts, pos + 1));
753 }
754 if line.starts_with("@!") {
755 pos += 1;
756 continue;
757 }
758
759 let fields = tokenize_record(line);
760 if fields.is_empty() {
761 pos += 1;
762 continue;
763 }
764
765 let line_num = pos + 1;
766 if fields.len() < 5 {
768 pos += 1;
769 continue;
770 }
771
772 let bus = parse_f64(&fields[0], line_num, "bus")? as u32;
773 let status = parse_status(&fields[2], line_num, "STATUS")?;
776 let gl = parse_f64(&fields[3], line_num, "GL")?;
777 let bl = parse_f64(&fields[4], line_num, "BL")?;
778
779 shunts.push(RawShunt {
780 bus,
781 status,
782 gl,
783 bl,
784 });
785
786 pos += 1;
787 }
788
789 Err(PsseError::UnexpectedEof("Fixed Shunt Data".into()))
790}
791
792fn parse_switched_shunt_section(
821 lines: &[&str],
822 start: usize,
823) -> Result<(Vec<RawSwitchedShunt>, usize), PsseError> {
824 let mut shunts = Vec::new();
825 let mut pos = start;
826
827 while pos < lines.len() {
828 let line = lines[pos].trim();
829
830 if is_section_end(line) {
831 return Ok((shunts, pos + 1));
832 }
833 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
834 return Ok((shunts, pos + 1));
835 }
836 if line.is_empty() || line.starts_with("@!") {
837 pos += 1;
838 continue;
839 }
840
841 let fields = tokenize_record(line);
842 if fields.is_empty() {
843 pos += 1;
844 continue;
845 }
846
847 if fields.len() < 10 {
849 return Err(PsseError::Parse {
850 line: pos + 1,
851 message: format!(
852 "switched shunt record is truncated: expected at least 10 fields, got {}",
853 fields.len()
854 ),
855 });
856 }
857
858 let bus = match fields[0].trim_matches('\'').parse::<f64>() {
859 Ok(v) => v as u32,
860 Err(_) => {
861 return Err(PsseError::Parse {
862 line: pos + 1,
863 message: "invalid switched shunt bus number".into(),
864 });
865 }
866 };
867
868 let modsw = fields[1].trim_matches('\'').parse::<f64>().unwrap_or(0.0) as i32;
869 let stat = match fields[3].trim_matches('\'').parse::<f64>() {
871 Ok(v) => v as i32,
872 Err(_) => {
873 return Err(PsseError::Parse {
874 line: pos + 1,
875 message: "invalid switched shunt status".into(),
876 });
877 }
878 };
879 let vswhi = fields[4].trim_matches('\'').parse::<f64>().unwrap_or(1.1);
880 let vswlo = fields[5].trim_matches('\'').parse::<f64>().unwrap_or(0.9);
881 let swrem = fields[6].trim_matches('\'').parse::<f64>().unwrap_or(0.0) as u32;
882 let binit_mvar = match fields[9].trim_matches('\'').parse::<f64>() {
884 Ok(v) => v,
885 Err(_) => {
886 return Err(PsseError::Parse {
887 line: pos + 1,
888 message: "invalid switched shunt BINIT".into(),
889 });
890 }
891 };
892
893 let mut blocks = Vec::new();
896 let mut fi = 10usize;
897 while fi + 1 < fields.len() && blocks.len() < 8 {
898 let ni = fields[fi].trim_matches('\'').parse::<f64>().unwrap_or(0.0) as i32;
899 let bi = fields[fi + 1]
900 .trim_matches('\'')
901 .parse::<f64>()
902 .unwrap_or(0.0);
903 blocks.push((ni, bi));
904 fi += 2;
905 }
906
907 shunts.push(RawSwitchedShunt {
908 bus,
909 modsw,
910 stat,
911 vswhi,
912 vswlo,
913 swrem,
914 binit: binit_mvar,
915 blocks,
916 });
917
918 pos += 1;
919 }
920
921 Ok((shunts, pos))
922}
923
924fn parse_generator_section(
929 lines: &[&str],
930 start: usize,
931) -> Result<(Vec<Generator>, usize), PsseError> {
932 let mut generators = Vec::new();
933 let mut pos = start;
934
935 while pos < lines.len() {
936 let line = lines[pos].trim();
937 if is_section_end(line) {
938 return Ok((generators, pos + 1));
939 }
940 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
941 return Ok((generators, pos + 1));
942 }
943 if line.starts_with("@!") {
944 pos += 1;
945 continue;
946 }
947
948 let fields = tokenize_record(line);
949 if fields.is_empty() {
950 pos += 1;
951 continue;
952 }
953
954 let line_num = pos + 1;
955 if fields.len() < 15 {
957 pos += 1;
958 continue;
959 }
960
961 let bus = parse_f64(&fields[0], line_num, "bus")? as u32;
962 let machine_id: Option<String> = fields.get(1).map(|s| {
964 let trimmed = s
965 .trim()
966 .trim_matches('\'')
967 .trim_matches('"')
968 .trim()
969 .to_string();
970 if trimmed.is_empty() {
971 "1".to_string()
972 } else {
973 trimmed
974 }
975 });
976 let pg = parse_f64(&fields[2], line_num, "PG")?;
977 let qg = parse_f64(&fields[3], line_num, "QG")?;
978 let qt = parse_f64(&fields[4], line_num, "QT")?;
979 let qb = parse_f64(&fields[5], line_num, "QB")?;
980 let vs = parse_f64(&fields[6], line_num, "VS")?;
981 let reg_bus: Option<u32> = if fields.len() > 7 && !fields[7].trim().is_empty() {
983 let ireg = parse_f64(&fields[7], line_num, "IREG").unwrap_or(0.0) as i64;
984 if ireg != 0 {
985 Some(ireg.unsigned_abs() as u32)
986 } else {
987 None
988 }
989 } else {
990 None
991 };
992 let mbase = parse_f64(&fields[8], line_num, "MBASE")?;
993 let xs = if fields.len() > 10 && !fields[10].trim().is_empty() {
995 let zx = parse_f64(&fields[10], line_num, "ZX").unwrap_or(0.0);
996 if zx > 0.0 { Some(zx) } else { None }
997 } else {
998 None
999 };
1000 let stat = if fields.len() > 14 {
1004 parse_status(&fields[14], line_num, "STAT")?
1005 } else {
1006 return Err(PsseError::Parse {
1007 line: line_num,
1008 message: "missing required generator status (STAT) field".into(),
1009 });
1010 };
1011
1012 let mut pt = if fields.len() > 16 && !fields[16].trim().is_empty() {
1013 parse_f64(&fields[16], line_num, "PT")?
1014 } else {
1015 9999.0 };
1017 let mut pb = if fields.len() > 17 {
1018 parse_f64(&fields[17], line_num, "PB")?
1019 } else {
1020 0.0
1021 };
1022
1023 if pt < 0.0 {
1026 pt = pt.abs();
1028 }
1029 if pt < pg && pg > 0.0 {
1030 pt = pg * 1.1;
1032 }
1033 if pt == 0.0 && pg > 0.0 {
1034 tracing::warn!(
1038 "PSS/E generator at bus {bus}: PT=0 with PG={pg:.1} MW; \
1039 setting Pmax=9999 MW (field not provided in source file)"
1040 );
1041 pt = 9999.0;
1042 }
1043 if pb > pt {
1045 pb = 0.0;
1046 }
1047
1048 generators.push(Generator {
1049 bus,
1050 machine_id,
1051 p: pg,
1052 q: qg,
1053 qmax: qt,
1054 qmin: qb,
1055 voltage_setpoint_pu: vs,
1056 reg_bus,
1057 machine_base_mva: mbase,
1058 pmax: pt,
1059 pmin: pb,
1060 in_service: stat > 0,
1061 cost: None,
1062 forced_outage_rate: None,
1063 agc_participation_factor: None,
1064 h_inertia_s: None,
1065 pfr_eligible: true,
1066 fault_data: xs.map(|xs_val| surge_network::network::GenFaultData {
1067 xs: Some(xs_val),
1068 ..Default::default()
1069 }),
1070 owners: parse_multi_owner_fields(&fields, 18),
1071 ..Generator::new(0, 0.0, 1.0)
1072 });
1073
1074 pos += 1;
1075 }
1076
1077 Err(PsseError::UnexpectedEof("Generator Data".into()))
1078}
1079
1080fn parse_branch_section(
1085 lines: &[&str],
1086 start: usize,
1087 sbase: f64,
1088) -> Result<(Vec<Branch>, Vec<RawShunt>, usize), PsseError> {
1089 let mut branches = Vec::new();
1090 let mut terminal_shunts: Vec<RawShunt> = Vec::new();
1091 let mut pos = start;
1092
1093 while pos < lines.len() {
1094 let line = lines[pos].trim();
1095 if is_section_end(line) {
1096 return Ok((branches, terminal_shunts, pos + 1));
1097 }
1098 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
1099 return Ok((branches, terminal_shunts, pos + 1));
1100 }
1101 if line.starts_with("@!") {
1102 pos += 1;
1103 continue;
1104 }
1105
1106 let fields = tokenize_record(line);
1107 if fields.is_empty() {
1108 pos += 1;
1109 continue;
1110 }
1111
1112 let line_num = pos + 1;
1113 if fields.len() < 14 {
1115 pos += 1;
1116 continue;
1117 }
1118
1119 let from_bus = parse_f64(&fields[0], line_num, "I")? as u32;
1120 let to_bus = (parse_f64(&fields[1], line_num, "J")? as i64).unsigned_abs() as u32;
1121 let circuit = unquote(&fields[2]);
1122 let r = parse_f64(&fields[3], line_num, "R")?;
1123 let x = parse_f64(&fields[4], line_num, "X")?;
1124 let b = parse_f64(&fields[5], line_num, "B")?;
1125 let rate_a = if fields.len() > 6 {
1126 parse_f64(&fields[6], line_num, "RATEA")?
1127 } else {
1128 0.0
1129 };
1130 let rate_b = if fields.len() > 7 {
1131 parse_f64(&fields[7], line_num, "RATEB")?
1132 } else {
1133 0.0
1134 };
1135 let rate_c = if fields.len() > 8 {
1136 parse_f64(&fields[8], line_num, "RATEC")?
1137 } else {
1138 0.0
1139 };
1140 let gi = if fields.len() > 9 {
1143 parse_f64(&fields[9], line_num, "GI")?
1144 } else {
1145 0.0
1146 };
1147 let bi = if fields.len() > 10 {
1148 parse_f64(&fields[10], line_num, "BI")?
1149 } else {
1150 0.0
1151 };
1152 let gj = if fields.len() > 11 {
1153 parse_f64(&fields[11], line_num, "GJ")?
1154 } else {
1155 0.0
1156 };
1157 let bj = if fields.len() > 12 {
1158 parse_f64(&fields[12], line_num, "BJ")?
1159 } else {
1160 0.0
1161 };
1162 if gi.abs() > 1e-12 || bi.abs() > 1e-12 {
1165 terminal_shunts.push(RawShunt {
1166 bus: from_bus,
1167 status: 1,
1168 gl: gi * sbase,
1169 bl: bi * sbase,
1170 });
1171 }
1172 if gj.abs() > 1e-12 || bj.abs() > 1e-12 {
1173 terminal_shunts.push(RawShunt {
1174 bus: to_bus,
1175 status: 1,
1176 gl: gj * sbase,
1177 bl: bj * sbase,
1178 });
1179 }
1180 let st = if fields.len() > 13 {
1181 parse_status(&fields[13], line_num, "ST")?
1182 } else {
1183 1
1184 };
1185
1186 branches.push(Branch {
1187 from_bus,
1188 to_bus,
1189 circuit,
1190 r,
1191 x,
1192 b,
1193 rating_a_mva: rate_a,
1194 rating_b_mva: rate_b,
1195 rating_c_mva: rate_c,
1196 tap: 1.0,
1197 phase_shift_rad: 0.0,
1198 in_service: st > 0,
1199 angle_diff_min_rad: None,
1200 angle_diff_max_rad: None,
1201 g_pi: 0.0,
1202 g_mag: 0.0,
1203 b_mag: 0.0,
1204 tab: None,
1205 owners: parse_multi_owner_fields(&fields, 16),
1206 ..Branch::default()
1207 });
1208
1209 pos += 1;
1210 }
1211
1212 Err(PsseError::UnexpectedEof("Branch Data".into()))
1213}
1214
1215pub(crate) fn apply_cz_conversion(
1223 r: f64,
1224 x: f64,
1225 mut sbase_winding: f64,
1226 sbase_sys: f64,
1227 cz: u32,
1228) -> (f64, f64) {
1229 if sbase_winding.abs() < 1e-10 {
1232 tracing::warn!(
1233 sbase_winding,
1234 sbase_sys,
1235 cz,
1236 "transformer sbase_winding is zero; falling back to sbase_sys to avoid NaN"
1237 );
1238 sbase_winding = sbase_sys;
1239 }
1240 if sbase_sys.abs() < 1e-10 {
1242 return (0.0, 0.0);
1243 }
1244 match cz {
1245 1 => {
1246 (r, x)
1248 }
1249 2 => {
1250 if (sbase_winding - sbase_sys).abs() > 1e-10 {
1252 (r * sbase_sys / sbase_winding, x * sbase_sys / sbase_winding)
1253 } else {
1254 (r, x)
1255 }
1256 }
1257 3 => {
1258 let r_pu = r / (1_000_000.0 * sbase_winding);
1261 let x_mag = x / 100.0; let x_pu = if x_mag * x_mag > r_pu * r_pu {
1263 (x_mag * x_mag - r_pu * r_pu).sqrt()
1264 } else {
1265 x_mag
1266 };
1267 (
1268 r_pu * sbase_sys / sbase_winding,
1269 x_pu * sbase_sys / sbase_winding,
1270 )
1271 }
1272 _ => (r, x),
1273 }
1274}
1275
1276pub(crate) fn compute_winding_tap_pu(windv: f64, nomv: f64, bus_bkv: f64, cw: u32) -> f64 {
1283 match cw {
1284 1 => windv, 2 => {
1286 if bus_bkv > 0.0 {
1288 windv / bus_bkv
1289 } else {
1290 windv
1291 }
1292 }
1293 3 => {
1294 let n = if nomv > 0.0 { nomv } else { bus_bkv };
1296 if bus_bkv > 0.0 {
1297 windv * n / bus_bkv
1298 } else {
1299 windv
1300 }
1301 }
1302 _ => windv,
1303 }
1304}
1305
1306#[allow(clippy::too_many_arguments)]
1308pub(crate) fn make_xfmr_branch(
1309 from_bus: u32,
1310 to_bus: u32,
1311 circuit: String,
1312 r: f64,
1313 x: f64,
1314 rating_a_mva: f64,
1315 rating_b_mva: f64,
1316 rating_c_mva: f64,
1317 tap: f64,
1318 phase_shift_rad: f64,
1319 in_service: bool,
1320 g_mag: f64,
1321 b_mag: f64,
1322) -> Branch {
1323 Branch {
1324 from_bus,
1325 to_bus,
1326 circuit,
1327 r,
1328 x: if x.abs() < 1e-10 {
1329 if x < 0.0 { -1e-6 } else { 1e-6 }
1330 } else {
1331 x
1332 },
1333 b: 0.0,
1334 rating_a_mva,
1335 rating_b_mva,
1336 rating_c_mva,
1337 tap,
1338 phase_shift_rad: phase_shift_rad.to_radians(),
1339 in_service,
1340 angle_diff_min_rad: None,
1341 angle_diff_max_rad: None,
1342 g_pi: 0.0,
1343 g_mag,
1344 b_mag,
1345 tab: None,
1346 branch_type: BranchType::Transformer,
1347 ..Branch::default()
1348 }
1349}
1350
1351fn apply_3w_step_data(br: &mut Branch, cod: i32, rma: f64, rmi: f64, ntp: u32) {
1353 let range = (rma - rmi).abs();
1354 let n = if ntp > 0 { ntp as f64 } else { 32.0 };
1355 match cod.abs() {
1356 1 | 2 => {
1357 let ts = range / n;
1358 if ts > 1e-9 {
1359 let ctrl = br.opf_control.get_or_insert_with(BranchOpfControl::default);
1360 ctrl.tap_step = ts;
1361 ctrl.tap_min = rmi.min(rma);
1362 ctrl.tap_max = rmi.max(rma);
1363 }
1364 }
1365 3 => {
1366 let sd = range / n;
1367 if sd > 1e-9 {
1368 let ctrl = br.opf_control.get_or_insert_with(BranchOpfControl::default);
1369 ctrl.phase_step_rad = sd.to_radians();
1370 ctrl.phase_min_rad = rmi.min(rma).to_radians();
1371 ctrl.phase_max_rad = rmi.max(rma).to_radians();
1372 }
1373 }
1374 _ => {}
1375 }
1376}
1377
1378type TransformerSectionResult = (Vec<Branch>, Vec<Bus>, Vec<OltcSpec>, Vec<ParSpec>, usize);
1379
1380fn parse_transformer_section(
1381 lines: &[&str],
1382 start: usize,
1383 sbase: f64,
1384 _version: u32,
1385 bus_basekv: &HashMap<u32, f64>,
1386) -> Result<TransformerSectionResult, PsseError> {
1387 let mut transformers: Vec<Branch> = Vec::new();
1388 let mut star_buses: Vec<Bus> = Vec::new();
1389 let mut oltc_specs: Vec<OltcSpec> = Vec::new();
1390 let mut par_specs: Vec<ParSpec> = Vec::new();
1391 let mut pos = start;
1392
1393 let mut max_bus_num: u32 = bus_basekv.keys().copied().max().unwrap_or(0);
1396
1397 while pos < lines.len() {
1398 let line = lines[pos].trim();
1399 if is_section_end(line) {
1400 return Ok((transformers, star_buses, oltc_specs, par_specs, pos + 1));
1401 }
1402 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
1403 return Ok((transformers, star_buses, oltc_specs, par_specs, pos + 1));
1404 }
1405 if line.starts_with("@!") {
1406 pos += 1;
1407 continue;
1408 }
1409
1410 let rec1_fields = tokenize_record(line);
1412 if rec1_fields.is_empty() {
1413 pos += 1;
1414 continue;
1415 }
1416
1417 let line_num = pos + 1;
1418 if rec1_fields.len() < 12 {
1419 return Err(PsseError::Parse {
1420 line: line_num,
1421 message: "truncated transformer record 1".into(),
1422 });
1423 }
1424
1425 let from_bus = parse_f64(&rec1_fields[0], line_num, "I")? as u32;
1426 let to_bus = (parse_f64(&rec1_fields[1], line_num, "J")? as i64).unsigned_abs() as u32;
1427 let k = parse_f64(&rec1_fields[2], line_num, "K")? as i64;
1428 let circuit = unquote(&rec1_fields[3]);
1429 let cw = if rec1_fields.len() > 4 {
1430 parse_f64(&rec1_fields[4], line_num, "CW")? as u32
1431 } else {
1432 1
1433 };
1434 let cz = if rec1_fields.len() > 5 {
1435 parse_f64(&rec1_fields[5], line_num, "CZ")? as u32
1436 } else {
1437 1
1438 };
1439 let mag1 = if rec1_fields.len() > 7 {
1441 parse_f64(&rec1_fields[7], line_num, "MAG1").unwrap_or(0.0)
1442 } else {
1443 0.0
1444 };
1445 let mag2 = if rec1_fields.len() > 8 {
1446 parse_f64(&rec1_fields[8], line_num, "MAG2").unwrap_or(0.0)
1447 } else {
1448 0.0
1449 };
1450 let stat = if rec1_fields.len() > 11 {
1452 parse_status(&rec1_fields[11], line_num, "STAT")?
1453 } else {
1454 return Err(PsseError::Parse {
1455 line: line_num,
1456 message: "missing required transformer status (STAT) field".into(),
1457 });
1458 };
1459
1460 let xfmr_owners = parse_multi_owner_fields(&rec1_fields, 12);
1461
1462 let is_3winding = k != 0;
1463 let k_bus = k.unsigned_abs() as u32;
1464
1465 pos += 1;
1467 if pos >= lines.len() {
1468 return Err(PsseError::UnexpectedEof("Transformer Record 2".into()));
1469 }
1470 {
1474 let peek = lines[pos].trim();
1475 if is_section_end(peek) || peek.starts_with('Q') || peek.eq_ignore_ascii_case("q") {
1476 tracing::warn!(
1477 line = pos + 1,
1478 from_bus,
1479 to_bus,
1480 "Truncated transformer Record 2 at line {}; skipping transformer.",
1481 pos + 1
1482 );
1483 return Err(PsseError::UnexpectedEof("Transformer Record 2".into()));
1484 }
1485 }
1486 let rec2_fields = tokenize_record(lines[pos].trim());
1487 let line_num2 = pos + 1;
1488
1489 let r12_raw = if !rec2_fields.is_empty() {
1490 parse_f64(&rec2_fields[0], line_num2, "R1-2")?
1491 } else {
1492 0.0
1493 };
1494 let x12_raw = if rec2_fields.len() > 1 {
1495 parse_f64(&rec2_fields[1], line_num2, "X1-2")?
1496 } else {
1497 0.01
1498 };
1499 let sbase12 = if rec2_fields.len() > 2 {
1500 parse_f64(&rec2_fields[2], line_num2, "SBASE1-2")?
1501 } else {
1502 sbase
1503 };
1504
1505 let (r12, x12) = apply_cz_conversion(r12_raw, x12_raw, sbase12, sbase, cz);
1506
1507 if is_3winding {
1508 tracing::warn!(
1509 from_bus,
1510 to_bus,
1511 k_bus,
1512 "3-winding transformer modeled via star (Y) bus expansion; \
1513 fictitious internal bus inserted, tap ratios and magnetizing \
1514 admittance applied to winding 1 only"
1515 );
1516 let r23_raw = if rec2_fields.len() > 3 {
1522 parse_f64(&rec2_fields[3], line_num2, "R2-3").unwrap_or(0.0)
1523 } else {
1524 0.0
1525 };
1526 let x23_raw = if rec2_fields.len() > 4 {
1527 parse_f64(&rec2_fields[4], line_num2, "X2-3").unwrap_or(0.01)
1528 } else {
1529 0.01
1530 };
1531 let sbase23 = if rec2_fields.len() > 5 {
1532 parse_f64(&rec2_fields[5], line_num2, "SBASE2-3").unwrap_or(sbase)
1533 } else {
1534 sbase
1535 };
1536 let r31_raw = if rec2_fields.len() > 6 {
1537 parse_f64(&rec2_fields[6], line_num2, "R3-1").unwrap_or(0.0)
1538 } else {
1539 0.0
1540 };
1541 let x31_raw = if rec2_fields.len() > 7 {
1542 parse_f64(&rec2_fields[7], line_num2, "X3-1").unwrap_or(0.01)
1543 } else {
1544 0.01
1545 };
1546 let sbase31 = if rec2_fields.len() > 8 {
1547 parse_f64(&rec2_fields[8], line_num2, "SBASE3-1").unwrap_or(sbase)
1548 } else {
1549 sbase
1550 };
1551 let vmstar = if rec2_fields.len() > 9 {
1552 parse_f64(&rec2_fields[9], line_num2, "VMSTAR").unwrap_or(1.0)
1553 } else {
1554 1.0
1555 };
1556 let anstar_deg = if rec2_fields.len() > 10 {
1557 parse_f64(&rec2_fields[10], line_num2, "ANSTAR").unwrap_or(0.0)
1558 } else {
1559 0.0
1560 };
1561
1562 let (r23, x23) = apply_cz_conversion(r23_raw, x23_raw, sbase23, sbase, cz);
1563 let (r31, x31) = apply_cz_conversion(r31_raw, x31_raw, sbase31, sbase, cz);
1564
1565 let r1 = (r12 + r31 - r23) / 2.0;
1570 let x1 = (x12 + x31 - x23) / 2.0;
1571 let r2 = (r12 + r23 - r31) / 2.0;
1572 let x2 = (x12 + x23 - x31) / 2.0;
1573 let r3 = (r23 + r31 - r12) / 2.0;
1574 let x3 = (x23 + x31 - x12) / 2.0;
1575
1576 pos += 1;
1578 if pos >= lines.len() {
1579 return Err(PsseError::UnexpectedEof("Transformer Record 3 (3W)".into()));
1580 }
1581 {
1583 let peek = lines[pos].trim();
1584 if is_section_end(peek) || peek.starts_with('Q') || peek.eq_ignore_ascii_case("q") {
1585 tracing::warn!(
1586 line = pos + 1,
1587 from_bus,
1588 to_bus,
1589 k_bus,
1590 "Truncated 3W transformer Record 3 at line {}; skipping transformer.",
1591 pos + 1
1592 );
1593 return Err(PsseError::UnexpectedEof("Transformer Record 3 (3W)".into()));
1594 }
1595 }
1596 let rec3_fields = tokenize_record(lines[pos].trim());
1597 let line_num3 = pos + 1;
1598 let windv1 = if !rec3_fields.is_empty() {
1599 parse_f64(&rec3_fields[0], line_num3, "WINDV1")?
1600 } else {
1601 1.0
1602 };
1603 let nomv1 = if rec3_fields.len() > 1 {
1604 parse_f64(&rec3_fields[1], line_num3, "NOMV1").unwrap_or(0.0)
1605 } else {
1606 0.0
1607 };
1608 let ang1 = if rec3_fields.len() > 2 {
1609 parse_f64(&rec3_fields[2], line_num3, "ANG1").unwrap_or(0.0)
1610 } else {
1611 0.0
1612 };
1613 let rata1 = if rec3_fields.len() > 3 {
1614 parse_f64(&rec3_fields[3], line_num3, "RATA1").unwrap_or(0.0)
1615 } else {
1616 0.0
1617 };
1618 let ratb1 = if rec3_fields.len() > 4 {
1619 parse_f64(&rec3_fields[4], line_num3, "RATB1").unwrap_or(0.0)
1620 } else {
1621 0.0
1622 };
1623 let ratc1 = if rec3_fields.len() > 5 {
1624 parse_f64(&rec3_fields[5], line_num3, "RATC1").unwrap_or(0.0)
1625 } else {
1626 0.0
1627 };
1628
1629 let cod1_3w = if rec3_fields.len() > 6 {
1632 parse_f64(&rec3_fields[6], line_num3, "COD1").unwrap_or(0.0) as i32
1633 } else {
1634 0
1635 };
1636 let rma1_3w = if rec3_fields.len() > 8 {
1637 parse_f64(&rec3_fields[8], line_num3, "RMA1").unwrap_or(1.1)
1638 } else {
1639 1.1
1640 };
1641 let rmi1_3w = if rec3_fields.len() > 9 {
1642 parse_f64(&rec3_fields[9], line_num3, "RMI1").unwrap_or(0.9)
1643 } else {
1644 0.9
1645 };
1646 let ntp1_3w = if rec3_fields.len() > 12 {
1647 parse_f64(&rec3_fields[12], line_num3, "NTP1").unwrap_or(33.0) as u32
1648 } else {
1649 33
1650 };
1651
1652 pos += 1;
1654 if pos >= lines.len() {
1655 return Err(PsseError::UnexpectedEof("Transformer Record 4 (3W)".into()));
1656 }
1657 {
1659 let peek = lines[pos].trim();
1660 if is_section_end(peek) || peek.starts_with('Q') || peek.eq_ignore_ascii_case("q") {
1661 tracing::warn!(
1662 line = pos + 1,
1663 from_bus,
1664 to_bus,
1665 k_bus,
1666 "Truncated 3W transformer Record 4 at line {}; skipping transformer.",
1667 pos + 1
1668 );
1669 return Err(PsseError::UnexpectedEof("Transformer Record 4 (3W)".into()));
1670 }
1671 }
1672 let rec4_fields = tokenize_record(lines[pos].trim());
1673 let line_num4 = pos + 1;
1674 let windv2 = if !rec4_fields.is_empty() {
1675 parse_f64(&rec4_fields[0], line_num4, "WINDV2")?
1676 } else {
1677 1.0
1678 };
1679 let nomv2 = if rec4_fields.len() > 1 {
1680 parse_f64(&rec4_fields[1], line_num4, "NOMV2").unwrap_or(0.0)
1681 } else {
1682 0.0
1683 };
1684 let ang2 = if rec4_fields.len() > 2 {
1685 parse_f64(&rec4_fields[2], line_num4, "ANG2").unwrap_or(0.0)
1686 } else {
1687 0.0
1688 };
1689 let rata2 = if rec4_fields.len() > 3 {
1690 parse_f64(&rec4_fields[3], line_num4, "RATA2").unwrap_or(0.0)
1691 } else {
1692 0.0
1693 };
1694 let ratb2 = if rec4_fields.len() > 4 {
1695 parse_f64(&rec4_fields[4], line_num4, "RATB2").unwrap_or(0.0)
1696 } else {
1697 0.0
1698 };
1699 let ratc2 = if rec4_fields.len() > 5 {
1700 parse_f64(&rec4_fields[5], line_num4, "RATC2").unwrap_or(0.0)
1701 } else {
1702 0.0
1703 };
1704 let cod2_3w = if rec4_fields.len() > 6 {
1706 parse_f64(&rec4_fields[6], line_num4, "COD2").unwrap_or(0.0) as i32
1707 } else {
1708 0
1709 };
1710 let rma2_3w = if rec4_fields.len() > 8 {
1711 parse_f64(&rec4_fields[8], line_num4, "RMA2").unwrap_or(1.1)
1712 } else {
1713 1.1
1714 };
1715 let rmi2_3w = if rec4_fields.len() > 9 {
1716 parse_f64(&rec4_fields[9], line_num4, "RMI2").unwrap_or(0.9)
1717 } else {
1718 0.9
1719 };
1720 let ntp2_3w = if rec4_fields.len() > 12 {
1721 parse_f64(&rec4_fields[12], line_num4, "NTP2").unwrap_or(33.0) as u32
1722 } else {
1723 33
1724 };
1725
1726 pos += 1;
1728 if pos >= lines.len() {
1729 return Err(PsseError::UnexpectedEof("Transformer Record 5 (3W)".into()));
1730 }
1731 {
1733 let peek = lines[pos].trim();
1734 if is_section_end(peek) || peek.starts_with('Q') || peek.eq_ignore_ascii_case("q") {
1735 tracing::warn!(
1736 line = pos + 1,
1737 from_bus,
1738 to_bus,
1739 k_bus,
1740 "Truncated 3W transformer Record 5 at line {}; skipping transformer.",
1741 pos + 1
1742 );
1743 return Err(PsseError::UnexpectedEof("Transformer Record 5 (3W)".into()));
1744 }
1745 }
1746 let rec5_fields = tokenize_record(lines[pos].trim());
1747 let line_num5 = pos + 1;
1748 let windv3 = if !rec5_fields.is_empty() {
1749 parse_f64(&rec5_fields[0], line_num5, "WINDV3")?
1750 } else {
1751 1.0
1752 };
1753 let nomv3 = if rec5_fields.len() > 1 {
1754 parse_f64(&rec5_fields[1], line_num5, "NOMV3").unwrap_or(0.0)
1755 } else {
1756 0.0
1757 };
1758 let ang3 = if rec5_fields.len() > 2 {
1759 parse_f64(&rec5_fields[2], line_num5, "ANG3").unwrap_or(0.0)
1760 } else {
1761 0.0
1762 };
1763 let rata3 = if rec5_fields.len() > 3 {
1764 parse_f64(&rec5_fields[3], line_num5, "RATA3").unwrap_or(0.0)
1765 } else {
1766 0.0
1767 };
1768 let ratb3 = if rec5_fields.len() > 4 {
1769 parse_f64(&rec5_fields[4], line_num5, "RATB3").unwrap_or(0.0)
1770 } else {
1771 0.0
1772 };
1773 let ratc3 = if rec5_fields.len() > 5 {
1774 parse_f64(&rec5_fields[5], line_num5, "RATC3").unwrap_or(0.0)
1775 } else {
1776 0.0
1777 };
1778 let cod3_3w = if rec5_fields.len() > 6 {
1780 parse_f64(&rec5_fields[6], line_num5, "COD3").unwrap_or(0.0) as i32
1781 } else {
1782 0
1783 };
1784 let rma3_3w = if rec5_fields.len() > 8 {
1785 parse_f64(&rec5_fields[8], line_num5, "RMA3").unwrap_or(1.1)
1786 } else {
1787 1.1
1788 };
1789 let rmi3_3w = if rec5_fields.len() > 9 {
1790 parse_f64(&rec5_fields[9], line_num5, "RMI3").unwrap_or(0.9)
1791 } else {
1792 0.9
1793 };
1794 let ntp3_3w = if rec5_fields.len() > 12 {
1795 parse_f64(&rec5_fields[12], line_num5, "NTP3").unwrap_or(33.0) as u32
1796 } else {
1797 33
1798 };
1799
1800 let bkv1 = bus_basekv.get(&from_bus).copied().unwrap_or(1.0);
1802 let bkv2 = bus_basekv.get(&to_bus).copied().unwrap_or(1.0);
1803 let bkv3 = bus_basekv.get(&k_bus).copied().unwrap_or(1.0);
1804 let tap1 = compute_winding_tap_pu(windv1, nomv1, bkv1, cw);
1805 let tap2 = compute_winding_tap_pu(windv2, nomv2, bkv2, cw);
1806 let tap3 = compute_winding_tap_pu(windv3, nomv3, bkv3, cw);
1807
1808 max_bus_num += 1;
1810 let star_bus_num = max_bus_num;
1811 star_buses.push(Bus {
1812 number: star_bus_num,
1813 name: format!("STAR_{from_bus}_{to_bus}_{k_bus}"),
1814 bus_type: BusType::PQ,
1815 shunt_conductance_mw: 0.0,
1816 shunt_susceptance_mvar: 0.0,
1817 area: 1,
1818 voltage_magnitude_pu: vmstar,
1819 voltage_angle_rad: anstar_deg.to_radians(),
1820 base_kv: bkv1.max(bkv2).max(bkv3).max(1.0), zone: 1,
1822 voltage_max_pu: 1.1,
1823 voltage_min_pu: 0.9,
1824 island_id: 0,
1825 latitude: None,
1826 longitude: None,
1827 ..Bus::new(0, BusType::PQ, 0.0)
1828 });
1829
1830 let in_service = stat > 0;
1831
1832 let mut w1 = make_xfmr_branch(
1835 from_bus,
1836 star_bus_num,
1837 circuit.clone(),
1838 r1,
1839 x1,
1840 rata1,
1841 ratb1,
1842 ratc1,
1843 tap1,
1844 ang1,
1845 in_service,
1846 mag1,
1847 mag2,
1848 );
1849 w1.branch_type = BranchType::Transformer3W;
1850 w1.owners = xfmr_owners.clone();
1851 apply_3w_step_data(&mut w1, cod1_3w, rma1_3w, rmi1_3w, ntp1_3w);
1852 transformers.push(w1);
1853
1854 let mut w2 = make_xfmr_branch(
1856 to_bus,
1857 star_bus_num,
1858 circuit.clone(),
1859 r2,
1860 x2,
1861 rata2,
1862 ratb2,
1863 ratc2,
1864 tap2,
1865 ang2,
1866 in_service,
1867 0.0,
1868 0.0,
1869 );
1870 w2.branch_type = BranchType::Transformer3W;
1871 w2.owners = xfmr_owners.clone();
1872 apply_3w_step_data(&mut w2, cod2_3w, rma2_3w, rmi2_3w, ntp2_3w);
1873 transformers.push(w2);
1874
1875 let mut w3 = make_xfmr_branch(
1877 k_bus,
1878 star_bus_num,
1879 circuit,
1880 r3,
1881 x3,
1882 rata3,
1883 ratb3,
1884 ratc3,
1885 tap3,
1886 ang3,
1887 in_service,
1888 0.0,
1889 0.0,
1890 );
1891 w3.branch_type = BranchType::Transformer3W;
1892 w3.owners = xfmr_owners.clone();
1893 apply_3w_step_data(&mut w3, cod3_3w, rma3_3w, rmi3_3w, ntp3_3w);
1894 transformers.push(w3);
1895
1896 tracing::debug!(
1897 from_bus,
1898 to_bus,
1899 k_bus,
1900 star_bus = star_bus_num,
1901 "3-winding transformer expanded to star topology"
1902 );
1903 } else {
1904 pos += 1;
1910 if pos >= lines.len() {
1911 return Err(PsseError::UnexpectedEof("Transformer Record 3".into()));
1912 }
1913 {
1915 let peek = lines[pos].trim();
1916 if is_section_end(peek) || peek.starts_with('Q') || peek.eq_ignore_ascii_case("q") {
1917 tracing::warn!(
1918 line = pos + 1,
1919 from_bus,
1920 to_bus,
1921 "Truncated 2W transformer Record 3 at line {}; skipping transformer.",
1922 pos + 1
1923 );
1924 return Err(PsseError::UnexpectedEof("Transformer Record 3".into()));
1925 }
1926 }
1927 let rec3_fields = tokenize_record(lines[pos].trim());
1928 let line_num3 = pos + 1;
1929
1930 let windv1 = if !rec3_fields.is_empty() {
1931 parse_f64(&rec3_fields[0], line_num3, "WINDV1")?
1932 } else {
1933 1.0
1934 };
1935 let nomv1 = if rec3_fields.len() > 1 {
1936 parse_f64(&rec3_fields[1], line_num3, "NOMV1")?
1937 } else {
1938 0.0
1939 };
1940 let ang1 = if rec3_fields.len() > 2 {
1941 parse_f64(&rec3_fields[2], line_num3, "ANG1")?
1942 } else {
1943 0.0
1944 };
1945 let rata1 = if rec3_fields.len() > 3 {
1946 parse_f64(&rec3_fields[3], line_num3, "RATA1")?
1947 } else {
1948 0.0
1949 };
1950 let ratb1 = if rec3_fields.len() > 4 {
1951 parse_f64(&rec3_fields[4], line_num3, "RATB1")?
1952 } else {
1953 0.0
1954 };
1955 let ratc1 = if rec3_fields.len() > 5 {
1956 parse_f64(&rec3_fields[5], line_num3, "RATC1")?
1957 } else {
1958 0.0
1959 };
1960 let cod1 = if rec3_fields.len() > 6 {
1962 parse_f64(&rec3_fields[6], line_num3, "COD1").unwrap_or(0.0) as i32
1963 } else {
1964 0
1965 };
1966 let cont1 = if rec3_fields.len() > 7 {
1967 parse_f64(&rec3_fields[7], line_num3, "CONT1").unwrap_or(0.0) as i32
1968 } else {
1969 0
1970 };
1971 let rma1 = if rec3_fields.len() > 8 {
1972 parse_f64(&rec3_fields[8], line_num3, "RMA1").unwrap_or(1.1)
1973 } else {
1974 1.1
1975 };
1976 let rmi1 = if rec3_fields.len() > 9 {
1977 parse_f64(&rec3_fields[9], line_num3, "RMI1").unwrap_or(0.9)
1978 } else {
1979 0.9
1980 };
1981 let vma1 = if rec3_fields.len() > 10 {
1982 parse_f64(&rec3_fields[10], line_num3, "VMA1").unwrap_or(1.1)
1983 } else {
1984 1.1
1985 };
1986 let vmi1 = if rec3_fields.len() > 11 {
1987 parse_f64(&rec3_fields[11], line_num3, "VMI1").unwrap_or(0.9)
1988 } else {
1989 0.9
1990 };
1991 let ntp1 = if rec3_fields.len() > 12 {
1992 parse_f64(&rec3_fields[12], line_num3, "NTP1").unwrap_or(33.0) as u32
1993 } else {
1994 33
1995 };
1996
1997 match cod1 {
2002 1 | 2 | -1 | -2 => {
2003 let regulated_bus = cont1.unsigned_abs(); let v_target = (vma1 + vmi1) * 0.5;
2008 let v_band = (vma1 - vmi1).abs().max(0.001); let tap_range = (rma1 - rmi1).abs();
2011 let tap_step = if ntp1 > 0 {
2012 tap_range / ntp1 as f64
2013 } else {
2014 tap_range / 32.0 };
2016 if tap_step > 1e-9 {
2017 oltc_specs.push(OltcSpec {
2018 from_bus,
2019 to_bus,
2020 circuit: circuit.to_string(),
2021 regulated_bus,
2022 v_target,
2023 v_band,
2024 tap_min: rmi1.min(rma1),
2025 tap_max: rmi1.max(rma1),
2026 tap_step,
2027 });
2028 }
2029 }
2030 3 | -3 => {
2031 let monitored_from_bus = cont1.unsigned_abs();
2036 let p_min_mw = vmi1.min(vma1);
2037 let p_max_mw = vmi1.max(vma1);
2038 let p_target_mw = (p_min_mw + p_max_mw) * 0.5;
2039 let p_band_mw = (p_max_mw - p_min_mw).max(1.0); let ang_range = (rma1 - rmi1).abs();
2041 let ang_step_deg = if ntp1 > 0 {
2042 ang_range / ntp1 as f64
2043 } else {
2044 ang_range / 32.0
2045 };
2046 if ang_step_deg > 1e-9 {
2047 par_specs.push(ParSpec {
2048 from_bus,
2049 to_bus,
2050 circuit: circuit.to_string(),
2051 monitored_from_bus,
2053 monitored_to_bus: 0, monitored_circuit: "1".to_string(),
2055 p_target_mw,
2056 p_band_mw,
2057 angle_min_deg: rmi1.min(rma1),
2058 angle_max_deg: rmi1.max(rma1),
2059 ang_step_deg,
2060 });
2061 }
2062 }
2063 _ => {} }
2065
2066 let tab1: Option<u32> = if rec3_fields.len() > 13 {
2067 let v = parse_f64(&rec3_fields[13], line_num3, "TAB1").unwrap_or(0.0) as i32;
2068 if v > 0 { Some(v as u32) } else { None }
2069 } else {
2070 None
2071 };
2072
2073 pos += 1;
2075 if pos >= lines.len() {
2076 return Err(PsseError::UnexpectedEof("Transformer Record 4".into()));
2077 }
2078 {
2080 let peek = lines[pos].trim();
2081 if is_section_end(peek) || peek.starts_with('Q') || peek.eq_ignore_ascii_case("q") {
2082 tracing::warn!(
2083 line = pos + 1,
2084 from_bus,
2085 to_bus,
2086 "Truncated 2W transformer Record 4 at line {}; skipping transformer.",
2087 pos + 1
2088 );
2089 return Err(PsseError::UnexpectedEof("Transformer Record 4".into()));
2090 }
2091 }
2092 let rec4_fields = tokenize_record(lines[pos].trim());
2093 let line_num4 = pos + 1;
2094
2095 let windv2 = if !rec4_fields.is_empty() {
2096 parse_f64(&rec4_fields[0], line_num4, "WINDV2")?
2097 } else {
2098 1.0
2099 };
2100
2101 let tap = match cw {
2103 1 => {
2104 if windv2 != 0.0 {
2106 windv1 / windv2
2107 } else {
2108 windv1
2109 }
2110 }
2111 2 => {
2112 let bkv1 = bus_basekv.get(&from_bus).copied().unwrap_or(1.0);
2114 let bkv2 = bus_basekv.get(&to_bus).copied().unwrap_or(1.0);
2115 let t1 = if bkv1 > 0.0 { windv1 / bkv1 } else { windv1 };
2116 let t2 = if bkv2 > 0.0 { windv2 / bkv2 } else { windv2 };
2117 if t2 != 0.0 { t1 / t2 } else { t1 }
2118 }
2119 3 => {
2120 let bkv1 = bus_basekv.get(&from_bus).copied().unwrap_or(1.0);
2122 let bkv2 = bus_basekv.get(&to_bus).copied().unwrap_or(1.0);
2123 let n1 = if nomv1 > 0.0 { nomv1 } else { bkv1 };
2124 let nomv2 = if rec4_fields.len() > 1 {
2125 parse_f64(&rec4_fields[1], line_num4, "NOMV2").unwrap_or(0.0)
2126 } else {
2127 0.0
2128 };
2129 let n2 = if nomv2 > 0.0 { nomv2 } else { bkv2 };
2130 let t1 = if bkv1 > 0.0 {
2131 windv1 * n1 / bkv1
2132 } else {
2133 windv1
2134 };
2135 let t2 = if bkv2 > 0.0 {
2136 windv2 * n2 / bkv2
2137 } else {
2138 windv2
2139 };
2140 if t2 != 0.0 { t1 / t2 } else { t1 }
2141 }
2142 _ => windv1,
2143 };
2144
2145 let mut xfmr = make_xfmr_branch(
2147 from_bus,
2148 to_bus,
2149 circuit,
2150 r12,
2151 x12,
2152 rata1,
2153 ratb1,
2154 ratc1,
2155 tap,
2156 ang1,
2157 stat > 0,
2158 mag1,
2159 mag2,
2160 );
2161 xfmr.tab = tab1;
2162 xfmr.owners = xfmr_owners.clone();
2163 match cod1.abs() {
2167 1 | 2 => {
2168 let tap_range = (rma1 - rmi1).abs();
2169 let ts = if ntp1 > 0 {
2170 tap_range / ntp1 as f64
2171 } else {
2172 tap_range / 32.0
2173 };
2174 if ts > 1e-9 {
2175 let ctrl = xfmr
2176 .opf_control
2177 .get_or_insert_with(BranchOpfControl::default);
2178 ctrl.tap_step = ts;
2179 ctrl.tap_min = rmi1.min(rma1);
2180 ctrl.tap_max = rmi1.max(rma1);
2181 }
2182 }
2183 3 => {
2184 let ang_range = (rma1 - rmi1).abs();
2185 let as_deg = if ntp1 > 0 {
2186 ang_range / ntp1 as f64
2187 } else {
2188 ang_range / 32.0
2189 };
2190 if as_deg > 1e-9 {
2191 let ctrl = xfmr
2192 .opf_control
2193 .get_or_insert_with(BranchOpfControl::default);
2194 ctrl.phase_step_rad = as_deg.to_radians();
2195 ctrl.phase_min_rad = rmi1.min(rma1).to_radians();
2196 ctrl.phase_max_rad = rmi1.max(rma1).to_radians();
2197 }
2198 }
2199 _ => {}
2200 }
2201 transformers.push(xfmr);
2202 }
2203
2204 pos += 1;
2205 }
2206
2207 Err(PsseError::UnexpectedEof("Transformer Data".into()))
2208}
2209
2210fn parse_area_schedule_section(
2220 lines: &[&str],
2221 start: usize,
2222) -> Result<(Vec<AreaSchedule>, usize), PsseError> {
2223 let mut areas = Vec::new();
2224 let mut pos = start;
2225
2226 while pos < lines.len() {
2227 let line = lines[pos].trim();
2228
2229 if is_section_end(line) {
2230 return Ok((areas, pos + 1));
2231 }
2232 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
2233 return Ok((areas, pos + 1));
2234 }
2235 if line.is_empty() || line.starts_with("@!") {
2236 pos += 1;
2237 continue;
2238 }
2239
2240 let fields = tokenize_record(line);
2241 if fields.is_empty() {
2242 pos += 1;
2243 continue;
2244 }
2245 if fields.len() < 5 {
2246 return Err(PsseError::Parse {
2247 line: pos + 1,
2248 message: "area interchange record is truncated".into(),
2249 });
2250 }
2251
2252 let line_num = pos + 1;
2253
2254 let number = parse_f64(&fields[0], line_num, "ARNUM")? as u32;
2255 let slack_bus = parse_f64(&fields[1], line_num, "ISW")? as u32;
2256 let p_desired_mw = parse_f64(&fields[2], line_num, "PDES")?;
2257 let p_tolerance_mw = parse_f64(&fields[3], line_num, "PTOL")?;
2258 let name = unquote(&fields[4]);
2259
2260 areas.push(AreaSchedule {
2261 number,
2262 slack_bus,
2263 p_desired_mw,
2264 p_tolerance_mw,
2265 name,
2266 });
2267
2268 pos += 1;
2269 }
2270
2271 Ok((areas, pos))
2272}
2273
2274fn parse_dc_line_section(
2287 lines: &[&str],
2288 start: usize,
2289) -> Result<(Vec<LccHvdcLink>, usize), PsseError> {
2290 let mut lcc_links = Vec::new();
2291 let mut pos = start;
2292
2293 while pos < lines.len() {
2294 let line1 = lines[pos].trim();
2296
2297 if is_section_end(line1) {
2298 return Ok((lcc_links, pos + 1));
2299 }
2300 if line1.starts_with('Q') || line1.eq_ignore_ascii_case("q") {
2301 return Ok((lcc_links, pos + 1));
2302 }
2303 if line1.is_empty() || line1.starts_with("@!") {
2304 pos += 1;
2305 continue;
2306 }
2307
2308 let f1 = tokenize_record(line1);
2309 if f1.is_empty() {
2310 pos += 1;
2311 continue;
2312 }
2313 if f1.len() < 12 {
2314 return Err(PsseError::Parse {
2315 line: pos + 1,
2316 message: format!(
2317 "truncated PSS/E two-terminal DC record: expected at least 12 fields, got {}",
2318 f1.len()
2319 ),
2320 });
2321 }
2322
2323 let line_num = pos + 1;
2324
2325 let name = unquote(&f1[0]);
2326 let mdc = parse_required_f64(&f1[1], line_num, "MDC")? as u32;
2327 let resistance_ohm = parse_required_f64(&f1[2], line_num, "RDC")?;
2328 let setvl = parse_required_f64(&f1[3], line_num, "SETVL")?;
2329 let vschd = parse_required_f64(&f1[4], line_num, "VSCHD")?;
2330 let vcmod = if f1.len() > 5 {
2331 parse_f64(&f1[5], line_num, "VCMOD").unwrap_or(0.0)
2332 } else {
2333 0.0
2334 };
2335 let rcomp = if f1.len() > 6 {
2336 parse_f64(&f1[6], line_num, "RCOMP").unwrap_or(0.0)
2337 } else {
2338 0.0
2339 };
2340 let delti = if f1.len() > 7 {
2341 parse_f64(&f1[7], line_num, "DELTI").unwrap_or(0.0)
2342 } else {
2343 0.0
2344 };
2345 let meter = if f1.len() > 8 {
2346 unquote(&f1[8]).chars().next().unwrap_or('I')
2347 } else {
2348 'I'
2349 };
2350 let dcvmin = if f1.len() > 9 {
2351 parse_f64(&f1[9], line_num, "DCVMIN").unwrap_or(0.0)
2352 } else {
2353 0.0
2354 };
2355 let cccitmx = if f1.len() > 10 {
2356 parse_f64(&f1[10], line_num, "CCCITMX").unwrap_or(20.0) as u32
2357 } else {
2358 20
2359 };
2360 let cccacc = if f1.len() > 11 {
2361 parse_f64(&f1[11], line_num, "CCCACC").unwrap_or(1.0)
2362 } else {
2363 1.0
2364 };
2365
2366 pos += 1;
2367
2368 if pos >= lines.len() {
2370 break;
2371 }
2372 while pos < lines.len()
2374 && (lines[pos].trim().is_empty() || lines[pos].trim().starts_with("@!"))
2375 {
2376 pos += 1;
2377 }
2378 if pos >= lines.len() {
2379 break;
2380 }
2381
2382 let f2 = tokenize_record(lines[pos].trim());
2383 let line_num2 = pos + 1;
2384 let rectifier = parse_dc_converter_record(&f2, line_num2)?;
2385 pos += 1;
2386
2387 if pos >= lines.len() {
2389 break;
2390 }
2391 while pos < lines.len()
2392 && (lines[pos].trim().is_empty() || lines[pos].trim().starts_with("@!"))
2393 {
2394 pos += 1;
2395 }
2396 if pos >= lines.len() {
2397 break;
2398 }
2399
2400 let f3 = tokenize_record(lines[pos].trim());
2401 let line_num3 = pos + 1;
2402 let inverter = parse_dc_converter_record(&f3, line_num3)?;
2403 pos += 1;
2404
2405 lcc_links.push(LccHvdcLink {
2406 name,
2407 mode: LccHvdcControlMode::from_u32(mdc),
2408 resistance_ohm,
2409 scheduled_setpoint: setvl,
2410 scheduled_voltage_kv: vschd,
2411 voltage_mode_switch_kv: vcmod,
2412 compounding_resistance_ohm: rcomp,
2413 current_margin_ka: delti,
2414 meter,
2415 voltage_min_kv: dcvmin,
2416 ac_dc_iteration_max: cccitmx,
2417 ac_dc_iteration_acceleration: cccacc,
2418 rectifier,
2419 inverter,
2420 p_dc_min_mw: 0.0,
2424 p_dc_max_mw: 0.0,
2425 });
2426 }
2427
2428 Ok((lcc_links, pos))
2429}
2430
2431fn parse_dc_converter_record(
2435 fields: &[String],
2436 line_num: usize,
2437) -> Result<LccConverterTerminal, PsseError> {
2438 if fields.len() < 13 {
2439 return Err(PsseError::Parse {
2440 line: line_num,
2441 message: format!(
2442 "truncated PSS/E DC converter record: expected at least 13 fields, got {}",
2443 fields.len()
2444 ),
2445 });
2446 }
2447 let bus = parse_required_f64(&fields[0], line_num, "IBUS")? as u32;
2448 let n_bridges = parse_required_f64(&fields[1], line_num, "NBRIDGES")? as u32;
2449 let alpha_max = parse_required_f64(&fields[2], line_num, "ANGMAX")?;
2450 let alpha_min = parse_required_f64(&fields[3], line_num, "ANGMIN")?;
2451 let r_comm = parse_required_f64(&fields[4], line_num, "RC")?;
2452 let x_comm = parse_required_f64(&fields[5], line_num, "XC")?;
2453 let e_base = parse_required_f64(&fields[6], line_num, "EBASE")?;
2454 let tr = parse_required_f64(&fields[7], line_num, "TR")?;
2455 let tap = parse_required_f64(&fields[8], line_num, "TAP")?;
2456 let tap_max = parse_required_f64(&fields[9], line_num, "TAPMAX")?;
2457 let tap_min = parse_required_f64(&fields[10], line_num, "TAPMIN")?;
2458 let tap_step = parse_required_f64(&fields[11], line_num, "TAPSTEP")?;
2459 let in_service = parse_status(&fields[12], line_num, "IC")? != 0;
2460
2461 Ok(LccConverterTerminal {
2462 bus,
2463 n_bridges,
2464 alpha_max,
2465 alpha_min,
2466 commutation_resistance_ohm: r_comm,
2467 commutation_reactance_ohm: x_comm,
2468 base_voltage_kv: e_base,
2469 turns_ratio: tr,
2470 tap,
2471 tap_max,
2472 tap_min,
2473 tap_step,
2474 in_service,
2475 })
2476}
2477
2478fn parse_vsc_dc_section(
2491 lines: &[&str],
2492 start: usize,
2493) -> Result<(Vec<VscHvdcLink>, usize), PsseError> {
2494 let mut vsc_lines = Vec::new();
2495 let mut pos = start;
2496
2497 while pos < lines.len() {
2498 let line1 = lines[pos].trim();
2500
2501 if is_section_end(line1) {
2502 return Ok((vsc_lines, pos + 1));
2503 }
2504 if line1.starts_with('Q') || line1.eq_ignore_ascii_case("q") {
2505 return Ok((vsc_lines, pos + 1));
2506 }
2507 if line1.is_empty() || line1.starts_with("@!") {
2508 pos += 1;
2509 continue;
2510 }
2511
2512 let f1 = tokenize_record(line1);
2513 if f1.is_empty() {
2514 pos += 1;
2515 continue;
2516 }
2517 if f1.len() < 3 {
2518 return Err(PsseError::Parse {
2519 line: pos + 1,
2520 message: format!(
2521 "truncated PSS/E VSC DC record: expected at least 3 fields, got {}",
2522 f1.len()
2523 ),
2524 });
2525 }
2526
2527 let line_num = pos + 1;
2528 let name = unquote(&f1[0]);
2529 let mdc = parse_required_f64(&f1[1], line_num, "MDC")? as u32;
2530 let resistance_ohm = parse_required_f64(&f1[2], line_num, "RDC")?;
2531 pos += 1;
2532
2533 while pos < lines.len()
2535 && (lines[pos].trim().is_empty() || lines[pos].trim().starts_with("@!"))
2536 {
2537 pos += 1;
2538 }
2539 if pos >= lines.len() {
2540 break;
2541 }
2542
2543 let f2 = tokenize_record(lines[pos].trim());
2544 let conv1 = parse_vsc_converter_record(&f2, pos + 1)?;
2545 pos += 1;
2546
2547 while pos < lines.len()
2549 && (lines[pos].trim().is_empty() || lines[pos].trim().starts_with("@!"))
2550 {
2551 pos += 1;
2552 }
2553 if pos >= lines.len() {
2554 break;
2555 }
2556
2557 let f3 = tokenize_record(lines[pos].trim());
2558 let conv2 = parse_vsc_converter_record(&f3, pos + 1)?;
2559 pos += 1;
2560
2561 vsc_lines.push(VscHvdcLink {
2562 name,
2563 mode: VscHvdcControlMode::from_u32(mdc),
2564 resistance_ohm,
2565 converter1: conv1,
2566 converter2: conv2,
2567 });
2568 }
2569
2570 Ok((vsc_lines, pos))
2571}
2572
2573fn parse_vsc_converter_record(
2577 fields: &[String],
2578 line_num: usize,
2579) -> Result<VscConverterTerminal, PsseError> {
2580 if fields.len() < 14 {
2581 return Err(PsseError::Parse {
2582 line: line_num,
2583 message: format!(
2584 "truncated PSS/E VSC converter record: expected at least 14 fields, got {}",
2585 fields.len()
2586 ),
2587 });
2588 }
2589 let bus = parse_required_f64(&fields[0], line_num, "IBUS")? as u32;
2590 let control_mode = if fields.len() > 2 {
2592 let m = parse_required_f64(&fields[2], line_num, "MODE")? as u32;
2593 VscConverterAcControlMode::from_u32(m)
2594 } else {
2595 VscConverterAcControlMode::ReactivePower
2596 };
2597 let dc_setpoint = if fields.len() > 3 {
2598 parse_required_f64(&fields[3], line_num, "DCSET")?
2599 } else {
2600 return Err(PsseError::Parse {
2601 line: line_num,
2602 message: "missing required DCSET field".into(),
2603 });
2604 };
2605 let ac_setpoint = if fields.len() > 4 {
2606 parse_required_f64(&fields[4], line_num, "ACSET")?
2607 } else {
2608 return Err(PsseError::Parse {
2609 line: line_num,
2610 message: "missing required ACSET field".into(),
2611 });
2612 };
2613 let loss_a = if fields.len() > 5 {
2614 parse_required_f64(&fields[5], line_num, "ALOSS")?
2615 } else {
2616 return Err(PsseError::Parse {
2617 line: line_num,
2618 message: "missing required ALOSS field".into(),
2619 });
2620 };
2621 let loss_b = if fields.len() > 6 {
2622 parse_required_f64(&fields[6], line_num, "BLOSS")?
2623 } else {
2624 return Err(PsseError::Parse {
2625 line: line_num,
2626 message: "missing required BLOSS field".into(),
2627 });
2628 };
2629 let q_max = if fields.len() > 8 {
2631 parse_f64(&fields[8], line_num, "SMX").unwrap_or(9999.0)
2632 } else {
2633 9999.0
2634 };
2635 let q_min = if fields.len() > 9 {
2636 parse_f64(&fields[9], line_num, "SMN").unwrap_or(-9999.0)
2637 } else {
2638 -9999.0
2639 };
2640 let v_max = if fields.len() > 10 {
2641 parse_f64(&fields[10], line_num, "GMX").unwrap_or(1.1)
2642 } else {
2643 1.1
2644 };
2645 let v_min = if fields.len() > 11 {
2646 parse_required_f64(&fields[11], line_num, "GMN")?
2647 } else {
2648 return Err(PsseError::Parse {
2649 line: line_num,
2650 message: "missing required GMN field".into(),
2651 });
2652 };
2653 let in_service = if fields.len() > 13 {
2655 parse_status(&fields[13], line_num, "STATE")? != 0
2656 } else {
2657 return Err(PsseError::Parse {
2658 line: line_num,
2659 message: "missing required STATE field".into(),
2660 });
2661 };
2662
2663 Ok(VscConverterTerminal {
2664 bus,
2665 control_mode,
2666 dc_setpoint,
2667 ac_setpoint,
2668 loss_constant_mw: loss_a,
2669 loss_linear: loss_b,
2670 q_min_mvar: q_min,
2671 q_max_mvar: q_max,
2672 voltage_min_pu: v_min,
2673 voltage_max_pu: v_max,
2674 in_service,
2675 })
2676}
2677
2678fn parse_facts_section(lines: &[&str], start: usize) -> (Vec<FactsDevice>, usize) {
2689 let mut devices = Vec::new();
2690 let mut pos = start;
2691
2692 while pos < lines.len() {
2693 let line = lines[pos].trim();
2694
2695 if is_section_end(line) {
2696 return (devices, pos + 1);
2697 }
2698 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
2699 return (devices, pos + 1);
2700 }
2701 if line.is_empty() || line.starts_with("@!") {
2702 pos += 1;
2703 continue;
2704 }
2705
2706 let fields = tokenize_record(line);
2707 if fields.is_empty() {
2708 pos += 1;
2709 continue;
2710 }
2711
2712 let line_num = pos + 1;
2713
2714 let name = unquote(&fields[0]);
2715 let bus_i = if fields.len() > 1 {
2716 parse_f64(&fields[1], line_num, "I").unwrap_or(0.0) as u32
2717 } else {
2718 0
2719 };
2720 let bus_j = if fields.len() > 2 {
2721 parse_f64(&fields[2], line_num, "J").unwrap_or(0.0) as u32
2722 } else {
2723 0
2724 };
2725 let mode_val = if fields.len() > 3 {
2726 parse_f64(&fields[3], line_num, "MODE").unwrap_or(0.0) as u32
2727 } else {
2728 0
2729 };
2730 let p_des = if fields.len() > 4 {
2731 parse_f64(&fields[4], line_num, "PDES").unwrap_or(0.0)
2732 } else {
2733 0.0
2734 };
2735 let q_des = if fields.len() > 5 {
2736 parse_f64(&fields[5], line_num, "QDES").unwrap_or(0.0)
2737 } else {
2738 0.0
2739 };
2740 let v_set = if fields.len() > 6 {
2741 parse_f64(&fields[6], line_num, "VSET").unwrap_or(1.0)
2742 } else {
2743 1.0
2744 };
2745 let q_max = if fields.len() > 7 {
2746 parse_f64(&fields[7], line_num, "SHMX").unwrap_or(9999.0)
2747 } else {
2748 9999.0
2749 };
2750 let linx = if fields.len() > 13 {
2755 parse_f64(&fields[13], line_num, "LINX").unwrap_or(0.0)
2756 } else {
2757 0.0
2758 };
2759
2760 let mode = FactsMode::from_u32(mode_val);
2761 let in_service = mode.in_service();
2762
2763 let facts_type = match mode {
2768 FactsMode::ShuntOnly => FactsType::Svc,
2769 FactsMode::SeriesOnly | FactsMode::ImpedanceModulation => FactsType::Tcsc,
2770 FactsMode::ShuntSeries | FactsMode::SeriesPowerControl => FactsType::Upfc,
2771 FactsMode::OutOfService => FactsType::Svc, };
2773
2774 devices.push(FactsDevice {
2775 name,
2776 bus_from: bus_i,
2777 bus_to: bus_j,
2778 mode,
2779 p_setpoint_mw: p_des,
2780 q_setpoint_mvar: q_des,
2781 voltage_setpoint_pu: v_set,
2782 q_max,
2783 series_reactance_pu: linx,
2784 in_service,
2785 facts_type,
2786 ..FactsDevice::default()
2787 });
2788
2789 pos += 1;
2790 }
2791
2792 (devices, pos)
2793}
2794
2795fn tokenize_record(line: &str) -> Vec<String> {
2800 let line = line.trim();
2801 let line = strip_comment(line);
2803 let line = line.trim();
2804 if line.is_empty() {
2805 return Vec::new();
2806 }
2807
2808 if line.contains(',') {
2809 let mut tokens = Vec::new();
2811 let mut current = String::new();
2812 let mut in_quotes = false;
2813
2814 for ch in line.chars() {
2815 match ch {
2816 '\'' => {
2817 in_quotes = !in_quotes;
2818 current.push(ch);
2819 }
2820 ',' if !in_quotes => {
2821 tokens.push(current.trim().to_string());
2822 current.clear();
2823 }
2824 _ => current.push(ch),
2825 }
2826 }
2827 let last = current.trim().to_string();
2829 if !last.is_empty() {
2830 tokens.push(last);
2831 }
2832 tokens
2833 } else {
2834 let mut tokens = Vec::new();
2836 let mut chars = line.chars().peekable();
2837
2838 while let Some(&ch) = chars.peek() {
2839 if ch == '\'' {
2840 chars.next();
2842 let mut s = String::new();
2843 while let Some(&c) = chars.peek() {
2844 if c == '\'' {
2845 chars.next();
2846 break;
2847 }
2848 s.push(c);
2849 chars.next();
2850 }
2851 tokens.push(format!("'{s}'"));
2852 } else if ch == ' ' || ch == '\t' {
2853 chars.next();
2854 } else {
2855 let mut tok = String::new();
2856 while let Some(&c) = chars.peek() {
2857 if c == ' ' || c == '\t' {
2858 break;
2859 }
2860 tok.push(c);
2861 chars.next();
2862 }
2863 tokens.push(tok);
2864 }
2865 }
2866 tokens
2867 }
2868}
2869
2870fn strip_comment(line: &str) -> &str {
2873 let mut in_quotes = false;
2874 for (i, ch) in line.char_indices() {
2875 match ch {
2876 '\'' => in_quotes = !in_quotes,
2877 '/' if !in_quotes => return &line[..i],
2878 _ => {}
2879 }
2880 }
2881 line
2882}
2883
2884fn is_section_end(line: &str) -> bool {
2891 let line = line.trim();
2892 if line == "0" {
2893 return true;
2894 }
2895 if line.starts_with("0 /") || line.starts_with("0\t/") {
2896 return true;
2897 }
2898 false
2899}
2900
2901fn parse_multi_owner_fields(
2903 fields: &[String],
2904 start_idx: usize,
2905) -> Vec<surge_network::network::OwnershipEntry> {
2906 let mut owners = Vec::new();
2907 for i in 0..4 {
2908 let oi_idx = start_idx + i * 2;
2909 let fi_idx = oi_idx + 1;
2910 if fields.len() <= oi_idx {
2911 break;
2912 }
2913 let oi = fields[oi_idx]
2914 .trim()
2915 .trim_matches('\'')
2916 .parse::<f64>()
2917 .unwrap_or(0.0) as u32;
2918 if oi == 0 {
2919 break;
2920 }
2921 let fi = if fields.len() > fi_idx {
2922 fields[fi_idx]
2923 .trim()
2924 .trim_matches('\'')
2925 .parse::<f64>()
2926 .unwrap_or(1.0)
2927 } else {
2928 1.0
2929 };
2930 owners.push(surge_network::network::OwnershipEntry {
2931 owner: oi,
2932 fraction: fi,
2933 });
2934 }
2935 owners
2936}
2937
2938fn parse_f64(token: &str, line: usize, field: &str) -> Result<f64, PsseError> {
2940 let token = token.trim();
2941 if token.is_empty() {
2942 return Ok(0.0); }
2944 let token = if token.starts_with('\'') && token.ends_with('\'') {
2946 &token[1..token.len() - 1]
2947 } else {
2948 token
2949 };
2950 let token = token.trim();
2951 if token.is_empty() {
2952 return Ok(0.0);
2953 }
2954 let val = token.parse::<f64>().map_err(|_| PsseError::Parse {
2955 line,
2956 message: format!("invalid {field} value: '{token}'"),
2957 })?;
2958 if !val.is_finite() {
2960 return Err(PsseError::NonFiniteValue {
2961 line,
2962 message: format!("field '{field}' parsed to non-finite value '{token}' (NaN or Inf)"),
2963 });
2964 }
2965 Ok(val)
2966}
2967
2968fn parse_required_f64(token: &str, line: usize, field: &str) -> Result<f64, PsseError> {
2969 let token = token.trim();
2970 if token.is_empty() {
2971 return Err(PsseError::Parse {
2972 line,
2973 message: format!("missing required {field} value"),
2974 });
2975 }
2976 parse_f64(token, line, field)
2977}
2978
2979fn parse_status(token: &str, line: usize, field: &str) -> Result<i32, PsseError> {
2987 let tok = token.trim().trim_matches('\'').trim_matches('"').trim();
2988 if tok.is_empty() {
2989 return Err(PsseError::Parse {
2990 line,
2991 message: format!("missing required {field} value"),
2992 });
2993 }
2994 if let Ok(v) = tok.parse::<f64>() {
2996 return Ok(v as i32);
2997 }
2998 match tok.to_uppercase().as_str() {
3000 "A" | "I" => Ok(1), "BL" | "O" => Ok(0), _ => Err(PsseError::Parse {
3003 line,
3004 message: format!("unknown {field} status code: '{tok}'"),
3005 }),
3006 }
3007}
3008
3009fn parse_zone_section(lines: &[&str], start: usize) -> (Vec<Region>, usize) {
3019 let mut zones = Vec::new();
3020 let mut pos = start;
3021
3022 while pos < lines.len() {
3023 let line = lines[pos].trim();
3024
3025 if is_section_end(line) {
3026 return (zones, pos + 1);
3027 }
3028 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
3029 return (zones, pos + 1);
3030 }
3031 if line.is_empty() || line.starts_with("@!") {
3032 pos += 1;
3033 continue;
3034 }
3035
3036 let fields = tokenize_record(line);
3037 if fields.is_empty() {
3038 pos += 1;
3039 continue;
3040 }
3041
3042 let line_num = pos + 1;
3043
3044 let number = match parse_f64(&fields[0], line_num, "ZONUM") {
3045 Ok(v) => v as u32,
3046 Err(_) => {
3047 pos += 1;
3048 continue;
3049 }
3050 };
3051 let name = if fields.len() > 1 {
3052 unquote(&fields[1])
3053 } else {
3054 String::new()
3055 };
3056
3057 zones.push(Region { number, name });
3058 pos += 1;
3059 }
3060
3061 (zones, pos)
3062}
3063
3064fn parse_owner_section(lines: &[&str], start: usize) -> (Vec<Owner>, usize) {
3074 let mut owners = Vec::new();
3075 let mut pos = start;
3076
3077 while pos < lines.len() {
3078 let line = lines[pos].trim();
3079
3080 if is_section_end(line) {
3081 return (owners, pos + 1);
3082 }
3083 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
3084 return (owners, pos + 1);
3085 }
3086 if line.is_empty() || line.starts_with("@!") {
3087 pos += 1;
3088 continue;
3089 }
3090
3091 let fields = tokenize_record(line);
3092 if fields.is_empty() {
3093 pos += 1;
3094 continue;
3095 }
3096
3097 let line_num = pos + 1;
3098
3099 let number = match parse_f64(&fields[0], line_num, "OWNUM") {
3100 Ok(v) => v as u32,
3101 Err(_) => {
3102 pos += 1;
3103 continue;
3104 }
3105 };
3106 let name = if fields.len() > 1 {
3107 unquote(&fields[1])
3108 } else {
3109 String::new()
3110 };
3111
3112 owners.push(Owner { number, name });
3113 pos += 1;
3114 }
3115
3116 (owners, pos)
3117}
3118
3119fn parse_inter_area_transfer_section(
3129 lines: &[&str],
3130 start: usize,
3131) -> (Vec<ScheduledAreaTransfer>, usize) {
3132 let mut transfers = Vec::new();
3133 let mut pos = start;
3134
3135 while pos < lines.len() {
3136 let line = lines[pos].trim();
3137
3138 if is_section_end(line) {
3139 return (transfers, pos + 1);
3140 }
3141 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
3142 return (transfers, pos + 1);
3143 }
3144 if line.is_empty() || line.starts_with("@!") {
3145 pos += 1;
3146 continue;
3147 }
3148
3149 let fields = tokenize_record(line);
3150 if fields.len() < 2 {
3151 pos += 1;
3152 continue;
3153 }
3154
3155 let line_num = pos + 1;
3156
3157 let from_area = match parse_f64(&fields[0], line_num, "ARFROM") {
3158 Ok(v) => v as u32,
3159 Err(_) => {
3160 pos += 1;
3161 continue;
3162 }
3163 };
3164 let to_area = match parse_f64(&fields[1], line_num, "ARTO") {
3165 Ok(v) => v as u32,
3166 Err(_) => {
3167 pos += 1;
3168 continue;
3169 }
3170 };
3171 let id = if fields.len() > 2 {
3172 parse_f64(&fields[2], line_num, "TRID").unwrap_or(1.0) as u32
3173 } else {
3174 1
3175 };
3176 let p_transfer_mw = if fields.len() > 3 {
3177 parse_f64(&fields[3], line_num, "PTRAN").unwrap_or(0.0)
3178 } else {
3179 0.0
3180 };
3181
3182 transfers.push(ScheduledAreaTransfer {
3183 from_area,
3184 to_area,
3185 id,
3186 p_transfer_mw,
3187 });
3188 pos += 1;
3189 }
3190
3191 (transfers, pos)
3192}
3193
3194fn parse_impedance_correction_section(
3207 lines: &[&str],
3208 start: usize,
3209) -> (Vec<ImpedanceCorrectionTable>, usize) {
3210 let mut tables = Vec::new();
3211 let mut pos = start;
3212
3213 while pos < lines.len() {
3214 let line = lines[pos].trim();
3215
3216 if is_section_end(line) {
3217 return (tables, pos + 1);
3218 }
3219 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
3220 return (tables, pos + 1);
3221 }
3222 if line.is_empty() || line.starts_with("@!") {
3223 pos += 1;
3224 continue;
3225 }
3226
3227 let fields = tokenize_record(line);
3228 if fields.is_empty() {
3229 pos += 1;
3230 continue;
3231 }
3232
3233 let line_num = pos + 1;
3234
3235 let number = match parse_f64(&fields[0], line_num, "I") {
3236 Ok(v) => v as u32,
3237 Err(_) => {
3238 pos += 1;
3239 continue;
3240 }
3241 };
3242
3243 let mut entries = Vec::new();
3244 let mut i = 1;
3246 while i + 1 < fields.len() {
3247 let t = parse_f64(&fields[i], line_num, "T").unwrap_or(0.0);
3248 let f = parse_f64(&fields[i + 1], line_num, "F").unwrap_or(0.0);
3249 if t != 0.0 || f != 0.0 {
3250 entries.push((t, f));
3251 }
3252 i += 2;
3253 }
3254
3255 tables.push(ImpedanceCorrectionTable { number, entries });
3256 pos += 1;
3257 }
3258
3259 (tables, pos)
3260}
3261
3262fn parse_multi_section_line_section(
3274 lines: &[&str],
3275 start: usize,
3276) -> (Vec<MultiSectionLineGroup>, usize) {
3277 let mut groups = Vec::new();
3278 let mut pos = start;
3279
3280 while pos < lines.len() {
3281 let line = lines[pos].trim();
3282
3283 if is_section_end(line) {
3284 return (groups, pos + 1);
3285 }
3286 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
3287 return (groups, pos + 1);
3288 }
3289 if line.is_empty() || line.starts_with("@!") {
3290 pos += 1;
3291 continue;
3292 }
3293
3294 let fields = tokenize_record(line);
3295 if fields.len() < 3 {
3296 pos += 1;
3297 continue;
3298 }
3299
3300 let line_num = pos + 1;
3301
3302 let from_bus = match parse_f64(&fields[0], line_num, "I") {
3303 Ok(v) => v as u32,
3304 Err(_) => {
3305 pos += 1;
3306 continue;
3307 }
3308 };
3309 let to_bus = match parse_f64(&fields[1], line_num, "J") {
3310 Ok(v) => v as u32,
3311 Err(_) => {
3312 pos += 1;
3313 continue;
3314 }
3315 };
3316 let id = unquote(&fields[2]);
3317 let metered_end = if fields.len() > 3 {
3318 parse_f64(&fields[3], line_num, "MET").unwrap_or(1.0) as u32
3319 } else {
3320 1
3321 };
3322
3323 let mut dummy_buses = Vec::new();
3325 for fi in 4..fields.len() {
3326 let bus = parse_f64(&fields[fi], line_num, "DUM").unwrap_or(0.0) as u32;
3327 if bus != 0 {
3328 dummy_buses.push(bus);
3329 }
3330 }
3331
3332 groups.push(MultiSectionLineGroup {
3333 from_bus,
3334 to_bus,
3335 id,
3336 metered_end,
3337 dummy_buses,
3338 });
3339 pos += 1;
3340 }
3341
3342 (groups, pos)
3343}
3344
3345fn parse_multi_terminal_dc_section(lines: &[&str], start: usize) -> (Vec<RawMtdcSystem>, usize) {
3361 let mut mt_lines = Vec::new();
3362 let mut pos = start;
3363
3364 while pos < lines.len() {
3365 let line = lines[pos].trim();
3366
3367 if is_section_end(line) {
3368 return (mt_lines, pos + 1);
3369 }
3370 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
3371 return (mt_lines, pos + 1);
3372 }
3373 if line.is_empty() || line.starts_with("@!") {
3374 pos += 1;
3375 continue;
3376 }
3377
3378 let fields = tokenize_record(line);
3380 if fields.is_empty() {
3381 pos += 1;
3382 continue;
3383 }
3384
3385 let line_num = pos + 1;
3386
3387 let name = unquote(&fields[0]);
3388 let nconv = if fields.len() > 1 {
3389 parse_f64(&fields[1], line_num, "NCONV").unwrap_or(0.0) as u32
3390 } else {
3391 0
3392 };
3393 let ndcbs = if fields.len() > 2 {
3394 parse_f64(&fields[2], line_num, "NDCBS").unwrap_or(0.0) as u32
3395 } else {
3396 0
3397 };
3398 let ndcln = if fields.len() > 3 {
3399 parse_f64(&fields[3], line_num, "NDCLN").unwrap_or(0.0) as u32
3400 } else {
3401 0
3402 };
3403 let mdc = if fields.len() > 4 {
3404 parse_f64(&fields[4], line_num, "MDC").unwrap_or(1.0) as u32
3405 } else {
3406 1
3407 };
3408 let vconv = if fields.len() > 5 {
3409 parse_f64(&fields[5], line_num, "VCONV").unwrap_or(0.0)
3410 } else {
3411 0.0
3412 };
3413 let vcmod = if fields.len() > 6 {
3414 parse_f64(&fields[6], line_num, "VCMOD").unwrap_or(0.0)
3415 } else {
3416 0.0
3417 };
3418 let vconvn = if fields.len() > 7 {
3419 parse_f64(&fields[7], line_num, "VCONVN").unwrap_or(0.0)
3420 } else {
3421 0.0
3422 };
3423
3424 pos += 1;
3425
3426 let mut converters = Vec::new();
3428 while converters.len() < nconv as usize && pos < lines.len() {
3429 let cline = lines[pos].trim();
3430 if cline.is_empty() || cline.starts_with("@!") {
3431 pos += 1;
3432 continue;
3433 }
3434 let cf = tokenize_record(cline);
3435 let cl = pos + 1;
3436
3437 let bus = if !cf.is_empty() {
3438 parse_f64(&cf[0], cl, "IB").unwrap_or(0.0) as u32
3439 } else {
3440 0
3441 };
3442 let n_bridges = if cf.len() > 1 {
3443 parse_f64(&cf[1], cl, "N").unwrap_or(1.0) as u32
3444 } else {
3445 1
3446 };
3447 let alpha_max = if cf.len() > 2 {
3448 parse_f64(&cf[2], cl, "ANGMX").unwrap_or(90.0)
3449 } else {
3450 90.0
3451 };
3452 let alpha_min = if cf.len() > 3 {
3453 parse_f64(&cf[3], cl, "ANGMN").unwrap_or(5.0)
3454 } else {
3455 5.0
3456 };
3457 let r_comm = if cf.len() > 4 {
3458 parse_f64(&cf[4], cl, "RC").unwrap_or(0.0)
3459 } else {
3460 0.0
3461 };
3462 let x_comm = if cf.len() > 5 {
3463 parse_f64(&cf[5], cl, "XC").unwrap_or(0.0)
3464 } else {
3465 0.0
3466 };
3467 let e_base = if cf.len() > 6 {
3468 parse_f64(&cf[6], cl, "EBAS").unwrap_or(0.0)
3469 } else {
3470 0.0
3471 };
3472 let tr = if cf.len() > 7 {
3473 parse_f64(&cf[7], cl, "TR").unwrap_or(1.0)
3474 } else {
3475 1.0
3476 };
3477 let tap = if cf.len() > 8 {
3478 parse_f64(&cf[8], cl, "TAP").unwrap_or(1.0)
3479 } else {
3480 1.0
3481 };
3482 let tap_max = if cf.len() > 9 {
3483 parse_f64(&cf[9], cl, "TPMX").unwrap_or(1.1)
3484 } else {
3485 1.1
3486 };
3487 let tap_min = if cf.len() > 10 {
3488 parse_f64(&cf[10], cl, "TPMN").unwrap_or(0.9)
3489 } else {
3490 0.9
3491 };
3492 let tap_step = if cf.len() > 11 {
3493 parse_f64(&cf[11], cl, "TSTP").unwrap_or(0.00625)
3494 } else {
3495 0.00625
3496 };
3497 let setvl = if cf.len() > 12 {
3498 parse_f64(&cf[12], cl, "SETVL").unwrap_or(0.0)
3499 } else {
3500 0.0
3501 };
3502 let dcpf = if cf.len() > 13 {
3503 parse_f64(&cf[13], cl, "DCPF").unwrap_or(0.0)
3504 } else {
3505 0.0
3506 };
3507 let marg = if cf.len() > 14 {
3508 parse_f64(&cf[14], cl, "MARG").unwrap_or(0.0)
3509 } else {
3510 0.0
3511 };
3512 let cnvcod = if cf.len() > 15 {
3513 parse_f64(&cf[15], cl, "CNVCOD").unwrap_or(1.0) as u32
3514 } else {
3515 1
3516 };
3517
3518 converters.push(RawMtdcConverter {
3519 bus,
3520 n_bridges,
3521 alpha_max,
3522 alpha_min,
3523 commutation_resistance_ohm: r_comm,
3524 commutation_reactance_ohm: x_comm,
3525 base_voltage_kv: e_base,
3526 turns_ratio: tr,
3527 tap,
3528 tap_max,
3529 tap_min,
3530 tap_step,
3531 scheduled_setpoint: setvl,
3532 dcpf,
3533 marg,
3534 cnvcod,
3535 });
3536 pos += 1;
3537 }
3538
3539 let mut dc_buses = Vec::new();
3541 while dc_buses.len() < ndcbs as usize && pos < lines.len() {
3542 let bline = lines[pos].trim();
3543 if bline.is_empty() || bline.starts_with("@!") {
3544 pos += 1;
3545 continue;
3546 }
3547 let bf = tokenize_record(bline);
3548 let bl = pos + 1;
3549
3550 let dc_bus = if !bf.is_empty() {
3551 parse_f64(&bf[0], bl, "IDC").unwrap_or(0.0) as u32
3552 } else {
3553 0
3554 };
3555 let ac_bus = if bf.len() > 1 {
3556 parse_f64(&bf[1], bl, "IB").unwrap_or(0.0) as u32
3557 } else {
3558 0
3559 };
3560 let area = if bf.len() > 2 {
3561 parse_f64(&bf[2], bl, "AREA").unwrap_or(1.0) as u32
3562 } else {
3563 1
3564 };
3565 let zone = if bf.len() > 3 {
3566 parse_f64(&bf[3], bl, "ZONE").unwrap_or(1.0) as u32
3567 } else {
3568 1
3569 };
3570 let dc_name = if bf.len() > 4 {
3571 unquote(&bf[4])
3572 } else {
3573 String::new()
3574 };
3575 let idc2 = if bf.len() > 5 {
3576 parse_f64(&bf[5], bl, "IDC2").unwrap_or(0.0) as u32
3577 } else {
3578 0
3579 };
3580 let rgrnd = if bf.len() > 6 {
3581 parse_f64(&bf[6], bl, "RGRND").unwrap_or(0.0)
3582 } else {
3583 0.0
3584 };
3585 let owner = if bf.len() > 7 {
3586 parse_f64(&bf[7], bl, "OWNER").unwrap_or(1.0) as u32
3587 } else {
3588 1
3589 };
3590
3591 dc_buses.push(RawMtdcBus {
3592 dc_bus,
3593 ac_bus,
3594 area,
3595 zone,
3596 name: dc_name,
3597 idc2,
3598 rgrnd,
3599 owner,
3600 });
3601 pos += 1;
3602 }
3603
3604 let mut dc_links = Vec::new();
3606 while dc_links.len() < ndcln as usize && pos < lines.len() {
3607 let lline = lines[pos].trim();
3608 if lline.is_empty() || lline.starts_with("@!") {
3609 pos += 1;
3610 continue;
3611 }
3612 let lf = tokenize_record(lline);
3613 let ll = pos + 1;
3614
3615 let from_dc_bus = if !lf.is_empty() {
3616 parse_f64(&lf[0], ll, "IDC").unwrap_or(0.0) as u32
3617 } else {
3618 0
3619 };
3620 let to_dc_bus = if lf.len() > 1 {
3621 parse_f64(&lf[1], ll, "JDC").unwrap_or(0.0) as u32
3622 } else {
3623 0
3624 };
3625 let circuit = if lf.len() > 2 {
3626 unquote(&lf[2])
3627 } else {
3628 "1".to_string()
3629 };
3630 let metered = if lf.len() > 3 {
3631 parse_f64(&lf[3], ll, "MET").unwrap_or(1.0) as u32
3632 } else {
3633 1
3634 };
3635 let resistance_ohm = if lf.len() > 4 {
3636 parse_f64(&lf[4], ll, "RDC").unwrap_or(0.0)
3637 } else {
3638 0.0
3639 };
3640 let ldc = if lf.len() > 5 {
3641 parse_f64(&lf[5], ll, "LDC").unwrap_or(0.0)
3642 } else {
3643 0.0
3644 };
3645
3646 dc_links.push(RawMtdcLink {
3647 from_dc_bus,
3648 to_dc_bus,
3649 circuit,
3650 metered,
3651 resistance_ohm,
3652 ldc,
3653 });
3654 pos += 1;
3655 }
3656
3657 mt_lines.push(RawMtdcSystem {
3658 name,
3659 n_converters: nconv,
3660 n_dc_buses: ndcbs,
3661 n_dc_links: ndcln,
3662 control_mode: mdc,
3663 dc_voltage_kv: vconv,
3664 voltage_mode_switch_kv: vcmod,
3665 dc_voltage_min_kv: vconvn,
3666 converters,
3667 dc_buses,
3668 dc_links,
3669 });
3670 }
3671
3672 (mt_lines, pos)
3673}
3674
3675fn normalize_dc_grids(network: &mut Network, systems: &[RawMtdcSystem]) {
3676 let mut next_grid = network.hvdc.next_dc_grid_id();
3677 let mut next_dc_bus_id = network.hvdc.next_dc_bus_id();
3678
3679 for system in systems {
3680 if system.control_mode == 0 {
3681 continue;
3682 }
3683
3684 let grid_id = next_grid;
3685 next_grid += 1;
3686
3687 let base_kv_dc = if system.dc_voltage_kv > 0.0 {
3688 system.dc_voltage_kv
3689 } else {
3690 system
3691 .converters
3692 .iter()
3693 .find(|converter| converter.base_voltage_kv > 0.0)
3694 .map(|converter| converter.base_voltage_kv)
3695 .unwrap_or(500.0)
3696 };
3697
3698 let mut local_to_global: HashMap<u32, u32> = HashMap::new();
3699 for dc_bus in &system.dc_buses {
3700 let global_id = next_dc_bus_id;
3701 next_dc_bus_id += 1;
3702 local_to_global.insert(dc_bus.dc_bus, global_id);
3703 network
3704 .hvdc
3705 .ensure_dc_grid(grid_id, Some(system.name.clone()))
3706 .buses
3707 .push(DcBus {
3708 bus_id: global_id,
3709 p_dc_mw: 0.0,
3710 v_dc_pu: 1.0,
3711 base_kv_dc,
3712 v_dc_max: 1.1,
3713 v_dc_min: 0.9,
3714 cost: 0.0,
3715 g_shunt_siemens: 0.0,
3716 r_ground_ohm: dc_bus.rgrnd,
3717 });
3718 }
3719
3720 for converter in &system.converters {
3721 let Some(&dc_bus) = system
3722 .dc_buses
3723 .iter()
3724 .find(|dc_bus| dc_bus.ac_bus == converter.bus)
3725 .and_then(|dc_bus| local_to_global.get(&dc_bus.dc_bus))
3726 else {
3727 continue;
3728 };
3729
3730 let scheduled_setpoint = if converter.cnvcod == 2 {
3731 -converter.scheduled_setpoint.abs()
3732 } else {
3733 converter.scheduled_setpoint.abs()
3734 };
3735
3736 network
3737 .hvdc
3738 .ensure_dc_grid(grid_id, Some(system.name.clone()))
3739 .converters
3740 .push(DcConverter::Lcc(LccDcConverter {
3741 id: String::new(),
3742 dc_bus,
3743 ac_bus: converter.bus,
3744 n_bridges: converter.n_bridges,
3745 alpha_max_deg: converter.alpha_max,
3746 alpha_min_deg: converter.alpha_min,
3747 gamma_min_deg: 15.0,
3748 commutation_resistance_ohm: converter.commutation_resistance_ohm,
3749 commutation_reactance_ohm: converter.commutation_reactance_ohm,
3750 base_voltage_kv: converter.base_voltage_kv.max(base_kv_dc),
3751 turns_ratio: converter.turns_ratio,
3752 tap_ratio: converter.tap,
3753 tap_max: converter.tap_max,
3754 tap_min: converter.tap_min,
3755 tap_step: converter.tap_step,
3756 scheduled_setpoint,
3757 power_share_percent: converter.dcpf,
3758 current_margin_percent: converter.marg,
3759 role: if converter.cnvcod == 2 {
3760 LccDcConverterRole::Inverter
3761 } else {
3762 LccDcConverterRole::Rectifier
3763 },
3764 in_service: true,
3765 }));
3766 }
3767
3768 for link in &system.dc_links {
3769 let Some(&from_bus) = local_to_global.get(&link.from_dc_bus) else {
3770 continue;
3771 };
3772 let Some(&to_bus) = local_to_global.get(&link.to_dc_bus) else {
3773 continue;
3774 };
3775 let grid = network
3776 .hvdc
3777 .ensure_dc_grid(grid_id, Some(system.name.clone()));
3778 grid.branches.push(DcBranch {
3779 id: format!("dc_grid_{}_branch_{}", grid.id, grid.branches.len() + 1),
3780 from_bus,
3781 to_bus,
3782 r_ohm: link.resistance_ohm,
3783 l_mh: link.ldc,
3784 c_uf: 0.0,
3785 rating_a_mva: 0.0,
3786 rating_b_mva: 0.0,
3787 rating_c_mva: 0.0,
3788 status: true,
3789 });
3790 }
3791 }
3792}
3793
3794#[allow(dead_code)]
3799fn skip_section(lines: &[&str], start: usize) -> usize {
3800 let mut pos = start;
3801 while pos < lines.len() {
3802 let line = lines[pos].trim();
3803 if is_section_end(line) {
3804 return pos + 1;
3805 }
3806 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
3807 return pos + 1;
3808 }
3809 pos += 1;
3810 }
3811 pos
3812}
3813
3814struct RawSystemSwitchDevice {
3820 bus_i: u32,
3822 bus_j: u32,
3824 ckt: String,
3826 name: String,
3828 device_type: u32,
3830 status: u32,
3832 normal_status: u32,
3834 #[allow(dead_code)]
3836 x_pu: f64,
3837 rate1: f64,
3839}
3840
3841fn psse_switch_type(device_type: u32) -> SwitchType {
3842 match device_type {
3843 1 => SwitchType::Switch, 2 => SwitchType::Breaker,
3845 3 => SwitchType::Disconnector,
3846 _ => SwitchType::Switch,
3847 }
3848}
3849
3850fn parse_voltage_droop_control_section(
3855 lines: &[&str],
3856 start: usize,
3857) -> (
3858 Vec<surge_network::network::voltage_droop_control::VoltageDroopControl>,
3859 usize,
3860) {
3861 let mut controls = Vec::new();
3862 let mut pos = start;
3863
3864 while pos < lines.len() {
3865 let line = lines[pos].trim();
3866 if line.is_empty() || line.starts_with("@!") {
3867 pos += 1;
3868 continue;
3869 }
3870 if is_section_end(line) {
3871 pos += 1;
3872 break;
3873 }
3874
3875 let fields = tokenize_record(line);
3876 if fields.len() < 7 {
3877 pos += 1;
3878 continue;
3879 }
3880
3881 controls.push(
3882 surge_network::network::voltage_droop_control::VoltageDroopControl {
3883 bus: fields[0].parse().unwrap_or(0),
3884 device_id: fields[1].clone(),
3885 device_type: fields[2].parse().unwrap_or(1),
3886 regulated_bus: fields[3].parse().unwrap_or(0),
3887 vdrp: fields[4].parse().unwrap_or(0.0),
3888 vmax: fields[5].parse().unwrap_or(1.1),
3889 vmin: fields[6].parse().unwrap_or(0.9),
3890 },
3891 );
3892 pos += 1;
3893 }
3894
3895 if !controls.is_empty() {
3896 tracing::debug!(count = controls.len(), "parsed voltage droop control data");
3897 }
3898 (controls, pos)
3899}
3900
3901fn parse_switching_device_rating_set_section(
3906 lines: &[&str],
3907 start: usize,
3908) -> (
3909 Vec<surge_network::network::switching_device_rating::SwitchingDeviceRatingSet>,
3910 usize,
3911) {
3912 let mut sets = Vec::new();
3913 let mut pos = start;
3914
3915 while pos < lines.len() {
3916 let line = lines[pos].trim();
3917 if line.is_empty() || line.starts_with("@!") {
3918 pos += 1;
3919 continue;
3920 }
3921 if is_section_end(line) {
3922 pos += 1;
3923 break;
3924 }
3925
3926 let fields = tokenize_record(line);
3927 if fields.len() < 7 {
3928 pos += 1;
3929 continue;
3930 }
3931
3932 let mut additional = Vec::new();
3933 for i in 7..fields.len() {
3934 if let Ok(r) = fields[i].parse::<f64>() {
3935 additional.push(r);
3936 }
3937 }
3938
3939 sets.push(
3940 surge_network::network::switching_device_rating::SwitchingDeviceRatingSet {
3941 from_bus: fields[0].parse().unwrap_or(0),
3942 to_bus: fields[1].parse().unwrap_or(0),
3943 circuit: fields[2].clone(),
3944 rating_set: fields[3].parse().unwrap_or(1),
3945 rate1: fields[4].parse().unwrap_or(0.0),
3946 rate2: fields[5].parse().unwrap_or(0.0),
3947 rate3: fields[6].parse().unwrap_or(0.0),
3948 additional_rates: additional,
3949 },
3950 );
3951 pos += 1;
3952 }
3953
3954 if !sets.is_empty() {
3955 tracing::debug!(
3956 count = sets.len(),
3957 "parsed switching device rating set data"
3958 );
3959 }
3960 (sets, pos)
3961}
3962
3963fn parse_system_switching_device_section(
3971 lines: &[&str],
3972 start: usize,
3973) -> (Vec<RawSystemSwitchDevice>, usize) {
3974 let mut devices = Vec::new();
3975 let mut pos = start;
3976
3977 while pos < lines.len() {
3978 let line = lines[pos].trim();
3979 if line.is_empty() || line.starts_with("@!") {
3980 pos += 1;
3981 continue;
3982 }
3983 if is_section_end(line) {
3984 pos += 1;
3985 break;
3986 }
3987
3988 let fields = tokenize_record(line);
3989 if fields.len() < 6 {
3990 pos += 1;
3991 continue;
3992 }
3993
3994 let bus_i = fields[0].parse::<u32>().unwrap_or(0);
3995 let bus_j = fields[1].parse::<u32>().unwrap_or(0);
3996 let ckt = fields.get(2).cloned().unwrap_or_else(|| "1".into());
3997 let name = fields.get(3).cloned().unwrap_or_default();
3998 let device_type = fields.get(4).and_then(|s| s.parse().ok()).unwrap_or(1);
3999 let status = fields.get(5).and_then(|s| s.parse().ok()).unwrap_or(1);
4000 let normal_status = fields.get(6).and_then(|s| s.parse().ok()).unwrap_or(1);
4001 let x_pu = fields.get(7).and_then(|s| s.parse().ok()).unwrap_or(0.0001);
4002 let rate1 = fields.get(8).and_then(|s| s.parse().ok()).unwrap_or(0.0);
4003
4004 devices.push(RawSystemSwitchDevice {
4005 bus_i,
4006 bus_j,
4007 ckt,
4008 name,
4009 device_type,
4010 status,
4011 normal_status,
4012 x_pu,
4013 rate1,
4014 });
4015 pos += 1;
4016 }
4017
4018 if !devices.is_empty() {
4019 tracing::debug!(count = devices.len(), "parsed system switching device data");
4020 }
4021 (devices, pos)
4022}
4023
4024fn parse_induction_machine_section(
4042 lines: &[&str],
4043 start: usize,
4044) -> Vec<surge_network::network::induction_machine::InductionMachine> {
4045 use surge_network::network::induction_machine::InductionMachine;
4046 let mut machines = Vec::new();
4047 let mut pos = start;
4049
4050 while pos < lines.len() {
4051 let l = lines[pos].trim();
4052 if l.is_empty() || l.starts_with("@!") {
4053 pos += 1;
4054 continue;
4055 }
4056 if is_section_end(l) {
4057 break;
4058 }
4059
4060 let f1 = tokenize_record(l);
4061 pos += 1;
4062 if f1.len() < 4 {
4063 continue;
4064 }
4065
4066 let f2 = if pos < lines.len() {
4068 let l2 = lines[pos].trim();
4069 if !l2.is_empty() && !is_section_end(l2) && !l2.starts_with("@!") {
4070 pos += 1;
4071 tokenize_record(l2)
4072 } else {
4073 Vec::new()
4074 }
4075 } else {
4076 Vec::new()
4077 };
4078
4079 let bus: u32 = f1[0].parse().unwrap_or(0);
4080 let id = f1
4081 .get(1)
4082 .map(|s| s.trim().trim_matches('\'').to_string())
4083 .unwrap_or_else(|| "1".to_string());
4084 let stat: i32 = f1.get(2).and_then(|t| t.parse().ok()).unwrap_or(1);
4085 let area: u32 = f1.get(5).and_then(|t| t.parse().ok()).unwrap_or(1);
4087 let zone: u32 = f1.get(6).and_then(|t| t.parse().ok()).unwrap_or(1);
4088 let owner: u32 = f1.get(7).and_then(|t| t.parse().ok()).unwrap_or(1);
4089 let pf = |s: &str| s.trim().parse::<f64>().unwrap_or(0.0);
4091 let mbase: f64 = f1.get(10).map(|t| pf(t)).unwrap_or(0.0);
4092 let rate_kv: f64 = f1.get(11).map(|t| pf(t)).unwrap_or(0.0);
4093 let pset: f64 = f1.get(13).map(|t| pf(t)).unwrap_or(0.0);
4095 let h: f64 = f1.get(14).map(|t| pf(t)).unwrap_or(0.0);
4096 let a: f64 = f1.get(15).map(|t| pf(t)).unwrap_or(0.0);
4097 let b: f64 = f1.get(16).map(|t| pf(t)).unwrap_or(0.0);
4098 let d: f64 = f1.get(17).map(|t| pf(t)).unwrap_or(0.0);
4099 let e: f64 = f1.get(18).map(|t| pf(t)).unwrap_or(0.0);
4100 let f_coeff: f64 = f1.get(19).map(|t| pf(t)).unwrap_or(0.0);
4101
4102 let ra: f64 = f2.first().map(|t| pf(t)).unwrap_or(0.0);
4103 let xa: f64 = f2.get(1).map(|t| pf(t)).unwrap_or(0.0);
4104 let xm: f64 = f2.get(2).map(|t| pf(t)).unwrap_or(0.0);
4105 let r1: f64 = f2.get(3).map(|t| pf(t)).unwrap_or(0.0);
4106 let x1: f64 = f2.get(4).map(|t| pf(t)).unwrap_or(0.0);
4107 let r2: f64 = f2.get(5).map(|t| pf(t)).unwrap_or(0.0);
4108 let x2: f64 = f2.get(6).map(|t| pf(t)).unwrap_or(0.0);
4109 let x3: f64 = f2.get(7).map(|t| pf(t)).unwrap_or(0.0);
4110
4111 machines.push(InductionMachine {
4112 bus,
4113 id,
4114 in_service: stat != 0,
4115 mbase,
4116 rate_kv,
4117 pset,
4118 h,
4119 a,
4120 b,
4121 d,
4122 e,
4123 f_coeff,
4124 ra,
4125 xa,
4126 xm,
4127 r1,
4128 x1,
4129 r2,
4130 x2,
4131 x3,
4132 area,
4133 zone,
4134 owner,
4135 load_id: None,
4136 });
4137 }
4138
4139 machines
4140}
4141
4142fn parse_substation_data_section(
4153 lines: &[&str],
4154 start: usize,
4155 bus_basekv: &HashMap<u32, f64>,
4156 sys_switch_devices: &[RawSystemSwitchDevice],
4157) -> NodeBreakerTopology {
4158 let mut substations = Vec::new();
4159 let mut voltage_levels = Vec::new();
4160 let mut connectivity_nodes = Vec::new();
4161 let mut busbar_sections = Vec::new();
4162 let mut switches = Vec::new();
4163 let mut terminal_connections = Vec::new();
4164 let bays = Vec::new();
4165
4166 let mut pos = start;
4167
4168 let mut vl_set: std::collections::HashSet<(String, u64)> = std::collections::HashSet::new();
4170
4171 let mut all_connectivity_node_to_bus: HashMap<String, u32> = HashMap::new();
4173
4174 while pos < lines.len() {
4175 let line = lines[pos].trim();
4176 if line.is_empty() || line.starts_with("@!") {
4177 pos += 1;
4178 continue;
4179 }
4180 if is_section_end(line) {
4181 pos += 1;
4183 break;
4184 }
4185 if line.starts_with('Q') || line.eq_ignore_ascii_case("q") {
4186 break;
4187 }
4188
4189 let fields = tokenize_record(line);
4191 if fields.is_empty() {
4192 pos += 1;
4193 continue;
4194 }
4195
4196 let isub: u32 = fields[0].parse().unwrap_or(0);
4197 let sub_name = fields.get(1).cloned().unwrap_or_default();
4198 let sub_id = format!("SUB_{isub}");
4199
4200 substations.push(SubstationData {
4201 id: sub_id.clone(),
4202 name: sub_name,
4203 region: None,
4204 });
4205
4206 pos += 1;
4207
4208 while pos < lines.len() {
4210 let l = lines[pos].trim();
4211 if l.is_empty() || l.starts_with("@!") || l.starts_with('/') {
4212 pos += 1;
4213 continue;
4214 }
4215 break;
4216 }
4217
4218 let mut sub_nodes: Vec<(u32, String, u32)> = Vec::new(); while pos < lines.len() {
4223 let l = lines[pos].trim();
4224 if l.is_empty() || l.starts_with("@!") {
4225 pos += 1;
4226 continue;
4227 }
4228 if is_section_end(l) {
4229 pos += 1;
4230 break;
4231 }
4232 let nf = tokenize_record(l);
4233 if nf.len() >= 3 {
4234 let inode: u32 = nf[0].parse().unwrap_or(0);
4235 let node_name = nf.get(1).cloned().unwrap_or_default();
4236 let ibus: u32 = nf[2].parse().unwrap_or(0);
4237
4238 let cn_id = format!("SUB_{isub}_N{inode}");
4239 let base_kv = bus_basekv.get(&ibus).copied().unwrap_or(1.0);
4240 let vl_id = format!("VL_{sub_id}_{}", (base_kv * 10.0) as u64);
4241
4242 let vl_key = (sub_id.clone(), (base_kv * 10.0) as u64);
4244 if vl_set.insert(vl_key) {
4245 voltage_levels.push(VoltageLevel {
4246 id: vl_id.clone(),
4247 name: format!("{base_kv} kV"),
4248 substation_id: sub_id.clone(),
4249 base_kv,
4250 });
4251 }
4252
4253 connectivity_nodes.push(ConnectivityNode {
4254 id: cn_id.clone(),
4255 name: node_name.clone(),
4256 voltage_level_id: vl_id,
4257 });
4258
4259 all_connectivity_node_to_bus.insert(cn_id.clone(), ibus);
4261
4262 if node_name.contains("NB") || node_name.contains("BB") {
4264 busbar_sections.push(BusbarSection {
4265 id: format!("BB_{cn_id}"),
4266 name: node_name.clone(),
4267 connectivity_node_id: cn_id.clone(),
4268 ip_max: None,
4269 });
4270 }
4271
4272 sub_nodes.push((inode, cn_id, ibus));
4273 }
4274 pos += 1;
4275 }
4276
4277 let node_cn: HashMap<u32, &str> = sub_nodes
4279 .iter()
4280 .map(|(inode, cn_id, _)| (*inode, cn_id.as_str()))
4281 .collect();
4282
4283 while pos < lines.len() {
4286 let l = lines[pos].trim();
4287 if l.is_empty() || l.starts_with("@!") {
4288 pos += 1;
4289 continue;
4290 }
4291 if is_section_end(l) {
4292 pos += 1;
4293 break;
4294 }
4295 let sf = tokenize_record(l);
4296 if sf.len() >= 6 {
4297 let ni: u32 = sf[0].parse().unwrap_or(0);
4298 let nj: u32 = sf[1].parse().unwrap_or(0);
4299 let ckt = sf.get(2).cloned().unwrap_or_else(|| "1".into());
4300 let sw_name = sf.get(3).cloned().unwrap_or_default();
4301 let device_type: u32 = sf.get(4).and_then(|s| s.parse().ok()).unwrap_or(1);
4302 let status: u32 = sf.get(5).and_then(|s| s.parse().ok()).unwrap_or(1);
4303 let normal_status: u32 = sf.get(6).and_then(|s| s.parse().ok()).unwrap_or(1);
4304 let rate1: f64 = sf.get(8).and_then(|s| s.parse().ok()).unwrap_or(0.0);
4305
4306 let cn1 = node_cn.get(&ni).unwrap_or(&"").to_string();
4307 let cn2 = node_cn.get(&nj).unwrap_or(&"").to_string();
4308 if !cn1.is_empty() && !cn2.is_empty() {
4309 let sw_id = format!("SW_{sub_id}_N{ni}_N{nj}_{ckt}");
4310 let rated_current = if rate1 > 0.0 { Some(rate1) } else { None };
4311
4312 switches.push(SwitchDevice {
4313 id: sw_id,
4314 name: sw_name,
4315 switch_type: psse_switch_type(device_type),
4316 cn1_id: cn1,
4317 cn2_id: cn2,
4318 open: status == 0,
4319 normal_open: normal_status == 0,
4320 retained: false,
4321 rated_current,
4322 });
4323 }
4324 }
4325 pos += 1;
4326 }
4327
4328 while pos < lines.len() {
4337 let l = lines[pos].trim();
4338 if l.is_empty() || l.starts_with("@!") {
4339 pos += 1;
4340 continue;
4341 }
4342 if is_section_end(l) {
4343 pos += 1;
4344 break;
4345 }
4346 let tf = tokenize_record(l);
4347 if tf.len() >= 3 {
4348 let _term_sub: u32 = tf[0].parse().unwrap_or(0);
4349 let term_node: u32 = tf[1].parse().unwrap_or(0);
4350 let term_type = tf.get(2).cloned().unwrap_or_default();
4351
4352 let cn_id = node_cn.get(&term_node).unwrap_or(&"").to_string();
4353 if cn_id.is_empty() {
4354 pos += 1;
4355 continue;
4356 }
4357
4358 let (equip_id, equip_class, seq) = match term_type.as_str() {
4359 "M" => {
4360 let ckt = tf.get(3).cloned().unwrap_or_else(|| "1".into());
4361 let ibus = sub_nodes
4363 .iter()
4364 .find(|(n, _, _)| *n == term_node)
4365 .map(|(_, _, b)| *b)
4366 .unwrap_or(0);
4367 (
4368 format!("GEN_{ibus}_{ckt}"),
4369 "SynchronousMachine".into(),
4370 1u32,
4371 )
4372 }
4373 "L" => {
4374 let ckt = tf.get(3).cloned().unwrap_or_else(|| "1".into());
4375 let ibus = sub_nodes
4376 .iter()
4377 .find(|(n, _, _)| *n == term_node)
4378 .map(|(_, _, b)| *b)
4379 .unwrap_or(0);
4380 (format!("LOAD_{ibus}_{ckt}"), "EnergyConsumer".into(), 1)
4381 }
4382 "B" | "X" => {
4383 let jbus: u32 = tf.get(3).and_then(|s| s.parse().ok()).unwrap_or(0);
4384 let ckt = tf.get(4).cloned().unwrap_or_else(|| "1".into());
4385 let ibus = sub_nodes
4386 .iter()
4387 .find(|(n, _, _)| *n == term_node)
4388 .map(|(_, _, b)| *b)
4389 .unwrap_or(0);
4390 let class = if term_type == "X" {
4391 "PowerTransformer"
4392 } else {
4393 "ACLineSegment"
4394 };
4395 (format!("BR_{ibus}_{jbus}_{ckt}"), class.into(), 1)
4396 }
4397 _ => {
4398 let ckt = tf.get(3).cloned().unwrap_or_else(|| "1".into());
4400 let ibus = sub_nodes
4401 .iter()
4402 .find(|(n, _, _)| *n == term_node)
4403 .map(|(_, _, b)| *b)
4404 .unwrap_or(0);
4405 (
4406 format!("EQUIP_{ibus}_{term_type}_{ckt}"),
4407 term_type.clone(),
4408 1,
4409 )
4410 }
4411 };
4412
4413 let term_id = format!("T_{cn_id}_{equip_id}");
4414 terminal_connections.push(TerminalConnection {
4415 terminal_id: term_id,
4416 equipment_id: equip_id,
4417 equipment_class: equip_class,
4418 sequence_number: seq,
4419 connectivity_node_id: cn_id,
4420 });
4421 }
4422 pos += 1;
4423 }
4424 }
4425 let _ = pos; for ssd in sys_switch_devices {
4431 let cn1_id = format!("SYSBUS_{}", ssd.bus_i);
4432 let cn2_id = format!("SYSBUS_{}", ssd.bus_j);
4433 let sw_id = format!("SYS_SW_{}_{}_{}", ssd.bus_i, ssd.bus_j, ssd.ckt);
4434
4435 if !connectivity_nodes.iter().any(|cn| cn.id == cn1_id) {
4437 let base_kv = bus_basekv.get(&ssd.bus_i).copied().unwrap_or(1.0);
4438 connectivity_nodes.push(ConnectivityNode {
4439 id: cn1_id.clone(),
4440 name: format!("Bus_{}", ssd.bus_i),
4441 voltage_level_id: format!("VL_SYS_{}", (base_kv * 10.0) as u64),
4442 });
4443 }
4444 if !connectivity_nodes.iter().any(|cn| cn.id == cn2_id) {
4445 let base_kv = bus_basekv.get(&ssd.bus_j).copied().unwrap_or(1.0);
4446 connectivity_nodes.push(ConnectivityNode {
4447 id: cn2_id.clone(),
4448 name: format!("Bus_{}", ssd.bus_j),
4449 voltage_level_id: format!("VL_SYS_{}", (base_kv * 10.0) as u64),
4450 });
4451 }
4452
4453 let rated_current = if ssd.rate1 > 0.0 {
4454 Some(ssd.rate1)
4455 } else {
4456 None
4457 };
4458 switches.push(SwitchDevice {
4459 id: sw_id,
4460 name: ssd.name.clone(),
4461 switch_type: psse_switch_type(ssd.device_type),
4462 cn1_id,
4463 cn2_id,
4464 open: ssd.status == 0,
4465 normal_open: ssd.normal_status == 0,
4466 retained: false,
4467 rated_current,
4468 });
4469 }
4470
4471 let mut bus_to_connectivity_nodes: HashMap<u32, Vec<String>> = HashMap::new();
4473 for (cn_id, &bus) in &all_connectivity_node_to_bus {
4474 bus_to_connectivity_nodes
4475 .entry(bus)
4476 .or_default()
4477 .push(cn_id.clone());
4478 }
4479
4480 let consumed_switch_ids: Vec<String> = switches
4482 .iter()
4483 .filter(|sw| !sw.open && !sw.retained)
4484 .filter(|sw| {
4485 all_connectivity_node_to_bus.get(&sw.cn1_id)
4486 == all_connectivity_node_to_bus.get(&sw.cn2_id)
4487 && all_connectivity_node_to_bus.contains_key(&sw.cn1_id)
4488 })
4489 .map(|sw| sw.id.clone())
4490 .collect();
4491
4492 let connected_cns: std::collections::HashSet<&str> = terminal_connections
4494 .iter()
4495 .map(|tc| tc.connectivity_node_id.as_str())
4496 .collect();
4497 let isolated_connectivity_node_ids: Vec<String> = connectivity_nodes
4498 .iter()
4499 .filter(|cn| !connected_cns.contains(cn.id.as_str()))
4500 .map(|cn| cn.id.clone())
4501 .collect();
4502
4503 let reduction = if all_connectivity_node_to_bus.is_empty() {
4504 None
4505 } else {
4506 Some(TopologyMapping {
4507 connectivity_node_to_bus: all_connectivity_node_to_bus,
4508 bus_to_connectivity_nodes,
4509 consumed_switch_ids,
4510 isolated_connectivity_node_ids,
4511 })
4512 };
4513
4514 tracing::debug!(
4515 substations = substations.len(),
4516 nodes = connectivity_nodes.len(),
4517 switches = switches.len(),
4518 terminals = terminal_connections.len(),
4519 "parsed PSS/E substation data"
4520 );
4521
4522 match reduction {
4523 Some(reduction) => NodeBreakerTopology::new(
4524 substations,
4525 voltage_levels,
4526 bays,
4527 connectivity_nodes,
4528 busbar_sections,
4529 switches,
4530 terminal_connections,
4531 )
4532 .with_mapping(reduction),
4533 None => NodeBreakerTopology::new(
4534 substations,
4535 voltage_levels,
4536 bays,
4537 connectivity_nodes,
4538 busbar_sections,
4539 switches,
4540 terminal_connections,
4541 ),
4542 }
4543}
4544
4545fn build_sys_switch_model(
4548 sys_devices: &[RawSystemSwitchDevice],
4549 _network: &Network,
4550 bus_basekv: &HashMap<u32, f64>,
4551) -> NodeBreakerTopology {
4552 let mut connectivity_nodes = Vec::new();
4553 let mut switches = Vec::new();
4554 let mut voltage_levels = Vec::new();
4555 let mut vl_set: std::collections::HashSet<u64> = std::collections::HashSet::new();
4556 let mut cn_set: std::collections::HashSet<u32> = std::collections::HashSet::new();
4557
4558 for ssd in sys_devices {
4559 for &bus in &[ssd.bus_i, ssd.bus_j] {
4560 if cn_set.insert(bus) {
4561 let base_kv = bus_basekv.get(&bus).copied().unwrap_or(1.0);
4562 let vl_key = (base_kv * 10.0) as u64;
4563 if vl_set.insert(vl_key) {
4564 voltage_levels.push(VoltageLevel {
4565 id: format!("VL_SYS_{vl_key}"),
4566 name: format!("{base_kv} kV"),
4567 substation_id: "SYS".into(),
4568 base_kv,
4569 });
4570 }
4571 connectivity_nodes.push(ConnectivityNode {
4572 id: format!("SYSBUS_{bus}"),
4573 name: format!("Bus_{bus}"),
4574 voltage_level_id: format!("VL_SYS_{vl_key}"),
4575 });
4576 }
4577 }
4578
4579 let sw_id = format!("SYS_SW_{}_{}_{}", ssd.bus_i, ssd.bus_j, ssd.ckt);
4580 let rated_current = if ssd.rate1 > 0.0 {
4581 Some(ssd.rate1)
4582 } else {
4583 None
4584 };
4585 switches.push(SwitchDevice {
4586 id: sw_id,
4587 name: ssd.name.clone(),
4588 switch_type: psse_switch_type(ssd.device_type),
4589 cn1_id: format!("SYSBUS_{}", ssd.bus_i),
4590 cn2_id: format!("SYSBUS_{}", ssd.bus_j),
4591 open: ssd.status == 0,
4592 normal_open: ssd.normal_status == 0,
4593 retained: false,
4594 rated_current,
4595 });
4596 }
4597
4598 NodeBreakerTopology::new(
4599 vec![SubstationData {
4600 id: "SYS".into(),
4601 name: "System".into(),
4602 region: None,
4603 }],
4604 voltage_levels,
4605 Vec::new(),
4606 connectivity_nodes,
4607 Vec::new(),
4608 switches,
4609 Vec::new(),
4610 )
4611}
4612
4613#[cfg(test)]
4614mod tests {
4615 use super::*;
4616 #[allow(dead_code)]
4617 fn data_available() -> bool {
4618 if let Ok(p) = std::env::var("SURGE_TEST_DATA") {
4619 return std::path::Path::new(&p).exists();
4620 }
4621 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4622 .parent()
4623 .unwrap()
4624 .parent()
4625 .unwrap()
4626 .join("tests/data")
4627 .exists()
4628 }
4629 #[allow(dead_code)]
4630 fn test_data_dir() -> std::path::PathBuf {
4631 if let Ok(p) = std::env::var("SURGE_TEST_DATA") {
4632 return std::path::PathBuf::from(p);
4633 }
4634 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4635 .parent()
4636 .unwrap()
4637 .parent()
4638 .unwrap()
4639 .join("tests/data")
4640 }
4641
4642 #[test]
4643 fn test_tokenize_comma_delimited() {
4644 let tokens = tokenize_record("1, 'BUS 1 ', 138.0, 1, 1, 1, 1, 1.050, -10.5");
4645 assert_eq!(tokens[0], "1");
4646 assert_eq!(tokens[1], "'BUS 1 '");
4647 assert_eq!(tokens[2], "138.0");
4648 assert_eq!(tokens[3], "1");
4649 assert_eq!(tokens.len(), 9);
4650 }
4651
4652 #[test]
4653 fn test_tokenize_with_comment() {
4654 let tokens = tokenize_record("1, 2, 3 / this is a comment");
4655 assert_eq!(tokens.len(), 3);
4656 assert_eq!(tokens[2], "3");
4657 }
4658
4659 #[test]
4660 fn test_section_end() {
4661 assert!(is_section_end("0 / END OF BUS DATA"));
4662 assert!(is_section_end("0"));
4663 assert!(is_section_end(" 0 / END OF DATA"));
4664 assert!(!is_section_end("10, 'BUS', 138.0"));
4665 assert!(!is_section_end("0,5,'1 ',0.01,0.1,0.0,100.0"));
4667 assert!(!is_section_end("0, 5, '1 '"));
4668 assert!(!is_section_end("01"));
4670 assert!(!is_section_end("0.0"));
4671 assert!(!is_section_end("0.01, 0.1"));
4672 }
4673
4674 #[test]
4675 fn test_parse_f64_rejects_nan_inf() {
4676 assert!(parse_f64("NaN", 1, "test").is_err());
4678 assert!(parse_f64("nan", 1, "test").is_err());
4679 assert!(parse_f64("inf", 1, "test").is_err());
4680 assert!(parse_f64("Inf", 1, "test").is_err());
4681 assert!(parse_f64("-inf", 1, "test").is_err());
4682 assert!(parse_f64("-Inf", 1, "test").is_err());
4683 assert_eq!(parse_f64("0.0", 1, "rate").unwrap(), 0.0);
4685 assert_eq!(parse_f64("0", 1, "rate").unwrap(), 0.0);
4686 assert!((parse_f64("1.5", 1, "r").unwrap() - 1.5).abs() < 1e-12);
4687 assert!((parse_f64("-3.15", 1, "x").unwrap() + 3.15).abs() < 1e-12);
4688 }
4689
4690 #[test]
4691 fn test_parse_switched_shunt_rejects_truncated_record() {
4692 let lines = vec!["1, 1", "Q"];
4693 let err = parse_switched_shunt_section(&lines, 0).unwrap_err();
4694 match err {
4695 PsseError::Parse { line, message } => {
4696 assert_eq!(line, 1);
4697 assert!(message.contains("switched shunt record is truncated"));
4698 }
4699 other => panic!("expected parse error for truncated switched shunt, got {other:?}"),
4700 }
4701 }
4702
4703 #[test]
4704 fn test_section_end_bus0_branch_not_truncated() {
4705 let raw = r#"0, 100.00, 33, 0, 0, 60.00
4708Test Case
4709Test Case
4710 0,'BUS0 ', 138.0000,3, 1, 1, 1, 1.06000, 0.0000
4711 5,'BUS5 ', 138.0000,1, 1, 1, 1, 1.00000, 0.0000
47120 / END OF BUS DATA
47130 / END OF LOAD DATA
47140 / END OF FIXED SHUNT DATA
4715 5,'1 ', 100.000, 0.000, 300.000, -300.000, 1.06000, 0, 100.000, 0.0, 1.0, 0.0, 0.0, 1.0,1, 100.0, 250.000, 10.000
47160 / END OF GENERATOR DATA
4717 0, 5,'1 ', 0.01000, 0.10000, 0.02000, 100.00, 100.00, 100.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.000,0,0,0,0,0
47180 / END OF NON-TRANSFORMER BRANCH DATA
47190 / END OF TRANSFORMER DATA
4720Q
4721"#;
4722 let net =
4723 parse_str(raw).expect("bus-0 branch should parse without early section termination");
4724 assert_eq!(net.n_branches(), 1, "branch from bus-0 must not be dropped");
4725 assert_eq!(net.branches[0].from_bus, 0);
4726 assert_eq!(net.branches[0].to_bus, 5);
4727 }
4728
4729 #[test]
4730 fn test_parse_truncated_transformer_errors() {
4731 let raw = r#"0, 100.00, 33, 0, 0, 60.00
4732Test Case
4733Test Case Heading 2
4734 1,'BUS1 ', 138.0000,3, 1, 1, 1, 1.06000, 0.0000
4735 2,'BUS2 ', 138.0000,1, 1, 1, 1, 1.00000, 0.0000
47360 / END OF BUS DATA
47370 / END OF LOAD DATA
47380 / END OF FIXED SHUNT DATA
4739 1,'1 ', 100.000, 0.000, 300.000, -300.000, 1.06000, 0, 100.000, 0.0, 1.0, 0.0, 0.0, 1.0,1, 100.0, 250.000, 10.000
47400 / END OF GENERATOR DATA
4741 1, 2,'1 ', 0.01000, 0.10000, 0.02000, 100.00, 100.00, 100.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.000,0,0,0,0,0
47420 / END OF NON-TRANSFORMER BRANCH DATA
4743 1, 2,'1 '
47440 / END OF TRANSFORMER DATA
4745Q
4746"#;
4747 let err = parse_str(raw).unwrap_err();
4748 match err {
4749 PsseError::Parse { message, .. } => {
4750 assert!(message.contains("truncated transformer record 1"));
4751 }
4752 other => panic!("expected transformer truncation error, got {other:?}"),
4753 }
4754 }
4755
4756 #[test]
4757 fn test_parse_area_schedule_section_rejects_invalid_numeric_fields() {
4758 let lines = vec!["1, BAD, 10.0, 5.0, 'AREA 1'", "Q"];
4759 let err = parse_area_schedule_section(&lines, 0).unwrap_err();
4760 match err {
4761 PsseError::Parse { line, message } => {
4762 assert_eq!(line, 1);
4763 assert!(message.contains("ISW"));
4764 }
4765 other => panic!("expected parse error for invalid area schedule row, got {other:?}"),
4766 }
4767 }
4768
4769 #[test]
4770 fn test_parse_area_schedule_section_rejects_truncated_rows() {
4771 let lines = vec!["1, 2, 10.0, 5.0", "Q"];
4772 let err = parse_area_schedule_section(&lines, 0).unwrap_err();
4773 match err {
4774 PsseError::Parse { line, message } => {
4775 assert_eq!(line, 1);
4776 assert!(message.contains("truncated"));
4777 }
4778 other => panic!("expected parse error for truncated area schedule row, got {other:?}"),
4779 }
4780 }
4781
4782 #[test]
4783 fn test_parse_minimal_raw() {
4784 let raw = r#"0, 100.00, 33, 0, 0, 60.00 / PSS/E 33 case
4786Test Case
4787Test Case Heading 2
4788 1,'BUS1 ', 138.0000,3, 1, 1, 1, 1.06000, 0.0000
4789 2,'BUS2 ', 138.0000,1, 1, 1, 1, 1.00000, 0.0000
47900 / END OF BUS DATA
4791 2,'1 ',1, 1, 1, 50.000, 20.000, 0.000, 0.000, 0.000, 0.000, 1,1,0,1,0,0
47920 / END OF LOAD DATA
47930 / END OF FIXED SHUNT DATA
4794 1,'1 ', 100.000, 0.000, 300.000, -300.000, 1.06000, 0, 100.000, 0.0, 1.0, 0.0, 0.0, 1.0,1, 100.0, 250.000, 10.000
47950 / END OF GENERATOR DATA
4796 1, 2,'1 ', 0.01000, 0.10000, 0.02000, 100.00, 100.00, 100.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.000,0,0,0,0,0
47970 / END OF NON-TRANSFORMER BRANCH DATA
47980 / END OF TRANSFORMER DATA
4799Q
4800"#;
4801 let net = parse_str(raw).expect("failed to parse minimal PSS/E RAW");
4802
4803 assert_eq!(net.base_mva, 100.0);
4804 assert_eq!(net.n_buses(), 2);
4805 assert_eq!(net.generators.len(), 1);
4806 assert_eq!(net.n_branches(), 1);
4807
4808 assert_eq!(net.buses[0].number, 1);
4810 assert_eq!(net.buses[0].bus_type, BusType::Slack);
4811 assert!((net.buses[0].voltage_magnitude_pu - 1.06).abs() < 1e-10);
4812 assert!((net.buses[0].base_kv - 138.0).abs() < 1e-10);
4813
4814 assert_eq!(net.buses[1].number, 2);
4815 assert_eq!(net.buses[1].bus_type, BusType::PQ);
4816
4817 let bus_pd = net.bus_load_p_mw();
4819 let bus_qd = net.bus_load_q_mvar();
4820 assert!((bus_pd[1] - 50.0).abs() < 1e-10);
4821 assert!((bus_qd[1] - 20.0).abs() < 1e-10);
4822
4823 assert_eq!(net.generators[0].bus, 1);
4825 assert!((net.generators[0].p - 100.0).abs() < 1e-10);
4826 assert!((net.generators[0].voltage_setpoint_pu - 1.06).abs() < 1e-10);
4827 assert!(net.generators[0].in_service);
4828
4829 assert_eq!(net.branches[0].from_bus, 1);
4831 assert_eq!(net.branches[0].to_bus, 2);
4832 assert!((net.branches[0].r - 0.01).abs() < 1e-10);
4833 assert!((net.branches[0].x - 0.10).abs() < 1e-10);
4834 assert!((net.branches[0].b - 0.02).abs() < 1e-10);
4835 assert!((net.branches[0].tap - 1.0).abs() < 1e-10);
4836 assert!(net.branches[0].in_service);
4837 }
4838
4839 #[test]
4840 fn test_parse_with_transformer() {
4841 let raw = r#"0, 100.00, 33, 0, 0, 60.00
4842Test Case
4843Test Case
4844 1,'BUS1 ', 345.0000,3, 1, 1, 1, 1.06000, 0.0000
4845 2,'BUS2 ', 138.0000,1, 1, 1, 1, 1.00000, 0.0000
48460 / END OF BUS DATA
48470 / END OF LOAD DATA
48480 / END OF FIXED SHUNT DATA
4849 1,'1 ', 100.000, 0.000, 300.000, -300.000, 1.06000, 0, 100.000, 0.0, 1.0, 0.0, 0.0, 1.0,1, 100.0, 250.000, 10.000
48500 / END OF GENERATOR DATA
48510 / END OF NON-TRANSFORMER BRANCH DATA
4852 1, 2, 0,'1 ',1,1,1, 0.00000, 0.00000,2,' ',1,1,1.00000,0,1.00000,0,1.00000,0,0, 0.00000,' '
4853 0.00000, 0.10000, 100.00
4854 1.05000, 0.000, 0.000, 100.00, 0.00, 0.00, 0, 0, 1.10000, 0.90000, 1.10000, 0.90000, 33, 0, 0.00000, 0.00000, 0.000
4855 1.00000, 0.000
48560 / END OF TRANSFORMER DATA
4857Q
4858"#;
4859 let net = parse_str(raw).expect("failed to parse PSS/E with transformer");
4860
4861 assert_eq!(net.n_buses(), 2);
4862 assert_eq!(net.n_branches(), 1); assert_eq!(net.generators.len(), 1);
4864
4865 let xfmr = &net.branches[0];
4866 assert_eq!(xfmr.from_bus, 1);
4867 assert_eq!(xfmr.to_bus, 2);
4868 assert!((xfmr.x - 0.10).abs() < 1e-10);
4869 assert!((xfmr.tap - 1.05).abs() < 1e-10);
4871 assert!(xfmr.in_service);
4872 }
4873
4874 #[test]
4875 fn test_parse_with_fixed_shunt() {
4876 let raw = r#"0, 100.00, 33, 0, 0, 60.00
4877Test
4878Test
4879 1,'BUS1 ', 138.0000,3, 1, 1, 1, 1.06000, 0.0000
4880 2,'BUS2 ', 138.0000,1, 1, 1, 1, 1.00000, 0.0000
48810 / END OF BUS DATA
48820 / END OF LOAD DATA
4883 2,'1 ',1, 0.000, 19.000
48840 / END OF FIXED SHUNT DATA
4885 1,'1 ', 100.000, 0.000, 300.000, -300.000, 1.06000, 0, 100.000, 0.0, 1.0, 0.0, 0.0, 1.0,1, 100.0, 250.000, 10.000
48860 / END OF GENERATOR DATA
4887 1, 2,'1 ', 0.01000, 0.10000, 0.02000, 100.00, 100.00, 100.00, 0.00000, 0.00000, 0.00000, 0.00000,1,1, 0.000,0,0,0,0,0
48880 / END OF NON-TRANSFORMER BRANCH DATA
48890 / END OF TRANSFORMER DATA
4890Q
4891"#;
4892 let net = parse_str(raw).expect("failed to parse PSS/E with shunt");
4893
4894 let bus2 = net.buses.iter().find(|b| b.number == 2).unwrap();
4896 assert!((bus2.shunt_susceptance_mvar - 19.0).abs() < 1e-10);
4897 }
4898
4899 #[test]
4900 fn test_parse_status_rejects_empty_and_unknown() {
4901 assert!(parse_status("", 1, "STAT").is_err());
4902 assert!(parse_status("maybe", 1, "STAT").is_err());
4903 assert_eq!(parse_status("1", 1, "STAT").unwrap(), 1);
4904 assert_eq!(parse_status("0", 1, "STAT").unwrap(), 0);
4905 }
4906
4907 #[test]
4908 fn test_dc_converter_record_requires_status_field() {
4909 let fields = vec![
4910 "1".to_string(),
4911 "1".to_string(),
4912 "30".to_string(),
4913 "5".to_string(),
4914 "0.1".to_string(),
4915 "0.1".to_string(),
4916 "230".to_string(),
4917 "1".to_string(),
4918 "1".to_string(),
4919 "1".to_string(),
4920 "1".to_string(),
4921 "1".to_string(),
4922 "".to_string(),
4923 ];
4924 assert!(
4925 parse_dc_converter_record(&fields, 1).is_err(),
4926 "blank IC must be rejected"
4927 );
4928 }
4929
4930 #[test]
4931 fn test_vsc_converter_record_requires_state_field() {
4932 let fields = vec![
4933 "1".to_string(),
4934 "1".to_string(),
4935 "2".to_string(),
4936 "0.0".to_string(),
4937 "1.0".to_string(),
4938 "0.0".to_string(),
4939 "0.0".to_string(),
4940 "0.0".to_string(),
4941 "100.0".to_string(),
4942 "-100.0".to_string(),
4943 "50.0".to_string(),
4944 "-50.0".to_string(),
4945 "1.0".to_string(),
4946 "".to_string(),
4947 ];
4948 assert!(
4949 parse_vsc_converter_record(&fields, 1).is_err(),
4950 "blank STATE must be rejected"
4951 );
4952 }
4953
4954 #[test]
4955 fn test_parse_ieee14_raw() {
4956 let path = test_data_dir().join("IEEE_14_bus.raw");
4957 if !path.exists() {
4958 return; }
4960 let net = parse_file(&path).expect("failed to parse IEEE 14 bus RAW");
4961 assert_eq!(net.n_buses(), 14);
4962 assert_eq!(net.n_branches(), 20);
4963 assert_eq!(net.generators.len(), 5);
4964 assert!(net.total_load_mw() > 250.0);
4965 }
4966
4967 #[test]
4968 fn test_parse_ieee30_raw() {
4969 let path = test_data_dir().join("IEEE_30_bus.raw");
4970 if !path.exists() {
4971 return;
4972 }
4973 let net = parse_file(&path).expect("failed to parse IEEE 30 bus RAW");
4974 assert_eq!(net.n_buses(), 30);
4975 assert_eq!(net.n_branches(), 41);
4976 assert_eq!(net.generators.len(), 6);
4977 }
4978
4979 #[test]
4980 fn test_parse_ieee57_raw() {
4981 let path = test_data_dir().join("IEEE_57_bus.raw");
4982 if !path.exists() {
4983 return;
4984 }
4985 let net = parse_file(&path).expect("failed to parse IEEE 57 bus RAW");
4986 assert_eq!(net.n_buses(), 57);
4987 assert_eq!(net.n_branches(), 80);
4988 assert_eq!(net.generators.len(), 7);
4989 }
4990
4991 #[test]
4992 fn test_parse_ieee118_raw() {
4993 let path = test_data_dir().join("IEEE_118_bus.raw");
4994 if !path.exists() {
4995 return;
4996 }
4997 let net = parse_file(&path).expect("failed to parse IEEE 118 bus RAW");
4998 assert_eq!(net.n_buses(), 118);
4999 assert_eq!(net.n_branches(), 186);
5000 assert_eq!(net.generators.len(), 54);
5001 }
5002
5003 #[test]
5005 fn test_parse_wecc_240_v36_with_annotations() {
5006 let path = test_data_dir()
5007 .join("raw")
5008 .join("240busWECC_2018_PSS_PQLoad_fixedshunt_noremotebus_yuan_v36.raw");
5009 if !path.exists() {
5010 return;
5011 }
5012 let net = parse_file(&path).expect("failed to parse WECC 240-bus v36 RAW");
5013 assert_eq!(net.n_buses(), 243, "bus count");
5016 assert!(net.n_branches() > 0, "must have branches");
5017 assert!(!net.generators.is_empty(), "must have generators");
5018 }
5021
5022 #[test]
5024 fn test_parse_wecc_240_v34_with_annotations() {
5025 let path = test_data_dir()
5026 .join("raw")
5027 .join("240busWECC_2018_PSS_fixedshunt.raw");
5028 if !path.exists() {
5029 return;
5030 }
5031 let net = parse_file(&path).expect("failed to parse WECC 240-bus v34 RAW");
5032 assert_eq!(net.n_buses(), 243, "bus count");
5034 assert!(net.n_branches() > 0, "must have branches");
5035 }
5036
5037 #[test]
5039 fn test_cross_format_ieee14_raw_vs_matpower() {
5040 let raw_path = test_data_dir().join("IEEE_14_bus.raw");
5041 let m_path = test_data_dir().join("case14.m");
5042 if !raw_path.exists() {
5043 return;
5044 }
5045
5046 let raw_net = parse_file(&raw_path).expect("failed to parse RAW");
5047 let m_net = crate::matpower::load(&m_path).expect("failed to parse MATPOWER");
5048
5049 assert_eq!(raw_net.n_buses(), m_net.n_buses());
5051 assert_eq!(raw_net.n_branches(), m_net.n_branches());
5052 assert_eq!(raw_net.generators.len(), m_net.generators.len());
5053
5054 assert!(
5056 (raw_net.total_load_mw() - m_net.total_load_mw()).abs() < 1.0,
5057 "Load mismatch: RAW={:.1}, MATPOWER={:.1}",
5058 raw_net.total_load_mw(),
5059 m_net.total_load_mw()
5060 );
5061 }
5062
5063 #[test]
5064 fn test_psse_v35_substation_data() {
5065 let raw = r#"@!IC,SBASE,REV,XFRRAT,NXFRAT,BASFRQ
5067 0, 100.00, 35, 0, 0, 60.00
5068CASE HEADING 1
5069CASE HEADING 2
5070 1, 'BUS 1', 138.0, 3, 1, 1, 1, 1.060, 0.0, 1.1, 0.9
5071 2, 'BUS 2', 138.0, 1, 1, 1, 1, 1.045, -5.0, 1.1, 0.9
50720 / END OF BUS DATA, BEGIN LOAD DATA
5073 2, '1 ', 1, 1, 1, 21.7, 12.7, 0.0, 0.0, 0.0, 0.0, 1
50740 / END OF LOAD DATA, BEGIN FIXED SHUNT DATA
50750 / END OF FIXED SHUNT DATA, BEGIN GENERATOR DATA
5076 1, '1 ', 40.0, 0.0, 999.0, -999.0, 1.060, 0, 100.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1, 100.0, 999.0, 0.0, 1, 1.0, 0, 0, 1.0, 0.0, 0, 0.0
50770 / END OF GENERATOR DATA, BEGIN BRANCH DATA
5078 1, 2, '1 ', 0.01938, 0.05917, 0.05280, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1, 1, 0.0, 1, 1.0, 0, 1.0, 0, 1.0, 0, 1.0
50790 / END OF BRANCH DATA, BEGIN SYSTEM SWITCHING DEVICE DATA
50800 / END OF SYSTEM SWITCHING DEVICE DATA, BEGIN TRANSFORMER DATA
50810 / END OF TRANSFORMER DATA, BEGIN AREA DATA
50820 / END OF AREA DATA, BEGIN TWO-TERMINAL DC DATA
50830 / END OF TWO-TERMINAL DC DATA, BEGIN VOLTAGE SOURCE CONVERTER DATA
50840 / END OF VOLTAGE SOURCE CONVERTER DATA, BEGIN IMPEDANCE CORRECTION DATA
50850 / END OF IMPEDANCE CORRECTION DATA, BEGIN MULTI-TERMINAL DC DATA
50860 / END OF MULTI-TERMINAL DC DATA, BEGIN MULTI-SECTION LINE DATA
50870 / END OF MULTI-SECTION LINE DATA, BEGIN ZONE DATA
50880 / END OF ZONE DATA, BEGIN INTER-AREA TRANSFER DATA
50890 / END OF INTER-AREA TRANSFER DATA, BEGIN OWNER DATA
50900 / END OF OWNER DATA, BEGIN FACTS CONTROL DEVICE DATA
50910 / END OF FACTS CONTROL DEVICE DATA, BEGIN SWITCHED SHUNT DATA
50920 /END OF SWITCHED SHUNT DATA, BEGIN GNE DEVICE DATA
50930 /END OF GNE DEVICE DATA, BEGIN INDUCTION MACHINE DATA
50940 /END OF INDUCTION MACHINE DATA, BEGIN SUBSTATION DATA
5095 1, 'STATION 1', 0.0, 0.0, 0.1
5096 / BEGIN SUBSTATION NODE DATA
5097 1, 'NB1', 1, 1, 1.0, 0.0
5098 2, 'NB2', 1, 1, 1.0, 0.0
5099 3, 'NL2', 1, 1, 1.0, 0.0
5100 4, 'NG1', 1, 1, 1.0, 0.0
5101 0 / END OF SUBSTATION NODE DATA, BEGIN SUBSTATION SWITCHING DEVICE DATA
5102 1, 2, '1 ', 'Sw-BusBars', 2, 1, 1, 0, 0, 0, 0
5103 1, 3, '1 ', 'Sw-Branch2', 2, 1, 1, 0, 0, 0, 0
5104 2, 4, '1 ', 'Sw-Gen1', 2, 1, 1, 0, 0, 0, 0
5105 0 / END OF SUBSTATION SWITCHING DEVICE DATA, BEGIN SUBSTATION TERMINAL DATA
5106 1, 4, 'M', '1 '
5107 1, 3, 'B', 2, '1 '
5108 0 / END OF SUBSTATION TERMINAL DATA
51090 /END OF SUBSTATION DATA
5110Q
5111"#;
5112
5113 let net = parse_str(raw).expect("failed to parse v35 RAW with substation data");
5114 assert_eq!(net.n_buses(), 2);
5115 assert_eq!(net.n_branches(), 1);
5116 assert_eq!(net.generators.len(), 1);
5117
5118 let sm = net.topology.as_ref().expect("topology should be Some");
5120 assert_eq!(sm.substations.len(), 1, "should have 1 substation");
5121 assert_eq!(
5122 sm.connectivity_nodes.len(),
5123 4,
5124 "should have 4 connectivity nodes"
5125 );
5126 assert_eq!(sm.switches.len(), 3, "should have 3 switches");
5127 assert_eq!(
5128 sm.terminal_connections.len(),
5129 2,
5130 "should have 2 terminal connections"
5131 );
5132
5133 for sw in &sm.switches {
5135 assert!(!sw.open, "switch {} should be closed", sw.id);
5136 assert_eq!(sw.switch_type, SwitchType::Breaker);
5137 }
5138
5139 let reduction = sm
5142 .current_mapping()
5143 .expect("topology reduction should exist");
5144 assert_eq!(reduction.connectivity_node_to_bus.len(), 4);
5145 for cn_id in reduction.connectivity_node_to_bus.keys() {
5146 assert_eq!(
5147 reduction.connectivity_node_to_bus[cn_id], 1,
5148 "all nodes in station 1 should map to bus 1"
5149 );
5150 }
5151
5152 assert_eq!(sm.busbar_sections.len(), 2, "should detect NB1 and NB2");
5154 }
5155
5156 #[test]
5157 fn test_psse_v35_sys_switching_device() {
5158 let raw = r#"@!IC,SBASE,REV,XFRRAT,NXFRAT,BASFRQ
5160 0, 100.00, 35, 0, 0, 60.00
5161CASE HEADING 1
5162CASE HEADING 2
5163 1, 'BUS 1', 138.0, 3, 1, 1, 1, 1.060, 0.0, 1.1, 0.9
5164 2, 'BUS 2', 138.0, 1, 1, 1, 1, 1.045, -5.0, 1.1, 0.9
51650 / END OF BUS DATA, BEGIN LOAD DATA
51660 / END OF LOAD DATA, BEGIN FIXED SHUNT DATA
51670 / END OF FIXED SHUNT DATA, BEGIN GENERATOR DATA
51680 / END OF GENERATOR DATA, BEGIN BRANCH DATA
51690 / END OF BRANCH DATA, BEGIN SYSTEM SWITCHING DEVICE DATA
5170 1, 2, '1 ', 'BRK_1_2', 2, 1, 1, 0.0001, 0.0, 0.0, 0.0
51710 / END OF SYSTEM SWITCHING DEVICE DATA, BEGIN TRANSFORMER DATA
51720 / END OF TRANSFORMER DATA, BEGIN AREA DATA
51730 / END OF AREA DATA, BEGIN TWO-TERMINAL DC DATA
51740 / END OF TWO-TERMINAL DC DATA, BEGIN VOLTAGE SOURCE CONVERTER DATA
51750 / END OF VOLTAGE SOURCE CONVERTER DATA, BEGIN IMPEDANCE CORRECTION DATA
51760 / END OF IMPEDANCE CORRECTION DATA, BEGIN MULTI-TERMINAL DC DATA
51770 / END OF MULTI-TERMINAL DC DATA, BEGIN MULTI-SECTION LINE DATA
51780 / END OF MULTI-SECTION LINE DATA, BEGIN ZONE DATA
51790 / END OF ZONE DATA, BEGIN INTER-AREA TRANSFER DATA
51800 / END OF INTER-AREA TRANSFER DATA, BEGIN OWNER DATA
51810 / END OF OWNER DATA, BEGIN FACTS CONTROL DEVICE DATA
51820 / END OF FACTS CONTROL DEVICE DATA, BEGIN SWITCHED SHUNT DATA
51830 /END OF SWITCHED SHUNT DATA
5184Q
5185"#;
5186
5187 let net = parse_str(raw).expect("failed to parse v35 with sys switching device");
5188 assert_eq!(net.n_buses(), 2);
5189
5190 let sm = net
5192 .topology
5193 .as_ref()
5194 .expect("topology should be Some for sys switch devices");
5195 assert_eq!(sm.switches.len(), 1, "should have 1 sys switch device");
5196 assert_eq!(sm.switches[0].switch_type, SwitchType::Breaker);
5197 assert!(!sm.switches[0].open, "switch should be closed (status=1)");
5198 assert_eq!(
5199 sm.connectivity_nodes.len(),
5200 2,
5201 "should have 2 CNs for the two bus endpoints"
5202 );
5203 }
5204
5205 #[test]
5207 fn test_parse_induction_machine_data() {
5208 let raw = r#" 0, 100.0, 35, 0, 0, 60.0 / PSS/E 35 Raw Data
5209 Test Case
5210 Exported by Surge
5211 1,'BUS1',138.0,3,1,1,1,1.0,0.0,1
52120 / END OF BUS DATA, BEGIN LOAD DATA
52130 / END OF LOAD DATA, BEGIN FIXED SHUNT DATA
52140 / END OF FIXED SHUNT DATA, BEGIN GENERATOR DATA
52150 / END OF GENERATOR DATA, BEGIN BRANCH DATA
52160 / END OF BRANCH DATA, BEGIN SYSTEM SWITCHING DEVICE DATA
52170 / END OF SYSTEM SWITCHING DEVICE DATA, BEGIN TRANSFORMER DATA
52180 / END OF TRANSFORMER DATA, BEGIN AREA DATA
52190 / END OF AREA DATA, BEGIN TWO-TERMINAL DC DATA
52200 / END OF TWO-TERMINAL DC DATA, BEGIN VOLTAGE SOURCE CONVERTER DATA
52210 / END OF VOLTAGE SOURCE CONVERTER DATA, BEGIN IMPEDANCE CORRECTION DATA
52220 / END OF IMPEDANCE CORRECTION DATA, BEGIN MULTI-TERMINAL DC DATA
52230 / END OF MULTI-TERMINAL DC DATA, BEGIN MULTI-SECTION LINE DATA
52240 / END OF MULTI-SECTION LINE DATA, BEGIN ZONE DATA
52250 / END OF ZONE DATA, BEGIN INTER-AREA TRANSFER DATA
52260 / END OF INTER-AREA TRANSFER DATA, BEGIN OWNER DATA
52270 / END OF OWNER DATA, BEGIN FACTS CONTROL DEVICE DATA
52280 / END OF FACTS CONTROL DEVICE DATA, BEGIN SWITCHED SHUNT DATA
52290 /END OF SWITCHED SHUNT DATA, BEGIN GNE DEVICE DATA
52300 /END OF GNE DEVICE DATA, BEGIN INDUCTION MACHINE DATA
5231 1,'M1',1,1,3,1,1,1,1,1,5.0,4.16,1,3.0,1.5,0.0,1.0,0.0,0.0,0.0
5232 0.01,0.05,2.5,0.03,0.08,0.0,0.0,0.0,1.0,0.0,1.2,0.0,0.0,0.0,1.0
52330 /END OF INDUCTION MACHINE DATA, BEGIN SUBSTATION DATA
52340 /END OF SUBSTATION DATA
5235Q
5236"#;
5237 let net = parse_str(raw).expect("failed to parse v35 RAW with induction machine");
5238 assert_eq!(
5239 net.induction_machines.len(),
5240 1,
5241 "expected 1 induction machine"
5242 );
5243 let m = &net.induction_machines[0];
5244 assert_eq!(m.bus, 1);
5245 assert_eq!(m.id.trim_matches('\'').trim(), "M1");
5246 assert!(m.in_service);
5247 assert!((m.mbase - 5.0).abs() < 1e-9, "mbase={}", m.mbase);
5248 assert!((m.h - 1.5).abs() < 1e-9, "H={}", m.h);
5249 assert!((m.ra - 0.01).abs() < 1e-9, "ra={}", m.ra);
5250 assert!((m.xm - 2.5).abs() < 1e-9, "xm={}", m.xm);
5251 assert!((m.r1 - 0.03).abs() < 1e-9, "r1={}", m.r1);
5252 }
5253}