diff --git a/changelog.md b/changelog.md index e0c5bc4..66811b2 100644 --- a/changelog.md +++ b/changelog.md @@ -22,6 +22,7 @@ - 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 @@ -42,6 +43,7 @@ - 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 # Version 1.6.0 diff --git a/changelog_major.md b/changelog_major.md index 8cf367b..ff0db32 100644 --- a/changelog_major.md +++ b/changelog_major.md @@ -12,6 +12,7 @@ - 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 @@ -31,4 +32,5 @@ - 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 \ No newline at end of file +- 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 \ No newline at end of file diff --git a/flares.py b/flares.py index 5636089..fe505e1 100644 --- a/flares.py +++ b/flares.py @@ -162,8 +162,6 @@ QC_METRIC_LABELS = { "total_processing_seconds": "Processing Time (s)", } -ROI_MAP = {} # TODO: Should be grabbed from the json file - DOWNSAMPLE: bool DOWNSAMPLE_FREQUENCY: int @@ -6059,21 +6057,22 @@ def process_participant(file_path, file_start, progress_callback=None): # Step 27.5: Extract FIR Waveform Features & Enqueue Metric Plots fir_feature_dict = {'features': np.array([]), 'feature_names': [], 'feature_channels': []} - # if HRF_MODEL.lower() == "fir": - # try: - # fir_feature_dict = extract_fir_features_real_data( - # raw=raw_haemo, - # target_condition=None, # e.g., 'reach' - # fir_delays=FIR_DELAYS, # e.g., np.arange(0, 15) - # selected_metrics=tuple(METRIC_REGISTRY.keys()), # e.g., ('Peak_Amp', 'TTP', 'AUC') - # roi_map=ROI_MAP, - # glm_est=glm_est, - # df_design_matrix=df_design_matrix, - # png_queue=png_queue - # ) - # logger.info("Step 27.5: FIR features successfully extracted and metric images enqueued.") - # except Exception as e: - # logger.warning(f"Step 27.5 Failed to extract FIR features: {e}") + if HRF_MODEL.lower() == "fir": + try: + fir_feature_dict = extract_fir_features_real_data( + raw=raw_haemo, + target_condition=None, # e.g., 'reach' + fir_delays=FIR_DELAYS, # e.g., np.arange(0, 15) + selected_metrics=tuple(METRIC_REGISTRY.keys()), # e.g., ('Peak_Amp', 'TTP', 'AUC') + roi_map=JSON_LOCATION, + chromophores=('hbo', 'hbr', 'hbt'), + glm_est=glm_est, + df_design_matrix=df_design_matrix, + png_queue=png_queue + ) + logger.info("Step 27.5: FIR features successfully extracted and metric images enqueued.") + except Exception as e: + logger.warning(f"Step 27.5 Failed to extract FIR features: {e}") # Step 28: Finishing Up @@ -7197,8 +7196,8 @@ def _compute_roi_fir_curves( raw=None, target_condition='reach', fir_delays=np.arange(0, 15), - roi_map=ROI_MAP, - chromophores=('hbr',), + roi_map={}, + chromophores=('hbo', 'hbr', 'hbt'), glm_est=None, df_design_matrix=None ): @@ -7207,6 +7206,7 @@ def _compute_roi_fir_curves( it reuses pre-calculated GLM results directly to avoid duplicate processing. """ roi_curves = {} + active_roi_map = normalize_roi_map(roi_map) # --- SHORT-CIRCUIT: Reuse pre-calculated GLM estimation if available --- if glm_est is not None and df_design_matrix is not None: @@ -7225,25 +7225,36 @@ def _compute_roi_fir_curves( fir_df = glm_df[glm_df[cond_col].astype(str).str.lower().str.contains(target_condition.lower())].copy() print(f"Matched rows for '{target_condition}': {len(fir_df)}") + active_roi_map = normalize_roi_map(roi_map) + if fir_df.empty: logger.warning(f"Condition '{target_condition}' not found in precalculated GLM estimates.") return roi_curves - for chromo in chromophores: - chromo_df = fir_df[fir_df[ch_col].str.lower().str.contains(chromo.lower())] if ch_col in fir_df.columns else fir_df + # 1. Extract base measured chromophores (e.g., hbo, hbr) directly from GLM data + base_chromos = [c.lower() for c in chromophores if c.lower() != 'hbt'] + + for chromo in base_chromos: + chromo_df = fir_df[fir_df[ch_col].str.lower().str.contains(chromo)] if ch_col in fir_df.columns else fir_df ch_curves = {} for ch_name, ch_group in chromo_df.groupby(ch_col): pair = ch_name.split(' ')[0] ch_curves[pair] = ch_group['theta'].values if 'theta' in ch_group.columns else ch_group['beta'].values - print("Extracted channel keys:", list(ch_curves.keys())[:5]) - - for roi_name, channels in roi_map.items(): + for roi_name, channels in active_roi_map.items(): matching_curves = [ch_curves[ch] for ch in channels if ch in ch_curves] if matching_curves: roi_curves[(chromo, roi_name)] = np.mean(matching_curves, axis=0) + # 2. Derive HbT (HbO + HbR) dynamically if requested + if 'hbt' in [c.lower() for c in chromophores]: + for roi_name in active_roi_map.keys(): + hbo_key = ('hbo', roi_name) + hbr_key = ('hbr', roi_name) + if hbo_key in roi_curves and hbr_key in roi_curves: + roi_curves[('hbt', roi_name)] = roi_curves[hbo_key] + roi_curves[hbr_key] + return roi_curves @@ -7252,7 +7263,8 @@ def extract_fir_features_real_data( target_condition=None, fir_delays=np.arange(0, 15), selected_metrics=('Peak_Amp',), - roi_map=ROI_MAP, + roi_map={}, + chromophores=('hbo', 'hbr', 'hbt'), glm_est=None, df_design_matrix=None, png_queue=None @@ -7285,7 +7297,7 @@ def extract_fir_features_real_data( target_condition=cond, fir_delays=fir_delays, roi_map=roi_map, - chromophores=('hbr',), + chromophores=chromophores, glm_est=glm_est, df_design_matrix=df_design_matrix ) @@ -7300,9 +7312,10 @@ def extract_fir_features_real_data( for (chromo, roi_name), roi_fir_curve in roi_curves.items(): metrics = compute_waveform_metrics(roi_fir_curve, fir_delays=fir_delays, selected_metrics=selected_metrics) collapsed_features.extend(metrics) - # Prefix feature names with condition - feature_names.extend([f"{cond}_{roi_name}_{m}" for m in metric_labels]) - feature_channels.extend([roi_name] * len(metric_labels)) + + # Prefix feature names with condition AND chromophore + feature_names.extend([f"{cond}_{chromo.upper()}_{roi_name}_{m}" for m in metric_labels]) + feature_channels.extend([f"{roi_name} ({chromo.upper()})"] * len(metric_labels)) # Enqueue plots for this specific condition plot_and_enqueue_waveform_metrics( @@ -7366,6 +7379,43 @@ def compute_waveform_metrics(fir_curve, fir_delays, selected_metrics=('Peak_Amp' return [calculated_metrics[m] for m in selected_metrics] +def normalize_roi_map(roi_map): + """ + Normalizes ROI mapping inputs into a flat {roi_name: [channel_list]} dict. + + Accepts: + 1. String or Path pointing to a JSON file (JSON_LOCATION). + 2. Dict with 'regions_of_interest' list (loaded JSON). + 3. Standard flat dict {roi_name: [channels]}. + """ + # 1. If roi_map is a file path string or Path, load JSON from disk + if isinstance(roi_map, (str, Path)): + json_path = Path(roi_map) + if json_path.is_file(): + try: + with open(json_path, 'r', encoding='utf-8') as f: + roi_map = json.load(f) + except Exception as e: + logger.error(f"Failed to load ROI JSON file from {json_path}: {e}") + return {} + else: + logger.error(f"ROI JSON file path does not exist: {json_path}") + return {} + + # 2. Handle nested JSON structure with 'regions_of_interest' + if isinstance(roi_map, dict) and 'regions_of_interest' in roi_map: + return { + roi['name']: roi['channels'] + for roi in roi_map['regions_of_interest'] + } + + # 3. Fallback for flat dictionary {roi_name: [channels]} + if isinstance(roi_map, dict): + return roi_map + + return {} + + if __name__ == "__main__": print("This file has no functionality when not used in tandem with the FLARES application.")