Compare commits
5 Commits
v1.1.4
...
45c6176dba
| Author | SHA1 | Date | |
|---|---|---|---|
| 45c6176dba | |||
| a4bbdb90c8 | |||
| 953ea90c67 | |||
| 20b255321b | |||
| b5afcec37d |
26
changelog.md
26
changelog.md
@@ -1,3 +1,29 @@
|
|||||||
|
# Version 1.1.6
|
||||||
|
|
||||||
|
- Fixed Process button from appearing when no files are selected
|
||||||
|
- Fix for instand child process crash on Windows
|
||||||
|
- Added L_FREQ and H_FREQ parameters for more user control over low and high pass filtering
|
||||||
|
|
||||||
|
|
||||||
|
# Version 1.1.5
|
||||||
|
|
||||||
|
- 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
|
||||||
|
- Changed the default bandpass filtering parameters
|
||||||
|
|
||||||
|
|
||||||
# Version 1.1.4
|
# Version 1.1.4
|
||||||
|
|
||||||
- Fixed some display text to now display the correct information
|
- Fixed some display text to now display the correct information
|
||||||
|
|||||||
137
flares.py
137
flares.py
@@ -50,8 +50,8 @@ from scipy.spatial.distance import cdist
|
|||||||
|
|
||||||
# Backen visualization needed to be defined for pyinstaller
|
# Backen visualization needed to be defined for pyinstaller
|
||||||
import pyvistaqt # type: ignore
|
import pyvistaqt # type: ignore
|
||||||
# import vtkmodules.util.data_model
|
import vtkmodules.util.data_model
|
||||||
# import vtkmodules.util.execution_model
|
import vtkmodules.util.execution_model
|
||||||
|
|
||||||
# External library imports for mne
|
# External library imports for mne
|
||||||
from mne import (
|
from mne import (
|
||||||
@@ -130,6 +130,9 @@ TDDR: bool
|
|||||||
|
|
||||||
ENHANCE_NEGATIVE_CORRELATION: bool
|
ENHANCE_NEGATIVE_CORRELATION: bool
|
||||||
|
|
||||||
|
L_FREQ: float
|
||||||
|
H_FREQ: float
|
||||||
|
|
||||||
SHORT_CHANNEL: bool
|
SHORT_CHANNEL: bool
|
||||||
|
|
||||||
REMOVE_EVENTS: list
|
REMOVE_EVENTS: list
|
||||||
@@ -186,7 +189,9 @@ REQUIRED_KEYS: dict[str, Any] = {
|
|||||||
"SHORT_CHANNEL": bool,
|
"SHORT_CHANNEL": bool,
|
||||||
"REMOVE_EVENTS": list,
|
"REMOVE_EVENTS": list,
|
||||||
"TIME_WINDOW_START": int,
|
"TIME_WINDOW_START": int,
|
||||||
"TIME_WINDOW_END": int
|
"TIME_WINDOW_END": int,
|
||||||
|
"L_FREQ": float,
|
||||||
|
"H_FREQ": float,
|
||||||
# "REJECT_PAIRS": bool,
|
# "REJECT_PAIRS": bool,
|
||||||
# "FORCE_DROP_ANNOTATIONS": list,
|
# "FORCE_DROP_ANNOTATIONS": list,
|
||||||
# "FILTER_LOW_PASS": float,
|
# "FILTER_LOW_PASS": float,
|
||||||
@@ -268,40 +273,42 @@ def set_metadata(file_path, metadata: dict[str, Any]) -> None:
|
|||||||
val = file_metadata.get(key, None)
|
val = file_metadata.get(key, None)
|
||||||
if val not in (None, '', [], {}, ()): # check for "empty" values
|
if val not in (None, '', [], {}, ()): # check for "empty" values
|
||||||
globals()[key] = val
|
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:
|
def gui_entry(config: dict[str, Any], gui_queue: Queue, progress_queue: Queue) -> None:
|
||||||
try:
|
def forward_progress():
|
||||||
# Start a thread to forward progress messages back to GUI
|
while True:
|
||||||
def forward_progress():
|
try:
|
||||||
while True:
|
msg = progress_queue.get(timeout=1)
|
||||||
try:
|
if msg == "__done__":
|
||||||
msg = progress_queue.get(timeout=1)
|
break
|
||||||
if msg == "__done__":
|
gui_queue.put(msg)
|
||||||
break
|
except Empty:
|
||||||
gui_queue.put(msg)
|
continue
|
||||||
except:
|
except Exception as e:
|
||||||
continue
|
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 = threading.Thread(target=forward_progress, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
try:
|
||||||
file_paths = config['SNIRF_FILES']
|
file_paths = config['SNIRF_FILES']
|
||||||
file_params = config['PARAMS']
|
file_params = config['PARAMS']
|
||||||
file_metadata = config['METADATA']
|
file_metadata = config['METADATA']
|
||||||
|
|
||||||
max_workers = file_params.get("MAX_WORKERS", int(os.cpu_count()/4))
|
max_workers = file_params.get("MAX_WORKERS", int(os.cpu_count()/4))
|
||||||
|
|
||||||
# Run the actual processing, with progress_queue passed down
|
results = process_multiple_participants(
|
||||||
print("actual call")
|
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})
|
gui_queue.put({"success": True, "result": results})
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
gui_queue.put({
|
gui_queue.put({
|
||||||
"success": False,
|
"success": False,
|
||||||
@@ -309,6 +316,14 @@ def gui_entry(config: dict[str, Any], gui_queue: Queue, progress_queue: Queue) -
|
|||||||
"traceback": traceback.format_exc()
|
"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):
|
def process_participant_worker(args):
|
||||||
@@ -343,9 +358,16 @@ def process_multiple_participants(file_paths, file_params, file_metadata, progre
|
|||||||
try:
|
try:
|
||||||
file_path, result, error = future.result()
|
file_path, result, error = future.result()
|
||||||
if error:
|
if error:
|
||||||
print(f"Error processing {file_path}: {error[0]}")
|
error_message, error_traceback = error
|
||||||
print(error[1])
|
if progress_queue:
|
||||||
|
progress_queue.put({
|
||||||
|
"type": "error",
|
||||||
|
"file": file_path,
|
||||||
|
"error": error_message,
|
||||||
|
"traceback": error_traceback
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
results_by_file[file_path] = result
|
results_by_file[file_path] = result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Unexpected error processing {file_path}: {e}")
|
print(f"Unexpected error processing {file_path}: {e}")
|
||||||
@@ -1052,8 +1074,17 @@ def filter_the_data(raw_haemo):
|
|||||||
fig_filter = raw_haemo.compute_psd(fmax=2).plot(
|
fig_filter = raw_haemo.compute_psd(fmax=2).plot(
|
||||||
average=True, xscale="log", color="r", show=False, amplitude=False
|
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)
|
if L_FREQ == 0 and H_FREQ != 0:
|
||||||
|
raw_haemo = raw_haemo.filter(l_freq=None, h_freq=H_FREQ, h_trans_bandwidth=0.02)
|
||||||
|
elif L_FREQ != 0 and H_FREQ == 0:
|
||||||
|
raw_haemo = raw_haemo.filter(l_freq=L_FREQ, h_freq=None, l_trans_bandwidth=0.002)
|
||||||
|
elif L_FREQ != 0 and H_FREQ == 0:
|
||||||
|
raw_haemo = raw_haemo.filter(l_freq=L_FREQ, h_freq=H_FREQ, l_trans_bandwidth=0.002, h_trans_bandwidth=0.02)
|
||||||
|
|
||||||
|
#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.7, h_trans_bandwidth=0.2)
|
||||||
|
#raw_haemo = raw_haemo.filter(0.005, 0.7, h_trans_bandwidth=0.02, l_trans_bandwidth=0.002)
|
||||||
|
|
||||||
raw_haemo.compute_psd(fmax=2).plot(
|
raw_haemo.compute_psd(fmax=2).plot(
|
||||||
average=True, xscale="log", axes=fig_filter.axes, color="g", amplitude=False, show=False
|
average=True, xscale="log", axes=fig_filter.axes, color="g", amplitude=False, show=False
|
||||||
@@ -1499,7 +1530,6 @@ def fold_channels(raw: BaseRaw) -> None:
|
|||||||
# Format the output to make it slightly easier to read
|
# Format the output to make it slightly easier to read
|
||||||
|
|
||||||
if True:
|
if True:
|
||||||
|
|
||||||
num_channels = len(hbo_channel_names)
|
num_channels = len(hbo_channel_names)
|
||||||
rows, cols = 4, 7 # 6 rows and 4 columns of pie charts
|
rows, cols = 4, 7 # 6 rows and 4 columns of pie charts
|
||||||
fig, axes = plt.subplots(rows, cols, figsize=(16, 10), constrained_layout=True)
|
fig, axes = plt.subplots(rows, cols, figsize=(16, 10), constrained_layout=True)
|
||||||
@@ -2259,21 +2289,25 @@ def brain_landmarks_3d(raw_haemo: BaseRaw, show_optodes: Literal['sensors', 'lab
|
|||||||
|
|
||||||
|
|
||||||
if show_brodmann:# Add Brodmann labels
|
if show_brodmann:# Add Brodmann labels
|
||||||
labels = cast(list[Label], read_labels_from_annot("fsaverage", "PALS_B12_Brodmann", "rh", verbose=False)) # type: ignore
|
labels = cast(list[Label], read_labels_from_annot("fsaverage", "PALS_B12_Brodmann", "lh", verbose=False)) # type: ignore
|
||||||
|
|
||||||
label_colors = {
|
label_colors = {
|
||||||
"Brodmann.39-rh": "blue",
|
"Brodmann.1-lh": "red",
|
||||||
"Brodmann.40-rh": "green",
|
"Brodmann.2-lh": "red",
|
||||||
"Brodmann.6-rh": "pink",
|
"Brodmann.3-lh": "red",
|
||||||
"Brodmann.7-rh": "orange",
|
"Brodmann.4-lh": "orange",
|
||||||
"Brodmann.17-rh": "red",
|
"Brodmann.5-lh": "green",
|
||||||
"Brodmann.1-rh": "yellow",
|
"Brodmann.6-lh": "yellow",
|
||||||
"Brodmann.2-rh": "yellow",
|
"Brodmann.7-lh": "green",
|
||||||
"Brodmann.3-rh": "yellow",
|
"Brodmann.17-lh": "blue",
|
||||||
"Brodmann.18-rh": "red",
|
"Brodmann.18-lh": "blue",
|
||||||
"Brodmann.19-rh": "red",
|
"Brodmann.19-lh": "blue",
|
||||||
"Brodmann.4-rh": "purple",
|
"Brodmann.39-lh": "purple",
|
||||||
"Brodmann.8-rh": "white"
|
"Brodmann.40-lh": "pink",
|
||||||
|
"Brodmann.42-lh": "white",
|
||||||
|
"Brodmann.44-lh": "white",
|
||||||
|
"Brodmann.48-lh": "white",
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for label in labels:
|
for label in labels:
|
||||||
@@ -2810,7 +2844,7 @@ def calculate_dpf(file_path):
|
|||||||
# order is hbo / hbr
|
# order is hbo / hbr
|
||||||
with h5py.File(file_path, 'r') as f:
|
with h5py.File(file_path, 'r') as f:
|
||||||
wavelengths = f['/nirs/probe/wavelengths'][:]
|
wavelengths = f['/nirs/probe/wavelengths'][:]
|
||||||
logger.info("Wavelengths (nm):", wavelengths)
|
logger.info(f"Wavelengths (nm): {wavelengths}")
|
||||||
wavelengths = sorted(wavelengths, reverse=True)
|
wavelengths = sorted(wavelengths, reverse=True)
|
||||||
age = float(AGE)
|
age = float(AGE)
|
||||||
logger.info(f"Their age was {AGE}")
|
logger.info(f"Their age was {AGE}")
|
||||||
@@ -2926,7 +2960,7 @@ def process_participant(file_path, progress_callback=None):
|
|||||||
|
|
||||||
# Step 11: Get short / long channels
|
# Step 11: Get short / long channels
|
||||||
if SHORT_CHANNEL:
|
if SHORT_CHANNEL:
|
||||||
short_chans = get_short_channels(raw_haemo, max_dist=0.015)
|
short_chans = get_short_channels(raw_haemo, max_dist=0.02)
|
||||||
fig_short_chans = short_chans.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="Short Channels Only", show=False)
|
fig_short_chans = short_chans.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="Short Channels Only", show=False)
|
||||||
fig_individual["short"] = fig_short_chans
|
fig_individual["short"] = fig_short_chans
|
||||||
else:
|
else:
|
||||||
@@ -3069,5 +3103,16 @@ def process_participant(file_path, progress_callback=None):
|
|||||||
|
|
||||||
if progress_callback: progress_callback(20)
|
if progress_callback: progress_callback(20)
|
||||||
logger.info("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 |
574
main.py
574
main.py
@@ -19,7 +19,7 @@ import zipfile
|
|||||||
import platform
|
import platform
|
||||||
import traceback
|
import traceback
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path, PurePosixPath
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from multiprocessing import Process, current_process, freeze_support, Manager
|
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.preprocessing.nirs import source_detector_distances
|
||||||
from mne_nirs.io import write_raw_snirf
|
from mne_nirs.io import write_raw_snirf
|
||||||
from mne.channels import make_dig_montage
|
from mne.channels import make_dig_montage
|
||||||
|
from mne_nirs.channels import get_short_channels # type: ignore
|
||||||
from mne import Annotations
|
from mne import Annotations
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication, QWidget, QMessageBox, QVBoxLayout, QHBoxLayout, QTextEdit, QScrollArea, QComboBox, QGridLayout,
|
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.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleValidator, QPixmap, QStandardItemModel, QStandardItem
|
||||||
from PySide6.QtSvgWidgets import QSvgWidget # needed to show svgs when app is not frozen
|
from PySide6.QtSvgWidgets import QSvgWidget # needed to show svgs when app is not frozen
|
||||||
|
|
||||||
@@ -57,34 +58,34 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Preprocessing",
|
"title": "Preprocessing",
|
||||||
"params": [
|
"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": "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": "Downsample snirf 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."},
|
{"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",
|
"title": "Scalp Coupling Index",
|
||||||
"params": [
|
"params": [
|
||||||
{"name": "SCI", "default": True, "type": bool, "help": "Calculate Scalp Coupling Index."},
|
{"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": "SCI time window."},
|
{"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 (0-1)."},
|
{"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",
|
"title": "Signal to Noise Ratio",
|
||||||
"params": [
|
"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_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",
|
"title": "Peak Spectral Power",
|
||||||
"params": [
|
"params": [
|
||||||
|
|
||||||
{"name": "PSP", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
{"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": "PSP time window."},
|
{"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."},
|
{"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",
|
"title": "Temporal Derivative Distribution Repair filtering",
|
||||||
"params": [
|
"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."},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -122,13 +123,16 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Filtering",
|
"title": "Filtering",
|
||||||
"params": [
|
"params": [
|
||||||
|
{"name": "L_FREQ", "default": 0.005, "type": float, "help": "Any frequencies lower than this value will be removed."},
|
||||||
|
{"name": "H_FREQ", "default": 0.7, "type": float, "help": "Any frequencies higher than this value will be removed."},
|
||||||
#{"name": "FILTER", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
#{"name": "FILTER", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Short Channels",
|
"title": "Short Channels",
|
||||||
"params": [
|
"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 +173,7 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Other",
|
"title": "Other",
|
||||||
"params": [
|
"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 +449,34 @@ class UserGuideWindow(QWidget):
|
|||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent, Qt.WindowType.Window)
|
super().__init__(parent, Qt.WindowType.Window)
|
||||||
self.setWindowTitle("User Guide for FLARES")
|
self.setWindowTitle("User Guide - FLARES")
|
||||||
self.resize(250, 100)
|
self.resize(250, 100)
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
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(label)
|
||||||
|
layout.addWidget(label2)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
@@ -582,7 +607,7 @@ class UpdateOptodesWindow(QWidget):
|
|||||||
|
|
||||||
def show_help_popup(self, text):
|
def show_help_popup(self, text):
|
||||||
msg = QMessageBox(self)
|
msg = QMessageBox(self)
|
||||||
msg.setWindowTitle("Parameter Info")
|
msg.setWindowTitle("Parameter Info - FLARES")
|
||||||
msg.setText(text)
|
msg.setText(text)
|
||||||
msg.exec()
|
msg.exec()
|
||||||
|
|
||||||
@@ -863,7 +888,7 @@ class UpdateEventsWindow(QWidget):
|
|||||||
|
|
||||||
def show_help_popup(self, text):
|
def show_help_popup(self, text):
|
||||||
msg = QMessageBox(self)
|
msg = QMessageBox(self)
|
||||||
msg.setWindowTitle("Parameter Info")
|
msg.setWindowTitle("Parameter Info - FLARES")
|
||||||
msg.setText(text)
|
msg.setText(text)
|
||||||
msg.exec()
|
msg.exec()
|
||||||
|
|
||||||
@@ -1154,7 +1179,8 @@ class ProgressBubble(QWidget):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
clicked = Signal(object)
|
clicked = Signal(object)
|
||||||
|
rightClicked = Signal(object, QPoint)
|
||||||
|
|
||||||
def __init__(self, display_name, file_path):
|
def __init__(self, display_name, file_path):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@@ -1174,7 +1200,7 @@ class ProgressBubble(QWidget):
|
|||||||
self.progress_layout = QHBoxLayout()
|
self.progress_layout = QHBoxLayout()
|
||||||
|
|
||||||
self.rects = []
|
self.rects = []
|
||||||
for _ in range(19):
|
for _ in range(20):
|
||||||
rect = QFrame()
|
rect = QFrame()
|
||||||
rect.setFixedSize(10, 18)
|
rect.setFixedSize(10, 18)
|
||||||
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
|
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
|
||||||
@@ -1212,7 +1238,10 @@ class ProgressBubble(QWidget):
|
|||||||
rect.setStyleSheet("background-color: red; border: 1px solid gray;")
|
rect.setStyleSheet("background-color: red; border: 1px solid gray;")
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
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)
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
def setSuffixText(self, suffix):
|
def setSuffixText(self, suffix):
|
||||||
@@ -1394,7 +1423,7 @@ class ParamSection(QWidget):
|
|||||||
|
|
||||||
def show_help_popup(self, text):
|
def show_help_popup(self, text):
|
||||||
msg = QMessageBox(self)
|
msg = QMessageBox(self)
|
||||||
msg.setWindowTitle("Parameter Info")
|
msg.setWindowTitle("Parameter Info - FLARES")
|
||||||
msg.setText(text)
|
msg.setText(text)
|
||||||
msg.exec()
|
msg.exec()
|
||||||
|
|
||||||
@@ -1529,52 +1558,40 @@ class ParamSection(QWidget):
|
|||||||
# You can customize how you display selected items here:
|
# You can customize how you display selected items here:
|
||||||
widget.lineEdit().setText(", ".join(selected))
|
widget.lineEdit().setText(", ".join(selected))
|
||||||
|
|
||||||
def update_annotation_dropdown_from_folder(self, folder_path):
|
def update_annotation_dropdown_from_loaded_files(self, bubble_widgets, button1):
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
annotation_sets = []
|
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:
|
try:
|
||||||
raw = read_raw_snirf(full_path, preload=False, verbose="ERROR")
|
raw = read_raw_snirf(file_path, preload=False, verbose="ERROR")
|
||||||
annotations = raw.annotations
|
annotations = raw.annotations
|
||||||
if annotations is not None:
|
if annotations is not None:
|
||||||
|
print(f"[ParamSection] Found annotations with descriptions: {annotations.description}")
|
||||||
labels = set(annotations.description)
|
labels = set(annotations.description)
|
||||||
annotation_sets.append(labels)
|
annotation_sets.append(labels)
|
||||||
except Exception as e:
|
else:
|
||||||
print(f"[ParamSection] Skipping file '{filename}' due to error: {e}")
|
print(f"[ParamSection] No annotations found in file: {file_path}")
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
|
||||||
if not annotation_sets:
|
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
|
return
|
||||||
|
|
||||||
# Get common annotations
|
|
||||||
common_annotations = set.intersection(*annotation_sets) if len(annotation_sets) > 1 else annotation_sets[0]
|
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}")
|
print(f"[ParamSection] Common annotations: {common_annotations}")
|
||||||
|
|
||||||
# Update the dropdown
|
|
||||||
|
|
||||||
self.update_dropdown_items("REMOVE_EVENTS", common_annotations)
|
self.update_dropdown_items("REMOVE_EVENTS", common_annotations)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class FullClickLineEdit(QLineEdit):
|
class FullClickLineEdit(QLineEdit):
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
combo = self.parent()
|
combo = self.parent()
|
||||||
@@ -3869,6 +3886,11 @@ class MainApplication(QMainWindow):
|
|||||||
self.folder_paths = []
|
self.folder_paths = []
|
||||||
self.section_widget = None
|
self.section_widget = None
|
||||||
self.first_run = True
|
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.init_ui()
|
||||||
self.create_menu_bar()
|
self.create_menu_bar()
|
||||||
@@ -3938,6 +3960,19 @@ class MainApplication(QMainWindow):
|
|||||||
right_column_layout.addWidget(label)
|
right_column_layout.addWidget(label)
|
||||||
right_column_layout.addWidget(field)
|
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
|
right_column_layout.addStretch() # Push fields to top
|
||||||
|
|
||||||
self.right_column_widget.hide()
|
self.right_column_widget.hide()
|
||||||
@@ -4062,7 +4097,7 @@ class MainApplication(QMainWindow):
|
|||||||
file_actions = [
|
file_actions = [
|
||||||
("Open File...", "Ctrl+O", self.open_file_dialog, resource_path("icons/file_open_24dp_1F1F1F.svg")),
|
("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 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")),
|
("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...", "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")),
|
("Save Project As...", "Ctrl+Shift+S", self.save_project, resource_path("icons/save_as_24dp_1F1F1F.svg")),
|
||||||
@@ -4070,7 +4105,7 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
for i, (name, shortcut, slot, icon) in enumerate(file_actions):
|
for i, (name, shortcut, slot, icon) in enumerate(file_actions):
|
||||||
file_menu.addAction(make_action(name, shortcut, slot, icon=icon))
|
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()
|
||||||
|
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
@@ -4216,26 +4251,59 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
def open_file_dialog(self):
|
def open_file_dialog(self):
|
||||||
file_path, _ = QFileDialog.getOpenFileName(
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
self, "Open File", "", "All Files (*);;Text Files (*.txt)"
|
self, "Open File", "", "SNIRF Files (*.snirf);;All Files (*)"
|
||||||
)
|
)
|
||||||
if file_path:
|
if file_path:
|
||||||
self.selected_path = file_path # store the file path
|
# Create and display the bubble directly for this file
|
||||||
self.show_files_as_bubbles(file_path)
|
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)
|
self.button1.setVisible(True)
|
||||||
|
|
||||||
|
|
||||||
def open_folder_dialog(self):
|
def open_folder_dialog(self):
|
||||||
folder_path = QFileDialog.getExistingDirectory(
|
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder", "")
|
||||||
self, "Select Folder", ""
|
|
||||||
)
|
|
||||||
if folder_path:
|
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)
|
self.show_files_as_bubbles(folder_path)
|
||||||
|
|
||||||
for section_widget in self.param_sections:
|
for section_widget in self.param_sections:
|
||||||
if "REMOVE_EVENTS" in section_widget.widgets:
|
if hasattr(section_widget, 'update_annotation_dropdown_from_loaded_files'):
|
||||||
section_widget.update_annotation_dropdown_from_folder(folder_path)
|
if "REMOVE_EVENTS" in section_widget.widgets:
|
||||||
break
|
section_widget.update_annotation_dropdown_from_loaded_files(self.bubble_widgets, self.button1)
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
print("[MainWindow] Could not find ParamSection with 'REMOVE_EVENTS' widget")
|
print("[MainWindow] Could not find ParamSection with 'REMOVE_EVENTS' widget")
|
||||||
|
|
||||||
@@ -4244,16 +4312,29 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
def open_multiple_folders_dialog(self):
|
def open_multiple_folders_dialog(self):
|
||||||
while True:
|
while True:
|
||||||
folder = QFileDialog.getExistingDirectory(self, "Select Folder")
|
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder")
|
||||||
if not folder:
|
if not folder_path:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")]
|
||||||
|
|
||||||
if not hasattr(self, 'selected_paths'):
|
if not hasattr(self, 'selected_paths'):
|
||||||
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
|
# Ask if the user wants to add another
|
||||||
more = QMessageBox.question(
|
more = QMessageBox.question(
|
||||||
@@ -4267,21 +4348,42 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
self.button1.setVisible(True)
|
self.button1.setVisible(True)
|
||||||
|
|
||||||
def save_project(self):
|
|
||||||
|
|
||||||
filename, _ = QFileDialog.getSaveFileName(
|
def save_project(self, onCrash=False):
|
||||||
self, "Save Project", "", "FLARE Project (*.flare)"
|
|
||||||
)
|
|
||||||
if not filename:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
project_data = {
|
# Ensure the filename has the proper extension
|
||||||
"file_list": [bubble.file_path for bubble in self.bubble_widgets.values()],
|
if not filename.endswith(".flare"):
|
||||||
"progress_states": {
|
filename += ".flare"
|
||||||
bubble.file_path: bubble.current_step for bubble in self.bubble_widgets.values()
|
|
||||||
},
|
|
||||||
|
|
||||||
|
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,
|
"raw_haemo_dict": self.raw_haemo_dict,
|
||||||
"epochs_dict": self.epochs_dict,
|
"epochs_dict": self.epochs_dict,
|
||||||
"fig_bytes_dict": self.fig_bytes_dict,
|
"fig_bytes_dict": self.fig_bytes_dict,
|
||||||
@@ -4294,6 +4396,17 @@ class MainApplication(QMainWindow):
|
|||||||
"group_dict": self.group_dict,
|
"group_dict": self.group_dict,
|
||||||
"valid_dict": self.valid_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:
|
with open(filename, "wb") as f:
|
||||||
pickle.dump(project_data, f)
|
pickle.dump(project_data, f)
|
||||||
@@ -4301,7 +4414,8 @@ class MainApplication(QMainWindow):
|
|||||||
QMessageBox.information(self, "Success", f"Project saved to:\n{filename}")
|
QMessageBox.information(self, "Success", f"Project saved to:\n{filename}")
|
||||||
|
|
||||||
except Exception as e:
|
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 +4442,20 @@ class MainApplication(QMainWindow):
|
|||||||
self.group_dict = data.get("group_dict", {})
|
self.group_dict = data.get("group_dict", {})
|
||||||
self.valid_dict = data.get("valid_dict", {})
|
self.valid_dict = data.get("valid_dict", {})
|
||||||
|
|
||||||
# Restore bubbles and progress
|
project_dir = Path(filename).parent
|
||||||
self.show_files_as_bubbles_from_list(data["file_list"], data.get("progress_states", {}), filename)
|
|
||||||
|
|
||||||
|
# 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
|
# Re-enable buttons
|
||||||
# self.button1.setVisible(True)
|
# self.button1.setVisible(True)
|
||||||
self.button3.setVisible(True)
|
self.button3.setVisible(True)
|
||||||
@@ -4348,11 +4473,11 @@ class MainApplication(QMainWindow):
|
|||||||
folder_paths = [folder_paths]
|
folder_paths = [folder_paths]
|
||||||
|
|
||||||
# Clear previous bubbles
|
# Clear previous bubbles
|
||||||
while self.bubble_layout.count():
|
# while self.bubble_layout.count():
|
||||||
item = self.bubble_layout.takeAt(0)
|
# item = self.bubble_layout.takeAt(0)
|
||||||
widget = item.widget()
|
# widget = item.widget()
|
||||||
if widget:
|
# if widget:
|
||||||
widget.deleteLater()
|
# widget.deleteLater()
|
||||||
|
|
||||||
temp_bubble = ProgressBubble("Test Bubble", "") # A dummy bubble for measurement
|
temp_bubble = ProgressBubble("Test Bubble", "") # A dummy bubble for measurement
|
||||||
temp_bubble.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy. Preferred)
|
temp_bubble.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy. Preferred)
|
||||||
@@ -4364,21 +4489,26 @@ class MainApplication(QMainWindow):
|
|||||||
cols = max(1, available_width // bubble_width) # Ensure at least 1 column
|
cols = max(1, available_width // bubble_width) # Ensure at least 1 column
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
|
if not hasattr(self, 'selected_paths'):
|
||||||
|
self.selected_paths = []
|
||||||
|
|
||||||
for folder_path in folder_paths:
|
for folder_path in folder_paths:
|
||||||
if not os.path.isdir(folder_path):
|
if not os.path.isdir(folder_path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
files = os.listdir(folder_path)
|
snirf_files = [str(f) for f in Path(folder_path).glob("*.snirf")]
|
||||||
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}"
|
|
||||||
|
|
||||||
|
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 = ProgressBubble(display_name, full_path)
|
||||||
bubble.clicked.connect(self.on_bubble_clicked)
|
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
|
row = index // cols
|
||||||
col = index % cols
|
col = index % cols
|
||||||
@@ -4418,6 +4548,7 @@ class MainApplication(QMainWindow):
|
|||||||
# Create bubble with full path
|
# Create bubble with full path
|
||||||
bubble = ProgressBubble(display_name, file_path)
|
bubble = ProgressBubble(display_name, file_path)
|
||||||
bubble.clicked.connect(self.on_bubble_clicked)
|
bubble.clicked.connect(self.on_bubble_clicked)
|
||||||
|
bubble.rightClicked.connect(self.on_bubble_right_clicked)
|
||||||
self.bubble_widgets[file_path] = bubble
|
self.bubble_widgets[file_path] = bubble
|
||||||
|
|
||||||
step = progress_states.get(file_path, 0)
|
step = progress_states.get(file_path, 0)
|
||||||
@@ -4506,6 +4637,77 @@ class MainApplication(QMainWindow):
|
|||||||
field.blockSignals(False)
|
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):
|
def eventFilter(self, watched, event):
|
||||||
if event.type() == QEvent.Type.MouseButtonPress:
|
if event.type() == QEvent.Type.MouseButtonPress:
|
||||||
widget = self.childAt(event.pos())
|
widget = self.childAt(event.pos())
|
||||||
@@ -4587,10 +4789,11 @@ class MainApplication(QMainWindow):
|
|||||||
self.button1.clicked.disconnect(self.on_run_task)
|
self.button1.clicked.disconnect(self.on_run_task)
|
||||||
self.button1.setText("Cancel")
|
self.button1.setText("Cancel")
|
||||||
self.button1.clicked.connect(self.cancel_task)
|
self.button1.clicked.connect(self.cancel_task)
|
||||||
|
|
||||||
if not self.first_run:
|
if not self.first_run:
|
||||||
for bubble in self.bubble_widgets.values():
|
for bubble in self.bubble_widgets.values():
|
||||||
bubble.mark_cancelled()
|
pass
|
||||||
|
# bubble.mark_cancelled()
|
||||||
self.first_run = False
|
self.first_run = False
|
||||||
|
|
||||||
# Collect all selected snirf files in a flat list
|
# Collect all selected snirf files in a flat list
|
||||||
@@ -4617,7 +4820,41 @@ class MainApplication(QMainWindow):
|
|||||||
if not snirf_files:
|
if not snirf_files:
|
||||||
raise ValueError("No .snirf files found in selection")
|
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 = {}
|
all_params = {}
|
||||||
for section_widget in self.param_sections:
|
for section_widget in self.param_sections:
|
||||||
section_params = section_widget.get_param_values()
|
section_params = section_widget.get_param_values()
|
||||||
@@ -4659,7 +4896,12 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
if isinstance(msg, dict):
|
if isinstance(msg, dict):
|
||||||
if msg.get("success"):
|
if msg.get("success"):
|
||||||
|
|
||||||
results = msg["result"] # from flares.py
|
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
|
# Initialize storage
|
||||||
# TODO: Is this check needed?
|
# TODO: Is this check needed?
|
||||||
@@ -4691,26 +4933,84 @@ class MainApplication(QMainWindow):
|
|||||||
self.valid_dict[file_path] = valid
|
self.valid_dict[file_path] = valid
|
||||||
|
|
||||||
# self.statusbar.showMessage(f"Processing complete! Time elapsed: {elapsed_time:.2f} seconds")
|
# 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)
|
self.button3.setVisible(True)
|
||||||
|
|
||||||
|
|
||||||
else:
|
elif msg.get("success") is False:
|
||||||
error_msg = msg.get("error", "Unknown error")
|
error_msg = msg.get("error", "Unknown error")
|
||||||
print("Error during processing:", error_msg)
|
traceback_str = msg.get("traceback", "")
|
||||||
self.statusbar.showMessage(f"Processing failed! {error_msg}")
|
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':
|
elif isinstance(msg, tuple) and msg[0] == 'progress':
|
||||||
_, file_path, step_index = msg
|
_, file_path, step_index = msg
|
||||||
file_name = os.path.basename(file_path) # extract file name
|
self.progress_update_signal.emit(file_path, step_index)
|
||||||
self.progress_update_signal.emit(file_name, 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):
|
def cleanup_after_process(self):
|
||||||
|
|
||||||
if hasattr(self, 'result_process'):
|
if hasattr(self, 'result_process'):
|
||||||
@@ -4738,8 +5038,9 @@ class MainApplication(QMainWindow):
|
|||||||
self.manager.shutdown()
|
self.manager.shutdown()
|
||||||
|
|
||||||
|
|
||||||
def update_file_progress(self, filename, step_index):
|
def update_file_progress(self, file_path, step_index):
|
||||||
bubble = self.bubble_widgets.get(filename)
|
key = os.path.normpath(file_path)
|
||||||
|
bubble = self.bubble_widgets.get(key)
|
||||||
if bubble:
|
if bubble:
|
||||||
bubble.update_progress(step_index)
|
bubble.update_progress(step_index)
|
||||||
|
|
||||||
@@ -4895,6 +5196,14 @@ class MainApplication(QMainWindow):
|
|||||||
# Measurement date
|
# Measurement date
|
||||||
snirf_info['Measurement Date'] = str(raw.info.get('meas_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
|
# Source-detector distances
|
||||||
distances = source_detector_distances(raw.info)
|
distances = source_detector_distances(raw.info)
|
||||||
distance_info = []
|
distance_info = []
|
||||||
@@ -5169,11 +5478,50 @@ def exception_hook(exc_type, exc_value, exc_traceback):
|
|||||||
if app is None:
|
if app is None:
|
||||||
app = QApplication(sys.argv)
|
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
|
# Exit the app after user acknowledges
|
||||||
sys.exit(1)
|
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__":
|
if __name__ == "__main__":
|
||||||
# Redirect exceptions to the popup window
|
# Redirect exceptions to the popup window
|
||||||
|
|||||||
Reference in New Issue
Block a user