use crate::diode::V_T;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BjtKind {
#[cfg_attr(feature = "serde", serde(rename = "npn"))]
Npn,
#[cfg_attr(feature = "serde", serde(rename = "pnp"))]
Pnp,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BjtRegion {
Cutoff,
Active,
Saturation,
}
impl BjtRegion {
pub fn as_str(&self) -> &'static str {
match self {
BjtRegion::Cutoff => "cutoff",
BjtRegion::Active => "active",
BjtRegion::Saturation => "saturation",
}
}
}
#[derive(Debug, Clone)]
pub struct BjtParams {
pub is: f64, pub bf: f64, pub nf: f64, pub br: f64, pub nr: f64, pub vaf: f64, pub var: f64, pub temperature: f64, }
impl BjtParams {
pub fn new(bf: f64) -> Self {
Self {
is: 1e-14,
bf,
nf: 1.0,
br: 1.0,
nr: 1.0,
vaf: 0.0,
var: 0.0,
temperature: 300.15,
}
}
}
pub struct BjtCompanion {
pub g_be: f64, pub g_bc: f64, pub g_ce: f64, pub ic: f64, pub ib: f64, pub vbe: f64, pub vbc: f64, }
const V_MAX_EXP: f64 = 40.0 * V_T;
pub fn bjt_companion(vbe: f64, vbc: f64, params: &BjtParams) -> BjtCompanion {
let nf_vt = params.nf * V_T;
let nr_vt = params.nr * V_T;
let (exp_be, vbe_eval) = if vbe > V_MAX_EXP {
let e = (V_MAX_EXP / nf_vt).exp();
(e * (1.0 + (vbe - V_MAX_EXP) / nf_vt), vbe)
} else {
((vbe / nf_vt).exp(), vbe)
};
let (exp_bc, vbc_eval) = if vbc > V_MAX_EXP {
let e = (V_MAX_EXP / nr_vt).exp();
(e * (1.0 + (vbc - V_MAX_EXP) / nr_vt), vbc)
} else {
((vbc / nr_vt).exp(), vbc)
};
let _ = (vbe_eval, vbc_eval);
let i_f = params.is * (exp_be - 1.0);
let i_r = params.is * (exp_bc - 1.0);
const G_MAX: f64 = 10.0;
let g_be = ((params.is / nf_vt) * exp_be).min(G_MAX);
let g_bc = ((params.is / nr_vt) * exp_bc).min(G_MAX);
let alpha_r = params.br / (params.br + 1.0);
let ic = i_f - i_r / alpha_r;
let ib = i_f / params.bf + i_r / params.br;
let g_ce = if params.vaf > 0.0 && params.vaf.is_finite() {
ic.abs() / params.vaf
} else {
0.0
};
BjtCompanion {
g_be,
g_bc,
g_ce,
ic,
ib,
vbe,
vbc,
}
}
pub fn detect_region(vbe: f64, vbc: f64) -> BjtRegion {
if vbe < 0.1 {
BjtRegion::Cutoff
} else if vbc < 0.0 {
BjtRegion::Active
} else {
BjtRegion::Saturation
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn bjt_companion_at_zero_voltages() {
let params = BjtParams::new(100.0);
let comp = bjt_companion(0.0, 0.0, ¶ms);
assert_relative_eq!(comp.ic, 0.0, epsilon = 1e-20);
assert_relative_eq!(comp.ib, 0.0, epsilon = 1e-20);
let expected_g_be = params.is / (params.nf * V_T);
assert_relative_eq!(comp.g_be, expected_g_be, epsilon = 1e-20);
assert!(comp.g_be > 0.0);
let expected_g_bc = params.is / (params.nr * V_T);
assert_relative_eq!(comp.g_bc, expected_g_bc, epsilon = 1e-20);
assert!(comp.g_bc > 0.0);
}
#[test]
fn bjt_companion_forward_active() {
let params = BjtParams::new(100.0);
let comp = bjt_companion(0.7, -5.0, ¶ms);
assert!(comp.ic > 1e-3, "Ic should be in mA range, got {}", comp.ic);
assert!(comp.ib > 0.0);
assert!(comp.ib < comp.ic);
assert!(
comp.g_be > comp.g_bc * 1e6,
"g_be={} should be >> g_bc={}",
comp.g_be,
comp.g_bc
);
}
#[test]
fn detect_region_cutoff() {
assert_eq!(detect_region(-0.5, -5.0), BjtRegion::Cutoff);
}
#[test]
fn detect_region_active() {
assert_eq!(detect_region(0.7, -5.0), BjtRegion::Active);
}
#[test]
fn detect_region_saturation() {
assert_eq!(detect_region(0.7, 0.3), BjtRegion::Saturation);
}
#[test]
fn test_bjt_companion_saturation_both_junctions() {
let params = BjtParams::new(100.0);
let comp = bjt_companion(0.7, 0.6, ¶ms);
assert!(comp.g_be > 1e-3, "g_be={} should be significant", comp.g_be);
assert!(comp.g_bc > 1e-3, "g_bc={} should be significant", comp.g_bc);
assert!(
comp.g_be > comp.g_bc,
"g_be={} should > g_bc={}",
comp.g_be,
comp.g_bc
);
let i_f = params.is * ((0.7 / (params.nf * V_T)).exp() - 1.0);
let i_r = params.is * ((0.6 / (params.nr * V_T)).exp() - 1.0);
assert!(i_f > 0.0, "i_f should be positive");
assert!(i_r > 0.0, "i_r should be positive");
let active_comp = bjt_companion(0.7, -5.0, ¶ms);
assert!(
comp.ic < active_comp.ic,
"Saturation Ic={} should < Active Ic={}",
comp.ic,
active_comp.ic
);
let ie = -(comp.ic + comp.ib);
assert!((comp.ic + comp.ib + ie).abs() < 1e-15, "KCL violated");
}
#[test]
fn bjt_early_voltage_zero_means_no_effect() {
let params = BjtParams::new(100.0);
assert_eq!(params.vaf, 0.0);
let comp = bjt_companion(0.7, -5.0, ¶ms);
assert_eq!(comp.g_ce, 0.0, "g_ce should be 0 when vaf=0");
}
#[test]
fn bjt_early_voltage_nonzero_adds_conductance() {
let mut params = BjtParams::new(100.0);
params.vaf = 100.0;
let comp = bjt_companion(0.7, -5.0, ¶ms);
assert!(
comp.g_ce > 0.0,
"g_ce should be positive when vaf>0 and Ic>0"
);
let expected_g_ce = comp.ic.abs() / 100.0;
assert_relative_eq!(comp.g_ce, expected_g_ce, epsilon = 1e-15);
}
#[test]
fn test_bjt_companion_pnp_sign_convention() {
let params = BjtParams::new(100.0);
let npn = bjt_companion(0.7, -5.0, ¶ms);
let pnp = bjt_companion(0.7, -5.0, ¶ms);
assert_relative_eq!(npn.ic, pnp.ic, epsilon = 1e-15);
assert_relative_eq!(npn.ib, pnp.ib, epsilon = 1e-15);
assert_relative_eq!(npn.g_be, pnp.g_be, epsilon = 1e-15);
assert_relative_eq!(npn.g_bc, pnp.g_bc, epsilon = 1e-15);
}
}