Skip to main content

surge_io/psse/
rawx.rs

1// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0
2//! PSS/E RAWX (.rawx) JSON file parser.
3//!
4//! Parses the JSON-based PSS/E RAWX power flow data format introduced in PSS/E v35.
5//! RAWX encodes the same power system data as the positional-text RAW format but uses
6//! a structured JSON schema with self-describing field names per section.
7//!
8//! # JSON Structure
9//! ```json
10//! {
11//!   "network": {
12//!     "caseid": { "fields": [...], "data": [...] },
13//!     "bus":    { "fields": [...], "data": [[...], ...] },
14//!     ...
15//!   }
16//! }
17//! ```
18//!
19//! The `caseid` section has a flat `data` array; all other sections have `data`
20//! as an array of arrays (one per record).
21
22use std::collections::HashMap;
23use std::path::Path;
24
25use serde_json::Value;
26use surge_network::Network;
27use surge_network::network::AreaSchedule;
28use surge_network::network::facts::FactsType;
29use surge_network::network::{Branch, BranchType, Bus, BusType, Generator};
30use surge_network::network::{FactsDevice, FactsMode};
31use surge_network::network::{LccConverterTerminal, LccHvdcControlMode, LccHvdcLink};
32use surge_network::network::{
33    VscConverterAcControlMode, VscConverterTerminal, VscHvdcControlMode, VscHvdcLink,
34};
35use thiserror::Error;
36
37use super::reader::{apply_cz_conversion, compute_winding_tap_pu, make_xfmr_branch};
38
39#[derive(Error, Debug)]
40pub enum RawxError {
41    #[error("I/O error: {0}")]
42    Io(#[from] std::io::Error),
43
44    #[error("JSON parse error: {0}")]
45    Json(#[from] serde_json::Error),
46
47    #[error("missing section: {0}")]
48    MissingSection(String),
49
50    #[error("missing field '{field}' in section '{section}'")]
51    MissingField { section: String, field: String },
52
53    #[error("invalid value in section '{section}', field '{field}': {message}")]
54    InvalidValue {
55        section: String,
56        field: String,
57        message: String,
58    },
59}
60
61/// Parse a PSS/E RAWX file from disk.
62pub fn parse_file(path: &Path) -> Result<Network, RawxError> {
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
72/// Parse a PSS/E RAWX case from a JSON string.
73pub fn parse_str(content: &str) -> Result<Network, RawxError> {
74    parse_string_with_name(content, "unknown")
75}
76
77fn parse_string_with_name(content: &str, name: &str) -> Result<Network, RawxError> {
78    let root: Value = serde_json::from_str(content)?;
79    let net_obj = root
80        .get("network")
81        .ok_or_else(|| RawxError::MissingSection("network".into()))?;
82
83    // Parse caseid (flat data array)
84    let (sbase, _version, freq_hz) = parse_caseid(net_obj)?;
85
86    let mut network = Network::new(name);
87    network.base_mva = sbase;
88    network.freq_hz = freq_hz;
89
90    // Bus data
91    let (buses, bus_idx, bus_kv) = parse_buses(net_obj)?;
92    network.buses = buses;
93
94    // Post-parse: fix vmin/vmax in kV rather than p.u.
95    crate::parse_utils::sanitize_voltage_limits(&mut network);
96
97    // Load data
98    if let Some(section) = get_section(net_obj, "load") {
99        parse_loads(&section, &bus_idx, &mut network)?;
100    }
101
102    // Fixed shunt data
103    if let Some(section) = get_section(net_obj, "fixshunt") {
104        parse_fixshunts(&section, &bus_idx, &mut network)?;
105    }
106
107    // Generator data
108    if let Some(section) = get_section(net_obj, "generator") {
109        network.generators = parse_generators(&section)?;
110    }
111
112    // AC line (branch) data
113    if let Some(section) = get_section(net_obj, "acline") {
114        network.branches = parse_aclines(&section)?;
115    }
116
117    // Transformer data
118    if let Some(section) = get_section(net_obj, "transformer") {
119        let (xfmrs, star_buses) = parse_transformers(&section, sbase, &bus_kv)?;
120        network.buses.extend(star_buses);
121        network.branches.extend(xfmrs);
122    }
123
124    // Area interchange data
125    if let Some(section) = get_section(net_obj, "area") {
126        network.area_schedules = parse_areas(&section)?;
127    }
128
129    // Two-terminal DC line data
130    if let Some(section) = get_section(net_obj, "twotermdc") {
131        network.hvdc.links = parse_twotermdc(&section)?
132            .into_iter()
133            .map(surge_network::network::HvdcLink::Lcc)
134            .collect();
135    }
136
137    // VSC DC line data
138    if let Some(section) = get_section(net_obj, "vscdc") {
139        network.hvdc.links.extend(
140            parse_vscdc(&section)?
141                .into_iter()
142                .map(surge_network::network::HvdcLink::Vsc),
143        );
144    }
145
146    // FACTS device data
147    if let Some(section) = get_section(net_obj, "facts") {
148        network.facts_devices = parse_facts(&section)?;
149    }
150
151    // Switched shunt data
152    if let Some(section) = get_section(net_obj, "swshunt") {
153        parse_switched_shunts(&section, &bus_idx, &mut network)?;
154    }
155    Ok(network)
156}
157
158// ---------------------------------------------------------------------------
159// Section helper
160// ---------------------------------------------------------------------------
161
162/// A parsed RAWX section with field-name → column-index mapping.
163struct RawxSection<'a> {
164    fields: HashMap<String, usize>,
165    data: &'a Vec<Value>,
166}
167
168impl<'a> RawxSection<'a> {
169    fn get_f64(&self, row: &Value, field: &str) -> Option<f64> {
170        let idx = *self.fields.get(field)?;
171        let arr = row.as_array()?;
172        arr.get(idx)?.as_f64()
173    }
174
175    fn get_f64_or(&self, row: &Value, field: &str, default: f64) -> f64 {
176        self.get_f64(row, field).unwrap_or(default)
177    }
178
179    fn get_i64(&self, row: &Value, field: &str) -> Option<i64> {
180        let idx = *self.fields.get(field)?;
181        let arr = row.as_array()?;
182        let val = arr.get(idx)?;
183        val.as_i64().or_else(|| val.as_f64().map(|f| f as i64))
184    }
185
186    fn get_i64_or(&self, row: &Value, field: &str, default: i64) -> i64 {
187        self.get_i64(row, field).unwrap_or(default)
188    }
189
190    fn get_str<'b>(&self, row: &'b Value, field: &str) -> Option<&'b str> {
191        let idx = *self.fields.get(field)?;
192        let arr = row.as_array()?;
193        arr.get(idx)?.as_str()
194    }
195
196    fn get_str_or<'b>(&self, row: &'b Value, field: &str, default: &'b str) -> &'b str {
197        self.get_str(row, field).unwrap_or(default)
198    }
199}
200
201fn get_section<'a>(net_obj: &'a Value, name: &str) -> Option<RawxSection<'a>> {
202    let section = net_obj.get(name)?;
203    let fields_arr = section.get("fields")?.as_array()?;
204    let data = section.get("data")?.as_array()?;
205    if data.is_empty() {
206        return None;
207    }
208    let mut fields = HashMap::new();
209    for (i, f) in fields_arr.iter().enumerate() {
210        if let Some(s) = f.as_str() {
211            fields.insert(s.to_lowercase(), i);
212        }
213    }
214    Some(RawxSection { fields, data })
215}
216
217// ---------------------------------------------------------------------------
218// Case ID
219// ---------------------------------------------------------------------------
220
221fn parse_caseid(net_obj: &Value) -> Result<(f64, u32, f64), RawxError> {
222    let caseid = net_obj
223        .get("caseid")
224        .ok_or_else(|| RawxError::MissingSection("caseid".into()))?;
225    let fields_arr = caseid
226        .get("fields")
227        .and_then(|v| v.as_array())
228        .ok_or_else(|| RawxError::MissingField {
229            section: "caseid".into(),
230            field: "fields".into(),
231        })?;
232    let data = caseid
233        .get("data")
234        .and_then(|v| v.as_array())
235        .ok_or_else(|| RawxError::MissingField {
236            section: "caseid".into(),
237            field: "data".into(),
238        })?;
239
240    let mut field_map: HashMap<String, usize> = HashMap::new();
241    for (i, f) in fields_arr.iter().enumerate() {
242        if let Some(s) = f.as_str() {
243            field_map.insert(s.to_lowercase(), i);
244        }
245    }
246
247    let get = |name: &str| -> Option<f64> {
248        let idx = *field_map.get(name)?;
249        data.get(idx)?.as_f64()
250    };
251
252    let sbase = get("sbase").unwrap_or(100.0);
253    let rev = get("rev").unwrap_or(35.0) as u32;
254    let freq = get("basfrq").unwrap_or(60.0);
255    let freq_hz = if freq > 0.0 { freq } else { 60.0 };
256
257    Ok((sbase, rev, freq_hz))
258}
259
260// ---------------------------------------------------------------------------
261// Bus Data
262// ---------------------------------------------------------------------------
263
264#[allow(clippy::type_complexity)]
265fn parse_buses(
266    net_obj: &Value,
267) -> Result<(Vec<Bus>, HashMap<u32, usize>, HashMap<u32, f64>), RawxError> {
268    let section =
269        get_section(net_obj, "bus").ok_or_else(|| RawxError::MissingSection("bus".into()))?;
270
271    let mut buses = Vec::new();
272    let mut bus_idx: HashMap<u32, usize> = HashMap::new();
273    let mut bus_kv: HashMap<u32, f64> = HashMap::new();
274
275    for row in section.data {
276        let number = section.get_i64_or(row, "ibus", 0) as u32;
277        if number == 0 {
278            continue;
279        }
280        let name = section.get_str_or(row, "name", "").trim().to_string();
281        let base_kv = section.get_f64_or(row, "baskv", 1.0);
282        let ide = section.get_i64_or(row, "ide", 1);
283        let bus_type = match ide {
284            2 => BusType::PV,
285            3 => BusType::Slack,
286            4 => BusType::Isolated,
287            _ => BusType::PQ,
288        };
289        let area = section.get_i64_or(row, "area", 1) as u32;
290        let zone = section.get_i64_or(row, "zone", 1) as u32;
291        let vm = section.get_f64_or(row, "vm", 1.0);
292        let va_deg = section.get_f64_or(row, "va", 0.0);
293        let vmax = section.get_f64_or(row, "nvhi", 1.1);
294        let vmin = section.get_f64_or(row, "nvlo", 0.9);
295
296        let idx = buses.len();
297        bus_idx.insert(number, idx);
298        bus_kv.insert(number, base_kv);
299
300        buses.push(Bus {
301            number,
302            name,
303            bus_type,
304            shunt_conductance_mw: 0.0,
305            shunt_susceptance_mvar: 0.0,
306            area,
307            voltage_magnitude_pu: vm,
308            voltage_angle_rad: va_deg.to_radians(),
309            base_kv,
310            zone,
311            voltage_max_pu: vmax,
312            voltage_min_pu: vmin,
313            island_id: 0,
314            latitude: None,
315            longitude: None,
316            ..Bus::new(0, BusType::PQ, 0.0)
317        });
318    }
319
320    Ok((buses, bus_idx, bus_kv))
321}
322
323// ---------------------------------------------------------------------------
324// Load Data
325// ---------------------------------------------------------------------------
326
327fn parse_loads(
328    section: &RawxSection,
329    _bus_idx: &HashMap<u32, usize>,
330    network: &mut Network,
331) -> Result<(), RawxError> {
332    for row in section.data {
333        let bus = section.get_i64_or(row, "ibus", 0) as u32;
334        let stat = section.get_i64_or(row, "stat", 1);
335        if stat == 0 || bus == 0 {
336            continue;
337        }
338        let pl = section.get_f64_or(row, "pl", 0.0);
339        let ql = section.get_f64_or(row, "ql", 0.0);
340
341        use surge_network::network::Load;
342        network.loads.push(Load::new(bus, pl, ql));
343    }
344    Ok(())
345}
346
347// ---------------------------------------------------------------------------
348// Fixed Shunt Data
349// ---------------------------------------------------------------------------
350
351fn parse_fixshunts(
352    section: &RawxSection,
353    bus_idx: &HashMap<u32, usize>,
354    network: &mut Network,
355) -> Result<(), RawxError> {
356    for row in section.data {
357        let bus = section.get_i64_or(row, "ibus", 0) as u32;
358        let stat = section.get_i64_or(row, "stat", 1);
359        if stat == 0 || bus == 0 {
360            continue;
361        }
362        let gl = section.get_f64_or(row, "gl", 0.0);
363        let bl = section.get_f64_or(row, "bl", 0.0);
364
365        if let Some(&idx) = bus_idx.get(&bus) {
366            network.buses[idx].shunt_conductance_mw += gl;
367            network.buses[idx].shunt_susceptance_mvar += bl;
368        }
369    }
370    Ok(())
371}
372
373// ---------------------------------------------------------------------------
374// Generator Data
375// ---------------------------------------------------------------------------
376
377fn parse_generators(section: &RawxSection) -> Result<Vec<Generator>, RawxError> {
378    let mut generators = Vec::new();
379
380    for row in section.data {
381        let bus = section.get_i64_or(row, "ibus", 0) as u32;
382        if bus == 0 {
383            continue;
384        }
385        let machid = section
386            .get_str(row, "machid")
387            .map(|s| s.trim().trim_matches('\'').to_string());
388        let stat = section.get_i64_or(row, "stat", 1);
389        let pg = section.get_f64_or(row, "pg", 0.0);
390        let qg = section.get_f64_or(row, "qg", 0.0);
391        let qt = section.get_f64_or(row, "qt", 9999.0); // qmax
392        let qb = section.get_f64_or(row, "qb", -9999.0); // qmin
393        let vs = section.get_f64_or(row, "vs", 1.0);
394        let mbase = section.get_f64_or(row, "mbase", 100.0);
395        let pt = section.get_f64_or(row, "pt", 9999.0); // pmax
396        let pb = section.get_f64_or(row, "pb", 0.0); // pmin
397        let zx = section.get_f64_or(row, "zx", 0.0); // machine leakage reactance
398
399        // Reactive capability curve
400        let pc1 = section.get_f64(row, "pc1");
401        let pc2 = section.get_f64(row, "pc2");
402        let qc1min = section.get_f64(row, "qc1min");
403        let qc1max = section.get_f64(row, "qc1max");
404        let qc2min = section.get_f64(row, "qc2min");
405        let qc2max = section.get_f64(row, "qc2max");
406
407        let mut g = Generator::new(bus, pg, vs);
408        g.machine_id = machid;
409        g.q = qg;
410        g.qmax = qt;
411        g.qmin = qb;
412        g.machine_base_mva = mbase;
413        g.pmax = pt;
414        g.pmin = pb;
415        g.in_service = stat != 0;
416        if zx != 0.0 {
417            g.fault_data.get_or_insert_with(Default::default).xs = Some(zx);
418        }
419        if pc1.is_some()
420            || pc2.is_some()
421            || qc1min.is_some()
422            || qc1max.is_some()
423            || qc2min.is_some()
424            || qc2max.is_some()
425        {
426            let rc = g.reactive_capability.get_or_insert_with(Default::default);
427            rc.pc1 = pc1;
428            rc.pc2 = pc2;
429            rc.qc1min = qc1min;
430            rc.qc1max = qc1max;
431            rc.qc2min = qc2min;
432            rc.qc2max = qc2max;
433        }
434
435        generators.push(g);
436    }
437
438    Ok(generators)
439}
440
441// ---------------------------------------------------------------------------
442// AC Line (Branch) Data
443// ---------------------------------------------------------------------------
444
445fn parse_aclines(section: &RawxSection) -> Result<Vec<Branch>, RawxError> {
446    let mut branches = Vec::new();
447
448    for row in section.data {
449        let from_bus = section.get_i64_or(row, "ibus", 0) as u32;
450        let to_bus = section.get_i64_or(row, "jbus", 0) as u32;
451        if from_bus == 0 || to_bus == 0 {
452            continue;
453        }
454        let ckt_str = section.get_str_or(row, "ckt", "1").trim().to_string();
455        let circuit = ckt_str.trim_matches('\'').trim().to_string();
456        let r = section.get_f64_or(row, "rpu", 0.0);
457        let x = section.get_f64_or(row, "xpu", 0.01);
458        let b = section.get_f64_or(row, "bpu", 0.0);
459        let stat = section.get_i64_or(row, "stat", 1);
460
461        let rate_a = section.get_f64_or(row, "rate1", 0.0);
462        let rate_b = section.get_f64_or(row, "rate2", 0.0);
463        let rate_c = section.get_f64_or(row, "rate3", 0.0);
464
465        // Terminal shunt admittances (GI/BI/GJ/BJ) — accumulate into branch
466        // These are rarely used but supported.
467        let _gi = section.get_f64_or(row, "gi", 0.0);
468        let _bi = section.get_f64_or(row, "bi", 0.0);
469        let _gj = section.get_f64_or(row, "gj", 0.0);
470        let _bj = section.get_f64_or(row, "bj", 0.0);
471
472        let mut branch = Branch::new_line(from_bus, to_bus, r, x, b);
473        branch.circuit = circuit;
474        branch.rating_a_mva = rate_a;
475        branch.rating_b_mva = rate_b;
476        branch.rating_c_mva = rate_c;
477        branch.in_service = stat != 0;
478
479        branches.push(branch);
480    }
481
482    Ok(branches)
483}
484
485// ---------------------------------------------------------------------------
486// Transformer Data
487// ---------------------------------------------------------------------------
488
489fn parse_transformers(
490    section: &RawxSection,
491    sbase: f64,
492    bus_kv: &HashMap<u32, f64>,
493) -> Result<(Vec<Branch>, Vec<Bus>), RawxError> {
494    let mut transformers = Vec::new();
495    let mut star_buses = Vec::new();
496    let mut max_bus_num: u32 = bus_kv.keys().copied().max().unwrap_or(0);
497
498    for row in section.data {
499        let from_bus = section.get_i64_or(row, "ibus", 0) as u32;
500        let to_bus = section.get_i64_or(row, "jbus", 0).unsigned_abs() as u32;
501        let k = section.get_i64_or(row, "kbus", 0);
502        if from_bus == 0 || to_bus == 0 {
503            continue;
504        }
505
506        let ckt_str = section.get_str_or(row, "ckt", "1").trim().to_string();
507        let circuit = ckt_str.trim_matches('\'').trim().to_string();
508
509        let cw = section.get_i64_or(row, "cw", 1) as u32;
510        let cz = section.get_i64_or(row, "cz", 1) as u32;
511        let mag1 = section.get_f64_or(row, "mag1", 0.0);
512        let mag2 = section.get_f64_or(row, "mag2", 0.0);
513        let stat = section.get_i64_or(row, "stat", 1);
514
515        // Record 2 fields (impedance)
516        let r12_raw = section.get_f64_or(row, "r1-2", 0.0);
517        let x12_raw = section.get_f64_or(row, "x1-2", 0.01);
518        let sbase12 = section.get_f64_or(row, "sbase1-2", sbase);
519
520        let (r12, x12) = apply_cz_conversion(r12_raw, x12_raw, sbase12, sbase, cz);
521
522        let is_3winding = k != 0;
523        let k_bus = k.unsigned_abs() as u32;
524
525        if is_3winding {
526            // 3-winding transformer: star (Y) topology expansion
527            let r23_raw = section.get_f64_or(row, "r2-3", 0.0);
528            let x23_raw = section.get_f64_or(row, "x2-3", 0.01);
529            let sbase23 = section.get_f64_or(row, "sbase2-3", sbase);
530            let r31_raw = section.get_f64_or(row, "r3-1", 0.0);
531            let x31_raw = section.get_f64_or(row, "x3-1", 0.01);
532            let sbase31 = section.get_f64_or(row, "sbase3-1", sbase);
533            let vmstar = section.get_f64_or(row, "vmstar", 1.0);
534            let anstar_deg = section.get_f64_or(row, "anstar", 0.0);
535
536            let (r23, x23) = apply_cz_conversion(r23_raw, x23_raw, sbase23, sbase, cz);
537            let (r31, x31) = apply_cz_conversion(r31_raw, x31_raw, sbase31, sbase, cz);
538
539            // Star-delta impedance conversion
540            let r1 = (r12 + r31 - r23) / 2.0;
541            let x1 = (x12 + x31 - x23) / 2.0;
542            let r2 = (r12 + r23 - r31) / 2.0;
543            let x2 = (x12 + x23 - x31) / 2.0;
544            let r3 = (r23 + r31 - r12) / 2.0;
545            let x3 = (x23 + x31 - x12) / 2.0;
546
547            // Winding taps
548            let windv1 = section.get_f64_or(row, "windv1", 1.0);
549            let nomv1 = section.get_f64_or(row, "nomv1", 0.0);
550            let ang1 = section.get_f64_or(row, "ang1", 0.0);
551            let rata1 = section.get_f64_or(row, "wdg1rate1", 0.0);
552            let ratb1 = section.get_f64_or(row, "wdg1rate2", 0.0);
553            let ratc1 = section.get_f64_or(row, "wdg1rate3", 0.0);
554
555            let windv2 = section.get_f64_or(row, "windv2", 1.0);
556            let nomv2 = section.get_f64_or(row, "nomv2", 0.0);
557            let ang2 = section.get_f64_or(row, "ang2", 0.0);
558            let rata2 = section.get_f64_or(row, "wdg2rate1", 0.0);
559            let ratb2 = section.get_f64_or(row, "wdg2rate2", 0.0);
560            let ratc2 = section.get_f64_or(row, "wdg2rate3", 0.0);
561
562            let windv3 = section.get_f64_or(row, "windv3", 1.0);
563            let nomv3 = section.get_f64_or(row, "nomv3", 0.0);
564            let ang3 = section.get_f64_or(row, "ang3", 0.0);
565            let rata3 = section.get_f64_or(row, "wdg3rate1", 0.0);
566            let ratb3 = section.get_f64_or(row, "wdg3rate2", 0.0);
567            let ratc3 = section.get_f64_or(row, "wdg3rate3", 0.0);
568
569            let bkv1 = bus_kv.get(&from_bus).copied().unwrap_or(1.0);
570            let bkv2 = bus_kv.get(&to_bus).copied().unwrap_or(1.0);
571            let bkv3 = bus_kv.get(&k_bus).copied().unwrap_or(1.0);
572            let tap1 = compute_winding_tap_pu(windv1, nomv1, bkv1, cw);
573            let tap2 = compute_winding_tap_pu(windv2, nomv2, bkv2, cw);
574            let tap3 = compute_winding_tap_pu(windv3, nomv3, bkv3, cw);
575
576            // Create fictitious star bus
577            max_bus_num += 1;
578            let star_bus_num = max_bus_num;
579            star_buses.push(Bus {
580                number: star_bus_num,
581                name: format!("STAR_{from_bus}_{to_bus}_{k_bus}"),
582                bus_type: BusType::PQ,
583                shunt_conductance_mw: 0.0,
584                shunt_susceptance_mvar: 0.0,
585                area: 1,
586                voltage_magnitude_pu: vmstar,
587                voltage_angle_rad: anstar_deg.to_radians(),
588                base_kv: bkv1.max(bkv2).max(bkv3).max(1.0),
589                zone: 1,
590                voltage_max_pu: 1.1,
591                voltage_min_pu: 0.9,
592                island_id: 0,
593                latitude: None,
594                longitude: None,
595                ..Bus::new(0, BusType::PQ, 0.0)
596            });
597
598            let in_service = stat > 0;
599
600            // Winding 1 → star
601            let mut w1 = make_xfmr_branch(
602                from_bus,
603                star_bus_num,
604                circuit.clone(),
605                r1,
606                x1,
607                rata1,
608                ratb1,
609                ratc1,
610                tap1,
611                ang1,
612                in_service,
613                mag1,
614                mag2,
615            );
616            w1.branch_type = BranchType::Transformer3W;
617            transformers.push(w1);
618            // Winding 2 → star
619            let mut w2 = make_xfmr_branch(
620                to_bus,
621                star_bus_num,
622                circuit.clone(),
623                r2,
624                x2,
625                rata2,
626                ratb2,
627                ratc2,
628                tap2,
629                ang2,
630                in_service,
631                0.0,
632                0.0,
633            );
634            w2.branch_type = BranchType::Transformer3W;
635            transformers.push(w2);
636            // Winding 3 → star
637            let mut w3 = make_xfmr_branch(
638                k_bus,
639                star_bus_num,
640                circuit,
641                r3,
642                x3,
643                rata3,
644                ratb3,
645                ratc3,
646                tap3,
647                ang3,
648                in_service,
649                0.0,
650                0.0,
651            );
652            w3.branch_type = BranchType::Transformer3W;
653            transformers.push(w3);
654        } else {
655            // 2-winding transformer
656            let windv1 = section.get_f64_or(row, "windv1", 1.0);
657            let nomv1 = section.get_f64_or(row, "nomv1", 0.0);
658            let ang1 = section.get_f64_or(row, "ang1", 0.0);
659            let rata1 = section.get_f64_or(row, "wdg1rate1", 0.0);
660            let ratb1 = section.get_f64_or(row, "wdg1rate2", 0.0);
661            let ratc1 = section.get_f64_or(row, "wdg1rate3", 0.0);
662
663            let windv2 = section.get_f64_or(row, "windv2", 1.0);
664            let nomv2 = section.get_f64_or(row, "nomv2", 0.0);
665
666            // Compute 2-winding tap ratio based on CW code
667            let tap = match cw {
668                1 => {
669                    if windv2 != 0.0 {
670                        windv1 / windv2
671                    } else {
672                        windv1
673                    }
674                }
675                2 => {
676                    let bkv1 = bus_kv.get(&from_bus).copied().unwrap_or(1.0);
677                    let bkv2 = bus_kv.get(&to_bus).copied().unwrap_or(1.0);
678                    let t1 = if bkv1 > 0.0 { windv1 / bkv1 } else { windv1 };
679                    let t2 = if bkv2 > 0.0 { windv2 / bkv2 } else { windv2 };
680                    if t2 != 0.0 { t1 / t2 } else { t1 }
681                }
682                3 => {
683                    let bkv1 = bus_kv.get(&from_bus).copied().unwrap_or(1.0);
684                    let bkv2 = bus_kv.get(&to_bus).copied().unwrap_or(1.0);
685                    let n1 = if nomv1 > 0.0 { nomv1 } else { bkv1 };
686                    let n2 = if nomv2 > 0.0 { nomv2 } else { bkv2 };
687                    let t1 = if bkv1 > 0.0 {
688                        windv1 * n1 / bkv1
689                    } else {
690                        windv1
691                    };
692                    let t2 = if bkv2 > 0.0 {
693                        windv2 * n2 / bkv2
694                    } else {
695                        windv2
696                    };
697                    if t2 != 0.0 { t1 / t2 } else { t1 }
698                }
699                _ => windv1,
700            };
701
702            transformers.push(make_xfmr_branch(
703                from_bus,
704                to_bus,
705                circuit,
706                r12,
707                x12,
708                rata1,
709                ratb1,
710                ratc1,
711                tap,
712                ang1,
713                stat > 0,
714                mag1,
715                mag2,
716            ));
717        }
718    }
719
720    Ok((transformers, star_buses))
721}
722
723// ---------------------------------------------------------------------------
724// Area Interchange Data
725// ---------------------------------------------------------------------------
726
727fn parse_areas(section: &RawxSection) -> Result<Vec<AreaSchedule>, RawxError> {
728    let mut areas = Vec::new();
729    for row in section.data {
730        let number = section.get_i64_or(row, "iarea", 0) as u32;
731        if number == 0 {
732            continue;
733        }
734        let slack_bus = section.get_i64_or(row, "isw", 0) as u32;
735        let pdes = section.get_f64_or(row, "pdes", 0.0);
736        let ptol = section.get_f64_or(row, "ptol", 10.0);
737        let name = section
738            .get_str(row, "arnam")
739            .unwrap_or("")
740            .trim()
741            .trim_matches('\'')
742            .to_string();
743
744        areas.push(AreaSchedule {
745            number,
746            slack_bus,
747            p_desired_mw: pdes,
748            p_tolerance_mw: ptol,
749            name,
750        });
751    }
752    Ok(areas)
753}
754
755// ---------------------------------------------------------------------------
756// Switched Shunt Data
757// ---------------------------------------------------------------------------
758
759fn parse_switched_shunts(
760    section: &RawxSection,
761    _bus_idx: &HashMap<u32, usize>,
762    network: &mut Network,
763) -> Result<(), RawxError> {
764    use crate::parse_utils::{RawSwitchedShunt, apply_switched_shunts};
765
766    let mut raw_shunts: Vec<RawSwitchedShunt> = Vec::new();
767
768    for row in section.data {
769        let bus = section.get_i64_or(row, "ibus", 0) as u32;
770        if bus == 0 {
771            continue;
772        }
773        let modsw = section.get_i64_or(row, "modsw", 0) as i32;
774        let stat = section.get_i64_or(row, "stat", 1) as i32;
775        let vswhi = section.get_f64_or(row, "vswhi", 1.1);
776        let vswlo = section.get_f64_or(row, "vswlo", 0.9);
777        let swrem = section.get_i64_or(row, "swrem", 0) as u32;
778        let binit = section.get_f64_or(row, "binit", 0.0);
779
780        // Parse up to 8 (N, B) step blocks.
781        let mut blocks = Vec::new();
782        for i in 1u32..=8 {
783            let nk = section.get_i64_or(row, &format!("n{i}"), 0) as i32;
784            let bk = section.get_f64_or(row, &format!("b{i}"), 0.0);
785            blocks.push((nk, bk));
786        }
787
788        raw_shunts.push(RawSwitchedShunt {
789            bus,
790            modsw,
791            stat,
792            vswhi,
793            vswlo,
794            swrem,
795            binit,
796            blocks,
797        });
798    }
799
800    let base_mva = network.base_mva;
801    apply_switched_shunts(network, &raw_shunts, base_mva);
802    Ok(())
803}
804
805// ---------------------------------------------------------------------------
806// Two-Terminal DC Line Data
807// ---------------------------------------------------------------------------
808
809fn parse_twotermdc(section: &RawxSection) -> Result<Vec<LccHvdcLink>, RawxError> {
810    let mut lcc_links = Vec::new();
811    for row in section.data {
812        let name = section
813            .get_str(row, "name")
814            .unwrap_or("")
815            .trim()
816            .trim_matches('\'')
817            .to_string();
818        let mdc = section.get_i64_or(row, "mdc", 0) as u32;
819        let resistance_ohm = section.get_f64_or(row, "resistance_ohm", 0.0);
820        let setvl = section.get_f64_or(row, "setvl", 0.0);
821        let vschd = section.get_f64_or(row, "vschd", 0.0);
822        let vcmod = section.get_f64_or(row, "vcmod", 0.0);
823        let rcomp = section.get_f64_or(row, "rcomp", 0.0);
824        let delti = section.get_f64_or(row, "delti", 0.0);
825
826        // Rectifier
827        let ipr = section.get_i64_or(row, "ipr", 0) as u32;
828        let nbr = section.get_i64_or(row, "nbr", 6) as u32;
829        let anmxr = section.get_f64_or(row, "anmxr", 90.0);
830        let anmnr = section.get_f64_or(row, "anmnr", 0.0);
831        let rcr = section.get_f64_or(row, "rcr", 0.0);
832        let xcr = section.get_f64_or(row, "xcr", 0.0);
833        let ebasr = section.get_f64_or(row, "ebasr", 0.0);
834        let trr = section.get_f64_or(row, "trr", 1.0);
835        let tapr = section.get_f64_or(row, "tapr", 1.0);
836        let tmxr = section.get_f64_or(row, "tmxr", 1.5);
837        let tmnr = section.get_f64_or(row, "tmnr", 0.51);
838        let stpr = section.get_f64_or(row, "stpr", 0.00625);
839
840        // Inverter
841        let ipi = section.get_i64_or(row, "ipi", 0) as u32;
842        let nbi = section.get_i64_or(row, "nbi", 6) as u32;
843        let anmxi = section.get_f64_or(row, "anmxi", 90.0);
844        let anmni = section.get_f64_or(row, "anmni", 0.0);
845        let rci = section.get_f64_or(row, "rci", 0.0);
846        let xci = section.get_f64_or(row, "xci", 0.0);
847        let ebasi = section.get_f64_or(row, "ebasi", 0.0);
848        let tri = section.get_f64_or(row, "tri", 1.0);
849        let tapi = section.get_f64_or(row, "tapi", 1.0);
850        let tmxi = section.get_f64_or(row, "tmxi", 1.5);
851        let tmni = section.get_f64_or(row, "tmni", 0.51);
852        let stpi = section.get_f64_or(row, "stpi", 0.00625);
853
854        lcc_links.push(LccHvdcLink {
855            name,
856            mode: LccHvdcControlMode::from_u32(mdc),
857            resistance_ohm,
858            scheduled_setpoint: setvl,
859            scheduled_voltage_kv: vschd,
860            voltage_mode_switch_kv: vcmod,
861            compounding_resistance_ohm: rcomp,
862            current_margin_ka: delti,
863            meter: 'I',
864            voltage_min_kv: 0.0,
865            ac_dc_iteration_max: 20,
866            ac_dc_iteration_acceleration: 1.0,
867            rectifier: LccConverterTerminal {
868                bus: ipr,
869                n_bridges: nbr,
870                alpha_max: anmxr,
871                alpha_min: anmnr,
872                commutation_resistance_ohm: rcr,
873                commutation_reactance_ohm: xcr,
874                base_voltage_kv: ebasr,
875                turns_ratio: trr,
876                tap: tapr,
877                tap_max: tmxr,
878                tap_min: tmnr,
879                tap_step: stpr,
880                in_service: true,
881            },
882            inverter: LccConverterTerminal {
883                bus: ipi,
884                n_bridges: nbi,
885                alpha_max: anmxi,
886                alpha_min: anmni,
887                commutation_resistance_ohm: rci,
888                commutation_reactance_ohm: xci,
889                base_voltage_kv: ebasi,
890                turns_ratio: tri,
891                tap: tapi,
892                tap_max: tmxi,
893                tap_min: tmni,
894                tap_step: stpi,
895                in_service: true,
896            },
897            // PSS/E raw data doesn't carry a variable-P range; leave at 0/0
898            // so the link is treated as fixed at `scheduled_setpoint` by the
899            // joint AC-DC OPF (caller can set a range later if wanted).
900            p_dc_min_mw: 0.0,
901            p_dc_max_mw: 0.0,
902        });
903    }
904    Ok(lcc_links)
905}
906
907// ---------------------------------------------------------------------------
908// VSC DC Line Data
909// ---------------------------------------------------------------------------
910
911fn parse_vscdc(section: &RawxSection) -> Result<Vec<VscHvdcLink>, RawxError> {
912    let mut vsc_lines = Vec::new();
913    for row in section.data {
914        let name = section
915            .get_str(row, "name")
916            .unwrap_or("")
917            .trim()
918            .trim_matches('\'')
919            .to_string();
920        let mdc = section.get_i64_or(row, "mdc", 0) as u32;
921        let resistance_ohm = section.get_f64_or(row, "resistance_ohm", 0.0);
922
923        let ibus1 = section.get_i64_or(row, "ibus1", 0) as u32;
924        let mode1 = section.get_i64_or(row, "mode1", 1) as u32;
925        let acset1 = section.get_f64_or(row, "acset1", 1.0);
926        let dcset1 = section.get_f64_or(row, "dcset1", 0.0);
927        let aloss1 = section.get_f64_or(row, "aloss1", 0.0);
928        let bloss1 = section.get_f64_or(row, "bloss1", 0.0);
929
930        let ibus2 = section.get_i64_or(row, "ibus2", 0) as u32;
931        let mode2 = section.get_i64_or(row, "mode2", 1) as u32;
932        let acset2 = section.get_f64_or(row, "acset2", 1.0);
933        let dcset2 = section.get_f64_or(row, "dcset2", 0.0);
934        let aloss2 = section.get_f64_or(row, "aloss2", 0.0);
935        let bloss2 = section.get_f64_or(row, "bloss2", 0.0);
936
937        vsc_lines.push(VscHvdcLink {
938            name,
939            mode: VscHvdcControlMode::from_u32(mdc),
940            resistance_ohm,
941            converter1: VscConverterTerminal {
942                bus: ibus1,
943                control_mode: VscConverterAcControlMode::from_u32(mode1),
944                ac_setpoint: acset1,
945                dc_setpoint: dcset1,
946                loss_constant_mw: aloss1,
947                loss_linear: bloss1,
948                ..VscConverterTerminal::default()
949            },
950            converter2: VscConverterTerminal {
951                bus: ibus2,
952                control_mode: VscConverterAcControlMode::from_u32(mode2),
953                ac_setpoint: acset2,
954                dc_setpoint: dcset2,
955                loss_constant_mw: aloss2,
956                loss_linear: bloss2,
957                ..VscConverterTerminal::default()
958            },
959        });
960    }
961    Ok(vsc_lines)
962}
963
964// ---------------------------------------------------------------------------
965// FACTS Device Data
966// ---------------------------------------------------------------------------
967
968fn parse_facts(section: &RawxSection) -> Result<Vec<FactsDevice>, RawxError> {
969    let mut facts = Vec::new();
970    for row in section.data {
971        let name = section
972            .get_str(row, "name")
973            .unwrap_or("")
974            .trim()
975            .trim_matches('\'')
976            .to_string();
977        let bus_i = section.get_i64_or(row, "ibus", 0) as u32;
978        let bus_j = section.get_i64_or(row, "jbus", 0) as u32;
979        let mode = section.get_i64_or(row, "mode", 1) as u32;
980        let pdes = section.get_f64_or(row, "pdes", 0.0);
981        let qdes = section.get_f64_or(row, "qdes", 0.0);
982        let vset = section.get_f64_or(row, "vset", 1.0);
983        let shmx = section.get_f64_or(row, "shmx", 9999.0);
984        let linx = section.get_f64_or(row, "linx", 0.05);
985        let stat = section.get_i64_or(row, "stat", 1);
986
987        let facts_mode = FactsMode::from_u32(mode);
988
989        // Infer FACTS device type from operating mode
990        let facts_type = match facts_mode {
991            FactsMode::ShuntOnly => FactsType::Svc,
992            FactsMode::SeriesOnly | FactsMode::ImpedanceModulation => FactsType::Tcsc,
993            FactsMode::ShuntSeries | FactsMode::SeriesPowerControl => FactsType::Upfc,
994            FactsMode::OutOfService => FactsType::Svc,
995        };
996
997        facts.push(FactsDevice {
998            name,
999            bus_from: bus_i,
1000            bus_to: bus_j,
1001            mode: facts_mode,
1002            p_setpoint_mw: pdes,
1003            q_setpoint_mvar: qdes,
1004            voltage_setpoint_pu: vset,
1005            q_max: shmx,
1006            series_reactance_pu: linx,
1007            in_service: stat != 0,
1008            facts_type,
1009            ..FactsDevice::default()
1010        });
1011    }
1012    Ok(facts)
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017    use super::*;
1018
1019    #[test]
1020    fn test_parse_minimal_rawx() {
1021        let json = r#"{
1022            "network": {
1023                "caseid": {
1024                    "fields": ["ic", "sbase", "rev", "xfrrat", "nxfrat", "basfrq", "title1", "title2"],
1025                    "data": [0, 100.0, 35, 0, 0, 60.0, "test", ""]
1026                },
1027                "bus": {
1028                    "fields": ["ibus", "name", "baskv", "ide", "area", "zone", "owner", "vm", "va"],
1029                    "data": [
1030                        [1, "Bus 1", 138.0, 3, 1, 1, 1, 1.06, 0.0],
1031                        [2, "Bus 2", 138.0, 1, 1, 1, 1, 1.0, -5.0]
1032                    ]
1033                },
1034                "load": {
1035                    "fields": ["ibus", "loadid", "stat", "area", "zone", "pl", "ql"],
1036                    "data": [
1037                        [2, "1", 1, 1, 1, 100.0, 35.0]
1038                    ]
1039                },
1040                "generator": {
1041                    "fields": ["ibus", "machid", "pg", "qg", "qt", "qb", "vs", "ireg", "mbase", "zr", "zx", "rt", "xt", "gtap", "stat", "rmpct", "pt", "pb"],
1042                    "data": [
1043                        [1, "1", 80.0, 30.0, 200.0, -200.0, 1.06, 0, 100.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1, 100.0, 200.0, 0.0]
1044                    ]
1045                },
1046                "acline": {
1047                    "fields": ["ibus", "jbus", "ckt", "rpu", "xpu", "bpu", "name", "rate1", "rate2", "rate3", "gi", "bi", "gj", "bj", "stat", "met", "len"],
1048                    "data": [
1049                        [1, 2, "1", 0.02, 0.06, 0.03, "Line 1-2", 100.0, 100.0, 100.0, 0.0, 0.0, 0.0, 0.0, 1, 1, 0.0]
1050                    ]
1051                }
1052            }
1053        }"#;
1054
1055        let net = parse_str(json).unwrap();
1056        assert_eq!(net.n_buses(), 2);
1057        assert_eq!(net.branches.len(), 1);
1058        assert_eq!(net.generators.len(), 1);
1059        assert_eq!(net.base_mva, 100.0);
1060        assert_eq!(net.freq_hz, 60.0);
1061
1062        // Check bus data
1063        assert_eq!(net.buses[0].number, 1);
1064        assert_eq!(net.buses[0].bus_type, BusType::Slack);
1065        assert_eq!(net.buses[0].base_kv, 138.0);
1066        assert!((net.buses[0].voltage_magnitude_pu - 1.06).abs() < 1e-10);
1067
1068        assert_eq!(net.buses[1].number, 2);
1069        assert_eq!(net.buses[1].bus_type, BusType::PQ);
1070        let bus_pd = net.bus_load_p_mw();
1071        let bus_qd = net.bus_load_q_mvar();
1072        assert!((bus_pd[1] - 100.0).abs() < 1e-10);
1073        assert!((bus_qd[1] - 35.0).abs() < 1e-10);
1074
1075        // Check generator
1076        assert_eq!(net.generators[0].bus, 1);
1077        assert!((net.generators[0].p - 80.0).abs() < 1e-10);
1078        assert!((net.generators[0].voltage_setpoint_pu - 1.06).abs() < 1e-10);
1079        assert!((net.generators[0].pmax - 200.0).abs() < 1e-10);
1080
1081        // Check branch
1082        assert_eq!(net.branches[0].from_bus, 1);
1083        assert_eq!(net.branches[0].to_bus, 2);
1084        assert!((net.branches[0].r - 0.02).abs() < 1e-10);
1085        assert!((net.branches[0].x - 0.06).abs() < 1e-10);
1086    }
1087
1088    #[test]
1089    fn test_parse_rawx_with_transformer() {
1090        let json = r#"{
1091            "network": {
1092                "caseid": {
1093                    "fields": ["ic", "sbase", "rev", "xfrrat", "nxfrat", "basfrq"],
1094                    "data": [0, 100.0, 35, 0, 0, 60.0]
1095                },
1096                "bus": {
1097                    "fields": ["ibus", "name", "baskv", "ide", "area", "zone", "owner", "vm", "va"],
1098                    "data": [
1099                        [1, "HV Bus", 345.0, 3, 1, 1, 1, 1.04, 0.0],
1100                        [2, "LV Bus", 138.0, 1, 1, 1, 1, 1.0, -3.0]
1101                    ]
1102                },
1103                "transformer": {
1104                    "fields": ["ibus", "jbus", "kbus", "ckt", "cw", "cz", "cm", "mag1", "mag2", "nmet", "name", "stat", "o1", "f1", "o2", "f2", "o3", "f3", "o4", "f4", "vecgrp", "zcod", "r1-2", "x1-2", "sbase1-2", "windv1", "nomv1", "ang1", "wdg1rate1", "wdg1rate2", "wdg1rate3", "cod1", "cont1", "node1", "rma1", "rmi1", "vma1", "vmi1", "ntp1", "tab1", "cr1", "cx1", "cnxa1", "windv2", "nomv2", "ang2"],
1105                    "data": [
1106                        [1, 2, 0, "1", 1, 1, 1, 0.0, 0.0, 2, "Xfmr 1-2", 1, 1, 1.0, 0, 1.0, 0, 1.0, 0, 1.0, "", 0, 0.005, 0.1, 100.0, 1.0, 0.0, 0.0, 200.0, 200.0, 200.0, 0, 0, 0, 1.1, 0.9, 1.1, 0.9, 33, 0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0]
1107                    ]
1108                }
1109            }
1110        }"#;
1111
1112        let net = parse_str(json).unwrap();
1113        assert_eq!(net.n_buses(), 2);
1114        // 1 transformer branch
1115        assert_eq!(net.branches.len(), 1);
1116        let xfmr = &net.branches[0];
1117        assert_eq!(xfmr.from_bus, 1);
1118        assert_eq!(xfmr.to_bus, 2);
1119        assert!((xfmr.r - 0.005).abs() < 1e-10);
1120        assert!((xfmr.x - 0.1).abs() < 1e-10);
1121        // CW=1, WINDV1=1.0, WINDV2=1.0 → tap = 1.0
1122        assert!((xfmr.tap - 1.0).abs() < 1e-10);
1123        assert!((xfmr.rating_a_mva - 200.0).abs() < 1e-10);
1124    }
1125
1126    #[test]
1127    fn test_parse_rawx_empty_sections() {
1128        // Minimal file with only caseid and bus — no loads, gens, branches
1129        let json = r#"{
1130            "network": {
1131                "caseid": {
1132                    "fields": ["ic", "sbase"],
1133                    "data": [0, 100.0]
1134                },
1135                "bus": {
1136                    "fields": ["ibus", "name", "baskv", "ide"],
1137                    "data": [
1138                        [1, "Solo Bus", 69.0, 3]
1139                    ]
1140                }
1141            }
1142        }"#;
1143
1144        let net = parse_str(json).unwrap();
1145        assert_eq!(net.n_buses(), 1);
1146        assert_eq!(net.branches.len(), 0);
1147        assert_eq!(net.generators.len(), 0);
1148        assert_eq!(net.base_mva, 100.0);
1149    }
1150
1151    #[test]
1152    fn test_parse_rawx_fixshunt_accumulation() {
1153        let json = r#"{
1154            "network": {
1155                "caseid": {
1156                    "fields": ["ic", "sbase"],
1157                    "data": [0, 100.0]
1158                },
1159                "bus": {
1160                    "fields": ["ibus", "name", "baskv", "ide"],
1161                    "data": [
1162                        [1, "Bus 1", 138.0, 3]
1163                    ]
1164                },
1165                "fixshunt": {
1166                    "fields": ["ibus", "shntid", "stat", "gl", "bl"],
1167                    "data": [
1168                        [1, "1", 1, 0.0, 25.0],
1169                        [1, "2", 1, 0.0, 10.0]
1170                    ]
1171                }
1172            }
1173        }"#;
1174
1175        let net = parse_str(json).unwrap();
1176        assert!((net.buses[0].shunt_susceptance_mvar - 35.0).abs() < 1e-10);
1177    }
1178}