use crate::error::{FFTError, FFTResult};
use crate::spectrogram::{spectrogram, spectrogram_normalized};
use crate::window::Window;
use scirs2_core::ndarray::{Array1, Array2, Array3};
use scirs2_core::numeric::NumCast;
use std::f64::consts::PI;
#[allow(dead_code)]
pub fn waterfall_3d<T>(
x: &[T],
fs: Option<f64>,
nperseg: Option<usize>,
noverlap: Option<usize>,
log_scale: Option<bool>,
db_range: Option<f64>,
) -> FFTResult<(Array1<f64>, Array1<f64>, Array3<f64>)>
where
T: NumCast + Copy + std::fmt::Debug,
{
let fs = fs.unwrap_or(1.0);
let nperseg = nperseg.unwrap_or(256);
let noverlap = noverlap.unwrap_or(nperseg / 2);
let log_scale = log_scale.unwrap_or(true);
let db_range = db_range.unwrap_or(60.0);
let (freqs, times, spec_data) = if log_scale {
spectrogram_normalized(x, Some(fs), Some(nperseg), Some(noverlap), Some(db_range))?
} else {
spectrogram(
x,
Some(fs),
Some(Window::Hann),
Some(nperseg),
Some(noverlap),
None,
None,
Some("density"),
Some("magnitude"),
)?
};
let n_times = times.len();
let n_freqs = freqs.len();
let mut waterfall_data = Array3::zeros((n_times, n_freqs, 3));
for t_idx in 0..n_times {
for f_idx in 0..n_freqs {
let amplitude = spec_data[[f_idx, t_idx]];
waterfall_data[[t_idx, f_idx, 0]] = times[t_idx]; waterfall_data[[t_idx, f_idx, 1]] = freqs[f_idx]; waterfall_data[[t_idx, f_idx, 2]] = amplitude; }
}
let times_array = Array1::from_vec(times);
let freqs_array = Array1::from_vec(freqs);
Ok((times_array, freqs_array, waterfall_data))
}
#[allow(dead_code)]
pub fn waterfall_mesh<T>(
x: &[T],
fs: Option<f64>,
nperseg: Option<usize>,
noverlap: Option<usize>,
log_scale: Option<bool>,
db_range: Option<f64>,
) -> FFTResult<(Array2<f64>, Array2<f64>, Array2<f64>)>
where
T: NumCast + Copy + std::fmt::Debug,
{
let fs = fs.unwrap_or(1.0);
let nperseg = nperseg.unwrap_or(256);
let noverlap = noverlap.unwrap_or(nperseg / 2);
let log_scale = log_scale.unwrap_or(true);
let db_range = db_range.unwrap_or(60.0);
let (freqs, times, spec_data) = if log_scale {
spectrogram_normalized(x, Some(fs), Some(nperseg), Some(noverlap), Some(db_range))?
} else {
spectrogram(
x,
Some(fs),
Some(Window::Hann),
Some(nperseg),
Some(noverlap),
None,
None,
Some("density"),
Some("magnitude"),
)?
};
let n_times = times.len();
let n_freqs = freqs.len();
let mut time_mesh = Array2::zeros((n_freqs, n_times));
let mut freq_mesh = Array2::zeros((n_freqs, n_times));
for t_idx in 0..n_times {
for f_idx in 0..n_freqs {
time_mesh[[f_idx, t_idx]] = times[t_idx];
freq_mesh[[f_idx, t_idx]] = freqs[f_idx];
}
}
Ok((time_mesh, freq_mesh, spec_data))
}
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub fn waterfall_lines<T>(
x: &[T],
fs: Option<f64>,
nperseg: Option<usize>,
noverlap: Option<usize>,
n_lines: Option<usize>,
offset: Option<f64>,
log_scale: Option<bool>,
db_range: Option<f64>,
) -> FFTResult<(Array1<f64>, Array1<f64>, Array3<f64>)>
where
T: NumCast + Copy + std::fmt::Debug,
{
let fs = fs.unwrap_or(1.0);
let nperseg = nperseg.unwrap_or(256);
let noverlap = noverlap.unwrap_or(nperseg / 2);
let log_scale = log_scale.unwrap_or(true);
let db_range = db_range.unwrap_or(60.0);
let (freqs, times, spec_data) = if log_scale {
spectrogram_normalized(x, Some(fs), Some(nperseg), Some(noverlap), Some(db_range))?
} else {
spectrogram(
x,
Some(fs),
Some(Window::Hann),
Some(nperseg),
Some(noverlap),
None,
None,
Some("density"),
Some("magnitude"),
)?
};
let n_times = times.len();
let n_lines = n_lines.unwrap_or(20).min(n_times);
let offset = offset.unwrap_or(0.1);
let time_indices: Vec<usize> = if n_lines >= n_times {
(0..n_times).collect()
} else {
(0..n_lines)
.map(|i| i * (n_times - 1) / (n_lines - 1))
.collect()
};
let selected_times: Vec<f64> = time_indices.iter().map(|&idx| times[idx]).collect();
let n_freqs = freqs.len();
let mut line_data = Array3::zeros((n_lines, n_freqs, 2));
for (line_idx, &time_idx) in time_indices.iter().enumerate() {
let line_offset = line_idx as f64 * offset;
for f_idx in 0..n_freqs {
let amplitude = spec_data[[f_idx, time_idx]];
line_data[[line_idx, f_idx, 0]] = freqs[f_idx]; line_data[[line_idx, f_idx, 1]] = amplitude + line_offset; }
}
let times_array = Array1::from_vec(selected_times);
let freqs_array = Array1::from_vec(freqs);
Ok((times_array, freqs_array, line_data))
}
#[allow(dead_code)]
pub fn waterfall_mesh_colored<T>(
x: &[T],
fs: Option<f64>,
nperseg: Option<usize>,
noverlap: Option<usize>,
log_scale: Option<bool>,
db_range: Option<f64>,
) -> FFTResult<(Array2<f64>, Array2<usize>, Array2<f64>)>
where
T: NumCast + Copy + std::fmt::Debug,
{
let fs = fs.unwrap_or(1.0);
let nperseg = nperseg.unwrap_or(256);
let noverlap = noverlap.unwrap_or(nperseg / 2);
let log_scale = log_scale.unwrap_or(true);
let db_range = db_range.unwrap_or(60.0);
let (freqs, times, spec_data) = if log_scale {
spectrogram_normalized(x, Some(fs), Some(nperseg), Some(noverlap), Some(db_range))?
} else {
spectrogram(
x,
Some(fs),
Some(Window::Hann),
Some(nperseg),
Some(noverlap),
None,
None,
Some("density"),
Some("magnitude"),
)?
};
let n_times = times.len();
let n_freqs = freqs.len();
let n_vertices = n_times * n_freqs;
let mut vertices = Array2::zeros((n_vertices, 3));
let mut colors = Array2::zeros((n_vertices, 3));
let mut vertex_idx = 0;
for (t_idx, &t) in times.iter().enumerate() {
for (f_idx, &f) in freqs.iter().enumerate() {
let amplitude = spec_data[[f_idx, t_idx]];
vertices[[vertex_idx, 0]] = t; vertices[[vertex_idx, 1]] = f; vertices[[vertex_idx, 2]] = amplitude;
colors[[vertex_idx, 0]] = amplitude; colors[[vertex_idx, 1]] = amplitude * 0.5; colors[[vertex_idx, 2]] = 1.0 - amplitude;
vertex_idx += 1;
}
}
let n_cells_t = n_times - 1;
let n_cells_f = n_freqs - 1;
let n_faces = 2 * n_cells_t * n_cells_f;
let mut faces = Array2::zeros((n_faces, 3));
let mut face_idx = 0;
for t in 0..n_cells_t {
for f in 0..n_cells_f {
let v00 = t * n_freqs + f; let v01 = t * n_freqs + (f + 1); let v10 = (t + 1) * n_freqs + f; let v11 = (t + 1) * n_freqs + (f + 1);
faces[[face_idx, 0]] = v00;
faces[[face_idx, 1]] = v10;
faces[[face_idx, 2]] = v11;
face_idx += 1;
faces[[face_idx, 0]] = v00;
faces[[face_idx, 1]] = v11;
faces[[face_idx, 2]] = v01;
face_idx += 1;
}
}
Ok((vertices, faces, colors))
}
#[allow(dead_code)]
pub fn apply_colormap(amplitudes: &Array1<f64>, colormap: &str) -> FFTResult<Array2<f64>> {
for & in amplitudes.iter() {
if !(0.0..=1.0).contains(&) {
return Err(FFTError::ValueError(
"Amplitude values must be in range [0, 1]".to_string(),
));
}
}
let n_values = amplitudes.len();
let mut colors = Array2::zeros((n_values, 3));
match colormap {
"jet" => {
for (i, &) in amplitudes.iter().enumerate() {
colors[[i, 0]] = if amp < 0.35 {
0.0
} else if amp < 0.66 {
(amp - 0.35) / 0.31
} else {
1.0
};
colors[[i, 1]] = if amp < 0.125 {
0.0
} else if amp < 0.375 {
(amp - 0.125) / 0.25
} else if amp < 0.875 {
1.0
} else {
(1.0 - amp) / 0.125
};
colors[[i, 2]] = if amp < 0.125 {
(0.5 + amp * 4.0).min(1.0)
} else if amp < 0.375 {
1.0
} else if amp < 0.625 {
(0.625 - amp) / 0.25
} else {
0.0
};
}
}
"viridis" => {
for (i, &) in amplitudes.iter().enumerate() {
colors[[i, 0]] = amp.powf(1.5) * 0.9; colors[[i, 1]] = amp.powf(0.8) * 0.9; colors[[i, 2]] = if amp < 0.5 {
0.4 + (0.5 - amp) * 0.6 } else {
0.4 * (1.0 - amp) };
}
}
"plasma" => {
for (i, &) in amplitudes.iter().enumerate() {
colors[[i, 0]] = 0.05 + amp.powf(0.7) * 0.95; colors[[i, 1]] = amp.powf(2.0) * 0.9; colors[[i, 2]] = if amp < 0.7 {
0.5 - amp * 0.5 } else {
0.15 * (1.0 - amp) / 0.3 };
}
}
"grayscale" => {
for (i, &) in amplitudes.iter().enumerate() {
colors[[i, 0]] = amp; colors[[i, 1]] = amp; colors[[i, 2]] = amp; }
}
"hot" => {
for (i, &) in amplitudes.iter().enumerate() {
colors[[i, 0]] = (amp * 3.0).min(1.0); colors[[i, 1]] = ((amp - 0.33) * 3.0).clamp(0.0, 1.0); colors[[i, 2]] = ((amp - 0.66) * 3.0).clamp(0.0, 1.0); }
}
_ => {
return Err(FFTError::ValueError(format!(
"Unknown colormap: {colormap}. Use 'jet', 'viridis', 'plasma', 'grayscale', or 'hot'."
)));
}
}
Ok(colors)
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_chirp(fs: f64, duration: f64) -> Vec<f64> {
let n_samples = (fs * duration) as usize;
let t: Vec<f64> = (0..n_samples).map(|i| i as f64 / fs).collect();
t.iter()
.map(|&ti| (2.0 * PI * (10.0 + 50.0 * ti) * ti).sin())
.collect()
}
#[test]
fn test_waterfall_3d_dimensions() {
let fs = 1000.0;
let signal = generate_chirp(fs, 1.0);
let (times, freqs, data) = waterfall_3d(
&signal,
Some(fs),
Some(128),
Some(64),
Some(true),
Some(60.0),
)
.expect("Operation failed");
assert_eq!(data.shape()[0], times.len()); assert_eq!(data.shape()[1], freqs.len()); assert_eq!(data.shape()[2], 3);
for t in times.iter() {
assert!(0.0 <= *t && *t <= 1.0); }
for f in freqs.iter() {
assert!(0.0 <= *f && *f <= fs / 2.0); }
for i in 0..data.shape()[0] {
for j in 0..data.shape()[1] {
assert!(0.0 <= data[[i, j, 2]] && data[[i, j, 2]] <= 1.0);
}
}
}
#[test]
fn test_waterfall_mesh_dimensions() {
let fs = 1000.0;
let signal = generate_chirp(fs, 1.0);
let (time_mesh, freq_mesh, amplitudes) = waterfall_mesh(
&signal,
Some(fs),
Some(128),
Some(64),
Some(true),
Some(60.0),
)
.expect("Operation failed");
assert_eq!(time_mesh.shape(), freq_mesh.shape());
assert_eq!(time_mesh.shape(), amplitudes.shape());
assert!(0.0 <= time_mesh.iter().fold(f64::MAX, |a, &b| a.min(b)));
assert!(time_mesh.iter().fold(f64::MIN, |a, &b| a.max(b)) <= 1.0);
assert!(0.0 <= freq_mesh.iter().fold(f64::MAX, |a, &b| a.min(b)));
assert!(freq_mesh.iter().fold(f64::MIN, |a, &b| a.max(b)) <= fs / 2.0);
assert!(0.0 <= amplitudes.iter().fold(f64::MAX, |a, &b| a.min(b)));
assert!(amplitudes.iter().fold(f64::MIN, |a, &b| a.max(b)) <= 1.0);
}
#[test]
fn test_waterfall_lines() {
let fs = 1000.0;
let signal = generate_chirp(fs, 1.0);
let n_lines = 10;
let (times, freqs, lines) = waterfall_lines(
&signal,
Some(fs),
Some(128),
Some(64),
Some(n_lines),
Some(0.2),
Some(true),
Some(60.0),
)
.expect("Operation failed");
assert_eq!(times.len(), n_lines);
assert_eq!(lines.shape()[0], n_lines);
assert_eq!(lines.shape()[1], freqs.len());
assert_eq!(lines.shape()[2], 2);
for i in 1..times.len() {
assert!(times[i] > times[i - 1]);
}
for i in 0..n_lines {
for j in 0..freqs.len() {
assert_eq!(lines[[i, j, 0]], freqs[j]);
}
}
let offset = 0.2;
for i in 1..n_lines {
let prev_line_max = (0..freqs.len())
.map(|j| lines[[i - 1, j, 1]])
.fold(f64::MIN, f64::max);
let curr_line_min = (0..freqs.len())
.map(|j| lines[[i, j, 1]])
.fold(f64::MAX, f64::min);
assert!(curr_line_min > prev_line_max - 1.0 + offset);
}
}
#[test]
fn test_waterfall_mesh_colored() {
let fs = 1000.0;
let signal = generate_chirp(fs, 1.0);
let (vertices, faces, colors) = waterfall_mesh_colored(
&signal,
Some(fs),
Some(128),
Some(64),
Some(true),
Some(60.0),
)
.expect("Operation failed");
assert_eq!(vertices.shape()[1], 3); assert_eq!(faces.shape()[1], 3); assert_eq!(colors.shape()[1], 3); assert_eq!(vertices.shape()[0], colors.shape()[0]);
let max_vertex_idx = vertices.shape()[0] - 1;
for face in faces.outer_iter() {
for &idx in face {
assert!(idx <= max_vertex_idx);
}
}
for color in colors.outer_iter() {
for &component in color {
assert!((0.0..=1.0).contains(&component));
}
}
}
#[test]
fn test_apply_colormap() {
let amplitudes = Array1::from_vec(vec![0.0, 0.25, 0.5, 0.75, 1.0]);
let colormaps = ["jet", "viridis", "plasma", "grayscale", "hot"];
for &cmap in &colormaps {
let colors = apply_colormap(&litudes, cmap).expect("Operation failed");
assert_eq!(colors.shape(), &[5, 3]);
let epsilon = 1e-10;
for row in colors.outer_iter() {
for &component in row {
assert!(
-epsilon <= component && component <= 1.0 + epsilon,
"Color component out of range: {component}"
);
}
}
if cmap == "grayscale" {
for i in 0..amplitudes.len() {
assert_eq!(colors[[i, 0]], colors[[i, 1]]);
assert_eq!(colors[[i, 1]], colors[[i, 2]]);
assert_eq!(colors[[i, 0]], amplitudes[i]);
}
}
}
let result = apply_colormap(&litudes, "invalid");
assert!(result.is_err());
}
}