1use std::cell::{Cell, RefCell};
54use std::collections::HashMap;
55use std::collections::hash_map::Entry;
56
57use super::map::{BRANCH_DEVICE_TYPE, LINE_CIRCUIT, derive_bus_kinds};
58use crate::network::{
59 BalancedNetwork, Branch, Bus, BusId, BusType, Extras, Generator, Load, Shunt, SourceFormat,
60};
61use crate::{Error, Result};
62
63const FMT: &str = "PowerWorld .pwb";
64
65const MVA_BASE: f64 = 100.0;
69
70const RESYNC_WINDOW: usize = 1024;
74
75const SEARCH_PROBE_BUDGET: u64 = 128_000_000;
87
88struct SearchBudget(Cell<u64>);
92
93impl SearchBudget {
94 fn new() -> Self {
95 Self(Cell::new(0))
96 }
97
98 fn tick(&self) -> bool {
99 let spent = self.0.get().saturating_add(1);
100 self.0.set(spent);
101 spent <= SEARCH_PROBE_BUDGET
102 }
103
104 fn exhausted(&self) -> bool {
105 self.0.get() > SEARCH_PROBE_BUDGET
106 }
107}
108
109type Probe<T> = std::result::Result<T, &'static str>;
114
115pub fn parse_pwb(bytes: &[u8], name_hint: Option<&str>) -> Result<BalancedNetwork> {
122 let header_constant = expect_header(bytes)?;
123 reject_unsupported_vintage(bytes)?;
124 let gen_variants = match header_constant {
132 338 | 368 | 425 => GenVariants {
133 plain: true,
134 reg: false,
135 simple_reg: false,
136 },
137 508 => GenVariants {
138 plain: true,
139 reg: true,
140 simple_reg: false,
141 },
142 554 => GenVariants {
143 plain: false,
144 reg: false,
145 simple_reg: true,
146 },
147 _ => GenVariants {
148 plain: false,
149 reg: true,
150 simple_reg: false,
151 },
152 };
153 let branch_count_can_include_trailer = header_constant == 554;
154 let narrow_glue = if header_constant == 425 {
155 DeviceGlue::old_425()
156 } else {
157 DeviceGlue::wide()
158 };
159 let wide_glue = DeviceGlue::wide();
160 let bus_runs = RefCell::new(HashMap::new());
168 let budget = SearchBudget::new();
169 let found = search_table_chain(
170 bytes,
171 name_hint,
172 gen_variants,
173 branch_count_can_include_trailer,
174 &bus_runs,
175 narrow_glue,
176 false,
177 &budget,
178 )
179 .or_else(|| {
180 let retry = |wide_bus_glue, device_glue| {
181 search_table_chain(
182 bytes,
183 name_hint,
184 gen_variants,
185 branch_count_can_include_trailer,
186 &bus_runs,
187 device_glue,
188 wide_bus_glue,
189 &budget,
190 )
191 };
192 (wide_glue != narrow_glue)
193 .then(|| retry(false, wide_glue))
194 .flatten()
195 .or_else(|| retry(true, narrow_glue))
196 .or_else(|| {
197 (wide_glue != narrow_glue)
198 .then(|| retry(true, wide_glue))
199 .flatten()
200 })
201 });
202 found.unwrap_or_else(|| {
203 if budget.exhausted() {
204 return Err(Error::FormatRead {
205 format: FMT,
206 message: "table search exceeded its work budget; the bytes are not a \
207 decodable .pwb layout"
208 .into(),
209 });
210 }
211 Err(Error::FormatRead {
212 format: FMT,
213 message: "no table chain matches the validated .pwb layouts \
214 (buses, loads, generators, shunts, branches in sequence)"
215 .into(),
216 })
217 })
218}
219
220#[derive(Clone, Copy)]
226struct GenVariants {
227 plain: bool,
228 reg: bool,
229 simple_reg: bool,
230}
231
232#[derive(Clone, Copy, PartialEq, Eq)]
233struct DeviceGlue {
234 load: usize,
235 plain_gen: usize,
236 reg_gen: usize,
237 simple_reg_gen: usize,
238}
239
240impl DeviceGlue {
241 fn old_425() -> Self {
242 Self {
243 load: 48,
244 plain_gen: 48,
245 reg_gen: 128,
246 simple_reg_gen: 128,
247 }
248 }
249
250 fn wide() -> Self {
251 Self {
252 load: 128,
253 plain_gen: 128,
254 reg_gen: 128,
255 simple_reg_gen: 128,
256 }
257 }
258}
259
260#[expect(clippy::too_many_lines, clippy::too_many_arguments)]
264fn search_table_chain(
265 bytes: &[u8],
266 name_hint: Option<&str>,
267 gen_variants: GenVariants,
268 branch_count_can_include_trailer: bool,
269 bus_runs: &RefCell<BusRuns>,
270 device_glue: DeviceGlue,
271 wide_bus_glue: bool,
272 budget: &SearchBudget,
273) -> Option<Result<BalancedNetwork>> {
274 for (buses, bus_shunts, bus_end, last_bus_unk) in
284 bus_table_candidates(bytes, bus_runs, wide_bus_glue, budget)
285 {
286 let Some(bus_ids) = BusIdSet::new(&buses) else {
287 continue; };
289 let mut best = None;
290 let bus_names = bus_name_map(&buses);
291 let load_runs = RefCell::new(HashMap::new());
294 let gen_runs = RefCell::new(HashMap::new());
295 let gen_reg_runs = RefCell::new(HashMap::new());
296 let gen_reg_simple_runs = RefCell::new(HashMap::new());
297 let shunt_runs = RefCell::new(HashMap::new());
298 let branch_runs = RefCell::new(HashMap::new());
299 let load_scan_end = resync_end(bytes, bus_end, last_bus_unk & 0x10 != 0);
304 for (loads, l_end) in device_table_candidates(
305 bytes,
306 bus_end..load_scan_end,
307 &bus_ids,
308 read_load_record,
309 &load_runs,
310 device_glue.load,
311 12,
312 budget,
313 ) {
314 let gen_candidates = gen_variants
325 .reg
326 .then(|| {
327 device_table_candidates(
328 bytes,
329 l_end..l_end.saturating_add(RESYNC_WINDOW),
330 &bus_ids,
331 read_gen_reg_record,
332 &gen_reg_runs,
333 device_glue.reg_gen,
334 40,
335 budget,
336 )
337 })
338 .into_iter()
339 .flatten()
340 .chain(
341 gen_variants
342 .simple_reg
343 .then(|| {
344 device_table_candidates(
345 bytes,
346 l_end..l_end.saturating_add(RESYNC_WINDOW),
347 &bus_ids,
348 read_gen_reg_simple_record,
349 &gen_reg_simple_runs,
350 device_glue.simple_reg_gen,
351 40,
352 budget,
353 )
354 })
355 .into_iter()
356 .flatten(),
357 )
358 .chain(
359 gen_variants
360 .plain
361 .then(|| {
362 device_table_candidates(
363 bytes,
364 l_end..l_end.saturating_add(RESYNC_WINDOW),
365 &bus_ids,
366 read_gen_record,
367 &gen_runs,
368 device_glue.plain_gen,
369 32,
370 budget,
371 )
372 })
373 .into_iter()
374 .flatten(),
375 );
376 for (generators, g_end) in gen_candidates {
377 if gen_table_continues(bytes, g_end, &bus_ids, gen_variants, budget) {
378 continue;
379 }
380 if !bus_shunts.is_empty() {
381 if let Some(branches) = find_branch_table(
382 bytes,
383 g_end,
384 &bus_ids,
385 &bus_names,
386 &branch_runs,
387 branch_count_can_include_trailer,
388 budget,
389 ) {
390 keep_best_chain(
391 &mut best,
392 chain_score(&loads, &bus_shunts, &branches, &generators),
393 checked_network(
394 name_hint,
395 buses.clone(),
396 loads.clone(),
397 bus_shunts.clone(),
398 branches,
399 generators.clone(),
400 ),
401 );
402 }
403 }
404 for (shunts, s_end) in device_table_candidates(
405 bytes,
406 g_end..g_end.saturating_add(RESYNC_WINDOW),
407 &bus_ids,
408 read_shunt_record,
409 &shunt_runs,
410 48,
411 28,
412 budget,
413 ) {
414 let Some(branches) = find_branch_table(
415 bytes,
416 s_end,
417 &bus_ids,
418 &bus_names,
419 &branch_runs,
420 branch_count_can_include_trailer,
421 budget,
422 ) else {
423 continue;
424 };
425 let mut shunts = shunts;
426 extend_unique_shunts(&mut shunts, &bus_shunts);
427 let score = chain_score(&loads, &shunts, &branches, &generators);
428 let net = checked_network(
429 name_hint,
430 buses.clone(),
431 loads.clone(),
432 shunts,
433 branches,
434 generators.clone(),
435 );
436 keep_best_chain(&mut best, score, net);
437 }
438 if let Some(branches) = find_branch_table(
439 bytes,
440 g_end,
441 &bus_ids,
442 &bus_names,
443 &branch_runs,
444 branch_count_can_include_trailer,
445 budget,
446 ) {
447 keep_best_chain(
448 &mut best,
449 chain_score(&loads, &bus_shunts, &branches, &generators),
450 checked_network(
451 name_hint,
452 buses.clone(),
453 loads.clone(),
454 bus_shunts.clone(),
455 branches,
456 generators.clone(),
457 ),
458 );
459 }
460 }
461 }
462 if let Some((_, net)) = best {
463 return Some(net);
464 }
465 }
466 None
467}
468
469fn keep_best_chain(
471 best: &mut Option<(usize, Result<BalancedNetwork>)>,
472 score: usize,
473 net: Result<BalancedNetwork>,
474) {
475 let candidate_ok = net.is_ok();
476 let replace = match best.as_ref() {
477 None => true,
478 Some((best_score, best_net)) => match (best_net.is_ok(), candidate_ok) {
479 (false, true) => true,
480 (true, false) => false,
481 _ => score > *best_score,
482 },
483 };
484 if replace {
485 *best = Some((score, net));
486 }
487}
488
489fn chain_score(
491 loads: &[Load],
492 shunts: &[Shunt],
493 branches: &[Branch],
494 generators: &[Generator],
495) -> usize {
496 loads.len() + shunts.len() + branches.len() + generators.len()
497}
498
499fn extend_unique_shunts(shunts: &mut Vec<Shunt>, extra: &[Shunt]) {
501 for shunt in extra {
502 if !shunts.iter().any(|existing| {
503 existing.bus == shunt.bus
504 && (existing.g - shunt.g).abs() <= 1e-9
505 && (existing.b - shunt.b).abs() <= 1e-9
506 }) {
507 shunts.push(shunt.clone());
508 }
509 }
510}
511
512fn gen_table_continues(
517 bytes: &[u8],
518 after: usize,
519 bus_ids: &BusIdSet,
520 variants: GenVariants,
521 budget: &SearchBudget,
522) -> bool {
523 (after..after.saturating_add(RESYNC_WINDOW).min(bytes.len()))
524 .take_while(|_| budget.tick())
525 .any(|p| {
526 (variants.plain && read_gen_record(bytes, p, bus_ids).is_ok())
527 || (variants.reg && read_gen_reg_record(bytes, p, bus_ids).is_ok())
528 || (variants.simple_reg && read_gen_reg_simple_record(bytes, p, bus_ids).is_ok())
529 })
530}
531
532fn checked_network(
534 name_hint: Option<&str>,
535 mut buses: Vec<Bus>,
536 loads: Vec<Load>,
537 shunts: Vec<Shunt>,
538 branches: Vec<Branch>,
539 generators: Vec<Generator>,
540) -> Result<BalancedNetwork> {
541 derive_bus_kinds(&mut buses, &generators);
542 let net = BalancedNetwork {
543 name: name_hint.unwrap_or("case").to_string(),
544 base_mva: MVA_BASE,
545 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
546 geo: None,
547 buses,
548 loads,
549 shunts,
550 branches,
551 switches: Vec::new(),
552 generators,
553 storage: Vec::new(),
554 hvdc: Vec::new(),
555 transformers_3w: Vec::new(),
556 areas: Vec::new(),
557 solver: None,
558 source_format: SourceFormat::PowerWorldBinary,
559 source: None,
560 };
561 net.check_references(FMT).map(|()| net)
562}
563
564struct Cur<'a> {
568 b: &'a [u8],
569 pos: usize,
570}
571
572impl<'a> Cur<'a> {
573 fn take(&mut self, n: usize) -> Probe<&'a [u8]> {
575 let end = self.pos.checked_add(n).ok_or("truncated record")?;
576 let s = self.b.get(self.pos..end).ok_or("truncated record")?;
577 self.pos = end;
578 Ok(s)
579 }
580
581 fn u8(&mut self) -> Probe<u8> {
583 Ok(self.take(1)?[0])
584 }
585 fn u16(&mut self) -> Probe<u16> {
587 Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
588 }
589 fn u32(&mut self) -> Probe<u32> {
591 Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
592 }
593 fn f32(&mut self) -> Probe<f64> {
595 Ok(f64::from(f32::from_le_bytes(
596 self.take(4)?.try_into().unwrap(),
597 )))
598 }
599 fn f64(&mut self) -> Probe<f64> {
601 Ok(f64::from_le_bytes(self.take(8)?.try_into().unwrap()))
602 }
603
604 fn string(&mut self, max: usize) -> Probe<&'a [u8]> {
608 let n = self.u32()? as usize;
609 if n > max {
610 return Err("string length exceeds the field maximum");
611 }
612 let s = self.take(n)?;
613 if !printable(s) {
614 return Err("string has non printable bytes");
615 }
616 Ok(s)
617 }
618
619 fn short_string(&mut self, max: usize) -> Probe<&'a [u8]> {
621 let n = self.u8()? as usize;
622 if n > max {
623 return Err("device ID length exceeds the field maximum");
624 }
625 let s = self.take(n)?;
626 if !printable(s) {
627 return Err("device ID has non printable bytes");
628 }
629 Ok(s)
630 }
631
632 fn short_string_2(&mut self) -> Probe<&'a [u8]> {
637 let n = self.u8()? as usize;
638 if n == 0 || n > 2 {
639 return Err("fixed capacity ID length not 1 or 2");
640 }
641 let text = self.take(2)?;
642 if !printable(&text[..n]) {
643 return Err("fixed capacity ID has non printable bytes");
644 }
645 Ok(&text[..n])
646 }
647}
648
649const BLOB_WINDOW: usize = 4 << 20;
655
656fn resync_end(b: &[u8], after: usize, prev_bit4: bool) -> usize {
662 if prev_bit4 {
663 after.saturating_add(BLOB_WINDOW).min(b.len())
664 } else {
665 after.saturating_add(RESYNC_WINDOW).min(b.len())
666 }
667}
668
669fn printable(s: &[u8]) -> bool {
671 s.iter().all(|&c| (0x20..0x7f).contains(&c))
672}
673
674fn slice_at(b: &[u8], at: usize, n: usize) -> Option<&[u8]> {
676 at.checked_add(n).and_then(|end| b.get(at..end))
677}
678
679fn checked_offset(at: usize, add: usize) -> Probe<usize> {
681 at.checked_add(add).ok_or("truncated record")
682}
683
684fn count_fits(b: &[u8], first: usize, count: usize, min_record_len: usize) -> bool {
686 let Some(remaining) = b.len().checked_sub(first) else {
687 return false;
688 };
689 count
690 .checked_mul(min_record_len)
691 .is_some_and(|min_bytes| min_bytes <= remaining)
692}
693
694fn u32_at(b: &[u8], at: usize) -> Probe<u32> {
696 slice_at(b, at, 4)
697 .and_then(|s| <[u8; 4]>::try_from(s).ok())
698 .map(u32::from_le_bytes)
699 .ok_or("truncated record")
700}
701
702fn f32_at(b: &[u8], at: usize) -> Probe<f64> {
704 slice_at(b, at, 4)
705 .and_then(|s| <[u8; 4]>::try_from(s).ok())
706 .map(f32::from_le_bytes)
707 .map(f64::from)
708 .ok_or("truncated record")
709}
710
711fn string_at(b: &[u8], at: usize, max: usize) -> Probe<String> {
713 let n = u32_at(b, at)? as usize;
714 if n > max {
715 return Err("string length exceeds the field maximum");
716 }
717 let s = slice_at(b, checked_offset(at, 4)?, n).ok_or("truncated record")?;
718 if !printable(s) {
719 return Err("string has non printable bytes");
720 }
721 Ok(String::from_utf8_lossy(s).into_owned())
722}
723
724fn expect_header(b: &[u8]) -> Result<u64> {
727 const DECODED: [u64; 9] = [338, 368, 425, 483, 508, 537, 550, 551, 554];
728 let bad = || Error::FormatRead {
729 format: FMT,
730 message: "not a recognized PowerWorld binary case (header magic mismatch); \
731 only the validated .pwb layouts are read"
732 .into(),
733 };
734 if b.len() < 0x40 {
735 return Err(bad());
736 }
737 let word = |i: usize| u64::from_le_bytes(b[i * 8..i * 8 + 8].try_into().unwrap());
738 let (a, v, c) = (word(0), word(1), word(2));
739 if a != 15000 {
740 return Err(bad());
741 }
742 if c != 20 || !DECODED.contains(&v) {
749 return Err(unsupported_vintage(format!(
750 "header format words ({v}, {c}); the decoded eras are \
751 338/368/425/483/508/537/550/551/554 with 20"
752 )));
753 }
754 Ok(v)
755}
756
757fn reject_unsupported_vintage(b: &[u8]) -> Result<()> {
763 let scan = b.len().min(0x10000).saturating_sub(8);
764 let mut heads = 0usize;
765 let mut at = 0x20;
766 while at < scan {
767 let Ok((_, after)) = read_bus_head(b, at) else {
768 at += 1;
769 continue;
770 };
771 heads += 1;
772 if heads >= 2 {
773 return Ok(());
774 }
775 at = after;
776 }
777 Err(unsupported_vintage(
778 "no recognized bus record layout in the leading 64 KiB",
779 ))
780}
781
782fn unsupported_vintage(detail: impl std::fmt::Display) -> Error {
785 Error::FormatRead {
786 format: FMT,
787 message: format!(
788 "unsupported PowerWorld .pwb vintage: {detail}; only the validated \
789 338/368/425/483/508/537/550/551/554 layouts are decoded \
790 (see powerio/src/format/powerworld/FORMAT.md)"
791 ),
792 }
793}
794
795enum BusIdSet {
805 Bitmap(Vec<u64>),
806 Sparse(Vec<usize>),
807}
808
809impl BusIdSet {
810 fn new(buses: &[Bus]) -> Option<Self> {
813 let max = buses.iter().map(|b| b.id.0).max().unwrap_or(0);
814 let words = max / 64 + 1;
815 if words > (buses.len() * 4).max(1024) {
816 let mut ids: Vec<usize> = buses.iter().map(|b| b.id.0).collect();
817 ids.sort_unstable();
818 if ids.windows(2).any(|w| w[0] == w[1]) {
819 return None;
820 }
821 return Some(Self::Sparse(ids));
822 }
823 let mut bits = vec![0u64; words];
824 for bus in buses {
825 let (w, bit) = (bus.id.0 / 64, 1u64 << (bus.id.0 % 64));
826 if bits[w] & bit != 0 {
827 return None;
828 }
829 bits[w] |= bit;
830 }
831 Some(Self::Bitmap(bits))
832 }
833
834 #[inline]
836 fn contains(&self, id: usize) -> bool {
837 match self {
838 Self::Bitmap(words) => words
839 .get(id / 64)
840 .is_some_and(|w| w & (1 << (id % 64)) != 0),
841 Self::Sparse(ids) => ids.binary_search(&id).is_ok(),
842 }
843 }
844}
845
846fn bus_name_map(buses: &[Bus]) -> HashMap<String, BusId> {
848 buses
849 .iter()
850 .filter_map(|bus| {
851 bus.name
852 .as_ref()
853 .map(|name| (name.trim().to_ascii_uppercase(), bus.id))
854 })
855 .collect()
856}
857
858struct Run<T> {
864 items: Vec<T>,
865 ends: Vec<usize>,
867 dead: bool,
869}
870
871impl<T: Clone> Run<T> {
872 fn start(item: T, end: usize) -> Self {
874 Run {
875 items: vec![item],
876 ends: vec![end],
877 dead: false,
878 }
879 }
880
881 fn prefix(
886 &mut self,
887 count: usize,
888 mut next: impl FnMut(usize, &T) -> Option<(T, usize)>,
889 ) -> Option<(Vec<T>, usize)> {
890 if count == 0 {
891 return None; }
893 while !self.dead && self.items.len() < count {
894 let after = *self.ends.last().unwrap();
895 match next(after, self.items.last().unwrap()) {
896 Some((item, end)) => {
897 self.items.push(item);
898 self.ends.push(end);
899 }
900 None => self.dead = true,
901 }
902 }
903 (self.items.len() >= count).then(|| (self.items[..count].to_vec(), self.ends[count - 1]))
904 }
905}
906
907struct BusHead {
910 bus: Bus,
911 shunt: Option<Shunt>,
912 unk: u32,
923}
924
925fn known_bus_flags(unk: u32) -> bool {
931 unk & !0x3571 == 0x06
932}
933
934fn bus_family(unk: u32) -> u32 {
939 unk & !0x3551
940}
941
942type BusRunItem = (Bus, u32, Option<Shunt>);
945type BusRuns = HashMap<usize, (Run<BusRunItem>, u32)>;
946
947fn bus_table_candidates<'a>(
953 b: &'a [u8],
954 runs: &'a RefCell<BusRuns>,
955 wide_glue: bool,
956 budget: &'a SearchBudget,
957) -> impl Iterator<Item = (Vec<Bus>, Vec<Shunt>, usize, u32)> + 'a {
958 let limit = b.len().saturating_sub(4).min(0x10000);
959 (0x20..limit)
960 .take_while(move |_| !budget.exhausted())
961 .flat_map(move |at| {
962 let count = u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) as usize;
963 let glues = if wide_glue {
974 (count != 0 && count < 256).then_some(49..=96)
975 } else {
976 let max_glue = if count >= 256 { 96 } else { 48 };
977 (count != 0 && count <= 2_000_000).then_some(0..=max_glue)
978 };
979 glues
980 .into_iter()
981 .flatten()
982 .filter_map(move |glue| {
983 if !budget.tick() {
984 return None;
985 }
986 let first = at.checked_add(4)?.checked_add(glue)?;
987 count_fits(b, first, count, 32)
988 .then(|| bus_run(b, runs, first, count, budget))
989 .flatten()
990 })
991 .map(|(heads, end)| {
992 let last_unk = heads.last().map_or(0, |(_, unk, _)| *unk);
993 let shunts = heads
994 .iter()
995 .filter_map(|(bus, _, shunt)| {
996 shunt.clone().map(|mut shunt| {
997 shunt.bus = bus.id;
998 shunt
999 })
1000 })
1001 .collect();
1002 (
1003 heads.into_iter().map(|(bus, _, _)| bus).collect(),
1004 shunts,
1005 end,
1006 last_unk,
1007 )
1008 })
1009 })
1010}
1011
1012fn bus_run(
1019 b: &[u8],
1020 runs: &RefCell<BusRuns>,
1021 first: usize,
1022 count: usize,
1023 budget: &SearchBudget,
1024) -> Option<(Vec<BusRunItem>, usize)> {
1025 let mut map = runs.borrow_mut();
1026 let (run, family) = match map.entry(first) {
1027 Entry::Occupied(e) => e.into_mut(),
1028 Entry::Vacant(e) => {
1032 let (head, end) = read_bus_head(b, first).ok()?;
1033 let family = bus_family(head.unk);
1034 e.insert((Run::start((head.bus, head.unk, head.shunt), end), family))
1035 }
1036 };
1037 let family = *family;
1038 run.prefix(count, |after, prev| {
1039 (after..resync_end(b, after, prev.1 & 0x10 != 0))
1043 .take_while(|_| budget.tick())
1044 .find_map(|p| {
1045 read_bus_head(b, p)
1046 .ok()
1047 .filter(|(h, _)| bus_family(h.unk) == family)
1048 .map(|(h, end)| ((h.bus, h.unk, h.shunt), end))
1049 })
1050 })
1051}
1052
1053fn read_bus_head(b: &[u8], at: usize) -> Probe<(BusHead, usize)> {
1058 let mut c = Cur { b, pos: at };
1059 let num = c.u32()? as usize;
1060 if num == 0 || num > 99_999_999 {
1061 return Err("implausible bus number");
1062 }
1063 let name_len = c.u32()? as usize;
1064 if name_len == 0 {
1065 return Err("empty bus name");
1066 }
1067 if name_len > 64 {
1068 return Err("string length exceeds the field maximum");
1069 }
1070 let name = c.take(name_len)?;
1071 let unk = c.u32()?;
1075 if !known_bus_flags(unk) {
1076 return Err("bus record flags not in the validated set");
1077 }
1078 if !printable(name) {
1079 return Err("string has non printable bytes");
1080 }
1081 if unk & 1 == 0 {
1082 let _extra = c.u16()?;
1083 }
1084 let kv = c.f32()?;
1085 if !kv.is_finite() || !(0.0..=10_000.0).contains(&kv) {
1086 return Err("implausible nominal kV");
1087 }
1088 let area = c.u32()? as usize;
1089 let zone = c.u32()? as usize;
1090 if area > 100_000_000 || zone > 100_000_000 {
1091 return Err("implausible area/zone/BA number");
1092 }
1093 let after_zone = c.pos;
1094 let mut with_ba = Cur { b, pos: after_zone };
1095 let with_ba_result = (|| -> Probe<(f64, f64)> {
1096 let ba = with_ba.u32()?;
1097 if ba > 100_000_000 {
1098 return Err("implausible area/zone/BA number");
1099 }
1100 read_bus_label_and_solution(&mut with_ba)
1101 })();
1102 let (vm, va_rad) = if let Ok(solution) = with_ba_result {
1103 c.pos = with_ba.pos;
1104 solution
1105 } else {
1106 let mut old = Cur { b, pos: after_zone };
1107 let solution = read_bus_label_and_solution(&mut old)?;
1108 c.pos = old.pos;
1109 solution
1110 };
1111 if !vm.is_finite() || !(0.0..=10.0).contains(&vm) || !va_rad.is_finite() || va_rad.abs() > 100.0
1112 {
1113 return Err("implausible voltage solution");
1114 }
1115 let bus = Bus {
1116 id: BusId(num),
1117 kind: BusType::Pq,
1118 vm,
1119 va: va_rad.to_degrees(),
1120 base_kv: kv,
1121 vmax: 1.1,
1122 vmin: 0.9,
1123 evhi: None,
1124 evlo: None,
1125 area,
1126 zone,
1127 name: Some(String::from_utf8_lossy(name).into_owned()),
1128 uid: None,
1129 location: None,
1130 extras: Extras::new(),
1131 };
1132 let shunt = bus_tail_shunt(b, c.pos, BusId(num));
1133 Ok((BusHead { bus, shunt, unk }, c.pos))
1134}
1135
1136fn bus_tail_shunt(b: &[u8], after_head: usize, bus: BusId) -> Option<Shunt> {
1138 let g_pu = b
1139 .get(after_head.checked_add(1)?..after_head.checked_add(5)?)
1140 .and_then(|s| <[u8; 4]>::try_from(s).ok())
1141 .map(f32::from_le_bytes)
1142 .map(f64::from)
1143 .filter(|g| g.is_finite() && g.abs() <= 1.0e6)
1144 .unwrap_or(0.0);
1145 let b_pu = b
1146 .get(after_head.checked_add(5)?..after_head.checked_add(9)?)
1147 .and_then(|s| <[u8; 4]>::try_from(s).ok())
1148 .map(f32::from_le_bytes)
1149 .map(f64::from)?;
1150 if !b_pu.is_finite() || b_pu.abs() > 1.0e6 || (g_pu.abs() <= 1e-9 && b_pu.abs() <= 1e-9) {
1151 return None;
1152 }
1153 let mut extras = Extras::new();
1154 extras.insert(
1155 "ShuntID".into(),
1156 serde_json::Value::String("BusShunt".into()),
1157 );
1158 Some(Shunt {
1159 bus,
1160 g: g_pu * MVA_BASE,
1161 b: b_pu * MVA_BASE,
1162 in_service: true,
1163 control: None,
1164 uid: None,
1165 extras,
1166 })
1167}
1168
1169fn read_bus_label_and_solution(c: &mut Cur<'_>) -> Probe<(f64, f64)> {
1171 let _label = c.string(64)?;
1172 let vm = c.f64()?;
1173 let va_rad = c.f64()?;
1174 Ok((vm, va_rad))
1175}
1176
1177trait ReadRecord<T>: Fn(&[u8], usize, &BusIdSet) -> Probe<(T, usize)> + Copy {}
1186impl<T, F: Fn(&[u8], usize, &BusIdSet) -> Probe<(T, usize)> + Copy> ReadRecord<T> for F {}
1187
1188fn read_device_head<T>(
1192 b: &[u8],
1193 at: usize,
1194 bus_ids: &BusIdSet,
1195 read: fn(&mut Cur, BusId, &[u8]) -> Probe<T>,
1196) -> Probe<(T, usize)> {
1197 let mut c = Cur { b, pos: at };
1198 let bus = c.u32()? as usize;
1199 if !bus_ids.contains(bus) {
1200 return Err("record references an unknown bus");
1201 }
1202 let id = c.short_string(8)?;
1203 if id.is_empty() {
1204 return Err("empty device ID");
1205 }
1206 let v = read(&mut c, BusId(bus), id)?;
1207 Ok((v, c.pos))
1208}
1209
1210fn read_load_record(b: &[u8], at: usize, bus_ids: &BusIdSet) -> Probe<(Load, usize)> {
1212 read_device_head(b, at, bus_ids, read_load)
1213}
1214
1215fn read_gen_record(b: &[u8], at: usize, bus_ids: &BusIdSet) -> Probe<(Generator, usize)> {
1217 read_device_head(b, at, bus_ids, read_gen)
1218}
1219
1220fn read_shunt_record(b: &[u8], at: usize, bus_ids: &BusIdSet) -> Probe<(Shunt, usize)> {
1222 read_device_head(b, at, bus_ids, read_shunt)
1223}
1224
1225#[expect(clippy::too_many_arguments)]
1229fn device_table_candidates<'a, T: Clone + 'a>(
1230 b: &'a [u8],
1231 scan: std::ops::Range<usize>,
1232 bus_ids: &'a BusIdSet,
1233 read: impl ReadRecord<T> + 'a,
1234 runs: &'a RefCell<HashMap<usize, Run<T>>>,
1235 max_glue: usize,
1236 min_record_len: usize,
1237 budget: &'a SearchBudget,
1238) -> impl Iterator<Item = (Vec<T>, usize)> + 'a {
1239 let limit = scan.end.min(b.len().saturating_sub(4));
1240 (scan.start..limit)
1241 .take_while(move |_| !budget.exhausted())
1242 .flat_map(move |at| {
1243 let count = u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) as usize;
1244 let glues = (count != 0 && count <= 10_000_000).then_some(0..=max_glue);
1245 glues.into_iter().flatten().filter_map(move |glue| {
1246 if !budget.tick() {
1247 return None;
1248 }
1249 let first = at.checked_add(4)?.checked_add(glue)?;
1250 count_fits(b, first, count, min_record_len)
1251 .then(|| device_run(b, runs, first, count, bus_ids, read, budget))
1252 .flatten()
1253 })
1254 })
1255}
1256
1257fn device_run<T: Clone>(
1260 b: &[u8],
1261 runs: &RefCell<HashMap<usize, Run<T>>>,
1262 first: usize,
1263 count: usize,
1264 bus_ids: &BusIdSet,
1265 read: impl ReadRecord<T>,
1266 budget: &SearchBudget,
1267) -> Option<(Vec<T>, usize)> {
1268 let mut map = runs.borrow_mut();
1269 let run = match map.entry(first) {
1270 Entry::Occupied(e) => e.into_mut(),
1271 Entry::Vacant(e) => {
1273 let (item, end) = read(b, first, bus_ids).ok()?;
1274 e.insert(Run::start(item, end))
1275 }
1276 };
1277 run.prefix(count, |after, _| {
1278 (after..after.saturating_add(RESYNC_WINDOW).min(b.len()))
1280 .take_while(|_| budget.tick())
1281 .find_map(|p| read(b, p, bus_ids).ok())
1282 })
1283}
1284
1285fn read_load(c: &mut Cur, bus: BusId, id: &[u8]) -> Probe<Load> {
1290 let record_start = c.pos - (4 + 1) - id.len(); let flag = c.u8()?;
1292 if flag > 1 {
1293 return Err("load status byte not in the validated set");
1294 }
1295 let mut p = c.f32()? * MVA_BASE;
1296 let mut q = c.f32()? * MVA_BASE;
1297 let mut in_service = true;
1298 if flag == 0 && p.abs() < 1e-30 && q.abs() < 1e-30 {
1299 let early_p = f32_at(c.b, checked_offset(record_start, 25)?)? * MVA_BASE;
1300 let early_q = f32_at(c.b, checked_offset(record_start, 29)?)? * MVA_BASE;
1301 let late_p = f32_at(c.b, checked_offset(record_start, 33)?)? * MVA_BASE;
1302 let late_q = f32_at(c.b, checked_offset(record_start, 37)?)? * MVA_BASE;
1303 let early_is_marker = (early_p - MVA_BASE).abs() <= 1e-6 && early_q.abs() <= 1e-30;
1304 let (alt_p, alt_q, end) = if early_is_marker {
1305 (late_p, late_q, checked_offset(record_start, 41)?)
1306 } else {
1307 (early_p, early_q, checked_offset(record_start, 33)?)
1308 };
1309 if alt_p.abs() > 1e-30 || alt_q.abs() > 1e-30 {
1310 p = alt_p;
1311 q = alt_q;
1312 in_service = !early_is_marker;
1313 }
1314 c.pos = c.pos.max(end);
1315 }
1316 if !p.is_finite() || !q.is_finite() || p.abs() > 1.0e6 || q.abs() > 1.0e6 {
1317 return Err("implausible load power");
1318 }
1319 let mut extras = Extras::new();
1320 extras.insert(
1321 "LoadID".into(),
1322 serde_json::Value::String(String::from_utf8_lossy(id).into_owned()),
1323 );
1324 Ok(Load {
1325 bus,
1326 p,
1327 q,
1328 voltage_model: None,
1329 in_service,
1330 uid: None,
1331 extras,
1332 })
1333}
1334
1335fn read_gen(c: &mut Cur, bus: BusId, id: &[u8]) -> Probe<Generator> {
1345 let record_start = c.pos - (4 + 1) - id.len(); let mut chosen = None;
1347 for anchor in [9usize, 10, 11, 12] {
1350 let mut probe = Cur {
1351 b: c.b,
1352 pos: checked_offset(record_start, anchor)?,
1353 };
1354 if let Ok(vals) = read_gen_f32_block(&mut probe) {
1355 chosen = Some((vals, probe.pos));
1356 break;
1357 }
1358 }
1359 let Some((v, end)) = chosen else {
1360 return Err("generator record does not match the validated layouts");
1361 };
1362 c.pos = end;
1363 Ok(gen_from_block(bus, &v, true))
1366}
1367
1368fn read_gen_f32_block(c: &mut Cur) -> Probe<[f64; 8]> {
1373 let mut v = [0.0f64; 8];
1374 for (i, slot) in v.iter_mut().enumerate() {
1378 let x = c.f32()?;
1379 if !x.is_finite() || x.abs() >= 1.0e6 {
1380 return Err("generator record does not match the validated layouts");
1381 }
1382 if (i == 4 && !(0.5..=1.6).contains(&x)) || (i == 5 && !(0.1..=1.0e5).contains(&x)) {
1383 return Err("generator record does not match the validated layouts");
1384 }
1385 *slot = x;
1386 }
1387 Ok(v)
1388}
1389
1390fn gen_from_block(bus: BusId, v: &[f64; 8], in_service: bool) -> Generator {
1392 Generator {
1393 bus,
1394 pg: v[0] * MVA_BASE,
1395 qg: v[1] * MVA_BASE,
1396 qmax: v[2] * MVA_BASE,
1397 qmin: v[3] * MVA_BASE,
1398 vg: v[4],
1399 mbase: v[5],
1400 pmax: v[6] * MVA_BASE,
1401 pmin: v[7] * MVA_BASE,
1402 in_service,
1403 cost: None,
1404 caps: Default::default(),
1405 regulated_bus: None,
1406 uid: None,
1407 }
1408}
1409
1410fn read_gen_reg_record(b: &[u8], at: usize, bus_ids: &BusIdSet) -> Probe<(Generator, usize)> {
1423 let mut c = Cur { b, pos: at };
1424 let bus = c.u32()? as usize;
1425 if !bus_ids.contains(bus) {
1426 return Err("record references an unknown bus");
1427 }
1428 let reg = c.u32()? as usize;
1429 if !bus_ids.contains(reg) {
1430 return Err("regulated bus is not a known bus");
1431 }
1432 let _id = c.short_string_2()?;
1433 if c.u8()? != 1 {
1434 return Err("generator record lead byte not 1");
1435 }
1436 let _ = c.u8()?; let pres = c.u8()?;
1441 if pres & !0x23 != 0 {
1442 return Err("generator presence byte not in the validated set");
1443 }
1444 if pres & 0x22 == 0x22 {
1445 return Err("generator presence bits 1 and 5 together are unobserved");
1449 }
1450 for bit in [0x01, 0x20] {
1451 if pres & bit != 0 {
1452 let v = c.f32()?;
1453 if !v.is_finite() || v.abs() > 1.0e6 {
1454 return Err("implausible presence gated generator value");
1455 }
1456 }
1457 }
1458 if pres & 2 != 0 {
1459 let _ = c.u8()?;
1460 }
1461 let v = read_gen_f32_block(&mut c)?;
1462 read_gen_reg_tail(&mut c, bus, &v)
1463}
1464
1465fn read_gen_reg_simple_record(
1469 b: &[u8],
1470 at: usize,
1471 bus_ids: &BusIdSet,
1472) -> Probe<(Generator, usize)> {
1473 let mut c = Cur { b, pos: at };
1474 let bus = c.u32()? as usize;
1475 if !bus_ids.contains(bus) {
1476 return Err("record references an unknown bus");
1477 }
1478 let reg = c.u32()? as usize;
1479 if !bus_ids.contains(reg) {
1480 return Err("regulated bus is not a known bus");
1481 }
1482 let _id = c.short_string_2()?;
1483 if c.u8()? != 0 || c.u8()? != 0 {
1484 return Err("generator record separator bytes not zero");
1485 }
1486 let v = read_gen_f32_block(&mut c)?;
1487 read_gen_reg_tail(&mut c, bus, &v)
1488}
1489
1490fn read_gen_reg_tail(c: &mut Cur<'_>, bus: usize, v: &[f64; 8]) -> Probe<(Generator, usize)> {
1492 if c.u8()? != 0 {
1493 return Err("generator record separator byte not zero");
1494 }
1495 let status = c.u8()?;
1496 if status & !0x01 != 0x08 {
1497 return Err("generator status byte not in the validated set");
1498 }
1499 let rmpct = c.f32()?;
1500 if !rmpct.is_finite() || !(0.0..=1000.0).contains(&rmpct) {
1501 return Err("implausible remote regulation percentage");
1502 }
1503 Ok((gen_from_block(BusId(bus), v, status & 1 == 1), c.pos))
1504}
1505
1506fn read_shunt(c: &mut Cur, bus: BusId, id: &[u8]) -> Probe<Shunt> {
1511 let record_start = c.pos - (4 + 1) - id.len(); let mut probe = Cur {
1513 b: c.b,
1514 pos: checked_offset(record_start, 24)?,
1515 };
1516 let b_mvar = probe.f32()? * MVA_BASE;
1517 if !b_mvar.is_finite() || b_mvar.abs() > 1.0e6 {
1518 return Err("implausible shunt MVAr");
1519 }
1520 c.pos = probe.pos;
1521 let mut extras = Extras::new();
1522 extras.insert(
1523 "ShuntID".into(),
1524 serde_json::Value::String(String::from_utf8_lossy(id).into_owned()),
1525 );
1526 Ok(Shunt {
1527 bus,
1528 g: 0.0,
1531 b: b_mvar,
1532 in_service: true,
1533 control: None,
1534 uid: None,
1535 extras,
1536 })
1537}
1538
1539fn known_branch_flags(flags: u16) -> bool {
1563 flags & !0x44B3 == 0x004C
1564}
1565
1566fn find_branch_table(
1571 b: &[u8],
1572 from: usize,
1573 bus_ids: &BusIdSet,
1574 bus_names: &HashMap<String, BusId>,
1575 runs: &RefCell<HashMap<usize, Run<(Branch, u16)>>>,
1576 count_can_include_trailer: bool,
1577 budget: &SearchBudget,
1578) -> Option<Vec<Branch>> {
1579 let limit = from
1582 .saturating_add(RESYNC_WINDOW * 2)
1583 .min(b.len().saturating_sub(4));
1584 for at in from..limit {
1585 let count = u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) as usize;
1586 if count == 0 || count > 10_000_000 {
1587 continue;
1588 }
1589 let Some(first) = (at.saturating_add(4)..at.saturating_add(64).min(b.len()))
1592 .take_while(|_| budget.tick())
1593 .find(|&p| read_branch_head(b, p, bus_ids, bus_names).is_ok())
1594 else {
1595 continue;
1596 };
1597 let counts = [
1598 Some(count),
1599 (count_can_include_trailer && count > 1).then_some(count - 1),
1600 ];
1601 for effective_count in counts.into_iter().flatten() {
1602 if !count_fits(b, at.saturating_add(4), effective_count, 24) {
1603 continue;
1604 }
1605 if let Some((branches, after)) =
1606 branch_run(b, runs, first, effective_count, bus_ids, bus_names, budget)
1607 {
1608 let last_bit4 = branches.last().is_some_and(|(_, flags)| flags & 0x10 != 0);
1613 let continues = (after..resync_end(b, after, last_bit4))
1614 .take_while(|_| budget.tick())
1615 .any(|p| read_branch_head(b, p, bus_ids, bus_names).is_ok());
1616 if budget.exhausted() {
1619 return None;
1620 }
1621 if !continues {
1622 return Some(branches.into_iter().map(|(br, _)| br).collect());
1623 }
1624 }
1625 }
1626 }
1627 None
1628}
1629
1630fn branch_run(
1634 b: &[u8],
1635 runs: &RefCell<HashMap<usize, Run<(Branch, u16)>>>,
1636 first: usize,
1637 count: usize,
1638 bus_ids: &BusIdSet,
1639 bus_names: &HashMap<String, BusId>,
1640 budget: &SearchBudget,
1641) -> Option<(Vec<(Branch, u16)>, usize)> {
1642 let mut map = runs.borrow_mut();
1643 let run = match map.entry(first) {
1644 Entry::Occupied(e) => e.into_mut(),
1645 Entry::Vacant(e) => {
1647 let (br, end, flags) = read_branch_head(b, first, bus_ids, bus_names).ok()?;
1648 e.insert(Run::start((br, flags), end))
1649 }
1650 };
1651 run.prefix(count, |after, prev| {
1652 (after..resync_end(b, after, prev.1 & 0x10 != 0))
1655 .take_while(|_| budget.tick())
1656 .find_map(|p| {
1657 read_branch_head(b, p, bus_ids, bus_names)
1658 .ok()
1659 .map(|(br, end, flags)| ((br, flags), end))
1660 })
1661 })
1662}
1663
1664#[allow(clippy::many_single_char_names)] fn read_branch_head(
1673 b: &[u8],
1674 at: usize,
1675 bus_ids: &BusIdSet,
1676 bus_names: &HashMap<String, BusId>,
1677) -> Probe<(Branch, usize, u16)> {
1678 read_step_up_transformer_head(b, at, bus_ids, bus_names)
1679 .or_else(|_| read_standard_branch_head(b, at, bus_ids))
1680}
1681
1682#[allow(clippy::many_single_char_names)] fn read_standard_branch_head(
1684 b: &[u8],
1685 at: usize,
1686 bus_ids: &BusIdSet,
1687) -> Probe<(Branch, usize, u16)> {
1688 let mut c = Cur { b, pos: at };
1689 let from = branch_endpoint(&mut c)?;
1690 let to = branch_endpoint(&mut c)?;
1691 if !bus_ids.contains(from) || !bus_ids.contains(to) || from == to {
1692 return Err("branch references unknown buses");
1693 }
1694 let flags = c.u16()?;
1695 if !known_branch_flags(flags) {
1696 return Err("branch record flags not in the validated set");
1697 }
1698 let circuit = if flags & 1 == 0 {
1699 Some(c.short_string_2()?)
1700 } else {
1701 None
1704 };
1705 let r = c.f32()?;
1706 let x = c.f32()?;
1707 let b_chg = c.f32()?;
1708 for v in [r, x, b_chg] {
1709 if !v.is_finite() || v.abs() > 1.0e4 {
1710 return Err("implausible branch impedance");
1711 }
1712 }
1713 let _g = c.f32()?;
1714 let inline = if flags & 2 == 0 { 3 } else { 2 };
1715 let mut rates = [0.0f64; 3];
1716 for slot in rates.iter_mut().take(inline) {
1717 let v = c.f32()?;
1718 if !v.is_finite() || !(0.0..=1.0e6).contains(&v) {
1719 return Err("implausible branch rating");
1720 }
1721 *slot = v * MVA_BASE;
1722 }
1723 let tail_start = c.pos;
1724 let tag = c.u32()?;
1725 let (device, tap) = match tag {
1726 12 => read_modern_branch_tail(&mut c)?,
1727 5 => read_legacy_branch_tail(&mut c, tail_start)?,
1728 _ => return Err("branch tail tag not in the validated set"),
1729 };
1730 let mut extras = Extras::new();
1731 extras.insert(
1732 LINE_CIRCUIT.into(),
1733 serde_json::Value::String(circuit.map_or_else(
1734 || " 1".to_string(),
1735 |s| String::from_utf8_lossy(s).into_owned(),
1736 )),
1737 );
1738 extras.insert(
1739 BRANCH_DEVICE_TYPE.into(),
1740 serde_json::Value::String(device.into()),
1741 );
1742 let br = Branch {
1743 from: BusId(from),
1744 to: BusId(to),
1745 r,
1746 x,
1747 b: b_chg,
1748 charging: None,
1749 rate_a: rates[0],
1750 rate_b: rates[1],
1751 rate_c: rates[2],
1752 rating_sets: Vec::new(),
1753 current_ratings: None,
1754 tap,
1755 shift: 0.0,
1758 in_service: true,
1762 angmin: -360.0,
1763 angmax: 360.0,
1764 control: None,
1765 solution: None,
1766 uid: None,
1767 route: None,
1768 extras,
1769 };
1770 Ok((br, c.pos, flags))
1771}
1772
1773fn branch_endpoint(c: &mut Cur<'_>) -> Probe<usize> {
1776 let raw = i32::from_le_bytes(c.take(4)?.try_into().unwrap());
1777 raw.checked_abs()
1778 .and_then(|id| (id > 0).then_some(id as usize))
1779 .ok_or("invalid branch endpoint")
1780}
1781
1782fn read_step_up_transformer_head(
1788 b: &[u8],
1789 at: usize,
1790 bus_ids: &BusIdSet,
1791 bus_names: &HashMap<String, BusId>,
1792) -> Probe<(Branch, usize, u16)> {
1793 if at < 8 {
1794 return Err("step up transformer anchor before record");
1795 }
1796 let from = u32_at(b, at)? as usize;
1797 if !bus_ids.contains(from) {
1798 return Err("step up transformer high side bus is unknown");
1799 }
1800 let nominal = f32_at(b, at - 8)?;
1801 if !nominal.is_finite() || (nominal - 100.0).abs() > 1e-3 {
1802 return Err("step up transformer nominal anchor missing");
1803 }
1804 let x_device = f32_at(b, checked_offset(at, 17)?)?;
1805 let mbase = f32_at(b, checked_offset(at, 197)?)?;
1806 if !x_device.is_finite()
1807 || !(0.0..=100.0).contains(&x_device)
1808 || !mbase.is_finite()
1809 || !(0.1..=1.0e5).contains(&mbase)
1810 {
1811 return Err("step up transformer impedance anchor missing");
1812 }
1813 let name_at = checked_offset(at, 356)?;
1814 let name = string_at(b, name_at, 64)?;
1815 let Some(gen_name) = name.split_whitespace().next() else {
1816 return Err("step up transformer name is empty");
1817 };
1818 if !name.to_ascii_uppercase().contains("STEP UP") {
1819 return Err("step up transformer name anchor missing");
1820 }
1821 let to = bus_names
1822 .get(&format!("GEN {}", gen_name.to_ascii_uppercase()))
1823 .copied()
1824 .ok_or("step up transformer low side bus is unknown")?;
1825 if to.0 == from {
1826 return Err("step up transformer has identical endpoints");
1827 }
1828 let mut extras = Extras::new();
1829 extras.insert(LINE_CIRCUIT.into(), serde_json::Value::String(" 1".into()));
1830 extras.insert(
1831 BRANCH_DEVICE_TYPE.into(),
1832 serde_json::Value::String("Transformer".into()),
1833 );
1834 let br = Branch {
1835 from: BusId(from),
1836 to,
1837 r: 0.0,
1838 x: x_device * mbase / MVA_BASE,
1839 b: 0.0,
1840 charging: None,
1841 rate_a: 0.0,
1842 rate_b: 0.0,
1843 rate_c: 0.0,
1844 rating_sets: Vec::new(),
1845 current_ratings: None,
1846 tap: 1.0,
1847 shift: 0.0,
1848 in_service: true,
1849 angmin: -360.0,
1850 angmax: 360.0,
1851 control: None,
1852 solution: None,
1853 uid: None,
1854 route: None,
1855 extras,
1856 };
1857 Ok((
1858 br,
1859 checked_offset(checked_offset(name_at, 4)?, name.len())?,
1860 0,
1861 ))
1862}
1863
1864fn read_modern_branch_tail(c: &mut Cur<'_>) -> Probe<(&'static str, f64)> {
1868 for _ in 0..11 {
1869 let v = c.f32()?;
1870 if !v.is_finite() || v.abs() > 1.0e6 {
1871 return Err("implausible branch rating block value");
1872 }
1873 }
1874 if c.u8()? != 0 {
1875 return Err("branch record separator byte not zero");
1876 }
1877 match c.u8()? {
1878 0x01 => Ok(("Line", 0.0)),
1879 0x00 => {
1880 let tap = c.f32()?;
1881 if !tap.is_finite() || !(0.2..=5.0).contains(&tap) {
1882 return Err("implausible transformer tap");
1883 }
1884 Ok(("Transformer", tap))
1885 }
1886 _ => Err("branch kind marker not in the validated set"),
1887 }
1888}
1889
1890fn read_legacy_branch_tail(c: &mut Cur<'_>, tail_start: usize) -> Probe<(&'static str, f64)> {
1894 for _ in 0..4 {
1895 if c.u32()? != 0 {
1896 return Err("legacy branch tail marker is not zero filled");
1897 }
1898 }
1899 let tap = tail_start
1900 .checked_add(22)
1901 .and_then(|at| slice_at(c.b, at, 4))
1902 .and_then(|s| <[u8; 4]>::try_from(s).ok())
1903 .map_or(0.0, |raw| f64::from(f32::from_le_bytes(raw)));
1904 if tap.is_finite() && (0.2..=5.0).contains(&tap) && (tap - 1.0).abs() > 1e-6 {
1905 Ok(("Transformer", tap))
1906 } else {
1907 Ok(("Line", 0.0))
1908 }
1909}
1910
1911#[cfg(test)]
1912mod tests {
1913 use super::*;
1914
1915 fn empty_network(name: &str) -> BalancedNetwork {
1916 BalancedNetwork {
1917 name: name.to_string(),
1918 base_mva: MVA_BASE,
1919 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
1920 geo: None,
1921 buses: Vec::new(),
1922 loads: Vec::new(),
1923 shunts: Vec::new(),
1924 branches: Vec::new(),
1925 switches: Vec::new(),
1926 generators: Vec::new(),
1927 storage: Vec::new(),
1928 hvdc: Vec::new(),
1929 transformers_3w: Vec::new(),
1930 areas: Vec::new(),
1931 solver: None,
1932 source_format: SourceFormat::PowerWorldBinary,
1933 source: None,
1934 }
1935 }
1936
1937 #[test]
1938 fn best_chain_prefers_valid_chain_over_higher_scoring_error() {
1939 let mut best = None;
1940 keep_best_chain(&mut best, 100, Err(unsupported_vintage("bad candidate")));
1941 keep_best_chain(&mut best, 1, Ok(empty_network("valid")));
1942
1943 let (_, net) = best.unwrap();
1944 assert!(net.is_ok());
1945 }
1946
1947 #[test]
1948 fn alternate_load_record_reads_late_p_and_q() {
1949 let mut bytes = vec![0u8; 41];
1950 bytes[6] = 0;
1951 bytes[25..29].copy_from_slice(&1.0f32.to_le_bytes());
1952 bytes[29..33].copy_from_slice(&0.0f32.to_le_bytes());
1953 bytes[33..37].copy_from_slice(&0.5f32.to_le_bytes());
1954 bytes[37..41].copy_from_slice(&0.25f32.to_le_bytes());
1955
1956 let mut c = Cur { b: &bytes, pos: 6 };
1957 let load = read_load(&mut c, BusId(1), b"1").unwrap();
1958
1959 assert!((load.p - 50.0).abs() < 1e-9);
1960 assert!((load.q - 25.0).abs() < 1e-9);
1961 assert_eq!(c.pos, 41);
1962 }
1963}