Skip to main content

pedalkernel/compiler/components/
passives.rs

1//! Passive component structs: Resistor, Capacitor, Inductor, Potentiometer,
2//! Tempco, CapSwitched, InductorSwitched, ResistorSwitched.
3
4use crate::compiler::component::{
5    Component, ComponentEdge, ControlParam, ControlParamKind, EdgeKind, GraphRole, PinConfig,
6    PinDirection, StampContext, StampResult, OPEN_CIRCUIT_R,
7};
8use crate::compiler::dyn_node::DynNode;
9use crate::compiler::validate::Severity;
10use crate::dsl::{CapConfig, PotTaper};
11use crate::tree::MnaSystem;
12
13use super::impl_component_dyn;
14
15// ═══════════════════════════════════════════════════════════════════════════
16// Resistor
17// ═══════════════════════════════════════════════════════════════════════════
18
19#[derive(Debug, Clone, PartialEq)]
20pub struct Resistor {
21    pub value: f64,
22}
23
24impl Component for Resistor {
25    impl_component_dyn!();
26
27    fn type_tag(&self) -> &'static str {
28        "resistor"
29    }
30
31    fn is_passive(&self) -> bool {
32        true
33    }
34
35    fn pin_config(&self) -> PinConfig {
36        PinConfig {
37            valid_pins: &["a", "b"],
38            aliases: &[],
39        }
40    }
41
42    fn graph_role(&self) -> GraphRole {
43        GraphRole::Edge {
44            pin_a: "a",
45            pin_b: "b",
46        }
47    }
48
49    fn stamp_mna(
50        &self,
51        _comp_id: &str,
52        n1: Option<usize>,
53        n2: Option<usize>,
54        mna: &mut MnaSystem,
55        _sample_rate: f64,
56    ) -> StampResult {
57        mna.stamp_resistor(n1, n2, self.value as crate::Wave);
58        StampResult::Stamped
59    }
60
61    fn make_leaf(&self, comp_id: &str, _sample_rate: f64) -> Option<DynNode> {
62        if self.value.is_infinite() {
63            Some(DynNode::Resistor(Some(comp_id.to_string()), OPEN_CIRCUIT_R as crate::Wave))
64        } else {
65            Some(DynNode::Resistor(Some(comp_id.to_string()), self.value as crate::Wave))
66        }
67    }
68
69    fn edges(&self) -> Vec<ComponentEdge> {
70        vec![ComponentEdge {
71            pin_a: "a",
72            pin_b: "b",
73            kind: EdgeKind::Linear,
74            port_group: None,
75        }]
76    }
77
78    fn resistance(&self) -> Option<f64> {
79        Some(self.value)
80    }
81
82    fn validate_values(&self, comp_id: &str) -> Vec<(Severity, String)> {
83        let mut w = Vec::new();
84        if self.value <= 0.0 {
85            w.push((
86                Severity::Error,
87                format!(
88                    "Resistor '{}' has non-positive value {:.2} \u{2126}",
89                    comp_id, self.value
90                ),
91            ));
92        } else if self.value < 1.0 {
93            w.push((
94                Severity::Info,
95                format!(
96                    "Resistor '{}' is {:.2} \u{2126} \u{2014} extremely low, did you mean {}k?",
97                    comp_id, self.value, self.value
98                ),
99            ));
100        } else if self.value > 100e6 {
101            w.push((
102                Severity::Info,
103                format!(
104                    "Resistor '{}' is {:.1} M\u{2126} \u{2014} unusually high",
105                    comp_id,
106                    self.value / 1e6
107                ),
108            ));
109        }
110        w
111    }
112
113    fn footprint_ref(&self) -> (&'static str, &'static str) {
114        ("Device:R", "R")
115    }
116
117    fn symbol_name(&self) -> &'static str {
118        "resistor"
119    }
120    fn layout_class(&self) -> &'static str {
121        "resistor"
122    }
123    fn display_value(&self) -> Option<String> {
124        Some(crate::kicad::format_eng(self.value, "Ω"))
125    }
126}
127
128// ═══════════════════════════════════════════════════════════════════════════
129// Capacitor
130// ═══════════════════════════════════════════════════════════════════════════
131
132#[derive(Debug, Clone, PartialEq)]
133pub struct Capacitor {
134    pub config: CapConfig,
135}
136
137impl Component for Capacitor {
138    impl_component_dyn!();
139
140    fn type_tag(&self) -> &'static str {
141        "capacitor"
142    }
143
144    fn is_passive(&self) -> bool {
145        true
146    }
147
148    fn pin_config(&self) -> PinConfig {
149        PinConfig {
150            valid_pins: &["a", "b"],
151            aliases: &[],
152        }
153    }
154
155    fn graph_role(&self) -> GraphRole {
156        GraphRole::Edge {
157            pin_a: "a",
158            pin_b: "b",
159        }
160    }
161
162    fn stamp_mna(
163        &self,
164        comp_id: &str,
165        _n1: Option<usize>,
166        _n2: Option<usize>,
167        _mna: &mut MnaSystem,
168        sample_rate: f64,
169    ) -> StampResult {
170        let rp = 1.0 / (2.0 * sample_rate * self.config.value);
171        StampResult::Reactive {
172            dyn_node: DynNode::Capacitor(Some(comp_id.to_string()), self.config.value as crate::Wave, rp as crate::Wave),
173            rp,
174        }
175    }
176
177    fn make_leaf(&self, comp_id: &str, sample_rate: f64) -> Option<DynNode> {
178        if self.config.leakage.is_some() || self.config.da.is_some() {
179            let rp = 1.0 / (2.0 * sample_rate * self.config.value);
180            let leakage_decay = if let Some(r_leak) = self.config.leakage {
181                let tau = r_leak * self.config.value;
182                let dt = 1.0 / sample_rate;
183                (-dt / tau).exp()
184            } else {
185                1.0
186            };
187            const DA_TIME_CONSTANT: f64 = 0.5;
188            let da_rate = if self.config.da.is_some() {
189                1.0 / (sample_rate * DA_TIME_CONSTANT)
190            } else {
191                0.0
192            };
193            Some(DynNode::LeakyCapacitor(
194                Some(comp_id.to_string()),
195                self.config.value as crate::Wave,
196                rp as crate::Wave,
197                leakage_decay as crate::Wave,
198                self.config.da.map(|v| v as crate::Wave),
199                0.0,
200                da_rate as crate::Wave,
201            ))
202        } else {
203            Some(DynNode::Capacitor(
204                Some(comp_id.to_string()),
205                self.config.value as crate::Wave,
206                (1.0 / (2.0 * sample_rate * self.config.value)) as crate::Wave,
207            ))
208        }
209    }
210
211    fn edges(&self) -> Vec<ComponentEdge> {
212        vec![ComponentEdge {
213            pin_a: "a",
214            pin_b: "b",
215            kind: EdgeKind::Reactive,
216            port_group: None,
217        }]
218    }
219
220    fn capacitance(&self) -> Option<f64> {
221        Some(self.config.value)
222    }
223
224    fn validate_values(&self, comp_id: &str) -> Vec<(Severity, String)> {
225        let mut w = Vec::new();
226        if self.config.value <= 0.0 {
227            w.push((
228                Severity::Error,
229                format!(
230                    "Capacitor '{}' has non-positive value {:.2e} F",
231                    comp_id, self.config.value
232                ),
233            ));
234        } else if self.config.value > 1.0 {
235            w.push((
236                Severity::Warning,
237                format!(
238                    "Capacitor '{}' is {:.2} F \u{2014} over 1 farad, did you forget a unit suffix (n, u, p)?",
239                    comp_id, self.config.value
240                ),
241            ));
242        }
243        if let Some(leak) = self.config.leakage {
244            if leak <= 0.0 {
245                w.push((
246                    Severity::Error,
247                    format!(
248                        "Capacitor '{}' has non-positive leakage resistance {:.2} \u{2126}",
249                        comp_id, leak
250                    ),
251                ));
252            }
253        }
254        if let Some(da) = self.config.da {
255            if !(0.0..=1.0).contains(&da) {
256                w.push((
257                    Severity::Warning,
258                    format!(
259                        "Capacitor '{}' has dielectric absorption {:.3} \u{2014} should be 0.0 to 1.0",
260                        comp_id, da
261                    ),
262                ));
263            }
264        }
265        w
266    }
267
268    fn footprint_ref(&self) -> (&'static str, &'static str) {
269        ("Device:C", "C")
270    }
271
272    fn symbol_name(&self) -> &'static str {
273        "capacitor"
274    }
275    fn layout_class(&self) -> &'static str {
276        "capacitor"
277    }
278    fn display_value(&self) -> Option<String> {
279        Some(crate::kicad::format_eng(self.config.value, "F"))
280    }
281}
282
283// ═══════════════════════════════════════════════════════════════════════════
284// Inductor
285// ═══════════════════════════════════════════════════════════════════════════
286
287#[derive(Debug, Clone, PartialEq)]
288pub struct Inductor {
289    pub value: f64,
290}
291
292impl Component for Inductor {
293    impl_component_dyn!();
294
295    fn type_tag(&self) -> &'static str {
296        "inductor"
297    }
298
299    fn is_passive(&self) -> bool {
300        true
301    }
302
303    fn pin_config(&self) -> PinConfig {
304        PinConfig {
305            valid_pins: &["a", "b"],
306            aliases: &[],
307        }
308    }
309
310    fn graph_role(&self) -> GraphRole {
311        GraphRole::Edge {
312            pin_a: "a",
313            pin_b: "b",
314        }
315    }
316
317    fn stamp_mna(
318        &self,
319        comp_id: &str,
320        _n1: Option<usize>,
321        _n2: Option<usize>,
322        _mna: &mut MnaSystem,
323        sample_rate: f64,
324    ) -> StampResult {
325        let rp = 2.0 * sample_rate * self.value;
326        StampResult::Reactive {
327            dyn_node: DynNode::Inductor(Some(comp_id.to_string()), self.value as crate::Wave, rp as crate::Wave),
328            rp,
329        }
330    }
331
332    fn make_leaf(&self, comp_id: &str, sample_rate: f64) -> Option<DynNode> {
333        Some(DynNode::Inductor(
334            Some(comp_id.to_string()),
335            self.value as crate::Wave,
336            (2.0 * sample_rate * self.value) as crate::Wave,
337        ))
338    }
339
340    fn edges(&self) -> Vec<ComponentEdge> {
341        vec![ComponentEdge {
342            pin_a: "a",
343            pin_b: "b",
344            kind: EdgeKind::Reactive,
345            port_group: None,
346        }]
347    }
348
349    fn inductance(&self) -> Option<f64> {
350        Some(self.value)
351    }
352
353    fn validate_values(&self, comp_id: &str) -> Vec<(Severity, String)> {
354        let mut w = Vec::new();
355        if self.value <= 0.0 {
356            w.push((
357                Severity::Error,
358                format!(
359                    "Inductor '{}' has non-positive value {:.2e} H",
360                    comp_id, self.value
361                ),
362            ));
363        }
364        w
365    }
366
367    fn footprint_ref(&self) -> (&'static str, &'static str) {
368        ("Device:L", "L")
369    }
370
371    fn symbol_name(&self) -> &'static str {
372        "inductor"
373    }
374    fn layout_class(&self) -> &'static str {
375        "inductor"
376    }
377    fn display_value(&self) -> Option<String> {
378        Some(crate::kicad::format_eng(self.value, "H"))
379    }
380}
381
382// ═══════════════════════════════════════════════════════════════════════════
383// Potentiometer
384// ═══════════════════════════════════════════════════════════════════════════
385
386#[derive(Debug, Clone, PartialEq)]
387pub struct Potentiometer {
388    pub max_r: f64,
389    pub taper: PotTaper,
390}
391
392impl Component for Potentiometer {
393    impl_component_dyn!();
394
395    fn type_tag(&self) -> &'static str {
396        "potentiometer"
397    }
398
399    fn ports(&self) -> Vec<(&'static str, &'static str)> {
400        // 2 ports: a→wiper (top half), wiper→b (bottom half)
401        // For 2-terminal use (no wiper connected), graph_role handles it.
402        vec![("a", "w"), ("w", "b")]
403    }
404
405    fn is_passive(&self) -> bool {
406        true
407    }
408
409    fn pin_config(&self) -> PinConfig {
410        PinConfig {
411            valid_pins: &["a", "b", "w", "wiper", "position"],
412            aliases: &[],
413        }
414    }
415
416    fn graph_role(&self) -> GraphRole {
417        GraphRole::Pot
418    }
419
420    fn stamp_mna(
421        &self,
422        comp_id: &str,
423        n1: Option<usize>,
424        n2: Option<usize>,
425        mna: &mut MnaSystem,
426        _sample_rate: f64,
427    ) -> StampResult {
428        let initial_pos = 0.5;
429        let tapered_pos = self.taper.apply(initial_pos);
430        let r = (tapered_pos * self.max_r as crate::Wave).max(1.0);
431        mna.stamp_resistor(n1, n2, r);
432        StampResult::Pot {
433            dyn_node: DynNode::Pot(comp_id.to_string(), self.max_r as crate::Wave, initial_pos, self.taper),
434            initial_conductance: 1.0 / r as f64,
435        }
436    }
437
438    fn stamp_mna_multi(
439        &self,
440        _comp_id: &str,
441        ctx: &mut StampContext,
442        mna: &mut MnaSystem,
443    ) -> StampResult {
444        let initial_pos = 0.5;
445        let tapered_pos = self.taper.apply(initial_pos);
446        // a→w half: R_aw = pos * max_R
447        let r_aw = (tapered_pos * self.max_r as crate::Wave).max(1.0);
448        let n_a = (ctx.pin_to_mna)("a");
449        let n_w = (ctx.pin_to_mna)("w").or_else(|| (ctx.pin_to_mna)("wiper"));
450        mna.stamp_resistor(n_a, n_w, r_aw);
451
452        // w→b half: R_wb = (1-pos) * max_R
453        let r_wb = ((1.0 - tapered_pos) * self.max_r as crate::Wave).max(1.0);
454        let n_b = (ctx.pin_to_mna)("b");
455        mna.stamp_resistor(n_w, n_b, r_wb);
456
457        StampResult::Stamped
458    }
459
460    fn make_leaf(&self, comp_id: &str, _sample_rate: f64) -> Option<DynNode> {
461        let initial_pos = 0.5;
462        let tapered_pos = self.taper.apply(initial_pos);
463        Some(DynNode::Pot(
464            comp_id.to_string(),
465            self.max_r as crate::Wave,
466            initial_pos,
467            self.taper,
468        ))
469    }
470
471    fn is_variable(&self) -> bool {
472        true
473    }
474
475    fn edges(&self) -> Vec<ComponentEdge> {
476        vec![ComponentEdge {
477            pin_a: "a",
478            pin_b: "b",
479            kind: EdgeKind::Linear,
480            port_group: None,
481        }]
482    }
483
484    fn controls(&self) -> Vec<ControlParam> {
485        vec![ControlParam {
486            name: "position",
487            kind: ControlParamKind::PotPosition,
488        }]
489    }
490
491    fn resistance(&self) -> Option<f64> {
492        Some(self.max_r)
493    }
494
495    fn validate_values(&self, comp_id: &str) -> Vec<(Severity, String)> {
496        let mut w = Vec::new();
497        if self.max_r <= 0.0 {
498            w.push((
499                Severity::Error,
500                format!(
501                    "Potentiometer '{}' has non-positive max resistance {:.2} \u{2126}",
502                    comp_id, self.max_r
503                ),
504            ));
505        }
506        w
507    }
508
509    fn footprint_ref(&self) -> (&'static str, &'static str) {
510        ("Device:R_Potentiometer", "RV")
511    }
512
513    fn symbol_name(&self) -> &'static str {
514        "pot"
515    }
516    fn layout_class(&self) -> &'static str {
517        "pot"
518    }
519    fn display_value(&self) -> Option<String> {
520        Some(crate::kicad::format_eng(self.max_r, "\u{2126}"))
521    }
522
523    fn signal_adjacencies(&self) -> Vec<(&'static str, &'static str)> {
524        vec![
525            ("a", "b"),
526            ("a", "wiper"),
527            ("wiper", "b"),
528            ("a", "w"),
529            ("w", "b"),
530        ]
531    }
532
533    fn pin_direction(&self, pin: &str) -> PinDirection {
534        match pin {
535            "a" => PinDirection::Input,
536            "wiper" | "b" | "w" => PinDirection::Output,
537            _ => PinDirection::Bidirectional,
538        }
539    }
540
541    fn is_pot(&self) -> bool {
542        true
543    }
544    fn pot_taper(&self) -> Option<crate::dsl::PotTaper> {
545        Some(self.taper)
546    }
547}
548
549// ═══════════════════════════════════════════════════════════════════════════
550// Tempco
551// ═══════════════════════════════════════════════════════════════════════════
552
553#[derive(Debug, Clone, PartialEq)]
554pub struct Tempco {
555    pub resistance: f64,
556    pub ppm: f64,
557}
558
559impl Component for Tempco {
560    impl_component_dyn!();
561
562    fn type_tag(&self) -> &'static str {
563        "tempco resistor"
564    }
565
566    fn is_passive(&self) -> bool {
567        true
568    }
569
570    fn pin_config(&self) -> PinConfig {
571        PinConfig {
572            valid_pins: &["a", "b"],
573            aliases: &[],
574        }
575    }
576
577    fn graph_role(&self) -> GraphRole {
578        GraphRole::Edge {
579            pin_a: "a",
580            pin_b: "b",
581        }
582    }
583
584    fn stamp_mna(
585        &self,
586        _comp_id: &str,
587        n1: Option<usize>,
588        n2: Option<usize>,
589        mna: &mut MnaSystem,
590        _sample_rate: f64,
591    ) -> StampResult {
592        mna.stamp_resistor(n1, n2, self.resistance as crate::Wave);
593        StampResult::Stamped
594    }
595
596    fn make_leaf(&self, comp_id: &str, _sample_rate: f64) -> Option<DynNode> {
597        Some(DynNode::Resistor(
598            Some(comp_id.to_string()),
599            self.resistance as crate::Wave,
600        ))
601    }
602
603    fn edges(&self) -> Vec<ComponentEdge> {
604        vec![ComponentEdge {
605            pin_a: "a",
606            pin_b: "b",
607            kind: EdgeKind::Linear,
608            port_group: None,
609        }]
610    }
611
612    fn resistance(&self) -> Option<f64> {
613        Some(self.resistance)
614    }
615
616    fn footprint_ref(&self) -> (&'static str, &'static str) {
617        ("Device:R", "RT")
618    }
619
620    fn symbol_name(&self) -> &'static str {
621        "resistor"
622    }
623    fn layout_class(&self) -> &'static str {
624        "tempco"
625    }
626    fn display_value(&self) -> Option<String> {
627        None
628    }
629}
630
631// ═══════════════════════════════════════════════════════════════════════════
632// CapSwitched
633// ═══════════════════════════════════════════════════════════════════════════
634
635#[derive(Debug, Clone, PartialEq)]
636pub struct CapSwitched {
637    pub values: Vec<f64>,
638}
639
640impl Component for CapSwitched {
641    impl_component_dyn!();
642
643    fn type_tag(&self) -> &'static str {
644        "switched capacitor"
645    }
646
647    fn is_passive(&self) -> bool {
648        true
649    }
650
651    fn pin_config(&self) -> PinConfig {
652        PinConfig {
653            valid_pins: &["a", "b"],
654            aliases: &[],
655        }
656    }
657
658    fn graph_role(&self) -> GraphRole {
659        GraphRole::Edge {
660            pin_a: "a",
661            pin_b: "b",
662        }
663    }
664
665    fn stamp_mna(
666        &self,
667        comp_id: &str,
668        _n1: Option<usize>,
669        _n2: Option<usize>,
670        _mna: &mut MnaSystem,
671        sample_rate: f64,
672    ) -> StampResult {
673        if let Some(&c) = self.values.first() {
674            if c.is_finite() && c > 0.0 {
675                let rp = 1.0 / (2.0 * sample_rate * c);
676                return StampResult::Reactive {
677                    dyn_node: DynNode::Capacitor(Some(comp_id.to_string()), c as crate::Wave, rp as crate::Wave),
678                    rp,
679                };
680            }
681        }
682        StampResult::Skip
683    }
684
685    fn make_leaf(&self, comp_id: &str, sample_rate: f64) -> Option<DynNode> {
686        // Position 0 (first listed value) — mirrors stamp_mna above and the
687        // fixed Capacitor::make_leaf. Live switching is F7 phase 2.
688        let &c = self.values.first()?;
689        if c.is_finite() && c > 0.0 {
690            Some(DynNode::Capacitor(
691                Some(comp_id.to_string()),
692                c as crate::Wave,
693                (1.0 / (2.0 * sample_rate * c)) as crate::Wave,
694            ))
695        } else {
696            None
697        }
698    }
699
700    fn edges(&self) -> Vec<ComponentEdge> {
701        vec![ComponentEdge {
702            pin_a: "a",
703            pin_b: "b",
704            kind: EdgeKind::Reactive,
705            port_group: None,
706        }]
707    }
708
709    fn capacitance(&self) -> Option<f64> {
710        self.values.first().copied()
711    }
712
713    fn footprint_ref(&self) -> (&'static str, &'static str) {
714        ("Device:C", "C")
715    }
716
717    fn symbol_name(&self) -> &'static str {
718        "capacitor"
719    }
720    fn layout_class(&self) -> &'static str {
721        "cap_switched"
722    }
723}
724
725// ═══════════════════════════════════════════════════════════════════════════
726// InductorSwitched
727// ═══════════════════════════════════════════════════════════════════════════
728
729#[derive(Debug, Clone, PartialEq)]
730pub struct InductorSwitched {
731    pub values: Vec<f64>,
732}
733
734impl Component for InductorSwitched {
735    impl_component_dyn!();
736
737    fn type_tag(&self) -> &'static str {
738        "switched inductor"
739    }
740
741    fn is_passive(&self) -> bool {
742        true
743    }
744
745    fn pin_config(&self) -> PinConfig {
746        PinConfig {
747            valid_pins: &["a", "b"],
748            aliases: &[],
749        }
750    }
751
752    fn graph_role(&self) -> GraphRole {
753        GraphRole::Edge {
754            pin_a: "a",
755            pin_b: "b",
756        }
757    }
758
759    fn stamp_mna(
760        &self,
761        comp_id: &str,
762        _n1: Option<usize>,
763        _n2: Option<usize>,
764        _mna: &mut MnaSystem,
765        sample_rate: f64,
766    ) -> StampResult {
767        if let Some(&l) = self.values.first() {
768            if l.is_finite() && l > 0.0 {
769                let rp = 2.0 * sample_rate * l;
770                return StampResult::Reactive {
771                    dyn_node: DynNode::Inductor(Some(comp_id.to_string()), l as crate::Wave, rp as crate::Wave),
772                    rp,
773                };
774            }
775        }
776        StampResult::Skip
777    }
778
779    fn make_leaf(&self, comp_id: &str, sample_rate: f64) -> Option<DynNode> {
780        // Position 0 (first listed value) — mirrors stamp_mna above and the
781        // fixed Inductor::make_leaf. Live switching is F7 phase 2.
782        let &l = self.values.first()?;
783        if l.is_finite() && l > 0.0 {
784            Some(DynNode::Inductor(
785                Some(comp_id.to_string()),
786                l as crate::Wave,
787                (2.0 * sample_rate * l) as crate::Wave,
788            ))
789        } else {
790            None
791        }
792    }
793
794    fn edges(&self) -> Vec<ComponentEdge> {
795        vec![ComponentEdge {
796            pin_a: "a",
797            pin_b: "b",
798            kind: EdgeKind::Reactive,
799            port_group: None,
800        }]
801    }
802
803    fn inductance(&self) -> Option<f64> {
804        self.values.first().copied()
805    }
806
807    fn footprint_ref(&self) -> (&'static str, &'static str) {
808        ("Device:L", "L")
809    }
810
811    fn symbol_name(&self) -> &'static str {
812        "inductor"
813    }
814    fn layout_class(&self) -> &'static str {
815        "inductor_switched"
816    }
817}
818
819// ═══════════════════════════════════════════════════════════════════════════
820// ResistorSwitched
821// ═══════════════════════════════════════════════════════════════════════════
822
823#[derive(Debug, Clone, PartialEq)]
824pub struct ResistorSwitched {
825    pub values: Vec<f64>,
826}
827
828impl Component for ResistorSwitched {
829    impl_component_dyn!();
830
831    fn type_tag(&self) -> &'static str {
832        "switched resistor"
833    }
834
835    fn is_passive(&self) -> bool {
836        true
837    }
838
839    fn pin_config(&self) -> PinConfig {
840        PinConfig {
841            valid_pins: &["a", "b"],
842            aliases: &[],
843        }
844    }
845
846    fn graph_role(&self) -> GraphRole {
847        GraphRole::Edge {
848            pin_a: "a",
849            pin_b: "b",
850        }
851    }
852
853    fn stamp_mna(
854        &self,
855        _comp_id: &str,
856        n1: Option<usize>,
857        n2: Option<usize>,
858        mna: &mut MnaSystem,
859        _sample_rate: f64,
860    ) -> StampResult {
861        if let Some(&r) = self.values.first() {
862            if r.is_finite() && r > 0.0 {
863                mna.stamp_resistor(n1, n2, r as crate::Wave);
864            }
865        }
866        StampResult::Stamped
867    }
868
869    fn make_leaf(&self, comp_id: &str, _sample_rate: f64) -> Option<DynNode> {
870        // Position 0 (first listed value) — mirrors stamp_mna above and the
871        // fixed Resistor::make_leaf (`inf` → open circuit). Live switching is
872        // F7 phase 2.
873        let &r = self.values.first()?;
874        if r.is_infinite() {
875            Some(DynNode::Resistor(Some(comp_id.to_string()), OPEN_CIRCUIT_R as crate::Wave))
876        } else if r.is_finite() && r > 0.0 {
877            Some(DynNode::Resistor(Some(comp_id.to_string()), r as crate::Wave))
878        } else {
879            None
880        }
881    }
882
883    fn edges(&self) -> Vec<ComponentEdge> {
884        vec![ComponentEdge {
885            pin_a: "a",
886            pin_b: "b",
887            kind: EdgeKind::Linear,
888            port_group: None,
889        }]
890    }
891
892    fn resistance(&self) -> Option<f64> {
893        self.values.first().copied()
894    }
895
896    fn footprint_ref(&self) -> (&'static str, &'static str) {
897        ("Device:R", "R")
898    }
899
900    fn symbol_name(&self) -> &'static str {
901        "resistor"
902    }
903    fn layout_class(&self) -> &'static str {
904        "resistor_switched"
905    }
906}