1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Chantler tables: anomalous scattering factors and mass attenuation coefficients.
use crate::db::{ChantlerLogs, XrayDb};
use crate::error::{Result, XrayDbError};
use crate::interp::interp_loglog_pre;
use crate::spline::elam_spline_one;
/// Which Chantler mass attenuation coefficient to evaluate.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChantlerKind {
/// Total attenuation.
Total,
/// Photoelectric absorption only.
Photo,
/// Incoherent (Compton) scattering only.
Incoherent,
}
/// Upper bound applied to Chantler energies, matching upstream XrayDB.
const CHANTLER_EMAX_CAP: f64 = 1e6;
impl XrayDb {
fn chantler_record(
&self,
element: &str,
) -> Result<(&'static xraydb_data::ChantlerRecord, &'static ChantlerLogs)> {
let sym = self.symbol(element)?;
self.chantler_by_symbol(sym)
.ok_or_else(|| XrayDbError::UnknownElement(element.to_string()))
}
/// Tabulated Chantler energy grid for an element, in eV.
///
/// Optionally filtered to `[emin, emax]`.
pub fn chantler_energies(
&self,
element: &str,
emin: Option<f64>,
emax: Option<f64>,
) -> Result<Vec<f64>> {
let (row, _) = self.chantler_record(element)?;
let emin = emin.unwrap_or(0.0);
let emax = emax.unwrap_or(1e9);
Ok(row
.energy
.iter()
.copied()
.filter(|&e| e >= emin && e <= emax)
.collect())
}
/// Valid energy range `(min, max)` in eV for this element's Chantler table.
///
/// Queries outside this range are silently clamped to the endpoints; use this to
/// detect that before it happens.
pub fn chantler_energy_range(&self, element: &str) -> Result<(f64, f64)> {
let (row, _) = self.chantler_record(element)?;
Ok(energy_bounds(row))
}
/// f1 — the real part of the anomalous scattering factor, at one energy.
///
/// Evaluated with an interpolating natural cubic spline through the tabulated grid.
///
/// The tabulated quantity is f′ (the anomalous correction with Z already subtracted),
/// not the full f1 — add Z for the total.
///
/// # Agreement with upstream
///
/// Upstream XrayDB fits `UnivariateSpline(s=0)` to a *window* of the grid spanning
/// the requested energies padded by three points either side, so its answer depends
/// on what else is in the same call: `f1_chantler('Au', 11919)` alone gives
/// −17.745813, but the same energy inside a wide batch gives −17.769546.
///
/// This implementation fits the spline once, globally, which is the limit upstream
/// converges to as its window grows and does not depend on the query. Agreement with
/// upstream's wide-window values is within 5e-12 across the tabulated range.
///
/// Energies outside the tabulated range are clamped to its endpoints; see
/// [`XrayDb::chantler_energy_range`].
pub fn f1_chantler_at(&self, element: &str, energy: f64) -> Result<f64> {
let (row, logs) = self.chantler_record(element)?;
let (emin, emax) = energy_bounds(row);
Ok(elam_spline_one(
&row.energy,
&row.f1,
&logs.f1_spline,
energy.clamp(emin, emax),
))
}
/// f1 at each of several energies. See [`XrayDb::f1_chantler_at`].
pub fn f1_chantler(&self, element: &str, energies: &[f64]) -> Result<Vec<f64>> {
let (row, logs) = self.chantler_record(element)?;
let (emin, emax) = energy_bounds(row);
Ok(energies
.iter()
.map(|&e| elam_spline_one(&row.energy, &row.f1, &logs.f1_spline, e.clamp(emin, emax)))
.collect())
}
/// f2 — the imaginary part of the anomalous scattering factor, at one energy.
///
/// Evaluated by log-log linear interpolation. Energies outside the tabulated range
/// are clamped; see [`XrayDb::chantler_energy_range`].
pub fn f2_chantler_at(&self, element: &str, energy: f64) -> Result<f64> {
let (row, logs) = self.chantler_record(element)?;
let (emin, emax) = energy_bounds(row);
Ok(interp_loglog_pre(
energy.clamp(emin, emax),
&logs.log_energy,
&logs.log_f2,
))
}
/// f2 at each of several energies. See [`XrayDb::f2_chantler_at`].
pub fn f2_chantler(&self, element: &str, energies: &[f64]) -> Result<Vec<f64>> {
let (row, logs) = self.chantler_record(element)?;
let (emin, emax) = energy_bounds(row);
Ok(energies
.iter()
.map(|&e| interp_loglog_pre(e.clamp(emin, emax), &logs.log_energy, &logs.log_f2))
.collect())
}
/// Chantler mass attenuation coefficient µ/ρ in cm²/g, at one energy.
///
/// Evaluated by log-log linear interpolation. Energies outside the tabulated range
/// are clamped; see [`XrayDb::chantler_energy_range`].
pub fn mu_chantler_at(&self, element: &str, energy: f64, kind: ChantlerKind) -> Result<f64> {
let (row, logs) = self.chantler_record(element)?;
let (emin, emax) = energy_bounds(row);
Ok(interp_loglog_pre(
energy.clamp(emin, emax),
&logs.log_energy,
log_values(logs, kind),
))
}
/// Chantler µ/ρ at each of several energies. See [`XrayDb::mu_chantler_at`].
pub fn mu_chantler(
&self,
element: &str,
energies: &[f64],
kind: ChantlerKind,
) -> Result<Vec<f64>> {
let (row, logs) = self.chantler_record(element)?;
let (emin, emax) = energy_bounds(row);
let values = log_values(logs, kind);
Ok(energies
.iter()
.map(|&e| interp_loglog_pre(e.clamp(emin, emax), &logs.log_energy, values))
.collect())
}
}
#[inline]
fn log_values(logs: &ChantlerLogs, kind: ChantlerKind) -> &[f64] {
match kind {
ChantlerKind::Total => &logs.log_total,
ChantlerKind::Photo => &logs.log_photo,
ChantlerKind::Incoherent => &logs.log_incoh,
}
}
#[inline]
fn energy_bounds(row: &xraydb_data::ChantlerRecord) -> (f64, f64) {
let emin = row.energy.first().copied().unwrap_or(0.0);
let emax = row
.energy
.last()
.copied()
.unwrap_or(emin)
.min(CHANTLER_EMAX_CAP);
(emin, emax)
}