use crate::error::OxiPhotonError;
use super::{
continuity::bernoulli,
material::{SemiconductorMaterial, StatisticsModel, Q},
poisson::eps_fcm,
recombination::total_recombination,
};
fn thomas_solve(
lower: &[f64],
diag: &mut [f64],
upper: &[f64],
rhs: &mut [f64],
) -> Result<Vec<f64>, OxiPhotonError> {
let n = diag.len();
for i in 1..n {
let pivot = diag[i - 1];
if pivot.abs() < 1e-300 {
return Err(OxiPhotonError::NumericalError(format!(
"Thomas: near-zero pivot at row {}",
i - 1
)));
}
let w = lower[i - 1] / pivot;
diag[i] -= w * upper[i - 1];
rhs[i] -= w * rhs[i - 1];
}
let mut x = vec![0.0_f64; n];
let last = n - 1;
if diag[last].abs() < 1e-300 {
return Err(OxiPhotonError::NumericalError(
"Thomas: zero pivot at last row".to_string(),
));
}
x[last] = rhs[last] / diag[last];
for i in (0..last).rev() {
x[i] = (rhs[i] - upper[i] * x[i + 1]) / diag[i];
}
Ok(x)
}
#[allow(clippy::too_many_arguments)]
fn solve_poisson_equilibrium(
psi: &mut [f64],
nd: &[f64],
na: &[f64],
dx: &[f64],
eps_r: f64,
ni_eff: &[f64],
vt: f64,
psi_left: f64,
psi_right: f64,
max_iter: usize,
) -> Result<usize, OxiPhotonError> {
let nn = psi.len();
let eps = eps_fcm(eps_r);
for iter in 0..max_iter {
let mut lower = vec![0.0_f64; nn - 1];
let mut diag = vec![0.0_f64; nn];
let mut upper = vec![0.0_f64; nn - 1];
let mut f = vec![0.0_f64; nn];
diag[0] = 1.0;
f[0] = psi[0] - psi_left;
diag[nn - 1] = 1.0;
f[nn - 1] = psi[nn - 1] - psi_right;
for i in 1..nn - 1 {
let dx_l = dx[i - 1];
let dx_r = dx[i];
let dx_avg = 0.5 * (dx_l + dx_r);
let ep = (psi[i] / vt).exp();
let em = (-psi[i] / vt).exp();
let ni_i = ni_eff[i];
let n_i = ni_i * ep;
let p_i = ni_i * em;
f[i] = eps * (psi[i + 1] - psi[i]) / dx_r - eps * (psi[i] - psi[i - 1]) / dx_l
+ Q * (p_i - n_i + nd[i] - na[i]) * dx_avg;
lower[i - 1] = eps / dx_l;
diag[i] = -eps * (1.0 / dx_l + 1.0 / dx_r) - Q * dx_avg * ni_i / vt * (ep + em);
upper[i] = eps / dx_r;
}
let f_norm = f[1..nn - 1]
.iter()
.map(|&x| x.abs())
.fold(0.0_f64, f64::max);
if f_norm < 1e-8 {
return Ok(iter);
}
let mut neg_f: Vec<f64> = f.iter().map(|&x| -x).collect();
let delta = thomas_solve(&lower, &mut diag, &upper, &mut neg_f)?;
let max_dpsi = delta[1..nn - 1]
.iter()
.map(|&d| d.abs())
.fold(0.0_f64, f64::max);
let damp = if max_dpsi > 5.0 * vt {
5.0 * vt / max_dpsi
} else {
1.0
};
for i in 1..nn - 1 {
psi[i] += damp * delta[i];
}
}
Err(OxiPhotonError::NumericalError(format!(
"Poisson equilibrium Newton did not converge in {max_iter} iterations"
)))
}
#[allow(clippy::too_many_arguments)]
fn solve_n_tridiag(
n: &mut [f64],
psi: &[f64],
p_old: &[f64],
gen: &[f64],
dx: &[f64],
mat: &SemiconductorMaterial,
temp_k: f64,
n_left: f64,
n_right: f64,
ni_eff_node: &[f64],
dn_per_edge: &[f64],
) -> Result<(), OxiPhotonError> {
let nn = n.len();
let ni = mat.ni_cm3;
let vt = mat.vt_at(temp_k);
let mut lower = vec![0.0_f64; nn - 1];
let mut diag = vec![0.0_f64; nn];
let mut upper = vec![0.0_f64; nn - 1];
let mut rhs = vec![0.0_f64; nn];
diag[0] = 1.0;
rhs[0] = n_left;
diag[nn - 1] = 1.0;
rhs[nn - 1] = n_right;
for i in 1..nn - 1 {
let dx_l = dx[i - 1];
let dx_r = dx[i];
let dx_avg = 0.5 * (dx_l + dx_r);
let dpsi_r = (psi[i + 1] - psi[i]) / vt;
let dpsi_l = (psi[i] - psi[i - 1]) / vt;
let br_pos = bernoulli(dpsi_r);
let br_neg = bernoulli(-dpsi_r);
let bl_pos = bernoulli(dpsi_l);
let bl_neg = bernoulli(-dpsi_l);
let cr = Q * dn_per_edge[i] / dx_r;
let cl = Q * dn_per_edge[i - 1] / dx_l;
lower[i - 1] = -cl * bl_neg;
diag[i] = cr * br_neg + cl * bl_pos;
upper[i] = -cr * br_pos;
let r_old = total_recombination(n[i], p_old[i], ni_eff_node[i], mat);
rhs[i] = Q * (r_old - gen[i]) * dx_avg;
}
let n_new = thomas_solve(&lower, &mut diag, &upper, &mut rhs)?;
for i in 0..nn {
n[i] = n_new[i].max(1e-10 * ni);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn solve_p_tridiag(
p: &mut [f64],
psi: &[f64],
n_cur: &[f64],
gen: &[f64],
dx: &[f64],
mat: &SemiconductorMaterial,
temp_k: f64,
p_left: f64,
p_right: f64,
ni_eff_node: &[f64],
dp_per_edge: &[f64],
) -> Result<(), OxiPhotonError> {
let nn = p.len();
let ni = mat.ni_cm3;
let vt = mat.vt_at(temp_k);
let mut lower = vec![0.0_f64; nn - 1];
let mut diag = vec![0.0_f64; nn];
let mut upper = vec![0.0_f64; nn - 1];
let mut rhs = vec![0.0_f64; nn];
diag[0] = 1.0;
rhs[0] = p_left;
diag[nn - 1] = 1.0;
rhs[nn - 1] = p_right;
for i in 1..nn - 1 {
let dx_l = dx[i - 1];
let dx_r = dx[i];
let dx_avg = 0.5 * (dx_l + dx_r);
let dpsi_r = (psi[i + 1] - psi[i]) / vt;
let dpsi_l = (psi[i] - psi[i - 1]) / vt;
let br_pos = bernoulli(dpsi_r);
let br_neg = bernoulli(-dpsi_r);
let bl_pos = bernoulli(dpsi_l);
let bl_neg = bernoulli(-dpsi_l);
let cr = Q * dp_per_edge[i] / dx_r;
let cl = Q * dp_per_edge[i - 1] / dx_l;
lower[i - 1] = -cl * bl_pos;
diag[i] = cl * bl_neg + cr * br_pos;
upper[i] = -cr * br_neg;
let r_old = total_recombination(n_cur[i], p[i], ni_eff_node[i], mat);
rhs[i] = -Q * (r_old - gen[i]) * dx_avg;
}
let p_new = thomas_solve(&lower, &mut diag, &upper, &mut rhs)?;
for i in 0..nn {
p[i] = p_new[i].max(1e-10 * ni);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn solve_poisson_gummel_inner(
psi: &mut [f64],
n_ref: &[f64],
p_ref: &[f64],
psi_ref: &[f64],
nd: &[f64],
na: &[f64],
dx: &[f64],
eps_r: f64,
vt: f64,
psi_left: f64,
psi_right: f64,
max_inner: usize,
) -> Result<(), OxiPhotonError> {
let nn = psi.len();
let eps = eps_fcm(eps_r);
for _iter in 0..max_inner {
let mut lower = vec![0.0_f64; nn - 1];
let mut diag = vec![0.0_f64; nn];
let mut upper = vec![0.0_f64; nn - 1];
let mut f = vec![0.0_f64; nn];
diag[0] = 1.0;
f[0] = psi[0] - psi_left;
diag[nn - 1] = 1.0;
f[nn - 1] = psi[nn - 1] - psi_right;
for i in 1..nn - 1 {
let dx_l = dx[i - 1];
let dx_r = dx[i];
let dx_avg = 0.5 * (dx_l + dx_r);
let dv = (psi[i] - psi_ref[i]) / vt;
let n_i = n_ref[i] * dv.exp();
let p_i = p_ref[i] * (-dv).exp();
f[i] = eps * (psi[i + 1] - psi[i]) / dx_r - eps * (psi[i] - psi[i - 1]) / dx_l
+ Q * (p_i - n_i + nd[i] - na[i]) * dx_avg;
lower[i - 1] = eps / dx_l;
diag[i] = -eps * (1.0 / dx_l + 1.0 / dx_r) - Q * dx_avg * (n_i + p_i) / vt;
upper[i] = eps / dx_r;
}
let f_norm = f[1..nn - 1]
.iter()
.map(|&x| x.abs())
.fold(0.0_f64, f64::max);
if f_norm < 1e-8 {
return Ok(());
}
let mut neg_f: Vec<f64> = f.iter().map(|&x| -x).collect();
let delta = thomas_solve(&lower, &mut diag, &upper, &mut neg_f)?;
let max_dpsi = delta[1..nn - 1]
.iter()
.map(|&d| d.abs())
.fold(0.0_f64, f64::max);
let damp = if max_dpsi > 5.0 * vt {
5.0 * vt / max_dpsi
} else {
1.0
};
for i in 1..nn - 1 {
psi[i] += damp * delta[i];
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn gummel_nonequil_solve(
psi: &mut [f64],
n: &mut [f64],
p: &mut [f64],
nd: &[f64],
na: &[f64],
gen: &[f64],
dx: &[f64],
mat: &SemiconductorMaterial,
temp_k: f64,
psi_left: f64,
psi_right: f64,
n_left: f64,
p_left: f64,
n_right: f64,
p_right: f64,
max_iter: usize,
tol: f64,
) -> Result<usize, OxiPhotonError> {
let vt = mat.vt_at(temp_k);
let eps_r = mat.eps_r;
let nn = psi.len();
let n_edges = nn - 1;
let effective_max_iters = if matches!(mat.statistics, StatisticsModel::FermiDirac) {
max_iter.max(150)
} else {
max_iter
};
let ni_eff_node: Vec<f64> = (0..nn)
.map(|i| mat.n_ie_squared(temp_k, nd[i], na[i]).sqrt())
.collect();
for iter in 0..effective_max_iters {
let psi_old: Vec<f64> = psi.to_vec();
let n_old: Vec<f64> = n.to_vec();
let p_old: Vec<f64> = p.to_vec();
let dn_node: Vec<f64> = (0..nn)
.map(|i| match mat.statistics {
StatisticsModel::Boltzmann => mat.dn_cm2_s(temp_k),
StatisticsModel::FermiDirac => mat.dn_cm2_s_fd(temp_k, n_old[i]),
})
.collect();
let dp_node: Vec<f64> = (0..nn)
.map(|i| match mat.statistics {
StatisticsModel::Boltzmann => mat.dp_cm2_s(temp_k),
StatisticsModel::FermiDirac => mat.dp_cm2_s_fd(temp_k, p_old[i]),
})
.collect();
let dn_edge: Vec<f64> = (0..n_edges)
.map(|i| {
let a = dn_node[i];
let b = dn_node[i + 1];
if a == 0.0 || b == 0.0 {
0.0
} else {
2.0 * a * b / (a + b)
}
})
.collect();
let dp_edge: Vec<f64> = (0..n_edges)
.map(|i| {
let a = dp_node[i];
let b = dp_node[i + 1];
if a == 0.0 || b == 0.0 {
0.0
} else {
2.0 * a * b / (a + b)
}
})
.collect();
solve_poisson_gummel_inner(
psi, &n_old, &p_old, &psi_old, nd, na, dx, eps_r, vt, psi_left, psi_right, 10,
)?;
solve_n_tridiag(
n,
psi,
&p_old,
gen,
dx,
mat,
temp_k,
n_left,
n_right,
&ni_eff_node,
&dn_edge,
)?;
let n_cur: Vec<f64> = n.to_vec();
solve_p_tridiag(
p,
psi,
&n_cur,
gen,
dx,
mat,
temp_k,
p_left,
p_right,
&ni_eff_node,
&dp_edge,
)?;
let dpsi = psi
.iter()
.zip(psi_old.iter())
.map(|(a, b)| ((a - b) / vt).abs())
.fold(0.0_f64, f64::max);
let dn_s = n
.iter()
.zip(n_old.iter())
.enumerate()
.map(|(i, (a, b))| {
let scale = b.max(ni_eff_node[i]);
((a - b) / scale).abs()
})
.fold(0.0_f64, f64::max);
let dp_s = p
.iter()
.zip(p_old.iter())
.enumerate()
.map(|(i, (a, b))| {
let scale = b.max(ni_eff_node[i]);
((a - b) / scale).abs()
})
.fold(0.0_f64, f64::max);
if dpsi < tol && dn_s < tol && dp_s < tol {
return Ok(iter + 1);
}
}
Err(OxiPhotonError::NumericalError(format!(
"Gummel iteration did not converge in {effective_max_iters} iterations"
)))
}
#[allow(clippy::too_many_arguments)]
pub fn solve_equilibrium_gummel(
psi: &mut [f64],
n: &mut [f64],
p: &mut [f64],
nd: &[f64],
na: &[f64],
dx: &[f64],
mat: &SemiconductorMaterial,
temp_k: f64,
psi_left: f64,
psi_right: f64,
n_left: f64,
p_left: f64,
n_right: f64,
p_right: f64,
) -> Result<usize, OxiPhotonError> {
let nn = psi.len();
let ni = mat.ni_cm3;
let vt = mat.vt_at(temp_k);
let ni_eff_node: Vec<f64> = (0..nn)
.map(|i| mat.n_ie_squared(temp_k, nd[i], na[i]).sqrt())
.collect();
psi[0] = psi_left;
n[0] = n_left;
p[0] = p_left;
psi[nn - 1] = psi_right;
n[nn - 1] = n_right;
p[nn - 1] = p_right;
for i in 1..nn - 1 {
let ni_i = ni_eff_node[i];
n[i] = (ni_i * (psi[i] / vt).exp()).max(1e-10 * ni);
p[i] = (ni_i * (-psi[i] / vt).exp()).max(1e-10 * ni);
}
let n_iters = solve_poisson_equilibrium(
psi,
nd,
na,
dx,
mat.eps_r,
&ni_eff_node,
vt,
psi_left,
psi_right,
200,
)?;
for i in 1..nn - 1 {
let ni_i = ni_eff_node[i];
n[i] = (ni_i * (psi[i] / vt).exp()).max(1e-10 * ni);
p[i] = (ni_i * (-psi[i] / vt).exp()).max(1e-10 * ni);
}
Ok(n_iters)
}
#[allow(clippy::too_many_arguments)]
pub fn newton_solve(
psi: &mut [f64],
n: &mut [f64],
p: &mut [f64],
nd: &[f64],
na: &[f64],
gen: &[f64],
dx: &[f64],
mat: &SemiconductorMaterial,
temp_k: f64,
v_left: f64,
v_right: f64,
max_iter: usize,
tol: f64,
) -> Result<usize, OxiPhotonError> {
let nn = psi.len();
let n_left = n[0];
let p_left = p[0];
let n_right = n[nn - 1];
let p_right = p[nn - 1];
gummel_nonequil_solve(
psi, n, p, nd, na, gen, dx, mat, temp_k, v_left, v_right, n_left, p_left, n_right, p_right,
max_iter, tol,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solar::drift_diffusion::material::SemiconductorMaterial;
#[test]
fn residual_flat_potential_charge_neutral_zero() {
let mat = SemiconductorMaterial::silicon();
let ni = mat.ni_cm3;
let n_nodes = 10;
let dx = vec![1e-5_f64; n_nodes - 1];
let mut psi = vec![0.0_f64; n_nodes];
let mut n = vec![ni; n_nodes];
let mut p = vec![ni; n_nodes];
let nd = vec![0.0_f64; n_nodes];
let na = vec![0.0_f64; n_nodes];
let vt = mat.vt_at(300.0);
let ni_eff_node = vec![ni; n_nodes];
let result = solve_poisson_equilibrium(
&mut psi,
&nd,
&na,
&dx,
mat.eps_r,
&ni_eff_node,
vt,
0.0,
0.0,
10,
);
assert!(result.is_ok(), "Should converge: {:?}", result);
for i in 0..n_nodes {
n[i] = (ni * (psi[i] / vt).exp()).max(1e-10 * ni);
p[i] = (ni * (-psi[i] / vt).exp()).max(1e-10 * ni);
let np = n[i] * p[i];
assert!(
(np - ni * ni).abs() / (ni * ni) < 1e-6,
"Mass action at {i}: n*p={np:.3e}, ni²={:.3e}",
ni * ni
);
}
}
#[test]
fn thomas_solve_small_system() {
let lower = vec![-1.0_f64, -1.0];
let mut diag = vec![2.0_f64, 2.0, 2.0];
let upper = vec![-1.0_f64, -1.0];
let mut rhs = vec![1.0_f64, 0.0, 1.0];
let x = thomas_solve(&lower, &mut diag, &upper, &mut rhs).expect("solve");
assert!((x[0] - 1.0).abs() < 1e-12, "x[0] = {}", x[0]);
assert!((x[1] - 1.0).abs() < 1e-12, "x[1] = {}", x[1]);
assert!((x[2] - 1.0).abs() < 1e-12, "x[2] = {}", x[2]);
}
}