1use std::collections::HashMap;
28use std::path::Path;
29
30use surge_network::Network;
31use surge_network::network::AreaSchedule;
32use surge_network::network::{Branch, BranchType, Bus, BusType, Generator};
33use thiserror::Error;
34
35#[derive(Error, Debug)]
40pub enum EpcError {
41 #[error("I/O error: {0}")]
42 Io(#[from] std::io::Error),
43
44 #[error("parse error on line {line}: {message}")]
45 Parse { line: usize, message: String },
46
47 #[error("missing section: {0}")]
48 MissingSection(String),
49
50 #[error("unexpected end of file in {0} section")]
51 UnexpectedEof(String),
52
53 #[error("non-finite float value on line {line}: {message}")]
54 NonFiniteValue { line: usize, message: String },
55}
56
57pub fn parse_file(path: &Path) -> Result<Network, EpcError> {
63 let content = std::fs::read_to_string(path)?;
64 let name = path
65 .file_stem()
66 .and_then(|s| s.to_str())
67 .unwrap_or("unknown")
68 .to_string();
69 parse_string_with_name(&content, &name)
70}
71
72pub fn parse_str(content: &str) -> Result<Network, EpcError> {
74 parse_string_with_name(content, "unknown")
75}
76
77use crate::parse_utils::{RawLoad, RawShunt, apply_loads, apply_shunts};
82
83#[derive(Debug, PartialEq)]
88enum EpcSection {
89 Title,
90 Comments,
91 SolutionParameters,
92 SubstationData,
93 BusData,
94 BranchData,
95 TransformerData,
96 GeneratorData,
97 LoadData,
98 ShuntData,
99 SvdData,
100 AreaData,
101 ZoneData,
102 InterfaceData,
103 InterfaceBranchData,
104 DcBusData,
105 DcLineData,
106 DcConverterData,
107 VsConverterData,
108 ZTableData,
109 GcdData,
110 TransactionData,
111 OwnerData,
112 QtableData,
113 BaData,
114 InjGroupData,
115 InjGrpElemData,
116 End,
117}
118
119fn detect_section(line: &str) -> Option<EpcSection> {
121 let lower = line.trim().to_ascii_lowercase();
122 if lower == "title" {
124 return Some(EpcSection::Title);
125 }
126 if lower == "comments" {
127 return Some(EpcSection::Comments);
128 }
129 if lower.starts_with("solution parameters") || lower.starts_with("solution_parameters") {
130 return Some(EpcSection::SolutionParameters);
131 }
132 if lower.starts_with("substation data") {
133 return Some(EpcSection::SubstationData);
134 }
135 if lower.starts_with("bus data") {
136 return Some(EpcSection::BusData);
137 }
138 if lower.starts_with("branch data") {
139 return Some(EpcSection::BranchData);
140 }
141 if lower.starts_with("transformer data") {
142 return Some(EpcSection::TransformerData);
143 }
144 if lower.starts_with("generator data") {
145 return Some(EpcSection::GeneratorData);
146 }
147 if lower.starts_with("load data") {
148 return Some(EpcSection::LoadData);
149 }
150 if lower.starts_with("shunt data") {
151 return Some(EpcSection::ShuntData);
152 }
153 if lower.starts_with("svd data") {
154 return Some(EpcSection::SvdData);
155 }
156 if lower.starts_with("interface branch data") {
157 return Some(EpcSection::InterfaceBranchData);
158 }
159 if lower.starts_with("interface data") {
160 return Some(EpcSection::InterfaceData);
161 }
162 if lower.starts_with("area data") {
163 return Some(EpcSection::AreaData);
164 }
165 if lower.starts_with("zone data") {
166 return Some(EpcSection::ZoneData);
167 }
168 if lower.starts_with("dc bus data") {
169 return Some(EpcSection::DcBusData);
170 }
171 if lower.starts_with("dc line data") {
172 return Some(EpcSection::DcLineData);
173 }
174 if lower.starts_with("dc converter data") {
175 return Some(EpcSection::DcConverterData);
176 }
177 if lower.starts_with("vs converter data") {
178 return Some(EpcSection::VsConverterData);
179 }
180 if lower.starts_with("z table data") {
181 return Some(EpcSection::ZTableData);
182 }
183 if lower.starts_with("gcd data") {
184 return Some(EpcSection::GcdData);
185 }
186 if lower.starts_with("transaction data") {
187 return Some(EpcSection::TransactionData);
188 }
189 if lower.starts_with("owner data") {
190 return Some(EpcSection::OwnerData);
191 }
192 if lower.starts_with("qtable data") {
193 return Some(EpcSection::QtableData);
194 }
195 if lower.starts_with("ba data") {
196 return Some(EpcSection::BaData);
197 }
198 if lower.starts_with("injgrpelem data") {
199 return Some(EpcSection::InjGrpElemData);
200 }
201 if lower.starts_with("injgroup data") {
202 return Some(EpcSection::InjGroupData);
203 }
204 if lower == "end" {
205 return Some(EpcSection::End);
206 }
207 None
208}
209
210fn tokenize_epc(line: &str) -> Vec<String> {
217 let mut tokens = Vec::new();
218 let mut chars = line.chars().peekable();
219 let mut current = String::new();
220
221 while let Some(&ch) = chars.peek() {
222 match ch {
223 '"' => {
224 chars.next();
226 let mut quoted = String::new();
227 while let Some(&qch) = chars.peek() {
228 if qch == '"' {
229 chars.next();
230 break;
231 }
232 quoted.push(qch);
233 chars.next();
234 }
235 if !current.is_empty() {
237 tokens.push(std::mem::take(&mut current));
238 }
239 tokens.push(quoted);
240 }
241 ':' => {
242 if !current.is_empty() {
243 tokens.push(std::mem::take(&mut current));
244 }
245 tokens.push(":".into());
246 chars.next();
247 }
248 ' ' | '\t' => {
249 if !current.is_empty() {
250 tokens.push(std::mem::take(&mut current));
251 }
252 chars.next();
253 }
254 _ => {
255 current.push(ch);
256 chars.next();
257 }
258 }
259 }
260 if !current.is_empty() {
261 tokens.push(current);
262 }
263 tokens
264}
265
266fn parse_f64(token: &str, line: usize, field: &str) -> Result<f64, EpcError> {
268 let s = token.trim().trim_matches('"');
269 if s.is_empty() {
270 return Ok(0.0);
271 }
272 let v: f64 = s.parse().map_err(|_| EpcError::Parse {
273 line,
274 message: format!("cannot parse '{s}' as f64 for field '{field}'"),
275 })?;
276 if !v.is_finite() {
277 return Err(EpcError::NonFiniteValue {
278 line,
279 message: format!("non-finite value {v} for field '{field}'"),
280 });
281 }
282 Ok(v)
283}
284
285fn parse_u32(token: &str, line: usize, field: &str) -> Result<u32, EpcError> {
287 let s = token.trim().trim_matches('"');
288 if s.is_empty() {
289 return Ok(0);
290 }
291 if s.contains('.') || s.contains('E') || s.contains('e') {
293 let v = parse_f64(s, line, field)?;
294 return Ok(v as u32);
295 }
296 s.parse().map_err(|_| EpcError::Parse {
297 line,
298 message: format!("cannot parse '{s}' as u32 for field '{field}'"),
299 })
300}
301
302fn parse_i32(token: &str, line: usize, field: &str) -> Result<i32, EpcError> {
304 let s = token.trim().trim_matches('"');
305 if s.is_empty() {
306 return Ok(0);
307 }
308 if s.contains('.') || s.contains('E') || s.contains('e') {
309 let v = parse_f64(s, line, field)?;
310 return Ok(v as i32);
311 }
312 s.parse().map_err(|_| EpcError::Parse {
313 line,
314 message: format!("cannot parse '{s}' as i32 for field '{field}'"),
315 })
316}
317
318fn tok(tokens: &[String], idx: usize) -> &str {
320 tokens.get(idx).map(|s| s.as_str()).unwrap_or("")
321}
322
323struct LogicalLine {
330 text: String,
331 line_num: usize,
332}
333
334fn preprocess_lines(raw_lines: &[&str]) -> Vec<LogicalLine> {
337 let mut result = Vec::new();
338 let mut i = 0;
339 while i < raw_lines.len() {
340 let line = raw_lines[i];
341 let trimmed = line.trim();
342
343 if trimmed.starts_with('@') {
345 i += 1;
346 continue;
347 }
348
349 if trimmed.is_empty() {
351 i += 1;
352 continue;
353 }
354
355 if let Some(prefix) = trimmed.strip_suffix('/') {
357 let first_line = i + 1; let mut joined = prefix.to_string();
359 i += 1;
360 while i < raw_lines.len() {
361 let next = raw_lines[i].trim();
362 if let Some(next_prefix) = next.strip_suffix('/') {
363 joined.push(' ');
364 joined.push_str(next_prefix);
365 i += 1;
366 } else {
367 joined.push(' ');
368 joined.push_str(next);
369 i += 1;
370 break;
371 }
372 }
373 result.push(LogicalLine {
374 text: joined,
375 line_num: first_line,
376 });
377 } else {
378 result.push(LogicalLine {
379 text: trimmed.to_string(),
380 line_num: i + 1,
381 });
382 i += 1;
383 }
384 }
385 result
386}
387
388#[cfg(test)]
390fn parse_section_count(header: &str) -> Option<usize> {
391 let start = header.find('[')?;
392 let end = header.find(']')?;
393 if end <= start {
394 return None;
395 }
396 header[start + 1..end].trim().parse().ok()
397}
398
399fn split_on_colon(tokens: &[String]) -> (Vec<String>, Vec<String>) {
406 if let Some(pos) = tokens.iter().position(|t| t == ":") {
407 let before = tokens[..pos].to_vec();
408 let after = if pos + 1 < tokens.len() {
409 tokens[pos + 1..].to_vec()
410 } else {
411 Vec::new()
412 };
413 (before, after)
414 } else {
415 (Vec::new(), tokens.to_vec())
417 }
418}
419
420fn parse_string_with_name(content: &str, name: &str) -> Result<Network, EpcError> {
425 let raw_lines: Vec<&str> = content.lines().collect();
426 let lines = preprocess_lines(&raw_lines);
427
428 let mut network = Network::new(name);
429 network.base_mva = 100.0; let mut raw_loads: Vec<RawLoad> = Vec::new();
432 let mut raw_shunts: Vec<RawShunt> = Vec::new();
433 let mut bus_vsched: HashMap<u32, f64> = HashMap::new();
434
435 let mut pos = 0;
436
437 while pos < lines.len() {
438 let line = &lines[pos].text;
439
440 if let Some(section) = detect_section(line) {
441 pos += 1; match section {
444 EpcSection::Title | EpcSection::Comments => {
445 while pos < lines.len() {
447 if lines[pos].text.trim() == "!" {
448 pos += 1;
449 break;
450 }
451 if detect_section(&lines[pos].text).is_some() {
452 break;
453 }
454 pos += 1;
455 }
456 }
457 EpcSection::SolutionParameters => {
458 while pos < lines.len() {
459 let sp_line = &lines[pos].text;
460 if sp_line.trim() == "!" {
461 pos += 1;
462 break;
463 }
464 if detect_section(sp_line).is_some() {
465 break;
466 }
467 let lower = sp_line.trim().to_ascii_lowercase();
469 if lower.starts_with("sbase") {
470 let parts: Vec<&str> = sp_line.split_whitespace().collect();
471 if parts.len() >= 2
472 && let Ok(v) = parts[1].parse::<f64>()
473 {
474 network.base_mva = v;
475 }
476 }
477 pos += 1;
478 }
479 }
480 EpcSection::SubstationData => {
481 pos = skip_section(&lines, pos);
482 }
483 EpcSection::BusData => {
484 let (buses, vsched_map, next) = parse_bus_section(&lines, pos)?;
485 network.buses = buses;
486 bus_vsched = vsched_map;
487 pos = next;
488 }
489 EpcSection::BranchData => {
490 let (branches, next) = parse_branch_section(&lines, pos)?;
491 network.branches.extend(branches);
492 pos = next;
493 }
494 EpcSection::TransformerData => {
495 let (xfmr_branches, next) =
496 parse_transformer_section(&lines, pos, &network.buses, network.base_mva)?;
497 network.branches.extend(xfmr_branches);
498 pos = next;
499 }
500 EpcSection::GeneratorData => {
501 let (gens, next) = parse_generator_section(&lines, pos, &bus_vsched)?;
502 network.generators = gens;
503 pos = next;
504 }
505 EpcSection::LoadData => {
506 let (loads, next) = parse_load_section(&lines, pos)?;
507 raw_loads.extend(loads);
508 pos = next;
509 }
510 EpcSection::ShuntData => {
511 let (shunts, next) = parse_shunt_section(&lines, pos)?;
512 raw_shunts.extend(shunts);
513 pos = next;
514 }
515 EpcSection::SvdData => {
516 let (shunts, next) = parse_svd_section(&lines, pos, network.base_mva)?;
517 raw_shunts.extend(shunts);
518 pos = next;
519 }
520 EpcSection::AreaData => {
521 let (areas, next) = parse_area_section(&lines, pos);
522 network.area_schedules = areas;
523 pos = next;
524 }
525 EpcSection::End => break,
526 _ => {
527 pos = skip_section(&lines, pos);
529 }
530 }
531 } else {
532 pos += 1;
533 }
534 }
535
536 apply_loads(&mut network, &raw_loads).map_err(|err| EpcError::Parse {
538 line: 1,
539 message: err.to_string(),
540 })?;
541 apply_shunts(&mut network, &raw_shunts);
542 fixup_bus_types(&mut network);
543 fixup_voltage_limits(&mut network);
544 fixup_latlon(&mut network);
545 Ok(network)
546}
547
548fn skip_section(lines: &[LogicalLine], start: usize) -> usize {
550 let mut pos = start;
551 while pos < lines.len() {
552 if detect_section(&lines[pos].text).is_some() {
553 return pos;
554 }
555 pos += 1;
556 }
557 pos
558}
559
560#[allow(clippy::type_complexity)]
573fn parse_bus_section(
574 lines: &[LogicalLine],
575 start: usize,
576) -> Result<(Vec<Bus>, HashMap<u32, f64>, usize), EpcError> {
577 let mut buses = Vec::new();
578 let mut bus_vsched: HashMap<u32, f64> = HashMap::new();
579 let mut pos = start;
580
581 while pos < lines.len() {
582 if detect_section(&lines[pos].text).is_some() {
583 return Ok((buses, bus_vsched, pos));
584 }
585
586 let line_num = lines[pos].line_num;
587 let tokens = tokenize_epc(&lines[pos].text);
588 if tokens.is_empty() {
589 pos += 1;
590 continue;
591 }
592
593 let (ident, data) = split_on_colon(&tokens);
594 if ident.len() < 3 || data.len() < 14 {
595 pos += 1;
596 continue;
597 }
598
599 let number = parse_u32(tok(&ident, 0), line_num, "bus")?;
600 let name = ident.get(1).cloned().unwrap_or_default();
601 let base_kv = parse_f64(tok(&ident, 2), line_num, "basekv")?;
602
603 let ty = parse_i32(tok(&data, 0), line_num, "ty")?;
607 let vsched = parse_f64(tok(&data, 1), line_num, "vsched")?;
608 let volt = parse_f64(tok(&data, 2), line_num, "volt")?;
609 let angle_deg = parse_f64(tok(&data, 3), line_num, "angle")?;
610 let area = parse_u32(tok(&data, 4), line_num, "ar")?;
611 let zone = parse_u32(tok(&data, 5), line_num, "zone")?;
612 let vmax = parse_f64(tok(&data, 6), line_num, "vmax")?;
613 let vmin = parse_f64(tok(&data, 7), line_num, "vmin")?;
614 let st = parse_i32(tok(&data, 13), line_num, "st")?;
618 let lat = if data.len() > 14 {
619 parse_f64(tok(&data, 14), line_num, "latitude").unwrap_or(0.0)
620 } else {
621 0.0
622 };
623 let lon = if data.len() > 15 {
624 parse_f64(tok(&data, 15), line_num, "longitude").unwrap_or(0.0)
625 } else {
626 0.0
627 };
628
629 let bus_type = match ty {
630 2 => BusType::PV,
631 3 => BusType::Slack,
632 4 => BusType::Isolated,
633 _ => BusType::PQ, };
635
636 let in_service = st == 0;
638 let effective_type = if !in_service {
639 BusType::Isolated
640 } else {
641 bus_type
642 };
643
644 let vm = if volt > 0.0 { volt } else { 1.0 };
645
646 let mut bus = Bus::new(number, effective_type, base_kv);
647 bus.name = name;
648 bus.voltage_magnitude_pu = vm;
649 bus.voltage_angle_rad = angle_deg.to_radians();
650 bus.area = if area > 0 { area } else { 1 };
651 bus.zone = if zone > 0 { zone } else { 1 };
652 bus.voltage_max_pu = vmax;
653 bus.voltage_min_pu = vmin;
654 bus.latitude = Some(lat);
655 bus.longitude = Some(lon);
656
657 let vs = if vsched > 0.0 { vsched } else { vm };
659 bus_vsched.insert(number, vs);
660
661 buses.push(bus);
662 pos += 1;
663 }
664
665 Ok((buses, bus_vsched, pos))
666}
667
668fn parse_branch_section(
682 lines: &[LogicalLine],
683 start: usize,
684) -> Result<(Vec<Branch>, usize), EpcError> {
685 let mut branches = Vec::new();
686 let mut pos = start;
687
688 while pos < lines.len() {
689 if detect_section(&lines[pos].text).is_some() {
690 return Ok((branches, pos));
691 }
692
693 let line_num = lines[pos].line_num;
694 let tokens = tokenize_epc(&lines[pos].text);
695 if tokens.is_empty() {
696 pos += 1;
697 continue;
698 }
699
700 let (ident, data) = split_on_colon(&tokens);
701 if ident.len() < 7 || data.len() < 6 {
702 pos += 1;
703 continue;
704 }
705
706 let from_bus = parse_u32(tok(&ident, 0), line_num, "from_bus")?;
709 let to_bus = parse_u32(tok(&ident, 3), line_num, "to_bus")?;
710 let ck_str = ident.get(6).cloned().unwrap_or_default();
711 let ck = ck_str.trim().parse::<u32>().unwrap_or(1);
712 let se = if ident.len() > 7 {
713 parse_u32(tok(&ident, 7), line_num, "se").unwrap_or(1)
714 } else {
715 1
716 };
717
718 let circuit = if se > 1 {
720 format!("{}", ck * 100 + se)
721 } else {
722 ck.to_string()
723 };
724
725 let st = parse_i32(tok(&data, 0), line_num, "st")?;
728 let r = parse_f64(tok(&data, 1), line_num, "resist")?;
729 let x = parse_f64(tok(&data, 2), line_num, "react")?;
730 let b = parse_f64(tok(&data, 3), line_num, "charge")?;
731 let rate_a = parse_f64(tok(&data, 4), line_num, "rate1")?;
732 let rate_b = parse_f64(tok(&data, 5), line_num, "rate2").unwrap_or(0.0);
733 let rate_c = parse_f64(tok(&data, 6), line_num, "rate3").unwrap_or(0.0);
734
735 let in_service = st == 1;
736
737 let mut branch = Branch::new_line(from_bus, to_bus, r, x, b);
738 branch.circuit = circuit;
739 branch.rating_a_mva = rate_a;
740 branch.rating_b_mva = rate_b;
741 branch.rating_c_mva = rate_c;
742 branch.in_service = in_service;
743
744 branches.push(branch);
745 pos += 1;
746 }
747
748 Ok((branches, pos))
749}
750
751fn parse_transformer_section(
767 lines: &[LogicalLine],
768 start: usize,
769 buses: &[Bus],
770 base_mva: f64,
771) -> Result<(Vec<Branch>, usize), EpcError> {
772 let mut branches = Vec::new();
773 let mut pos = start;
774
775 let bus_basekv: HashMap<u32, f64> = buses.iter().map(|b| (b.number, b.base_kv)).collect();
777
778 while pos < lines.len() {
779 if detect_section(&lines[pos].text).is_some() {
780 return Ok((branches, pos));
781 }
782
783 let line_num = lines[pos].line_num;
784 let tokens = tokenize_epc(&lines[pos].text);
785 if tokens.is_empty() {
786 pos += 1;
787 continue;
788 }
789
790 let (ident, data) = split_on_colon(&tokens);
791 if ident.len() < 7 || data.len() < 20 {
792 pos += 1;
793 continue;
794 }
795
796 let from_bus = parse_u32(tok(&ident, 0), line_num, "from_bus")?;
799 let to_bus = parse_u32(tok(&ident, 3), line_num, "to_bus")?;
800 let ck_str = ident.get(6).cloned().unwrap_or_default();
801 let ck = ck_str.trim().parse::<u32>().unwrap_or(1);
802
803 let st = parse_i32(tok(&data, 0), line_num, "st")?;
809 let _ty = parse_i32(tok(&data, 1), line_num, "ty")?;
810
811 let mut tbase_idx = None;
831 for i in 10..data.len().saturating_sub(6) {
832 let s = tok(&data, i);
834 if (s.contains("E-") || s.contains("E+") || s.contains("e-") || s.contains("e+"))
835 && i > 0
836 {
837 tbase_idx = Some(i - 1);
839 break;
840 }
841 }
842
843 if tbase_idx.is_none() {
845 for i in 10..data.len().saturating_sub(4) {
847 let val = parse_f64(tok(&data, i), line_num, "tbase_scan").unwrap_or(0.0);
848 if (val - 100.0).abs() < 1.0 || val > 50.0 {
849 let a = parse_u32(tok(&data, i.wrapping_sub(2)), line_num, "area_scan")
851 .unwrap_or(999);
852 let z = parse_u32(tok(&data, i.wrapping_sub(1)), line_num, "zone_scan")
853 .unwrap_or(999);
854 if a < 100 && z < 100 {
855 tbase_idx = Some(i);
856 break;
857 }
858 }
859 }
860 }
861
862 let (tbase, ps_r, ps_x) = if let Some(ti) = tbase_idx {
863 let tbase = parse_f64(tok(&data, ti), line_num, "tbase")?;
864 let ps_r = parse_f64(tok(&data, ti + 1), line_num, "ps_r")?;
865 let ps_x = parse_f64(tok(&data, ti + 2), line_num, "ps_x")?;
866 (tbase, ps_r, ps_x)
867 } else {
868 pos += 1;
870 continue;
871 };
872
873 let cont_start = tbase_idx.expect("tbase_idx guaranteed Some by prior branch") + 7; let kv_primary = if data.len() > cont_start {
878 parse_f64(tok(&data, cont_start), line_num, "kv_primary")?
879 } else {
880 0.0
881 };
882 let kv_secondary = if data.len() > cont_start + 1 {
883 parse_f64(tok(&data, cont_start + 1), line_num, "kv_secondary")?
884 } else {
885 0.0
886 };
887
888 let rate_a = if data.len() > cont_start + 6 {
890 parse_f64(tok(&data, cont_start + 6), line_num, "rate1").unwrap_or(0.0)
891 } else {
892 0.0
893 };
894 let rate_b = if data.len() > cont_start + 7 {
895 parse_f64(tok(&data, cont_start + 7), line_num, "rate2").unwrap_or(0.0)
896 } else {
897 0.0
898 };
899 let rate_c = if data.len() > cont_start + 8 {
900 parse_f64(tok(&data, cont_start + 8), line_num, "rate3").unwrap_or(0.0)
901 } else {
902 0.0
903 };
904
905 let r = if tbase > 0.0 && (tbase - base_mva).abs() > 1e-6 {
907 ps_r * base_mva / tbase
908 } else {
909 ps_r
910 };
911 let x = if tbase > 0.0 && (tbase - base_mva).abs() > 1e-6 {
912 ps_x * base_mva / tbase
913 } else {
914 ps_x
915 };
916
917 let from_basekv = bus_basekv.get(&from_bus).copied().unwrap_or(kv_primary);
919 let to_basekv = bus_basekv.get(&to_bus).copied().unwrap_or(kv_secondary);
920
921 let tap = if kv_primary > 0.0 && kv_secondary > 0.0 && from_basekv > 0.0 && to_basekv > 0.0
922 {
923 (kv_primary / from_basekv) / (kv_secondary / to_basekv)
924 } else {
925 1.0
926 };
927
928 let in_service = st == 1;
929
930 let mut branch = Branch::new_line(from_bus, to_bus, r, x, 0.0);
931 branch.circuit = ck.to_string();
932 branch.tap = tap;
933 branch.rating_a_mva = rate_a;
934 branch.rating_b_mva = rate_b;
935 branch.rating_c_mva = rate_c;
936 branch.in_service = in_service;
937 branch.branch_type = BranchType::Transformer;
938
939 branches.push(branch);
940 pos += 1;
941 }
942
943 Ok((branches, pos))
944}
945
946fn parse_generator_section(
961 lines: &[LogicalLine],
962 start: usize,
963 bus_vsched: &HashMap<u32, f64>,
964) -> Result<(Vec<Generator>, usize), EpcError> {
965 let mut generators = Vec::new();
966 let mut pos = start;
967
968 while pos < lines.len() {
969 if detect_section(&lines[pos].text).is_some() {
970 return Ok((generators, pos));
971 }
972
973 let line_num = lines[pos].line_num;
974 let tokens = tokenize_epc(&lines[pos].text);
975 if tokens.is_empty() {
976 pos += 1;
977 continue;
978 }
979
980 let (ident, data) = split_on_colon(&tokens);
981 if ident.len() < 4 || data.len() < 18 {
982 pos += 1;
983 continue;
984 }
985
986 let bus = parse_u32(tok(&ident, 0), line_num, "gen_bus")?;
988 let gen_id = ident.get(3).cloned().unwrap_or_default();
989
990 let st = parse_i32(tok(&data, 0), line_num, "st")?;
997
998 let pgen = parse_f64(tok(&data, 8), line_num, "pgen")?;
1004 let pmax = parse_f64(tok(&data, 9), line_num, "pmax")?;
1005 let pmin = parse_f64(tok(&data, 10), line_num, "pmin")?;
1006 let qgen = parse_f64(tok(&data, 11), line_num, "qgen")?;
1007 let qmax = parse_f64(tok(&data, 12), line_num, "qmax")?;
1008 let qmin = parse_f64(tok(&data, 13), line_num, "qmin")?;
1009 let mbase = parse_f64(tok(&data, 14), line_num, "mbase")?;
1010
1011 let in_service = st == 1;
1012
1013 let vs = bus_vsched.get(&bus).copied().unwrap_or(1.0);
1015
1016 let mut generator = Generator::new(bus, pgen, vs);
1017 generator.machine_id = Some(gen_id.trim().to_string());
1018 generator.q = qgen;
1019 generator.qmax = qmax;
1020 generator.qmin = qmin;
1021 generator.pmax = pmax;
1022 generator.pmin = pmin;
1023 generator.machine_base_mva = if mbase > 0.0 { mbase } else { 100.0 };
1024 generator.in_service = in_service;
1025
1026 generators.push(generator);
1027 pos += 1;
1028 }
1029
1030 Ok((generators, pos))
1031}
1032
1033fn parse_load_section(
1045 lines: &[LogicalLine],
1046 start: usize,
1047) -> Result<(Vec<RawLoad>, usize), EpcError> {
1048 let mut loads = Vec::new();
1049 let mut pos = start;
1050
1051 while pos < lines.len() {
1052 if detect_section(&lines[pos].text).is_some() {
1053 return Ok((loads, pos));
1054 }
1055
1056 let line_num = lines[pos].line_num;
1057 let tokens = tokenize_epc(&lines[pos].text);
1058 if tokens.is_empty() {
1059 pos += 1;
1060 continue;
1061 }
1062
1063 let (ident, data) = split_on_colon(&tokens);
1064 if ident.len() < 3 || data.len() < 3 {
1065 pos += 1;
1066 continue;
1067 }
1068
1069 let bus = parse_u32(tok(&ident, 0), line_num, "load_bus")?;
1070
1071 let st = parse_i32(tok(&data, 0), line_num, "st")?;
1073 let pl = parse_f64(tok(&data, 1), line_num, "mw")?;
1074 let ql = parse_f64(tok(&data, 2), line_num, "mvar")?;
1075
1076 loads.push(RawLoad {
1077 bus,
1078 id: String::new(),
1079 status: st,
1080 owner: None,
1081 pl,
1082 ql,
1083 conforming: true,
1084 zip_p_impedance_frac: 0.0,
1085 zip_p_current_frac: 0.0,
1086 zip_p_power_frac: 1.0,
1087 zip_q_impedance_frac: 0.0,
1088 zip_q_current_frac: 0.0,
1089 zip_q_power_frac: 1.0,
1090 });
1091 pos += 1;
1092 }
1093
1094 Ok((loads, pos))
1095}
1096
1097fn parse_shunt_section(
1106 lines: &[LogicalLine],
1107 start: usize,
1108) -> Result<(Vec<RawShunt>, usize), EpcError> {
1109 let mut shunts = Vec::new();
1110 let mut pos = start;
1111
1112 while pos < lines.len() {
1113 if detect_section(&lines[pos].text).is_some() {
1114 return Ok((shunts, pos));
1115 }
1116
1117 let line_num = lines[pos].line_num;
1118 let tokens = tokenize_epc(&lines[pos].text);
1119 if tokens.is_empty() {
1120 pos += 1;
1121 continue;
1122 }
1123
1124 let (ident, data) = split_on_colon(&tokens);
1125 if ident.len() < 3 || data.len() < 5 {
1126 pos += 1;
1127 continue;
1128 }
1129
1130 let bus = parse_u32(tok(&ident, 0), line_num, "shunt_bus")?;
1131
1132 let st = parse_i32(tok(&data, 0), line_num, "st")?;
1134 let gl = parse_f64(tok(&data, 3), line_num, "pu_mw")?;
1135 let bl = parse_f64(tok(&data, 4), line_num, "pu_mvar")?;
1136
1137 shunts.push(RawShunt {
1138 bus,
1139 status: st,
1140 gl,
1141 bl,
1142 });
1143 pos += 1;
1144 }
1145
1146 Ok((shunts, pos))
1147}
1148
1149fn parse_svd_section(
1162 lines: &[LogicalLine],
1163 start: usize,
1164 base_mva: f64,
1165) -> Result<(Vec<RawShunt>, usize), EpcError> {
1166 let mut shunts = Vec::new();
1167 let mut pos = start;
1168
1169 while pos < lines.len() {
1170 if detect_section(&lines[pos].text).is_some() {
1171 return Ok((shunts, pos));
1172 }
1173
1174 let line_num = lines[pos].line_num;
1175 let tokens = tokenize_epc(&lines[pos].text);
1176 if tokens.is_empty() {
1177 pos += 1;
1178 continue;
1179 }
1180
1181 let (ident, data) = split_on_colon(&tokens);
1182 if ident.len() < 3 || data.len() < 8 {
1183 pos += 1;
1184 continue;
1185 }
1186
1187 let bus = parse_u32(tok(&ident, 0), line_num, "svd_bus")?;
1188
1189 let st = parse_i32(tok(&data, 0), line_num, "st")?;
1193 let g = parse_f64(tok(&data, 7), line_num, "g").unwrap_or(0.0);
1196 let b = parse_f64(tok(&data, 8), line_num, "b").unwrap_or(0.0);
1197
1198 shunts.push(RawShunt {
1203 bus,
1204 status: st,
1205 gl: g * base_mva,
1206 bl: b * base_mva,
1207 });
1208 pos += 1;
1209 }
1210
1211 Ok((shunts, pos))
1212}
1213
1214fn parse_area_section(lines: &[LogicalLine], start: usize) -> (Vec<AreaSchedule>, usize) {
1223 let mut areas = Vec::new();
1224 let mut pos = start;
1225
1226 while pos < lines.len() {
1227 if detect_section(&lines[pos].text).is_some() {
1228 return (areas, pos);
1229 }
1230
1231 let tokens = tokenize_epc(&lines[pos].text);
1232 if tokens.len() < 2 {
1233 pos += 1;
1234 continue;
1235 }
1236
1237 let number = tokens[0].parse::<u32>().unwrap_or(0);
1238 let name = tokens.get(1).cloned().unwrap_or_default();
1239
1240 if number > 0 {
1241 areas.push(AreaSchedule {
1242 number,
1243 name,
1244 ..Default::default()
1245 });
1246 }
1247 pos += 1;
1248 }
1249
1250 (areas, pos)
1251}
1252
1253fn fixup_bus_types(network: &mut Network) {
1263 let gen_bus_set: HashMap<u32, bool> = network
1264 .generators
1265 .iter()
1266 .filter(|g| g.in_service)
1267 .map(|g| {
1268 let avr_on = (g.qmax - g.qmin).abs() > 2.0;
1269 (g.bus, avr_on)
1270 })
1271 .collect();
1272
1273 for bus in &mut network.buses {
1274 if bus.bus_type == BusType::Isolated {
1275 continue;
1276 }
1277 if let Some(&avr_on) = gen_bus_set.get(&bus.number)
1278 && bus.bus_type == BusType::PQ
1279 && avr_on
1280 {
1281 bus.bus_type = BusType::PV;
1282 }
1283 }
1284
1285 let has_slack = network.buses.iter().any(|b| b.bus_type == BusType::Slack);
1287 if !has_slack {
1288 if let Some(largest_gen) =
1290 network
1291 .generators
1292 .iter()
1293 .filter(|g| g.in_service)
1294 .max_by(|a, b| {
1295 a.pmax
1296 .partial_cmp(&b.pmax)
1297 .unwrap_or(std::cmp::Ordering::Equal)
1298 })
1299 {
1300 let slack_bus = largest_gen.bus;
1301 for bus in &mut network.buses {
1302 if bus.number == slack_bus {
1303 bus.bus_type = BusType::Slack;
1304 break;
1305 }
1306 }
1307 }
1308 }
1309}
1310
1311fn fixup_voltage_limits(network: &mut Network) {
1313 for bus in &mut network.buses {
1314 if bus.voltage_max_pu > 10.0 || bus.voltage_min_pu > 10.0 {
1315 bus.voltage_max_pu = 1.1;
1316 bus.voltage_min_pu = 0.9;
1317 }
1318 if bus.voltage_max_pu <= 0.0 {
1319 bus.voltage_max_pu = 1.1;
1320 }
1321 if bus.voltage_min_pu <= 0.0 {
1322 bus.voltage_min_pu = 0.9;
1323 }
1324 }
1325}
1326
1327fn fixup_latlon(network: &mut Network) {
1329 for bus in &mut network.buses {
1330 if let (Some(lat), Some(lon)) = (bus.latitude, bus.longitude)
1331 && lat.abs() < 1e-10
1332 && lon.abs() < 1e-10
1333 {
1334 bus.latitude = None;
1335 bus.longitude = None;
1336 }
1337 }
1338}
1339
1340#[cfg(test)]
1345mod tests {
1346 use super::*;
1347
1348 #[test]
1349 fn test_tokenize_epc_basic() {
1350 let tokens = tokenize_epc(" 1001 115.0000 0.005240 0.035800 ");
1351 assert_eq!(tokens, vec!["1001", "115.0000", "0.005240", "0.035800"]);
1352 }
1353
1354 #[test]
1355 fn test_tokenize_epc_quoted() {
1356 let tokens = tokenize_epc(r#" 1001 "ODESSA 2 0 " 115.0000 " " 0 : "#);
1357 assert_eq!(tokens[0], "1001");
1358 assert_eq!(tokens[1], "ODESSA 2 0 ");
1359 assert_eq!(tokens[2], "115.0000");
1360 assert_eq!(tokens[3], " ");
1361 assert_eq!(tokens[4], "0");
1362 assert_eq!(tokens[5], ":");
1363 }
1364
1365 #[test]
1366 fn test_tokenize_epc_empty() {
1367 assert!(tokenize_epc("").is_empty());
1368 assert!(tokenize_epc(" ").is_empty());
1369 }
1370
1371 #[test]
1372 fn test_detect_section() {
1373 assert_eq!(
1374 detect_section("bus data [2751] ty vsched"),
1375 Some(EpcSection::BusData)
1376 );
1377 assert_eq!(
1378 detect_section("branch data [ 3993] ck se"),
1379 Some(EpcSection::BranchData)
1380 );
1381 assert_eq!(
1382 detect_section("transformer data [1351]"),
1383 Some(EpcSection::TransformerData)
1384 );
1385 assert_eq!(
1386 detect_section("generator data [1099]"),
1387 Some(EpcSection::GeneratorData)
1388 );
1389 assert_eq!(
1390 detect_section("load data [1410]"),
1391 Some(EpcSection::LoadData)
1392 );
1393 assert_eq!(
1394 detect_section("shunt data [ 0]"),
1395 Some(EpcSection::ShuntData)
1396 );
1397 assert_eq!(
1398 detect_section("svd data [ 202]"),
1399 Some(EpcSection::SvdData)
1400 );
1401 assert_eq!(
1402 detect_section("area data [ 8]"),
1403 Some(EpcSection::AreaData)
1404 );
1405 assert_eq!(
1406 detect_section("zone data [ 1]"),
1407 Some(EpcSection::ZoneData)
1408 );
1409 assert_eq!(detect_section("end"), Some(EpcSection::End));
1410 assert_eq!(detect_section("title"), Some(EpcSection::Title));
1411 assert_eq!(
1412 detect_section("solution parameters"),
1413 Some(EpcSection::SolutionParameters)
1414 );
1415 assert_eq!(detect_section(" not a section"), None);
1416 }
1417
1418 #[test]
1419 fn test_parse_section_count() {
1420 assert_eq!(
1421 parse_section_count("bus data [2751] ty vsched"),
1422 Some(2751)
1423 );
1424 assert_eq!(parse_section_count("shunt data [ 0]"), Some(0));
1425 assert_eq!(parse_section_count("branch data [ 3993]"), Some(3993));
1426 assert_eq!(parse_section_count("no brackets here"), None);
1427 }
1428
1429 #[test]
1430 fn test_split_on_colon() {
1431 let tokens = tokenize_epc("1001 \"name\" 115.00 : 1 0.005 0.035");
1432 let (before, after) = split_on_colon(&tokens);
1433 assert_eq!(before.len(), 3);
1434 assert_eq!(after.len(), 3);
1435 assert_eq!(before[0], "1001");
1436 assert_eq!(after[0], "1");
1437 }
1438
1439 #[test]
1440 fn test_preprocess_continuation() {
1441 let raw = vec!["first line /", " second line", "third line"];
1442 let lines = preprocess_lines(&raw);
1443 assert_eq!(lines.len(), 2);
1444 assert!(lines[0].text.contains("first line"));
1445 assert!(lines[0].text.contains("second line"));
1446 assert_eq!(lines[1].text, "third line");
1447 }
1448
1449 #[test]
1450 fn test_preprocess_annotations_skip() {
1451 let raw = vec!["@! this is an annotation", "real data line"];
1452 let lines = preprocess_lines(&raw);
1453 assert_eq!(lines.len(), 1);
1454 assert_eq!(lines[0].text, "real data line");
1455 }
1456
1457 #[test]
1458 fn test_parse_f64_edge_cases() {
1459 assert_eq!(parse_f64("", 1, "test").unwrap(), 0.0);
1460 assert_eq!(parse_f64("3.15", 1, "test").unwrap(), 3.15);
1461 assert_eq!(parse_f64("1.000000E-04", 1, "test").unwrap(), 1e-4);
1462 assert_eq!(parse_f64("-17.299999", 1, "test").unwrap(), -17.299999);
1463 assert!(parse_f64("abc", 1, "test").is_err());
1464 }
1465
1466 #[test]
1467 fn test_parse_mini_epc() {
1468 let epc = r#"title
1469!
1470comments
1471!
1472solution parameters
1473sbase 100.0000 system mva base
1474!
1475bus data [3] ty vsched volt angle ar zone vmax vmin date_in date_out pid L own st
1476 1 "BUS 1 " 345.0000 " " 0 : 3 1.060000 1.060000 0.000000 1 1 1.1000 0.9000 19400101 21991231 0 0 1 0
1477 2 "BUS 2 " 345.0000 " " 0 : 2 1.045000 1.045000 -10.000000 1 1 1.1000 0.9000 19400101 21991231 0 0 1 0
1478 3 "BUS 3 " 345.0000 " " 0 : 1 1.000000 1.007000 -15.000000 1 1 1.1000 0.9000 19400101 21991231 0 0 1 0
1479branch data [ 1] ck se long_id st resist react charge rate1 rate2 rate3 rate4 aloss lngth
1480 1 "BUS 1 " 345.00 3 "BUS 3 " 345.00 "1 " 1 " " : 1 0.010000 0.100000 0.020000 250.0 0.0 0.0 0.0 0.000 0.0 1 1 0.000000 0.000000 1.000000 19400101 21991231 0 1
1481transformer data [1] ck long_id st ty
1482 1 "BUS 1 " 345.00 2 "BUS 2 " 345.00 "1 " " " : 1 1 0 " " 0.00 0 0 " " 0.00 0 " " 0.00 1 1 100.000000 5.000000E-03 5.000000E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 345.000000 345.000000 0.000000 0.000000 0.000000E+00 0.000000E+00 200.0 200.0 200.0 0.0 0.000 1.500000 0.510000 1.500000 0.510000 -0.006250 1.000000 1.000000 1.000000 1.000000 19400101 21991231 0 1 0.0 0.0 0.0 0.0 1 1.000 0 0.000 0 0.000 0 0.000 0 0.000 0 0.000 0 0.000 0 0.000 0 0.000000 0.000000 0.000000 0.000000 0.0 0.0 0.0 0.0 0.0 0.0 0.000 0.000 1 1 1 0.0000 0.0000 0 0 0 0.000000 0.000000 0.000000 0.000000 " "
1483generator data [1] id long_id st
1484 1 "BUS 1 " 345.00 "1 " " " : 1 1 "BUS 1 " 345.00 1.000000 1.000000 1 1 100.000000 200.000000 0.000000 10.000000 50.000000 -30.000000 200.0000 0.000 0.000 0.000 1.000 -1 " " 0.00 -1 " " 0.00 19400101 21991231 0 0 0.0000 0.0000 1.0000 1 1.000 0 0.000 0 0.000 0 0.000 " "
1485load data [1] id long_id st mw mvar
1486 3 "BUS 3 " 345.00 "1 " " " : 1 150.000000 50.000000 0.000000 0.000000 0.000000 0.000000 1 1 19400101 21991231 0 0 1 0
1487area data [ 1]
1488 1 "Area 1 " 0 0.000 1.000 0.0 0.0 0 0 0 " " 0.000000
1489zone data [ 1]
1490 1 "Zone 1 " 0.000 0.000 0
1491end
1492"#;
1493
1494 let net = parse_str(epc).expect("failed to parse mini EPC");
1495 assert_eq!(net.n_buses(), 3, "expected 3 buses");
1496 assert_eq!(
1497 net.branches.len(),
1498 2,
1499 "expected 2 branches (1 line + 1 xfmr)"
1500 );
1501 assert_eq!(net.generators.len(), 1, "expected 1 generator");
1502
1503 assert_eq!(net.buses[0].bus_type, BusType::Slack);
1505 assert_eq!(net.buses[1].bus_type, BusType::PV);
1506 assert_eq!(net.buses[2].bus_type, BusType::PQ);
1507
1508 let bus_pd = net.bus_load_p_mw();
1510 let bus_qd = net.bus_load_q_mvar();
1511 assert!((bus_pd[2] - 150.0).abs() < 0.01);
1512 assert!((bus_qd[2] - 50.0).abs() < 0.01);
1513
1514 assert_eq!(net.generators[0].bus, 1);
1516 assert!((net.generators[0].p - 100.0).abs() < 0.01);
1517 assert!((net.generators[0].pmax - 200.0).abs() < 0.01);
1518
1519 let line = &net.branches[0];
1521 assert!((line.r - 0.01).abs() < 1e-6);
1522 assert!((line.x - 0.1).abs() < 1e-6);
1523 assert!((line.b - 0.02).abs() < 1e-6);
1524
1525 let xfmr = &net.branches[1];
1527 assert!((xfmr.r - 0.005).abs() < 1e-6);
1528 assert!((xfmr.x - 0.05).abs() < 1e-6);
1529 assert!((xfmr.tap - 1.0).abs() < 1e-6); assert_eq!(net.area_schedules.len(), 1);
1533 }
1534
1535 #[test]
1536 fn test_parse_texas2k_epc() {
1537 let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
1539 let path = std::path::PathBuf::from(&manifest)
1540 .join("../..")
1541 .join("tests/data/epc/Texas2k.epc");
1542 if !path.exists() {
1543 return; }
1545
1546 let net = parse_file(&path).expect("failed to parse Texas2k EPC");
1547
1548 assert_eq!(net.n_buses(), 2751, "expected 2751 buses");
1550 assert!(
1552 net.branches.len() > 4000,
1553 "expected >4000 branches, got {}",
1554 net.branches.len()
1555 );
1556 assert!(
1558 net.generators.len() > 1000,
1559 "expected >1000 generators, got {}",
1560 net.generators.len()
1561 );
1562
1563 assert!((net.base_mva - 100.0).abs() < 0.01);
1565
1566 let slack_count = net
1568 .buses
1569 .iter()
1570 .filter(|b| b.bus_type == BusType::Slack)
1571 .count();
1572 assert!(slack_count >= 1, "no slack bus found");
1573
1574 let total_load: f64 = net.total_load_mw();
1576 assert!(
1577 total_load > 10000.0,
1578 "total load too low: {total_load:.0} MW"
1579 );
1580
1581 assert_eq!(net.area_schedules.len(), 8, "expected 8 areas");
1583 }
1584
1585 #[test]
1586 fn test_parse_texas7k_epc() {
1587 let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
1589 let path = std::path::PathBuf::from(&manifest)
1590 .join("../..")
1591 .join("tests/data/epc/Texas7k.epc");
1592 if !path.exists() {
1593 return; }
1595
1596 let net = parse_file(&path).expect("failed to parse Texas7k EPC");
1597
1598 assert!(
1600 net.n_buses() > 6000,
1601 "expected >6000 buses, got {}",
1602 net.n_buses()
1603 );
1604 assert!(
1605 net.branches.len() > 7000,
1606 "expected >7000 branches, got {}",
1607 net.branches.len()
1608 );
1609 assert!(
1610 net.generators.len() > 500,
1611 "expected >500 generators, got {}",
1612 net.generators.len()
1613 );
1614
1615 assert!((net.base_mva - 100.0).abs() < 0.01);
1617
1618 let slack_count = net
1620 .buses
1621 .iter()
1622 .filter(|b| b.bus_type == BusType::Slack)
1623 .count();
1624 assert!(slack_count >= 1, "no slack bus found");
1625
1626 let total_load: f64 = net.total_load_mw();
1628 assert!(
1629 total_load > 10000.0,
1630 "total load too low: {total_load:.0} MW"
1631 );
1632
1633 eprintln!(
1634 "Texas7k: {} buses, {} branches, {} gens, {:.0} MW load",
1635 net.n_buses(),
1636 net.branches.len(),
1637 net.generators.len(),
1638 total_load
1639 );
1640 }
1641
1642 #[test]
1643 fn test_epc_vs_matpower_texas7k() {
1644 let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
1646 let epc_path = std::path::PathBuf::from(&manifest)
1647 .join("../..")
1648 .join("tests/data/epc/Texas7k.epc");
1649 let mat_path = std::path::PathBuf::from(&manifest)
1650 .join("../..")
1651 .join("tests/data/epc/Texas7k.m");
1652
1653 if !epc_path.exists() || !mat_path.exists() {
1654 return; }
1656
1657 let epc_net = parse_file(&epc_path).expect("EPC parse failed");
1658 let mat_net = crate::matpower::parse_file(&mat_path).expect("MATPOWER parse failed");
1659
1660 assert_eq!(
1662 epc_net.n_buses(),
1663 mat_net.n_buses(),
1664 "bus count mismatch: EPC={} vs MATPOWER={}",
1665 epc_net.n_buses(),
1666 mat_net.n_buses()
1667 );
1668
1669 let epc_load: f64 = epc_net.total_load_mw();
1671 let mat_load: f64 = mat_net.total_load_mw();
1672 let load_diff = (epc_load - mat_load).abs();
1673 let load_pct = load_diff / mat_load.abs().max(1.0) * 100.0;
1674 eprintln!(
1675 "Texas7k load: EPC={epc_load:.1} MW, MATPOWER={mat_load:.1} MW, diff={load_pct:.2}%"
1676 );
1677 assert!(load_pct < 1.0, "load mismatch too large: {load_pct:.2}%");
1678
1679 let epc_gens = epc_net.generators.len();
1681 let mat_gens = mat_net.generators.len();
1682 eprintln!("Texas7k gens: EPC={epc_gens}, MATPOWER={mat_gens}");
1683 }
1684
1685 #[test]
1686 fn test_epc_vs_matpower_texas2k() {
1687 let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
1689 let epc_path = std::path::PathBuf::from(&manifest)
1690 .join("../..")
1691 .join("tests/data/epc/Texas2k.epc");
1692 let mat_path = std::path::PathBuf::from(&manifest)
1693 .join("../..")
1694 .join("tests/data/epc/Texas2k.m");
1695
1696 if !epc_path.exists() || !mat_path.exists() {
1697 return; }
1699
1700 let epc_net = parse_file(&epc_path).expect("EPC parse failed");
1701 let mat_net = crate::matpower::parse_file(&mat_path).expect("MATPOWER parse failed");
1702
1703 assert_eq!(
1705 epc_net.n_buses(),
1706 mat_net.n_buses(),
1707 "bus count mismatch: EPC={} vs MATPOWER={}",
1708 epc_net.n_buses(),
1709 mat_net.n_buses()
1710 );
1711
1712 let epc_branches = epc_net.branches.len();
1714 let mat_branches = mat_net.branches.len();
1715 let branch_diff = (epc_branches as i64 - mat_branches as i64).unsigned_abs();
1716 eprintln!("Branches: EPC={epc_branches}, MATPOWER={mat_branches}, diff={branch_diff}");
1717
1718 let epc_gens = epc_net.generators.len();
1720 let mat_gens = mat_net.generators.len();
1721 eprintln!("Generators: EPC={epc_gens}, MATPOWER={mat_gens}");
1722
1723 let epc_load: f64 = epc_net.total_load_mw();
1725 let mat_load: f64 = mat_net.total_load_mw();
1726 let load_diff = (epc_load - mat_load).abs();
1727 eprintln!(
1728 "Total load: EPC={epc_load:.1} MW, MATPOWER={mat_load:.1} MW, diff={load_diff:.1} MW"
1729 );
1730
1731 let load_pct = load_diff / mat_load.abs().max(1.0) * 100.0;
1733 assert!(load_pct < 1.0, "load mismatch too large: {load_pct:.2}%");
1734 }
1735}