Skip to main content

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}