1use std::path::Path;
18
19use arrow::array::{Array, ArrayRef, Float64Array, Int64Array};
20use arrow::record_batch::RecordBatch;
21use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
22
23use powerio_tx::network::{
24 Branch, BranchSolution, Bus, BusId, BusType, Generator, Load, Shunt, SourceFormat,
25};
26use powerio_tx::{BalancedNetwork, GenCost};
27
28use crate::collect::Diagnostics;
29
30type Error = powerio_tx::Error;
31type Result<T> = std::result::Result<T, Error>;
32
33pub mod codes {
35 powerio_core::diagnostic_codes! {
36 READ_GRIDFM_FIELD_DROPPED = "READ.GRIDFM.FIELD_DROPPED", Warning,
37 "a field the gridfm schema does not carry is absent from the network";
38 READ_GRIDFM_VALUE_DEFAULTED = "READ.GRIDFM.VALUE_DEFAULTED", Warning,
39 "a manifest value the reader needs was absent and was defaulted";
40 READ_GRIDFM_VALUE_INFERRED = "READ.GRIDFM.VALUE_INFERRED", Warning,
41 "an identity the gridfm schema does not store was synthesized";
42 READ_GRIDFM_VALUE_COLLAPSED = "READ.GRIDFM.VALUE_COLLAPSED", Warning,
43 "nodal totals were folded into synthetic per bus elements";
44 READ_GRIDFM_ELEMENT_RELABELED = "READ.GRIDFM.ELEMENT_RELABELED", Warning,
45 "a unity ratio transformer is indistinguishable from a line and reads as one";
46 }
47}
48#[derive(Debug, Clone)]
50pub struct GridfmRead {
51 pub network: BalancedNetwork,
53 pub scenario: i64,
55 pub diagnostics: Vec<powerio_core::Diagnostic>,
57}
58
59pub fn read_gridfm_network(
69 bus_table: &RecordBatch,
70 generator_table: &RecordBatch,
71 branch_table: &RecordBatch,
72 scenario: i64,
73 base_mva: f64,
74 name: &str,
75) -> Result<GridfmRead> {
76 let bus = bus_columns(std::slice::from_ref(bus_table))?;
77 let gens = gen_columns(std::slice::from_ref(generator_table))?;
78 let branch = branch_columns(std::slice::from_ref(branch_table))?;
79 build_network_from_columns(
80 &bus,
81 &gens,
82 &branch,
83 scenario,
84 base_mva,
85 name,
86 Diagnostics::new(),
87 )
88}
89
90struct DatasetTables {
95 base_mva: f64,
96 name: String,
97 meta_warnings: Diagnostics,
98 bus: BusColumns,
99 gens: GenColumns,
100 branch: BranchColumns,
101}
102
103fn open_dataset_tables(dir: &Path) -> Result<DatasetTables> {
104 let source =
105 powerio_core::Source::open(dir).map_err(|error| powerio_tx::Error::FormatRead {
106 format: "gridfm",
107 message: format!("opening {}: {error}", dir.display()),
108 })?;
109 let entries = source
110 .entry_names()
111 .map_err(|error| powerio_tx::Error::FormatRead {
112 format: "gridfm",
113 message: format!("listing {}: {error}", dir.display()),
114 })?;
115 let prefix = resolve_raw_prefix(&entries)?;
116 let (base_mva, name, meta_warnings) = read_meta_source(&source, &prefix);
117 let bus = bus_columns(&read_parquet_buffer(&source, &prefix, "bus_data.parquet")?)?;
118 let gens = gen_columns(&read_parquet_buffer(&source, &prefix, "gen_data.parquet")?)?;
119 let branch = branch_columns(&read_parquet_buffer(
120 &source,
121 &prefix,
122 "branch_data.parquet",
123 )?)?;
124 Ok(DatasetTables {
125 base_mva,
126 name,
127 meta_warnings,
128 bus,
129 gens,
130 branch,
131 })
132}
133
134pub fn read_gridfm_dataset(dir: impl AsRef<Path>, scenario: i64) -> Result<GridfmRead> {
145 let tables = open_dataset_tables(dir.as_ref())?;
146 build_network_from_columns(
147 &tables.bus,
148 &tables.gens,
149 &tables.branch,
150 scenario,
151 tables.base_mva,
152 &tables.name,
153 tables.meta_warnings,
154 )
155}
156
157pub fn read_gridfm_scenarios(dir: impl AsRef<Path>) -> Result<Vec<GridfmRead>> {
165 let tables = open_dataset_tables(dir.as_ref())?;
169 distinct_sorted(&tables.bus.scenario)
170 .into_iter()
171 .map(|s| {
172 build_network_from_columns(
173 &tables.bus,
174 &tables.gens,
175 &tables.branch,
176 s,
177 tables.base_mva,
178 &tables.name,
179 tables.meta_warnings.clone(),
180 )
181 })
182 .collect()
183}
184
185pub fn list_gridfm_scenario_ids(dir: impl AsRef<Path>) -> Result<Vec<i64>> {
193 let source = powerio_core::Source::open(dir.as_ref()).map_err(|error| {
194 powerio_tx::Error::FormatRead {
195 format: "gridfm",
196 message: format!("opening {}: {error}", dir.as_ref().display()),
197 }
198 })?;
199 let entries = source
200 .entry_names()
201 .map_err(|error| powerio_tx::Error::FormatRead {
202 format: "gridfm",
203 message: format!("listing {}: {error}", dir.as_ref().display()),
204 })?;
205 let prefix = resolve_raw_prefix(&entries)?;
206 let bus = bus_columns(&read_parquet_buffer(&source, &prefix, "bus_data.parquet")?)?;
207 Ok(distinct_sorted(&bus.scenario))
208}
209
210fn distinct_sorted(scenario: &[i64]) -> Vec<i64> {
212 let mut ids = scenario.to_vec();
213 ids.sort_unstable();
214 ids.dedup();
215 ids
216}
217
218pub fn read_gridfm_scenario_set(
235 dir: impl AsRef<Path>,
236) -> std::result::Result<
237 (
238 powerio_core::ScenarioSet<BalancedNetwork>,
239 Vec<powerio_core::Diagnostic>,
240 ),
241 powerio_core::Error,
242> {
243 let reads = read_gridfm_scenarios(dir)
244 .map_err(|error| powerio_core::Error::new(error.code(), error.to_string()))?;
245 let mut diagnostics = Vec::new();
246 let mut scenarios = Vec::with_capacity(reads.len());
247 let mut donor: Option<BalancedNetwork> = None;
248 for read in reads {
249 let mut network = read.network;
250 match &donor {
251 Some(base) => network.share_equal_tables(base),
252 None => donor = Some(network.clone()),
253 }
254 diagnostics.extend(read.diagnostics);
255 let id = powerio_core::ScenarioId::new(read.scenario.to_string())?;
256 scenarios.push(powerio_core::Scenario::new(id, None, network));
257 }
258 let set = powerio_core::ScenarioSet::new(scenarios)?;
259 Ok((set, diagnostics))
260}
261
262pub fn read_gridfm_base_case(dir: impl AsRef<Path>) -> Result<GridfmRead> {
270 read_gridfm_dataset(dir, 0)
271}
272
273struct BusColumns {
277 scenario: Vec<i64>,
278 bus: Vec<i64>,
279 pv: Vec<i64>,
280 refc: Vec<i64>,
281 vm: Vec<f64>,
282 va: Vec<f64>,
283 vn_kv: Vec<f64>,
284 min_vm: Vec<f64>,
285 max_vm: Vec<f64>,
286 pd: Vec<f64>,
287 qd: Vec<f64>,
288 gs: Vec<f64>,
289 bs: Vec<f64>,
290}
291
292fn bus_columns(batches: &[RecordBatch]) -> Result<BusColumns> {
293 Ok(BusColumns {
294 scenario: i64_col(batches, "scenario")?,
295 bus: i64_col(batches, "bus")?,
296 pv: i64_col(batches, "PV")?,
297 refc: i64_col(batches, "REF")?,
298 vm: f64_col(batches, "Vm")?,
299 va: f64_col(batches, "Va")?,
300 vn_kv: f64_col(batches, "vn_kv")?,
301 min_vm: f64_col(batches, "min_vm_pu")?,
302 max_vm: f64_col(batches, "max_vm_pu")?,
303 pd: f64_col(batches, "Pd")?,
304 qd: f64_col(batches, "Qd")?,
305 gs: f64_col(batches, "GS")?,
306 bs: f64_col(batches, "BS")?,
307 })
308}
309
310struct GenColumns {
312 scenario: Vec<i64>,
313 bus: Vec<i64>,
314 p_mw: Vec<f64>,
315 q_mvar: Vec<f64>,
316 min_p: Vec<f64>,
317 max_p: Vec<f64>,
318 min_q: Vec<f64>,
319 max_q: Vec<f64>,
320 cp0: Vec<f64>,
321 cp1: Vec<f64>,
322 cp2: Vec<f64>,
323 in_service: Vec<i64>,
324}
325
326fn gen_columns(batches: &[RecordBatch]) -> Result<GenColumns> {
327 Ok(GenColumns {
328 scenario: i64_col(batches, "scenario")?,
329 bus: i64_col(batches, "bus")?,
330 p_mw: f64_col(batches, "p_mw")?,
331 q_mvar: f64_col(batches, "q_mvar")?,
332 min_p: f64_col(batches, "min_p_mw")?,
333 max_p: f64_col(batches, "max_p_mw")?,
334 min_q: f64_col(batches, "min_q_mvar")?,
335 max_q: f64_col(batches, "max_q_mvar")?,
336 cp0: f64_col(batches, "cp0_eur")?,
337 cp1: f64_col(batches, "cp1_eur_per_mw")?,
338 cp2: f64_col(batches, "cp2_eur_per_mw2")?,
339 in_service: i64_col(batches, "in_service")?,
340 })
341}
342
343struct BranchColumns {
346 scenario: Vec<i64>,
347 from_bus: Vec<i64>,
348 to_bus: Vec<i64>,
349 r: Vec<f64>,
350 x: Vec<f64>,
351 b: Vec<f64>,
352 tap: Vec<f64>,
353 shift: Vec<f64>,
354 ang_min: Vec<f64>,
355 ang_max: Vec<f64>,
356 rate_a: Vec<f64>,
357 status: Vec<i64>,
358 pf: Vec<f64>,
359 qf: Vec<f64>,
360 pt: Vec<f64>,
361 qt: Vec<f64>,
362}
363
364fn branch_columns(batches: &[RecordBatch]) -> Result<BranchColumns> {
365 Ok(BranchColumns {
366 scenario: i64_col(batches, "scenario")?,
367 from_bus: i64_col(batches, "from_bus")?,
368 to_bus: i64_col(batches, "to_bus")?,
369 r: f64_col(batches, "r")?,
370 x: f64_col(batches, "x")?,
371 b: f64_col(batches, "b")?,
372 tap: f64_col(batches, "tap")?,
373 shift: f64_col(batches, "shift")?,
374 ang_min: f64_col(batches, "ang_min")?,
375 ang_max: f64_col(batches, "ang_max")?,
376 rate_a: f64_col(batches, "rate_a")?,
377 status: i64_col(batches, "br_status")?,
378 pf: f64_col(batches, "pf")?,
379 qf: f64_col(batches, "qf")?,
380 pt: f64_col(batches, "pt")?,
381 qt: f64_col(batches, "qt")?,
382 })
383}
384
385#[allow(clippy::float_cmp, clippy::too_many_lines)]
393fn build_network_from_columns(
394 bus: &BusColumns,
395 gens: &GenColumns,
396 branch: &BranchColumns,
397 scenario: i64,
398 base_mva: f64,
399 name: &str,
400 warnings: Diagnostics,
401) -> Result<GridfmRead> {
402 let bus_rows = scenario_rows(&bus.scenario, scenario);
404 if bus_rows.is_empty() {
405 let mut avail = bus.scenario.clone();
406 avail.sort_unstable();
407 avail.dedup();
408 return Err(powerio_tx::Error::FormatRead {
409 format: "gridfm",
410 message: format!("scenario {scenario} not present; available: {avail:?}"),
411 });
412 }
413
414 let bus_id = &bus.bus;
415 let pv = &bus.pv;
416 let refc = &bus.refc;
417 let vm = &bus.vm;
418 let va = &bus.va;
419 let vn_kv = &bus.vn_kv;
420 let min_vm = &bus.min_vm;
421 let max_vm = &bus.max_vm;
422 let pd = &bus.pd;
423 let qd = &bus.qd;
424 let gs = &bus.gs;
425 let bs = &bus.bs;
426
427 let mut buses = Vec::with_capacity(bus_rows.len());
428 let mut loads = Vec::new();
429 let mut shunts = Vec::new();
430 let mut bus_vm: std::collections::HashMap<i64, f64> =
434 std::collections::HashMap::with_capacity(bus_rows.len());
435 for &r in &bus_rows {
436 let id = dense_bus_id(bus_id[r])?;
437 bus_vm.insert(bus_id[r], vm[r]);
438 let kind = if refc[r] != 0 {
441 BusType::Ref
442 } else if pv[r] != 0 {
443 BusType::Pv
444 } else {
445 BusType::Pq
446 };
447 let mut bus = Bus::new(id, kind, vn_kv[r]);
448 bus.vm = vm[r];
449 bus.va = va[r];
450 bus.vmax = max_vm[r];
451 bus.vmin = min_vm[r];
452 bus.area = 0;
453 bus.zone = 0;
454 buses.push(bus);
455 if pd[r] != 0.0 || qd[r] != 0.0 {
456 loads.push(Load::new(id, pd[r], qd[r]));
457 }
458 if gs[r] != 0.0 || bs[r] != 0.0 {
460 shunts.push(Shunt::new(id, gs[r] * base_mva, bs[r] * base_mva));
461 }
462 }
463
464 let gen_rows = scenario_rows(&gens.scenario, scenario);
466 require_scenario_block(&gens.scenario, scenario, &gen_rows, "gen_data")?;
467 let g_bus = &gens.bus;
468 let p_mw = &gens.p_mw;
469 let q_mvar = &gens.q_mvar;
470 let min_p = &gens.min_p;
471 let max_p = &gens.max_p;
472 let min_q = &gens.min_q;
473 let max_q = &gens.max_q;
474 let cp0 = &gens.cp0;
475 let cp1 = &gens.cp1;
476 let cp2 = &gens.cp2;
477 let g_in = &gens.in_service;
478
479 let mut generators = Vec::with_capacity(gen_rows.len());
480 for &r in &gen_rows {
481 let cost = Some(GenCost::new(2, 0.0, 0.0, vec![cp2[r], cp1[r], cp0[r]]));
485 let mut generator = Generator::new(dense_bus_id(g_bus[r])?);
486 generator.pg = p_mw[r];
487 generator.qg = q_mvar[r];
488 generator.pmax = max_p[r];
489 generator.pmin = min_p[r];
490 generator.qmax = max_q[r];
491 generator.qmin = min_q[r];
492 generator.vg = bus_vm.get(&g_bus[r]).copied().unwrap_or(1.0);
496 generator.mbase = base_mva;
497 generator.in_service = g_in[r] != 0;
498 generator.cost = cost;
499 generators.push(generator);
500 }
501
502 let br_rows = scenario_rows(&branch.scenario, scenario);
504 require_scenario_block(&branch.scenario, scenario, &br_rows, "branch_data")?;
505 let from_bus = &branch.from_bus;
506 let to_bus = &branch.to_bus;
507 let r_col = &branch.r;
508 let x_col = &branch.x;
509 let b_col = &branch.b;
510 let tap = &branch.tap;
511 let shift = &branch.shift;
512 let ang_min = &branch.ang_min;
513 let ang_max = &branch.ang_max;
514 let rate_a = &branch.rate_a;
515 let br_status = &branch.status;
516 let pf = &branch.pf;
517 let qf = &branch.qf;
518 let pt = &branch.pt;
519 let qt = &branch.qt;
520
521 let mut branches = Vec::with_capacity(br_rows.len());
522 for &row in &br_rows {
530 let shift_v = shift[row];
531 let tap_out = if tap[row] == 1.0 && shift_v == 0.0 {
532 0.0
533 } else {
534 tap[row]
535 };
536 let mut branch = Branch::new(
537 dense_bus_id(from_bus[row])?,
538 dense_bus_id(to_bus[row])?,
539 r_col[row],
540 x_col[row],
541 );
542 branch.b = b_col[row];
543 branch.rate_a = rate_a[row];
544 branch.tap = tap_out;
545 branch.shift = shift_v;
546 branch.in_service = br_status[row] != 0;
547 branch.angmin = ang_min[row];
548 branch.angmax = ang_max[row];
549 branch.solution = Some(BranchSolution::new(pf[row], qf[row], pt[row], qt[row]));
550 branches.push(branch);
551 }
552
553 let mut net = BalancedNetwork::new(name, base_mva);
554 *net.buses_mut() = buses;
555 *net.loads_mut() = loads;
556 *net.shunts_mut() = shunts;
557 *net.branches_mut() = branches;
558 *net.generators_mut() = generators;
559 *net.source_format_mut() = SourceFormat::Gridfm;
560 net.validate()?;
561
562 Ok(GridfmRead {
563 network: net,
564 scenario,
565 diagnostics: warnings.into_records(),
566 })
567}
568
569fn defaulted_meta(message: impl Into<String>) -> Diagnostics {
574 let mut diagnostics = Diagnostics::new();
575 diagnostics.push(&codes::READ_GRIDFM_VALUE_DEFAULTED, message);
576 diagnostics
577}
578
579fn scenario_rows(scen: &[i64], scenario: i64) -> Vec<usize> {
581 scen.iter()
582 .enumerate()
583 .filter_map(|(i, &s)| (s == scenario).then_some(i))
584 .collect()
585}
586
587fn require_scenario_block(
592 scen_col: &[i64],
593 scenario: i64,
594 rows: &[usize],
595 table: &str,
596) -> Result<()> {
597 if rows.is_empty() && !scen_col.is_empty() {
598 return Err(powerio_tx::Error::FormatRead {
599 format: "gridfm",
600 message: format!(
601 "scenario {scenario} has no {table} rows, but the table holds {} row(s) for other \
602 scenarios — a partial or corrupt dataset",
603 scen_col.len()
604 ),
605 });
606 }
607 Ok(())
608}
609
610fn dense_bus_id(v: i64) -> Result<BusId> {
612 let idx = usize::try_from(v).map_err(|_| powerio_tx::Error::FormatRead {
613 format: "gridfm",
614 message: format!("negative dense bus index {v}"),
615 })?;
616 Ok(BusId(idx + 1))
617}
618
619fn column<'a>(b: &'a RecordBatch, name: &str) -> Result<&'a ArrayRef> {
621 b.column_by_name(name)
622 .ok_or_else(|| powerio_tx::Error::FormatRead {
623 format: "gridfm",
624 message: format!("missing column `{name}`"),
625 })
626}
627
628fn i64_col(batches: &[RecordBatch], name: &str) -> Result<Vec<i64>> {
630 let mut out = Vec::with_capacity(batches.iter().map(RecordBatch::num_rows).sum());
631 for b in batches {
632 let arr = column(b, name)?;
633 let col = arr.as_any().downcast_ref::<Int64Array>().ok_or_else(|| {
634 powerio_tx::Error::FormatRead {
635 format: "gridfm",
636 message: format!("column `{name}` is not Int64"),
637 }
638 })?;
639 if col.null_count() > 0 {
640 return Err(powerio_tx::Error::FormatRead {
641 format: "gridfm",
642 message: format!("column `{name}` has nulls"),
643 });
644 }
645 out.extend_from_slice(col.values());
646 }
647 Ok(out)
648}
649
650fn f64_col(batches: &[RecordBatch], name: &str) -> Result<Vec<f64>> {
652 let mut out = Vec::with_capacity(batches.iter().map(RecordBatch::num_rows).sum());
653 for b in batches {
654 let arr = column(b, name)?;
655 let col = arr.as_any().downcast_ref::<Float64Array>().ok_or_else(|| {
656 powerio_tx::Error::FormatRead {
657 format: "gridfm",
658 message: format!("column `{name}` is not Float64"),
659 }
660 })?;
661 if col.null_count() > 0 {
662 return Err(powerio_tx::Error::FormatRead {
663 format: "gridfm",
664 message: format!("column `{name}` has nulls"),
665 });
666 }
667 out.extend_from_slice(col.values());
668 }
669 Ok(out)
670}
671
672pub fn read_dataset_dir(
682 dir: impl AsRef<std::path::Path>,
683 from: &str,
684 scenario: i64,
685) -> Result<GridfmRead> {
686 require_dataset_format(from)?;
687 read_gridfm_dataset(dir, scenario)
688}
689
690pub fn list_dataset_scenario_ids(dir: impl AsRef<std::path::Path>, from: &str) -> Result<Vec<i64>> {
696 require_dataset_format(from)?;
697 list_gridfm_scenario_ids(dir)
698}
699
700fn require_dataset_format(from: &str) -> Result<()> {
701 if from.eq_ignore_ascii_case("gridfm") {
702 return Ok(());
703 }
704 Err(powerio_tx::Error::UnknownFormat(format!(
705 "{from} is not a dataset directory format (dataset formats: gridfm); \
706 PyPSA CSV directories parse through the ordinary parse"
707 )))
708}
709
710struct BufferBytes(powerio_core::SourceBuffer);
713
714impl AsRef<[u8]> for BufferBytes {
715 fn as_ref(&self) -> &[u8] {
716 self.0.bytes()
717 }
718}
719
720fn core_error(error: &powerio_tx::Error) -> powerio_core::Error {
721 powerio_core::Error::new(error.code(), error.to_string())
722}
723
724fn resolve_raw_prefix(entries: &[powerio_core::ArtifactPath]) -> Result<String> {
728 let holds = |prefix: &str| {
729 entries
730 .iter()
731 .any(|entry| entry.as_str() == format!("{prefix}bus_data.parquet"))
732 };
733 if holds("") {
734 return Ok(String::new());
735 }
736 if holds("raw/") {
737 return Ok("raw/".to_owned());
738 }
739 let mut nested: Vec<String> = entries
740 .iter()
741 .filter_map(|entry| {
742 entry
743 .as_str()
744 .strip_suffix("/raw/bus_data.parquet")
745 .filter(|case| !case.contains('/'))
746 .map(|case| format!("{case}/raw/"))
747 })
748 .collect();
749 nested.sort();
750 nested.dedup();
751 match nested.len() {
752 0 => Err(powerio_tx::Error::FormatRead {
753 format: "gridfm",
754 message: "no gridfm dataset found: no bus_data.parquet at the root, under raw/, \
755 or under a single <case>/raw/"
756 .to_owned(),
757 }),
758 1 => Ok(nested.remove(0)),
759 n => Err(powerio_tx::Error::FormatRead {
760 format: "gridfm",
761 message: format!(
762 "{n} <case>/raw/ dataset directories found; open one of them directly"
763 ),
764 }),
765 }
766}
767
768fn read_parquet_buffer(
769 source: &powerio_core::Source,
770 prefix: &str,
771 file: &str,
772) -> Result<Vec<RecordBatch>> {
773 let name = format!("{prefix}{file}");
774 let path = powerio_core::ArtifactPath::new(name.clone()).map_err(|error| {
775 powerio_tx::Error::FormatRead {
776 format: "gridfm",
777 message: error.to_string(),
778 }
779 })?;
780 let buffer = source
781 .buffer(&path)
782 .map_err(|error| powerio_tx::Error::FormatRead {
783 format: "gridfm",
784 message: format!("acquiring {name}: {error}"),
785 })?;
786 let bytes = bytes::Bytes::from_owner(BufferBytes(buffer));
787 let reader = ParquetRecordBatchReaderBuilder::try_new(bytes)
788 .and_then(ParquetRecordBatchReaderBuilder::build)
789 .map_err(|e| powerio_tx::Error::FormatRead {
790 format: "gridfm",
791 message: format!("reading {name}: {e}"),
792 })?;
793 reader
794 .collect::<std::result::Result<Vec<_>, _>>()
795 .map_err(|e| powerio_tx::Error::FormatRead {
796 format: "gridfm",
797 message: format!("decoding {name}: {e}"),
798 })
799}
800
801fn read_meta_source(source: &powerio_core::Source, prefix: &str) -> (f64, String, Diagnostics) {
802 let fallback_name = || {
803 prefix.strip_suffix("/raw/").map_or_else(
804 || {
805 std::path::Path::new(source.name())
806 .file_name()
807 .and_then(|s| s.to_str())
808 .map_or_else(|| "gridfm".to_owned(), str::to_owned)
809 },
810 str::to_owned,
811 )
812 };
813 let Ok(meta_path) = powerio_core::ArtifactPath::new(format!("{prefix}gridfm_meta.json")) else {
814 return (
815 100.0,
816 fallback_name(),
817 defaulted_meta("gridfm_meta.json name did not validate; base_mva defaulted to 100"),
818 );
819 };
820 let Ok(buffer) = source.buffer(&meta_path) else {
821 return (
822 100.0,
823 fallback_name(),
824 defaulted_meta("gridfm_meta.json could not be acquired; base_mva defaulted to 100"),
825 );
826 };
827 let Ok(text) = std::str::from_utf8(buffer.content_bytes()) else {
828 return (
829 100.0,
830 fallback_name(),
831 defaulted_meta("gridfm_meta.json is not UTF-8; base_mva defaulted to 100"),
832 );
833 };
834 let Ok(meta) = serde_json::from_str::<serde_json::Value>(text) else {
835 return (
836 100.0,
837 fallback_name(),
838 defaulted_meta("gridfm_meta.json is not valid JSON; base_mva defaulted to 100"),
839 );
840 };
841 let name = meta
842 .get("case_name")
843 .and_then(serde_json::Value::as_str)
844 .map_or_else(fallback_name, str::to_string);
845 let mut warnings = Diagnostics::new();
846 let base = match meta.get("base_mva").and_then(serde_json::Value::as_f64) {
847 Some(b) if b.is_finite() && b > 0.0 => b,
848 _ => {
849 warnings.push(
850 &codes::READ_GRIDFM_VALUE_DEFAULTED,
851 "gridfm_meta.json has no usable base_mva (absent or not a positive number); \
852 defaulted to 100",
853 );
854 100.0
855 }
856 };
857 (base, name, warnings)
858}
859
860pub(crate) fn parse_gridfm_source(
864 source: &powerio_core::Source,
865) -> std::result::Result<
866 (
867 powerio_core::ScenarioSet<BalancedNetwork>,
868 Vec<powerio_core::Diagnostic>,
869 ),
870 powerio_core::Error,
871> {
872 let entries = source.entry_names()?;
873 let prefix = resolve_raw_prefix(&entries).map_err(|error| core_error(&error))?;
874 let (base_mva, name, meta_warnings) = read_meta_source(source, &prefix);
875 let bus = bus_columns(
876 &read_parquet_buffer(source, &prefix, "bus_data.parquet")
877 .map_err(|error| core_error(&error))?,
878 )
879 .map_err(|error| core_error(&error))?;
880 let gens = gen_columns(
881 &read_parquet_buffer(source, &prefix, "gen_data.parquet")
882 .map_err(|error| core_error(&error))?,
883 )
884 .map_err(|error| core_error(&error))?;
885 let branch = branch_columns(
886 &read_parquet_buffer(source, &prefix, "branch_data.parquet")
887 .map_err(|error| core_error(&error))?,
888 )
889 .map_err(|error| core_error(&error))?;
890
891 let mut diagnostics = Vec::new();
892 let mut scenarios = Vec::new();
893 let mut donor: Option<BalancedNetwork> = None;
894 for scenario in distinct_sorted(&bus.scenario) {
895 let read = build_network_from_columns(
896 &bus,
897 &gens,
898 &branch,
899 scenario,
900 base_mva,
901 &name,
902 meta_warnings.clone(),
903 )
904 .map_err(|error| core_error(&error))?;
905 let mut network = read.network;
906 match &donor {
907 Some(base) => network.share_equal_tables(base),
908 None => donor = Some(network.clone()),
909 }
910 diagnostics.extend(read.diagnostics);
911 let id = powerio_core::ScenarioId::new(read.scenario.to_string())?;
912 scenarios.push(powerio_core::Scenario::new(id, None, network));
913 }
914 let set = powerio_core::ScenarioSet::new(scenarios)?;
915 Ok((set, diagnostics))
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921
922 #[test]
923 fn require_scenario_block_flags_partial_tables() {
924 assert!(require_scenario_block(&[], 0, &[], "gen_data").is_ok());
928 assert!(require_scenario_block(&[0, 0, 1], 0, &[0, 1], "gen_data").is_ok());
929 let err = require_scenario_block(&[0, 0], 1, &[], "branch_data").unwrap_err();
930 assert!(
931 matches!(
932 err,
933 powerio_tx::Error::FormatRead {
934 format: "gridfm",
935 ..
936 }
937 ),
938 "got {err:?}"
939 );
940 }
941}