Skip to main content

pedalkernel/
model_lookup.rs

1//! Model lookup functions — bridges pedalkernel's model DB to pedalkernel-rt types.
2//!
3//! pedalkernel-rt defines the model structs (JfetModel, TriodeModel, etc.) but
4//! cannot contain the SPICE model database (it's no_std). These free functions
5//! look up models by name from pedalkernel's embedded model DB and convert them
6//! to the runtime types.
7
8use crate::dsl::TransformerConfig;
9use crate::models::{
10    bjt_by_name, jfet_by_name, opamp_by_name, pentode_by_name, transformer_by_name, triode_by_name,
11    SpiceBjtModel, SpiceJfetModel, SpiceOpAmpModel, SpicePentodeModel, SpiceTransformerModel,
12    SpiceTriodeModel,
13};
14use pedalkernel_rt::elements::nonlinear::{
15    GummelPoonModel, JfetModel, OpAmpModel, PentodeModel, TriodeModel,
16};
17
18// ── JFET ────────────────────────────────────────────────────────────────
19
20/// Look up a JFET model by name from the embedded SPICE model file.
21///
22/// Panics if the model name is not found.
23pub fn jfet_model_by_name(name: &str) -> JfetModel {
24    jfet_try_by_name(name).unwrap_or_else(|| panic!("Unknown JFET model: '{name}'"))
25}
26
27/// Try to look up a JFET model by name. Returns `None` if not found.
28pub fn jfet_try_by_name(name: &str) -> Option<JfetModel> {
29    jfet_by_name(name).map(|s| jfet_from_spice(s))
30}
31
32fn jfet_from_spice(s: &SpiceJfetModel) -> JfetModel {
33    JfetModel {
34        vto: s.vto as crate::Wave,
35        beta: s.beta as crate::Wave,
36        lambda: s.lambda as crate::Wave,
37        gate_is: s.is as crate::Wave,
38        n: s.n as crate::Wave,
39        rd: s.rd as crate::Wave,
40        rs: s.rs as crate::Wave,
41        cgs: s.cgs as crate::Wave,
42        cgd: s.cgd as crate::Wave,
43        is_n_channel: s.is_n_channel,
44    }
45}
46
47// ── Triode ──────────────────────────────────────────────────────────────
48
49/// Look up a triode model by name from the model registry.
50/// Panics if the name is not found.
51pub fn triode_model_by_name(name: &str) -> TriodeModel {
52    triode_try_by_name(name).unwrap_or_else(|| {
53        panic!(
54            "Unknown triode model: '{}'. Use triode_model_names() to list available models.",
55            name
56        )
57    })
58}
59
60/// Look up a triode model by name, returning None if not found.
61pub fn triode_try_by_name(name: &str) -> Option<TriodeModel> {
62    triode_by_name(name).map(|s| triode_from_spice(s))
63}
64
65fn triode_from_spice(spice: &SpiceTriodeModel) -> TriodeModel {
66    TriodeModel {
67        mu: spice.mu as crate::Wave,
68        kp: spice.kp as crate::Wave,
69        kvb: spice.kvb as crate::Wave,
70        ex: spice.ex as crate::Wave,
71        kg1: spice.kg1 as crate::Wave,
72        rp: spice.rp as crate::Wave,
73    }
74}
75
76// ── Pentode ─────────────────────────────────────────────────────────────
77
78/// Look up a pentode model by name from the model registry.
79/// Panics if the name is not found.
80pub fn pentode_model_by_name(name: &str) -> PentodeModel {
81    pentode_try_by_name(name).unwrap_or_else(|| {
82        panic!(
83            "Unknown pentode model: '{}'. Use pentode_model_names() to list available models.",
84            name
85        )
86    })
87}
88
89/// Look up a pentode model by name, returning None if not found.
90pub fn pentode_try_by_name(name: &str) -> Option<PentodeModel> {
91    pentode_by_name(name).map(|s| pentode_from_spice(s))
92}
93
94fn pentode_from_spice(spice: &SpicePentodeModel) -> PentodeModel {
95    PentodeModel {
96        mu: spice.mu as crate::Wave,
97        kp: spice.kp as crate::Wave,
98        kvb: spice.kvb as crate::Wave,
99        ex: spice.ex as crate::Wave,
100        kvb2: spice.kvb2 as crate::Wave,
101        vg2_default: spice.vg2_default as crate::Wave,
102        kg1: spice.kg1 as crate::Wave,
103        kg2: spice.kg2 as crate::Wave,
104        rp: spice.rp as crate::Wave,
105    }
106}
107
108// ── BJT (Gummel-Poon) ──────────────────────────────────────────────────
109
110/// Look up a Gummel-Poon BJT model by name from the model registry.
111/// Panics if the model name is not found.
112pub fn bjt_model_by_name(name: &str) -> GummelPoonModel {
113    bjt_try_by_name(name).unwrap_or_else(|| panic!("Unknown BJT model: '{name}'"))
114}
115
116/// Try to look up a Gummel-Poon model by name. Returns `None` if not found.
117pub fn bjt_try_by_name(name: &str) -> Option<GummelPoonModel> {
118    bjt_by_name(name).map(|s| bjt_from_spice(s))
119}
120
121// Shared ngspice-matching thermal voltage (27 °C, CODATA-2018) — see the
122// rustdoc on `pedalkernel_rt::SPICE_VT_27C` for the BA283 evidence. All
123// junction devices (BJT/diode/JFET/OTA) must use this single constant.
124use pedalkernel_rt::SPICE_VT_27C;
125
126fn bjt_from_spice(spice: &SpiceBjtModel) -> GummelPoonModel {
127    GummelPoonModel {
128        is: spice.is as crate::Wave,
129        bf: spice.bf as crate::Wave,
130        br: spice.br as crate::Wave,
131        nf: spice.nf as crate::Wave,
132        nr: spice.nr as crate::Wave,
133        vt: SPICE_VT_27C,
134        vaf: spice.vaf as crate::Wave,
135        var: spice.var as crate::Wave,
136        ikf: spice.ikf as crate::Wave,
137        ikr: spice.ikr as crate::Wave,
138        ise: spice.ise as crate::Wave,
139        ne: spice.ne as crate::Wave,
140        isc: spice.isc as crate::Wave,
141        nc: spice.nc as crate::Wave,
142        cje: spice.cje as crate::Wave,
143        vje: spice.vje as crate::Wave,
144        mje: spice.mje as crate::Wave,
145        cjc: spice.cjc as crate::Wave,
146        vjc: spice.vjc as crate::Wave,
147        mjc: spice.mjc as crate::Wave,
148        rb: spice.rb as crate::Wave,
149        re: spice.re as crate::Wave,
150        rc: spice.rc as crate::Wave,
151        tf: spice.tf as crate::Wave,
152        tr: spice.tr as crate::Wave,
153        is_pnp: spice.is_pnp,
154    }
155}
156
157// ── Op-Amp ──────────────────────────────────────────────────────────────
158
159/// Look up a compact op-amp model by name from the model registry.
160/// Panics if the model name is not found.
161pub fn opamp_model_by_name(name: &str) -> OpAmpModel {
162    opamp_try_by_name(name).unwrap_or_else(|| panic!("Unknown op-amp model: '{name}'"))
163}
164
165/// Try to look up a compact op-amp model by name. Returns `None` if not found.
166pub fn opamp_try_by_name(name: &str) -> Option<OpAmpModel> {
167    opamp_by_name(name).map(opamp_from_spice)
168}
169
170fn opamp_from_spice(spice: &SpiceOpAmpModel) -> OpAmpModel {
171    OpAmpModel {
172        open_loop_gain: spice.open_loop_gain as crate::Wave,
173        gbw: spice.gbw as crate::Wave,
174        slew_rate: spice.slew_rate as crate::Wave,
175        v_rail_pos: spice.v_rail_pos as crate::Wave,
176        v_rail_neg: spice.v_rail_neg as crate::Wave,
177        output_impedance: spice.output_impedance as crate::Wave,
178        output_capacitance: spice.output_capacitance as crate::Wave,
179    }
180}
181
182/// Convert from DSL OpAmpType to runtime OpAmpModel.
183pub fn opamp_model_from_type(ot: &crate::dsl::OpAmpType) -> OpAmpModel {
184    opamp_model_by_name(opamp_type_model_name(ot))
185}
186
187/// Return an OTA transconductance from the model registry.
188pub fn ota_gm_from_type(ot: &crate::dsl::OpAmpType) -> Option<f64> {
189    let model = opamp_by_name(opamp_type_model_name(ot))?;
190    model.is_ota.then_some(model.ota_gm)
191}
192
193fn opamp_type_model_name(ot: &crate::dsl::OpAmpType) -> &'static str {
194    use crate::dsl::OpAmpType;
195    match ot {
196        OpAmpType::Generic => "GENERIC",
197        OpAmpType::Tl071 => "TL071",
198        OpAmpType::Tl072 => "TL072",
199        OpAmpType::Tl082 => "TL082",
200        OpAmpType::Jrc4558 => "JRC4558",
201        OpAmpType::Rc4558 => "RC4558",
202        OpAmpType::Lm308 => "LM308",
203        OpAmpType::Lm741 => "LM741",
204        OpAmpType::Ne5532 => "NE5532",
205        OpAmpType::Op07 => "OP07",
206        OpAmpType::Ca3080 => "CA3080",
207    }
208}
209
210// ── Transformer ─────────────────────────────────────────────────────────
211
212/// Resolve a DSL transformer instance against the embedded model registry.
213///
214/// Explicit scalar fields on `cfg` override model-library defaults. The
215/// current DSL cannot distinguish explicit default zeros for DCR/Cp from
216/// unspecified values, so zero-valued DCR/Cp intentionally remain explicit
217/// only for generic `transformer(ratio, Lp, ...)` instances; model-backed
218/// instances inherit model DCR/Cp unless a non-zero override is present.
219pub fn transformer_config_from_dsl(cfg: &TransformerConfig) -> TransformerConfig {
220    let Some(model_name) = cfg.model.as_deref() else {
221        return cfg.clone();
222    };
223
224    let model = transformer_by_name(model_name)
225        .unwrap_or_else(|| panic!("Unknown transformer model: '{model_name}'"));
226    let mut resolved = transformer_config_from_model(model, cfg.turns_ratio);
227
228    resolved.primary_type = cfg.primary_type;
229    resolved.secondary_type = cfg.secondary_type;
230    resolved.tertiary_turns_ratio = cfg.tertiary_turns_ratio;
231
232    // Instance overrides.
233    if cfg.primary_inductance > 0.0 {
234        resolved.primary_inductance = cfg.primary_inductance;
235    }
236    if cfg.primary_dcr > 0.0 {
237        resolved.primary_dcr = cfg.primary_dcr;
238    }
239    if cfg.secondary_dcr > 0.0 {
240        resolved.secondary_dcr = cfg.secondary_dcr;
241    }
242    if cfg.capacitance > 0.0 {
243        resolved.capacitance = cfg.capacitance;
244    }
245    if cfg.coupling > 0.0 {
246        resolved.coupling = cfg.coupling;
247    }
248    if cfg.primary_leakage.is_some() {
249        resolved.primary_leakage = cfg.primary_leakage;
250    }
251    if cfg.secondary_leakage.is_some() {
252        resolved.secondary_leakage = cfg.secondary_leakage;
253    }
254    if cfg.magnetizing_inductance.is_some() {
255        resolved.magnetizing_inductance = cfg.magnetizing_inductance;
256    }
257    if cfg.core_loss_resistance.is_some() {
258        resolved.core_loss_resistance = cfg.core_loss_resistance;
259    }
260    if cfg.core_primary_turns.is_some() {
261        resolved.core_primary_turns = cfg.core_primary_turns;
262    }
263    if cfg.core_area.is_some() {
264        resolved.core_area = cfg.core_area;
265    }
266    if cfg.core_path_length.is_some() {
267        resolved.core_path_length = cfg.core_path_length;
268    }
269    if cfg.core_gap.is_some() {
270        resolved.core_gap = cfg.core_gap;
271    }
272    if cfg.dc_bias_current.is_some() {
273        resolved.dc_bias_current = cfg.dc_bias_current;
274    }
275    if cfg.ja_ms.is_some() {
276        resolved.ja_ms = cfg.ja_ms;
277    }
278    if cfg.ja_a.is_some() {
279        resolved.ja_a = cfg.ja_a;
280    }
281    if cfg.ja_alpha.is_some() {
282        resolved.ja_alpha = cfg.ja_alpha;
283    }
284    if cfg.ja_k.is_some() {
285        resolved.ja_k = cfg.ja_k;
286    }
287    if cfg.ja_c.is_some() {
288        resolved.ja_c = cfg.ja_c;
289    }
290
291    resolved.model = Some(model.name.clone());
292    resolved
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn transformer_model_values_merge_with_overrides() {
301        let mut cfg = TransformerConfig::with_model(10.0, "JT11P1".to_string());
302        cfg.magnetizing_inductance = Some(8.0);
303        cfg.core_loss_resistance = Some(250_000.0);
304        cfg.capacitance = 100e-12;
305
306        let resolved = transformer_config_from_dsl(&cfg);
307        assert_eq!(resolved.model.as_deref(), Some("JT11P1"));
308        assert!((resolved.turns_ratio - 10.0).abs() < 1e-12);
309        assert!((resolved.primary_inductance - 2.0).abs() < 1e-12);
310        assert!((resolved.primary_dcr - 75.0).abs() < 1e-12);
311        assert!((resolved.magnetizing_inductance.unwrap() - 8.0).abs() < 1e-12);
312        assert!((resolved.core_loss_resistance.unwrap() - 250_000.0).abs() < 1e-6);
313        assert!((resolved.capacitance - 100e-12).abs() < 1e-21);
314    }
315
316    #[test]
317    fn transformer_model_propagates_ja_core_fields() {
318        let cfg = TransformerConfig::with_model(26.0, "OT-DEMO-SE".to_string());
319        let resolved = transformer_config_from_dsl(&cfg);
320
321        assert_eq!(resolved.model.as_deref(), Some("OT-DEMO-SE"));
322        assert!((resolved.core_primary_turns.unwrap() - 2000.0).abs() < 1e-12);
323        assert!((resolved.core_area.unwrap() - 2.0e-4).abs() < 1e-18);
324        assert!((resolved.core_path_length.unwrap() - 0.10).abs() < 1e-12);
325        assert!((resolved.core_gap.unwrap() - 1.0e-3).abs() < 1e-18);
326        assert!((resolved.dc_bias_current.unwrap() - 45.0e-3).abs() < 1e-18);
327        assert!((resolved.ja_ms.unwrap() - 1.6e6).abs() < 1e-6);
328        assert!((resolved.ja_a.unwrap() - 1100.0).abs() < 1e-12);
329        assert!((resolved.ja_alpha.unwrap() - 1.6e-3).abs() < 1e-18);
330        assert!((resolved.ja_k.unwrap() - 400.0).abs() < 1e-12);
331        assert!((resolved.ja_c.unwrap() - 0.2).abs() < 1e-12);
332    }
333}
334
335fn transformer_config_from_model(
336    model: &SpiceTransformerModel,
337    turns_ratio: f64,
338) -> TransformerConfig {
339    TransformerConfig {
340        model: Some(model.name.clone()),
341        turns_ratio,
342        primary_inductance: model.primary_inductance,
343        primary_dcr: model.primary_dcr,
344        secondary_dcr: model.secondary_dcr,
345        capacitance: model.capacitance,
346        coupling: model.coupling,
347        primary_leakage: Some(model.primary_leakage),
348        secondary_leakage: Some(model.secondary_leakage),
349        magnetizing_inductance: Some(model.magnetizing_inductance),
350        core_loss_resistance: (model.core_loss_resistance > 0.0)
351            .then_some(model.core_loss_resistance),
352        core_primary_turns: (model.primary_turns > 0.0).then_some(model.primary_turns),
353        core_area: (model.core_area > 0.0).then_some(model.core_area),
354        core_path_length: (model.magnetic_path_length > 0.0).then_some(model.magnetic_path_length),
355        core_gap: (model.gap_length > 0.0).then_some(model.gap_length),
356        dc_bias_current: (model.dc_bias_current != 0.0).then_some(model.dc_bias_current),
357        ja_ms: (model.ja_ms > 0.0).then_some(model.ja_ms),
358        ja_a: (model.ja_a > 0.0).then_some(model.ja_a),
359        ja_alpha: (model.ja_alpha != 0.0).then_some(model.ja_alpha),
360        ja_k: (model.ja_k > 0.0).then_some(model.ja_k),
361        ja_c: (model.ja_c > 0.0).then_some(model.ja_c),
362        ..Default::default()
363    }
364}