Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Fix several bugs #839

Merged
merged 10 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/v1.6.md.inc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:new: New features & enhancements

- Added [`regress_artifact`][mne_bids_pipeline._config.regress_artifact] to allow artifact regression (e.g., of MEG reference sensors in KIT systems) (#837 by @larsoner)
- Chosen `reject` parameters are now saved in the generated HTML reports (#839 by @larsoner)

[//]: # (### :warning: Behavior changes)

Expand All @@ -17,6 +18,9 @@
### :bug: Bug fixes

- Fix minor issues with path handling for cross-talk and calibration files (#834 by @larsoner)
- Fix bug where EEG `reject` params were not used for `ch_types = ["meg", "eeg"]` (#839 by @larsoner)
- Fix bug where implicit `mf_reference_run` could change across invocations of `mne_bids_pipeline`, breaking caching (#839 by @larsoner)
- Fix bug where `--no-cache` had no effect (#839 by @larsoner)

### :medical_symbol: Code health

Expand Down
2 changes: 1 addition & 1 deletion mne_bids_pipeline/_config_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def _pydantic_validate(
if config_path is not None:
name += f" from {config_path}"
model_config = ConfigDict(
arbitrary_types_allowed=False,
arbitrary_types_allowed=True, # needed in 2.6.0 to allow DigMontage for example
validate_assignment=True,
strict=True, # do not allow float for int for example
extra="forbid",
Expand Down
40 changes: 21 additions & 19 deletions mne_bids_pipeline/_config_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
from .typing import ArbitraryContrast

try:
_keys_arbitrary_contrast = set(ArbitraryContrast.__required_keys__)
_set_keys_arbitrary_contrast = set(ArbitraryContrast.__required_keys__)
except Exception:
_keys_arbitrary_contrast = set(ArbitraryContrast.__annotations__.keys())
_set_keys_arbitrary_contrast = set(ArbitraryContrast.__annotations__.keys())


def get_fs_subjects_dir(config: SimpleNamespace) -> pathlib.Path:
Expand Down Expand Up @@ -96,11 +96,14 @@ def get_subjects(config: SimpleNamespace) -> list[str]:
else:
s = config.subjects

subjects = set(s) - set(config.exclude_subjects)
# Drop empty-room subject.
subjects = subjects - set(["emptyroom"])
# Preserve order and remove excluded subjects
subjects = [
subject
for subject in s
if subject not in config.exclude_subjects and subject != "emptyroom"
]

return sorted(subjects)
return subjects


def get_sessions(config: SimpleNamespace) -> Union[list[None], list[str]]:
Expand Down Expand Up @@ -176,7 +179,17 @@ def _get_runs_all_subjects_cached(
def get_intersect_run(config: SimpleNamespace) -> list[str]:
"""Return the intersection of all the runs of all subjects."""
subj_runs = get_runs_all_subjects(config)
return list(set.intersection(*map(set, subj_runs.values())))
# Do not use something like:
# list(set.intersection(*map(set, subj_runs.values())))
# as it will not preserve order. Instead just be explicit and preserve order.
# We could use "sorted", but it's probably better to use the order provided by
# the user (if they want to put `runs=["02", "01"]` etc. it's better to use "02")
all_runs = list()
for runs in subj_runs.values():
for run in runs:
if run not in all_runs:
all_runs.append(run)
return all_runs


def get_runs(
Expand Down Expand Up @@ -429,17 +442,6 @@ def _restrict_analyze_channels(
return inst


def _get_scalp_in_files(cfg: SimpleNamespace) -> dict[str, pathlib.Path]:
subject_path = pathlib.Path(cfg.subjects_dir) / cfg.fs_subject
seghead = subject_path / "surf" / "lh.seghead"
in_files = dict()
if seghead.is_file():
in_files["seghead"] = seghead
else:
in_files["t1"] = subject_path / "mri" / "T1.mgz"
return in_files


def _get_bem_conductivity(cfg: SimpleNamespace) -> tuple[tuple[float], str]:
if cfg.fs_subject in ("fsaverage", cfg.use_template_mri):
conductivity = None # should never be used
Expand Down Expand Up @@ -573,7 +575,7 @@ def _validate_contrasts(contrasts: SimpleNamespace) -> None:
if len(contrast) != 2:
raise ValueError("Contrasts' tuples MUST be two conditions")
elif isinstance(contrast, dict):
if not _keys_arbitrary_contrast.issubset(set(contrast.keys())):
if not _set_keys_arbitrary_contrast.issubset(set(contrast.keys())):
raise ValueError(f"Missing key(s) in contrast {contrast}")
if len(contrast["conditions"]) != len(contrast["weights"]):
raise ValueError(
Expand Down
1 change: 0 additions & 1 deletion mne_bids_pipeline/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ def main():
steps = (steps,)

on_error = "debug" if debug else None
cache = "1" if cache else "0"

processing_stages = []
processing_steps = []
Expand Down
10 changes: 5 additions & 5 deletions mne_bids_pipeline/_reject.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ def _get_reject(

# Only keep thresholds for channel types of interest
reject = reject.copy()
if ch_types == ["eeg"]:
ch_types_to_remove = ("mag", "grad")
else:
ch_types_to_remove = ("eeg",)

ch_types_to_remove = list()
if "meg" not in ch_types:
ch_types_to_remove.extend(("mag", "grad"))
if "eeg" not in ch_types:
ch_types_to_remove.append("eeg")
for ch_type in ch_types_to_remove:
try:
del reject[ch_type]
Expand Down
14 changes: 12 additions & 2 deletions mne_bids_pipeline/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,21 @@ def _prep_out_files(
*,
exec_params: SimpleNamespace,
out_files: dict[str, BIDSPath],
check_relative: Optional[pathlib.Path] = None,
):
if check_relative is None:
check_relative = exec_params.deriv_root
for key, fname in out_files.items():
# Sanity check that we only ever write to the derivatives directory
fname = pathlib.Path(fname)
if not fname.is_relative_to(check_relative):
raise RuntimeError(
f"Output BIDSPath not relative to expected root {check_relative}:"
f"\n{fname}"
)
out_files[key] = _path_to_str_hash(
key,
pathlib.Path(fname),
fname,
method=exec_params.memory_file_method,
kind="out",
)
Expand All @@ -401,7 +411,7 @@ def _path_to_str_hash(
assert isinstance(v, pathlib.Path), f'Bad type {type(v)}: {kind}_files["{k}"] = {v}'
assert v.exists(), f'missing {kind}_files["{k}"] = {v}'
if method == "mtime":
this_hash = v.lstat().st_mtime
this_hash = v.stat().st_mtime
else:
assert method == "hash" # guaranteed
this_hash = hash_file_path(v)
Expand Down
22 changes: 17 additions & 5 deletions mne_bids_pipeline/steps/freesurfer/_02_coreg_surfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import mne.bem

from ..._config_utils import (
_get_scalp_in_files,
get_fs_subject,
get_fs_subjects_dir,
get_subjects,
Expand All @@ -22,6 +21,17 @@
fs_bids_app = Path(__file__).parent / "contrib" / "run.py"


def _get_scalp_in_files(cfg: SimpleNamespace) -> dict[str, Path]:
subject_path = Path(cfg.fs_subjects_dir) / cfg.fs_subject
seghead = subject_path / "surf" / "lh.seghead"
in_files = dict()
if seghead.is_file():
in_files["seghead"] = seghead
else:
in_files["t1"] = subject_path / "mri" / "T1.mgz"
return in_files


def get_input_fnames_coreg_surfaces(
*,
cfg: SimpleNamespace,
Expand All @@ -32,7 +42,7 @@ def get_input_fnames_coreg_surfaces(

def get_output_fnames_coreg_surfaces(*, cfg: SimpleNamespace, subject: str) -> dict:
out_files = dict()
subject_path = Path(cfg.subjects_dir) / cfg.fs_subject
subject_path = Path(cfg.fs_subjects_dir) / cfg.fs_subject
out_files["seghead"] = subject_path / "surf" / "lh.seghead"
for key in ("dense", "medium", "sparse"):
out_files[f"head-{key}"] = (
Expand All @@ -57,19 +67,21 @@ def make_coreg_surfaces(
in_files.pop("t1" if "t1" in in_files else "seghead")
mne.bem.make_scalp_surfaces(
subject=cfg.fs_subject,
subjects_dir=cfg.subjects_dir,
subjects_dir=cfg.fs_subjects_dir,
force=True,
overwrite=True,
)
out_files = get_output_fnames_coreg_surfaces(cfg=cfg, subject=subject)
return _prep_out_files(exec_params=exec_params, out_files=out_files)
return _prep_out_files(
exec_params=exec_params, out_files=out_files, check_relative=cfg.fs_subjects_dir
)


def get_config(*, config, subject) -> SimpleNamespace:
cfg = SimpleNamespace(
subject=subject,
fs_subject=get_fs_subject(config, subject),
subjects_dir=get_fs_subjects_dir(config),
fs_subjects_dir=get_fs_subjects_dir(config),
)
return cfg

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ def run_regress_artifact(
out_files["regress"] = bids_path_in.copy().update(
processing=None,
split=None,
run=None,
suffix="regress",
extension=".h5",
)
Expand Down
19 changes: 16 additions & 3 deletions mne_bids_pipeline/steps/preprocessing/_09_ptp_reject.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ def drop_ptp(
psd = True
else:
psd = 30
tags = ("epochs", "reject")
kind = cfg.reject if isinstance(cfg.reject, str) else "Rejection"
title = "Epochs: after cleaning"
with _open_report(
cfg=cfg, exec_params=exec_params, subject=subject, session=session
) as report:
Expand All @@ -201,18 +204,28 @@ def drop_ptp(
fig=reject_log.plot(
orientation="horizontal", aspect="auto", show=False
),
title="Epochs: Autoreject cleaning",
title=f"{kind} cleaning",
caption=caption,
tags=("epochs", "autoreject"),
section=title,
tags=tags,
replace=True,
)
del caption
else:
report.add_html(
html=f"<code>{reject}</code>",
title=f"{kind} thresholds",
section=title,
replace=True,
tags=tags,
)

report.add_epochs(
epochs=epochs,
title="Epochs: after cleaning",
title=title,
psd=psd,
drop_log_ignore=(),
tags=tags,
replace=True,
)
return _prep_out_files(exec_params=exec_params, out_files=out_files)
Expand Down
4 changes: 3 additions & 1 deletion mne_bids_pipeline/steps/source/_01_make_bem_surfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ def make_bem_surfaces(
subject=subject,
session=session,
)
return _prep_out_files(exec_params=exec_params, out_files=out_files)
return _prep_out_files(
exec_params=exec_params, out_files=out_files, check_relative=cfg.fs_subjects_dir
)


def get_config(
Expand Down
4 changes: 3 additions & 1 deletion mne_bids_pipeline/steps/source/_02_make_bem_solution.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ def make_bem_solution(
out_files = get_output_fnames_make_bem_solution(cfg=cfg, subject=subject)
mne.write_bem_surfaces(out_files["model"], bem_model, overwrite=True)
mne.write_bem_solution(out_files["sol"], bem_sol, overwrite=True)
return _prep_out_files(exec_params=exec_params, out_files=out_files)
return _prep_out_files(
exec_params=exec_params, out_files=out_files, check_relative=cfg.fs_subjects_dir
)


def get_config(
Expand Down
4 changes: 3 additions & 1 deletion mne_bids_pipeline/steps/source/_03_setup_source_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ def run_setup_source_space(
in_files.clear() # all used by setup_source_space
out_files = get_output_fnames_setup_source_space(cfg=cfg, subject=subject)
mne.write_source_spaces(out_files["src"], src, overwrite=True)
return _prep_out_files(exec_params=exec_params, out_files=out_files)
return _prep_out_files(
exec_params=exec_params, out_files=out_files, check_relative=cfg.fs_subjects_dir
)


def get_config(
Expand Down
Loading