|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import json |
| 4 | +from pathlib import Path |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +from bids import BIDSLayout # type: ignore |
| 8 | + |
| 9 | +from . import _version |
| 10 | +from bidsmreye.configuration import Config |
| 11 | +from bidsmreye.configuration import config_to_dict |
| 12 | +from bidsmreye.configuration import get_bids_filter_config |
| 13 | +from bidsmreye.configuration import get_bidsname_config |
| 14 | +from bidsmreye.configuration import get_pybids_config |
| 15 | +from bidsmreye.logging import bidsmreye_log |
| 16 | +from bidsmreye.methods import methods |
| 17 | +from bidsmreye.utils import copy_license |
| 18 | +from bidsmreye.utils import create_dir_if_absent |
| 19 | + |
| 20 | +log = bidsmreye_log("bidsmreye") |
| 21 | + |
| 22 | +__version__ = _version.get_versions()["version"] |
| 23 | + |
| 24 | + |
| 25 | +def check_layout(cfg: Config, layout: BIDSLayout, for_file: str = "bold") -> None: |
| 26 | + """Check layout. |
| 27 | +
|
| 28 | + :param cfg: Configuration object |
| 29 | + :type cfg: Config |
| 30 | +
|
| 31 | + :param layout: BIDSLayout of the dataset. |
| 32 | + :type layout: BIDSLayout |
| 33 | +
|
| 34 | + :raises RuntimeError: _description_ |
| 35 | + :raises RuntimeError: _description_ |
| 36 | + """ |
| 37 | + desc = layout.get_dataset_description() |
| 38 | + if ( |
| 39 | + "DatasetType" not in desc |
| 40 | + and "PipelineDescription" not in desc |
| 41 | + or "DatasetType" in desc |
| 42 | + and desc["DatasetType"] != "derivative" |
| 43 | + ): |
| 44 | + raise RuntimeError("Input dataset should be BIDS derivative") |
| 45 | + |
| 46 | + this_filter = get_bids_filter_config()[for_file] |
| 47 | + |
| 48 | + if "GeneratedBy" in desc: |
| 49 | + generated_by = desc["GeneratedBy"] |
| 50 | + elif "PipelineDescription" in desc: |
| 51 | + generated_by = desc["PipelineDescription"] |
| 52 | + |
| 53 | + if isinstance(generated_by, list): |
| 54 | + generated_by = generated_by[0] |
| 55 | + |
| 56 | + if generated_by["Name"].lower() == "bidsmreye": |
| 57 | + this_filter = get_bids_filter_config()["mask"] |
| 58 | + |
| 59 | + this_filter["task"] = cfg.task |
| 60 | + this_filter["space"] = cfg.space |
| 61 | + this_filter["run"] = cfg.run |
| 62 | + |
| 63 | + log.debug(f"Looking for files with filter\n{this_filter}") |
| 64 | + |
| 65 | + bf = layout.get( |
| 66 | + return_type="filename", |
| 67 | + regex_search=True, |
| 68 | + **this_filter, |
| 69 | + ) |
| 70 | + |
| 71 | + if bf == []: |
| 72 | + raise RuntimeError( |
| 73 | + f""" |
| 74 | + Input dataset {layout.root} does not have any data to process for filter\n{this_filter} |
| 75 | + """ |
| 76 | + ) |
| 77 | + |
| 78 | + |
| 79 | +def create_bidsname( |
| 80 | + layout: BIDSLayout, filename: dict[str, str] | str | Path, filetype: str |
| 81 | +) -> Path: |
| 82 | + """Return a BIDS valid filename for layout and a filename or a dict of BIDS entities. |
| 83 | +
|
| 84 | + :param layout: BIDSLayout of the dataset. |
| 85 | + :type layout: BIDSLayout |
| 86 | +
|
| 87 | + :param filename: Dictionary of BIDS entities or a Path to a file. |
| 88 | + :type filename: Union[dict, str, Path] |
| 89 | +
|
| 90 | + :param filetype: One of the file type available in the BIDS name config. |
| 91 | + :type filetype: str |
| 92 | +
|
| 93 | + :raises TypeError: |
| 94 | +
|
| 95 | + :return: |
| 96 | + :rtype: Path |
| 97 | + """ |
| 98 | + if isinstance(filename, dict): |
| 99 | + entities = filename |
| 100 | + elif isinstance(filename, (Path, str)): |
| 101 | + entities = layout.parse_file_entities(filename) |
| 102 | + else: |
| 103 | + raise TypeError(f"filename must be a dict or a Path, not {type(filename)}") |
| 104 | + |
| 105 | + bids_name_config = get_bidsname_config() |
| 106 | + |
| 107 | + output_file = layout.build_path(entities, bids_name_config[filetype], validate=False) |
| 108 | + |
| 109 | + output_file = Path(layout.root).joinpath(output_file) |
| 110 | + |
| 111 | + return output_file.resolve() |
| 112 | + |
| 113 | + |
| 114 | +def create_sidecar( |
| 115 | + layout: BIDSLayout, filename: str, SamplingFrequency: float | None = None |
| 116 | +) -> None: |
| 117 | + """Create sidecar for the eye motion timeseries.""" |
| 118 | + if SamplingFrequency is None: |
| 119 | + SamplingFrequency = 0 |
| 120 | + content = { |
| 121 | + "SamplingFrequency": SamplingFrequency, |
| 122 | + } |
| 123 | + sidecar_name = create_bidsname(layout, filename, "confounds_json") |
| 124 | + json.dump(content, open(sidecar_name, "w"), indent=4) |
| 125 | + |
| 126 | + |
| 127 | +def get_dataset_layout( |
| 128 | + dataset_path: str | Path, |
| 129 | + config: dict[str, str] | None = None, |
| 130 | + use_database: bool = False, |
| 131 | +) -> BIDSLayout: |
| 132 | + """Return a BIDSLayout object for the dataset at the given path. |
| 133 | +
|
| 134 | + :param dataset_path: Path to the dataset. |
| 135 | + :type dataset_path: Union[str, Path] |
| 136 | +
|
| 137 | + :param config: Pybids config to use. Defaults to None. |
| 138 | + :type config: Optional[dict], optional |
| 139 | +
|
| 140 | + :param use_database: Defaults to False |
| 141 | + :type use_database: bool, optional |
| 142 | +
|
| 143 | + :return: _description_ |
| 144 | + :rtype: BIDSLayout |
| 145 | + """ |
| 146 | + if isinstance(dataset_path, str): |
| 147 | + dataset_path = Path(dataset_path) |
| 148 | + create_dir_if_absent(dataset_path) |
| 149 | + |
| 150 | + dataset_path = dataset_path.resolve() |
| 151 | + |
| 152 | + if config is None: |
| 153 | + pybids_config = get_pybids_config() |
| 154 | + |
| 155 | + log.info(f"indexing {dataset_path}") |
| 156 | + |
| 157 | + if not use_database: |
| 158 | + return BIDSLayout( |
| 159 | + dataset_path, |
| 160 | + validate=False, |
| 161 | + derivatives=False, |
| 162 | + config=pybids_config, |
| 163 | + ) |
| 164 | + |
| 165 | + database_path = dataset_path.joinpath("pybids_db") |
| 166 | + return BIDSLayout( |
| 167 | + dataset_path, |
| 168 | + validate=False, |
| 169 | + derivatives=False, |
| 170 | + config=pybids_config, |
| 171 | + database_path=database_path, |
| 172 | + ) |
| 173 | + |
| 174 | + |
| 175 | +def init_dataset(cfg: Config, qc_only: bool = False) -> BIDSLayout: |
| 176 | + |
| 177 | + layout_out = init_derivatives_layout(cfg) |
| 178 | + |
| 179 | + citation_file = methods(cfg.output_dir, qc_only=qc_only) |
| 180 | + log.info(f"Method section generated: {citation_file}") |
| 181 | + |
| 182 | + license_file = copy_license(cfg.output_dir) |
| 183 | + log.info(f"License file added: {license_file}") |
| 184 | + |
| 185 | + return layout_out |
| 186 | + |
| 187 | + |
| 188 | +def init_derivatives_layout(cfg: Config) -> BIDSLayout: |
| 189 | + """Initialize a derivatives dataset and returns its layout. |
| 190 | +
|
| 191 | + :param output_dir: |
| 192 | + :type output_dir: Path |
| 193 | +
|
| 194 | + :return: |
| 195 | + :rtype: BIDSLayout |
| 196 | + """ |
| 197 | + create_dir_if_absent(cfg.output_dir) |
| 198 | + layout_out = get_dataset_layout(cfg.output_dir) |
| 199 | + layout_out = set_dataset_description(layout_out) |
| 200 | + layout_out.dataset_description["DatasetType"] = "derivative" |
| 201 | + layout_out.dataset_description["GeneratedBy"][0]["Name"] = "bidsmreye" |
| 202 | + layout_out.dataset_description["GeneratedBy"][0]["config"] = config_to_dict(cfg) |
| 203 | + write_dataset_description(layout_out) |
| 204 | + return layout_out |
| 205 | + |
| 206 | + |
| 207 | +def list_subjects(cfg: Config, layout: BIDSLayout) -> list[str]: |
| 208 | + """List subject in a BIDS dataset for a given Config. |
| 209 | +
|
| 210 | + :param cfg: Configuration object |
| 211 | + :type cfg: Config |
| 212 | +
|
| 213 | + :param layout: BIDSLayout of the dataset. |
| 214 | + :type layout: BIDSLayout |
| 215 | +
|
| 216 | + :raises RuntimeError: _description_ |
| 217 | +
|
| 218 | + :return: _description_ |
| 219 | + :rtype: list |
| 220 | + """ |
| 221 | + subjects = layout.get(return_type="id", target="subject", subject=cfg.participant) |
| 222 | + |
| 223 | + if subjects == [] or subjects is None: |
| 224 | + raise RuntimeError("No subject found") |
| 225 | + |
| 226 | + if cfg.debug: |
| 227 | + subjects = [subjects[0]] |
| 228 | + log.debug("Running first subject only.") |
| 229 | + |
| 230 | + log.info(f"processing subjects: {subjects}") |
| 231 | + |
| 232 | + return subjects |
| 233 | + |
| 234 | + |
| 235 | +def set_dataset_description(layout: BIDSLayout, is_derivative: bool = True) -> BIDSLayout: |
| 236 | + """Add dataset description to a layout. |
| 237 | +
|
| 238 | + :param layout: _description_ |
| 239 | + :type layout: BIDSLayout |
| 240 | +
|
| 241 | + :param is_derivative: Defaults to True |
| 242 | + :type is_derivative: bool, optional |
| 243 | +
|
| 244 | + :return: Updated BIDSLayout of the dataset |
| 245 | + :rtype: BIDSLayout |
| 246 | + """ |
| 247 | + data: dict[str, Any] = { |
| 248 | + "Name": "dataset name", |
| 249 | + "BIDSVersion": "1.7.0", |
| 250 | + "DatasetType": "raw", |
| 251 | + "License": "CCO", |
| 252 | + "Authors": ["", ""], |
| 253 | + "Acknowledgements": "Special thanks to ", |
| 254 | + "HowToAcknowledge": """Please cite this paper: Frey, M., Nau, M. & Doeller, C.F. |
| 255 | +Magnetic resonance-based eye tracking using deep neural networks. |
| 256 | +Nat Neurosci 24, 1772-1779 (2021). |
| 257 | +https://doi.org/10.1038/s41593-021-00947-w""", |
| 258 | + "Funding": ["", ""], |
| 259 | + "ReferencesAndLinks": ["https://doi.org/10.1038/s41593-021-00947-w"], |
| 260 | + "DatasetDOI": "doi:", |
| 261 | + } |
| 262 | + |
| 263 | + if is_derivative: |
| 264 | + data["GeneratedBy"] = [ |
| 265 | + { |
| 266 | + "Name": "bidsMReye", |
| 267 | + "Version": __version__, |
| 268 | + "Container": {"Type": "", "Tag": ""}, |
| 269 | + "Description": "", |
| 270 | + "CodeURL": "https://github.com/cpp-lln-lab/bidsMReye.git", |
| 271 | + }, |
| 272 | + ] |
| 273 | + |
| 274 | + data["SourceDatasets"] = [ |
| 275 | + { |
| 276 | + "DOI": "doi:", |
| 277 | + "URL": "", |
| 278 | + "Version": "", |
| 279 | + } |
| 280 | + ] |
| 281 | + |
| 282 | + layout.dataset_description = data |
| 283 | + |
| 284 | + return layout |
| 285 | + |
| 286 | + |
| 287 | +def write_dataset_description(layout: BIDSLayout) -> None: |
| 288 | + """Add a dataset_description.json to a BIDS dataset. |
| 289 | +
|
| 290 | + :param layout: BIDSLayout of the dataset to update. |
| 291 | + :type layout: BIDSLayout |
| 292 | + """ |
| 293 | + output_file = Path(layout.root).joinpath("dataset_description.json") |
| 294 | + |
| 295 | + with open(output_file, "w") as ff: |
| 296 | + json.dump(layout.dataset_description, ff, indent=4) |
0 commit comments