new preferences and bug fixes

This commit is contained in:
2026-07-30 14:18:26 -07:00
parent f15a5d9433
commit ebd13927e2
6 changed files with 325 additions and 247 deletions
+15 -84
View File
@@ -182,6 +182,10 @@ IQR: float
WAVELET_TYPE: str
WAVELET_LEVEL: int
OVERRIDE_PPF: bool
PPF_LOWER_WAVELENGTH: float
PPF_UPPER_WAVELENGTH: float
ENHANCE_NEGATIVE_CORRELATION: bool
FILTER: bool
@@ -214,12 +218,10 @@ N_JOBS: int
JSON_LOCATION: str
TIME_WINDOW_START: int
TIME_WINDOW_END: int
MAX_WORKERS: int
VERBOSITY: bool
AGE: int = 25 # Assume 25 if not set from the GUI. This will result in a reasonable PPF
AGE: int = 25 # Assume 25 if not set from the GUI. This will result in a reasonable PPF if calculated dynamically
GENDER: str = ""
GROUP: str = "Default"
@@ -257,8 +259,6 @@ REQUIRED_KEYS: dict[str, Any] = {
"REMOVE_EVENTS": list,
"TIME_WINDOW_START": int,
"TIME_WINDOW_END": int,
"L_FREQ": float,
"H_FREQ": float,
@@ -870,71 +870,6 @@ def calculate_scalp_coupling(data, l_freq: float = 0.7, h_freq: float = 1.5):
def calculate_signal_noise_ratio(data):
"""
Calculates the signal-to-noise ratio (SNR) for each channel and identifies those below a defined threshold.
Parameters
----------
data : BaseRaw
The loaded data object to process.
Returns
-------
tuple[list[str], Figure]
- list[str]: A list of channel names that fall below the SNR threshold and are considered bad.
- Figure: A matplotlib Figure showing the channels' SNR values.
"""
print("Calculating signal to noise ratio...")
# Compute the signal-to-noise ratio values
print("Computing the signal to noise power...")
signal_band=(0.01, 0.5)
noise_band=(1.0, 10.0)
data_signal = data.copy().filter(*signal_band, verbose=False) #type: ignore
data_noise = data.copy().filter(*noise_band, verbose=False) #type: ignore
signal_power = np.mean(data_signal.get_data()**2, axis=1) #type: ignore
noise_power = np.mean(data_noise.get_data()**2, axis=1) #type: ignore
# Calculate the snr using the standard formula for dB
snr = 10 * np.log10(signal_power / (noise_power + np.finfo(float).eps))
# TODO: Understand what this does
groups: dict[str, list[str]] = {}
for ch in getattr(data, "ch_names"):
# Look for the space in the channel names and remove the characters after
# This is so we can get both oxy and deoxy to remove, as they will have the same source and detector
base = ch.rsplit(' ', 1)[0]
groups.setdefault(base, []).append(ch) # type: ignore
# If any of the channels do not meet our threshold, they will get inserted into the bad_channels set
bad_channels: set[str] = set()
for base, ch_list in groups.items():
if any(s < SNR_THRESHOLD for s, ch in zip(snr, getattr(data, "ch_names")) if ch in ch_list):
bad_channels.update(ch_list)
# Design and create the figure
print("Creating the figure...")
snr_fig, ax = plt.subplots(figsize=(12, 4), layout="constrained") # type: ignore
colors = [(0/20, 'red'), (SNR_THRESHOLD/20, 'red'), ((SNR_THRESHOLD+.5)/20, 'yellow'), ((SNR_THRESHOLD+1)/20, 'green'), (20/20, 'green')]
cmap = LinearSegmentedColormap.from_list('custom_snr_cmap', colors)
norm = mcolors.Normalize(vmin=0, vmax=20)
scatter = ax.scatter(range(len(snr)), snr, c=snr, cmap=cmap, alpha=0.8, s=100, norm=norm) # type: ignore
ax.set(xlabel="Channel Number", ylabel="Signal-to-Noise Ratio (dB)", xlim=[0, len(snr)], ylim=[0, 20])
ax.axhline(SNR_THRESHOLD, color='black', linestyle='--', alpha=0.3, linewidth=1) # type: ignore
cbar = snr_fig.colorbar(scatter, ax=ax, label="SNR Thresholds (dB)") # type: ignore
cbar.set_ticks([0, SNR_THRESHOLD, SNR_THRESHOLD+1, 20]) # type: ignore
cbar.set_ticklabels(['0', str(SNR_THRESHOLD), str(SNR_THRESHOLD+1), '20']) # type: ignore
plt.close()
print("Successfully calculated signal to noise ratio.")
return list(bad_channels), snr_fig
def build_fnirs_adjacency(raw, threshold_meters=0.03):
"""Build an adjacency dictionary for fNIRS channels using 3D distance."""
# Extract channel positions
@@ -2468,16 +2403,6 @@ def brain_landmarks_3d(raw_haemo: BaseRaw, show_optodes: Literal['sensors', 'lab
return brain
def verify_channel_positions(data: BaseRaw) -> None:
"""
Visualizes the sensor/channel positions of the raw data for verification.
Parameters
----------
data : BaseRaw
The loaded data object to process.
"""
def convert_fig_dict_to_png_bytes(fig_dict: dict[str, Figure]) -> dict[str, bytes]:
png_dict = {}
for label, fig in fig_dict.items():
@@ -5515,6 +5440,14 @@ def generate_contrast_results(df_design_matrix, glm_est, file_path):
return contrast_results_dict
def haemoglobin_concentration(raw_od, file_path, override_ppf=False, ppf_lower_wavelength=6.0, ppf_upper_wavelength=6.0):
if override_ppf:
raw_haemo = beer_lambert_law(raw_od, ppf=(ppf_lower_wavelength, ppf_upper_wavelength))
else:
raw_haemo = beer_lambert_law(raw_od, ppf=calculate_dpf(file_path))
return raw_haemo
def process_participant(file_path, progress_callback=None):
# Step 0: Setting up
@@ -5525,8 +5458,6 @@ def process_participant(file_path, progress_callback=None):
if k in globals() and k != "REQUIRED_KEYS"
}
print(config_dict)
# Step 1: Preprocessing
raw = load_snirf(file_path)
fig_raw = raw.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="Loaded Raw", show=False)
@@ -5667,7 +5598,7 @@ def process_participant(file_path, progress_callback=None):
logger.info("Step 16 Completed.")
# Step 17: Haemoglobin Concentration
raw_haemo = beer_lambert_law(raw_od, ppf=calculate_dpf(file_path))
raw_haemo = haemoglobin_concentration(raw_od, file_path, OVERRIDE_PPF, PPF_LOWER_WAVELENGTH, PPF_UPPER_WAVELENGTH)
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["Modified Beer Lambert Law"] = fig_raw_haemo_bll
if progress_callback: progress_callback(17)
@@ -5711,7 +5642,7 @@ def process_participant(file_path, progress_callback=None):
if progress_callback: progress_callback(22)
logger.info("Step 22 Completed.")
# Step 23: Run GLM
# Step 23: General Linear Model
glm_est, fig_glm_topo = make_and_run_glm(raw_haemo, df_design_matrix)
fig_individual["GLM Topography"] = fig_glm_topo
if progress_callback: progress_callback(23)