logicaffeine_compile/codegen_ana/eval.rs
1//! Tier-1 behavioral evaluation of an analog netlist.
2//!
3//! `eval_analog_dc` is the analog analogue of the digital fabric's `eval_grid`: the IDEAL
4//! reference semantics. The plane settles to a DC operating point per control tick; with no
5//! non-ideality and unit scale, that point is exactly the SSA dataflow evaluated in f64 — the
6//! same operations in the same order as the VM, so the ideal tier is compared with
7//! `logos_approx_eq` and is bit-exact in practice. Physics (scales, quantization, mismatch,
8//! noise) enters through later layers, each with its own budgeted comparison — never here.
9
10use super::cell::AnalogNetlist;
11use super::gain::GainSchedule;
12use super::target::AnalogTarget;
13
14/// THE converter model — the single definition of the block scale and both converter grids
15/// every physical tier shares (the behavioral evaluators, the instance evaluator, the streamed
16/// and integrated steppers — and the mixed-signal SoC's fixed-point surrogate, which must
17/// mirror these semantics bit-for-bit).
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct ConverterGrid {
20 /// The block scale, volts per math unit (`2^scale_log2`).
21 pub s: f64,
22 /// DAC step in volts over the usable differential span.
23 pub dac_lsb: f64,
24 /// ADC step in volts.
25 pub adc_lsb: f64,
26}
27
28impl ConverterGrid {
29 pub fn of(schedule: &GainSchedule, target: &AnalogTarget) -> ConverterGrid {
30 let half_span = target.half_span_v();
31 ConverterGrid {
32 s: f64::powi(2.0, schedule.scale_log2),
33 dac_lsb: 2.0 * half_span / f64::powi(2.0, i32::from(target.dac_bits)),
34 adc_lsb: 2.0 * half_span / f64::powi(2.0, i32::from(target.adc_bits)),
35 }
36 }
37
38 /// A math-domain input as the DAC actually drives it: rounded to the DAC grid at the block
39 /// scale, returned in the MATH domain (round-half-away-from-zero, deterministic).
40 pub fn dac_q(&self, x_math: f64) -> f64 {
41 ((x_math * self.s) / self.dac_lsb).round() * self.dac_lsb / self.s
42 }
43
44 /// A math-domain result as the ADC reads it back: rounded to the ADC grid, math domain.
45 pub fn adc_q(&self, y_math: f64) -> f64 {
46 ((y_math * self.s) / self.adc_lsb).round() * self.adc_lsb / self.s
47 }
48}
49
50/// Ideal DC operating point: evaluate the SSA tiles in order and read the output tap.
51pub fn eval_analog_dc(netlist: &AnalogNetlist, inputs: &[f64]) -> f64 {
52 assert_eq!(
53 inputs.len(),
54 netlist.n_in,
55 "input arity must match the netlist"
56 );
57 let mut vals = Vec::with_capacity(netlist.cells.len());
58 for cell in &netlist.cells {
59 let v = cell.eval(inputs, &vals);
60 vals.push(v);
61 }
62 netlist.out.eval(inputs, &vals)
63}
64
65/// DC operating point through the CONVERTER path: each input is driven by the DAC (rounded to
66/// its LSB at the block scale), the plane settles in the VOLT domain (the `Mul` tile divides by
67/// its programmed `V_ref`, returning products to the block scale), and the output is read
68/// through the ADC (rounded to its LSB) and descaled exactly (power-of-two shift).
69///
70/// Rounding is round-half-away-from-zero (`f64::round`) — deterministic, and the half-LSB error
71/// model in [`super::error_budget`](mod@super::error_budget) covers it. This is the first physically-shaped tier: its
72/// deviation from [`eval_analog_dc`] must stay inside the compiler's own `OutputBudget`.
73///
74/// [`super::error_budget`]: mod@crate::codegen_ana::error_budget
75pub fn eval_analog_quantized(
76 schedule: &GainSchedule,
77 target: &AnalogTarget,
78 inputs: &[f64],
79) -> f64 {
80 // The physical tier runs what silicon runs — the weight-quantized twin — through the ONE
81 // converter model: DAC-grid the inputs, settle (the math-domain walk; the volt-domain plane
82 // with the Mul's V_ref normalization is exactly this times the block scale), ADC-grid out.
83 let grid = ConverterGrid::of(schedule, target);
84 let vin: Vec<f64> = inputs.iter().map(|x| grid.dac_q(*x)).collect();
85 grid.adc_q(eval_analog_dc(&schedule.quantized, &vin))
86}