updates for next version
This commit is contained in:
16
changelog.md
Normal file
16
changelog.md
Normal 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.
|
||||
@@ -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
122
main.py
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user