prepare for new version
This commit is contained in:
+2
-1
@@ -182,4 +182,5 @@ cython_debug/
|
|||||||
flares-*
|
flares-*
|
||||||
*.flare
|
*.flare
|
||||||
*.cfg
|
*.cfg
|
||||||
tempCodeRunnerFile.py
|
tempCodeRunnerFile.py
|
||||||
|
*.pkl
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
# Version 1.6.1
|
# Version 1.7.0
|
||||||
|
|
||||||
|
- This is potentially a save-changing release due to adding more data into the save file. Please update your project files to ensure compatibility
|
||||||
- Renamed all instances of "Inter" to properly read as "Intra" and changed "Cross" to now read as "Inter"
|
- Renamed all instances of "Inter" to properly read as "Intra" and changed "Cross" to now read as "Inter"
|
||||||
- Changed RESAMPLE to only apply where it is required to avoid having Functional Connectivity analysis methods running on data that has been resampled
|
- Changed RESAMPLE to only apply where it is required to avoid having Functional Connectivity analysis methods running on data that has been resampled
|
||||||
- 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
|
- 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
|
||||||
|
|||||||
@@ -163,6 +163,42 @@ QC_METRIC_LABELS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ROI_MAP = {
|
||||||
|
'ROI_OccipitoParietal_BA18_19_7': [
|
||||||
|
'S1_D1', 'S1_D2', 'S4_D3'
|
||||||
|
],
|
||||||
|
'ROI_SuperiorParietal_BA7_5': [
|
||||||
|
'S2_D1', 'S3_D1', 'S2_D2', 'S2_D4', 'S4_D4'
|
||||||
|
],
|
||||||
|
'ROI_TPJ_AngularGyrus_BA39_19_40': [
|
||||||
|
'S4_D2', 'S5_D3', 'S4_D5', 'S5_D5'
|
||||||
|
],
|
||||||
|
# --- SPLIT BA40 REGIONS ---
|
||||||
|
'ROI_Posterior_BA40_Parietal': [
|
||||||
|
'S6_D4', 'S6_D5'
|
||||||
|
],
|
||||||
|
'ROI_Anterior_BA40_Sensorimotor': [
|
||||||
|
'S10_D5', 'S8_D5'
|
||||||
|
],
|
||||||
|
# --------------------------
|
||||||
|
'ROI_Supramarginal_Inferior_BA40_6': [
|
||||||
|
'S6_D6'
|
||||||
|
],
|
||||||
|
'ROI_VentralSomatosensory_BA1_2_3_43_48': [
|
||||||
|
'S8_D6', 'S9_D6'
|
||||||
|
],
|
||||||
|
'ROI_Sensorimotor_BA1_2_3_4_6': [
|
||||||
|
'S6_D8', 'S10_D8'
|
||||||
|
],
|
||||||
|
'ROI_DLPFC_FEF_BA6_8_9': [
|
||||||
|
'S7_D7', 'S7_D8'
|
||||||
|
],
|
||||||
|
'ROI_Broca_VLPFC_BA6_44_45_4': [
|
||||||
|
'S9_D7', 'S10_D7'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
DOWNSAMPLE: bool
|
DOWNSAMPLE: bool
|
||||||
DOWNSAMPLE_FREQUENCY: int
|
DOWNSAMPLE_FREQUENCY: int
|
||||||
|
|
||||||
@@ -303,6 +339,7 @@ GROUP: str = "Default"
|
|||||||
FOLDING_BYP: bool = False
|
FOLDING_BYP: bool = False
|
||||||
|
|
||||||
FEATURE_1: bool = False
|
FEATURE_1: bool = False
|
||||||
|
FEATURE_2: bool = False
|
||||||
|
|
||||||
# Ensure that we are working in the directory of this file
|
# Ensure that we are working in the directory of this file
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -610,10 +647,19 @@ def process_multiple_participants(file_paths, file_params, file_metadata,
|
|||||||
process_multiple_participants._success_count = success_count["value"]
|
process_multiple_participants._success_count = success_count["value"]
|
||||||
process_multiple_participants._failed_stages = failed_stages["value"]
|
process_multiple_participants._failed_stages = failed_stages["value"]
|
||||||
|
|
||||||
|
successes = [r for r in qc_rows if r.get("status") == "success"]
|
||||||
|
population_flags = flag_population_outliers(successes)
|
||||||
|
|
||||||
|
for path, flags in population_flags.items():
|
||||||
|
metric_summary = ", ".join(f"{f['metric']}={f['value']} (median={f['median']:.1f}, z={f['z']})" for f in flags)
|
||||||
|
n_metrics_flagged = len(flags)
|
||||||
|
severity = "STRONG" if n_metrics_flagged >= 3 else "possible"
|
||||||
|
logger.warning(f"{severity} outlier: {path} - flagged on {n_metrics_flagged} metric(s): {metric_summary}")
|
||||||
|
|
||||||
if qc_summary_path and qc_rows:
|
if qc_summary_path and qc_rows:
|
||||||
if FEATURE_1:
|
if FEATURE_1:
|
||||||
try:
|
try:
|
||||||
write_qc_excel_summary(qc_rows, qc_summary_path)
|
write_qc_excel_summary(qc_rows, qc_summary_path, population_flags=population_flags)
|
||||||
logger.info(f"QC summary written to {qc_summary_path} ({len(qc_rows)} participant(s))")
|
logger.info(f"QC summary written to {qc_summary_path} ({len(qc_rows)} participant(s))")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to write QC summary: {e}")
|
logger.error(f"Failed to write QC summary: {e}")
|
||||||
@@ -622,6 +668,86 @@ def process_multiple_participants(file_paths, file_params, file_metadata,
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def flag_population_outliers(
|
||||||
|
qc_rows: list[dict],
|
||||||
|
metrics: list[str] | None = None,
|
||||||
|
z_threshold: float = 2.5,
|
||||||
|
) -> dict[str, list[dict]]:
|
||||||
|
"""
|
||||||
|
Given QC metrics collected across an entire batch (each participant
|
||||||
|
processed with identical, fixed thresholds - nothing adaptive changes
|
||||||
|
per-participant), flags participants whose value on a given metric is
|
||||||
|
a statistical outlier relative to the rest of the batch.
|
||||||
|
|
||||||
|
This does NOT change what gets marked as a bad channel, and does NOT
|
||||||
|
imply anything is wrong with the metric thresholds themselves - it
|
||||||
|
answers a different question: "given everyone went through the same
|
||||||
|
pipeline with the same settings, does this participant's result look
|
||||||
|
unusual compared to everyone else who did." A real, uniformly bad
|
||||||
|
dataset (e.g. wrong population for the config) would show up as
|
||||||
|
outliers across MULTIPLE metrics; a participant with one borderline
|
||||||
|
metric and nothing else is much weaker evidence of a real problem.
|
||||||
|
|
||||||
|
Uses a modified z-score (median + MAD-based, not mean + std) since
|
||||||
|
QC metric distributions across a real population are often skewed by
|
||||||
|
a few genuinely bad files - MAD-based scoring is robust to those
|
||||||
|
outliers dominating the very estimate used to detect them, unlike a
|
||||||
|
standard mean/std z-score.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
qc_rows : list of per-participant QC dicts (successful runs only -
|
||||||
|
filter out FAILED entries before calling this).
|
||||||
|
metrics : list of QC dict keys to check. Defaults to the standard
|
||||||
|
bad-channel-count metrics if not specified.
|
||||||
|
z_threshold : float, default 2.5
|
||||||
|
Modified z-score magnitude above which a participant is flagged
|
||||||
|
for that metric. 2.5 is a commonly used, moderately conservative
|
||||||
|
starting point for outlier flagging - not validated against your
|
||||||
|
specific data, worth adjusting based on what you see in practice.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict[str, list[dict]]
|
||||||
|
Keyed by file_path, listing which metric(s) that participant was
|
||||||
|
flagged as an outlier on and by how much, e.g.
|
||||||
|
{"sub-07.snirf": [{"metric": "n_bad_sci", "z": 3.1, "value": 22, "median": 3}]}
|
||||||
|
"""
|
||||||
|
if metrics is None:
|
||||||
|
metrics = [
|
||||||
|
"n_bad_sci", "n_bad_snr", "n_bad_psp", "n_bad_coeff_var",
|
||||||
|
"n_bad_mad", "n_bad_psd_noise", "n_bad_dropout",
|
||||||
|
"n_bad_channels_total", "pct_bad_channels",
|
||||||
|
]
|
||||||
|
|
||||||
|
flagged: dict[str, list[dict]] = {}
|
||||||
|
|
||||||
|
for metric in metrics:
|
||||||
|
values = np.array([row.get(metric) for row in qc_rows if row.get(metric) is not None], dtype=float)
|
||||||
|
if len(values) < 4:
|
||||||
|
logger.info(f"Skipping outlier check for '{metric}' - too few participants ({len(values)}) for a meaningful comparison.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
median = np.median(values)
|
||||||
|
mad = np.median(np.abs(values - median))
|
||||||
|
if mad == 0:
|
||||||
|
continue # every participant identical on this metric - nothing to flag
|
||||||
|
|
||||||
|
for row in qc_rows:
|
||||||
|
val = row.get(metric)
|
||||||
|
if val is None:
|
||||||
|
continue
|
||||||
|
modified_z = 0.6745 * (val - median) / mad
|
||||||
|
if abs(modified_z) >= z_threshold:
|
||||||
|
path = row.get("file_path", "unknown")
|
||||||
|
flagged.setdefault(path, []).append({
|
||||||
|
"metric": metric, "z": round(float(modified_z), 2),
|
||||||
|
"value": val, "median": float(median),
|
||||||
|
})
|
||||||
|
|
||||||
|
return flagged
|
||||||
|
|
||||||
|
|
||||||
def markbad(data, ax, ch_names: list[str]) -> None:
|
def markbad(data, ax, ch_names: list[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Add a strikethrough to a plot for channels marked as bad.
|
Add a strikethrough to a plot for channels marked as bad.
|
||||||
@@ -1239,16 +1365,13 @@ def calculate_peak_power(data: BaseRaw, time_window: int = 3, threshold: float =
|
|||||||
psp = scores.mean(axis=1)
|
psp = scores.mean(axis=1)
|
||||||
bad_channels = list(compress(cast(list[str], data.ch_names), psp < threshold))
|
bad_channels = list(compress(cast(list[str], data.ch_names), psp < threshold))
|
||||||
|
|
||||||
plot_data = data.copy()
|
|
||||||
plot_data.info["bads"] = bad_channels
|
|
||||||
|
|
||||||
existing_bads = set(data.info.get("bads", []))
|
existing_bads = set(data.info.get("bads", []))
|
||||||
data.info["bads"] = list(existing_bads | set(bad_channels))
|
data.info["bads"] = list(existing_bads | set(bad_channels))
|
||||||
|
|
||||||
# Determine the colors based on the threshold, and create the figures
|
# Determine the colors based on the threshold, and create the figures
|
||||||
color_stops = ([0.0, threshold, threshold+0.1, threshold+0.2, 1.0], [0.0, threshold, threshold, 1.0])
|
color_stops = ([0.0, threshold, threshold+0.1, threshold+0.2, 1.0], [0.0, threshold, threshold, 1.0])
|
||||||
psp1, psp2 = plot_timechannel_quality_metrics(plot_data, scores, times, color_stops, threshold, "Peak Spectral Power")
|
psp1, psp2 = plot_timechannel_quality_metrics(data, scores, times, color_stops, threshold, "Peak Spectral Power")
|
||||||
print(f"thresh: {threshold}")
|
|
||||||
|
|
||||||
return list(compress(cast(list[str], getattr(data, "ch_names")), psp < threshold)), psp1, psp2
|
return list(compress(cast(list[str], getattr(data, "ch_names")), psp < threshold)), psp1, psp2
|
||||||
|
|
||||||
@@ -5128,6 +5251,89 @@ def find_bad_channels_coeff_var(raw, coeff_var_threshold=25.0):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def detect_hbo_hbr_anticorrelation(raw_haemo, threshold: float = -0.2):
|
||||||
|
"""
|
||||||
|
Flags channels where HbO and HbR are NOT showing the expected
|
||||||
|
physiologically anti-correlated relationship. Real hemodynamic response
|
||||||
|
typically shows HbO rising while HbR falls (and vice versa) - a channel
|
||||||
|
pair with weak or positive correlation is a common signature of motion
|
||||||
|
artifact or poor optode coupling that other QC metrics can miss, since
|
||||||
|
it's checking signal SHAPE/relationship rather than amplitude, variance,
|
||||||
|
or noise level.
|
||||||
|
|
||||||
|
IMPORTANT: must be run on raw_haemo BEFORE enhance_negative_correlation
|
||||||
|
(or any similar correction step) - that step actively forces HbO/HbR
|
||||||
|
anti-correlation, so measuring this diagnostic afterward would just be
|
||||||
|
checking whether the correction worked, not the underlying data quality.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
raw_haemo : BaseRaw
|
||||||
|
Haemoglobin-concentration data (post Beer-Lambert, pre-correction).
|
||||||
|
threshold : float, default -0.2
|
||||||
|
Channels with HbO/HbR correlation ABOVE this value are flagged as
|
||||||
|
bad (i.e. not sufficiently anti-correlated). -0.2 is a permissive
|
||||||
|
starting point - real channels often land well below this (-0.5 to
|
||||||
|
-0.9), but very low SNR or task designs can naturally weaken the
|
||||||
|
correlation without indicating artifact, so this shouldn't be set
|
||||||
|
aggressively without checking against your own known-good data.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple[list[str], Figure]
|
||||||
|
- list[str]: channel names (both hbo AND hbr for each flagged pair)
|
||||||
|
below the anti-correlation threshold.
|
||||||
|
- Figure: bar chart of correlation per channel pair, matching the
|
||||||
|
visual style of detect_sensor_dropout.
|
||||||
|
"""
|
||||||
|
ch_names = raw_haemo.ch_names
|
||||||
|
data = raw_haemo.get_data()
|
||||||
|
|
||||||
|
base_names = sorted({ch.split()[0] for ch in ch_names})
|
||||||
|
correlations = {}
|
||||||
|
bad_bases = []
|
||||||
|
|
||||||
|
for base in base_names:
|
||||||
|
try:
|
||||||
|
hbo_idx = ch_names.index(f"{base} hbo")
|
||||||
|
hbr_idx = ch_names.index(f"{base} hbr")
|
||||||
|
except ValueError:
|
||||||
|
continue # channel doesn't have both chromophores present
|
||||||
|
|
||||||
|
hbo_signal = data[hbo_idx]
|
||||||
|
hbr_signal = data[hbr_idx]
|
||||||
|
|
||||||
|
if np.std(hbo_signal) == 0 or np.std(hbr_signal) == 0:
|
||||||
|
corr = 0.0 # flat channel - can't meaningfully correlate
|
||||||
|
else:
|
||||||
|
corr = float(np.corrcoef(hbo_signal, hbr_signal)[0, 1])
|
||||||
|
|
||||||
|
correlations[base] = corr
|
||||||
|
if corr > threshold:
|
||||||
|
bad_bases.append(base)
|
||||||
|
print(f"Flagged {base}: HbO/HbR correlation = {corr:.3f} (expected below {threshold})")
|
||||||
|
|
||||||
|
bad_names = [ch for ch in ch_names if ch.split()[0] in bad_bases]
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 5), constrained_layout=True)
|
||||||
|
bases_sorted = list(correlations.keys())
|
||||||
|
corr_values = [correlations[b] for b in bases_sorted]
|
||||||
|
colors = ['coral' if c > threshold else 'skyblue' for c in corr_values]
|
||||||
|
|
||||||
|
ax.bar(range(len(corr_values)), corr_values, color=colors)
|
||||||
|
ax.axhline(threshold, color='red', linestyle='--', label=f'Threshold ({threshold})')
|
||||||
|
ax.axhline(0, color='black', linewidth=0.8)
|
||||||
|
ax.set_title("HbO/HbR Anti-Correlation Check")
|
||||||
|
ax.set_ylabel("Pearson r (HbO vs HbR)")
|
||||||
|
ax.set_xlabel("Channel Pair Index")
|
||||||
|
ax.legend()
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
print(f"Anti-correlation check: flagged {len(bad_bases)} optode pair(s).")
|
||||||
|
return bad_names, fig
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def hr_calc(raw, seconds_to_strip_hr, l_freq, h_freq, search_min, search_max, max_low_hr, max_high_hr, smoothing_window_hr, hr_window, short_channels, short_channels_threshold, verbosity, psd_confidence_threshold: float = 3.0, band_halfwidth_hz: float = 0.3):
|
def hr_calc(raw, seconds_to_strip_hr, l_freq, h_freq, search_min, search_max, max_low_hr, max_high_hr, smoothing_window_hr, hr_window, short_channels, short_channels_threshold, verbosity, psd_confidence_threshold: float = 3.0, band_halfwidth_hz: float = 0.3):
|
||||||
if short_channels:
|
if short_channels:
|
||||||
short_chans = get_short_channels(raw, max_dist=short_channels_threshold)
|
short_chans = get_short_channels(raw, max_dist=short_channels_threshold)
|
||||||
@@ -5745,6 +5951,12 @@ def process_participant(file_path, file_start, progress_callback=None):
|
|||||||
logger.info("Step 17 Completed.")
|
logger.info("Step 17 Completed.")
|
||||||
step_start = lap(step_start, timings, "Step 17")
|
step_start = lap(step_start, timings, "Step 17")
|
||||||
|
|
||||||
|
bad_anticorr = []
|
||||||
|
if FEATURE_2 and not FOLDING_BYP:
|
||||||
|
bad_anticorr, fig_anticorr = detect_hbo_hbr_anticorrelation(raw_haemo, threshold=-0.2)
|
||||||
|
_enqueue("HbO-HbR Anti-Correlation", fig_anticorr, png_queue)
|
||||||
|
qc["n_bad_anticorrelation"] = len(bad_anticorr)
|
||||||
|
|
||||||
# Step 18: Enhance Negative Correlation
|
# Step 18: Enhance Negative Correlation
|
||||||
if ENHANCE_NEGATIVE_CORRELATION and not FOLDING_BYP:
|
if ENHANCE_NEGATIVE_CORRELATION and not FOLDING_BYP:
|
||||||
raw_haemo = enhance_negative_correlation(raw_haemo)
|
raw_haemo = enhance_negative_correlation(raw_haemo)
|
||||||
@@ -5877,6 +6089,27 @@ def process_participant(file_path, file_start, progress_callback=None):
|
|||||||
if progress_callback: progress_callback(27)
|
if progress_callback: progress_callback(27)
|
||||||
logger.info("27")
|
logger.info("27")
|
||||||
step_start = lap(step_start, timings, "Step 27")
|
step_start = lap(step_start, timings, "Step 27")
|
||||||
|
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
|
|
||||||
# Step 28: Finishing Up
|
# Step 28: Finishing Up
|
||||||
png_queue.put(None) # sentinel
|
png_queue.put(None) # sentinel
|
||||||
@@ -5895,7 +6128,7 @@ def process_participant(file_path, file_start, progress_callback=None):
|
|||||||
logger.info(f" {name:<25} {elapsed:7.3f}s")
|
logger.info(f" {name:<25} {elapsed:7.3f}s")
|
||||||
|
|
||||||
logger.info(f"Total processing time: {sum(timings.values()):.3f}s")
|
logger.info(f"Total processing time: {sum(timings.values()):.3f}s")
|
||||||
return raw_haemo, epochs, df_cha, df_roi, df_design_matrix, config_dict, fig_bytes_dict, contrast_results_dict, roi_channel_map, qc, True
|
return raw_haemo, epochs, df_cha, df_roi, df_design_matrix, config_dict, fig_bytes_dict, contrast_results_dict, roi_channel_map, fir_feature_dict, qc, True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -6769,7 +7002,11 @@ def peak_power_fast(
|
|||||||
return raw, scores, times
|
return raw, scores, times
|
||||||
|
|
||||||
|
|
||||||
def write_qc_excel_summary(qc_rows: list[dict], output_path: str) -> None:
|
def write_qc_excel_summary(
|
||||||
|
qc_rows: list[dict],
|
||||||
|
output_path: str,
|
||||||
|
population_flags: dict[str, list[dict]] | None = None,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Writes one Excel workbook summarizing QC metrics across all participants
|
Writes one Excel workbook summarizing QC metrics across all participants
|
||||||
in a batch run - participants as columns, metrics as rows, each row
|
in a batch run - participants as columns, metrics as rows, each row
|
||||||
@@ -6838,18 +7075,23 @@ def write_qc_excel_summary(qc_rows: list[dict], output_path: str) -> None:
|
|||||||
|
|
||||||
header_range = f"{first_col_letter}1:{last_col_letter}1"
|
header_range = f"{first_col_letter}1:{last_col_letter}1"
|
||||||
if key in QC_METRIC_DEVIATION_BASED:
|
if key in QC_METRIC_DEVIATION_BASED:
|
||||||
# "worst" = furthest from the row's own median
|
row_values = [row.get(key) for row in successes if row.get(key) is not None]
|
||||||
worst_formula = (
|
row_paths = [row.get("file_path") for row in successes if row.get(key) is not None]
|
||||||
f"=INDEX({header_range},MATCH(MAX(ABS({data_range}-MEDIAN({data_range}))),"
|
if row_values:
|
||||||
f"ABS({data_range}-MEDIAN({data_range})),0))"
|
med = float(np.median(row_values))
|
||||||
)
|
deviations = [abs(v - med) for v in row_values]
|
||||||
elif QC_METRIC_DIRECTIONS[key]: # lower is better -> worst = max
|
worst_idx = int(np.argmax(deviations))
|
||||||
worst_formula = f"=INDEX({header_range},MATCH(MAX({data_range}),{data_range},0))"
|
worst_value = row_paths[worst_idx]
|
||||||
else: # higher is better -> worst = min
|
else:
|
||||||
worst_formula = f"=INDEX({header_range},MATCH(MIN({data_range}),{data_range},0))"
|
worst_value = ""
|
||||||
|
ws.cell(row=row_num, column=worst_col, value=worst_value).font = body_font
|
||||||
ws.cell(row=row_num, column=worst_col, value=worst_formula).font = body_font
|
else:
|
||||||
|
# existing live-formula path for direction-based metrics, unchanged
|
||||||
|
if QC_METRIC_DIRECTIONS[key]:
|
||||||
|
worst_formula = f"=INDEX({header_range},MATCH(MAX({data_range}),{data_range},0))"
|
||||||
|
else:
|
||||||
|
worst_formula = f"=INDEX({header_range},MATCH(MIN({data_range}),{data_range},0))"
|
||||||
|
ws.cell(row=row_num, column=worst_col, value=worst_formula).font = body_font
|
||||||
# --- Color scale, direction-aware ---
|
# --- Color scale, direction-aware ---
|
||||||
if key in QC_METRIC_DEVIATION_BASED:
|
if key in QC_METRIC_DEVIATION_BASED:
|
||||||
# color by |value - row median| via a helper column pattern isn't
|
# color by |value - row median| via a helper column pattern isn't
|
||||||
@@ -6876,9 +7118,32 @@ def write_qc_excel_summary(qc_rows: list[dict], output_path: str) -> None:
|
|||||||
)
|
)
|
||||||
ws.conditional_formatting.add(data_range, rule)
|
ws.conditional_formatting.add(data_range, rule)
|
||||||
|
|
||||||
|
if population_flags:
|
||||||
|
outlier_row = len(metric_keys) + 2
|
||||||
|
ws.cell(row=outlier_row, column=1, value="Population Outlier Flags").font = label_font
|
||||||
|
|
||||||
|
any_flags = bool(population_flags)
|
||||||
|
for i, row in enumerate(successes):
|
||||||
|
col = i + 2
|
||||||
|
path = row.get("file_path", "")
|
||||||
|
flags = population_flags.get(path, []) if population_flags else []
|
||||||
|
if flags:
|
||||||
|
text = "; ".join(f"{f['metric']} (z={f['z']})" for f in flags)
|
||||||
|
c = ws.cell(row=outlier_row, column=col, value=text)
|
||||||
|
c.fill = PatternFill(
|
||||||
|
start_color="FFEB84" if len(flags) < 3 else "F8696B",
|
||||||
|
end_color="FFEB84" if len(flags) < 3 else "F8696B",
|
||||||
|
fill_type="solid",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
c = ws.cell(row=outlier_row, column=col, value="No outliers detected")
|
||||||
|
c.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
|
||||||
|
c.font = body_font
|
||||||
|
c.alignment = Alignment(horizontal="center")
|
||||||
|
|
||||||
# --- Failed participants block, separate and clearly marked ---
|
# --- Failed participants block, separate and clearly marked ---
|
||||||
if failures:
|
if failures:
|
||||||
fail_row_start = len(metric_keys) + 4
|
fail_row_start = len(metric_keys) + 5
|
||||||
ws.cell(row=fail_row_start, column=1, value="FAILED PARTICIPANTS").font = Font(name="Arial", bold=True, size=12, color="CC0000")
|
ws.cell(row=fail_row_start, column=1, value="FAILED PARTICIPANTS").font = Font(name="Arial", bold=True, size=12, color="CC0000")
|
||||||
for i, row in enumerate(failures):
|
for i, row in enumerate(failures):
|
||||||
r = fail_row_start + 1 + i
|
r = fail_row_start + 1 + i
|
||||||
@@ -6888,6 +7153,11 @@ def write_qc_excel_summary(qc_rows: list[dict], output_path: str) -> None:
|
|||||||
err_cell = ws.cell(row=r, column=2, value=row.get("error", "unknown error"))
|
err_cell = ws.cell(row=r, column=2, value=row.get("error", "unknown error"))
|
||||||
err_cell.font = body_font
|
err_cell.font = body_font
|
||||||
err_cell.fill = fail_fill
|
err_cell.fill = fail_fill
|
||||||
|
else:
|
||||||
|
fail_row_start = len(metric_keys) + 5
|
||||||
|
c = ws.cell(row=fail_row_start, column=1, value="All participants processed successfully - no failures.")
|
||||||
|
c.font = Font(name="Arial", bold=True, size=11, color="006100")
|
||||||
|
c.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid")
|
||||||
|
|
||||||
ws.column_dimensions['A'].width = 32
|
ws.column_dimensions['A'].width = 32
|
||||||
for i in range(n_participants):
|
for i in range(n_participants):
|
||||||
@@ -6897,6 +7167,240 @@ def write_qc_excel_summary(qc_rows: list[dict], output_path: str) -> None:
|
|||||||
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(
|
||||||
|
roi_curves,
|
||||||
|
fir_delays,
|
||||||
|
selected_metrics,
|
||||||
|
target_condition='reach',
|
||||||
|
png_queue=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Generates and enqueues visual plots for every calculated waveform metric
|
||||||
|
across all Regions of Interest (ROIs).
|
||||||
|
"""
|
||||||
|
# Structure metric values per ROI
|
||||||
|
metric_data = {METRIC_REGISTRY[m]: {} for m in selected_metrics}
|
||||||
|
|
||||||
|
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)
|
||||||
|
for m_key, val in zip(selected_metrics, metrics):
|
||||||
|
label = METRIC_REGISTRY[m_key]
|
||||||
|
metric_data[label][f"{roi_name} ({chromo.upper()})"] = val
|
||||||
|
|
||||||
|
# Generate an image for each metric across ROIs
|
||||||
|
for metric_label, roi_dict in metric_data.items():
|
||||||
|
if not roi_dict:
|
||||||
|
continue
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(8, 4.5))
|
||||||
|
rois = list(roi_dict.keys())
|
||||||
|
values = list(roi_dict.values())
|
||||||
|
|
||||||
|
colors = ['#2b5c8f' if v >= 0 else '#d9534f' for v in values]
|
||||||
|
ax.bar(rois, values, color=colors, alpha=0.85, edgecolor='black')
|
||||||
|
|
||||||
|
ax.axhline(0, color='black', linewidth=0.8, linestyle='--')
|
||||||
|
ax.set_title(f"FIR Waveform Metric: {metric_label} [{target_condition}]", fontsize=12, fontweight='bold')
|
||||||
|
ax.set_xlabel("Region of Interest (ROI)", fontsize=10)
|
||||||
|
ax.set_ylabel(metric_label, fontsize=10)
|
||||||
|
plt.xticks(rotation=35, ha='right')
|
||||||
|
plt.grid(axis='y', linestyle=':', alpha=0.6)
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# Enqueue figure or close
|
||||||
|
if png_queue is not None:
|
||||||
|
_enqueue(f"FIR Waveform Metric - {metric_label}", fig, png_queue)
|
||||||
|
else:
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_roi_fir_curves(
|
||||||
|
raw=None,
|
||||||
|
target_condition='reach',
|
||||||
|
fir_delays=np.arange(0, 15),
|
||||||
|
roi_map=ROI_MAP,
|
||||||
|
chromophores=('hbr',),
|
||||||
|
glm_est=None,
|
||||||
|
df_design_matrix=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Shared FIR-GLM curve extraction. If `glm_est` and `df_design_matrix` are supplied,
|
||||||
|
it reuses pre-calculated GLM results directly to avoid duplicate processing.
|
||||||
|
"""
|
||||||
|
roi_curves = {}
|
||||||
|
|
||||||
|
# --- SHORT-CIRCUIT: Reuse pre-calculated GLM estimation if available ---
|
||||||
|
if glm_est is not None and df_design_matrix is not None:
|
||||||
|
if hasattr(glm_est, 'to_dataframe'):
|
||||||
|
glm_df = glm_est.to_dataframe().reset_index()
|
||||||
|
elif isinstance(glm_est, pd.DataFrame):
|
||||||
|
glm_df = glm_est.copy()
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported format for precalculated glm_est.")
|
||||||
|
|
||||||
|
glm_df.columns = [str(col).lower() for col in glm_df.columns]
|
||||||
|
cond_col = 'condition' if 'condition' in glm_df.columns else 'regressor'
|
||||||
|
ch_col = 'ch_name' if 'ch_name' in glm_df.columns else ('source' if 'source' in glm_df.columns else 'channel')
|
||||||
|
|
||||||
|
print("Available conditions in GLM:", glm_df[cond_col].unique())
|
||||||
|
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)}")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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():
|
||||||
|
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)
|
||||||
|
|
||||||
|
return roi_curves
|
||||||
|
|
||||||
|
|
||||||
|
def extract_fir_features_real_data(
|
||||||
|
raw=None,
|
||||||
|
target_condition=None,
|
||||||
|
fir_delays=np.arange(0, 15),
|
||||||
|
selected_metrics=('Peak_Amp',),
|
||||||
|
roi_map=ROI_MAP,
|
||||||
|
glm_est=None,
|
||||||
|
df_design_matrix=None,
|
||||||
|
png_queue=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Extracts FIR scalar waveform metrics, plots and enqueues figures for each
|
||||||
|
waveform metric, and returns all outputs collapsed into a single dictionary variable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
raw_cols = [
|
||||||
|
col for col in df_design_matrix.columns
|
||||||
|
if not any(k in col.lower() for k in ['drift', 'constant', 'short', 'nuisance'])
|
||||||
|
]
|
||||||
|
|
||||||
|
# Strip '_delay_0', '_delay_1', etc. to get base condition names
|
||||||
|
target_conditions = list(dict.fromkeys(
|
||||||
|
col.split('_delay_')[0] if '_delay_' in col else col
|
||||||
|
for col in raw_cols
|
||||||
|
))
|
||||||
|
|
||||||
|
collapsed_features = []
|
||||||
|
feature_names = []
|
||||||
|
feature_channels = []
|
||||||
|
|
||||||
|
# 2. Iterate over EVERY condition
|
||||||
|
for cond in target_conditions:
|
||||||
|
try:
|
||||||
|
roi_curves = _compute_roi_fir_curves(
|
||||||
|
raw=raw,
|
||||||
|
target_condition=cond,
|
||||||
|
fir_delays=fir_delays,
|
||||||
|
roi_map=roi_map,
|
||||||
|
chromophores=('hbr',),
|
||||||
|
glm_est=glm_est,
|
||||||
|
df_design_matrix=df_design_matrix
|
||||||
|
)
|
||||||
|
|
||||||
|
if not roi_curves:
|
||||||
|
print("999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999")
|
||||||
|
continue
|
||||||
|
|
||||||
|
metric_labels = [METRIC_REGISTRY[m] for m in selected_metrics]
|
||||||
|
|
||||||
|
# Extract metrics per ROI for this condition
|
||||||
|
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))
|
||||||
|
|
||||||
|
# Enqueue plots for this specific condition
|
||||||
|
plot_and_enqueue_waveform_metrics(
|
||||||
|
roi_curves=roi_curves,
|
||||||
|
fir_delays=fir_delays,
|
||||||
|
selected_metrics=selected_metrics,
|
||||||
|
target_condition=cond,
|
||||||
|
png_queue=png_queue
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed extracting FIR metrics for condition '{cond}': {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'features': np.array(collapsed_features),
|
||||||
|
'feature_names': feature_names,
|
||||||
|
'feature_channels': feature_channels
|
||||||
|
}
|
||||||
|
|
||||||
|
def compute_waveform_metrics(fir_curve, fir_delays, selected_metrics=('Peak_Amp',)):
|
||||||
|
peak_idx = np.argmax(fir_curve)
|
||||||
|
peak_amp = fir_curve[peak_idx]
|
||||||
|
ttp = fir_delays[peak_idx]
|
||||||
|
|
||||||
|
calculated_metrics = {}
|
||||||
|
|
||||||
|
if 'Peak_Amp' in selected_metrics:
|
||||||
|
calculated_metrics['Peak_Amp'] = peak_amp
|
||||||
|
|
||||||
|
if 'TTP' in selected_metrics:
|
||||||
|
calculated_metrics['TTP'] = ttp
|
||||||
|
|
||||||
|
if 'AUC' in selected_metrics:
|
||||||
|
trapz_fn = getattr(np, 'trapezoid', getattr(np, 'trapz', None))
|
||||||
|
calculated_metrics['AUC'] = trapz_fn(fir_curve, fir_delays)
|
||||||
|
|
||||||
|
if 'Rising_Slope' in selected_metrics:
|
||||||
|
calculated_metrics['Rising_Slope'] = (peak_amp - fir_curve[0]) / (ttp - fir_delays[0]) if ttp > fir_delays[0] else 0.0
|
||||||
|
|
||||||
|
if 'Recovery_Slope' in selected_metrics:
|
||||||
|
calculated_metrics['Recovery_Slope'] = (fir_curve[-1] - peak_amp) / (fir_delays[-1] - ttp) if fir_delays[-1] > ttp else 0.0
|
||||||
|
|
||||||
|
if 'FWHM' in selected_metrics:
|
||||||
|
half_max = peak_amp / 2.0
|
||||||
|
above_half = np.where(fir_curve >= half_max)[0]
|
||||||
|
calculated_metrics['FWHM'] = fir_delays[above_half[-1]] - fir_delays[above_half[0]] if len(above_half) > 1 else 0.0
|
||||||
|
|
||||||
|
if 'Onset_Latency' in selected_metrics:
|
||||||
|
onset_thresh = 0.2 * peak_amp
|
||||||
|
above_onset = np.where(fir_curve >= onset_thresh)[0]
|
||||||
|
calculated_metrics['Onset_Latency'] = fir_delays[above_onset[0]] if len(above_onset) > 0 else 0.0
|
||||||
|
|
||||||
|
if 'P2P_Amp' in selected_metrics:
|
||||||
|
calculated_metrics['P2P_Amp'] = peak_amp - np.min(fir_curve)
|
||||||
|
|
||||||
|
if 'Signal_Std' in selected_metrics:
|
||||||
|
calculated_metrics['Signal_Std'] = np.std(fir_curve)
|
||||||
|
|
||||||
|
if 'RMS' in selected_metrics:
|
||||||
|
calculated_metrics['RMS'] = np.sqrt(np.mean(fir_curve**2))
|
||||||
|
|
||||||
|
return [calculated_metrics[m] for m in selected_metrics]
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("This file has no functionality when not used in tandem with the FLARES application.")
|
print("This file has no functionality when not used in tandem with the FLARES application.")
|
||||||
|
|
||||||
|
|||||||
@@ -1215,6 +1215,8 @@ class MainApplication(QMainWindow):
|
|||||||
data_map["fig_bytes_dict"],
|
data_map["fig_bytes_dict"],
|
||||||
data_map["contrast_results_dict"],
|
data_map["contrast_results_dict"],
|
||||||
data_map["roi_channel_map_dict"],
|
data_map["roi_channel_map_dict"],
|
||||||
|
data_map["fir_feature_dict"],
|
||||||
|
data_map["qc_dict"],
|
||||||
self.folding_bypass,
|
self.folding_bypass,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -336,7 +336,7 @@ class ProjectManager:
|
|||||||
|
|
||||||
has_data = any(len(getattr(app, item["key"], {})) > 0 for item in DATA_SCHEMA)
|
has_data = any(len(getattr(app, item["key"], {})) > 0 for item in DATA_SCHEMA)
|
||||||
if hasattr(app, "button1"):
|
if hasattr(app, "button1"):
|
||||||
app.button1.setVisible(not has_data)
|
app.button1.setVisible(has_data)
|
||||||
if hasattr(app, "button3"):
|
if hasattr(app, "button3"):
|
||||||
app.button3.setVisible(has_data)
|
app.button3.setVisible(has_data)
|
||||||
|
|
||||||
|
|||||||
@@ -32,20 +32,27 @@ class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
|
|||||||
design_matrix_dict: dict[str, DataFrame],
|
design_matrix_dict: dict[str, DataFrame],
|
||||||
contrast_results_dict: dict[str, dict[str, Any]],
|
contrast_results_dict: dict[str, dict[str, Any]],
|
||||||
group_dict: dict[str, str],
|
group_dict: dict[str, str],
|
||||||
config_dict: dict[str, dict[str, Any]]
|
config_dict: dict[str, dict[str, Any]],
|
||||||
|
fir_feature_dict: dict[str, dict[str, Any]],
|
||||||
|
qc_dict: dict[str, dict[str, Any]],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__("ExportToCSV")
|
super().__init__("ExportToCSV")
|
||||||
self.setWindowTitle(f"Export To CSV Viewer - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Export To CSV Viewer - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
self.cha_dict = cha_dict
|
self.cha_dict = cha_dict
|
||||||
# self.df_ind = df_ind_dict
|
self.df_ind_dict = df_ind_dict
|
||||||
# self.design_matrix = design_matrix_dict
|
self.design_matrix = design_matrix_dict
|
||||||
# self.contrast_results_dict = contrast_results_dict
|
self.contrast_results_dict = contrast_results_dict
|
||||||
# self.group = group_dict
|
self.group_dict = group_dict
|
||||||
self.config_dict = config_dict
|
self.config_dict = config_dict
|
||||||
|
self.fir_feature_dict = fir_feature_dict
|
||||||
|
self.qc_dict = qc_dict
|
||||||
|
|
||||||
self.setup_csv_ui(["0 (Export Data to CSV)", "1 (CSV for SPARKS)", "2 (Export Configuration to CSV)", "3 (Paragraph of Configuration)"])
|
self.setup_csv_ui(["0 (Export Data to CSV)", "1 (CSV for SPARKS)", "2 (Export Configuration to CSV)", "3 (Paragraph of Configuration)", "4 (Export FIR Waveform Features to CSV)",
|
||||||
|
"5 (Export Quality Control Metrics to CSV)",
|
||||||
|
"6 (Export Master Consolidated Matrix [All Participants into 1 CSV])"
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def process_request(self):
|
def process_request(self):
|
||||||
@@ -113,6 +120,68 @@ class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
|
|||||||
magic_string = self.gen_magic_str(first_params)
|
magic_string = self.gen_magic_str(first_params)
|
||||||
self.placeholder_label.setText(magic_string)
|
self.placeholder_label.setText(magic_string)
|
||||||
|
|
||||||
|
# elif idx == 4:
|
||||||
|
# # FIR Waveform Features Export
|
||||||
|
# fir_data = self.fir_feature_dict.get(file_path)
|
||||||
|
# if fir_data and isinstance(fir_data, dict):
|
||||||
|
# names = fir_data.get("feature_names", [])
|
||||||
|
# vals = fir_data.get("features", [])
|
||||||
|
# chans = fir_data.get("feature_channels", [])
|
||||||
|
|
||||||
|
# fir_df = DataFrame({
|
||||||
|
# "Feature_Name": names,
|
||||||
|
# "Channel": chans if len(chans) == len(names) else ["N/A"] * len(names),
|
||||||
|
# "Value": vals
|
||||||
|
# })
|
||||||
|
# save_path = os.path.join(output_dir, f"{base_filename}_fir_features.csv")
|
||||||
|
# fir_df.to_csv(save_path, index=False)
|
||||||
|
# success_count += 1
|
||||||
|
|
||||||
|
# elif idx == 5:
|
||||||
|
# # Quality Control (QC) Metrics Export
|
||||||
|
# qc_data = self.qc_dict.get(file_path)
|
||||||
|
# if qc_data and isinstance(qc_data, dict):
|
||||||
|
# qc_df = DataFrame(list(qc_data.items()), columns=["Metric", "Value"])
|
||||||
|
# save_path = os.path.join(output_dir, f"{base_filename}_qc_metrics.csv")
|
||||||
|
# qc_df.to_csv(save_path, index=False)
|
||||||
|
# success_count += 1
|
||||||
|
|
||||||
|
# elif idx == 6:
|
||||||
|
# # Master Consolidated Matrix (1 single CSV combining all selected participants)
|
||||||
|
# save_path = os.path.join(output_dir, f"{APP_NAME}_master_consolidated.csv")
|
||||||
|
# if not os.path.exists(save_path):
|
||||||
|
# master_rows: list[dict[str, Any]] = []
|
||||||
|
# for fp in selected_file_paths:
|
||||||
|
# abs_path = os.path.abspath(fp)
|
||||||
|
# grp = self.group_dict.get(fp, "Unknown")
|
||||||
|
# row: dict[str, Any] = {"Participant": abs_path, "Group": grp}
|
||||||
|
|
||||||
|
# # QC Metrics
|
||||||
|
# qc_info = self.qc_dict.get(fp, {})
|
||||||
|
# if isinstance(qc_info, dict):
|
||||||
|
# for mk, mv in qc_info.items():
|
||||||
|
# row[f"QC_{mk}"] = mv
|
||||||
|
|
||||||
|
# # FIR Features
|
||||||
|
# fir_info = self.fir_feature_dict.get(fp, {})
|
||||||
|
# print("1")
|
||||||
|
# if isinstance(fir_info, dict):
|
||||||
|
# print("2")
|
||||||
|
# f_names = fir_info.get("feature_names", [])
|
||||||
|
# f_vals = fir_info.get("features", [])
|
||||||
|
# print("3")
|
||||||
|
# if len(f_names) == len(f_vals):
|
||||||
|
# print("4")
|
||||||
|
# for fn, fv in zip(f_names, f_vals):
|
||||||
|
# row[f"FIR_{fn}"] = fv
|
||||||
|
|
||||||
|
# master_rows.append(row)
|
||||||
|
|
||||||
|
# if master_rows:
|
||||||
|
# df_master = DataFrame(master_rows)
|
||||||
|
# df_master.to_csv(save_path, index=False)
|
||||||
|
# success_count += 1
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f"No method defined for index {idx}")
|
print(f"No method defined for index {idx}")
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ DATA_SCHEMA = [
|
|||||||
{"key": "config_dict", "help": "Dict[file_path, dict]: Processing configuration parameters"},
|
{"key": "config_dict", "help": "Dict[file_path, dict]: Processing configuration parameters"},
|
||||||
{"key": "fig_bytes_dict", "help": "Dict[file_path, dict]: Serialized figure data"},
|
{"key": "fig_bytes_dict", "help": "Dict[file_path, dict]: Serialized figure data"},
|
||||||
{"key": "contrast_results_dict", "help": "Dict[file_path, dict]: Calculated contrast statistical results"},
|
{"key": "contrast_results_dict", "help": "Dict[file_path, dict]: Calculated contrast statistical results"},
|
||||||
{"key": "roi_channel_map_dict", "help": "Dict[file_path, dict]: Calculated contrast statistical results"},
|
{"key": "roi_channel_map_dict", "help": "Dict[file_path, dict]: ROI channel mappings"},
|
||||||
|
{"key": "fir_feature_dict", "help": "Dict[file_path, dict]: Extracted FIR waveform features (features, names, channels)"},
|
||||||
|
{"key": "qc_dict", "help": "Dict[file_path, dict]: Quality control metrics"},
|
||||||
{"key": "valid_dict", "help": "Dict[file_path, bool]: Boolean validity status per file"}
|
{"key": "valid_dict", "help": "Dict[file_path, bool]: Boolean validity status per file"}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ class ViewerLauncherWidget(QWidget):
|
|||||||
fig_bytes_dict: dict[str, dict[str, bytes]],
|
fig_bytes_dict: dict[str, dict[str, bytes]],
|
||||||
contrast_results_dict: dict[str, dict[str, Any]],
|
contrast_results_dict: dict[str, dict[str, Any]],
|
||||||
roi_channel_map_dict: dict[str, dict[str, str]],
|
roi_channel_map_dict: dict[str, dict[str, str]],
|
||||||
|
fir_feature_dict: dict[str, dict[str, Any]],
|
||||||
|
qc_dict: dict[str, dict[str, Any]],
|
||||||
folding_bypass: bool,
|
folding_bypass: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
@@ -62,7 +64,7 @@ class ViewerLauncherWidget(QWidget):
|
|||||||
("Inter-Group Stats Viewer", InterGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, roi_channel_map_dict, group_dict], True),
|
("Inter-Group Stats Viewer", InterGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, roi_channel_map_dict, group_dict], True),
|
||||||
("Intra-Group Brain and Image Viewer", IntraGroupBrainImageWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
|
("Intra-Group Brain and Image Viewer", IntraGroupBrainImageWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
|
||||||
("Inter-Group Brain and Image Viewer", InterGroupBrainImageWidget, [haemo_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
|
("Inter-Group Brain and Image Viewer", InterGroupBrainImageWidget, [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, contrast_results_dict, group_dict, config_dict], True)
|
("Export To CSV Viewer", ExportToCSVWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict, config_dict, fir_feature_dict, qc_dict], True)
|
||||||
]
|
]
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
|
|||||||
Reference in New Issue
Block a user