sindr_devices/jfet.rs
1//! JFET (Junction Field-Effect Transistor) device model.
2//!
3//! Shichman–Hodges square-law model. N-channel and P-channel are both
4//! supported via [`JfetKind`]; the P-channel case mirrors N-channel through
5//! sign conventions on `vgs` and `vds`.
6//!
7//! At each Newton–Raphson iteration, [`jfet_companion`] returns a
8//! linearised contribution that an MNA solver stamps for the [gate, drain,
9//! source] terminals.
10
11const GMIN: f64 = 1e-12; // minimum conductance floor
12
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum JfetKind {
16 #[cfg_attr(feature = "serde", serde(rename = "nchannel"))]
17 NChannel,
18 #[cfg_attr(feature = "serde", serde(rename = "pchannel"))]
19 PChannel,
20}
21
22/// Linearised JFET companion model — the output of one Newton–Raphson
23/// iteration ready for MNA stamping.
24pub struct JfetCompanion {
25 /// Transconductance `∂Id/∂Vgs` (S).
26 pub gm: f64,
27 /// Drain–source conductance `∂Id/∂Vds` (S).
28 pub gds: f64,
29 /// Equivalent current source for the MNA right-hand side (A).
30 pub i_eq: f64,
31}
32
33/// Compute JFET companion at operating point (vgs, vds).
34/// Nodes order: [gate, drain, source] — same as MOSFET convention.
35/// N-channel: Vp < 0 (typically -2 to -6 V), Idss > 0.
36pub fn jfet_companion(vgs: f64, vds: f64, kind: JfetKind, idss: f64, vp: f64) -> JfetCompanion {
37 let (vgs, vds, idss, vp) = match kind {
38 JfetKind::NChannel => (vgs, vds, idss, vp),
39 JfetKind::PChannel => (-vgs, -vds, idss, -vp.abs()), // sign-flip for P-channel
40 };
41
42 // Clamp vgs to avoid numerical issues
43 let vgs = vgs.max(vp - 1.0); // don't go too deep into cutoff
44
45 if vgs <= vp {
46 // Cutoff region: Id = 0
47 return JfetCompanion {
48 gm: GMIN,
49 gds: GMIN,
50 i_eq: 0.0,
51 };
52 }
53
54 let vgs_norm = 1.0 - vgs / vp; // (1 - Vgs/Vp)
55
56 if vds >= vgs - vp {
57 // Saturation region: Id = Idss * (1 - Vgs/Vp)²
58 let id = idss * vgs_norm * vgs_norm;
59 // gm = dId/dVgs = 2*Idss*(1 - Vgs/Vp)*(-1/Vp) = -2*Idss*vgs_norm/Vp
60 let gm = (-2.0 * idss * vgs_norm / vp).max(GMIN);
61 let gds = GMIN;
62 let i_eq = id - gm * vgs - gds * vds;
63 JfetCompanion { gm, gds, i_eq }
64 } else {
65 // Triode (ohmic) region: Id = Idss/Vp² * (2*(Vgs-Vp)*Vds - Vds²)
66 let vp_sq = vp * vp;
67 let id = idss / vp_sq * (2.0 * (vgs - vp) * vds - vds * vds);
68 // gm = dId/dVgs = 2*Idss*Vds/Vp²
69 let gm = (2.0 * idss * vds / vp_sq).abs().max(GMIN);
70 // gds = dId/dVds = 2*Idss*(Vgs-Vp-Vds)/Vp²
71 let gds = (2.0 * idss * (vgs - vp - vds) / vp_sq).max(GMIN);
72 let i_eq = id - gm * vgs - gds * vds;
73 JfetCompanion { gm, gds, i_eq }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn nchannel_saturation_at_vgs_zero() {
83 // Vgs=0, Vds > |Vp|: Id should equal Idss
84 let c = jfet_companion(0.0, 5.0, JfetKind::NChannel, 10e-3, -2.0);
85 // Id = Idss * (1 - 0/(-2))² = Idss * 1 = 10 mA
86 // Verify via i_eq + gm*0 + gds*5 ≈ 10 mA
87 let id_approx = c.i_eq + c.gm * 0.0 + c.gds * 5.0;
88 assert!(
89 (id_approx - 10e-3).abs() < 1e-4,
90 "Id at Vgs=0 should be ~Idss=10mA, got {}",
91 id_approx
92 );
93 }
94
95 #[test]
96 fn nchannel_cutoff() {
97 // Vgs = Vp (pinch-off)
98 let c = jfet_companion(-2.0, 5.0, JfetKind::NChannel, 10e-3, -2.0);
99 assert!(
100 c.i_eq.abs() < 1e-9,
101 "Cutoff Id should be ~0, got {}",
102 c.i_eq
103 );
104 }
105}