updates for next version

This commit is contained in:
2025-08-19 13:57:14 -07:00
parent 6b50e7f9f5
commit 65e33c8619
3 changed files with 173 additions and 26 deletions

16
changelog.md Normal file
View File

@@ -0,0 +1,16 @@
# 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).
# Version 1.0.0
- Initial release.

View File

@@ -42,14 +42,14 @@ from pandas import DataFrame
import matplotlib.pyplot as plt
from matplotlib.axes import Axes
from numpy.typing import NDArray
import vtkmodules.util.data_model
#import vtkmodules.util.data_model
from numpy import floating, float64
from matplotlib.lines import Line2D
import matplotlib.colors as mcolors
from scipy.stats import ttest_1samp # type: ignore
from matplotlib.figure import Figure
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 scipy.signal import welch, butter, filtfilt # type: ignore
from matplotlib.colors import LinearSegmentedColormap
@@ -127,7 +127,7 @@ EXCLUDE_CHANNELS: bool
MAX_BAD_CHANNELS: int
LONG_CHANNEL_THRESH: float
PPF: float
METADATA: dict
DRIFT_MODEL: str
DURATION_BETWEEN_ACTIVITIES: int
@@ -223,7 +223,7 @@ REQUIRED_KEYS: dict[str, Any] = {
"EXCLUDE_CHANNELS": bool,
"MAX_BAD_CHANNELS": int,
"LONG_CHANNEL_THRESH": float,
"PPF": float,
"METADATA": dict,
"DRIFT_MODEL": str,
"DURATION_BETWEEN_ACTIVITIES": int,
"HRF_MODEL": str,
@@ -284,8 +284,6 @@ else:
logger = logging.getLogger()
class ProcessingError(Exception):
def __init__(self, message: str = "Something went wrong!"):
self.message = message
@@ -1615,7 +1613,7 @@ def calculate_optical_density(data: BaseRaw, ID: str) -> tuple[BaseRaw, Figure]:
# 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.
@@ -1625,7 +1623,9 @@ def calculate_haemoglobin_concentration(optical_density_data: BaseRaw, ID: str)
The data in optical density format.
ID : str
File name of the the snirf file that was loaded.
file_path : str
Entire file path if snirf file that was loaded.
Returns
-------
tuple[BaseRaw, Figure]
@@ -1636,7 +1636,7 @@ def calculate_haemoglobin_concentration(optical_density_data: BaseRaw, ID: str)
logger.info("Calculating haemoglobin concentration data...")
# 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...")
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 +1720,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)
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
event_dict = {"Reach": 1, "Start of Rest": 2}
@@ -1917,6 +1917,33 @@ def run_GLM_analysis(data: BaseRaw, design_matrix: DataFrame) -> RegressionResul
return glm_est
def calculate_dpf(file_path):
# order is hbo / hbr
import h5py
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]:
"""
@@ -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
raw_full_layout = data
logger.info(file_path)
logger.info(ID)
logger.info(METADATA.get(file_path))
calculate_dpf(file_path)
try:
# Did the user want to load new channel positions from an optode file?
# 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
temp = data.copy()
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
# STEP 3
@@ -2082,11 +2115,11 @@ def individual_GLM_analysis(file_path: str, ID: str, stim_duration: float = 5.0,
if SHORT_CHANNEL:
short_chans_od = cast(BaseRaw, optical_density(short_chans))
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
# STEP 9
data, fig = calculate_haemoglobin_concentration(data, ID)
data, fig = calculate_haemoglobin_concentration(data, ID, file_path)
order_of_operations += " + Haemoglobin Concentration"
fig_dict['HaemoglobinConcentration'] = fig
if progress_callback: progress_callback(9)

122
main.py
View File

@@ -27,7 +27,7 @@ import psutil
import requests
from PySide6.QtWidgets import (
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.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleValidator
@@ -114,7 +114,7 @@ SECTIONS = [
{
"title": "Haemoglobin Concentration",
"params": [
{"name": "PPF", "default": 0.1, "type": float, "help": "Partial Pathlength Factor."},
# Intentionally empty (TODO)
]
},
{
@@ -128,7 +128,7 @@ SECTIONS = [
{
"title": "General Linear Model",
"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)
]
},
{
"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)
# Store the file path
self.file_path = file_path
self.file_path = os.path.normpath(file_path)
self.current_step = 0
# Make the bubble clickable
@@ -619,6 +625,10 @@ class MainApplication(QMainWindow):
self.platform_suffix = "-" + PLATFORM_NAME
self.pending_update_version = None
self.pending_update_path = None
self.file_metadata = {}
self.current_file = None
# Start local pending update check thread
self.local_check_thread = LocalPendingUpdateCheckThread(CURRENT_VERSION, self.platform_suffix)
@@ -642,11 +652,51 @@ class MainApplication(QMainWindow):
left_container.setLayout(left_layout)
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.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.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
self.bubble_container = QWidget()
@@ -660,7 +710,6 @@ class MainApplication(QMainWindow):
self.scroll_area.setMinimumHeight(300)
# Add top left and bottom left to left layout
left_layout.addWidget(self.top_left_widget)
left_layout.addWidget(self.scroll_area)
self.progress_update_signal.connect(self.update_file_progress)
@@ -713,6 +762,7 @@ class MainApplication(QMainWindow):
self.button2.setMinimumSize(100, 40)
self.button3.setMinimumSize(100, 40)
self.button1.setVisible(False)
self.button3.setVisible(False)
self.button1.clicked.connect(self.on_run_task)
@@ -828,6 +878,8 @@ class MainApplication(QMainWindow):
def clear_all(self):
self.right_column_widget.hide()
# Clear the bubble layout
while self.bubble_layout.count():
item = self.bubble_layout.takeAt(0)
@@ -846,6 +898,7 @@ class MainApplication(QMainWindow):
self.all_figures = None
# Reset any visible UI elements
self.button1.setVisible(False)
self.button3.setVisible(False)
self.top_left_widget.clear()
@@ -889,6 +942,8 @@ class MainApplication(QMainWindow):
if file_path:
self.selected_path = file_path # store the file path
self.show_files_as_bubbles(file_path)
self.button1.setVisible(True)
def open_folder_dialog(self):
folder_path = QFileDialog.getExistingDirectory(
@@ -897,6 +952,8 @@ class MainApplication(QMainWindow):
if folder_path:
self.selected_path = folder_path # store the folder path
self.show_files_as_bubbles(folder_path)
self.button1.setVisible(True)
def open_multiple_folders_dialog(self):
@@ -921,6 +978,8 @@ class MainApplication(QMainWindow):
)
if more == QMessageBox.StandardButton.No:
break
self.button1.setVisible(True)
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)
# Re-enable the "Viewer" button
# Re-enable the buttons
self.button1.setVisible(True)
self.button3.setVisible(True)
QMessageBox.information(self, "Loaded", f"Project loaded from:\n{filename}")
@@ -1089,6 +1149,10 @@ class MainApplication(QMainWindow):
def on_bubble_clicked(self, bubble):
# show age / gender
self.right_column_widget.show()
file_path = bubble.file_path
if not os.path.exists(file_path):
self.top_left_widget.setText("File not found.")
@@ -1121,15 +1185,51 @@ class MainApplication(QMainWindow):
# info += f" {k}: {v}\n"
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):
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'''
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 = {}
@@ -1164,7 +1264,6 @@ class MainApplication(QMainWindow):
collected_data["SNIRF_SUBFOLDERS"] = [Path(p).name for p in self.selected_paths]
collected_data["STIM_DURATION"] = [0 for _ in self.selected_paths]
elif hasattr(self, "selected_path") and self.selected_path:
# Handle single folder
selected_path = Path(self.selected_path)
@@ -1176,10 +1275,9 @@ class MainApplication(QMainWindow):
# No folder selected - handle gracefully or raise error
raise ValueError("No folder(s) selected")
collected_data["METADATA"] = all_metadata
collected_data["HRF_MODEL"] = 'fir'
collected_data["MAX_WORKERS"] = 12
collected_data["FORCE_DROP_CHANNELS"] = []
collected_data["TARGET_ACTIVITY"] = "Reach"