Skip to main content

logicaffeine_compile/codegen_ana/
cell.rs

1//! The analog tile netlist — `codegen_ana`'s SSA IR.
2//!
3//! The analog analogue of the digital fabric's `LoopCell` graph, in the MATH domain: values are
4//! `f64` quantities on nets (the gain-scaling pass later attaches per-net voltage scales; the
5//! netlist itself stays scale-free). Cells are SSA-ordered: cell `i` defines `Val(i)`, and may
6//! reference inputs, earlier vals, and constants.
7
8use super::target::AnalogCellKind;
9
10/// An operand of an analog tile: a plane input (a DAC channel), a prior tile's output net, or a
11/// compile-time constant (a programmed reference).
12///
13/// Physically every operand is a DIFFERENTIAL net (`Vsignal = V⁺ − V⁻` around mid-rail): the
14/// signed math value is the differential swing, which is why signed weights cost only a wire
15/// swap and why the ±half-span voltage model is realizable on a single-supply process.
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub enum ASrc {
18    In(usize),
19    Val(usize),
20    Const(f64),
21}
22
23impl ASrc {
24    pub fn eval(&self, inputs: &[f64], vals: &[f64]) -> f64 {
25        match self {
26            ASrc::In(k) => inputs[*k],
27            ASrc::Val(k) => vals[*k],
28            ASrc::Const(c) => *c,
29        }
30    }
31}
32
33/// One analog tile. Ideal (math-domain) semantics are given per variant; physical parameters
34/// (gm, C, rails) attach at the target/gain layers, not here.
35#[derive(Clone, Debug, PartialEq)]
36pub enum AnalogCell {
37    /// Kirchhoff current-summing node with programmed input-conductance weights:
38    /// `v = Σ wᵢ·xᵢ`. Signed weights are free (differential wiring).
39    Sum {
40        inputs: Vec<ASrc>,
41        weights: Vec<f64>,
42    },
43    /// Four-quadrant transconductance multiplier for variable×variable: `v = a·b`.
44    Mul { a: ASrc, b: ASrc },
45    /// Programmed-conductance constant gain: `v = k·a`.
46    GainConst { a: ASrc, k: f64 },
47    /// Comparator-driven analog switch: `v = if c < 0 { t } else { f }` (the O5 tile). The
48    /// condition is a DIFFERENCE net (the lowering emits `c = ca − cb` as a Sum); strict vs
49    /// inclusive comparison is indistinguishable on the analog plane — the boundary lives
50    /// inside the comparator's ambiguity band, which the budget prices (`CompareBand`).
51    Select { c: ASrc, t: ASrc, f: ASrc },
52    /// Translinear exponential (subthreshold-MOS / diff-pair current law): `v = eˣ`. NOT
53    /// scale-invariant — translinear nets run at unit block scale (volt ≡ math), enforced by
54    /// the schedule (the O6 pinning rule).
55    Exp { a: ASrc },
56    /// Translinear logarithm (forward-diode law): `v = ln x`, domain `x > 0` — certified
57    /// fail-closed by the range rules.
58    Log { a: ASrc },
59    /// Differential-pair saturation: `v = tanh x`.
60    Tanh { a: ASrc },
61    /// Translinear square root (Gilbert 1975 loop): `v = √x`, domain `x > 0` for a bounded
62    /// error slope (the budget's Lipschitz factor diverges at 0).
63    Sqrt { a: ASrc },
64    /// Translinear ratio (Gilbert 1975): `v = a / b`; the divisor's certified box must clear
65    /// zero (`DivisorRangeSpansZero` otherwise — the starvation refusal).
66    Div { a: ASrc, b: ASrc },
67}
68
69impl AnalogCell {
70    /// The tile kind this cell demands of a target.
71    pub fn kind(&self) -> AnalogCellKind {
72        match self {
73            AnalogCell::Sum { .. } => AnalogCellKind::Sum,
74            AnalogCell::Mul { .. } => AnalogCellKind::Mul,
75            AnalogCell::GainConst { .. } => AnalogCellKind::GainConst,
76            AnalogCell::Select { .. } => AnalogCellKind::Select,
77            AnalogCell::Exp { .. } => AnalogCellKind::Exp,
78            AnalogCell::Log { .. } => AnalogCellKind::Log,
79            AnalogCell::Tanh { .. } => AnalogCellKind::Tanh,
80            AnalogCell::Sqrt { .. } => AnalogCellKind::Sqrt,
81            AnalogCell::Div { .. } => AnalogCellKind::Div,
82        }
83    }
84
85    /// Ideal math-domain evaluation.
86    pub fn eval(&self, inputs: &[f64], vals: &[f64]) -> f64 {
87        match self {
88            AnalogCell::Sum {
89                inputs: xs,
90                weights,
91            } => xs
92                .iter()
93                .zip(weights)
94                .map(|(x, w)| w * x.eval(inputs, vals))
95                .sum(),
96            AnalogCell::Mul { a, b } => a.eval(inputs, vals) * b.eval(inputs, vals),
97            AnalogCell::GainConst { a, k } => k * a.eval(inputs, vals),
98            AnalogCell::Select { c, t, f } => {
99                if c.eval(inputs, vals) < 0.0 {
100                    t.eval(inputs, vals)
101                } else {
102                    f.eval(inputs, vals)
103                }
104            }
105            AnalogCell::Exp { a } => a.eval(inputs, vals).exp(),
106            AnalogCell::Log { a } => a.eval(inputs, vals).ln(),
107            AnalogCell::Tanh { a } => a.eval(inputs, vals).tanh(),
108            AnalogCell::Sqrt { a } => a.eval(inputs, vals).sqrt(),
109            AnalogCell::Div { a, b } => a.eval(inputs, vals) / b.eval(inputs, vals),
110        }
111    }
112}
113
114/// A flat SSA analog netlist: the mapped region's dataflow, output tapped at `out`.
115#[derive(Clone, Debug, PartialEq)]
116pub struct AnalogNetlist {
117    /// Plane input arity (the region function's float parameters, in declaration order).
118    pub n_in: usize,
119    /// SSA tiles: cell `i` defines `Val(i)`.
120    pub cells: Vec<AnalogCell>,
121    /// The region's output net (the ADC tap).
122    pub out: ASrc,
123}