app
Adom Fields
Public Made by Adomby adom
Adom Fields: where the current and the heat go on your board.
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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
//! 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)); }
}