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}