gamlss_transform/transforms/
asinh_scale.rs1use crate::{TargetTransform, TransformError, median_sorted, validate_non_empty_finite};
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub struct AsinhScale;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct AsinhScaleState {
14 pub scale: f64,
16}
17
18impl TargetTransform for AsinhScale {
19 type State = AsinhScaleState;
20
21 fn fit(y: &[f64]) -> Result<Self::State, TransformError> {
22 validate_non_empty_finite(y)?;
23
24 let mut abs_values: Vec<_> = y.iter().map(|value| value.abs()).collect();
25 abs_values.sort_by(f64::total_cmp);
26 let scale = median_sorted(&abs_values)
27 .filter(|value| value.is_finite() && *value > 0.0)
28 .unwrap_or(1.0);
29
30 Ok(AsinhScaleState { scale })
31 }
32
33 fn transform(state: &Self::State, y: f64) -> f64 {
34 (y / state.scale).asinh()
35 }
36
37 fn inverse(state: &Self::State, value: f64) -> f64 {
38 value.sinh() * state.scale
39 }
40}
41
42#[cfg(test)]
43mod tests {
44 use approx::assert_relative_eq;
45
46 use crate::{AsinhScale, TargetTransform};
47
48 #[test]
49 fn round_trips_signed_values() {
50 let y = [-100.0, -1.0, 0.0, 2.0, 50.0];
51 let (state, transformed) = AsinhScale::fit_transform(&y).unwrap();
52 let restored = AsinhScale::inverse_slice(&state, &transformed).unwrap();
53
54 assert!(state.scale > 0.0);
55 for (actual, expected) in restored.iter().zip(y) {
56 assert_relative_eq!(*actual, expected, epsilon = 1.0e-10);
57 }
58 }
59
60 #[test]
61 fn uses_unit_scale_for_all_zero_targets() {
62 let state = AsinhScale::fit(&[0.0, 0.0]).unwrap();
63
64 assert_relative_eq!(state.scale, 1.0);
65 assert_relative_eq!(AsinhScale::transform(&state, 0.0), 0.0);
66 }
67}