rexafs 0.2.5

Rust-powered X-ray absorption spectroscopy analysis and EXAFS fitting
Documentation
//! Legacy JSON serialization for [`XASGroupFile`].
//! Writes replace files directly and do not create desktop project backups.

use std::error::Error;
use std::fs::File;
use std::io::{Read, Write};
use std::mem;

use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use serde::{Deserialize, Serialize};
use serde_json::{self, json};
use version::version;

use crate::xafs::errors::IOError;
use crate::xafs::io::xasdatatype::{XASDataType, XASGroupFile};
use crate::xafs::xasgroup::XASGroup;
use crate::xafs::xasspectrum::XASSpectrum;

/// Read and write owned group envelopes as JSON; this is not the `.rxs` format.
pub trait XASJson {
    /// Replace the envelope after successfully reading plain JSON. I/O or JSON errors leave it unchanged.
    fn read_json(&mut self, filename: &str) -> Result<&mut Self, IOError>;

    /// Update version/type metadata and overwrite the destination with plain JSON. Returns I/O or serialization errors.
    fn write_json(&mut self, filename: &str) -> Result<&mut Self, IOError>;

    /// Read gzip-compressed JSON, except a `.json` filename selects plain JSON. The envelope changes only after successful decoding.
    fn read_jsongz(&mut self, filename: &str) -> Result<&mut Self, IOError>;

    /// Overwrite with gzip-compressed JSON, except a `.json` filename selects plain JSON. Updates version/type metadata before writing.
    fn write_jsongz(&mut self, filename: &str) -> Result<&mut Self, IOError>;
}

impl XASJson for XASGroupFile {
    fn read_json(&mut self, filename: &str) -> Result<&mut Self, IOError> {
        let f_buffer = File::open(filename).map_err(|e| IOError::ReadFailed {
            path: filename.to_string(),
            kind: e.kind(),
        })?;

        let doc = serde_json::from_reader(f_buffer).map_err(|e| IOError::JsonError {
            message: e.to_string(),
        })?;
        _ = mem::replace(self, doc);

        Ok(self)
    }

    fn write_json(&mut self, filename: &str) -> Result<&mut Self, IOError> {
        self.version = version!().to_string();
        self.datatype = XASDataType::XASGroup;

        let mut data_file = File::create(filename).map_err(|e| IOError::ReadFailed {
            path: filename.to_string(),
            kind: e.kind(),
        })?;

        serde_json::to_writer(&mut data_file, &self).map_err(|e| IOError::JsonError {
            message: e.to_string(),
        })?;

        Ok(self)
    }

    fn read_jsongz(&mut self, filename: &str) -> Result<&mut Self, IOError> {
        if filename.ends_with(".json") {
            return self.read_json(filename);
        }

        let f_buffer = File::open(filename).map_err(|e| IOError::ReadFailed {
            path: filename.to_string(),
            kind: e.kind(),
        })?;
        let f_buffer = GzDecoder::new(f_buffer);
        let doc = serde_json::from_reader(f_buffer).map_err(|e| IOError::CompressionError {
            message: format!("decompression failed: {}", e),
        })?;

        _ = mem::replace(self, doc);

        Ok(self)
    }

    fn write_jsongz(&mut self, filename: &str) -> Result<&mut Self, IOError> {
        if filename.ends_with(".json") {
            return self.write_json(filename);
        }

        self.version = version!().to_string();
        self.datatype = XASDataType::XASGroup;

        let mut data_file = File::create(filename).map_err(|e| IOError::ReadFailed {
            path: filename.to_string(),
            kind: e.kind(),
        })?;

        let mut encoder = GzEncoder::new(&mut data_file, Compression::default());

        serde_json::to_writer(&mut encoder, &self).map_err(|e| IOError::CompressionError {
            message: format!("compression failed: {}", e),
        })?;

        Ok(self)
    }
}

#[cfg(test)]
mod tests {
    use approx;
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::*;
    use crate::prelude::*;
    use approx::{assert_abs_diff_eq, assert_relative_eq};

    use data_reader::reader::{load_txt_f64, Delimiter, ReaderParams};

    use crate::xafs::tests::PARAM_LOADTXT;
    use crate::xafs::tests::TEST_TOL;
    use crate::xafs::tests::TOP_DIR;

    const ACCEPTABLE_MU_DIFF: f64 = 1e-6;
    const CHI_MSE_TOL: f64 = 1e-2;
    const CHI_Q_TOL: f64 = 1e-1;

    fn unique_testfile_path(ext: &str) -> String {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        format!("{}/tests/testfiles/test_{}.{}", TOP_DIR, nanos, ext)
    }

    #[test]
    #[allow(non_snake_case)]
    fn test_xas_json_write() -> Result<(), Box<dyn std::error::Error>> {
        let path = String::from(TOP_DIR) + "/tests/testfiles/Ru_QAS.dat";
        let save_path = unique_testfile_path("json");
        let mut xafs_test_group = io::load_spectrum_QAS_trans(&path).unwrap();

        xafs_test_group.set_background_method(Some(BackgroundMethod::AUTOBK(AUTOBK {
            rbkg: Some(1.4),
            ..Default::default()
        })))?;
        xafs_test_group.calc_background()?;

        xafs_test_group.xftf = Some(XrayFFTF {
            window: Some(FTWindow::Hanning),
            dk: Some(f64::EPSILON),
            kmin: Some(0.0),
            kmax: Some(15.0),
            kweight: Some(2.0),
            ..Default::default()
        });
        xafs_test_group.fft()?;

        xafs_test_group.xftr = Some(XrayFFTR {
            window: Some(FTWindow::Hanning),
            rweight: Some(0.0),
            dr: Some(f64::EPSILON),
            rmin: Some(0.0),
            rmax: Some(10.0),
            ..Default::default()
        });
        xafs_test_group.ifft()?;

        let mut xas_group = XASGroup::new();
        xas_group.add_spectrum(xafs_test_group);

        let mut xas_group_file = XASGroupFile::new();

        xas_group_file.name = "test.json".into();
        xas_group_file.data = xas_group;

        xas_group_file.write_json(&save_path)?;
        std::fs::remove_file(&save_path).ok();

        Ok(())
    }

    #[test]
    #[allow(non_snake_case)]
    fn test_xas_json_read() -> Result<(), Box<dyn std::error::Error>> {
        let path = String::from(TOP_DIR) + "/tests/testfiles/Ru_QAS.dat";
        let save_path = unique_testfile_path("json");
        let mut xafs_test_group = io::load_spectrum_QAS_trans(&path).unwrap();

        xafs_test_group.set_background_method(Some(BackgroundMethod::AUTOBK(AUTOBK {
            rbkg: Some(1.4),
            ..Default::default()
        })))?;
        xafs_test_group.calc_background()?;

        xafs_test_group.xftf = Some(XrayFFTF {
            window: Some(FTWindow::Hanning),
            dk: Some(f64::EPSILON),
            kmin: Some(0.0),
            kmax: Some(15.0),
            kweight: Some(2.0),
            ..Default::default()
        });
        xafs_test_group.fft()?;

        xafs_test_group.xftr = Some(XrayFFTR {
            window: Some(FTWindow::Hanning),
            rweight: Some(0.0),
            dr: Some(f64::EPSILON),
            rmin: Some(0.0),
            rmax: Some(10.0),
            ..Default::default()
        });
        xafs_test_group.ifft()?;

        let mut xas_group = XASGroup::new();
        xas_group.add_spectrum(xafs_test_group);

        let mut xas_group_file = XASGroupFile::new();

        xas_group_file.name = "test.json".into();
        xas_group_file.data = xas_group;

        // Write the file first
        xas_group_file.write_json(&save_path)?;

        // Then read it back
        let mut xas_group_read = XASGroupFile::new();
        xas_group_read.read_json(&save_path)?;

        // TODO:: Assertion of the struct is not working at this momment. Float has to be handled
        // properly.
        // assert_eq!(xas_group_read.data, xas_group_file.data);
        std::fs::remove_file(&save_path).ok();

        Ok(())
    }

    #[test]
    #[allow(non_snake_case)]
    fn test_xas_jsongz_write() -> Result<(), Box<dyn std::error::Error>> {
        let path = String::from(TOP_DIR) + "/tests/testfiles/Ru_QAS.dat";
        let save_path = unique_testfile_path("json.gz");
        let mut xafs_test_group = io::load_spectrum_QAS_trans(&path).unwrap();

        xafs_test_group.set_background_method(Some(BackgroundMethod::AUTOBK(AUTOBK {
            rbkg: Some(1.4),
            ..Default::default()
        })))?;
        xafs_test_group.calc_background()?;

        xafs_test_group.xftf = Some(XrayFFTF {
            window: Some(FTWindow::Hanning),
            dk: Some(f64::EPSILON),
            kmin: Some(0.0),
            kmax: Some(15.0),
            kweight: Some(2.0),
            ..Default::default()
        });
        xafs_test_group.fft()?;

        xafs_test_group.xftr = Some(XrayFFTR {
            window: Some(FTWindow::Hanning),
            rweight: Some(0.0),
            dr: Some(f64::EPSILON),
            rmin: Some(0.0),
            rmax: Some(10.0),
            ..Default::default()
        });
        xafs_test_group.ifft()?;

        let mut xas_group = XASGroup::new();
        xas_group.add_spectrum(xafs_test_group);

        let mut xas_group_file = XASGroupFile::new();

        xas_group_file.name = "test.json.gz".into();
        xas_group_file.data = xas_group;

        xas_group_file.write_jsongz(&save_path)?;
        std::fs::remove_file(&save_path).ok();

        Ok(())
    }

    #[test]
    #[allow(non_snake_case)]
    fn test_xas_jsongz_read() -> Result<(), Box<dyn std::error::Error>> {
        let path = String::from(TOP_DIR) + "/tests/testfiles/Ru_QAS.dat";
        let save_path = unique_testfile_path("json.gz");
        let mut xafs_test_group = io::load_spectrum_QAS_trans(&path).unwrap();

        xafs_test_group.set_background_method(Some(BackgroundMethod::AUTOBK(AUTOBK {
            rbkg: Some(1.4),
            ..Default::default()
        })))?;
        xafs_test_group.calc_background()?;

        xafs_test_group.xftf = Some(XrayFFTF {
            window: Some(FTWindow::Hanning),
            dk: Some(f64::EPSILON),
            kmin: Some(0.0),
            kmax: Some(15.0),
            kweight: Some(2.0),
            ..Default::default()
        });
        xafs_test_group.fft()?;

        xafs_test_group.xftr = Some(XrayFFTR {
            window: Some(FTWindow::Hanning),
            rweight: Some(0.0),
            dr: Some(f64::EPSILON),
            rmin: Some(0.0),
            rmax: Some(10.0),
            ..Default::default()
        });
        xafs_test_group.ifft()?;

        let mut xas_group = XASGroup::new();
        xas_group.add_spectrum(xafs_test_group);

        let mut xas_group_file = XASGroupFile::new();

        xas_group_file.name = "test.json.gz".into();
        xas_group_file.data = xas_group;

        // Write the file first
        xas_group_file.write_jsongz(&save_path)?;

        // Then read it back
        let mut xas_group_read = XASGroupFile::new();
        xas_group_read.read_jsongz(&save_path)?;

        // TODO:: Assertion of the struct is not working at this momment. Float has to be handled
        // properly.
        // assert_eq!(xas_group_read.data, xas_group_file.data);
        std::fs::remove_file(&save_path).ok();

        Ok(())
    }
}