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.8.0: heat through the parts and the heatsink (solids), fan levels, whole-scene default view
c278351
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
//! Explicit steady-state package nodes. Heat is injected once at the junction;
//! board terminals and the external sink share it through declared resistances.
//! This is a supplied-parameter screen, not an airflow or assembly qualification.
use crate::solve::System;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Package {
pub reference: String,
pub junction_case_k_per_w: f64,
pub case_sink_k_per_w: f64,
pub sink_ambient_still_k_per_w: f64,
pub sink_ambient_moving_k_per_w: f64,
pub board_links: Vec<BoardLink>,
/// Cite the part/TIM/sink and conditions, and identify assumptions.
pub provenance: String,
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct BoardLink {
pub pad: String,
pub at: [f64; 2],
pub layer: String,
pub resistance_k_per_w: f64,
}
pub struct Nodes {
pub junction: usize,
pub case: usize,
pub sink: usize,
pub links: Vec<(usize, f64)>,
pub sink_conductance: f64,
}
impl Package {
pub fn validate_board(
&self,
board: &crate::board::Board,
layers: &[&str],
pitch: f64,
) -> Result<(), String> {
self.validate()?;
if !pitch.is_finite() || pitch <= 0.0 {
return Err("invalid package grid pitch".into());
}
let width = ((board.width() / (pitch / 2.0)).round() as usize).max(1) / 2;
let height = ((board.height() / (pitch / 2.0)).round() as usize).max(1) / 2;
for link in &self.board_links {
if !layers.contains(&link.layer.as_str()) {
return Err(format!(
"{}: package link names an unsupported layer",
self.reference
));
}
let matches: Vec<_> = board
.pads
.iter()
.filter(|p| {
p.reference == self.reference
&& p.pad == link.pad
&& (p.x - link.at[0]).abs() < 1e-6
&& (p.y - link.at[1]).abs() < 1e-6
&& p.layers.contains(&link.layer)
})
.collect();
if matches.len() != 1 {
return Err(format!(
"{}: package link must identify exactly one physical pad on its copper layer",
self.reference
));
}
if link.at[0] < board.x0
|| link.at[0] >= board.x0 + width as f64 * pitch
|| link.at[1] < board.y0
|| link.at[1] >= board.y0 + height as f64 * pitch
{
return Err(format!(
"{}: package link lies outside thermal grid",
self.reference
));
}
}
Ok(())
}
pub fn validate(&self) -> Result<(), String> {
if self.reference.trim().is_empty() || self.provenance.trim().is_empty() {
return Err("packageCooling requires reference and parameter provenance".into());
}
for r in [
self.junction_case_k_per_w,
self.case_sink_k_per_w,
self.sink_ambient_still_k_per_w,
self.sink_ambient_moving_k_per_w,
]
.into_iter()
.chain(self.board_links.iter().map(|l| l.resistance_k_per_w))
{
if !r.is_finite() || r <= 0.0 || !(1.0 / r).is_finite() {
return Err(format!(
"{}: package resistances must be finite and positive",
self.reference
));
}
}
for (i, link) in self.board_links.iter().enumerate() {
if link.pad.trim().is_empty() || !link.at.iter().all(|x| x.is_finite()) {
return Err(format!(
"{}: every board link needs a pad and finite coordinates",
self.reference
));
}
if self.board_links[..i]
.iter()
.any(|p| p.pad == link.pad && p.at == link.at && p.layer == link.layer)
{
return Err(format!("{}: duplicate package board link", self.reference));
}
}
Ok(())
}
/// Caller resolves and validates physical pads and layer indices first.
pub fn attach(&self, sys: &mut System, board_nodes: &[usize], level: f64, fan_n: f64) -> Nodes {
assert_eq!(board_nodes.len(), self.board_links.len());
let board_count = sys.nl * sys.h * sys.w;
assert!(board_nodes.iter().all(|i| *i < board_count));
// the sink's conductance to the air follows the fan level like h does (crate::fields::fan_mix)
let sink_conductance = crate::fields::fan_mix(1.0 / self.sink_ambient_still_k_per_w, 1.0 / self.sink_ambient_moving_k_per_w, level, fan_n);
let junction = sys.add_node(0.0);
let case = sys.add_node(0.0);
let sink = sys.add_node(sink_conductance);
sys.edges
.push((junction, case, 1.0 / self.junction_case_k_per_w));
sys.edges.push((case, sink, 1.0 / self.case_sink_k_per_w));
let links = board_nodes
.iter()
.zip(&self.board_links)
.map(|(node, link)| {
let conductance = 1.0 / link.resistance_k_per_w;
sys.edges.push((case, *node, conductance));
(*node, conductance)
})
.collect();
Nodes {
junction,
case,
sink,
links,
sink_conductance,
}
}
}
impl Nodes {
pub fn result(&self, temperatures: &[f64], watts: f64) -> Value {
let board_w: f64 = self
.links
.iter()
.map(|(i, g)| g * (temperatures[self.case] - temperatures[*i]))
.sum();
let ambient_w = temperatures[self.sink] * self.sink_conductance;
json!({
"junctionRiseC": temperatures[self.junction], "caseRiseC": temperatures[self.case],
"sinkRiseC": temperatures[self.sink], "boardCouplingW": board_w,
"sinkToAmbientW": ambient_w, "sourceW": watts,
"energyResidualW": watts - board_w - ambient_w,
"boardLinks": self.links.iter().map(|(i,g)| json!({
"boardRiseC": temperatures[*i],
"caseToBoardW": g*(temperatures[self.case]-temperatures[*i])
})).collect::<Vec<_>>(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn package() -> Package {
Package {
reference: "Q1".into(),
junction_case_k_per_w: 0.5,
case_sink_k_per_w: 1.0,
sink_ambient_still_k_per_w: 3.0,
sink_ambient_moving_k_per_w: 2.0,
board_links: vec![],
provenance: "Analytical test resistances, not physical qualification".into(),
}
}
fn close(a: f64, b: f64) {
assert!((a - b).abs() < 1e-8, "{a} != {b}");
}
#[test]
fn isolated_series_path_has_exact_temperatures_and_conserves_power() {
let p = package();
let mut s = System::new(1, 1, 1);
s.leak[0][0] = 1.0;
let n = p.attach(&mut s, &[], 0.0, 0.5);
let mut q = vec![0.0; s.n()];
q[n.junction] = 2.0;
let (t, _, res) = s.solve(&q, 1e-12, 100);
assert!(res < 1e-10);
close(t[n.sink], 6.0);
close(t[n.case], 8.0);
close(t[n.junction], 9.0);
close(n.result(&t, 2.0)["energyResidualW"].as_f64().unwrap(), 0.0);
}
#[test]
fn parallel_board_path_splits_power_without_double_injection() {
let mut p = package();
p.board_links.push(BoardLink {
pad: "2".into(),
at: [0.0, 0.0],
layer: "F.Cu".into(),
resistance_k_per_w: 2.0,
});
let mut s = System::new(1, 1, 1);
s.leak[0][0] = 0.5;
let n = p.attach(&mut s, &[0], 0.0, 0.5);
let mut q = vec![0.0; s.n()];
q[n.junction] = 2.0;
let (t, _, _) = s.solve(&q, 1e-12, 100);
let r = n.result(&t, 2.0);
close(t[n.case], 4.0);
close(t[n.junction], 5.0);
close(t[0], 2.0);
close(r["boardCouplingW"].as_f64().unwrap(), 1.0);
close(r["sinkToAmbientW"].as_f64().unwrap(), 1.0);
}
#[test]
fn hotter_board_can_send_heat_back_into_external_sink() {
let mut p = package();
p.board_links.push(BoardLink {
pad: "2".into(),
at: [0.0, 0.0],
layer: "F.Cu".into(),
resistance_k_per_w: 2.0,
});
let mut s = System::new(1, 1, 1);
s.leak[0][0] = 0.5;
let n = p.attach(&mut s, &[0], 0.0, 0.5);
let mut q = vec![0.0; s.n()];
q[0] = 10.0;
q[n.junction] = 1.0;
let (t, _, _) = s.solve(&q, 1e-12, 100);
let r = n.result(&t, 1.0);
assert!(r["boardCouplingW"].as_f64().unwrap() < 0.0);
close(r["energyResidualW"].as_f64().unwrap(), 0.0);
close(t[0] * 0.5 + r["sinkToAmbientW"].as_f64().unwrap(), 11.0);
}
#[test]
fn invalid_or_untraceable_resistances_are_rejected() {
for r in [0.0, -1.0, f64::NAN, f64::INFINITY] {
let mut p = package();
p.case_sink_k_per_w = r;
assert!(p.validate().is_err());
}
let mut p = package();
p.provenance.clear();
assert!(p.validate().is_err());
}
}