1use std::fmt::Write as FmtWrite;
18use std::path::Path;
19
20use surge_network::Network;
21use surge_network::network::BusType;
22use thiserror::Error;
23
24#[derive(Error, Debug)]
25pub enum EpcWriteError {
26 #[error("I/O error: {0}")]
27 Io(#[from] std::io::Error),
28 #[error("format error: {0}")]
29 Fmt(#[from] std::fmt::Error),
30}
31
32pub fn write_file(network: &Network, path: &Path) -> Result<(), EpcWriteError> {
34 let content = to_string(network)?;
35 std::fs::write(path, content)?;
36 Ok(())
37}
38
39pub fn to_string(network: &Network) -> Result<String, EpcWriteError> {
41 let mut out = String::with_capacity(64 * 1024);
42
43 write_title(&mut out, network)?;
44 write_comments(&mut out)?;
45 write_solution_parameters(&mut out, network)?;
46 write_bus_data(&mut out, network)?;
47 write_branch_data(&mut out, network)?;
48 write_transformer_data(&mut out, network)?;
49 write_generator_data(&mut out, network)?;
50 write_load_data(&mut out, network)?;
51 write_shunt_data(&mut out, network)?;
52 write_area_data(&mut out, network)?;
53 writeln!(out, "end")?;
54
55 Ok(out)
56}
57
58fn write_title(out: &mut String, network: &Network) -> Result<(), EpcWriteError> {
63 writeln!(out, "title")?;
64 writeln!(
65 out,
66 "{} — exported by Surge (https://github.com/amptimal/surge)",
67 network.name
68 )?;
69 writeln!(out, "!")?;
70 Ok(())
71}
72
73fn write_comments(out: &mut String) -> Result<(), EpcWriteError> {
74 writeln!(out, "comments")?;
75 writeln!(out, "!")?;
76 Ok(())
77}
78
79fn write_solution_parameters(out: &mut String, network: &Network) -> Result<(), EpcWriteError> {
80 writeln!(out, "solution parameters")?;
81 writeln!(out, " {:.1}", network.base_mva)?;
82 writeln!(out, "!")?;
83 Ok(())
84}
85
86fn write_bus_data(out: &mut String, network: &Network) -> Result<(), EpcWriteError> {
87 writeln!(out, "bus data [{}]", network.buses.len())?;
88
89 for bus in &network.buses {
90 let ty = match bus.bus_type {
91 BusType::PQ => 0,
92 BusType::PV => 2,
93 BusType::Slack => 3,
94 BusType::Isolated => 4,
95 };
96 let st = if bus.bus_type == BusType::Isolated {
97 1
98 } else {
99 0
100 };
101 let name = format_epc_name(&bus.name);
102 let va_deg = bus.voltage_angle_rad.to_degrees();
103 let lat = bus.latitude.unwrap_or(0.0);
104 let lon = bus.longitude.unwrap_or(0.0);
105
106 writeln!(
109 out,
110 " {} {} {:.4} : {} {:.6} {:.6} {:.6} {} {} {:.6} {:.6} 0 0 0 0 0 {} {:.6} {:.6}",
111 bus.number,
112 name,
113 bus.base_kv,
114 ty,
115 bus.voltage_magnitude_pu, bus.voltage_magnitude_pu, va_deg,
118 bus.area,
119 bus.zone,
120 bus.voltage_max_pu,
121 bus.voltage_min_pu,
122 st,
123 lat,
124 lon,
125 )?;
126 }
127
128 Ok(())
129}
130
131fn write_branch_data(out: &mut String, network: &Network) -> Result<(), EpcWriteError> {
132 let lines: Vec<_> = network.branches.iter().filter(|b| is_line(b)).collect();
134
135 writeln!(out, "branch data [{}]", lines.len())?;
136
137 let bus_info: std::collections::HashMap<u32, (&str, f64)> = network
139 .buses
140 .iter()
141 .map(|b| (b.number, (b.name.as_str(), b.base_kv)))
142 .collect();
143
144 for br in &lines {
145 let (from_name, from_kv) = bus_info.get(&br.from_bus).copied().unwrap_or(("", 0.0));
146 let (to_name, to_kv) = bus_info.get(&br.to_bus).copied().unwrap_or(("", 0.0));
147
148 let st = if br.in_service { 1 } else { 0 };
149 let ck = &br.circuit;
150
151 writeln!(
154 out,
155 " {} {} {:.4} {} {} {:.4} \"{}\" 1 : {} {:.8E} {:.8E} {:.8E} {:.2} {:.2} {:.2} 0.00 0.0 0.0",
156 br.from_bus,
157 format_epc_name(from_name),
158 from_kv,
159 br.to_bus,
160 format_epc_name(to_name),
161 to_kv,
162 ck,
163 st,
164 br.r,
165 br.x,
166 br.b,
167 br.rating_a_mva,
168 br.rating_b_mva,
169 br.rating_c_mva,
170 )?;
171 }
172
173 Ok(())
174}
175
176fn write_transformer_data(out: &mut String, network: &Network) -> Result<(), EpcWriteError> {
177 let xfmrs: Vec<_> = network.branches.iter().filter(|b| !is_line(b)).collect();
179
180 writeln!(out, "transformer data [{}]", xfmrs.len())?;
181
182 let bus_info: std::collections::HashMap<u32, (&str, f64)> = network
183 .buses
184 .iter()
185 .map(|b| (b.number, (b.name.as_str(), b.base_kv)))
186 .collect();
187
188 for br in &xfmrs {
189 let (from_name, from_kv) = bus_info.get(&br.from_bus).copied().unwrap_or(("", 0.0));
190 let (to_name, to_kv) = bus_info.get(&br.to_bus).copied().unwrap_or(("", 0.0));
191
192 let st = if br.in_service { 1 } else { 0 };
193 let ck = &br.circuit;
194 let tbase = network.base_mva;
195
196 let kv_primary = br.tap * from_kv;
200 let kv_secondary = to_kv;
201
202 writeln!(
209 out,
210 " {} {} {:.4} {} {} {:.4} \"{}\" \"\" : {} 0 0 \"\" 0.000 0 0 \"\" 0.000 /",
211 br.from_bus,
212 format_epc_name(from_name),
213 from_kv,
214 br.to_bus,
215 format_epc_name(to_name),
216 to_kv,
217 ck,
218 st,
219 )?;
220 writeln!(
221 out,
222 " 0 \"\" 0.000 {} {} {:.1} {:.8E} {:.8E} 0.0000E+00 0.0000E+00 0.0000E+00 0.0000E+00 /",
223 br.from_bus.min(99999), 1, tbase,
226 br.r,
227 br.x,
228 )?;
229 writeln!(
230 out,
231 " {:.4} {:.4} 0.0 0.0 0.0 0.0 {:.2} {:.2} {:.2} 0.00 /",
232 kv_primary, kv_secondary, br.rating_a_mva, br.rating_b_mva, br.rating_c_mva,
233 )?;
234 writeln!(out, " 0 1.0 0 1.0 0 1.0 0 1.0")?;
235 }
236
237 Ok(())
238}
239
240fn write_generator_data(out: &mut String, network: &Network) -> Result<(), EpcWriteError> {
241 writeln!(out, "generator data [{}]", network.generators.len())?;
242
243 let bus_info: std::collections::HashMap<u32, (&str, f64)> = network
244 .buses
245 .iter()
246 .map(|b| (b.number, (b.name.as_str(), b.base_kv)))
247 .collect();
248
249 for g in &network.generators {
250 let (bus_name, bus_kv) = bus_info.get(&g.bus).copied().unwrap_or(("", 0.0));
251 let st = if g.in_service { 1 } else { 0 };
252 let gen_id = g.machine_id.as_deref().unwrap_or("1");
253
254 writeln!(
259 out,
260 " {} {} {:.4} \"{}\" \"\" : {} 0 \"\" 0.000 /",
261 g.bus,
262 format_epc_name(bus_name),
263 bus_kv,
264 gen_id,
265 st,
266 )?;
267 writeln!(
268 out,
269 " 1.0 1.0 1 1 {:.2} {:.2} {:.2} {:.2} {:.2} {:.2} {:.1} 0.0 0.0 0.0 0.0 /",
270 g.p, g.pmax, g.pmin, g.q, g.qmax, g.qmin, g.machine_base_mva,
271 )?;
272 writeln!(out, " 0 \"\" 0.000 0 \"\" 0.000 0 0 0 0")?;
273 }
274
275 Ok(())
276}
277
278fn write_load_data(out: &mut String, network: &Network) -> Result<(), EpcWriteError> {
279 let bus_demand_p = network.bus_load_p_mw();
281 let bus_demand_q = network.bus_load_q_mvar();
282 let _bus_map = network.bus_index_map();
283
284 let load_buses: Vec<(usize, &surge_network::network::Bus)> = network
285 .buses
286 .iter()
287 .enumerate()
288 .filter(|(i, _)| {
289 let pd = bus_demand_p.get(*i).copied().unwrap_or(0.0);
290 let qd = bus_demand_q.get(*i).copied().unwrap_or(0.0);
291 pd.abs() > 1e-10 || qd.abs() > 1e-10
292 })
293 .collect();
294
295 writeln!(out, "load data [{}]", load_buses.len())?;
296
297 for (bi, bus) in &load_buses {
298 let name = format_epc_name(&bus.name);
299 let pd = bus_demand_p.get(*bi).copied().unwrap_or(0.0);
300 let qd = bus_demand_q.get(*bi).copied().unwrap_or(0.0);
301 writeln!(
304 out,
305 " {} {} {:.4} \"1\" \"\" : 1 {:.4} {:.4} 0.0 0.0 0.0 0.0 {} {}",
306 bus.number, name, bus.base_kv, pd, qd, bus.area, bus.zone,
307 )?;
308 }
309
310 Ok(())
311}
312
313fn write_shunt_data(out: &mut String, network: &Network) -> Result<(), EpcWriteError> {
314 let shunts: Vec<_> = network
316 .buses
317 .iter()
318 .filter(|b| b.shunt_conductance_mw.abs() > 1e-10 || b.shunt_susceptance_mvar.abs() > 1e-10)
319 .collect();
320
321 writeln!(out, "shunt data [{}]", shunts.len())?;
322
323 for bus in &shunts {
324 let name = format_epc_name(&bus.name);
325 writeln!(
328 out,
329 " {} {} {:.4} \"1\" \"\" \"1\" 1 \"\" : 1 {} {} {:.6} {:.6}",
330 bus.number,
331 name,
332 bus.base_kv,
333 bus.area,
334 bus.zone,
335 bus.shunt_conductance_mw,
336 bus.shunt_susceptance_mvar,
337 )?;
338 }
339
340 Ok(())
341}
342
343fn write_area_data(out: &mut String, network: &Network) -> Result<(), EpcWriteError> {
344 if network.area_schedules.is_empty() {
345 writeln!(out, "area data [0]")?;
346 return Ok(());
347 }
348
349 writeln!(out, "area data [{}]", network.area_schedules.len())?;
350 for area in &network.area_schedules {
351 let name = format_epc_name(&area.name);
352 writeln!(
354 out,
355 " {} {} : {} {:.2} 10.0 0.0 0.0",
356 area.number, name, area.slack_bus, area.p_desired_mw,
357 )?;
358 }
359
360 Ok(())
361}
362
363fn format_epc_name(name: &str) -> String {
369 let trimmed = name.trim();
370 if trimmed.is_empty() {
371 "\"\"".to_string()
372 } else {
373 format!("\"{}\"", trimmed.replace('"', "'"))
374 }
375}
376
377fn is_line(branch: &surge_network::network::Branch) -> bool {
379 branch.tap == 0.0 || (branch.tap - 1.0).abs() < 1e-6
380}
381
382#[cfg(test)]
387mod tests {
388 use super::*;
389 use surge_network::network::{Branch, Bus, BusType, Generator, Load};
390
391 fn mini_network() -> Network {
392 let mut net = Network::new("test_epc");
393 net.base_mva = 100.0;
394
395 let mut b1 = Bus::new(1, BusType::Slack, 345.0);
396 b1.name = "Bus1".into();
397 b1.voltage_magnitude_pu = 1.04;
398 b1.voltage_angle_rad = 0.0;
399 b1.area = 1;
400 b1.zone = 1;
401 b1.voltage_max_pu = 1.06;
402 b1.voltage_min_pu = 0.94;
403
404 let mut b2 = Bus::new(2, BusType::PV, 345.0);
405 b2.name = "Bus2".into();
406 b2.voltage_magnitude_pu = 1.025;
407 b2.voltage_angle_rad = 0.17;
408 b2.area = 1;
409 b2.zone = 1;
410 b2.voltage_max_pu = 1.06;
411 b2.voltage_min_pu = 0.94;
412
413 let mut b3 = Bus::new(3, BusType::PQ, 138.0);
414 b3.name = "Bus3".into();
415 b3.voltage_magnitude_pu = 1.01;
416 b3.voltage_angle_rad = -0.05;
417 b3.area = 1;
418 b3.zone = 1;
419 b3.voltage_max_pu = 1.06;
420 b3.voltage_min_pu = 0.94;
421 b3.shunt_conductance_mw = 5.0;
422 b3.shunt_susceptance_mvar = -10.0;
423
424 net.buses = vec![b1, b2, b3];
425 net.loads = vec![Load::new(2, 50.0, 20.0), Load::new(3, 100.0, 40.0)];
426
427 let mut line = Branch::new_line(1, 2, 0.01, 0.1, 0.02);
429 line.rating_a_mva = 200.0;
430 line.circuit = "1".to_string();
431
432 let mut xfmr = Branch::new_line(2, 3, 0.005, 0.05, 0.0);
434 xfmr.tap = 1.0; xfmr.tap = 1.05;
437 xfmr.rating_a_mva = 150.0;
438 xfmr.circuit = "1".to_string();
439
440 net.branches = vec![line, xfmr];
441
442 let mut g1 = Generator::new(1, 100.0, 1.04);
443 g1.machine_id = Some("1".into());
444 g1.pmax = 200.0;
445 g1.pmin = 10.0;
446 g1.qmax = 100.0;
447 g1.qmin = -50.0;
448 g1.machine_base_mva = 100.0;
449
450 net.generators = vec![g1];
451 net
452 }
453
454 #[test]
455 fn test_round_trip_to_string() {
456 let net = mini_network();
457 let epc = to_string(&net).unwrap();
458
459 assert!(epc.contains("title"));
461 assert!(epc.contains("bus data [3]"));
462 assert!(epc.contains("branch data [1]"));
463 assert!(epc.contains("transformer data [1]"));
464 assert!(epc.contains("generator data [1]"));
465 assert!(epc.contains("load data [2]"));
466 assert!(epc.contains("shunt data [1]"));
467 assert!(epc.contains("end"));
468 }
469
470 #[test]
471 fn test_round_trip_parse() {
472 let net = mini_network();
473 let epc = to_string(&net).unwrap();
474
475 let parsed = crate::epc::parse_str(&epc).unwrap();
477
478 assert_eq!(parsed.buses.len(), net.buses.len());
480 assert_eq!(parsed.generators.len(), net.generators.len());
481 assert_eq!(parsed.branches.len(), net.branches.len());
483
484 let bus_nums: Vec<u32> = parsed.buses.iter().map(|b| b.number).collect();
486 assert!(bus_nums.contains(&1));
487 assert!(bus_nums.contains(&2));
488 assert!(bus_nums.contains(&3));
489 }
490
491 #[test]
492 fn test_write_file_round_trip() {
493 let net = mini_network();
494 let tmp = std::env::temp_dir().join("surge_test_epc_writer.epc");
495 write_file(&net, &tmp).unwrap();
496
497 let parsed = crate::epc::parse_file(&tmp).unwrap();
498 assert_eq!(parsed.buses.len(), 3);
499 assert_eq!(parsed.generators.len(), 1);
500
501 let _ = std::fs::remove_file(&tmp);
503 }
504}