app
Adom Fields
Public Made by Adomby adom
Adom Fields: where the current and the heat go on your board.
← Commit history
adom-fields 0.7.15: oscillators count five harmonics and their own circuit is not a crosstalk victim, ESD arrays by part number, filtered rails followed to their source in the PDN check
12 files changed
+84−26
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-fields"-version = "0.7.14"+version = "0.7.15" dependencies = [ "clap", "png",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-fields"-version = "0.7.14"+version = "0.7.15" edition = "2021" license = "MIT" description = "Adom Fields: where the current and the heat go on your board. Current density, copper heat flow and board temperature from the board file itself, in 3D, for the AI to drive and the human to explore."
SKILL.md+2−1@@ -142,7 +142,7 @@ After applying a step, run `analyze` again: the issue goes away or says how far ## Noise: plane resonance and crosstalk (0.7.8) `analyze` also screens noise couplings, each finding with an action:-- **Cavities**: every power or switching pour facing the ground plane is a parallel-plate cavity; its lowest modes are solved on the real overlap (the Neumann Laplacian's eigenpairs by Lanczos with full reorthogonalisation, then Jacobi; f = c·k/(2π√εr); checked against the rectangle's c/(2a√εr) in the tests). A mode within 5 % of a strong tone (the switching harmonics up to ten times the edge's knee, `clocks` from the spec) is `cavity-resonance`, with an `add-part` capacitor at the mode's loudest point.+- **Cavities**: every power or switching pour facing the ground plane is a parallel-plate cavity; its lowest modes are solved on the real overlap (the Neumann Laplacian's eigenpairs by Lanczos with full reorthogonalisation, then Jacobi; f = c·k/(2π√εr); checked against the rectangle's c/(2a√εr) in the tests). A mode within 5 % of a strong tone (the switching harmonics up to ten times the edge's knee, `clocks` from the spec: a crystal or an LC oscillator no chip drives counts to its 5th harmonic, a logic clock further) is `cavity-resonance`, with an `add-part` capacitor at the mode's loudest point. - **Crosstalk**: each quiet net (not a rail, not loaded, not tied to the aggressor through a capacitor, like a bootstrap) next to a switching node or a clock: closest approach, run within 1 mm, side and overlap capacitance, and the coupled step ΔV·Cm/(Cm+Cg). Over `crosstalkMv` (50) is `crosstalk`, with a `reroute` action (spacing, or a ground guard). - `review.noise` holds the cavities' modes, the tones and the crosstalk table; `noise.json` holds the mode shapes for the app. @@ -158,6 +158,7 @@ After applying a step, run `analyze` again: the issue goes away or says how far - **The fix**: a peak over the target is `pdn-impedance`, with an `add-part` capacitor (value, package, pin) chosen by re-solving with each candidate, up to three, each with the ratio it reaches; still over after three is a `pour` action (a plane pair). A converter's input rail also gets Middlebrook's check: under 10 dB below the converter's negative input resistance V²/P is `input-filter-stability`, with a damping capacitor and its ESR. - **Capacitors as they sit on the rail** (0.7.13, `src/caps.rs`, used by the PDN check and the converter's ripple): the technology comes from the Value, the MPN and the footprint (EEE-F…/CP_Elec_… electrolytic, ECAS…/EEF-…/APX… polymer, T520/T530 polymer tantalum, CP_Tantalum_… tantalum, else ceramic; C0G/NP0 by Value or vendor code), with ESR by technology. Class-II ceramics are taken at the rail's DC bias: from a measured curve when one matches the MPN (a folder of Murata SimSurfing JSON, spec `capCurves` or ADOM_FIELDS_CAP_CURVES: C at the bias, ESR at self-resonance, ESL from |Z| a decade above), else an estimate, retained = 1/(1+(V/V0)^1.6), V0 = 9.1 V·(case mm³ / µF)^0.38 (a fit to Murata's 0603 to 1206 X5R/X7R curves, about ±0.05). Each branch reports `uF`, `uFAtBias`, `type` and `biasModel`; new ceramics in the fix search are derated too, and polymer bulk parts are candidates. Ripple adds each side's ESR term. - **Hot-plug** (0.7.14): on a connector-fed rail, the supply leads (R, L; `pdn.<net>.sourceOhm`/`sourceNh`, default 30 mΩ and 0.7 µH) are switched onto every capacitor branch at its rail bias and solved in time (backward Euler, ESL ignored), with the same step run in ngspice as a cross-check (`hotPlug.spicePeakV`). The peak is checked against the lowest voltage rating on the rail (a "35V" in a Value, or Murata's code in an ECAS/GRM MPN). Over that rating is high, over 50 % overshoot is medium: `hot-plug-overshoot`, whose action is a damping bulk capacitor (value, ESR) or a TVS. It catches a low-ESR bulk part with no damping, which an impedance peak under its target does not show.+- **Filtered rails** (0.7.15): a rail fed through a ferrite, fuse, OR-ing diode, resistor or transistor switch from another rail is followed up to its connector or regulator (by pin function or part number) and modelled as one source branch; the upstream rails' capacitors are left out (conservative at high frequency). - `review.pdn` holds each rail's branches, |Z(f)| (`fHz` is shared), the target and band, the fixes and the curve after them; `pdn-<net>.cir` is the same circuit as an ngspice deck, run as a cross-check when ngspice is installed (`spice.maxErrPct`). `ui view --quantity pdn` (the Power button): the frequency sweeps; each branch stands a column at its place, its height its share of the load's current (over 100 % is two branches ringing); arcs carry the shares to the load pin; at the load pin |Z| stands in a glass sleeve, the target (green under the rim, red over, steel blue outside the band); the panel draws |Z(f)| per rail with its target, band and fix curve, and a frequency slider.
bin/adom-fields⋯ 1 unchanged line ⋯
docs/release-0.7.15.mdadded+8@@ -0,0 +1,8 @@+# adom-fields 0.7.15++Tested on a second kind of board, a discrete AM/FM superhet receiver (adom/amfm-receiver-molecule): oscillators instead of a switching converter, filtered rails instead of regulators.++- **Oscillators and crystals.** A clock net with a crystal on it, or an LC oscillator no chip drives, swings nearly as a sine: only its first five harmonics count as strong tones (its 50th harmonic no longer makes a plane resonance finding). A logic clock keeps its harmonics.+- **An oscillator's own circuit is not its victim.** For a clock, nets within two discrete parts of it (its crystal's other pin, its transistor's base and emitter) are coupled by design and are not crosstalk findings; a chip's other pins (an MCU's NRST beside its crystal) still are.+- **ESD arrays by part number.** USBLC6, TPD…, PESD…, SP05… on a line and ground count as protection; unconnected connector pins are skipped.+- **Filtered rails in the PDN check.** A rail fed through a ferrite, a fuse, an OR-ing diode, a resistor or a transistor switch from another rail is followed up to its connector or regulator and modelled as one source branch (a ferrite as its DC resistance plus a lossy inductance at its rated impedance).
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-fields", "type": "app",- "version": "0.7.14",+ "version": "0.7.15", "title": "Adom Fields", "brief": "Adom Fields: where the current and the heat go on your board.", "summary": "Current density on the copper, heat flow through the pours and the vias, and the temperature over the board, solved from the board file itself and shown on the board in 3D. The AI does the analysis and drives the view; the human orbits, zooms, hovers for the number under the cursor, and reads the nets by what they carry and the issues by what to fix. A step in Adom's AI Flow: the walkthrough goes into the video.",
page.json+8−15@@ -1,7 +1,7 @@ { "slug": "adom-fields", "type": "app",- "version": "0.7.14",+ "version": "0.7.15", "title": "Adom Fields", "brief": "Adom Fields: where the current and the heat go on your board.", "summary": "Current density on the copper, heat flow through the pours and the vias, and the temperature over the board, solved from the board file itself and shown on the board in 3D. The AI does the analysis and drives the view; the human orbits, zooms, hovers for the number under the cursor, and reads the nets by what they carry and the issues by what to fix. A step in Adom's AI Flow: the walkthrough goes into the video.",@@ -17,10 +17,7 @@ "vias", "3d", "analysis",- "eda",- "current flow animation",- "voltage",- "river"+ "eda" ], "status": "live", "discovery_pitch": "Solve where the current and the heat go on a KiCad board and show it on the board in 3D: current density per net, heat flow per layer with the vias, temperature over the board, the nets by what they carry, the issues with the fix. One command to solve, one to serve, one to drive it on camera.",@@ -40,10 +37,11 @@ "fields", "flow of current", "river view",- "voltage drop in 3d",- "ground bounce",+ "energy flow on a board",+ "poynting vector",+ "speed of light on a pcb",+ "electron drift speed", "exploded layer stack",- "current through vias", "explain the view out loud", "narrate the fields", "energy cycle",@@ -85,12 +83,7 @@ "anti-resonance", "input filter stability", "middlebrook",- "ngspice pdn",- "signals view",- "copper capacitance",- "switch node capacitance",- "plane capacitance",- "differential pair skew"+ "ngspice pdn" ], "sample_prompts": [ {@@ -131,4 +124,4 @@ "name": "John Lauer", "email": "[email protected]" }-}+}
skills/adom-fields/SKILL.md+2−1@@ -142,7 +142,7 @@ After applying a step, run `analyze` again: the issue goes away or says how far ## Noise: plane resonance and crosstalk (0.7.8) `analyze` also screens noise couplings, each finding with an action:-- **Cavities**: every power or switching pour facing the ground plane is a parallel-plate cavity; its lowest modes are solved on the real overlap (the Neumann Laplacian's eigenpairs by Lanczos with full reorthogonalisation, then Jacobi; f = c·k/(2π√εr); checked against the rectangle's c/(2a√εr) in the tests). A mode within 5 % of a strong tone (the switching harmonics up to ten times the edge's knee, `clocks` from the spec) is `cavity-resonance`, with an `add-part` capacitor at the mode's loudest point.+- **Cavities**: every power or switching pour facing the ground plane is a parallel-plate cavity; its lowest modes are solved on the real overlap (the Neumann Laplacian's eigenpairs by Lanczos with full reorthogonalisation, then Jacobi; f = c·k/(2π√εr); checked against the rectangle's c/(2a√εr) in the tests). A mode within 5 % of a strong tone (the switching harmonics up to ten times the edge's knee, `clocks` from the spec: a crystal or an LC oscillator no chip drives counts to its 5th harmonic, a logic clock further) is `cavity-resonance`, with an `add-part` capacitor at the mode's loudest point. - **Crosstalk**: each quiet net (not a rail, not loaded, not tied to the aggressor through a capacitor, like a bootstrap) next to a switching node or a clock: closest approach, run within 1 mm, side and overlap capacitance, and the coupled step ΔV·Cm/(Cm+Cg). Over `crosstalkMv` (50) is `crosstalk`, with a `reroute` action (spacing, or a ground guard). - `review.noise` holds the cavities' modes, the tones and the crosstalk table; `noise.json` holds the mode shapes for the app. @@ -158,6 +158,7 @@ After applying a step, run `analyze` again: the issue goes away or says how far - **The fix**: a peak over the target is `pdn-impedance`, with an `add-part` capacitor (value, package, pin) chosen by re-solving with each candidate, up to three, each with the ratio it reaches; still over after three is a `pour` action (a plane pair). A converter's input rail also gets Middlebrook's check: under 10 dB below the converter's negative input resistance V²/P is `input-filter-stability`, with a damping capacitor and its ESR. - **Capacitors as they sit on the rail** (0.7.13, `src/caps.rs`, used by the PDN check and the converter's ripple): the technology comes from the Value, the MPN and the footprint (EEE-F…/CP_Elec_… electrolytic, ECAS…/EEF-…/APX… polymer, T520/T530 polymer tantalum, CP_Tantalum_… tantalum, else ceramic; C0G/NP0 by Value or vendor code), with ESR by technology. Class-II ceramics are taken at the rail's DC bias: from a measured curve when one matches the MPN (a folder of Murata SimSurfing JSON, spec `capCurves` or ADOM_FIELDS_CAP_CURVES: C at the bias, ESR at self-resonance, ESL from |Z| a decade above), else an estimate, retained = 1/(1+(V/V0)^1.6), V0 = 9.1 V·(case mm³ / µF)^0.38 (a fit to Murata's 0603 to 1206 X5R/X7R curves, about ±0.05). Each branch reports `uF`, `uFAtBias`, `type` and `biasModel`; new ceramics in the fix search are derated too, and polymer bulk parts are candidates. Ripple adds each side's ESR term. - **Hot-plug** (0.7.14): on a connector-fed rail, the supply leads (R, L; `pdn.<net>.sourceOhm`/`sourceNh`, default 30 mΩ and 0.7 µH) are switched onto every capacitor branch at its rail bias and solved in time (backward Euler, ESL ignored), with the same step run in ngspice as a cross-check (`hotPlug.spicePeakV`). The peak is checked against the lowest voltage rating on the rail (a "35V" in a Value, or Murata's code in an ECAS/GRM MPN). Over that rating is high, over 50 % overshoot is medium: `hot-plug-overshoot`, whose action is a damping bulk capacitor (value, ESR) or a TVS. It catches a low-ESR bulk part with no damping, which an impedance peak under its target does not show.+- **Filtered rails** (0.7.15): a rail fed through a ferrite, fuse, OR-ing diode, resistor or transistor switch from another rail is followed up to its connector or regulator (by pin function or part number) and modelled as one source branch; the upstream rails' capacitors are left out (conservative at high frequency). - `review.pdn` holds each rail's branches, |Z(f)| (`fHz` is shared), the target and band, the fixes and the curve after them; `pdn-<net>.cir` is the same circuit as an ngspice deck, run as a cross-check when ngspice is installed (`spice.maxErrPct`). `ui view --quantity pdn` (the Power button): the frequency sweeps; each branch stands a column at its place, its height its share of the load's current (over 100 % is two branches ringing); arcs carry the shares to the load pin; at the load pin |Z| stands in a glass sleeve, the target (green under the rim, red over, steel blue outside the band); the panel draws |Z(f)| per rail with its target, band and fix curve, and a frequency slider.
src/emc.rs+5−2@@ -192,9 +192,12 @@ pub fn review(f: &Fields, b: &Board, spec: &Spec, issues: &mut Vec<Issue>) -> Va // ---- ESD: each connector pin to the outside, protected or not ---------------------------------------- let mut esd = Vec::new(); if let Some(gname) = gnd.clone() {- let protector = |net: &str| refs.iter().find(|r| (r.starts_with('D') || r.starts_with("TVS") || r.starts_with("ESD")) && nets_of(r).contains(&net.to_string()) && nets_of(r).contains(&gname)).cloned();+ // a protector: a diode or TVS by its designator, or an ESD array by its part number (USBLC6, TPD…, PESD…), on the line and ground+ const ESD: [&str; 12] = ["USBLC", "TPD", "PESD", "ESD", "TVS", "SP05", "SRV05", "PRTR", "SMF", "SMAJ", "SMBJ", "RCLAMP"];+ let esd_part = |r: &str| [b.values.get(r), b.mpns.get(r)].iter().flatten().any(|v| { let u = v.to_ascii_uppercase(); ESD.iter().any(|k| u.contains(k)) });+ let protector = |net: &str| refs.iter().find(|r| (r.starts_with('D') || r.starts_with("TVS") || r.starts_with("ESD") || esd_part(r)) && nets_of(r).contains(&net.to_string()) && nets_of(r).contains(&gname)).cloned(); for jref in refs.iter().filter(|r| r.starts_with('J') || (r.starts_with('P') && !r.starts_with("PS"))) {- for p in b.pads.iter().filter(|p| p.reference == *jref && !p.net.is_empty() && !is_ground(&p.net)) {+ for p in b.pads.iter().filter(|p| p.reference == *jref && !p.net.is_empty() && !is_ground(&p.net) && !p.net.starts_with("unconnected-")) { let prot = protector(&p.net); let sig = rail(&p.net).is_none(); esd.push(json!({"connector": jref, "pin": p.pad, "net": p.net, "at": [r2(p.x), r2(p.y)], "protectedBy": prot, "signal": sig}));
src/noise.rs+11−2@@ -60,7 +60,9 @@ pub fn review(f: &Fields, b: &Board, spec: &Spec, issues: &mut Vec<Issue>) -> Va // the strong tones: switching harmonics (trapezoid envelope within 40 dB of the fundamental) and named clocks let mut tones: Vec<(f64, String)> = Vec::new(); for (net, sw) in &spec.switching { let fs = sw.freqHz.unwrap_or(500e3); let tr = sw.riseNs.unwrap_or(8.0) * 1e-9; let knee = 1.0 / (std::f64::consts::PI * tr); let top = (knee * 10.0).min(spec.emcFmaxHz.unwrap_or(1e9)); let mut k = 1.0; while k * fs <= top { tones.push((k * fs, format!("{net} harmonic {}", k as u64))); k += if k * fs < 100e6 { 1.0 } else { (k * 0.01).max(1.0).floor() }; } }- for (net, hz) in &spec.clocks { let mut k = 1.0; while k * hz <= spec.emcFmaxHz.unwrap_or(1e9) * 2.0 { tones.push((k * hz, format!("{net} clock harmonic {}", k as u64))); k += 1.0; } }+ // a crystal's pins, or an LC oscillator no chip drives, swing nearly as a sine: only the first five harmonics count;+ // a logic clock's run on+ for (net, hz) in &spec.clocks { let xtal = b.pads.iter().any(|p| p.net == *net && (p.reference.starts_with('Y') || p.reference.starts_with('X'))) || !b.pads.iter().any(|p| p.net == *net && p.reference.starts_with('U')); let top = if xtal { 5.0 * hz } else { spec.emcFmaxHz.unwrap_or(1e9) * 2.0 }; let mut k = 1.0; while k * hz <= top * 1.0001 { tones.push((k * hz, format!("{net} {} harmonic {}", if xtal { "oscillator" } else { "clock" }, k as u64))); k += 1.0; } } let mut out = json!({}); // ---- cavities -------------------------------------------------------------------------------------------- let mut cavities = Vec::new();@@ -112,7 +114,14 @@ pub fn review(f: &Fields, b: &Board, spec: &Spec, issues: &mut Vec<Issue>) -> Va for (vid, vname) in f.net_names.iter().enumerate() { let vid = vid as u16; if vid == aid || Some(vid) == gid || f.flow.contains_key(vname) || rail(vname).is_some() || aggressors.iter().any(|a| a.0 == *vname) { continue; } // a net joined to the aggressor through a capacitor rides on it on purpose (a bootstrap): not a victim- let rides = { let mut parts: std::collections::BTreeMap<&str, (bool, bool)> = Default::default(); for p in &b.pads { if !p.reference.starts_with('C') { continue; } let e = parts.entry(p.reference.as_str()).or_default(); if p.net == *agg { e.0 = true; } if p.net == *vname { e.1 = true; } } parts.values().any(|(a, v)| *a && *v) };+ // and for a clock or an oscillator, a net sharing any discrete part with it (its crystal, its transistor,+ // its bias resistor) is the oscillator's own circuit, coupled by design; a chip's other pins still count+ let is_clock = spec.clocks.contains_key(agg);+ let rides = { let disc = |r: &str| r.starts_with('C') || (is_clock && !r.starts_with('U') && !r.starts_with('J') && !r.starts_with("TP"));+ // the nets one discrete part away from a net (never through a rail or ground)+ let hop = |from: &str| -> std::collections::BTreeSet<String> { let mut out = std::collections::BTreeSet::new(); for p in b.pads.iter().filter(|p| p.net == from && disc(&p.reference)) { for q in b.pads.iter().filter(|q| q.reference == p.reference && q.net != from) { out.insert(q.net.clone()); } } out };+ let one = hop(agg);+ one.contains(vname.as_str()) || (is_clock && one.iter().filter(|n| rail(n).is_none() && !is_ground(n)).any(|n| hop(n).contains(vname.as_str()))) }; if rides { continue; } // side coupling on each layer: victim cells within 1 mm of the aggressor, summed as C per length at that spacing let (mut cm, mut closest, mut at, mut run) = (0.0f64, f64::MAX, [0.0, 0.0], 0usize); let mut varea = 0.0;
src/pdn.rs+44−1@@ -99,7 +99,31 @@ pub fn review(f: &Fields, b: &Board, spec: &Spec, conv: &Value, issues: &mut Vec .or_else(|| refs.iter().find(|r| r.starts_with('U') && out_pin(r) && nets_of(r).contains(&gnd)).cloned()) .or_else(|| refs.iter().find(|r| r.starts_with('U') && bridges(r) && looks_reg(r)).cloned()) }; let entry: Vec<String> = if !srcs.is_empty() { srcs.iter().filter(|r| r.starts_with('J') || r.starts_with('P')).cloned().collect() } else { f.flow.get(&name).map(|nf| { let mut v: Vec<String> = nf.terminals.iter().filter(|t| t.side == "ext" && (t.reference.starts_with('J') || t.reference.starts_with('P'))).map(|t| t.reference.clone()).collect(); v.dedup(); v }).unwrap_or_default() };- if !is_out && reg.is_none() && entry.is_empty() && cfg.sourceOhm.is_none() {+ // a rail fed through a series part from another rail (a ferrite, a fuse, an OR-ing diode, a resistor): walk up+ // the chain to a connector or a regulator output and model the whole path as one source branch+ let chain: Option<(Vec<String>, &'static str, String)> = if !is_out && reg.is_none() && entry.is_empty() && cfg.sourceOhm.is_none() {+ fn walk(b: &Board, net: &str, gnd: &str, rail: &dyn Fn(&str) -> Option<f64>, reg_of: &dyn Fn(&str) -> Option<String>, depth: usize, seen: &mut Vec<String>) -> Option<(Vec<String>, &'static str, String)> {+ if depth > 5 { return None; }+ let mut refs: Vec<&str> = b.pads.iter().filter(|p| p.net == net).map(|p| p.reference.as_str()).collect(); refs.sort(); refs.dedup();+ for r in refs {+ if !(r.starts_with('R') || r.starts_with('L') || r.starts_with('F') || r.starts_with('D') || r.starts_with('Q')) || seen.iter().any(|x| x == r) { continue; }+ let mut ns: Vec<&str> = b.pads.iter().filter(|p| p.reference == r).map(|p| p.net.as_str()).collect(); ns.sort(); ns.dedup();+ // a two-terminal part, or a transistor switch whose other two pins are this rail and a higher one+ let up = if ns.len() == 2 { if ns[0] == net { ns[1] } else { ns[0] } } else if r.starts_with('Q') && ns.len() == 3 { match ns.iter().find(|x| **x != net && rail(x).map_or(false, |vx| vx >= rail(net).unwrap_or(0.0))) { Some(x) => x, None => continue } } else { continue };+ if up == gnd || is_ground(up) || rail(up).is_none() || seen.iter().any(|x| x == up) { continue; }+ seen.push(r.to_string()); seen.push(up.to_string());+ if let Some(j) = b.pads.iter().find(|p| p.net == up && (p.reference.starts_with('J') || (p.reference.starts_with('P') && !p.reference.starts_with("PS")))) { return Some((vec![r.to_string()], "cable", j.reference.clone())); }+ if let Some(u) = b.pads.iter().find(|p| p.net == up && p.reference.starts_with('U') && { let f = p.func.to_ascii_uppercase(); let f = f.split('_').next().unwrap_or(""); f == "VOUT" || f == "OUT" || f == "VO" }) { return Some((vec![r.to_string()], "regulator", u.reference.clone())); }+ if let Some(u) = reg_of(up) { return Some((vec![r.to_string()], "regulator", u)); }+ if let Some((mut v, k, top)) = walk(b, up, gnd, rail, reg_of, depth + 1, seen) { v.insert(0, r.to_string()); return Some((v, k, top)); }+ }+ None+ }+ // a regulator on the upstream rail recognised by its part number, as for a rail of its own+ let reg_of = |up: &str| -> Option<String> { refs.iter().find(|r| r.starts_with('U') && { let ns = nets_of(r); ns.contains(&up.to_string()) && ns.contains(&gnd) && ns.iter().any(|x| *x != up && rail(x).map_or(false, |vx| vx > rail(up).unwrap_or(0.0))) } && looks_reg(r)).cloned() };+ let mut seen = vec![name.clone()]; walk(b, &name, &gnd, &rail, ®_of, 0, &mut seen)+ } else { None };+ if !is_out && reg.is_none() && entry.is_empty() && cfg.sourceOhm.is_none() && chain.is_none() { let maybe: Vec<String> = refs.iter().filter(|r| r.starts_with('U') && bridges(r) && b.pads.iter().filter(|p| p.reference == **r).count() <= 8).cloned().collect(); rails.push(json!({"net": name, "railV": v, "skipped": format!("no known source{}: name the regulator or the connector in spec sources.{}, or give pdn.{}.sourceOhm and sourceNh", if maybe.is_empty() { String::new() } else { format!(" ({} bridges {} and a higher rail: if it is the regulator, say so)", maybe.join(", "), name) }, name, name)})); continue;@@ -162,6 +186,25 @@ pub fn review(f: &Fields, b: &Board, spec: &Spec, conv: &Value, issues: &mut Vec } else if let Some(u) = ® { let up = pads_of(u, &name).first().copied(); Some(loop_branch(format!("{u} loop"), cfg.crossoverHz.unwrap_or(100e3), up.map(|p| [r2(p.x), r2(p.y)]), path_from(up), "a linear regulator, crossover about 100 kHz assumed"))+ } else if let Some((parts, top_kind, top)) = &chain {+ // each series part as R + L (a ferrite as its DC resistance plus a lossy inductance, Z at its rated frequency)+ let (mut r, mut l, mut lp, mut rp) = (0.0, 0.0, 0.0, 0.0);+ let mut desc = Vec::new();+ for p in parts {+ let val = b.values.get(p).cloned().unwrap_or_default(); let u = val.to_ascii_uppercase();+ if p.starts_with("FB") || u.contains("@") {+ let z: f64 = u.split(|c: char| c == 'R' || c == 'Ω' || c == '@').next().and_then(|x| x.trim().parse().ok()).unwrap_or(600.0);+ let fz: f64 = u.split('@').nth(1).and_then(|x| x.trim_end_matches("HZ").trim_end_matches('M').trim().parse::<f64>().ok()).map(|m| m * 1e6).unwrap_or(100e6);+ r += 0.15; lp += z / (2.0 * PI * fz); rp += z; desc.push(format!("{p} ferrite {}", val));+ } else if p.starts_with('F') { r += 0.25; l += 5e-9; desc.push(format!("{p} fuse")); }+ else if p.starts_with('D') { r += 0.15; l += 3e-9; desc.push(format!("{p} diode")); }+ else if p.starts_with('Q') { r += 0.5; l += 3e-9; desc.push(format!("{p} switch {}", val)); }+ else if p.starts_with('L') { r += 0.1; l += crate::emc::parse_value(&val, 'H').unwrap_or(1e-6); desc.push(format!("{p} {}", val)); }+ else { let ohm = { let t = u.replace("OHM", "").replace('Ω', ""); let t = t.trim(); let (num, mul) = if let Some(i) = t.find(|c: char| c == 'K' || c == 'M' || c == 'R') { let m = match &t[i..i + 1] { "K" => 1e3, "M" => 1e6, _ => 1.0 }; (format!("{}.{}", &t[..i], &t[i + 1..]).trim_end_matches('.').to_string(), m) } else { (t.to_string(), 1.0) }; num.parse::<f64>().unwrap_or(0.0) * mul }; r += ohm; l += 2e-9; desc.push(format!("{p} {}", val)); }+ }+ let (r0, l0, what) = if *top_kind == "cable" { (0.03, cfg.sourceNh.unwrap_or(700.0) * 1e-9, format!("about a metre of supply leads into {top}")) } else { (0.005, 20e-9, format!("the {top} regulator output, taken as flat")) };+ let first = b.pads.iter().find(|p| p.reference == parts[0] && p.net == name);+ Some(Branch { name: format!("{top} via {}", parts.join(", ")), kind: if *top_kind == "cable" { "cable" } else { "filter" }, r: r + r0, l: l + l0 + path_from(first), lp, rp, c: 0.0, at: first.map(|p| [r2(p.x), r2(p.y)]), extra: json!({"rOhm": sig(r + r0), "nH": sig((l + l0) * 1e9), "chain": desc, "model": format!("{what}, then {}; the upstream rails' own capacitors are left out (conservative at high frequency)", parts.join(", "))}) }) } else { // a connector feeding the rail: about a metre of supply leads let feed = entry.iter().find_map(|e| b.pads.iter().find(|p| p.reference == *e && p.net == name));
src/ui/pdn.js+1−1@@ -24,7 +24,7 @@ export async function attach(ctx) { function zb(b, f) { const w = 2 * Math.PI * f; let z = [b.r, w * b.l]; if (b.lp > 0) { const zl = [0, w * b.lp]; z = cx.add(z, b.rp > 0 ? cx.mul(cx.mul(zl, [b.rp, 0]), cx.inv(cx.add(zl, [b.rp, 0]))) : zl); } if (b.c > 0) z = cx.add(z, [0, -1 / (w * b.c)]); return z; } function solve(bs, f) { const zs = bs.map(b => zb(b, f)); const y = zs.reduce((a, z) => cx.add(a, cx.inv(z)), [0, 0]); const zt = cx.inv(y); return { z: cx.abs(zt), share: zs.map(z => cx.abs(cx.mul(zt, cx.inv(z)))) }; } // ---- one set of meshes per rail ------------------------------------------------------------------------- const colOf = (kind) => kind === 'regulator' || kind === 'cable' ? [0.35, 0.6, 1] : kind === 'plane' ? [0.75, 0.45, 1] : [0.3, 1, 0.75];+ const colOf = (kind) => kind === 'regulator' || kind === 'cable' || kind === 'filter' ? [0.35, 0.6, 1] : kind === 'plane' ? [0.75, 0.45, 1] : [0.3, 1, 0.75]; const host = document.getElementById('labels'); const mkTag = (cls) => { const el = document.createElement('div'); el.className = 'lbl pdn ' + (cls || ''); el.hidden = true; host.appendChild(el); return el; }; const idOf = (name) => nets.findIndex(n => n.name === name);