Skip to main content

logicaffeine_compile/codegen_ana/
stack_place.rs

1//! 3D placement over composed analog tiers — vertical interconnect as a COUNTED, refusable
2//! resource, and the cross-tier Pelgrom category priced into the budget.
3//!
4//! The 2D placer's greedy operand adjacency generalizes: sites now span every analog tier,
5//! the cost of an operand edge is lateral pitches plus `hop_cost_pitches` per crossed gap,
6//! and each crossing CONSUMES one via from each crossed gap's budget — running out is a typed
7//! [`AnaUnplaceable::VerticalResourceExhausted`], never an overcommit. The budget composes
8//! through the SAME engine as 2D (`nonideal_budget_with`), fed the full separation law: a
9//! cross-tier leg pays the flat categorical ceiling
10//! ([`Separation::CrossTier`]), which is how the budget itself teaches the placer to keep
11//! matched clusters in-tier. `place_stack_blind` is the deliberate straw man the anti-vacuity
12//! lock beats three ways (wire, ε, vias).
13
14use 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/// A physical site in the stack: which STACK tier, and where on its array.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct StackSite {
25    pub tier: usize,
26    pub row: usize,
27    pub col: usize,
28}
29
30/// Per-cell site assignment plus the per-gap via ledger.
31#[derive(Clone, Debug, PartialEq)]
32pub struct StackPlacement {
33    pub site: Vec<StackSite>,
34    /// `vertical_used[g]` = crossings consumed from gap `g`'s budget by operand edges.
35    pub vertical_used: Vec<usize>,
36}
37
38impl StackPlacement {
39    /// The Pelgrom-law separation between two placed cells.
40    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    /// Total operand cost: lateral pitches plus `hop_cost_pitches` per crossed gap.
52    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    /// Every analog-tier site, tier-major then row-major — the deterministic scan order.
90    sites: Vec<StackSite>,
91    /// Per-gap via budgets (summed over the gap's classes).
92    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
118/// The gaps an edge between stack tiers `a` and `b` crosses.
119fn 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
173/// Greedy operand-adjacent 3D placement: seat cells in SSA order at the FEASIBLE free site
174/// minimizing lateral-plus-hop cost to the placed operands (tier-major row-major tie-break —
175/// deterministic forever). A cell with no feasible site refuses on the blocking gap.
176pub 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
247/// The deliberate straw man: max-stride scatter over the tier-major site list, adjacency- and
248/// hop-blind (it still refuses rather than overcommit a gap — legality is non-negotiable).
249pub 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
302/// The thermally-coupled stacked budget: the stacked budget PLUS the drift terms priced
303/// against the CERTIFIED thermal map — offset drifts with each tile's absolute rise
304/// (`offset_tempco · ΔT`), gain with the tile-to-operand GRADIENT
305/// (`gain_tempco · |ΔT_cell − ΔT_operand|`) — matching cares about gradients. One Picard
306/// pass, deterministic: placement + power → temperature → ε; hot placement provably costs ε
307/// (`thermal_cooking_lock` measures it). The ℚ→f64 bridge rounds outward.
308pub 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    // Outward ℚ→f64 bridge: a cell's rise is its coarse thermal node's ΔT, rounded UP.
332    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
416/// The stacked physical budget: the SAME composition engine as 2D, fed the full separation
417/// law — cross-tier legs pay the categorical ceiling, so hot scattering provably costs ε.
418pub 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}