# Copyright (c) 2023 David Bruneau
# SPDX-License-Identifier: GPL-3.0-or-later
"""
This script creates spectrograms, power spectral density and chromagrams from formatted matrices (.npz files)"
"""
import logging
from pathlib import Path
from typing import Union, Optional
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
from vasp.automatedPostprocessing.postprocessing_h5py import spectrograms as spec
from vasp.automatedPostprocessing.postprocessing_common import read_parameters_from_file
[docs]
def create_spectrogram_composite(case_name: str, quantity: str, df: pd.DataFrame, start_t: float, end_t: float,
num_windows_per_sec: float, overlap_frac: float, window: str, lowcut: float,
min_color: float, max_color: float, image_folder: Union[str, Path],
flow_rate_file: Optional[Union[str, Path]] = None,
amplitude_file: Optional[Union[str, Path]] = None, power_scaled: bool = False,
ylim: Optional[float] = None) -> None:
"""
Create a composite spectrogram figure.
Args:
case_name (str): Path to simulation results.
quantity (str): Quantity to postprocess.
df (pd.DataFrame): Input dataframe.
start_t (float): Start time for analysis.
end_t (float): End time for analysis.
num_windows_per_sec (float): Number of windows per second.
overlap_frac (float): Overlap fraction for windows.
window (str): Type of window for spectrogram.
lowcut (float): Lowcut frequency for high-pass filter.
min_color (float): Minimum value for the color range.
max_color (float): Maximum value for the color range.
image_folder (str or Path): Folder to save the images.
flow_rate_file (str or Path, optional): File containing flow rate data.
amplitude_file (str or Path, optional): File containing amplitude data.
power_scaled (bool, optional): Whether to scale the power.
ylim (float, optional): Y-axis limit for the plot.
"""
# Calculate number of windows (you can adjust this equation to fit your temporal/frequency resolution needs)
num_windows = np.round(num_windows_per_sec * (end_t - start_t)) + 3
# Get sampling constants
logging.info("--- Getting sampling constants\n")
T, _, fs = spec.get_sampling_constants(df, start_t, end_t)
# High-pass filter dataframe for spectrogram
logging.info("--- Filtering time data")
df_filtered = spec.filter_time_data(df, fs, lowcut=lowcut, highcut=15000.0, order=6, btype='highpass')
# PSD calculation
logging.info("\n--- Calculating power spectral density of filtered dataframe")
Pxx_array, freq_array = spec.get_psd(df_filtered, fs)
# Plot PSD
plt.plot(freq_array, Pxx_array)
plt.xlabel('Freq. (Hz)')
plt.ylabel('input units^2/Hz')
# Set ylim if provided
if ylim is not None:
plt.xlim([0, ylim])
psd_filename = f"{quantity}_psd_{case_name}"
path_to_psd_figure = Path(image_folder) / (psd_filename + '.png')
# Save the figure
logging.info(f"--- Saving PSD figure to {path_to_psd_figure}\n")
plt.savefig(path_to_psd_figure)
# Create composite figure
if amplitude_file and flow_rate_file:
fig1, axes = plt.subplots(5, sharex=True, gridspec_kw={'height_ratios': [1, 3, 1, 1, 1]})
assert isinstance(axes, np.ndarray) and axes.shape == (5,)
ax1, ax2, ax3, ax4, ax5 = axes
elif flow_rate_file:
fig1, axes = plt.subplots(4, sharex=True, gridspec_kw={'height_ratios': [1, 3, 1, 1]})
assert isinstance(axes, np.ndarray) and axes.shape == (4,)
ax1, ax2, ax3, ax4 = axes
elif amplitude_file:
fig1, axes = plt.subplots(4, sharex=True, gridspec_kw={'height_ratios': [3, 1, 1, 1]})
assert isinstance(axes, np.ndarray) and axes.shape == (4,)
ax2, ax3, ax4, ax5 = axes
else:
fig1, axes = plt.subplots(3, sharex=True, gridspec_kw={'height_ratios': [3, 1, 1]})
assert isinstance(axes, np.ndarray) and axes.shape == (3,)
ax2, ax3, ax4 = axes
fig1.set_size_inches(7.5, 9)
logging.info("--- Computing average spectrogram")
# Specs with Reyynolds number
bins, freqs, Pxx, max_val, min_val, lower_thresh = \
spec.compute_average_spectrogram(df_filtered, fs, num_windows, overlap_frac, window, start_t, end_t, min_color,
scaling="spectrum", filter_data=False, thresh_method="old")
bins = bins + start_t # Need to shift bins so that spectrogram timing is correct
spec.plot_spectrogram(fig1, ax2, bins, freqs, Pxx, ylim, color_range=[min_color, max_color])
# Chromagram ------------------------------------------------------------
n_fft = spec.shift_bit_length(int(df.shape[1] / num_windows)) * 2
n_chroma = 24
logging.info("\n--- Recomputing average spectrogram without filter")
# Recalculate spectrogram without filtering the data
bins_raw, freqs_raw, Pxx_raw, max_val_raw, min_val_raw, lower_thresh_raw = \
spec.compute_average_spectrogram(df, fs, num_windows, overlap_frac, window, start_t, end_t, min_color,
scaling="spectrum", filter_data=False, thresh_method="old")
bins_raw = bins_raw + start_t # Need to shift bins so that spectrogram timing is correct
# Reverse the log of the data
Pxx_raw = np.exp(Pxx_raw)
# Calculate chromagram
# Normalize so that all chroma in column sum to 1 (other option is "max", which sets the max value
# in each column to 1)
norm = "sum"
chroma = spec.chromagram_from_spectrogram(Pxx_raw, fs, n_fft, n_chroma=n_chroma, norm=norm)
if power_scaled:
chroma_power = chroma * (Pxx.max(axis=0) - min_color)
spec.plot_chromagram(fig1, ax3, bins_raw, chroma_power)
else:
spec.plot_chromagram(fig1, ax3, bins_raw, chroma)
# Hack to make all the x axes of the subplots align
divider2 = make_axes_locatable(ax4)
cax2 = divider2.append_axes("right", size="5%", pad=0.9)
cax2.remove()
# Calculate SBI
chroma_entropy = spec.calc_chroma_entropy(chroma, n_chroma)
# Plot SBI
if power_scaled:
chroma_entropy_power = chroma_entropy * (Pxx.max(axis=0) - min_color)
ax4.plot(bins, chroma_entropy_power)
else:
ax4.plot(bins, chroma_entropy)
ax4.set_ylabel('SBI')
# Plot Flow Rate or inlet velocity from input file
if flow_rate_file:
flow_rates = np.loadtxt(flow_rate_file)
flow_rates = flow_rates[np.where((flow_rates[:, 0] > start_t) & (flow_rates[:, 0] < end_t))]
ax1.plot(flow_rates[:, 0], flow_rates[:, 1])
ax1.set_ylabel('Flow Rate (normalized)')
# Hack to make all the x axes of the subplots align
divider = make_axes_locatable(ax1)
cax = divider.append_axes("right", size="5%", pad=0.9)
cax.remove()
if amplitude_file:
# Hack to make all the x axes of the subplots align
divider = make_axes_locatable(ax5)
cax = divider.append_axes("right", size="5%", pad=0.9)
cax.remove()
output_amplitudes = np.genfromtxt(amplitude_file, delimiter=',')
output_amplitudes = output_amplitudes[output_amplitudes[:, 0] >= start_t]
output_amplitudes = output_amplitudes[output_amplitudes[:, 0] <= end_t]
ax5.plot(output_amplitudes[:, 0], output_amplitudes[:, 10], label=case_name)
ax5.set_ylabel("Amplitude")
ax5.set_xlabel('Time [s]')
else:
ax4.set_xlabel('Time [s]')
# Name composite figure and save
composite_figure_name = f"{quantity}_{case_name}_{num_windows}_windows_thresh{min_color}_composite_figure"
if power_scaled:
composite_figure_name += "_power_scaled"
composite_figure_path = Path(image_folder) / (composite_figure_name + '.png')
logging.info(f"\n--- Saving composite figure to {composite_figure_path}\n")
fig1.savefig(composite_figure_path)
# create separate spectrogram figure
fig2, ax2_1 = plt.subplots()
fig2.set_size_inches(7.5, 5)
title = f"Pxx max = {max_val:.2e}, Pxx min = {min_val:.2e}, threshold Pxx = {lower_thresh}"
spec.plot_spectrogram(fig2, ax2_1, bins, freqs, Pxx, ylim, title=title,
x_label="Time (s)", color_range=[min_color, max_color])
# Save data to files (spectrogram)
spec_filename = f"{quantity}_{case_name}_{num_windows}_windows_thresh{min_color}_spectrogram"
path_to_spec = Path(image_folder) / (spec_filename + '.png')
logging.info(f"--- Saving spectrogram figure to {path_to_spec}\n")
fig2.savefig(path_to_spec)
output_csv_path = path_to_spec.with_suffix('.csv')
data_csv = np.append(freqs[np.newaxis].T, Pxx, axis=1)
bins_txt = np.array2string(bins, max_line_width=10000, precision=2, separator=',').replace("[", "").replace("]", "")
np.savetxt(output_csv_path, data_csv, header=bins_txt, delimiter=",")
# Save data to files (chromagram)
chroma_filename = f"{quantity}_{case_name}_{num_windows}_windows_chromagram"
path_to_chroma = Path(image_folder) / (chroma_filename + '.png')
chroma_y = np.linspace(0, 1, chroma.shape[0])
output_csv_path = path_to_chroma.with_suffix('.csv')
data_csv = np.append(chroma_y[np.newaxis].T, chroma, axis=1)
bins_txt = \
np.array2string(bins_raw, max_line_width=10000, precision=2, separator=',').replace("[", "").replace("]", "")
np.savetxt(output_csv_path, data_csv, header=bins_txt, delimiter=",")
# Save data to files (SBI)
sbi_filename = f"{quantity}_{case_name}_{num_windows}_windows_SBI"
path_to_sbi = Path(image_folder) / (sbi_filename + '.png')
csv_output_path = path_to_sbi.with_suffix('.csv')
data_csv = np.array([bins, chroma_entropy]).T
np.savetxt(csv_output_path, data_csv, header="t (s), SBI", delimiter=",")
[docs]
def main():
# Load in case-specific parameters
args = spec.read_command_line_spec()
# Create logger and set log level
logging.basicConfig(level=args.log_level, format="%(message)s")
# Load parameters from default_parameters.json
parameters = read_parameters_from_file(args.folder)
# Extract parameters
fsi_region = parameters["fsi_region"]
fluid_domain_id = parameters["dx_f_id"]
solid_domain_id = parameters["dx_s_id"]
end_time = args.end_time if args.end_time is not None else parameters["T"]
save_deg = args.save_deg if args.save_deg is not None else (1 if args.quantity == 'p' else parameters["save_deg"])
# Create or read in spectrogram dataframe
quantity, df, case_name, image_folder, visualization_hi_pass_folder = \
spec.read_spectrogram_data(args.folder, args.mesh_path, save_deg, args.stride, args.start_time,
end_time, args.n_samples, args.sampling_region, args.fluid_sampling_domain_id,
args.solid_sampling_domain_id, fsi_region, args.quantity, args.interface_only,
args.component, args.point_ids, fluid_domain_id, solid_domain_id,
sampling_method=args.sampling_method)
# Should these files be used?
# amplitude_file = Path(visualization_hi_pass_folder) / args.amplitude_file_name
# flow_rate_file = Path(args.folder) / args.flow_rate_file_name
# Create spectrograms
create_spectrogram_composite(case_name, quantity, df, args.start_time, end_time, args.num_windows_per_sec,
args.overlap_frac, args.window, args.lowcut, args.min_color, args.max_color,
image_folder, flow_rate_file=None, amplitude_file=None, power_scaled=False,
ylim=args.ylim)
if args.sampling_method == "PointList":
spec.sonify_point(case_name, quantity, df, args.start_time, end_time, args.overlap_frac, args.lowcut,
image_folder)
if __name__ == '__main__':
main()