Skip to main content

pedalkernel/compiler/components/
diodes.rs

1//! Diode component structs: Diode, DiodePair, Zener, Neon.
2
3use hashbrown::HashMap;
4
5use crate::compiler::classify::NonlinearKind;
6use crate::compiler::component::{
7    Component, ComponentEdge, EdgeKind, GraphRole, KMethodSpec, PinConfig, PinDirection,
8    SignalTerminals, SolverMethod, StampResult, K_AXIS_INCIDENT_1D,
9};
10use crate::compiler::graph::NodeId;
11use crate::compiler::validate::Severity;
12use crate::dsl::{DiodeType, NeonType};
13use crate::tree::MnaSystem;
14
15use super::impl_component_dyn;
16
17// ═══════════════════════════════════════════════════════════════════════════
18// Diode
19// ═══════════════════════════════════════════════════════════════════════════
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct Diode {
23    pub diode_type: DiodeType,
24}
25
26impl Component for Diode {
27    impl_component_dyn!();
28
29    fn type_tag(&self) -> &'static str {
30        "diode"
31    }
32
33    fn signal_terminals(&self) -> SignalTerminals {
34        SignalTerminals::TwoPort {
35            input: "a",
36            output: "b",
37        }
38    }
39
40    fn is_passive(&self) -> bool {
41        false
42    }
43
44    fn is_nonlinear(&self) -> bool {
45        true
46    }
47
48    fn pin_config(&self) -> PinConfig {
49        PinConfig {
50            valid_pins: &["a", "b"],
51            aliases: &[],
52        }
53    }
54
55    fn graph_role(&self) -> GraphRole {
56        GraphRole::Edge {
57            pin_a: "a",
58            pin_b: "b",
59        }
60    }
61
62    fn stamp_mna(
63        &self,
64        _comp_id: &str,
65        _n1: Option<usize>,
66        _n2: Option<usize>,
67        _mna: &mut MnaSystem,
68        _sample_rate: f64,
69    ) -> StampResult {
70        StampResult::Skip
71    }
72
73    fn edges(&self) -> Vec<ComponentEdge> {
74        vec![ComponentEdge {
75            pin_a: "a",
76            pin_b: "b",
77            kind: EdgeKind::Nonlinear,
78            port_group: None,
79        }]
80    }
81
82    fn classify_nonlinear(
83        &self,
84        _comp_id: &str,
85        node_a: NodeId,
86        node_b: NodeId,
87        gnd_node: NodeId,
88        _node_names: &HashMap<String, NodeId>,
89    ) -> Option<(NonlinearKind, Vec<NodeId>)> {
90        let diode_jn = if node_b == gnd_node {
91            node_a
92        } else if node_a == gnd_node {
93            node_b
94        } else {
95            node_a
96        };
97        Some((NonlinearKind::SingleDiode(self.diode_type), vec![diode_jn]))
98    }
99
100    fn footprint_ref(&self) -> (&'static str, &'static str) {
101        ("Device:D", "D")
102    }
103
104    fn symbol_name(&self) -> &'static str {
105        "diode"
106    }
107    fn layout_class(&self) -> &'static str {
108        "diode"
109    }
110
111    fn pin_direction(&self, pin: &str) -> PinDirection {
112        match pin {
113            "a" => PinDirection::Input,
114            "b" => PinDirection::Output,
115            _ => PinDirection::Bidirectional,
116        }
117    }
118
119    fn solver_hint(&self) -> Option<SolverMethod> {
120        Some(SolverMethod::WrightOmega)
121    }
122
123    fn is_diode_family(&self) -> bool {
124        true
125    }
126    fn diode_type(&self) -> Option<DiodeType> {
127        Some(self.diode_type)
128    }
129    fn k_method_spec(&self) -> Option<KMethodSpec> {
130        Some(KMethodSpec {
131            axes: K_AXIS_INCIDENT_1D,
132            reason: "diode: 1D memoryless monotonic I-V",
133        })
134    }
135}
136
137// ═══════════════════════════════════════════════════════════════════════════
138// DiodePair
139// ═══════════════════════════════════════════════════════════════════════════
140
141#[derive(Debug, Clone, PartialEq)]
142pub struct DiodePair {
143    pub diode_type: DiodeType,
144}
145
146impl Component for DiodePair {
147    impl_component_dyn!();
148
149    fn type_tag(&self) -> &'static str {
150        "diode pair"
151    }
152
153    fn signal_terminals(&self) -> SignalTerminals {
154        SignalTerminals::TwoPort {
155            input: "a",
156            output: "b",
157        }
158    }
159
160    fn is_passive(&self) -> bool {
161        false
162    }
163
164    fn is_nonlinear(&self) -> bool {
165        true
166    }
167
168    fn pin_config(&self) -> PinConfig {
169        PinConfig {
170            valid_pins: &["a", "b"],
171            aliases: &[],
172        }
173    }
174
175    fn graph_role(&self) -> GraphRole {
176        GraphRole::Edge {
177            pin_a: "a",
178            pin_b: "b",
179        }
180    }
181
182    fn stamp_mna(
183        &self,
184        _comp_id: &str,
185        _n1: Option<usize>,
186        _n2: Option<usize>,
187        _mna: &mut MnaSystem,
188        _sample_rate: f64,
189    ) -> StampResult {
190        StampResult::Skip
191    }
192
193    fn edges(&self) -> Vec<ComponentEdge> {
194        vec![ComponentEdge {
195            pin_a: "a",
196            pin_b: "b",
197            kind: EdgeKind::Nonlinear,
198            port_group: None,
199        }]
200    }
201
202    fn classify_nonlinear(
203        &self,
204        _comp_id: &str,
205        node_a: NodeId,
206        node_b: NodeId,
207        gnd_node: NodeId,
208        _node_names: &HashMap<String, NodeId>,
209    ) -> Option<(NonlinearKind, Vec<NodeId>)> {
210        let diode_jn = if node_b == gnd_node {
211            node_a
212        } else if node_a == gnd_node {
213            node_b
214        } else {
215            node_a
216        };
217        Some((NonlinearKind::DiodePair(self.diode_type), vec![diode_jn]))
218    }
219
220    fn footprint_ref(&self) -> (&'static str, &'static str) {
221        ("Device:D", "D")
222    }
223
224    fn symbol_name(&self) -> &'static str {
225        "diode"
226    }
227    fn layout_class(&self) -> &'static str {
228        "diode_pair"
229    }
230
231    fn pin_direction(&self, pin: &str) -> PinDirection {
232        match pin {
233            "a" => PinDirection::Input,
234            "b" => PinDirection::Output,
235            _ => PinDirection::Bidirectional,
236        }
237    }
238
239    fn solver_hint(&self) -> Option<SolverMethod> {
240        Some(SolverMethod::WrightOmega)
241    }
242
243    fn is_diode_family(&self) -> bool {
244        true
245    }
246    fn diode_type(&self) -> Option<DiodeType> {
247        Some(self.diode_type)
248    }
249    fn k_method_spec(&self) -> Option<KMethodSpec> {
250        Some(KMethodSpec {
251            axes: K_AXIS_INCIDENT_1D,
252            reason: "diode pair: 1D memoryless symmetric I-V",
253        })
254    }
255}
256
257// ═══════════════════════════════════════════════════════════════════════════
258// Zener
259// ═══════════════════════════════════════════════════════════════════════════
260
261#[derive(Debug, Clone, PartialEq)]
262pub struct Zener {
263    pub breakdown_voltage: f64,
264}
265
266impl Component for Zener {
267    impl_component_dyn!();
268
269    fn type_tag(&self) -> &'static str {
270        "zener diode"
271    }
272
273    fn signal_terminals(&self) -> SignalTerminals {
274        SignalTerminals::TwoPort {
275            input: "a",
276            output: "b",
277        }
278    }
279
280    fn is_passive(&self) -> bool {
281        false
282    }
283
284    fn is_nonlinear(&self) -> bool {
285        true
286    }
287
288    fn pin_config(&self) -> PinConfig {
289        PinConfig {
290            valid_pins: &["a", "b"],
291            aliases: &[],
292        }
293    }
294
295    fn graph_role(&self) -> GraphRole {
296        GraphRole::Edge {
297            pin_a: "a",
298            pin_b: "b",
299        }
300    }
301
302    fn stamp_mna(
303        &self,
304        _comp_id: &str,
305        _n1: Option<usize>,
306        _n2: Option<usize>,
307        _mna: &mut MnaSystem,
308        _sample_rate: f64,
309    ) -> StampResult {
310        StampResult::Skip
311    }
312
313    fn validate_values(&self, comp_id: &str) -> Vec<(Severity, String)> {
314        let mut w = vec![];
315        if self.breakdown_voltage <= 0.0 {
316            w.push((
317                Severity::Error,
318                format!(
319                    "Zener '{}' has non-positive breakdown voltage {:.2} V",
320                    comp_id, self.breakdown_voltage
321                ),
322            ));
323        }
324        w
325    }
326
327    fn edges(&self) -> Vec<ComponentEdge> {
328        vec![ComponentEdge {
329            pin_a: "a",
330            pin_b: "b",
331            kind: EdgeKind::Nonlinear,
332            port_group: None,
333        }]
334    }
335
336    fn classify_nonlinear(
337        &self,
338        _comp_id: &str,
339        _node_a: NodeId,
340        node_b: NodeId,
341        gnd_node: NodeId,
342        _node_names: &HashMap<String, NodeId>,
343    ) -> Option<(NonlinearKind, Vec<NodeId>)> {
344        let jn = if node_b == gnd_node { _node_a } else { node_b };
345        Some((
346            NonlinearKind::Zener {
347                voltage: self.breakdown_voltage,
348            },
349            vec![jn],
350        ))
351    }
352
353    fn footprint_ref(&self) -> (&'static str, &'static str) {
354        ("Device:D_Zener", "D")
355    }
356
357    fn symbol_name(&self) -> &'static str {
358        "zener"
359    }
360    fn layout_class(&self) -> &'static str {
361        "zener"
362    }
363    fn display_value(&self) -> Option<String> {
364        Some(format!("{}V", self.breakdown_voltage))
365    }
366
367    fn pin_direction(&self, pin: &str) -> PinDirection {
368        match pin {
369            "a" => PinDirection::Input,
370            "b" => PinDirection::Output,
371            _ => PinDirection::Bidirectional,
372        }
373    }
374
375    fn is_diode_family(&self) -> bool {
376        true
377    }
378    fn k_method_spec(&self) -> Option<KMethodSpec> {
379        Some(KMethodSpec {
380            axes: K_AXIS_INCIDENT_1D,
381            reason: "zener: 1D memoryless I-V with reverse breakdown",
382        })
383    }
384}
385
386// ═══════════════════════════════════════════════════════════════════════════
387// Neon
388// ═══════════════════════════════════════════════════════════════════════════
389
390#[derive(Debug, Clone, PartialEq)]
391pub struct Neon {
392    pub neon_type: NeonType,
393}
394
395impl Component for Neon {
396    impl_component_dyn!();
397
398    fn type_tag(&self) -> &'static str {
399        "neon bulb"
400    }
401
402    fn signal_terminals(&self) -> SignalTerminals {
403        SignalTerminals::TwoPort {
404            input: "a",
405            output: "b",
406        }
407    }
408
409    fn is_passive(&self) -> bool {
410        false
411    }
412
413    fn is_nonlinear(&self) -> bool {
414        true
415    }
416
417    fn pin_config(&self) -> PinConfig {
418        PinConfig {
419            valid_pins: &["a", "b"],
420            aliases: &[],
421        }
422    }
423
424    fn graph_role(&self) -> GraphRole {
425        GraphRole::Edge {
426            pin_a: "a",
427            pin_b: "b",
428        }
429    }
430
431    fn stamp_mna(
432        &self,
433        _comp_id: &str,
434        _n1: Option<usize>,
435        _n2: Option<usize>,
436        _mna: &mut MnaSystem,
437        _sample_rate: f64,
438    ) -> StampResult {
439        StampResult::Skip
440    }
441
442    fn edges(&self) -> Vec<ComponentEdge> {
443        vec![ComponentEdge {
444            pin_a: "a",
445            pin_b: "b",
446            kind: EdgeKind::Nonlinear,
447            port_group: None,
448        }]
449    }
450
451    fn footprint_ref(&self) -> (&'static str, &'static str) {
452        match self.neon_type {
453            NeonType::Ne2 | NeonType::Ne51 | NeonType::Ne83 => ("Device:Lamp_Neon", "NE"),
454        }
455    }
456
457    fn symbol_name(&self) -> &'static str {
458        "neon"
459    }
460    fn layout_class(&self) -> &'static str {
461        "neon"
462    }
463    fn k_method_candidacy(&self) -> (bool, usize, &'static str) {
464        // Neon bulbs have hysteresis (strike vs maintain voltage) —
465        // the I-V curve folds back, violating the monotonicity requirement.
466        (
467            false,
468            1,
469            "neon: hysteretic I-V (strike/maintain) — not monotonic",
470        )
471    }
472}