Skip to main content

powerio/format/powerworld/
pwb.rs

1//! Read PowerWorld `.pwb` binary case files (read only).
2//!
3//! The format is undocumented. The decoder was verified against paired `.pwb`
4//! and `.aux` exports of the ACTIVSg synthetic grids; the evidence is recorded
5//! in `powerio/src/format/powerworld/FORMAT.md`. It reads buses, loads,
6//! generators, shunts, and branches. Substations, areas, contingencies, and
7//! options remain undecoded.
8//!
9//! Each parsed record must use known flags, reference an existing bus, and
10//! contain finite values within the validated ranges. An unsupported layout
11//! returns an error rather than guessing a record shape.
12//!
13//! Supported header constants: 338, 368, 425, 483, 508, 537, 550, 551, and 554.
14//! These constants gate only the writer era; a recognized constant still has
15//! to pass the table walk. Constants 338/368/425 use the older generator record
16//! (`bus`, ID, f32 block), 483/537/550/551 use the regulated bus record, 508
17//! has been observed with both generator families, and 554 uses the regulated
18//! record without the 2021 era presence byte. The bus, load, shunt, and branch
19//! heads are more general: their flag words are Delphi field presence bitmasks,
20//! so one decoded head model admits the observed 0x06, 0x26, and 0x66 families
21//! as long as the later table walk still validates.
22//!
23//! Known limits, documented rather than guessed:
24//!
25//! - Status bytes: the 483 era generator record is the one located,
26//!   validated status in the corpus (bit 0 of the byte one past the f32
27//!   block, proven against the 94 open machines in the Texas7k aux). Every
28//!   other device in every available case is in service, so no other out of
29//!   service encoding is validated and those devices read as in service.
30//!   The load record's post ID byte, once treated as a status, is 0x00 in
31//!   the 425 era files and 0x01 in the 2021 era ones with every load Closed
32//!   in both, so it is no status byte; the 425 era generator, the shunt,
33//!   and the branch status bytes are unlocated.
34//! - Transformer phase shift: every available case has zero phase, so the
35//!   field's offset is unknown; transformers read with `shift = 0`.
36//! - The slack designation is not stored in the bus record; buses read as
37//!   PQ/PV (from the generators) and no bus is marked `Ref`.
38//! - The system MVA base is not decoded; per unit values are converted with
39//!   the 100 MVA default.
40//! - The shunt record's nominal MW slot is unlocated: every available case
41//!   stores zero shunt MW, and the slot once assumed to hold it carries 0.99
42//!   in the 2016 export (a regulation target, not a power). Shunts read with
43//!   `g = 0` and only the nominal MVAr is decoded.
44//! - Branch ratings beyond the inline slots (two or three, by flag bit 1)
45//!   are zero in every available case; the trailing rating block is
46//!   validated as zero filled f32s and read as zero ratings.
47//! - Bus voltage limits are not decoded; buses read with the 1.1/0.9
48//!   defaults the aux reader also falls back to when the per rating set
49//!   fields are absent.
50//! - Branch angle limits have no PowerWorld field at all; branches read
51//!   with the +-360 degree placeholder every reader uses for absence.
52
53use 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
65/// The system MVA base used to convert the file's per unit f32 storage into
66/// physical units. The base itself is not decoded (see the module docs);
67/// every available sibling case uses PowerWorld's 100 MVA default.
68const MVA_BASE: f64 = 100.0;
69
70/// How far ahead a bounded scan may look for the next record or table. Large
71/// enough for every observed record tail, small enough that a derailed parse
72/// fails fast instead of wandering.
73const RESYNC_WINDOW: usize = 1024;
74
75/// Cap on table-location probes across the whole chain search. A count word is
76/// attacker-controlled, so a crafted file can pack the header window with
77/// valid-looking table heads that never complete a chain and force the nested
78/// bus × load × generator × shunt × branch search to run to exhaustion (a bit-4
79/// tail on the last bus record stretches the load scan to `BLOB_WINDOW`, so the
80/// blowup is multiplicative). A probe is one candidate `(count, glue)` attempt
81/// or one scanned record head position, charged at every scan site. The
82/// largest corpus file (Texas7k, 13.7 MB) spends 16M probes and a 64 KB
83/// truncation fixture peaks at 42M; exceeding the cap means the bytes are not
84/// a decodable layout, so the search stops and reports a read error instead
85/// of spinning.
86const SEARCH_PROBE_BUDGET: u64 = 128_000_000;
87
88/// Shared probe counter for one `parse_pwb` call. `tick` charges one probe and
89/// returns whether the budget still has room; `exhausted` reports afterward
90/// whether the search stopped because it ran out.
91struct 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
109/// The probe layer's error type. Probe rejections are pure control flow (the
110/// table search discards them wholesale and the loud user visible errors are
111/// built at the parse boundary), so they carry a static description and never
112/// allocate; the texts document why each check exists.
113type Probe<T> = std::result::Result<T, &'static str>;
114
115/// Parse `.pwb` bytes into a [`BalancedNetwork`]. `name_hint` (the file stem) names
116/// the network; the binary carries no case name in the decoded region.
117///
118/// # Errors
119/// [`Error::FormatRead`] when the header is not the known magic, a record
120/// does not match the validated layouts, or a table cannot be located.
121pub fn parse_pwb(bytes: &[u8], name_hint: Option<&str>) -> Result<BalancedNetwork> {
122    let header_constant = expect_header(bytes)?;
123    reject_unsupported_vintage(bytes)?;
124    // The header constant pins the generator record layout wherever the
125    // corpus is unambiguous: every 425 file carries the bus + ID shape and
126    // every 483/537/550/551 file the regulated bus shape, while 508 saves
127    // exist with both (Hawaii40 against the Texas7k v21 resave), so only
128    // they try the two in sequence. Beyond pricing, this keeps the layout
129    // a file cannot carry from ever outbidding the right one in the chain
130    // search; a hypothetical file mixing eras fails loudly instead.
131    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    // The narrow bus glue window prices the common files (see
161    // bus_table_candidates); the wide retry exists so a small node level
162    // resave (a bus table under 256 records with the v21 writer's 52 byte
163    // glue) is a second slower search instead of a coverage cliff. The
164    // retry only runs on files the narrow search already failed, enumerates
165    // only the glue combinations the narrow pass could not reach, and
166    // shares the bus run cache so nothing is walked twice.
167    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/// Which generator record layouts the header constant admits (see
221/// [`parse_pwb`]): the 425/508 era bus + ID shape (`plain`), the 2021 era
222/// regulated bus shape (`reg`, [`read_gen_reg_record`]), and the 554 shape
223/// whose regulated bus record omits the presence byte (`simple_reg`,
224/// [`read_gen_reg_simple_record`]).
225#[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/// One full depth first search for the table chain; `None` when no chain
261/// matches. `wide_bus_glue` lifts the bus table's count gated glue window
262/// (see [`bus_table_candidates`]) for the retry pass.
263#[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    // A count word can be forged by record interiors and the case
275    // description, so table location is a depth first search: a candidate at
276    // any stage is kept only if every later table parses behind it. The
277    // common dedicated shunt table path returns on the first checked chain;
278    // the optional bus tail shunt paths keep the largest checked electrical
279    // core. Wrong candidates die fast on their bounded windows; a file with
280    // no valid chain fails loudly. The run caches make the backtracking
281    // affordable: candidates pointing at the same first record share one walk
282    // however many count words and search retries reach it.
283    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; // duplicate ids: not a real bus table
288        };
289        let mut best = None;
290        let bus_names = bus_name_map(&buses);
291        // The device and branch runs validate bus references, so their
292        // caches are scoped to one bus table candidate.
293        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        // The load table's count word sits past the final bus record's
300        // undecoded tail, which a bit 4 list can stretch beyond one window
301        // (the 2030 build's lists run 1341 bytes); the seam scan honors it
302        // exactly as the intra table stepping does.
303        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            // The generator table reads through the record layouts the
315            // header constant admits (see parse_pwb). A file's table uses
316            // exactly one; each gets its own run cache and the structural
317            // gauntlets keep the wrong one from parsing. The 508 era is
318            // ambiguous in the corpus, so the regulated layout goes first:
319            // on Texas7k v21, the older probe can accept a false table
320            // before the true regulated table if it gets first refusal.
321            // The newer layout's table glue runs to 86 bytes in the v21
322            // resave; the older table glue reaches 104 bytes in the IEEE
323            // 24 bus save.
324            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
469/// Keep the table chain with the largest decoded electrical core.
470fn 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
489/// Score a candidate table chain by decoded element count.
490fn 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
499/// Add bus tail shunts without duplicating the dedicated shunt table rows.
500fn 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
512/// Check whether another generator record starts soon after a candidate table.
513///
514/// This rejects short prefixes when a wrong count word points into the real
515/// generator table.
516fn 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
532/// Assemble the decoded tables and run the common reference checks.
533fn 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
564// ---- Cursor -----------------------------------------------------------------
565
566/// Bounds checked cursor for little endian record probes.
567struct Cur<'a> {
568    b: &'a [u8],
569    pos: usize,
570}
571
572impl<'a> Cur<'a> {
573    /// Take `n` bytes and advance the cursor.
574    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    /// Read one byte.
582    fn u8(&mut self) -> Probe<u8> {
583        Ok(self.take(1)?[0])
584    }
585    /// Read a little endian u16.
586    fn u16(&mut self) -> Probe<u16> {
587        Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
588    }
589    /// Read a little endian u32.
590    fn u32(&mut self) -> Probe<u32> {
591        Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
592    }
593    /// Read a little endian f32 and widen to f64.
594    fn f32(&mut self) -> Probe<f64> {
595        Ok(f64::from(f32::from_le_bytes(
596            self.take(4)?.try_into().unwrap(),
597        )))
598    }
599    /// Read a little endian f64.
600    fn f64(&mut self) -> Probe<f64> {
601        Ok(f64::from_le_bytes(self.take(8)?.try_into().unwrap()))
602    }
603
604    /// A u32 length prefixed string of printable ASCII, at most `max` bytes.
605    /// Returns the raw slice; accepted records convert it once, so the
606    /// rejected probe offsets (the overwhelming majority) never allocate.
607    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    /// A Pascal ShortString (one length byte), printable, at most `max` bytes.
620    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    /// A fixed capacity Delphi `string[2]`: one length byte plus a fixed two
633    /// byte text area (a one character value leaves the second byte unused).
634    /// Branch circuit IDs and generator IDs are stored this way; the fixed
635    /// capacity was established by the v19 file's parallel circuit records.
636    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
649/// How far a bit 4 record's tail blob may push the next record: the largest
650/// observed blob is 406 KiB (an ACTIVSg500 branch record, see
651/// `powerio/src/format/powerworld/FORMAT.md`), so four MiB
652/// is an order of magnitude of headroom
653/// while bounding what a crafted file can make the scan walk per record.
654const BLOB_WINDOW: usize = 4 << 20;
655
656/// How far the scan for the next record may look past `after`: one bounded
657/// window normally, the blob window when the preceding record's flag bit 4
658/// inserted a count prefixed list (the 2019+ era branch blobs run to 406 KiB
659/// and the 2030 build's bus lists past one window; the record head gauntlets
660/// keep blob bytes from forging a record).
661fn 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
669/// True when a probed string is printable ASCII.
670fn printable(s: &[u8]) -> bool {
671    s.iter().all(|&c| (0x20..0x7f).contains(&c))
672}
673
674/// Borrow a bounded byte slice at an absolute offset.
675fn slice_at(b: &[u8], at: usize, n: usize) -> Option<&[u8]> {
676    at.checked_add(n).and_then(|end| b.get(at..end))
677}
678
679/// Add an absolute offset without wrapping.
680fn checked_offset(at: usize, add: usize) -> Probe<usize> {
681    at.checked_add(add).ok_or("truncated record")
682}
683
684/// Reject impossible count words before walking a table.
685fn 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
694/// Read a little endian u32 at an absolute offset.
695fn 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
702/// Read a little endian f32 at an absolute offset and widen to f64.
703fn 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
711/// Read a length prefixed printable ASCII string at an absolute offset.
712fn 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
724/// Validate the file head and return the writer format constant (the u64 at
725/// offset 0x08) for the layout keying in [`parse_pwb`].
726fn 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    // Every known PowerWorld binary starts with 15000. The next two words
743    // identify the writer family: the decoded constants cover older 0x06 bus
744    // records (338/368), the Simulator 19/20/current 425 family, the 2021
745    // regulated generator family (483/537/550/551), the mixed 508 saves, and
746    // the 554 regulated generator variant. Header admission is not trust:
747    // every table still has to pass the record probes below.
748    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
757/// Reject files whose leading 64 KiB carries no run of validated bus record
758/// heads before the table search reaches a generic "no chain" error. The
759/// decoded bus head families share enough structure that fewer than two
760/// validated heads in this window means an unrecognized body layout, not a
761/// sparse case.
762fn 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
782/// The single rejection path for recognized-but-undecoded writer vintages;
783/// every message names the detected evidence and points at the docs.
784fn 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
795// ---- Search machinery --------------------------------------------------------
796
797/// Bus id membership for the record probes, the hottest check in the table
798/// search (every probed byte offset starts with one or two lookups). A
799/// bitmap over the id range replaces hashing; [`read_bus_head`] caps ids at
800/// 99,999,999 and the corpus tops out around 790,000, but a forged
801/// candidate can pair a tiny count with an id near the cap, so tables whose
802/// id range dwarfs their count fall back to a sorted list instead of
803/// allocating megabytes per forged candidate.
804enum BusIdSet {
805    Bitmap(Vec<u64>),
806    Sparse(Vec<usize>),
807}
808
809impl BusIdSet {
810    /// `None` when an id repeats: a table with duplicate bus numbers is a
811    /// forged candidate, not a real bus table.
812    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    /// Check whether a decoded bus id exists.
835    #[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
846/// Build an uppercase bus name index for records that point by name.
847fn 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
858/// The record run from one first record offset: the walk from a given offset
859/// is unique, so every count word candidate pointing at the same first
860/// record shares it. A count that is a prefix of a longer run reuses the
861/// boundaries already walked; a count past the point where extension failed
862/// is rejected without rescanning.
863struct Run<T> {
864    items: Vec<T>,
865    /// End offset just past `items[i]`.
866    ends: Vec<usize>,
867    /// Extension past `items.len()` already failed; never retried.
868    dead: bool,
869}
870
871impl<T: Clone> Run<T> {
872    /// Start a record run with its first validated record.
873    fn start(item: T, end: usize) -> Self {
874        Run {
875            items: vec![item],
876            ends: vec![end],
877            dead: false,
878        }
879    }
880
881    /// Extend to `count` records if the bytes allow, finding each next
882    /// record with `next(after, prev)` (the record tails are undecoded and
883    /// vary, so each step is a bounded scan). Returns the `count` record
884    /// prefix and the offset just past it.
885    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; // the candidate scans filter zero counts out
892        }
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
907// ---- Bus table --------------------------------------------------------------
908
909struct BusHead {
910    bus: Bus,
911    shunt: Option<Shunt>,
912    /// The flags u32 between name and nominal kV: a Delphi field presence
913    /// bitmask, not a per file constant. Bit 5 set marks the Simulator 20
914    /// era record family (clear on the Simulator 19 era 0x06/0x07 family,
915    /// whose tails are shorter), bit 4 set marks a count prefixed list in
916    /// the record tail (2016/2017 era exports and the 2030 build), bit 0
917    /// clear means one extra u16 sits before the nominal kV (observed on
918    /// generator buses). The 2019+ era writers add bits 6 and 8, both per
919    /// record (the v21 resave clears bit 6 on its slack bus record; the
920    /// bit 6 tails carry a location string block), with their fields in
921    /// the undecoded tail.
922    unk: u32,
923}
924
925/// Whether a bus record flag word is one this reader decodes: base bits
926/// `0x06` plus any combination of the observed presence bits. Bit 5 changes
927/// the tail family (`0x06` vs `0x26` era), while bits 6, 8, 10, 12, and 13
928/// were admitted only after full table walks showed they leave the decoded
929/// head layout unchanged.
930fn known_bus_flags(unk: u32) -> bool {
931    unk & !0x3571 == 0x06
932}
933
934/// The record family bits of a bus flag word. One bus table cannot mix tail
935/// families, but individual records can toggle optional presence bits inside
936/// a family. Bit 5 stays in the family key because the 0x06 and 0x26 era tails
937/// differ; the other admitted bits are per record fields or skipped tails.
938fn bus_family(unk: u32) -> u32 {
939    unk & !0x3551
940}
941
942/// The bus run cache: keyed by first record offset, each entry carrying the
943/// walked `(bus, flag word)` records and the table's family bits.
944type BusRunItem = (Bus, u32, Option<Shunt>);
945type BusRuns = HashMap<usize, (Run<BusRunItem>, u32)>;
946
947/// Bus table candidates: each `(count, glue)` position after the header whose
948/// record walk succeeds, in scan order, yielding the records, the offset
949/// past the last decoded head, and the last record's flag word (the load
950/// table seam needs its bit 4). The caller validates each candidate by
951/// parsing the tables that must follow it.
952fn 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            // Table glue between the count and the first record varies by a few
964            // bytes per table and vintage; scan a small window for the record.
965            // The v21 resave's bus glue runs 52 bytes, past the 48 every other
966            // export observes; the first search pass widens the window only for
967            // large counts (every observed wide glue table is a node level
968            // resave with thousands of buses, and forged count words are
969            // overwhelmingly small values, so widening their window prices
970            // every file). The retry pass covers exactly the complement (the
971            // wide glues for small counts), so with the shared run cache the
972            // two passes together cost one full sweep.
973            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
1012/// The bus record run from `first`, extended to `count` records if the bytes
1013/// allow. The run remembers the first record's family: one file's bus table
1014/// never mixes families, so the scan for each next record skips heads of the
1015/// other family (see [`bus_family`]). The items keep their flag words: the
1016/// scan window for the next record depends on the preceding record's bit 4,
1017/// as in the branch run.
1018fn 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        // A failed head parse is not cached: the table search probes far
1029        // more offsets than it accepts, and the probe itself is cheaper
1030        // than a map entry.
1031        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        // The record tail (undecoded; longer when flag bit 4 inserts a
1040        // count prefixed list) separates this record from the next; find
1041        // the next head by bounded scan (see resync_end).
1042        (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
1053/// Parse one bus record head at `at`; everything through the voltage angle.
1054/// Header 338 and some small header 425 saves omit the balancing authority
1055/// field between zone and label. Returns the parsed bus and leaves undecoded
1056/// tail bytes, including the bit 4 list, to the resync.
1057fn 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    // The flag mask (a handful of admitted words out of 2^32) is far more
1072    // selective than the name text scan, so it gates first; the accept set
1073    // is unchanged, only the rejection order.
1074    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
1136/// Decode the optional fixed shunt stored in some bus record tails.
1137fn 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
1169/// Read the bus label plus solved voltage magnitude and angle.
1170fn 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
1177// ---- Device tables (loads, generators, shunts) -------------------------------
1178
1179/// One whole device record: parse at `at`, return the element and the offset
1180/// just past the decoded head (undecoded tail bytes are the resync scan's to
1181/// skip). One function per validated record layout. The bound is generic
1182/// rather than a `fn` pointer so each table's probe monomorphizes and the
1183/// early rejection checks inline into the resync scans, the hottest loops
1184/// in the search.
1185trait 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
1188/// The bus + ShortString ID prefix the 425/508 era device records share.
1189/// `read` parses the rest of the record head at the cursor and returns the
1190/// element.
1191fn 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
1210/// Probe one load record using the shared device head.
1211fn 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
1215/// Probe one plain generator record using the shared device head.
1216fn 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
1220/// Probe one switched shunt record using the shared device head.
1221fn 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/// Candidates for a count prefixed device table after `from`: every
1226/// `(count, glue)` whose full record walk succeeds, in scan order. The caller
1227/// keeps a candidate only if the tables that must follow it parse too.
1228#[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
1257/// The device record run from `first`, extended to `count` records if the
1258/// bytes allow (see [`Run`]).
1259fn 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        // A failed head parse is not cached, as in the sibling run lookups.
1272        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        // The undecoded record tail separates this record from the next.
1279        (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
1285/// Load record: one undecoded byte, then constant power P and Q in per unit
1286/// (f32). The byte is 0x00 in every 425 era record and 0x01 in every 483
1287/// era one while both auxes say every load is Closed, so it is not a status
1288/// byte; loads read as in service (see the module docs).
1289fn read_load(c: &mut Cur, bus: BusId, id: &[u8]) -> Probe<Load> {
1290    let record_start = c.pos - (4 + 1) - id.len(); // u32 bus + the ID length byte
1291    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
1335/// Generator record: the ID is a fixed capacity ShortString[2] (so the
1336/// payload sits at constant offsets from the record start), undecoded flag
1337/// bytes, then eight consecutive f32s: MW setpoint, MVAr setpoint, MVAr
1338/// max, MVAr min (per unit), voltage setpoint (p.u.), MVA base, MW max, MW
1339/// min (per unit). The f32 block starts at +9 or +10 in the 2016/2017
1340/// exports (the flag bytes before it vary per record) and +11 in the 2018
1341/// one; the voltage setpoint and MVA base ranges anchor the choice, and a
1342/// record that puts implausible values at every offset is a loud error, not
1343/// a generator.
1344fn read_gen(c: &mut Cur, bus: BusId, id: &[u8]) -> Probe<Generator> {
1345    let record_start = c.pos - (4 + 1) - id.len(); // u32 bus + the ID length byte
1346    let mut chosen = None;
1347    // +12 extends the observed set to two character IDs in a 2018 era
1348    // export (unobserved, but the pre-rework probe covered it).
1349    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    // The status byte is unlocated within the flag bytes of this era's
1364    // record; every available machine is Closed (see the module docs).
1365    Ok(gen_from_block(bus, &v, true))
1366}
1367
1368/// The eight consecutive f32 per unit values both generator record eras
1369/// share: MW setpoint, MVAr setpoint, MVRMax, MVRMin, GenVoltSet, GenMVABase,
1370/// MWMax, MWMin. The voltage setpoint and MVA base ranges anchor the layout;
1371/// a block that fails them is not a generator record.
1372fn read_gen_f32_block(c: &mut Cur) -> Probe<[f64; 8]> {
1373    let mut v = [0.0f64; 8];
1374    // Each slot checks as it reads, and the two anchor ranges right after
1375    // their slots, so a forged offset stops within a few reads instead of
1376    // always paying all eight; same predicates, same accept set.
1377    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
1390/// A [`Generator`] from the shared f32 block (see [`read_gen_f32_block`]).
1391fn 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
1410/// 2021 era generator record (header constant 483, the Texas7k export),
1411/// validated against all 731 machines of the same day aux: u32 terminal
1412/// bus, u32 regulated bus (the inserted field that distinguishes this
1413/// layout; on plants regulating a remote bus the two differ, which is what
1414/// made the older record model misread until the boundary was re-fit), a
1415/// fixed capacity ShortString[2] ID, a constant 0x01 byte, one undecoded
1416/// byte, then a presence byte whose bit 0 inserts an f32 and bit 1 one
1417/// byte, then the same eight f32 block as the older eras. One past the
1418/// block sit a zero byte and the status byte: bit 0 is the in service bit,
1419/// validated against the aux's 637 Closed and 94 Open machines (the
1420/// corpus's first out of service devices). The f32 after it reads as
1421/// GenRMPCT in the aux (100.0 on every record) and anchors the layout.
1422fn 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()?; // varies per record (7 through 37 observed); undecoded
1437    // Presence byte: bit 0 inserts an f32, bit 1 one byte (both in the
1438    // 2021 export), bit 5 another f32 (the 2030 build); the eight f32
1439    // block follows whatever the bits insert.
1440    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        // Bits 1 and 5 never co-occur in the corpus, so the order of their
1446        // inserted fields is unestablished; guessing it risks reading a
1447        // misaligned f32, so the combination rejects until a file shows it.
1448        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
1465/// Header 554 regulated generator record: terminal bus, regulated bus,
1466/// fixed capacity ID, two zero bytes, then the shared f32 block and the
1467/// same status/RMPCT tail as [`read_gen_reg_record`].
1468fn 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
1490/// Read the status and RMPCT tail shared by regulated generator records.
1491fn 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
1506/// Shunt record: nominal MVAr as f32 at +24 from the record start, validated
1507/// on all 199 shunts across the three sibling cases. The slot at +20 is 0.0
1508/// in the Simulator 20 era files but 0.99 in the 2016 export, so it is not
1509/// the nominal MW (see the module docs); shunts read with `g = 0`.
1510fn read_shunt(c: &mut Cur, bus: BusId, id: &[u8]) -> Probe<Shunt> {
1511    let record_start = c.pos - (4 + 1) - id.len(); // u32 bus + the ID length byte
1512    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        // The nominal MW slot is unlocated (every available case stores
1529        // zero); see the module docs.
1530        g: 0.0,
1531        b: b_mvar,
1532        in_service: true,
1533        control: None,
1534        uid: None,
1535        extras,
1536    })
1537}
1538
1539// ---- Branch table ------------------------------------------------------------
1540
1541/// Whether a branch record flag word is one this reader decodes: base bits
1542/// `0x4C` plus any combination of bits 0, 1, 4, 5, and 7, a Delphi field
1543/// presence bitmask like the bus record's. Bit 0 set omits
1544/// the circuit ID string and its status byte (the PowerWorld default " 1"
1545/// applies), bit 1 set means two inline rating slots instead of three
1546/// (the Simulator 19 era writer inlines three), bit 4 marks a count
1547/// prefixed list in the record tail. Bit 7 is set on every 425/508 era
1548/// record; the 2021 era Texas7k exports clear it on most lines while
1549/// setting it on every transformer and a few dozen lines, with the head
1550/// layout through the kind byte identical either way (its field lives in
1551/// the undecoded tail). Admitting the bit 7 clear words doubles the flag
1552/// vocabulary; the measured cost on the 425 era corpus is a few
1553/// microseconds (benchmarks/RESULTS.md), and a mask keyed to the generator
1554/// layout was tried and rejected anyway, since the strict mask turns real
1555/// bit 7 clear records invisible to the table end check and a forged
1556/// short table can win.
1557/// Observed words: 0xEC/0xFC (2016), 0xEE/0xEF (2018 and v19), 0xFE/0xFF
1558/// (v19), 0x6C and 0xEC/0xED (Texas7k), 0xCE on the Australian series
1559/// capacitor records, and the same families with bits 10 or 14 set in the
1560/// Kundur save. Other combinations of the same bits are admitted by the bit
1561/// logic and guarded by the structural anchors in [`read_branch_head`].
1562fn known_branch_flags(flags: u16) -> bool {
1563    flags & !0x44B3 == 0x004C
1564}
1565
1566/// Locate and walk the branch table after `from`: the first `(count, glue)`
1567/// candidate whose walk succeeds and after which no further branch record
1568/// follows (a forged count word inside the glue can parse a prefix of the
1569/// real table; the true count lands where no further record follows).
1570fn 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    // The gap between the shunt table end and the branch count word can
1580    // exceed one resync window; two cover every observed file.
1581    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        // The branch table glue is longer than the device tables'; scan a
1590        // window after the count for the first record.
1591        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                // The end check must step exactly like the run: a bit 4 tail on
1609                // the last record can hold more than one window of blob, and a
1610                // forged short count ending on such a record would otherwise
1611                // read as "no further record" and win.
1612                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                // A scan cut short by the budget cannot vouch that the table
1617                // ends here; reject rather than accept a possibly forged count.
1618                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
1630/// The branch record run from `first`, extended to `count` records if the
1631/// bytes allow (see [`Run`]). The items keep their flag words: the scan
1632/// window for the next record depends on the preceding record's bit 4.
1633fn 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        // A failed head parse is not cached, as in the sibling run lookups.
1646        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        // The undecoded record tail separates this record from the next;
1653        // find the next head by bounded scan (see resync_end).
1654        (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/// Branch record, validated field by field against the aux siblings of all
1665/// three cases (6,491 records). After the impedances: two or three inline
1666/// per unit rating slots (by flag bit 1), a constant u32 tag, eleven f32
1667/// slots (zero in every available case), one zero byte, then the kind byte
1668/// that separates lines from transformers (which carry their tap next). The
1669/// tag and the zero byte are structural anchors: an unobserved variant
1670/// shifts them and dies loudly instead of misreading.
1671#[allow(clippy::many_single_char_names)] // r, x, b are the domain names
1672fn 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)] // r, x, b are the domain names
1683fn 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        // Omitted circuit: PowerWorld's default, observed as " 1" in the
1702        // sibling aux.
1703        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        // Phase shift is undecoded: every available case has zero phase, so
1756        // the field's location is unknown (see the module docs).
1757        shift: 0.0,
1758        // The branch status byte is unlocated (the byte once assumed to be
1759        // it was the circuit ID's unused capacity byte); every available
1760        // record is Closed. See the module docs.
1761        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
1773/// Read a signed branch endpoint. Some saves store a negative endpoint for
1774/// orientation metadata; the network bus id is the positive magnitude.
1775fn 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
1782/// Probe the fixed-layout generator step-up transformer records found in the
1783/// Australian cases. They do not use the normal branch head, so this reader
1784/// keeps them behind a separate probe with several anchors: known high side
1785/// bus, 100 MVA nominal marker, plausible device X/MBASE fields, "STEP UP" in
1786/// the name, and a low side bus named `GEN <unit>`.
1787fn 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
1864/// Read the modern branch tail after the common electrical head. The tail is
1865/// a rating block, separator byte, and kind marker; kind 1 is a line, kind 0
1866/// is a transformer followed by the tap.
1867fn 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
1890/// Read the older short branch tail. These saves do not carry the modern kind
1891/// marker, so transformer detection uses the validated zero marker block plus
1892/// a plausible non-unit tap at the observed tail offset.
1893fn 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}