Files
flares/src/analysis/exporttocsv.py
T

266 lines
13 KiB
Python

"""
Filename: exporttocsv.py
Description: Logic for the Export To CSV analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
import os
from typing import Any
# External library imports
from mne import Epochs
from pandas import DataFrame
from mne.io.base import BaseRaw
from PySide6.QtWidgets import QFileDialog, QMessageBox
from flares import sparks_csv_export
from src.shared.flaresbasewidget import CSVUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
def __init__(
self,
haemo_dict: dict[str, BaseRaw],
epochs_dict: dict[str, Epochs],
cha_dict: dict[str, DataFrame],
df_ind_dict: dict[str, DataFrame],
design_matrix_dict: dict[str, DataFrame],
contrast_results_dict: dict[str, dict[str, Any]],
group_dict: dict[str, str],
config_dict: dict[str, dict[str, Any]],
fir_feature_dict: dict[str, dict[str, Any]],
qc_dict: dict[str, dict[str, Any]],
) -> None:
super().__init__("ExportToCSV")
self.setWindowTitle(f"Export To CSV Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.epochs_dict = epochs_dict
self.cha_dict = cha_dict
self.df_ind_dict = df_ind_dict
self.design_matrix = design_matrix_dict
self.contrast_results_dict = contrast_results_dict
self.group_dict = group_dict
self.config_dict = config_dict
self.fir_feature_dict = fir_feature_dict
self.qc_dict = qc_dict
self.setup_csv_ui(["0 (Export Data to CSV)", "1 (CSV for SPARKS)", "2 (Export Configuration to CSV)", "3 (Paragraph of Configuration)", "4 (Export FIR Waveform Features to CSV)",
"5 (Export Quality Control Metrics to CSV)", "6 (Export Master Consolidated Matrix [All Participants into 1 CSV])"
])
def process_request(self):
selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths: list[str] = []
for display_name in selected_display_names:
for fp, short_label in self.participant_map.items():
expected_display = f"{short_label} ({os.path.basename(fp)})"
if display_name == expected_display:
selected_file_paths.append(fp)
break
selected_indexes = [
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
if not selected_file_paths or not selected_indexes:
QMessageBox.warning(self, "Selection Missing", "Please select at least one participant and one export type.")
return
output_dir = QFileDialog.getExistingDirectory(self, "Select Output Folder for CSV Exports")
if not output_dir:
print("Export cancelled: No folder selected.")
return
success_count = 0
# Pass the necessary arguments to each method
for file_path in selected_file_paths:
base_filename = os.path.splitext(os.path.basename(file_path))[0]
haemo_obj = self.haemo_dict.get(file_path)
if haemo_obj is None:
continue
cha = self.cha_dict.get(file_path)
for idx in selected_indexes:
try:
if idx == 0:
save_path = os.path.join(output_dir, f"{base_filename}_exported.csv")
if cha is not None:
cha.to_csv(save_path)
success_count += 1
elif idx == 1:
# SPARKS Export
save_path = os.path.join(output_dir, f"{base_filename}_sparks.csv")
sparks_csv_export(haemo_obj, save_path)
success_count += 1
elif idx == 2:
formatted_data = {
os.path.basename(path.replace("\\", "/")): inner_dict
for path, inner_dict in self.config_dict.items()
}
df = DataFrame(formatted_data)
df.index.name = "Parameter"
save_path = os.path.join(output_dir, f"{APP_NAME}_configuration.csv")
df.to_csv(save_path)
success_count += 1
elif idx == 3:
first_params = next(iter(self.config_dict.values()))
magic_string = self.gen_magic_str(first_params)
self.placeholder_label.setText(magic_string)
elif idx == 4:
# FIR Waveform Features Export
fir_data = self.fir_feature_dict.get(file_path)
if fir_data:
names = fir_data.get("feature_names", [])
vals = fir_data.get("features", [])
chans = fir_data.get("feature_channels", [])
fir_df = DataFrame({
"Feature_Name": names,
"Channel": chans if len(chans) == len(names) else ["N/A"] * len(names),
"Value": vals
})
save_path = os.path.join(output_dir, f"{base_filename}_fir_features.csv")
fir_df.to_csv(save_path, index=False)
success_count += 1
elif idx == 5:
# Quality Control (QC) Metrics Export
qc_data = self.qc_dict.get(file_path)
if qc_data and isinstance(qc_data, dict):
qc_df = DataFrame(list(qc_data.items()), columns=["Metric", "Value"])
save_path = os.path.join(output_dir, f"{base_filename}_qc_metrics.csv")
qc_df.to_csv(save_path, index=False)
success_count += 1
elif idx == 6:
# Master Consolidated Matrix (1 single CSV combining all selected participants)
save_path = os.path.join(output_dir, f"{APP_NAME}_master_consolidated.csv")
if not os.path.exists(save_path):
master_rows: list[dict[str, Any]] = []
for fp in selected_file_paths:
abs_path = os.path.abspath(fp)
grp = self.group_dict.get(fp, "Unknown")
row: dict[str, Any] = {"Participant": abs_path, "Group": grp}
# QC Metrics
qc_info = self.qc_dict.get(fp, {})
if isinstance(qc_info, dict):
for mk, mv in qc_info.items():
row[f"QC_{mk}"] = mv
# FIR Features
fir_info = self.fir_feature_dict.get(fp, {})
print("1")
if isinstance(fir_info, dict):
print("2")
f_names = fir_info.get("feature_names", [])
f_vals = fir_info.get("features", [])
print("3")
if len(f_names) == len(f_vals):
print("4")
for fn, fv in zip(f_names, f_vals):
row[f"FIR_{fn}"] = fv
master_rows.append(row)
if master_rows:
df_master = DataFrame(master_rows)
df_master.to_csv(save_path, index=False)
success_count += 1
else:
print(f"No method defined for index {idx}")
except Exception as e:
print(f"Failed to export {file_path} (Type {idx}): {e}")
# 4. Final Notification
if success_count > 0:
QMessageBox.information(self, "Export Complete", f"Successfully saved {success_count} CSV files to:\n{output_dir}")
# # If SPARKS export was included, show the Event Window once at the end
# if 1 in selected_indexes:
# win = UpdateEventsWindow(
# parent=self,
# mode=EventUpdateMode.WRITE_JSON,
# caller="Video Alignment Tool"
# )
# win.show()
def gen_magic_str(self, all_params: dict[str, str]) -> str:
magic_str = "To start, the data was loaded into the application. "
if all_params['DOWNSAMPLE']:
magic_str += f"The data was downsampled to {all_params['DOWNSAMPLE_FREQUENCY']}hz. "
else:
magic_str += "The data was not downsampled and was retained at its original sampling frequency. "
if all_params['TRIM']:
magic_str += f"The data was then trimmed to only keep {all_params['SECONDS_TO_KEEP']} seconds before the first event, as the earlier data with no events would not be needed. "
else:
magic_str += "The data was not trimmed and was retained at its original length. "
if all_params['OPTODE_PLACEMENT']:
magic_str += f"A visualization check of the optode locations in 3D space was performed to ensure that they were at the correct location. "
if all_params['SHORT_CHANNELS']:
magic_str += f"A short channel(s) was specified with the maximum length of a short channel being {all_params['SHORT_CHANNELS_THRESHOLD']} metres. Channels that were over this distance but under {all_params['LONG_CHANNELS_THRESHOLD']} metres were retained. "
else:
magic_str += f"No short channel was specified. Channels between the lengths of {all_params['SHORT_CHANNELS_THRESHOLD']} and {all_params['LONG_CHANNELS_THRESHOLD']} meters were used with all other channles being discarded. "
if all_params['HEART_RATE']:
magic_str += f"The heart rate of the participant was attempted to be calculated. {all_params['SECONDS_TO_STRIP_HR']} seconds were ignored from the start of the file to avoid motion artifacts. "
magic_str += f"The heart rate had hard limits of {all_params['MAX_LOW_HR']} to {all_params['MAX_HIGH_HR']} and was smoothed across {all_params['SMOOTHING_WINDOW_HR']} samples. "
else:
magic_str += "The heart rate of the participants was not attempted to be calculated. "
if all_params['SCI']:
magic_str += f"The Scalp Coupling Index was calculated over {all_params['SCI_TIME_WINDOW']} second windows. The threshold value was {all_params['SCI_THRESHOLD']}. "
else:
magic_str += "Scalp Coupling Index was not used. "
if all_params['SNR']:
magic_str += f"The Signal to Noise Ratio was calculated. The threshold value was set to {all_params['SNR_THRESHOLD']}db. "
else:
magic_str += "The Signal to Noise Ratio was not used. "
if all_params['PSP']:
magic_str += f"Peak Spectral Power was calculated over {all_params['PSP_TIME_WINDOW']} second windows. The threshold value was {all_params['PSP_THRESHOLD']}. "
else:
magic_str += "Peak Spectral Power was not used. "
if all_params['COEFF_VAR']:
magic_str += f"The Coefficient of Variation was calculated. The threshold value was set to {all_params['COEFF_VAR_THRESHOLD']}. "
else:
magic_str += "The Coefficient of Variation was not used. "
if all_params['MAD']:
magic_str += f"Median Absolute Deviation was calculated. The threshold value was set to {all_params['MAD_THRESHOLD']}. "
else:
magic_str += "Median Absolute Deviation was not used. "
if all_params['PSD_NOISE']:
magic_str += f"Power Spectral Density Noise was calculated with a target frequency divisor of {all_params['TARGET_FREQ_DIV']} and a decibal limit of {all_params['DB_LIMIT']}. "
else:
magic_str += f"Power Spectral Density Noise was not used. "
magic_str += "More coming soon."
return magic_str