preparation for 1.5.1

This commit is contained in:
2026-07-17 23:59:42 -07:00
parent 0680718398
commit 2b019c1bc0
11 changed files with 350 additions and 59 deletions
+12 -25
View File
@@ -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,
+2 -2
View File
@@ -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)
+117 -1
View File
@@ -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 == "<None Selected>":
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)
+3 -3
View File
@@ -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"
]
+36 -3
View File
@@ -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}"
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."
+1 -1
View File
@@ -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)