1use super::drude::DrudeLorentzOscillator;
100use super::lindhard_gas::LindhardGas;
101use super::mermin_fit::{fit_mermin_oscillators, MerminFit, MerminFitOptions};
102use super::penn::{hartree_ev, InelasticPoint};
103use super::quadrature::{integrate_segments, GaussLegendre};
104use crate::constants::BOHR_RADIUS;
105use crate::electron::data::{ElectronDataError, OpticalElf};
106use std::f64::consts::PI;
107use std::ops::{Add, Div, Mul, Sub};
108
109type Result<T> = std::result::Result<T, ElectronDataError>;
110
111pub const DEFAULT_MERMIN_TOLERANCE: f64 = 1.0e-5;
113
114pub(crate) const LOWEST_LOSS_FRACTION: f64 = 1.0e-8;
123
124const GL_ORDER: usize = 5;
125
126#[derive(Debug, Clone, Copy, PartialEq)]
129struct C {
130 re: f64,
131 im: f64,
132}
133
134impl C {
135 const fn new(re: f64, im: f64) -> Self {
136 Self { re, im }
137 }
138 fn real(re: f64) -> Self {
139 Self { re, im: 0.0 }
140 }
141 fn norm_sq(self) -> f64 {
142 self.re * self.re + self.im * self.im
143 }
144 fn abs(self) -> f64 {
145 self.re.hypot(self.im)
146 }
147 fn ln(self) -> Self {
148 Self::new(self.abs().ln(), self.im.atan2(self.re))
149 }
150 fn scale(self, k: f64) -> Self {
151 Self::new(self.re * k, self.im * k)
152 }
153}
154
155impl Add for C {
156 type Output = C;
157 fn add(self, o: C) -> C {
158 C::new(self.re + o.re, self.im + o.im)
159 }
160}
161impl Sub for C {
162 type Output = C;
163 fn sub(self, o: C) -> C {
164 C::new(self.re - o.re, self.im - o.im)
165 }
166}
167impl Mul for C {
168 type Output = C;
169 fn mul(self, o: C) -> C {
170 C::new(
171 self.re * o.re - self.im * o.im,
172 self.re * o.im + self.im * o.re,
173 )
174 }
175}
176impl Div for C {
177 type Output = C;
178 fn div(self, o: C) -> C {
179 let d = o.norm_sq();
180 C::new(
181 (self.re * o.re + self.im * o.im) / d,
182 (self.im * o.re - self.re * o.im) / d,
183 )
184 }
185}
186
187fn a_complex(s: C) -> C {
189 let one = C::real(1.0);
190 let d = one - s * s;
191 if d.re == 0.0 && d.im == 0.0 {
192 return C::real(0.0);
193 }
194 d * ((s + one) / (s - one)).ln()
195}
196
197fn bracket(z: f64, u: C) -> C {
199 let hi = u + C::real(z);
200 let lo = u - C::real(z);
201 if hi.abs() > 4.0 && lo.abs() > 4.0 {
202 return bracket_series(hi, lo);
203 }
204 let half = C::real(0.5);
205 half + (a_complex(C::real(z) - u) + a_complex(C::real(z) + u)).scale(1.0 / (8.0 * z))
206}
207
208fn bracket_series(hi: C, lo: C) -> C {
212 let d = hi * lo;
213 let mut sum = C::real(0.0);
214 let mut s_m = C::real(1.0); let mut lo_pow = lo; let mut d_pow = d; for m in 1..=400usize {
218 if m % 2 == 1 {
219 let k = (m / 2) as f64;
220 let ck = 4.0 / ((2.0 * k + 1.0) * (2.0 * k + 3.0));
221 let term = (s_m / d_pow).scale(ck);
222 sum = sum + term;
223 if term.abs() <= 1e-17 * sum.abs() {
224 break;
225 }
226 }
227 s_m = hi * s_m + lo_pow;
229 lo_pow = lo_pow * lo;
230 d_pow = d_pow * d;
231 }
232 sum.scale(-0.25)
233}
234
235fn lindhard_complex(gas: &LindhardGas, q: f64, w: C) -> C {
237 let kf = gas.fermi_wavenumber();
238 let z = q / (2.0 * kf);
239 let u = w.scale(1.0 / (q * kf));
240 C::real(1.0) + bracket(z, u).scale(gas.thomas_fermi_wavenumber_sq() / (q * q))
241}
242
243#[derive(Debug, Clone, Copy, PartialEq)]
246pub struct MerminGas {
247 gas: LindhardGas,
248 gamma: f64,
249}
250
251impl MerminGas {
252 pub fn new(gas: LindhardGas, gamma: f64) -> Result<Self> {
255 if !(gamma.is_finite() && gamma >= 0.0) {
256 return Err(ElectronDataError::Invalid {
257 what: "Mermin relaxation energy",
258 reason: format!("must be finite and non-negative, got {gamma} Hartree"),
259 });
260 }
261 Ok(Self { gas, gamma })
262 }
263
264 pub fn gas(&self) -> &LindhardGas {
266 &self.gas
267 }
268
269 pub fn gamma(&self) -> f64 {
271 self.gamma
272 }
273
274 pub fn epsilon(&self, q: f64, w: f64) -> (f64, f64) {
276 if self.gamma == 0.0 {
277 return self.gas.epsilon(q, w);
278 }
279 let l_w = lindhard_complex(&self.gas, q, C::new(w, self.gamma)) - C::real(1.0);
280 let z = q / (2.0 * self.gas.fermi_wavenumber());
281 let l_0 = bracket(z, C::real(0.0)).re * self.gas.thomas_fermi_wavenumber_sq() / (q * q);
282 let ig = C::new(0.0, self.gamma / w);
283 let num = (C::real(1.0) + ig) * l_w;
284 let den = C::real(1.0) + ig * l_w.scale(1.0 / l_0);
285 let eps = C::real(1.0) + num / den;
286 (eps.re, eps.im)
287 }
288
289 pub fn loss(&self, q: f64, w: f64) -> f64 {
291 let (re, im) = self.epsilon(q, w);
292 im / (re * re + im * im)
293 }
294}
295
296#[derive(Clone)]
297struct Term {
298 weight: f64,
300 gas: MerminGas,
301}
302
303#[derive(Clone)]
307pub struct MerminPenn {
308 elf: OpticalElf,
309 fit: MerminFit,
310 terms: Vec<Term>,
311 fermi: f64,
312 rel_tol: f64,
313 gl: GaussLegendre,
314}
315
316impl std::fmt::Debug for MerminPenn {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.debug_struct("MerminPenn")
319 .field("material", &self.elf.material())
320 .field("oscillators", &self.fit.oscillators)
321 .field("fermi_hartree", &self.fermi)
322 .field("rel_tol", &self.rel_tol)
323 .finish()
324 }
325}
326
327fn check_energy(what: &'static str, v: f64) -> Result<()> {
328 if v.is_finite() && v > 0.0 {
329 Ok(())
330 } else {
331 Err(ElectronDataError::Invalid {
332 what,
333 reason: format!("must be finite and positive, got {v} eV"),
334 })
335 }
336}
337
338impl MerminPenn {
339 pub fn new(elf: OpticalElf, fit: MerminFit) -> Result<Self> {
343 let h = hartree_ev();
344 let mut terms = Vec::new();
345 for o in &fit.oscillators {
346 check_oscillator(o)?;
347 if o.strength_ev2 == 0.0 {
348 continue;
349 }
350 let wp = o.energy_ev / h;
351 terms.push(Term {
352 weight: o.strength_ev2 / (o.energy_ev * o.energy_ev),
353 gas: MerminGas::new(LindhardGas::from_plasma_frequency(wp), o.width_ev / h)?,
354 });
355 }
356 if terms.is_empty() {
357 return Err(ElectronDataError::Invalid {
358 what: "Mermin fit",
359 reason: "every oscillator has zero amplitude".into(),
360 });
361 }
362 Ok(Self {
363 elf,
364 fit,
365 terms,
366 fermi: 0.0,
367 rel_tol: DEFAULT_MERMIN_TOLERANCE,
368 gl: GaussLegendre::new(GL_ORDER),
369 })
370 }
371
372 pub fn fit(elf: OpticalElf, options: &MerminFitOptions) -> Result<Self> {
375 let fit = fit_mermin_oscillators(&elf, options)?;
376 Self::new(elf, fit)
377 }
378
379 pub fn with_fermi_energy_ev(mut self, fermi_ev: f64) -> Result<Self> {
383 if !(fermi_ev.is_finite() && fermi_ev >= 0.0) {
384 return Err(ElectronDataError::Invalid {
385 what: "Fermi energy",
386 reason: format!("must be finite and non-negative, got {fermi_ev} eV"),
387 });
388 }
389 self.fermi = fermi_ev / hartree_ev();
390 Ok(self)
391 }
392
393 pub fn with_relative_tolerance(mut self, rel_tol: f64) -> Result<Self> {
395 if !(1e-9..=1e-2).contains(&rel_tol) {
396 return Err(ElectronDataError::Invalid {
397 what: "integration tolerance",
398 reason: format!("must be in [1e-9, 1e-2], got {rel_tol}"),
399 });
400 }
401 self.rel_tol = rel_tol;
402 Ok(self)
403 }
404
405 pub fn optical_elf(&self) -> &OpticalElf {
407 &self.elf
408 }
409
410 pub fn fit_report(&self) -> &MerminFit {
412 &self.fit
413 }
414
415 pub fn fermi_energy_ev(&self) -> f64 {
417 self.fermi * hartree_ev()
418 }
419
420 pub fn relative_tolerance(&self) -> f64 {
422 self.rel_tol
423 }
424
425 fn loss_au(&self, q: f64, w: f64) -> f64 {
426 self.terms.iter().map(|t| t.weight * t.gas.loss(q, w)).sum()
427 }
428
429 pub fn loss_function(&self, q_per_m: f64, loss_ev: f64) -> f64 {
433 if !(q_per_m.is_finite() && q_per_m >= 0.0 && loss_ev.is_finite() && loss_ev > 0.0) {
434 return 0.0;
435 }
436 if q_per_m == 0.0 {
437 return self.fit.model_elf(loss_ev);
438 }
439 self.loss_au(q_per_m * BOHR_RADIUS, loss_ev / hartree_ev())
440 }
441
442 pub fn diimfp_per_m_ev(&self, energy_ev: f64, loss_ev: f64) -> Result<f64> {
446 check_energy("electron energy", energy_ev)?;
447 if !loss_ev.is_finite() {
448 return Err(ElectronDataError::Invalid {
449 what: "energy loss",
450 reason: format!("must be finite, got {loss_ev} eV"),
451 });
452 }
453 let h = hartree_ev();
454 Ok(self.diimfp_au(energy_ev / h, loss_ev / h) / (BOHR_RADIUS * h))
455 }
456
457 fn diimfp_au(&self, t: f64, w: f64) -> f64 {
458 if !(w > 0.0 && w <= t) {
459 return 0.0;
460 }
461 let tp = t + self.fermi;
462 let k = (2.0 * tp).sqrt();
463 let s = (2.0 * (tp - w)).max(0.0).sqrt();
464 let (q_minus, q_plus) = (2.0 * w / (k + s), k + s);
465 if q_plus.is_nan() || q_plus <= q_minus {
466 return 0.0;
467 }
468 let (lo, hi) = (q_minus.ln(), q_plus.ln());
469 let mut breaks: Vec<f64> = (0..=8).map(|i| lo + (hi - lo) * i as f64 / 8.0).collect();
470 for t in &self.terms {
472 let kf = t.gas.gas().fermi_wavenumber();
473 for q in [2.0 * kf, w / kf] {
474 if q > q_minus && q < q_plus {
475 breaks.push(q.ln());
476 }
477 }
478 }
479 breaks.sort_by(f64::total_cmp);
480 breaks.dedup();
481 let integral = integrate_segments(
482 &self.gl,
483 &mut |u: f64| [self.loss_au(u.exp(), w)],
484 &breaks,
485 self.rel_tol,
486 )[0];
487 integral / (PI * tp)
488 }
489
490 fn imfp_and_stopping_au(&self, t: f64) -> (f64, f64) {
492 if t.is_nan() || t <= 0.0 {
493 return (0.0, 0.0);
494 }
495 let (lo, hi) = ((LOWEST_LOSS_FRACTION * t).ln(), t.ln());
496 let n = 24;
497 let mut breaks: Vec<f64> = (0..=n)
498 .map(|i| lo + (hi - lo) * i as f64 / n as f64)
499 .collect();
500 for term in &self.terms {
501 let e = term.gas.gas().plasma_frequency();
502 if e > lo.exp() && e < t {
503 breaks.push(e.ln());
504 }
505 }
506 breaks.sort_by(f64::total_cmp);
507 breaks.dedup();
508 let r = integrate_segments(
509 &self.gl,
510 &mut |u: f64| {
511 let w = u.exp();
512 let p = self.diimfp_au(t, w);
513 [p * w, p * w * w]
515 },
516 &breaks,
517 self.rel_tol,
518 );
519 (r[0], r[1])
520 }
521
522 pub fn imfp_and_stopping(&self, energy_ev: f64) -> Result<InelasticPoint> {
525 check_energy("electron energy", energy_ev)?;
526 let h = hartree_ev();
527 let (inv, s) = self.imfp_and_stopping_au(energy_ev / h);
528 Ok(InelasticPoint {
529 energy_ev,
530 inverse_imfp_per_m: inv / BOHR_RADIUS,
531 stopping_ev_per_m: s * h / BOHR_RADIUS,
532 })
533 }
534
535 pub fn imfp_m(&self, energy_ev: f64) -> Result<f64> {
537 Ok(self.imfp_and_stopping(energy_ev)?.imfp_m())
538 }
539
540 pub fn stopping_power_ev_per_m(&self, energy_ev: f64) -> Result<f64> {
542 Ok(self.imfp_and_stopping(energy_ev)?.stopping_ev_per_m)
543 }
544
545 pub fn tabulate(&self, energies_ev: &[f64]) -> Result<Vec<InelasticPoint>> {
548 energies_ev
549 .iter()
550 .map(|&e| self.imfp_and_stopping(e))
551 .collect()
552 }
553}
554
555pub(crate) fn check_oscillator(o: &DrudeLorentzOscillator) -> Result<()> {
557 let bad = |name: &str, v: f64| ElectronDataError::Invalid {
558 what: "Mermin oscillator",
559 reason: format!("{name} is invalid: {v}"),
560 };
561 if !(o.strength_ev2.is_finite() && o.strength_ev2 >= 0.0) {
562 return Err(bad("amplitude (must be finite, >= 0)", o.strength_ev2));
563 }
564 if !(o.energy_ev.is_finite() && o.energy_ev > 0.0) {
565 return Err(bad("energy (must be finite, > 0)", o.energy_ev));
566 }
567 if !(o.width_ev.is_finite() && o.width_ev > 0.0) {
568 return Err(bad("width (must be finite, > 0)", o.width_ev));
569 }
570 Ok(())
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 fn gas(kf: f64) -> LindhardGas {
578 LindhardGas::from_fermi_wavenumber(kf)
579 }
580
581 #[test]
582 fn complex_bracket_series_matches_direct_where_both_hold() {
583 for (z, u) in [
584 (0.3, C::new(5.0, 0.4)),
585 (1.5, C::new(7.0, 2.0)),
586 (0.1, C::new(4.5, 0.1)),
587 (2.0, C::new(0.5, 5.0)),
588 (3.0, C::new(0.0, 5.0)),
589 ] {
590 let (hi, lo) = (u + C::real(z), u - C::real(z));
591 assert!(hi.abs() > 4.0 && lo.abs() > 4.0, "{z} {u:?}");
592 let direct = C::real(0.5)
593 + (a_complex(C::real(z) - u) + a_complex(C::real(z) + u)).scale(1.0 / (8.0 * z));
594 let series = bracket_series(hi, lo);
595 let diff = (direct - series).abs();
596 assert!(diff <= 1e-9 * series.abs().max(1e-3), "{z} {u:?} {diff:e}");
597 }
598 }
599
600 #[test]
601 fn complex_lindhard_reduces_to_the_real_frequency_form() {
602 for kf in [0.4, 0.9] {
605 let g = gas(kf);
606 for (q, w) in [(0.3, 0.2), (0.8, 0.5), (1.5, 1.4), (0.2, 1.0), (2.5, 0.3)] {
607 let c = lindhard_complex(&g, q, C::new(w, 1e-13));
608 let (re, im) = g.epsilon(q, w);
609 assert!(
610 (c.re - re).abs() <= 1e-9 * re.abs().max(1.0),
611 "{kf} {q} {w}: {c:?} vs {re} {im}"
612 );
613 assert!(
614 (c.im - im).abs() <= 1e-9 * im.abs().max(1.0),
615 "{kf} {q} {w}: {c:?} vs {re} {im}"
616 );
617 }
618 }
619 }
620
621 #[test]
622 fn mermin_reduces_to_lindhard_as_gamma_goes_to_zero_to_1e8() {
623 for kf in [0.4, 0.9] {
624 let g = gas(kf);
625 for (q, w) in [(0.3, 0.2), (0.8, 0.5), (1.5, 1.4), (0.2, 1.0), (2.5, 0.3)] {
626 let (lre, lim) = g.epsilon(q, w);
627 let m0 = MerminGas::new(g, 0.0).unwrap().epsilon(q, w);
629 assert_eq!(m0, (lre, lim));
630 for gamma in [1e-11, 1e-13] {
631 let (re, im) = MerminGas::new(g, gamma).unwrap().epsilon(q, w);
632 assert!(
633 (re - lre).abs() <= 1e-8 * lre.abs().max(1.0),
634 "{kf} {q} {w}"
635 );
636 assert!(
637 (im - lim).abs() <= 1e-8 * lim.abs().max(1.0),
638 "{kf} {q} {w}"
639 );
640 }
641 }
642 }
643 }
644
645 #[test]
646 fn mermin_is_drude_at_small_q() {
647 let g = LindhardGas::from_plasma_frequency(0.5);
649 let gamma = 0.12;
650 let m = MerminGas::new(g, gamma).unwrap();
651 let wp2 = 0.25;
652 for w in [0.1, 0.5, 1.3] {
653 let (re, im) = m.epsilon(1e-3, w);
654 let d = C::new(w * w, gamma * w);
655 let drude = C::real(1.0) - C::real(wp2) / d;
656 assert!((re - drude.re).abs() < 1e-3, "{w} {re} {}", drude.re);
657 assert!((im - drude.im).abs() < 1e-3, "{w} {im} {}", drude.im);
658 }
659 }
660
661 #[test]
662 fn mermin_static_limit_is_the_static_lindhard_function() {
663 let g = gas(0.7);
664 let m = MerminGas::new(g, 0.05).unwrap();
665 for q in [0.3, 0.9, 2.0] {
666 let (re, im) = m.epsilon(q, 1e-9);
667 let (lre, _) = g.epsilon(q, 0.0);
668 assert!((re - lre).abs() <= 1e-6 * lre, "{q} {re} {lre}");
669 assert!(im.abs() <= 1e-6 * lre, "{q} {im}");
670 }
671 }
672
673 #[test]
674 fn mermin_obeys_the_f_sum_rule_at_finite_q() {
675 let g = LindhardGas::from_plasma_frequency(0.6);
678 let wp2 = g.plasma_frequency().powi(2);
679 let gl = GaussLegendre::new(8);
680 for (gamma, q) in [(0.08, 0.3), (0.15, 0.8), (0.05, 1.2)] {
681 let m = MerminGas::new(g, gamma).unwrap();
682 let w_c = 4000.0;
683 let breaks: Vec<f64> = (0..=60)
684 .map(|i| (1e-6f64).ln() + ((w_c / 1e-6f64).ln()) * i as f64 / 60.0)
685 .collect();
686 let r = integrate_segments(
687 &gl,
688 &mut |u: f64| {
689 let w = u.exp();
690 [w * w * m.loss(q, w)]
691 },
692 &breaks,
693 1e-9,
694 )[0];
695 let total = r + wp2 * gamma / w_c;
696 let want = 0.5 * PI * wp2;
697 assert!(
698 ((total - want) / want).abs() < 2e-4,
699 "gamma {gamma} q {q}: {total} vs {want}"
700 );
701 }
702 }
703
704 #[test]
705 fn invalid_relaxation_energy_is_rejected() {
706 let g = gas(0.5);
707 assert!(MerminGas::new(g, -1.0).is_err());
708 assert!(MerminGas::new(g, f64::NAN).is_err());
709 }
710}