Skip to content

Commit

Permalink
Implement Serialize and Deserialize for types in the deconvolution mo…
Browse files Browse the repository at this point in the history
…dule

- Deconvolution, Lorentzian and Settings enums
- Add tests for the serialization/deserialization
  • Loading branch information
SombkeMaximilian committed Feb 10, 2025
1 parent e514be4 commit fe920c4
Show file tree
Hide file tree
Showing 10 changed files with 403 additions and 7 deletions.
5 changes: 5 additions & 0 deletions metabodecon/src/deconvolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ pub use deconvolution::Deconvolution;
mod lorentzian;
pub use lorentzian::Lorentzian;

#[cfg(feature = "serde")]
mod serialized_representations;
#[cfg(feature = "serde")]
pub(crate) use serialized_representations::{SerializedDeconvolution, SerializedLorentzian};

mod fitting;
pub use fitting::FittingSettings;

Expand Down
71 changes: 71 additions & 0 deletions metabodecon/src/deconvolution/deconvolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ use crate::deconvolution::peak_selection::SelectionSettings;
use crate::deconvolution::smoothing::SmoothingSettings;
use std::sync::Arc;

#[cfg(feature = "serde")]
use crate::deconvolution::SerializedDeconvolution;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Data structure representing the result of a deconvolution.
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(into = "SerializedDeconvolution", try_from = "SerializedDeconvolution")
)]
pub struct Deconvolution {
/// The deconvoluted signals.
lorentzians: Arc<[Lorentzian]>,
Expand All @@ -19,6 +29,12 @@ pub struct Deconvolution {
mse: f64,
}

impl AsRef<Deconvolution> for Deconvolution {
fn as_ref(&self) -> &Deconvolution {
self
}
}

impl Deconvolution {
/// Constructs a new `Deconvolution`.
pub fn new(
Expand Down Expand Up @@ -68,11 +84,66 @@ impl Deconvolution {
#[cfg(test)]
mod tests {
use super::*;
use crate::deconvolution::ScoringMethod;
use crate::{assert_send, assert_sync};
use float_cmp::assert_approx_eq;

#[test]
fn thread_safety() {
assert_send!(Deconvolution);
assert_sync!(Deconvolution);
}

#[cfg(feature = "serde")]
#[test]
fn serialization_round_trip() {
let lorentzians = vec![
Lorentzian::new(5.5, 0.25, 3.0),
Lorentzian::new(7.0, 0.16, 5.0),
Lorentzian::new(5.5, 0.25, 7.0),
];
let deconvolution = Deconvolution::new(
lorentzians.clone(),
SmoothingSettings::default(),
SelectionSettings::default(),
FittingSettings::default(),
0.5,
);
let serialized = serde_json::to_string(&deconvolution).unwrap();
let deserialized = serde_json::from_str::<Deconvolution>(&serialized).unwrap();
deconvolution
.lorentzians
.iter()
.zip(deserialized.lorentzians())
.for_each(|(init, rec)| {
assert_approx_eq!(f64, init.sfhw(), rec.sfhw());
assert_approx_eq!(f64, init.hw2(), rec.hw2());
assert_approx_eq!(f64, init.maxp(), rec.maxp());
});
match deserialized.smoothing_settings() {
SmoothingSettings::MovingAverage {
iterations,
window_size,
} => {
assert_eq!(iterations, 2);
assert_eq!(window_size, 5);
}
};
match deserialized.selection_settings() {
SelectionSettings::NoiseScoreFilter {
scoring_method,
threshold,
} => {
match scoring_method {
ScoringMethod::MinimumSum => {}
}
assert_approx_eq!(f64, threshold, 6.4);
}
};
match deserialized.fitting_settings() {
FittingSettings::Analytical { iterations } => {
assert_eq!(iterations, 10);
}
};
}
}
8 changes: 8 additions & 0 deletions metabodecon/src/deconvolution/fitting/fitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use crate::deconvolution::lorentzian::Lorentzian;
use crate::deconvolution::peak_selection::Peak;
use crate::spectrum::Spectrum;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Trait interface for fitting algorithms.
pub(crate) trait Fitter {
/// Fits Lorentzian functions to a spectrum using the given peaks.
Expand All @@ -18,6 +21,11 @@ pub(crate) trait Fitter {
/// Fitting methods.
#[non_exhaustive]
#[derive(Copy, Clone, Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "method", rename_all_fields = "camelCase")
)]
pub enum FittingSettings {
/// Fitting by solving a system of linear equations analytically.
Analytical {
Expand Down
54 changes: 47 additions & 7 deletions metabodecon/src/deconvolution/lorentzian.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#[cfg(feature = "parallel")]
use rayon::prelude::*;

#[cfg(feature = "serde")]
use crate::deconvolution::SerializedLorentzian;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Data structure that represents a [Lorentzian function].
///
/// # Definition
Expand Down Expand Up @@ -42,20 +47,30 @@ use rayon::prelude::*;
///
/// [Lorentzian function]: https://en.wikipedia.org/wiki/Cauchy_distribution
///
/// # Negative Scale Factors and Half-Widths
/// # Negative Transformed Parameters
///
/// In order for `Lorentzian` to represent a peak shape, the scale factor `sf`
/// and half-width `hw` must be positive. This is not an invariant, though, as
/// enforcing it would require additional checks and overhead. Any `Lorentzian`
/// instances generated by the library will have positive scale factors and
/// half-widths. For manually created instances, it is the user's responsibility
/// to ensure that the parameters are valid within the context of the problem.
/// For `Lorentzian` to represent a valid peak shape, the transformed parameters
/// `sfhw` and `hw2` must be positive. This is not strictly enforced to avoid
/// unnecessary overhead. Instances created by the library are guaranteed to
/// have valid values, but if you construct a `Lorentzian` manually, you are
/// responsible for ensuring the parameters are meaningful in your context.
///
/// # Thread Safety
///
/// The `Lorentzian` type is both [`Send`] and [`Sync`], allowing safe sharing
/// and access across threads.
///
/// # Serialization with `serde`
///
/// When the `serde` feature is enabled, `Lorentzian` can be serialized and
/// deserialized using `serde`. During serialization, the transformation is
/// reversed, and the original parameters `(sf, hw, maxp)` are stored.
///
/// **Important:** Since `hw2` must be non-negative, an invalid value can lead
/// to corruption when serializing, as taking the square root is needed to
/// recover `hw`. As mentioned above, the library ensures valid values, but
/// manual construction requires caution.
///
/// # Example
///
/// The following demonstrates basic usage of the `Lorentzian` struct. It is,
Expand Down Expand Up @@ -111,6 +126,11 @@ use rayon::prelude::*;
/// let sup2 = Lorentzian::par_superposition_vec(&chemical_shifts, &triplet);
/// ```
#[derive(Copy, Clone, Debug, Default)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(into = "SerializedLorentzian", try_from = "SerializedLorentzian")
)]
pub struct Lorentzian {
/// Scale factor multiplied by the half-width (`sfhw = sf * hw`).
sfhw: f64,
Expand Down Expand Up @@ -820,4 +840,24 @@ mod tests {
});
}
}

#[cfg(feature = "serde")]
#[test]
fn serialization_round_trip() {
let lorentzians = vec![
Lorentzian::new(5.5, 0.25, 3.0),
Lorentzian::new(7.0, 0.16, 5.0),
Lorentzian::new(5.5, 0.25, 7.0),
];
let serialized = serde_json::to_string(&lorentzians).unwrap();
let deserialized = serde_json::from_str::<Vec<Lorentzian>>(&serialized).unwrap();
lorentzians
.iter()
.zip(deserialized.iter())
.for_each(|(init, rec)| {
assert_approx_eq!(f64, init.sfhw(), rec.sfhw());
assert_approx_eq!(f64, init.hw2(), rec.hw2());
assert_approx_eq!(f64, init.maxp(), rec.maxp());
});
}
}
8 changes: 8 additions & 0 deletions metabodecon/src/deconvolution/peak_selection/scorer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ use crate::Result;
use crate::deconvolution::Settings;
use crate::deconvolution::peak_selection::Peak;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Trait interface for peak scoring methods.
pub(crate) trait Scorer {
/// Scores the given peak.
Expand All @@ -11,6 +14,11 @@ pub(crate) trait Scorer {
/// Scoring methods for the peaks.
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Default)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "method", rename_all_fields = "camelCase")
)]
pub enum ScoringMethod {
/// Minimum Sum of the absolute second derivative.
///
Expand Down
8 changes: 8 additions & 0 deletions metabodecon/src/deconvolution/peak_selection/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use crate::deconvolution::Settings;
use crate::deconvolution::error::{Error, Kind};
use crate::deconvolution::peak_selection::{Peak, ScoringMethod};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Trait interface for peak selection algorithms.
pub(crate) trait Selector {
/// Detects peaks in a spectrum and returns the ones that pass a filter.
Expand All @@ -17,6 +20,11 @@ pub(crate) trait Selector {
/// Peak selection methods.
#[non_exhaustive]
#[derive(Copy, Clone, Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(tag = "method", rename_all_fields = "camelCase")
)]
pub enum SelectionSettings {
/// Filter based on the score of peaks found in the signal free region.
///
Expand Down
5 changes: 5 additions & 0 deletions metabodecon/src/deconvolution/serialized_representations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod serialized_deconvolution;
pub(crate) use serialized_deconvolution::SerializedDeconvolution;

mod serialized_lorentzian;
pub(crate) use serialized_lorentzian::SerializedLorentzian;
Loading

0 comments on commit fe920c4

Please sign in to comment.