Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 953ea90c67 | |||
| 20b255321b | |||
| b5afcec37d |
18
changelog.md
18
changelog.md
@@ -1,3 +1,21 @@
|
||||
# Next Release
|
||||
|
||||
- Fixed Windows saves not being able to be opened by a Mac (hopefully the other way too!)
|
||||
- Added the option to right click loaded snirf files to reveal them in a file browser or delete them if they are no longer desired
|
||||
- Changed the way folders are opened to store the files seperately rather than the folder as a whole to allow for the removal of files
|
||||
- Fixed issues with dropdowns and bubbles not populating correctly when opening a single file and temporarily removed the option to open multiple folders
|
||||
- Improved crash handling and the message that is displayed to the user if the application crashes
|
||||
- Progress bar will now colour the stage that fails as red if a file fails during processing
|
||||
- A warning message will be displayed when a file fails to process with information on what went wrong. This message does not halt the rest of the processing of the other files
|
||||
- Fixed the number of rectangles in the progress bar to 20 (was incorrect in v1.1.1)
|
||||
- Added validation to ensure loaded files do not have 2 dimensional data when clicking process to prevent inaccurate results from being generated
|
||||
- Added more metadata information to the top left information panel
|
||||
- Changed the Status Bar message when processing is complete to state how many were successful and how many were not
|
||||
- Added a clickable link below the selected file's metadata explaining the independent parameters and why they are useful
|
||||
- Updated some tooltips to provide better, more accurate information
|
||||
- Added details about the processing steps and their order into the user guide
|
||||
|
||||
|
||||
# Version 1.1.4
|
||||
|
||||
- Fixed some display text to now display the correct information
|
||||
|
||||
83
flares.py
83
flares.py
@@ -268,40 +268,42 @@ def set_metadata(file_path, metadata: dict[str, Any]) -> None:
|
||||
val = file_metadata.get(key, None)
|
||||
if val not in (None, '', [], {}, ()): # check for "empty" values
|
||||
globals()[key] = val
|
||||
from queue import Empty # This works with multiprocessing.Manager().Queue()
|
||||
|
||||
|
||||
def gui_entry(config: dict[str, Any], gui_queue: Queue, progress_queue: Queue) -> None:
|
||||
try:
|
||||
# Start a thread to forward progress messages back to GUI
|
||||
def forward_progress():
|
||||
while True:
|
||||
try:
|
||||
msg = progress_queue.get(timeout=1)
|
||||
if msg == "__done__":
|
||||
break
|
||||
gui_queue.put(msg)
|
||||
except:
|
||||
continue
|
||||
def forward_progress():
|
||||
while True:
|
||||
try:
|
||||
msg = progress_queue.get(timeout=1)
|
||||
if msg == "__done__":
|
||||
break
|
||||
gui_queue.put(msg)
|
||||
except Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
gui_queue.put({
|
||||
"type": "error",
|
||||
"error": f"Forwarding thread crashed: {e}",
|
||||
"traceback": traceback.format_exc()
|
||||
})
|
||||
break
|
||||
|
||||
t = threading.Thread(target=forward_progress, daemon=True)
|
||||
t.start()
|
||||
t = threading.Thread(target=forward_progress, daemon=True)
|
||||
t.start()
|
||||
|
||||
try:
|
||||
file_paths = config['SNIRF_FILES']
|
||||
file_params = config['PARAMS']
|
||||
file_metadata = config['METADATA']
|
||||
|
||||
max_workers = file_params.get("MAX_WORKERS", int(os.cpu_count()/4))
|
||||
|
||||
# Run the actual processing, with progress_queue passed down
|
||||
print("actual call")
|
||||
results = process_multiple_participants(file_paths, file_params, file_metadata, progress_queue, max_workers)
|
||||
results = process_multiple_participants(
|
||||
file_paths, file_params, file_metadata, progress_queue, max_workers
|
||||
)
|
||||
|
||||
# Signal end of progress
|
||||
progress_queue.put("__done__")
|
||||
t.join()
|
||||
|
||||
gui_queue.put({"success": True, "result": results})
|
||||
|
||||
|
||||
except Exception as e:
|
||||
gui_queue.put({
|
||||
"success": False,
|
||||
@@ -309,6 +311,14 @@ def gui_entry(config: dict[str, Any], gui_queue: Queue, progress_queue: Queue) -
|
||||
"traceback": traceback.format_exc()
|
||||
})
|
||||
|
||||
finally:
|
||||
# Always send done to the thread and avoid hanging
|
||||
try:
|
||||
progress_queue.put("__done__")
|
||||
except:
|
||||
pass
|
||||
t.join(timeout=5) # prevent permanent hang
|
||||
|
||||
|
||||
|
||||
def process_participant_worker(args):
|
||||
@@ -343,9 +353,16 @@ def process_multiple_participants(file_paths, file_params, file_metadata, progre
|
||||
try:
|
||||
file_path, result, error = future.result()
|
||||
if error:
|
||||
print(f"Error processing {file_path}: {error[0]}")
|
||||
print(error[1])
|
||||
error_message, error_traceback = error
|
||||
if progress_queue:
|
||||
progress_queue.put({
|
||||
"type": "error",
|
||||
"file": file_path,
|
||||
"error": error_message,
|
||||
"traceback": error_traceback
|
||||
})
|
||||
continue
|
||||
|
||||
results_by_file[file_path] = result
|
||||
except Exception as e:
|
||||
print(f"Unexpected error processing {file_path}: {e}")
|
||||
@@ -1053,7 +1070,8 @@ def filter_the_data(raw_haemo):
|
||||
average=True, xscale="log", color="r", show=False, amplitude=False
|
||||
)
|
||||
|
||||
raw_haemo = raw_haemo.filter(l_freq=None, h_freq=0.4, h_trans_bandwidth=0.2)
|
||||
#raw_haemo = raw_haemo.filter(l_freq=None, h_freq=0.4, h_trans_bandwidth=0.2)
|
||||
raw_haemo = raw_haemo.filter(0.05, 0.7, h_trans_bandwidth=0.2, l_trans_bandwidth=0.02)
|
||||
|
||||
raw_haemo.compute_psd(fmax=2).plot(
|
||||
average=True, xscale="log", axes=fig_filter.axes, color="g", amplitude=False, show=False
|
||||
@@ -2810,7 +2828,7 @@ def calculate_dpf(file_path):
|
||||
# order is hbo / hbr
|
||||
with h5py.File(file_path, 'r') as f:
|
||||
wavelengths = f['/nirs/probe/wavelengths'][:]
|
||||
logger.info("Wavelengths (nm):", wavelengths)
|
||||
logger.info(f"Wavelengths (nm): {wavelengths}")
|
||||
wavelengths = sorted(wavelengths, reverse=True)
|
||||
age = float(AGE)
|
||||
logger.info(f"Their age was {AGE}")
|
||||
@@ -3069,5 +3087,16 @@ def process_participant(file_path, progress_callback=None):
|
||||
|
||||
if progress_callback: progress_callback(20)
|
||||
logger.info("20")
|
||||
|
||||
sanitize_paths_for_pickle(raw_haemo, epochs)
|
||||
|
||||
return raw_haemo, epochs, fig_bytes, cha, contrast_results, df_ind, design_matrix, AGE, GENDER, GROUP, True
|
||||
return raw_haemo, epochs, fig_bytes, cha, contrast_results, df_ind, design_matrix, AGE, GENDER, GROUP, True
|
||||
|
||||
def sanitize_paths_for_pickle(raw_haemo, epochs):
|
||||
# Fix raw_haemo._filenames
|
||||
if hasattr(raw_haemo, '_filenames'):
|
||||
raw_haemo._filenames = [str(p) for p in raw_haemo._filenames]
|
||||
|
||||
# Fix epochs._raw._filenames
|
||||
if hasattr(epochs, '_raw') and hasattr(epochs._raw, '_filenames'):
|
||||
epochs._raw._filenames = [str(p) for p in epochs._raw._filenames]
|
||||
1
icons/folder_eye_24dp_1F1F1F.svg
Normal file
1
icons/folder_eye_24dp_1F1F1F.svg
Normal 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-800h240l80 80h320q33 0 56.5 23.5T880-640v242q-18-14-38-23t-42-19v-200H447l-80-80H160v480h120v80H160ZM640-40q-91 0-168-48T360-220q35-84 112-132t168-48q91 0 168 48t112 132q-35 84-112 132T640-40Zm0-80q57 0 107.5-26t82.5-74q-32-48-82.5-74T640-320q-57 0-107.5 26T450-220q32 48 82.5 74T640-120Zm0-40q-25 0-42.5-17.5T580-220q0-25 17.5-42.5T640-280q25 0 42.5 17.5T700-220q0 25-17.5 42.5T640-160Zm-480-80v-480 277-37 240Z"/></svg>
|
||||
|
After Width: | Height: | Size: 593 B |
1
icons/remove_24dp_1F1F1F.svg
Normal file
1
icons/remove_24dp_1F1F1F.svg
Normal 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="M200-440v-80h560v80H200Z"/></svg>
|
||||
|
After Width: | Height: | Size: 149 B |
573
main.py
573
main.py
@@ -19,7 +19,7 @@ import zipfile
|
||||
import platform
|
||||
import traceback
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePosixPath
|
||||
from datetime import datetime
|
||||
from multiprocessing import Process, current_process, freeze_support, Manager
|
||||
|
||||
@@ -34,13 +34,14 @@ from mne.io import read_raw_snirf
|
||||
from mne.preprocessing.nirs import source_detector_distances
|
||||
from mne_nirs.io import write_raw_snirf
|
||||
from mne.channels import make_dig_montage
|
||||
from mne_nirs.channels import get_short_channels # type: ignore
|
||||
from mne import Annotations
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QWidget, QMessageBox, QVBoxLayout, QHBoxLayout, QTextEdit, QScrollArea, QComboBox, QGridLayout,
|
||||
QPushButton, QMainWindow, QFileDialog, QLabel, QLineEdit, QFrame, QSizePolicy, QGroupBox, QDialog, QListView,
|
||||
QPushButton, QMainWindow, QFileDialog, QLabel, QLineEdit, QFrame, QSizePolicy, QGroupBox, QDialog, QListView, QMenu
|
||||
)
|
||||
from PySide6.QtCore import QThread, Signal, Qt, QTimer, QEvent, QSize
|
||||
from PySide6.QtCore import QThread, Signal, Qt, QTimer, QEvent, QSize, QPoint
|
||||
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
|
||||
|
||||
@@ -57,34 +58,34 @@ SECTIONS = [
|
||||
{
|
||||
"title": "Preprocessing",
|
||||
"params": [
|
||||
{"name": "SECONDS_TO_STRIP", "default": 0, "type": int, "help": "Seconds to remove from beginning of file. Setting this to 0 will remove nothing from the file."},
|
||||
{"name": "DOWNSAMPLE", "default": True, "type": bool, "help": "Downsample snirf files."},
|
||||
{"name": "DOWNSAMPLE_FREQUENCY", "default": 25, "type": int, "help": "Frequency (Hz) to downsample to. If this is set higher than the input data, new data will be interpolated."},
|
||||
{"name": "SECONDS_TO_STRIP", "default": 0, "type": int, "help": "Seconds to remove from beginning of all loaded snirf files. Setting this to 0 will remove nothing from the files."},
|
||||
{"name": "DOWNSAMPLE", "default": True, "type": bool, "help": "Should the snirf files be downsampled? If this is set to True, DOWNSAMPLE_FREQUENCY will be used as the target frequency to downsample to."},
|
||||
{"name": "DOWNSAMPLE_FREQUENCY", "default": 25, "type": int, "help": "Frequency (Hz) to downsample to. If this is set higher than the input data, new data will be interpolated. Only used if DOWNSAMPLE is set to True"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Scalp Coupling Index",
|
||||
"params": [
|
||||
{"name": "SCI", "default": True, "type": bool, "help": "Calculate Scalp Coupling Index."},
|
||||
{"name": "SCI_TIME_WINDOW", "default": 3, "type": int, "help": "SCI time window."},
|
||||
{"name": "SCI_THRESHOLD", "default": 0.6, "type": float, "help": "SCI threshold (0-1)."},
|
||||
{"name": "SCI", "default": True, "type": bool, "help": "Calculate and mark channels bad based on their Scalp Coupling Index. This metric calculates the quality of the connection between the optode and the scalp."},
|
||||
{"name": "SCI_TIME_WINDOW", "default": 3, "type": int, "help": "Independent SCI calculations will be perfomed in a time window for the duration of the value provided, until the end of the file is reached."},
|
||||
{"name": "SCI_THRESHOLD", "default": 0.6, "type": float, "help": "SCI threshold on a scale of 0-1. A value of 0 is bad coupling while a value of 1 is perfect coupling. If SCI is True, any channels lower than this value will be marked as bad."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Signal to Noise Ratio",
|
||||
"params": [
|
||||
{"name": "SNR", "default": True, "type": bool, "help": "Calculate Signal to Noise Ratio."},
|
||||
{"name": "SNR", "default": True, "type": bool, "help": "Calculate and mark channels bad based on their Signal to Noise Ratio. This metric calculates how much of the observed signal was noise versus how much of it was a useful signal."},
|
||||
# {"name": "SNR_TIME_WINDOW", "default": -1, "type": int, "help": "SNR time window."},
|
||||
{"name": "SNR_THRESHOLD", "default": 5.0, "type": float, "help": "SNR threshold (dB)."},
|
||||
{"name": "SNR_THRESHOLD", "default": 5.0, "type": float, "help": "SNR threshold (dB). A typical scale would be 0-25, but it is possible for values to be both above and below this range. Higher values correspond to a better signal. If SNR is True, any channels lower than this value will be marked as bad."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Peak Spectral Power",
|
||||
"params": [
|
||||
|
||||
{"name": "PSP", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
||||
{"name": "PSP_TIME_WINDOW", "default": 3, "type": int, "help": "PSP time window."},
|
||||
{"name": "PSP_THRESHOLD", "default": 0.1, "type": float, "help": "PSP threshold."},
|
||||
{"name": "PSP", "default": True, "type": bool, "help": "Calculate and mark channels bad based on their Peak Spectral Power. This metric calculates the amplitude or strength of a frequency component that is most prominent in a particular frequency range or spectrum."},
|
||||
{"name": "PSP_TIME_WINDOW", "default": 3, "type": int, "help": "Independent PSP calculations will be perfomed in a time window for the duration of the value provided, until the end of the file is reached."},
|
||||
{"name": "PSP_THRESHOLD", "default": 0.1, "type": float, "help": "PSP threshold. A typical scale would be 0-0.5, but it is possible for values to be above this range. Higher values correspond to a better signal. If PSP is True, any channels lower than this value will be marked as bad."},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -104,7 +105,7 @@ SECTIONS = [
|
||||
{
|
||||
"title": "Temporal Derivative Distribution Repair filtering",
|
||||
"params": [
|
||||
{"name": "TDDR", "default": True, "type": bool, "help": "Apply TDDR filtering."},
|
||||
{"name": "TDDR", "default": True, "type": bool, "help": "Apply Temporal Derivitave Distribution Repair filtering - a method that removes baseline shift and spike artifacts from the data."},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -128,7 +129,7 @@ SECTIONS = [
|
||||
{
|
||||
"title": "Short Channels",
|
||||
"params": [
|
||||
{"name": "SHORT_CHANNEL", "default": True, "type": bool, "help": "Does the data have a short channel?"},
|
||||
{"name": "SHORT_CHANNEL", "default": True, "type": bool, "help": "This should be set to True if the data has a short channel present in the data."},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -169,7 +170,7 @@ SECTIONS = [
|
||||
{
|
||||
"title": "Other",
|
||||
"params": [
|
||||
{"name": "MAX_WORKERS", "default": 4, "type": int, "help": "Number of files to process at once."},
|
||||
{"name": "MAX_WORKERS", "default": 4, "type": int, "help": "Number of files to be processed at once. Lowering this value may help on underpowered systems."},
|
||||
]
|
||||
},
|
||||
]
|
||||
@@ -445,13 +446,34 @@ class UserGuideWindow(QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent, Qt.WindowType.Window)
|
||||
self.setWindowTitle("User Guide for FLARES")
|
||||
self.setWindowTitle("User Guide - FLARES")
|
||||
self.resize(250, 100)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
label = QLabel("No user guide available yet!", self)
|
||||
label = QLabel("Progress Bar Stages:", self)
|
||||
label2 = QLabel("Stage 1: Load the snirf file\n"
|
||||
"Stage 2: Check the optode positions\n"
|
||||
"Stage 3: Scalp Coupling Index\n"
|
||||
"Stage 4: Signal to Noise Ratio\n"
|
||||
"Stage 5: Peak Spectral Power\n"
|
||||
"Stage 6: Identify bad channels\n"
|
||||
"Stage 7: Interpolate bad channels\n"
|
||||
"Stage 8: Optical Density\n"
|
||||
"Stage 9: Temporal Derivative Distribution Repair\n"
|
||||
"Stage 10: Beer Lambert Law\n"
|
||||
"Stage 11: Heart Rate Filtering\n"
|
||||
"Stage 12: Get Short/Long Channels\n"
|
||||
"Stage 13: Calculate Events from Annotations\n"
|
||||
"Stage 14: Epoch Calculations\n"
|
||||
"Stage 15: Design Matrix\n"
|
||||
"Stage 16: General Linear Model\n"
|
||||
"Stage 17: Generate Plots from the GLM\n"
|
||||
"Stage 18: Individual Significance\n"
|
||||
"Stage 19: Channel, Region of Interest, and Contrast Results\n"
|
||||
"Stage 20: Image Conversion\n", self)
|
||||
|
||||
layout.addWidget(label)
|
||||
layout.addWidget(label2)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
@@ -582,7 +604,7 @@ class UpdateOptodesWindow(QWidget):
|
||||
|
||||
def show_help_popup(self, text):
|
||||
msg = QMessageBox(self)
|
||||
msg.setWindowTitle("Parameter Info")
|
||||
msg.setWindowTitle("Parameter Info - FLARES")
|
||||
msg.setText(text)
|
||||
msg.exec()
|
||||
|
||||
@@ -863,7 +885,7 @@ class UpdateEventsWindow(QWidget):
|
||||
|
||||
def show_help_popup(self, text):
|
||||
msg = QMessageBox(self)
|
||||
msg.setWindowTitle("Parameter Info")
|
||||
msg.setWindowTitle("Parameter Info - FLARES")
|
||||
msg.setText(text)
|
||||
msg.exec()
|
||||
|
||||
@@ -1154,7 +1176,8 @@ class ProgressBubble(QWidget):
|
||||
"""
|
||||
|
||||
clicked = Signal(object)
|
||||
|
||||
rightClicked = Signal(object, QPoint)
|
||||
|
||||
def __init__(self, display_name, file_path):
|
||||
super().__init__()
|
||||
|
||||
@@ -1174,7 +1197,7 @@ class ProgressBubble(QWidget):
|
||||
self.progress_layout = QHBoxLayout()
|
||||
|
||||
self.rects = []
|
||||
for _ in range(19):
|
||||
for _ in range(20):
|
||||
rect = QFrame()
|
||||
rect.setFixedSize(10, 18)
|
||||
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
|
||||
@@ -1212,7 +1235,10 @@ class ProgressBubble(QWidget):
|
||||
rect.setStyleSheet("background-color: red; border: 1px solid gray;")
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.clicked.emit(self)
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit(self)
|
||||
elif event.button() == Qt.MouseButton.RightButton:
|
||||
self.rightClicked.emit(self, event.globalPosition().toPoint())
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def setSuffixText(self, suffix):
|
||||
@@ -1394,7 +1420,7 @@ class ParamSection(QWidget):
|
||||
|
||||
def show_help_popup(self, text):
|
||||
msg = QMessageBox(self)
|
||||
msg.setWindowTitle("Parameter Info")
|
||||
msg.setWindowTitle("Parameter Info - FLARES")
|
||||
msg.setText(text)
|
||||
msg.exec()
|
||||
|
||||
@@ -1529,52 +1555,40 @@ class ParamSection(QWidget):
|
||||
# You can customize how you display selected items here:
|
||||
widget.lineEdit().setText(", ".join(selected))
|
||||
|
||||
def update_annotation_dropdown_from_folder(self, folder_path):
|
||||
"""
|
||||
Reads all EEG files in the given folder, extracts annotations using MNE,
|
||||
and updates the dropdown for the `target_param` with the set of common annotations.
|
||||
|
||||
Args:
|
||||
folder_param (str): The name of the parameter holding the folder path.
|
||||
target_param (str): The name of the multi-select dropdown to update.
|
||||
"""
|
||||
# folder_path_widget = self.widgets.get(folder_param, {}).get("widget")
|
||||
# if not folder_path_widget:
|
||||
# print(f"[ParamSection] Folder path param '{folder_param}' not found.")
|
||||
# return
|
||||
|
||||
# folder_path = folder_path_widget.text().strip()
|
||||
# if not os.path.isdir(folder_path):
|
||||
# print(f"[ParamSection] '{folder_path}' is not a valid directory.")
|
||||
# return
|
||||
|
||||
def update_annotation_dropdown_from_loaded_files(self, bubble_widgets, button1):
|
||||
annotation_sets = []
|
||||
for filename in os.listdir(folder_path):
|
||||
full_path = os.path.join(folder_path, filename)
|
||||
|
||||
print(f"[ParamSection] Number of loaded bubbles: {len(bubble_widgets)}")
|
||||
for bubble in bubble_widgets.values():
|
||||
file_path = bubble.file_path
|
||||
print(f"[ParamSection] Trying file: {file_path}")
|
||||
try:
|
||||
raw = read_raw_snirf(full_path, preload=False, verbose="ERROR")
|
||||
raw = read_raw_snirf(file_path, preload=False, verbose="ERROR")
|
||||
annotations = raw.annotations
|
||||
if annotations is not None:
|
||||
print(f"[ParamSection] Found annotations with descriptions: {annotations.description}")
|
||||
labels = set(annotations.description)
|
||||
annotation_sets.append(labels)
|
||||
except Exception as e:
|
||||
print(f"[ParamSection] Skipping file '{filename}' due to error: {e}")
|
||||
else:
|
||||
print(f"[ParamSection] No annotations found in file: {file_path}")
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
if not annotation_sets:
|
||||
print(f"[ParamSection] No annotations found in folder '{folder_path}'")
|
||||
print("[ParamSection] No annotations found in loaded files")
|
||||
self.update_dropdown_items("REMOVE_EVENTS", [])
|
||||
button1.setVisible(False)
|
||||
return
|
||||
|
||||
# Get common annotations
|
||||
common_annotations = set.intersection(*annotation_sets) if len(annotation_sets) > 1 else annotation_sets[0]
|
||||
common_annotations = sorted(list(common_annotations)) # for consistent order
|
||||
common_annotations = sorted(list(common_annotations))
|
||||
|
||||
print(f"[ParamSection] Common annotations: {common_annotations}")
|
||||
|
||||
# Update the dropdown
|
||||
|
||||
self.update_dropdown_items("REMOVE_EVENTS", common_annotations)
|
||||
|
||||
|
||||
|
||||
class FullClickLineEdit(QLineEdit):
|
||||
def mousePressEvent(self, event):
|
||||
combo = self.parent()
|
||||
@@ -3869,6 +3883,11 @@ class MainApplication(QMainWindow):
|
||||
self.folder_paths = []
|
||||
self.section_widget = None
|
||||
self.first_run = True
|
||||
|
||||
self.files_total = 0 # total number of files to process
|
||||
self.files_done = set() # set of file paths done (success or fail)
|
||||
self.files_failed = set() # set of failed file paths
|
||||
self.files_results = {} # dict for successful results (if needed)
|
||||
|
||||
self.init_ui()
|
||||
self.create_menu_bar()
|
||||
@@ -3938,6 +3957,19 @@ class MainApplication(QMainWindow):
|
||||
right_column_layout.addWidget(label)
|
||||
right_column_layout.addWidget(field)
|
||||
|
||||
label_desc = QLabel('<a href="#">Why are these useful?</a>')
|
||||
label_desc.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
label_desc.setOpenExternalLinks(False)
|
||||
|
||||
def show_info_popup():
|
||||
QMessageBox.information(None, "Parameter Info - FLARES",
|
||||
"Age: Used to calculate the DPF factor.\nGender: Not currently used. "
|
||||
"Will be able to sort into groups by gender in the near future.\nGroup: Allows contrast "
|
||||
"images to be created comparing one group to another once the processing has completed.")
|
||||
|
||||
label_desc.linkActivated.connect(show_info_popup)
|
||||
right_column_layout.addWidget(label_desc)
|
||||
|
||||
right_column_layout.addStretch() # Push fields to top
|
||||
|
||||
self.right_column_widget.hide()
|
||||
@@ -4012,7 +4044,7 @@ class MainApplication(QMainWindow):
|
||||
self.button2.setMinimumSize(100, 40)
|
||||
self.button3.setMinimumSize(100, 40)
|
||||
|
||||
self.button1.setVisible(False)
|
||||
# self.button1.setVisible(False)
|
||||
self.button3.setVisible(False)
|
||||
|
||||
self.button1.clicked.connect(self.on_run_task)
|
||||
@@ -4062,7 +4094,7 @@ class MainApplication(QMainWindow):
|
||||
file_actions = [
|
||||
("Open File...", "Ctrl+O", self.open_file_dialog, resource_path("icons/file_open_24dp_1F1F1F.svg")),
|
||||
("Open Folder...", "Ctrl+Alt+O", self.open_folder_dialog, resource_path("icons/folder_24dp_1F1F1F.svg")),
|
||||
("Open Folders...", "Ctrl+Shift+O", self.open_multiple_folders_dialog, resource_path("icons/folder_copy_24dp_1F1F1F.svg")),
|
||||
# ("Open Folders...", "Ctrl+Shift+O", self.open_folder_dialog, resource_path("icons/folder_copy_24dp_1F1F1F.svg")),
|
||||
("Load Project...", "Ctrl+L", self.load_project, resource_path("icons/article_24dp_1F1F1F.svg")),
|
||||
("Save Project...", "Ctrl+S", self.save_project, resource_path("icons/save_24dp_1F1F1F.svg")),
|
||||
("Save Project As...", "Ctrl+Shift+S", self.save_project, resource_path("icons/save_as_24dp_1F1F1F.svg")),
|
||||
@@ -4070,7 +4102,7 @@ class MainApplication(QMainWindow):
|
||||
|
||||
for i, (name, shortcut, slot, icon) in enumerate(file_actions):
|
||||
file_menu.addAction(make_action(name, shortcut, slot, icon=icon))
|
||||
if i == 2: # after the first 3 actions (0,1,2)
|
||||
if i == 1: # after the first 3 actions (0,1,2)
|
||||
file_menu.addSeparator()
|
||||
|
||||
file_menu.addSeparator()
|
||||
@@ -4216,26 +4248,59 @@ class MainApplication(QMainWindow):
|
||||
|
||||
def open_file_dialog(self):
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self, "Open File", "", "All Files (*);;Text Files (*.txt)"
|
||||
self, "Open File", "", "SNIRF Files (*.snirf);;All Files (*)"
|
||||
)
|
||||
if file_path:
|
||||
self.selected_path = file_path # store the file path
|
||||
self.show_files_as_bubbles(file_path)
|
||||
# Create and display the bubble directly for this file
|
||||
display_name = os.path.basename(file_path)
|
||||
bubble = ProgressBubble(display_name, file_path)
|
||||
bubble.clicked.connect(self.on_bubble_clicked)
|
||||
bubble.rightClicked.connect(self.on_bubble_right_clicked)
|
||||
|
||||
if not hasattr(self, 'bubble_widgets'):
|
||||
self.bubble_widgets = {}
|
||||
if not hasattr(self, 'selected_paths'):
|
||||
self.selected_paths = []
|
||||
|
||||
file_path = os.path.normpath(file_path)
|
||||
self.bubble_widgets[file_path] = bubble
|
||||
self.selected_paths.append(file_path)
|
||||
|
||||
self.bubble_layout.addWidget(bubble)
|
||||
|
||||
for section_widget in self.param_sections:
|
||||
if hasattr(section_widget, 'update_annotation_dropdown_from_loaded_files'):
|
||||
if "REMOVE_EVENTS" in section_widget.widgets:
|
||||
section_widget.update_annotation_dropdown_from_loaded_files(self.bubble_widgets, self.button1)
|
||||
break
|
||||
else:
|
||||
print("[MainWindow] Could not find ParamSection with 'REMOVE_EVENTS' widget")
|
||||
|
||||
self.statusBar().showMessage(f"{file_path} loaded.")
|
||||
|
||||
self.button1.setVisible(True)
|
||||
|
||||
|
||||
def open_folder_dialog(self):
|
||||
folder_path = QFileDialog.getExistingDirectory(
|
||||
self, "Select Folder", ""
|
||||
)
|
||||
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder", "")
|
||||
|
||||
if folder_path:
|
||||
self.selected_path = folder_path # store the folder path
|
||||
snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")]
|
||||
|
||||
if not hasattr(self, 'selected_paths'):
|
||||
self.selected_paths = []
|
||||
|
||||
for file_path in snirf_files:
|
||||
if file_path not in self.selected_paths:
|
||||
self.selected_paths.append(file_path)
|
||||
|
||||
self.show_files_as_bubbles(folder_path)
|
||||
|
||||
for section_widget in self.param_sections:
|
||||
if "REMOVE_EVENTS" in section_widget.widgets:
|
||||
section_widget.update_annotation_dropdown_from_folder(folder_path)
|
||||
break
|
||||
if hasattr(section_widget, 'update_annotation_dropdown_from_loaded_files'):
|
||||
if "REMOVE_EVENTS" in section_widget.widgets:
|
||||
section_widget.update_annotation_dropdown_from_loaded_files(self.bubble_widgets, self.button1)
|
||||
break
|
||||
else:
|
||||
print("[MainWindow] Could not find ParamSection with 'REMOVE_EVENTS' widget")
|
||||
|
||||
@@ -4244,16 +4309,29 @@ class MainApplication(QMainWindow):
|
||||
|
||||
def open_multiple_folders_dialog(self):
|
||||
while True:
|
||||
folder = QFileDialog.getExistingDirectory(self, "Select Folder")
|
||||
if not folder:
|
||||
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
|
||||
if not folder_path:
|
||||
break
|
||||
|
||||
snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")]
|
||||
|
||||
if not hasattr(self, 'selected_paths'):
|
||||
self.selected_paths = []
|
||||
if folder not in self.selected_paths:
|
||||
self.selected_paths.append(folder)
|
||||
|
||||
for file_path in snirf_files:
|
||||
if file_path not in self.selected_paths:
|
||||
self.selected_paths.append(file_path)
|
||||
|
||||
self.show_files_as_bubbles(self.selected_paths)
|
||||
self.show_files_as_bubbles(folder_path)
|
||||
|
||||
for section_widget in self.param_sections:
|
||||
if hasattr(section_widget, 'update_annotation_dropdown_from_loaded_files'):
|
||||
if "REMOVE_EVENTS" in section_widget.widgets:
|
||||
section_widget.update_annotation_dropdown_from_loaded_files(self.bubble_widgets, self.button1)
|
||||
break
|
||||
else:
|
||||
print("[MainWindow] Could not find ParamSection with 'REMOVE_EVENTS' widget")
|
||||
|
||||
|
||||
# Ask if the user wants to add another
|
||||
more = QMessageBox.question(
|
||||
@@ -4267,21 +4345,42 @@ class MainApplication(QMainWindow):
|
||||
|
||||
self.button1.setVisible(True)
|
||||
|
||||
def save_project(self):
|
||||
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self, "Save Project", "", "FLARE Project (*.flare)"
|
||||
)
|
||||
if not filename:
|
||||
return
|
||||
def save_project(self, onCrash=False):
|
||||
|
||||
if not onCrash:
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self, "Save Project", "", "FLARE Project (*.flare)"
|
||||
)
|
||||
if not filename:
|
||||
return
|
||||
else:
|
||||
if PLATFORM_NAME == "darwin":
|
||||
filename = os.path.join(os.path.dirname(sys.executable), "../../../flares_autosave.flare")
|
||||
else:
|
||||
filename = os.path.join(os.getcwd(), "flares_autosave.flare")
|
||||
|
||||
try:
|
||||
project_data = {
|
||||
"file_list": [bubble.file_path for bubble in self.bubble_widgets.values()],
|
||||
"progress_states": {
|
||||
bubble.file_path: bubble.current_step for bubble in self.bubble_widgets.values()
|
||||
},
|
||||
# Ensure the filename has the proper extension
|
||||
if not filename.endswith(".flare"):
|
||||
filename += ".flare"
|
||||
|
||||
project_path = Path(filename).resolve()
|
||||
project_dir = project_path.parent
|
||||
|
||||
file_list = [
|
||||
str(PurePosixPath(Path(bubble.file_path).resolve().relative_to(project_dir)))
|
||||
for bubble in self.bubble_widgets.values()
|
||||
]
|
||||
|
||||
progress_states = {
|
||||
str(PurePosixPath(Path(bubble.file_path).resolve().relative_to(project_dir))): bubble.current_step
|
||||
for bubble in self.bubble_widgets.values()
|
||||
}
|
||||
|
||||
project_data = {
|
||||
"file_list": file_list,
|
||||
"progress_states": progress_states,
|
||||
"raw_haemo_dict": self.raw_haemo_dict,
|
||||
"epochs_dict": self.epochs_dict,
|
||||
"fig_bytes_dict": self.fig_bytes_dict,
|
||||
@@ -4294,6 +4393,17 @@ class MainApplication(QMainWindow):
|
||||
"group_dict": self.group_dict,
|
||||
"valid_dict": self.valid_dict,
|
||||
}
|
||||
|
||||
def sanitize(obj):
|
||||
if isinstance(obj, Path):
|
||||
return str(PurePosixPath(obj))
|
||||
elif isinstance(obj, dict):
|
||||
return {sanitize(k): sanitize(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [sanitize(i) for i in obj]
|
||||
return obj
|
||||
|
||||
project_data = sanitize(project_data)
|
||||
|
||||
with open(filename, "wb") as f:
|
||||
pickle.dump(project_data, f)
|
||||
@@ -4301,7 +4411,8 @@ class MainApplication(QMainWindow):
|
||||
QMessageBox.information(self, "Success", f"Project saved to:\n{filename}")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to save project:\n{e}")
|
||||
if not onCrash:
|
||||
QMessageBox.critical(self, "Error", f"Failed to save project:\n{e}")
|
||||
|
||||
|
||||
|
||||
@@ -4328,9 +4439,20 @@ class MainApplication(QMainWindow):
|
||||
self.group_dict = data.get("group_dict", {})
|
||||
self.valid_dict = data.get("valid_dict", {})
|
||||
|
||||
# Restore bubbles and progress
|
||||
self.show_files_as_bubbles_from_list(data["file_list"], data.get("progress_states", {}), filename)
|
||||
project_dir = Path(filename).parent
|
||||
|
||||
# Convert saved relative paths to absolute paths
|
||||
file_list = [str((project_dir / Path(rel_path)).resolve()) for rel_path in data["file_list"]]
|
||||
|
||||
# Also resolve progress_states with updated paths
|
||||
raw_progress = data.get("progress_states", {})
|
||||
progress_states = {
|
||||
str((project_dir / Path(rel_path)).resolve()): step
|
||||
for rel_path, step in raw_progress.items()
|
||||
}
|
||||
|
||||
self.show_files_as_bubbles_from_list(file_list, progress_states, filename)
|
||||
|
||||
# Re-enable buttons
|
||||
# self.button1.setVisible(True)
|
||||
self.button3.setVisible(True)
|
||||
@@ -4348,11 +4470,11 @@ class MainApplication(QMainWindow):
|
||||
folder_paths = [folder_paths]
|
||||
|
||||
# Clear previous bubbles
|
||||
while self.bubble_layout.count():
|
||||
item = self.bubble_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.deleteLater()
|
||||
# while self.bubble_layout.count():
|
||||
# item = self.bubble_layout.takeAt(0)
|
||||
# widget = item.widget()
|
||||
# if widget:
|
||||
# widget.deleteLater()
|
||||
|
||||
temp_bubble = ProgressBubble("Test Bubble", "") # A dummy bubble for measurement
|
||||
temp_bubble.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy. Preferred)
|
||||
@@ -4364,21 +4486,26 @@ class MainApplication(QMainWindow):
|
||||
cols = max(1, available_width // bubble_width) # Ensure at least 1 column
|
||||
|
||||
index = 0
|
||||
|
||||
if not hasattr(self, 'selected_paths'):
|
||||
self.selected_paths = []
|
||||
|
||||
for folder_path in folder_paths:
|
||||
if not os.path.isdir(folder_path):
|
||||
continue
|
||||
|
||||
files = os.listdir(folder_path)
|
||||
files = [f for f in files if os.path.isfile(os.path.join(folder_path, f))]
|
||||
|
||||
for filename in files:
|
||||
full_path = os.path.join(folder_path, filename)
|
||||
display_name = f"{os.path.basename(folder_path)} / {filename}"
|
||||
snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")]
|
||||
|
||||
for full_path in snirf_files:
|
||||
|
||||
display_name = f"{os.path.basename(folder_path)} / {os.path.basename(full_path)}"
|
||||
bubble = ProgressBubble(display_name, full_path)
|
||||
bubble.clicked.connect(self.on_bubble_clicked)
|
||||
self.bubble_widgets[filename] = bubble
|
||||
bubble.rightClicked.connect(self.on_bubble_right_clicked)
|
||||
|
||||
self.bubble_widgets[full_path] = bubble
|
||||
|
||||
if full_path not in self.selected_paths:
|
||||
self.selected_paths.append(full_path)
|
||||
|
||||
row = index // cols
|
||||
col = index % cols
|
||||
@@ -4418,6 +4545,7 @@ class MainApplication(QMainWindow):
|
||||
# Create bubble with full path
|
||||
bubble = ProgressBubble(display_name, file_path)
|
||||
bubble.clicked.connect(self.on_bubble_clicked)
|
||||
bubble.rightClicked.connect(self.on_bubble_right_clicked)
|
||||
self.bubble_widgets[file_path] = bubble
|
||||
|
||||
step = progress_states.get(file_path, 0)
|
||||
@@ -4506,6 +4634,77 @@ class MainApplication(QMainWindow):
|
||||
field.blockSignals(False)
|
||||
|
||||
|
||||
def on_bubble_right_clicked(self, bubble, global_pos):
|
||||
menu = QMenu(self)
|
||||
action1 = menu.addAction(QIcon(resource_path("icons/folder_eye_24dp_1F1F1F.svg")), "Reveal")
|
||||
action2 = menu.addAction(QIcon(resource_path("icons/remove_24dp_1F1F1F.svg")), "Remove")
|
||||
|
||||
action = menu.exec(global_pos)
|
||||
if action == action1:
|
||||
path = bubble.file_path
|
||||
if os.path.exists(path):
|
||||
if PLATFORM_NAME == "windows":
|
||||
subprocess.run(["explorer", "/select,", os.path.normpath(path)])
|
||||
elif PLATFORM_NAME == "darwin": # macOS
|
||||
subprocess.run(["open", "-R", path])
|
||||
else: # Linux
|
||||
folder = os.path.dirname(path)
|
||||
subprocess.run(["xdg-open", folder])
|
||||
else:
|
||||
print("File not found:", path)
|
||||
|
||||
elif action == action2:
|
||||
if self.button3.isVisible():
|
||||
reply = QMessageBox.warning(
|
||||
self,
|
||||
"Confirm Remove",
|
||||
"Are you sure you want to remove this file? This will remove the analysis option and the processing will have to be performed again.",
|
||||
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel
|
||||
)
|
||||
if reply != QMessageBox.StandardButton.Ok:
|
||||
return
|
||||
else:
|
||||
self.button3.setVisible(False)
|
||||
|
||||
self.top_left_widget.clear()
|
||||
self.right_column_widget.hide()
|
||||
|
||||
parent_layout = bubble.parent().layout()
|
||||
if parent_layout is not None:
|
||||
parent_layout.removeWidget(bubble)
|
||||
|
||||
key_to_delete = None
|
||||
for path, b in self.bubble_widgets.items():
|
||||
if b is bubble:
|
||||
key_to_delete = path
|
||||
break
|
||||
|
||||
if key_to_delete:
|
||||
del self.bubble_widgets[key_to_delete]
|
||||
|
||||
# Remove from selected_paths
|
||||
if hasattr(self, 'selected_paths'):
|
||||
try:
|
||||
self.selected_paths.remove(bubble.file_path)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Remove from selected_path (if used)
|
||||
if hasattr(self, 'selected_path') and self.selected_path == bubble.file_path:
|
||||
self.selected_path = None
|
||||
|
||||
for section_widget in self.param_sections:
|
||||
if hasattr(section_widget, 'update_annotation_dropdown_from_loaded_files'):
|
||||
if "REMOVE_EVENTS" in section_widget.widgets:
|
||||
section_widget.update_annotation_dropdown_from_loaded_files(self.bubble_widgets, self.button1)
|
||||
break
|
||||
|
||||
bubble.setParent(None)
|
||||
bubble.deleteLater()
|
||||
|
||||
if getattr(self, 'last_clicked_bubble', None) is bubble:
|
||||
self.last_clicked_bubble = None
|
||||
|
||||
def eventFilter(self, watched, event):
|
||||
if event.type() == QEvent.Type.MouseButtonPress:
|
||||
widget = self.childAt(event.pos())
|
||||
@@ -4587,10 +4786,11 @@ class MainApplication(QMainWindow):
|
||||
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()
|
||||
pass
|
||||
# bubble.mark_cancelled()
|
||||
self.first_run = False
|
||||
|
||||
# Collect all selected snirf files in a flat list
|
||||
@@ -4617,7 +4817,41 @@ class MainApplication(QMainWindow):
|
||||
if not snirf_files:
|
||||
raise ValueError("No .snirf files found in selection")
|
||||
|
||||
# validate
|
||||
for i in snirf_files:
|
||||
x_coords = set()
|
||||
y_coords = set()
|
||||
z_coords = set()
|
||||
raw = read_raw_snirf(i)
|
||||
dig = raw.info.get('dig', None)
|
||||
if dig is not None:
|
||||
for point in dig:
|
||||
if point['kind'] == 3:
|
||||
coord = point['r']
|
||||
x_coords.add(coord[0])
|
||||
y_coords.add(coord[1])
|
||||
z_coords.add(coord[2])
|
||||
print(f"Coord: {coord}")
|
||||
is_2d = (
|
||||
all(abs(x) < 1e-6 for x in x_coords) or
|
||||
all(abs(y) < 1e-6 for y in y_coords) or
|
||||
all(abs(z) < 1e-6 for z in z_coords)
|
||||
)
|
||||
|
||||
if is_2d:
|
||||
QMessageBox.critical(None, "Error - 2D Data Detected - FLARES", f"Error: 2 dimensional data was found in {i}. "
|
||||
"It is not possible to process this file. Please update the coordinates "
|
||||
"using the 'Update optodes in snirf file...' option from the Options menu or by pressing 'F6'.")
|
||||
self.button1.clicked.disconnect(self.cancel_task)
|
||||
self.button1.setText("Process")
|
||||
self.button1.clicked.connect(self.on_run_task)
|
||||
return
|
||||
|
||||
self.files_total = len(snirf_files)
|
||||
self.files_done = set()
|
||||
self.files_failed = set()
|
||||
self.files_results = {}
|
||||
|
||||
all_params = {}
|
||||
for section_widget in self.param_sections:
|
||||
section_params = section_widget.get_param_values()
|
||||
@@ -4659,7 +4893,12 @@ class MainApplication(QMainWindow):
|
||||
|
||||
if isinstance(msg, dict):
|
||||
if msg.get("success"):
|
||||
|
||||
results = msg["result"] # from flares.py
|
||||
|
||||
for file_path, result_tuple in results.items():
|
||||
self.files_done.add(file_path)
|
||||
self.files_results[file_path] = result_tuple
|
||||
|
||||
# Initialize storage
|
||||
# TODO: Is this check needed?
|
||||
@@ -4691,26 +4930,84 @@ class MainApplication(QMainWindow):
|
||||
self.valid_dict[file_path] = valid
|
||||
|
||||
# self.statusbar.showMessage(f"Processing complete! Time elapsed: {elapsed_time:.2f} seconds")
|
||||
self.statusbar.showMessage(f"Processing complete!")
|
||||
# self.statusbar.showMessage(f"Processing complete!")
|
||||
|
||||
self.button3.setVisible(True)
|
||||
|
||||
|
||||
else:
|
||||
elif msg.get("success") is False:
|
||||
error_msg = msg.get("error", "Unknown error")
|
||||
print("Error during processing:", error_msg)
|
||||
self.statusbar.showMessage(f"Processing failed! {error_msg}")
|
||||
traceback_str = msg.get("traceback", "")
|
||||
self.show_error_popup("Processing failed!", error_msg, traceback_str)
|
||||
self.files_done = set(self.files_results.keys())
|
||||
self.statusbar.showMessage(f"Processing failed!")
|
||||
self.result_timer.stop()
|
||||
self.cleanup_after_process()
|
||||
return
|
||||
|
||||
self.result_timer.stop()
|
||||
elif msg.get("type") == "error":
|
||||
# Error forwarded from a single file (e.g. from a worker)
|
||||
file_path = msg.get("file", "Unknown file")
|
||||
error_msg = msg.get("error", "Unknown error")
|
||||
traceback_str = msg.get("traceback", "")
|
||||
|
||||
self.files_done.add(file_path)
|
||||
self.files_failed.add(file_path)
|
||||
|
||||
self.show_error_popup(f"{file_path}", error_msg, traceback_str)
|
||||
self.statusbar.showMessage(f"Error processing {file_path}")
|
||||
if file_path in self.bubble_widgets:
|
||||
self.bubble_widgets[file_path].mark_cancelled()
|
||||
|
||||
|
||||
self.cleanup_after_process()
|
||||
return
|
||||
|
||||
elif isinstance(msg, tuple) and msg[0] == 'progress':
|
||||
_, file_path, step_index = msg
|
||||
file_name = os.path.basename(file_path) # extract file name
|
||||
self.progress_update_signal.emit(file_name, step_index)
|
||||
self.progress_update_signal.emit(file_path, step_index)
|
||||
|
||||
|
||||
if len(self.files_done) == self.files_total:
|
||||
self.result_timer.stop()
|
||||
self.cleanup_after_process()
|
||||
|
||||
success_count = len(self.files_results)
|
||||
fail_count = len(self.files_failed)
|
||||
|
||||
summary_msg = f"Processing complete: {success_count} succeeded, {fail_count} failed."
|
||||
self.statusbar.showMessage(summary_msg)
|
||||
|
||||
if success_count > 0:
|
||||
self.button3.setVisible(True)
|
||||
|
||||
self.button1.clicked.disconnect(self.cancel_task)
|
||||
self.button1.setText("Process")
|
||||
self.button1.clicked.connect(self.on_run_task)
|
||||
|
||||
|
||||
def show_error_popup(self, title, error_message, traceback_str=""):
|
||||
msgbox = QMessageBox(self)
|
||||
msgbox.setIcon(QMessageBox.Warning)
|
||||
msgbox.setWindowTitle("Warning - FLARES")
|
||||
|
||||
message = (
|
||||
f"FLARES has encountered an error processing the file {title}.<br><br>"
|
||||
"This error was likely due to incorrect parameters on the right side of the screen and not an error with your data. "
|
||||
"Processing of the remaining files continues in the background and this participant will be ignored in the analysis. "
|
||||
"If you think the parameters on the right side are correct for your data, raise an issue <a href='https://git.research.dezeeuw.ca/tyler/flares/issues'>here</a>.<br><br>"
|
||||
f"Error message: {error_message}"
|
||||
)
|
||||
|
||||
msgbox.setTextFormat(Qt.TextFormat.RichText)
|
||||
msgbox.setText(message)
|
||||
msgbox.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
|
||||
# Add traceback to detailed text
|
||||
if traceback_str:
|
||||
msgbox.setDetailedText(traceback_str)
|
||||
|
||||
msgbox.setStandardButtons(QMessageBox.Ok)
|
||||
msgbox.exec_()
|
||||
|
||||
|
||||
def cleanup_after_process(self):
|
||||
|
||||
if hasattr(self, 'result_process'):
|
||||
@@ -4738,8 +5035,9 @@ class MainApplication(QMainWindow):
|
||||
self.manager.shutdown()
|
||||
|
||||
|
||||
def update_file_progress(self, filename, step_index):
|
||||
bubble = self.bubble_widgets.get(filename)
|
||||
def update_file_progress(self, file_path, step_index):
|
||||
key = os.path.normpath(file_path)
|
||||
bubble = self.bubble_widgets.get(key)
|
||||
if bubble:
|
||||
bubble.update_progress(step_index)
|
||||
|
||||
@@ -4895,6 +5193,14 @@ class MainApplication(QMainWindow):
|
||||
# Measurement date
|
||||
snirf_info['Measurement Date'] = str(raw.info.get('meas_date'))
|
||||
|
||||
try:
|
||||
short_chans = get_short_channels(raw, max_dist=0.015)
|
||||
snirf_info['Short Channels'] = f"Likely - {short_chans.ch_names}"
|
||||
if len(short_chans.ch_names) > 6:
|
||||
snirf_info['Short Channels'] += "\n There are a lot of short channels. Optode distances are likely incorrect!"
|
||||
except:
|
||||
snirf_info['Short Channels'] = "Unlikely"
|
||||
|
||||
# Source-detector distances
|
||||
distances = source_detector_distances(raw.info)
|
||||
distance_info = []
|
||||
@@ -5169,11 +5475,50 @@ def exception_hook(exc_type, exc_value, exc_traceback):
|
||||
if app is None:
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
QMessageBox.critical(None, "Unexpected Error", f"An unhandled exception occurred:\n\n{error_msg}")
|
||||
show_critical_error(error_msg)
|
||||
|
||||
# Exit the app after user acknowledges
|
||||
sys.exit(1)
|
||||
|
||||
def show_critical_error(error_msg):
|
||||
msg_box = QMessageBox()
|
||||
msg_box.setIcon(QMessageBox.Icon.Critical)
|
||||
msg_box.setWindowTitle("Something went wrong!")
|
||||
|
||||
if PLATFORM_NAME == "darwin":
|
||||
log_path = os.path.join(os.path.dirname(sys.executable), "../../../flares.log")
|
||||
log_path2 = os.path.join(os.path.dirname(sys.executable), "../../../flares_error.log")
|
||||
save_path = os.path.join(os.path.dirname(sys.executable), "../../../flares_autosave.flare")
|
||||
|
||||
else:
|
||||
log_path = os.path.join(os.getcwd(), "flares.log")
|
||||
log_path2 = os.path.join(os.getcwd(), "flares_error.log")
|
||||
save_path = os.path.join(os.getcwd(), "flares_autosave.flare")
|
||||
|
||||
|
||||
shutil.copy(log_path, log_path2)
|
||||
log_path2 = Path(log_path2).absolute().as_posix()
|
||||
autosave_path = Path(save_path).absolute().as_posix()
|
||||
log_link = f"file:///{log_path2}"
|
||||
autosave_link = f"file:///{autosave_path}"
|
||||
|
||||
window.save_project(True)
|
||||
|
||||
message = (
|
||||
"FLARES has encountered an unrecoverable error and needs to close.<br><br>"
|
||||
f"We are sorry for the inconvenience. An autosave was attempted to be saved to <a href='{autosave_link}'>{autosave_path}</a>, but it may not have been saved. "
|
||||
"If the file was saved, it still may not be intact, openable, or contain the correct data. Use the autosave at your discretion.<br><br>"
|
||||
"This unrecoverable error was likely due to an error with FLARES and not your data.<br>"
|
||||
f"Please raise an issue <a href='https://git.research.dezeeuw.ca/tyler/flares/issues'>here</a> and attach the error file located at <a href='{log_link}'>{log_path2}</a><br><br>"
|
||||
f"<pre>{error_msg}</pre>"
|
||||
)
|
||||
|
||||
msg_box.setTextFormat(Qt.TextFormat.RichText)
|
||||
msg_box.setText(message)
|
||||
msg_box.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
|
||||
msg_box.exec()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Redirect exceptions to the popup window
|
||||
|
||||
Reference in New Issue
Block a user