1use hashbrown::HashMap;
11use std::sync::LazyLock;
12
13static TRANSISTOR_MODELS_SRC: &str = include_str!("../models/transistors.model");
18static JFET_MODELS_SRC: &str = include_str!("../models/jfets.model");
19static TRIODE_MODELS_SRC: &str = include_str!("../models/triodes.model");
20static PENTODE_MODELS_SRC: &str = include_str!("../models/pentodes.model");
21static DIODE_MODELS_SRC: &str = include_str!("../models/diodes.model");
22static LED_MODELS_SRC: &str = include_str!("../models/leds.model");
23static SCHOTTKY_MODELS_SRC: &str = include_str!("../models/schottky.model");
24static ZENER_MODELS_SRC: &str = include_str!("../models/zeners.model");
25static OPAMP_MODELS_SRC: &str = include_str!("../models/opamps.model");
26static TRANSFORMER_MODELS_SRC: &str = include_str!("../models/transformers.model");
27
28pub static BJT_MODELS: LazyLock<HashMap<String, SpiceBjtModel>> =
34 LazyLock::new(|| parse_bjt_models(TRANSISTOR_MODELS_SRC));
35
36pub static JFET_MODELS: LazyLock<HashMap<String, SpiceJfetModel>> =
38 LazyLock::new(|| parse_jfet_models(JFET_MODELS_SRC));
39
40pub static TRIODE_MODELS: LazyLock<HashMap<String, SpiceTriodeModel>> =
42 LazyLock::new(|| parse_triode_models(TRIODE_MODELS_SRC));
43
44pub static PENTODE_MODELS: LazyLock<HashMap<String, SpicePentodeModel>> =
46 LazyLock::new(|| parse_pentode_models(PENTODE_MODELS_SRC));
47
48pub static DIODE_MODELS: LazyLock<HashMap<String, ShockleyDiodeModel>> =
50 LazyLock::new(|| parse_diode_models(DIODE_MODELS_SRC));
51
52pub static LED_MODELS: LazyLock<HashMap<String, ShockleyDiodeModel>> =
54 LazyLock::new(|| parse_diode_models(LED_MODELS_SRC));
55
56pub static SCHOTTKY_MODELS: LazyLock<HashMap<String, ShockleyDiodeModel>> =
58 LazyLock::new(|| parse_diode_models(SCHOTTKY_MODELS_SRC));
59
60pub static ZENER_MODELS: LazyLock<HashMap<String, ShockleyDiodeModel>> =
62 LazyLock::new(|| parse_diode_models(ZENER_MODELS_SRC));
63
64pub static OPAMP_MODELS: LazyLock<HashMap<String, SpiceOpAmpModel>> =
66 LazyLock::new(|| parse_opamp_models(OPAMP_MODELS_SRC));
67
68pub static TRANSFORMER_MODELS: LazyLock<HashMap<String, SpiceTransformerModel>> =
70 LazyLock::new(|| parse_transformer_models(TRANSFORMER_MODELS_SRC));
71
72#[derive(Debug, Clone)]
80pub struct SpiceBjtModel {
81 pub name: String,
82 pub is_pnp: bool,
83
84 pub is: f64,
86 pub bf: f64,
87 pub br: f64,
88 pub nf: f64,
89 pub nr: f64,
90
91 pub vaf: f64,
93 pub var: f64,
94
95 pub ikf: f64,
97 pub ikr: f64,
98
99 pub ise: f64,
101 pub ne: f64,
102 pub isc: f64,
103 pub nc: f64,
104
105 pub rb: f64,
107 pub re: f64,
108 pub rc: f64,
109
110 pub cje: f64,
112 pub vje: f64,
113 pub mje: f64,
114 pub cjc: f64,
115 pub vjc: f64,
116 pub mjc: f64,
117
118 pub tf: f64,
120 pub tr: f64,
121}
122
123impl SpiceBjtModel {
124 fn defaults(name: &str, is_pnp: bool) -> Self {
126 Self {
127 name: name.to_uppercase(),
128 is_pnp,
129 is: 1e-16,
130 bf: 100.0,
131 br: 1.0,
132 nf: 1.0,
133 nr: 1.0,
134 vaf: f64::INFINITY,
135 var: f64::INFINITY,
136 ikf: f64::INFINITY,
137 ikr: f64::INFINITY,
138 ise: 0.0,
139 ne: 1.5,
140 isc: 0.0,
141 nc: 2.0,
142 rb: 0.0,
143 re: 0.0,
144 rc: 0.0,
145 cje: 0.0,
146 vje: 0.75,
147 mje: 0.33,
148 cjc: 0.0,
149 vjc: 0.75,
150 mjc: 0.33,
151 tf: 0.0,
152 tr: 0.0,
153 }
154 }
155}
156
157#[derive(Debug, Clone)]
165pub struct SpiceJfetModel {
166 pub name: String,
167 pub is_n_channel: bool,
168
169 pub vto: f64, pub beta: f64, pub lambda: f64, pub is: f64, pub n: f64, pub rd: f64, pub rs: f64, pub cgs: f64, pub cgd: f64, pub pb: f64, }
187
188impl SpiceJfetModel {
189 fn defaults(name: &str, is_n_channel: bool) -> Self {
190 Self {
191 name: name.to_uppercase(),
192 is_n_channel,
193 vto: -2.0,
194 beta: 1e-4, lambda: 0.0,
196 is: 1e-14,
197 n: 1.0,
198 rd: 0.0,
199 rs: 0.0,
200 cgs: 0.0,
201 cgd: 0.0,
202 pb: 1.0,
203 }
204 }
205}
206
207#[derive(Debug, Clone)]
219pub struct ShockleyDiodeModel {
220 pub name: String,
221
222 pub is: f64,
225 pub rs: f64,
227 pub n: f64,
229 pub tt: f64,
231
232 pub cjo: f64,
235 pub vj: f64,
237 pub m: f64,
239
240 pub bv: f64,
243 pub ibv: f64,
245
246 pub eg: f64,
249 pub xti: f64,
251}
252
253impl ShockleyDiodeModel {
254 fn defaults(name: &str) -> Self {
256 Self {
257 name: name.to_uppercase(),
258 is: 1e-14,
259 rs: 0.0,
260 n: 1.0,
261 tt: 0.0,
262 cjo: 0.0,
263 vj: 1.0,
264 m: 0.5,
265 bv: f64::INFINITY,
266 ibv: 1e-3,
267 eg: 1.11,
268 xti: 3.0,
269 }
270 }
271}
272
273#[derive(Debug, Clone)]
281pub struct SpiceTriodeModel {
282 pub name: String,
283 pub mu: f64,
285 pub ex: f64,
287 pub kg1: f64,
289 pub kp: f64,
291 pub kvb: f64,
293 pub rp: f64,
296}
297
298#[derive(Debug, Clone)]
306pub struct SpicePentodeModel {
307 pub name: String,
308 pub mu: f64,
310 pub ex: f64,
312 pub kg1: f64,
314 pub kg2: f64,
316 pub kp: f64,
318 pub kvb: f64,
320 pub kvb2: f64,
322 pub vg2_default: f64,
324 pub rp: f64,
327}
328
329#[derive(Debug, Clone)]
339pub struct SpiceOpAmpModel {
340 pub name: String,
341 pub is_ota: bool,
342 pub open_loop_gain: f64,
343 pub gbw: f64,
344 pub slew_rate: f64,
345 pub v_rail_pos: f64,
346 pub v_rail_neg: f64,
347 pub output_impedance: f64,
348 pub output_capacitance: f64,
349 pub ota_iabc: f64,
350 pub ota_vt: f64,
351 pub ota_gm: f64,
352 pub ota_r_load: f64,
353}
354
355#[derive(Debug, Clone)]
364pub struct SpiceTransformerModel {
365 pub name: String,
366
367 pub primary_inductance: f64,
369 pub coupling: f64,
370 pub primary_dcr: f64,
371 pub secondary_dcr: f64,
372 pub primary_leakage: f64,
373 pub secondary_leakage: f64,
374 pub magnetizing_inductance: f64,
375 pub core_loss_resistance: f64,
376 pub capacitance: f64,
377
378 pub primary_turns: f64,
380 pub secondary_turns: f64,
381 pub core_area: f64,
382 pub magnetic_path_length: f64,
383 pub gap_length: f64,
384 pub dc_bias_current: f64,
385
386 pub ja_ms: f64,
388 pub ja_a: f64,
389 pub ja_alpha: f64,
390 pub ja_k: f64,
391 pub ja_c: f64,
392
393 pub hc: f64,
395 pub br: f64,
396 pub bs: f64,
397}
398
399impl SpiceTransformerModel {
400 fn defaults(name: &str) -> Self {
401 Self {
402 name: name.to_uppercase(),
403 primary_inductance: 1.0,
404 coupling: 0.99,
405 primary_dcr: 0.0,
406 secondary_dcr: 0.0,
407 primary_leakage: 0.01,
408 secondary_leakage: 0.01,
409 magnetizing_inductance: 0.99,
410 core_loss_resistance: 0.0,
411 capacitance: 0.0,
412 primary_turns: 1.0,
413 secondary_turns: 1.0,
414 core_area: 0.0,
415 magnetic_path_length: 0.0,
416 gap_length: 0.0,
417 dc_bias_current: 0.0,
418 ja_ms: 0.0,
419 ja_a: 0.0,
420 ja_alpha: 0.0,
421 ja_k: 0.0,
422 ja_c: 0.0,
423 hc: 0.0,
424 br: 0.0,
425 bs: 0.0,
426 }
427 }
428}
429
430impl SpiceOpAmpModel {
431 fn defaults(name: &str, is_ota: bool) -> Self {
432 Self {
433 name: name.to_uppercase(),
434 is_ota,
435 open_loop_gain: 100_000.0,
436 gbw: 1e6,
437 slew_rate: 1.0,
438 v_rail_pos: 12.0,
439 v_rail_neg: 12.0,
440 output_impedance: 75.0,
441 output_capacitance: 20e-12,
442 ota_iabc: 100e-6,
443 ota_vt: pedalkernel_rt::SPICE_VT_27C as f64,
444 ota_gm: 0.0,
445 ota_r_load: 10_000.0,
446 }
447 }
448}
449
450trait NamedModel {
451 fn model_name(&self) -> &str;
452}
453
454macro_rules! impl_named_model {
455 ($($ty:ty),+ $(,)?) => {
456 $(
457 impl NamedModel for $ty {
458 fn model_name(&self) -> &str {
459 &self.name
460 }
461 }
462 )+
463 };
464}
465
466impl_named_model!(
467 SpiceBjtModel,
468 SpiceJfetModel,
469 ShockleyDiodeModel,
470 SpiceTriodeModel,
471 SpicePentodeModel,
472 SpiceOpAmpModel,
473 SpiceTransformerModel,
474);
475
476fn parse_spice_value(s: &str) -> Option<f64> {
485 let s = s.trim();
486 if s.is_empty() {
487 return None;
488 }
489
490 let mut split_pos = s.len();
493 let bytes = s.as_bytes();
494 for i in 0..bytes.len() {
495 let c = bytes[i] as char;
496 if c.is_ascii_alphabetic() {
497 if (c == 'e' || c == 'E') && i > 0 {
499 if i + 1 < bytes.len() {
501 let next = bytes[i + 1] as char;
502 if next.is_ascii_digit() || next == '+' || next == '-' {
503 continue;
504 }
505 }
506 }
507 split_pos = i;
508 break;
509 }
510 }
511
512 let num_part = &s[..split_pos];
513 let suffix = &s[split_pos..];
514
515 let base: f64 = num_part.parse().ok()?;
516
517 let multiplier = match suffix.to_uppercase().as_str() {
518 "" => 1.0,
519 "T" => 1e12,
520 "G" => 1e9,
521 "MEG" => 1e6,
522 "K" => 1e3,
523 "M" => 1e-3,
524 "MIL" => 25.4e-6,
525 "U" => 1e-6,
526 "N" => 1e-9,
527 "P" => 1e-12,
528 "F" => 1e-15,
529 _ => return None,
530 };
531
532 Some(base * multiplier)
533}
534
535fn parse_model_registry<T, F>(src: &str, mut parse_line: F) -> HashMap<String, T>
536where
537 T: NamedModel,
538 F: FnMut(&str) -> Option<T>,
539{
540 let mut models = HashMap::new();
541
542 for line in src.lines() {
543 let trimmed = line.trim();
544 if trimmed.is_empty() || trimmed.starts_with('*') || trimmed.starts_with('#') {
545 continue;
546 }
547
548 if let Some(model) = parse_line(trimmed) {
549 models.insert(model.model_name().to_string(), model);
550 }
551 }
552
553 models
554}
555
556fn for_each_param<F>(params: &str, mut apply: F)
557where
558 F: FnMut(&str, f64),
559{
560 for pair in params.split_whitespace() {
561 if let Some((key, val_str)) = pair.split_once('=') {
562 if let Some(val) = parse_spice_value(val_str) {
563 let key_upper = key.to_uppercase();
564 apply(&key_upper, val);
565 }
566 }
567 }
568}
569
570fn strip_directive<'a>(line: &'a str, directive: &str) -> Option<&'a str> {
571 let trimmed = line.trim_start();
572 let prefix = trimmed.get(..directive.len())?;
573 if !prefix.eq_ignore_ascii_case(directive) {
574 return None;
575 }
576
577 let rest = &trimmed[directive.len()..];
578 if rest
579 .chars()
580 .next()
581 .is_some_and(|c| !c.is_ascii_whitespace())
582 {
583 return None;
584 }
585
586 Some(rest.trim_start())
587}
588
589fn lookup_model<'a, T>(models: &'a HashMap<String, T>, name: &str) -> Option<&'a T> {
590 models.get(&name.to_uppercase())
591}
592
593fn model_names<T>(models: &'static HashMap<String, T>) -> Vec<&'static str> {
594 models.keys().map(|s| s.as_str()).collect()
595}
596
597fn parse_bjt_models(src: &str) -> HashMap<String, SpiceBjtModel> {
603 parse_model_registry(src, parse_model_line)
604}
605
606fn parse_model_line(line: &str) -> Option<SpiceBjtModel> {
608 let rest = strip_directive(line, ".MODEL")?;
609
610 let (name, rest) = rest.split_once(|c: char| c.is_whitespace())?;
612 let rest = rest.trim_start();
613
614 let (type_str, params_block) = if let Some(paren_pos) = rest.find('(') {
617 let type_str = rest[..paren_pos].trim();
618 let close_paren = rest.rfind(')')?;
619 let params = &rest[paren_pos + 1..close_paren];
620 (type_str, params)
621 } else {
622 (rest.trim(), "")
624 };
625
626 let is_pnp = match type_str.to_uppercase().as_str() {
627 "NPN" => false,
628 "PNP" => true,
629 _ => return None, };
631
632 let mut model = SpiceBjtModel::defaults(name, is_pnp);
633
634 for_each_param(params_block, |key, val| match key {
635 "IS" => model.is = val,
636 "BF" => model.bf = val,
637 "BR" => model.br = val,
638 "NF" => model.nf = val,
639 "NR" => model.nr = val,
640 "VT" => {} "VAF" | "VA" => model.vaf = val,
642 "VAR" | "VB" => model.var = val,
643 "IKF" | "JBF" => model.ikf = val,
644 "IKR" | "JBR" => model.ikr = val,
645 "ISE" => model.ise = val,
646 "NE" => model.ne = val,
647 "ISC" => model.isc = val,
648 "NC" => model.nc = val,
649 "RB" => model.rb = val,
650 "RE" => model.re = val,
651 "RC" => model.rc = val,
652 "CJE" | "CEB" => model.cje = val,
653 "VJE" | "PE" => model.vje = val,
654 "MJE" | "ME" => model.mje = val,
655 "CJC" | "CCB" => model.cjc = val,
656 "VJC" | "PC" => model.vjc = val,
657 "MJC" | "MC" => model.mjc = val,
658 "TF" => model.tf = val,
659 "TR" => model.tr = val,
660 _ => {} });
662
663 Some(model)
664}
665
666fn parse_jfet_models(src: &str) -> HashMap<String, SpiceJfetModel> {
672 parse_model_registry(src, parse_jfet_model_line)
673}
674
675fn parse_jfet_model_line(line: &str) -> Option<SpiceJfetModel> {
677 let rest = strip_directive(line, ".MODEL")?;
678 let (name, rest) = rest.split_once(|c: char| c.is_whitespace())?;
679 let rest = rest.trim_start();
680
681 let (type_str, params_block) = if let Some(paren_pos) = rest.find('(') {
683 let type_str = rest[..paren_pos].trim();
684 let close_paren = rest.rfind(')')?;
685 let params = &rest[paren_pos + 1..close_paren];
686 (type_str, params)
687 } else {
688 let type_end = rest.find(|c: char| c.is_whitespace()).unwrap_or(rest.len());
691 let type_str = &rest[..type_end];
692 let params = if type_end < rest.len() {
693 &rest[type_end..]
694 } else {
695 ""
696 };
697 (type_str.trim(), params)
698 };
699
700 let is_n_channel = match type_str.to_uppercase().as_str() {
701 "NJF" => true,
702 "PJF" => false,
703 _ => return None, };
705
706 let mut model = SpiceJfetModel::defaults(name, is_n_channel);
707
708 for_each_param(params_block, |key, val| match key {
709 "VTO" => model.vto = val,
710 "BETA" => model.beta = val,
711 "LAMBDA" => model.lambda = val,
712 "IS" => model.is = val,
713 "N" => model.n = val,
714 "RD" => model.rd = val,
715 "RS" => model.rs = val,
716 "CGS" => model.cgs = val,
717 "CGD" => model.cgd = val,
718 "PB" => model.pb = val,
719 _ => {} });
721
722 Some(model)
723}
724
725fn parse_diode_models(src: &str) -> HashMap<String, ShockleyDiodeModel> {
734 parse_model_registry(src, parse_diode_model_line)
735}
736
737fn parse_diode_model_line(line: &str) -> Option<ShockleyDiodeModel> {
739 let rest = strip_directive(line, ".MODEL")?;
740 let (name, rest) = rest.split_once(|c: char| c.is_whitespace())?;
741 let rest = rest.trim_start();
742
743 let (type_str, params_block) = if let Some(paren_pos) = rest.find('(') {
745 let type_str = rest[..paren_pos].trim();
746 let close_paren = rest.rfind(')')?;
747 let params = &rest[paren_pos + 1..close_paren];
748 (type_str, params)
749 } else {
750 return None;
751 };
752
753 if !type_str.eq_ignore_ascii_case("D") {
754 return None;
755 }
756
757 let mut model = ShockleyDiodeModel::defaults(name);
758
759 for_each_param(params_block, |key, val| match key {
760 "IS" => model.is = val,
761 "RS" => model.rs = val,
762 "N" => model.n = val,
763 "TT" => model.tt = val,
764 "CJO" => model.cjo = val,
765 "VJ" => model.vj = val,
766 "M" => model.m = val,
767 "BV" => model.bv = val,
768 "IBV" => model.ibv = val,
769 "EG" => model.eg = val,
770 "XTI" => model.xti = val,
771 _ => {} });
773
774 Some(model)
775}
776
777fn parse_triode_models(src: &str) -> HashMap<String, SpiceTriodeModel> {
783 parse_model_registry(src, parse_triode_model_line)
784}
785
786fn parse_triode_model_line(line: &str) -> Option<SpiceTriodeModel> {
788 let rest = strip_directive(line, ".TRIODE")?;
789 let (name, params) = rest.split_once(|c: char| c.is_whitespace())?;
790
791 let mut mu = 100.0;
792 let mut ex = 1.4;
793 let mut kg1 = 1060.0;
794 let mut kp = 600.0;
795 let mut kvb = 300.0;
796 let mut rp = 62500.0; for_each_param(params, |key, val| match key {
799 "MU" => mu = val,
800 "EX" => ex = val,
801 "KG1" => kg1 = val,
802 "KP" => kp = val,
803 "KVB" => kvb = val,
804 "RP" => rp = val,
805 _ => {}
806 });
807
808 Some(SpiceTriodeModel {
809 name: name.to_uppercase(),
810 mu,
811 ex,
812 kg1,
813 kp,
814 kvb,
815 rp,
816 })
817}
818
819fn parse_pentode_models(src: &str) -> HashMap<String, SpicePentodeModel> {
825 parse_model_registry(src, parse_pentode_model_line)
826}
827
828fn parse_pentode_model_line(line: &str) -> Option<SpicePentodeModel> {
830 let rest = strip_directive(line, ".PENTODE")?;
831 let (name, params) = rest.split_once(|c: char| c.is_whitespace())?;
832
833 let mut mu = 10.0;
834 let mut ex = 1.35;
835 let mut kg1 = 1000.0;
836 let mut kg2 = 4200.0;
837 let mut kp = 60.0;
838 let mut kvb = 24.0;
839 let mut kvb2 = 20.0;
840 let mut vg2_default = 250.0;
841 let mut rp = 50000.0; for_each_param(params, |key, val| match key {
844 "MU" => mu = val,
845 "EX" => ex = val,
846 "KG1" => kg1 = val,
847 "KG2" => kg2 = val,
848 "KP" => kp = val,
849 "KVB" => kvb = val,
850 "KVB2" => kvb2 = val,
851 "VG2" => vg2_default = val,
852 "RP" => rp = val,
853 _ => {}
854 });
855
856 Some(SpicePentodeModel {
857 name: name.to_uppercase(),
858 mu,
859 ex,
860 kg1,
861 kg2,
862 kp,
863 kvb,
864 kvb2,
865 vg2_default,
866 rp,
867 })
868}
869
870fn parse_opamp_models(src: &str) -> HashMap<String, SpiceOpAmpModel> {
876 parse_model_registry(src, parse_opamp_model_line)
877}
878
879fn parse_opamp_model_line(line: &str) -> Option<SpiceOpAmpModel> {
881 let (rest, is_ota) = if let Some(rest) = strip_directive(line, ".OPAMP") {
882 (rest, false)
883 } else if let Some(rest) = strip_directive(line, ".OTA") {
884 (rest, true)
885 } else {
886 return None;
887 };
888
889 let (name, params) = rest.split_once(|c: char| c.is_whitespace())?;
890 let mut model = SpiceOpAmpModel::defaults(name, is_ota);
891
892 for_each_param(params, |key, val| match key {
893 "A0" | "AOL" | "OPEN_LOOP_GAIN" => model.open_loop_gain = val,
894 "GBW" | "GBP" => model.gbw = val,
895 "SR" | "SLEW" | "SLEW_RATE" => model.slew_rate = val,
896 "VPOS" | "VRAILPOS" | "V_RAIL_POS" => model.v_rail_pos = val,
897 "VNEG" | "VRAILNEG" | "V_RAIL_NEG" => model.v_rail_neg = val,
898 "RO" | "ROUT" | "OUTPUT_IMPEDANCE" => model.output_impedance = val,
899 "COUT" | "OUTPUT_CAPACITANCE" => model.output_capacitance = val,
900 "IABC" => model.ota_iabc = val,
901 "VT" => model.ota_vt = val,
902 "GM" | "GMO" => model.ota_gm = val,
903 "RLOAD" | "RL" => model.ota_r_load = val,
904 _ => {}
905 });
906
907 Some(model)
908}
909
910fn parse_transformer_models(src: &str) -> HashMap<String, SpiceTransformerModel> {
916 parse_model_registry(src, parse_transformer_model_line)
917}
918
919fn parse_transformer_model_line(line: &str) -> Option<SpiceTransformerModel> {
921 let rest = strip_directive(line, ".TRANSFORMER")?;
922 let (name, params) = rest.split_once(|c: char| c.is_whitespace())?;
923 let mut model = SpiceTransformerModel::defaults(name);
924
925 for_each_param(params, |key, val| match key {
926 "LP" | "LPRI" | "PRIMARY_INDUCTANCE" => model.primary_inductance = val,
927 "K" | "COUPLING" => model.coupling = val,
928 "RP" | "RPRI" | "PRIMARY_DCR" => model.primary_dcr = val,
929 "RS" | "RSEC" | "SECONDARY_DCR" => model.secondary_dcr = val,
930 "LLP" | "PRIMARY_LEAKAGE" => model.primary_leakage = val,
931 "LLS" | "SECONDARY_LEAKAGE" => model.secondary_leakage = val,
932 "LM" | "MAGNETIZING_INDUCTANCE" => model.magnetizing_inductance = val,
933 "RC" | "CORE_LOSS" | "CORE_LOSS_RESISTANCE" => model.core_loss_resistance = val,
934 "CP" | "CW" | "CAPACITANCE" | "INTERWINDING_CAPACITANCE" => model.capacitance = val,
935 "N1" | "NP" | "PRIMARY_TURNS" => model.primary_turns = val,
936 "N2" | "NS" | "SECONDARY_TURNS" => model.secondary_turns = val,
937 "AE" | "CORE_AREA" => model.core_area = val,
938 "LE" | "MAGNETIC_PATH_LENGTH" => model.magnetic_path_length = val,
939 "GAP" | "LG" | "GAP_LENGTH" => model.gap_length = val,
940 "IDC" | "BIAS_CURRENT" | "DC_BIAS_CURRENT" => model.dc_bias_current = val,
941 "MS" | "JA_MS" => model.ja_ms = val,
942 "A" | "JA_A" => model.ja_a = val,
943 "ALPHA" | "JA_ALPHA" => model.ja_alpha = val,
944 "JA_K" | "KJA" => model.ja_k = val,
945 "C" | "JA_C" => model.ja_c = val,
946 "HC" => model.hc = val,
947 "BR" => model.br = val,
948 "BS" => model.bs = val,
949 _ => {}
950 });
951
952 Some(model)
953}
954
955pub fn bjt_by_name(name: &str) -> Option<&'static SpiceBjtModel> {
963 lookup_model(&BJT_MODELS, name)
964}
965
966pub fn bjt_is_germanium(name: &str) -> bool {
971 bjt_by_name(name).map(|m| m.is > 1e-6).unwrap_or(false)
972}
973
974pub fn bjt_is_pnp(name: &str) -> bool {
976 bjt_by_name(name).map(|m| m.is_pnp).unwrap_or(false)
977}
978
979pub fn bjt_model_names() -> Vec<&'static str> {
981 model_names(&BJT_MODELS)
982}
983
984pub fn jfet_by_name(name: &str) -> Option<&'static SpiceJfetModel> {
988 lookup_model(&JFET_MODELS, name)
989}
990
991pub fn jfet_model_names() -> Vec<&'static str> {
993 model_names(&JFET_MODELS)
994}
995
996pub fn triode_by_name(name: &str) -> Option<&'static SpiceTriodeModel> {
998 lookup_model(&TRIODE_MODELS, name)
999}
1000
1001pub fn triode_model_names() -> Vec<&'static str> {
1003 model_names(&TRIODE_MODELS)
1004}
1005
1006pub fn pentode_by_name(name: &str) -> Option<&'static SpicePentodeModel> {
1008 lookup_model(&PENTODE_MODELS, name)
1009}
1010
1011pub fn pentode_model_names() -> Vec<&'static str> {
1013 model_names(&PENTODE_MODELS)
1014}
1015
1016pub fn diode_by_name(name: &str) -> Option<&'static ShockleyDiodeModel> {
1020 lookup_model(&DIODE_MODELS, name)
1021}
1022
1023pub fn diode_model_names() -> Vec<&'static str> {
1025 model_names(&DIODE_MODELS)
1026}
1027
1028pub fn led_by_name(name: &str) -> Option<&'static ShockleyDiodeModel> {
1033 lookup_model(&LED_MODELS, name)
1034}
1035
1036pub fn led_model_names() -> Vec<&'static str> {
1038 model_names(&LED_MODELS)
1039}
1040
1041pub fn schottky_by_name(name: &str) -> Option<&'static ShockleyDiodeModel> {
1043 lookup_model(&SCHOTTKY_MODELS, name)
1044}
1045
1046pub fn schottky_model_names() -> Vec<&'static str> {
1048 model_names(&SCHOTTKY_MODELS)
1049}
1050
1051pub fn zener_by_name(name: &str) -> Option<&'static ShockleyDiodeModel> {
1055 lookup_model(&ZENER_MODELS, name)
1056}
1057
1058pub fn zener_model_names() -> Vec<&'static str> {
1060 model_names(&ZENER_MODELS)
1061}
1062
1063pub fn opamp_by_name(name: &str) -> Option<&'static SpiceOpAmpModel> {
1065 lookup_model(&OPAMP_MODELS, name)
1066}
1067
1068pub fn opamp_model_names() -> Vec<&'static str> {
1070 model_names(&OPAMP_MODELS)
1071}
1072
1073pub fn transformer_by_name(name: &str) -> Option<&'static SpiceTransformerModel> {
1075 lookup_model(&TRANSFORMER_MODELS, name)
1076}
1077
1078pub fn transformer_model_names() -> Vec<&'static str> {
1080 model_names(&TRANSFORMER_MODELS)
1081}
1082
1083pub fn any_diode_by_name(name: &str) -> Option<&'static ShockleyDiodeModel> {
1088 let upper = name.to_uppercase();
1089 DIODE_MODELS
1090 .get(&upper)
1091 .or_else(|| LED_MODELS.get(&upper))
1092 .or_else(|| SCHOTTKY_MODELS.get(&upper))
1093 .or_else(|| ZENER_MODELS.get(&upper))
1094}
1095
1096#[cfg(test)]
1101mod tests {
1102 use super::*;
1103
1104 fn assert_close(a: Option<f64>, b: Option<f64>) {
1105 match (a, b) {
1106 (Some(a), Some(b)) => {
1107 let rel = if b.abs() > 1e-30 {
1108 ((a - b) / b).abs()
1109 } else {
1110 (a - b).abs()
1111 };
1112 assert!(rel < 1e-10, "expected {b}, got {a}");
1113 }
1114 _ => assert_eq!(a, b),
1115 }
1116 }
1117
1118 #[test]
1119 fn parse_spice_suffixes() {
1120 assert_close(parse_spice_value("1.0"), Some(1.0));
1121 assert_close(parse_spice_value("6.734f"), Some(6.734e-15));
1122 assert_close(parse_spice_value("4.493p"), Some(4.493e-12));
1123 assert_close(parse_spice_value("301.2p"), Some(301.2e-12));
1124 assert_close(parse_spice_value("24n"), Some(24e-9));
1125 assert_close(parse_spice_value("20.66u"), Some(20.66e-6));
1126 assert_close(parse_spice_value("66.78m"), Some(66.78e-3));
1127 assert_close(parse_spice_value("10"), Some(10.0));
1128 assert_close(parse_spice_value("1k"), Some(1e3));
1129 assert_close(parse_spice_value("1MEG"), Some(1e6));
1130 }
1131
1132 #[test]
1133 fn parse_scientific_notation() {
1134 assert_eq!(parse_spice_value("1e-14"), Some(1e-14));
1136 assert_eq!(parse_spice_value("6.734e-15"), Some(6.734e-15));
1137 assert_eq!(parse_spice_value("3.295E-14"), Some(3.295e-14));
1138 assert_eq!(parse_spice_value("1.41e-15"), Some(1.41e-15));
1139 }
1140
1141 #[test]
1142 fn parse_model_line_npn() {
1143 let line = ".MODEL 2N3904 NPN(IS=6.734f BF=416.4 BR=0.7371 VAF=74.03)";
1144 let model = parse_model_line(line).unwrap();
1145 assert_eq!(model.name, "2N3904");
1146 assert!(!model.is_pnp);
1147 assert!((model.is - 6.734e-15).abs() < 1e-30);
1148 assert!((model.bf - 416.4).abs() < 1e-10);
1149 assert!((model.br - 0.7371).abs() < 1e-10);
1150 assert!((model.vaf - 74.03).abs() < 1e-10);
1151 }
1152
1153 #[test]
1154 fn parse_model_line_pnp() {
1155 let line = ".MODEL AC128 PNP(IS=20.66u BF=229.6 BR=14.66 NF=1.133 NR=1.140 VAF=19.68)";
1156 let model = parse_model_line(line).unwrap();
1157 assert_eq!(model.name, "AC128");
1158 assert!(model.is_pnp);
1159 assert!((model.is - 20.66e-6).abs() < 1e-15);
1160 assert!((model.bf - 229.6).abs() < 1e-10);
1161 assert!((model.nf - 1.133).abs() < 1e-10);
1162 }
1163
1164 #[test]
1165 fn embedded_models_load() {
1166 let names = [
1168 "2N3904",
1169 "2N2222",
1170 "BC108",
1171 "BC109",
1172 "2N5088",
1173 "2N5089",
1174 "2N3906",
1175 "AC128",
1176 "OC44",
1177 "NKT275",
1178 "GENERIC_NPN",
1179 "GENERIC_PNP",
1180 ];
1181 for name in &names {
1182 assert!(
1183 bjt_by_name(name).is_some(),
1184 "Model '{}' not found in embedded transistors.model",
1185 name
1186 );
1187 }
1188 }
1189
1190 #[test]
1191 fn case_insensitive_lookup() {
1192 assert!(bjt_by_name("2n3904").is_some());
1193 assert!(bjt_by_name("2N3904").is_some());
1194 assert!(bjt_by_name("ac128").is_some());
1195 assert!(bjt_by_name("AC128").is_some());
1196 }
1197
1198 #[test]
1199 fn model_polarity() {
1200 assert!(!bjt_by_name("2N3904").unwrap().is_pnp);
1201 assert!(!bjt_by_name("2N2222").unwrap().is_pnp);
1202 assert!(bjt_by_name("2N3906").unwrap().is_pnp);
1203 assert!(bjt_by_name("AC128").unwrap().is_pnp);
1204 assert!(bjt_by_name("OC44").unwrap().is_pnp);
1205 assert!(bjt_by_name("NKT275").unwrap().is_pnp);
1206 }
1207
1208 #[test]
1209 fn germanium_has_high_leakage() {
1210 let ac128 = bjt_by_name("AC128").unwrap();
1211 let n2n3904 = bjt_by_name("2N3904").unwrap();
1212 assert!(ac128.is > n2n3904.is * 1e6);
1214 }
1215
1216 #[test]
1217 fn defaults_for_missing_params() {
1218 let line = ".MODEL MINIMAL NPN(IS=1f BF=100)";
1219 let model = parse_model_line(line).unwrap();
1220 assert_eq!(model.br, 1.0);
1222 assert_eq!(model.nf, 1.0);
1223 assert!(model.vaf.is_infinite());
1224 assert_eq!(model.ise, 0.0);
1225 }
1226
1227 #[test]
1232 fn parse_jfet_njf() {
1233 let line = ".MODEL 2N5457 NJF VTO=-1.25 BETA=1.04m LAMBDA=30.8m CGS=3.58p CGD=3.58p PB=3.99 IS=95.6f";
1234 let model = parse_jfet_model_line(line).unwrap();
1235 assert_eq!(model.name, "2N5457");
1236 assert!(model.is_n_channel);
1237 assert!((model.vto - (-1.25)).abs() < 1e-10);
1238 assert!((model.beta - 1.04e-3).abs() < 1e-12);
1239 assert!((model.lambda - 30.8e-3).abs() < 1e-10);
1240 }
1241
1242 #[test]
1243 fn parse_jfet_pjf() {
1244 let line = ".MODEL 2N5460 PJF VTO=-2.16 BETA=517u LAMBDA=12.6m CGS=5.32p CGD=5.32p PB=722m IS=147f";
1245 let model = parse_jfet_model_line(line).unwrap();
1246 assert_eq!(model.name, "2N5460");
1247 assert!(!model.is_n_channel);
1248 assert!((model.vto - (-2.16)).abs() < 1e-10);
1249 assert_close(Some(model.beta), Some(517e-6));
1250 }
1251
1252 #[test]
1253 fn parse_jfet_space_separated() {
1254 let line = ".MODEL TEST-NJF NJF VTO=-2.0 BETA=1.5m LAMBDA=9m RD=1 RS=1 CGS=2.9p CGD=2.8p";
1256 let model = parse_jfet_model_line(line).unwrap();
1257 assert_eq!(model.name, "TEST-NJF");
1258 assert!(model.is_n_channel);
1259 assert!((model.vto - (-2.0)).abs() < 1e-10);
1260 }
1261
1262 #[test]
1263 fn jfet_embedded_models_load() {
1264 let names = [
1265 "J201",
1266 "2N5457",
1267 "2N5460",
1268 "2N5952",
1269 "2SK30A",
1270 "2SK30A-GR",
1271 "2SK30A-Y",
1272 "2SK30A-BL",
1273 ];
1274 for name in &names {
1275 assert!(
1276 jfet_by_name(name).is_some(),
1277 "JFET model '{}' not found in embedded jfets.model",
1278 name
1279 );
1280 }
1281 }
1282
1283 #[test]
1284 fn jfet_case_insensitive_lookup() {
1285 assert!(jfet_by_name("j201").is_some());
1286 assert!(jfet_by_name("J201").is_some());
1287 assert!(jfet_by_name("2n5457").is_some());
1288 assert!(jfet_by_name("2N5457").is_some());
1289 }
1290
1291 #[test]
1292 fn jfet_polarity() {
1293 assert!(jfet_by_name("2N5457").unwrap().is_n_channel);
1294 assert!(jfet_by_name("J201").unwrap().is_n_channel);
1295 assert!(!jfet_by_name("2N5460").unwrap().is_n_channel);
1296 }
1297
1298 #[test]
1299 fn jfet_rejects_bjt() {
1300 assert!(parse_jfet_model_line(".MODEL 2N3904 NPN(IS=6.734f BF=416.4)").is_none());
1301 }
1302
1303 #[test]
1308 fn parse_triode_line() {
1309 let line = ".TRIODE 12AX7 MU=100 EX=1.4 KG1=1060 KP=600 KVB=300";
1310 let model = parse_triode_model_line(line).unwrap();
1311 assert_eq!(model.name, "12AX7");
1312 assert!((model.mu - 100.0).abs() < 1e-10);
1313 assert!((model.ex - 1.4).abs() < 1e-10);
1314 assert!((model.kg1 - 1060.0).abs() < 1e-10);
1315 assert!((model.kp - 600.0).abs() < 1e-10);
1316 assert!((model.kvb - 300.0).abs() < 1e-10);
1317 }
1318
1319 #[test]
1320 fn triode_embedded_models_load() {
1321 let names = [
1322 "12AX7", "12AT7", "12AU7", "12AY7", "12BH7", "6386", "6DJ8", "2A3", "300B", "6C33C",
1323 "6AN8T", "ECC83", "ECC81", "ECC82", "6072",
1324 ];
1325 for name in &names {
1326 assert!(
1327 triode_by_name(name).is_some(),
1328 "Triode model '{}' not found in embedded triodes.model",
1329 name
1330 );
1331 }
1332 }
1333
1334 #[test]
1335 fn triode_case_insensitive_lookup() {
1336 assert!(triode_by_name("12ax7").is_some());
1337 assert!(triode_by_name("12AX7").is_some());
1338 assert!(triode_by_name("ecc83").is_some());
1339 }
1340
1341 #[test]
1342 fn triode_koren_values_correct() {
1343 let ax7 = triode_by_name("12AX7").unwrap();
1345 assert!((ax7.mu - 100.0).abs() < 1e-10);
1346 assert!((ax7.ex - 1.4).abs() < 1e-10);
1347 assert!((ax7.kg1 - 1060.0).abs() < 1e-10);
1348
1349 let at7 = triode_by_name("12AT7").unwrap();
1350 assert!((at7.mu - 60.0).abs() < 1e-10);
1351 assert!((at7.ex - 1.35).abs() < 1e-10);
1352
1353 let au7 = triode_by_name("12AU7").unwrap();
1354 assert!((au7.mu - 21.5).abs() < 1e-10);
1355 assert!((au7.ex - 1.3).abs() < 1e-10);
1356 }
1357
1358 #[test]
1359 fn triode_aliases_match() {
1360 let ax7 = triode_by_name("12AX7").unwrap();
1361 let ecc83 = triode_by_name("ECC83").unwrap();
1362 assert!((ax7.mu - ecc83.mu).abs() < 1e-10);
1363 assert!((ax7.kp - ecc83.kp).abs() < 1e-10);
1364 }
1365
1366 #[test]
1371 fn parse_pentode_line() {
1372 let line = ".PENTODE 6550 MU=7.9 EX=1.35 KG1=890 KG2=4200 KP=60 KVB=24 KVB2=28 VG2=450";
1373 let model = parse_pentode_model_line(line).unwrap();
1374 assert_eq!(model.name, "6550");
1375 assert!((model.mu - 7.9).abs() < 1e-10);
1376 assert!((model.ex - 1.35).abs() < 1e-10);
1377 assert!((model.kg1 - 890.0).abs() < 1e-10);
1378 assert!((model.kg2 - 4200.0).abs() < 1e-10);
1379 assert!((model.kp - 60.0).abs() < 1e-10);
1380 assert!((model.kvb - 24.0).abs() < 1e-10);
1381 assert!((model.kvb2 - 28.0).abs() < 1e-10);
1382 assert!((model.vg2_default - 450.0).abs() < 1e-10);
1383 }
1384
1385 #[test]
1386 fn pentode_embedded_models_load() {
1387 let names = [
1388 "6550", "EL34", "6L6GC", "KT88", "6AN8P", "EF86", "EL84", "6AQ5A", "6973", "6CA7",
1389 "KT77", "5881", "KT66", "KT90", "6L6", "6BQ5", "6267", "6AQ5",
1390 ];
1391 for name in &names {
1392 assert!(
1393 pentode_by_name(name).is_some(),
1394 "Pentode model '{}' not found in embedded pentodes.model",
1395 name
1396 );
1397 }
1398 }
1399
1400 #[test]
1401 fn pentode_case_insensitive_lookup() {
1402 assert!(pentode_by_name("el34").is_some());
1403 assert!(pentode_by_name("EL34").is_some());
1404 assert!(pentode_by_name("kt88").is_some());
1405 }
1406
1407 #[test]
1408 fn pentode_koren_values_correct() {
1409 let p6550 = pentode_by_name("6550").unwrap();
1411 assert!((p6550.mu - 7.9).abs() < 1e-10);
1412 assert!((p6550.kp - 60.0).abs() < 1e-10);
1413 assert!((p6550.kvb - 24.0).abs() < 1e-10);
1414
1415 let kt88 = pentode_by_name("KT88").unwrap();
1416 assert!((kt88.mu - 8.8).abs() < 1e-10);
1417 assert!((kt88.kp - 32.0).abs() < 1e-10);
1418 }
1419
1420 #[test]
1421 fn pentode_kt88_is_distinct_from_6550() {
1422 let p6550 = pentode_by_name("6550").unwrap();
1423 let kt88 = pentode_by_name("KT88").unwrap();
1424 assert!((p6550.kp - kt88.kp).abs() > 1.0);
1426 }
1427
1428 #[test]
1429 fn pentode_aliases_match() {
1430 let el34 = pentode_by_name("EL34").unwrap();
1431 let p6ca7 = pentode_by_name("6CA7").unwrap();
1432 assert!((el34.mu - p6ca7.mu).abs() < 1e-10);
1433 assert!((el34.kp - p6ca7.kp).abs() < 1e-10);
1434 }
1435
1436 #[test]
1441 fn parse_opamp_line() {
1442 let line = ".OPAMP TL072 A0=200k GBW=3MEG SR=13 VPOS=12 VNEG=12 RO=75 COUT=20p";
1443 let model = parse_opamp_model_line(line).unwrap();
1444 assert_eq!(model.name, "TL072");
1445 assert!(!model.is_ota);
1446 assert!((model.open_loop_gain - 200_000.0).abs() < 1e-10);
1447 assert!((model.gbw - 3e6).abs() < 1e-3);
1448 assert!((model.slew_rate - 13.0).abs() < 1e-10);
1449 assert!((model.output_capacitance - 20e-12).abs() < 1e-20);
1450 }
1451
1452 #[test]
1453 fn parse_ota_line() {
1454 let line = ".OTA CA3080 A0=100k GBW=2MEG SR=50 IABC=100u VT=25.85m GM=2m RLOAD=10k";
1455 let model = parse_opamp_model_line(line).unwrap();
1456 assert_eq!(model.name, "CA3080");
1457 assert!(model.is_ota);
1458 assert!((model.gbw - 2e6).abs() < 1e-3);
1459 assert!((model.ota_iabc - 100e-6).abs() < 1e-15);
1460 assert!((model.ota_vt - 25.85e-3).abs() < 1e-12);
1461 assert!((model.ota_gm - 2e-3).abs() < 1e-15);
1462 assert!((model.ota_r_load - 10_000.0).abs() < 1e-10);
1463 }
1464
1465 #[test]
1466 fn opamp_embedded_models_load() {
1467 let names = [
1468 "GENERIC", "TL072", "TL082", "JRC4558", "RC4558", "LM308", "LM741", "NE5532", "OP07",
1469 "CA3080",
1470 ];
1471 for name in &names {
1472 assert!(
1473 opamp_by_name(name).is_some(),
1474 "Op-amp model '{}' not found in embedded opamps.model",
1475 name
1476 );
1477 }
1478 }
1479
1480 #[test]
1481 fn opamp_case_insensitive() {
1482 assert!(opamp_by_name("tl072").is_some());
1483 assert!(opamp_by_name("jrc4558").is_some());
1484 assert!(opamp_by_name("ca3080").is_some());
1485 }
1486
1487 #[test]
1492 fn parse_transformer_directive_line() {
1493 let line = ".TRANSFORMER TEST LP=2 K=0.99 RP=75 RS=50 LLP=20m LLS=200u LM=1.98 RC=500k CP=200p N1=10 N2=1";
1494 let model = parse_transformer_model_line(line).unwrap();
1495 assert_eq!(model.name, "TEST");
1496 assert!((model.primary_inductance - 2.0).abs() < 1e-12);
1497 assert!((model.coupling - 0.99).abs() < 1e-12);
1498 assert!((model.primary_dcr - 75.0).abs() < 1e-12);
1499 assert!((model.secondary_dcr - 50.0).abs() < 1e-12);
1500 assert!((model.primary_leakage - 20e-3).abs() < 1e-15);
1501 assert!((model.secondary_leakage - 200e-6).abs() < 1e-18);
1502 assert!((model.magnetizing_inductance - 1.98).abs() < 1e-12);
1503 assert!((model.core_loss_resistance - 500_000.0).abs() < 1e-6);
1504 assert!((model.capacitance - 200e-12).abs() < 1e-21);
1505 assert!((model.primary_turns - 10.0).abs() < 1e-12);
1506 assert!((model.secondary_turns - 1.0).abs() < 1e-12);
1507 }
1508
1509 #[test]
1510 fn transformer_embedded_models_load() {
1511 for name in &["GENERIC_600_600", "JT11P1", "JT10KB-D", "A262A2E"] {
1512 assert!(
1513 transformer_by_name(name).is_some(),
1514 "Transformer model '{}' not found in embedded transformers.model",
1515 name
1516 );
1517 }
1518 }
1519
1520 #[test]
1521 fn transformer_case_insensitive_lookup() {
1522 assert!(transformer_by_name("jt11p1").is_some());
1523 assert!(transformer_by_name("JT11P1").is_some());
1524 }
1525
1526 #[test]
1531 fn parse_diode_line() {
1532 let line = ".MODEL 1N914 D(IS=7.075E-9 RS=0.78 N=1.95 TT=7.2E-9 CJO=4E-12 VJ=0.657 M=0.4 BV=100 IBV=0.0001)";
1533 let model = parse_diode_model_line(line).unwrap();
1534 assert_eq!(model.name, "1N914");
1535 assert!((model.is - 7.075e-9).abs() < 1e-20);
1536 assert!((model.rs - 0.78).abs() < 1e-10);
1537 assert!((model.n - 1.95).abs() < 1e-10);
1538 assert!((model.bv - 100.0).abs() < 1e-10);
1539 }
1540
1541 #[test]
1542 fn diode_embedded_models_load() {
1543 let names = [
1544 "1N34", "1N914", "1N4001", "1N4002", "1N4003", "1N4004", "1N4005", "1N4006", "1N4007",
1545 "1N4148", "1N5400",
1546 ];
1547 for name in &names {
1548 assert!(
1549 diode_by_name(name).is_some(),
1550 "Diode model '{}' not found in embedded diodes.model",
1551 name
1552 );
1553 }
1554 }
1555
1556 #[test]
1557 fn diode_case_insensitive() {
1558 assert!(diode_by_name("1n914").is_some());
1559 assert!(diode_by_name("1N914").is_some());
1560 assert!(diode_by_name("1n4148").is_some());
1561 }
1562
1563 #[test]
1564 fn diode_germanium_higher_is() {
1565 let ge = diode_by_name("1N34").unwrap();
1566 let si = diode_by_name("1N914").unwrap();
1567 assert!(
1569 ge.n > si.n,
1570 "Ge diode should have higher N: Ge={} Si={}",
1571 ge.n,
1572 si.n
1573 );
1574 }
1575
1576 #[test]
1581 fn led_embedded_models_load() {
1582 let names = [
1583 "DLED0",
1584 "DLED1",
1585 "DLED2",
1586 "DLED3",
1587 "LED_IR",
1588 "LED_RED",
1589 "LED_GREEN",
1590 "LED_BLUE",
1591 ];
1592 for name in &names {
1593 assert!(
1594 led_by_name(name).is_some(),
1595 "LED model '{}' not found in embedded leds.model",
1596 name
1597 );
1598 }
1599 }
1600
1601 #[test]
1602 fn led_blue_higher_n() {
1603 let red = led_by_name("LED_RED").unwrap();
1604 let blue = led_by_name("LED_BLUE").unwrap();
1605 assert!(
1607 blue.n > red.n,
1608 "Blue LED should have higher N (Vf): {} vs {}",
1609 blue.n,
1610 red.n
1611 );
1612 }
1613
1614 #[test]
1619 fn schottky_embedded_models_load() {
1620 let names = ["11DQ03", "11DQ04", "1N5828"];
1621 for name in &names {
1622 assert!(
1623 schottky_by_name(name).is_some(),
1624 "Schottky model '{}' not found in embedded schottky.model",
1625 name
1626 );
1627 }
1628 }
1629
1630 #[test]
1631 fn schottky_has_low_bandgap() {
1632 let s = schottky_by_name("11DQ03").unwrap();
1633 assert!(
1634 (s.eg - 0.69).abs() < 0.01,
1635 "Schottky EG should be ~0.69: {}",
1636 s.eg
1637 );
1638 assert!(
1639 (s.xti - 2.0).abs() < 0.1,
1640 "Schottky XTI should be ~2: {}",
1641 s.xti
1642 );
1643 }
1644
1645 #[test]
1650 fn zener_embedded_models_load() {
1651 let names = [
1652 "1N746", "1N750", "1N753", "1N758", "1N759", "1N4728", "1N4733", "1N4742", "1N4751",
1653 ];
1654 for name in &names {
1655 assert!(
1656 zener_by_name(name).is_some(),
1657 "Zener model '{}' not found in embedded zeners.model",
1658 name
1659 );
1660 }
1661 }
1662
1663 #[test]
1664 fn zener_voltage_from_bv() {
1665 let z4v7 = zener_by_name("1N750").unwrap();
1666 assert!(
1668 z4v7.bv > 4.0 && z4v7.bv < 5.0,
1669 "1N750 BV should be ~4.7V: {}",
1670 z4v7.bv
1671 );
1672
1673 let z12v = zener_by_name("1N759").unwrap();
1674 assert!(
1676 z12v.bv > 11.0 && z12v.bv < 13.0,
1677 "1N759 BV should be ~12V: {}",
1678 z12v.bv
1679 );
1680 }
1681
1682 #[test]
1683 fn any_diode_cross_registry() {
1684 assert!(any_diode_by_name("1N914").is_some(), "Standard diode");
1686 assert!(any_diode_by_name("LED_RED").is_some(), "LED");
1687 assert!(any_diode_by_name("11DQ03").is_some(), "Schottky");
1688 assert!(any_diode_by_name("1N750").is_some(), "Zener");
1689 }
1690}