1use logicaffeine_base::numeric::Rational;
20use logicaffeine_proof::thermal_replay::ThermalClaim;
21
22use super::cell::AnalogNetlist;
23use super::stack::{CoolingClass, StackTarget, TierFabric};
24use super::stack_place::StackPlacement;
25use super::AnaLowerError;
26
27#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct ThermalOpts {
31 pub analog_grid: (usize, usize),
33 pub cell_edge_um: f64,
35 pub bond_thickness_um: f64,
37 pub tile_bias_w: f64,
39}
40
41pub fn thermal_default_opts() -> ThermalOpts {
43 ThermalOpts {
44 analog_grid: (4, 4),
45 cell_edge_um: 500.0,
46 bond_thickness_um: 10.0,
47 tile_bias_w: 1.0e-4,
48 }
49}
50
51pub const THERMAL_MODEL_PROVENANCE: &str =
53 "Sourced: bulk k per Material (Incropera/CRC, benchmarks/silicon/analog/thermal-constants.md); digital \
54 tier power from benchmarks/silicon/sky130_power.json (OpenLane signoff, carried verbatim); \
55 TSV per-via g DERIVED from sourced k_Cu. Modeled (OUR design parameters, never silicon): \
56 film-coefficient carried points h = 10 / 100 / 2000 W/(m2.K) for natural / forced-air / \
57 cold-plate (Incropera Table 1.1 RANGES are the sourced part), analog-tier coarsening grid, \
58 500 um thermal cell edge, 10 um bond thickness, underfill-class bond k, 1e-4 W per-tile \
59 bias power, cross-tier via attachment at mapped coarse cells. Full dossier: \
60 benchmarks/silicon/analog/thermal-constants.md";
61
62pub fn film_coefficient_w_m2k(class: CoolingClass) -> f64 {
64 match class {
65 CoolingClass::NaturalConvection => 10.0,
66 CoolingClass::ForcedAir => 100.0,
67 CoolingClass::ColdPlate => 2000.0,
68 }
69}
70
71#[derive(Clone, Debug, PartialEq)]
73pub struct ThermalMap {
74 pub claim: ThermalClaim,
75 pub delta_t_c: Vec<f64>,
77 pub node_tier: Vec<usize>,
79 pub hottest: usize,
80}
81
82fn rat(x: f64) -> Result<Rational, AnaLowerError> {
86 if !x.is_finite() {
87 return Err(AnaLowerError::ThermalModelDisconnected);
88 }
89 let scaled = (x * 1.0e12).round() as i64;
90 Ok(Rational::from_i64(scaled)
91 .div(&Rational::from_i64(1_000_000_000_000))
92 .expect("nonzero denominator"))
93}
94
95pub fn thermal_map(
97 stack: &StackTarget,
98 netlist: &AnalogNetlist,
99 placement: &StackPlacement,
100 opts: &ThermalOpts,
101) -> Result<ThermalMap, AnaLowerError> {
102 if let Err(e) = stack.validate() {
103 return Err(AnaLowerError::UnloweredOp(format!(
104 "malformed stack: {e:?}"
105 )));
106 }
107 let (gr, gc) = opts.analog_grid;
108 assert!(
109 gr > 0 && gc > 0,
110 "a zero-size thermal grid is not a coarsening"
111 );
112 let cell_edge_m = opts.cell_edge_um * 1.0e-6;
113 let cell_area_m2 = cell_edge_m * cell_edge_m;
114
115 let mut base = Vec::with_capacity(stack.tiers.len());
117 let mut node_tier = Vec::new();
118 for (ti, tier) in stack.tiers.iter().enumerate() {
119 base.push(node_tier.len());
120 let count = match tier.fabric {
121 TierFabric::Analog(_) => gr * gc,
122 _ => 1,
123 };
124 node_tier.extend(std::iter::repeat(ti).take(count));
125 }
126 let n = node_tier.len();
127 let grid_node = |ti: usize, r: usize, c: usize| base[ti] + r * gc + c;
128 let coarse_of = |ti: usize, row: usize, col: usize| -> (usize, usize) {
129 let TierFabric::Analog(a) = &stack.tiers[ti].fabric else {
130 return (0, 0);
131 };
132 (
133 (row * gr / a.rows.max(1)).min(gr - 1),
134 (col * gc / a.cols.max(1)).min(gc - 1),
135 )
136 };
137 let tier_rep_node = |ti: usize| -> usize {
138 match stack.tiers[ti].fabric {
139 TierFabric::Analog(_) => grid_node(ti, gr / 2, gc / 2),
140 _ => base[ti],
141 }
142 };
143
144 let mut edges: Vec<(usize, usize, Rational)> = Vec::new();
145 let mut grounds: Vec<(usize, Rational)> = Vec::new();
146 let mut injections: Vec<(usize, Rational)> = Vec::new();
147
148 for (ti, tier) in stack.tiers.iter().enumerate() {
150 if let TierFabric::Analog(_) = tier.fabric {
151 let g_lat = rat(tier.material.k_w_per_m_k * tier.thickness_um * 1.0e-6)?;
152 for r in 0..gr {
153 for c in 0..gc {
154 if r + 1 < gr {
155 edges.push((grid_node(ti, r, c), grid_node(ti, r + 1, c), g_lat.clone()));
156 }
157 if c + 1 < gc {
158 edges.push((grid_node(ti, r, c), grid_node(ti, r, c + 1), g_lat.clone()));
159 }
160 }
161 }
162 }
163 }
164
165 for (gi, gap) in stack.gaps.iter().enumerate() {
167 let (lo, hi) = (gi, gi + 1);
168 let k_bond = super::stack::Material::underfill_class().k_w_per_m_k;
169 let g_bond_cell = rat(k_bond * cell_area_m2 / (opts.bond_thickness_um * 1.0e-6))?;
170 let pair_nodes: Vec<(usize, usize)> = match (
171 matches!(stack.tiers[lo].fabric, TierFabric::Analog(_)),
172 matches!(stack.tiers[hi].fabric, TierFabric::Analog(_)),
173 ) {
174 (true, true) => (0..gr)
175 .flat_map(|r| (0..gc).map(move |c| (r, c)))
176 .map(|(r, c)| (grid_node(lo, r, c), grid_node(hi, r, c)))
177 .collect(),
178 (true, false) => (0..gr)
179 .flat_map(|r| (0..gc).map(move |c| (r, c)))
180 .map(|(r, c)| (grid_node(lo, r, c), base[hi]))
181 .collect(),
182 (false, true) => (0..gr)
183 .flat_map(|r| (0..gc).map(move |c| (r, c)))
184 .map(|(r, c)| (base[lo], grid_node(hi, r, c)))
185 .collect(),
186 (false, false) => vec![(base[lo], base[hi])],
187 };
188 for (a, b) in &pair_nodes {
189 edges.push((*a, *b, g_bond_cell.clone()));
190 }
191 let per_via_g: f64 = gap.first().map(|v| v.g_w_per_k).unwrap_or(0.0);
195 let provisioned: usize = gap.iter().map(|v| v.count).sum();
196 let consumed = placement
197 .vertical_used
198 .get(gi)
199 .copied()
200 .unwrap_or(0)
201 .min(provisioned);
202 if per_via_g > 0.0 && provisioned > 0 {
203 let mut attached = 0usize;
204 for (i, cell_site) in placement.site.iter().enumerate() {
205 for o in super::stack_place::operand_cells_of(&netlist.cells[i]) {
206 let so = placement.site[o];
207 let (t1, t2) = (cell_site.tier.min(so.tier), cell_site.tier.max(so.tier));
208 if t1 <= lo && hi <= t2 && t1 != t2 && attached < consumed {
209 let na = match stack.tiers[lo].fabric {
210 TierFabric::Analog(_) => {
211 let (r, c) = coarse_of(lo, cell_site.row, cell_site.col);
212 grid_node(lo, r, c)
213 }
214 _ => base[lo],
215 };
216 let nb = match stack.tiers[hi].fabric {
217 TierFabric::Analog(_) => {
218 let (r, c) = coarse_of(hi, so.row, so.col);
219 grid_node(hi, r, c)
220 }
221 _ => base[hi],
222 };
223 edges.push((na, nb, rat(per_via_g)?));
224 attached += 1;
225 }
226 }
227 }
228 let dark = provisioned - attached;
229 if dark > 0 {
230 let g_dark_total = rat(per_via_g * dark as f64)?;
231 let share = g_dark_total
232 .div(&Rational::from_i64(pair_nodes.len() as i64))
233 .expect("pair_nodes is non-empty");
234 for (a, b) in &pair_nodes {
235 edges.push((*a, *b, share.clone()));
236 }
237 }
238 }
239 }
240
241 let h_top = film_coefficient_w_m2k(stack.cooling.top);
243 let h_bot = film_coefficient_w_m2k(stack.cooling.bottom);
244 let top = stack.tiers.len() - 1;
245 match stack.tiers[top].fabric {
246 TierFabric::Analog(_) => {
247 let g = rat(h_top * cell_area_m2)?;
248 for r in 0..gr {
249 for c in 0..gc {
250 grounds.push((grid_node(top, r, c), g.clone()));
251 }
252 }
253 }
254 _ => grounds.push((base[top], rat(h_top * cell_area_m2 * (gr * gc) as f64)?)),
255 }
256 match stack.tiers[0].fabric {
257 TierFabric::Analog(_) => {
258 let g = rat(h_bot * cell_area_m2)?;
259 for r in 0..gr {
260 for c in 0..gc {
261 grounds.push((grid_node(0, r, c), g.clone()));
262 }
263 }
264 }
265 _ => grounds.push((base[0], rat(h_bot * cell_area_m2 * (gr * gc) as f64)?)),
266 }
267
268 for (ti, tier) in stack.tiers.iter().enumerate() {
270 if let TierFabric::Digital(d) = &tier.fabric {
271 injections.push((tier_rep_node(ti), rat(d.power_w)?));
272 }
273 }
274 let bias = rat(opts.tile_bias_w)?;
275 for s in &placement.site {
276 if matches!(stack.tiers[s.tier].fabric, TierFabric::Analog(_)) {
277 let (r, c) = coarse_of(s.tier, s.row, s.col);
278 injections.push((grid_node(s.tier, r, c), bias.clone()));
279 }
280 }
281
282 let mut g_mat = vec![vec![Rational::zero(); n]; n];
284 for (a, b, g) in &edges {
285 g_mat[*a][*a] = g_mat[*a][*a].add(g);
286 g_mat[*b][*b] = g_mat[*b][*b].add(g);
287 g_mat[*a][*b] = g_mat[*a][*b].sub(g);
288 g_mat[*b][*a] = g_mat[*b][*a].sub(g);
289 }
290 for (node, g) in &grounds {
291 g_mat[*node][*node] = g_mat[*node][*node].add(g);
292 }
293 let mut p = vec![Rational::zero(); n];
294 for (node, w) in &injections {
295 p[*node] = p[*node].add(w);
296 }
297 let delta_t = exact_solve(&mut g_mat, p).ok_or(AnaLowerError::ThermalModelDisconnected)?;
298
299 let delta_t_c: Vec<f64> = delta_t.iter().map(|t| t.to_f64()).collect();
300 let hottest = (0..n)
301 .max_by(|a, b| delta_t[*a].cmp(&delta_t[*b]))
302 .expect("n > 0 — a stack has tiers");
303 Ok(ThermalMap {
304 claim: ThermalClaim {
305 n,
306 edges,
307 grounds,
308 injections,
309 delta_t,
310 },
311 delta_t_c,
312 node_tier,
313 hottest,
314 })
315}
316
317pub fn cooling_class_name(class: CoolingClass) -> &'static str {
319 match class {
320 CoolingClass::NaturalConvection => "natural-convection",
321 CoolingClass::ForcedAir => "forced-air",
322 CoolingClass::ColdPlate => "cold-plate",
323 }
324}
325
326pub fn check_cooling(map: &ThermalMap, stack: &StackTarget) -> Result<(), AnaLowerError> {
331 let t_junction_c = stack.cooling.ambient_c + map.delta_t_c[map.hottest];
332 if t_junction_c > stack.cooling.t_junction_max_c {
333 return Err(AnaLowerError::ThermalBudgetExceeded {
334 node: map.hottest,
335 tier: map.node_tier[map.hottest],
336 t_junction_c,
337 t_max_c: stack.cooling.t_junction_max_c,
338 cooling: cooling_class_name(stack.cooling.top),
339 });
340 }
341 Ok(())
342}
343
344fn exact_solve(a: &mut [Vec<Rational>], b: Vec<Rational>) -> Option<Vec<Rational>> {
350 let n = b.len();
351 let grid = Rational::from_i64(1_000_000_000_000);
352 let mut m: Vec<Vec<Rational>> = a
353 .iter()
354 .map(|row| row.iter().map(|x| x.mul(&grid)).collect())
355 .collect();
356 let mut rhs: Vec<Rational> = b.iter().map(|x| x.mul(&grid)).collect();
357
358 let mut prev = Rational::one();
359 for k in 0..n {
360 let p = (k..n).find(|r| !m[*r][k].is_zero())?;
361 m.swap(p, k);
362 rhs.swap(p, k);
363 let pivot = m[k][k].clone();
364 for i in (k + 1)..n {
365 for j in (k + 1)..n {
366 let num = m[i][j].mul(&pivot).sub(&m[i][k].mul(&m[k][j]));
367 m[i][j] = num.div(&prev).expect("Bareiss division is exact");
368 }
369 let num = rhs[i].mul(&pivot).sub(&m[i][k].mul(&rhs[k]));
370 rhs[i] = num.div(&prev).expect("Bareiss division is exact");
371 m[i][k] = Rational::zero();
372 }
373 prev = pivot;
374 }
375 let mut x = vec![Rational::zero(); n];
376 for row in (0..n).rev() {
377 let mut acc = rhs[row].clone();
378 for c in (row + 1)..n {
379 acc = acc.sub(&m[row][c].mul(&x[c]));
380 }
381 x[row] = acc.div(&m[row][row])?;
382 }
383 Some(x)
384}