main
John Lauer adom-fields 0.7.16: the community's issues. #10 plated pads only on real copper, stackup from the board; #11 six-layer… ee2efa2
//! The flow and river data: the current as a signed vector on every cell, in the physical sense (from
//! where it enters the net to where it leaves), the voltage on every cell of copper (the net's rail plus
//! the IR drop the solve found), the current down each via barrel, and each net's terminals. One binary
//! file the app streams straight into its particle and terrain buffers, and the metadata that reads it.
use crate::board::{Board, LAYERS};
use crate::fields::{Fields, Spec};
use crate::raster::Grid;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::io::Write;
use std::path::Path;

/// A switching node: copper that swings between two rails (a buck's SW). The river shows it as a tide.
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Switching { pub highV: f64, #[serde(default)] pub lowV: f64, #[serde(default)] pub freqHz: Option<f64>, /** the node's DC average (a buck's SW averages its output voltage); the energy view uses it, the midpoint otherwise */ #[serde(default)] pub meanV: Option<f64>, /** the switch's edge time, ns (the EMC estimate's harmonics roll off above 1/(pi tr)); 8 ns when not given */ #[serde(default)] pub riseNs: Option<f64> }

#[derive(Serialize, Clone, Debug)]
pub struct Terminal { pub reference: String, pub pad: String, pub x: f64, pub y: f64, pub amps: f64, pub side: String }

/// One loaded net as solved: current density (A/mm², the solve's sense), potential (V, arbitrary zero),
/// the cells it covers, the via currents (from layer, to layer, cell, amps), the mean potential at each
/// terminal side and the terminals.
pub struct NetFlow { pub jx: Vec<Grid>, pub jy: Vec<Grid>, pub v: Vec<Grid>, pub on: Vec<Grid>, pub vias: Vec<(usize, usize, usize, f64)>, pub v_ext: f64, pub v_chip: f64, pub terminals: Vec<Terminal>, pub amps: f64 }

impl NetFlow {
    pub fn new(w: usize, h: usize, nl: usize) -> NetFlow {
        let g = || (0..nl).map(|_| Grid::new(w, h)).collect::<Vec<_>>();
        NetFlow { jx: g(), jy: g(), v: g(), on: g(), vias: Vec::new(), v_ext: 0.0, v_chip: 0.0, terminals: Vec::new(), amps: 0.0 }
    }
}

pub fn is_ground(name: &str) -> bool {
    let u = name.trim_start_matches(['+', '-', '/']).to_uppercase();
    ["GND", "PGND", "AGND", "DGND", "SGND", "GNDA", "GNDD", "VSS", "0V", "EARTH", "CHASSIS"].contains(&u.as_str()) || u.starts_with("GND_") || u.ends_with("_GND") || u.ends_with("GND")
}

pub fn is_switch_name(name: &str) -> bool {
    let u = name.trim_start_matches(['+', '-', '/']).to_uppercase();
    ["SW", "LX", "PH", "SW_NODE", "SWNODE", "SW1", "SW2", "LX1", "LX2"].contains(&u.as_str())
}

/// The rail a net's name says: GND 0, 3V3 3.3, +12V 12, 5V_AUX 5, VCC_1V8 1.8. None when the name does not say.
pub fn rail_from_name(name: &str) -> Option<f64> {
    if is_ground(name) { return Some(0.0); }
    let u: Vec<char> = name.to_uppercase().chars().collect();
    for i in 0..u.len() {
        if u[i] != 'V' || i == 0 || !u[i - 1].is_ascii_digit() { continue; }
        let mut s = i; while s > 0 && u[s - 1].is_ascii_digit() { s -= 1; }
        // a digit run glued to letters on its left (VCC2V5 is fine, R12V is not a rail)
        if s > 0 && u[s - 1].is_ascii_alphabetic() && u[s - 1] != 'C' && u[s - 1] != 'D' && u[s - 1] != 'A' { continue; }
        // a decimal point before the V: +3.3VA is 3.3 V, not 3 (issue 9)
        if s >= 2 && u[s - 1] == '.' && u[s - 2].is_ascii_digit() { let mut s2 = s - 1; while s2 > 0 && u[s2 - 1].is_ascii_digit() { s2 -= 1; } let txt: String = u[s2..i].iter().collect(); if let Ok(v) = txt.parse::<f64>() { if v > 0.0 && v <= 1000.0 { return Some(v); } } }
        let whole: String = u[s..i].iter().collect();
        let mut e = i + 1; while e < u.len() && u[e].is_ascii_digit() { e += 1; }
        let frac: String = u[i + 1..e].iter().collect();
        let v: f64 = if frac.is_empty() { whole.parse().ok()? } else { format!("{whole}.{frac}").parse().ok()? };
        if v > 0.0 && v <= 1000.0 { return Some(v); }
    }
    None
}

/// Write `<out>/flow.bin` and return the metadata that reads it (also stored as fields.json `flow`).
pub fn write(out: &Path, f: &Fields, b: &Board, spec: &Spec) -> Result<Value, String> {
    let (w, h, n) = (f.w, f.h, f.w * f.h);
    let nl = LAYERS.len();
    let known_rails: Vec<f64> = f.net_names.iter().filter_map(|nm| spec.voltages.get(nm).copied().or_else(|| rail_from_name(nm))).collect();
    let top_rail = known_rails.iter().cloned().fold(0.0, f64::max);
    let mut jx: Vec<Vec<f32>> = (0..nl).map(|_| vec![0.0; n]).collect();
    let mut jy = jx.clone();
    let mut volt: Vec<Vec<f32>> = (0..nl).map(|_| vec![f32::NAN; n]).collect();
    let mut netf: Vec<Vec<f32>> = (0..nl).map(|_| vec![-1.0; n]).collect();
    let mut metas: Vec<Value> = Vec::new();
    let (mut jmax, mut vmin, mut vmax) = (0.0f64, f64::MAX, f64::MIN);
    for (id, name) in f.net_names.iter().enumerate() {
        let switching = spec.switching.get(name).cloned().or_else(|| if is_switch_name(name) && !spec.voltages.contains_key(name) && top_rail > 0.0 { Some(Switching { highV: top_rail, lowV: 0.0, freqHz: None, meanV: None, riseNs: None }) } else { None });
        let rail = spec.voltages.get(name).copied().or_else(|| rail_from_name(name));
        let ground = rail == Some(0.0);
        for k in 0..nl { for i in 0..n { if f.net_ids[k][i] as usize == id { netf[k][i] = id as f32; } } }
        let Some(nf) = f.flow.get(name) else {
            // copper that carries no solved current: a still pond at its rail
            if let (Some(r), None) = (rail, &switching) { for k in 0..nl { for i in 0..n { if f.net_ids[k][i] as usize == id { volt[k][i] = r as f32; } } } vmin = vmin.min(r); vmax = vmax.max(r); }
            metas.push(json!({"name": name, "railV": rail, "loaded": false, "switching": switching}));
            continue;
        };
        // the physical sense: the solve drives current from the connector side (ext) to the parts (chip);
        // a ground returns the other way, and the spec's `sources` names a part side that feeds the net
        let refs_on = |side: &str, list: &[String]| nf.terminals.iter().any(|t| t.side == side && list.contains(&t.reference));
        let mut note: Option<String> = None;
        let sign: f64 = match spec.sources.get(name) {
            Some(list) if refs_on("chip", list) => -1.0,
            Some(list) if refs_on("ext", list) => 1.0,
            Some(list) => { note = Some(format!("sources {:?} are not terminals of {name}; kept the default sense", list)); if ground { -1.0 } else { 1.0 } }
            None => if ground { -1.0 } else { 1.0 },
        };
        // the voltage is the rail where the net is anchored: at its source for a supply, at the connector for a ground
        let anchor_ext = if ground { true } else { sign > 0.0 };
        let v_anchor = if anchor_ext { nf.v_ext } else { nf.v_chip };
        let base = if switching.is_some() { 0.0 } else { rail.unwrap_or(0.0) };
        let (mut lo, mut hi) = (f64::MAX, f64::MIN);
        // the net's copper the terminals do not reach carries nothing: it sits still at the anchor's level
        for k in 0..nl { for i in 0..n { if f.net_ids[k][i] as usize == id { volt[k][i] = base as f32; } } }
        for k in 0..nl { for i in 0..n {
            if nf.on[k].v[i] == 0.0 { continue; }
            let vx = base + sign * (nf.v[k].v[i] as f64 - v_anchor);
            if f.net_ids[k][i] as usize == id { volt[k][i] = vx as f32; }
            jx[k][i] += (sign * nf.jx[k].v[i] as f64) as f32; jy[k][i] += (sign * nf.jy[k].v[i] as f64) as f32;
            jmax = jmax.max((nf.jx[k].v[i] as f64).hypot(nf.jy[k].v[i] as f64));
            lo = lo.min(vx); hi = hi.max(vx);
        } }
        if switching.is_none() && rail.is_some() { vmin = vmin.min(lo); vmax = vmax.max(hi); }
        let source_side = if sign > 0.0 { "ext" } else { "chip" };
        let terminals: Vec<Value> = nf.terminals.iter().map(|t| json!({"ref": t.reference, "pad": t.pad, "x": t.x, "y": t.y, "amps": t.amps, "role": if t.side == source_side { "source" } else { "sink" }})).collect();
        let refs = |src: bool| { let mut v: Vec<String> = nf.terminals.iter().filter(|t| (t.side == source_side) == src).map(|t| t.reference.clone()).collect(); v.sort(); v.dedup(); v.join(", ") };
        let sense = format!("from {} to {}", refs(true), refs(false));
        let vias: Vec<Value> = nf.vias.iter().map(|(a, bb, i, amps)| json!({"x": f.x0 + ((i % w) as f64 + 0.5) * f.d, "y": f.y0 + ((i / w) as f64 + 0.5) * f.d, "from": LAYERS[*a], "to": LAYERS[*bb], "amps": sign * amps})).collect();
        metas.push(json!({"name": name, "railV": rail, "loaded": true, "amps": nf.amps, "sense": sense, "ground": ground, "switching": switching,
            "dropV": if lo <= hi { hi - lo } else { 0.0 }, "voltRange": [lo, hi], "voltKnown": rail.is_some() || switching.is_some(), "terminals": terminals, "vias": vias, "note": note}));
    }
    if vmin > vmax { vmin = 0.0; vmax = 1.0; }
    let path = out.join("flow.bin");
    let file = std::fs::File::create(&path).map_err(|e| format!("{}: {e}", path.display()))?;
    let mut wr = std::io::BufWriter::new(file);
    for k in 0..nl { for arr in [&jx[k], &jy[k], &volt[k], &netf[k]] { for x in arr.iter() { wr.write_all(&x.to_le_bytes()).map_err(|e| e.to_string())?; } } }
    wr.flush().map_err(|e| e.to_string())?;
    Ok(json!({
        "file": "flow.bin", "cols": w, "rows": h, "cellMm": f.d, "x0": b.x0, "y0": b.y0, "layers": LAYERS,
        "layout": "for each layer in order: jx, jy (A/mm², physical sense, +x right, +y down the board as KiCad), volt (V; NaN off copper or where the rail is unknown), net (index into nets, -1 off copper); each float32 little-endian, rows*cols, row-major from (x0, y0)",
        "nets": metas, "jMax": jmax, "voltMin": vmin, "voltMax": vmax, "topRailV": top_rail,
        "method": "The DC solve per loaded net, signed: current enters at the source terminals and leaves at the sinks (a ground returns to its connector; the spec's `sources` names the part a net is fed from). Voltage = the net's rail at its anchor plus the solved IR drop; the rail comes from the spec's `voltages` or the net's name. A switching node is drawn as a tide between its rails at a slowed, stated rate."
    }))
}

#[cfg(test)] mod tests {
    use super::*;
    #[test] fn rails_from_names() {
        assert_eq!(rail_from_name("GND"), Some(0.0)); assert_eq!(rail_from_name("PGND"), Some(0.0));
        assert_eq!(rail_from_name("+12V"), Some(12.0)); assert_eq!(rail_from_name("3V3"), Some(3.3));
        assert_eq!(rail_from_name("5V_AUX"), Some(5.0)); assert_eq!(rail_from_name("VCC_1V8"), Some(1.8));
        assert_eq!(rail_from_name("VIN"), None); assert_eq!(rail_from_name("V_GATE"), None); assert_eq!(rail_from_name("SW"), None);
        assert!(is_switch_name("SW")); assert!(!is_switch_name("SWDIO"));
    }

    /// Two strips from a connector J1 (left) to a chip U1 (right): +5V is fed from J1, GND returns to J1.
    #[test] fn supply_flows_from_its_source_and_ground_returns_to_its_connector() {
        use crate::board::{Board, Pad, Poly};
        let mut b = Board { x0: 0., y0: 0., x1: 20., y1: 6., ..Board::default() };
        let strip = |y0: f64, y1: f64| vec![(1., y0), (19., y0), (19., y1), (1., y1)];
        b.polys.push(Poly { net: "+5V".into(), layer: "F.Cu".into(), pts: strip(0.5, 2.5) });
        b.polys.push(Poly { net: "GND".into(), layer: "F.Cu".into(), pts: strip(3.5, 5.5) });
        for (r, x) in [("J1", 1.5), ("U1", 18.5)] { for (net, y) in [("+5V", 1.5), ("GND", 4.5)] {
            b.pads.push(Pad { reference: r.into(), pad: "1".into(), net: net.into(), x, y, layers: vec!["F.Cu".into()], area: 1., func: String::new() });
        } }
        let spec: Spec = serde_json::from_value(json!({"copperUm": 35, "loads": {"+5V": {"amps": 2}, "GND": {"amps": 2}}})).unwrap();
        let f = crate::fields::analyze(&b, &spec, 0.25, 60., &mut |_| {});
        let dir = std::env::temp_dir().join(format!("adom-fields-flow-test-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let meta = write(&dir, &f, &b, &spec).unwrap();
        let bytes = std::fs::read(dir.join("flow.bin")).unwrap();
        let vals: Vec<f32> = bytes.chunks(4).map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect();
        let (w, n) = (f.w, f.w * f.h);
        let at = |arr: usize, x: f64, y: f64| vals[arr * n + ((y / f.d) as usize) * w + (x / f.d) as usize];
        let (jx, volt) = (0, 2);   // layer F.Cu: jx, jy, volt, net
        // +5V: current runs left to right, the voltage is 5 V at J1 and falls toward U1
        assert!(at(jx, 10., 1.5) > 0.0);
        let (v_src, v_mid, v_load) = (at(volt, 1.6, 1.5) as f64, at(volt, 10., 1.5) as f64, at(volt, 18.4, 1.5) as f64);
        assert!((v_src - 5.0).abs() < 1e-3 && v_src > v_mid && v_mid > v_load && v_load > 4.9, "{v_src} {v_mid} {v_load}");
        // GND: the same current comes back right to left, 0 V at the connector and above it under the chip
        assert!(at(jx, 10., 4.5) < 0.0);
        let (g_conn, g_chip) = (at(volt, 1.6, 4.5) as f64, at(volt, 18.4, 4.5) as f64);
        assert!(g_conn.abs() < 1e-3 && g_chip > g_conn, "{g_conn} {g_chip}");
        let net = |name: &str| meta["nets"].as_array().unwrap().iter().find(|x| x["name"] == name).cloned().unwrap();
        assert_eq!(net("+5V")["sense"], "from J1 to U1"); assert_eq!(net("GND")["sense"], "from U1 to J1");
        assert!((net("+5V")["dropV"].as_f64().unwrap() - (v_src - v_load)).abs() < 2e-3);
        std::fs::remove_dir_all(&dir).ok();
    }
}

#[cfg(test)] mod rail_tests {
    use super::*;
    #[test] fn rail_names() { assert_eq!(rail_from_name("+3.3VA"), Some(3.3)); assert_eq!(rail_from_name("+3V3"), Some(3.3)); assert_eq!(rail_from_name("+5V_FM"), Some(5.0)); assert_eq!(rail_from_name("GND"), Some(0.0)); assert_eq!(rail_from_name("VCC2V5"), Some(2.5)); }
}