6 Commits

Author SHA1 Message Date
5361f6ea21 changed the changelog 2025-10-15 16:12:51 -07:00
ee023c26c1 changelog fix 2025-10-15 16:10:48 -07:00
06c9ff0ecf update changelog 2025-10-15 15:59:26 -07:00
542dd85a78 general fixes 2025-10-15 15:51:02 -07:00
3e0f70ea49 fixes to build version 1.1.3 2025-10-15 12:59:24 -07:00
d6c71e0ab2 changelog fixes and further updates to cancel running process 2025-10-15 10:48:07 -07:00
5 changed files with 461 additions and 102 deletions

View File

@@ -1,3 +1,34 @@
# Version 1.1.4
- Fixed some display text to now display the correct information
- A new option under Analysis has been added to export the data from a specified participant as a csv file. Fixes [Issue 19](https://git.research.dezeeuw.ca/tyler/flares/issues/19), [Issue 27](https://git.research.dezeeuw.ca/tyler/flares/issues/27)
- Added 2 new parameters - TIME_WINDOW_START and TIME_WINDOW_END. Fixes [Issue 29](https://git.research.dezeeuw.ca/tyler/flares/issues/29)
- These parameters affect the visualization of the significance and contrast images but do not change the total time modeled underneath
- Fixed the duration of annotations edited from a BORIS file from 0 seconds to their proper duration
- Added the annotation information to each participant under their "File information" window
- Fixed Macs not being able to save snirfs attempting to be updated from BORIS files, and in general the updated files not respecting the path chosen by the user
# Version 1.1.3
- Added back the ability to use the fOLD dataset. Fixes [Issue 23](https://git.research.dezeeuw.ca/tyler/flares/issues/23)
- 5th option has been added under Analysis to get to fOLD channels per participant
- Added an option to cancel the running process. Fixes [Issue 15](https://git.research.dezeeuw.ca/tyler/flares/issues/15)
- Prevented graph images from showing when participants are being processed. Fixes [Issue 24](https://git.research.dezeeuw.ca/tyler/flares/issues/24)
- Allow the option to remove all events of a type from all loaded snirfs. Fixes [Issue 25](https://git.research.dezeeuw.ca/tyler/flares/issues/25)
- Added new icons in the menu bar
- Added a terminal to interact with the app in a more command-like form
- Currently the terminal has no functionality but some features for batch operations will be coming soon!
- Inter-Group viewer now has the option to visualize the average response on the brain of all participants in the group. Fixes [Issue 26](https://git.research.dezeeuw.ca/tyler/flares/issues/24)
- Fixed the description under "Update events in snirf file..."
# Version 1.1.2
- Fixed incorrect colormaps being applied
- Added functionality to utilize external event markers from a file. Fixes [Issue 6](https://git.research.dezeeuw.ca/tyler/flares/issues/6)
# Version 1.1.1
- Fixed the number of rectangles in the progress bar to 19

View File

@@ -134,6 +134,9 @@ SHORT_CHANNEL: bool
REMOVE_EVENTS: list
TIME_WINDOW_START: int
TIME_WINDOW_END: int
VERBOSITY = True
# FIXME: Shouldn't need each ordering - just order it before checking
@@ -182,6 +185,8 @@ REQUIRED_KEYS: dict[str, Any] = {
"SHORT_CHANNEL": bool,
"REMOVE_EVENTS": list,
"TIME_WINDOW_START": int,
"TIME_WINDOW_END": int
# "REJECT_PAIRS": bool,
# "FORCE_DROP_ANNOTATIONS": list,
# "FILTER_LOW_PASS": float,
@@ -1077,7 +1082,7 @@ def epochs_calculations(raw_haemo, events, event_dict):
# Plot drop log
# TODO: Why show this if we never use epochs2?
fig_epochs_dropped = epochs2.plot_drop_log()
fig_epochs_dropped = epochs2.plot_drop_log(show=False)
fig_epochs.append(("fig_epochs_dropped", fig_epochs_dropped))
# Plot for each condition
@@ -3031,7 +3036,11 @@ def process_participant(file_path, progress_callback=None):
contrast_dict = {}
for condition in all_conditions:
delay_cols = [col for col in all_delay_cols if col.startswith(f"{condition}_delay_")]
delay_cols = [
col for col in all_delay_cols
if col.startswith(f"{condition}_delay_") and
TIME_WINDOW_START <= int(col.split("_delay_")[-1]) <= TIME_WINDOW_END
]
if not delay_cols:
continue # skip if no columns found (shouldn't happen?)

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H160v400Zm140-40-56-56 103-104-104-104 57-56 160 160-160 160Zm180 0v-80h240v80H480Z"/></svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M280-160v-80h400v80H280Zm160-160v-327L336-544l-56-56 200-200 200 200-56 56-104-103v327h-80Z"/></svg>

After

Width:  |  Height:  |  Size: 216 B

511
main.py
View File

@@ -42,9 +42,10 @@ from PySide6.QtWidgets import (
)
from PySide6.QtCore import QThread, Signal, Qt, QTimer, QEvent, QSize
from PySide6.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleValidator, QPixmap, QStandardItemModel, QStandardItem
from PySide6.QtSvgWidgets import QSvgWidget # needed to show svgs when app is not frozen
CURRENT_VERSION = "1.0.1"
CURRENT_VERSION = "1.0.0"
API_URL = "https://git.research.dezeeuw.ca/api/v1/repos/tyler/flares/releases"
API_URL_SECONDARY = "https://git.research2.dezeeuw.ca/api/v1/repos/tyler/flares/releases"
@@ -154,6 +155,8 @@ SECTIONS = [
{
"title": "General Linear Model",
"params": [
{"name": "TIME_WINDOW_START", "default": "0", "type": int, "help": "Where to start averaging the fir model bins. Only affects the significance and contrast images."},
{"name": "TIME_WINDOW_END", "default": "15", "type": int, "help": "Where to end averaging the fir model bins. Only affects the significance and contrast images."},
#{"name": "N_JOBS", "default": 1, "type": int, "help": "Number of jobs for GLM processing."},
]
},
@@ -173,10 +176,10 @@ SECTIONS = [
class CommandConsole(QWidget):
class TerminalWindow(QWidget):
def __init__(self, parent=None):
super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle("Custom Console")
self.setWindowTitle("Terminal - FLARES")
self.output_area = QTextEdit()
self.output_area.setReadOnly(True)
@@ -189,10 +192,8 @@ class CommandConsole(QWidget):
layout.addWidget(self.input_line)
self.setLayout(layout)
# Define your commands
self.commands = {
"hello": self.cmd_hello,
"add": self.cmd_add,
"help": self.cmd_help
}
@@ -219,12 +220,9 @@ class CommandConsole(QWidget):
else:
self.output_area.append(f"[Unknown command] '{command_name}'")
# Example commands
def cmd_hello(self, *args):
return "Hello from the console!"
def cmd_add(self, a, b):
return f"Result: {int(a) + int(b)}"
def cmd_hello(self, *args):
return "Hello from the terminal!"
def cmd_help(self, *args):
return f"Available commands: {', '.join(self.commands.keys())}"
@@ -734,13 +732,12 @@ class UpdateEventsWindow(QWidget):
self.description = QLabel()
self.description.setTextFormat(Qt.TextFormat.RichText)
self.description.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
self.description.setOpenExternalLinks(False) # Handle the click internally
self.description.setOpenExternalLinks(True)
self.description.setText("Some software when creating snirf files will insert a template of optode positions as the correct position of the optodes for the participant.<br>"
"This is rarely correct as each head differs slightly in shape or size, and a lot of calculations require the optodes to be in the correct location.<br>"
"Using a .txt file, we can update the positions in the snirf file to match those of a digitization system such as one from Polhemus or elsewhere.<br>"
"The .txt file should have the fiducials, detectors, and sources clearly labeled, followed by the x, y, and z coordinates seperated by a space.<br>"
"An example format of what a digitization text file should look like can be found <a href='custom_link'>by clicking here</a>.")
self.description.setText("The events that are present in a snirf file may not be the events that are to be studied and examined.<br>"
"Utilizing different software and video recordings, it is easy enough to see when an action actually occured in a file.<br>"
"The software <a href='https://www.boris.unito.it/'>BORIS</a> is used to add these events to video files, and these events can be applied to the snirf file <br>"
"selected below by selecting the correct BORIS observation and time syncing it to an event that it shares with the snirf file.")
layout.addWidget(self.description)
@@ -1021,64 +1018,94 @@ class UpdateEventsWindow(QWidget):
try:
raw = read_raw_snirf(snirf_path, preload=True)
# Build new Annotations from shifted BORIS events
onsets = []
durations = []
descriptions = []
open_events = {} # label -> list of start times
label_counts = {}
used_times = set()
sfreq = raw.info['sfreq'] # sampling frequency in Hz
min_shift = 1.0 / sfreq
max_attempts = 10
for event in boris_events:
if not isinstance(event, list) or len(event) < 3:
continue
orig_time = event[0]
desc = event[2]
event_time = event[0]
label = event[2]
# Count occurrences per event label
count = label_counts.get(desc, 0)
label_counts[desc] = count + 1
count = label_counts.get(label, 0) + 1
label_counts[label] = count
# Only use 1st, 3rd, 5th... (odd occurrences)
if (count % 2) == 0:
shifted_time = orig_time + time_shift
if label not in open_events:
open_events[label] = []
# Ensure unique timestamp by checking and adjusting slightly
adjusted_time = shifted_time
if count % 2 == 1:
# Odd occurrence = start event
open_events[label].append(event_time)
else:
# Even occurrence = end event
if open_events[label]:
matched_start = open_events[label].pop(0)
duration = event_time - matched_start
# Try to find a unique timestamp
if duration <= 0:
print(f"Warning: Duration for {label} is non-positive ({duration}). Skipping.")
continue
shifted_start = matched_start + time_shift
adjusted_time = shifted_start
attempts = 0
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
adjusted_time += min_shift
attempts += 1
if attempts == max_attempts:
print(f"Warning: Couldn't find unique time for {label} @ {matched_start}s. Skipping.")
continue
adjusted_time = round(adjusted_time, 6)
used_times.add(adjusted_time)
print(f"Adding event: {label} @ {adjusted_time:.3f}s for {duration:.3f}s")
onsets.append(adjusted_time)
durations.append(duration)
descriptions.append(label)
else:
print(f"Warning: Unmatched end for label '{label}' at {event_time:.3f}s. Skipping.")
# Optionally warn about any unmatched starts left open
for label, starts in open_events.items():
for start_time in starts:
shifted_start = start_time + time_shift
adjusted_time = shifted_start
attempts = 0
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
adjusted_time += min_shift
attempts += 1
if attempts == max_attempts:
print(f"Warning: Could not find unique timestamp for event '{desc}' at original time {orig_time:.3f}s. Skipping.")
continue # Skip problematic event
print(f"Warning: Couldn't find unique time for unmatched start {label} @ {start_time}s. Skipping.")
continue
adjusted_time = round(adjusted_time, 6)
used_times.add(adjusted_time)
print(f"Applying event: {desc} @ {adjusted_time:.3f}s (original: {orig_time:.3f}s)")
print(f"Warning: Unmatched start for label '{label}' at {start_time:.3f}s. Adding with duration 0.")
onsets.append(adjusted_time)
durations.append(0.0)
descriptions.append(desc)
descriptions.append(label)
new_annotations = Annotations(onset=onsets, duration=durations, description=descriptions)
# Replace annotations in raw object
raw.set_annotations(new_annotations)
# Write a new SNIRF file
write_raw_snirf(raw, suggested_name)
write_raw_snirf(raw, save_path)
QMessageBox.information(self, "Success", "SNIRF file updated with aligned BORIS events.")
@@ -1179,6 +1206,11 @@ class ProgressBubble(QWidget):
else:
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
def mark_cancelled(self):
if 0 <= self.current_step < len(self.rects):
rect = self.rects[self.current_step]
rect.setStyleSheet("background-color: red; border: 1px solid gray;")
def mousePressEvent(self, event):
self.clicked.emit(self)
super().mousePressEvent(event)
@@ -2191,12 +2223,212 @@ class ParticipantFoldChannelsWidget(QWidget):
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.submit_button.clicked.connect(self.show_fold_images)
self.top_bar.addWidget(QLabel("Participants:"))
self.top_bar.addWidget(self.participant_dropdown)
self.top_bar.addWidget(QLabel("Fold Type:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(QLabel("This will cause the app to hang for ~30s/Participant!"))
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 _create_multiselect_dropdown(self, items):
combo = FullClickComboBox()
combo.setView(QListView())
model = QStandardItemModel()
combo.setModel(model)
combo.setEditable(True)
combo.lineEdit().setReadOnly(True)
combo.lineEdit().setPlaceholderText("Select...")
dummy_item = QStandardItem("<None Selected>")
dummy_item.setFlags(Qt.ItemIsEnabled)
model.appendRow(dummy_item)
toggle_item = QStandardItem("Toggle Select All")
toggle_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
toggle_item.setData(Qt.Unchecked, Qt.CheckStateRole)
model.appendRow(toggle_item)
for item in items:
standard_item = QStandardItem(item)
standard_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
standard_item.setData(Qt.Unchecked, Qt.CheckStateRole)
model.appendRow(standard_item)
combo.setInsertPolicy(QComboBox.NoInsert)
def on_view_clicked(index):
item = model.itemFromIndex(index)
if item.isCheckable():
new_state = Qt.Checked if item.checkState() == Qt.Unchecked else Qt.Unchecked
item.setCheckState(new_state)
combo.view().pressed.connect(on_view_clicked)
self._updating_checkstates = False
def on_item_changed(item):
if self._updating_checkstates:
return
self._updating_checkstates = True
normal_items = [model.item(i) for i in range(2, model.rowCount())] # skip dummy and toggle
if item == toggle_item:
all_checked = all(i.checkState() == Qt.Checked for i in normal_items)
if all_checked:
for i in normal_items:
i.setCheckState(Qt.Unchecked)
toggle_item.setCheckState(Qt.Unchecked)
else:
for i in normal_items:
i.setCheckState(Qt.Checked)
toggle_item.setCheckState(Qt.Checked)
elif item == dummy_item:
pass
else:
# When normal items change, update toggle item
all_checked = all(i.checkState() == Qt.Checked for i in normal_items)
toggle_item.setCheckState(Qt.Checked if all_checked else Qt.Unchecked)
# Update label text immediately after change
if combo == self.participant_dropdown:
self.update_participant_dropdown_label()
elif combo == self.image_index_dropdown:
self.update_image_index_dropdown_label()
self._updating_checkstates = False
model.itemChanged.connect(on_item_changed)
combo.setInsertPolicy(QComboBox.NoInsert)
return combo
def _get_checked_items(self, combo):
checked = []
model = combo.model()
for i in range(model.rowCount()):
item = model.item(i)
# Skip dummy and toggle items:
if item.text() in ("<None Selected>", "Toggle Select All"):
continue
if item.checkState() == Qt.Checked:
checked.append(item.text())
return checked
def update_participant_dropdown_label(self):
selected = self._get_checked_items(self.participant_dropdown)
if not selected:
self.participant_dropdown.lineEdit().setText("<None Selected>")
else:
# Extract just "Participant N" from "Participant N (filename)"
selected_short = [s.split(" ")[0] + " " + s.split(" ")[1] for s in selected]
self.participant_dropdown.lineEdit().setText(", ".join(selected_short))
def update_image_index_dropdown_label(self):
selected = self._get_checked_items(self.image_index_dropdown)
if not selected:
self.image_index_dropdown.lineEdit().setText("<None Selected>")
else:
# Only show the index part
index_labels = [s.split(" ")[0] for s in selected]
self.image_index_dropdown.lineEdit().setText(", ".join(index_labels))
def show_fold_images(self):
import flares
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)
]
# Pass the necessary arguments to each method
for file_path in selected_file_paths:
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:
if idx == 0:
flares.fold_channels(haemo_obj)
else:
print(f"No method defined for index {idx}")
class ExportDataAsCSVViewerWidget(QWidget):
def __init__(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict):
super().__init__()
self.setWindowTitle("FLARES Export Data As CSV Viewer")
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 (second image)",
# "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()
@@ -2316,8 +2548,7 @@ class ParticipantFoldChannelsWidget(QWidget):
index_labels = [s.split(" ")[0] for s in selected]
self.image_index_dropdown.lineEdit().setText(", ".join(index_labels))
def show_brain_images(self):
import flares
def generate_and_save_csv(self):
selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths = []
@@ -2332,42 +2563,6 @@ class ParticipantFoldChannelsWidget(QWidget):
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
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": "show_brodmann",
"label": "Show common brodmann areas on the brain.",
"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
# Pass the necessary arguments to each method
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
@@ -2375,26 +2570,40 @@ class ParticipantFoldChannelsWidget(QWidget):
if haemo_obj is None:
continue
#cha = self.cha_dict.get(file_path)
cha = self.cha_dict.get(file_path)
for idx in selected_indexes:
if idx == 0:
try:
suggested_name = f"{file_path}.csv"
params = param_values.get(idx, {})
show_optodes = params.get("show_optodes", None)
show_brodmann = params.get("show_brodmann", None)
# Open save dialog
save_path, _ = QFileDialog.getSaveFileName(
self,
"Save SNIRF File As",
suggested_name,
"CSV Files (*.csv)"
)
if show_optodes is None or show_brodmann is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
if not save_path:
print("Save cancelled.")
return
if not save_path.lower().endswith(".csv"):
save_path += ".csv"
# Save the CSV here
cha.to_csv(save_path)
QMessageBox.information(self, "Success", "CSV file has been saved.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
flares.fold_channels(haemo_obj)
else:
print(f"No method defined for index {idx}")
class ClickableLabel(QLabel):
def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap):
super().__init__()
@@ -2600,7 +2809,7 @@ class GroupViewerWidget(QWidget):
self.index_texts = [
"0 (GLM Results)",
"1 (Significance)",
# "2 (third_image)",
"2 (Brain Activity Visualization)",
# "3 (fourth image)",
]
@@ -2903,6 +3112,32 @@ class GroupViewerWidget(QWidget):
"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
@@ -2925,8 +3160,13 @@ class GroupViewerWidget(QWidget):
all_cha = pd.DataFrame()
for fp in selected_file_paths:
cha_df = self.cha.get(fp)
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
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)
@@ -2981,6 +3221,20 @@ class GroupViewerWidget(QWidget):
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
flares.brain_3d_visualization(haemo_obj, 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
@@ -3557,12 +3811,16 @@ class ViewerLauncherWidget(QWidget):
btn5 = QPushButton("Open Cross Group Brain Viewer")
btn5.clicked.connect(lambda: self.open_group_brain_viewer(haemo_dict, df_ind, design_matrix, group, contrast_results_dict))
btn6 = QPushButton("Open Export Data As CSV Viewer")
btn6.clicked.connect(lambda: self.open_export_data_as_csv_viewer(haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict))
layout.addWidget(btn1)
layout.addWidget(btn2)
layout.addWidget(btn3)
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)
@@ -3584,6 +3842,10 @@ class ViewerLauncherWidget(QWidget):
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()
class MainApplication(QMainWindow):
"""
@@ -3606,6 +3868,7 @@ class MainApplication(QMainWindow):
self.param_sections = []
self.folder_paths = []
self.section_widget = None
self.first_run = True
self.init_ui()
self.create_menu_bar()
@@ -3834,8 +4097,8 @@ class MainApplication(QMainWindow):
options_actions = [
("User Guide", "F1", self.user_guide, resource_path("icons/help_24dp_1F1F1F.svg")),
("Check for Updates", "F5", self.manual_check_for_updates, resource_path("icons/update_24dp_1F1F1F.svg")),
("Update optodes in snirf file...", "F6", self.update_optode_positions, resource_path("icons/update_24dp_1F1F1F.svg")),
("Update events in snirf file...", "F7", self.update_event_markers, resource_path("icons/update_24dp_1F1F1F.svg")),
("Update optodes in snirf file...", "F6", self.update_optode_positions, resource_path("icons/upgrade_24dp_1F1F1F.svg")),
("Update events in snirf file...", "F7", self.update_event_markers, resource_path("icons/upgrade_24dp_1F1F1F.svg")),
("About", "F12", self.about_window, resource_path("icons/info_24dp_1F1F1F.svg"))
]
@@ -3846,9 +4109,7 @@ class MainApplication(QMainWindow):
terminal_menu = menu_bar.addMenu("Terminal")
terminal_actions = [
("Cut", "Ctrl+X", self.terminal_gui, resource_path("icons/content_cut_24dp_1F1F1F.svg")),
("Copy", "Ctrl+C", self.terminal_gui, resource_path("icons/content_copy_24dp_1F1F1F.svg")),
("Paste", "Ctrl+V", self.terminal_gui, resource_path("icons/content_paste_24dp_1F1F1F.svg"))
("New Terminal", "Ctrl+Alt+T", self.terminal_gui, resource_path("icons/terminal_24dp_1F1F1F.svg")),
]
for name, shortcut, slot, icon in terminal_actions:
terminal_menu.addAction(make_action(name, shortcut, slot, icon=icon))
@@ -3875,6 +4136,8 @@ class MainApplication(QMainWindow):
def clear_all(self):
self.cancel_task()
self.right_column_widget.hide()
# Clear the bubble layout
@@ -3937,7 +4200,7 @@ class MainApplication(QMainWindow):
def terminal_gui(self):
if self.terminal is None or not self.terminal.isVisible():
self.terminal = CommandConsole(self)
self.terminal = TerminalWindow(self)
self.terminal.show()
def update_optode_positions(self):
@@ -4284,8 +4547,52 @@ class MainApplication(QMainWindow):
return self.file_metadata
def cancel_task(self):
self.button1.clicked.disconnect(self.cancel_task)
self.button1.setText("Stopping...")
if hasattr(self, "result_process") and self.result_process.is_alive():
parent = psutil.Process(self.result_process.pid)
children = parent.children(recursive=True)
for child in children:
try:
child.kill()
except psutil.NoSuchProcess:
pass
self.result_process.terminate()
self.result_process.join()
if hasattr(self, "result_timer") and self.result_timer.isActive():
self.result_timer.stop()
# if hasattr(self, "result_process") and self.result_process.is_alive():
# self.result_process.terminate() # Forcefully terminate the process
# self.result_process.join() # Wait for it to properly close
# # Stop the QTimer if running
# if hasattr(self, "result_timer") and self.result_timer.isActive():
# self.result_timer.stop()
for bubble in self.bubble_widgets.values():
bubble.mark_cancelled()
self.statusbar.showMessage("Processing cancelled.")
self.button1.clicked.connect(self.on_run_task)
self.button1.setText("Process")
'''MODULE FILE'''
def on_run_task(self):
self.button1.clicked.disconnect(self.on_run_task)
self.button1.setText("Cancel")
self.button1.clicked.connect(self.cancel_task)
if not self.first_run:
for bubble in self.bubble_widgets.values():
bubble.mark_cancelled()
self.first_run = False
# Collect all selected snirf files in a flat list
snirf_files = []
@@ -4608,6 +4915,16 @@ class MainApplication(QMainWindow):
else:
snirf_info['Digitization Points'] = "Not found"
if raw.annotations is not None and len(raw.annotations) > 0:
annot_info = []
for onset, duration, desc in zip(raw.annotations.onset,
raw.annotations.duration,
raw.annotations.description):
annot_info.append(f"Onset: {onset:.2f}s, Duration: {duration:.2f}s, Description: {desc}")
snirf_info['Annotations'] = annot_info
else:
snirf_info['Annotations'] = "No annotations found"
return snirf_info
except:
return None
@@ -4813,7 +5130,7 @@ def resource_path(relative_path):
# PyInstaller bundle path
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)