Skip to main content

pedalkernel/compiler/components/
active_ics.rs

1//! Active IC component structs: OpAmp, Vco, Vcf, Vca, Comparator, AnalogSwitch,
2//! MatchedNpn, MatchedPnp.
3
4use hashbrown::HashMap;
5
6use crate::compiler::classify::NonlinearKind;
7use crate::compiler::component::{
8    Component, ComponentEdge, EdgeKind, GraphRole, ModulationSink, ModulationSinkKind, NonIdealFx,
9    PinConfig, PinDirection, ResolveContext, SignalTerminals, StampContext, StampResult,
10};
11use crate::compiler::graph::NodeId;
12use crate::dsl::{
13    AnalogSwitchType, ComparatorType, MatchedTransistorType, OpAmpType, VcaType, VcfType, VcoType,
14    VcoWaveformDsl,
15};
16use crate::tree::MnaSystem;
17
18use super::impl_component_dyn;
19
20// ═══════════════════════════════════════════════════════════════════════════
21// OpAmp
22// ═══════════════════════════════════════════════════════════════════════════
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct OpAmp {
26    pub op_type: OpAmpType,
27}
28
29impl Component for OpAmp {
30    impl_component_dyn!();
31
32    fn type_tag(&self) -> &'static str {
33        if self.op_type.is_ota() {
34            "OTA"
35        } else {
36            "op-amp"
37        }
38    }
39
40    fn ports(&self) -> Vec<(&'static str, &'static str)> {
41        if self.op_type.is_ota() {
42            vec![("pos", "neg")]
43        } else {
44            vec![("neg", "out")]
45        }
46    }
47
48    fn output_impedance(&self) -> crate::compiler::component::OutputImpedance {
49        if self.op_type.is_ota() {
50            // OTA output is a current source (high impedance)
51            crate::compiler::component::OutputImpedance::Finite
52        } else {
53            // Op-amp output is a voltage source (zero impedance via feedback)
54            crate::compiler::component::OutputImpedance::VoltageSource
55        }
56    }
57
58    fn nonideal_fx(&self, _sample_rate: f64) -> Vec<NonIdealFx> {
59        if self.op_type.is_ota() {
60            return Vec::new(); // OTA non-idealities handled differently
61        }
62        let model = crate::model_lookup::opamp_model_from_type(&self.op_type);
63        vec![
64            NonIdealFx::OpAmpBandwidth {
65                gbw: model.gbw,
66                slew_rate: model.slew_rate,
67            },
68            // Default rail saturation — overridden by apply_bias() when a
69            // bias network is detected. The v_max here assumes the minimum
70            // common pedal supply (9V). Pipeline patches this after bias
71            // analysis with the actual supply voltage and bias point.
72            NonIdealFx::RailSaturation { v_max: 3.0 },
73        ]
74    }
75
76    fn signal_terminals(&self) -> SignalTerminals {
77        if self.op_type.is_ota() {
78            SignalTerminals::Amplifier {
79                input: "pos",
80                output: "neg",
81                control: None,
82            }
83        } else {
84            SignalTerminals::Amplifier {
85                input: "neg",
86                output: "out",
87                control: Some("pos"),
88            }
89        }
90    }
91
92    fn is_passive(&self) -> bool {
93        false
94    }
95
96    fn feedback_input_is_barrier(&self) -> bool {
97        // Op-amp neg node is a summing junction shared with interstage
98        // coupling. OTAs don't have this property.
99        !self.op_type.is_ota()
100    }
101
102    fn is_nonlinear(&self) -> bool {
103        self.op_type.is_ota()
104    }
105
106    fn is_active_ic(&self) -> bool {
107        !self.op_type.is_ota()
108    }
109
110    fn port_semantic(&self, pin_a: &str, pin_b: &str) -> crate::compiler::component::PortSemantic {
111        if self.op_type.is_ota() {
112            return crate::compiler::component::PortSemantic::LinearPassive;
113        }
114        // VCVS output is a voltage constraint
115        let pins = [pin_a, pin_b];
116        if pins.contains(&"out") || pins.contains(&"output") {
117            crate::compiler::component::PortSemantic::VoltageConstraint
118        } else {
119            crate::compiler::component::PortSemantic::LinearPassive
120        }
121    }
122
123    fn pin_config(&self) -> PinConfig {
124        PinConfig {
125            valid_pins: &["pos", "neg", "out", "output", "vp", "in", "input"],
126            aliases: &[("in", "input"), ("out", "output")],
127        }
128    }
129
130    fn modulation_pins(&self) -> &'static [&'static str] {
131        if self.op_type == OpAmpType::Ca3080 {
132            &["iabc"]
133        } else {
134            &[]
135        }
136    }
137
138    fn graph_role(&self) -> GraphRole {
139        if self.op_type.is_ota() {
140            GraphRole::Edge {
141                pin_a: "pos",
142                pin_b: "neg",
143            }
144        } else {
145            // Non-OTA op-amps participate in the graph as three-pin nullors
146            // (pos, neg, out). The R-type stage builder emits a VCVS stamp.
147            GraphRole::VcvsEdge {
148                pin_pos: "pos",
149                pin_neg: "neg",
150                pin_out: "out",
151            }
152        }
153    }
154
155    fn stamp_mna(
156        &self,
157        _comp_id: &str,
158        _n1: Option<usize>,
159        _n2: Option<usize>,
160        _mna: &mut MnaSystem,
161        _sample_rate: f64,
162    ) -> StampResult {
163        // Two-terminal path unused for op-amps. Use stamp_mna_multi.
164        StampResult::Skip
165    }
166
167    fn mna_vsource_count(&self) -> usize {
168        if self.op_type.is_ota() {
169            0
170        } else {
171            1
172        }
173    }
174
175    fn mna_internal_node_count(&self) -> usize {
176        0 // No internal node — simple VCVS stamp
177    }
178
179    fn stamp_mna_multi(
180        &self,
181        _comp_id: &str,
182        ctx: &mut StampContext,
183        mna: &mut MnaSystem,
184    ) -> StampResult {
185        if self.op_type.is_ota() {
186            return StampResult::Skip;
187        }
188        let model = crate::model_lookup::opamp_model_from_type(&self.op_type);
189        let ro = model.output_impedance;
190
191        let aol = model.open_loop_gain;
192
193        let out_mna = (ctx.pin_to_mna)("out");
194        mna.stamp_vcvs(
195            (ctx.pin_to_mna)("pos"),
196            (ctx.pin_to_mna)("neg"),
197            out_mna,
198            None,
199            aol,
200            ro,
201            ctx.vsrc_base,
202        );
203
204        // Output capacitance: keeps the output node as a dynamic state
205        // in the Schur complement reduction, preserving the feedback loop's
206        // energy recirculation. Without this, the output is eliminated
207        // algebraically and the Q drops to ~0.3.
208        if model.output_capacitance > 0.0 {
209            if let Some(ref mut reactive_one_ports) = ctx.reactive_one_ports {
210                reactive_one_ports.push(pedalkernel_rt::boundary_math::OnePort::new(
211                    pedalkernel_rt::boundary_math::MnaPortTerminals::maybe_single_ended(
212                        out_mna.map(pedalkernel_rt::boundary_math::MnaNodeId::new),
213                    ),
214                    pedalkernel_rt::boundary_math::OnePortKind::Capacitor(model.output_capacitance),
215                ));
216            }
217        }
218
219        StampResult::Stamped
220    }
221
222    fn edges(&self) -> Vec<ComponentEdge> {
223        if self.op_type.is_ota() {
224            vec![ComponentEdge {
225                pin_a: "pos",
226                pin_b: "neg",
227                kind: EdgeKind::Vccs,
228                port_group: None,
229            }]
230        } else {
231            // VCVS edge: neg→out carries the nullor constraint.
232            // pos is resolved via stamp_mna_multi's pin_to_mna("pos").
233            vec![ComponentEdge {
234                pin_a: "neg",
235                pin_b: "out",
236                kind: EdgeKind::Vcvs,
237                port_group: None,
238            }]
239        }
240    }
241
242    fn resolve_edges(&self, ctx: &ResolveContext) -> Option<Vec<ComponentEdge>> {
243        if self.op_type.is_ota() && ctx.control_pin_is_modulated {
244            Some(vec![ComponentEdge {
245                pin_a: "pos",
246                pin_b: "neg",
247                kind: EdgeKind::Vccs,
248                port_group: None,
249            }])
250        } else {
251            None
252        }
253    }
254
255    fn classify_nonlinear(
256        &self,
257        _comp_id: &str,
258        _node_a: NodeId,
259        node_b: NodeId,
260        gnd_node: NodeId,
261        _node_names: &HashMap<String, NodeId>,
262    ) -> Option<(NonlinearKind, Vec<NodeId>)> {
263        if self.op_type.is_ota() {
264            let jn = if node_b == gnd_node { _node_a } else { node_b };
265            Some((NonlinearKind::Ota, vec![jn]))
266        } else {
267            None
268        }
269    }
270
271    fn modulation_sink(&self, pin: &str) -> Option<ModulationSink> {
272        if self.op_type.is_ota() {
273            match pin {
274                "iabc" => Some(ModulationSink {
275                    target_kind: ModulationSinkKind::OtaIabc,
276                    bias: 0.5,
277                    range: 0.5,
278                }),
279                _ => None,
280            }
281        } else {
282            None
283        }
284    }
285
286    fn footprint_ref(&self) -> (&'static str, &'static str) {
287        let lib = match self.op_type {
288            OpAmpType::Generic => "Amplifier_Operational:TL072",
289            OpAmpType::Tl071 => "Amplifier_Operational:TL071",
290            OpAmpType::Tl072 => "Amplifier_Operational:TL072",
291            OpAmpType::Tl082 => "Amplifier_Operational:TL082",
292            OpAmpType::Jrc4558 => "Amplifier_Operational:JRC4558",
293            OpAmpType::Rc4558 => "Amplifier_Operational:RC4558",
294            OpAmpType::Lm308 => "Amplifier_Operational:LM308",
295            OpAmpType::Lm741 => "Amplifier_Operational:LM741",
296            OpAmpType::Ne5532 => "Amplifier_Operational:NE5532",
297            OpAmpType::Ca3080 => "Amplifier_Operational:CA3080",
298            OpAmpType::Op07 => "Amplifier_Operational:OP07",
299        };
300        (lib, "U")
301    }
302
303    fn symbol_name(&self) -> &'static str {
304        "opamp"
305    }
306    fn layout_class(&self) -> &'static str {
307        "opamp"
308    }
309
310    fn signal_adjacencies(&self) -> Vec<(&'static str, &'static str)> {
311        vec![("pos", "out"), ("neg", "out")]
312    }
313
314    fn pin_direction(&self, pin: &str) -> PinDirection {
315        match pin {
316            "pos" | "neg" => PinDirection::Input,
317            "out" | "output" => PinDirection::Output,
318            _ => PinDirection::Bidirectional,
319        }
320    }
321
322    fn op_amp_type(&self) -> Option<OpAmpType> {
323        Some(self.op_type)
324    }
325
326    fn apply_bias(
327        &self,
328        bias_voltages: &hashbrown::HashMap<String, f64>,
329        supply_voltage: f64,
330    ) -> crate::compiler::component::BiasResult {
331        // Op-amp bias sets the DC operating point (output swing center).
332        // The bias voltage at the pos/neg input determines the output's
333        // midpoint. Rail limits are supply_voltage minus headroom on each side.
334        //
335        // For a symmetric divider (bias = supply/2 = 4.5V at 9V):
336        //   v_rail_pos = supply - bias - headroom = 9 - 4.5 - 1.5 = 3.0V
337        //   v_rail_neg = bias - headroom = 4.5 - 1.5 = 3.0V
338        //
339        // For asymmetric (bias = 2.25V at 9V):
340        //   v_rail_pos = 9 - 2.25 - 1.5 = 5.25V
341        //   v_rail_neg = 2.25 - 1.5 = 0.75V
342        const HEADROOM: f64 = 1.5; // Typical output stage headroom
343
344        // Use bias voltage if provided; default to supply/2 (symmetric)
345        let bias_v = bias_voltages
346            .get("pos")
347            .or_else(|| bias_voltages.get("neg"))
348            .copied()
349            .unwrap_or(supply_voltage / 2.0);
350
351        let v_rail_pos = (supply_voltage - bias_v - HEADROOM).max(0.5);
352        let v_rail_neg = (bias_v - HEADROOM).max(0.5);
353
354        crate::compiler::component::BiasResult::Applied {
355            v_rail_pos,
356            v_rail_neg,
357        }
358    }
359}
360
361// ═══════════════════════════════════════════════════════════════════════════
362// Vco
363// ═══════════════════════════════════════════════════════════════════════════
364
365#[derive(Debug, Clone, PartialEq)]
366pub struct Vco {
367    pub vco_type: VcoType,
368    /// Base frequency in Hz (default 440.0).
369    pub base_freq: f64,
370    /// Default waveform output.
371    pub waveform: VcoWaveformDsl,
372}
373
374impl Component for Vco {
375    impl_component_dyn!();
376
377    fn type_tag(&self) -> &'static str {
378        "VCO"
379    }
380
381    fn is_passive(&self) -> bool {
382        false
383    }
384
385    fn is_active_ic(&self) -> bool {
386        false
387    }
388
389    fn pin_config(&self) -> PinConfig {
390        PinConfig {
391            valid_pins: &["cv", "saw", "tri", "pulse", "pw", "sync", "out", "output"],
392            aliases: &[("out", "output")],
393        }
394    }
395
396    fn graph_role(&self) -> GraphRole {
397        GraphRole::Virtual
398    }
399
400    fn stamp_mna(
401        &self,
402        _comp_id: &str,
403        _n1: Option<usize>,
404        _n2: Option<usize>,
405        _mna: &mut MnaSystem,
406        _sample_rate: f64,
407    ) -> StampResult {
408        StampResult::Skip
409    }
410
411    fn footprint_ref(&self) -> (&'static str, &'static str) {
412        match self.vco_type {
413            VcoType::Cem3340 => ("Oscillator:CEM3340", "U"),
414            VcoType::As3340 => ("Oscillator:AS3340", "U"),
415            VcoType::V3340 => ("Oscillator:V3340", "U"),
416        }
417    }
418
419    fn symbol_name(&self) -> &'static str {
420        "ic_chip"
421    }
422    fn layout_class(&self) -> &'static str {
423        "vco"
424    }
425}
426
427// ═══════════════════════════════════════════════════════════════════════════
428// Vcf
429// ═══════════════════════════════════════════════════════════════════════════
430
431#[derive(Debug, Clone, PartialEq)]
432pub struct Vcf {
433    pub vcf_type: VcfType,
434}
435
436impl Component for Vcf {
437    impl_component_dyn!();
438
439    fn type_tag(&self) -> &'static str {
440        "VCF"
441    }
442
443    fn is_passive(&self) -> bool {
444        false
445    }
446
447    fn is_active_ic(&self) -> bool {
448        true
449    }
450
451    fn pin_config(&self) -> PinConfig {
452        PinConfig {
453            valid_pins: &["in", "input", "out", "output", "cv", "res"],
454            aliases: &[("in", "input"), ("out", "output")],
455        }
456    }
457
458    fn graph_role(&self) -> GraphRole {
459        GraphRole::ActiveIc
460    }
461
462    fn stamp_mna(
463        &self,
464        _comp_id: &str,
465        _n1: Option<usize>,
466        _n2: Option<usize>,
467        _mna: &mut MnaSystem,
468        _sample_rate: f64,
469    ) -> StampResult {
470        StampResult::Skip
471    }
472
473    fn footprint_ref(&self) -> (&'static str, &'static str) {
474        match self.vcf_type {
475            VcfType::Cem3320 => ("Analog:CEM3320", "U"),
476            VcfType::As3320 => ("Analog:AS3320", "U"),
477        }
478    }
479
480    fn symbol_name(&self) -> &'static str {
481        "ic_chip"
482    }
483    fn layout_class(&self) -> &'static str {
484        "vcf"
485    }
486}
487
488// ═══════════════════════════════════════════════════════════════════════════
489// Vca
490// ═══════════════════════════════════════════════════════════════════════════
491
492#[derive(Debug, Clone, PartialEq)]
493pub struct Vca {
494    pub vca_type: VcaType,
495}
496
497impl Component for Vca {
498    impl_component_dyn!();
499
500    fn type_tag(&self) -> &'static str {
501        "VCA"
502    }
503
504    fn is_passive(&self) -> bool {
505        false
506    }
507
508    fn is_active_ic(&self) -> bool {
509        false
510    }
511
512    fn pin_config(&self) -> PinConfig {
513        PinConfig {
514            valid_pins: &[
515                "in", "input", "out", "output", "cv", "in1", "out1", "cv1", "in2", "out2", "cv2",
516                "in3", "out3", "cv3", "in4", "out4", "cv4",
517            ],
518            aliases: &[("in", "input"), ("out", "output")],
519        }
520    }
521
522    fn graph_role(&self) -> GraphRole {
523        GraphRole::Virtual
524    }
525
526    fn stamp_mna(
527        &self,
528        _comp_id: &str,
529        _n1: Option<usize>,
530        _n2: Option<usize>,
531        _mna: &mut MnaSystem,
532        _sample_rate: f64,
533    ) -> StampResult {
534        StampResult::Skip
535    }
536
537    fn modulation_sink(&self, pin: &str) -> Option<ModulationSink> {
538        match pin {
539            // Scaling rationale (audit gap G4): the runtime Vca maps a
540            // normalized CV 0..1 onto 0..-80 dB, the SSM2164-class dB-linear
541            // law (see pedalkernel-rt `Vca::set_cv_normalized`). The engine's
542            // envelope followers output a rectified average — ~1.3-2 for
543            // full-scale program (2/π × a typical sensitivity of 2.0 from a
544            // 20k sensitivity resistor, times any buffer gain ahead of the
545            // tap) — so cv = bias + env × range with bias 0, range 0.05 puts
546            // full-scale program at cv ≈ 0.07-0.1 ≈ 5-8 dB of gain reduction
547            // and a ~2:1 ratio over the top 12 dB (SSL-bus-comp-like glue).
548            // Larger ranges over-compress: because the detector is linear in
549            // amplitude, GR in dB grows ~10^(in_db/20), and beyond ~11 dB of
550            // full-scale GR the static curve's top end slopes DOWNWARD
551            // (output falls as input rises — negative ratio).
552            "cv" | "cv1" | "cv2" | "cv3" | "cv4" => Some(ModulationSink {
553                target_kind: ModulationSinkKind::VcaCv,
554                bias: 0.0,
555                range: 0.05,
556            }),
557            _ => None,
558        }
559    }
560
561    fn footprint_ref(&self) -> (&'static str, &'static str) {
562        match self.vca_type {
563            VcaType::Ssm2164 => ("Analog:SSM2164", "U"),
564            VcaType::V2164 => ("Analog:V2164", "U"),
565        }
566    }
567
568    fn symbol_name(&self) -> &'static str {
569        "ic_chip"
570    }
571    fn layout_class(&self) -> &'static str {
572        "vca"
573    }
574}
575
576// ═══════════════════════════════════════════════════════════════════════════
577// Comparator
578// ═══════════════════════════════════════════════════════════════════════════
579
580#[derive(Debug, Clone, PartialEq)]
581pub struct Comparator {
582    pub comp_type: ComparatorType,
583}
584
585impl Component for Comparator {
586    impl_component_dyn!();
587
588    fn type_tag(&self) -> &'static str {
589        "comparator"
590    }
591
592    fn is_passive(&self) -> bool {
593        false
594    }
595
596    fn is_active_ic(&self) -> bool {
597        true
598    }
599
600    fn pin_config(&self) -> PinConfig {
601        PinConfig {
602            valid_pins: &["pos", "neg", "out", "output"],
603            aliases: &[("out", "output")],
604        }
605    }
606
607    fn graph_role(&self) -> GraphRole {
608        GraphRole::ActiveIc
609    }
610
611    fn stamp_mna(
612        &self,
613        _comp_id: &str,
614        _n1: Option<usize>,
615        _n2: Option<usize>,
616        _mna: &mut MnaSystem,
617        _sample_rate: f64,
618    ) -> StampResult {
619        StampResult::Skip
620    }
621
622    fn footprint_ref(&self) -> (&'static str, &'static str) {
623        match self.comp_type {
624            ComparatorType::Lm311 => ("Comparator:LM311", "U"),
625            ComparatorType::Lm393 => ("Comparator:LM393", "U"),
626        }
627    }
628
629    fn symbol_name(&self) -> &'static str {
630        "ic_chip"
631    }
632    fn layout_class(&self) -> &'static str {
633        "comparator"
634    }
635
636    fn pin_direction(&self, pin: &str) -> PinDirection {
637        match pin {
638            "pos" | "neg" => PinDirection::Input,
639            "out" | "output" => PinDirection::Output,
640            _ => PinDirection::Bidirectional,
641        }
642    }
643}
644
645// ═══════════════════════════════════════════════════════════════════════════
646// AnalogSwitch
647// ═══════════════════════════════════════════════════════════════════════════
648
649#[derive(Debug, Clone, PartialEq)]
650pub struct AnalogSwitch {
651    pub switch_type: AnalogSwitchType,
652}
653
654impl Component for AnalogSwitch {
655    impl_component_dyn!();
656
657    fn type_tag(&self) -> &'static str {
658        "analog switch"
659    }
660
661    fn is_passive(&self) -> bool {
662        false
663    }
664
665    fn is_active_ic(&self) -> bool {
666        true
667    }
668
669    fn pin_config(&self) -> PinConfig {
670        PinConfig {
671            valid_pins: &[
672                "in1", "out1", "ctrl1", "in2", "out2", "ctrl2", "in3", "out3", "ctrl3", "in4",
673                "out4", "ctrl4",
674            ],
675            aliases: &[],
676        }
677    }
678
679    fn graph_role(&self) -> GraphRole {
680        GraphRole::ActiveIc
681    }
682
683    fn stamp_mna(
684        &self,
685        _comp_id: &str,
686        _n1: Option<usize>,
687        _n2: Option<usize>,
688        _mna: &mut MnaSystem,
689        _sample_rate: f64,
690    ) -> StampResult {
691        StampResult::Skip
692    }
693
694    fn footprint_ref(&self) -> (&'static str, &'static str) {
695        match self.switch_type {
696            AnalogSwitchType::Cd4066 => ("Analog_Switch:CD4066", "U"),
697            AnalogSwitchType::Dg411 => ("Analog_Switch:DG411", "U"),
698        }
699    }
700
701    fn symbol_name(&self) -> &'static str {
702        "ic_chip"
703    }
704    fn layout_class(&self) -> &'static str {
705        "analog_switch"
706    }
707}
708
709// ═══════════════════════════════════════════════════════════════════════════
710// MatchedNpn
711// ═══════════════════════════════════════════════════════════════════════════
712
713#[derive(Debug, Clone, PartialEq)]
714pub struct MatchedNpn {
715    pub matched_type: MatchedTransistorType,
716}
717
718impl Component for MatchedNpn {
719    impl_component_dyn!();
720
721    fn type_tag(&self) -> &'static str {
722        "matched NPN pair"
723    }
724
725    fn is_passive(&self) -> bool {
726        false
727    }
728
729    fn is_active_ic(&self) -> bool {
730        true
731    }
732
733    fn pin_config(&self) -> PinConfig {
734        PinConfig {
735            valid_pins: &[
736                "base",
737                "collector",
738                "emitter",
739                "base1",
740                "base2",
741                "collector1",
742                "collector2",
743                "emitter1",
744                "emitter2",
745            ],
746            aliases: &[],
747        }
748    }
749
750    fn graph_role(&self) -> GraphRole {
751        GraphRole::ActiveIc
752    }
753
754    fn stamp_mna(
755        &self,
756        _comp_id: &str,
757        _n1: Option<usize>,
758        _n2: Option<usize>,
759        _mna: &mut MnaSystem,
760        _sample_rate: f64,
761    ) -> StampResult {
762        StampResult::Skip
763    }
764
765    fn footprint_ref(&self) -> (&'static str, &'static str) {
766        match self.matched_type {
767            MatchedTransistorType::Ssm2210 => ("Transistor_BJT:SSM2210", "Q"),
768            MatchedTransistorType::Ca3046 => ("Transistor_Array:CA3046", "Q"),
769            MatchedTransistorType::Lm394 => ("Transistor_BJT:LM394", "Q"),
770            MatchedTransistorType::That340 => ("Transistor_BJT:THAT340", "Q"),
771        }
772    }
773
774    fn symbol_name(&self) -> &'static str {
775        "npn_bjt"
776    }
777    fn layout_class(&self) -> &'static str {
778        "matched_npn"
779    }
780}
781
782// ═══════════════════════════════════════════════════════════════════════════
783// MatchedPnp
784// ═══════════════════════════════════════════════════════════════════════════
785
786#[derive(Debug, Clone, PartialEq)]
787pub struct MatchedPnp {
788    pub matched_type: MatchedTransistorType,
789}
790
791impl Component for MatchedPnp {
792    impl_component_dyn!();
793
794    fn type_tag(&self) -> &'static str {
795        "matched PNP pair"
796    }
797
798    fn is_passive(&self) -> bool {
799        false
800    }
801
802    fn is_active_ic(&self) -> bool {
803        true
804    }
805
806    fn pin_config(&self) -> PinConfig {
807        PinConfig {
808            valid_pins: &[
809                "base",
810                "collector",
811                "emitter",
812                "base1",
813                "base2",
814                "collector1",
815                "collector2",
816                "emitter1",
817                "emitter2",
818            ],
819            aliases: &[],
820        }
821    }
822
823    fn graph_role(&self) -> GraphRole {
824        GraphRole::ActiveIc
825    }
826
827    fn stamp_mna(
828        &self,
829        _comp_id: &str,
830        _n1: Option<usize>,
831        _n2: Option<usize>,
832        _mna: &mut MnaSystem,
833        _sample_rate: f64,
834    ) -> StampResult {
835        StampResult::Skip
836    }
837
838    fn footprint_ref(&self) -> (&'static str, &'static str) {
839        match self.matched_type {
840            MatchedTransistorType::Ssm2210 => ("Transistor_BJT:SSM2210", "Q"),
841            MatchedTransistorType::Ca3046 => ("Transistor_Array:CA3046", "Q"),
842            MatchedTransistorType::Lm394 => ("Transistor_BJT:LM394", "Q"),
843            MatchedTransistorType::That340 => ("Transistor_BJT:THAT340", "Q"),
844        }
845    }
846
847    fn symbol_name(&self) -> &'static str {
848        "pnp_bjt"
849    }
850    fn layout_class(&self) -> &'static str {
851        "matched_pnp"
852    }
853}