|
| 1 | +//! D1 — the first real runtime edge: perturbation-sim `Grid` → canonical SoA `NodeRow`. |
| 2 | +//! |
| 3 | +//! Runs the DC-power-flow cascade over a small in-tree lattice and encodes each |
| 4 | +//! bus's final perturbation magnitude (`|θ_final − θ_base|`) into one canonical |
| 5 | +//! `NodeRow` (key = `NodeGuid::local(bus)`, `value[0..8]` = f64 little-endian). |
| 6 | +//! |
| 7 | +//! This is the degenerate first case of the Spain-grid acceptance gate: real |
| 8 | +//! nodes, on the actual SoA substrate, NaN-free. It turns the golden image from |
| 9 | +//! a link-only probe into a harness that exercises one genuine runtime edge |
| 10 | +//! between two of the five crates (perturbation-sim ↔ lance-graph-contract). |
| 11 | +//! |
| 12 | +//! `NodeRow` carries no live simulation state — the cascade owns its `Vec<f64>` |
| 13 | +//! working buffers; we encode only the *result* into the SoA. One-directional. |
| 14 | +
|
| 15 | +use lance_graph_contract::canonical_node::NodeRowPacket; |
| 16 | +use lance_graph_contract::soa_envelope::SoaEnvelope; // brings `as_le_bytes` into scope |
| 17 | +use lance_graph_contract::{EdgeBlock, NodeGuid, NodeRow}; |
| 18 | +use perturbation_sim::{simulate_outage, CascadeConfig, Edge, Grid}; |
| 19 | + |
| 20 | +/// Stride of one canonical node on the wire / in the SoA backing store. |
| 21 | +const NODE_ROW_STRIDE: usize = 512; |
| 22 | +/// Slab offset for the encoded perturbation magnitude (raw f64, not the named |
| 23 | +/// `ValueTenant::Energy` carve — honest and decode-trivial). |
| 24 | +const NODE_FIELD_OFFSET: usize = 0; |
| 25 | + |
| 26 | +/// Build a `rows × cols` lattice with a corner-to-corner dipole injection. |
| 27 | +/// Returns the grid plus a balanced injection vector `p` (Σ = 0, one bus per |
| 28 | +/// node). Pure in-tree — no network, no files. |
| 29 | +pub fn build_demo_grid(rows: usize, cols: usize) -> (Grid, Vec<f64>) { |
| 30 | + let n = rows * cols; |
| 31 | + let idx = |r: usize, c: usize| r * cols + c; |
| 32 | + let mut edges = Vec::new(); |
| 33 | + for r in 0..rows { |
| 34 | + for c in 0..cols { |
| 35 | + if c + 1 < cols { |
| 36 | + edges.push(Edge::new(idx(r, c), idx(r, c + 1), 1.0, 5.0)); |
| 37 | + } |
| 38 | + if r + 1 < rows { |
| 39 | + edges.push(Edge::new(idx(r, c), idx(r + 1, c), 1.0, 5.0)); |
| 40 | + } |
| 41 | + } |
| 42 | + } |
| 43 | + let grid = Grid::new(n, edges); |
| 44 | + let mut p = vec![0.0_f64; n]; |
| 45 | + p[0] = 1.0; // source at one corner |
| 46 | + p[n - 1] = -1.0; // sink at the opposite corner |
| 47 | + (grid, p) |
| 48 | +} |
| 49 | + |
| 50 | +/// Run the cascade over `grid` and encode the result into one canonical |
| 51 | +/// `NodeRow` per bus: identity = bus index, `value[0..8]` = `node_field[i]` f64 LE. |
| 52 | +pub fn grid_to_noderows(grid: &Grid, p: &[f64], seed_line: usize) -> Vec<NodeRow> { |
| 53 | + let result = simulate_outage(grid, p, seed_line, CascadeConfig::default()); |
| 54 | + (0..grid.n) |
| 55 | + .map(|i| { |
| 56 | + let mut value = [0u8; 480]; |
| 57 | + value[NODE_FIELD_OFFSET..NODE_FIELD_OFFSET + 8] |
| 58 | + .copy_from_slice(&result.shape.node_field[i].to_le_bytes()); |
| 59 | + NodeRow { |
| 60 | + key: NodeGuid::local(i as u32), |
| 61 | + edges: EdgeBlock::default(), |
| 62 | + value, |
| 63 | + } |
| 64 | + }) |
| 65 | + .collect() |
| 66 | +} |
| 67 | + |
| 68 | +/// Decode a bus's perturbation magnitude back out of its `NodeRow` value slab. |
| 69 | +#[inline] |
| 70 | +pub fn decode_node_field(row: &NodeRow) -> f64 { |
| 71 | + let bytes: [u8; 8] = row.value[NODE_FIELD_OFFSET..NODE_FIELD_OFFSET + 8] |
| 72 | + .try_into() |
| 73 | + .expect("8 bytes"); |
| 74 | + f64::from_le_bytes(bytes) |
| 75 | +} |
| 76 | + |
| 77 | +/// The acceptance-gate demo (first real instance): build → cascade → encode → |
| 78 | +/// assert finite → report. Returns the encoded rows so callers can sweep them. |
| 79 | +pub fn run_demo() -> Vec<NodeRow> { |
| 80 | + let (grid, p) = build_demo_grid(8, 8); // 64 buses |
| 81 | + let rows = grid_to_noderows(&grid, &p, 0); |
| 82 | + |
| 83 | + // The acceptance-gate invariant, at SoA scale: no NaN reaches a node. |
| 84 | + assert!( |
| 85 | + rows.iter().all(|r| decode_node_field(r).is_finite()), |
| 86 | + "NaN escaped into a NodeRow value slab" |
| 87 | + ); |
| 88 | + |
| 89 | + // Prove the zero-copy SoA stride (512 B/row) without re-serializing. |
| 90 | + let packet = NodeRowPacket::new(&rows, 0); |
| 91 | + let bytes = packet.as_le_bytes().len(); |
| 92 | + |
| 93 | + let max = rows |
| 94 | + .iter() |
| 95 | + .map(decode_node_field) |
| 96 | + .fold(0.0_f64, f64::max); |
| 97 | + println!( |
| 98 | + "D1 bridge: {} buses → {} NodeRows (key=NodeGuid::local), all node_field finite; \ |
| 99 | + SoA packet {bytes} bytes ({} B/row); max |perturbation| = {max:.6}", |
| 100 | + grid.n, |
| 101 | + rows.len(), |
| 102 | + bytes / rows.len().max(1), |
| 103 | + ); |
| 104 | + rows |
| 105 | +} |
| 106 | + |
| 107 | +#[cfg(test)] |
| 108 | +mod tests { |
| 109 | + use super::*; |
| 110 | + |
| 111 | + /// The probe: every bus encodes a finite f64 that round-trips bit-exactly |
| 112 | + /// through the canonical SoA value slab. (Phase-A finiteness + B-series |
| 113 | + /// over-the-SoA, from the battle-test plan.) |
| 114 | + #[test] |
| 115 | + fn grid_to_noderows_is_always_finite_and_roundtrips() { |
| 116 | + let (grid, p) = build_demo_grid(6, 6); |
| 117 | + let result = simulate_outage(&grid, &p, 0, CascadeConfig::default()); |
| 118 | + let rows = grid_to_noderows(&grid, &p, 0); |
| 119 | + assert_eq!(rows.len(), grid.n); |
| 120 | + for (i, row) in rows.iter().enumerate() { |
| 121 | + let decoded = decode_node_field(row); |
| 122 | + // bit-exact round-trip through the SoA value slab |
| 123 | + assert_eq!(decoded.to_bits(), result.shape.node_field[i].to_bits()); |
| 124 | + assert!(decoded.is_finite()); |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + /// The SoA backing store is a flat 512-B stride — zero-copy to Lance. |
| 129 | + #[test] |
| 130 | + fn soa_packet_stride_is_512() { |
| 131 | + let (grid, p) = build_demo_grid(5, 5); |
| 132 | + let rows = grid_to_noderows(&grid, &p, 0); |
| 133 | + let packet = NodeRowPacket::new(&rows, 0); |
| 134 | + assert_eq!(packet.as_le_bytes().len(), rows.len() * NODE_ROW_STRIDE); |
| 135 | + } |
| 136 | +} |
0 commit comments