spectrafit_models/tauc.rs
1use crate::Model;
2
3/// Tauc optical band-gap edge: `A · ((x − e_gap) · H(x − e_gap))^p`.
4///
5/// Parameters (in order): `[amplitude, e_gap, exponent]`
6///
7/// - `amplitude` (`A`) scales the absorption above the gap.
8/// - `e_gap` is the optical band-gap energy: the edge onset. Below it the model is
9/// exactly `0` (Heaviside `H(x − e_gap)`); at and above it the absorption rises as a
10/// power law in the excess energy `(x − e_gap)`.
11/// - `exponent` (`p`) is the Tauc power (`p = 2` for an allowed indirect transition,
12/// `p = 1/2` for an allowed direct one). The classic Tauc plot fits `(α·hν)^{1/p}`
13/// linearly in `hν`; here we expose the forward edge `A·(hν − E_g)^p` directly.
14///
15/// The kernel is `0` for `x ≤ e_gap` (where the base `(x − e_gap)` is non-positive and
16/// a fractional `exponent` would otherwise be NaN). The numpy benchmark formula is
17/// identical — `np.where(x > e_gap, A·(x − e_gap)^p, 0)` — so numpy↔Rust parity is exact.
18pub struct Tauc;
19
20impl Model for Tauc {
21 fn eval(&self, x: &[f64], params: &[f64]) -> f64 {
22 let (a, e_gap, exponent) = (params[0], params[1], params[2]);
23 let excess = x[0] - e_gap;
24 if excess > 0.0 {
25 a * excess.powf(exponent)
26 } else {
27 0.0
28 }
29 }
30
31 /// Central finite-difference Jacobian. The Heaviside cut-off makes the closed-form
32 /// derivative piecewise (and the power law's `∂/∂e_gap` diverges as `x → e_gap⁺` for
33 /// `exponent < 1`), so a numerical Jacobian is used — matching `log_normal`.
34 fn jacobian(&self, x: &[f64], params: &[f64]) -> Vec<f64> {
35 let mut p = params.to_vec();
36 (0..params.len())
37 .map(|i| {
38 let h = 1e-7_f64 * params[i].abs().max(1e-7);
39 p[i] = params[i] + h;
40 let f_plus = self.eval(x, &p);
41 p[i] = params[i] - h;
42 let f_minus = self.eval(x, &p);
43 p[i] = params[i];
44 (f_plus - f_minus) / (2.0 * h)
45 })
46 .collect()
47 }
48
49 fn param_names(&self) -> Vec<std::borrow::Cow<'static, str>> {
50 vec!["amplitude".into(), "e_gap".into(), "exponent".into()]
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use approx::assert_relative_eq;
58
59 #[test]
60 fn below_gap_is_zero() {
61 let m = Tauc;
62 assert_eq!(m.eval(&[1.0], &[2.0, 1.5, 2.0]), 0.0);
63 assert_eq!(m.eval(&[1.5], &[2.0, 1.5, 2.0]), 0.0); // exactly at the gap
64 }
65
66 #[test]
67 fn above_gap_power_law() {
68 // (x − e_gap) = 2.0, p = 2 ⇒ A·4.0.
69 let m = Tauc;
70 assert_relative_eq!(m.eval(&[3.5], &[2.0, 1.5, 2.0]), 2.0 * 4.0, epsilon = 1e-12);
71 }
72
73 #[test]
74 fn param_names_are_canonical() {
75 assert_eq!(
76 Tauc.param_names()
77 .iter()
78 .map(|c| c.as_ref())
79 .collect::<Vec<_>>(),
80 &["amplitude", "e_gap", "exponent"]
81 );
82 }
83
84 #[test]
85 fn jacobian_shape_and_amplitude() {
86 let m = Tauc;
87 let j = m.jacobian(&[3.5], &[2.0, 1.5, 2.0]);
88 assert_eq!(j.len(), 3);
89 // ∂/∂amplitude above the gap is (x − e_gap)^p = 4.0.
90 assert_relative_eq!(j[0], 4.0, epsilon = 1e-6);
91 }
92}