1use 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#[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 crate::compiler::component::OutputImpedance::Finite
52 } else {
53 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(); }
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 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 !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 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 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 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 }
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 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 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 const HEADROOM: f64 = 1.5; 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#[derive(Debug, Clone, PartialEq)]
366pub struct Vco {
367 pub vco_type: VcoType,
368 pub base_freq: f64,
370 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#[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#[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 "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#[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#[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#[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#[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}