Skip to main content

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}