Skip to main content

logicaffeine_compile/codegen_ana/
stack.rs

1//! The constructive 2.5D/3D stack ADL.
2//!
3//! A stack is COMPOSED, not monolithic: tiers (each an analog fabric, a digital power-map
4//! descriptor, or a bare interposer) separated by gaps whose vertical interconnect is a
5//! SCARCE, counted resource — TSVs conduct heat as well as signal (`g_w_per_k` is a design
6//! lever the thermal phases measure, not an afterthought). Every physical figure carries a
7//! [`FigureStatus`]: sourced (cited in `benchmarks/silicon/analog/thermal-constants.md`), derived
8//! (arithmetic shown), or loudly modeled — never invented.
9//!
10//! Phase S1 is the SPINE + the degenerate embed: [`StackTarget::single`] wraps today's 2D
11//! analog lane with zero behavioral change ([`analog_artifact_on_stack`] delegates and the
12//! lock proves bit-identity), and [`StackTarget::sky130_stack_v1`] states the V1 stack —
13//! the digital tier DELIBERATELY in the analog tier's heat path, so the thermal phases
14//! (S3–S5) have something honest to refuse. Placement (S2), the certified temperature map
15//! (S3), and cooling refusals (S5) build on this vocabulary without reshaping it.
16
17use super::target::AnalogTarget;
18use super::{analog_artifact_from_source, AnaLowerError, AnalogArtifact};
19
20/// Provenance of a physical figure — the dossier discipline as a type.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum FigureStatus {
23    /// A cited figure; the string names the source (full entry in the dossier).
24    Sourced(&'static str),
25    /// Computed from sourced inputs; the string shows the arithmetic.
26    Derived(&'static str),
27    /// OUR design parameter awaiting characterization; the string says so.
28    Modeled(&'static str),
29}
30
31/// A tier's bulk material for lateral heat spreading.
32#[derive(Clone, Copy, Debug, PartialEq)]
33pub struct Material {
34    pub name: &'static str,
35    /// Bulk thermal conductivity, W/(m·K).
36    pub k_w_per_m_k: f64,
37    pub status: FigureStatus,
38}
39
40impl Material {
41    /// Bulk silicon at 300 K (dossier: Incropera property tables).
42    pub fn silicon() -> Material {
43        Material {
44            name: "silicon",
45            k_w_per_m_k: 148.0,
46            status: FigureStatus::Sourced(
47                "Incropera & DeWitt property tables: silicon ~148 W/(m.K) at 300 K \
48                 (benchmarks/silicon/analog/thermal-constants.md)",
49            ),
50        }
51    }
52
53    /// The organic bond/underfill background class (loudly modeled).
54    pub fn underfill_class() -> Material {
55        Material {
56            name: "underfill-class",
57            k_w_per_m_k: 0.5,
58            status: FigureStatus::Modeled(
59                "V1 bond-layer background conductance class; vendor filled underfills span \
60                 0.3-3 W/(m.K) — OUR declared figure, not a citation",
61            ),
62        }
63    }
64}
65
66/// What computes on a tier.
67#[derive(Clone, Debug, PartialEq)]
68pub enum TierFabric {
69    /// An analog execution plane — the full characterized ADL rides along.
70    Analog(AnalogTarget),
71    /// A digital tier as a POWER DESCRIPTOR (heat source + name), NOT a codegen_sva import —
72    /// the stack cares what it dissipates, the digital lane keeps its own regime.
73    Digital(DigitalTierPower),
74    /// Passive routing/spreading only.
75    Interposer,
76}
77
78/// The digital tier's sourced power injection.
79#[derive(Clone, Debug, PartialEq)]
80pub struct DigitalTierPower {
81    pub design: &'static str,
82    /// Total dissipation, watts.
83    pub power_w: f64,
84    /// Carried VERBATIM from the measurement artifact (`benchmarks/silicon/sky130_power.json`).
85    pub provenance: &'static str,
86}
87
88/// One tier of the stack, bottom-up.
89#[derive(Clone, Debug, PartialEq)]
90pub struct Tier {
91    pub name: &'static str,
92    pub fabric: TierFabric,
93    pub material: Material,
94    pub thickness_um: f64,
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub enum VerticalKind {
99    MicroBump,
100    HybridBond,
101    Tsv,
102}
103
104/// A counted vertical interconnect class in one gap — SCARCE like `mul_tiles`. TSVs conduct
105/// heat: `g_w_per_k` is per-via thermal conductance, a placement-dependent cooling lever.
106#[derive(Clone, Debug, PartialEq)]
107pub struct VerticalClass {
108    pub kind: VerticalKind,
109    pub pitch_um: f64,
110    pub r_ohm: f64,
111    pub c_ff: f64,
112    pub g_w_per_k: f64,
113    /// The scarce budget: how many of this class this gap provides.
114    pub count: usize,
115    pub status: FigureStatus,
116}
117
118impl VerticalClass {
119    /// The V1 copper TSV class (thermal conductance DERIVED from sourced k_Cu; see dossier).
120    pub fn tsv_v1(count: usize) -> VerticalClass {
121        VerticalClass {
122            kind: VerticalKind::Tsv,
123            pitch_um: 40.0,
124            r_ohm: 0.05,
125            c_ff: 40.0,
126            g_w_per_k: 3.15e-4,
127            count,
128            status: FigureStatus::Derived(
129                "g = k_Cu*A/L = 401 * pi*(5e-6)^2 / 100e-6 ~ 3.15e-4 W/K per via (k_Cu SOURCED, \
130                 CRC 401 W/(m.K)); R/C/pitch MODELED (benchmarks/silicon/analog/thermal-constants.md)",
131            ),
132        }
133    }
134
135    /// The V1 microbump class (modeled class figures).
136    pub fn microbump_v1(count: usize) -> VerticalClass {
137        VerticalClass {
138            kind: VerticalKind::MicroBump,
139            pitch_um: 50.0,
140            r_ohm: 0.02,
141            c_ff: 25.0,
142            g_w_per_k: 1.0e-4,
143            count,
144            status: FigureStatus::Modeled(
145                "V1 class figures (benchmarks/silicon/analog/thermal-constants.md)",
146            ),
147        }
148    }
149}
150
151/// How the stack sheds heat at its faces. S1 states the boundary; S5 makes the refusals.
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub enum CoolingClass {
154    NaturalConvection,
155    ForcedAir,
156    ColdPlate,
157}
158
159#[derive(Clone, Copy, Debug, PartialEq)]
160pub struct CoolingBoundary {
161    pub ambient_c: f64,
162    pub top: CoolingClass,
163    pub bottom: CoolingClass,
164    pub t_junction_max_c: f64,
165}
166
167impl CoolingBoundary {
168    /// The V1 default: forced air over the top face, board-side natural convection, the
169    /// commodity 85 °C junction ceiling (modeled envelope, tightened by S5's refusal logic).
170    pub fn forced_air_default() -> CoolingBoundary {
171        CoolingBoundary {
172            ambient_c: 25.0,
173            top: CoolingClass::ForcedAir,
174            bottom: CoolingClass::NaturalConvection,
175            t_junction_max_c: 85.0,
176        }
177    }
178}
179
180/// A shape violation — a stack that cannot mean anything is refused with a name.
181#[derive(Clone, Debug, PartialEq)]
182pub enum StackShapeError {
183    /// `gaps.len()` must be exactly `tiers.len() − 1`.
184    GapCountMismatch {
185        tiers: usize,
186        gaps: usize,
187    },
188    NoTiers,
189    /// A vertical class with `count == 0` is a phantom resource.
190    EmptyVerticalClass {
191        gap: usize,
192    },
193}
194
195/// The composed stack — tiers bottom-up, gap `i` between tier `i` and tier `i+1`.
196#[derive(Clone, Debug, PartialEq)]
197pub struct StackTarget {
198    pub name: &'static str,
199    pub tiers: Vec<Tier>,
200    pub gaps: Vec<Vec<VerticalClass>>,
201    pub cooling: CoolingBoundary,
202}
203
204impl StackTarget {
205    /// The 1-tier degenerate embed: today's 2D analog lane, unchanged — the lock proves the
206    /// compiled artifact is bit-identical to the direct path.
207    pub fn single(t: AnalogTarget) -> StackTarget {
208        StackTarget {
209            name: "single-tier",
210            tiers: vec![Tier {
211                name: "analog",
212                fabric: TierFabric::Analog(t),
213                material: Material::silicon(),
214                thickness_um: 300.0,
215            }],
216            gaps: vec![],
217            cooling: CoolingBoundary::forced_air_default(),
218        }
219    }
220
221    /// The V1 stack: the sourced-power digital tier BELOW the analog tier — deliberately in
222    /// its heat path, so the thermal phases have something honest to measure and refuse.
223    pub fn sky130_stack_v1() -> StackTarget {
224        StackTarget {
225            name: "sky130-stack-v1",
226            tiers: vec![
227                Tier {
228                    name: "digital",
229                    fabric: TierFabric::Digital(DigitalTierPower {
230                        design: "soft_core_scalar",
231                        power_w: 0.0295,
232                        provenance: "OpenLane Sky130 signoff, OpenSTA report_power on the post-route RC-extracted netlist the \
233                                     ledger ships \
234                                     (tools/OpenLane/designs/logos_soft_core_scalar/runs/RUN_2026.10.15_17.14.37, \
235                                     31492840-byte GDS) at its 15 ns signoff clock, typical corner. Switching activity is \
236                                     OpenSTA's propagated default (no workload VCD) — a signoff EDA power number, not a \
237                                     bench-measured one.",
238                    }),
239                    material: Material::silicon(),
240                    thickness_um: 300.0,
241                },
242                Tier {
243                    name: "analog",
244                    fabric: TierFabric::Analog(AnalogTarget::sky130_fpaa()),
245                    material: Material::silicon(),
246                    thickness_um: 100.0,
247                },
248            ],
249            gaps: vec![vec![VerticalClass::tsv_v1(64), VerticalClass::microbump_v1(256)]],
250            cooling: CoolingBoundary::forced_air_default(),
251        }
252    }
253
254    /// Structural coherence — refused with a name, never a meaningless stack.
255    pub fn validate(&self) -> Result<(), StackShapeError> {
256        if self.tiers.is_empty() {
257            return Err(StackShapeError::NoTiers);
258        }
259        if self.gaps.len() + 1 != self.tiers.len() {
260            return Err(StackShapeError::GapCountMismatch {
261                tiers: self.tiers.len(),
262                gaps: self.gaps.len(),
263            });
264        }
265        for (i, gap) in self.gaps.iter().enumerate() {
266            if gap.iter().any(|v| v.count == 0) {
267                return Err(StackShapeError::EmptyVerticalClass { gap: i });
268            }
269        }
270        Ok(())
271    }
272
273    /// The stack's FIRST analog tier, if any (the region-compilation anchor; placement spans
274    /// all analog tiers via `stack_place`).
275    pub fn analog_tier(&self) -> Option<(usize, &AnalogTarget)> {
276        self.tiers
277            .iter()
278            .enumerate()
279            .find_map(|(i, t)| match &t.fabric {
280                TierFabric::Analog(a) => Some((i, a)),
281                _ => None,
282            })
283    }
284}
285
286/// Compile an analog region ONTO a stack: resolve the analog tier and delegate to the 2D
287/// lane — the degenerate embed is bit-identical by construction (locked, not assumed).
288pub fn analog_artifact_on_stack(
289    src: &str,
290    fn_name: &str,
291    stack: &StackTarget,
292) -> Result<AnalogArtifact, AnaLowerError> {
293    if let Err(e) = stack.validate() {
294        return Err(AnaLowerError::UnloweredOp(format!(
295            "malformed stack: {e:?}"
296        )));
297    }
298    let Some((_, target)) = stack.analog_tier() else {
299        return Err(AnaLowerError::Unsupported("stack has no analog tier"));
300    };
301    analog_artifact_from_source(src, fn_name, target)
302}