1use super::cell::{ASrc, AnalogCell, AnalogNetlist};
14use super::gain::{GainSchedule, MathBound, RangeCertificate};
15use super::nonideal::{leg_sigma, NonIdealityModel, KSIGMA};
16use super::place::AnalogPlacement;
17use super::target::AnalogTarget;
18
19#[derive(Clone, Copy, Debug, PartialEq)]
22pub enum ErrorSource {
23 DacQuant { input: usize, bits: u8 },
25 AdcQuant { bits: u8 },
27 WeightQuant,
30 MulInteraction,
32 EvalRounding,
34 TileGain,
37 TileOffset,
39 Noise,
41 ThermalGain,
44 ThermalOffset,
46 CompareBand,
50 ChopResidual,
53}
54
55pub fn error_budget_chopped(
60 netlist: &AnalogNetlist,
61 cert: &RangeCertificate,
62 schedule: &GainSchedule,
63 target: &AnalogTarget,
64 model: &NonIdealityModel,
65 placement: &AnalogPlacement,
66) -> OutputBudget {
67 let unchopped = error_budget_nonideal(netlist, cert, schedule, target, model, placement);
68 let bound_of = |src: &ASrc| -> MathBound {
69 match src {
70 ASrc::In(k) => cert.input_bounds[*k],
71 ASrc::Val(v) => cert.per_net[*v].math,
72 ASrc::Const(c) => MathBound { lo: *c, hi: *c },
73 }
74 };
75 let realized = AnalogNetlist {
76 n_in: schedule.quantized.n_in,
77 cells: schedule.realized_math_cells(),
78 out: schedule.quantized.out,
79 };
80 let s_of = |i: usize| f64::powi(2.0, schedule.per_net_scale_log2[i]);
81 let n = realized.cells.len();
82 let resid_d: Vec<f64> = (0..n)
83 .map(|i| (KSIGMA * model.chop_residual_v.0 / s_of(i)).next_up())
84 .collect();
85 let tr = propagate_injected(&realized, &bound_of, &resid_d);
86
87 let mut terms = Vec::with_capacity(unchopped.terms.len() + 1);
88 for t in &unchopped.terms {
89 match t.source {
90 ErrorSource::TileOffset => {}
92 ErrorSource::Noise => terms.push(ErrorTerm {
94 eps_math: (t.eps_math / std::f64::consts::SQRT_2).next_up(),
95 source: ErrorSource::Noise,
96 }),
97 _ => terms.push(*t),
98 }
99 }
100 if tr > 0.0 {
101 terms.push(ErrorTerm {
102 eps_math: tr,
103 source: ErrorSource::ChopResidual,
104 });
105 }
106 let eps_math = outward_sum(terms.iter().map(|t| t.eps_math));
107 OutputBudget { eps_math, terms }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq)]
112pub struct ErrorTerm {
113 pub eps_math: f64,
114 pub source: ErrorSource,
115}
116
117#[derive(Clone, Debug, PartialEq)]
119pub struct OutputBudget {
120 pub eps_math: f64,
121 pub terms: Vec<ErrorTerm>,
122}
123
124impl OutputBudget {
125 pub fn recheck(&self) -> bool {
129 if self.terms.is_empty() || !self.eps_math.is_finite() {
130 return false;
131 }
132 if self
133 .terms
134 .iter()
135 .any(|t| !t.eps_math.is_finite() || t.eps_math < 0.0)
136 {
137 return false;
138 }
139 outward_sum(self.terms.iter().map(|t| t.eps_math)) == self.eps_math
140 }
141
142 #[must_use]
145 pub fn with_term_removed(&self, idx: usize) -> OutputBudget {
146 let mut terms = self.terms.clone();
147 terms.remove(idx);
148 OutputBudget {
149 eps_math: self.eps_math,
150 terms,
151 }
152 }
153}
154
155pub(crate) fn outward_sum(terms: impl Iterator<Item = f64>) -> f64 {
156 let mut acc = 0.0_f64;
157 for t in terms {
158 if t > 0.0 {
160 acc = (acc + t).next_up();
161 }
162 }
163 acc
164}
165
166pub(crate) fn ulp_of(x: f64) -> f64 {
168 let a = x.abs().max(f64::MIN_POSITIVE);
169 a.next_up() - a
170}
171
172pub(crate) fn bump_add(acc: f64, t: f64) -> f64 {
174 if t > 0.0 {
175 (acc + t).next_up()
176 } else {
177 acc
178 }
179}
180
181pub(crate) fn bump_mul(a: f64, b: f64) -> f64 {
183 let p = a * b;
184 if p > 0.0 {
185 p.next_up()
186 } else {
187 0.0
188 }
189}
190
191pub(crate) struct EpsPass {
204 pub cell_eps: Vec<Vec<f64>>,
205}
206
207#[allow(clippy::too_many_arguments)]
208pub(crate) fn propagate_eps_pass(
209 cells_orig: &[AnalogCell],
210 cells_q: &[AnalogCell],
211 bound_of: &dyn Fn(&ASrc) -> MathBound,
212 lanes: usize,
213 wq_lane: usize,
214 cross_lane: usize,
215 input_eps: &dyn Fn(&ASrc, usize) -> f64,
216 input_total: &dyn Fn(&ASrc) -> f64,
217) -> EpsPass {
218 let mut cell_eps: Vec<Vec<f64>> = Vec::with_capacity(cells_q.len());
219 let mut cell_total: Vec<f64> = Vec::with_capacity(cells_q.len());
220 let src_eps = |src: &ASrc, cell_eps: &[Vec<f64>], k: usize| -> f64 {
221 match src {
222 ASrc::Val(v) => cell_eps[*v][k],
223 ASrc::Const(_) => 0.0,
224 input => input_eps(input, k),
225 }
226 };
227 let src_total = |src: &ASrc, cell_total: &[f64]| -> f64 {
228 match src {
229 ASrc::Val(v) => cell_total[*v],
230 ASrc::Const(_) => 0.0,
231 input => input_total(input),
232 }
233 };
234 for (orig, phys) in cells_orig.iter().zip(cells_q) {
235 let delta = match (orig, phys) {
237 (AnalogCell::Sum { inputs, weights: w }, AnalogCell::Sum { weights: wq, .. }) => {
238 outward_sum(
239 inputs
240 .iter()
241 .zip(w.iter().zip(wq))
242 .map(|(x, (a, b))| bump_mul((a - b).abs(), bound_of(x).max_abs())),
243 )
244 }
245 (AnalogCell::GainConst { a, k }, AnalogCell::GainConst { k: kq, .. }) => {
246 bump_mul((k - kq).abs(), bound_of(a).max_abs())
247 }
248 _ => 0.0,
249 };
250 let mut eps_k = vec![0.0_f64; lanes];
251 let total;
252 match phys {
253 AnalogCell::Sum { inputs, weights } => {
254 for k in 0..lanes {
255 let mut acc = 0.0_f64;
256 for (x, w) in inputs.iter().zip(weights) {
257 acc = bump_add(acc, bump_mul(w.abs(), src_eps(x, &cell_eps, k)));
258 }
259 eps_k[k] = acc;
260 }
261 total = outward_sum(
262 inputs
263 .iter()
264 .zip(weights)
265 .map(|(x, w)| bump_mul(w.abs(), src_total(x, &cell_total))),
266 );
267 }
268 AnalogCell::GainConst { a, k: gain } => {
269 for k in 0..lanes {
270 eps_k[k] = bump_mul(gain.abs(), src_eps(a, &cell_eps, k));
271 }
272 total = bump_mul(gain.abs(), src_total(a, &cell_total));
273 }
274 AnalogCell::Mul { a, b } => {
275 let (amax, bmax) = (bound_of(a).max_abs(), bound_of(b).max_abs());
276 for k in 0..lanes {
277 let ea = src_eps(a, &cell_eps, k);
278 let eb = src_eps(b, &cell_eps, k);
279 eps_k[k] = bump_add(bump_mul(bmax, ea), bump_mul(amax, eb));
280 }
281 let (ta, tb) = (src_total(a, &cell_total), src_total(b, &cell_total));
282 eps_k[cross_lane] = bump_add(eps_k[cross_lane], bump_mul(ta, tb));
283 total = bump_add(
284 bump_add(bump_mul(bmax, ta), bump_mul(amax, tb)),
285 bump_mul(ta, tb),
286 );
287 }
288 AnalogCell::Select { t, f, .. } => {
289 for k in 0..lanes {
292 eps_k[k] = src_eps(t, &cell_eps, k).max(src_eps(f, &cell_eps, k));
293 }
294 total = src_total(t, &cell_total).max(src_total(f, &cell_total));
295 }
296 AnalogCell::Exp { a } => {
300 let l = bound_of(a).hi.exp().next_up();
301 for k in 0..lanes {
302 eps_k[k] = bump_mul(l, src_eps(a, &cell_eps, k));
303 }
304 total = bump_mul(l, src_total(a, &cell_total));
305 }
306 AnalogCell::Log { a } => {
307 let l = (1.0 / bound_of(a).lo).next_up();
308 for k in 0..lanes {
309 eps_k[k] = bump_mul(l, src_eps(a, &cell_eps, k));
310 }
311 total = bump_mul(l, src_total(a, &cell_total));
312 }
313 AnalogCell::Tanh { a } => {
314 for k in 0..lanes {
315 eps_k[k] = src_eps(a, &cell_eps, k);
316 }
317 total = src_total(a, &cell_total);
318 }
319 AnalogCell::Sqrt { a } => {
320 let l = (0.5 / bound_of(a).lo.sqrt()).next_up();
321 for k in 0..lanes {
322 eps_k[k] = bump_mul(l, src_eps(a, &cell_eps, k));
323 }
324 total = bump_mul(l, src_total(a, &cell_total));
325 }
326 AnalogCell::Div { a, b } => {
327 let bb = bound_of(b);
330 let min_b = bb.lo.abs().min(bb.hi.abs());
331 let la = (1.0 / min_b).next_up();
332 let lb = (bound_of(a).max_abs() / (min_b * min_b)).next_up();
333 for k in 0..lanes {
334 let ea = src_eps(a, &cell_eps, k);
335 let eb = src_eps(b, &cell_eps, k);
336 eps_k[k] = bump_add(bump_mul(la, ea), bump_mul(lb, eb));
337 }
338 let (ta, tb) = (src_total(a, &cell_total), src_total(b, &cell_total));
339 let cross = bump_mul(ta * tb, (1.0 / (min_b * min_b)).next_up());
340 eps_k[cross_lane] = bump_add(eps_k[cross_lane], cross);
341 total = bump_add(bump_add(bump_mul(la, ta), bump_mul(lb, tb)), cross);
342 }
343 }
344 if delta > 0.0 {
345 eps_k[wq_lane] = bump_add(eps_k[wq_lane], delta);
346 }
347 cell_eps.push(eps_k);
348 cell_total.push(if delta > 0.0 {
349 bump_add(total, delta)
350 } else {
351 total
352 });
353 }
354 EpsPass { cell_eps }
355}
356
357pub(crate) fn out_lanes(
359 pass: &EpsPass,
360 out: &ASrc,
361 lanes: usize,
362 input_eps: &dyn Fn(&ASrc, usize) -> f64,
363) -> Vec<f64> {
364 (0..lanes)
365 .map(|k| match out {
366 ASrc::Val(v) => pass.cell_eps[*v][k],
367 ASrc::Const(_) => 0.0,
368 input => input_eps(input, k),
369 })
370 .collect()
371}
372
373pub(crate) fn eval_rounding_guard(cert: &RangeCertificate, ops_evaluated: usize) -> f64 {
376 let max_mag = cert
377 .per_net
378 .iter()
379 .map(|n| n.math.max_abs())
380 .chain(cert.input_bounds.iter().map(|b| b.max_abs()))
381 .fold(f64::MIN_POSITIVE, f64::max);
382 (16.0 * (ops_evaluated as f64 + 2.0) * ulp_of(max_mag)).next_up()
383}
384
385pub(crate) fn half_lsb(cert: &RangeCertificate, schedule: &GainSchedule, bits: u8) -> f64 {
387 cert.rail_half_span_v / f64::powi(2.0, i32::from(bits)) / f64::powi(2.0, schedule.scale_log2)
388}
389
390pub fn error_budget(
393 netlist: &AnalogNetlist,
394 cert: &RangeCertificate,
395 schedule: &GainSchedule,
396 target: &AnalogTarget,
397) -> OutputBudget {
398 let n_in = netlist.n_in;
399 let dac_eps = half_lsb(cert, schedule, target.dac_bits);
400 let adc_eps = half_lsb(cert, schedule, target.adc_bits);
401 let bound_of = |src: &ASrc| -> MathBound {
402 match src {
403 ASrc::In(k) => cert.input_bounds[*k],
404 ASrc::Val(v) => cert.per_net[*v].math,
405 ASrc::Const(c) => MathBound { lo: *c, hi: *c },
406 }
407 };
408 let (lanes, wq_lane, cross_lane) = (n_in + 2, n_in, n_in + 1);
409 let input_eps = |src: &ASrc, k: usize| -> f64 {
410 match src {
411 ASrc::In(i) if *i == k && k < n_in => dac_eps,
412 _ => 0.0,
413 }
414 };
415 let input_total = |_: &ASrc| dac_eps;
416 let realized = schedule.realized_math_cells();
417 let pass = propagate_eps_pass(
418 &netlist.cells,
419 &realized,
420 &bound_of,
421 lanes,
422 wq_lane,
423 cross_lane,
424 &input_eps,
425 &input_total,
426 );
427 let out = out_lanes(&pass, &netlist.out, lanes, &input_eps);
428
429 let mut terms: Vec<ErrorTerm> = out[..n_in]
430 .iter()
431 .enumerate()
432 .filter(|(_, e)| **e > 0.0)
433 .map(|(k, e)| ErrorTerm {
434 eps_math: *e,
435 source: ErrorSource::DacQuant {
436 input: k,
437 bits: target.dac_bits,
438 },
439 })
440 .collect();
441 terms.push(ErrorTerm {
442 eps_math: adc_eps,
443 source: ErrorSource::AdcQuant {
444 bits: target.adc_bits,
445 },
446 });
447 if out[wq_lane] > 0.0 {
448 terms.push(ErrorTerm {
449 eps_math: out[wq_lane],
450 source: ErrorSource::WeightQuant,
451 });
452 }
453 if out[cross_lane] > 0.0 {
454 terms.push(ErrorTerm {
455 eps_math: out[cross_lane],
456 source: ErrorSource::MulInteraction,
457 });
458 }
459 terms.push(ErrorTerm {
460 eps_math: eval_rounding_guard(cert, netlist.cells.len()),
461 source: ErrorSource::EvalRounding,
462 });
463
464 let eps_math = outward_sum(terms.iter().map(|t| t.eps_math));
465 OutputBudget { eps_math, terms }
466}
467
468pub(crate) fn propagate_injected_pub(
470 netlist_q: &AnalogNetlist,
471 bound_of: &dyn Fn(&ASrc) -> MathBound,
472 deltas: &[f64],
473) -> f64 {
474 propagate_injected(netlist_q, bound_of, deltas)
475}
476
477fn propagate_injected(
481 netlist_q: &AnalogNetlist,
482 bound_of: &dyn Fn(&ASrc) -> MathBound,
483 deltas: &[f64],
484) -> f64 {
485 let mut cell_eps: Vec<f64> = Vec::with_capacity(netlist_q.cells.len());
486 let src_eps = |s: &ASrc, cell_eps: &[f64]| -> f64 {
487 match s {
488 ASrc::Val(v) => cell_eps[*v],
489 _ => 0.0,
490 }
491 };
492 for (i, cell) in netlist_q.cells.iter().enumerate() {
493 let carried = match cell {
494 AnalogCell::Sum { inputs, weights } => outward_sum(
495 inputs
496 .iter()
497 .zip(weights)
498 .map(|(x, w)| bump_mul(w.abs(), src_eps(x, &cell_eps))),
499 ),
500 AnalogCell::GainConst { a, k } => bump_mul(k.abs(), src_eps(a, &cell_eps)),
501 AnalogCell::Select { t, f, .. } => src_eps(t, &cell_eps).max(src_eps(f, &cell_eps)),
502 AnalogCell::Exp { a } => {
503 bump_mul(bound_of(a).hi.exp().next_up(), src_eps(a, &cell_eps))
504 }
505 AnalogCell::Log { a } => {
506 bump_mul((1.0 / bound_of(a).lo).next_up(), src_eps(a, &cell_eps))
507 }
508 AnalogCell::Tanh { a } => src_eps(a, &cell_eps),
509 AnalogCell::Sqrt { a } => bump_mul(
510 (0.5 / bound_of(a).lo.sqrt()).next_up(),
511 src_eps(a, &cell_eps),
512 ),
513 AnalogCell::Div { a, b } => {
514 let bb = bound_of(b);
515 let min_b = bb.lo.abs().min(bb.hi.abs());
516 bump_add(
517 bump_mul((1.0 / min_b).next_up(), src_eps(a, &cell_eps)),
518 bump_mul(
519 (bound_of(a).max_abs() / (min_b * min_b)).next_up(),
520 src_eps(b, &cell_eps),
521 ),
522 )
523 }
524 AnalogCell::Mul { a, b } => {
525 let (amax, bmax) = (bound_of(a).max_abs(), bound_of(b).max_abs());
526 bump_add(
527 bump_mul(bmax, src_eps(a, &cell_eps)),
528 bump_mul(amax, src_eps(b, &cell_eps)),
529 )
530 }
531 };
532 cell_eps.push(if deltas[i] > 0.0 {
533 bump_add(carried, deltas[i])
534 } else {
535 carried
536 });
537 }
538 match netlist_q.out {
539 ASrc::Val(v) => cell_eps[v],
540 _ => 0.0,
541 }
542}
543
544pub fn error_budget_nonideal(
549 netlist: &AnalogNetlist,
550 cert: &RangeCertificate,
551 schedule: &GainSchedule,
552 target: &AnalogTarget,
553 model: &NonIdealityModel,
554 placement: &AnalogPlacement,
555) -> OutputBudget {
556 let q = schedule.realized_math_cells();
557 let sigma_of = move |i: usize, l: usize| leg_sigma(i, l, &q, placement, model);
558 nonideal_budget_with(netlist, cert, schedule, target, model, &sigma_of)
559}
560
561pub(crate) fn nonideal_budget_with(
565 netlist: &AnalogNetlist,
566 cert: &RangeCertificate,
567 schedule: &GainSchedule,
568 target: &AnalogTarget,
569 model: &NonIdealityModel,
570 sigma_of: &dyn Fn(usize, usize) -> f64,
571) -> OutputBudget {
572 let base = error_budget(netlist, cert, schedule, target);
573 let realized = AnalogNetlist {
574 n_in: schedule.quantized.n_in,
575 cells: schedule.realized_math_cells(),
576 out: schedule.quantized.out,
577 };
578 let s_of = |i: usize| f64::powi(2.0, schedule.per_net_scale_log2[i]);
579 let q = &realized;
580 let bound_of = |src: &ASrc| -> MathBound {
581 match src {
582 ASrc::In(k) => cert.input_bounds[*k],
583 ASrc::Val(v) => cert.per_net[*v].math,
584 ASrc::Const(c) => MathBound { lo: *c, hi: *c },
585 }
586 };
587
588 let mut gain_d = Vec::with_capacity(q.cells.len());
590 let mut off_d = Vec::with_capacity(q.cells.len());
591 let mut noise_d = Vec::with_capacity(q.cells.len());
592 for (i, cell) in q.cells.iter().enumerate() {
593 let g = match cell {
594 AnalogCell::Sum { inputs, weights } => {
595 outward_sum(inputs.iter().zip(weights).enumerate().map(|(l, (x, w))| {
596 bump_mul(KSIGMA * sigma_of(i, l), w.abs() * bound_of(x).max_abs())
597 }))
598 }
599 AnalogCell::GainConst { a, k } => {
600 bump_mul(KSIGMA * sigma_of(i, 0), k.abs() * bound_of(a).max_abs())
601 }
602 AnalogCell::Mul { a, b } => bump_mul(
603 KSIGMA * sigma_of(i, 0),
604 bound_of(a).max_abs() * bound_of(b).max_abs(),
605 ),
606 AnalogCell::Select { .. } => 0.0,
607 AnalogCell::Exp { .. }
610 | AnalogCell::Log { .. }
611 | AnalogCell::Tanh { .. }
612 | AnalogCell::Sqrt { .. }
613 | AnalogCell::Div { .. } => {
614 bump_mul(KSIGMA * sigma_of(i, 0), cert.per_net[i].math.max_abs())
615 }
616 };
617 gain_d.push(g);
618 off_d.push((KSIGMA * model.offset_sigma_v.0 / s_of(i)).next_up());
619 noise_d.push((KSIGMA * model.noise_floor_v.0 / s_of(i)).next_up());
620 }
621
622 let mut band_d = vec![0.0_f64; q.cells.len()];
625 let mut any_band = false;
626 for (i, cell) in q.cells.iter().enumerate() {
627 if let AnalogCell::Select { c, t, f } = cell {
628 let s_c = match c {
629 ASrc::Val(v) => f64::powi(2.0, schedule.per_net_scale_log2[*v]),
630 _ => f64::powi(2.0, schedule.scale_log2),
631 };
632 let band_math = KSIGMA * model.offset_sigma_v.0 / s_c;
633 let cb = bound_of(c);
634 if cb.lo < band_math && cb.hi > -band_math {
635 let (tb, fb) = (bound_of(t), bound_of(f));
636 let hull_w = (tb.hi.max(fb.hi) - tb.lo.min(fb.lo)).next_up();
637 band_d[i] = hull_w;
638 any_band = true;
639 }
640 }
641 }
642 let mut terms = base.terms;
643 if any_band {
644 let tb = propagate_injected(q, &bound_of, &band_d);
645 if tb > 0.0 {
646 terms.push(ErrorTerm {
647 eps_math: tb,
648 source: ErrorSource::CompareBand,
649 });
650 }
651 }
652 let tg = propagate_injected(q, &bound_of, &gain_d);
653 if tg > 0.0 {
654 terms.push(ErrorTerm {
655 eps_math: tg,
656 source: ErrorSource::TileGain,
657 });
658 }
659 let to = propagate_injected(q, &bound_of, &off_d);
660 if to > 0.0 {
661 terms.push(ErrorTerm {
662 eps_math: to,
663 source: ErrorSource::TileOffset,
664 });
665 }
666 let tn = propagate_injected(q, &bound_of, &noise_d);
667 if tn > 0.0 {
668 terms.push(ErrorTerm {
669 eps_math: tn,
670 source: ErrorSource::Noise,
671 });
672 }
673 let eps_math = outward_sum(terms.iter().map(|t| t.eps_math));
674 OutputBudget { eps_math, terms }
675}