1use std::f64::consts::PI;
33
34use nereids_core::constants::BOLTZMANN_EV_PER_K;
35use nereids_endf::resonance::ResonanceData;
36use num_complex::Complex64;
37use rayon::prelude::*;
38
39use crate::continuous_doppler::{Channel, TierOneBroadening};
40use crate::doppler::{DopplerError, DopplerParams, validate_doppler_grid};
41use crate::reich_moore::CrossSectionPlan;
42
43const KERNEL_SIGMAS: f64 = 8.0;
46const KERNEL_PHONONS: f64 = 10.0;
47
48const MAX_POINTS: usize = 1 << 22;
50
51const MAX_WORK: f64 = 1e10;
53
54const MASS_TOLERANCE: f64 = 1e-7;
57
58const STEPS_PER_KNOT: f64 = 10.0;
60
61const GAUSS_LEGENDRE_8: [(f64, f64); 4] = [
63 (0.183_434_642_495_649_8, 0.362_683_783_378_362),
64 (0.525_532_409_916_329, 0.313_706_645_877_887_3),
65 (0.796_666_477_413_626_7, 0.222_381_034_453_374_5),
66 (0.960_289_856_497_536_3, 0.101_228_536_290_376_26),
67];
68
69#[derive(Debug)]
71pub enum LatticeError {
72 InvalidSpectrum(String),
74 InvalidTemperature(f64),
76 Grid(DopplerError),
78 BelowZero(f64),
80 Jump(f64),
82 Unresolved {
84 energy_ev: f64,
86 mass: f64,
88 },
89 TooManyPoints(usize),
91 TooMuchWork(f64),
93}
94
95impl std::fmt::Display for LatticeError {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 match self {
98 Self::InvalidSpectrum(message) => write!(f, "invalid phonon spectrum: {message}"),
99 Self::InvalidTemperature(t) => {
100 write!(f, "the temperature must be finite and positive, got {t} K")
101 }
102 Self::Grid(error) => write!(f, "{error}"),
103 Self::BelowZero(e) => {
104 write!(
105 f,
106 "the lattice kernel reaches {e} eV, at or below zero energy"
107 )
108 }
109 Self::TooManyPoints(n) => {
110 write!(
111 f,
112 "lattice broadening needs {n} points, more than {MAX_POINTS}"
113 )
114 }
115 Self::Jump(e) => write!(
116 f,
117 "File 3 jumps at {e} eV, within the lattice kernel's reach"
118 ),
119 Self::Unresolved { energy_ev, mass } => write!(
120 f,
121 "the lattice kernel at {energy_ev} eV is not resolved: its weights sum to {mass}"
122 ),
123 Self::TooMuchWork(work) => {
124 write!(f, "lattice broadening needs {work:e} terms, more than 1e10")
125 }
126 }
127 }
128}
129
130impl std::error::Error for LatticeError {}
131
132impl From<DopplerError> for LatticeError {
133 fn from(error: DopplerError) -> Self {
134 Self::Grid(error)
135 }
136}
137
138#[derive(Debug, Clone, PartialEq)]
143pub struct PhononSpectrum {
144 step_ev: f64,
145 density: Vec<f64>,
146}
147
148impl PhononSpectrum {
149 pub fn new(step_ev: f64, mut density: Vec<f64>) -> Result<Self, LatticeError> {
156 if !(step_ev.is_finite() && step_ev > 0.0)
157 || density.len() < 2
158 || density[0] != 0.0
159 || !density.iter().all(|r| r.is_finite() && *r >= 0.0)
160 || !density.iter().any(|r| *r > 0.0)
161 {
162 return Err(LatticeError::InvalidSpectrum(format!(
163 "needs a finite positive step and at least two finite values, 0 or more, the \
164 first 0 and another positive; got step {step_ev} and {} values",
165 density.len()
166 )));
167 }
168 let peak = density.iter().fold(0.0_f64, |m, &r| m.max(r));
169 for r in &mut density {
170 *r /= peak;
171 }
172 let area = step_ev * density[1] / 3.0
173 + step_ev
174 * density[1..]
175 .windows(2)
176 .map(|w| 0.5 * (w[0] + w[1]))
177 .sum::<f64>();
178 for r in &mut density {
179 *r /= area;
180 }
181 if !(area.is_finite() && area > 0.0 && density.iter().all(|r| r.is_finite())) {
182 return Err(LatticeError::InvalidSpectrum(format!(
183 "its area {area} eV after scaling to a peak of 1 leaves values that are not finite"
184 )));
185 }
186 Ok(Self { step_ev, density })
187 }
188
189 pub fn debye(theta_k: f64, points: usize) -> Result<Self, LatticeError> {
195 let cutoff = BOLTZMANN_EV_PER_K * theta_k;
196 if !(cutoff.is_finite() && cutoff > 0.0) || points < 2 {
197 return Err(LatticeError::InvalidSpectrum(format!(
198 "Debye needs a finite positive temperature and at least 2 points, got {theta_k} K \
199 and {points}"
200 )));
201 }
202 let step = cutoff / (points - 1) as f64;
203 Self::new(
204 step,
205 (0..points)
206 .map(|i| (i as f64 * step / cutoff).powi(2))
207 .collect(),
208 )
209 }
210
211 #[must_use]
213 pub fn max_ev(&self) -> f64 {
214 self.step_ev * (self.density.len() - 1) as f64
215 }
216
217 #[must_use]
220 pub fn effective_temperature_k(&self, temperature_k: f64) -> f64 {
221 Phonons::new(&self.nodes(0.0), temperature_k, 0.0, 0).kt_eff / BOLTZMANN_EV_PER_K
222 }
223
224 #[must_use]
228 pub fn reach_ev(&self, energy_ev: f64, temperature_k: f64, awr: f64) -> f64 {
229 let kt_eff = BOLTZMANN_EV_PER_K * self.effective_temperature_k(temperature_k);
230 reach(energy_ev / (awr + 1.0), kt_eff, self.max_ev())
231 }
232
233 fn node_count(&self, t_max: f64) -> f64 {
235 (self.density.len() - 1) as f64 * (self.step_ev * t_max).ceil().max(1.0) * 16.0
236 }
237
238 fn nodes(&self, t_max: f64) -> Vec<(f64, f64)> {
241 let mut nodes = Vec::new();
242 let h = self.step_ev;
243 let pieces = ((h * t_max).ceil() as usize).max(1);
244 for i in 0..self.density.len() - 1 {
245 let (r0, r1) = (self.density[i], self.density[i + 1]);
246 let at = |omega: f64| {
247 if i == 0 {
248 r1 * (omega / h).powi(2)
249 } else {
250 r0 + (r1 - r0) * (omega / h - i as f64)
251 }
252 };
253 let width = h / pieces as f64;
254 for p in 0..pieces {
255 let centre = i as f64 * h + (p as f64 + 0.5) * width;
256 for &(x, w) in &GAUSS_LEGENDRE_8 {
257 for omega in [centre - 0.5 * width * x, centre + 0.5 * width * x] {
258 nodes.push((omega, 0.5 * width * w * at(omega)));
259 }
260 }
261 }
262 }
263 nodes
264 }
265}
266
267fn reach(r: f64, kt_eff: f64, omega_max: f64) -> f64 {
269 KERNEL_SIGMAS * (2.0 * r * kt_eff).sqrt() + KERNEL_PHONONS * omega_max
270}
271
272struct Phonons {
275 g: Vec<Complex64>,
276 g_slope: Vec<f64>,
277 kt_eff: f64,
278}
279
280impl Phonons {
281 fn new(nodes: &[(f64, f64)], temperature_k: f64, dt: f64, half: usize) -> Self {
282 let kt = BOLTZMANN_EV_PER_K * temperature_k;
283 let terms: Vec<(f64, f64, f64, f64)> = nodes
284 .iter()
285 .map(|&(omega, weight)| {
286 let x = omega / (2.0 * kt);
287 let coth = 1.0 / x.tanh();
288 let slope = x / (temperature_k * x.sinh().powi(2));
289 (omega, weight / omega, coth, slope)
290 })
291 .collect();
292 let kt_eff = 0.5
293 * terms
294 .iter()
295 .map(|&(omega, a, coth, _)| a * omega * omega * coth)
296 .sum::<f64>();
297 let (g, g_slope) = (0..=half)
298 .into_par_iter()
299 .map(|j| {
300 let t = j as f64 * dt;
301 let (mut g, mut slope) = (Complex64::new(0.0, 0.0), 0.0);
302 for &(omega, a, coth, dcoth) in &terms {
303 let (sin, cos) = (omega * t).sin_cos();
304 g += a * Complex64::new(coth * cos, -sin);
305 slope += a * dcoth * cos;
306 }
307 (g, slope)
308 })
309 .unzip();
310 Self { g, g_slope, kt_eff }
311 }
312}
313
314fn dft(values: &mut [Complex64]) {
317 let n = values.len();
318 let mut j = 0;
319 for i in 1..n {
320 let mut bit = n >> 1;
321 while j & bit != 0 {
322 j ^= bit;
323 bit >>= 1;
324 }
325 j |= bit;
326 if i < j {
327 values.swap(i, j);
328 }
329 }
330 let mut len = 2;
331 while len <= n {
332 let step = Complex64::from_polar(1.0, -2.0 * PI / len as f64);
333 for start in (0..n).step_by(len) {
334 let mut w = Complex64::new(1.0, 0.0);
335 for k in 0..len / 2 {
336 let (a, b) = (values[start + k], values[start + k + len / 2] * w);
337 values[start + k] = a + b;
338 values[start + k + len / 2] = a - b;
339 w *= step;
340 }
341 }
342 len <<= 1;
343 }
344}
345
346pub fn broaden_in_lattice(
359 energies: &[f64],
360 data: &ResonanceData,
361 temperature_k: f64,
362 spectrum: &PhononSpectrum,
363 channel: Channel,
364) -> Result<TierOneBroadening, LatticeError> {
365 if !(temperature_k.is_finite() && temperature_k > 0.0) {
366 return Err(LatticeError::InvalidTemperature(temperature_k));
367 }
368 DopplerParams::new(temperature_k, data.awr).map_err(DopplerError::from)?;
369 validate_doppler_grid(energies)?;
370 if energies.is_empty() {
371 return Err(DopplerError::EmptyGrid.into());
372 }
373 let recoil = |energy: f64| energy / (data.awr + 1.0);
374 let omega_max = spectrum.max_ev();
375 let kt_eff = BOLTZMANN_EV_PER_K * spectrum.effective_temperature_k(temperature_k);
376 let reach_at = |energy: f64| reach(recoil(energy), kt_eff, omega_max);
377 let lowest = energies
378 .iter()
379 .map(|&e| e - reach_at(e))
380 .fold(f64::INFINITY, f64::min);
381 let highest = energies
382 .iter()
383 .map(|&e| e + reach_at(e))
384 .fold(f64::NEG_INFINITY, f64::max);
385 if lowest <= 0.0 {
386 return Err(LatticeError::BelowZero(lowest));
387 }
388 let narrowest = data
389 .ranges
390 .iter()
391 .filter(|range| range.is_evaluable())
392 .flat_map(|range| range.levels())
393 .filter(|&(energy, _)| energy >= lowest - 1.0 && energy <= highest + 1.0)
394 .map(|(_, width)| width)
395 .filter(|&width| width > 0.0)
396 .fold(f64::INFINITY, f64::min);
397 let mut h = (PI * narrowest / 30.0).min(PI * omega_max / 100.0);
398 for table in data.smooth.tables() {
399 let start = table.points.partition_point(|&(e, _)| e < lowest);
400 let stop = table.points.partition_point(|&(e, _)| e <= highest);
401 let around = &table.points[start.saturating_sub(1)..(stop + 1).min(table.points.len())];
402 for pair in around.windows(2) {
403 let spacing = pair[1].0 - pair[0].0;
404 if spacing == 0.0 {
405 return Err(LatticeError::Jump(pair[0].0));
406 }
407 h = h.min(spacing / STEPS_PER_KNOT);
408 }
409 }
410 let widest = energies.iter().map(|&e| reach_at(e)).fold(0.0, f64::max);
411 let transform = ((2.0 * widest / h).ceil() as usize + 1).next_power_of_two();
412 let origin = lowest - 2.0 * h;
413 if origin <= 0.0 {
414 return Err(LatticeError::BelowZero(origin));
415 }
416 let grid_points = ((highest - origin) / h).ceil() as usize + 3;
417 for needed in [transform, grid_points] {
418 if needed > MAX_POINTS {
419 return Err(LatticeError::TooManyPoints(needed));
420 }
421 }
422 let half = transform / 2;
423 let dt = 2.0 * PI / (transform as f64 * h);
424 let t_max = (100.0 / omega_max).min(PI / h);
425 let samples = half.min((t_max / dt).ceil() as usize);
426 let work = spectrum.node_count(t_max) * (samples + 1) as f64
427 + energies.len() as f64 * transform as f64 * (transform as f64).log2();
428 if work > MAX_WORK {
429 return Err(LatticeError::TooMuchWork(work));
430 }
431 let phonons = Phonons::new(&spectrum.nodes(t_max), temperature_k, dt, samples);
432 let plan = CrossSectionPlan::new(data);
433 let sigma = |energy: f64| channel.pick(&plan.evaluate_one(energy));
434 let grid: Vec<f64> = (0..grid_points)
435 .into_par_iter()
436 .map(|m| sigma(origin + m as f64 * h))
437 .collect();
438 let g0 = phonons.g[0].re;
439 let g0_slope = phonons.g_slope[0];
440 let results: Vec<Result<(f64, f64), LatticeError>> = energies
441 .par_iter()
442 .map(|&energy| {
443 let r = recoil(energy);
444 let zero = (-r * g0).exp();
445 let zero_slope = -zero * r * g0_slope;
446 let shift = energy + r - origin;
447 let mut buffer = vec![Complex64::new(0.0, 0.0); transform];
448 for j in 0..samples.min(half) {
449 let phi = (r * (phonons.g[j] - g0)).exp();
450 let value = phi - zero;
451 let slope = phi * r * (phonons.g_slope[j] - g0_slope) - zero_slope;
452 let t = j as f64 * dt;
453 let rotate = Complex64::from_polar(dt / (2.0 * PI), shift * t);
454 buffer[j] = rotate * (value + Complex64::i() * slope);
455 if j > 0 {
456 let back = rotate.conj() * (value.conj() + Complex64::i() * slope.conj());
457 buffer[transform - j] = back;
458 }
459 }
460 dft(&mut buffer);
461 let own = reach_at(energy);
462 let first = ((energy - own - origin) / h).floor().max(0.0) as usize;
463 let last = (((energy + own - origin) / h).ceil() as usize).min(grid_points - 1);
464 let (mut mass, mut value, mut slope) = (zero, 0.0, 0.0);
465 for (m, &s) in grid.iter().enumerate().take(last + 1).skip(first) {
466 let kernel = buffer[m % transform];
467 mass += h * kernel.re;
468 value += kernel.re * s;
469 slope += kernel.im * s;
470 }
471 if (mass - 1.0).abs() > MASS_TOLERANCE {
472 return Err(LatticeError::Unresolved {
473 energy_ev: energy,
474 mass,
475 });
476 }
477 let at_recoil = sigma(energy + r);
478 Ok((
479 h * value + zero * at_recoil,
480 h * slope + zero_slope * at_recoil,
481 ))
482 })
483 .collect();
484 let (values, derivatives): (Vec<f64>, Vec<f64>) = results
485 .into_iter()
486 .collect::<Result<Vec<_>, _>>()?
487 .into_iter()
488 .unzip();
489 if let Some((k, &value)) = values
490 .iter()
491 .chain(&derivatives)
492 .enumerate()
493 .find(|(_, v)| !v.is_finite())
494 {
495 return Err(DopplerError::NonFiniteIntegral {
496 energy_ev: energies[k % energies.len()],
497 value,
498 derivative: k >= energies.len(),
499 }
500 .into());
501 }
502 let negative_values = values.iter().filter(|&&v| v < 0.0).count();
503 Ok(TierOneBroadening {
504 values,
505 derivatives,
506 negative_values,
507 roundoff_limited: 0,
508 })
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 #[test]
516 fn the_transform_is_the_discrete_fourier_transform() {
517 let n = 32;
518 let values: Vec<Complex64> = (0..n)
519 .map(|j| Complex64::new((0.7 * j as f64).sin(), (1.3 * j as f64).cos() - 0.2))
520 .collect();
521 let mut fast = values.clone();
522 dft(&mut fast);
523 for (m, fast) in fast.iter().enumerate() {
524 let direct: Complex64 = values
525 .iter()
526 .enumerate()
527 .map(|(j, v)| v * Complex64::from_polar(1.0, -2.0 * PI * (m * j) as f64 / n as f64))
528 .sum();
529 assert!((fast - direct).norm() <= 1e-12, "{m}: {fast} {direct}");
530 }
531 }
532}