← Commit history

adom-fields 0.7.18: a switching node with no load is solved with its inductor's current (issue 13); the method names the real copper layers

John Lauer ·6d2433722f · ·parent bd2cb73
8 files changed +28−28
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-fields"-version = "0.7.17"+version = "0.7.18" dependencies = [  "clap",  "png",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-fields"-version = "0.7.17"+version = "0.7.18" 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."
bin/adom-fields
⋯ 1 unchanged line ⋯
docs/release-0.7.18.mdadded+5
@@ -0,0 +1,5 @@+# adom-fields 0.7.18++- A switching node with no `loads` entry is solved with the current of the net across its inductor (else the largest load) and listed in `switchingLoads` and the hints, so its copper is never silently unsolved (issue 13).+- The method text names the board's real copper layers; a two-layer board's nets list only F.Cu and B.Cu (the phantom inner via copper went in 0.7.17).+- The hints say which loads were filled in rather than taken from the spec.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-fields",   "type": "app",-  "version": "0.7.17",+  "version": "0.7.18",   "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−23
@@ -1,7 +1,7 @@ {   "slug": "adom-fields",   "type": "app",-  "version": "0.7.17",+  "version": "0.7.18",   "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,20 +83,7 @@     "anti-resonance",     "input filter stability",     "middlebrook",-    "ngspice pdn",-    "signals view",-    "copper capacitance",-    "switch node capacitance",-    "plane capacitance",-    "differential pair skew",-    "six layer board",-    "receiver frequency plan",-    "lo leakage",-    "tone in band",-    "rf crosstalk",-    "return path over supply plane",-    "antenna ground loop",-    "stage feed bypass"+    "ngspice pdn"   ],   "sample_prompts": [     {@@ -139,4 +124,4 @@     "name": "John Lauer",     "email": "[email protected]"   }-}\ No newline at end of file+}
src/fields.rs+1−1
@@ -458,7 +458,7 @@ pub fn summary(f: &Fields, b: &Board, spec: &Spec, textures: &[String]) -> Value         "textures": textures,         "partValues": b.values,         "pourNets": pour_nets,-        "method": "Copper fraction per cell from the board's filled polygons, tracks, pads and vias; DC conduction per loaded net between its terminals (σ 5.8e4 S/mm × thickness); heat: four layers coupled through the dielectric and every via barrel, copper 0.40 W/mm·K, FR4 0.3/0.6 W/m·K, 10 (still) or 25 (moving) W/m²K to the air on both faces; one steady-state solve each. Optional rectangular heatsinks use a 3D finite-volume conducting block with specified conductivity, contact resistance and air loss on exposed faces. Copper thickness may differ by layer; Joule heat is deposited on its source layer. This remains a screening model: rectangular board outline, prescribed chip watts and convection; no fins, radiation, airflow solve or junction-temperature qualification."+        "method": format!("Copper fraction per cell from the board's filled polygons, tracks, pads and vias, drill holes left empty; DC conduction per loaded net between its terminals (σ 5.8e4 S/mm × thickness), each via and plated pad barrel joining its layers around its wall; heat: the board's {} copper layer(s) ({}) coupled through the dielectric and every via and plated pad barrel,", if spec.real_copper.is_empty() { 4 } else { spec.real_copper.len() }, if spec.real_copper.is_empty() { "F.Cu, In1.Cu, In2.Cu, B.Cu".to_string() } else { spec.real_copper.join(", ") }) + " copper 0.40 W/mm·K, FR4 0.3/0.6 W/m·K, 10 (still) or 25 (moving) W/m²K to the air on both faces; one steady-state solve each. Optional rectangular heatsinks use a 3D finite-volume conducting block with specified conductivity, contact resistance and air loss on exposed faces. Copper thickness may differ by layer; Joule heat is deposited on its source layer. This remains a screening model: rectangular board outline, prescribed chip watts and convection; no fins, radiation, airflow solve or junction-temperature qualification."     }) } 
src/main.rs+11−1
@@ -85,6 +85,15 @@ pub fn run_analyze(board: &str, spec_path: &str, out: &PathBuf, pitch: f64, neck     spec.stackupMm = fields::spread(&gaps, &cu); spec.real_gaps = gaps; spec.real_copper = cu.clone();     // ground carries the return current even when the spec lists only the supplies (issue 9): solve it with the     // largest supply load as its current, and say so+    // a switching node with no load still carries the inductor's whole current (issue 13): solve it with the load of the net+    // across its inductor (the converter's output), else the largest load, and say so+    let mut auto_switch: Vec<(String, f64)> = Vec::new();+    for sw in spec.switching.keys().cloned().collect::<Vec<_>>() {+        if spec.loads.contains_key(&sw) || spec.loads.is_empty() { continue; }+        let across = b.pads.iter().filter(|p| p.net == sw && p.reference.starts_with('L')).find_map(|p| b.pads.iter().find(|q| q.reference == p.reference && q.net != sw).map(|q| q.net.clone()));+        let amps = across.as_ref().and_then(|n| spec.loads.get(n)).map(|l| l.amps).unwrap_or_else(|| spec.loads.values().map(|l| l.amps).fold(0.0, f64::max));+        if amps > 0.0 { spec.loads.insert(sw.clone(), fields::Load { amps, maxRiseC: None, terminals: None }); auto_switch.push((sw, amps)); }+    }     let mut auto_ground: Option<(String, f64)> = None;     if let Some(gname) = b.pads.iter().map(|p| p.net.clone()).find(|n| crate::flow::is_ground(n)) {         if !spec.loads.contains_key(&gname) && !spec.loads.is_empty() {@@ -180,7 +189,7 @@ pub fn run_analyze(board: &str, spec_path: &str, out: &PathBuf, pitch: f64, neck     summary["review"] = review;     summary["fixPlan"] = emc::fix_plan(&f.issues);     summary["flow"] = flow::write(out, &f, &b, &spec)?;-    summary["board"]["stackupFrom"] = serde_json::json!(stack_from); if let Some((g, a)) = &auto_ground { summary["groundReturn"] = serde_json::json!({"net": g, "amps": a, "note": "not in the spec: solved with the largest supply load as the return current (set loads.<ground> to change it)"}); }+    summary["board"]["stackupFrom"] = serde_json::json!(stack_from); if !auto_switch.is_empty() { summary["switchingLoads"] = serde_json::json!(auto_switch.iter().map(|(n, a)| serde_json::json!({"net": n, "amps": a, "note": "a switching node with no load in the spec: solved with the current of the net across its inductor (else the largest load); set loads.<net> to change it"})).collect::<Vec<_>>()); } if let Some((g, a)) = &auto_ground { summary["groundReturn"] = serde_json::json!({"net": g, "amps": a, "note": "not in the spec: solved with the largest supply load as the return current (set loads.<ground> to change it)"}); }     summary["board"]["name"] = serde_json::json!(spec.board.split('(').next().unwrap_or(&spec.board).trim());     summary["board"]["description"] = serde_json::json!(spec.board);     summary["log"] = serde_json::json!(lines);@@ -213,6 +222,7 @@ fn main() {                         { let all = s["review"]["pdn"]["rails"].as_array().cloned().unwrap_or_default(); let rs: Vec<Value> = all.iter().filter(|r| r["skipped"].is_null()).cloned().collect(); let sk: String = all.iter().filter(|r| !r["skipped"].is_null()).map(|r| format!("; {} not checked: {}", r["net"].as_str().unwrap_or(""), r["skipped"].as_str().unwrap_or(""))).collect(); if rs.is_empty() { format!("PDN: no rail checked{}.", sk) } else { format!("PDN (|Z| seen from each load pin against V·tol/step): {}{}.", rs.iter().map(|r| format!("{} {}× target at {} Hz (from {})", r["net"].as_str().unwrap_or(""), r["worst"]["ratio"], r["worst"]["hz"], r["load"].as_str().unwrap_or(""))).collect::<Vec<_>>().join(", "), if rs.iter().all(|r| r["spice"]["ran"] == true) { format!("; ngspice agrees within {} %{}", rs.iter().map(|r| r["spice"]["maxErrPct"].as_f64().unwrap_or(0.0)).fold(0.0, f64::max), sk) } else { sk }) } },                         { let p = s["fixPlan"].as_array().cloned().unwrap_or_default(); if p.is_empty() { "Fix plan: nothing to change.".to_string() } else { format!("Fix plan ({} step(s), fields.json fixPlan has the actions): {}", p.len(), p.iter().take(5).map(|x| format!("{}) [{}] {}", x["step"], x["kind"].as_str().unwrap_or(""), x["do"].as_str().unwrap_or(""))).collect::<Vec<_>>().join(" ")) } },                         if s["parts"].is_null() { "Parts: skipped (--no-parts).".to_string() } else { format!("Parts: {} with 3D models, {} as boxes, {} bare{}.", s["parts"]["modelled"], s["parts"]["boxed"], s["parts"]["bare"], if s["parts"]["failed"].as_array().map_or(false, |a| !a.is_empty()) { "; some models did not convert (see the log above; --models-dir adds a folder to search)" } else { "" }) },+                        { let mut v: Vec<String> = s["switchingLoads"].as_array().into_iter().flatten().map(|x| format!("{} (switching, {} A)", x["net"].as_str().unwrap_or(""), x["amps"])).collect(); if let Some(g) = s["groundReturn"].as_object() { v.push(format!("{} (return, {} A)", g["net"].as_str().unwrap_or(""), g["amps"])); } if v.is_empty() { "Loads: all from the spec.".to_string() } else { format!("Loads filled in (not in the spec): {}; set loads.<net> to change them.", v.join(", ")) } },                         if s["heat"]["sourcesModelled"] == 0 { "Thermal: no heat sources modelled, so the temperature is only the copper's own I²R loss: this is not a thermal pass for any part. Add each dissipating part to spec hot (watts; tabNet if it has an exposed tab, else the watts go through all its leads by pad area).".to_string() } else { format!("Thermal: {} part(s) modelled as heat sources.", s["heat"]["sourcesModelled"]) },                         format!("Heatsinks: {}; signal capacitance checks: {}; differential pairs: {}. Signals are screening estimates; unsupported geometry is reported, never certified.", s["heatsinks"].as_array().map_or(0,|x|x.len()),s["signals"]["capacitance"].as_array().map_or(0,|x|x.len()),s["signals"]["differentialPairs"].as_array().map_or(0,|x|x.len())),                         format!("Next: `adom-fields serve --fields {}` and open it (pup or the browser); `adom-fields ui view --quantity current --net <net>` drives it; `adom-fields tour` walks it for a video.", out.display()),