logicaffeine_compile/codegen_ana/
nonideal.rs1use logicaffeine_base::si::Volts;
16
17use super::cell::{ASrc, AnalogCell};
18use super::gain::GainSchedule;
19use super::place::AnalogPlacement;
20use super::target::AnalogTarget;
21
22#[derive(Clone, Debug, PartialEq)]
24pub struct NonIdealityModel {
25 pub weight_gain_sigma: f64,
27 pub gm_distance_sigma_per_pitch: f64,
30 pub offset_sigma_v: Volts,
32 pub noise_floor_v: Volts,
34 pub cross_tier_gain_sigma: f64,
38 pub gain_tempco_per_k: f64,
42 pub offset_tempco_v_per_k: f64,
44 pub chop_residual_v: Volts,
47 pub provenance: &'static str,
49}
50
51pub fn sky130_nonideal() -> NonIdealityModel {
68 NonIdealityModel {
69 weight_gain_sigma: 0.01,
70 gm_distance_sigma_per_pitch: 0.002,
71 offset_sigma_v: Volts(2.8e-3),
72 noise_floor_v: Volts(64.4e-6),
73 cross_tier_gain_sigma: 0.02,
74 gain_tempco_per_k: 1.0e-3,
75 offset_tempco_v_per_k: 1.0e-5,
76 chop_residual_v: Volts(50.0e-6),
77 provenance: "weight_gain_sigma: Hasler Proc. IEEE 2020, FG programming better than 1% \
78 (SOURCED, conservative). offset_sigma_v: DERIVED from SkyWater sky130_fd_pr \
79 vth0_slope mismatch model (3.356e-03 V.um nfet / 5.856e-03 pfet) on a stated \
80 5T example — model-derived, not silicon. noise_floor_v: kT/C at 1 pF, 300 K \
81 = 64.4 uV rms (Johnson-Nyquist). gm_distance_sigma_per_pitch: MODELED design \
82 parameter (Pelgrom 1989 distance term; no published sky130 coefficient), \
83 replaced by characterization, never presented as silicon. \
84 cross_tier_gain_sigma: MODELED design parameter (2%): separately \
85 manufactured tiers share no Pelgrom field — a flat categorical class \
86 figure, never a distance law. gain_tempco_per_k (1e-3/K) and \
87 offset_tempco_v_per_k (10 uV/K): MODELED drift-class design parameters \
88 (amplifier-offset-drift textbook class), priced against the CERTIFIED \
89 thermal map, never silicon. chop_residual_v (50 uV): MODELED intra-pair \
90 drift class surviving the chop average — the frozen offset cancels \
91 exactly by construction. Full dossier: \
92 benchmarks/silicon/analog/sourced-constants.md",
93 }
94}
95
96pub const KSIGMA: f64 = 6.0;
99
100fn splitmix(state: &mut u64) -> u64 {
101 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
102 let mut z = *state;
103 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
104 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
105 z ^ (z >> 31)
106}
107
108fn uniform(state: &mut u64) -> f64 {
109 ((splitmix(state) >> 11) as f64 + 0.5) / (1u64 << 53) as f64
110}
111
112fn gaussian(state: &mut u64) -> f64 {
116 let (u1, u2) = (uniform(state), uniform(state));
117 let g = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
118 g.clamp(-KSIGMA, KSIGMA)
119}
120
121#[derive(Clone, Debug)]
124pub struct InstanceSample {
125 pub leg_gain_err: Vec<Vec<f64>>,
126 pub offset_v: Vec<f64>,
127 pub noise_v: Vec<f64>,
128}
129
130fn legs_of(cell: &AnalogCell) -> usize {
131 match cell {
132 AnalogCell::Sum { inputs, .. } => inputs.len(),
133 AnalogCell::Mul { .. } | AnalogCell::GainConst { .. } => 1,
134 AnalogCell::Select { .. } => 0,
136 AnalogCell::Exp { .. }
138 | AnalogCell::Log { .. }
139 | AnalogCell::Tanh { .. }
140 | AnalogCell::Sqrt { .. }
141 | AnalogCell::Div { .. } => 1,
142 }
143}
144
145#[derive(Clone, Copy, Debug, PartialEq)]
149pub enum Separation {
150 SameTier { pitches: f64 },
151 CrossTier,
152}
153
154pub fn separation_sigma(model: &NonIdealityModel, sep: Separation) -> f64 {
158 match sep {
159 Separation::SameTier { pitches } => {
160 model.weight_gain_sigma + model.gm_distance_sigma_per_pitch * pitches
161 }
162 Separation::CrossTier => model.weight_gain_sigma + model.cross_tier_gain_sigma,
163 }
164}
165
166pub(crate) fn leg_operand(cells: &[AnalogCell], cell_idx: usize, leg: usize) -> Option<usize> {
169 let operand = match &cells[cell_idx] {
170 AnalogCell::Sum { inputs, .. } => inputs[leg],
171 AnalogCell::GainConst { a, .. } => *a,
172 AnalogCell::Mul { a, .. } => *a,
173 AnalogCell::Select { .. } => return None,
174 AnalogCell::Exp { a }
175 | AnalogCell::Log { a }
176 | AnalogCell::Tanh { a }
177 | AnalogCell::Sqrt { a }
178 | AnalogCell::Div { a, .. } => *a,
179 };
180 match operand {
181 ASrc::Val(v) => Some(v),
182 _ => None,
183 }
184}
185
186pub fn leg_sigma(
188 cell_idx: usize,
189 leg: usize,
190 cells: &[AnalogCell],
191 placement: &AnalogPlacement,
192 model: &NonIdealityModel,
193) -> f64 {
194 let pitches = leg_operand(cells, cell_idx, leg)
195 .map(|v| placement.pitch_distance(cell_idx, v))
196 .unwrap_or(0.0);
197 separation_sigma(model, Separation::SameTier { pitches })
198}
199
200pub fn sample_instance(
202 cells: &[AnalogCell],
203 placement: &AnalogPlacement,
204 model: &NonIdealityModel,
205 seed: u64,
206) -> InstanceSample {
207 let mut st = seed ^ 0xD1E5_ED0F_ABBA_C0DE;
208 let mut leg_gain_err = Vec::with_capacity(cells.len());
209 let mut offset_v = Vec::with_capacity(cells.len());
210 let mut noise_v = Vec::with_capacity(cells.len());
211 for (i, cell) in cells.iter().enumerate() {
212 let legs = (0..legs_of(cell))
213 .map(|l| gaussian(&mut st) * leg_sigma(i, l, cells, placement, model))
214 .collect();
215 leg_gain_err.push(legs);
216 offset_v.push(gaussian(&mut st) * model.offset_sigma_v.0);
217 noise_v.push(gaussian(&mut st) * model.noise_floor_v.0);
218 }
219 InstanceSample {
220 leg_gain_err,
221 offset_v,
222 noise_v,
223 }
224}
225
226pub fn eval_analog_instance(
229 schedule: &GainSchedule,
230 target: &AnalogTarget,
231 inst: &InstanceSample,
232 inputs: &[f64],
233) -> f64 {
234 let netlist = &schedule.quantized;
235 assert_eq!(inputs.len(), netlist.n_in);
236 let grid = super::eval::ConverterGrid::of(schedule, target);
237 let s = grid.s;
238 let vin: Vec<f64> = inputs.iter().map(|x| grid.dac_q(*x) * s).collect();
239 let mut vals: Vec<f64> = Vec::with_capacity(netlist.cells.len());
240 let src = |sc: &ASrc, vals: &[f64]| -> f64 {
241 match sc {
242 ASrc::In(k) => vin[*k],
243 ASrc::Val(v) => vals[*v],
244 ASrc::Const(c) => c * s,
245 }
246 };
247 for (i, cell) in netlist.cells.iter().enumerate() {
248 let disturb = inst.offset_v[i] + inst.noise_v[i];
249 let v = match cell {
250 AnalogCell::Sum {
251 inputs: xs,
252 weights,
253 } => {
254 xs.iter()
255 .zip(weights)
256 .enumerate()
257 .map(|(l, (x, w))| w * (1.0 + inst.leg_gain_err[i][l]) * src(x, &vals))
258 .sum::<f64>()
259 + disturb
260 }
261 AnalogCell::Mul { a, b } => {
262 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals) * src(b, &vals)
263 / schedule.mul_vref_v
264 + disturb
265 }
266 AnalogCell::GainConst { a, k } => {
267 (1.0 + inst.leg_gain_err[i][0]) * k * src(a, &vals) + disturb
268 }
269 AnalogCell::Select { c, t, f } => {
270 let decided = src(c, &vals) + inst.offset_v[i];
271 let branch = if decided < 0.0 {
272 src(t, &vals)
273 } else {
274 src(f, &vals)
275 };
276 branch + inst.noise_v[i]
277 }
278 AnalogCell::Exp { a } => {
279 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals).exp() + disturb
280 }
281 AnalogCell::Log { a } => (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals).ln() + disturb,
282 AnalogCell::Tanh { a } => {
283 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals).tanh() + disturb
284 }
285 AnalogCell::Sqrt { a } => {
286 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals).sqrt() + disturb
287 }
288 AnalogCell::Div { a, b } => {
289 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals) / src(b, &vals) + disturb
290 }
291 };
292 vals.push(v);
293 }
294 let out_v = src(&netlist.out, &vals);
295 grid.adc_q(out_v / s)
296}
297
298pub fn eval_analog_instance_chopped(
303 schedule: &GainSchedule,
304 target: &AnalogTarget,
305 inst: &InstanceSample,
306 model: &NonIdealityModel,
307 inputs: &[f64],
308 frame_seed: u64,
309) -> f64 {
310 let netlist = &schedule.quantized;
311 assert_eq!(inputs.len(), netlist.n_in);
312 let grid = super::eval::ConverterGrid::of(schedule, target);
313 let s = grid.s;
314 let vin: Vec<f64> = inputs.iter().map(|x| grid.dac_q(*x) * s).collect();
315 let mut st = frame_seed ^ 0xC0FF_EEC0_5EED_0A11;
316 let run_frame = |polarity: f64, st: &mut u64| -> f64 {
317 let mut vals: Vec<f64> = Vec::with_capacity(netlist.cells.len());
318 let src = |sc: &ASrc, vals: &[f64]| -> f64 {
319 match sc {
320 ASrc::In(k) => vin[*k],
321 ASrc::Val(v) => vals[*v],
322 ASrc::Const(c) => c * s,
323 }
324 };
325 for (i, cell) in netlist.cells.iter().enumerate() {
326 let residual = gaussian(st) * model.chop_residual_v.0;
328 let noise = gaussian(st) * model.noise_floor_v.0;
329 let disturb = polarity * inst.offset_v[i] + residual + noise;
330 let v = match cell {
331 AnalogCell::Sum {
332 inputs: xs,
333 weights,
334 } => {
335 xs.iter()
336 .zip(weights)
337 .enumerate()
338 .map(|(l, (x, w))| w * (1.0 + inst.leg_gain_err[i][l]) * src(x, &vals))
339 .sum::<f64>()
340 + disturb
341 }
342 AnalogCell::Mul { a, b } => {
343 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals) * src(b, &vals)
344 / schedule.mul_vref_v
345 + disturb
346 }
347 AnalogCell::GainConst { a, k } => {
348 (1.0 + inst.leg_gain_err[i][0]) * k * src(a, &vals) + disturb
349 }
350 AnalogCell::Select { c, t, f } => {
351 let decided = src(c, &vals) + polarity * inst.offset_v[i];
352 let branch = if decided < 0.0 {
353 src(t, &vals)
354 } else {
355 src(f, &vals)
356 };
357 branch + noise
358 }
359 AnalogCell::Exp { a } => {
360 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals).exp() + disturb
361 }
362 AnalogCell::Log { a } => {
363 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals).ln() + disturb
364 }
365 AnalogCell::Tanh { a } => {
366 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals).tanh() + disturb
367 }
368 AnalogCell::Sqrt { a } => {
369 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals).sqrt() + disturb
370 }
371 AnalogCell::Div { a, b } => {
372 (1.0 + inst.leg_gain_err[i][0]) * src(a, &vals) / src(b, &vals) + disturb
373 }
374 };
375 vals.push(v);
376 }
377 match netlist.out {
378 ASrc::In(k) => vin[k],
379 ASrc::Val(v) => vals[v],
380 ASrc::Const(c) => c * s,
381 }
382 };
383 let a = run_frame(1.0, &mut st);
384 let b = run_frame(-1.0, &mut st);
385 grid.adc_q((a + b) / 2.0 / s)
386}