1use super::cell::{ASrc, AnalogCell, AnalogNetlist};
15use super::error_budget::{nonideal_budget_with, OutputBudget};
16use super::gain::{GainSchedule, RangeCertificate};
17use super::nonideal::{leg_operand, separation_sigma, NonIdealityModel, Separation};
18use super::stack::{StackTarget, TierFabric};
19use super::target::{AnaUnplaceable, AnalogTarget};
20use super::AnaLowerError;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct StackSite {
25 pub tier: usize,
26 pub row: usize,
27 pub col: usize,
28}
29
30#[derive(Clone, Debug, PartialEq)]
32pub struct StackPlacement {
33 pub site: Vec<StackSite>,
34 pub vertical_used: Vec<usize>,
36}
37
38impl StackPlacement {
39 pub fn separation(&self, a: usize, b: usize) -> Separation {
41 let (sa, sb) = (self.site[a], self.site[b]);
42 if sa.tier == sb.tier {
43 Separation::SameTier {
44 pitches: (sa.row.abs_diff(sb.row) + sa.col.abs_diff(sb.col)) as f64,
45 }
46 } else {
47 Separation::CrossTier
48 }
49 }
50
51 pub fn wire_cost(&self, netlist: &AnalogNetlist, hop_cost_pitches: f64) -> f64 {
53 let mut cost = 0.0;
54 for (i, cell) in netlist.cells.iter().enumerate() {
55 let mut add = |s: &ASrc| {
56 if let ASrc::Val(v) = s {
57 let (a, b) = (self.site[i], self.site[*v]);
58 cost += (a.row.abs_diff(b.row) + a.col.abs_diff(b.col)) as f64
59 + hop_cost_pitches * a.tier.abs_diff(b.tier) as f64;
60 }
61 };
62 match cell {
63 AnalogCell::Sum { inputs, .. } => inputs.iter().for_each(&mut add),
64 AnalogCell::Mul { a, b } => {
65 add(a);
66 add(b);
67 }
68 AnalogCell::GainConst { a, .. } => add(a),
69 AnalogCell::Select { c, t, f } => {
70 add(c);
71 add(t);
72 add(f);
73 }
74 AnalogCell::Exp { a }
75 | AnalogCell::Log { a }
76 | AnalogCell::Tanh { a }
77 | AnalogCell::Sqrt { a } => add(a),
78 AnalogCell::Div { a, b } => {
79 add(a);
80 add(b);
81 }
82 }
83 }
84 cost
85 }
86}
87
88struct SiteSpace {
89 sites: Vec<StackSite>,
91 gap_budget: Vec<usize>,
93}
94
95fn site_space(stack: &StackTarget) -> SiteSpace {
96 let mut sites = Vec::new();
97 for (ti, tier) in stack.tiers.iter().enumerate() {
98 if let TierFabric::Analog(a) = &tier.fabric {
99 for r in 0..a.rows {
100 for c in 0..a.cols {
101 sites.push(StackSite {
102 tier: ti,
103 row: r,
104 col: c,
105 });
106 }
107 }
108 }
109 }
110 let gap_budget = stack
111 .gaps
112 .iter()
113 .map(|g| g.iter().map(|v| v.count).sum())
114 .collect();
115 SiteSpace { sites, gap_budget }
116}
117
118fn crossed_gaps(a: usize, b: usize) -> std::ops::Range<usize> {
120 a.min(b)..a.max(b)
121}
122
123fn edge_crossings(
124 site: &[StackSite],
125 operands: &[usize],
126 candidate: StackSite,
127 n_gaps: usize,
128) -> Vec<usize> {
129 let mut per_gap = vec![0usize; n_gaps];
130 for o in operands {
131 for g in crossed_gaps(candidate.tier, site[*o].tier) {
132 per_gap[g] += 1;
133 }
134 }
135 per_gap
136}
137
138pub(crate) fn operand_cells_of(cell: &AnalogCell) -> Vec<usize> {
139 operand_cells(cell)
140}
141
142fn operand_cells(cell: &AnalogCell) -> Vec<usize> {
143 let mut out = Vec::new();
144 let mut add = |s: &ASrc| {
145 if let ASrc::Val(v) = s {
146 out.push(*v);
147 }
148 };
149 match cell {
150 AnalogCell::Sum { inputs, .. } => inputs.iter().for_each(&mut add),
151 AnalogCell::Mul { a, b } => {
152 add(a);
153 add(b);
154 }
155 AnalogCell::GainConst { a, .. } => add(a),
156 AnalogCell::Select { c, t, f } => {
157 add(c);
158 add(t);
159 add(f);
160 }
161 AnalogCell::Exp { a }
162 | AnalogCell::Log { a }
163 | AnalogCell::Tanh { a }
164 | AnalogCell::Sqrt { a } => add(a),
165 AnalogCell::Div { a, b } => {
166 add(a);
167 add(b);
168 }
169 }
170 out
171}
172
173pub fn place_stack(
177 netlist: &AnalogNetlist,
178 stack: &StackTarget,
179 hop_cost_pitches: f64,
180) -> Result<StackPlacement, AnaLowerError> {
181 if let Err(e) = stack.validate() {
182 return Err(AnaLowerError::UnloweredOp(format!(
183 "malformed stack: {e:?}"
184 )));
185 }
186 let space = site_space(stack);
187 if netlist.cells.len() > space.sites.len() {
188 return Err(AnaLowerError::Unplaceable(AnaUnplaceable::StackTooSmall {
189 needed_cells: netlist.cells.len(),
190 sites: space.sites.len(),
191 stack: stack.name,
192 }));
193 }
194 let mut used = vec![false; space.sites.len()];
195 let mut vertical_used = vec![0usize; space.gap_budget.len()];
196 let mut site: Vec<StackSite> = Vec::with_capacity(netlist.cells.len());
197 for cell in &netlist.cells {
198 let operands = operand_cells(cell);
199 let mut best: Option<(f64, usize, Vec<usize>)> = None;
200 let mut blocking_gap: Option<usize> = None;
201 for (si, cand) in space.sites.iter().enumerate() {
202 if used[si] {
203 continue;
204 }
205 let need = edge_crossings(&site, &operands, *cand, vertical_used.len());
206 if let Some(g) =
207 (0..need.len()).find(|g| vertical_used[*g] + need[*g] > space.gap_budget[*g])
208 {
209 if blocking_gap.is_none() {
210 blocking_gap = Some(g);
211 }
212 continue;
213 }
214 let d: f64 = operands
215 .iter()
216 .map(|o| {
217 let s = site[*o];
218 (cand.row.abs_diff(s.row) + cand.col.abs_diff(s.col)) as f64
219 + hop_cost_pitches * cand.tier.abs_diff(s.tier) as f64
220 })
221 .sum();
222 if best.as_ref().map(|(bd, _, _)| d < *bd).unwrap_or(true) {
223 best = Some((d, si, need));
224 }
225 }
226 let Some((_, si, need)) = best else {
227 let gap = blocking_gap.unwrap_or(0);
228 return Err(AnaLowerError::Unplaceable(
229 AnaUnplaceable::VerticalResourceExhausted {
230 gap,
231 budget: space.gap_budget.get(gap).copied().unwrap_or(0),
232 },
233 ));
234 };
235 used[si] = true;
236 for (g, n) in need.iter().enumerate() {
237 vertical_used[g] += n;
238 }
239 site.push(space.sites[si]);
240 }
241 Ok(StackPlacement {
242 site,
243 vertical_used,
244 })
245}
246
247pub fn place_stack_blind(
250 netlist: &AnalogNetlist,
251 stack: &StackTarget,
252 _hop_cost_pitches: f64,
253) -> Result<StackPlacement, AnaLowerError> {
254 if let Err(e) = stack.validate() {
255 return Err(AnaLowerError::UnloweredOp(format!(
256 "malformed stack: {e:?}"
257 )));
258 }
259 let space = site_space(stack);
260 if netlist.cells.len() > space.sites.len() {
261 return Err(AnaLowerError::Unplaceable(AnaUnplaceable::StackTooSmall {
262 needed_cells: netlist.cells.len(),
263 sites: space.sites.len(),
264 stack: stack.name,
265 }));
266 }
267 let total = space.sites.len();
268 let stride = (total / 2).max(1) | 1;
269 let mut used = vec![false; total];
270 let mut cursor = 0usize;
271 let mut site = Vec::with_capacity(netlist.cells.len());
272 for _ in &netlist.cells {
273 while used[cursor] {
274 cursor = (cursor + 1) % total;
275 }
276 used[cursor] = true;
277 site.push(space.sites[cursor]);
278 cursor = (cursor + stride) % total;
279 }
280 let mut vertical_used = vec![0usize; space.gap_budget.len()];
281 for (i, cell) in netlist.cells.iter().enumerate() {
282 for o in operand_cells(cell) {
283 for g in crossed_gaps(site[i].tier, site[o].tier) {
284 vertical_used[g] += 1;
285 }
286 }
287 }
288 if let Some(g) = (0..vertical_used.len()).find(|g| vertical_used[*g] > space.gap_budget[*g]) {
289 return Err(AnaLowerError::Unplaceable(
290 AnaUnplaceable::VerticalResourceExhausted {
291 gap: g,
292 budget: space.gap_budget[g],
293 },
294 ));
295 }
296 Ok(StackPlacement {
297 site,
298 vertical_used,
299 })
300}
301
302pub fn error_budget_thermal_stacked(
309 netlist: &AnalogNetlist,
310 cert: &RangeCertificate,
311 schedule: &GainSchedule,
312 target: &AnalogTarget,
313 model: &NonIdealityModel,
314 placement: &StackPlacement,
315 map: &super::thermal::ThermalMap,
316 stack: &StackTarget,
317 opts: &super::thermal::ThermalOpts,
318) -> OutputBudget {
319 use super::error_budget::{bump_add, bump_mul, outward_sum, ErrorSource, ErrorTerm};
320
321 let base = error_budget_nonideal_stacked(netlist, cert, schedule, target, model, placement);
322 let s = f64::powi(2.0, schedule.scale_log2);
323 let q = &schedule.quantized;
324 let bound_of = |src: &ASrc| -> super::gain::MathBound {
325 match src {
326 ASrc::In(k) => cert.input_bounds[*k],
327 ASrc::Val(v) => cert.per_net[*v].math,
328 ASrc::Const(c) => super::gain::MathBound { lo: *c, hi: *c },
329 }
330 };
331 let (gr, gc) = opts.analog_grid;
333 let tile_dt = |cell: usize| -> f64 {
334 let site = placement.site[cell];
335 let TierFabric::Analog(a) = &stack.tiers[site.tier].fabric else {
336 return 0.0;
337 };
338 let mut node = 0usize;
339 for (ti, tier) in stack.tiers.iter().enumerate() {
340 if ti == site.tier {
341 let r = (site.row * gr / a.rows.max(1)).min(gr - 1);
342 let c = (site.col * gc / a.cols.max(1)).min(gc - 1);
343 node += r * gc + c;
344 break;
345 }
346 node += match tier.fabric {
347 TierFabric::Analog(_) => gr * gc,
348 _ => 1,
349 };
350 }
351 map.claim.delta_t[node].to_f64().next_up()
352 };
353
354 let mut gain_d = Vec::with_capacity(q.cells.len());
355 let mut off_d = Vec::with_capacity(q.cells.len());
356 for (i, cell) in q.cells.iter().enumerate() {
357 let dt_i = tile_dt(i);
358 let grad_of = |leg: usize| -> f64 {
359 match leg_operand(&q.cells, i, leg) {
360 Some(v) => (dt_i - tile_dt(v)).abs().next_up(),
361 None => 0.0,
362 }
363 };
364 let g = match cell {
365 AnalogCell::Sum { inputs, weights } => {
366 outward_sum(inputs.iter().zip(weights).enumerate().map(|(l, (x, w))| {
367 bump_mul(
368 model.gain_tempco_per_k * grad_of(l),
369 w.abs() * bound_of(x).max_abs(),
370 )
371 }))
372 }
373 AnalogCell::GainConst { a, k } => bump_mul(
374 model.gain_tempco_per_k * grad_of(0),
375 k.abs() * bound_of(a).max_abs(),
376 ),
377 AnalogCell::Mul { a, b } => bump_mul(
378 model.gain_tempco_per_k * grad_of(0),
379 bound_of(a).max_abs() * bound_of(b).max_abs(),
380 ),
381 AnalogCell::Select { .. } => 0.0,
382 AnalogCell::Exp { a }
383 | AnalogCell::Log { a }
384 | AnalogCell::Tanh { a }
385 | AnalogCell::Sqrt { a } => {
386 bump_mul(model.gain_tempco_per_k * grad_of(0), bound_of(a).max_abs())
387 }
388 AnalogCell::Div { a, b } => bump_mul(
389 model.gain_tempco_per_k * grad_of(0),
390 bound_of(a).max_abs().max(bound_of(b).max_abs()),
391 ),
392 };
393 gain_d.push(g);
394 off_d.push(bump_add(0.0, model.offset_tempco_v_per_k * dt_i / s));
395 }
396
397 let mut terms = base.terms;
398 let tg = super::error_budget::propagate_injected_pub(q, &bound_of, &gain_d);
399 if tg > 0.0 {
400 terms.push(ErrorTerm {
401 eps_math: tg,
402 source: ErrorSource::ThermalGain,
403 });
404 }
405 let to = super::error_budget::propagate_injected_pub(q, &bound_of, &off_d);
406 if to > 0.0 {
407 terms.push(ErrorTerm {
408 eps_math: to,
409 source: ErrorSource::ThermalOffset,
410 });
411 }
412 let eps_math = outward_sum(terms.iter().map(|t| t.eps_math));
413 OutputBudget { eps_math, terms }
414}
415
416pub fn error_budget_nonideal_stacked(
419 netlist: &AnalogNetlist,
420 cert: &RangeCertificate,
421 schedule: &GainSchedule,
422 target: &AnalogTarget,
423 model: &NonIdealityModel,
424 placement: &StackPlacement,
425) -> OutputBudget {
426 let cells = schedule.quantized.cells.clone();
427 let sigma_of = move |i: usize, l: usize| {
428 let sep = leg_operand(&cells, i, l)
429 .map(|v| placement.separation(i, v))
430 .unwrap_or(Separation::SameTier { pitches: 0.0 });
431 separation_sigma(model, sep)
432 };
433 nonideal_budget_with(netlist, cert, schedule, target, model, &sigma_of)
434}