Skip to main content

logicaffeine_compile/codegen_ana/
effects.rs

1//! The physics-effect provenance grid.
2//!
3//! Every [`Element`] carries an [`EffectRow`]: a FIXED-SIZE array with one [`EffectSlot`] per
4//! [`PhysicsEffect`], so a row physically cannot omit an effect — the pit of success is the
5//! type. A slot is `Sourced` (citation anchored in `benchmarks/silicon/analog/sourced-constants.md`),
6//! `Derived` (arithmetic shown), `Modeled` (a loud design parameter awaiting the
7//! characterization ladder), `Ideal` (a physics justification for why the effect is absent),
8//! or `Unknown` — and `Unknown` is legal, ledgered, and ceiling-bounded in
9//! `analog_effect_grid_lock`, because an honest gap beats an invented figure forever.
10//!
11//! Two coherence theorems the lock enforces:
12//! - **No orphan physics, no phantom reference**: every [`NonIdealityModel`](crate::codegen_ana::nonideal::NonIdealityModel) field appears in
13//!   exactly one slot's `params` across the whole grid.
14//! - **No budget term outside the taxonomy**: [`effect_of`] totally classifies every
15//!   [`ErrorSource`] as either aggregated physics effects or explicit `Analysis` bookkeeping
16//!   (the bound's own arithmetic — `MulInteraction`, `EvalRounding` — is not a die mechanism,
17//!   and saying so is part of the honesty).
18//!
19//! [`NonIdealityModel`]: crate::codegen_ana::nonideal::NonIdealityModel
20
21use super::error_budget::ErrorSource;
22use super::ontology::Element;
23use super::ontology::{FunctionalMacro, Passive, Semiconductor, SourceElement};
24
25/// Every physical effect the grid classifies, one slot each. Declaration order IS the row
26/// index (`ALL_EFFECTS[i] as usize == i`, pinned by the lock).
27#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
28pub enum PhysicsEffect {
29    // Noise:
30    NoiseThermal,
31    NoiseFlicker,
32    NoiseShot,
33    NoiseBurst,
34    // Mismatch (Pelgrom):
35    MismatchArea,
36    MismatchDistance,
37    // Drift and aging:
38    DriftTemperature,
39    AgingNbti,
40    AgingHci,
41    DriftPackageStress,
42    // Parasitics:
43    ParasiticResistance,
44    ParasiticCapacitance,
45    ParasiticInductance,
46    // Coupling:
47    CouplingCapacitive,
48    CouplingSubstrate,
49    CouplingSupply,
50    CouplingElectromagnetic,
51    CouplingThermal,
52    // Converters and representation:
53    ConverterInl,
54    ConverterDnl,
55    Quantization,
56    // Dynamics:
57    SettlingBandwidth,
58}
59
60pub const EFFECT_COUNT: usize = 22;
61
62/// The effect census, in declaration (= index) order.
63pub const ALL_EFFECTS: &[PhysicsEffect; EFFECT_COUNT] = &[
64    PhysicsEffect::NoiseThermal,
65    PhysicsEffect::NoiseFlicker,
66    PhysicsEffect::NoiseShot,
67    PhysicsEffect::NoiseBurst,
68    PhysicsEffect::MismatchArea,
69    PhysicsEffect::MismatchDistance,
70    PhysicsEffect::DriftTemperature,
71    PhysicsEffect::AgingNbti,
72    PhysicsEffect::AgingHci,
73    PhysicsEffect::DriftPackageStress,
74    PhysicsEffect::ParasiticResistance,
75    PhysicsEffect::ParasiticCapacitance,
76    PhysicsEffect::ParasiticInductance,
77    PhysicsEffect::CouplingCapacitive,
78    PhysicsEffect::CouplingSubstrate,
79    PhysicsEffect::CouplingSupply,
80    PhysicsEffect::CouplingElectromagnetic,
81    PhysicsEffect::CouplingThermal,
82    PhysicsEffect::ConverterInl,
83    PhysicsEffect::ConverterDnl,
84    PhysicsEffect::Quantization,
85    PhysicsEffect::SettlingBandwidth,
86];
87
88/// One provenance-classified cell of the grid. `params` names the [`NonIdealityModel`](crate::codegen_ana::nonideal::NonIdealityModel) fields
89/// this slot is the census home of (each field lives in exactly one slot, grid-wide); a slot
90/// mixing provenances takes the WEAKEST of its params' statuses.
91///
92/// [`NonIdealityModel`]: crate::codegen_ana::nonideal::NonIdealityModel
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum EffectSlot {
95    /// A published figure. The citation's first word anchors in the dossier file.
96    Sourced {
97        params: &'static [&'static str],
98        citation: &'static str,
99    },
100    /// Computed from sourced inputs — the arithmetic is stated.
101    Derived {
102        params: &'static [&'static str],
103        arithmetic: &'static str,
104    },
105    /// A design parameter of our fabric, awaiting the characterization ladder. Loud, never
106    /// presented as silicon.
107    Modeled {
108        params: &'static [&'static str],
109        note: &'static str,
110    },
111    /// The effect is genuinely absent for this element — with the physics reason.
112    Ideal { justification: &'static str },
113    /// Recognized, unclassified. Legal, ledgered, ceiling-bounded.
114    Unknown,
115}
116
117/// A total row: one slot per effect, by construction.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct EffectRow(pub [EffectSlot; EFFECT_COUNT]);
120
121impl EffectRow {
122    pub const fn unknown() -> Self {
123        EffectRow([EffectSlot::Unknown; EFFECT_COUNT])
124    }
125
126    /// Classify one slot. Double classification is a bug, not an override.
127    pub fn with(mut self, e: PhysicsEffect, s: EffectSlot) -> Self {
128        assert!(
129            matches!(self.0[e as usize], EffectSlot::Unknown),
130            "{e:?} classified twice in one row"
131        );
132        self.0[e as usize] = s;
133        self
134    }
135
136    pub fn slot(&self, e: PhysicsEffect) -> EffectSlot {
137        self.0[e as usize]
138    }
139
140    /// Non-`Unknown` slot count — the grow-only per-element ledger quantity.
141    pub fn classified(&self) -> usize {
142        self.0
143            .iter()
144            .filter(|s| !matches!(s, EffectSlot::Unknown))
145            .count()
146    }
147}
148
149/// THE grid — exhaustive over every element, total over every effect. Elements not yet
150/// studied return the all-`Unknown` row (honest, ledgered); classified rows carry the
151/// dossier's provenance. The [`NonIdealityModel`](crate::codegen_ana::nonideal::NonIdealityModel) fields live in the linear tile's row —
152/// the model is tile-generic today and that row is its characterization home.
153///
154/// [`NonIdealityModel`]: crate::codegen_ana::nonideal::NonIdealityModel
155pub fn effect_row(e: Element) -> EffectRow {
156    use EffectSlot::{Derived, Ideal, Modeled, Sourced};
157    use PhysicsEffect as Fx;
158    match e {
159        Element::Passive(Passive::Resistor) => EffectRow::unknown()
160            .with(
161                Fx::NoiseThermal,
162                Sourced {
163                    params: &[],
164                    citation: "Johnson 1928 / Nyquist 1928: 4kTR thermal noise of any \
165                               dissipative element — the law behind the dossier's kT/C entry",
166                },
167            )
168            .with(
169                Fx::NoiseShot,
170                Ideal {
171                    justification: "no potential barrier: carrier flow in a homogeneous \
172                                    resistive film is not discrete-injection limited, so a \
173                                    resistor exhibits Johnson noise only",
174                },
175            ),
176        Element::Passive(Passive::Capacitor) => EffectRow::unknown()
177            .with(
178                Fx::NoiseThermal,
179                Ideal {
180                    justification: "a lossless reactance dissipates nothing and generates no \
181                                    Johnson noise; the sampled kT/C variance is the switch \
182                                    resistance's noise, booked on the sampling tile",
183                },
184            )
185            .with(
186                Fx::NoiseFlicker,
187                Ideal {
188                    justification: "1/f noise requires carrier trapping in an active \
189                                    conduction path; an ideal dielectric conducts no signal \
190                                    carriers",
191                },
192            )
193            .with(
194                Fx::NoiseShot,
195                Ideal {
196                    justification: "no barrier and no DC conduction current to be \
197                                    shot-noise limited",
198                },
199            ),
200        Element::Semiconductor(Semiconductor::FloatingGate) => EffectRow::unknown().with(
201            Fx::MismatchArea,
202            Sourced {
203                params: &[],
204                citation: "Hasler Proc. IEEE 108(8) 2020: programmed analog parameters at \
205                           better than 1% accuracy over six decades — the programming-accuracy \
206                           class of the fabric's weights",
207            },
208        ),
209        Element::Macro(FunctionalMacro::WinnerTakeAll) => EffectRow::unknown().with(
210            Fx::MismatchArea,
211            Modeled {
212                params: &[],
213                note: "competitor input-referred offsets decide near-ties — the O7 margin \
214                       certificate's resolution band (2*k*sigma) prices exactly this class",
215            },
216        ),
217        Element::Source(SourceElement::Vccs) => EffectRow::unknown().with(
218            Fx::MismatchArea,
219            Modeled {
220                params: &[],
221                note: "gm mismatch of the transconductor is the physical origin of the linear \
222                       tile's gain-error class — the figure is booked on that tile's row",
223            },
224        ),
225        Element::Macro(FunctionalMacro::LinearCombTile) => EffectRow::unknown()
226            .with(
227                Fx::MismatchArea,
228                Derived {
229                    params: &["weight_gain_sigma", "offset_sigma_v", "chop_residual_v"],
230                    arithmetic: "gain: Hasler 1% programming envelope taken as the 1-sigma \
231                                 class (SOURCED, conservative round-up); offset: sigma(V_os) \
232                                 ~ 2.8 mV from the SkyWater vth0_slope mismatch model on a \
233                                 stated 5T example; chop residual: MODELED intra-pair drift \
234                                 class — the slot takes the weakest param status",
235                },
236            )
237            .with(
238                Fx::MismatchDistance,
239                Modeled {
240                    params: &["gm_distance_sigma_per_pitch", "cross_tier_gain_sigma"],
241                    note: "the Pelgrom 1989 distance term exists; no published sky130 \
242                           coefficient — 0.2%/pitch parameterizes the placer until the \
243                           characterization ladder replaces it. Across separately \
244                           manufactured tiers the gradient gives way to a flat 2% categorical \
245                           ceiling (dies share no Pelgrom field) — equally MODELED",
246                },
247            )
248            .with(
249                Fx::NoiseThermal,
250                Derived {
251                    params: &["noise_floor_v"],
252                    arithmetic: "kT/C at C = 1 pF, T = 300 K: sqrt(4.142e-21/1e-12) = 64.4 uV \
253                                 rms (Johnson-Nyquist; arithmetic in the dossier)",
254                },
255            )
256            .with(
257                Fx::DriftTemperature,
258                Modeled {
259                    params: &["gain_tempco_per_k", "offset_tempco_v_per_k"],
260                    note: "drift-class design parameters priced against the CERTIFIED thermal \
261                           map (S3): gain drifts with the tile-to-operand GRADIENT, offset \
262                           with the absolute rise — awaiting characterization",
263                },
264            )
265            .with(
266                Fx::Quantization,
267                Derived {
268                    params: &[],
269                    arithmetic: "programmed weights snap to the unit-element grid — the \
270                                 WeightQuant budget term; half-grid arithmetic in gain.rs",
271                },
272            )
273            .with(
274                Fx::SettlingBandwidth,
275                Modeled {
276                    params: &[],
277                    note: "behavioral tiers assume settled outputs; the ngspice tier measures \
278                           sim_term against the pinned settling options",
279                },
280            ),
281        Element::Macro(FunctionalMacro::GilbertMultiplier) => EffectRow::unknown()
282            .with(
283                Fx::MismatchArea,
284                Modeled {
285                    params: &[],
286                    note: "shares the programmed-weight accuracy class characterized on the \
287                           linear tile (the model is tile-generic today; a mul-specific \
288                           figure awaits characterization)",
289                },
290            )
291            .with(
292                Fx::Quantization,
293                Derived {
294                    params: &[],
295                    arithmetic: "the multiplier reference is drawn from the discrete mul_vref \
296                                 option set — realizability gated at schedule time in gain.rs",
297                },
298            )
299            .with(
300                Fx::SettlingBandwidth,
301                Modeled {
302                    params: &[],
303                    note: "as the linear tile",
304                },
305            ),
306        Element::Macro(FunctionalMacro::OtaIntegrator) => EffectRow::unknown()
307            .with(
308                Fx::MismatchArea,
309                Modeled {
310                    params: &[],
311                    note: "tile-generic gain/offset class today",
312                },
313            )
314            .with(
315                Fx::SettlingBandwidth,
316                Modeled {
317                    params: &[],
318                    note: "integrator rate realizability is target-gated \
319                           (require_integrator_rate)",
320                },
321            ),
322        Element::Macro(FunctionalMacro::Comparator) => EffectRow::unknown()
323            .with(
324                Fx::MismatchArea,
325                Modeled {
326                    params: &[],
327                    note: "input-referred offset of the pair — tile-generic class today; the \
328                           O5 CompareBand term prices the ambiguity band",
329                },
330            )
331            .with(
332                Fx::SettlingBandwidth,
333                Modeled {
334                    params: &[],
335                    note: "regeneration time — control-plane scheduled",
336                },
337            ),
338        Element::Macro(FunctionalMacro::SampleHold) => EffectRow::unknown()
339            .with(
340                Fx::NoiseThermal,
341                Modeled {
342                    params: &[],
343                    note: "kT/C sampled noise — the switch's Johnson noise frozen on the hold \
344                           cap; tile-generic noise class today",
345                },
346            )
347            .with(
348                Fx::CouplingCapacitive,
349                Modeled {
350                    params: &[],
351                    note: "switch charge injection and clock feedthrough onto the hold node — \
352                           awaiting characterization",
353                },
354            )
355            .with(
356                Fx::SettlingBandwidth,
357                Modeled {
358                    params: &[],
359                    note: "acquisition time — control-plane scheduled",
360                },
361            ),
362        Element::Macro(FunctionalMacro::Dac) => EffectRow::unknown()
363            .with(
364                Fx::Quantization,
365                Derived {
366                    params: &[],
367                    arithmetic: "half-LSB input quantization — the DacQuant budget term \
368                                 (half_lsb in error_budget.rs)",
369                },
370            )
371            .with(
372                Fx::ConverterInl,
373                Modeled {
374                    params: &[],
375                    note: "V1 spec class INL — characterization ladder",
376                },
377            )
378            .with(
379                Fx::ConverterDnl,
380                Modeled {
381                    params: &[],
382                    note: "V1 spec class DNL — characterization ladder",
383                },
384            ),
385        Element::Macro(FunctionalMacro::Adc) => EffectRow::unknown()
386            .with(
387                Fx::Quantization,
388                Derived {
389                    params: &[],
390                    arithmetic: "half-LSB output quantization — the AdcQuant budget term \
391                                 (half_lsb in error_budget.rs)",
392                },
393            )
394            .with(
395                Fx::ConverterInl,
396                Modeled {
397                    params: &[],
398                    note: "V1 spec class INL — characterization ladder",
399                },
400            )
401            .with(
402                Fx::ConverterDnl,
403                Modeled {
404                    params: &[],
405                    note: "V1 spec class DNL — characterization ladder",
406                },
407            ),
408        // Everything else: recognized, honestly unclassified — the ledgered frontier.
409        Element::Passive(
410            Passive::Inductor
411            | Passive::MutualInductor
412            | Passive::TransmissionLine
413            | Passive::Varactor
414            | Passive::Memristor
415            | Passive::AnalogSwitch
416            | Passive::Crystal,
417        )
418        | Element::Source(
419            SourceElement::IndependentV
420            | SourceElement::IndependentI
421            | SourceElement::Vcvs
422            | SourceElement::Cccs
423            | SourceElement::Ccvs,
424        )
425        | Element::Semiconductor(
426            Semiconductor::Diode(_)
427            | Semiconductor::Bjt(_)
428            | Semiconductor::Mosfet(_)
429            | Semiconductor::Jfet
430            | Semiconductor::PhotoDiode,
431        )
432        | Element::Macro(
433            FunctionalMacro::DiffPair
434            | FunctionalMacro::TranslinearLoop
435            | FunctionalMacro::Chopper
436            | FunctionalMacro::AnalogSelect
437            | FunctionalMacro::Ota
438            | FunctionalMacro::OpAmp
439            | FunctionalMacro::CurrentMirror
440            | FunctionalMacro::BandgapReference
441            | FunctionalMacro::BiasGenerator
442            | FunctionalMacro::VoltageReference
443            | FunctionalMacro::ClockDistribution
444            | FunctionalMacro::FilterBiquad
445            | FunctionalMacro::Oscillator
446            | FunctionalMacro::Vco
447            | FunctionalMacro::Pll
448            | FunctionalMacro::Lna
449            | FunctionalMacro::Pga
450            | FunctionalMacro::LdoRegulator,
451        ) => EffectRow::unknown(),
452    }
453}
454
455/// What a budget term IS: physics of the die, or arithmetic of the bound itself.
456#[derive(Clone, Copy, Debug, PartialEq, Eq)]
457pub enum BudgetTermClass {
458    /// The physical effects this term aggregates — non-empty, each classified somewhere in
459    /// the grid (the lock proves it).
460    Physics(&'static [PhysicsEffect]),
461    /// Bookkeeping of the bound, not a die mechanism — with the reason.
462    Analysis(&'static str),
463}
464
465/// Total classification of every [`ErrorSource`] into the taxonomy — a budget term cannot
466/// exist outside it (no wildcard; a new variant breaks this match until classified).
467pub fn effect_of(src: &ErrorSource) -> BudgetTermClass {
468    use PhysicsEffect as Fx;
469    match src {
470        ErrorSource::DacQuant { .. } | ErrorSource::AdcQuant { .. } | ErrorSource::WeightQuant => {
471            BudgetTermClass::Physics(&[Fx::Quantization])
472        }
473        ErrorSource::TileGain => {
474            BudgetTermClass::Physics(&[Fx::MismatchArea, Fx::MismatchDistance])
475        }
476        ErrorSource::TileOffset => BudgetTermClass::Physics(&[Fx::MismatchArea]),
477        ErrorSource::Noise => BudgetTermClass::Physics(&[Fx::NoiseThermal]),
478        ErrorSource::ThermalGain | ErrorSource::ThermalOffset => {
479            BudgetTermClass::Physics(&[Fx::DriftTemperature])
480        }
481        ErrorSource::CompareBand => BudgetTermClass::Physics(&[Fx::MismatchArea]),
482        ErrorSource::ChopResidual => BudgetTermClass::Physics(&[Fx::MismatchArea]),
483        ErrorSource::MulInteraction => BudgetTermClass::Analysis(
484            "the second-order eps_a*eps_b cross term of the perturbation bound — arithmetic \
485             of the analysis, not a physical mechanism of the die",
486        ),
487        ErrorSource::EvalRounding => BudgetTermClass::Analysis(
488            "f64 rounding of the behavioral evaluator — the model tier's arithmetic, not the \
489             die's physics",
490        ),
491    }
492}