Skip to main content

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}