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
12pub trait Normalization {
18 fn normalize(
26 &mut self,
27 energy: &DVector<f64>,
28 mu: &DVector<f64>,
29 ) -> Result<&mut Self, NormalizationError>;
30
31 fn get_norm(&self) -> Option<&DVector<f64>>;
33 fn get_flat(&self) -> Option<&DVector<f64>>;
35 fn get_edge_step(&self) -> Option<f64>;
37 fn get_e0(&self) -> Option<f64>;
39 fn set_e0(&mut self, e0: Option<f64>) -> &mut Self;
41 fn set_edge_step(&mut self, edge_step: Option<f64>) -> &mut Self;
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub enum NormalizationMethod {
52 PrePostEdge(PrePostEdge),
54 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
75impl 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 pub fn new() -> NormalizationMethod {
91 NormalizationMethod::PrePostEdge(PrePostEdge::new())
92 }
93
94 pub fn new_prepostedge() -> NormalizationMethod {
96 NormalizationMethod::PrePostEdge(PrePostEdge::new())
97 }
98
99 pub fn new_mback() -> NormalizationMethod {
102 NormalizationMethod::MBack(MBack::new())
103 }
104
105 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 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 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 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 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 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 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259#[serde(default)]
260pub struct PrePostEdge {
261 pub pre_edge_start: Option<f64>,
264 pub pre_edge_end: Option<f64>,
267 pub norm_start: Option<f64>,
270 pub norm_end: Option<f64>,
273 pub norm_polyorder: Option<i32>,
277 pub n_victoreen: Option<i32>,
280 pub e0: Option<f64>,
286 pub edge_step: Option<f64>,
290 pub pre_edge: Option<DVector<f64>>,
293 pub post_edge: Option<DVector<f64>>,
296 pub norm: Option<DVector<f64>>,
299 pub flat: Option<DVector<f64>>,
303 pub pre_coefficients: Option<Vec<f64>>,
306 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 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 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 pub fn get_pre_edge_start(&self) -> Option<f64> {
486 self.pre_edge_start
487 }
488
489 pub fn get_pre_edge_end(&self) -> Option<f64> {
494 self.pre_edge_end
495 }
496
497 pub fn get_norm_start(&self) -> Option<f64> {
502 self.norm_start
503 }
504
505 pub fn get_norm_end(&self) -> Option<f64> {
510 self.norm_end
511 }
512
513 pub fn get_norm_polyorder(&self) -> Option<i32> {
519 self.norm_polyorder
520 }
521
522 pub fn get_n_victoreen(&self) -> Option<i32> {
527 self.n_victoreen
528 }
529
530 pub fn get_pre_edge(&self) -> Option<&DVector<f64>> {
535 self.pre_edge.as_ref()
536 }
537
538 pub fn get_post_edge(&self) -> Option<&DVector<f64>> {
543 self.post_edge.as_ref()
544 }
545
546 pub fn get_norm_coefficients(&self) -> Option<&Vec<f64>> {
551 self.norm_coefficients.as_ref()
552 }
553
554 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
732pub 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}