Skip to main content

rexafs/xafs/
normalization.rs

1#![allow(dead_code)]
2#![allow(unused_imports)]
3
4use nalgebra::DVector;
5use polyfit_rs::polyfit_rs::polyfit;
6use serde::{Deserialize, Serialize};
7
8use super::errors::{DataError, NormalizationError};
9use super::mathutils::{self, MathUtils};
10use super::xafsutils;
11
12/// Common interface for absorption normalization and its cached results.
13///
14/// Energy is in eV; normalized and flattened outputs are dimensionless.
15/// Setters on these standalone objects do not clear cached arrays; recalculate
16/// after changing settings, or use Spectrum setters to manage invalidation.
17pub trait Normalization {
18    /// Normalize absorption using the selected method and refresh cached outputs.
19    /// Energy is in eV. Use matching finite arrays with sufficient fit points;
20    /// method-specific failures are returned as `NormalizationError`.
21    /// The low-level PrePostEdge implementation zips its inputs and filters
22    /// nonfinite pairs before fitting: unequal lengths can discard the longer
23    /// tail, and result arrays then describe only retained pairs. Spectrum
24    /// validates finite matching arrays before this step; prefer that contract.
25    fn normalize(
26        &mut self,
27        energy: &DVector<f64>,
28        mu: &DVector<f64>,
29    ) -> Result<&mut Self, NormalizationError>;
30
31    /// Borrow dimensionless edge-step-normalized absorption, or `None` before calculation.
32    fn get_norm(&self) -> Option<&DVector<f64>>;
33    /// Borrow the flattened normalized absorption, or `None` before calculation.
34    fn get_flat(&self) -> Option<&DVector<f64>>;
35    /// Read the absorption jump in input mu units, or `None` while automatic/unresolved.
36    fn get_edge_step(&self) -> Option<f64>;
37    /// Read edge energy in eV, or `None` while automatic/unresolved.
38    fn get_e0(&self) -> Option<f64>;
39    /// Set edge energy in eV or `None` for automatic detection; recalculate results afterward.
40    fn set_e0(&mut self, e0: Option<f64>) -> &mut Self;
41    /// Set the jump in mu units or `None` for estimation; recalculate results afterward.
42    fn set_edge_step(&mut self, edge_step: Option<f64>) -> &mut Self;
43}
44
45/// Absorption normalization algorithm and its settings/results.
46///
47/// Pre/post-edge normalization remains the default; full Chantler MBACK is
48/// available with explicit absorber/edge settings. `new()` uses automatic ranges while `default()`
49/// uses `PrePostEdge::default()` with fixed initial ranges.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub enum NormalizationMethod {
52    /// Implemented pre/post-edge subtraction and edge-step normalization.
53    PrePostEdge(PrePostEdge),
54    /// Full Chantler MBACK; requires absorber and edge (since 0.2.10).
55    MBack(MBack),
56}
57
58impl From<MBack> for NormalizationMethod {
59    fn from(parameters: MBack) -> Self {
60        Self::MBack(parameters)
61    }
62}
63impl From<MBack> for Option<NormalizationMethod> {
64    fn from(parameters: MBack) -> Self {
65        Some(NormalizationMethod::MBack(parameters))
66    }
67}
68
69impl From<PrePostEdge> for NormalizationMethod {
70    fn from(parameters: PrePostEdge) -> Self {
71        Self::PrePostEdge(parameters)
72    }
73}
74
75// Allow direct settings while preserving existing optional-enum setter calls.
76impl From<PrePostEdge> for Option<NormalizationMethod> {
77    fn from(parameters: PrePostEdge) -> Self {
78        Some(parameters.into())
79    }
80}
81
82impl Default for NormalizationMethod {
83    fn default() -> Self {
84        NormalizationMethod::PrePostEdge(PrePostEdge::default())
85    }
86}
87
88impl NormalizationMethod {
89    /// Select pre/post-edge normalization with automatic settings.
90    pub fn new() -> NormalizationMethod {
91        NormalizationMethod::PrePostEdge(PrePostEdge::new())
92    }
93
94    /// Select pre/post-edge normalization with automatic settings.
95    pub fn new_prepostedge() -> NormalizationMethod {
96        NormalizationMethod::PrePostEdge(PrePostEdge::new())
97    }
98
99    /// Create historical empty MBACK settings. Normalization requires an explicit
100    /// absorber and edge; prefer `MBack::for_edge(element, edge).into()`.
101    pub fn new_mback() -> NormalizationMethod {
102        NormalizationMethod::MBack(MBack::new())
103    }
104
105    /// Resolve the selected algorithm's automatic settings without fitting.
106    ///
107    /// For PrePostEdge, use matching finite energy/mu arrays with at least two
108    /// nondecreasing energy samples in eV; this resolves ranges and low-level E0
109    /// automatically. Spectrum validates an explicit E0 more strictly before
110    /// normalizing. MBACK resolves its checked ranges during fitting; this variant performs no work.
111    pub fn fill_parameter(
112        &mut self,
113        energy: &DVector<f64>,
114        mu: &DVector<f64>,
115    ) -> Result<&mut Self, NormalizationError> {
116        match self {
117            NormalizationMethod::PrePostEdge(pre_post_edge) => {
118                pre_post_edge.fill_parameter(energy, mu)?;
119            }
120            NormalizationMethod::MBack(mback) => {
121                mback.fill_parameter();
122            }
123        }
124
125        Ok(self)
126    }
127
128    /// Run the selected normalization method and replace its cached results.
129    ///
130    /// Input energy is in eV and mu has a consistent absorption scale.
131    /// Insufficient fit points or failed fits return `NormalizationError`.
132    /// The low-level PrePostEdge path zips and filters input pairs before
133    /// validation, so unequal tails/nonfinite pairs may be discarded; Spectrum
134    /// instead rejects such inputs before dispatch. MBACK also rejects invalid arrays,
135    /// unsupported atomic data and nonidentifiable fits. Borrowed inputs are unchanged.
136    pub fn normalize(
137        &mut self,
138        energy: &DVector<f64>,
139        mu: &DVector<f64>,
140    ) -> Result<&mut Self, NormalizationError> {
141        match self {
142            NormalizationMethod::PrePostEdge(pre_post_edge) => {
143                pre_post_edge.normalize(energy, mu)?;
144            }
145            NormalizationMethod::MBack(mback) => {
146                mback.normalize(energy, mu)?;
147            }
148        }
149
150        Ok(self)
151    }
152
153    /// Read the stored edge energy in eV without detecting or validating it.
154    /// `None` means no value is stored. Both algorithms resolve a missing E₀ during
155    /// normalization; neither detector shifts the atomic reference table.
156    pub fn get_e0(&self) -> Option<f64> {
157        match self {
158            NormalizationMethod::PrePostEdge(pre_post_edge) => pre_post_edge.get_e0(),
159            NormalizationMethod::MBack(mback) => mback.get_e0(),
160        }
161    }
162
163    /// Read the stored absorption jump in input mu units without calculating it.
164    /// `None` means no value is stored. PrePostEdge estimates a missing jump during
165    /// normalization; MBACK calculates its step from the matched atomic scale.
166    pub fn get_edge_step(&self) -> Option<f64> {
167        match self {
168            NormalizationMethod::PrePostEdge(pre_post_edge) => pre_post_edge.get_edge_step(),
169            NormalizationMethod::MBack(mback) => mback.get_edge_step(),
170        }
171    }
172
173    /// Borrow the stored dimensionless flattened absorption without calculating.
174    /// PrePostEdge removes its fitted post-edge trend above E0. Returns `None`
175    /// when no successful result is stored. MBACK uses its auxiliary fitted trend.
176    pub fn get_flat(&self) -> Option<&DVector<f64>> {
177        match self {
178            NormalizationMethod::PrePostEdge(pre_post_edge) => pre_post_edge.get_flat(),
179            NormalizationMethod::MBack(mback) => mback.get_flat(),
180        }
181    }
182
183    /// Borrow the stored dimensionless normalized absorption without calculating.
184    /// PrePostEdge produces `(mu - pre_edge) / edge_step`. Returns `None` when no
185    /// array is stored. MBACK uses `(s*mu-P)/Delta` from its saved auxiliary model.
186    pub fn get_norm(&self) -> Option<&DVector<f64>> {
187        match self {
188            NormalizationMethod::PrePostEdge(pre_post_edge) => pre_post_edge.get_norm(),
189            NormalizationMethod::MBack(mback) => mback.get_norm(),
190        }
191    }
192
193    /// Store edge energy in eV, or `None` to request PrePostEdge detection.
194    /// An explicit value used through Spectrum must be finite and strictly inside
195    /// the input energy range. This standalone setter does not validate, recalculate
196    /// or clear arrays; use Spectrum setters to invalidate dependent results.
197    pub fn set_e0(&mut self, e0: Option<f64>) -> &mut Self {
198        match self {
199            NormalizationMethod::PrePostEdge(pre_post_edge) => {
200                pre_post_edge.set_e0(e0);
201            }
202            NormalizationMethod::MBack(mback) => {
203                mback.set_e0(e0);
204            }
205        }
206
207        self
208    }
209
210    /// Store the absorption jump in mu units, or `None` for PrePostEdge estimation.
211    /// This standalone setter does not validate, recalculate or clear stored arrays.
212    /// For MBACK this is only historical output storage: the next fit determines
213    /// its step from atomic data and does not use this value as an override.
214    pub fn set_edge_step(&mut self, edge_step: Option<f64>) -> &mut Self {
215        match self {
216            NormalizationMethod::PrePostEdge(pre_post_edge) => {
217                pre_post_edge.set_edge_step(edge_step);
218            }
219            NormalizationMethod::MBack(mback) => {
220                mback.set_edge_step(edge_step);
221            }
222        }
223
224        self
225    }
226}
227
228/// Pre-edge subtraction, edge-step normalization and post-edge flattening.
229///
230/// Use `new()` for data-dependent fit ranges and polynomial degree, as in the
231/// Python/TypeScript constructors. `default()` instead uses fixed offsets
232/// −200/−30/150/2000 eV and degree 2. Endpoints are offsets from E0, not absolute
233/// energies. Results are cached in this object after `normalize`.
234///
235/// The method fits a line to `mu * E.powi(n_victoreen)`, removes that background,
236/// and fits a post-edge polynomial to estimate the edge step. See
237/// [Newville, Fundamentals of XAFS](https://docs.xrayabsorption.org/tutorials/XAFS_Fundamentals.pdf).
238/// The precise automatic ranges and flattening convention are rexafs choices.
239///
240/// For photon energy E in eV and input absorption mu(E), the fitted models are
241/// `p(E) = (a + b*E) * E^(-v)` and a polynomial P(E) fitted to `mu(E)-p(E)`.
242/// Here v is `n_victoreen`; coefficients have the units required to match mu.
243/// `pre_edge` is p(E), and `post_edge` is p(E)+P(E). If E* is the measured
244/// energy nearest E0, the automatic jump is P(E*), in mu units. With the
245/// resolved jump D, the dimensionless outputs are
246///
247/// ```text
248/// norm(E) = (mu(E) - p(E)) / D
249/// flat(E) = norm(E)                              for E < E*
250///         = norm(E) - (P(E) - P(E*)) / D          for E >= E*
251/// ```
252///
253/// The jump must be finite and is floored at 1e-12. This numerical floor is not
254/// evidence of a usable absorption edge. Flattening is a separate display
255/// output; AUTOBK uses the original absorption and resolved edge step.
256/// Automatic values filled into this object are retained. Set a field back to
257/// None, or construct fresh settings, to request its automatic resolution again.
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259#[serde(default)]
260pub struct PrePostEdge {
261    /// Lower pre-edge fit offset from E0 in eV. `new()` leaves this automatic;
262    /// `default()` uses −200. Automatic selection follows the available pre-edge data.
263    pub pre_edge_start: Option<f64>,
264    /// Upper pre-edge fit offset from E0 in eV. `new()` is automatic; `default()`
265    /// uses −30. Choose a range below edge structure with enough points for a line.
266    pub pre_edge_end: Option<f64>,
267    /// Lower post-edge fit offset from E0 in eV. `new()` is automatic; `default()`
268    /// uses 150. Automatic selection depends on the resolved upper endpoint.
269    pub norm_start: Option<f64>,
270    /// Upper post-edge fit offset from E0 in eV. `new()` uses the available range;
271    /// `default()` uses 2000. Fit intervals are evaluated on available samples.
272    pub norm_end: Option<f64>,
273    /// Degree of the post-edge polynomial. Automatic degrees are 0, 1, or 2 for
274    /// spans below 50 eV, below 350 eV, or at least 350 eV; explicit values are
275    /// clamped to 0–5. `new()` is automatic and `default()` uses 2.
276    pub norm_polyorder: Option<i32>,
277    /// Exponent v in the pre-edge model `(a + b*E) * E.powi(-v)`.
278    /// The line is fitted to `mu * E.powi(v)`; automatic v is 0 (ordinary line).
279    pub n_victoreen: Option<i32>,
280    /// Absorption edge energy in eV; `None` requests automatic detection.
281    /// When using Spectrum, an explicit E0 must be finite and strictly inside the
282    /// energy range or normalization returns `E0OutOfRange`. The lower-level
283    /// `PrePostEdge::fill_parameter` instead replaces a nonfinite value or one
284    /// outside the first through penultimate input energies with a derivative estimate.
285    pub e0: Option<f64>,
286    /// Absorption jump in the same units as mu. `None` uses post-edge minus
287    /// pre-edge at the energy sample nearest E0. A finite result is floored at
288    /// 1e-12; inspect nonpositive or tiny jumps rather than trusting that floor.
289    pub edge_step: Option<f64>,
290    /// Fitted pre-edge background on the energy grid, in input mu units.
291    /// `None` until normalization runs.
292    pub pre_edge: Option<DVector<f64>>,
293    /// Pre-edge background plus the post-edge polynomial, in input mu units.
294    /// `None` until normalization runs.
295    pub post_edge: Option<DVector<f64>>,
296    /// Dimensionless `(mu - pre_edge) / edge_step` on the energy grid.
297    /// `None` until normalization runs.
298    pub norm: Option<DVector<f64>>,
299    /// Dimensionless normalized absorption with the post-edge trend removed above
300    /// the sample nearest E0. Values below that sample equal `norm`. This is a
301    /// separate display output and is not substituted into AUTOBK.
302    pub flat: Option<DVector<f64>>,
303    /// Fitted line coefficients `[a, b]` for `mu * E.powi(n_victoreen)`.
304    /// Coefficient units depend on the exponent and the use of E in eV.
305    pub pre_coefficients: Option<Vec<f64>>,
306    /// Post-edge polynomial coefficients in increasing powers of absolute E in eV.
307    /// Their weighted sum has input mu units; they are not powers of E − E0.
308    pub norm_coefficients: Option<Vec<f64>>,
309}
310
311impl Default for PrePostEdge {
312    fn default() -> Self {
313        PrePostEdge {
314            pre_edge_start: Some(-200.0),
315            pre_edge_end: Some(-30.0),
316            norm_start: Some(150.0),
317            norm_end: Some(2000.0),
318            norm_polyorder: Some(2),
319            n_victoreen: Some(0),
320            e0: None,
321            edge_step: None,
322            pre_edge: None,
323            post_edge: None,
324            norm: None,
325            flat: None,
326            norm_coefficients: None,
327            pre_coefficients: None,
328        }
329    }
330}
331
332impl PrePostEdge {
333    const MAX_NORM_POLYORDER: i32 = 5;
334
335    /// Create automatic normalization settings.
336    ///
337    /// Unlike `default()`, every configurable field starts as `None`; the next
338    /// calculation resolves fit ranges, degree, edge energy and edge step from data.
339    pub fn new() -> PrePostEdge {
340        PrePostEdge {
341            pre_edge_start: None,
342            pre_edge_end: None,
343            norm_start: None,
344            norm_end: None,
345            norm_polyorder: None,
346            n_victoreen: None,
347            e0: None,
348            edge_step: None,
349            pre_edge: None,
350            post_edge: None,
351            norm: None,
352            flat: None,
353            norm_coefficients: None,
354            pre_coefficients: None,
355        }
356    }
357
358    /// Resolve automatic settings from equal-length finite energy/mu arrays.
359    ///
360    /// Requires at least two samples and nondecreasing energy in eV. This mutates
361    /// settings but does not fit the backgrounds or refresh result arrays.
362    /// Resolved values remain stored; reset a field to None or create a fresh
363    /// object to request automatic selection again.
364    pub fn fill_parameter(
365        &mut self,
366        energy: &DVector<f64>,
367        mu: &DVector<f64>,
368    ) -> Result<&mut Self, NormalizationError> {
369        Self::validate_inputs(energy, mu)?;
370
371        let mut e0 = self.e0.unwrap_or(f64::NAN);
372        if !e0.is_finite() || e0 > energy[energy.len() - 2] || e0 < energy[0] {
373            e0 = xafsutils::find_e0(energy, mu)?;
374            self.e0 = Some(e0);
375        }
376
377        let energy_slice = energy.as_slice();
378        let ie0 = mathutils::index_nearest_sorted(energy_slice, &e0)?;
379        let e0 = energy[ie0];
380
381        let n_victoreen = self.n_victoreen.unwrap_or(0);
382        self.n_victoreen = Some(n_victoreen);
383
384        let mut pre_edge_start = self.pre_edge_start.unwrap_or_else(|| {
385            let estimate = if ie0 > 20 {
386                5.0 * ((energy[1] - e0) / 5.0).round()
387            } else {
388                2.0 * ((energy[1] - e0) / 2.0).round()
389            };
390            estimate.max(energy.min() - e0)
391        });
392
393        let mut pre_edge_end = self
394            .pre_edge_end
395            .unwrap_or_else(|| 5.0 * (pre_edge_start / 15.0).round());
396
397        if pre_edge_start > pre_edge_end {
398            std::mem::swap(&mut pre_edge_start, &mut pre_edge_end);
399        }
400
401        let mut norm_end = self.norm_end.unwrap_or_else(|| {
402            let estimate = 5.0 * ((energy.max() - e0) / 5.0).round();
403            let estimate = if estimate < 0.0 {
404                energy.max() - e0 - estimate
405            } else {
406                estimate
407            };
408            estimate.min(energy.max() - e0)
409        });
410
411        let mut norm_start = self
412            .norm_start
413            .unwrap_or_else(|| (5.0 * (norm_end / 15.0).round()).min(25.0));
414
415        if norm_start > norm_end + 5.0 {
416            std::mem::swap(&mut norm_start, &mut norm_end);
417        }
418        norm_start = norm_start.min(norm_end - 10.0);
419
420        let mut norm_polyorder = self.norm_polyorder.unwrap_or_else(|| {
421            let diff = norm_end - norm_start;
422            if diff < 50.0 {
423                0
424            } else if diff < 350.0 {
425                1
426            } else {
427                2
428            }
429        });
430        norm_polyorder = norm_polyorder.clamp(0, PrePostEdge::MAX_NORM_POLYORDER);
431
432        self.pre_edge_start = Some(pre_edge_start);
433        self.pre_edge_end = Some(pre_edge_end);
434        self.norm_start = Some(norm_start);
435        self.norm_end = Some(norm_end);
436        self.norm_polyorder = Some(norm_polyorder);
437
438        Ok(self)
439    }
440
441    fn validate_inputs(energy: &DVector<f64>, mu: &DVector<f64>) -> Result<(), NormalizationError> {
442        if energy.len() != mu.len() {
443            return Err(DataError::LengthMismatch {
444                energy_len: energy.len(),
445                mu_len: mu.len(),
446            }
447            .into());
448        }
449        if energy.len() < 2 {
450            return Err(DataError::InsufficientData {
451                min: 2,
452                actual: energy.len(),
453            }
454            .into());
455        }
456
457        let non_finite = energy
458            .iter()
459            .zip(mu.iter())
460            .enumerate()
461            .filter_map(|(index, (e, m))| (!e.is_finite() || !m.is_finite()).then_some(index))
462            .collect::<Vec<_>>();
463        if !non_finite.is_empty() {
464            return Err(DataError::NonFiniteValues {
465                indices: non_finite,
466            }
467            .into());
468        }
469
470        for index in 1..energy.len() {
471            let prev = energy[index - 1];
472            let curr = energy[index];
473            if curr < prev {
474                return Err(DataError::NonMonotonicEnergy { index, prev, curr }.into());
475            }
476        }
477
478        Ok(())
479    }
480
481    /// Read the stored value without recalculating.
482    ///
483    /// Lower pre-edge fit offset from E0 in eV. `new()` leaves this automatic;
484    /// `default()` uses −200. Automatic selection follows the available pre-edge data.
485    pub fn get_pre_edge_start(&self) -> Option<f64> {
486        self.pre_edge_start
487    }
488
489    /// Read the stored value without recalculating.
490    ///
491    /// Upper pre-edge fit offset from E0 in eV. `new()` is automatic; `default()`
492    /// uses −30. Choose a range below edge structure with enough points for a line.
493    pub fn get_pre_edge_end(&self) -> Option<f64> {
494        self.pre_edge_end
495    }
496
497    /// Read the stored value without recalculating.
498    ///
499    /// Lower post-edge fit offset from E0 in eV. `new()` is automatic; `default()`
500    /// uses 150. Automatic selection depends on the resolved upper endpoint.
501    pub fn get_norm_start(&self) -> Option<f64> {
502        self.norm_start
503    }
504
505    /// Read the stored value without recalculating.
506    ///
507    /// Upper post-edge fit offset from E0 in eV. `new()` uses the available range;
508    /// `default()` uses 2000. Fit intervals are evaluated on available samples.
509    pub fn get_norm_end(&self) -> Option<f64> {
510        self.norm_end
511    }
512
513    /// Read the stored value without recalculating.
514    ///
515    /// Degree of the post-edge polynomial. Automatic degrees are 0, 1, or 2 for
516    /// spans below 50 eV, below 350 eV, or at least 350 eV; explicit values are
517    /// clamped to 0–5. `new()` is automatic and `default()` uses 2.
518    pub fn get_norm_polyorder(&self) -> Option<i32> {
519        self.norm_polyorder
520    }
521
522    /// Read the stored value without recalculating.
523    ///
524    /// Exponent v in the pre-edge model `(a + b*E) * E.powi(-v)`.
525    /// The line is fitted to `mu * E.powi(v)`; automatic v is 0 (ordinary line).
526    pub fn get_n_victoreen(&self) -> Option<i32> {
527        self.n_victoreen
528    }
529
530    /// Read the stored value without recalculating.
531    ///
532    /// Fitted pre-edge background on the energy grid, in input mu units.
533    /// `None` until normalization runs.
534    pub fn get_pre_edge(&self) -> Option<&DVector<f64>> {
535        self.pre_edge.as_ref()
536    }
537
538    /// Read the stored value without recalculating.
539    ///
540    /// Pre-edge background plus the post-edge polynomial, in input mu units.
541    /// `None` until normalization runs.
542    pub fn get_post_edge(&self) -> Option<&DVector<f64>> {
543        self.post_edge.as_ref()
544    }
545
546    /// Read the stored value without recalculating.
547    ///
548    /// Post-edge polynomial coefficients in increasing powers of absolute E in eV.
549    /// Their weighted sum has input mu units; they are not powers of E − E0.
550    pub fn get_norm_coefficients(&self) -> Option<&Vec<f64>> {
551        self.norm_coefficients.as_ref()
552    }
553
554    /// Read the stored value without recalculating.
555    ///
556    /// Fitted line coefficients `[a, b]` for `mu * E.powi(n_victoreen)`.
557    /// Coefficient units depend on the exponent and the use of E in eV.
558    pub fn get_pre_coefficients(&self) -> Option<&Vec<f64>> {
559        self.pre_coefficients.as_ref()
560    }
561}
562
563impl Normalization for PrePostEdge {
564    fn normalize(
565        &mut self,
566        energy: &DVector<f64>,
567        mu: &DVector<f64>,
568    ) -> Result<&mut Self, NormalizationError> {
569        let (energy, mu): (Vec<f64>, Vec<f64>) = energy
570            .iter()
571            .zip(mu.iter())
572            .filter(|(e, m)| e.is_finite() && m.is_finite())
573            .map(|(e, m)| (*e, *m))
574            .unzip();
575        let energy = DVector::from_vec(energy);
576        let mu = DVector::from_vec(mu);
577        Self::validate_inputs(&energy, &mu)?;
578
579        self.fill_parameter(&energy, &mu)?;
580
581        let e0 = self.e0.ok_or_else(|| DataError::MissingData {
582            field: "e0".to_string(),
583        })?;
584        let pre_edge_start = self.pre_edge_start.ok_or_else(|| DataError::MissingData {
585            field: "pre_edge_start".to_string(),
586        })?;
587        let pre_edge_end = self.pre_edge_end.ok_or_else(|| DataError::MissingData {
588            field: "pre_edge_end".to_string(),
589        })?;
590        let norm_start = self.norm_start.ok_or_else(|| DataError::MissingData {
591            field: "norm_start".to_string(),
592        })?;
593        let norm_end = self.norm_end.ok_or_else(|| DataError::MissingData {
594            field: "norm_end".to_string(),
595        })?;
596        let norm_polyorder = self.norm_polyorder.ok_or_else(|| DataError::MissingData {
597            field: "norm_polyorder".to_string(),
598        })?;
599        let nvict = self.n_victoreen.unwrap_or(0);
600
601        let energy_slice = energy.as_slice();
602        let p1 = mathutils::index_of_sorted(energy_slice, &(pre_edge_start + e0))?;
603        let mut p2 = mathutils::index_nearest_sorted(energy_slice, &(pre_edge_end + e0))?;
604        if p2 <= p1 || p2 - p1 < 2 {
605            if p1 + 2 > energy.len() {
606                return Err(NormalizationError::PreEdgeFitFailed {
607                    start: pre_edge_start + e0,
608                    end: pre_edge_end + e0,
609                });
610            }
611            p2 = (p1 + 2).min(energy.len());
612        }
613
614        let mut energy_x = Vec::with_capacity(p2 - p1);
615        let mut mu_x = Vec::with_capacity(p2 - p1);
616        for i in p1..p2 {
617            let e = energy[i];
618            let y = mu[i] * e.powi(nvict);
619            if e.is_finite() && y.is_finite() {
620                energy_x.push(e);
621                mu_x.push(y);
622            }
623        }
624        if energy_x.len() < 2 {
625            return Err(NormalizationError::PreEdgeFitFailed {
626                start: pre_edge_start + e0,
627                end: pre_edge_end + e0,
628            });
629        }
630
631        let pre_coefficients: Vec<f64> = polyfit(&energy_x, &mu_x, 1)?;
632
633        let mut pre_edge = DVector::zeros(energy.len());
634        for i in 0..energy.len() {
635            pre_edge[i] =
636                (energy[i] * pre_coefficients[1] + pre_coefficients[0]) * energy[i].powi(-nvict);
637        }
638
639        let mut p1 = mathutils::index_of_sorted(energy_slice, &(norm_start + e0))?;
640        let mut p2 = mathutils::index_nearest_sorted(energy_slice, &(norm_end + e0))?;
641
642        if p2 <= p1 || p2 - p1 < 2 {
643            if p1 + 2 > energy.len() {
644                return Err(NormalizationError::PostEdgeFitFailed {
645                    order: norm_polyorder as usize,
646                    n_points: 0,
647                });
648            }
649            p2 = (p1 + 2).min(energy.len());
650            p1 = (p1 + 1).min(energy.len() - 1);
651        }
652
653        let mut presub = Vec::with_capacity(p2 - p1);
654        for i in p1..p2 {
655            presub.push(mu[i] - pre_edge[i]);
656        }
657        if presub.len() <= norm_polyorder as usize {
658            return Err(NormalizationError::PostEdgeFitFailed {
659                order: norm_polyorder as usize,
660                n_points: presub.len(),
661            });
662        }
663        let post_coefficients = polyfit(&energy_slice[p1..p2], &presub, norm_polyorder as usize)?;
664
665        let mut post_edge = pre_edge.clone();
666        for (i, c) in post_coefficients.iter().enumerate() {
667            for j in 0..post_edge.len() {
668                post_edge[j] += energy[j].powi(i as i32) * *c;
669            }
670        }
671
672        let ie0 = mathutils::index_nearest_sorted(energy_slice, &e0)?;
673        let edge_step = self.edge_step.unwrap_or(post_edge[ie0] - pre_edge[ie0]);
674        if !edge_step.is_finite() {
675            return Err(NormalizationError::EdgeStepTooSmall {
676                edge_step,
677                min: 1.0e-12,
678            });
679        }
680        let edge_step = edge_step.max(1.0e-12);
681
682        let norm = (&mu - &pre_edge) / edge_step;
683        let flat_residue = (&post_edge - &pre_edge) / edge_step;
684
685        let mut flat = &norm - &flat_residue;
686        let residue_offset = flat_residue[ie0];
687        for i in 0..flat.len() {
688            flat[i] += residue_offset;
689        }
690        for i in 0..ie0 {
691            flat[i] = norm[i];
692        }
693
694        self.edge_step = Some(edge_step);
695        self.pre_edge = Some(pre_edge);
696        self.post_edge = Some(post_edge);
697        self.norm = Some(norm);
698        self.flat = Some(flat);
699        self.norm_coefficients = Some(post_coefficients);
700        self.pre_coefficients = Some(pre_coefficients);
701
702        Ok(self)
703    }
704
705    fn get_e0(&self) -> Option<f64> {
706        self.e0
707    }
708
709    fn get_edge_step(&self) -> Option<f64> {
710        self.edge_step
711    }
712
713    fn get_flat(&self) -> Option<&DVector<f64>> {
714        self.flat.as_ref()
715    }
716
717    fn get_norm(&self) -> Option<&DVector<f64>> {
718        self.norm.as_ref()
719    }
720
721    fn set_e0(&mut self, e0: Option<f64>) -> &mut Self {
722        self.e0 = e0;
723        self
724    }
725
726    fn set_edge_step(&mut self, edge_step: Option<f64>) -> &mut Self {
727        self.edge_step = edge_step;
728        self
729    }
730}
731
732/// Full Chantler MBACK model and cached results (since 0.2.10).
733pub use super::mback::MBack;
734
735#[cfg(test)]
736mod tests {
737    use crate::xafs::io;
738    use data_reader::reader::load_txt_f64;
739
740    use super::*;
741    use crate::xafs::tests::PARAM_LOADTXT;
742    use crate::xafs::tests::TOP_DIR;
743    use crate::xafs::tests::{TEST_TOL, TEST_TOL_LESS_ACC};
744    use approx::assert_abs_diff_eq;
745    const ACCEPTABLE_MU_DIFF: f64 = 1e-6;
746
747    #[test]
748    fn test_pre_post_edge_fill_parameter() {
749        let path = String::from(TOP_DIR) + "/tests/testfiles/Ru_QAS.dat";
750        let xafs_test_group = io::load_spectrum_QAS_trans(&path).unwrap();
751
752        let mut pre_post_edge = PrePostEdge::new();
753        let energy = xafs_test_group.energy.clone().unwrap();
754        let mu = xafs_test_group.mu.clone().unwrap();
755
756        let _ = pre_post_edge.fill_parameter(&energy, &mu);
757
758        let expected = PrePostEdge {
759            pre_edge_start: Some(-200.0),
760            pre_edge_end: Some(-65.0),
761            norm_start: Some(25.0),
762            norm_end: Some(944.5331719999995),
763            norm_polyorder: Some(2),
764            n_victoreen: None,
765            e0: Some(22118.8),
766            edge_step: None,
767            pre_edge: None,
768            post_edge: None,
769            norm: None,
770            flat: None,
771            norm_coefficients: None,
772            pre_coefficients: None,
773        };
774
775        assert_abs_diff_eq!(
776            pre_post_edge.e0.unwrap(),
777            expected.e0.unwrap(),
778            epsilon = TEST_TOL
779        );
780
781        assert_abs_diff_eq!(
782            pre_post_edge.pre_edge_start.unwrap(),
783            expected.pre_edge_start.unwrap(),
784            epsilon = TEST_TOL
785        );
786
787        assert_abs_diff_eq!(
788            pre_post_edge.pre_edge_end.unwrap(),
789            expected.pre_edge_end.unwrap(),
790            epsilon = TEST_TOL
791        );
792
793        assert_abs_diff_eq!(
794            pre_post_edge.norm_start.unwrap(),
795            expected.norm_start.unwrap(),
796            epsilon = TEST_TOL
797        );
798
799        assert_abs_diff_eq!(
800            pre_post_edge.norm_end.unwrap(),
801            expected.norm_end.unwrap(),
802            epsilon = TEST_TOL
803        );
804
805        assert_abs_diff_eq!(
806            pre_post_edge.norm_polyorder.unwrap() as f64,
807            expected.norm_polyorder.unwrap() as f64,
808            epsilon = TEST_TOL
809        );
810    }
811
812    #[test]
813    fn test_normalization() {
814        let path = String::from(TOP_DIR) + "/tests/testfiles/Ru_QAS.dat";
815        let xafs_test_group = io::load_spectrum_QAS_trans(&path).unwrap();
816        let energy = xafs_test_group.energy.clone().unwrap();
817        let mu = xafs_test_group.mu.clone().unwrap();
818
819        let mut pre_post_edge = PrePostEdge::new();
820        let _ = pre_post_edge.fill_parameter(&energy, &mu);
821        let _ = pre_post_edge.normalize(&energy, &mu);
822
823        assert_abs_diff_eq!(
824            pre_post_edge.edge_step.unwrap(),
825            0.862815921384477,
826            epsilon = TEST_TOL_LESS_ACC
827        );
828
829        pre_post_edge
830            .pre_coefficients
831            .as_ref()
832            .unwrap()
833            .iter()
834            .zip([-0.05298882571982536, -1.9039451808611713e-7].iter())
835            .for_each(|(a, b)| assert_abs_diff_eq!(a, b, epsilon = TEST_TOL_LESS_ACC));
836
837        pre_post_edge
838            .norm_coefficients
839            .as_ref()
840            .unwrap()
841            .iter()
842            .zip(
843                [
844                    8.985714230146124,
845                    -0.0005540674890038064,
846                    8.446567273641622e-9,
847                ]
848                .iter(),
849            )
850            .for_each(|(a, b)| assert_abs_diff_eq!(a, b, epsilon = TEST_TOL_LESS_ACC.sqrt()));
851
852        let path = String::from(TOP_DIR) + "/tests/testfiles/Ru_QAS_pre_post_edge_expected.dat";
853        let reference_dat = load_txt_f64(&path, &PARAM_LOADTXT).unwrap();
854
855        let reference_norm = reference_dat.get_col(4);
856        let reference_flat = reference_dat.get_col(5);
857
858        pre_post_edge
859            .norm
860            .as_ref()
861            .unwrap()
862            .iter()
863            .zip(reference_norm.iter())
864            .for_each(|(a, b)| assert_abs_diff_eq!(a, b, epsilon = TEST_TOL_LESS_ACC));
865
866        pre_post_edge
867            .flat
868            .as_ref()
869            .unwrap()
870            .iter()
871            .zip(reference_flat.iter())
872            .for_each(|(a, b)| assert_abs_diff_eq!(a, b, epsilon = TEST_TOL_LESS_ACC));
873
874        let larch_norm_path = String::from(TOP_DIR) + "/tests/testfiles/Ru_QAS_preedge_larch.txt";
875        let larch_norm = load_txt_f64(&larch_norm_path, &PARAM_LOADTXT).unwrap();
876
877        let norm_expected = larch_norm.get_col(1);
878
879        let expected = PrePostEdge {
880            pre_edge_start: Some(-200.0),
881            pre_edge_end: Some(-65.0),
882            norm_start: Some(25.0),
883            norm_end: Some(945.0),
884            norm_polyorder: Some(2),
885            n_victoreen: None,
886            e0: Some(22118.8),
887            edge_step: Some(0.8614006777730155),
888            pre_edge: None,
889            post_edge: None,
890            norm: None,
891            flat: None,
892            norm_coefficients: Some(vec![
893                8.985714130708697,
894                -0.0005540674801681585,
895                8.446567483044725e-09,
896            ]),
897            pre_coefficients: Some(vec![-5.29888257e-02, -1.90394518e-07]),
898        };
899
900        assert_abs_diff_eq!(
901            pre_post_edge.e0.unwrap(),
902            expected.e0.unwrap(),
903            epsilon = TEST_TOL
904        );
905
906        pre_post_edge
907            .norm
908            .as_ref()
909            .unwrap()
910            .iter()
911            .zip(norm_expected.iter())
912            .for_each(|(a, b)| {
913                assert_abs_diff_eq!(a, b, epsilon = ACCEPTABLE_MU_DIFF);
914            });
915    }
916}