Skip to main content

logicaffeine_compile/codegen_ana/
calibrate.rs

1//! Factory calibration — manufacturing ADJUSTABLE circuits, not impossibly perfect ones.
2//!
3//! Production test measures each die through the loopback paths (DAC → tile → ADC), computes
4//! per-leg gain trims and per-tile offset trims on the trim grid, and burns them beside the
5//! configuration. The effective die is `compiler programming + per-die calibration`; what the
6//! budget then carries for gain and offset is the RESIDUAL — half a trim step, a deterministic
7//! bound, instead of the full k·σ manufacturing envelope. Noise cannot be trimmed and keeps its
8//! envelope. This is the lever that turns per-die physics into a product-grade ε.
9
10use super::cell::AnalogNetlist;
11use super::cell::{ASrc, AnalogCell};
12use super::error_budget::{bump_mul, outward_sum, ErrorSource, ErrorTerm, OutputBudget};
13use super::gain::{GainSchedule, MathBound, RangeCertificate};
14use super::nonideal::{InstanceSample, NonIdealityModel, KSIGMA};
15use super::place::AnalogPlacement;
16use super::target::AnalogTarget;
17
18/// The trim DAC resolution — a design parameter of the tile (finer than the weight grid, since
19/// trims correct errors, not values).
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub struct TrimSpec {
22    /// Fractional gain-trim step.
23    pub gain_step: f64,
24    /// Offset-trim step in volts.
25    pub offset_step_v: f64,
26}
27
28/// The V1 tile's trim resolution: gain trim = a quarter unit element; offset trim = 0.5 mV
29/// steps (a 6-bit trim over ±16 mV). Design parameters of OUR tile, not silicon claims.
30pub fn v1_trim(target: &AnalogTarget) -> TrimSpec {
31    TrimSpec {
32        gain_step: target.weight_lsb() / 4.0,
33        offset_step_v: 0.5e-3,
34    }
35}
36
37/// Per-die trim values, each ON the trim grid.
38#[derive(Clone, Debug, PartialEq)]
39pub struct CalibrationTable {
40    pub gain_trim: Vec<Vec<f64>>,
41    pub offset_trim_v: Vec<f64>,
42}
43
44fn snap(x: f64, step: f64) -> f64 {
45    (x / step).round() * step
46}
47
48/// Factory calibration of one measured die: the quantized negation of every measured error.
49pub fn calibrate(inst: &InstanceSample, spec: &TrimSpec) -> CalibrationTable {
50    CalibrationTable {
51        gain_trim: inst
52            .leg_gain_err
53            .iter()
54            .map(|legs| legs.iter().map(|e| snap(-e, spec.gain_step)).collect())
55            .collect(),
56        offset_trim_v: inst
57            .offset_v
58            .iter()
59            .map(|o| snap(-o, spec.offset_step_v))
60            .collect(),
61    }
62}
63
64/// The calibrated die: residual gain/offset errors (≤ half a trim step by construction), the
65/// noise realization untouched (noise is dynamic — no trim reaches it).
66pub fn apply_calibration(inst: &InstanceSample, table: &CalibrationTable) -> InstanceSample {
67    InstanceSample {
68        leg_gain_err: inst
69            .leg_gain_err
70            .iter()
71            .zip(&table.gain_trim)
72            .map(|(legs, trims)| legs.iter().zip(trims).map(|(e, t)| e + t).collect())
73            .collect(),
74        offset_v: inst
75            .offset_v
76            .iter()
77            .zip(&table.offset_trim_v)
78            .map(|(o, t)| o + t)
79            .collect(),
80        noise_v: inst.noise_v.clone(),
81    }
82}
83
84/// The CALIBRATED physical budget: gain and offset carry the residual half-step bound instead
85/// of the k·σ manufacturing envelope; noise keeps its envelope; the converter/quantization
86/// terms are unchanged.
87pub fn error_budget_calibrated(
88    netlist: &AnalogNetlist,
89    cert: &RangeCertificate,
90    schedule: &GainSchedule,
91    target: &AnalogTarget,
92    model: &NonIdealityModel,
93    _placement: &AnalogPlacement,
94    spec: &TrimSpec,
95) -> OutputBudget {
96    let base = super::error_budget::error_budget(netlist, cert, schedule, target);
97    let realized = super::cell::AnalogNetlist {
98        n_in: schedule.quantized.n_in,
99        cells: schedule.realized_math_cells(),
100        out: schedule.quantized.out,
101    };
102    let s_of = |i: usize| f64::powi(2.0, schedule.per_net_scale_log2[i]);
103    let q = &realized;
104    let bound_of = |src: &ASrc| -> MathBound {
105        match src {
106            ASrc::In(k) => cert.input_bounds[*k],
107            ASrc::Val(v) => cert.per_net[*v].math,
108            ASrc::Const(c) => MathBound { lo: *c, hi: *c },
109        }
110    };
111    // Residual per-cell deltas: half a trim step, deterministic (calibration nulls the whole
112    // measured error INCLUDING the distance term — that is the point of measuring the die).
113    let mut gain_d = Vec::with_capacity(q.cells.len());
114    let mut off_d = Vec::with_capacity(q.cells.len());
115    let mut noise_d = Vec::with_capacity(q.cells.len());
116    for (i, cell) in q.cells.iter().enumerate() {
117        let g = match cell {
118            AnalogCell::Sum { inputs, weights } => outward_sum(
119                inputs
120                    .iter()
121                    .zip(weights)
122                    .map(|(x, w)| bump_mul(spec.gain_step / 2.0, w.abs() * bound_of(x).max_abs())),
123            ),
124            AnalogCell::GainConst { a, k } => {
125                bump_mul(spec.gain_step / 2.0, k.abs() * bound_of(a).max_abs())
126            }
127            AnalogCell::Mul { a, b } => bump_mul(
128                spec.gain_step / 2.0,
129                bound_of(a).max_abs() * bound_of(b).max_abs(),
130            ),
131            AnalogCell::Select { .. } => 0.0,
132            AnalogCell::Exp { .. }
133            | AnalogCell::Log { .. }
134            | AnalogCell::Tanh { .. }
135            | AnalogCell::Sqrt { .. }
136            | AnalogCell::Div { .. } => {
137                bump_mul(spec.gain_step / 2.0, cert.per_net[i].math.max_abs())
138            }
139        };
140        gain_d.push(g);
141        off_d.push((spec.offset_step_v / 2.0 / s_of(i)).next_up());
142        noise_d.push((KSIGMA * model.noise_floor_v.0 / s_of(i)).next_up());
143    }
144    let mut terms = base.terms;
145    let tg = super::error_budget::propagate_injected_pub(q, &bound_of, &gain_d);
146    if tg > 0.0 {
147        terms.push(ErrorTerm {
148            eps_math: tg,
149            source: ErrorSource::TileGain,
150        });
151    }
152    let to = super::error_budget::propagate_injected_pub(q, &bound_of, &off_d);
153    if to > 0.0 {
154        terms.push(ErrorTerm {
155            eps_math: to,
156            source: ErrorSource::TileOffset,
157        });
158    }
159    let tn = super::error_budget::propagate_injected_pub(q, &bound_of, &noise_d);
160    if tn > 0.0 {
161        terms.push(ErrorTerm {
162            eps_math: tn,
163            source: ErrorSource::Noise,
164        });
165    }
166    let eps_math = outward_sum(terms.iter().map(|t| t.eps_math));
167    OutputBudget { eps_math, terms }
168}