logicaffeine_compile/codegen_ana/target.rs
1//! The `AnalogTarget` ADL — the analog lane's described-fabric spine, shaped as a
2//! **characterized analog PDK for the compiler**, not a feature checklist.
3//!
4//! Mirrors the *pattern* of [`crate::codegen_sva::fabric_target::FabricTarget`] (capability
5//! gating + typed refusal + cited presets) without sharing its lattice: the digital fabric's
6//! vocabulary is exact ≡VM; this one is tolerance-≡ by physics. Two regimes, two ADLs.
7//!
8//! ## The physical model the descriptor encodes
9//! Two physical tile classes realize the whole compute vocabulary:
10//! - the **linear tile** — programmable-transconductance elements into a current-summing node
11//! (`Iᵢ = gmᵢ·Vᵢ`, Kirchhoff sums the currents): realizes `Sum`, and `GainConst` as the
12//! one-input degenerate case (NOT a separate physical macro);
13//! - the **multiplier tile** — a differential four-quadrant (Gilbert/translinear) cell
14//! normalizing by a programmed reference (`Vout = Va·Vb/Vref`): scarce, like FPGA DSP blocks.
15//!
16//! Every net is DIFFERENTIAL (`Vsignal = V⁺ − V⁻` around `Vcm ≈ mid-rail`): the math-domain
17//! signed span `±half_span` is the differential swing, signed weights are a wire swap, and
18//! common-mode noise cancels. Fan-in is a physical resource (summing-node loading, settling,
19//! noise), so `sum_fanin` is a hard per-tile limit the compiler trees around. Weights are
20//! digitally programmed from matched unit elements (`weight_bits` sign+magnitude), so a weight
21//! is QUANTIZED and its snap error is an itemized error-budget term. The multiplier's `Vref`
22//! comes from a DISCRETE realizable set — rail safety does NOT imply reference realizability,
23//! and the gain schedule must land the block scale on a member or refuse.
24//!
25//! Provenance discipline: a preset may carry exactly two kinds of numbers — a **sourced
26//! physical figure** (cited in the doc block AND in [`AnalogTarget::provenance`]) or a
27//! **modeled-design parameter** of OUR fabric. An invented physical constant is neither, and
28//! ships never. The characterization ladder that ultimately fills these fields is
29//! schematic-SPICE → PVT → Monte-Carlo mismatch → layout extraction → silicon measurement.
30
31use std::collections::BTreeSet;
32
33use logicaffeine_base::si::{Hertz, Nanoseconds, Volts};
34
35/// An analog tile kind the fabric can realize.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
37pub enum AnalogCellKind {
38 /// Kirchhoff current-summing node with programmed input-conductance weights (linear tile).
39 Sum,
40 /// Four-quadrant transconductance (Gilbert-style) multiplier for variable×variable.
41 Mul,
42 /// Programmed-conductance constant gain — the ONE-INPUT linear tile (same physical macro
43 /// as [`AnalogCellKind::Sum`]).
44 GainConst,
45 /// OTA-C integrator: `dv/dt = k·a`, `k = gm/C`.
46 Integrate,
47 /// Continuous comparator — the analog→digital decision boundary.
48 Compare,
49 /// Comparator-driven analog switch (`if c < 0 then t else f`) — the O5 data-dependent
50 /// tile: the comparator half decides, the transmission-gate half passes the voltage.
51 Select,
52 /// Translinear exponential (subthreshold current law) — O6.
53 Exp,
54 /// Translinear logarithm (forward-diode law) — O6.
55 Log,
56 /// Differential-pair saturation — O6.
57 Tanh,
58 /// Translinear square root (Gilbert loop) — O6.
59 Sqrt,
60 /// Translinear ratio — O6 (the divisor's range must clear zero).
61 Div,
62 /// Sample-and-hold, strobed by the control plane (the ZOH boundary between ticks).
63 SampleHold,
64 /// Plane boundary in: digital word → differential voltage (DAC).
65 DacIn,
66 /// Plane boundary out: differential voltage → digital word (ADC).
67 AdcOut,
68}
69
70/// Every analog tile kind — the cell universe a superset target provides.
71pub const ALL_ANALOG_CELL_KINDS: &[AnalogCellKind] = &[
72 AnalogCellKind::Sum,
73 AnalogCellKind::Mul,
74 AnalogCellKind::GainConst,
75 AnalogCellKind::Integrate,
76 AnalogCellKind::Compare,
77 AnalogCellKind::Select,
78 AnalogCellKind::Exp,
79 AnalogCellKind::Log,
80 AnalogCellKind::Tanh,
81 AnalogCellKind::Sqrt,
82 AnalogCellKind::Div,
83 AnalogCellKind::SampleHold,
84 AnalogCellKind::DacIn,
85 AnalogCellKind::AdcOut,
86];
87
88/// A discrete capability a lowering demands of an analog target — the unit of typed refusal.
89/// Continuous demands (gain ratios, integrator rates, multiplier references) refuse through
90/// their own [`AnaUnplaceable`] variants, which carry the offending value.
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub enum AnalogCapability {
93 /// A tile kind the netlist needs.
94 Cell(AnalogCellKind),
95 /// At least this many DAC input channels.
96 DacChannels(usize),
97 /// At least this many ADC output channels.
98 AdcChannels(usize),
99 /// At least this many multiplier tiles (the scarce resource).
100 MulTiles(usize),
101}
102
103/// Why a netlist cannot be realized on a given analog target — a TYPED refusal, never a
104/// silently-wrong circuit.
105#[derive(Clone, Debug, PartialEq)]
106pub enum AnaUnplaceable {
107 /// The target lacks a discrete capability the netlist needs.
108 MissingCapability {
109 needed: AnalogCapability,
110 target: &'static str,
111 },
112 /// The netlist needs more tiles than the described array carries.
113 TooSmall {
114 needed_cells: usize,
115 target_rows: usize,
116 target_cols: usize,
117 target: &'static str,
118 },
119 /// A required programmed weight/gain ratio falls outside the target's realizable range.
120 GainRatioUnrealizable {
121 needed: f64,
122 lo: f64,
123 hi: f64,
124 target: &'static str,
125 },
126 /// A required integrator rate (`gm/C`, in 1/s) falls outside the target's realizable range.
127 IntegratorRateUnrealizable {
128 needed_per_s: f64,
129 lo: f64,
130 hi: f64,
131 target: &'static str,
132 },
133 /// The block scale a `Mul`-bearing graph needs is not a realizable multiplier reference —
134 /// rail safety does not imply `Vref` realizability.
135 MulReferenceUnrealizable {
136 needed_v: f64,
137 options_v: Vec<f64>,
138 target: &'static str,
139 },
140 /// A summing tile demands more fan-in than the physical summing node carries (fan-in is a
141 /// real resource: node loading, settling, noise) — tree the sum instead.
142 SumFaninExceeded {
143 needed: usize,
144 max: usize,
145 target: &'static str,
146 },
147 /// A net's declared range needs more dynamic range (rail span over noise floor, in dB)
148 /// than the target provides.
149 DynamicRangeExceeded {
150 net: usize,
151 needed_db: f64,
152 available_db: f64,
153 target: &'static str,
154 },
155 /// A translinear ratio whose divisor's certified box crosses (or touches) zero — the
156 /// loop starves; no output box exists.
157 DivisorRangeSpansZero { net: usize, lo: f64, hi: f64 },
158 /// A stack's analog tiers together carry fewer sites than the netlist needs.
159 StackTooSmall {
160 needed_cells: usize,
161 sites: usize,
162 stack: &'static str,
163 },
164 /// A gap's counted vertical interconnect budget cannot carry the crossings a legal
165 /// placement needs — vias are SCARCE, and running out is a refusal, not an overcommit.
166 VerticalResourceExhausted { gap: usize, budget: usize },
167}
168
169impl std::fmt::Display for AnaUnplaceable {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 match self {
172 AnaUnplaceable::MissingCapability { needed, target } => {
173 write!(f, "analog target `{target}` lacks capability {needed:?}")
174 }
175 AnaUnplaceable::TooSmall {
176 needed_cells,
177 target_rows,
178 target_cols,
179 target,
180 } => {
181 write!(f, "analog target `{target}` is too small: needs {needed_cells} cells, array is {target_rows}x{target_cols}")
182 }
183 AnaUnplaceable::DivisorRangeSpansZero { net, lo, hi } => {
184 write!(f, "translinear divisor on net {net} spans zero: certified box [{lo}, {hi}] — the loop starves")
185 }
186 AnaUnplaceable::StackTooSmall {
187 needed_cells,
188 sites,
189 stack,
190 } => {
191 write!(f, "stack `{stack}` is too small: needs {needed_cells} cells, analog tiers carry {sites} sites")
192 }
193 AnaUnplaceable::VerticalResourceExhausted { gap, budget } => {
194 write!(f, "gap {gap}'s vertical interconnect budget ({budget} vias) cannot carry the required crossings")
195 }
196 AnaUnplaceable::GainRatioUnrealizable {
197 needed,
198 lo,
199 hi,
200 target,
201 } => {
202 write!(f, "analog target `{target}` cannot realize gain ratio {needed}: realizable range is [{lo}, {hi}]")
203 }
204 AnaUnplaceable::IntegratorRateUnrealizable {
205 needed_per_s,
206 lo,
207 hi,
208 target,
209 } => {
210 write!(f, "analog target `{target}` cannot realize integrator rate {needed_per_s} /s: realizable range is [{lo}, {hi}] /s")
211 }
212 AnaUnplaceable::MulReferenceUnrealizable {
213 needed_v,
214 options_v,
215 target,
216 } => {
217 write!(f, "analog target `{target}` cannot realize multiplier reference {needed_v} V: options are {options_v:?} V")
218 }
219 AnaUnplaceable::SumFaninExceeded {
220 needed,
221 max,
222 target,
223 } => {
224 write!(f, "analog target `{target}` summing fan-in exceeded: needs {needed}, tile carries {max}")
225 }
226 AnaUnplaceable::DynamicRangeExceeded {
227 net,
228 needed_db,
229 available_db,
230 target,
231 } => {
232 write!(f, "analog target `{target}` dynamic range exceeded on net {net}: needs {needed_db} dB, {available_db} dB available")
233 }
234 }
235 }
236}
237
238/// A described analog fabric. One lowering retargets across shapes by varying this descriptor;
239/// each demand is either realizable on it or refused with a typed [`AnaUnplaceable`].
240#[derive(Clone, Debug, PartialEq)]
241pub struct AnalogTarget {
242 /// Human name (carried into refusal errors).
243 pub name: &'static str,
244 /// Described tile-array dimensions (physical tile SITES; the linear/multiplier mix below
245 /// says what populates them).
246 pub rows: usize,
247 pub cols: usize,
248 /// Multiplier tiles among the sites — the scarce resource (DSP-block discipline).
249 pub mul_tiles: usize,
250 /// Supply rails in volts, `(low, high)`. Signals are differential around mid-rail; the
251 /// usable signed span is `±headroom·(high−low)/2`.
252 pub rails_v: (Volts, Volts),
253 /// Usable fraction of the differential half-span (clipping guard band).
254 pub headroom: f64,
255 /// The tile kinds this target provides.
256 pub cells: BTreeSet<AnalogCellKind>,
257 /// Hard per-tile summing fan-in (a physical resource: node loading, settling, noise). The
258 /// compiler trees larger sums.
259 pub sum_fanin: usize,
260 /// Weight resolution: sign + magnitude from matched unit elements. A programmed weight is
261 /// quantized to `weight_lsb` steps; the snap error is a budget term.
262 pub weight_bits: u8,
263 /// Realizable programmed weight magnitudes, `(min, max)` — the unit-element range.
264 pub weight_range: (f64, f64),
265 /// The DISCRETE realizable multiplier normalization references in volts. A `Mul`-bearing
266 /// graph's block scale must land on a member (or refuse) — see the module doc.
267 pub mul_vref_options_v: Vec<Volts>,
268 /// The block-scale exponent range the fabric's references/biasing support
269 /// (`volts-per-math-unit = 2^e`, `e ∈ [min, max]`).
270 pub scale_log2_range: (i32, i32),
271 /// DAC / ADC converter resolutions in bits.
272 pub dac_bits: u8,
273 pub adc_bits: u8,
274 /// DAC input / ADC output channel counts.
275 pub n_dac: usize,
276 pub n_adc: usize,
277 /// Realizable integrator rates `gm/C` in 1/s, `(min, max)`.
278 pub integrator_k_range: (Hertz, Hertz),
279 /// Per-tick DC settling budget in nanoseconds (linear tile; the multiplier and longer
280 /// routing legs get their own figures with the characterization phases).
281 pub settle_ns: Nanoseconds,
282 /// Sourcing statement: which figures are cited physical facts and which are modeled-design
283 /// parameters. Pinned verbatim by `analog_target_lock` — changing sourcing is a conscious edit.
284 pub provenance: &'static str,
285}
286
287impl AnalogTarget {
288 /// IDEAL — the abstract superset preset: every tile kind, generous converters and ranges,
289 /// no non-ideality. Even the ideal preset keeps the PHYSICAL DISCIPLINES finite (fan-in,
290 /// quantized weights, discrete references) so every certification exercises the real
291 /// compiler obligations. Makes NO physical claims.
292 pub fn ideal() -> AnalogTarget {
293 AnalogTarget {
294 name: "ideal",
295 rows: 16,
296 cols: 16,
297 mul_tiles: 64,
298 rails_v: (Volts(0.0), Volts(1.8)),
299 headroom: 0.8,
300 cells: ALL_ANALOG_CELL_KINDS.iter().copied().collect(),
301 sum_fanin: 8,
302 weight_bits: 12,
303 weight_range: (1.0 / 4096.0, 4.0),
304 // Powers of two down from the half-span decade — generous but DISCRETE.
305 mul_vref_options_v: [
306 0.5,
307 0.25,
308 0.125,
309 0.0625,
310 0.03125,
311 0.015625,
312 0.0078125,
313 0.00390625,
314 0.001953125,
315 0.0009765625,
316 ]
317 .map(Volts)
318 .to_vec(),
319 scale_log2_range: (-20, 0),
320 dac_bits: 12,
321 adc_bits: 12,
322 n_dac: 16,
323 n_adc: 16,
324 integrator_k_range: (Hertz(1.0), Hertz(1.0e9)),
325 settle_ns: Nanoseconds(100.0),
326 provenance: "abstract ideal preset — design parameters only, no physical claims",
327 }
328 }
329
330 /// SKY130-FPAA — a modeled FPAA on the open SkyWater SKY130 process, in the deliberately
331 /// boring V1 shape: a 16×16 site array with a scarce multiplier population, differential
332 /// linear tiles with 8-bit unit-element weights and fan-in 8, modest converters, and ~8
333 /// effective bits as the accuracy ambition.
334 ///
335 /// The rail is the ONE sourced physical figure: SKY130's nominal digital-domain supply is
336 /// **1.8 V** (SkyWater SKY130 PDK, sky130_fd_pr device models; skywater-pdk.readthedocs.io).
337 /// Everything else is OUR modeled fabric's design parameters — design choices, not silicon
338 /// measurements. The non-ideality physics (mismatch sigmas, parasitics, noise floor) ships
339 /// in its own phase with cited constants; until then this preset carries none rather than
340 /// invented ones.
341 pub fn sky130_fpaa() -> AnalogTarget {
342 AnalogTarget {
343 name: "sky130-fpaa",
344 rows: 16,
345 cols: 16,
346 mul_tiles: 36,
347 rails_v: (Volts(0.0), Volts(1.8)),
348 headroom: 0.8,
349 cells: ALL_ANALOG_CELL_KINDS.iter().copied().collect(),
350 sum_fanin: 8,
351 weight_bits: 8,
352 weight_range: (1.0 / 64.0, 127.0 / 64.0),
353 mul_vref_options_v: [0.5, 0.25, 0.125].map(Volts).to_vec(),
354 scale_log2_range: (-8, 0),
355 dac_bits: 8,
356 adc_bits: 8,
357 n_dac: 8,
358 n_adc: 2,
359 integrator_k_range: (Hertz(1.0e3), Hertz(1.0e7)),
360 settle_ns: Nanoseconds(1000.0),
361 provenance: "rails: SkyWater SKY130 PDK nominal digital supply 1.8 V (sky130_fd_pr; \
362 skywater-pdk.readthedocs.io). All other figures are modeled-FPAA design \
363 parameters, not silicon measurements.",
364 }
365 }
366
367 /// The usable differential half-span in volts (`headroom · (rail_hi − rail_lo) / 2`).
368 pub fn half_span_v(&self) -> f64 {
369 self.headroom * (self.rails_v.1 .0 - self.rails_v.0 .0) / 2.0
370 }
371
372 /// The weight quantum: the smallest programmed-weight step the unit elements realize.
373 /// Sign+magnitude with `weight_bits` total ⇒ `2^(weight_bits−1) − 1` magnitude steps over
374 /// `weight_range.1`.
375 pub fn weight_lsb(&self) -> f64 {
376 let steps = (1u64 << (self.weight_bits - 1)) - 1;
377 self.weight_range.1 / steps as f64
378 }
379
380 /// Whether this target provides a discrete capability.
381 pub fn provides(&self, cap: AnalogCapability) -> bool {
382 match cap {
383 AnalogCapability::Cell(kind) => self.cells.contains(&kind),
384 AnalogCapability::DacChannels(n) => self.n_dac >= n,
385 AnalogCapability::AdcChannels(n) => self.n_adc >= n,
386 AnalogCapability::MulTiles(n) => self.mul_tiles >= n,
387 }
388 }
389
390 /// Gate a required programmed weight/gain magnitude against the realizable range.
391 pub fn require_gain_ratio(&self, needed: f64) -> Result<(), AnaUnplaceable> {
392 let (lo, hi) = self.weight_range;
393 if needed >= lo && needed <= hi {
394 Ok(())
395 } else {
396 Err(AnaUnplaceable::GainRatioUnrealizable {
397 needed,
398 lo,
399 hi,
400 target: self.name,
401 })
402 }
403 }
404
405 /// Gate a required integrator rate (`gm/C`, 1/s) against the realizable range.
406 pub fn require_integrator_rate(&self, needed_per_s: f64) -> Result<(), AnaUnplaceable> {
407 let (Hertz(lo), Hertz(hi)) = self.integrator_k_range;
408 if needed_per_s >= lo && needed_per_s <= hi {
409 Ok(())
410 } else {
411 Err(AnaUnplaceable::IntegratorRateUnrealizable {
412 needed_per_s,
413 lo,
414 hi,
415 target: self.name,
416 })
417 }
418 }
419
420 /// Gate a required multiplier reference voltage against the discrete realizable set
421 /// (exact match — the schedule chooses FROM this set, it does not approximate into it).
422 pub fn require_mul_reference(&self, needed_v: f64) -> Result<(), AnaUnplaceable> {
423 if self.mul_vref_options_v.iter().any(|v| v.0 == needed_v) {
424 Ok(())
425 } else {
426 Err(AnaUnplaceable::MulReferenceUnrealizable {
427 needed_v,
428 options_v: self.mul_vref_options_v.iter().map(|v| v.0).collect(),
429 target: self.name,
430 })
431 }
432 }
433}