1use std::path::Path;
18
19use surge_network::Network;
20use surge_network::market::CostCurve;
21use surge_network::network::{
22 Branch, BranchType, Bus, BusType, DcBranch, DcBus, DcConverter, DcConverterStation, FuelParams,
23 GenType, Generator, GeneratorTechnology, Load,
24};
25use thiserror::Error;
26
27fn bus_type_from_matpower(code: u32) -> BusType {
29 match code {
30 2 => BusType::PV,
31 3 => BusType::Slack,
32 4 => BusType::Isolated,
33 _ => BusType::PQ,
34 }
35}
36
37#[derive(Error, Debug)]
38pub enum MatpowerError {
39 #[error("I/O error: {0}")]
40 Io(#[from] std::io::Error),
41
42 #[error("parse error on line {line}: {message}")]
43 Parse { line: usize, message: String },
44
45 #[error("missing required section: {0}")]
46 MissingSection(String),
47
48 #[error("insufficient columns in {section} row {row}: expected {expected}, got {got}")]
49 InsufficientColumns {
50 section: String,
51 row: usize,
52 expected: usize,
53 got: usize,
54 },
55
56 #[error("invalid gencost: {0}")]
57 InvalidGencost(String),
58
59 #[error("invalid float value: {0}")]
60 InvalidFloat(String),
61
62 #[error("non-finite value in {section} row {row}: {field} = {value}")]
63 NonFiniteValue {
64 section: &'static str,
65 row: usize,
66 field: &'static str,
67 value: f64,
68 },
69
70 #[error("expression nesting too deep: {0}")]
71 ExpressionTooDeep(String),
72
73 #[error("file too large: {0} bytes exceeds limit of {1} bytes")]
74 FileTooLarge(u64, u64),
75}
76
77pub fn parse_file(path: &Path) -> Result<Network, MatpowerError> {
79 const MAX_FILE_SIZE: u64 = 500 * 1024 * 1024; let metadata = std::fs::metadata(path)?;
81 if metadata.len() > MAX_FILE_SIZE {
82 return Err(MatpowerError::FileTooLarge(metadata.len(), MAX_FILE_SIZE));
83 }
84 let content = std::fs::read_to_string(path)?;
85 parse_str(&content)
86}
87
88pub fn parse_str(content: &str) -> Result<Network, MatpowerError> {
90 let mut name = String::from("unknown");
91 let mut base_mva = 100.0;
92 let mut bus_rows: Vec<Vec<f64>> = Vec::new();
93 let mut gen_rows: Vec<Vec<f64>> = Vec::new();
94 let mut branch_rows: Vec<Vec<f64>> = Vec::new();
95 let mut gencost_rows: Vec<Vec<f64>> = Vec::new();
96 let mut bus_name_rows: Vec<String> = Vec::new();
97 let mut gentype_rows: Vec<String> = Vec::new();
98 let mut genfuel_rows: Vec<String> = Vec::new();
99 let mut dc_bus_rows: Vec<Vec<f64>> = Vec::new();
100 let mut dc_conv_rows: Vec<Vec<f64>> = Vec::new();
101 let mut dc_branch_rows: Vec<Vec<f64>> = Vec::new();
102
103 #[derive(PartialEq)]
104 enum Section {
105 None,
106 Bus,
107 Gen,
108 Branch,
109 GenCost,
110 BusName,
111 GenType,
112 GenFuel,
113 DcBus,
114 DcConv,
115 DcBranch,
116 Other,
117 }
118 let mut section = Section::None;
119
120 for (line_idx, raw_line) in content.lines().enumerate() {
121 let line_num = line_idx + 1;
122
123 let line = match raw_line.find('%') {
127 Some(idx) => &raw_line[..idx],
128 None => raw_line,
129 };
130 let line = line.trim();
131
132 if line.is_empty() {
133 continue;
134 }
135
136 if line.contains("};")
138 && matches!(
139 section,
140 Section::BusName | Section::GenType | Section::GenFuel
141 )
142 {
143 section = Section::None;
144 continue;
145 }
146
147 let is_bracket_end = line.contains("];");
149 let is_brace_end = line.contains("};");
150 if is_bracket_end || is_brace_end {
151 if section == Section::Bus
153 || section == Section::Gen
154 || section == Section::Branch
155 || section == Section::GenCost
156 || section == Section::DcBus
157 || section == Section::DcConv
158 || section == Section::DcBranch
159 {
160 let delim = if is_bracket_end { "];" } else { "};" };
161 let data_part = line.split(delim).next().unwrap_or("").trim();
162 let data_part = data_part
163 .trim_end_matches(';')
164 .trim_end_matches(']')
165 .trim_end_matches('}')
166 .trim();
167 if !data_part.is_empty()
168 && let Some(row) = parse_numeric_row(data_part)
169 {
170 match section {
171 Section::Bus => bus_rows.push(row),
172 Section::Gen => gen_rows.push(row),
173 Section::Branch => branch_rows.push(row),
174 Section::GenCost => gencost_rows.push(row),
175 Section::DcBus => dc_bus_rows.push(row),
176 Section::DcConv => dc_conv_rows.push(row),
177 Section::DcBranch => dc_branch_rows.push(row),
178 _ => {}
179 }
180 }
181 }
182 section = Section::None;
183 continue;
184 }
185
186 if line.starts_with("function") {
188 if let Some(eq_pos) = line.find('=') {
189 name = line[eq_pos + 1..]
190 .trim()
191 .trim_end_matches(';')
192 .trim()
193 .to_string();
194 }
195 continue;
196 }
197
198 if line.contains("baseMVA") && line.contains('=') {
201 if let Some(eq_pos) = line.find('=') {
202 let lhs = line[..eq_pos].trim();
203 if lhs == "mpc.baseMVA" || lhs == "baseMVA" {
204 let val_str = line[eq_pos + 1..].trim().trim_end_matches(';').trim();
205 base_mva = eval_simple_expr(val_str).ok_or_else(|| MatpowerError::Parse {
206 line: line_num,
207 message: format!("invalid baseMVA value: '{val_str}'"),
208 })?;
209 }
210 }
211 continue;
212 }
213
214 if is_section_start(line, "bus") {
216 section = Section::Bus;
217 try_parse_inline_data(line, &mut bus_rows);
218 continue;
219 }
220 if is_section_start(line, "gencost") {
221 section = Section::GenCost;
222 try_parse_inline_data(line, &mut gencost_rows);
223 continue;
224 }
225 if is_section_start(line, "gen") {
226 section = Section::Gen;
227 try_parse_inline_data(line, &mut gen_rows);
228 continue;
229 }
230 if is_section_start(line, "branch") {
231 section = Section::Branch;
232 try_parse_inline_data(line, &mut branch_rows);
233 continue;
234 }
235
236 if is_section_start(line, "busdc") || is_section_start(line, "dcbus") {
238 section = Section::DcBus;
239 try_parse_inline_data(line, &mut dc_bus_rows);
240 continue;
241 }
242 if is_section_start(line, "convdc") || is_section_start(line, "dcconv") {
244 section = Section::DcConv;
245 try_parse_inline_data(line, &mut dc_conv_rows);
246 continue;
247 }
248 if is_section_start(line, "branchdc") || is_section_start(line, "dcbranch") {
250 section = Section::DcBranch;
251 try_parse_inline_data(line, &mut dc_branch_rows);
252 continue;
253 }
254
255 if line.contains("mpc.bus_name") && line.contains('=') && line.contains('{') {
257 section = Section::BusName;
258 continue;
259 }
260 if line.contains("mpc.gentype") && line.contains('=') && line.contains('{') {
261 section = Section::GenType;
262 continue;
263 }
264 if line.contains("mpc.genfuel") && line.contains('=') && line.contains('{') {
265 section = Section::GenFuel;
266 continue;
267 }
268
269 if line.contains("mpc.") && line.contains('=') {
271 if section != Section::Bus
272 && section != Section::Gen
273 && section != Section::Branch
274 && section != Section::DcBus
275 && section != Section::DcConv
276 && section != Section::DcBranch
277 {
278 section = Section::Other;
279 }
280 continue;
281 }
282
283 match section {
285 Section::Bus
286 | Section::Gen
287 | Section::Branch
288 | Section::GenCost
289 | Section::DcBus
290 | Section::DcConv
291 | Section::DcBranch => {
292 let row_str = line.trim_end_matches(';').trim();
293 if let Some(row) = parse_numeric_row(row_str) {
294 match section {
295 Section::Bus => bus_rows.push(row),
296 Section::Gen => gen_rows.push(row),
297 Section::Branch => branch_rows.push(row),
298 Section::GenCost => gencost_rows.push(row),
299 Section::DcBus => dc_bus_rows.push(row),
300 Section::DcConv => dc_conv_rows.push(row),
301 Section::DcBranch => dc_branch_rows.push(row),
302 _ => {}
303 }
304 }
305 }
306 Section::BusName => {
307 if line.contains('\'') {
310 bus_name_rows.push(parse_cell_array_entry(line));
311 }
312 }
313 Section::GenType => {
314 if line.contains('\'') {
315 gentype_rows.push(parse_cell_array_entry(line));
316 }
317 }
318 Section::GenFuel => {
319 if line.contains('\'') {
320 genfuel_rows.push(parse_cell_array_entry(line));
321 }
322 }
323 _ => {}
324 }
325 }
326
327 if bus_rows.is_empty() {
329 return Err(MatpowerError::MissingSection("bus".into()));
330 }
331 if branch_rows.is_empty() {
332 return Err(MatpowerError::MissingSection("branch".into()));
333 }
334
335 let conversions = detect_conversions(content);
337
338 let mut network = Network::new(&name);
340 network.base_mva = base_mva;
341
342 let mut bus_pd_qd: Vec<(u32, f64, f64)> = Vec::new();
344
345 for (i, row) in bus_rows.iter().enumerate() {
347 if row.len() < 13 {
348 return Err(MatpowerError::InsufficientColumns {
349 section: "bus".into(),
350 row: i + 1,
351 expected: 13,
352 got: row.len(),
353 });
354 }
355 network.buses.push(Bus {
356 number: row[0] as u32,
357 name: String::new(),
358 bus_type: bus_type_from_matpower(row[1] as u32),
359 shunt_conductance_mw: row[4],
360 shunt_susceptance_mvar: row[5],
361 area: row[6] as u32,
362 voltage_magnitude_pu: row[7],
363 voltage_angle_rad: row[8].to_radians(), base_kv: row[9],
365 zone: row[10] as u32,
366 voltage_max_pu: row[11],
367 voltage_min_pu: row[12],
368 island_id: 0,
370 latitude: None, longitude: None,
372 ..Bus::new(0, BusType::PQ, 0.0)
373 });
374 bus_pd_qd.push((row[0] as u32, row[2], row[3]));
376 let row_num = i + 1;
378 check_finite(row[2], "bus", row_num, "pd")?;
379 check_finite(row[3], "bus", row_num, "qd")?;
380 check_finite(row[4], "bus", row_num, "gs")?;
381 check_finite(row[5], "bus", row_num, "bs")?;
382 check_finite(row[7], "bus", row_num, "vm")?;
383 check_finite(row[8], "bus", row_num, "va")?;
384 check_finite(row[9], "bus", row_num, "base_kv")?;
385 check_finite(row[11], "bus", row_num, "vmax")?;
386 check_finite(row[12], "bus", row_num, "vmin")?;
387 }
388
389 let bus_set: std::collections::HashSet<u32> = network.buses.iter().map(|b| b.number).collect();
392 let mut gen_row_to_network_idx: Vec<Option<usize>> = Vec::with_capacity(gen_rows.len());
393
394 for (i, row) in gen_rows.iter().enumerate() {
396 if row.len() < 10 {
397 return Err(MatpowerError::InsufficientColumns {
398 section: "gen".into(),
399 row: i + 1,
400 expected: 10,
401 got: row.len(),
402 });
403 }
404 let gen_bus_number = row[0] as u32;
405 if !bus_set.contains(&gen_bus_number) {
407 return Err(MatpowerError::Parse {
408 line: i + 1,
409 message: format!("generator at bus {gen_bus_number} references missing bus"),
410 });
411 }
412 let reg_ramp_up_curve: Vec<(f64, f64)> = row
417 .get(16)
418 .copied()
419 .filter(|&v| v.abs() > 1e-20)
420 .map(|v| vec![(0.0, v)])
421 .unwrap_or_default();
422 let ramp_up_curve: Vec<(f64, f64)> = row
423 .get(17)
424 .copied()
425 .filter(|&v| v.abs() > 1e-20)
426 .map(|v| vec![(0.0, v / 10.0)])
427 .or_else(|| {
428 row.get(18)
429 .copied()
430 .filter(|&v| v.abs() > 1e-20)
431 .map(|v| vec![(0.0, v / 30.0)])
432 })
433 .unwrap_or_default();
434 let ramping = if !reg_ramp_up_curve.is_empty() || !ramp_up_curve.is_empty() {
435 Some(surge_network::network::RampingParams {
436 reg_ramp_up_curve,
437 ramp_up_curve,
438 ..Default::default()
439 })
440 } else {
441 None
442 };
443
444 let reactive_capability = {
446 let pc1_val = row.get(10).copied();
447 let pc2_val = row.get(11).copied();
448 let qc1min_val = row.get(12).copied();
449 let qc1max_val = row.get(13).copied();
450 let qc2min_val = row.get(14).copied();
451 let qc2max_val = row.get(15).copied();
452 let pc1_f = pc1_val.unwrap_or(0.0);
456 let pc2_f = pc2_val.unwrap_or(0.0);
457 let pq_curve = match (row.get(12), row.get(13), row.get(14), row.get(15)) {
458 (Some(&qc1min), Some(&qc1max), Some(&qc2min), Some(&qc2max))
459 if (pc2_f - pc1_f).abs() > 1e-6 =>
460 {
461 let inv = 1.0 / base_mva;
462 if pc1_f <= pc2_f {
463 vec![
464 (pc1_f * inv, qc1max * inv, qc1min * inv),
465 (pc2_f * inv, qc2max * inv, qc2min * inv),
466 ]
467 } else {
468 vec![
469 (pc2_f * inv, qc2max * inv, qc2min * inv),
470 (pc1_f * inv, qc1max * inv, qc1min * inv),
471 ]
472 }
473 }
474 _ => vec![],
475 };
476 let has_any = pc1_val.is_some()
477 || pc2_val.is_some()
478 || qc1min_val.is_some()
479 || qc1max_val.is_some()
480 || qc2min_val.is_some()
481 || qc2max_val.is_some()
482 || !pq_curve.is_empty();
483 if has_any {
484 Some(surge_network::network::ReactiveCapability {
485 pc1: pc1_val,
486 pc2: pc2_val,
487 qc1min: qc1min_val,
488 qc1max: qc1max_val,
489 qc2min: qc2min_val,
490 qc2max: qc2max_val,
491 pq_curve,
492 pq_linear_equality: None,
493 pq_linear_upper: None,
494 pq_linear_lower: None,
495 })
496 } else {
497 None
498 }
499 };
500
501 network.generators.push(Generator {
502 bus: gen_bus_number,
503 machine_id: None,
504 p: row[1],
505 q: row[2],
506 qmax: row[3],
507 qmin: row[4],
508 voltage_setpoint_pu: row[5],
509 reg_bus: None,
510 machine_base_mva: if row[6].is_finite() && row[6].abs() > 1e-10 {
511 row[6]
512 } else {
513 network.base_mva
514 },
515 in_service: row[7] as i32 > 0,
516 pmax: row[8],
517 pmin: row[9],
518 cost: None,
519 ramping,
520 reactive_capability,
521 agc_participation_factor: row.get(20).copied(),
524 forced_outage_rate: None,
526 h_inertia_s: None,
527 pfr_eligible: true,
528 ..Generator::new(0, 0.0, 1.0)
529 });
530 gen_row_to_network_idx.push(Some(network.generators.len() - 1));
531 let row_num = i + 1;
533 check_finite(row[1], "gen", row_num, "pg")?;
534 check_finite(row[2], "gen", row_num, "qg")?;
535 if row[3].is_nan() {
537 return Err(MatpowerError::NonFiniteValue {
538 section: "gen",
539 row: row_num,
540 field: "qmax",
541 value: row[3],
542 });
543 }
544 if row[4].is_nan() {
545 return Err(MatpowerError::NonFiniteValue {
546 section: "gen",
547 row: row_num,
548 field: "qmin",
549 value: row[4],
550 });
551 }
552 check_finite(row[5], "gen", row_num, "vs")?;
553 }
555
556 for (i, row) in gencost_rows.iter().enumerate() {
561 if i >= gen_rows.len() {
562 break; }
564 let Some(gen_idx) = gen_row_to_network_idx.get(i).copied().flatten() else {
565 continue;
566 };
567 if row.len() < 4 {
568 continue;
569 }
570 let cost_type = row[0] as i32;
571 let startup = row[1];
572 let shutdown = row[2];
573
574 let n_raw = row[3];
577 if !(0.0..=1000.0).contains(&n_raw) || !n_raw.is_finite() {
578 return Err(MatpowerError::InvalidGencost(format!(
579 "gencost row {} has n={} breakpoints, must be 0–1000",
580 i + 1,
581 n_raw
582 )));
583 }
584 let n = n_raw as usize;
585
586 let expected_len = 4 + if cost_type == 1 { 2 * n } else { n };
588 if row.len() < expected_len {
589 return Err(MatpowerError::InvalidGencost(format!(
590 "gencost row {} has {} fields, expected at least {}",
591 i + 1,
592 row.len(),
593 expected_len
594 )));
595 }
596
597 let cost = match cost_type {
598 2 => {
599 Some(CostCurve::Polynomial {
601 startup,
602 shutdown,
603 coeffs: row[4..4 + n].to_vec(),
604 })
605 }
606 1 => {
607 let points: Vec<(f64, f64)> = (0..n)
609 .map(|j| (row[4 + 2 * j], row[4 + 2 * j + 1]))
610 .collect();
611 Some(CostCurve::PiecewiseLinear {
612 startup,
613 shutdown,
614 points,
615 })
616 }
617 _ => None,
618 };
619
620 if let Some(c) = cost {
621 network.generators[gen_idx].cost = Some(c);
622 }
623 }
624
625 for (i, raw_code) in gentype_rows.iter().enumerate() {
626 let Some(gen_idx) = gen_row_to_network_idx.get(i).copied().flatten() else {
627 continue;
628 };
629 let raw_code = raw_code.trim();
630 if raw_code.is_empty() {
631 continue;
632 }
633 let generator = &mut network.generators[gen_idx];
634 generator.source_technology_code = Some(raw_code.to_string());
635 generator.technology = Some(matpower_technology_from_code(raw_code));
636 if let Some(class) = matpower_electrical_class_from_code(raw_code) {
637 generator.gen_type = class;
638 }
639 }
640
641 for (i, raw_fuel) in genfuel_rows.iter().enumerate() {
642 let Some(gen_idx) = gen_row_to_network_idx.get(i).copied().flatten() else {
643 continue;
644 };
645 let raw_fuel = raw_fuel.trim();
646 if raw_fuel.is_empty() {
647 continue;
648 }
649 let generator = &mut network.generators[gen_idx];
650 generator
651 .fuel
652 .get_or_insert_with(FuelParams::default)
653 .fuel_type = Some(raw_fuel.to_string());
654 if generator.technology.is_none() {
655 generator.technology = matpower_technology_from_fuel(raw_fuel);
656 }
657 if generator.gen_type == GenType::Unknown
658 && let Some(class) = matpower_electrical_class_from_fuel(raw_fuel)
659 {
660 generator.gen_type = class;
661 }
662 }
663
664 for (i, row) in branch_rows.iter().enumerate() {
666 if row.len() < 11 {
667 return Err(MatpowerError::InsufficientColumns {
668 section: "branch".into(),
669 row: i + 1,
670 expected: 11,
671 got: row.len(),
672 });
673 }
674 let tap = if row[8] == 0.0 { 1.0 } else { row[8] };
676
677 let rate_a = if row[5].is_infinite() { 0.0 } else { row[5] };
680 let rate_b = if row[6].is_infinite() { 0.0 } else { row[6] };
681 let rate_c = if row[7].is_infinite() { 0.0 } else { row[7] };
682
683 network.branches.push(Branch {
684 from_bus: row[0] as u32,
685 to_bus: row[1] as u32,
686 circuit: "1".to_string(),
687 r: row[2],
688 x: row[3],
689 b: row[4],
690 rating_a_mva: rate_a,
691 rating_b_mva: rate_b,
692 rating_c_mva: rate_c,
693 tap,
694 phase_shift_rad: row[9].to_radians(),
695 in_service: row[10] as i32 > 0,
696 angle_diff_min_rad: row.get(11).copied().map(f64::to_radians),
698 angle_diff_max_rad: row.get(12).copied().map(f64::to_radians),
699 g_pi: 0.0,
700 g_mag: 0.0,
701 b_mag: 0.0,
702 tab: None,
703 branch_type: if (tap - 1.0).abs() > 1e-6 || row[9].abs() > 1e-6 {
705 BranchType::Transformer
706 } else {
707 BranchType::Line
708 },
709 ..Branch::default()
710 });
711 let row_num = i + 1;
713 check_finite(row[2], "branch", row_num, "r")?;
714 check_finite(row[3], "branch", row_num, "x")?;
715 check_finite(row[4], "branch", row_num, "b")?;
716 }
717
718 {
722 let mut counts: std::collections::HashMap<(u32, u32), u32> =
723 std::collections::HashMap::new();
724 for branch in &mut network.branches {
725 let key = (branch.from_bus, branch.to_bus);
726 let n = counts.entry(key).or_insert(0);
727 *n += 1;
728 branch.circuit = n.to_string();
729 }
730 }
731
732 apply_conversions(&mut network, &conversions, &mut bus_pd_qd);
734
735 for &(bus_num, pd, qd) in &bus_pd_qd {
740 if pd.abs() > 1e-10 || qd.abs() > 1e-10 {
741 network.loads.push(Load::new(bus_num, pd, qd));
742 }
743 }
744
745 for (i, name) in bus_name_rows.iter().enumerate() {
748 if let Some(bus) = network.buses.get_mut(i)
749 && !name.is_empty()
750 {
751 bus.name = name.clone();
752 }
753 }
754
755 {
759 let bus_map = network.bus_index_map();
760 for g in &network.generators {
761 if !g.in_service {
762 continue;
763 }
764 if let Some(&idx) = bus_map.get(&g.bus) {
765 let bt = network.buses[idx].bus_type;
766 if bt == BusType::PV || bt == BusType::Slack {
767 network.buses[idx].voltage_magnitude_pu = g.voltage_setpoint_pu;
768 }
769 }
770 }
771 }
772
773 for (row_idx, row) in dc_bus_rows.iter().enumerate() {
775 if row.len() < 7 {
776 return Err(MatpowerError::InsufficientColumns {
777 section: "busdc".to_string(),
778 row: row_idx,
779 expected: 7,
780 got: row.len(),
781 });
782 }
783 network
784 .hvdc
785 .ensure_dc_grid(row[1] as u32, None)
786 .buses
787 .push(DcBus {
788 bus_id: row[0] as u32,
789 p_dc_mw: row[2],
790 v_dc_pu: row[3],
791 base_kv_dc: row[4],
792 v_dc_max: row[5],
793 v_dc_min: row[6],
794 cost: row.get(7).copied().unwrap_or(0.0),
795 g_shunt_siemens: 0.0,
796 r_ground_ohm: 0.0,
797 });
798 }
799
800 for (row_idx, row) in dc_conv_rows.iter().enumerate() {
802 if row.len() < 22 {
803 return Err(MatpowerError::InsufficientColumns {
804 section: "convdc".to_string(),
805 row: row_idx,
806 expected: 22,
807 got: row.len(),
808 });
809 }
810 if let Some(dc_grid) = network.hvdc.find_dc_grid_by_bus_mut(row[0] as u32) {
811 dc_grid
812 .converters
813 .push(DcConverter::Vsc(DcConverterStation {
814 id: String::new(),
815 dc_bus: row[0] as u32,
816 ac_bus: row[1] as u32,
817 control_type_dc: row[2] as u32,
818 control_type_ac: row[3] as u32,
819 active_power_mw: row[4],
820 reactive_power_mvar: row[5],
821 is_lcc: row[6] as u32 != 0,
822 voltage_setpoint_pu: row[7],
823 transformer_r_pu: row[8],
824 transformer_x_pu: row[9],
825 transformer: row[10] as u32 != 0,
826 tap_ratio: row[11],
827 filter_susceptance_pu: row[12],
828 filter: row[13] as u32 != 0,
829 reactor_r_pu: row[14],
830 reactor_x_pu: row[15],
831 reactor: row[16] as u32 != 0,
832 base_kv_ac: row.get(17).copied().unwrap_or(0.0),
833 voltage_max_pu: row.get(18).copied().unwrap_or(1.1),
834 voltage_min_pu: row.get(19).copied().unwrap_or(0.9),
835 current_max_pu: row.get(20).copied().unwrap_or(0.0),
836 status: row.get(21).map(|&v| v as u32 != 0).unwrap_or(true),
837 loss_constant_mw: row.get(22).copied().unwrap_or(0.0),
838 loss_linear: row.get(23).copied().unwrap_or(0.0),
839 loss_quadratic_rectifier: row.get(24).copied().unwrap_or(0.0),
840 loss_quadratic_inverter: row.get(25).copied().unwrap_or(0.0),
841 droop: row.get(26).copied().unwrap_or(0.0),
842 power_dc_setpoint_mw: row.get(27).copied().unwrap_or(0.0),
843 voltage_dc_setpoint_pu: row.get(28).copied().unwrap_or(1.0),
844 active_power_ac_max_mw: if row.len() >= 34 {
847 row.get(30).copied().unwrap_or(f64::MAX)
848 } else {
849 row.get(29).copied().unwrap_or(f64::MAX)
850 },
851 active_power_ac_min_mw: if row.len() >= 34 {
852 row.get(31).copied().unwrap_or(f64::MIN)
853 } else {
854 row.get(30).copied().unwrap_or(f64::MIN)
855 },
856 reactive_power_ac_max_mvar: if row.len() >= 34 {
857 row.get(32).copied().unwrap_or(f64::MAX)
858 } else {
859 row.get(31).copied().unwrap_or(f64::MAX)
860 },
861 reactive_power_ac_min_mvar: if row.len() >= 34 {
862 row.get(33).copied().unwrap_or(f64::MIN)
863 } else {
864 row.get(32).copied().unwrap_or(f64::MIN)
865 },
866 }));
867 }
868 }
869
870 for (row_idx, row) in dc_branch_rows.iter().enumerate() {
872 if row.len() < 6 {
873 return Err(MatpowerError::InsufficientColumns {
874 section: "branchdc".to_string(),
875 row: row_idx,
876 expected: 6,
877 got: row.len(),
878 });
879 }
880 if let Some(dc_grid) = network.hvdc.find_dc_grid_by_bus_mut(row[0] as u32) {
881 dc_grid.branches.push(DcBranch {
882 id: format!(
883 "dc_grid_{}_branch_{}",
884 dc_grid.id,
885 dc_grid.branches.len() + 1
886 ),
887 from_bus: row[0] as u32,
888 to_bus: row[1] as u32,
889 r_ohm: row[2],
890 l_mh: row[3],
891 c_uf: row[4],
892 rating_a_mva: row[5],
893 rating_b_mva: row.get(6).copied().unwrap_or(0.0),
894 rating_c_mva: row.get(7).copied().unwrap_or(0.0),
895 status: row.get(8).map(|&v| v as u32 != 0).unwrap_or(true),
896 });
897 }
898 }
899 Ok(network)
900}
901
902fn parse_cell_array_entry(line: &str) -> String {
906 let line = line.trim();
907 if let Some(start) = line.find('\'')
908 && let Some(end) = line[start + 1..].find('\'')
909 {
910 return line[start + 1..start + 1 + end].trim_end().to_string();
911 }
912 String::new()
913}
914
915fn matpower_technology_from_code(code: &str) -> GeneratorTechnology {
916 match code.trim().to_ascii_uppercase().as_str() {
917 "ST" => GeneratorTechnology::SteamTurbine,
918 "GT" | "JE" => GeneratorTechnology::CombustionTurbine,
919 "CC" => GeneratorTechnology::CombinedCycle,
920 "IC" => GeneratorTechnology::InternalCombustion,
921 "HY" | "HB" | "HR" => GeneratorTechnology::Hydro,
922 "PS" => GeneratorTechnology::PumpedStorage,
923 "HK" => GeneratorTechnology::Hydrokinetic,
924 "NB" | "NU" => GeneratorTechnology::Nuclear,
925 "GE" => GeneratorTechnology::Geothermal,
926 "WT" | "WS" | "W1" | "W2" | "W3" | "W4" => GeneratorTechnology::Wind,
927 "PV" => GeneratorTechnology::SolarPv,
928 "CP" => GeneratorTechnology::SolarThermal,
929 "BA" | "ES" => GeneratorTechnology::BatteryStorage,
930 "CE" => GeneratorTechnology::CompressedAirStorage,
931 "FW" => GeneratorTechnology::FlywheelStorage,
932 "FC" => GeneratorTechnology::FuelCell,
933 "SC" => GeneratorTechnology::SynchronousCondenser,
934 "SV" => GeneratorTechnology::StaticVarCompensator,
935 "DL" => GeneratorTechnology::DispatchableLoad,
936 "DC" => GeneratorTechnology::DcTie,
937 "OT" => GeneratorTechnology::Other,
938 _ => GeneratorTechnology::Other,
939 }
940}
941
942fn matpower_electrical_class_from_code(code: &str) -> Option<GenType> {
943 match code.trim().to_ascii_uppercase().as_str() {
944 "ST" | "GT" | "JE" | "CC" | "IC" | "HY" | "HB" | "HR" | "PS" | "NB" | "NU" | "GE"
945 | "SC" => Some(GenType::Synchronous),
946 "W1" | "W2" => Some(GenType::Asynchronous),
947 "PV" | "W3" | "W4" | "BA" | "ES" | "FW" | "FC" | "SV" => Some(GenType::InverterBased),
948 "DL" => Some(GenType::Hybrid),
949 "WT" | "WS" | "CP" | "CE" | "DC" | "OT" => Some(GenType::Unknown),
950 _ => None,
951 }
952}
953
954fn matpower_technology_from_fuel(fuel: &str) -> Option<GeneratorTechnology> {
955 match fuel.trim().to_ascii_lowercase().as_str() {
956 "wind" => Some(GeneratorTechnology::Wind),
957 "solar" => Some(GeneratorTechnology::SolarPv),
958 "hydro" => Some(GeneratorTechnology::Hydro),
959 "hydrops" => Some(GeneratorTechnology::PumpedStorage),
960 "nuclear" => Some(GeneratorTechnology::Nuclear),
961 "geothermal" => Some(GeneratorTechnology::Geothermal),
962 "battery" | "ess" => Some(GeneratorTechnology::BatteryStorage),
963 "dl" => Some(GeneratorTechnology::DispatchableLoad),
964 _ => None,
965 }
966}
967
968fn matpower_electrical_class_from_fuel(fuel: &str) -> Option<GenType> {
969 match fuel.trim().to_ascii_lowercase().as_str() {
970 "solar" | "battery" | "ess" => Some(GenType::InverterBased),
971 "hydro" | "hydrops" | "nuclear" | "coal" | "oil" | "gas" | "ng" => {
972 Some(GenType::Synchronous)
973 }
974 _ => None,
975 }
976}
977
978fn is_section_start(line: &str, section: &str) -> bool {
981 let pattern = format!("mpc.{section}");
982 if let Some(pos) = line.find(&pattern) {
983 let end = pos + pattern.len();
984 if end >= line.len() {
985 return false;
986 }
987 let next_byte = line.as_bytes()[end];
989 if next_byte.is_ascii_alphanumeric() || next_byte == b'_' {
990 return false;
991 }
992 let rest = &line[end..];
994 rest.contains('=') && (rest.contains('[') || rest.contains('{'))
995 } else {
996 false
997 }
998}
999
1000fn try_parse_inline_data(line: &str, rows: &mut Vec<Vec<f64>>) {
1002 let bracket_pos = line.find('[').or_else(|| line.find('{'));
1004 if let Some(pos) = bracket_pos {
1005 let rest = &line[pos + 1..];
1006 let rest = rest
1007 .trim_end_matches(';')
1008 .trim_end_matches(']')
1009 .trim_end_matches('}')
1010 .trim();
1011 if !rest.is_empty()
1012 && let Some(row) = parse_numeric_row(rest)
1013 {
1014 rows.push(row);
1015 }
1016 }
1017}
1018
1019fn check_finite(
1023 val: f64,
1024 section: &'static str,
1025 row: usize,
1026 field: &'static str,
1027) -> Result<(), MatpowerError> {
1028 if !val.is_finite() {
1029 return Err(MatpowerError::NonFiniteValue {
1030 section,
1031 row,
1032 field,
1033 value: val,
1034 });
1035 }
1036 Ok(())
1037}
1038
1039fn parse_finite_f64(s: &str) -> Option<f64> {
1044 let val: f64 = s.trim().parse().ok()?;
1045 if val.is_nan() {
1047 return None;
1048 }
1049 Some(val)
1051}
1052
1053fn parse_numeric_row(s: &str) -> Option<Vec<f64>> {
1056 let s = s.trim();
1057 if s.is_empty() || s.starts_with(']') {
1058 return None;
1059 }
1060
1061 let values: Result<Vec<f64>, _> = s
1062 .split_whitespace()
1063 .filter(|t| !t.is_empty() && *t != ";")
1064 .map(|t| {
1065 let t = t.trim_end_matches(';');
1066 parse_finite_f64(t)
1068 .or_else(|| eval_simple_expr(t))
1069 .ok_or(())
1070 })
1071 .collect();
1072
1073 match values {
1074 Ok(v) if !v.is_empty() => Some(v),
1075 _ => None,
1076 }
1077}
1078
1079struct Conversions {
1085 kw_to_mw: bool,
1087 ohms_to_pu: bool,
1089 power_factor: Option<f64>,
1091}
1092
1093fn detect_conversions(content: &str) -> Conversions {
1095 let code_content: String = content
1099 .lines()
1100 .filter(|line| {
1101 let trimmed = line.trim();
1102 !trimmed.starts_with('%') && !trimmed.is_empty()
1103 })
1104 .collect::<Vec<_>>()
1105 .join("\n");
1106
1107 let kw_to_mw = code_content.contains("/ 1e3") && code_content.contains("PD, QD");
1109
1110 let ohms_to_pu =
1112 code_content.contains("Vbase^2 / Sbase") || code_content.contains("Vbase^2/Sbase");
1113
1114 let power_factor = if code_content.contains("* pf") || code_content.contains("*pf") {
1116 code_content
1118 .lines()
1119 .find(|l| {
1120 let t = l.trim();
1121 t.starts_with("pf ") || t.starts_with("pf=")
1122 })
1123 .and_then(|l| {
1124 l.find('=').and_then(|eq| {
1125 l[eq + 1..]
1126 .trim()
1127 .trim_end_matches(';')
1128 .trim()
1129 .parse::<f64>()
1130 .ok()
1131 })
1132 })
1133 } else {
1134 None
1135 };
1136
1137 Conversions {
1138 kw_to_mw,
1139 ohms_to_pu,
1140 power_factor,
1141 }
1142}
1143
1144fn apply_conversions(network: &mut Network, conv: &Conversions, bus_pd_qd: &mut [(u32, f64, f64)]) {
1150 if conv.kw_to_mw {
1151 for entry in bus_pd_qd.iter_mut() {
1152 entry.1 /= 1000.0;
1153 entry.2 /= 1000.0;
1154 }
1155 }
1156
1157 if conv.ohms_to_pu {
1158 let bus_kv: std::collections::HashMap<u32, f64> = network
1160 .buses
1161 .iter()
1162 .map(|b| (b.number, b.base_kv))
1163 .collect();
1164 let base_mva = network.base_mva;
1165 if base_mva > 0.0 {
1166 for branch in &mut network.branches {
1167 let base_kv = bus_kv.get(&branch.from_bus).copied().unwrap_or(1.0);
1168 if base_kv > 0.0 {
1169 let z_base = base_kv * base_kv / base_mva;
1170 branch.r /= z_base;
1171 branch.x /= z_base;
1172 }
1173 }
1174 }
1175 }
1176
1177 if let Some(pf) = conv.power_factor {
1178 let sin_phi = (1.0 - pf * pf).sqrt();
1181 for entry in bus_pd_qd.iter_mut() {
1182 let apparent = entry.1;
1183 entry.2 = apparent * sin_phi;
1184 entry.1 = apparent * pf;
1185 }
1186 }
1187}
1188
1189fn eval_simple_expr(s: &str) -> Option<f64> {
1193 eval_expr_depth(s, 0)
1194}
1195
1196fn eval_expr_depth(s: &str, depth: usize) -> Option<f64> {
1199 if depth > 100 {
1200 return None;
1201 }
1202 let s = s.trim();
1203 if let Ok(v) = s.parse::<f64>() {
1205 return Some(v);
1206 }
1207 if s.starts_with("sqrt(") && s.ends_with(')') {
1209 let inner = &s[5..s.len() - 1];
1210 return eval_expr_depth(inner, depth + 1).map(|v| v.sqrt());
1211 }
1212 let bytes = s.as_bytes();
1213 for i in (1..bytes.len()).rev() {
1216 if bytes[i] == b'+' || bytes[i] == b'-' {
1217 let left = eval_expr_depth(&s[..i], depth + 1)?;
1218 let right = eval_expr_depth(&s[i + 1..], depth + 1)?;
1219 return Some(if bytes[i] == b'+' {
1220 left + right
1221 } else {
1222 left - right
1223 });
1224 }
1225 }
1226 for i in (1..bytes.len()).rev() {
1228 if bytes[i] == b'*' || bytes[i] == b'/' {
1229 let left = eval_expr_depth(&s[..i], depth + 1)?;
1230 let right = eval_expr_depth(&s[i + 1..], depth + 1)?;
1231 return Some(if bytes[i] == b'*' {
1232 left * right
1233 } else {
1234 left / right
1235 });
1236 }
1237 }
1238 None
1239}
1240
1241#[cfg(test)]
1242mod tests {
1243 use super::*;
1244 use surge_network::network::{GenType, GeneratorTechnology};
1245
1246 #[allow(dead_code)]
1247 fn data_available() -> bool {
1248 if let Ok(p) = std::env::var("SURGE_TEST_DATA") {
1249 return std::path::Path::new(&p).exists();
1250 }
1251 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1252 .parent()
1253 .unwrap()
1254 .parent()
1255 .unwrap()
1256 .join("tests/data")
1257 .exists()
1258 }
1259 #[allow(dead_code)]
1260 fn test_data_dir() -> std::path::PathBuf {
1261 if let Ok(p) = std::env::var("SURGE_TEST_DATA") {
1262 return std::path::PathBuf::from(p);
1263 }
1264 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1265 .parent()
1266 .unwrap()
1267 .parent()
1268 .unwrap()
1269 .join("tests/data")
1270 }
1271
1272 #[test]
1273 fn test_parse_case9() {
1274 if !data_available() {
1275 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1276 return;
1277 }
1278 let path = test_data_dir().join("case9.m");
1279 let net = parse_file(&path).expect("failed to parse case9");
1280
1281 assert_eq!(net.name, "case9");
1282 assert_eq!(net.base_mva, 100.0);
1283 assert_eq!(net.n_buses(), 9);
1284 assert_eq!(net.n_branches(), 9);
1285 assert_eq!(net.generators.len(), 3);
1286
1287 let slack = net.buses.iter().find(|b| b.is_slack()).unwrap();
1289 assert_eq!(slack.number, 1);
1290
1291 let pv_buses: Vec<u32> = net
1293 .buses
1294 .iter()
1295 .filter(|b| b.is_pv())
1296 .map(|b| b.number)
1297 .collect();
1298 assert_eq!(pv_buses, vec![2, 3]);
1299
1300 let bus5_pd: f64 = net
1302 .loads
1303 .iter()
1304 .filter(|l| l.bus == 5)
1305 .map(|l| l.active_power_demand_mw)
1306 .sum();
1307 let bus5_qd: f64 = net
1308 .loads
1309 .iter()
1310 .filter(|l| l.bus == 5)
1311 .map(|l| l.reactive_power_demand_mvar)
1312 .sum();
1313 assert!((bus5_pd - 90.0).abs() < 1e-10);
1314 assert!((bus5_qd - 30.0).abs() < 1e-10);
1315
1316 let bus7_pd: f64 = net
1317 .loads
1318 .iter()
1319 .filter(|l| l.bus == 7)
1320 .map(|l| l.active_power_demand_mw)
1321 .sum();
1322 assert!((bus7_pd - 100.0).abs() < 1e-10);
1323
1324 let bus9_pd: f64 = net
1325 .loads
1326 .iter()
1327 .filter(|l| l.bus == 9)
1328 .map(|l| l.active_power_demand_mw)
1329 .sum();
1330 assert!((bus9_pd - 125.0).abs() < 1e-10);
1331
1332 let gen1 = &net.generators[0];
1334 assert_eq!(gen1.bus, 1);
1335 assert!((gen1.p - 72.3).abs() < 1e-10);
1336 assert!((gen1.voltage_setpoint_pu - 1.04).abs() < 1e-10);
1337 assert!(gen1.in_service);
1338
1339 let br0 = &net.branches[0];
1341 assert_eq!(br0.from_bus, 1);
1342 assert_eq!(br0.to_bus, 4);
1343 assert!((br0.r - 0.0).abs() < 1e-10);
1344 assert!((br0.x - 0.0576).abs() < 1e-10);
1345 assert!((br0.tap - 1.0).abs() < 1e-10); assert!(br0.in_service);
1347
1348 assert!((net.total_generation_mw() - 320.3).abs() < 1e-10);
1350 assert!((net.total_load_mw() - 315.0).abs() < 1e-10);
1351 }
1352
1353 #[test]
1354 fn test_parse_case14() {
1355 if !data_available() {
1356 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1357 return;
1358 }
1359 let path = test_data_dir().join("case14.m");
1360 let net = parse_file(&path).expect("failed to parse case14");
1361
1362 assert_eq!(net.name, "case14");
1363 assert_eq!(net.base_mva, 100.0);
1364 assert_eq!(net.n_buses(), 14);
1365 assert_eq!(net.n_branches(), 20);
1366 assert_eq!(net.generators.len(), 5);
1367
1368 let br_4_7 = net
1370 .branches
1371 .iter()
1372 .find(|b| b.from_bus == 4 && b.to_bus == 7)
1373 .unwrap();
1374 assert!((br_4_7.tap - 0.978).abs() < 1e-10);
1375
1376 let br_4_9 = net
1377 .branches
1378 .iter()
1379 .find(|b| b.from_bus == 4 && b.to_bus == 9)
1380 .unwrap();
1381 assert!((br_4_9.tap - 0.969).abs() < 1e-10);
1382
1383 let bus9 = net.buses.iter().find(|b| b.number == 9).unwrap();
1385 assert!((bus9.shunt_susceptance_mvar - 19.0).abs() < 1e-10);
1386
1387 let bus1 = net.buses.iter().find(|b| b.number == 1).unwrap();
1391 assert!(
1392 (bus1.base_kv - 132.0).abs() < 1e-6,
1393 "bus 1 should be 132 kV HV"
1394 );
1395 let bus5 = net.buses.iter().find(|b| b.number == 5).unwrap();
1396 assert!(
1397 (bus5.base_kv - 132.0).abs() < 1e-6,
1398 "bus 5 should be 132 kV HV"
1399 );
1400 let bus6 = net.buses.iter().find(|b| b.number == 6).unwrap();
1401 assert!(
1402 (bus6.base_kv - 33.0).abs() < 1e-6,
1403 "bus 6 should be 33 kV LV"
1404 );
1405 let bus8 = net.buses.iter().find(|b| b.number == 8).unwrap();
1406 assert!(
1407 (bus8.base_kv - 132.0).abs() < 1e-6,
1408 "bus 8 should be 132 kV HV"
1409 );
1410 let bus9_kv = net.buses.iter().find(|b| b.number == 9).unwrap();
1411 assert!(
1412 (bus9_kv.base_kv - 33.0).abs() < 1e-6,
1413 "bus 9 should be 33 kV LV"
1414 );
1415 for bus in &net.buses {
1417 assert!(
1418 bus.base_kv > 0.0,
1419 "bus {} has zero baseKV — fault analysis will fail",
1420 bus.number
1421 );
1422 }
1423 }
1424
1425 #[test]
1426 fn test_parse_case30() {
1427 if !data_available() {
1428 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1429 return;
1430 }
1431 let path = test_data_dir().join("case30.m");
1432 let net = parse_file(&path).expect("failed to parse case30");
1433
1434 assert_eq!(net.n_buses(), 30);
1435 assert_eq!(net.generators.len(), 6);
1436 }
1437
1438 #[test]
1439 fn test_parse_case118() {
1440 if !data_available() {
1441 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1442 return;
1443 }
1444 let path = test_data_dir().join("case118.m");
1445 let net = parse_file(&path).expect("failed to parse case118");
1446
1447 assert_eq!(net.n_buses(), 118);
1448 assert_eq!(net.generators.len(), 54);
1449 }
1450
1451 #[test]
1452 fn test_parse_string_minimal() {
1453 if !data_available() {
1454 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1455 return;
1456 }
1457 let case = r#"
1458function mpc = testcase
1459mpc.version = '2';
1460mpc.baseMVA = 100;
1461mpc.bus = [
1462 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1463 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
1464];
1465mpc.gen = [
1466 1 100 0 300 -300 1.0 100 1 250 10 0 0 0 0 0 0 0 0 0 0 0;
1467];
1468mpc.branch = [
1469 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
1470];
1471"#;
1472 let net = parse_str(case).expect("failed to parse minimal case");
1473 assert_eq!(net.name, "testcase");
1474 assert_eq!(net.n_buses(), 2);
1475 assert_eq!(net.generators.len(), 1);
1476 assert_eq!(net.n_branches(), 1);
1477 let bus_pd = net.bus_load_p_mw();
1478 assert!((bus_pd[1] - 50.0).abs() < 1e-10);
1479 }
1480
1481 #[test]
1482 fn test_parse_generator_classification_sections() {
1483 let case = r#"
1484function mpc = testcase
1485mpc.version = '2';
1486mpc.baseMVA = 100;
1487mpc.bus = [
1488 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1489 2 2 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1490];
1491mpc.gen = [
1492 1 100 0 300 -300 1.0 100 1 250 10;
1493 2 40 0 100 -50 1.0 100 1 80 0;
1494];
1495mpc.branch = [
1496 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
1497];
1498mpc.gentype = {
1499 'W2';
1500 'PV';
1501};
1502mpc.genfuel = {
1503 'wind';
1504 'solar';
1505};
1506"#;
1507 let net = parse_str(case).expect("failed to parse classification case");
1508 assert_eq!(net.generators.len(), 2);
1509 assert_eq!(
1510 net.generators[0].source_technology_code.as_deref(),
1511 Some("W2")
1512 );
1513 assert_eq!(
1514 net.generators[0].technology,
1515 Some(GeneratorTechnology::Wind)
1516 );
1517 assert_eq!(net.generators[0].gen_type, GenType::Asynchronous);
1518 assert_eq!(
1519 net.generators[0]
1520 .fuel
1521 .as_ref()
1522 .and_then(|fuel| fuel.fuel_type.as_deref()),
1523 Some("wind")
1524 );
1525 assert_eq!(
1526 net.generators[1].source_technology_code.as_deref(),
1527 Some("PV")
1528 );
1529 assert_eq!(
1530 net.generators[1].technology,
1531 Some(GeneratorTechnology::SolarPv)
1532 );
1533 assert_eq!(net.generators[1].gen_type, GenType::InverterBased);
1534 }
1535
1536 #[test]
1537 fn test_parse_gencost_case9() {
1538 if !data_available() {
1539 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1540 return;
1541 }
1542 let path = test_data_dir().join("case9.m");
1543 let net = parse_file(&path).expect("failed to parse case9");
1544
1545 assert_eq!(net.generators.len(), 3);
1547 for g in &net.generators {
1548 assert!(
1549 g.cost.is_some(),
1550 "generator at bus {} should have cost",
1551 g.bus
1552 );
1553 }
1554
1555 let cost0 = net.generators[0].cost.as_ref().unwrap();
1557 match cost0 {
1558 CostCurve::Polynomial {
1559 startup,
1560 shutdown,
1561 coeffs,
1562 } => {
1563 assert!((startup - 1500.0).abs() < 1e-10);
1564 assert!((shutdown - 0.0).abs() < 1e-10);
1565 assert_eq!(coeffs.len(), 3);
1566 assert!((coeffs[0] - 0.11).abs() < 1e-10);
1567 assert!((coeffs[1] - 5.0).abs() < 1e-10);
1568 assert!((coeffs[2] - 150.0).abs() < 1e-10);
1569 }
1570 _ => panic!("expected polynomial cost"),
1571 }
1572
1573 let cost1 = net.generators[1].cost.as_ref().unwrap();
1575 match cost1 {
1576 CostCurve::Polynomial {
1577 startup, coeffs, ..
1578 } => {
1579 assert!((startup - 2000.0).abs() < 1e-10);
1580 assert_eq!(coeffs.len(), 3);
1581 assert!((coeffs[0] - 0.085).abs() < 1e-10);
1582 assert!((coeffs[1] - 1.2).abs() < 1e-10);
1583 assert!((coeffs[2] - 600.0).abs() < 1e-10);
1584 }
1585 _ => panic!("expected polynomial cost"),
1586 }
1587
1588 let cost2 = net.generators[2].cost.as_ref().unwrap();
1590 match cost2 {
1591 CostCurve::Polynomial {
1592 startup, coeffs, ..
1593 } => {
1594 assert!((startup - 3000.0).abs() < 1e-10);
1595 assert_eq!(coeffs.len(), 3);
1596 assert!((coeffs[0] - 0.1225).abs() < 1e-10);
1597 assert!((coeffs[1] - 1.0).abs() < 1e-10);
1598 assert!((coeffs[2] - 335.0).abs() < 1e-10);
1599 }
1600 _ => panic!("expected polynomial cost"),
1601 }
1602 }
1603
1604 #[test]
1605 fn test_parse_gencost_rejects_invalid_generator_rows() {
1606 let case = r#"
1607function mpc = testcase
1608mpc.baseMVA = 100;
1609mpc.bus = [
1610 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1611 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
1612];
1613mpc.gen = [
1614 99 10 0 200 -100 1.0 100 1 200 0;
1615 2 40 0 150 -75 1.0 100 1 150 0;
1616];
1617mpc.branch = [
1618 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
1619];
1620mpc.gencost = [
1621 2 0 0 3 1 2 3;
1622 2 0 0 3 4 5 6;
1623];
1624"#;
1625 let err = parse_str(case).expect_err("invalid generator row should be rejected");
1626 assert!(
1627 matches!(err, MatpowerError::Parse { .. }),
1628 "unexpected error: {err:?}"
1629 );
1630 }
1631
1632 #[test]
1633 fn test_parse_gencost_case118() {
1634 if !data_available() {
1635 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1636 return;
1637 }
1638 let path = test_data_dir().join("case118.m");
1639 let net = parse_file(&path).expect("failed to parse case118");
1640
1641 let with_cost = net.generators.iter().filter(|g| g.cost.is_some()).count();
1643 assert_eq!(with_cost, 54, "all 54 generators should have cost data");
1644 }
1645
1646 #[test]
1647 fn test_parse_no_gencost() {
1648 if !data_available() {
1649 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1650 return;
1651 }
1652 let case = r#"
1654function mpc = testcase
1655mpc.baseMVA = 100;
1656mpc.bus = [
1657 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1658 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
1659];
1660mpc.gen = [
1661 1 100 0 300 -300 1.0 100 1 250 10;
1662];
1663mpc.branch = [
1664 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
1665];
1666"#;
1667 let net = parse_str(case).expect("failed to parse");
1668 assert!(net.generators[0].cost.is_none());
1669 }
1670
1671 #[test]
1672 fn test_parse_gencost_piecewise_linear() {
1673 if !data_available() {
1674 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1675 return;
1676 }
1677 let case = r#"
1678function mpc = testcase
1679mpc.baseMVA = 100;
1680mpc.bus = [
1681 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1682 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
1683];
1684mpc.gen = [
1685 1 100 0 300 -300 1.0 100 1 250 10;
1686];
1687mpc.branch = [
1688 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
1689];
1690mpc.gencost = [
1691 1 0 0 3 0 0 100 1000 200 3000;
1692];
1693"#;
1694 let net = parse_str(case).expect("failed to parse");
1695 let cost = net.generators[0].cost.as_ref().unwrap();
1696 match cost {
1697 CostCurve::PiecewiseLinear { points, .. } => {
1698 assert_eq!(points.len(), 3);
1699 assert!((points[0].0 - 0.0).abs() < 1e-10);
1700 assert!((points[0].1 - 0.0).abs() < 1e-10);
1701 assert!((points[1].0 - 100.0).abs() < 1e-10);
1702 assert!((points[1].1 - 1000.0).abs() < 1e-10);
1703 assert!((points[2].0 - 200.0).abs() < 1e-10);
1704 assert!((points[2].1 - 3000.0).abs() < 1e-10);
1705 }
1706 _ => panic!("expected piecewise-linear cost"),
1707 }
1708 }
1709
1710 #[test]
1711 fn test_parse_ramp_rates_rts_gmlc() {
1712 if !data_available() {
1713 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1714 return;
1715 }
1716 let path = test_data_dir().join("case_RTS_GMLC.m");
1717 let net = parse_file(&path).expect("failed to parse RTS-GMLC");
1718
1719 let g0 = &net.generators[0];
1725 assert_eq!(g0.ramp_agc_mw_per_min(), Some(3.0));
1726 assert!(
1727 (g0.ramp_up_mw_per_min().unwrap() - 0.3).abs() < 1e-10,
1728 "ramp_up={:?}",
1729 g0.ramp_up_mw_per_min()
1730 );
1731
1732 let g2 = &net.generators[2];
1735 assert_eq!(g2.ramp_agc_mw_per_min(), Some(2.0));
1736 assert!(
1737 (g2.ramp_up_mw_per_min().unwrap() - 0.2).abs() < 1e-10,
1738 "ramp_up={:?}",
1739 g2.ramp_up_mw_per_min()
1740 );
1741 }
1742
1743 #[test]
1744 fn test_parse_ramp_rates_case9_none() {
1745 if !data_available() {
1746 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1747 return;
1748 }
1749 let path = test_data_dir().join("case9.m");
1751 let net = parse_file(&path).expect("failed to parse case9");
1752 for g in &net.generators {
1753 assert!(
1754 g.ramp_up_mw_per_min().is_none(),
1755 "case9 should have no ramp_up"
1756 );
1757 assert!(
1758 g.ramp_agc_mw_per_min().is_none(),
1759 "case9 should have no ramp_agc"
1760 );
1761 }
1762 }
1763
1764 #[test]
1765 fn test_parse_bus_name_case14() {
1766 if !data_available() {
1767 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1768 return;
1769 }
1770 let path = test_data_dir().join("case14.m");
1771 let net = parse_file(&path).expect("failed to parse case14");
1772
1773 assert_eq!(net.n_buses(), 14);
1774 assert_eq!(net.buses[0].name, "Bus 1 HV");
1776 assert_eq!(net.buses[5].name, "Bus 6 LV");
1777 assert_eq!(net.buses[13].name, "Bus 14 LV");
1778 }
1779
1780 #[test]
1781 fn test_parse_bus_name_case118() {
1782 if !data_available() {
1783 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1784 return;
1785 }
1786 let path = test_data_dir().join("case118.m");
1787 let net = parse_file(&path).expect("failed to parse case118");
1788
1789 assert_eq!(net.n_buses(), 118);
1790 assert_eq!(net.buses[0].name, "Riversde V2");
1792 assert_eq!(net.buses[1].name, "Pokagon V2");
1793 assert_eq!(net.buses[4].name, "Olive V2");
1794 for bus in &net.buses {
1796 assert!(
1797 !bus.name.is_empty(),
1798 "bus {} should have a name",
1799 bus.number
1800 );
1801 }
1802 }
1803
1804 #[test]
1805 fn test_parse_bus_name_inline() {
1806 if !data_available() {
1807 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1808 return;
1809 }
1810 let case = r#"
1811function mpc = testcase
1812mpc.baseMVA = 100;
1813mpc.bus = [
1814 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1815 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
1816 3 2 30 10 0 0 1 1.0 0 345 1 1.1 0.9;
1817];
1818mpc.gen = [
1819 1 100 0 300 -300 1.0 100 1 250 10;
1820];
1821mpc.branch = [
1822 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
1823 2 3 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
1824];
1825mpc.bus_name = {
1826 'SUBSTATION A';
1827 'SUBSTATION B';
1828 'SUBSTATION C';
1829};
1830"#;
1831 let net = parse_str(case).expect("failed to parse");
1832 assert_eq!(net.buses[0].name, "SUBSTATION A");
1833 assert_eq!(net.buses[1].name, "SUBSTATION B");
1834 assert_eq!(net.buses[2].name, "SUBSTATION C");
1835 }
1836
1837 #[test]
1838 fn test_parse_bus_name_activsg2000() {
1839 if !data_available() {
1840 eprintln!("SKIP: SURGE_TEST_DATA not set and tests/data not present");
1841 return;
1842 }
1843 let path = test_data_dir().join("case_ACTIVSg2000.m");
1844 let net = parse_file(&path).expect("failed to parse ACTIVSg2000");
1845
1846 assert_eq!(net.n_buses(), 2000);
1847 assert_eq!(net.buses[0].name, "ODESSA 2 0");
1849 assert_eq!(net.buses[1].name, "PRESIDIO 2 0");
1850 let unnamed = net.buses.iter().filter(|b| b.name.is_empty()).count();
1852 assert_eq!(
1853 unnamed, 0,
1854 "all 2000 buses should have names from mpc.bus_name"
1855 );
1856 }
1857
1858 #[test]
1859 fn test_bus_angles_converted_to_radians() {
1860 let case = r#"
1861function mpc = test
1862mpc.baseMVA = 100;
1863mpc.bus = [
1864 1 3 0 0 0 0 1 1.0 45.0 345 1 1.1 0.9;
1865];
1866mpc.gen = [
1867 1 0 0 300 -300 1.0 100 1 250 10 0 0 0 0 0 0 0 0 0 0 0;
1868];
1869mpc.branch = [
1870 1 1 0.01 0.1 0 100 100 100 0 0 1 -360 360;
1871];
1872"#;
1873 let net = parse_str(case).expect("failed to parse");
1874 let expected_radians = 45.0_f64.to_radians();
1875 assert!((net.buses[0].voltage_angle_rad - expected_radians).abs() < 1e-10);
1876 }
1877
1878 #[test]
1879 fn test_branch_angmin_angmax_converted_to_radians() {
1880 let case = r#"
1881function mpc = test
1882mpc.baseMVA = 100;
1883mpc.bus = [
1884 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1885 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
1886];
1887mpc.gen = [
1888 1 100 0 300 -300 1.0 100 1 250 10;
1889];
1890mpc.branch = [
1891 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -30 30;
1892];
1893"#;
1894 let net = parse_str(case).expect("failed to parse");
1895 let br = &net.branches[0];
1896 let expected_min = (-30.0_f64).to_radians();
1897 let expected_max = 30.0_f64.to_radians();
1898 assert!(
1899 (br.angle_diff_min_rad.unwrap() - expected_min).abs() < 1e-12,
1900 "angmin should be converted to radians: got {}, expected {}",
1901 br.angle_diff_min_rad.unwrap(),
1902 expected_min
1903 );
1904 assert!(
1905 (br.angle_diff_max_rad.unwrap() - expected_max).abs() < 1e-12,
1906 "angmax should be converted to radians: got {}, expected {}",
1907 br.angle_diff_max_rad.unwrap(),
1908 expected_max
1909 );
1910 }
1911
1912 #[test]
1913 fn test_branch_angmin_angmax_default_360_converted() {
1914 let case = r#"
1916function mpc = test
1917mpc.baseMVA = 100;
1918mpc.bus = [
1919 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1920 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
1921];
1922mpc.gen = [
1923 1 100 0 300 -300 1.0 100 1 250 10;
1924];
1925mpc.branch = [
1926 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
1927];
1928"#;
1929 let net = parse_str(case).expect("failed to parse");
1930 let br = &net.branches[0];
1931 let expected_min = (-360.0_f64).to_radians(); let expected_max = 360.0_f64.to_radians(); assert!(
1934 (br.angle_diff_min_rad.unwrap() - expected_min).abs() < 1e-12,
1935 "angmin -360 deg -> -2*pi rad"
1936 );
1937 assert!(
1938 (br.angle_diff_max_rad.unwrap() - expected_max).abs() < 1e-12,
1939 "angmax +360 deg -> +2*pi rad"
1940 );
1941 }
1942
1943 #[test]
1944 fn test_branch_angmin_angmax_none_when_missing() {
1945 let case = r#"
1947function mpc = test
1948mpc.baseMVA = 100;
1949mpc.bus = [
1950 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1951 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
1952];
1953mpc.gen = [
1954 1 100 0 300 -300 1.0 100 1 250 10;
1955];
1956mpc.branch = [
1957 1 2 0.01 0.1 0.02 100 100 100 0 0 1;
1958];
1959"#;
1960 let net = parse_str(case).expect("failed to parse");
1961 let br = &net.branches[0];
1962 assert!(
1963 br.angle_diff_min_rad.is_none(),
1964 "angmin should be None when column absent"
1965 );
1966 assert!(
1967 br.angle_diff_max_rad.is_none(),
1968 "angmax should be None when column absent"
1969 );
1970 }
1971
1972 #[test]
1973 fn test_parse_dc_busdc() {
1974 let content = r#"
1975function mpc = case_acdc
1976mpc.version = '2';
1977mpc.baseMVA = 100;
1978mpc.bus = [
1979 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
1980 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
1981];
1982mpc.gen = [
1983 1 100 0 300 -300 1.0 100 1 250 10;
1984];
1985mpc.branch = [
1986 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
1987];
1988mpc.busdc = [
1989 1 1 0 1.0 345 1.1 0.9;
1990 2 1 0 1.0 345 1.1 0.9;
1991];
1992mpc.convdc = [
1993 1 1 1 1 0 0 0 1.0 0.01 0.01 1 1.0 0.01 1 0.01 0.01 1 345 1.1 0.9 1.1 1;
1994 2 2 2 1 0 0 0 1.0 0.01 0.01 1 1.0 0.01 1 0.01 0.01 1 345 1.1 0.9 1.1 1;
1995];
1996mpc.branchdc = [
1997 1 2 0.052 0 0 100 0 0 1;
1998];
1999"#;
2000 let net = parse_str(content).expect("Should parse DC network format");
2001 let dc_buses: Vec<_> = net.hvdc.dc_buses().collect();
2002 let dc_converters: Vec<_> = net
2003 .hvdc
2004 .dc_converters()
2005 .filter_map(|c| c.as_vsc())
2006 .collect();
2007 let dc_branches: Vec<_> = net.hvdc.dc_branches().collect();
2008 assert_eq!(dc_buses.len(), 2);
2009 assert_eq!(dc_converters.len(), 2);
2010 assert_eq!(dc_branches.len(), 1);
2011 assert_eq!(net.hvdc.dc_grids.len(), 1);
2012
2013 assert_eq!(dc_buses[0].bus_id, 1);
2015 assert!((dc_buses[0].v_dc_pu - 1.0).abs() < 1e-10);
2016
2017 assert_eq!(dc_converters[0].dc_bus, 1);
2019 assert_eq!(dc_converters[0].ac_bus, 1);
2020 assert_eq!(dc_converters[0].control_type_dc, 1);
2021 assert_eq!(dc_converters[1].control_type_dc, 2);
2022
2023 assert_eq!(dc_branches[0].from_bus, 1);
2025 assert_eq!(dc_branches[0].to_bus, 2);
2026 assert!((dc_branches[0].r_ohm - 0.052).abs() < 1e-10);
2027 }
2028
2029 #[test]
2030 fn test_parse_dc_alternate_keys() {
2031 let content = r#"
2033function mpc = case_pglib
2034mpc.version = '2';
2035mpc.baseMVA = 100;
2036mpc.bus = [
2037 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
2038 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
2039];
2040mpc.gen = [
2041 1 100 0 300 -300 1.0 100 1 250 10;
2042];
2043mpc.branch = [
2044 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
2045];
2046mpc.dcbus = [
2047 1 1 0 1.0 345 1.1 0.9;
2048];
2049mpc.dcconv = [
2050 1 1 1 1 0 0 0 1.0 0.01 0.01 1 1.0 0.01 1 0.01 0.01 1 345 1.1 0.9 1.1 1;
2051];
2052mpc.dcbranch = [
2053 1 1 0.1 0 0 100 0 0 1;
2054];
2055"#;
2056 let net = parse_str(content).expect("Should parse pglib alternate keys");
2057 assert_eq!(net.hvdc.dc_bus_count(), 1);
2058 assert_eq!(net.hvdc.dc_converter_count(), 1);
2059 assert_eq!(net.hvdc.dc_branch_count(), 1);
2060 }
2061
2062 #[test]
2063 fn test_parse_dc_converter_loss_params() {
2064 let content = r#"
2065function mpc = case_loss
2066mpc.version = '2';
2067mpc.baseMVA = 100;
2068mpc.bus = [
2069 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
2070 2 1 50 20 0 0 1 1.0 0 345 1 1.1 0.9;
2071];
2072mpc.gen = [
2073 1 100 0 300 -300 1.0 100 1 250 10;
2074];
2075mpc.branch = [
2076 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
2077];
2078mpc.busdc = [
2079 1 1 0 1.0 345 1.1 0.9;
2080];
2081mpc.convdc = [
2082 1 1 1 1 50 10 0 1.0 0.01 0.05 1 1.01 0.02 1 0.005 0.08 1 345 1.1 0.9 1.5 1 1.103 0.887 2.885 4.371 0.005 50.0 1.0 100 -100 50 -50;
2083];
2084mpc.branchdc = [
2085 1 1 0.1 0 0 100 0 0 1;
2086];
2087"#;
2088 let net = parse_str(content).expect("Should parse converter loss params");
2089 let dc_converters: Vec<_> = net
2090 .hvdc
2091 .dc_converters()
2092 .filter_map(|c| c.as_vsc())
2093 .collect();
2094 let conv = dc_converters[0];
2095 assert!((conv.loss_constant_mw - 1.103).abs() < 1e-10);
2096 assert!((conv.loss_linear - 0.887).abs() < 1e-10);
2097 assert!((conv.loss_quadratic_rectifier - 2.885).abs() < 1e-10);
2098 assert!((conv.loss_quadratic_inverter - 4.371).abs() < 1e-10);
2099 assert!((conv.droop - 0.005).abs() < 1e-10);
2100 assert!((conv.power_dc_setpoint_mw - 50.0).abs() < 1e-10);
2101 assert!((conv.active_power_mw - 50.0).abs() < 1e-10);
2102 assert!((conv.reactive_power_mvar - 10.0).abs() < 1e-10);
2103 }
2104
2105 #[test]
2106 fn test_parse_dc_curly_brace_syntax() {
2107 let content = r#"
2109function mpc = case_curly
2110mpc.version = '2';
2111mpc.baseMVA = 100;
2112mpc.bus = [
2113 1 3 0 0 0 0 1 1.0 0 500 1 1.1 0.9;
2114 2 1 50 20 0 0 1 1.0 0 500 1 1.1 0.9;
2115];
2116mpc.gen = [
2117 1 100 0 300 -300 1.0 100 1 250 10;
2118];
2119mpc.branch = [
2120 1 2 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
2121];
2122mpc.busdc = {
2123 1 1 0 1.0 400 1.1 0.9 0;
2124 2 1 0 1.0 400 1.1 0.9 0;
2125};
2126mpc.convdc = {
2127 1 1 1 1 50 10 0 1 0.00086 0.03 1 1 0.01 0 0.0005 0.015 1 500 1.1 0.9 500.0 1 0.5517 3.031 0.0175 0.0 0.005 -58.6 1.008 0 500 -500 500 -500;
2128 2 2 2 1 -50 -10 0 1 0.00086 0.03 1 1 0.01 0 0.0005 0.015 1 500 1.1 0.9 500.0 1 0.5517 3.031 0.0175 0.0 0.007 21.9 1.000 0 500 -500 500 -500;
2129};
2130mpc.branchdc = {
2131 1 2 0.001 0 0 510 500 500 1;
2132};
2133"#;
2134 let net = parse_str(content).expect("Should parse curly-brace DC network sections");
2135 let dc_buses: Vec<_> = net.hvdc.dc_buses().collect();
2136 let dc_converters: Vec<_> = net
2137 .hvdc
2138 .dc_converters()
2139 .filter_map(|c| c.as_vsc())
2140 .collect();
2141 let dc_branches: Vec<_> = net.hvdc.dc_branches().collect();
2142 assert_eq!(dc_buses.len(), 2);
2143 assert_eq!(dc_buses[0].bus_id, 1);
2144 assert!((dc_buses[0].base_kv_dc - 400.0).abs() < 1e-10);
2145 assert_eq!(dc_converters.len(), 2);
2146 assert_eq!(dc_converters[0].dc_bus, 1);
2147 assert_eq!(dc_converters[0].ac_bus, 1);
2148 assert!((dc_converters[0].loss_constant_mw - 0.5517).abs() < 1e-10);
2149 assert!((dc_converters[0].loss_linear - 3.031).abs() < 1e-10);
2150 assert!((dc_converters[0].active_power_ac_max_mw - 500.0).abs() < 1e-10);
2152 assert!((dc_converters[0].active_power_ac_min_mw - (-500.0)).abs() < 1e-10);
2153 assert_eq!(dc_branches.len(), 1);
2154 assert!((dc_branches[0].r_ohm - 0.001).abs() < 1e-10);
2155 }
2156
2157 #[test]
2158 fn test_parse_dc_no_dc_sections() {
2159 let content = r#"
2161function mpc = case_plain
2162mpc.version = '2';
2163mpc.baseMVA = 100;
2164mpc.bus = [
2165 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
2166];
2167mpc.gen = [
2168 1 100 0 300 -300 1.0 100 1 250 10;
2169];
2170mpc.branch = [
2171 1 1 0.01 0.1 0.02 100 100 100 0 0 1 -360 360;
2172];
2173"#;
2174 let net = parse_str(content).expect("Standard MATPOWER should still parse");
2175 assert_eq!(net.hvdc.dc_bus_count(), 0);
2176 assert_eq!(net.hvdc.dc_converter_count(), 0);
2177 assert_eq!(net.hvdc.dc_branch_count(), 0);
2178 }
2179
2180 #[test]
2187 fn test_pq_curve_from_pc_fields() {
2188 let case_equal = r#"
2192function mpc = test_equal_pc
2193mpc.baseMVA = 100;
2194mpc.bus = [
2195 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
2196 2 1 80 30 0 0 1 1.0 0 345 1 1.1 0.9;
2197];
2198mpc.gen = [
2199 1 100 0 200 -100 1.0 100 1 200 0 100 100 -30 80 -20 60;
2200];
2201mpc.branch = [
2202 1 2 0.01 0.1 0.02 300 300 300 0 0 1;
2203];
2204mpc.gencost = [
2205 2 0 0 3 0 1 0;
2206];
2207"#;
2208 let net = parse_str(case_equal).expect("failed to parse equal-pc case");
2209 assert!(
2210 net.generators[0]
2211 .reactive_capability
2212 .as_ref()
2213 .is_none_or(|r| r.pq_curve.is_empty()),
2214 "equal pc1=pc2=100 → degenerate → empty pq_curve"
2215 );
2216
2217 let case_curve = r#"
2223function mpc = test_dcurve
2224mpc.baseMVA = 100;
2225mpc.bus = [
2226 1 3 0 0 0 0 1 1.0 0 345 1 1.1 0.9;
2227 2 1 80 30 0 0 1 1.0 0 345 1 1.1 0.9;
2228];
2229mpc.gen = [
2230 1 150 0 200 -100 1.0 100 1 200 0 50 200 -50 150 -20 80;
2231];
2232mpc.branch = [
2233 1 2 0.01 0.1 0.02 300 300 300 0 0 1;
2234];
2235mpc.gencost = [
2236 2 0 0 3 0 1 0;
2237];
2238"#;
2239 let net2 = parse_str(case_curve).expect("failed to parse D-curve case");
2240 let g = &net2.generators[0];
2241 let pq_curve = &g
2242 .reactive_capability
2243 .as_ref()
2244 .expect("should have reactive_capability")
2245 .pq_curve;
2246 assert_eq!(
2247 pq_curve.len(),
2248 2,
2249 "two distinct pc breakpoints → 2 pq_curve points"
2250 );
2251
2252 let (p1_pu, qmax1_pu, qmin1_pu) = pq_curve[0];
2253 let (p2_pu, qmax2_pu, qmin2_pu) = pq_curve[1];
2254
2255 assert!(
2256 (p1_pu - 0.5).abs() < 1e-10,
2257 "p1 should be 0.5 pu, got {p1_pu}"
2258 );
2259 assert!(
2260 (qmax1_pu - 1.5).abs() < 1e-10,
2261 "qmax1 should be 1.5 pu, got {qmax1_pu}"
2262 );
2263 assert!(
2264 (qmin1_pu - (-0.5)).abs() < 1e-10,
2265 "qmin1 should be -0.5 pu, got {qmin1_pu}"
2266 );
2267
2268 assert!(
2269 (p2_pu - 2.0).abs() < 1e-10,
2270 "p2 should be 2.0 pu, got {p2_pu}"
2271 );
2272 assert!(
2273 (qmax2_pu - 0.8).abs() < 1e-10,
2274 "qmax2 should be 0.8 pu, got {qmax2_pu}"
2275 );
2276 assert!(
2277 (qmin2_pu - (-0.2)).abs() < 1e-10,
2278 "qmin2 should be -0.2 pu, got {qmin2_pu}"
2279 );
2280 }
2281}