Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3470e6c96 | |||
| 391b42df50 | |||
| 65e33c8619 |
17
changelog.md
Normal file
17
changelog.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Version 1.0.1
|
||||||
|
|
||||||
|
- Two new options have been added when clicking on a participant's file.
|
||||||
|
- First new option is Age which allows the option to specify age on a per-file basis to calculate DPF by using the Scholkmann & Wolf calculation instead of having one DPF for all files. Fixes [Issue 1](https://git.research.dezeeuw.ca/tyler/flares/issues/1). This also allows for proper calculation of DPF for the two seperate wavelengths.
|
||||||
|
- If no age is set by the user, the code will fall back to the age of 25
|
||||||
|
- Second new option is Gender to be used in future analysis options (currently not implemented).
|
||||||
|
- UI elements have been shuffled to make room for the new parameters.
|
||||||
|
- Removed the process button from being available until a file or project is opened. Fixes [Issue 2](https://git.research.dezeeuw.ca/tyler/flares/issues/2).
|
||||||
|
- Removed the option for PPF under Haemoglobin Concentration as it is no longer used.
|
||||||
|
- Added a new category in the options called Other
|
||||||
|
- Added MAX_WORKERS to the Other category. This is how many files should be processed at once. Fixes [Issue 13](https://git.research.dezeeuw.ca/tyler/flares/issues/13).
|
||||||
|
- Added more information about the snirf file to be displayed when selected
|
||||||
|
|
||||||
|
|
||||||
|
# Version 1.0.0
|
||||||
|
|
||||||
|
- Initial release.
|
||||||
@@ -24,6 +24,7 @@ from multiprocessing import Queue
|
|||||||
from typing import Any, Optional, cast, Literal, Iterator, Union
|
from typing import Any, Optional, cast, Literal, Iterator, Union
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
|
import h5py
|
||||||
import pywt # type: ignore
|
import pywt # type: ignore
|
||||||
import qtpy # type: ignore
|
import qtpy # type: ignore
|
||||||
import xlrd # type: ignore
|
import xlrd # type: ignore
|
||||||
@@ -42,14 +43,14 @@ from pandas import DataFrame
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from matplotlib.axes import Axes
|
from matplotlib.axes import Axes
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
import vtkmodules.util.data_model
|
#import vtkmodules.util.data_model
|
||||||
from numpy import floating, float64
|
from numpy import floating, float64
|
||||||
from matplotlib.lines import Line2D
|
from matplotlib.lines import Line2D
|
||||||
import matplotlib.colors as mcolors
|
import matplotlib.colors as mcolors
|
||||||
from scipy.stats import ttest_1samp # type: ignore
|
from scipy.stats import ttest_1samp # type: ignore
|
||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
import statsmodels.formula.api as smf # type: ignore
|
import statsmodels.formula.api as smf # type: ignore
|
||||||
import vtkmodules.util.execution_model
|
#import vtkmodules.util.execution_model
|
||||||
from nilearn.plotting import plot_design_matrix # type: ignore
|
from nilearn.plotting import plot_design_matrix # type: ignore
|
||||||
from scipy.signal import welch, butter, filtfilt # type: ignore
|
from scipy.signal import welch, butter, filtfilt # type: ignore
|
||||||
from matplotlib.colors import LinearSegmentedColormap
|
from matplotlib.colors import LinearSegmentedColormap
|
||||||
@@ -127,7 +128,7 @@ EXCLUDE_CHANNELS: bool
|
|||||||
MAX_BAD_CHANNELS: int
|
MAX_BAD_CHANNELS: int
|
||||||
LONG_CHANNEL_THRESH: float
|
LONG_CHANNEL_THRESH: float
|
||||||
|
|
||||||
PPF: float
|
METADATA: dict
|
||||||
|
|
||||||
DRIFT_MODEL: str
|
DRIFT_MODEL: str
|
||||||
DURATION_BETWEEN_ACTIVITIES: int
|
DURATION_BETWEEN_ACTIVITIES: int
|
||||||
@@ -223,7 +224,7 @@ REQUIRED_KEYS: dict[str, Any] = {
|
|||||||
"EXCLUDE_CHANNELS": bool,
|
"EXCLUDE_CHANNELS": bool,
|
||||||
"MAX_BAD_CHANNELS": int,
|
"MAX_BAD_CHANNELS": int,
|
||||||
"LONG_CHANNEL_THRESH": float,
|
"LONG_CHANNEL_THRESH": float,
|
||||||
"PPF": float,
|
"METADATA": dict,
|
||||||
"DRIFT_MODEL": str,
|
"DRIFT_MODEL": str,
|
||||||
"DURATION_BETWEEN_ACTIVITIES": int,
|
"DURATION_BETWEEN_ACTIVITIES": int,
|
||||||
"HRF_MODEL": str,
|
"HRF_MODEL": str,
|
||||||
@@ -284,8 +285,6 @@ else:
|
|||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessingError(Exception):
|
class ProcessingError(Exception):
|
||||||
def __init__(self, message: str = "Something went wrong!"):
|
def __init__(self, message: str = "Something went wrong!"):
|
||||||
self.message = message
|
self.message = message
|
||||||
@@ -1615,7 +1614,7 @@ def calculate_optical_density(data: BaseRaw, ID: str) -> tuple[BaseRaw, Figure]:
|
|||||||
|
|
||||||
|
|
||||||
# STEP 9: Haemoglobin concentration
|
# STEP 9: Haemoglobin concentration
|
||||||
def calculate_haemoglobin_concentration(optical_density_data: BaseRaw, ID: str) -> tuple[BaseRaw, Figure]:
|
def calculate_haemoglobin_concentration(optical_density_data: BaseRaw, ID: str, file_path: str) -> tuple[BaseRaw, Figure]:
|
||||||
"""
|
"""
|
||||||
Calculates haemoglobin concentration from optical density data using the Beer-Lambert law and generates a plot.
|
Calculates haemoglobin concentration from optical density data using the Beer-Lambert law and generates a plot.
|
||||||
|
|
||||||
@@ -1625,7 +1624,9 @@ def calculate_haemoglobin_concentration(optical_density_data: BaseRaw, ID: str)
|
|||||||
The data in optical density format.
|
The data in optical density format.
|
||||||
ID : str
|
ID : str
|
||||||
File name of the the snirf file that was loaded.
|
File name of the the snirf file that was loaded.
|
||||||
|
file_path : str
|
||||||
|
Entire file path if snirf file that was loaded.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
tuple[BaseRaw, Figure]
|
tuple[BaseRaw, Figure]
|
||||||
@@ -1636,7 +1637,7 @@ def calculate_haemoglobin_concentration(optical_density_data: BaseRaw, ID: str)
|
|||||||
logger.info("Calculating haemoglobin concentration data...")
|
logger.info("Calculating haemoglobin concentration data...")
|
||||||
|
|
||||||
# Get the haemoglobin concentration using beer lambert law
|
# Get the haemoglobin concentration using beer lambert law
|
||||||
haemoglobin_concentration_data = beer_lambert_law(optical_density_data, PPF)
|
haemoglobin_concentration_data = beer_lambert_law(optical_density_data, ppf=calculate_dpf(file_path))
|
||||||
|
|
||||||
logger.info("Creating the figure...")
|
logger.info("Creating the figure...")
|
||||||
fig = cast(Figure, optical_density_data.plot(show=False, n_channels=len(getattr(optical_density_data, "ch_names")), duration=getattr(optical_density_data, "times")[-1]).figure) # type: ignore
|
fig = cast(Figure, optical_density_data.plot(show=False, n_channels=len(getattr(optical_density_data, "ch_names")), duration=getattr(optical_density_data, "times")[-1]).figure) # type: ignore
|
||||||
@@ -1720,10 +1721,10 @@ def calculate_and_apply_negative_correlation_enhancement(haemoglobin_concentrati
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_and_apply_short_channel_correction(optical_density_data: BaseRaw) -> dict[str, list[Any] | mne.evoked.EvokedArray]:
|
def calculate_and_apply_short_channel_correction(optical_density_data: BaseRaw, file_path: str) -> dict[str, list[Any] | mne.evoked.EvokedArray]:
|
||||||
|
|
||||||
od_corrected = short_channel_regression(optical_density_data, SHORT_CHANNEL_THRESH)
|
od_corrected = short_channel_regression(optical_density_data, SHORT_CHANNEL_THRESH)
|
||||||
haemoglobin_concentration_data = beer_lambert_law(od_corrected, PPF)
|
haemoglobin_concentration_data = beer_lambert_law(od_corrected, ppf=calculate_dpf(file_path))
|
||||||
|
|
||||||
events, _ = mne.events_from_annotations(haemoglobin_concentration_data, event_id={"Reach": 1, "Start of Rest": 2}, verbose=VERBOSITY) # type: ignore
|
events, _ = mne.events_from_annotations(haemoglobin_concentration_data, event_id={"Reach": 1, "Start of Rest": 2}, verbose=VERBOSITY) # type: ignore
|
||||||
event_dict = {"Reach": 1, "Start of Rest": 2}
|
event_dict = {"Reach": 1, "Start of Rest": 2}
|
||||||
@@ -1917,6 +1918,32 @@ def run_GLM_analysis(data: BaseRaw, design_matrix: DataFrame) -> RegressionResul
|
|||||||
return glm_est
|
return glm_est
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_dpf(file_path):
|
||||||
|
# order is hbo / hbr
|
||||||
|
with h5py.File(file_path, 'r') as f:
|
||||||
|
wavelengths = f['/nirs/probe/wavelengths'][:]
|
||||||
|
print("Wavelengths (nm):", wavelengths)
|
||||||
|
wavelengths = sorted(wavelengths, reverse=True)
|
||||||
|
data = METADATA.get(file_path)
|
||||||
|
if data is None:
|
||||||
|
age = 25
|
||||||
|
else:
|
||||||
|
age = data['Age']
|
||||||
|
logger.info(age)
|
||||||
|
age = float(age)
|
||||||
|
a = 223.3
|
||||||
|
b = 0.05624
|
||||||
|
c = 0.8493
|
||||||
|
d = -5.723e-7
|
||||||
|
e = 0.001245
|
||||||
|
f = -0.9025
|
||||||
|
dpf = []
|
||||||
|
for w in wavelengths:
|
||||||
|
logger.info(w)
|
||||||
|
dpf.append(a + b * (age**c) + d* (w**3) + e * (w**2) + f*w)
|
||||||
|
logger.info(dpf)
|
||||||
|
return dpf
|
||||||
|
|
||||||
|
|
||||||
def individual_GLM_analysis(file_path: str, ID: str, stim_duration: float = 5.0, progress_callback=None) -> tuple[BaseRaw, BaseRaw, DataFrame, DataFrame, DataFrame, DataFrame, dict[str, Figure], str, bool, bool]:
|
def individual_GLM_analysis(file_path: str, ID: str, stim_duration: float = 5.0, progress_callback=None) -> tuple[BaseRaw, BaseRaw, DataFrame, DataFrame, DataFrame, DataFrame, dict[str, Figure], str, bool, bool]:
|
||||||
"""
|
"""
|
||||||
@@ -1971,6 +1998,12 @@ def individual_GLM_analysis(file_path: str, ID: str, stim_duration: float = 5.0,
|
|||||||
# Initalize the participants full layout to be the current data regardless if it will be updated later
|
# Initalize the participants full layout to be the current data regardless if it will be updated later
|
||||||
raw_full_layout = data
|
raw_full_layout = data
|
||||||
|
|
||||||
|
|
||||||
|
logger.info(file_path)
|
||||||
|
logger.info(ID)
|
||||||
|
logger.info(METADATA.get(file_path))
|
||||||
|
calculate_dpf(file_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Did the user want to load new channel positions from an optode file?
|
# Did the user want to load new channel positions from an optode file?
|
||||||
# STEP 2
|
# STEP 2
|
||||||
@@ -1985,7 +2018,7 @@ def individual_GLM_analysis(file_path: str, ID: str, stim_duration: float = 5.0,
|
|||||||
# but i shouldnt need to do od and bll just check the last three numbers
|
# but i shouldnt need to do od and bll just check the last three numbers
|
||||||
temp = data.copy()
|
temp = data.copy()
|
||||||
temp_od = cast(BaseRaw, optical_density(temp, verbose=VERBOSITY))
|
temp_od = cast(BaseRaw, optical_density(temp, verbose=VERBOSITY))
|
||||||
raw_full_layout = beer_lambert_law(temp_od, ppf=PPF)
|
raw_full_layout = beer_lambert_law(temp_od, ppf=calculate_dpf(file_path))
|
||||||
|
|
||||||
# If specified, apply TDDR to the data
|
# If specified, apply TDDR to the data
|
||||||
# STEP 3
|
# STEP 3
|
||||||
@@ -2082,11 +2115,11 @@ def individual_GLM_analysis(file_path: str, ID: str, stim_duration: float = 5.0,
|
|||||||
if SHORT_CHANNEL:
|
if SHORT_CHANNEL:
|
||||||
short_chans_od = cast(BaseRaw, optical_density(short_chans))
|
short_chans_od = cast(BaseRaw, optical_density(short_chans))
|
||||||
data_recombined = cast(BaseRaw, data.copy().add_channels([short_chans_od])) # type: ignore
|
data_recombined = cast(BaseRaw, data.copy().add_channels([short_chans_od])) # type: ignore
|
||||||
evoked_dict_corr = calculate_and_apply_short_channel_correction(data_recombined.copy())
|
evoked_dict_corr = calculate_and_apply_short_channel_correction(data_recombined.copy(), file_path)
|
||||||
|
|
||||||
# Calculate the haemoglobin concentration
|
# Calculate the haemoglobin concentration
|
||||||
# STEP 9
|
# STEP 9
|
||||||
data, fig = calculate_haemoglobin_concentration(data, ID)
|
data, fig = calculate_haemoglobin_concentration(data, ID, file_path)
|
||||||
order_of_operations += " + Haemoglobin Concentration"
|
order_of_operations += " + Haemoglobin Concentration"
|
||||||
fig_dict['HaemoglobinConcentration'] = fig
|
fig_dict['HaemoglobinConcentration'] = fig
|
||||||
if progress_callback: progress_callback(9)
|
if progress_callback: progress_callback(9)
|
||||||
|
|||||||
179
main.py
179
main.py
@@ -27,7 +27,7 @@ import psutil
|
|||||||
import requests
|
import requests
|
||||||
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
|
QPushButton, QMainWindow, QFileDialog, QLabel, QLineEdit, QFrame, QSizePolicy, QGroupBox
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import QThread, Signal, Qt, QTimer
|
from PySide6.QtCore import QThread, Signal, Qt, QTimer
|
||||||
from PySide6.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleValidator
|
from PySide6.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleValidator
|
||||||
@@ -114,7 +114,7 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Haemoglobin Concentration",
|
"title": "Haemoglobin Concentration",
|
||||||
"params": [
|
"params": [
|
||||||
{"name": "PPF", "default": 0.1, "type": float, "help": "Partial Pathlength Factor."},
|
# Intentionally empty (TODO)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -128,7 +128,7 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "General Linear Model",
|
"title": "General Linear Model",
|
||||||
"params": [
|
"params": [
|
||||||
{"name": "N_JOBS", "default": 1, "type": int, "help": "Number of jobs for processing."},
|
{"name": "N_JOBS", "default": 1, "type": int, "help": "Number of jobs for GLM processing."},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -137,6 +137,12 @@ SECTIONS = [
|
|||||||
# Intentionally empty (TODO)
|
# Intentionally empty (TODO)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Other",
|
||||||
|
"params": [
|
||||||
|
{"name": "MAX_WORKERS", "default": 4, "type": int, "help": "Number of files to process at once."},
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -403,7 +409,7 @@ class ProgressBubble(QWidget):
|
|||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
# Store the file path
|
# Store the file path
|
||||||
self.file_path = file_path
|
self.file_path = os.path.normpath(file_path)
|
||||||
|
|
||||||
self.current_step = 0
|
self.current_step = 0
|
||||||
# Make the bubble clickable
|
# Make the bubble clickable
|
||||||
@@ -619,6 +625,10 @@ class MainApplication(QMainWindow):
|
|||||||
self.platform_suffix = "-" + PLATFORM_NAME
|
self.platform_suffix = "-" + PLATFORM_NAME
|
||||||
self.pending_update_version = None
|
self.pending_update_version = None
|
||||||
self.pending_update_path = None
|
self.pending_update_path = None
|
||||||
|
|
||||||
|
self.file_metadata = {}
|
||||||
|
self.current_file = None
|
||||||
|
|
||||||
|
|
||||||
# Start local pending update check thread
|
# Start local pending update check thread
|
||||||
self.local_check_thread = LocalPendingUpdateCheckThread(CURRENT_VERSION, self.platform_suffix)
|
self.local_check_thread = LocalPendingUpdateCheckThread(CURRENT_VERSION, self.platform_suffix)
|
||||||
@@ -642,11 +652,51 @@ class MainApplication(QMainWindow):
|
|||||||
left_container.setLayout(left_layout)
|
left_container.setLayout(left_layout)
|
||||||
left_container.setMinimumWidth(300)
|
left_container.setMinimumWidth(300)
|
||||||
|
|
||||||
# Top left widget (for demo, use QTextEdit)
|
top_left_container = QGroupBox() # Optional: add a title inside parentheses
|
||||||
|
top_left_container.setTitle("File information") # Optional visible title
|
||||||
|
top_left_container.setStyleSheet("QGroupBox { font-weight: bold; }") # Style if needed
|
||||||
|
top_left_container.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
|
||||||
|
|
||||||
|
top_left_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
top_left_container.setLayout(top_left_layout)
|
||||||
|
|
||||||
|
# QTextEdit with fixed height, but only 80% width
|
||||||
self.top_left_widget = QTextEdit()
|
self.top_left_widget = QTextEdit()
|
||||||
self.top_left_widget.setReadOnly(True)
|
self.top_left_widget.setReadOnly(True)
|
||||||
self.top_left_widget.setPlaceholderText("Click a file below to get started! No files below? Open one using File > Open!")
|
self.top_left_widget.setPlaceholderText("Click a file below to get started! No files below? Open one using File > Open!")
|
||||||
self.top_left_widget.setFixedHeight(250)
|
|
||||||
|
# Add QTextEdit to the layout with a stretch factor
|
||||||
|
top_left_layout.addWidget(self.top_left_widget, stretch=4) # 80%
|
||||||
|
|
||||||
|
# Create a vertical box layout for the right 20%
|
||||||
|
self.right_column_widget = QWidget()
|
||||||
|
right_column_layout = QVBoxLayout()
|
||||||
|
self.right_column_widget.setLayout(right_column_layout)
|
||||||
|
|
||||||
|
|
||||||
|
self.meta_fields = {
|
||||||
|
"Age": QLineEdit(),
|
||||||
|
"Gender": QLineEdit(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Inside your top-left container's right column layout:
|
||||||
|
for key, field in self.meta_fields.items():
|
||||||
|
label = QLabel(key.capitalize())
|
||||||
|
field.setPlaceholderText(f"Enter {key}")
|
||||||
|
right_column_layout.addWidget(label)
|
||||||
|
right_column_layout.addWidget(field)
|
||||||
|
|
||||||
|
right_column_layout.addStretch() # Push fields to top
|
||||||
|
|
||||||
|
self.right_column_widget.hide()
|
||||||
|
|
||||||
|
# Add right column widget to the top-left layout (takes 20% width)
|
||||||
|
top_left_layout.addWidget(self.right_column_widget, stretch=1)
|
||||||
|
|
||||||
|
# Add top_left_container to the main left_layout
|
||||||
|
left_layout.addWidget(top_left_container)
|
||||||
|
|
||||||
# Bottom left: the bubbles inside a scroll area
|
# Bottom left: the bubbles inside a scroll area
|
||||||
self.bubble_container = QWidget()
|
self.bubble_container = QWidget()
|
||||||
@@ -660,7 +710,6 @@ class MainApplication(QMainWindow):
|
|||||||
self.scroll_area.setMinimumHeight(300)
|
self.scroll_area.setMinimumHeight(300)
|
||||||
|
|
||||||
# Add top left and bottom left to left layout
|
# Add top left and bottom left to left layout
|
||||||
left_layout.addWidget(self.top_left_widget)
|
|
||||||
left_layout.addWidget(self.scroll_area)
|
left_layout.addWidget(self.scroll_area)
|
||||||
|
|
||||||
self.progress_update_signal.connect(self.update_file_progress)
|
self.progress_update_signal.connect(self.update_file_progress)
|
||||||
@@ -713,6 +762,7 @@ class MainApplication(QMainWindow):
|
|||||||
self.button2.setMinimumSize(100, 40)
|
self.button2.setMinimumSize(100, 40)
|
||||||
self.button3.setMinimumSize(100, 40)
|
self.button3.setMinimumSize(100, 40)
|
||||||
|
|
||||||
|
self.button1.setVisible(False)
|
||||||
self.button3.setVisible(False)
|
self.button3.setVisible(False)
|
||||||
|
|
||||||
self.button1.clicked.connect(self.on_run_task)
|
self.button1.clicked.connect(self.on_run_task)
|
||||||
@@ -828,6 +878,8 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
def clear_all(self):
|
def clear_all(self):
|
||||||
|
|
||||||
|
self.right_column_widget.hide()
|
||||||
|
|
||||||
# Clear the bubble layout
|
# Clear the bubble layout
|
||||||
while self.bubble_layout.count():
|
while self.bubble_layout.count():
|
||||||
item = self.bubble_layout.takeAt(0)
|
item = self.bubble_layout.takeAt(0)
|
||||||
@@ -846,6 +898,7 @@ class MainApplication(QMainWindow):
|
|||||||
self.all_figures = None
|
self.all_figures = None
|
||||||
|
|
||||||
# Reset any visible UI elements
|
# Reset any visible UI elements
|
||||||
|
self.button1.setVisible(False)
|
||||||
self.button3.setVisible(False)
|
self.button3.setVisible(False)
|
||||||
self.top_left_widget.clear()
|
self.top_left_widget.clear()
|
||||||
|
|
||||||
@@ -889,6 +942,8 @@ class MainApplication(QMainWindow):
|
|||||||
if file_path:
|
if file_path:
|
||||||
self.selected_path = file_path # store the file path
|
self.selected_path = file_path # store the file path
|
||||||
self.show_files_as_bubbles(file_path)
|
self.show_files_as_bubbles(file_path)
|
||||||
|
|
||||||
|
self.button1.setVisible(True)
|
||||||
|
|
||||||
def open_folder_dialog(self):
|
def open_folder_dialog(self):
|
||||||
folder_path = QFileDialog.getExistingDirectory(
|
folder_path = QFileDialog.getExistingDirectory(
|
||||||
@@ -897,6 +952,8 @@ class MainApplication(QMainWindow):
|
|||||||
if folder_path:
|
if folder_path:
|
||||||
self.selected_path = folder_path # store the folder path
|
self.selected_path = folder_path # store the folder path
|
||||||
self.show_files_as_bubbles(folder_path)
|
self.show_files_as_bubbles(folder_path)
|
||||||
|
|
||||||
|
self.button1.setVisible(True)
|
||||||
|
|
||||||
|
|
||||||
def open_multiple_folders_dialog(self):
|
def open_multiple_folders_dialog(self):
|
||||||
@@ -921,6 +978,8 @@ class MainApplication(QMainWindow):
|
|||||||
)
|
)
|
||||||
if more == QMessageBox.StandardButton.No:
|
if more == QMessageBox.StandardButton.No:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self.button1.setVisible(True)
|
||||||
|
|
||||||
def save_project(self):
|
def save_project(self):
|
||||||
|
|
||||||
@@ -990,7 +1049,8 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
self.show_files_as_bubbles_from_list(data["file_list"], data.get("progress_states", {}), filename)
|
self.show_files_as_bubbles_from_list(data["file_list"], data.get("progress_states", {}), filename)
|
||||||
|
|
||||||
# Re-enable the "Viewer" button
|
# Re-enable the buttons
|
||||||
|
self.button1.setVisible(True)
|
||||||
self.button3.setVisible(True)
|
self.button3.setVisible(True)
|
||||||
|
|
||||||
QMessageBox.information(self, "Loaded", f"Project loaded from:\n{filename}")
|
QMessageBox.information(self, "Loaded", f"Project loaded from:\n{filename}")
|
||||||
@@ -1089,6 +1149,10 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
|
|
||||||
def on_bubble_clicked(self, bubble):
|
def on_bubble_clicked(self, bubble):
|
||||||
|
|
||||||
|
# show age / gender
|
||||||
|
self.right_column_widget.show()
|
||||||
|
|
||||||
file_path = bubble.file_path
|
file_path = bubble.file_path
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
self.top_left_widget.setText("File not found.")
|
self.top_left_widget.setText("File not found.")
|
||||||
@@ -1098,7 +1162,7 @@ class MainApplication(QMainWindow):
|
|||||||
created = time.ctime(os.path.getctime(file_path))
|
created = time.ctime(os.path.getctime(file_path))
|
||||||
modified = time.ctime(os.path.getmtime(file_path))
|
modified = time.ctime(os.path.getmtime(file_path))
|
||||||
|
|
||||||
# snirf_info = self.get_snirf_metadata_mne(file_path)
|
snirf_info = self.get_snirf_metadata_mne(file_path)
|
||||||
|
|
||||||
info = f"""\
|
info = f"""\
|
||||||
File: {os.path.basename(file_path)}
|
File: {os.path.basename(file_path)}
|
||||||
@@ -1108,28 +1172,64 @@ class MainApplication(QMainWindow):
|
|||||||
Full Path: {file_path}
|
Full Path: {file_path}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# if "Error" in snirf_info:
|
if "Error" in snirf_info:
|
||||||
# info += f"\nSNIRF Metadata could not be loaded: {snirf_info['Error']}"
|
info += f"\nSNIRF Metadata could not be loaded: {snirf_info['Error']}"
|
||||||
# else:
|
else:
|
||||||
# info += "\nSNIRF Metadata:\n"
|
info += "\nSNIRF Metadata:\n"
|
||||||
# for k, v in snirf_info.items():
|
for k, v in snirf_info.items():
|
||||||
# if isinstance(v, list):
|
if isinstance(v, list):
|
||||||
# info += f" {k}:\n"
|
info += f" {k}:\n"
|
||||||
# for item in v:
|
for item in v:
|
||||||
# info += f" - {item}\n"
|
info += f" - {item}\n"
|
||||||
# else:
|
else:
|
||||||
# info += f" {k}: {v}\n"
|
info += f" {k}: {v}\n"
|
||||||
|
|
||||||
self.top_left_widget.setText(info)
|
self.top_left_widget.setText(info)
|
||||||
|
|
||||||
|
clicked_bubble = self.sender()
|
||||||
|
file_path = clicked_bubble.file_path
|
||||||
|
|
||||||
|
# Save current file's metadata
|
||||||
|
if self.current_file:
|
||||||
|
self.save_metadata(self.current_file)
|
||||||
|
|
||||||
|
# Update current file
|
||||||
|
self.current_file = file_path
|
||||||
|
|
||||||
|
# Load new file's metadata into the fields
|
||||||
|
metadata = self.file_metadata.get(file_path, {})
|
||||||
|
for key, field in self.meta_fields.items():
|
||||||
|
field.blockSignals(True)
|
||||||
|
field.setText(metadata.get(key, ""))
|
||||||
|
field.blockSignals(False)
|
||||||
|
|
||||||
def placeholder(self):
|
def placeholder(self):
|
||||||
QMessageBox.information(self, "Placeholder", "This feature is not implemented yet.")
|
QMessageBox.information(self, "Placeholder", "This feature is not implemented yet.")
|
||||||
|
|
||||||
|
def save_metadata(self, file_path):
|
||||||
|
if not file_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.file_metadata[file_path] = {
|
||||||
|
key: field.text()
|
||||||
|
for key, field in self.meta_fields.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_all_metadata(self):
|
||||||
|
# First, make sure current file's edits are saved
|
||||||
|
if self.current_file:
|
||||||
|
self.save_metadata(self.current_file)
|
||||||
|
|
||||||
|
return self.file_metadata
|
||||||
|
|
||||||
|
|
||||||
'''MODULE FILE'''
|
'''MODULE FILE'''
|
||||||
def on_run_task(self):
|
def on_run_task(self):
|
||||||
|
|
||||||
|
all_metadata = self.get_all_metadata()
|
||||||
|
|
||||||
|
for file_path, data in all_metadata.items():
|
||||||
|
print(f"{file_path}: {data}")
|
||||||
|
|
||||||
collected_data = {}
|
collected_data = {}
|
||||||
|
|
||||||
@@ -1164,7 +1264,6 @@ class MainApplication(QMainWindow):
|
|||||||
collected_data["SNIRF_SUBFOLDERS"] = [Path(p).name for p in self.selected_paths]
|
collected_data["SNIRF_SUBFOLDERS"] = [Path(p).name for p in self.selected_paths]
|
||||||
collected_data["STIM_DURATION"] = [0 for _ in self.selected_paths]
|
collected_data["STIM_DURATION"] = [0 for _ in self.selected_paths]
|
||||||
|
|
||||||
|
|
||||||
elif hasattr(self, "selected_path") and self.selected_path:
|
elif hasattr(self, "selected_path") and self.selected_path:
|
||||||
# Handle single folder
|
# Handle single folder
|
||||||
selected_path = Path(self.selected_path)
|
selected_path = Path(self.selected_path)
|
||||||
@@ -1176,10 +1275,9 @@ class MainApplication(QMainWindow):
|
|||||||
# No folder selected - handle gracefully or raise error
|
# No folder selected - handle gracefully or raise error
|
||||||
raise ValueError("No folder(s) selected")
|
raise ValueError("No folder(s) selected")
|
||||||
|
|
||||||
|
collected_data["METADATA"] = all_metadata
|
||||||
collected_data["HRF_MODEL"] = 'fir'
|
collected_data["HRF_MODEL"] = 'fir'
|
||||||
|
|
||||||
collected_data["MAX_WORKERS"] = 12
|
|
||||||
collected_data["FORCE_DROP_CHANNELS"] = []
|
collected_data["FORCE_DROP_CHANNELS"] = []
|
||||||
|
|
||||||
collected_data["TARGET_ACTIVITY"] = "Reach"
|
collected_data["TARGET_ACTIVITY"] = "Reach"
|
||||||
@@ -1445,6 +1543,39 @@ class MainApplication(QMainWindow):
|
|||||||
def normalize(v): return [int(x) for x in v.split(".")]
|
def normalize(v): return [int(x) for x in v.split(".")]
|
||||||
return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2))
|
return (normalize(v1) > normalize(v2)) - (normalize(v1) < normalize(v2))
|
||||||
|
|
||||||
|
def get_snirf_metadata_mne(self, file_name):
|
||||||
|
import mne
|
||||||
|
from mne.preprocessing.nirs import source_detector_distances
|
||||||
|
|
||||||
|
raw = mne.io.read_raw_snirf(file_name, preload=True)
|
||||||
|
|
||||||
|
snirf_info = {}
|
||||||
|
|
||||||
|
# Measurement date
|
||||||
|
snirf_info['Measurement Date'] = str(raw.info.get('meas_date'))
|
||||||
|
|
||||||
|
# Source-detector distances
|
||||||
|
distances = source_detector_distances(raw.info)
|
||||||
|
distance_info = []
|
||||||
|
for ch_name, dist in zip(raw.info['ch_names'], distances):
|
||||||
|
distance_info.append(f"{ch_name}: {dist:.4f} m")
|
||||||
|
snirf_info['Source-Detector Distances'] = distance_info
|
||||||
|
|
||||||
|
# Digitization points / optode positions
|
||||||
|
dig = raw.info.get('dig', None)
|
||||||
|
if dig is not None:
|
||||||
|
dig_info = []
|
||||||
|
for point in dig:
|
||||||
|
kind = point['kind']
|
||||||
|
ident = point['ident']
|
||||||
|
coord = point['r']
|
||||||
|
dig_info.append(f"Kind: {kind}, ID: {ident}, Coord: {coord}")
|
||||||
|
snirf_info['Digitization Points'] = dig_info
|
||||||
|
else:
|
||||||
|
snirf_info['Digitization Points'] = "Not found"
|
||||||
|
|
||||||
|
return snirf_info
|
||||||
|
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
# Gracefully shut down multiprocessing children
|
# Gracefully shut down multiprocessing children
|
||||||
|
|||||||
Reference in New Issue
Block a user