use crate::signal::{
FreqSignal,
data::{ComplexData, RealData},
math::{gain_to_db, wrap_phase_2pi},
};
const NEG_INF_DB: f64 = -100.0;
impl ComplexData {
pub fn to_magnitude(&self) -> RealData {
RealData::from_axis(
self.x_axis().clone(),
self.y_data().mapv(|value| value.norm()),
)
.expect("x/y data originated from ComplexData")
}
pub fn to_magnitude_db(&self) -> RealData {
RealData::from_axis(
self.x_axis().clone(),
self.y_data()
.mapv(|value| gain_to_db(value.norm()).max(NEG_INF_DB)),
)
.expect("x/y data originated from ComplexData")
}
pub fn to_phase(&self) -> RealData {
RealData::from_axis(
self.x_axis().clone(),
self.y_data().mapv(|value| value.arg()),
)
.expect("x/y data originated from ComplexData")
}
pub fn to_phase_unwrapped(&self) -> RealData {
let mut phase = self.y_data().mapv(|value| value.arg());
for mut channel in phase.outer_iter_mut() {
unwrap_phase_in_place(channel.as_slice_mut().expect("channel view is contiguous"));
}
RealData::from_axis(self.x_axis().clone(), phase)
.expect("x/y data originated from ComplexData")
}
pub fn to_phase_2pi(&self) -> RealData {
RealData::from_axis(
self.x_axis().clone(),
self.y_data().mapv(|value| wrap_phase_2pi(value.arg())),
)
.expect("x/y data originated from ComplexData")
}
pub fn to_phase_degrees(&self) -> RealData {
RealData::from_axis(
self.x_axis().clone(),
self.y_data().mapv(|value| value.arg().to_degrees()),
)
.expect("x/y data originated from ComplexData")
}
pub fn to_phase_degrees_unwrapped(&self) -> RealData {
let mut phase = self.y_data().mapv(|value| value.arg());
for mut channel in phase.outer_iter_mut() {
unwrap_phase_in_place(channel.as_slice_mut().expect("channel view is contiguous"));
}
RealData::from_axis(self.x_axis().clone(), phase.mapv(f64::to_degrees))
.expect("x/y data originated from ComplexData")
}
}
impl FreqSignal {
pub fn to_magnitude(&self) -> RealData {
self.data().to_magnitude()
}
pub fn to_magnitude_db(&self) -> RealData {
self.data().to_magnitude_db()
}
pub fn to_phase(&self) -> RealData {
self.data().to_phase()
}
pub fn to_phase_unwrapped(&self) -> RealData {
self.data().to_phase_unwrapped()
}
pub fn to_phase_2pi(&self) -> RealData {
self.data().to_phase_2pi()
}
pub fn to_phase_degrees(&self) -> RealData {
self.data().to_phase_degrees()
}
pub fn to_phase_degrees_unwrapped(&self) -> RealData {
self.data().to_phase_degrees_unwrapped()
}
}
fn unwrap_phase_in_place(phase: &mut [f64]) {
if phase.len() < 2 {
return;
}
let two_pi = 2.0 * std::f64::consts::PI;
let mut offset = 0.0;
let mut previous = phase[0];
for current in phase.iter_mut().skip(1) {
let current_value = *current;
let delta = current_value - previous;
if delta > std::f64::consts::PI {
offset -= two_pi;
} else if delta < -std::f64::consts::PI {
offset += two_pi;
}
*current = current_value + offset;
previous = current_value;
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
use approx::assert_abs_diff_eq;
use ndarray::{arr1, arr2};
use num::complex::Complex64;
use crate::signal::data::ComplexData;
#[test]
fn complex_data_conversions() {
let data = ComplexData::new(
arr1(&[10.0, 20.0, 30.0]),
arr2(&[[
Complex64::new(3.0, 4.0),
Complex64::new(0.0, 1.0),
Complex64::new(-1.0, 0.0),
]]),
)
.unwrap();
let magnitude = data.to_magnitude();
assert_abs_diff_eq!(magnitude.channel(0)[0], 5.0, epsilon = 1e-12);
assert_abs_diff_eq!(magnitude.channel(0)[1], 1.0, epsilon = 1e-12);
let magnitude_db = data.to_magnitude_db();
assert_abs_diff_eq!(
magnitude_db.channel(0)[0],
20.0 * 5.0_f64.log10(),
epsilon = 1e-12
);
assert_abs_diff_eq!(magnitude_db.channel(0)[1], 0.0, epsilon = 1e-12);
let phase = data.to_phase();
assert_abs_diff_eq!(phase.channel(0)[0], (4.0_f64).atan2(3.0), epsilon = 1e-12);
assert_abs_diff_eq!(phase.channel(0)[1], FRAC_PI_2, epsilon = 1e-12);
assert_abs_diff_eq!(phase.channel(0)[2], PI, epsilon = 1e-12);
}
#[test]
fn complex_data_phase_quadrants() {
let data = ComplexData::new(
arr1(&[0.0, 1.0, 2.0, 3.0]),
arr2(&[[
Complex64::new(1.0, 0.0),
Complex64::new(0.0, 1.0),
Complex64::new(1.0, 1.0),
Complex64::new(1.0, -1.0),
]]),
)
.unwrap();
let phase = data.to_phase();
assert_abs_diff_eq!(phase.channel(0)[0], 0.0, epsilon = 1e-12);
assert_abs_diff_eq!(phase.channel(0)[1], FRAC_PI_2, epsilon = 1e-12);
assert_abs_diff_eq!(phase.channel(0)[2], FRAC_PI_4, epsilon = 1e-12);
assert_abs_diff_eq!(phase.channel(0)[3], -FRAC_PI_4, epsilon = 1e-12);
}
#[test]
fn complex_data_phase_variants() {
let data = ComplexData::new(
arr1(&[0.0, 1.0, 2.0]),
arr2(&[[
Complex64::from_polar(1.0, 3.0 * PI / 4.0),
Complex64::from_polar(1.0, -3.0 * PI / 4.0),
Complex64::from_polar(1.0, -PI / 2.0),
]]),
)
.unwrap();
let unwrapped = data.to_phase_unwrapped();
let wrapped_2pi = data.to_phase_2pi();
let degrees = data.to_phase_degrees();
let degrees_unwrapped = data.to_phase_degrees_unwrapped();
assert_abs_diff_eq!(unwrapped.channel(0)[0], 3.0 * PI / 4.0, epsilon = 1e-12);
assert_abs_diff_eq!(unwrapped.channel(0)[1], 5.0 * PI / 4.0, epsilon = 1e-12);
assert_abs_diff_eq!(unwrapped.channel(0)[2], 3.0 * PI / 2.0, epsilon = 1e-12);
assert_abs_diff_eq!(wrapped_2pi.channel(0)[1], 5.0 * PI / 4.0, epsilon = 1e-12);
assert_abs_diff_eq!(wrapped_2pi.channel(0)[2], 3.0 * PI / 2.0, epsilon = 1e-12);
assert_abs_diff_eq!(degrees.channel(0)[0], 135.0, epsilon = 1e-12);
assert_abs_diff_eq!(degrees.channel(0)[1], -135.0, epsilon = 1e-12);
assert_abs_diff_eq!(degrees_unwrapped.channel(0)[1], 225.0, epsilon = 1e-12);
assert_abs_diff_eq!(degrees_unwrapped.channel(0)[2], 270.0, epsilon = 1e-12);
}
}