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

[ENH] include model name in ouput file #232

Merged
merged 7 commits into from
Aug 12, 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
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ generalize: ## demo: predicts labels of MOAE dataset
generalize \
--model 1_guided_fixations \
-vv
bidsmreye $$PWD/tests/data/moae_fmriprep \
$$PWD/outputs/moae_fmriprep/derivatives \
participant \
generalize \
-vv

# run demo via boutiques
demo_boutiques: tests/data/moae_fmriprep
Expand Down
45 changes: 38 additions & 7 deletions bidsmreye/bids_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -88,7 +89,10 @@


def create_bidsname(
layout: BIDSLayout, filename: dict[str, str] | str | Path, filetype: str
layout: BIDSLayout,
filename: dict[str, str] | str | Path,
filetype: str,
extra_entities: dict[str, str] | None = None,
) -> Path:
"""Return a BIDS valid filename for layout and a filename or a dict of BIDS entities.

Expand Down Expand Up @@ -117,8 +121,9 @@
padding = len(x.split("-")[1])
entities["run"] = f"{entities['run']:0{padding}d}"

else:
raise TypeError(f"filename must be a dict or a Path, not {type(filename)}")
if extra_entities is not None:
for key in extra_entities:
entities[key] = extra_entities[key]

Check warning on line 126 in bidsmreye/bids_utils.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/bids_utils.py#L125-L126

Added lines #L125 - L126 were not covered by tests

bids_name_config = get_bidsname_config()

Expand All @@ -128,6 +133,32 @@
return output_file.absolute()


def return_desc_entity(model_filename: Path):
model_name = sanitize_filename(model_filename).replace("Dataset", "")
if model_name in ["1GuidedFixations", "5FreeViewing"]:
return model_name[1:]
elif model_name == "3Openclosed":
return "OpenClosed"
elif model_name in ["4Pursuit", "2Pursuit", "3Pursuit"]:
return model_name[1:] + model_name[0]
elif model_name in ["1to5", "1to6"]:
return model_name

Check warning on line 145 in bidsmreye/bids_utils.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/bids_utils.py#L137-L145

Added lines #L137 - L145 were not covered by tests
else:
return sanitize_filename(model_filename)

Check warning on line 147 in bidsmreye/bids_utils.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/bids_utils.py#L147

Added line #L147 was not covered by tests


def sanitize_filename(filename: Path):
"""Turn filename stem into its alphanumeric CamelCase equivalent.

To use as a BIDS entity label.
"""
# Remove non-alphanumeric characters and split into words
words = re.sub(r"[^a-zA-Z0-9]", " ", filename.stem).split()

Check warning on line 156 in bidsmreye/bids_utils.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/bids_utils.py#L156

Added line #L156 was not covered by tests
# Capitalize the first letter of each word and join them
camelcase_name = "".join(word.capitalize() for word in words)
return camelcase_name

Check warning on line 159 in bidsmreye/bids_utils.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/bids_utils.py#L158-L159

Added lines #L158 - L159 were not covered by tests


def create_sidecar(
layout: BIDSLayout,
filename: str,
Expand All @@ -142,9 +173,9 @@
}
if source is not None:
content["Sources"] = [source] # type: ignore
sidecar_name = create_bidsname(layout, filename, "confounds_json")
sidecar_name = create_bidsname(layout, filename, "no_label_json")
json.dump(content, open(sidecar_name, "w"), indent=4)
log.debug(f"sidecar saved to {sidecar_name}")
log.debug(f"Sidecar saved to {sidecar_name}")


def save_sampling_frequency_to_json(
Expand Down Expand Up @@ -191,7 +222,7 @@
if config is None:
pybids_config = get_pybids_config()

log.info(f"indexing {dataset_path}")
log.info(f"Indexing {dataset_path}")

if not use_database:
return BIDSLayout(
Expand Down Expand Up @@ -263,7 +294,7 @@
subjects = [subjects[0]]
log.debug("Running first subject only.")

log.info(f"processing subjects: {subjects}")
log.info(f"Processing subjects: {subjects}")

return subjects

Expand Down
13 changes: 7 additions & 6 deletions bidsmreye/config/config_bidsname.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"mask": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_mask.p",
"report": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_report.html",
"no_label": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-nolabel_bidsmreye.npz",
"confounds_tsv": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.tsv",
"confounds_json": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.json",
"confounds_svg": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.svg",
"confounds_html": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.html",
"confounds_numpy": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}_space-{space}_desc-bidsmreye_confounds.npy"
"no_label_bold": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_timeseries.npz",
"no_label_json": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_timeseries.json",
"confounds_tsv": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-{desc}_eyetrack.tsv",
"confounds_json": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-{desc}_eyetrack.json",
"confounds_html": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-{desc}_eyetrack.html",
"confounds_svg": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-{desc}_eyetrack.svg",
"confounds_numpy": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}_space-{space}_desc-{desc}_confounds.npy"
}
11 changes: 8 additions & 3 deletions bidsmreye/config/default_filter_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
"suffix": "mask",
"extension": "p"
},
"no_label": {
"desc": "nolabel",
"suffix": "^bidsmreye$$",
"no_label_bold": {
"desc": "eye",
"suffix": "^timeseries$$",
"extension": "npz"
},
"no_label_bold_json": {
"desc": "eye",
"suffix": "^timeseries$$",
"extension": "json"
},
"eyetrack": {
"suffix": "^eyetrack$$",
"extension": "tsv"
Expand Down
80 changes: 67 additions & 13 deletions bidsmreye/generalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from __future__ import annotations

import json
import logging
import os
import shutil
import warnings
from pathlib import Path
from typing import Any
Expand All @@ -20,12 +22,13 @@
create_bidsname,
get_dataset_layout,
list_subjects,
return_desc_entity,
)
from bidsmreye.configuration import Config
from bidsmreye.logger import bidsmreye_log
from bidsmreye.quality_control import quality_control_output
from bidsmreye.utils import (
add_sidecar_in_root,
add_timestamps_to_dataframe,
check_if_file_found,
create_dir_for_file,
move_file,
Expand Down Expand Up @@ -70,7 +73,9 @@
fig.write_image(confound_svg)


def convert_confounds(layout_out: BIDSLayout, file: str | Path) -> Path:
def convert_confounds(
layout_out: BIDSLayout, file: str | Path, extra_entities: dict[str, str] | None = None
) -> Path:
"""Convert numpy output to TSV.

:param layout_out: pybids layout to of the dataset to act on.
Expand All @@ -86,7 +91,43 @@
but should still be able to unpack the results from a numpy file
with results from multiple files.
"""
confound_numpy = create_bidsname(layout_out, file, "confounds_numpy")
COLUMNS = ["timestamp", "x_coordinate", "y_coordinate"]

bold_json = Path(file).with_suffix(".json")
confounds_json = create_bidsname(
layout_out, file, "confounds_json", extra_entities=extra_entities
)
shutil.copyfile(bold_json, confounds_json)
with open(confounds_json) as f:
metadata = json.load(f)
metadata["StartTime"] = 0.0
metadata["Columns"] = COLUMNS
metadata["PhysioType"] = "eyetrack"
metadata["EnvironmentCoordinates"] = "center"
metadata["RecordedEye"] = "cyclopean"
metadata["timestamp"] = {
"Description": (
"Timestamp indexing the continuous recordings "
"corresponding to the sampled eye."
),
"Units": "seconds",
}
metadata["x_coordinate"] = {
"Description": ("Gaze position x-coordinate of the recorded eye."),
"Units": "degrees",
}
metadata["y_coordinate"] = {
"Description": ("Gaze position y-coordinate of the recorded eye."),
"Units": "degrees",
}
with open(confounds_json, "w") as f:
metadata = {key: metadata[key] for key in sorted(metadata)}
json.dump(metadata, f, indent=4)
log.debug(f"Sidecar saved to {confounds_json}")

confound_numpy = create_bidsname(
layout_out, file, "confounds_numpy", extra_entities=extra_entities
)

content = np.load(
file=confound_numpy,
Expand All @@ -100,14 +141,19 @@

this_pred = np.nanmedian(item["pred_y"], axis=1)

confound_name = create_bidsname(layout_out, Path(key + "p"), "confounds_tsv")
confound_name = create_bidsname(
layout_out, Path(key + "p"), "confounds_tsv", extra_entities=extra_entities
)

log.info(f"Saving eye gaze data to {confound_name.relative_to(layout_out.root)}")

pd.DataFrame(this_pred).to_csv(
df = pd.DataFrame(this_pred)
df = add_timestamps_to_dataframe(df, metadata["SamplingFrequency"])

df.to_csv(
confound_name,
sep="\t",
header=["eye1_x_coordinate", "eye1_y_coordinate"],
header=COLUMNS,
index=None,
)

Expand All @@ -117,7 +163,12 @@
return confound_name


def create_confounds_tsv(layout_out: BIDSLayout, file: str, subject_label: str) -> None:
def create_confounds_tsv(
layout_out: BIDSLayout,
file: str,
subject_label: str,
extra_entities: dict[str, str] | None = None,
) -> None:
"""Generate a TSV file for the eye motion timeseries.

:param layout_out:
Expand All @@ -129,7 +180,9 @@
:param subject_label:
:type subject_label: str
"""
confound_numpy = create_bidsname(layout_out, file, "confounds_numpy")
confound_numpy = create_bidsname(

Check warning on line 183 in bidsmreye/generalize.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/generalize.py#L183

Added line #L183 was not covered by tests
layout_out, file, "confounds_numpy", extra_entities=extra_entities
)

source_file = Path(layout_out.root) / f"sub-{subject_label}" / "results_tmp.npy"

Expand All @@ -138,7 +191,7 @@
confound_numpy,
)

convert_confounds(layout_out, file)
convert_confounds(layout_out, file, extra_entities=extra_entities)

Check warning on line 194 in bidsmreye/generalize.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/generalize.py#L194

Added line #L194 was not covered by tests


def process_subject(cfg: Config, layout_out: BIDSLayout, subject_label: str) -> None:
Expand All @@ -155,7 +208,7 @@
"""
log.info(f"Running subject: {subject_label}")

this_filter = set_this_filter(cfg, subject_label, "no_label")
this_filter = set_this_filter(cfg, subject_label, "no_label_bold")

Check warning on line 211 in bidsmreye/generalize.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/generalize.py#L211

Added line #L211 was not covered by tests

bf = layout_out.get(
regex_search=True,
Expand Down Expand Up @@ -199,7 +252,10 @@
percentile_cut=80,
)

create_confounds_tsv(layout_out, file.path, subject_label)
extra_entities = None
if cfg.model_weights_file is not None:
extra_entities = {"desc": return_desc_entity(Path(cfg.model_weights_file))}
create_confounds_tsv(layout_out, file.path, subject_label, extra_entities)

Check warning on line 258 in bidsmreye/generalize.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/generalize.py#L255-L258

Added lines #L255 - L258 were not covered by tests


def generalize(cfg: Config) -> None:
Expand All @@ -211,8 +267,6 @@
layout_out = get_dataset_layout(cfg.output_dir)
check_layout(cfg, layout_out)

add_sidecar_in_root(layout_out)

subjects = list_subjects(cfg, layout_out)

text = "GENERALIZING"
Expand Down
4 changes: 2 additions & 2 deletions bidsmreye/prepare_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
subj["labels"].append(labels)
subj["ids"].append(([entities["subject"]] * labels.shape[0], [i] * labels.shape[0]))

output_file = create_bidsname(layout_out, Path(img), "no_label")
output_file = create_bidsname(layout_out, Path(img), "no_label_bold")
file_to_move = Path(layout_out.root) / ".." / "bidsmreye" / output_file.name

preprocess.save_data(
Expand Down Expand Up @@ -151,7 +151,7 @@

report_name = create_bidsname(layout_out, filename=img_path, filetype="report")
mask_name = create_bidsname(layout_out, filename=img_path, filetype="mask")
output_file = create_bidsname(layout_out, Path(img_path), "no_label")
output_file = create_bidsname(layout_out, Path(img_path), "no_label_bold")

Check warning on line 154 in bidsmreye/prepare_data.py

View check run for this annotation

Codecov / codecov/patch

bidsmreye/prepare_data.py#L154

Added line #L154 was not covered by tests

if (
not cfg.force
Expand Down
Loading