Skip to main content

logicaffeine_compile/codegen_ana/
spice_emit.rs

1//! Behavioral ngspice emission — the analog lane's "iverilog tier".
2//!
3//! The emitted netlist is a BEHAVIORAL macromodel: each tile is a controlled source computing
4//! its ideal volt-domain equation (Kirchhoff sum = weighted B-source, the four-quadrant
5//! multiplier = `V(a)·V(b)/Vref`, constant gain = `k·V(a)`), inputs are DC sources at the block
6//! scale, and the operating point is the settled plane. No transistor models are invented —
7//! the sky130 transistor tier arrives PDK-gated with real device cards under the same node
8//! names. Structural honesty: simulator tolerances are PINNED IN THE NETLIST (`.options`), and
9//! the comparison term a test may add derives from those pinned values, never hand-tuning.
10
11use super::cell::{ASrc, AnalogCell};
12use super::gain::GainSchedule;
13use super::target::AnalogTarget;
14
15/// The pinned simulator options — tests read these back from the emitted netlist to derive
16/// their simulator-discretization term.
17pub const SPICE_RELTOL: f64 = 1.0e-4;
18pub const SPICE_ABSTOL: f64 = 1.0e-12;
19pub const SPICE_VNTOL: f64 = 1.0e-9;
20
21/// Emit a DC operating-point netlist for one input point: sources at the block scale, one
22/// behavioral element per tile, the output node written via `wrdata`.
23pub fn emit_spice_dc(
24    schedule: &GainSchedule,
25    target: &AnalogTarget,
26    inputs: &[f64],
27    out_path: &str,
28) -> String {
29    let netlist = &schedule.quantized;
30    assert_eq!(inputs.len(), netlist.n_in);
31    let s = f64::powi(2.0, schedule.scale_log2);
32    let mut o = String::new();
33    o.push_str("* codegen_ana behavioral DC netlist (controlled-source macromodel)\n");
34    o.push_str(&format!(
35        "* target: {} | block scale 2^{} V/math | Vref {} V\n",
36        target.name, schedule.scale_log2, schedule.mul_vref_v
37    ));
38    o.push_str(&format!(
39        ".options reltol={SPICE_RELTOL} abstol={SPICE_ABSTOL} vntol={SPICE_VNTOL}\n"
40    ));
41    for (k, x) in inputs.iter().enumerate() {
42        o.push_str(&format!("VIN{k} in{k} 0 DC {}\n", x * s));
43    }
44    let node = |src: &ASrc| -> String {
45        match src {
46            ASrc::In(k) => format!("in{k}"),
47            ASrc::Val(v) => format!("n{v}"),
48            ASrc::Const(c) => format!("({})", c * s), // a programmed reference at the block scale
49        }
50    };
51    let volt = |src: &ASrc| -> String {
52        match src {
53            ASrc::Const(c) => format!("({})", c * s),
54            other => format!("V({})", node(other)),
55        }
56    };
57    for (i, cell) in netlist.cells.iter().enumerate() {
58        match cell {
59            AnalogCell::Exp { a } => {
60                o.push_str(&format!("BEXP{i} n{i} 0 V = exp({})\n", volt(a)));
61            }
62            AnalogCell::Log { a } => {
63                o.push_str(&format!("BLOG{i} n{i} 0 V = ln({})\n", volt(a)));
64            }
65            AnalogCell::Tanh { a } => {
66                o.push_str(&format!("BTANH{i} n{i} 0 V = tanh({})\n", volt(a)));
67            }
68            AnalogCell::Sqrt { a } => {
69                o.push_str(&format!("BSQRT{i} n{i} 0 V = sqrt({})\n", volt(a)));
70            }
71            AnalogCell::Div { a, b } => {
72                o.push_str(&format!(
73                    "BDIV{i} n{i} 0 V = ({}) / ({})\n",
74                    volt(a),
75                    volt(b)
76                ));
77            }
78            AnalogCell::Select { c, t, f } => {
79                o.push_str(&format!(
80                    "BSEL{i} n{i} 0 V = (({}) < 0) ? ({}) : ({})\n",
81                    volt(c),
82                    volt(t),
83                    volt(f)
84                ));
85            }
86            AnalogCell::Sum {
87                inputs: xs,
88                weights,
89            } => {
90                let terms: Vec<String> = xs
91                    .iter()
92                    .zip(weights)
93                    .map(|(x, w)| format!("({w})*{}", volt(x)))
94                    .collect();
95                o.push_str(&format!("BSUM{i} n{i} 0 V = {}\n", terms.join(" + ")));
96            }
97            AnalogCell::Mul { a, b } => {
98                o.push_str(&format!(
99                    "BMUL{i} n{i} 0 V = {}*{}/({})\n",
100                    volt(a),
101                    volt(b),
102                    schedule.mul_vref_v
103                ));
104            }
105            AnalogCell::GainConst { a, k } => {
106                o.push_str(&format!("BGAIN{i} n{i} 0 V = ({k})*{}\n", volt(a)));
107            }
108        }
109    }
110    let out_node = node(&netlist.out);
111    o.push_str(".op\n.control\nrun\n");
112    o.push_str(&format!("wrdata {out_path} v({out_node})\n"));
113    o.push_str("quit\n.endc\n.end\n");
114    o
115}