pylance standardization
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
- 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 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 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.
|
- 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.
|
||||||
|
- Decreased unnecessary processing time when fOLDing channels by an order of magnitude
|
||||||
- Added a welcome message when the terminal is opened, resized the terminal, and added more commands
|
- Added a welcome message when the terminal is opened, resized the terminal, and added more commands
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ License: GPL-3.0
|
|||||||
|
|
||||||
# Built-in imports
|
# Built-in imports
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
import threading
|
import threading
|
||||||
import logging
|
import logging
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, Optional, cast, Literal, Union
|
from typing import Any, Optional, Sequence, cast, Literal, Union
|
||||||
from itertools import compress
|
from itertools import compress
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from multiprocessing import Queue, Pool
|
from multiprocessing import Queue, Pool
|
||||||
@@ -1765,29 +1766,6 @@ def _check_load_fold(fold_files, atlas):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def fold_channel_specificity_normal(raw, fold_files=None, atlas="Juelich", interpolate=False):
|
|
||||||
"""Return the landmarks and specificity a channel is sensitive to.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
|
|
||||||
""" # noqa: E501
|
|
||||||
_validate_type(raw, BaseRaw, "raw")
|
|
||||||
|
|
||||||
reference_locations = generate_montage_locations()
|
|
||||||
|
|
||||||
fold_tbl = _check_load_fold(fold_files, atlas)
|
|
||||||
|
|
||||||
chan_spec = list()
|
|
||||||
for cidx in range(len(raw.ch_names)):
|
|
||||||
tbl = _source_detector_fold_table(
|
|
||||||
raw, cidx, reference_locations, fold_tbl, interpolate
|
|
||||||
)
|
|
||||||
chan_spec.append(tbl.reset_index(drop=True))
|
|
||||||
|
|
||||||
return chan_spec
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def resource_path(relative_path):
|
def resource_path(relative_path):
|
||||||
"""
|
"""
|
||||||
Get absolute path to resource regardless of running directly or packaged using PyInstaller
|
Get absolute path to resource regardless of running directly or packaged using PyInstaller
|
||||||
@@ -1958,35 +1936,42 @@ def resource_path(relative_path):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def fold_channels(raw: BaseRaw, p_name: str, progress_queue=None) -> dict[str, list[dict[str, Any]]]:
|
def fold_channels(raw: BaseRaw, p_name: str, atlas: str='Brodmann', progress_queue=None) -> dict[str, list[dict[str, Any]]]:
|
||||||
"""Runs in background process.
|
"""Runs in background process.
|
||||||
Does only heavy math/lookup. Returns data instead of a static image.
|
Does only heavy math/lookup. Returns data instead of a static image.
|
||||||
"""
|
"""
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
set_config('MNE_NIRS_FOLD_PATH', resource_path("./mne_data/fOLD/fOLD-public-master/Supplementary"))
|
fold_dir = resource_path("./mne_data/fOLD/fOLD-public-master/Supplementary")
|
||||||
else:
|
else:
|
||||||
path = os.path.expanduser("~") + "/mne_data/fOLD/fOLD-public-master/Supplementary"
|
path = os.path.expanduser("~") + "/mne_data/fOLD/fOLD-public-master/Supplementary"
|
||||||
set_config('MNE_NIRS_FOLD_PATH', resource_path(path))
|
fold_dir = resource_path(path)
|
||||||
|
|
||||||
|
set_config('MNE_NIRS_FOLD_PATH', fold_dir)
|
||||||
|
|
||||||
hbo_channel_names = cast(list[str], getattr(raw.copy().pick(picks='hbo'), "ch_names"))
|
hbo_channel_names = cast(list[str], getattr(raw.copy().pick(picks='hbo'), "ch_names"))
|
||||||
|
|
||||||
# Store clean, picklable data lists instead of complex DataFrames
|
_validate_type(raw, BaseRaw, "raw")
|
||||||
channel_results = {}
|
|
||||||
|
|
||||||
|
reference_locations = generate_montage_locations()
|
||||||
|
|
||||||
|
fold_tbl = _check_load_fold(fold_files=fold_dir, atlas=atlas)
|
||||||
|
|
||||||
|
channel_results = {}
|
||||||
step_idx = 0
|
step_idx = 0
|
||||||
|
|
||||||
for channel_name in hbo_channel_names:
|
for cidx, channel_name in enumerate(hbo_channel_names):
|
||||||
channel_data = raw.copy().pick(picks=channel_name)
|
tbl = _source_detector_fold_table(
|
||||||
output = cast(list[DataFrame], fold_channel_specificity_normal(channel_data, interpolate=True, atlas='Brodmann'))
|
raw, cidx, reference_locations, fold_tbl, interpolate=True
|
||||||
|
)
|
||||||
|
|
||||||
channel_results[channel_name] = []
|
channel_results[channel_name] = []
|
||||||
for df_data in output:
|
|
||||||
# Extract just raw primitive types so they transfer over process channels flawlessly
|
for _, row in tbl.iterrows():
|
||||||
for _, row in df_data.iterrows():
|
channel_results[channel_name].append({
|
||||||
channel_results[channel_name].append({
|
'Landmark': str(row['Landmark']),
|
||||||
'Landmark': str(row['Landmark']),
|
'Specificity': float(row['Specificity'])
|
||||||
'Specificity': float(row['Specificity'])
|
})
|
||||||
})
|
|
||||||
step_idx += 1
|
step_idx += 1
|
||||||
if progress_queue is not None:
|
if progress_queue is not None:
|
||||||
progress_queue.put((p_name, step_idx))
|
progress_queue.put((p_name, step_idx))
|
||||||
@@ -2180,7 +2165,7 @@ def plot_3d_evoked_array(
|
|||||||
return brain
|
return brain
|
||||||
|
|
||||||
|
|
||||||
def aggregate_fnirs_group_geometry(raw_list):
|
def aggregate_fnirs_group_geometry(raw_list: Sequence[BaseRaw | None]) -> BaseRaw:
|
||||||
"""
|
"""
|
||||||
Averages fNIRS geometry across participants in two tiers:
|
Averages fNIRS geometry across participants in two tiers:
|
||||||
1. Average by Channel Pairing (S_D).
|
1. Average by Channel Pairing (S_D).
|
||||||
@@ -2246,7 +2231,15 @@ def aggregate_fnirs_group_geometry(raw_list):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def brain_3d_visualization(raw_haemo, df_cha, selected_event, t_or_theta: Literal['t', 'theta'] = 'theta', show_optodes: Literal['sensors', 'labels', 'none', 'all'] = 'all', show_text: bool = True, brain_bounds: float = 1.0) -> None:
|
def brain_3d_visualization(
|
||||||
|
raw_haemo: BaseRaw | None,
|
||||||
|
df_cha: DataFrame | None,
|
||||||
|
selected_event: str | None,
|
||||||
|
t_or_theta: Literal["t", "theta"] = "theta",
|
||||||
|
show_optodes: Literal["sensors", "labels", "none", "all"] = "all",
|
||||||
|
show_text: bool = True,
|
||||||
|
brain_bounds: float | tuple[float, float] | Sequence[float] = 1.0,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
clim = dict(kind="value", pos_lims=(0, brain_bounds/2, brain_bounds))
|
clim = dict(kind="value", pos_lims=(0, brain_bounds/2, brain_bounds))
|
||||||
|
|
||||||
@@ -2582,7 +2575,14 @@ def plot_2d_3d_contrasts_between_groups(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def plot_fir_model_results(df, raw_haemo, dm, selected_event, l_bound, u_bound):
|
def plot_fir_model_results(
|
||||||
|
df: DataFrame,
|
||||||
|
raw_haemo: BaseRaw | None,
|
||||||
|
dm: DataFrame | None,
|
||||||
|
selected_event: str | None,
|
||||||
|
l_bound: float,
|
||||||
|
u_bound: float,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
|
||||||
df["isActivity"] = [f"{selected_event}" in n for n in df["Condition"]]
|
df["isActivity"] = [f"{selected_event}" in n for n in df["Condition"]]
|
||||||
@@ -2801,11 +2801,19 @@ def load_snirf(file_path: str) -> tuple[BaseRaw, Figure]:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_roi_second_level_analysis(df_roi_all, df_cha_all=None, raw_haemo=None,
|
def run_roi_second_level_analysis(
|
||||||
p_threshold=0.05, min_subjects=5,
|
df_roi_all: DataFrame,
|
||||||
correction_method='fdr_bh', target_chroma='hbo',
|
df_cha_all: DataFrame | None = None,
|
||||||
graph_bounds=None, roi_config=None,
|
raw_haemo: BaseRaw | None = None,
|
||||||
threshold_topo=False): # Added parameter
|
p_threshold: float = 0.05,
|
||||||
|
min_subjects: int = 5,
|
||||||
|
correction_method: str | None = "fdr_bh",
|
||||||
|
target_chroma: str = "hbo",
|
||||||
|
graph_bounds: float | None = None,
|
||||||
|
roi_config: str | Path | None = None,
|
||||||
|
threshold_topo: bool = False,
|
||||||
|
) -> DataFrame:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Perform group-level ROI analysis, prints stats to console, plots the ROI bar chart,
|
Perform group-level ROI analysis, prints stats to console, plots the ROI bar chart,
|
||||||
and dynamically plots isolated channel-level group topography maps based on a JSON config.
|
and dynamically plots isolated channel-level group topography maps based on a JSON config.
|
||||||
@@ -3064,13 +3072,24 @@ def clean_subject_id(path_or_id):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_cross_group_second_level_analysis(df_roi_all, file_paths_a, file_paths_b,
|
def run_cross_group_second_level_analysis(
|
||||||
group_a_name="Group A", group_b_name="Group B",
|
df_roi_all: DataFrame,
|
||||||
df_cha_all=None, raw_haemo=None,
|
file_paths_a: list[str],
|
||||||
p_threshold=0.05, min_subjects=3,
|
file_paths_b: list[str],
|
||||||
correction_method='fdr_bh', target_chroma='hbo',
|
group_a_name: str = "Group A",
|
||||||
selected_event=None, graph_bounds=None,
|
group_b_name: str = "Group B",
|
||||||
roi_config=None, threshold_topo=False):
|
df_cha_all: DataFrame | None = None,
|
||||||
|
raw_haemo: Any = None,
|
||||||
|
p_threshold: float = 0.05,
|
||||||
|
min_subjects: int = 3,
|
||||||
|
correction_method: str | None = "fdr_bh",
|
||||||
|
target_chroma: str = "hbo",
|
||||||
|
selected_event: str | None = None,
|
||||||
|
graph_bounds: tuple[float, float] | list[float] | None = None,
|
||||||
|
roi_config: Path | str | None = None,
|
||||||
|
threshold_topo: bool = False,
|
||||||
|
) -> DataFrame:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Perform cross-group independent statistical analyses (Group A vs Group B),
|
Perform cross-group independent statistical analyses (Group A vs Group B),
|
||||||
renders a grouped bar chart with significance brackets, and plots a group-contrast topography map.
|
renders a grouped bar chart with significance brackets, and plots a group-contrast topography map.
|
||||||
@@ -3331,11 +3350,21 @@ def run_cross_group_second_level_analysis(df_roi_all, file_paths_a, file_paths_b
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_cross_group_laterality_analysis(df_roi_all_a, df_roi_all_b, roi_pairs, condition,
|
def run_cross_group_laterality_analysis(
|
||||||
group_a_name="Group A", group_b_name="Group B",
|
df_roi_all_a: DataFrame,
|
||||||
target_chroma='hbo', min_subjects=3,
|
df_roi_all_b: DataFrame,
|
||||||
p_threshold=0.05, correction_method=None,
|
roi_pairs: tuple[str, str] | None,
|
||||||
roi_contra_label=None, roi_ipsi_label=None):
|
condition: str | None,
|
||||||
|
group_a_name: str = "Group A",
|
||||||
|
group_b_name: str = "Group B",
|
||||||
|
target_chroma: str = "hbo",
|
||||||
|
min_subjects: int = 3,
|
||||||
|
p_threshold: float = 0.05,
|
||||||
|
correction_method: str | None = None,
|
||||||
|
roi_contra_label: str | None = None,
|
||||||
|
roi_ipsi_label: str | None = None,
|
||||||
|
) -> DataFrame:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Compare LATERALITY between two independent groups of subjects (e.g. a
|
Compare LATERALITY between two independent groups of subjects (e.g. a
|
||||||
control group vs. a target group), using Welch's t-test on each
|
control group vs. a target group), using Welch's t-test on each
|
||||||
@@ -3582,11 +3611,20 @@ def run_cross_group_laterality_analysis(df_roi_all_a, df_roi_all_b, roi_pairs, c
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_cross_group_contrast_analysis(df_contrasts_a, df_contrasts_b, contrast_name, roi_json_path,
|
def run_cross_group_contrast_analysis(
|
||||||
group_a_name="Group A", group_b_name="Group B",
|
df_contrasts_a: DataFrame,
|
||||||
target_chroma='hbo', min_subjects=3,
|
df_contrasts_b: DataFrame,
|
||||||
p_threshold=0.05, correction_method='fdr_bh',
|
contrast_name: str,
|
||||||
weighted=True):
|
roi_json_path: str | Path | None,
|
||||||
|
group_a_name: str = "Group A",
|
||||||
|
group_b_name: str = "Group B",
|
||||||
|
target_chroma: str = "hbo",
|
||||||
|
min_subjects: int = 3,
|
||||||
|
p_threshold: float = 0.05,
|
||||||
|
correction_method: str = "fdr_bh",
|
||||||
|
weighted: bool = True,
|
||||||
|
) -> DataFrame:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Compare a JOINT-FIT TASK CONTRAST (e.g. '2.0_vs_3.0'), aggregated to ROI
|
Compare a JOINT-FIT TASK CONTRAST (e.g. '2.0_vs_3.0'), aggregated to ROI
|
||||||
level, between two independent groups. This is the cross-group analog
|
level, between two independent groups. This is the cross-group analog
|
||||||
@@ -3665,10 +3703,10 @@ def run_cross_group_contrast_analysis(df_contrasts_a, df_contrasts_b, contrast_n
|
|||||||
|
|
||||||
if df_a_filt.empty:
|
if df_a_filt.empty:
|
||||||
print(f"[ERROR] Contrast '{contrast_name}' not found anywhere in {group_a_name}'s data.")
|
print(f"[ERROR] Contrast '{contrast_name}' not found anywhere in {group_a_name}'s data.")
|
||||||
return pd.DataFrame()
|
return DataFrame()
|
||||||
if df_b_filt.empty:
|
if df_b_filt.empty:
|
||||||
print(f"[ERROR] Contrast '{contrast_name}' not found anywhere in {group_b_name}'s data.")
|
print(f"[ERROR] Contrast '{contrast_name}' not found anywhere in {group_b_name}'s data.")
|
||||||
return pd.DataFrame()
|
return DataFrame()
|
||||||
|
|
||||||
roi_a = aggregate_channel_contrasts_to_roi(df_a_filt, roi_json_path, weighted=weighted)
|
roi_a = aggregate_channel_contrasts_to_roi(df_a_filt, roi_json_path, weighted=weighted)
|
||||||
roi_b = aggregate_channel_contrasts_to_roi(df_b_filt, roi_json_path, weighted=weighted)
|
roi_b = aggregate_channel_contrasts_to_roi(df_b_filt, roi_json_path, weighted=weighted)
|
||||||
@@ -3679,7 +3717,7 @@ def run_cross_group_contrast_analysis(df_contrasts_a, df_contrasts_b, contrast_n
|
|||||||
if roi_a.empty or roi_b.empty:
|
if roi_a.empty or roi_b.empty:
|
||||||
print(f"[ERROR] No ROI-aggregated values produced for one or both groups "
|
print(f"[ERROR] No ROI-aggregated values produced for one or both groups "
|
||||||
f"(check regions.json channel names against this montage).")
|
f"(check regions.json channel names against this montage).")
|
||||||
return pd.DataFrame()
|
return DataFrame()
|
||||||
|
|
||||||
all_rois = sorted(set(roi_a['ROI'].unique()) | set(roi_b['ROI'].unique()))
|
all_rois = sorted(set(roi_a['ROI'].unique()) | set(roi_b['ROI'].unique()))
|
||||||
results = []
|
results = []
|
||||||
@@ -3798,10 +3836,17 @@ def run_cross_group_contrast_analysis(df_contrasts_a, df_contrasts_b, contrast_n
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run_roi_paired_contrast_analysis(df_roi_all, roi_pairs, condition,
|
def run_roi_paired_contrast_analysis(
|
||||||
target_chroma='hbo', min_subjects=5,
|
df_roi_all: DataFrame,
|
||||||
p_threshold=0.05, correction_method=None,
|
roi_pairs: Sequence[tuple[str, str]] | list[list[str]],
|
||||||
roi_a_label=None, roi_b_label=None):
|
condition: str,
|
||||||
|
target_chroma: str = 'hbo',
|
||||||
|
min_subjects: int = 5,
|
||||||
|
p_threshold: float = 0.05,
|
||||||
|
correction_method: str | None = None,
|
||||||
|
roi_a_label: str | None = None,
|
||||||
|
roi_b_label: str | None = None,
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Paired within-subject ROI contrast (e.g. contralateral minus ipsilateral
|
Paired within-subject ROI contrast (e.g. contralateral minus ipsilateral
|
||||||
motor ROI), as a companion to run_roi_second_level_analysis rather than a
|
motor ROI), as a companion to run_roi_second_level_analysis rather than a
|
||||||
@@ -4012,7 +4057,11 @@ def run_roi_paired_contrast_analysis(df_roi_all, roi_pairs, condition,
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def aggregate_channel_contrasts_to_roi(df_contrasts, roi_json_path, weighted=True):
|
def aggregate_channel_contrasts_to_roi(
|
||||||
|
df_contrasts: DataFrame,
|
||||||
|
roi_json_path: str | Path | None,
|
||||||
|
weighted: bool = True
|
||||||
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Combine already-computed per-channel CONTRAST results (e.g. your
|
Combine already-computed per-channel CONTRAST results (e.g. your
|
||||||
'2.0_vs_3.0' rows from contrasts.csv / contrast_results) into
|
'2.0_vs_3.0' rows from contrasts.csv / contrast_results) into
|
||||||
@@ -5519,13 +5568,13 @@ def process_participant(file_path, progress_callback=None):
|
|||||||
if BAD_CHANNELS_HANDLING != "None" and not FOLDING_BYP:
|
if BAD_CHANNELS_HANDLING != "None" and not FOLDING_BYP:
|
||||||
raw, fig_dropped, fig_raw_before, bad_channels = mark_bads(raw, bad_sci, bad_snr, bad_psp, bad_coeff_var, bad_amplitude_range, bad_noise, bad_disp)
|
raw, fig_dropped, fig_raw_before, bad_channels = mark_bads(raw, bad_sci, bad_snr, bad_psp, bad_coeff_var, bad_amplitude_range, bad_noise, bad_disp)
|
||||||
if fig_dropped and fig_raw_before is not None:
|
if fig_dropped and fig_raw_before is not None:
|
||||||
fig_individual["fig2"] = fig_dropped
|
fig_individual["Bad Channels by Method"] = fig_dropped
|
||||||
fig_individual["fig3"] = fig_raw_before
|
fig_individual["Bad Channels Data"] = fig_raw_before
|
||||||
if bad_channels:
|
if bad_channels:
|
||||||
if BAD_CHANNELS_HANDLING == "Interpolate":
|
if BAD_CHANNELS_HANDLING == "Interpolate":
|
||||||
raw, fig_raw_after, fig_compare = interpolate_fNIRS_bads_weighted_average(raw, max_dist=MAX_DIST, min_neighbors=MIN_NEIGHBORS)
|
raw, fig_raw_after, fig_compare = interpolate_fNIRS_bads_weighted_average(raw, max_dist=MAX_DIST, min_neighbors=MIN_NEIGHBORS)
|
||||||
fig_individual["fig4"] = fig_raw_after
|
fig_individual["Data after Interpolating Bad Channels"] = fig_raw_after
|
||||||
fig_individual["Compare"] = fig_compare
|
fig_individual["Bad Channels Interpolation Results"] = fig_compare
|
||||||
elif BAD_CHANNELS_HANDLING == "Remove":
|
elif BAD_CHANNELS_HANDLING == "Remove":
|
||||||
raw = remove_bad_channels(raw, bad_channels)
|
raw = remove_bad_channels(raw, bad_channels)
|
||||||
if progress_callback: progress_callback(13)
|
if progress_callback: progress_callback(13)
|
||||||
@@ -5542,7 +5591,7 @@ def process_participant(file_path, progress_callback=None):
|
|||||||
if TDDR and not FOLDING_BYP:
|
if TDDR and not FOLDING_BYP:
|
||||||
raw_od = temporal_derivative_distribution_repair(raw_od)
|
raw_od = temporal_derivative_distribution_repair(raw_od)
|
||||||
fig_raw_od_tddr = raw_od.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="After TDDR (Motion Correction)", show=False)
|
fig_raw_od_tddr = raw_od.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="After TDDR (Motion Correction)", show=False)
|
||||||
fig_individual["TDDR"] = fig_raw_od_tddr
|
fig_individual["Temporal Derivative Distribution Repair"] = fig_raw_od_tddr
|
||||||
if progress_callback: progress_callback(15)
|
if progress_callback: progress_callback(15)
|
||||||
logger.info("Step 15 Completed.")
|
logger.info("Step 15 Completed.")
|
||||||
|
|
||||||
@@ -5556,7 +5605,7 @@ def process_participant(file_path, progress_callback=None):
|
|||||||
# Step 17: Haemoglobin Concentration
|
# Step 17: Haemoglobin Concentration
|
||||||
raw_haemo = beer_lambert_law(raw_od, ppf=calculate_dpf(file_path))
|
raw_haemo = beer_lambert_law(raw_od, ppf=calculate_dpf(file_path))
|
||||||
fig_raw_haemo_bll = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="HbO and HbR Signals", show=False)
|
fig_raw_haemo_bll = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="HbO and HbR Signals", show=False)
|
||||||
fig_individual["BLL"] = fig_raw_haemo_bll
|
fig_individual["Modified Beer Lambert Law"] = fig_raw_haemo_bll
|
||||||
if progress_callback: progress_callback(17)
|
if progress_callback: progress_callback(17)
|
||||||
logger.info("Step 17 Completed.")
|
logger.info("Step 17 Completed.")
|
||||||
|
|
||||||
@@ -5564,15 +5613,15 @@ def process_participant(file_path, progress_callback=None):
|
|||||||
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)
|
||||||
fig_raw_haemo_enc = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="Enhance Negative Correlation", show=False)
|
fig_raw_haemo_enc = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="Enhance Negative Correlation", show=False)
|
||||||
fig_individual["ENC"] = fig_raw_haemo_enc
|
fig_individual["Enhance Negative Correlation"] = fig_raw_haemo_enc
|
||||||
if progress_callback: progress_callback(18)
|
if progress_callback: progress_callback(18)
|
||||||
logger.info("Step 18 Completed.")
|
logger.info("Step 18 Completed.")
|
||||||
|
|
||||||
# Step 19: Filter
|
# Step 19: Filter
|
||||||
if FILTER and not FOLDING_BYP:
|
if FILTER and not FOLDING_BYP:
|
||||||
raw_haemo, fig_filter, fig_raw_haemo_filter = filter_the_data(raw_haemo)
|
raw_haemo, fig_filter, fig_raw_haemo_filter = filter_the_data(raw_haemo)
|
||||||
fig_individual["filter1"] = fig_filter
|
fig_individual["Filter_1"] = fig_filter
|
||||||
fig_individual["filter2"] = fig_raw_haemo_filter
|
fig_individual["Filter_2"] = fig_raw_haemo_filter
|
||||||
if progress_callback: progress_callback(19)
|
if progress_callback: progress_callback(19)
|
||||||
logger.info("Step 19 Completed.")
|
logger.info("Step 19 Completed.")
|
||||||
|
|
||||||
@@ -5580,7 +5629,7 @@ def process_participant(file_path, progress_callback=None):
|
|||||||
if not FOLDING_BYP:
|
if not FOLDING_BYP:
|
||||||
events, event_dict = events_from_annotations(raw_haemo)
|
events, event_dict = events_from_annotations(raw_haemo)
|
||||||
fig_events = plot_events(events, event_id=event_dict, sfreq=raw_haemo.info["sfreq"], show=False)
|
fig_events = plot_events(events, event_id=event_dict, sfreq=raw_haemo.info["sfreq"], show=False)
|
||||||
fig_individual["events"] = fig_events
|
fig_individual["Events"] = fig_events
|
||||||
if progress_callback: progress_callback(20)
|
if progress_callback: progress_callback(20)
|
||||||
logger.info("Step 20 Completed.")
|
logger.info("Step 20 Completed.")
|
||||||
|
|
||||||
@@ -5652,7 +5701,11 @@ def sanitize_paths_for_pickle(raw_haemo, epochs):
|
|||||||
epochs._raw._filenames = [str(p) for p in epochs._raw._filenames]
|
epochs._raw._filenames = [str(p) for p in epochs._raw._filenames]
|
||||||
|
|
||||||
|
|
||||||
def functional_connectivity_spectral_epochs(epochs, n_lines, vmin):
|
def functional_connectivity_spectral_epochs(
|
||||||
|
epochs: DataFrame | None,
|
||||||
|
n_lines: int,
|
||||||
|
vmin: float,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
# will crash without this load
|
# will crash without this load
|
||||||
epochs.load_data()
|
epochs.load_data()
|
||||||
@@ -5691,7 +5744,11 @@ def functional_connectivity_spectral_epochs(epochs, n_lines, vmin):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def functional_connectivity_spectral_time(epochs, n_lines, vmin):
|
def functional_connectivity_spectral_time(
|
||||||
|
epochs: DataFrame | None,
|
||||||
|
n_lines: int,
|
||||||
|
vmin: float,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
# will crash without this load
|
# will crash without this load
|
||||||
epochs.load_data()
|
epochs.load_data()
|
||||||
@@ -5735,7 +5792,12 @@ def functional_connectivity_spectral_time(epochs, n_lines, vmin):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def functional_connectivity_envelope(epochs, n_lines, vmin):
|
def functional_connectivity_envelope(
|
||||||
|
epochs: DataFrame | None,
|
||||||
|
n_lines: int,
|
||||||
|
vmin: float,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
# will crash without this load
|
# will crash without this load
|
||||||
|
|
||||||
epochs.load_data()
|
epochs.load_data()
|
||||||
@@ -5765,7 +5827,12 @@ def functional_connectivity_envelope(epochs, n_lines, vmin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def functional_connectivity_betas(raw_hbo, n_lines, vmin, event_name=None):
|
def functional_connectivity_betas(
|
||||||
|
raw_hbo: BaseRaw,
|
||||||
|
n_lines: int,
|
||||||
|
vmin: float,
|
||||||
|
event_name: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
raw_hbo = raw_hbo.copy().pick(picks="hbo")
|
raw_hbo = raw_hbo.copy().pick(picks="hbo")
|
||||||
onsets = raw_hbo.annotations.onset
|
onsets = raw_hbo.annotations.onset
|
||||||
@@ -5966,7 +6033,15 @@ def get_single_subject_beta_corr(raw_hbo, event_name=None, config=None):
|
|||||||
return corr_matrix, raw_hbo.ch_names
|
return corr_matrix, raw_hbo.ch_names
|
||||||
|
|
||||||
|
|
||||||
def run_group_functional_connectivity(haemo_dict, config_dict, selected_paths, event_name, n_lines, vmin):
|
def run_group_functional_connectivity(
|
||||||
|
haemo_dict: dict[str | Path, BaseRaw],
|
||||||
|
config_dict: dict[str, Any],
|
||||||
|
selected_paths: list[str],
|
||||||
|
event_name: str | None,
|
||||||
|
n_lines: int,
|
||||||
|
vmin: float,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
"""Aggregates multiple participants and triggers the plot."""
|
"""Aggregates multiple participants and triggers the plot."""
|
||||||
all_z_matrices = []
|
all_z_matrices = []
|
||||||
common_names = None
|
common_names = None
|
||||||
@@ -6068,3 +6143,27 @@ def run_group_functional_connectivity(haemo_dict, config_dict, selected_paths, e
|
|||||||
title=f"Group Connectivity: {event_name if event_name else 'All Events'}",
|
title=f"Group Connectivity: {event_name if event_name else 'All Events'}",
|
||||||
vmin=vmin, vmax=1.0, colormap='hot'
|
vmin=vmin, vmax=1.0, colormap='hot'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sparks_csv_export(
|
||||||
|
haemo_obj: BaseRaw,
|
||||||
|
save_path: str,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
raw = haemo_obj
|
||||||
|
data, times = raw.get_data(return_times=True)
|
||||||
|
ann_col = np.full(times.shape, "", dtype=object)
|
||||||
|
|
||||||
|
if raw.annotations is not None and len(raw.annotations) > 0:
|
||||||
|
for onset, duration, desc in zip(
|
||||||
|
raw.annotations.onset,
|
||||||
|
raw.annotations.duration,
|
||||||
|
raw.annotations.description
|
||||||
|
):
|
||||||
|
mask = (times >= onset) & (times < onset + duration)
|
||||||
|
ann_col[mask] = desc
|
||||||
|
|
||||||
|
df = pd.DataFrame(data.T, columns=raw.ch_names)
|
||||||
|
df.insert(0, "annotation", ann_col)
|
||||||
|
df.insert(0, "time", times)
|
||||||
|
df.to_csv(save_path, index=False)
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
"""
|
"""
|
||||||
Filename: crossgroupbrainimage.py
|
Filename: crossgroupbrainimage.py
|
||||||
Description: Logic for the Cross-Group Brain & Image analysis window
|
Description: Logic for the Cross-Group Brain & Image analysis window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Built-in Imports
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
|
from mne.io.base import BaseRaw
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
from flares import aggregate_fnirs_group_geometry, plot_2d_3d_contrasts_between_groups
|
from flares import aggregate_fnirs_group_geometry, plot_2d_3d_contrasts_between_groups
|
||||||
from src.shared.flaresbasewidget import CrossGroupUIMixin, FlaresBaseWidget
|
from src.shared.flaresbasewidget import CrossGroupUIMixin, FlaresBaseWidget
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
PARAMETERIZED_INDEXES = {
|
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
|
||||||
0: [
|
0: [
|
||||||
{
|
{
|
||||||
"key": "show_optodes",
|
"key": "show_optodes",
|
||||||
@@ -52,7 +60,15 @@ PARAMETERIZED_INDEXES = {
|
|||||||
|
|
||||||
|
|
||||||
class CrossGroupBrainImageWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
class CrossGroupBrainImageWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict):
|
def __init__(
|
||||||
|
self,
|
||||||
|
haemo_dict: dict[str | Path, BaseRaw],
|
||||||
|
df_ind_dict: dict[str, DataFrame],
|
||||||
|
design_matrix_dict: dict[str, DataFrame],
|
||||||
|
contrast_results_dict: dict[str, dict[str, Any]],
|
||||||
|
group_dict: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__("CrossGroupBrainImage")
|
super().__init__("CrossGroupBrainImage")
|
||||||
self.setWindowTitle(f"Cross-Group Brain & Image Viewer - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Cross-Group Brain & Image Viewer - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
@@ -70,8 +86,9 @@ class CrossGroupBrainImageWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
|||||||
if request is None:
|
if request is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
(selected_event, file_paths_a, file_paths_b, all_selected_paths, selected_indexes, param_values,) = request
|
(selected_event, file_paths_a, file_paths_b, all_selected_paths, selected_indexes, raw_params) = request
|
||||||
|
|
||||||
|
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
|
||||||
|
|
||||||
# Build group-level contrast DataFrames
|
# Build group-level contrast DataFrames
|
||||||
def concat_group_contrasts(file_paths: list[str], event: str | None) -> pd.DataFrame:
|
def concat_group_contrasts(file_paths: list[str], event: str | None) -> pd.DataFrame:
|
||||||
@@ -102,8 +119,11 @@ class CrossGroupBrainImageWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
|||||||
|
|
||||||
if len(all_raw_objs) > 1:
|
if len(all_raw_objs) > 1:
|
||||||
processed_raw = aggregate_fnirs_group_geometry(all_raw_objs)
|
processed_raw = aggregate_fnirs_group_geometry(all_raw_objs)
|
||||||
|
elif len(all_raw_objs) == 1 and all_raw_objs[0] is not None:
|
||||||
|
processed_raw = all_raw_objs[0].copy()
|
||||||
|
processed_raw.pick(picks="hbo") # type: ignore
|
||||||
else:
|
else:
|
||||||
processed_raw = all_raw_objs[0].copy().pick(picks="hbo")
|
processed_raw = None
|
||||||
|
|
||||||
# Visualizations
|
# Visualizations
|
||||||
for idx in selected_indexes:
|
for idx in selected_indexes:
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
"""
|
"""
|
||||||
Filename: crossgroupstats.py
|
Filename: crossgroupstats.py
|
||||||
Description: Cross-Group stats analysis window
|
Description: Cross-Group stats analysis window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from mne.io.base import BaseRaw
|
||||||
|
|
||||||
from flares import run_cross_group_contrast_analysis, run_cross_group_laterality_analysis, run_cross_group_second_level_analysis
|
from flares import run_cross_group_contrast_analysis, run_cross_group_laterality_analysis, run_cross_group_second_level_analysis
|
||||||
from src.shared.flaresbasewidget import CrossGroupUIMixin, FlaresBaseWidget
|
from src.shared.flaresbasewidget import CrossGroupUIMixin, FlaresBaseWidget
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
PARAMETERIZED_INDEXES = {
|
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
|
||||||
0: [
|
0: [
|
||||||
{
|
{
|
||||||
"key": "p_threshold",
|
"key": "p_threshold",
|
||||||
@@ -136,7 +144,18 @@ DESCRIPTION = """0. Raw ROI Comparison (run_cross_group_second_level_analysis)
|
|||||||
|
|
||||||
|
|
||||||
class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict, json_location):
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
haemo_dict: dict[str | Path, BaseRaw],
|
||||||
|
cha_dict: dict[str, DataFrame],
|
||||||
|
df_ind_dict: dict[str, DataFrame],
|
||||||
|
design_matrix_dict: dict[str, DataFrame],
|
||||||
|
contrast_results_dict: dict[str, dict[str, Any]],
|
||||||
|
group_dict: dict[str, str],
|
||||||
|
json_location: str | Path
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__("CrossGroupStats")
|
super().__init__("CrossGroupStats")
|
||||||
self.setWindowTitle(f"Cross-Group Stats Viewer - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Cross-Group Stats Viewer - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
@@ -144,7 +163,7 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
|||||||
self.df_ind_dict = df_ind_dict
|
self.df_ind_dict = df_ind_dict
|
||||||
self.design_matrix_dict = design_matrix_dict
|
self.design_matrix_dict = design_matrix_dict
|
||||||
self.contrast_results_dict = contrast_results_dict
|
self.contrast_results_dict = contrast_results_dict
|
||||||
self.group_dict = group_dict
|
# self.group_dict = group_dict
|
||||||
self.json_location = json_location
|
self.json_location = json_location
|
||||||
|
|
||||||
self.setup_cross_group_ui(["0 (Raw ROI Comparison)", "1 (Laterality Comparison)", "2 (Contrast Comparison)",], placeholder_text=DESCRIPTION)
|
self.setup_cross_group_ui(["0 (Raw ROI Comparison)", "1 (Laterality Comparison)", "2 (Contrast Comparison)",], placeholder_text=DESCRIPTION)
|
||||||
@@ -155,23 +174,18 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
|||||||
if request is None:
|
if request is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
(selected_event, file_paths_a, file_paths_b, all_selected_paths, selected_indexes, param_values,) = request
|
(selected_event, file_paths_a, file_paths_b, _, selected_indexes, raw_params) = request
|
||||||
|
|
||||||
if isinstance(self.df_ind_dict, dict):
|
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
|
||||||
# Filter out empty entries and concatenate
|
|
||||||
valid_dfs = [df for df in self.df_ind_dict.values() if isinstance(df, pd.DataFrame) and not df.empty]
|
|
||||||
if valid_dfs:
|
|
||||||
df_ind_combined = pd.concat(valid_dfs, ignore_index=True)
|
|
||||||
else:
|
|
||||||
df_ind_combined = pd.DataFrame()
|
|
||||||
else:
|
|
||||||
df_ind_combined = self.df_ind_dict
|
|
||||||
|
|
||||||
if isinstance(self.cha_dict, dict):
|
valid_dfs = [df for df in self.df_ind_dict.values() if not df.empty]
|
||||||
valid_chas = [df for df in self.cha_dict.values() if isinstance(df, pd.DataFrame) and not df.empty]
|
if valid_dfs:
|
||||||
cha_combined = pd.concat(valid_chas, ignore_index=True) if valid_chas else pd.DataFrame()
|
df_ind_combined = pd.concat(valid_dfs, ignore_index=True)
|
||||||
else:
|
else:
|
||||||
cha_combined = self.cha_dict
|
df_ind_combined = pd.DataFrame()
|
||||||
|
|
||||||
|
valid_chas = [df for df in self.cha_dict.values() if not df.empty]
|
||||||
|
cha_combined = pd.concat(valid_chas, ignore_index=True) if valid_chas else pd.DataFrame()
|
||||||
|
|
||||||
sample_path = file_paths_a[0]
|
sample_path = file_paths_a[0]
|
||||||
p_haemo = self.haemo_dict.get(sample_path)
|
p_haemo = self.haemo_dict.get(sample_path)
|
||||||
@@ -213,8 +227,8 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
|||||||
min_subjects = params.get("min_subjects", 3)
|
min_subjects = params.get("min_subjects", 3)
|
||||||
correction_method = params.get("correction_method", "None")
|
correction_method = params.get("correction_method", "None")
|
||||||
target_chroma = params.get("target_chroma", "hbo")
|
target_chroma = params.get("target_chroma", "hbo")
|
||||||
roi_a = params.get("roi_a", "").strip()
|
roi_a: str = params.get("roi_a", "").strip()
|
||||||
roi_b = params.get("roi_b", "").strip()
|
roi_b: str = params.get("roi_b", "").strip()
|
||||||
|
|
||||||
if not roi_a or not roi_b:
|
if not roi_a or not roi_b:
|
||||||
print("Both a contralateral and ipsilateral ROI name must be specified.")
|
print("Both a contralateral and ipsilateral ROI name must be specified.")
|
||||||
@@ -225,14 +239,19 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
|||||||
|
|
||||||
# Build each group's dataframe directly from the dict using
|
# Build each group's dataframe directly from the dict using
|
||||||
# the file-path lists as keys - no ID cleaning/matching needed.
|
# the file-path lists as keys - no ID cleaning/matching needed.
|
||||||
def _build_group_df(file_paths, dict_source):
|
def _build_group_df(
|
||||||
|
file_paths: list[str],
|
||||||
|
dict_source: dict[str, DataFrame]
|
||||||
|
) -> DataFrame:
|
||||||
|
|
||||||
valid_dfs = [
|
valid_dfs = [
|
||||||
dict_source[fp] for fp in file_paths
|
dict_source[fp] for fp in file_paths
|
||||||
if fp in dict_source and isinstance(dict_source[fp], pd.DataFrame)
|
if fp in dict_source and not dict_source[fp].empty
|
||||||
and not dict_source[fp].empty
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return pd.concat(valid_dfs, ignore_index=True) if valid_dfs else pd.DataFrame()
|
return pd.concat(valid_dfs, ignore_index=True) if valid_dfs else pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
df_roi_a = _build_group_df(file_paths_a, self.df_ind_dict)
|
df_roi_a = _build_group_df(file_paths_a, self.df_ind_dict)
|
||||||
df_roi_b = _build_group_df(file_paths_b, self.df_ind_dict)
|
df_roi_b = _build_group_df(file_paths_b, self.df_ind_dict)
|
||||||
|
|
||||||
@@ -271,8 +290,13 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
|||||||
# directly from contrast_results_dict, keyed by file path -
|
# directly from contrast_results_dict, keyed by file path -
|
||||||
# same dict-key approach as the laterality patch, avoids
|
# same dict-key approach as the laterality patch, avoids
|
||||||
# any ID-string matching.
|
# any ID-string matching.
|
||||||
def _build_group_contrast_df(file_paths, contrast_dict, name):
|
def _build_group_contrast_df(
|
||||||
all_rows = []
|
file_paths: list[str],
|
||||||
|
contrast_dict: dict[str, dict[str, pd.DataFrame]],
|
||||||
|
name: str,
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
|
||||||
|
all_rows: list[DataFrame] = []
|
||||||
for fp in file_paths:
|
for fp in file_paths:
|
||||||
condition_dfs = contrast_dict.get(fp)
|
condition_dfs = contrast_dict.get(fp)
|
||||||
if condition_dfs is None:
|
if condition_dfs is None:
|
||||||
|
|||||||
+24
-31
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Filename: exporttocsv.py
|
Filename: exporttocsv.py
|
||||||
Description: Logic for the Export To CSV analysis window
|
Description: Logic for the Export To CSV analysis window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
@@ -8,35 +9,47 @@ License: GPL-3.0
|
|||||||
|
|
||||||
# Built-in imports
|
# Built-in imports
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
import numpy as np
|
from pandas import DataFrame
|
||||||
import pandas as pd
|
|
||||||
|
from mne.io.base import BaseRaw
|
||||||
|
|
||||||
from PySide6.QtWidgets import QFileDialog, QMessageBox
|
from PySide6.QtWidgets import QFileDialog, QMessageBox
|
||||||
|
|
||||||
|
from flares import sparks_csv_export
|
||||||
from src.shared.flaresbasewidget import CSVUIMixin, FlaresBaseWidget
|
from src.shared.flaresbasewidget import CSVUIMixin, FlaresBaseWidget
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
|
class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict):
|
def __init__(
|
||||||
|
self,
|
||||||
|
haemo_dict: dict[str | Path, BaseRaw],
|
||||||
|
cha_dict: dict[str, DataFrame],
|
||||||
|
df_ind_dict: dict[str, DataFrame],
|
||||||
|
design_matrix_dict: dict[str, DataFrame],
|
||||||
|
contrast_results_dict: dict[str, dict[str, Any]],
|
||||||
|
group_dict: dict[str, str],
|
||||||
|
) -> 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
|
# self.df_ind = df_ind_dict
|
||||||
self.design_matrix = design_matrix
|
# self.design_matrix = design_matrix_dict
|
||||||
self.group = group
|
# self.contrast_results_dict = contrast_results_dict
|
||||||
self.contrast_results_dict = contrast_results_dict
|
# self.group = group_dict
|
||||||
|
|
||||||
self.setup_csv_ui(["0 (Export Data to CSV)", "1 (CSV for SPARKS)",])
|
self.setup_csv_ui(["0 (Export Data to CSV)", "1 (CSV for SPARKS)",])
|
||||||
|
|
||||||
|
|
||||||
def process_request(self):
|
def process_request(self):
|
||||||
# TODO: Move this into flares for the call?
|
|
||||||
selected_display_names = self._get_checked_items(self.participant_dropdown)
|
selected_display_names = self._get_checked_items(self.participant_dropdown)
|
||||||
selected_file_paths = []
|
selected_file_paths: list[str] = []
|
||||||
for display_name in selected_display_names:
|
for display_name in selected_display_names:
|
||||||
for fp, short_label in self.participant_map.items():
|
for fp, short_label in self.participant_map.items():
|
||||||
expected_display = f"{short_label} ({os.path.basename(fp)})"
|
expected_display = f"{short_label} ({os.path.basename(fp)})"
|
||||||
@@ -52,7 +65,6 @@ class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
|
|||||||
QMessageBox.warning(self, "Selection Missing", "Please select at least one participant and one export type.")
|
QMessageBox.warning(self, "Selection Missing", "Please select at least one participant and one export type.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. ASK ONCE: Select Output Directory
|
|
||||||
output_dir = QFileDialog.getExistingDirectory(self, "Select Output Folder for CSV Exports")
|
output_dir = QFileDialog.getExistingDirectory(self, "Select Output Folder for CSV Exports")
|
||||||
|
|
||||||
if not output_dir:
|
if not output_dir:
|
||||||
@@ -78,29 +90,11 @@ class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
|
|||||||
cha.to_csv(save_path)
|
cha.to_csv(save_path)
|
||||||
success_count += 1
|
success_count += 1
|
||||||
|
|
||||||
|
|
||||||
elif idx == 1:
|
elif idx == 1:
|
||||||
# SPARKS Export
|
# SPARKS Export
|
||||||
save_path = os.path.join(output_dir, f"{base_filename}_sparks.csv")
|
save_path = os.path.join(output_dir, f"{base_filename}_sparks.csv")
|
||||||
if haemo_obj is not None:
|
sparks_csv_export(haemo_obj, save_path)
|
||||||
raw = haemo_obj
|
success_count += 1
|
||||||
data, times = raw.get_data(return_times=True)
|
|
||||||
ann_col = np.full(times.shape, "", dtype=object)
|
|
||||||
|
|
||||||
if raw.annotations is not None and len(raw.annotations) > 0:
|
|
||||||
for onset, duration, desc in zip(
|
|
||||||
raw.annotations.onset,
|
|
||||||
raw.annotations.duration,
|
|
||||||
raw.annotations.description
|
|
||||||
):
|
|
||||||
mask = (times >= onset) & (times < onset + duration)
|
|
||||||
ann_col[mask] = desc
|
|
||||||
|
|
||||||
df = pd.DataFrame(data.T, columns=raw.ch_names)
|
|
||||||
df.insert(0, "annotation", ann_col)
|
|
||||||
df.insert(0, "time", times)
|
|
||||||
df.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}")
|
||||||
@@ -120,4 +114,3 @@ class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
|
|||||||
# caller="Video Alignment Tool"
|
# caller="Video Alignment Tool"
|
||||||
# )
|
# )
|
||||||
# win.show()
|
# win.show()
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
"""
|
"""
|
||||||
Filename: intergroupbrainimage.py
|
Filename: intergroupbrainimage.py
|
||||||
Description: Logic for the Inter-Group Brain & Image analysis window
|
Description: Logic for the Inter-Group Brain & Image analysis window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Built-in Imports
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from mne import Annotations
|
||||||
|
from mne.io.base import BaseRaw
|
||||||
|
|
||||||
from flares import aggregate_fnirs_group_geometry, plot_fir_model_results, brain_3d_visualization
|
from flares import aggregate_fnirs_group_geometry, plot_fir_model_results, brain_3d_visualization
|
||||||
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
|
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
from mne.io import BaseRaw
|
||||||
|
|
||||||
|
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
|
||||||
PARAMETERIZED_INDEXES = {
|
|
||||||
0: [
|
0: [
|
||||||
{
|
{
|
||||||
"key": "lower_bound",
|
"key": "lower_bound",
|
||||||
@@ -74,15 +83,24 @@ PARAMETERIZED_INDEXES = {
|
|||||||
|
|
||||||
|
|
||||||
class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
|
class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group):
|
def __init__(
|
||||||
|
self,
|
||||||
|
haemo_dict: dict[str | Path, BaseRaw],
|
||||||
|
cha_dict: dict[str, DataFrame],
|
||||||
|
df_ind_dict: dict[str, DataFrame],
|
||||||
|
design_matrix_dict: dict[str, DataFrame],
|
||||||
|
contrast_results_dict: dict[str, dict[str, Any]],
|
||||||
|
group_dict: dict[str, str]
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__("InterGroupBrainImage")
|
super().__init__("InterGroupBrainImage")
|
||||||
self.setWindowTitle(f"Inter-Group Brain & Image Viewer - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Inter-Group Brain & Image Viewer - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
self.cha = cha
|
self.cha_dict = cha_dict
|
||||||
self.df_ind = df_ind
|
self.df_ind_dict = df_ind_dict
|
||||||
self.design_matrix = design_matrix
|
self.design_matrix_dict = design_matrix_dict
|
||||||
self.contrast_results = contrast_results
|
self.contrast_results_dict = contrast_results_dict
|
||||||
self.group = group
|
# self.group_dict = group_dict
|
||||||
|
|
||||||
self.setup_inter_group_ui(["0 (GLM Results)", "1 (Significance)", "2 (Brain Activity Visualization)",])
|
self.setup_inter_group_ui(["0 (GLM Results)", "1 (Significance)", "2 (Brain Activity Visualization)",])
|
||||||
|
|
||||||
@@ -92,35 +110,45 @@ class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
|
|||||||
if request is None:
|
if request is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
|
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
|
||||||
|
|
||||||
|
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
|
||||||
|
|
||||||
all_cha = pd.DataFrame()
|
all_cha = pd.DataFrame()
|
||||||
for file_path in selected_file_paths:
|
for file_path in selected_file_paths:
|
||||||
haemo_obj = self.haemo_dict.get(file_path)
|
haemo_obj = self.haemo_dict.get(file_path)
|
||||||
|
|
||||||
|
if haemo_obj is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if selected_event:
|
if selected_event:
|
||||||
participant_events = set(haemo_obj.annotations.description)
|
raw_annotations = getattr(haemo_obj, "annotations", None)
|
||||||
|
|
||||||
|
if raw_annotations is not None:
|
||||||
|
annotations = cast(Annotations, raw_annotations)
|
||||||
|
descriptions = cast(list[str], list(annotations.description))
|
||||||
|
participant_events: set[str] = set(descriptions)
|
||||||
|
else:
|
||||||
|
participant_events: set[str] = set()
|
||||||
|
|
||||||
if selected_event not in participant_events:
|
if selected_event not in participant_events:
|
||||||
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
|
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if haemo_obj is None:
|
cha_df = self.cha_dict.get(file_path)
|
||||||
continue
|
|
||||||
|
|
||||||
cha_df = self.cha.get(file_path)
|
|
||||||
if cha_df is not None:
|
if cha_df is not None:
|
||||||
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
|
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
|
||||||
|
|
||||||
# Pass the necessary arguments to each method
|
# Pass the necessary arguments to each method
|
||||||
file_path = selected_file_paths[0]
|
file_path = selected_file_paths[0]
|
||||||
p_haemo = self.haemo_dict.get(file_path)
|
p_haemo = self.haemo_dict.get(file_path)
|
||||||
p_design_matrix = self.design_matrix.get(file_path)
|
p_design_matrix = self.design_matrix_dict.get(file_path)
|
||||||
|
|
||||||
df_group = pd.DataFrame()
|
df_group = pd.DataFrame()
|
||||||
|
|
||||||
if selected_file_paths:
|
if selected_file_paths:
|
||||||
for file_path in selected_file_paths:
|
for file_path in selected_file_paths:
|
||||||
df = self.df_ind.get(file_path)
|
df = self.df_ind_dict.get(file_path)
|
||||||
if df is not None:
|
if df is not None:
|
||||||
df_group = pd.concat([df_group, df], ignore_index=True)
|
df_group = pd.concat([df_group, df], ignore_index=True)
|
||||||
|
|
||||||
@@ -147,9 +175,9 @@ class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
|
|||||||
print(f"Missing parameters for index {idx}, skipping.")
|
print(f"Missing parameters for index {idx}, skipping.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
all_contrasts = []
|
all_contrasts: list[DataFrame] = []
|
||||||
for fp in selected_file_paths:
|
for fp in selected_file_paths:
|
||||||
condition_dfs = self.contrast_results.get(fp, {})
|
condition_dfs = self.contrast_results_dict.get(fp, {})
|
||||||
if selected_event in condition_dfs:
|
if selected_event in condition_dfs:
|
||||||
df = condition_dfs[selected_event].copy()
|
df = condition_dfs[selected_event].copy()
|
||||||
df["ID"] = fp
|
df["ID"] = fp
|
||||||
@@ -159,7 +187,8 @@ class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
|
|||||||
print("No contrast data found for selected participants and event.")
|
print("No contrast data found for selected participants and event.")
|
||||||
return
|
return
|
||||||
|
|
||||||
df_contrasts = pd.concat(all_contrasts, ignore_index=True)
|
# TODO: look at intergroupstats and figure out what to do
|
||||||
|
_ = pd.concat(all_contrasts, ignore_index=True)
|
||||||
#flares.run_second_level_analysis(df_contrasts, p_haemo, p_val, graph_bounds)
|
#flares.run_second_level_analysis(df_contrasts, p_haemo, p_val, graph_bounds)
|
||||||
|
|
||||||
elif idx == 2:
|
elif idx == 2:
|
||||||
@@ -173,13 +202,15 @@ class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
|
|||||||
print(f"Missing parameters for index {idx}, skipping.")
|
print(f"Missing parameters for index {idx}, skipping.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raw_list = [self.haemo_dict.get(fp) for fp in selected_file_paths]
|
all_raw_objs = [self.haemo_dict.get(fp) for fp in selected_file_paths if self.haemo_dict.get(fp)]
|
||||||
|
|
||||||
if len(selected_file_paths) > 1:
|
if len(all_raw_objs) > 1:
|
||||||
print(f"Aggregating geometry for {len(selected_file_paths)} participants...")
|
processed_raw = aggregate_fnirs_group_geometry(all_raw_objs)
|
||||||
processed_raw = aggregate_fnirs_group_geometry(raw_list)
|
elif len(all_raw_objs) == 1 and all_raw_objs[0] is not None:
|
||||||
|
processed_raw = all_raw_objs[0].copy()
|
||||||
|
processed_raw.pick(picks="hbo") # type: ignore
|
||||||
else:
|
else:
|
||||||
processed_raw = raw_list[0].copy().pick(picks="hbo")
|
processed_raw = None
|
||||||
|
|
||||||
brain_3d_visualization(processed_raw, all_cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds)
|
brain_3d_visualization(processed_raw, all_cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds)
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
"""
|
"""
|
||||||
Filename: intergroupfunctionalconnectivity.py
|
Filename: intergroupfunctionalconnectivity.py
|
||||||
Description: Logic for the Inter-Group Functional Connectivity analysis window
|
Description: Logic for the Inter-Group Functional Connectivity analysis window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
from PySide6.QtWidgets import QMessageBox
|
from PySide6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from mne.io.base import BaseRaw
|
||||||
|
|
||||||
from flares import run_group_functional_connectivity
|
from flares import run_group_functional_connectivity
|
||||||
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
|
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
PARAMETERIZED_INDEXES = {
|
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
|
||||||
0: [
|
0: [
|
||||||
{
|
{
|
||||||
"key": "n_lines",
|
"key": "n_lines",
|
||||||
@@ -34,11 +41,17 @@ PARAMETERIZED_INDEXES = {
|
|||||||
|
|
||||||
|
|
||||||
class InterGroupFunctionalConnectivityWidget(InterGroupUIMixin, FlaresBaseWidget):
|
class InterGroupFunctionalConnectivityWidget(InterGroupUIMixin, FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, group, config_dict):
|
def __init__(
|
||||||
|
self,
|
||||||
|
haemo_dict: dict[str | Path, BaseRaw],
|
||||||
|
group_dict: dict[str, str],
|
||||||
|
config_dict: dict[str, str]
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__("InterGroupFunctionalConnectivity")
|
super().__init__("InterGroupFunctionalConnectivity")
|
||||||
self.setWindowTitle(f"Inter-Group Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Inter-Group Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
self.group = group
|
#self.group_dict = group_dict
|
||||||
self.config_dict = config_dict
|
self.config_dict = config_dict
|
||||||
|
|
||||||
QMessageBox.warning(self, f"Warning - {APP_NAME.upper()}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
|
QMessageBox.warning(self, f"Warning - {APP_NAME.upper()}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
|
||||||
@@ -52,7 +65,9 @@ class InterGroupFunctionalConnectivityWidget(InterGroupUIMixin, FlaresBaseWidget
|
|||||||
if request is None:
|
if request is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
|
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
|
||||||
|
|
||||||
|
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
|
||||||
|
|
||||||
for idx in selected_indexes:
|
for idx in selected_indexes:
|
||||||
if idx == 0:
|
if idx == 0:
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
"""
|
"""
|
||||||
Filename: intergroupstats.py
|
Filename: intergroupstats.py
|
||||||
Description: Logic for the Inter-Group Stats analysis window
|
Description: Logic for the Inter-Group Stats analysis window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from mne import Annotations
|
||||||
|
from mne.io.base import BaseRaw
|
||||||
|
|
||||||
from flares import run_roi_paired_contrast_analysis, run_roi_second_level_analysis, aggregate_channel_contrasts_to_roi
|
from flares import run_roi_paired_contrast_analysis, run_roi_second_level_analysis, aggregate_channel_contrasts_to_roi
|
||||||
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
|
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
PARAMETERIZED_INDEXES = {
|
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
|
||||||
0: [
|
0: [
|
||||||
{
|
{
|
||||||
"key": "p_threshold",
|
"key": "p_threshold",
|
||||||
@@ -148,40 +157,64 @@ DESCRIPTION = """0. ROI vs. Zero (run_roi_second_level_analysis)
|
|||||||
|
|
||||||
|
|
||||||
class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
|
class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group, json_location):
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
haemo_dict: dict[str | Path, BaseRaw],
|
||||||
|
cha_dict: dict[str, DataFrame],
|
||||||
|
df_ind_dict: dict[str, DataFrame],
|
||||||
|
design_matrix_dict: dict[str, DataFrame],
|
||||||
|
contrast_results_dict: dict[str, dict[str, Any]],
|
||||||
|
group_dict: dict[str, str],
|
||||||
|
json_location: str | Path
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__("InterGroupStats")
|
super().__init__("InterGroupStats")
|
||||||
self.setWindowTitle(f"Inter-Group Stats Viewer - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Inter-Group Stats Viewer - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
self.cha = cha
|
self.cha_dict = cha_dict
|
||||||
self.df_ind = df_ind
|
self.df_ind_dict = df_ind_dict
|
||||||
self.design_matrix = design_matrix
|
self.design_matrix_dict = design_matrix_dict
|
||||||
self.contrast_results = contrast_results
|
self.contrast_results_dict = contrast_results_dict
|
||||||
self.group = group
|
self.group_dict = group_dict
|
||||||
self.json_location = json_location
|
self.json_location = json_location
|
||||||
|
|
||||||
self.setup_inter_group_ui(["0 (ROI vs. Zero)", "1 (Paired ROI Contrast)", "2 (Joint Contrast, ROI-Aggregated)"], placeholder_text=DESCRIPTION)
|
self.setup_inter_group_ui(["0 (ROI vs. Zero)", "1 (Paired ROI Contrast)", "2 (Joint Contrast, ROI-Aggregated)"], placeholder_text=DESCRIPTION)
|
||||||
|
|
||||||
|
|
||||||
def process_request(self):
|
def process_request(self):
|
||||||
request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.json_location, self.contrast_results)
|
request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.json_location, self.contrast_results_dict)
|
||||||
if request is None:
|
if request is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
|
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
|
||||||
|
|
||||||
all_cha = pd.DataFrame()
|
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
|
||||||
|
|
||||||
|
all_cha = DataFrame()
|
||||||
for file_path in selected_file_paths:
|
for file_path in selected_file_paths:
|
||||||
haemo_obj = self.haemo_dict.get(file_path)
|
haemo_obj = self.haemo_dict.get(file_path)
|
||||||
|
|
||||||
if selected_event:
|
|
||||||
participant_events = set(haemo_obj.annotations.description)
|
|
||||||
if selected_event not in participant_events:
|
|
||||||
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if haemo_obj is None:
|
if haemo_obj is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cha_df = self.cha.get(file_path)
|
if selected_event:
|
||||||
|
raw_annotations = getattr(haemo_obj, "annotations", None)
|
||||||
|
|
||||||
|
if raw_annotations is not None:
|
||||||
|
annotations = cast(Annotations, raw_annotations)
|
||||||
|
descriptions = cast(list[str], list(annotations.description))
|
||||||
|
participant_events: set[str] = set(descriptions)
|
||||||
|
else:
|
||||||
|
participant_events: set[str] = set()
|
||||||
|
|
||||||
|
if selected_event not in participant_events:
|
||||||
|
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cha_df = self.cha_dict.get(file_path)
|
||||||
if cha_df is not None:
|
if cha_df is not None:
|
||||||
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
|
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
|
||||||
|
|
||||||
@@ -189,10 +222,10 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
|
|||||||
p_haemo = self.haemo_dict.get(file_path)
|
p_haemo = self.haemo_dict.get(file_path)
|
||||||
|
|
||||||
# Concatenate individual ROI stats (df_ind) for all chosen subjects
|
# Concatenate individual ROI stats (df_ind) for all chosen subjects
|
||||||
df_group = pd.DataFrame()
|
df_group = DataFrame()
|
||||||
if selected_file_paths:
|
if selected_file_paths:
|
||||||
for file_path in selected_file_paths:
|
for file_path in selected_file_paths:
|
||||||
df = self.df_ind.get(file_path)
|
df = self.df_ind_dict.get(file_path)
|
||||||
if df is not None:
|
if df is not None:
|
||||||
df_group = pd.concat([df_group, df], ignore_index=True)
|
df_group = pd.concat([df_group, df], ignore_index=True)
|
||||||
|
|
||||||
@@ -226,7 +259,7 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
|
|||||||
print(f"No ROI data matches the condition '{selected_event}'.")
|
print(f"No ROI data matches the condition '{selected_event}'.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
all_cha_filtered = pd.DataFrame()
|
all_cha_filtered = DataFrame()
|
||||||
if not all_cha.empty:
|
if not all_cha.empty:
|
||||||
if selected_event and 'Condition' in all_cha.columns:
|
if selected_event and 'Condition' in all_cha.columns:
|
||||||
all_cha_filtered = all_cha[all_cha['Condition'] == selected_event]
|
all_cha_filtered = all_cha[all_cha['Condition'] == selected_event]
|
||||||
@@ -304,9 +337,9 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
all_contrasts = []
|
all_contrasts: list[DataFrame] = []
|
||||||
for fp in selected_file_paths:
|
for fp in selected_file_paths:
|
||||||
condition_dfs = self.contrast_results.get(fp)
|
condition_dfs = self.contrast_results_dict.get(fp)
|
||||||
if condition_dfs is None:
|
if condition_dfs is None:
|
||||||
print(f" [MISSING] '{fp}' not found in contrast_results.")
|
print(f" [MISSING] '{fp}' not found in contrast_results.")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
"""
|
"""
|
||||||
Filename: participantbrain.py
|
Filename: participantbrain.py
|
||||||
Description: Logic for the Participant Brain analysis window
|
Description: Logic for the Participant Brain analysis window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
|
from mne import Annotations
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from mne.io.base import BaseRaw
|
||||||
|
|
||||||
from flares import brain_3d_visualization, brain_landmarks_3d
|
from flares import brain_3d_visualization, brain_landmarks_3d
|
||||||
from src.shared.flaresbasewidget import ParticipantUIMixin, FlaresBaseWidget
|
from src.shared.flaresbasewidget import ParticipantUIMixin, FlaresBaseWidget
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
PARAMETERIZED_INDEXES = {
|
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
|
||||||
0: [
|
0: [
|
||||||
{
|
{
|
||||||
"key": "show_optodes",
|
"key": "show_optodes",
|
||||||
@@ -57,7 +67,12 @@ PARAMETERIZED_INDEXES = {
|
|||||||
|
|
||||||
|
|
||||||
class ParticipantBrainViewerWidget(ParticipantUIMixin, FlaresBaseWidget):
|
class ParticipantBrainViewerWidget(ParticipantUIMixin, FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, cha_dict):
|
def __init__(
|
||||||
|
self,
|
||||||
|
haemo_dict: dict[str | Path, BaseRaw],
|
||||||
|
cha_dict: dict[str, DataFrame],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__("ParticipantBrain")
|
super().__init__("ParticipantBrain")
|
||||||
self.setWindowTitle(f"Participant Brain Viewer - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Participant Brain Viewer - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
@@ -72,21 +87,31 @@ class ParticipantBrainViewerWidget(ParticipantUIMixin, FlaresBaseWidget):
|
|||||||
if request is None:
|
if request is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
|
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
|
||||||
|
|
||||||
|
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
|
||||||
|
|
||||||
# Pass the necessary arguments to each method
|
# Pass the necessary arguments to each method
|
||||||
for file_path in selected_file_paths:
|
for file_path in selected_file_paths:
|
||||||
haemo_obj = self.haemo_dict.get(file_path)
|
haemo_obj = self.haemo_dict.get(file_path)
|
||||||
|
|
||||||
|
if haemo_obj is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if selected_event:
|
if selected_event:
|
||||||
participant_events = set(haemo_obj.annotations.description)
|
raw_annotations = getattr(haemo_obj, "annotations", None)
|
||||||
|
|
||||||
|
if raw_annotations is not None:
|
||||||
|
annotations = cast(Annotations, raw_annotations)
|
||||||
|
descriptions = cast(list[str], list(annotations.description))
|
||||||
|
participant_events: set[str] = set(descriptions)
|
||||||
|
else:
|
||||||
|
participant_events: set[str] = set()
|
||||||
|
|
||||||
if selected_event not in participant_events:
|
if selected_event not in participant_events:
|
||||||
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
|
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if haemo_obj is None:
|
|
||||||
raise Exception("How did we get here?")
|
|
||||||
|
|
||||||
cha = self.cha_dict.get(file_path)
|
cha = self.cha_dict.get(file_path)
|
||||||
|
|
||||||
for idx in selected_indexes:
|
for idx in selected_indexes:
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def single_participant_worker(file_path, raw_data, result_queue, progress_queue)
|
|||||||
try:
|
try:
|
||||||
from flares import fold_channels
|
from flares import fold_channels
|
||||||
# Perform the heavy fold_channels logic
|
# Perform the heavy fold_channels logic
|
||||||
channel_results = fold_channels(raw_data, p_name, progress_queue)
|
channel_results = fold_channels(raw=raw_data, p_name=p_name, progress_queue=progress_queue)
|
||||||
|
|
||||||
# Hand back results and signal completion
|
# Hand back results and signal completion
|
||||||
result_queue.put({file_path: channel_results})
|
result_queue.put({file_path: channel_results})
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
"""
|
"""
|
||||||
Filename: participantfunctionalconnectivity.py
|
Filename: participantfunctionalconnectivity.py
|
||||||
Description: Logic for the Participant Functional Connectivity analysis window
|
Description: Logic for the Participant Functional Connectivity analysis window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Built-in Imports
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
from PySide6.QtWidgets import QMessageBox
|
from PySide6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from mne import Annotations
|
||||||
|
from mne.io.base import BaseRaw
|
||||||
|
|
||||||
from flares import functional_connectivity_betas, functional_connectivity_envelope, functional_connectivity_spectral_epochs, functional_connectivity_spectral_time
|
from flares import functional_connectivity_betas, functional_connectivity_envelope, functional_connectivity_spectral_epochs, functional_connectivity_spectral_time
|
||||||
from src.shared.flaresbasewidget import ParticipantUIMixin, FlaresBaseWidget
|
from src.shared.flaresbasewidget import ParticipantUIMixin, FlaresBaseWidget
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|
||||||
PARAMETERIZED_INDEXES = {
|
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
|
||||||
0: [
|
0: [
|
||||||
{
|
{
|
||||||
"key": "n_lines",
|
"key": "n_lines",
|
||||||
@@ -79,7 +89,12 @@ PARAMETERIZED_INDEXES = {
|
|||||||
|
|
||||||
|
|
||||||
class ParticipantFunctionalConnectivityWidget(ParticipantUIMixin, FlaresBaseWidget):
|
class ParticipantFunctionalConnectivityWidget(ParticipantUIMixin, FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, epochs_dict):
|
def __init__(
|
||||||
|
self,
|
||||||
|
haemo_dict: dict[str | Path, BaseRaw],
|
||||||
|
epochs_dict: dict[str, DataFrame],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__("ParticipantFunctionalConnectivity")
|
super().__init__("ParticipantFunctionalConnectivity")
|
||||||
self.setWindowTitle(f"Participant Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Participant Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
@@ -97,22 +112,32 @@ class ParticipantFunctionalConnectivityWidget(ParticipantUIMixin, FlaresBaseWidg
|
|||||||
if request is None:
|
if request is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
|
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
|
||||||
|
|
||||||
|
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
|
||||||
|
|
||||||
# Pass the necessary arguments to each method
|
# Pass the necessary arguments to each method
|
||||||
for file_path in selected_file_paths:
|
for file_path in selected_file_paths:
|
||||||
haemo_obj = self.haemo_dict.get(file_path)
|
haemo_obj = self.haemo_dict.get(file_path)
|
||||||
epochs_obj = self.epochs_dict.get(file_path)
|
epochs_obj = self.epochs_dict.get(file_path)
|
||||||
|
|
||||||
|
if haemo_obj is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if selected_event:
|
if selected_event:
|
||||||
participant_events = set(haemo_obj.annotations.description)
|
raw_annotations = getattr(haemo_obj, "annotations", None)
|
||||||
|
|
||||||
|
if raw_annotations is not None:
|
||||||
|
annotations = cast(Annotations, raw_annotations)
|
||||||
|
descriptions = cast(list[str], list(annotations.description))
|
||||||
|
participant_events: set[str] = set(descriptions)
|
||||||
|
else:
|
||||||
|
participant_events: set[str] = set()
|
||||||
|
|
||||||
if selected_event not in participant_events:
|
if selected_event not in participant_events:
|
||||||
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
|
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if haemo_obj is None:
|
|
||||||
raise Exception("How did we get here?")
|
|
||||||
|
|
||||||
|
|
||||||
for idx in selected_indexes:
|
for idx in selected_indexes:
|
||||||
if idx == 0:
|
if idx == 0:
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
Filename: participantimage.py
|
Filename: participantimage.py
|
||||||
Description: Logic for the Participant Image analysis window
|
Description: Logic for the Participant Image analysis window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Built-in Imports
|
# Built-in Imports
|
||||||
import os
|
import os.path as op
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
|
from mne.io.base import BaseRaw
|
||||||
|
|
||||||
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QMessageBox, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
|
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QMessageBox, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
|
||||||
from PySide6.QtCore import Qt, QSize
|
from PySide6.QtCore import Qt, QSize
|
||||||
from PySide6.QtGui import QPixmap
|
from PySide6.QtGui import QPixmap
|
||||||
@@ -21,7 +24,13 @@ from src.shared.shareddata import APP_NAME
|
|||||||
|
|
||||||
|
|
||||||
class ParticipantImageViewerWidget(FlaresBaseWidget):
|
class ParticipantImageViewerWidget(FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, fig_bytes_dict):
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
haemo_dict: dict[str, BaseRaw],
|
||||||
|
fig_bytes_dict: dict[str, dict[str, bytes]]
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__("ParticipantImage")
|
super().__init__("ParticipantImage")
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||||
self.setWindowTitle(f"Participant Image Viewer - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Participant Image Viewer - {APP_NAME.upper()}")
|
||||||
@@ -29,12 +38,12 @@ class ParticipantImageViewerWidget(FlaresBaseWidget):
|
|||||||
self.fig_bytes_dict = fig_bytes_dict
|
self.fig_bytes_dict = fig_bytes_dict
|
||||||
|
|
||||||
# Create mappings: file_path -> participant label and dropdown display text
|
# Create mappings: file_path -> participant label and dropdown display text
|
||||||
self.participant_map = {} # file_path -> "Participant 1"
|
self.participant_map: dict[str, str] = {}
|
||||||
self.participant_dropdown_items = [] # "Participant 1 (filename)"
|
self.participant_dropdown_items: list[str] = []
|
||||||
|
|
||||||
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
|
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
|
||||||
short_label = f"Participant {i}"
|
short_label = f"Participant {i}"
|
||||||
display_label = f"{short_label} ({os.path.basename(file_path)})"
|
display_label = f"{short_label} ({op.basename(file_path)})"
|
||||||
self.participant_map[file_path] = short_label
|
self.participant_map[file_path] = short_label
|
||||||
self.participant_dropdown_items.append(display_label)
|
self.participant_dropdown_items.append(display_label)
|
||||||
|
|
||||||
@@ -87,23 +96,24 @@ class ParticipantImageViewerWidget(FlaresBaseWidget):
|
|||||||
|
|
||||||
selected_display_names = self._get_checked_items(self.participant_dropdown)
|
selected_display_names = self._get_checked_items(self.participant_dropdown)
|
||||||
# Map from display names back to file paths
|
# Map from display names back to file paths
|
||||||
selected_file_paths = []
|
selected_file_paths: list[str] = []
|
||||||
for display_name in selected_display_names:
|
for display_name in selected_display_names:
|
||||||
# Find file_path by matching display name
|
# Find file_path by matching display name
|
||||||
for fp, short_label in self.participant_map.items():
|
for fp, short_label in self.participant_map.items():
|
||||||
expected_display = f"{short_label} ({os.path.basename(fp)})"
|
expected_display = f"{short_label} ({Path(fp).name})"
|
||||||
if display_name == expected_display:
|
if display_name == expected_display:
|
||||||
selected_file_paths.append(fp)
|
selected_file_paths.append(str(fp))
|
||||||
break
|
break
|
||||||
|
|
||||||
selected_labels = self._get_checked_items(self.image_index_dropdown)
|
selected_labels = self._get_checked_items(self.image_index_dropdown)
|
||||||
|
|
||||||
row, col = 0, 0
|
row, col = 0, 0
|
||||||
for file_path in selected_file_paths:
|
for file_path in selected_file_paths:
|
||||||
fig_list = self.fig_bytes_dict.get(file_path, [])
|
fig_map: dict[str, bytes] = self.fig_bytes_dict.get(file_path, {})
|
||||||
participant_label = self.participant_map[file_path]
|
participant_label: str = self.participant_map.get(file_path, "Unknown")
|
||||||
|
|
||||||
for label in selected_labels:
|
for label in selected_labels:
|
||||||
fig_bytes = fig_list.get(label)
|
fig_bytes: bytes | None = fig_map.get(label)
|
||||||
if not fig_bytes:
|
if not fig_bytes:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -149,7 +159,7 @@ class ParticipantImageViewerWidget(FlaresBaseWidget):
|
|||||||
for display_name in selected_display_names:
|
for display_name in selected_display_names:
|
||||||
# Match display name to file path
|
# Match display name to file path
|
||||||
for file_path, short_label in self.participant_map.items():
|
for file_path, short_label in self.participant_map.items():
|
||||||
expected_display = f"{short_label} ({os.path.basename(file_path)})"
|
expected_display = f"{short_label} ({op.basename(file_path)})"
|
||||||
if display_name == expected_display:
|
if display_name == expected_display:
|
||||||
fig_dict = self.fig_bytes_dict.get(file_path, {})
|
fig_dict = self.fig_bytes_dict.get(file_path, {})
|
||||||
for label in selected_image_labels:
|
for label in selected_image_labels:
|
||||||
@@ -157,7 +167,7 @@ class ParticipantImageViewerWidget(FlaresBaseWidget):
|
|||||||
continue
|
continue
|
||||||
fig_bytes = fig_dict[label]
|
fig_bytes = fig_dict[label]
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
filename = f"{os.path.basename(file_path)}_{label}_{timestamp}.png"
|
filename = f"{op.basename(file_path)}_{label}_{timestamp}.png"
|
||||||
output_path = save_dir / filename
|
output_path = save_dir / filename
|
||||||
with open(output_path, "wb") as f:
|
with open(output_path, "wb") as f:
|
||||||
f.write(fig_bytes)
|
f.write(fig_bytes)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ License: GPL-3.0
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Sequence, Any
|
||||||
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QScrollArea, QVBoxLayout, QWidget, QFrame, QSpinBox
|
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QScrollArea, QVBoxLayout, QWidget, QFrame, QSpinBox
|
||||||
from PySide6.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIntValidator, QDoubleValidator
|
from PySide6.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIntValidator, QDoubleValidator
|
||||||
from PySide6.QtCore import QEvent, QSize, Qt
|
from PySide6.QtCore import QEvent, QSize, Qt
|
||||||
@@ -807,7 +809,11 @@ class FlaresBaseWidget(QWidget):
|
|||||||
self.image_index_dropdown = None
|
self.image_index_dropdown = None
|
||||||
|
|
||||||
|
|
||||||
def _create_multiselect_dropdown(self, items):
|
def _create_multiselect_dropdown(
|
||||||
|
self,
|
||||||
|
items: Sequence[str]
|
||||||
|
) -> FullClickComboBox:
|
||||||
|
|
||||||
combo = FullClickComboBox()
|
combo = FullClickComboBox()
|
||||||
combo.setView(QListView())
|
combo.setView(QListView())
|
||||||
model = QStandardItemModel()
|
model = QStandardItemModel()
|
||||||
@@ -874,7 +880,11 @@ class FlaresBaseWidget(QWidget):
|
|||||||
# checked.append(item.text())
|
# checked.append(item.text())
|
||||||
# return checked
|
# return checked
|
||||||
|
|
||||||
def _get_checked_items(self, combo=None):
|
def _get_checked_items(
|
||||||
|
self,
|
||||||
|
combo: QComboBox | None = None
|
||||||
|
) -> list[str]:
|
||||||
|
|
||||||
target = combo if combo is not None else getattr(self, 'participant_dropdown', None)
|
target = combo if combo is not None else getattr(self, 'participant_dropdown', None)
|
||||||
|
|
||||||
if target is None or target.model() is None:
|
if target is None or target.model() is None:
|
||||||
@@ -897,7 +907,10 @@ class FlaresBaseWidget(QWidget):
|
|||||||
return checked_items
|
return checked_items
|
||||||
|
|
||||||
|
|
||||||
def update_participant_dropdown_label(self, combo=None):
|
def update_participant_dropdown_label(
|
||||||
|
self,
|
||||||
|
combo: QComboBox | int | None = None
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Handles label updates for ANY participant dropdown.
|
Handles label updates for ANY participant dropdown.
|
||||||
If 'combo' is None, it defaults to the standard self.participant_dropdown.
|
If 'combo' is None, it defaults to the standard self.participant_dropdown.
|
||||||
@@ -1142,7 +1155,13 @@ class FlaresBaseWidget(QWidget):
|
|||||||
|
|
||||||
class CrossGroupUIMixin:
|
class CrossGroupUIMixin:
|
||||||
|
|
||||||
def setup_cross_group_ui(self, index_texts, placeholder_text=""):
|
participant_map: dict[str, str]
|
||||||
|
|
||||||
|
def setup_cross_group_ui(
|
||||||
|
self,
|
||||||
|
index_texts: Sequence[str],
|
||||||
|
placeholder_text: str = ""
|
||||||
|
) -> None:
|
||||||
|
|
||||||
self.group_to_paths = {}
|
self.group_to_paths = {}
|
||||||
for file_path, group_name in self.group_dict.items():
|
for file_path, group_name in self.group_dict.items():
|
||||||
@@ -1293,7 +1312,13 @@ class CrossGroupUIMixin:
|
|||||||
|
|
||||||
return file_paths
|
return file_paths
|
||||||
|
|
||||||
def get_common_request_data(self, parameterized_indexes, json_location=None, contrast_dfs=None):
|
def get_common_request_data(
|
||||||
|
self,
|
||||||
|
parameterized_indexes: dict[int, list[dict[str, Any]]],
|
||||||
|
json_location: str | Path | None = None,
|
||||||
|
contrast_dfs: dict[str, dict[str, Any]] | None = None,
|
||||||
|
) -> tuple[str | None, list[str], list[str], list[str], list[int], dict[str, Any]] | None:
|
||||||
|
|
||||||
selected_event = self.event_dropdown.currentText()
|
selected_event = self.event_dropdown.currentText()
|
||||||
if selected_event == "<None Selected>":
|
if selected_event == "<None Selected>":
|
||||||
selected_event = None
|
selected_event = None
|
||||||
@@ -1412,11 +1437,14 @@ class CrossGroupUIMixin:
|
|||||||
|
|
||||||
class CSVUIMixin:
|
class CSVUIMixin:
|
||||||
|
|
||||||
def setup_csv_ui(self, index_texts):
|
def setup_csv_ui(
|
||||||
|
self,
|
||||||
|
index_texts: Sequence[str]
|
||||||
|
) -> None:
|
||||||
|
|
||||||
# Create mappings: file_path -> participant label and dropdown display text
|
# Create mappings: file_path -> participant label and dropdown display text
|
||||||
self.participant_map = {} # file_path -> "Participant 1"
|
self.participant_map: dict[str, str] = {} # file_path -> "Participant 1"
|
||||||
self.participant_dropdown_items = [] # "Participant 1 (filename)"
|
self.participant_dropdown_items: list[str] = [] # "Participant 1 (filename)"
|
||||||
|
|
||||||
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
|
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
|
||||||
short_label = f"Participant {i}"
|
short_label = f"Participant {i}"
|
||||||
@@ -1428,12 +1456,12 @@ class CSVUIMixin:
|
|||||||
self.top_bar = QHBoxLayout()
|
self.top_bar = QHBoxLayout()
|
||||||
self.layout.addLayout(self.top_bar)
|
self.layout.addLayout(self.top_bar)
|
||||||
|
|
||||||
self.participant_dropdown = self._create_multiselect_dropdown(self.participant_dropdown_items)
|
self.participant_dropdown: FullClickComboBox = self._create_multiselect_dropdown(self.participant_dropdown_items)
|
||||||
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
|
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
|
||||||
|
|
||||||
self.index_texts = index_texts
|
self.index_texts = index_texts
|
||||||
|
|
||||||
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
|
self.image_index_dropdown: FullClickComboBox = self._create_multiselect_dropdown(self.index_texts)
|
||||||
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
|
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
|
||||||
|
|
||||||
self.submit_button = QPushButton("Submit")
|
self.submit_button = QPushButton("Submit")
|
||||||
@@ -1456,13 +1484,20 @@ class CSVUIMixin:
|
|||||||
self.showMaximized()
|
self.showMaximized()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InterGroupUIMixin:
|
class InterGroupUIMixin:
|
||||||
def setup_inter_group_ui(self, index_texts, placeholder_text=""):
|
|
||||||
|
def setup_inter_group_ui(
|
||||||
|
self,
|
||||||
|
index_texts: Sequence[str],
|
||||||
|
placeholder_text: str = ""
|
||||||
|
) -> None:
|
||||||
|
|
||||||
self.show_all_events = True
|
self.show_all_events = True
|
||||||
self._updating_checkstates = False
|
self._updating_checkstates = False
|
||||||
|
|
||||||
# Create mappings: file_path -> participant label and dropdown display text
|
# Create mappings: file_path -> participant label and dropdown display text
|
||||||
self.participant_map = {} # file_path -> "Participant 1"
|
self.participant_map: dict[str, str] = {} # file_path -> "Participant 1"
|
||||||
self.participant_dropdown_items = [] # "Participant 1 (filename)"
|
self.participant_dropdown_items = [] # "Participant 1 (filename)"
|
||||||
|
|
||||||
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
|
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
|
||||||
@@ -1525,7 +1560,13 @@ class InterGroupUIMixin:
|
|||||||
self.thumb_size = QSize(280, 180)
|
self.thumb_size = QSize(280, 180)
|
||||||
self.showMaximized()
|
self.showMaximized()
|
||||||
|
|
||||||
def get_common_request_data(self, parameterized_indexes, json_location=None, contrast_dfs=None):
|
def get_common_request_data(
|
||||||
|
self,
|
||||||
|
parameterized_indexes: dict[int, list[dict[str, Any]]],
|
||||||
|
json_location: str | Path | None = None,
|
||||||
|
contrast_dfs: dict[str, dict[str, Any]] | None = None,
|
||||||
|
) -> tuple[str | None, list[str], list[int], dict[str, Any]] | None:
|
||||||
|
|
||||||
selected_event = self.event_dropdown.currentText()
|
selected_event = self.event_dropdown.currentText()
|
||||||
if selected_event == "<None Selected>":
|
if selected_event == "<None Selected>":
|
||||||
selected_event = None
|
selected_event = None
|
||||||
@@ -1570,7 +1611,7 @@ class InterGroupUIMixin:
|
|||||||
dynamic_rois = []
|
dynamic_rois = []
|
||||||
|
|
||||||
# 1. Check for the JSON file and parse ROI names
|
# 1. Check for the JSON file and parse ROI names
|
||||||
if os.path.exists(json_location):
|
if json_location is not None and os.path.exists(json_location):
|
||||||
try:
|
try:
|
||||||
with open(json_location, 'r', encoding='utf-8') as f:
|
with open(json_location, 'r', encoding='utf-8') as f:
|
||||||
regions_data = json.load(f)
|
regions_data = json.load(f)
|
||||||
@@ -1645,9 +1686,13 @@ class InterGroupUIMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
class ParticipantUIMixin:
|
class ParticipantUIMixin:
|
||||||
def setup_participant_ui(self, index_texts):
|
def setup_participant_ui(
|
||||||
|
self,
|
||||||
|
index_texts: Sequence[str]
|
||||||
|
) -> None:
|
||||||
|
|
||||||
# Create mappings: file_path -> participant label and dropdown display text
|
# Create mappings: file_path -> participant label and dropdown display text
|
||||||
self.participant_map = {} # file_path -> "Participant 1"
|
self.participant_map: dict[str, str] = {} # file_path -> "Participant 1"
|
||||||
self.participant_dropdown_items = [] # "Participant 1 (filename)"
|
self.participant_dropdown_items = [] # "Participant 1 (filename)"
|
||||||
|
|
||||||
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
|
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
|
||||||
@@ -1694,7 +1739,11 @@ class ParticipantUIMixin:
|
|||||||
self.showMaximized()
|
self.showMaximized()
|
||||||
|
|
||||||
|
|
||||||
def get_common_request_data(self, parameterized_indexes):
|
def get_common_request_data(
|
||||||
|
self,
|
||||||
|
parameterized_indexes: dict[int, list[dict[str, Any]]]
|
||||||
|
) -> tuple[str | None, list[str], list[int], dict[str, Any]] | None:
|
||||||
|
|
||||||
selected_event = self.event_dropdown.currentText()
|
selected_event = self.event_dropdown.currentText()
|
||||||
if selected_event == "<None Selected>":
|
if selected_event == "<None Selected>":
|
||||||
selected_event = None
|
selected_event = None
|
||||||
|
|||||||
+12
-11
@@ -1,22 +1,27 @@
|
|||||||
"""
|
"""
|
||||||
Filename: shareddata.py
|
Filename: shareddata.py
|
||||||
Description: Shared constants and methods for FLARES
|
Description: Shared constants and methods other files depend on
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
# Built-in imports
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
|
|
||||||
CURRENT_VERSION = "1.5.0"
|
CURRENT_VERSION = "1.5.0"
|
||||||
APP_NAME = "flares"
|
APP_NAME = "flares"
|
||||||
|
APP_NAME_EXPANDED = "fNIRS Lightweight Analysis, Research, & Evaluation Suite"
|
||||||
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
||||||
API_URL_SECONDARY = f"https://git.research2.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
API_URL_SECONDARY = f"https://git.research2.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
|
||||||
PLATFORM_NAME = platform.system().lower()
|
PLATFORM_NAME = platform.system().lower()
|
||||||
CHANGELOG_URL = "https://git.research.dezeeuw.ca/tyler/flares/raw/branch/main/changelog_major.md"
|
CHANGELOG_URL = f"https://git.research.dezeeuw.ca/tyler/{APP_NAME}/raw/branch/main/changelog_major.md"
|
||||||
WIKI_URL = "https://git.research.dezeeuw.ca/tyler/flares/wiki"
|
WIKI_URL = f"https://git.research.dezeeuw.ca/tyler/{APP_NAME}/wiki"
|
||||||
|
|
||||||
|
|
||||||
PIPELINE_STAGES = [
|
PIPELINE_STAGES = [
|
||||||
"Preprocessing",
|
"Preprocessing",
|
||||||
@@ -49,15 +54,11 @@ PIPELINE_STAGES = [
|
|||||||
"Finishing Up"
|
"Finishing Up"
|
||||||
]
|
]
|
||||||
|
|
||||||
def resource_path(relative_path):
|
|
||||||
|
def resource_path(relative_path: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get absolute path to resource regardless of running directly or packaged using PyInstaller
|
Get absolute path to resource regardless of running directly or packaged using PyInstaller
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if hasattr(sys, '_MEIPASS'):
|
base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
|
||||||
# PyInstaller bundle path
|
|
||||||
base_path = sys._MEIPASS
|
|
||||||
else:
|
|
||||||
base_path = os.path.abspath(".")
|
|
||||||
|
|
||||||
return os.path.join(base_path, relative_path)
|
return os.path.join(base_path, relative_path)
|
||||||
+5
-4
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Filename: about.py
|
Filename: about.py
|
||||||
Description: About window for FLARES
|
Description: About window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
@@ -9,7 +10,7 @@ License: GPL-3.0
|
|||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from src.shared.shareddata import APP_NAME, CURRENT_VERSION
|
from src.shared.shareddata import APP_NAME, APP_NAME_EXPANDED, CURRENT_VERSION
|
||||||
|
|
||||||
class AboutWindow(QWidget):
|
class AboutWindow(QWidget):
|
||||||
"""
|
"""
|
||||||
@@ -19,14 +20,14 @@ class AboutWindow(QWidget):
|
|||||||
parent (QWidget, optional): Parent widget of this window. Defaults to None.
|
parent (QWidget, optional): Parent widget of this window. Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent: QWidget | None = None) -> None:
|
||||||
super().__init__(parent, Qt.WindowType.Window)
|
super().__init__(parent, Qt.WindowType.Window)
|
||||||
self.setWindowTitle(f"About {APP_NAME.upper()}")
|
self.setWindowTitle(f"About {APP_NAME.upper()}")
|
||||||
self.resize(250, 100)
|
self.resize(250, 100)
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
label = QLabel(f"About {APP_NAME.upper()}", self)
|
label = QLabel(f"About {APP_NAME.upper()}", self)
|
||||||
label2 = QLabel("fNIRS Lightweight Analysis, Research, & Evaluation Suite", self)
|
label2 = QLabel(f"{APP_NAME_EXPANDED}", self)
|
||||||
label3 = QLabel(f"{APP_NAME.upper()} is licensed under the GPL-3.0 licence. For more information, visit https://www.gnu.org/licenses/gpl-3.0.en.html", self)
|
label3 = QLabel(f"{APP_NAME.upper()} is licensed under the GPL-3.0 licence. For more information, visit https://www.gnu.org/licenses/gpl-3.0.en.html", self)
|
||||||
label4 = QLabel(f"Version v{CURRENT_VERSION}")
|
label4 = QLabel(f"Version v{CURRENT_VERSION}")
|
||||||
|
|
||||||
|
|||||||
+13
-10
@@ -1,21 +1,24 @@
|
|||||||
"""
|
"""
|
||||||
Filename: terminal.py
|
Filename: terminal.py
|
||||||
Description: Terminal window for FLARES
|
Description: Terminal window
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLineEdit
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLineEdit
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from src.shared.shareddata import API_URL, API_URL_SECONDARY, APP_NAME, CURRENT_VERSION, PLATFORM_NAME
|
from src.shared.shareddata import API_URL, API_URL_SECONDARY, APP_NAME, CURRENT_VERSION, PLATFORM_NAME
|
||||||
from src.window.about import AboutWindow
|
from src.window.about import AboutWindow
|
||||||
from updater import LocalPendingUpdateCheckThread, UpdateManager
|
from updater import UpdateManager
|
||||||
|
|
||||||
|
|
||||||
class TerminalWindow(QWidget):
|
class TerminalWindow(QWidget):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent: QWidget | None = None) -> None:
|
||||||
super().__init__(parent, Qt.WindowType.Window)
|
super().__init__(parent, Qt.WindowType.Window)
|
||||||
self.setWindowTitle(f"Terminal - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Terminal - {APP_NAME.upper()}")
|
||||||
self.resize(320, 180)
|
self.resize(320, 180)
|
||||||
@@ -30,7 +33,7 @@ class TerminalWindow(QWidget):
|
|||||||
layout.addWidget(self.input_line)
|
layout.addWidget(self.input_line)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
self.commands = {
|
self.commands: dict[str, Callable[..., Any]] = {
|
||||||
"hello": self.cmd_hello,
|
"hello": self.cmd_hello,
|
||||||
"help": self.cmd_help,
|
"help": self.cmd_help,
|
||||||
"version": self.cmd_version,
|
"version": self.cmd_version,
|
||||||
@@ -68,22 +71,22 @@ class TerminalWindow(QWidget):
|
|||||||
self.output_area.append(f"[Unknown command] '{command_name}'")
|
self.output_area.append(f"[Unknown command] '{command_name}'")
|
||||||
|
|
||||||
|
|
||||||
def cmd_hello(self, *args):
|
def cmd_hello(self, *args: Any) -> str:
|
||||||
return "Hello from the terminal!"
|
return "Hello from the terminal!"
|
||||||
|
|
||||||
def cmd_help(self, *args):
|
def cmd_help(self, *args: Any) -> str:
|
||||||
return f"Available commands: {', '.join(self.commands.keys())}"
|
return f"Available commands: {', '.join(self.commands.keys())}"
|
||||||
|
|
||||||
def cmd_version(self, *args):
|
def cmd_version(self, *args: Any) -> str:
|
||||||
return f"{APP_NAME.upper()} is running version {CURRENT_VERSION}."
|
return f"{APP_NAME.upper()} is running version {CURRENT_VERSION}."
|
||||||
|
|
||||||
def cmd_about(self, *args):
|
def cmd_about(self, *args: Any) -> None:
|
||||||
self.about = AboutWindow(self)
|
self.about = AboutWindow(self)
|
||||||
self.about.show()
|
self.about.show()
|
||||||
|
|
||||||
def cmd_update(self, *args):
|
def cmd_update(self, *args: Any) -> str:
|
||||||
main_win = self.parent()
|
main_win = self.parent()
|
||||||
if main_win is None:
|
if not isinstance(main_win, QWidget):
|
||||||
return "[Error] Main window context not found."
|
return "[Error] Main window context not found."
|
||||||
|
|
||||||
self.updater = UpdateManager(
|
self.updater = UpdateManager(
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import numpy as np
|
|||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QMessageBox, QLineEdit, QPushButton, QFileDialog
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QMessageBox, QLineEdit, QPushButton, QFileDialog
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
|
|
||||||
from mne.io import read_raw_snirf
|
from mne.io import read_raw_snirf #type: ignore
|
||||||
from mne_nirs.io import write_raw_snirf
|
from mne_nirs.io import write_raw_snirf #type: ignore
|
||||||
from mne.channels import make_dig_montage
|
from mne.channels import make_dig_montage #type: ignore
|
||||||
|
|
||||||
from src.shared.shareddata import APP_NAME
|
from src.shared.shareddata import APP_NAME
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class UserGuideWindow(QWidget):
|
|||||||
parent (QWidget, optional): Parent widget of this window. Defaults to None.
|
parent (QWidget, optional): Parent widget of this window. Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent: QWidget | None = None) -> None:
|
||||||
super().__init__(parent, Qt.WindowType.Window)
|
super().__init__(parent, Qt.WindowType.Window)
|
||||||
self.setWindowTitle(f"User Guide - {APP_NAME.upper()}")
|
self.setWindowTitle(f"User Guide - {APP_NAME.upper()}")
|
||||||
self.resize(250, 100)
|
self.resize(250, 100)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ViewerLauncherWidget(QWidget):
|
|||||||
("Cross-Group Stats Viewer", CrossGroupStatsWidget, [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, 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),
|
("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),
|
("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)
|
("Export To CSV Viewer", ExportToCSVWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True)
|
||||||
]
|
]
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Filename: welcome.py
|
Filename: welcome.py
|
||||||
Description: Welcome dialog for FLARES
|
Description: Welcome dialog for FLARES
|
||||||
|
Note: Compliant with pylance strict type checking
|
||||||
|
|
||||||
Author: Tyler de Zeeuw
|
Author: Tyler de Zeeuw
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
@@ -9,13 +10,13 @@ License: GPL-3.0
|
|||||||
from PySide6.QtWidgets import QTextBrowser, QVBoxLayout, QLabel, QDialog, QHBoxLayout, QPushButton
|
from PySide6.QtWidgets import QTextBrowser, QVBoxLayout, QLabel, QDialog, QHBoxLayout, QPushButton
|
||||||
from PySide6.QtGui import QDesktopServices, QIcon
|
from PySide6.QtGui import QDesktopServices, QIcon
|
||||||
from PySide6.QtCore import QUrl
|
from PySide6.QtCore import QUrl
|
||||||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||||
|
|
||||||
from src.shared.shareddata import APP_NAME, CURRENT_VERSION, CHANGELOG_URL, resource_path
|
from src.shared.shareddata import APP_NAME, CURRENT_VERSION, CHANGELOG_URL, resource_path
|
||||||
|
|
||||||
|
|
||||||
class WelcomeDialog(QDialog):
|
class WelcomeDialog(QDialog):
|
||||||
def __init__(self, parent=None, direct=True, first=False):
|
def __init__(self, parent: QDialog | None = None, direct: bool = True, first: bool = False):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle(f"What's New - {APP_NAME.upper()}")
|
self.setWindowTitle(f"What's New - {APP_NAME.upper()}")
|
||||||
self.setMinimumSize(550, 450)
|
self.setMinimumSize(550, 450)
|
||||||
@@ -64,10 +65,10 @@ class WelcomeDialog(QDialog):
|
|||||||
self.network_manager.get(QNetworkRequest(QUrl(CHANGELOG_URL)))
|
self.network_manager.get(QNetworkRequest(QUrl(CHANGELOG_URL)))
|
||||||
|
|
||||||
|
|
||||||
def _on_download_complete(self, reply):
|
def _on_download_complete(self, reply: QNetworkReply) -> None:
|
||||||
"""Processes the downloaded markdown and drops it into the view frame."""
|
"""Processes the downloaded markdown and drops it into the view frame."""
|
||||||
if reply.error() == reply.NetworkError.NoError:
|
if reply.error() == reply.NetworkError.NoError:
|
||||||
raw_bytes = reply.readAll()
|
raw_bytes = reply.readAll().data()
|
||||||
|
|
||||||
# Convert raw bytes to standard text string
|
# Convert raw bytes to standard text string
|
||||||
markdown_text = str(raw_bytes, encoding='utf-8')
|
markdown_text = str(raw_bytes, encoding='utf-8')
|
||||||
|
|||||||
Reference in New Issue
Block a user