mostly finish rewrite

This commit is contained in:
2026-06-28 10:10:27 -07:00
parent f10eb64332
commit ddb30d98f2
18 changed files with 2116 additions and 2350 deletions
+9 -2321
View File
File diff suppressed because it is too large Load Diff
+166
View File
@@ -0,0 +1,166 @@
"""
Filename: exportcsv.py
Description: Export data as csv analysis window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
import numpy as np
import pandas as pd
from PySide6.QtWidgets import QFileDialog, QGridLayout, QHBoxLayout, QMessageBox, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import QSize
from src.shared.flaresbasewidget import FlaresBaseWidget
from src.shared.shareddata import APP_NAME
class ExportDataAsCSVViewerWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict):
super().__init__("ExportDataAsCSVViewer")
self.setWindowTitle(f"Export Data As CSV Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha_dict = cha_dict
self.df_ind = df_ind
self.design_matrix = design_matrix
self.group = group
self.contrast_results_dict = contrast_results_dict
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
self.participant_map[file_path] = short_label
self.participant_dropdown_items.append(display_label)
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.layout.addLayout(self.top_bar)
self.participant_dropdown = self._create_multiselect_dropdown(self.participant_dropdown_items)
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
self.index_texts = [
"0 (Export Data to CSV)",
"1 (CSV for SPARKS)",
# "2 (third image)",
# "3 (fourth image)",
]
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.generate_and_save_csv)
self.top_bar.addWidget(QLabel("Participants:"))
self.top_bar.addWidget(self.participant_dropdown)
self.top_bar.addWidget(QLabel("Export Type:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def generate_and_save_csv(self):
selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths = []
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
# 2. ASK ONCE: Select Output Directory
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")
if haemo_obj is not None:
raw = haemo_obj
data, times = raw.get_data(return_times=True)
ann_col = np.full(times.shape, "", dtype=object)
if raw.annotations is not None and len(raw.annotations) > 0:
for onset, duration, desc in zip(
raw.annotations.onset,
raw.annotations.duration,
raw.annotations.description
):
mask = (times >= onset) & (times < onset + duration)
ann_col[mask] = desc
df = pd.DataFrame(data.T, columns=raw.ch_names)
df.insert(0, "annotation", ann_col)
df.insert(0, "time", times)
df.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()
+306
View File
@@ -0,0 +1,306 @@
"""
Filename: group.py
Description: Group analysis window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
import pandas as pd
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import QSize
from src.shared.flaresbasewidget import FlaresBaseWidget, ParameterInputDialog
from src.shared.shareddata import APP_NAME
class GroupViewerWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group):
super().__init__("GroupViewer")
self.setWindowTitle(f"Group Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha = cha
self.df_ind = df_ind
self.design_matrix = design_matrix
self.contrast_results = contrast_results
self.group = group
self.show_all_events = True
self._updating_checkstates = False
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
self.participant_map[file_path] = short_label
self.participant_dropdown_items.append(display_label)
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.layout.addLayout(self.top_bar)
self.group_to_paths = {}
for file_path, group_name in self.group.items():
self.group_to_paths.setdefault(group_name, []).append(file_path)
self.group_names = sorted(self.group_to_paths.keys())
self.group_dropdown = QComboBox()
self.group_dropdown.addItem("<None Selected>")
self.group_dropdown.addItems(self.group_names)
self.group_dropdown.setCurrentIndex(0)
self.group_dropdown.currentIndexChanged.connect(self.update_participant_list_for_group)
self.participant_dropdown = self._create_multiselect_dropdown(self.participant_dropdown_items)
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
self.participant_dropdown.setEnabled(False)
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.index_texts = [
"0 (GLM Results)",
"1 (Significance)",
"2 (Brain Activity Visualization)",
# "3 (fourth image)",
]
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.show_brain_images)
self.top_bar.addWidget(QLabel("Group:"))
self.top_bar.addWidget(self.group_dropdown)
self.top_bar.addWidget(QLabel("Participants:"))
self.top_bar.addWidget(self.participant_dropdown)
self.top_bar.addWidget(QLabel("Event:"))
self.top_bar.addWidget(self.event_dropdown)
self.top_bar.addWidget(QLabel("Image Indexes:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def show_brain_images(self):
import flares as flares
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths = []
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
if selected_event:
valid_paths = []
for fp in selected_file_paths:
raw = self.haemo_dict.get(fp)
# Check if this participant actually has the event in their annotations
if raw is not None and hasattr(raw, "annotations"):
if selected_event in raw.annotations.description:
valid_paths.append(fp)
selected_file_paths = valid_paths
selected_indexes = [
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
if not selected_file_paths:
print("No participants selected.")
return
# Only keep indexes 0 and 1 that need parameters
parameterized_indexes = {
0: [
{
"key": "lower_bound",
"label": "Lower bound + <description>",
"default": "-0.3",
"type": float, # specify int here
},
{
"key": "upper_bound",
"label": "Upper bound + <description>",
"default": "0.8",
"type": float, # specify int here
}
],
1: [
{
"key": "p_value",
"label": "Significance threshold P-value (e.g. 0.05)",
"default": "0.05",
"type": float,
},
{
"key": "graph_bounds",
"label": "Graph Upper/Lower Limit",
"default": "3.0",
"type": float,
}
],
2: [
{
"key": "show_optodes",
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
"default": "all",
"type": str,
},
{
"key": "t_or_theta",
"label": "Specify if t values or theta values should be plotted. Valid values are 't', 'theta'",
"default": "theta",
"type": str,
},
{
"key": "show_text",
"label": "Display informative text on the top left corner. THIS DOES NOT WORK AND SHOULD BE LEFT AT FALSE",
"default": "False",
"type": bool,
},
{
"key": "brain_bounds",
"label": "Graph Upper/Lower Limit",
"default": "1.0",
"type": float,
}
],
}
# Inject full_text from index_texts
for idx, params_list in parameterized_indexes.items():
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
for param_info in params_list:
param_info["full_text"] = full_text
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes}
param_values = {}
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
if dialog.exec_() == QDialog.Accepted:
param_values = dialog.get_values()
if param_values is None:
return
else:
return
all_cha = pd.DataFrame()
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
if selected_event:
participant_events = set(haemo_obj.annotations.description)
if selected_event not in participant_events:
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
continue
if haemo_obj is None:
continue
cha_df = self.cha.get(file_path)
if cha_df is not None:
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
# Pass the necessary arguments to each method
file_path = selected_file_paths[0]
p_haemo = self.haemo_dict.get(file_path)
p_design_matrix = self.design_matrix.get(file_path)
df_group = pd.DataFrame()
if selected_file_paths:
for file_path in selected_file_paths:
df = self.df_ind.get(file_path)
if df is not None:
df_group = pd.concat([df_group, df], ignore_index=True)
for idx in selected_indexes:
if idx == 0:
params = param_values.get(idx, {})
lower_bound = params.get("lower_bound", None)
upper_bound = params.get("upper_bound", None)
if lower_bound is None or upper_bound is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
flares.plot_fir_model_results(df_group, p_haemo, p_design_matrix, selected_event, lower_bound, upper_bound)
elif idx == 1:
params = param_values.get(idx, {})
p_val = params.get("p_value", None)
graph_bounds = params.get("graph_bounds", None)
if p_val is None or graph_bounds is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
all_contrasts = []
for fp in selected_file_paths:
condition_dfs = self.contrast_results.get(fp, {})
if selected_event in condition_dfs:
df = condition_dfs[selected_event].copy()
df["ID"] = fp
all_contrasts.append(df)
if not all_contrasts:
print("No contrast data found for selected participants and event.")
return
df_contrasts = pd.concat(all_contrasts, ignore_index=True)
flares.run_second_level_analysis(df_contrasts, p_haemo, p_val, graph_bounds)
elif idx == 2:
params = param_values.get(idx, {})
show_optodes = params.get("show_optodes", None)
t_or_theta = params.get("t_or_theta", None)
show_text = params.get("show_text", None)
brain_bounds = params.get("brain_bounds", None)
if show_optodes is None or t_or_theta is None or show_text is None or brain_bounds is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
raw_list = [self.haemo_dict.get(fp) for fp in selected_file_paths]
if len(selected_file_paths) > 1:
print(f"Aggregating geometry for {len(selected_file_paths)} participants...")
processed_raw = flares.aggregate_fnirs_group_geometry(raw_list)
else:
processed_raw = raw_list[0].copy().pick(picks="hbo")
flares.brain_3d_visualization(processed_raw, all_cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds)
elif idx == 3:
pass
else:
print(f"No method defined for index {idx}")
+311
View File
@@ -0,0 +1,311 @@
"""
Filename: groupbrain.py
Description: Group brain analysis window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
import pandas as pd
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import QSize
from src.shared.flaresbasewidget import FlaresBaseWidget, ParameterInputDialog
from src.shared.shareddata import APP_NAME
class GroupBrainViewerWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, df_ind, design_matrix, group, contrast_results_dict):
super().__init__("GroupBrainViewer")
self.setWindowTitle(f"Group Brain Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.df_ind = df_ind
self.design_matrix = design_matrix
self.group = group
self.contrast_results_dict = contrast_results_dict
self.group_to_paths = {}
for file_path, group_name in self.group.items():
self.group_to_paths.setdefault(group_name, []).append(file_path)
self.group_names = sorted(self.group_to_paths.keys())
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.layout.addLayout(self.top_bar)
self.group_a_dropdown = QComboBox()
self.group_a_dropdown.addItem("<None Selected>")
self.group_a_dropdown.addItems(self.group_names)
self.group_a_dropdown.currentIndexChanged.connect(self._update_group_a_options)
self.group_b_dropdown = QComboBox()
self.group_b_dropdown.addItem("<None Selected>")
self.group_b_dropdown.addItems(self.group_names)
self.group_b_dropdown.currentIndexChanged.connect(self._update_group_b_options)
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.participant_dropdown_a = self._create_multiselect_dropdown([])
self.participant_dropdown_a.lineEdit().setPlaceholderText("Select participants (Group A)")
self.participant_dropdown_a.model().itemChanged.connect(self._on_participants_changed)
self.participant_dropdown_b = self._create_multiselect_dropdown([])
self.participant_dropdown_b.lineEdit().setPlaceholderText("Select participants (Group B)")
self.participant_dropdown_b.model().itemChanged.connect(self._on_participants_changed)
self.index_texts = [
"0 (Contrast Image)",
# "1 (3D Brain Contrast)",
# "2 (third image)",
# "3 (fourth image)",
]
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.show_brain_images)
self.top_bar.addWidget(QLabel("Group A:"))
self.top_bar.addWidget(self.group_a_dropdown)
self.top_bar.addWidget(QLabel("Participants (Group A):"))
self.top_bar.addWidget(self.participant_dropdown_a)
self.top_bar.addWidget(QLabel("Group B:"))
self.top_bar.addWidget(self.group_b_dropdown)
self.top_bar.addWidget(QLabel("Participants (Group B):"))
self.top_bar.addWidget(self.participant_dropdown_b)
self.top_bar.addWidget(QLabel("Event:"))
self.top_bar.addWidget(self.event_dropdown)
self.top_bar.addWidget(QLabel("Image Indexes:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def _update_group_b_options(self):
"""Triggered when Group B changes: Update Group A to exclude B's choice"""
selected_b = self.group_b_dropdown.currentText()
# Refresh Group A and exclude what was just picked in Group B
self._refresh_group_dropdown(self.group_a_dropdown, exclude=selected_b)
# Update the participants for Group B
self.update_participant_list_for_group(selected_b, self.participant_dropdown_b)
self._update_event_dropdown()
def _update_group_a_options(self):
"""Triggered when Group A changes: Update Group B to exclude A's choice"""
selected_a = self.group_a_dropdown.currentText()
# Refresh Group B and exclude what was just picked in Group A
self._refresh_group_dropdown(self.group_b_dropdown, exclude=selected_a)
# Update the participants for Group A
self.update_participant_list_for_group(selected_a, self.participant_dropdown_a)
self._update_event_dropdown()
def _on_participants_changed(self, item=None):
self._update_event_dropdown()
def _refresh_group_dropdown(self, dropdown, exclude):
current = dropdown.currentText()
dropdown.blockSignals(True)
dropdown.clear()
dropdown.addItem("<None Selected>")
for group in self.group_names:
if group != exclude:
dropdown.addItem(group)
# Restore previous selection if still valid
if current != "<None Selected>" and current != exclude and dropdown.findText(current) != -1:
dropdown.setCurrentText(current)
else:
dropdown.setCurrentIndex(0) # Reset to "<None Selected>"
dropdown.blockSignals(False)
def _get_file_paths_from_labels(self, labels, group_name):
file_paths = []
if group_name == self.group_a_dropdown.currentText():
participant_map = self.participant_map_a
elif group_name == self.group_b_dropdown.currentText():
participant_map = self.participant_map_b
else:
return []
# Reverse map: display label -> file path
reverse_map = {
f"{label} ({os.path.basename(fp)})": fp
for fp, label in participant_map.items()
}
for label in labels:
file_path = reverse_map.get(label)
if file_path:
file_paths.append(file_path)
return file_paths
def show_brain_images(self):
import flares as flares
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
# Group A
participants_a = self._get_checked_items(self.participant_dropdown_a)
file_paths_a = self._get_file_paths_from_labels(participants_a, self.group_a_dropdown.currentText())
# Group B
participants_b = self._get_checked_items(self.participant_dropdown_b)
file_paths_b = self._get_file_paths_from_labels(participants_b, self.group_b_dropdown.currentText())
selected_indexes = [
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
all_selected_paths = list(set(file_paths_a + file_paths_b))
if not all_selected_paths:
print("No participants selected.")
return
parameterized_indexes = {
0: [
{
"key": "show_optodes",
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
"default": "all",
"type": str,
},
{
"key": "t_or_theta",
"label": "Specify if t values or theta values should be plotted. Valid values are 't', 'theta'",
"default": "theta",
"type": str,
},
{
"key": "show_text",
"label": "Display informative text on the top left corner about the contrast.",
"default": "True",
"type": bool,
},
{
"key": "brain_bounds",
"label": "Graph Upper/Lower Limit",
"default": "1.0",
"type": float,
},
{
"key": "is_3d",
"label": "Should we display the results in a 3D interactive window?",
"default": "True",
"type": bool,
}
],
}
# Inject full_text from index_texts
for idx, params_list in parameterized_indexes.items():
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
for param_info in params_list:
param_info["full_text"] = full_text
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes}
param_values = {}
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
if dialog.exec_() == QDialog.Accepted:
param_values = dialog.get_values()
if param_values is None:
return
else:
return
# Build group-level contrast DataFrames
def concat_group_contrasts(file_paths: list[str], event: str | None) -> pd.DataFrame:
group_df = pd.DataFrame()
for fp in file_paths:
print(f"Looking up contrast for: {fp}")
event_con_dict = self.contrast_results_dict.get(fp, {})
print("Available events for this file:", list(event_con_dict.keys()))
if event and event in event_con_dict:
df = event_con_dict[event]
print(f"Appending contrast df for event: {event}")
group_df = pd.concat([group_df, df], ignore_index=True)
else:
print(f"Event '{event}' not found for {fp}")
return group_df
print("Selected event:", selected_event)
print("File paths A:", file_paths_a)
print("File paths B:", file_paths_b)
contrast_df_a = concat_group_contrasts(file_paths_a, selected_event)
contrast_df_b = concat_group_contrasts(file_paths_b, selected_event)
print("contrast_df_a empty?", contrast_df_a.empty)
print("contrast_df_b empty?", contrast_df_b.empty)
all_raw_objs = [self.haemo_dict.get(fp) for fp in all_selected_paths if self.haemo_dict.get(fp)]
if len(all_raw_objs) > 1:
processed_raw = flares.aggregate_fnirs_group_geometry(all_raw_objs)
else:
processed_raw = all_raw_objs[0].copy().pick(picks="hbo")
# Visualizations
for idx in selected_indexes:
if idx == 0:
params = param_values.get(idx, {})
show_optodes = params.get("show_optodes", None)
t_or_theta = params.get("t_or_theta", None)
show_text = params.get("show_text", None)
brain_bounds = params.get("brain_bounds", None)
is_3d = params.get("is_3d", None)
if show_optodes is None or t_or_theta is None or show_text is None or brain_bounds is None or is_3d is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
if not contrast_df_a.empty and not contrast_df_b.empty and processed_raw:
flares.plot_2d_3d_contrasts_between_groups(
contrast_df_a,
contrast_df_b,
raw_haemo=processed_raw,
group_a_name=self.group_a_dropdown.currentText(),
group_b_name=self.group_b_dropdown.currentText(),
is_3d=is_3d,
t_or_theta=t_or_theta,
show_optodes=show_optodes,
show_text=show_text,
brain_bounds=brain_bounds
)
else:
print("no")
+10 -2
View File
@@ -1,3 +1,11 @@
"""
Filename: groupfunctionalconnectivity.py
Description: Group functional connectivity analysis window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel, QMessageBox
@@ -10,14 +18,14 @@ from src.shared.shareddata import APP_NAME
class GroupFunctionalConnectivityWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, group, config_dict):
super().__init__("GroupFunctionalConnectivityWidget")
self.setWindowTitle(f"{APP_NAME.upper} Group Viewer")
self.setWindowTitle(f"Group Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.group = group
self.config_dict = config_dict
self.show_all_events = True
self._updating_checkstates = False
QMessageBox.warning(self, f"Warning - {APP_NAME.upper}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
QMessageBox.warning(self, f"Warning - {APP_NAME.upper()}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
"By clicking OK, you accept that the images generated may not be factual.")
+9 -1
View File
@@ -1,3 +1,11 @@
"""
Filename: participant.py
Description: Participant analysis window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
from pathlib import Path
from datetime import datetime
@@ -14,7 +22,7 @@ class ParticipantViewerWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, fig_bytes_dict):
super().__init__("ParticipantViewer")
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.setWindowTitle(f"{APP_NAME.upper} Participant Viewer")
self.setWindowTitle(f"Participant Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.fig_bytes_dict = fig_bytes_dict
+9 -1
View File
@@ -1,3 +1,11 @@
"""
Filename: participantbrain.py
Description: Participant brain analysis window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
@@ -10,7 +18,7 @@ from src.shared.shareddata import APP_NAME
class ParticipantBrainViewerWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, cha_dict):
super().__init__("ParticipantBrainViewer")
self.setWindowTitle(f"{APP_NAME.upper} Participant Brain Viewer")
self.setWindowTitle(f"Participant Brain Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha_dict = cha_dict
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,11 @@
"""
Filename: participantfunctionalconnectivity.py
Description: Participant functional connectivity analysis window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel, QMessageBox
@@ -10,11 +18,11 @@ from src.shared.shareddata import APP_NAME
class ParticipantFunctionalConnectivityWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, epochs_dict):
super().__init__("FunctionalConnectivityWidget")
self.setWindowTitle(f"{APP_NAME.upper} Functional Connectivity Viewer [BETA]")
self.setWindowTitle(f"Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.epochs_dict = epochs_dict
QMessageBox.warning(self, f"Warning - {APP_NAME.upper}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
QMessageBox.warning(self, f"Warning - {APP_NAME.upper()}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
"By clicking OK, you accept that the images generated may not be factual.")
# Create mappings: file_path -> participant label and dropdown display text
+28 -1
View File
@@ -1,3 +1,11 @@
"""
Filename: flaresbasewidget.py
Description: Custom window design and supporting methods for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QVBoxLayout, QWidget, QFrame, QSpinBox
@@ -5,7 +13,26 @@ from PySide6.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIntValida
from PySide6.QtCore import QEvent, Qt
from src.shared.shareddata import APP_NAME
from src.shared.flaresbasewidget import FullClickComboBox
class FullClickComboBox(QComboBox):
def __init__(self, parent=None):
super().__init__(parent)
self.setEditable(True)
self.lineEdit().setReadOnly(True)
self.lineEdit().installEventFilter(self)
def eventFilter(self, obj, event):
if obj == self.lineEdit():
if event.type() == QEvent.MouseButtonPress:
return True
if event.type() == QEvent.MouseButtonRelease:
self.showPopup()
return True
return super().eventFilter(obj, event)
class ClickableLabel(QLabel):
+10
View File
@@ -1,3 +1,11 @@
"""
Filename: shareddata.py
Description: Shared constants and methods for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import sys
import os
import platform
@@ -7,6 +15,8 @@ APP_NAME = "flares"
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
API_URL_SECONDARY = f"https://git.research2.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
PLATFORM_NAME = platform.system().lower()
CHANGELOG_URL = "https://git.research.dezeeuw.ca/tyler/flares/raw/branch/main/changelog_major.md"
WIKI_URL = "https://git.research.dezeeuw.ca/tyler/flares/wiki"
PIPELINE_STAGES = [
"Preprocessing",
+8
View File
@@ -1,3 +1,11 @@
"""
Filename: about.py
Description: About window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import Qt
+8
View File
@@ -1,3 +1,11 @@
"""
Filename: terminal.py
Description: Terminal window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLineEdit
from PySide6.QtCore import Qt
+9 -3
View File
@@ -1,3 +1,11 @@
"""
Filename: updateevents.py
Description: Methods to update snirf events for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
import json
from enum import Enum, auto
@@ -26,7 +34,7 @@ class UpdateEventsWindow(QWidget):
self.mode = mode
self.caller = caller or self.__class__.__name__
self.setWindowTitle("Update event markers")
self.setWindowTitle(f"Update event markers - {APP_NAME.upper()}")
self.resize(760, 200)
print("INIT MODE:", mode)
@@ -845,5 +853,3 @@ class UpdateEventsBlazesWindow(QWidget):
f"Aligned {len(onsets)} events.\n(Filtered out {skipped_count} short events)")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
+11 -2
View File
@@ -1,3 +1,11 @@
"""
Filename: updateoptodes.py
Description: Methods to update optode locations for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
from pathlib import Path
@@ -9,6 +17,7 @@ from PySide6.QtCore import Qt
from mne.io import read_raw_snirf
from mne_nirs.io import write_raw_snirf
from mne.channels import make_dig_montage
from src.shared.shareddata import APP_NAME
@@ -17,7 +26,7 @@ class UpdateOptodesWindow(QWidget):
def __init__(self, parent=None):
super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle("Update optode positions")
self.setWindowTitle(f"Update optode positions - {APP_NAME.upper()}")
self.resize(760, 200)
self.label_file_a = QLabel("SNIRF file:")
@@ -136,7 +145,7 @@ class UpdateOptodesWindow(QWidget):
def show_help_popup(self, text):
msg = QMessageBox(self)
msg.setWindowTitle(f"Parameter Info - {APP_NAME.upper}")
msg.setWindowTitle(f"Parameter Info - {APP_NAME.upper()}")
msg.setText(text)
msg.exec()
+10 -2
View File
@@ -1,7 +1,15 @@
"""
Filename: userguide.py
Description: User guide for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import Qt
from src.shared.shareddata import APP_NAME, PIPELINE_STAGES
from src.shared.shareddata import APP_NAME, PIPELINE_STAGES, WIKI_URL
class UserGuideWindow(QWidget):
@@ -22,7 +30,7 @@ class UserGuideWindow(QWidget):
label2_text = "\n".join(f"Stage {idx + 1}: {name}" for idx, name in enumerate(PIPELINE_STAGES)) + "\n"
label2 = QLabel(label2_text, self)
label3 = QLabel(f"For more information, visit the Git wiki page <a href='https://git.research.dezeeuw.ca/tyler/{APP_NAME}/wiki'>here</a>.", self)
label3 = QLabel(f"For more information, visit the Git wiki page <a href='{WIKI_URL}'>here</a>.", self)
label3.setTextFormat(Qt.TextFormat.RichText)
label3.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
label3.setOpenExternalLinks(True)
+122
View File
@@ -0,0 +1,122 @@
"""
Filename: viewerlauncher.py
Description: Analysis options launcher for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
from PySide6.QtWidgets import QPushButton, QWidget, QVBoxLayout
from PySide6.QtCore import QTimer
from src.analysis.exportcsv import ExportDataAsCSVViewerWidget
from src.analysis.group import GroupViewerWidget
from src.analysis.groupbrain import GroupBrainViewerWidget
from src.analysis.groupfunctionalconnectivity import GroupFunctionalConnectivityWidget
from src.analysis.participant import ParticipantViewerWidget
from src.analysis.participantbrain import ParticipantBrainViewerWidget
from src.analysis.participantfoldchannels import ParticipantFoldChannelsWidget
from src.analysis.participantfunctionalconnectivity import ParticipantFunctionalConnectivityWidget
from src.shared.shareddata import APP_NAME
class ViewerLauncherWidget(QWidget):
def __init__(self, haemo_dict, config_dict, fig_bytes_dict, cha_dict, contrast_results_dict, df_ind, design_matrix, epochs_dict, folding_bypass):
super().__init__()
self.setWindowTitle(f"Viewer Launcher - {APP_NAME.upper()}")
group_dict = {
file_path: config.get("GROUP", "Unknown")
for file_path, config in config_dict.items()
}
def launch(func, btn, *args):
func(*args)
self._trigger_success(btn)
layout = QVBoxLayout(self)
btn1 = QPushButton("Open Participant Viewer")
btn1.clicked.connect(lambda: launch(self.open_participant_viewer, btn1, haemo_dict, fig_bytes_dict))
btn1.setEnabled(not folding_bypass)
btn2 = QPushButton("Open Participant Brain Viewer")
btn2.clicked.connect(lambda: launch(self.open_participant_brain_viewer, btn2, haemo_dict, cha_dict))
btn2.setEnabled(not folding_bypass)
btn3 = QPushButton("Open Participant Fold Channels Viewer")
btn3.clicked.connect(lambda: launch(self.open_participant_fold_channels_viewer, btn3, haemo_dict, cha_dict))
btn7 = QPushButton("Open Functional Connectivity Viewer [BETA]")
btn7.clicked.connect(lambda: launch(self.open_participant_functional_connectivity_viewer, btn7, haemo_dict, epochs_dict))
btn7.setEnabled(not folding_bypass)
btn8 = QPushButton("Open Group Functional Connectivity Viewer [BETA]")
btn8.clicked.connect(lambda: launch(self.open_group_functional_connectivity_viewer, btn8, haemo_dict, group_dict, config_dict))
btn8.setEnabled(not folding_bypass)
btn4 = QPushButton("Open Inter-Group Viewer")
btn4.clicked.connect(lambda: launch(self.open_group_viewer, btn4, haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group_dict))
btn4.setEnabled(not folding_bypass)
btn5 = QPushButton("Open Cross Group Brain Viewer")
btn5.clicked.connect(lambda: launch(self.open_group_brain_viewer, btn5, haemo_dict, df_ind, design_matrix, group_dict, contrast_results_dict))
btn5.setEnabled(not folding_bypass)
btn6 = QPushButton("Open Export Data As CSV Viewer")
btn6.clicked.connect(lambda: launch(self.open_export_data_as_csv_viewer, btn6, haemo_dict, cha_dict, df_ind, design_matrix, group_dict, contrast_results_dict))
btn6.setEnabled(not folding_bypass)
layout.addWidget(btn1)
layout.addWidget(btn2)
layout.addWidget(btn3)
layout.addWidget(btn7)
layout.addWidget(btn8)
layout.addWidget(btn4)
layout.addWidget(btn5)
layout.addWidget(btn6)
def open_participant_viewer(self, haemo_dict, fig_bytes_dict):
self.participant_viewer = ParticipantViewerWidget(haemo_dict, fig_bytes_dict)
self.participant_viewer.show()
def open_participant_brain_viewer(self, haemo_dict, cha_dict):
self.participant_brain_viewer = ParticipantBrainViewerWidget(haemo_dict, cha_dict)
self.participant_brain_viewer.show()
def open_participant_fold_channels_viewer(self, haemo_dict, cha_dict):
self.participant_fold_channels_viewer = ParticipantFoldChannelsWidget(haemo_dict, cha_dict)
self.participant_fold_channels_viewer.show()
def open_participant_functional_connectivity_viewer(self, haemo_dict, epochs_dict):
self.participant_brain_viewer = ParticipantFunctionalConnectivityWidget(haemo_dict, epochs_dict)
self.participant_brain_viewer.show()
def open_group_functional_connectivity_viewer(self, haemo_dict, group, config_dict):
self.participant_brain_viewer = GroupFunctionalConnectivityWidget(haemo_dict, group, config_dict)
self.participant_brain_viewer.show()
def open_group_viewer(self, haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group):
self.participant_brain_viewer = GroupViewerWidget(haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group)
self.participant_brain_viewer.show()
def open_group_brain_viewer(self, haemo_dict, df_ind, design_matrix, group, contrast_results_dict):
self.participant_brain_viewer = GroupBrainViewerWidget(haemo_dict, df_ind, design_matrix, group, contrast_results_dict)
self.participant_brain_viewer.show()
def open_export_data_as_csv_viewer(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict):
self.export_data_as_csv_viewer = ExportDataAsCSVViewerWidget(haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict)
self.export_data_as_csv_viewer.show()
def _trigger_success(self, button):
"""Temporarily adds a green checkmark to the button text."""
original_text = button.text()
button.setText(f"{original_text}")
button.setStyleSheet("color: green; font-weight: bold;")
# Revert after 1 second
QTimer.singleShot(1000, lambda: self._revert_button(button, original_text))
def _revert_button(self, button, original_text):
button.setText(original_text)
button.setStyleSheet("")
+18 -12
View File
@@ -1,9 +1,17 @@
"""
Filename: welcome.py
Description: Welcome dialog for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
from PySide6.QtWidgets import QTextBrowser, QVBoxLayout, QLabel, QDialog, QHBoxLayout, QPushButton
from PySide6.QtGui import QDesktopServices, QIcon
from PySide6.QtCore import QUrl
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest
from src.shared.shareddata import APP_NAME, CURRENT_VERSION, resource_path
from src.shared.shareddata import APP_NAME, CURRENT_VERSION, CHANGELOG_URL, resource_path
class WelcomeDialog(QDialog):
@@ -13,13 +21,13 @@ class WelcomeDialog(QDialog):
self.setMinimumSize(550, 450)
self.resize(800, 500)
# Main Layout
layout = QVBoxLayout(self)
# Header Layout (Logo + App Name)
header_layout = QHBoxLayout()
logo_label = QLabel(self)
logo_label.setPixmap(QIcon(resource_path("icons/main.ico")).pixmap(48, 48)) # Fits cleanly in a header
# NOTE: might not work on mac and need the icns file
logo_label.setPixmap(QIcon(resource_path("icons/main.ico")).pixmap(48, 48))
if direct:
title_label = QLabel(f"<h2>{APP_NAME.upper()} has been sucessfully updated to version {CURRENT_VERSION}!</h2>", self)
else:
@@ -30,19 +38,19 @@ class WelcomeDialog(QDialog):
header_layout.addStretch()
layout.addLayout(header_layout)
# Text Browser Area (Automatically converts Markdown syntax into clean formatted UI text)
self.text_browser = QTextBrowser(self)
self.text_browser.setHtml("<p style='color: gray;'>Loading latest updates from server...</p>")
self.text_browser.setOpenLinks(False) # Don't open links inside the viewer
# Ensure links open in the default web browser and not in this window
self.text_browser.setOpenLinks(False)
self.text_browser.anchorClicked.connect(QDesktopServices.openUrl)
layout.addWidget(self.text_browser)
# Footer Controls Layout
footer_layout = QHBoxLayout()
ok_button = QPushButton("OK", self)
ok_button.setDefault(True)
ok_button.clicked.connect(self.accept) # Closes the dialog with a success signal
ok_button.clicked.connect(self.accept)
footer_layout.addStretch()
footer_layout.addWidget(ok_button)
@@ -51,18 +59,16 @@ class WelcomeDialog(QDialog):
# Fetch markdown from the web asynchronously
self.network_manager = QNetworkAccessManager(self)
self.network_manager.finished.connect(self._on_download_complete)
md_url = "https://git.research.dezeeuw.ca/tyler/flares/raw/branch/main/changelog_major.md"
self.network_manager.get(QNetworkRequest(QUrl(md_url)))
self.network_manager.get(QNetworkRequest(QUrl(CHANGELOG_URL)))
def _on_download_complete(self, reply):
"""Processes the downloaded markdown and drops it into the view frame."""
if reply.error() == reply.NetworkError.NoError:
raw_bytes = reply.readAll()
# Convert raw bytes to standard text string
markdown_text = str(raw_bytes, encoding='utf-8')
# Qt's QTextBrowser natively renders markdown arrays beautifully!
self.text_browser.setMarkdown(markdown_text)
else:
self.text_browser.setHtml(