Compare commits
4
Commits
d1c9346e57
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ecafe09ac | ||
|
|
8270d7b7a8 | ||
|
|
2e9820336d | ||
|
|
a2456daca7 |
No files matched your search
+2
-1
@@ -184,4 +184,5 @@ flares-*
|
|||||||
*.flare
|
*.flare
|
||||||
*.cfg
|
*.cfg
|
||||||
tempCodeRunnerFile.py
|
tempCodeRunnerFile.py
|
||||||
*.pkl
|
*.pkl
|
||||||
|
*.joblib
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
# Version 1.8.1
|
||||||
|
|
||||||
|
- Quick fix to plugins not loading on certain platforms
|
||||||
|
|
||||||
|
|
||||||
# Version 1.8.0
|
# Version 1.8.0
|
||||||
|
|
||||||
- Plugin Manager now supports checking and downloading plugin updates to ensure plugins can be updated
|
- Plugin Manager now supports checking and downloading plugin updates to ensure plugins can be updated
|
||||||
@@ -12,7 +17,10 @@
|
|||||||
- Export To CSV Viewer export options now perform the actions they claim rather than having no functionality
|
- Export To CSV Viewer export options now perform the actions they claim rather than having no functionality
|
||||||
- Logs on a per participant basis are now available to be viewed from within the application
|
- Logs on a per participant basis are now available to be viewed from within the application
|
||||||
- They can be accessed by right clicking a participant bubble and selecting "Show Logs". Logs will be empty if they have not been processed
|
- They can be accessed by right clicking a participant bubble and selecting "Show Logs". Logs will be empty if they have not been processed
|
||||||
|
- A notification now appears when processing completes even if the application is not focused
|
||||||
- Fixed a regression where opening a folder would no longer load all the files in that folder
|
- Fixed a regression where opening a folder would no longer load all the files in that folder
|
||||||
|
- Added inspirational quotes to the About window
|
||||||
|
- Licence is now clickable from the About window
|
||||||
- Improved code stability and functionality
|
- Improved code stability and functionality
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-51
@@ -1,55 +1,24 @@
|
|||||||
# Version 1.7.2
|
# Version 1.8.1
|
||||||
|
|
||||||
- Fixed the Linux build not starting if shared library files were missing from the host machine
|
- Quick fix to plugins not loading on certain platforms
|
||||||
- Fixed the macOS version not loading plugins due to not having an HTTPS certificate
|
|
||||||
|
|
||||||
|
|
||||||
# Version 1.7.1
|
# Version 1.8.0
|
||||||
|
|
||||||
- Added support for Plugins! Plugins can be downloaded from the official repository or from custom ones. Support for creating a plugin can be found on the wiki
|
- Plugin Manager now supports checking and downloading plugin updates to ensure plugins can be updated
|
||||||
- Files and folders can now be dragged and dropped into the loaded files area to be loaded instead of relying on the file menu
|
- Plugin Manager now properly uninstalls plugins when requested rather than doing nothing
|
||||||
- Fixed an issue where shadow snirf files would attempt to be loaded by the application. Fixes [Issue 92](https://git.research.dezeeuw.ca/tyler/flares/issues/92)
|
- Installing plugins now displays a progress bar when downloading and no longer hangs the application when downloading a big plugin
|
||||||
- Fixed an issue where the application could not automatically update between versions 1.5.1 to 1.7.1. Sorry! 1.7.1 upgrading onward should be fixed and more robust
|
- Project loading now displays a loading message when loading big projects and no longer hangs the application when loading
|
||||||
- Fixed an issue where some log files would not generate, or would generate in an incorrect location
|
- New preferences options and menus are now available! All existing preferences have been moved into new sub categories
|
||||||
- Fixed an issue where updating events in a snirf file would not release memory properly
|
- The UI preference category now has the Show Advanced Parameters preference
|
||||||
- Fixed an issue where loading multiple files in quick succession could cause some of the files to never finish loading
|
- The Updates preference category has two ney options to prevent the automatic checking of application or plugin updates
|
||||||
- Fixed an issue where loading a saved project, pushing clear, and re-opening snirf files would cause them to appear incorrectly in the loaded files area
|
- The Theme category has remain untouched and all other preferences have been moved to the Processing category
|
||||||
- Fixed a crucial bug where short channels were still being sent to the donor pool to assist in interpolating long channels from. Fixes [Issue 80](https://git.research.dezeeuw.ca/tyler/flares/issues/80)
|
- Added a new preference in the Processing category called "Separate Events". This allows events to have their own values, and not be collapsed into a single value
|
||||||
|
- Export To CSV Viewer export options now perform the actions they claim rather than having no functionality
|
||||||
|
- Logs on a per participant basis are now available to be viewed from within the application
|
||||||
# Version 1.7.0
|
- They can be accessed by right clicking a participant bubble and selecting "Show Logs". Logs will be empty if they have not been processed
|
||||||
|
- A notification now appears when processing completes even if the application is not focused
|
||||||
- This is potentially a save-changing release due to adding more data into the save file. Please update your project files to ensure compatibility
|
- Fixed a regression where opening a folder would no longer load all the files in that folder
|
||||||
- Renamed all instances of "Inter" to properly read as "Intra" and changed "Cross" to now read as "Inter"
|
- Added inspirational quotes to the About window
|
||||||
- Changed RESAMPLE to only apply where it is required to avoid having Functional Connectivity analysis methods running on data that has been resampled
|
- Licence is now clickable from the About window
|
||||||
- Added parameters that appear when attempting to generate results from the Participant and Intra-Group Functional Connectivity viewers and removed the non-functional placeholder parameters
|
- Improved code stability and functionality
|
||||||
- Modified the Participant and Intra-Group Functional Connectivity analysis options to better perform their expected tasks. This remains as a BETA feature
|
|
||||||
- Removed the existing Intra-Group Functional Connectivity option and replaced it with two new ones: Beta-Series Correlation and Spectral Coherence (epochs)
|
|
||||||
- Updated the names of the methods provided for the Participant Functional Connectivity Viewer to better match the actions they perform
|
|
||||||
- Updated the warnings for the Functional Connectivity Viewers to better represent the challenges these analysis options now face
|
|
||||||
- Added basic unit testing to hopefully prevent any accidental processing changes from occurring in the future
|
|
||||||
- Added a new parameter to the PSP section on the right side of the screen: PSP_USE_HEART_RATE_BAND. This functions similarly to the existing SCI_USE_HEART_RATE_BAND
|
|
||||||
- Added description text to the Inter-Group and Intra-Group Brain and Image Viewers, as well as the Functional Connectivity windows to explain what output can be expected
|
|
||||||
- Added a new Preference option of Theme. Allows from selecting Auto (System default), Light, or Dark. Fixes [Issue 7](https://git.research.dezeeuw.ca/tyler/flares/issues/7)
|
|
||||||
- Optode updater can now update optode locations in multiple snirf files at once
|
|
||||||
- Removed image index 1 (Significance) from the Intra-Group Brain and Image Viewer as it is now provided more in depth with the Stats viewers
|
|
||||||
- Modified the timeout when waiting for the application to close while performing updates down to a reasonable number
|
|
||||||
- Modified the help messages for parameters in the SCI and PSP areas to better reflect how the parameters are used
|
|
||||||
- Modified the heart rate calculation to be more precise and correct when dealing with good data and not messy data
|
|
||||||
- Modified the heart rate calculation to not take only one channel in the data to use, but rather an average of channels. This still prefers short channels if they are present
|
|
||||||
- Fixed an issue where loading a project from a saved project file would not allow the data to be reprocessed with new parameters
|
|
||||||
- Fixed an issue where the "Hand" BIDS metadata value would not populate on bubbles correctly
|
|
||||||
- Fixed an issue where performing PSP could ignore the bad channels that were marked by SCI and SNR
|
|
||||||
- Fixed an issue where the heart rate calculation would disregard all calculations and fall back to an extremely rudimentary calculation
|
|
||||||
- Fixed an issue that could prevent log file generation while the application was in the middle of an update
|
|
||||||
- Fixed an issue where a rare crash could occur while the application was in the middle of an update
|
|
||||||
- Fixed an issue that could have passed multiple conditions when generating an Intra-Group Stats image
|
|
||||||
- Fixed an issue that could pass NaN values when attempting to collapse channels
|
|
||||||
- Fixed an issue that was causing the OLS model to always be used for brain images with multiple participants, and not the MixedLM model
|
|
||||||
- Fixed an issue that could cause the Wavelet filtering step to crash
|
|
||||||
- Fixed an issue where file associations appeared to work as intended but would not load the project on macOS and only open the application
|
|
||||||
- Fixed an issue where file associations would refuse to associate on macOS once they have attempted to be associated
|
|
||||||
- Fixed an issue where certain parameters would not enable or disable depending on other parameters when they should've
|
|
||||||
- Fixed an issue where not all widgets would close when attempting to close the application causing the application to crash
|
|
||||||
- Fixed an issue where events were not created correctly after the data had been resampled by the design matrix
|
|
||||||
- Fixed an issue where progress bar colors would not reset if the data was reprocessed
|
|
||||||
@@ -14,6 +14,7 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
import logging.handlers
|
||||||
import warnings
|
import warnings
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
@@ -105,7 +106,7 @@ from mne_nirs.statistics._glm_level_first import RegressionResults # type: igno
|
|||||||
from mne_connectivity.viz import plot_connectivity_circle # type: ignore
|
from mne_connectivity.viz import plot_connectivity_circle # type: ignore
|
||||||
from mne_connectivity import envelope_correlation, spectral_connectivity_epochs, spectral_connectivity_time # type: ignore
|
from mne_connectivity import envelope_correlation, spectral_connectivity_epochs, spectral_connectivity_time # type: ignore
|
||||||
|
|
||||||
from src.shared.shareddata import PLATFORM_NAME, resource_path, get_app_dir
|
from src.shared.shareddata import PLATFORM_NAME, resource_path, get_app_dir, METRIC_REGISTRY
|
||||||
|
|
||||||
|
|
||||||
PRIMARY_COLORS = {
|
PRIMARY_COLORS = {
|
||||||
@@ -310,21 +311,36 @@ script_dir = os.path.dirname(os.path.abspath(__file__))
|
|||||||
os.chdir(script_dir)
|
os.chdir(script_dir)
|
||||||
|
|
||||||
|
|
||||||
# Configure logging to file with timestamps and realtime flush
|
# Configure logging with rotation to prevent indefinite growth
|
||||||
if PLATFORM_NAME == 'darwin':
|
if PLATFORM_NAME == 'darwin':
|
||||||
log_path = os.path.abspath(os.path.join(os.path.dirname(sys.executable), "../../../fnirs_analysis.log"))
|
log_path = os.path.abspath(os.path.join(os.path.dirname(sys.executable), "../../../fnirs_analysis.log"))
|
||||||
else:
|
else:
|
||||||
log_path = os.path.join(get_app_dir(), "fnirs_analysis.log")
|
log_path = os.path.join(get_app_dir(), "fnirs_analysis.log")
|
||||||
|
|
||||||
logging.basicConfig(
|
# Create logger
|
||||||
filename=log_path,
|
logger = logging.getLogger()
|
||||||
level=logging.INFO,
|
logger.setLevel(logging.INFO)
|
||||||
format='%(asctime)s - %(processName)s - %(levelname)s - %(message)s',
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S',
|
# Clear any existing handlers
|
||||||
filemode='a'
|
logger.handlers.clear()
|
||||||
|
|
||||||
|
# Create rotating file handler (max 10MB per file, keep 5 backups = ~50MB max total)
|
||||||
|
rotating_handler = logging.handlers.RotatingFileHandler(
|
||||||
|
log_path,
|
||||||
|
maxBytes=10*1024*1024, # 10 MB
|
||||||
|
backupCount=5, # Keep 5 backup files
|
||||||
|
encoding='utf-8'
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger()
|
# Set formatter
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(processName)s - %(levelname)s - %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
rotating_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
# Add handler to logger
|
||||||
|
logger.addHandler(rotating_handler)
|
||||||
|
|
||||||
|
|
||||||
def set_config_me(config: dict[str, Any]) -> None:
|
def set_config_me(config: dict[str, Any]) -> None:
|
||||||
@@ -7152,20 +7168,6 @@ def write_qc_excel_summary(
|
|||||||
wb.save(output_path)
|
wb.save(output_path)
|
||||||
|
|
||||||
|
|
||||||
METRIC_REGISTRY = {
|
|
||||||
'Peak_Amp': 'Peak_Amp',
|
|
||||||
'TTP': 'Time_to_Peak',
|
|
||||||
'AUC': 'AUC',
|
|
||||||
'Rising_Slope': 'Rising_Slope',
|
|
||||||
'Recovery_Slope': 'Recovery_Slope',
|
|
||||||
'FWHM': 'FWHM',
|
|
||||||
'Onset_Latency': 'Onset_Latency',
|
|
||||||
'P2P_Amp': 'Peak_to_Peak_Amp',
|
|
||||||
'Signal_Std': 'Signal_Std',
|
|
||||||
'RMS': 'RMS'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def plot_and_enqueue_waveform_metrics(
|
def plot_and_enqueue_waveform_metrics(
|
||||||
roi_curves,
|
roi_curves,
|
||||||
fir_delays,
|
fir_delays,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ if sys.platform.startswith("linux"):
|
|||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication, QWidget, QMessageBox, QVBoxLayout, QHBoxLayout, QTextEdit, QScrollArea, QComboBox, QGridLayout, QSplitter, QDialogButtonBox, QHeaderView,
|
QApplication, QStyle, QSystemTrayIcon, QWidget, QMessageBox, QVBoxLayout, QHBoxLayout, QTextEdit, QScrollArea, QComboBox, QGridLayout, QSplitter, QDialogButtonBox, QHeaderView,
|
||||||
QPushButton, QMainWindow, QLabel, QLineEdit, QGroupBox, QDialog, QMenu, QSpinBox, QTableWidget, QTableWidgetItem
|
QPushButton, QMainWindow, QLabel, QLineEdit, QGroupBox, QDialog, QMenu, QSpinBox, QTableWidget, QTableWidgetItem
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import QEvent, QObject, Signal, Qt, QTimer
|
from PySide6.QtCore import QEvent, QObject, Signal, Qt, QTimer
|
||||||
@@ -738,6 +738,18 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
self.plugin_manager.reload_plugins()
|
self.plugin_manager.reload_plugins()
|
||||||
|
|
||||||
|
self.tray_icon = QSystemTrayIcon(self)
|
||||||
|
stock_icon = self.style().standardIcon(QStyle.StandardPixmap.SP_ComputerIcon) # Or use your own QIcon("path_to_logo.png")
|
||||||
|
self.tray_icon.setIcon(stock_icon)
|
||||||
|
self.tray_icon.show()
|
||||||
|
|
||||||
|
def bring_to_front():
|
||||||
|
self.showNormal() # Restores if minimized
|
||||||
|
self.raise_() # Brings to top of window stack (macOS/Linux)
|
||||||
|
self.activateWindow() # Focuses the window (Windows)
|
||||||
|
|
||||||
|
self.tray_icon.messageClicked.connect(bring_to_front)
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
# Check if we should pop up the welcome screen
|
# Check if we should pop up the welcome screen
|
||||||
@@ -2361,9 +2373,16 @@ class MainApplication(QMainWindow):
|
|||||||
elapsed_str = self._format_elapsed(msg.get("elapsed", getattr(self, "_last_elapsed", 0)))
|
elapsed_str = self._format_elapsed(msg.get("elapsed", getattr(self, "_last_elapsed", 0)))
|
||||||
speedup = msg.get("speedup")
|
speedup = msg.get("speedup")
|
||||||
speedup_str = f" | Speedup: {speedup:.1f}x" if speedup else ""
|
speedup_str = f" | Speedup: {speedup:.1f}x" if speedup else ""
|
||||||
self.statusbar.showMessage(
|
status_text = f"Complete: {success_count} succeeded, {fail_count} failed. | Total time: {elapsed_str}{speedup_str}"
|
||||||
f"Complete: {success_count} succeeded, {fail_count} failed. | Total time: {elapsed_str}{speedup_str}"
|
self.statusbar.showMessage(status_text)
|
||||||
)
|
|
||||||
|
if hasattr(self, "tray_icon") and self.tray_icon.isSystemTrayAvailable():
|
||||||
|
self.tray_icon.showMessage(
|
||||||
|
"Process Completed",
|
||||||
|
status_text,
|
||||||
|
QSystemTrayIcon.MessageIcon.Information,
|
||||||
|
4000 # Duration in milliseconds
|
||||||
|
)
|
||||||
|
|
||||||
if success_count > 0:
|
if success_count > 0:
|
||||||
self.is_saved = False
|
self.is_saved = False
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
Filename: inspiration.py
|
||||||
|
Description: Contains inspirational quotes to be shown at random
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
|
Author: Tyler de Zeeuw
|
||||||
|
License: GPL-3.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
|
import random
|
||||||
|
|
||||||
|
FNIRS_QUOTES = [
|
||||||
|
"Understanding the brain through light and data.",
|
||||||
|
"Every signal offers another perspective on brain function.",
|
||||||
|
"Exploring the relationship between brain activity and behavior.",
|
||||||
|
"Using light to investigate the complexity of the human brain.",
|
||||||
|
"Turning neural measurements into meaningful insights.",
|
||||||
|
"A deeper understanding of the brain begins with better questions.",
|
||||||
|
"Studying the brain means looking beyond what we can see.",
|
||||||
|
"Connecting brain activity to the way we think, feel, and act.",
|
||||||
|
"Even small changes in brain signals can reveal meaningful patterns.",
|
||||||
|
"Advancing neuroscience through new ways of measuring brain activity.",
|
||||||
|
"The brain is complex, and every measurement adds another piece.",
|
||||||
|
"Using fNIRS to explore the connection between the brain and the world around us.",
|
||||||
|
"Good science begins with curiosity and grows through careful observation.",
|
||||||
|
"There is still so much to learn about how the brain works.",
|
||||||
|
"Measuring brain activity is only the beginning; understanding it is the goal.",
|
||||||
|
"Exploring what brain activity can tell us about the human experience.",
|
||||||
|
"Every experiment brings a new opportunity to understand the brain.",
|
||||||
|
"The more we measure, the more questions we learn to ask.",
|
||||||
|
"Neuroscience advances through careful measurements and thoughtful interpretation.",
|
||||||
|
"Understanding brain function requires looking at both signals and context.",
|
||||||
|
"Each dataset offers a different view of the brain in action.",
|
||||||
|
"Exploring the neural processes behind everyday human experiences.",
|
||||||
|
"Better measurements can lead to better questions about the brain.",
|
||||||
|
"The challenge is not just collecting data, but understanding what it means.",
|
||||||
|
"Brain activity reflects a complex interaction of processes and experiences.",
|
||||||
|
"Scientific progress begins with observing what we do not yet understand.",
|
||||||
|
"Each discovery builds on the questions that came before it.",
|
||||||
|
"There is more to brain activity than a single signal can reveal.",
|
||||||
|
"New tools create new opportunities to investigate old questions.",
|
||||||
|
"The goal is to connect what we measure with what we want to understand.",
|
||||||
|
"Neural signals provide clues to the processes underlying human behavior.",
|
||||||
|
"A better understanding of brain function starts with better evidence.",
|
||||||
|
"The value of a measurement lies in what it helps us understand.",
|
||||||
|
"Every experiment adds another perspective to a complex picture.",
|
||||||
|
"Scientific discovery often begins with noticing something unexpected.",
|
||||||
|
"Understanding the brain is an ongoing process of questioning and refinement.",
|
||||||
|
"The best insights emerge when data meets thoughtful interpretation.",
|
||||||
|
"Research moves forward when curiosity is matched with rigorous methods.",
|
||||||
|
"There is always another question worth exploring.",
|
||||||
|
"Investigating the brain means embracing complexity rather than avoiding it.",
|
||||||
|
"Progress comes from asking precise questions about complicated systems.",
|
||||||
|
"The brain does not work in isolation from the person or environment.",
|
||||||
|
"Understanding behavior requires more than observing behavior alone.",
|
||||||
|
"The smallest details can sometimes change how we interpret the bigger picture.",
|
||||||
|
"Every method offers a different window into brain function.",
|
||||||
|
"Different perspectives help build a more complete understanding of the brain.",
|
||||||
|
"Good research turns uncertainty into questions that can be investigated.",
|
||||||
|
"The process of discovery is as important as the result.",
|
||||||
|
"Meaningful findings begin with reliable data and careful reasoning.",
|
||||||
|
"The brain is not simple, and our methods should reflect that complexity.",
|
||||||
|
"Scientific understanding grows when evidence challenges our assumptions.",
|
||||||
|
"Research is a process of learning what the data can—and cannot—tell us.",
|
||||||
|
"The most useful findings are those that move understanding forward.",
|
||||||
|
"Each new method expands the questions we can ask about the brain.",
|
||||||
|
"The path to understanding is built from observations, questions, and evidence.",
|
||||||
|
"Neuroscience is about understanding the processes behind the patterns.",
|
||||||
|
"The aim is not simply to measure brain activity, but to interpret it meaningfully.",
|
||||||
|
"Better tools help us investigate the questions that matter.",
|
||||||
|
"Progress in neuroscience depends on both technical precision and human curiosity.",
|
||||||
|
"The brain is complex, but every careful observation helps clarify a part of it.",
|
||||||
|
"Understanding brain function means connecting measurements to meaningful outcomes.",
|
||||||
|
"Research turns curiosity into evidence and evidence into understanding.",
|
||||||
|
"There is a story behind every pattern, but evidence determines what we can conclude.",
|
||||||
|
"New insights emerge when we approach familiar questions from different angles.",
|
||||||
|
"The strongest conclusions begin with careful measurements.",
|
||||||
|
"Understanding the brain takes more than data; it takes context.",
|
||||||
|
"The questions we ask shape what we learn from the brain.",
|
||||||
|
"Good research is not about finding answers quickly, but finding them carefully.",
|
||||||
|
"Every result contributes to a larger conversation about how the brain works.",
|
||||||
|
"The human brain remains an evolving question for science.",
|
||||||
|
"Measuring brain activity gives us another way to investigate human cognition.",
|
||||||
|
"The connection between brain function and behavior is rarely straightforward.",
|
||||||
|
"Scientific progress comes from combining new methods with careful interpretation.",
|
||||||
|
"The more we learn about the brain, the better our questions become.",
|
||||||
|
"Research is the practice of making uncertainty more understandable.",
|
||||||
|
"Understanding the brain means being willing to revise what we think we know.",
|
||||||
|
"Each experiment helps define what we know and what remains uncertain.",
|
||||||
|
"The most interesting findings often begin with questions that have no easy answers.",
|
||||||
|
"The purpose of research is to move beyond assumptions toward evidence.",
|
||||||
|
"A measurement becomes useful when it helps answer a meaningful question.",
|
||||||
|
"Neuroscience brings together technology, observation, and the study of human experience.",
|
||||||
|
"fNIRS offers another way to investigate changes in brain hemodynamics.",
|
||||||
|
"Optical measurements provide a noninvasive approach to studying brain function.",
|
||||||
|
"Studying brain function outside the lab can reveal aspects of everyday cognition.",
|
||||||
|
"The flexibility of fNIRS opens new possibilities for human neuroscience research.",
|
||||||
|
"Brain measurements become more meaningful when considered alongside behavior.",
|
||||||
|
"fNIRS provides a useful perspective on the relationship between physiology and cognition.",
|
||||||
|
"Understanding the strengths and limitations of a method is part of good science.",
|
||||||
|
"The future of neuroscience depends on asking better questions, not just collecting more data.",
|
||||||
|
"Technology expands what we can measure, while critical thinking guides what we conclude.",
|
||||||
|
"The goal of neuroscience is not just to observe the brain, but to understand its function.",
|
||||||
|
"Every research method brings its own perspective, possibilities, and limitations.",
|
||||||
|
"The most meaningful discoveries connect technical advances to real-world questions.",
|
||||||
|
"Better ways to study the brain can lead to a better understanding of human experience.",
|
||||||
|
"Science progresses when we remain curious about what the evidence cannot yet explain.",
|
||||||
|
"Understanding the brain is a long-term effort built on many small discoveries.",
|
||||||
|
"Behind every useful finding is a question worth investigating.",
|
||||||
|
"The value of research lies in what we learn, not just what we measure.",
|
||||||
|
"Good science makes room for complexity, uncertainty, and new perspectives.",
|
||||||
|
"The next insight may come from looking at a familiar problem in a different way.",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_quote() -> None:
|
||||||
|
return f"'{random.choice(FNIRS_QUOTES)}'"
|
||||||
@@ -8,12 +8,18 @@ License: GPL-3.0
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Built-in imports
|
# Built-in imports
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
|
try:
|
||||||
|
import winreg
|
||||||
|
except ImportError:
|
||||||
|
winreg = None
|
||||||
|
|
||||||
CURRENT_VERSION = "1.7.2"
|
CURRENT_VERSION = "1.8.2"
|
||||||
APP_NAME = "flares"
|
APP_NAME = "flares"
|
||||||
APP_NAME_EXPANDED = "fNIRS Lightweight Analysis, Research, & Evaluation Suite"
|
APP_NAME_EXPANDED = "fNIRS Lightweight Analysis, Research, & Evaluation Suite"
|
||||||
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
||||||
@@ -72,6 +78,63 @@ DATA_SCHEMA = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
METRIC_REGISTRY = {
|
||||||
|
"Peak_Amp": "Peak_Amp",
|
||||||
|
"TTP": "Time_to_Peak",
|
||||||
|
"AUC": "AUC",
|
||||||
|
"Rising_Slope": "Rising_Slope",
|
||||||
|
"Recovery_Slope": "Recovery_Slope",
|
||||||
|
"FWHM": "FWHM",
|
||||||
|
"Onset_Latency": "Onset_Latency",
|
||||||
|
"P2P_Amp": "Peak_to_Peak_Amp",
|
||||||
|
"Signal_Std": "Signal_Std",
|
||||||
|
"RMS": "RMS",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_cpu_model_name():
|
||||||
|
"""Detect detailed processor info, collapsing Windows registry whitespace padding."""
|
||||||
|
try:
|
||||||
|
system = platform.system()
|
||||||
|
|
||||||
|
if system == "Windows" and winreg is not None:
|
||||||
|
with winreg.OpenKey(
|
||||||
|
winreg.HKEY_LOCAL_MACHINE,
|
||||||
|
r"HARDWARE\DESCRIPTION\System\CentralProcessor\0",
|
||||||
|
) as key:
|
||||||
|
raw_cpu_name, _ = winreg.QueryValueEx(key, "ProcessorNameString")
|
||||||
|
cpu_name = " ".join(raw_cpu_name.split())
|
||||||
|
|
||||||
|
command = (
|
||||||
|
"Get-CimInstance Win32_Processor | "
|
||||||
|
"Select-Object NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed | "
|
||||||
|
"ConvertTo-Json -Compress"
|
||||||
|
)
|
||||||
|
cpu = json.loads(subprocess.check_output(
|
||||||
|
["powershell", "-NoProfile", "-Command", command],
|
||||||
|
text=True,
|
||||||
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||||
|
))
|
||||||
|
base_ghz = cpu["MaxClockSpeed"] / 1000
|
||||||
|
return (f"{cpu_name} @ {base_ghz:.1f}GHz "
|
||||||
|
f"{cpu['NumberOfCores']}c/{cpu['NumberOfLogicalProcessors']}t")
|
||||||
|
|
||||||
|
if system == "Darwin":
|
||||||
|
return subprocess.check_output(
|
||||||
|
["sysctl", "-n", "machdep.cpu.brand_string"], text=True
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
if system == "Linux":
|
||||||
|
with open("/proc/cpuinfo") as f:
|
||||||
|
for line in f:
|
||||||
|
if "model name" in line:
|
||||||
|
return line.split(":")[1].strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return platform.processor() or "Multi-Core CPU"
|
||||||
|
|
||||||
|
|
||||||
def resource_path(relative_path: str) -> str:
|
def resource_path(relative_path: str) -> str:
|
||||||
"""Get absolute path to resource regardless of running directly or packaged using PyInstaller"""
|
"""Get absolute path to resource regardless of running directly or packaged using PyInstaller"""
|
||||||
base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
|
base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
|
||||||
|
|||||||
+11
-1
@@ -12,6 +12,7 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
|||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from src.shared.shareddata import APP_NAME, APP_NAME_EXPANDED, CURRENT_VERSION
|
from src.shared.shareddata import APP_NAME, APP_NAME_EXPANDED, CURRENT_VERSION
|
||||||
|
from src.shared.inspiration import get_quote
|
||||||
|
|
||||||
|
|
||||||
class AboutWindow(QWidget):
|
class AboutWindow(QWidget):
|
||||||
@@ -30,12 +31,21 @@ class AboutWindow(QWidget):
|
|||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
label = QLabel(f"About {APP_NAME.upper()}", self)
|
label = QLabel(f"About {APP_NAME.upper()}", self)
|
||||||
label2 = QLabel(f"{APP_NAME_EXPANDED}", self)
|
label2 = QLabel(f"{APP_NAME_EXPANDED}", self)
|
||||||
label3 = QLabel(f"{APP_NAME.upper()} is licensed under the GPL-3.0 licence. For more information, visit https://www.gnu.org/licenses/gpl-3.0.en.html", self)
|
label3 = QLabel(
|
||||||
|
f'{APP_NAME.upper()} is licensed under the GPL-3.0 licence. '
|
||||||
|
'For more information, visit '
|
||||||
|
'<a href="https://www.gnu.org/licenses/gpl-3.0.en.html">'
|
||||||
|
'GNU GPL-3.0</a>',
|
||||||
|
self,
|
||||||
|
)
|
||||||
|
label3.setOpenExternalLinks(True)
|
||||||
label4 = QLabel(f"Version v{CURRENT_VERSION}")
|
label4 = QLabel(f"Version v{CURRENT_VERSION}")
|
||||||
|
label5 = QLabel(f"{get_quote()}", self)
|
||||||
|
|
||||||
layout.addWidget(label)
|
layout.addWidget(label)
|
||||||
layout.addWidget(label2)
|
layout.addWidget(label2)
|
||||||
layout.addWidget(label3)
|
layout.addWidget(label3)
|
||||||
layout.addWidget(label4)
|
layout.addWidget(label4)
|
||||||
|
layout.addWidget(label5)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
+90
-1
@@ -462,6 +462,88 @@ def _remove_readonly(func: Callable[[str], Any], path: str, exc: Any) -> None:
|
|||||||
func(path)
|
func(path)
|
||||||
|
|
||||||
|
|
||||||
|
def files_are_identical(src_path: str, dst_path: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if two files are identical by comparing size and modification time.
|
||||||
|
|
||||||
|
This is much faster than hashing and catches all real changes for update purposes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src_path (str): Path to source file.
|
||||||
|
dst_path (str): Path to destination file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if files are identical, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(src_path) or not os.path.exists(dst_path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
src_stat = os.stat(src_path)
|
||||||
|
dst_stat = os.stat(dst_path)
|
||||||
|
|
||||||
|
# Compare size first
|
||||||
|
if src_stat.st_size != dst_stat.st_size:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Compare modification time (rounded to 1 second to handle cross-platform differences)
|
||||||
|
src_mtime = int(src_stat.st_mtime)
|
||||||
|
dst_mtime = int(dst_stat.st_mtime)
|
||||||
|
|
||||||
|
return src_mtime == dst_mtime
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def smart_replace_folder(source_dir: str, dest_dir: str) -> Tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Replace files in dest_dir with files from source_dir, but only if they differ.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source_dir (str): Source directory containing new files.
|
||||||
|
dest_dir (str): Destination directory to update.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[int, int]: (files_updated, files_skipped) counts.
|
||||||
|
"""
|
||||||
|
files_updated = 0
|
||||||
|
files_skipped = 0
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(source_dir):
|
||||||
|
relative_path = os.path.relpath(root, source_dir)
|
||||||
|
dest_root = os.path.join(dest_dir, relative_path)
|
||||||
|
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
if not os.path.exists(dest_root):
|
||||||
|
os.makedirs(dest_root)
|
||||||
|
|
||||||
|
for filename in files:
|
||||||
|
src_file = os.path.join(root, filename)
|
||||||
|
dst_file = os.path.join(dest_root, filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Skip if file is identical
|
||||||
|
if files_are_identical(src_file, dst_file):
|
||||||
|
files_skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remove destination if it exists (handles locked files on some systems)
|
||||||
|
if os.path.exists(dst_file):
|
||||||
|
if os.path.isdir(dst_file):
|
||||||
|
shutil.rmtree(dst_file, onexc=_remove_readonly)
|
||||||
|
else:
|
||||||
|
os.remove(dst_file)
|
||||||
|
|
||||||
|
# Copy the file
|
||||||
|
shutil.copy2(src_file, dst_file)
|
||||||
|
files_updated += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Warning: Failed to update {filename}: {e}")
|
||||||
|
|
||||||
|
return files_updated, files_skipped
|
||||||
|
|
||||||
|
|
||||||
def finish_update_if_needed(platform_name: str, app_name: str, cfg_path: str, finish_update: bool) -> None:
|
def finish_update_if_needed(platform_name: str, app_name: str, cfg_path: str, finish_update: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Completes a pending application update if '--finish-update' is present in the command-line arguments.
|
Completes a pending application update if '--finish-update' is present in the command-line arguments.
|
||||||
@@ -565,7 +647,14 @@ def finish_update_if_needed(platform_name: str, app_name: str, cfg_path: str, fi
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to replace {app_name}_updater: {e}")
|
print(f"Failed to replace {app_name}_updater: {e}")
|
||||||
|
|
||||||
# 5. Clean up all temporary update folders (with retries for Windows file locks)
|
# 5. Use smart replacement - only update files that have changed
|
||||||
|
print("Using smart update: only replacing files that have changed...")
|
||||||
|
files_updated, files_skipped = smart_replace_folder(target_updater_folder, target_base)
|
||||||
|
print(f"\nUpdate summary:")
|
||||||
|
print(f" Files updated: {files_updated}")
|
||||||
|
print(f" Files skipped (unchanged): {files_skipped}")
|
||||||
|
|
||||||
|
# 6. Clean up all temporary update folders (with retries for Windows file locks)
|
||||||
for folder in update_folders:
|
for folder in update_folders:
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in new issue
Block a user