diff --git a/changelog.md b/changelog.md
index c2f7ed9..2ec877e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,39 @@
+# Version 1.5.1
+
+- Open a folder is now minimally BIDS compatible. It will recursively go through all folders, but not load external metadata
+- Renamed options in the Viewer Launcher window to better denote what actions they can perform
+- 2 new analysis options have been added in the viewer launcher window: Inter-Group Stats and Cross-Group Stats
+- Each of these analysis options have lots of text explaining what they do when their respective windows are opened
+- The parameter input dialog in the analysis windows will now be able to scroll down when lots of parameters are required
+- When a project is saved, it automatically will populate into the Recent Projects menu. Fixes [Issue 86](https://git.research.dezeeuw.ca/tyler/flares/issues/86)
+- The description when clicking "Why are these useful?" underneath the per-file parameters is no longer placeholder text. Fixes [Issue 85](https://git.research.dezeeuw.ca/tyler/flares/issues/85)
+- Added a new parameter section "Region of Interest". It has one parameter of "JSON_LOCATION", a json file containing Region of Interest data
+- Temporarily removed the parameter REMOVE_EVENTS due to its functionality being removed because of a memory leak
+- Fixed participant metadata remaing in the background when the participant was removed. Fixes [Issue 82](https://git.research.dezeeuw.ca/tyler/flares/issues/82)
+- Fixed processing remaining active hidden in the background when the "Clear" button was pushed. Fixes [Issue 81](https://git.research.dezeeuw.ca/tyler/flares/issues/81)
+- Now when "Clear" is pushed while data is processing, a popup will appear ensuring that the user understands that pushing "Clear" will stop processing
+- Reset to Default Configuration will now properly reset all of the parameters to their default values. [Issue 90](https://git.research.dezeeuw.ca/tyler/flares/issues/90)
+- A confirmation popup will now display once the application has been reset to default in addition to the status bar message
+- Changed the improper display text of "Cross Validation" to now properly read as "Coefficient of Variation"
+- Changed the parameters "CV" and "CV_THRESHOLD" to now be "COEFF_VAR and COEFF_VAR_THRESHOLD"
+- Changed the improper display text of "Mean Absolute Deviation" to now properly read as "Median Absolute Deviation"
+- Changed the parameters "SHORT_CHANNEL", "SHORT_CHANNEL_THRESH", and "LONG_CHANNEL_THRESH" to now be "SHORT_CHANNELS", "SHORT_CHANNELS_THRESHOLD", and "LONG_CHANNELS_THRESHOLD"
+- Changed the parameter section "Channel Variance" to now be "Sensor Dropout" to better reflect the action it performs, not the method employed to calculate it
+- Changed the parameters "CHANNEL_VAR" and "CHANNEL_THRESH" to now be "SENSOR_DROPOUT" and "SENSOR_DROPOUT_VARIANCE_THRESHOLD"
+- Changed the messaged displayed when the application crashes to better reflect what occured
+- Changed the processing stages 25-27 and updates their messages to better reflect their actions that they now perforn
+- Changed stage 25 from "Generate Channel Significance" to "Generate Channel Results"
+- Changed stage 26 from "Generate Channel, Region of Interest, and Contrast Results" to "Generate Region of Interest Results"
+- Changed stage 27 from "Compute Contrast Results" to "Generate Contrast Results"
+- Changed backend code to only load required methods and not load all methods every time
+- Fixed string parameters not going blue and bold when their value was different than default
+- Fixed all parameters not going blue and bold when a save file is loaded containing values that differ from default
+- Fixed a crucial bug where short channels were not being processed and filtered the same way as long channels before being used as regressors
+- Fixed a crucial bug where short channels were being presented to the design matrix as normal long channels
+- Fixed a crucial bug where long channels could be interpolated from short channels. Short channels are still potentially interpolated from long channels. See [this link](https://git.research.dezeeuw.ca/tyler/flares/issues/80) for more information regarding this issue.
+- Added a welcome message when the terminal is opened, resized the terminal, and added more commands
+
+
# Version 1.5.0
- This release introduces a new configuration file that may break existing installs. If your application does not update correctly, please download fresh from [this link.](https://git.research.dezeeuw.ca/tyler/flares/releases/)
diff --git a/changelog_major.md b/changelog_major.md
index 7081060..c8779f1 100644
--- a/changelog_major.md
+++ b/changelog_major.md
@@ -1,3 +1,39 @@
+# Version 1.5.1
+
+- Open a folder is now minimally BIDS compatible. It will recursively go through all folders, but not load external metadata
+- Renamed options in the Viewer Launcher window to better denote what actions they can perform
+- 2 new analysis options have been added in the viewer launcher window: Inter-Group Stats and Cross-Group Stats
+- Each of these analysis options have lots of text explaining what they do when their respective windows are opened
+- The parameter input dialog in the analysis windows will now be able to scroll down when lots of parameters are required
+- When a project is saved, it automatically will populate into the Recent Projects menu. Fixes [Issue 86](https://git.research.dezeeuw.ca/tyler/flares/issues/86)
+- The description when clicking "Why are these useful?" underneath the per-file parameters is no longer placeholder text. Fixes [Issue 85](https://git.research.dezeeuw.ca/tyler/flares/issues/85)
+- Added a new parameter section "Region of Interest". It has one parameter of "JSON_LOCATION", a json file containing Region of Interest data
+- Temporarily removed the parameter REMOVE_EVENTS due to its functionality being removed because of a memory leak
+- Fixed participant metadata remaing in the background when the participant was removed. Fixes [Issue 82](https://git.research.dezeeuw.ca/tyler/flares/issues/82)
+- Fixed processing remaining active hidden in the background when the "Clear" button was pushed. Fixes [Issue 81](https://git.research.dezeeuw.ca/tyler/flares/issues/81)
+- Now when "Clear" is pushed while data is processing, a popup will appear ensuring that the user understands that pushing "Clear" will stop processing
+- Reset to Default Configuration will now properly reset all of the parameters to their default values. [Issue 90](https://git.research.dezeeuw.ca/tyler/flares/issues/90)
+- A confirmation popup will now display once the application has been reset to default in addition to the status bar message
+- Changed the improper display text of "Cross Validation" to now properly read as "Coefficient of Variation"
+- Changed the parameters "CV" and "CV_THRESHOLD" to now be "COEFF_VAR and COEFF_VAR_THRESHOLD"
+- Changed the improper display text of "Mean Absolute Deviation" to now properly read as "Median Absolute Deviation"
+- Changed the parameters "SHORT_CHANNEL", "SHORT_CHANNEL_THRESH", and "LONG_CHANNEL_THRESH" to now be "SHORT_CHANNELS", "SHORT_CHANNELS_THRESHOLD", and "LONG_CHANNELS_THRESHOLD"
+- Changed the parameter section "Channel Variance" to now be "Sensor Dropout" to better reflect the action it performs, not the method employed to calculate it
+- Changed the parameters "CHANNEL_VAR" and "CHANNEL_THRESH" to now be "SENSOR_DROPOUT" and "SENSOR_DROPOUT_VARIANCE_THRESHOLD"
+- Changed the messaged displayed when the application crashes to better reflect what occured
+- Changed the processing stages 25-27 and updates their messages to better reflect their actions that they now perforn
+- Changed stage 25 from "Generate Channel Significance" to "Generate Channel Results"
+- Changed stage 26 from "Generate Channel, Region of Interest, and Contrast Results" to "Generate Region of Interest Results"
+- Changed stage 27 from "Compute Contrast Results" to "Generate Contrast Results"
+- Changed backend code to only load required methods and not load all methods every time
+- Fixed string parameters not going blue and bold when their value was different than default
+- Fixed all parameters not going blue and bold when a save file is loaded containing values that differ from default
+- Fixed a crucial bug where short channels were not being processed and filtered the same way as long channels before being used as regressors
+- Fixed a crucial bug where short channels were being presented to the design matrix as normal long channels
+- Fixed a crucial bug where long channels could be interpolated from short channels. Short channels are still potentially interpolated from long channels. See [this link](https://git.research.dezeeuw.ca/tyler/flares/issues/80) for more information regarding this issue.
+- Added a welcome message when the terminal is opened, resized the terminal, and added more commands
+
+
# Version 1.5.0
- This release introduces a new configuration file that may break existing installs. If your application does not update correctly, please download fresh from [this link.](https://git.research.dezeeuw.ca/tyler/flares/releases/)
diff --git a/flares.py b/flares.py
index 3ef32ee..a8918ab 100644
--- a/flares.py
+++ b/flares.py
@@ -967,19 +967,29 @@ def interpolate_fNIRS_bads_weighted_average(raw, max_dist=0.03, min_neighbors=2)
if hbo_names[i] != hbr_names[i]:
raise RuntimeError(f"Channel pairs do not match: {hbo_names[i]} vs {hbr_names[i]}")
+ all_distances = source_detector_distances(raw.info)
+ pair_distances = all_distances[hbo_picks]
+
# Identify bad pairs if either channel in pair is bad
bad_pairs = []
good_pairs = []
+ n_short_excluded = 0
for i, base in enumerate(hbo_names):
hbo_ch = raw.ch_names[hbo_picks[i]]
hbr_ch = raw.ch_names[hbr_picks[i]]
- if (hbo_ch in raw.info['bads']) or (hbr_ch in raw.info['bads']):
+ is_bad = (hbo_ch in raw.info['bads']) or (hbr_ch in raw.info['bads'])
+ is_short = pair_distances[i] < SHORT_CHANNELS_THRESHOLD
+
+ if is_bad:
bad_pairs.append(i)
+ elif is_short:
+ n_short_excluded += 1
else:
good_pairs.append(i)
-
+
print(f"Total pairs: {len(hbo_names)}")
- print(f"Good pairs: {len(good_pairs)}")
+ print(f"Good LONG pairs (eligible donors): {len(good_pairs)}")
+ print(f"Good SHORT pairs (excluded from donor pool): {n_short_excluded}")
print(f"Bad pairs to interpolate: {len(bad_pairs)}")
if len(bad_pairs) == 0:
@@ -1488,8 +1498,9 @@ def epochs_calculations(raw_haemo, events, event_dict):
def make_design_matrix(raw_haemo):
- events_to_remove = REMOVE_EVENTS
-
+ # events_to_remove = REMOVE_EVENTS
+ events_to_remove = ""
+
filtered_annotations = [ann for ann in raw_haemo.annotations if ann['description'] not in events_to_remove]
new_annot = Annotations(
@@ -2742,7 +2753,7 @@ def load_snirf(file_path: str) -> tuple[BaseRaw, Figure]:
# Read the snirf file
raw = read_raw_snirf(file_path, preload=True, verbose=VERBOSITY) # type: ignore
- raw.load_data(verbose=VERBOSITY) # type: ignore
+ #raw.load_data(verbose=VERBOSITY) # type: ignore redundant since preload is set to true
# TODO: Why was this commented again?
# Maybe this should be a bypass parameter?
@@ -3217,7 +3228,6 @@ def run_cross_group_second_level_analysis(df_roi_all, file_paths_a, file_paths_b
ax.set_xlabel('Region of Interest (ROI)', fontsize=12)
ax.set_title(f"Cross-Group Comparison: {group_a_name} vs {group_b_name}\n({target_chroma.upper()} - {selected_event})", fontsize=13, fontweight='bold', pad=15)
plt.tight_layout()
- plt.show()
# 4. Channel-by-Channel Group-Contrast Topography Map (Zero Hardcoding)
if df_cha_all is not None and raw_haemo is not None:
@@ -3231,7 +3241,7 @@ def run_cross_group_second_level_analysis(df_roi_all, file_paths_a, file_paths_b
(df_cha_all['Chroma'] == target_chroma) &
(df_cha_all['Condition'] == selected_event)
].copy()
- con_summary['clean_ID'] = con_summary['ID'].apply(clean_subject_id)
+ con_summary['clean_ID'] = con_summary['ID']
raw_picked = raw_haemo.copy().pick(picks=target_chroma)
@@ -3254,7 +3264,8 @@ def run_cross_group_second_level_analysis(df_roi_all, file_paths_a, file_paths_b
'ch_name': ch,
'Coef.': mean_diff, # Represents Mean A - Mean B
't': t_stat,
- 'P>|t|': p_val # For threshold masking
+ 'P>|t|': p_val,
+ 'Chroma': target_chroma, # For threshold masking
})
con_model_df = pd.DataFrame(contrast_data)
@@ -5401,6 +5412,8 @@ def process_participant(file_path, progress_callback=None):
if k in globals() and k != "REQUIRED_KEYS"
}
+ print(config_dict)
+
# Step 1: Preprocessing
raw = load_snirf(file_path)
fig_raw = raw.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="Loaded Raw", show=False)
@@ -5417,7 +5430,7 @@ def process_participant(file_path, progress_callback=None):
# Step 3: Verify Optode Placement
if OPTODE_PLACEMENT:
- fig_optodes = raw.plot_sensors(show_names=SHOW_OPTODE_NAMES, to_sphere=True, show=False) # type: ignore
+ fig_optodes = raw.plot_sensors(show_names=SHOW_OPTODE_NAMES, to_sphere=True, show=False, verbose=VERBOSITY) # type: ignore
fig_individual["Plot Sensors"] = fig_optodes
if progress_callback: progress_callback(3)
logger.info("Step 3 Completed.")
@@ -5426,7 +5439,7 @@ def process_participant(file_path, progress_callback=None):
if SHORT_CHANNELS and not FOLDING_BYP:
#NOTE: Have to split again later but since needed for heart rate, this will stay at step 4. Will split later again.
_short_chans = get_short_channels(raw, max_dist=SHORT_CHANNELS_THRESHOLD) # JUST FOR PLOTTING THEM SEPERATELY
- fig_short_chans = _short_chans.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="Short Channels Only", show=False)
+ fig_short_chans = _short_chans.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="Short Channels Only", show=False, verbose=VERBOSITY)
fig_individual["Short Channels Raw Data"] = fig_short_chans
raw = get_long_channels(raw, min_dist=0, max_dist=LONG_CHANNELS_THRESHOLD)
if progress_callback: progress_callback(4)
diff --git a/main.py b/main.py
index 912037d..088cf8a 100644
--- a/main.py
+++ b/main.py
@@ -43,7 +43,7 @@ from src.window.updateoptodes import UpdateOptodesWindow
from src.window.userguide import UserGuideWindow
from src.window.viewerlauncher import ViewerLauncherWidget
from src.window.welcome import WelcomeDialog
-from src.shared.flaresbasewidget import ParamSection
+from src.shared.flaresbasewidget import FilePickerWidget, ParamSection
from src.shared.shareddata import API_URL, API_URL_SECONDARY, APP_NAME, CURRENT_VERSION, PIPELINE_STAGES, PLATFORM_NAME
from updater import finish_update_if_needed, UpdateManager, LocalPendingUpdateCheckThread
@@ -91,7 +91,7 @@ SECTIONS = [
"title": "Trimming",
"params": [
{"name": "TRIM", "default": True, "type": bool, "help": "Should the start of the files be trimmed?"},
- {"name": "SECONDS_TO_KEEP", "default": 5, "type": float, "depends_on": "TRIM", "help": "Seconds to keep at the beginning of all loaded snirf files before the first annotation/event occurs. Calculation is done seperatly on all loaded snirf files. Setting this to 0 will have the first annotation/event be at time point 0. Only used if TRIM is set to True."},
+ {"name": "SECONDS_TO_KEEP", "default": 5.0, "type": float, "depends_on": "TRIM", "help": "Seconds to keep at the beginning of all loaded snirf files before the first annotation/event occurs. Calculation is done seperatly on all loaded snirf files. Setting this to 0 will have the first annotation/event be at time point 0. Only used if TRIM is set to True."},
]
},
{
@@ -257,7 +257,8 @@ SECTIONS = [
{"name": "FIR_DELAYS", "default": 15, "type": range, "depends_on": "HRF_MODEL", "depends_value": "fir", "help": "In case of FIR design, yields the array of delays used in the FIR model (in scans)."},
{"name": "MIN_ONSET", "default": -24, "type": int, "help": "Minimal onset relative to frame times (in seconds)"},
{"name": "OVERSAMPLING", "default": 50, "type": int, "help": "Oversampling factor used in temporal convolutions."},
- {"name": "REMOVE_EVENTS", "default": "None", "type": list, "help": "Remove events matching the names provided before generating the Design Matrix"},
+ # TODO: Re-implement this without causing a memory leak
+ # {"name": "REMOVE_EVENTS", "default": "None", "type": list, "help": "Remove events matching the names provided before generating the Design Matrix"},
{"name": "SHORT_CHANNEL_REGRESSION", "default": True, "type": bool, "depends_on": "SHORT_CHANNEL", "help": "Should short channel regression be used to create the design matrix? This will use the 'signal' from the short channel and regress it out of all other channels."},
]
},
@@ -272,7 +273,7 @@ SECTIONS = [
{
"title": "Region of Interest",
"params": [
- {"name": "JSON_LOCATION", "default": "", "type": str, "help": "Location of the JSON file containing region of interest results for significance calculations."},
+ {"name": "JSON_LOCATION", "default": "", "type": "json_file", "help": "Location of the JSON file containing region of interest results for significance calculations."},
]
},
{
@@ -505,7 +506,7 @@ class MainApplication(QMainWindow):
self.missing_events_bypass = False
self.analysis_clearing_bypass = False
self.folding_bypass = False
- self.json_location = r"C:\Users\tyler\Desktop\research\flares\regions.json"
+ self.json_location = ""
# Initialization to ensure that saving can occur
@@ -525,15 +526,13 @@ class MainApplication(QMainWindow):
self.files_failed = set() # set of failed file paths
self.files_results = {} # dict for successful results (if needed)
- self.platform_suffix = "-" + PLATFORM_NAME
-
self.updater = UpdateManager(
main_window=self,
api_url=API_URL,
api_url_sec=API_URL_SECONDARY,
current_version=CURRENT_VERSION,
platform_name=PLATFORM_NAME,
- platform_suffix=self.platform_suffix,
+ platform_suffix="-" + PLATFORM_NAME,
app_name=APP_NAME
)
@@ -546,7 +545,7 @@ class MainApplication(QMainWindow):
self.installEventFilter(self)
# Start local pending update check thread
- self.local_check_thread = LocalPendingUpdateCheckThread(CURRENT_VERSION, self.platform_suffix, PLATFORM_NAME, APP_NAME)
+ self.local_check_thread = LocalPendingUpdateCheckThread(CURRENT_VERSION, "-" + PLATFORM_NAME, PLATFORM_NAME, APP_NAME)
self.local_check_thread.pending_update_found.connect(self.updater.on_pending_update_found)
self.local_check_thread.no_pending_update.connect(self.updater.on_no_pending_update)
self.local_check_thread.start()
@@ -617,7 +616,7 @@ class MainApplication(QMainWindow):
label_desc = QLabel('Why are these useful?')
label_desc.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
- label_desc.linkActivated.connect(lambda: QMessageBox.information(None, "Info", "Parameter Info..."))
+ label_desc.linkActivated.connect(lambda: QMessageBox.information(None, f"Info - {APP_NAME.upper()} ", "Age: Used in determing the participants PPF.\nGender: Not currently used or implemented.\nGroup: Used to split participants into groups for comparisons between them."))
right_column_layout.addWidget(label_desc)
right_column_layout.addStretch()
self.right_column_widget.hide()
@@ -835,6 +834,23 @@ class MainApplication(QMainWindow):
and resets the memory heap.
"""
+ if hasattr(self, "result_process") and self.result_process and self.result_process.is_alive():
+ msg = QMessageBox(self)
+ msg.setWindowTitle(f"Confirm Clear - {APP_NAME.upper()}")
+ msg.setText("Data processing is currently active in the background. "
+ "Clearing now will forcefully kill all tasks and lose current progress.\n\n"
+ "Are you sure you want to proceed?")
+
+ msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel)
+ msg.setDefaultButton(QMessageBox.StandardButton.Cancel)
+
+ response = msg.exec()
+
+ if response == QMessageBox.StandardButton.Ok:
+ self.cancel_task()
+ else:
+ return
+
self.top_left_widget.clear()
if hasattr(self, "last_clicked_bubble"):
@@ -898,6 +914,16 @@ class MainApplication(QMainWindow):
self.metadata_cache = {}
+ self.file_metadata = {}
+
+ if hasattr(self, "meta_fields"):
+ for field in self.meta_fields.values():
+ field.blockSignals(True)
+ field.clear()
+ field.blockSignals(False)
+
+ self.current_file = None
+
if hasattr(self, "selected_paths"): self.selected_paths = []
if hasattr(self, "selected_path"): self.selected_path = None
@@ -1176,9 +1202,20 @@ class MainApplication(QMainWindow):
file_cfg.read_string(DEFAULT_CONFIG)
self.sync_app_with_config()
+ self.update_sections(0)
+
+ QTimer.singleShot(100, self._show_reset_success_dialog)
+
+
+ def _show_reset_success_dialog(self):
+ """Helper method triggered after the UI has completely finished redrawing."""
+ QMessageBox.information(
+ self,
+ "Reset Successful",
+ "All application settings have been successfully restored to their default values."
+ )
+ self.statusbar.showMessage("All settings have been reset to their default values.", 5000)
- self.statusbar.showMessage("All settings have been reset to their default values.", 5000)
-
def sync_app_with_config(self):
"""Reads values from file_cfg and updates both internal variables and UI checkmarks."""
@@ -1418,6 +1455,8 @@ class MainApplication(QMainWindow):
project_data = sanitize(project_data)
+ self.add_to_recent_projects(os.path.normpath(filename))
+
self.saving_overlay = SavingOverlay(self)
self.saving_overlay.resize(self.size()) # Cover the main window
self.saving_overlay.show()
@@ -1582,6 +1621,12 @@ class MainApplication(QMainWindow):
widget.blockSignals(False)
widget.update()
+ elif isinstance(widget, FilePickerWidget):
+ widget.blockSignals(True)
+ widget.setText(str(value)) # Updates the internal QLineEdit text safely
+ widget.blockSignals(False)
+ widget.update()
+
# QComboBox (bool, list)
elif isinstance(widget, QComboBox):
widget.blockSignals(True)
@@ -1599,10 +1644,19 @@ class MainApplication(QMainWindow):
widget.blockSignals(False)
widget.update()
+ if hasattr(section_widget, 'check_if_changed'):
+ if isinstance(widget, (QLineEdit, FilePickerWidget)):
+ section_widget.check_if_changed(name, widget.text())
+ elif isinstance(widget, QComboBox):
+ section_widget.check_if_changed(name, widget.currentText())
+ elif isinstance(widget, QSpinBox):
+ section_widget.check_if_changed(name, widget.value())
+
# After restoring, make sure dependencies are updated
if hasattr(section_widget, 'update_dependencies'):
section_widget.update_dependencies()
+ #TODO: Update blue bold text too
# def show_files_as_bubbles(self, folder_paths):
@@ -1659,7 +1713,6 @@ class MainApplication(QMainWindow):
current_ui_config = {}
try:
for section in self.param_sections:
- # This calls the get_param_values() method you shared earlier
section_values = section.get_param_values()
current_ui_config.update(section_values)
return current_ui_config
@@ -1839,6 +1892,19 @@ class MainApplication(QMainWindow):
self.top_left_widget.clear()
self.right_column_widget.hide()
+ target_path = bubble.file_path
+
+ if hasattr(self, 'file_metadata'):
+ self.file_metadata.pop(target_path, None)
+
+ if getattr(self, 'current_file', None) == target_path:
+ self.current_file = None
+ if hasattr(self, 'meta_fields'):
+ for field in self.meta_fields.values():
+ field.blockSignals(True)
+ field.clear()
+ field.blockSignals(False)
+
parent_layout = bubble.parent().layout()
if parent_layout is not None:
parent_layout.removeWidget(bubble)
@@ -2055,11 +2121,14 @@ class MainApplication(QMainWindow):
if self.folding_bypass:
all_params['FOLDING_BYP'] = True
+ self.json_location = all_params['JSON_LOCATION']
+
collected_data = {
"SNIRF_FILES": snirf_files,
"PARAMS": all_params, # add this line
"METADATA": self.get_all_metadata(), # optionally add metadata if needed
}
+
# Start processing
if current_process().name == 'MainProcess':
self.result_queue = Queue()
diff --git a/src/analysis/crossgroupstats.py b/src/analysis/crossgroupstats.py
index 8f9bcc3..90fe79c 100644
--- a/src/analysis/crossgroupstats.py
+++ b/src/analysis/crossgroupstats.py
@@ -40,12 +40,6 @@ PARAMETERIZED_INDEXES = {
"default": "hbo",
"type": str,
},
- {
- "key": "roi_config",
- "label": "Location of the ROI config file",
- "default": r"C:\Users\tyler\Desktop\research\flares\regions.json",
- "type": str,
- },
{
"key": "threshold_topo",
"label": "threshold_topo: TBD",
@@ -81,14 +75,14 @@ PARAMETERIZED_INDEXES = {
{
"key": "roi_a",
"label": "ROI A (e.g. contralateral region name from regions.json)",
- "default": "",
- "type": str,
+ "default": [],
+ "type": list,
},
{
"key": "roi_b",
"label": "ROI B (e.g. ipsilateral region name from regions.json)",
- "default": "",
- "type": str,
+ "default": [],
+ "type": list,
}
],
2: [
@@ -116,17 +110,11 @@ PARAMETERIZED_INDEXES = {
"default": "hbo",
"type": str,
},
- {
- "key": "roi_config",
- "label": "Location of the ROI config file",
- "default": r"C:\Users\tyler\Desktop\research\flares\regions.json",
- "type": str,
- },
{
"key": "contrast_name",
"label": "Name of the contrast to use",
- "default": "",
- "type": str,
+ "default": [],
+ "type": list,
},
],
}
@@ -148,7 +136,7 @@ DESCRIPTION = """0. Raw ROI Comparison (run_cross_group_second_level_analysis)
class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
- def __init__(self, haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict):
+ def __init__(self, haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict, json_location):
super().__init__("CrossGroupStats")
self.setWindowTitle(f"Cross-Group Stats Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
@@ -157,12 +145,13 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
self.design_matrix_dict = design_matrix_dict
self.contrast_results_dict = contrast_results_dict
self.group_dict = group_dict
+ self.json_location = json_location
self.setup_cross_group_ui(["0 (Raw ROI Comparison)", "1 (Laterality Comparison)", "2 (Contrast Comparison)",], placeholder_text=DESCRIPTION)
def process_request(self):
- request = self.get_common_request_data(PARAMETERIZED_INDEXES)
+ request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.json_location, self.contrast_results_dict)
if request is None:
return
@@ -195,7 +184,6 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
min_subjects = params.get("min_subjects", 3)
correction_method = params.get("correction_method", "fdr_bh")
target_chroma = params.get("target_chroma", "hbo")
- roi_config = params.get("roi_config", r"C:\Users\tyler\Desktop\research\flares\regions.json")
threshold_topo = params.get("threshold_topo", False)
run_cross_group_second_level_analysis(
@@ -211,7 +199,7 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
correction_method=correction_method,
target_chroma=target_chroma,
selected_event=selected_event,
- roi_config=roi_config,
+ roi_config=self.json_location,
threshold_topo=threshold_topo # Shows the raw difference map (Unthresholded)
)
elif idx == 1:
@@ -273,11 +261,10 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
min_subjects = params.get("min_subjects", 3)
correction_method = params.get("correction_method", "fdr_bh")
target_chroma = params.get("target_chroma", "hbo")
- roi_config = params.get("roi_config", r"C:\Users\tyler\Desktop\research\flares\regions.json")
contrast_name = params.get("contrast_name", "")
if not contrast_name:
- print("A contrast name must be specified (e.g. '2.0_vs_3.0').")
+ print("A contrast name must be specified.")
continue
# Build each group's channel-level contrast dataframe
@@ -311,7 +298,7 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
df_contrasts_a=df_contrasts_a,
df_contrasts_b=df_contrasts_b,
contrast_name=contrast_name,
- roi_json_path=roi_config,
+ roi_json_path=self.json_location,
group_a_name=self.group_a_dropdown.currentText(),
group_b_name=self.group_b_dropdown.currentText(),
target_chroma=target_chroma,
diff --git a/src/analysis/intergroupstats.py b/src/analysis/intergroupstats.py
index af14331..96acec0 100644
--- a/src/analysis/intergroupstats.py
+++ b/src/analysis/intergroupstats.py
@@ -271,7 +271,7 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
print("Both ROI A and ROI B must be specified.")
continue
-
+ print(min_subjects)
run_roi_paired_contrast_analysis(
df_roi_all=df_group,
roi_pairs=(roi_a, roi_b),
@@ -290,7 +290,7 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
min_subjects = params.get("min_subjects", 5)
correction_method = params.get("correction_method", "fdr_bh")
target_chroma = params.get("target_chroma", "hbo")
- contrast_name = params.get("contrast_name", "2.0_vs_3.0")
+ contrast_name = params.get("contrast_name", "")
weighted = params.get("weighted", True)
graph_bounds = params.get("graph_bounds", 0.0)
diff --git a/src/shared/flaresbasewidget.py b/src/shared/flaresbasewidget.py
index 84e4bba..b46da8c 100644
--- a/src/shared/flaresbasewidget.py
+++ b/src/shared/flaresbasewidget.py
@@ -239,6 +239,55 @@ class FullClickComboBox(QComboBox):
return super().eventFilter(obj, event)
+
+from PySide6.QtWidgets import QWidget, QHBoxLayout, QLineEdit, QPushButton, QFileDialog
+from PySide6.QtCore import Signal
+
+class FilePickerWidget(QWidget):
+ # This custom signal lets our container mimic a standard QLineEdit
+ textChanged = Signal(str)
+
+ def __init__(self, default_val="", file_filter="JSON Files (*.json);;All Files (*)", parent=None):
+ super().__init__(parent)
+
+ # Inline layout to hold line edit and button side-by-side
+ layout = QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ self.line_edit = QLineEdit()
+ self.line_edit.setText(str(default_val))
+ self.line_edit.setPlaceholderText("Select file path...")
+ # Forward internal text updates out through our custom component signal
+ self.line_edit.textChanged.connect(self.textChanged.emit)
+
+ self.browse_btn = QPushButton("Browse...")
+ self.browse_btn.clicked.connect(self.open_file_dialog)
+
+ layout.addWidget(self.line_edit)
+ layout.addWidget(self.browse_btn)
+
+ self.file_filter = file_filter
+
+ def open_file_dialog(self):
+ # Open PySide6 native file browser
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ "Select Configuration File",
+ self.line_edit.text(),
+ self.file_filter
+ )
+ if file_path:
+ self.line_edit.setText(file_path)
+
+ # Mimic standard text getter/setter behaviors so parent systems remain unbothered
+ def text(self):
+ return self.line_edit.text()
+
+ def setText(self, text):
+ self.line_edit.setText(text)
+
+
class ParamSection(QWidget):
"""
A widget section that dynamically creates labeled input fields from parameter metadata.
@@ -339,9 +388,15 @@ class ParamSection(QWidget):
else:
widget.setValue(15) # Default fallback
widget.valueChanged.connect(lambda val, p=param["name"]: self.check_if_changed(p, val))
+ elif param["type"] == "json_file":
+ # Create our custom dual-element compound layout widget
+ widget = FilePickerWidget(default_val=default_val, file_filter="JSON Files (*.json)")
+ # Your existing tracking signals connect seamlessly!
+ widget.textChanged.connect(lambda val, p=param["name"]: self.check_if_changed(p, val))
else:
widget = QLineEdit()
widget.setText(str(default_val))
+ widget.textChanged.connect(lambda val, p=param["name"]: self.check_if_changed(p, val))
if "depends_on" in param:
self.dependencies.append({
@@ -567,6 +622,8 @@ class ParamSection(QWidget):
values[name] = range(widget.value())
else:
values[name] = range(15) # Fallback
+ elif expected_type == "json_file":
+ values[name] = widget.text()
else:
raw_text = widget.text()
try:
@@ -1236,7 +1293,7 @@ class CrossGroupUIMixin:
return file_paths
- def get_common_request_data(self, parameterized_indexes):
+ def get_common_request_data(self, parameterized_indexes, json_location=None, contrast_dfs=None):
selected_event = self.event_dropdown.currentText()
if selected_event == "":
selected_event = None
@@ -1274,6 +1331,65 @@ class CrossGroupUIMixin:
if idx in parameterized_indexes
}
+ dynamic_rois = []
+
+ # 1. Check for the JSON file and parse ROI names
+ if os.path.exists(json_location):
+ try:
+ with open(json_location, 'r', encoding='utf-8') as f:
+ regions_data = json.load(f)
+
+ # Extract "name" from each region under "regions_of_interest"
+ regions_list = regions_data.get("regions_of_interest", [])
+ dynamic_rois = [region["name"] for region in regions_list if "name" in region]
+
+ except Exception as e:
+ # Safe log if JSON is corrupted or unreadable
+ print(f"Error reading ROI configurations from {json_location}: {e}")
+
+ # Fallback to prevent UI crashes if JSON file doesn't exist or is empty
+ if not dynamic_rois:
+ dynamic_rois = ["Option 1", "Option 2"]
+
+ dynamic_contrasts = []
+ if contrast_dfs:
+ contrast_set = set()
+ for fp in all_selected_paths:
+ # Get the contrasts dictionary associated with this file path
+ file_contrasts = contrast_dfs.get(fp, {})
+ for contrast_name in file_contrasts.keys():
+ # If no event is selected, display all contrasts.
+ # If an event is selected, only keep contrasts containing the event name as a substring.
+ if selected_event is None or selected_event in contrast_name:
+ contrast_set.add(contrast_name)
+
+ # Sort them cleanly for the UI
+ dynamic_contrasts = sorted(list(contrast_set))
+
+ # 2. Loop through the active parameters needing input and intercept 'roi_a' and 'roi_b'
+ for idx, params_list in indexes_needing_params.items():
+ for param_info in params_list:
+ if param_info["key"] == "roi_a":
+ # Inject options list dynamically
+ param_info["options"] = dynamic_rois
+ # Default to the very first item
+ param_info["default"] = dynamic_rois[0] if dynamic_rois else ""
+
+ elif param_info["key"] == "roi_b":
+ # Inject the same options list
+ param_info["options"] = dynamic_rois
+ # Default to the first item not taken (index 1), with safety fallbacks
+ if len(dynamic_rois) > 1:
+ param_info["default"] = dynamic_rois[1]
+ elif len(dynamic_rois) == 1:
+ param_info["default"] = dynamic_rois[0]
+ else:
+ param_info["default"] = ""
+
+ elif param_info["key"] == "contrast_name":
+ param_info["options"] = dynamic_contrasts
+ param_info["default"] = dynamic_contrasts[0] if dynamic_contrasts else ""
+
param_values = {}
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
diff --git a/src/shared/shareddata.py b/src/shared/shareddata.py
index be12859..b00086d 100644
--- a/src/shared/shareddata.py
+++ b/src/shared/shareddata.py
@@ -43,9 +43,9 @@ PIPELINE_STAGES = [
"Design Matrix",
"General Linear Model",
"Generate GLM Results",
- "Generate Channel Significance",
- "Generate Channel, Region of Interest, and Contrast Results",
- "Compute Contrast Results",
+ "Generate Channel Results",
+ "Generate Region of Interest Results",
+ "Generate Contrast Results",
"Finishing Up"
]
diff --git a/src/window/terminal.py b/src/window/terminal.py
index d733254..a8f8697 100644
--- a/src/window/terminal.py
+++ b/src/window/terminal.py
@@ -9,14 +9,16 @@ License: GPL-3.0
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLineEdit
from PySide6.QtCore import Qt
-from src.shared.shareddata import APP_NAME, CURRENT_VERSION
+from src.shared.shareddata import API_URL, API_URL_SECONDARY, APP_NAME, CURRENT_VERSION, PLATFORM_NAME
+from src.window.about import AboutWindow
+from updater import LocalPendingUpdateCheckThread, UpdateManager
class TerminalWindow(QWidget):
def __init__(self, parent=None):
super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle(f"Terminal - {APP_NAME.upper()}")
-
+ self.resize(320, 180)
self.output_area = QTextEdit()
self.output_area.setReadOnly(True)
@@ -32,8 +34,16 @@ class TerminalWindow(QWidget):
"hello": self.cmd_hello,
"help": self.cmd_help,
"version": self.cmd_version,
+ "about": self.cmd_about,
+ "update": self.cmd_update,
}
+ self.output_area.append(f"Welcome to {APP_NAME.upper()}. You are running version {CURRENT_VERSION}.")
+ self.output_area.append("Type 'help' for a list of available commands.\n")
+
+ self.input_line.setFocus()
+
+
def handle_command(self):
command_text = self.input_line.text()
self.input_line.clear()
@@ -65,4 +75,27 @@ class TerminalWindow(QWidget):
return f"Available commands: {', '.join(self.commands.keys())}"
def cmd_version(self, *args):
- return f"{CURRENT_VERSION}"
\ No newline at end of file
+ return f"{APP_NAME.upper()} is running version {CURRENT_VERSION}."
+
+ def cmd_about(self, *args):
+ self.about = AboutWindow(self)
+ self.about.show()
+
+ def cmd_update(self, *args):
+ main_win = self.parent()
+ if main_win is None:
+ return "[Error] Main window context not found."
+
+ self.updater = UpdateManager(
+ main_window=main_win,
+ api_url=API_URL,
+ api_url_sec=API_URL_SECONDARY,
+ current_version=CURRENT_VERSION,
+ platform_name=PLATFORM_NAME,
+ platform_suffix="-" + PLATFORM_NAME,
+ app_name=APP_NAME
+ )
+ self.output_area.append("Checking for updates...")
+
+ self.updater.manual_check_for_updates()
+ return "See status bar for update information."
\ No newline at end of file
diff --git a/src/window/viewerlauncher.py b/src/window/viewerlauncher.py
index e3898e7..baa55e2 100644
--- a/src/window/viewerlauncher.py
+++ b/src/window/viewerlauncher.py
@@ -37,7 +37,7 @@ class ViewerLauncherWidget(QWidget):
("Participant Functional Connectivity Viewer [BETA]", ParticipantFunctionalConnectivityWidget, [haemo_dict, epochs_dict], True),
("Inter-Group Functional Connectivity Viewer [BETA]", InterGroupFunctionalConnectivityWidget, [haemo_dict, group_dict, config_dict], True),
("Inter-Group Stats Viewer", InterGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict, json_location], True),
- ("Cross-Group Stats Viewer", CrossGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
+ ("Cross-Group Stats Viewer", CrossGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict, json_location], True),
("Inter-Group Brain and Image Viewer", InterGroupBrainImageWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
("Cross-Group Brain and Image Viewer", CrossGroupBrainImageWidget, [haemo_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
("Export To CSV Viewer", ExportToCSVWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, group_dict, contrast_results_dict], True)
diff --git a/tempCodeRunnerFile.py b/tempCodeRunnerFile.py
new file mode 100644
index 0000000..77c430a
--- /dev/null
+++ b/tempCodeRunnerFile.py
@@ -0,0 +1 @@
+update
\ No newline at end of file