neuralfetch 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/PKG-INFO +2 -2
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/download.py +69 -77
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/allen2022massive.py +271 -0
- neuralfetch-0.2.3/neuralfetch/studies/alvarez2022haaglanden.py +163 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/bel2026petit.py +2 -2
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/brennan2019hierarchical.py +38 -15
- neuralfetch-0.2.3/neuralfetch/studies/chang2019bold5000.py +278 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/ghassemi2018you.py +9 -4
- neuralfetch-0.2.3/neuralfetch/studies/hebart2023things.py +534 -0
- neuralfetch-0.2.3/neuralfetch/studies/levy2026noninvasive.py +1061 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/li2022petit.py +1 -1
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/moabb2025.py +5 -6
- neuralfetch-0.2.3/neuralfetch/studies/sivakumar2024emg2qwerty.py +189 -0
- neuralfetch-0.2.3/neuralfetch/studies/xu2024alljoined.py +163 -0
- neuralfetch-0.2.3/neuralfetch/studies/xu2025alljoined.py +210 -0
- neuralfetch-0.2.2/neuralfetch/studies/zyma2019eeg.py → neuralfetch-0.2.3/neuralfetch/studies/zyma2019electroencephalograms.py +10 -4
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/test_download.py +120 -71
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/test_moabb2025.py +6 -6
- neuralfetch-0.2.3/neuralfetch/test_sivakumar2024emg2qwerty.py +124 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/test_studies.py +44 -3
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/test_utils.py +1 -2
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/utils.py +8 -4
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/PKG-INFO +2 -2
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/SOURCES.txt +10 -3
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/requires.txt +1 -1
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/pyproject.toml +3 -3
- neuralfetch-0.2.2/neuralfetch/studies/hebart2023thingsmeg.py +0 -293
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/LICENSE +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/README.md +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/__init__.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/py.typed +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/__init__.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/albrecht2019increased.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/chen2023large.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/dan2023bids.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/gifford2022large.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/grootswagers2022human.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/hinss2023open.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/hollenstein2018zuco.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/kemp2000analysis.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/kueper2024eeg.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/liu2024eeg2video.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/miltiadous2023dice.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/mumtaz2018machine.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/nieuwland2018large.py +0 -0
- /neuralfetch-0.2.2/neuralfetch/studies/schalk2004bci.py → /neuralfetch-0.2.3/neuralfetch/studies/schalk2004bci2000.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/shirazi2024hbn.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/singh2021timing.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/tuh_eeg.py +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/dependency_links.txt +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/entry_points.txt +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/top_level.txt +0 -0
- {neuralfetch-0.2.2 → neuralfetch-0.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: neuralfetch
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Download backends and data-fetching utilities for neuralset.
|
|
5
5
|
Author: Meta FAIR
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,7 +12,7 @@ License-File: LICENSE
|
|
|
12
12
|
Requires-Dist: neuralset>=0.0.1
|
|
13
13
|
Requires-Dist: h5py
|
|
14
14
|
Requires-Dist: pybv>=0.7.3
|
|
15
|
-
Requires-Dist: mne_bids>=0.
|
|
15
|
+
Requires-Dist: mne_bids>=0.19
|
|
16
16
|
Provides-Extra: all
|
|
17
17
|
Requires-Dist: neuralset[all]; extra == "all"
|
|
18
18
|
Requires-Dist: matplotlib; extra == "all"
|
|
@@ -27,6 +27,42 @@ from neuralset.base import BaseModel, PathLike, _Module
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def ensure_study_symlink(studies_root: Path, link_name: str, target_name: str) -> None:
|
|
31
|
+
"""Create ``studies_root/link_name -> target_name`` symlink if absent.
|
|
32
|
+
|
|
33
|
+
Used when multiple study classes share one physical data directory. The
|
|
34
|
+
target directory is created if it does not yet exist so that the symlink is
|
|
35
|
+
valid even before data has been downloaded.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
studies_root:
|
|
40
|
+
Root folder that contains per-study subdirectories (value of
|
|
41
|
+
``NEURALSET_STUDY_FOLDER``).
|
|
42
|
+
link_name:
|
|
43
|
+
Name of the symlink to create (typically the subclass name, e.g.
|
|
44
|
+
``"Shirazi2024HbnVideo"``).
|
|
45
|
+
target_name:
|
|
46
|
+
Name of the directory to point at (typically the parent class name,
|
|
47
|
+
e.g. ``"Shirazi2024Hbn"``). A *relative* symlink is created so the
|
|
48
|
+
link stays valid if the studies root is moved.
|
|
49
|
+
"""
|
|
50
|
+
target = studies_root / target_name
|
|
51
|
+
symlink = studies_root / link_name
|
|
52
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
if symlink.is_symlink():
|
|
54
|
+
return
|
|
55
|
+
if symlink.exists():
|
|
56
|
+
logger.warning(
|
|
57
|
+
"%s exists as a real directory, not a symlink — data will not resolve to %s.",
|
|
58
|
+
symlink,
|
|
59
|
+
target_name,
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
symlink.symlink_to(target_name) # relative, portable
|
|
63
|
+
logger.info("Created symlink %s -> %s", symlink, target_name)
|
|
64
|
+
|
|
65
|
+
|
|
30
66
|
@contextlib.contextmanager
|
|
31
67
|
def success_writer(
|
|
32
68
|
fname: str | Path, suffix: str = "_success.txt", success_msg: str = "done"
|
|
@@ -568,7 +604,7 @@ class Datalad(BaseDownload):
|
|
|
568
604
|
# raise RuntimeError(f"Clone Failed: {proc.stderr}")
|
|
569
605
|
|
|
570
606
|
def _dl_item(self, cur_path: Path | str) -> None:
|
|
571
|
-
threads_ = "" if self.threads > 1 else
|
|
607
|
+
threads_ = f" -J {self.threads}" if self.threads > 1 else ""
|
|
572
608
|
cmd = f'datalad get "{cur_path}"{threads_}'
|
|
573
609
|
self._datalad(cmd, self._dl_dir / self.repo_name)
|
|
574
610
|
|
|
@@ -750,8 +786,6 @@ class Dryad(BaseDownload):
|
|
|
750
786
|
return files_resp.get("_embedded", {}).get("stash:files", [])
|
|
751
787
|
|
|
752
788
|
def _download(self) -> None:
|
|
753
|
-
import tempfile
|
|
754
|
-
import zipfile
|
|
755
789
|
from urllib.parse import quote
|
|
756
790
|
|
|
757
791
|
import requests as req
|
|
@@ -819,8 +853,10 @@ class Dryad(BaseDownload):
|
|
|
819
853
|
class Eegdash(BaseDownload):
|
|
820
854
|
"""Download datasets from the EEGDash cloud archive.
|
|
821
855
|
|
|
822
|
-
|
|
823
|
-
|
|
856
|
+
Thin wrapper over :class:`eegdash.EEGDashDataset`: record discovery, NEMAR
|
|
857
|
+
git-annex resolution, the anonymous ``s3fs`` transfer of recordings, and
|
|
858
|
+
sidecar fetching (from data.nemar.org via ``nemar-py``) are all delegated
|
|
859
|
+
to eegdash's ``download_all``.
|
|
824
860
|
|
|
825
861
|
Parameters
|
|
826
862
|
----------
|
|
@@ -832,68 +868,19 @@ class Eegdash(BaseDownload):
|
|
|
832
868
|
EEGDash database to query (``"eegdash"``, ``"eegdash_staging"``, …).
|
|
833
869
|
"""
|
|
834
870
|
|
|
835
|
-
requirements: tp.ClassVar[tuple[str, ...]] = ("eegdash",)
|
|
871
|
+
requirements: tp.ClassVar[tuple[str, ...]] = ("eegdash>=0.8.2",)
|
|
836
872
|
database: str = "eegdash"
|
|
837
873
|
|
|
838
|
-
@staticmethod
|
|
839
|
-
def _parse_s3_uri(uri: str) -> tuple[str, str]:
|
|
840
|
-
"""Split ``s3://bucket/key/prefix`` into ``("bucket", "key/prefix")``."""
|
|
841
|
-
without_scheme = uri.removeprefix("s3://")
|
|
842
|
-
bucket, _, prefix = without_scheme.partition("/")
|
|
843
|
-
return bucket, prefix
|
|
844
|
-
|
|
845
874
|
def _download(self) -> None:
|
|
846
|
-
from eegdash import
|
|
875
|
+
from eegdash import EEGDashDataset # type: ignore[import-not-found]
|
|
847
876
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
# Group files by bucket so we can batch them into S3 downloaders.
|
|
857
|
-
per_bucket: dict[str, list[tuple[str, str]]] = {}
|
|
858
|
-
for rec in records:
|
|
859
|
-
storage = rec.get("storage", {})
|
|
860
|
-
base = storage.get("base", "")
|
|
861
|
-
raw_key = storage.get("raw_key", "")
|
|
862
|
-
dep_keys: list[str] = storage.get("dep_keys", [])
|
|
863
|
-
bids_rel = rec.get("bids_relpath", raw_key)
|
|
864
|
-
|
|
865
|
-
if not base or not raw_key:
|
|
866
|
-
logger.warning("Skipping record with incomplete storage info: %s", rec)
|
|
867
|
-
continue
|
|
868
|
-
|
|
869
|
-
bucket, prefix = self._parse_s3_uri(base)
|
|
870
|
-
|
|
871
|
-
s3_key = f"{prefix}/{raw_key}" if prefix else raw_key
|
|
872
|
-
local_path = str(self._dl_dir / bids_rel)
|
|
873
|
-
per_bucket.setdefault(bucket, []).append((s3_key, local_path))
|
|
874
|
-
|
|
875
|
-
for dep in dep_keys:
|
|
876
|
-
dep_s3_key = f"{prefix}/{dep}" if prefix else dep
|
|
877
|
-
per_bucket.setdefault(bucket, []).append(
|
|
878
|
-
(dep_s3_key, str(self._dl_dir / dep))
|
|
879
|
-
)
|
|
880
|
-
|
|
881
|
-
total_files = sum(len(v) for v in per_bucket.values())
|
|
882
|
-
if total_files == 0:
|
|
883
|
-
raise RuntimeError(f"No downloadable files found for '{self.study}'")
|
|
884
|
-
|
|
885
|
-
for bucket, file_pairs in per_bucket.items():
|
|
886
|
-
dl = S3(
|
|
887
|
-
study=self.study,
|
|
888
|
-
dset_dir=Path(self.dset_dir),
|
|
889
|
-
bucket=bucket,
|
|
890
|
-
files_with_destinations=file_pairs,
|
|
891
|
-
anonymous=True,
|
|
892
|
-
skip_existing=True,
|
|
893
|
-
)
|
|
894
|
-
dl._download()
|
|
895
|
-
|
|
896
|
-
print(f"\nDownloaded {total_files} files for {self.study}")
|
|
877
|
+
EEGDashDataset(
|
|
878
|
+
cache_dir=self._dl_dir,
|
|
879
|
+
dataset=self.study,
|
|
880
|
+
database=self.database,
|
|
881
|
+
download=True,
|
|
882
|
+
).download_all()
|
|
883
|
+
logger.info("Downloaded %s", self.study)
|
|
897
884
|
|
|
898
885
|
|
|
899
886
|
class Figshare(BaseDownload):
|
|
@@ -1144,16 +1131,27 @@ class Openneuro(BaseDownload):
|
|
|
1144
1131
|
``'sub-1/**/*run-01*'``. The pattern ``**`` will match any files and
|
|
1145
1132
|
zero or more directories, subdirectories and symbolic links to
|
|
1146
1133
|
directories.
|
|
1134
|
+
|
|
1135
|
+
The ``nworkers`` parameter controls how many files are downloaded in
|
|
1136
|
+
parallel (forwarded to ``openneuro.download`` as
|
|
1137
|
+
``max_concurrent_downloads``). The openneuro-py default is 5; raise it to
|
|
1138
|
+
speed up datasets with many files when network bandwidth allows.
|
|
1147
1139
|
"""
|
|
1148
1140
|
|
|
1149
|
-
requirements: tp.ClassVar[tuple[str, ...]] = ("openneuro-py>=
|
|
1141
|
+
requirements: tp.ClassVar[tuple[str, ...]] = ("openneuro-py>=2026.4.0",)
|
|
1150
1142
|
excluded_patterns: list[str] = []
|
|
1151
1143
|
include: list[str] | None = None
|
|
1144
|
+
nworkers: int = 5
|
|
1152
1145
|
|
|
1153
1146
|
def _download(self) -> None:
|
|
1154
1147
|
import openneuro as on
|
|
1155
1148
|
|
|
1156
|
-
on.download(
|
|
1149
|
+
on.download(
|
|
1150
|
+
dataset=self.study,
|
|
1151
|
+
target_dir=self._dl_dir,
|
|
1152
|
+
include=self.include,
|
|
1153
|
+
max_concurrent_downloads=self.nworkers,
|
|
1154
|
+
)
|
|
1157
1155
|
|
|
1158
1156
|
|
|
1159
1157
|
class Osf(BaseDownload):
|
|
@@ -1205,7 +1203,7 @@ class Physionet(S3):
|
|
|
1205
1203
|
|
|
1206
1204
|
Extends ``S3`` with the convention that Physionet datasets live under
|
|
1207
1205
|
``<study>/<version>/`` in the ``physionet-open`` bucket. After download,
|
|
1208
|
-
the versioned directory is
|
|
1206
|
+
the versioned directory is ``_dl_dir/<study>/<version>``.
|
|
1209
1207
|
"""
|
|
1210
1208
|
|
|
1211
1209
|
bucket: str = "physionet-open"
|
|
@@ -1213,20 +1211,14 @@ class Physionet(S3):
|
|
|
1213
1211
|
|
|
1214
1212
|
def _download(self, overwrite=False) -> None:
|
|
1215
1213
|
self.prefix = f"{self.study}/{self.version}"
|
|
1216
|
-
|
|
1217
|
-
self.output_dir = temp_folder
|
|
1214
|
+
self.output_dir = self._dl_dir / self.study / self.version
|
|
1218
1215
|
|
|
1219
|
-
#
|
|
1220
|
-
#
|
|
1221
|
-
#
|
|
1222
|
-
#
|
|
1216
|
+
# Preserve PhysioNet source structure locally:
|
|
1217
|
+
# - list only keys under <study>/<version> via `prefix`
|
|
1218
|
+
# - S3 strips that prefix from each key before writing
|
|
1219
|
+
# - write under download/<study>/<version>/
|
|
1223
1220
|
super()._download()
|
|
1224
1221
|
|
|
1225
|
-
inner = temp_folder / self.study / self.version
|
|
1226
|
-
inner.rename(self._dl_dir)
|
|
1227
|
-
inner.parent.rmdir()
|
|
1228
|
-
temp_folder.rmdir()
|
|
1229
|
-
|
|
1230
1222
|
|
|
1231
1223
|
synapse_msg = """Requires creating a Synapse account with 2FA.
|
|
1232
1224
|
https://accounts.synapse.org/register1?appId=synapse.org
|
|
@@ -49,6 +49,8 @@ from tqdm import trange
|
|
|
49
49
|
|
|
50
50
|
from neuralfetch.download import success_writer
|
|
51
51
|
from neuralset.events import study
|
|
52
|
+
from neuralset.events import utils as event_utils
|
|
53
|
+
from neuralset.utils import get_bids_filepath, read_bids_events
|
|
52
54
|
|
|
53
55
|
logger = logging.getLogger(__name__)
|
|
54
56
|
|
|
@@ -60,6 +62,12 @@ def get_nsd_tcs_marker() -> Path:
|
|
|
60
62
|
return config_dir / "neuralfetch" / ".nsd_tcs_accepted"
|
|
61
63
|
|
|
62
64
|
|
|
65
|
+
def get_allen2022massive_common_path(path: str | Path) -> Path:
|
|
66
|
+
"""Sibling ``nsd_common`` folder shared by NSD variants (raw, betas)."""
|
|
67
|
+
path = Path(path)
|
|
68
|
+
return (path / ".." / "nsd_common").resolve(strict=False)
|
|
69
|
+
|
|
70
|
+
|
|
63
71
|
# Helper to load 'nsd_expdesign.mat' file Copy-pasted,
|
|
64
72
|
# from https://github.com/ozcelikfu/brain-diffuser/blob/main/data/prepare_nsddata.py
|
|
65
73
|
# Commit 1c07200
|
|
@@ -326,6 +334,14 @@ IIS-1822929."
|
|
|
326
334
|
logger.info("NSD Data Access Agreement already accepted (marker file found).")
|
|
327
335
|
return
|
|
328
336
|
|
|
337
|
+
accept = os.environ.get("NSD_ACCEPT_LICENCE", "").lower() in ("1", "true", "yes")
|
|
338
|
+
if not accept:
|
|
339
|
+
raise PermissionError(
|
|
340
|
+
"NSD dataset requires accepting the Data Access Agreement before "
|
|
341
|
+
"downloading. Set NSD_ACCEPT_LICENCE=1 to enable the interactive "
|
|
342
|
+
"consent flow, then re-run download()."
|
|
343
|
+
)
|
|
344
|
+
|
|
329
345
|
if not sys.stdin or not getattr(sys.stdin, "isatty", lambda: False)():
|
|
330
346
|
raise RuntimeError(
|
|
331
347
|
"NSD Data Access Agreement requires an interactive session. "
|
|
@@ -612,6 +628,261 @@ IIS-1822929."
|
|
|
612
628
|
return pd.concat([pd.DataFrame([fmri]), pd.DataFrame(image_events)], axis=0)
|
|
613
629
|
|
|
614
630
|
|
|
631
|
+
# # # # # BIDS / deepprep variant # # # # #
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class Allen2022MassiveRaw(Allen2022Massive):
|
|
635
|
+
"""Natural Scenes Dataset (NSD): 7T fMRI responses to natural images (BIDS/deepprep).
|
|
636
|
+
|
|
637
|
+
This variant requires fMRIPrep or deepprep preprocessing and loads data
|
|
638
|
+
from BIDS derivative directories. See Allen2022Massive for the version that uses
|
|
639
|
+
NSD's own preprocessed timeseries (no external preprocessing needed).
|
|
640
|
+
|
|
641
|
+
Experimental Design:
|
|
642
|
+
- 7T fMRI recordings (TR = 1.6 s)
|
|
643
|
+
- 8 participants
|
|
644
|
+
- 30-40 sessions, 12 runs per session
|
|
645
|
+
- Paradigm: passive viewing of 73,000 natural images from COCO dataset
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
description: tp.ClassVar[str] = (
|
|
649
|
+
"Natural Scenes Dataset: 7T BOLD fMRI from 8 participants viewing "
|
|
650
|
+
"73,000 natural images from Microsoft COCO dataset (BIDS/deepprep variant)"
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
requirements: tp.ClassVar[tuple[str, ...]] = (
|
|
654
|
+
"scipy>=1.11.4",
|
|
655
|
+
"h5py>=3.10.0",
|
|
656
|
+
"requests>=2.31.0",
|
|
657
|
+
"pybids",
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
RUNS_PER_SESSION: tp.ClassVar[int] = 12
|
|
661
|
+
CAPTION_SEPARATOR: tp.ClassVar[str] = "\n"
|
|
662
|
+
BIDS_FOLDER: tp.ClassVar[str] = "nsddata_rawdata"
|
|
663
|
+
DERIVATIVES_FOLDER: tp.ClassVar[str] = "derivatives/deepprep/bids"
|
|
664
|
+
BOLD_SPACE: tp.ClassVar[str] = "T1w" # MNI152NLin2009aSym"
|
|
665
|
+
SESSION_SUFFIX: tp.ClassVar[str] = "nsd"
|
|
666
|
+
TR_FMRI_S: tp.ClassVar[float] = 1.6
|
|
667
|
+
DEEPPREP_FSAVERAGE_OUTPUT_DIR: tp.ClassVar[str] = "derivatives/deepprep/bids"
|
|
668
|
+
|
|
669
|
+
_info: tp.ClassVar[study.StudyInfo] = study.StudyInfo(
|
|
670
|
+
num_timelines=3408,
|
|
671
|
+
num_subjects=8,
|
|
672
|
+
num_events_in_query=67,
|
|
673
|
+
event_types_in_query={"Fmri", "Image"},
|
|
674
|
+
data_shape=(79, 103, 83, 188),
|
|
675
|
+
frequency=0.625,
|
|
676
|
+
fmri_spaces={"MNI152NLin2009cAsym", "T1w", "fsaverage", "fsnative"},
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
def _download(self) -> None:
|
|
680
|
+
self._check_nsd_data_access_agreement()
|
|
681
|
+
with success_writer(self.path / "download_all") as already_done:
|
|
682
|
+
if already_done:
|
|
683
|
+
return
|
|
684
|
+
nsd_common_path = get_allen2022massive_common_path(self.path)
|
|
685
|
+
self._download_nsd_raw_dataset()
|
|
686
|
+
self._validate_downloaded_and_fmriprepped_dataset()
|
|
687
|
+
self._prepare_dataset(nsd_common_path)
|
|
688
|
+
self._validate_prepared_dataset(nsd_common_path)
|
|
689
|
+
|
|
690
|
+
def _validate_downloaded_and_fmriprepped_dataset(self) -> None:
|
|
691
|
+
nsd_common_path = get_allen2022massive_common_path(self.path)
|
|
692
|
+
for tl in self._iter_subject_session_run():
|
|
693
|
+
params = dict(tl)
|
|
694
|
+
params.update(ses_suffix=self.SESSION_SUFFIX, task=self.TASK)
|
|
695
|
+
fps = []
|
|
696
|
+
fps.append(
|
|
697
|
+
get_bids_filepath(
|
|
698
|
+
self.path / self.DERIVATIVES_FOLDER,
|
|
699
|
+
filetype="bold",
|
|
700
|
+
data_type="Fmri",
|
|
701
|
+
space=self.BOLD_SPACE,
|
|
702
|
+
**params,
|
|
703
|
+
)
|
|
704
|
+
)
|
|
705
|
+
fps.append(
|
|
706
|
+
get_bids_filepath(
|
|
707
|
+
self.path / self.DERIVATIVES_FOLDER,
|
|
708
|
+
filetype="bold_mask",
|
|
709
|
+
data_type="Fmri",
|
|
710
|
+
space=self.BOLD_SPACE,
|
|
711
|
+
**params,
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
fps.append(
|
|
715
|
+
get_bids_filepath(
|
|
716
|
+
self.path / self.BIDS_FOLDER,
|
|
717
|
+
filetype="events",
|
|
718
|
+
data_type="Fmri",
|
|
719
|
+
**params,
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
filenames = [
|
|
723
|
+
"nsd_expdesign.mat",
|
|
724
|
+
"nsd_stimuli.hdf5",
|
|
725
|
+
"COCO_73k_annots_curated.npy",
|
|
726
|
+
]
|
|
727
|
+
fps.extend([nsd_common_path / filename for filename in filenames])
|
|
728
|
+
for fp in fps:
|
|
729
|
+
if not fp.exists():
|
|
730
|
+
msg = f"Missing file {fp} for {tl}"
|
|
731
|
+
raise RuntimeError(msg)
|
|
732
|
+
|
|
733
|
+
def _validate_prepared_dataset(self, path: Path) -> None: # type: ignore[override]
|
|
734
|
+
self._validate_file_count(path, "nsd_captions", ".npy")
|
|
735
|
+
self._validate_file_count(path, "nsd_stimuli", ".png")
|
|
736
|
+
|
|
737
|
+
def _validate_file_count(self, path: Path, sub_dir: str, extension: str) -> None:
|
|
738
|
+
files = [f for f in (path / sub_dir).iterdir() if f.suffix == extension]
|
|
739
|
+
assert len(files) == self.N_STIMULI, (
|
|
740
|
+
f"There should be {self.N_STIMULI} {extension} files in"
|
|
741
|
+
f" {path / sub_dir} but found only {len(files)}"
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
def _download_nsd_raw_dataset(self) -> None:
|
|
745
|
+
self.path.mkdir(exist_ok=True, parents=True)
|
|
746
|
+
nsd_common_path = get_allen2022massive_common_path(self.path)
|
|
747
|
+
nsd_common_path.mkdir(parents=True, exist_ok=True)
|
|
748
|
+
|
|
749
|
+
aws_cmds = []
|
|
750
|
+
raw_bold_aws_cmd = (
|
|
751
|
+
"aws s3 sync --no-sign-request"
|
|
752
|
+
" s3://natural-scenes-dataset/nsddata_rawdata/"
|
|
753
|
+
f" {self.path}/{self.BIDS_FOLDER}"
|
|
754
|
+
)
|
|
755
|
+
aws_cmds.append(raw_bold_aws_cmd)
|
|
756
|
+
expdesign_mat_aws_cmd = (
|
|
757
|
+
"aws s3 cp --no-sign-request"
|
|
758
|
+
" s3://natural-scenes-dataset/nsddata/experiments/nsd/nsd_expdesign.mat"
|
|
759
|
+
f" {nsd_common_path}"
|
|
760
|
+
)
|
|
761
|
+
aws_cmds.append(expdesign_mat_aws_cmd)
|
|
762
|
+
stimuli_mat_aws_cmd = (
|
|
763
|
+
"aws s3 cp --no-sign-request"
|
|
764
|
+
" s3://natural-scenes-dataset/nsddata_stimuli/stimuli/nsd/nsd_stimuli.hdf5"
|
|
765
|
+
f" {nsd_common_path}"
|
|
766
|
+
)
|
|
767
|
+
aws_cmds.append(stimuli_mat_aws_cmd)
|
|
768
|
+
for aws_cmd in aws_cmds:
|
|
769
|
+
result = subprocess.run(aws_cmd, shell=True, capture_output=True, text=True)
|
|
770
|
+
if result.returncode != 0:
|
|
771
|
+
error_msg = result.stderr if result.stderr else result.stdout
|
|
772
|
+
raise RuntimeError(
|
|
773
|
+
f"AWS command failed with return code {result.returncode}:\n"
|
|
774
|
+
f"Command: {aws_cmd}\n"
|
|
775
|
+
f"Error: {error_msg}\n"
|
|
776
|
+
"Hint: Make sure the AWS CLI is installed and "
|
|
777
|
+
"that the S3 bucket is reachable."
|
|
778
|
+
)
|
|
779
|
+
url = (
|
|
780
|
+
"https://huggingface.co/datasets/pscotti/naturalscenesdataset/resolve/main/"
|
|
781
|
+
"COCO_73k_annots_curated.npy"
|
|
782
|
+
)
|
|
783
|
+
response = requests.get(url)
|
|
784
|
+
(nsd_common_path / "COCO_73k_annots_curated.npy").write_bytes(response.content)
|
|
785
|
+
|
|
786
|
+
def _prepare_dataset(self, path: Path) -> None: # type: ignore[override]
|
|
787
|
+
self._extract_stimuli(path)
|
|
788
|
+
self._extract_captions(path)
|
|
789
|
+
self._extract_test_images_ids(path)
|
|
790
|
+
|
|
791
|
+
def _extract_captions(self, path: Path) -> None:
|
|
792
|
+
path_to_caption_npys = path / "nsd_captions"
|
|
793
|
+
path_to_caption_npys.mkdir(exist_ok=True, parents=True)
|
|
794
|
+
path_to_caption_file = path / "COCO_73k_annots_curated.npy"
|
|
795
|
+
captions = np.load(path_to_caption_file, mmap_mode="r")
|
|
796
|
+
for idx in trange(captions.shape[0]):
|
|
797
|
+
annots_idx = np.array(
|
|
798
|
+
[annot for annot in captions[idx] if len(annot.strip()) > 0]
|
|
799
|
+
)
|
|
800
|
+
np.save(path_to_caption_npys / f"{idx}.npy", annots_idx)
|
|
801
|
+
|
|
802
|
+
def iter_timelines(self) -> tp.Iterator[dict[str, tp.Any]]:
|
|
803
|
+
self._validate_downloaded_and_fmriprepped_dataset()
|
|
804
|
+
for tl in self._iter_subject_session_run():
|
|
805
|
+
yield tl
|
|
806
|
+
|
|
807
|
+
def _load_timeline_events(self, timeline: dict[str, tp.Any]) -> pd.DataFrame:
|
|
808
|
+
fp = get_bids_filepath(
|
|
809
|
+
root_path=self.path / self.DERIVATIVES_FOLDER,
|
|
810
|
+
filetype="bold",
|
|
811
|
+
data_type="Fmri",
|
|
812
|
+
space="T1w",
|
|
813
|
+
ses_suffix=self.SESSION_SUFFIX,
|
|
814
|
+
**timeline,
|
|
815
|
+
)
|
|
816
|
+
fp = fp.parent / (fp.name.split("space", maxsplit=1)[0] + "*")
|
|
817
|
+
|
|
818
|
+
fmri_events = event_utils.expand_bids_fmri(
|
|
819
|
+
str(fp),
|
|
820
|
+
preproc="deepprep",
|
|
821
|
+
start=0.0,
|
|
822
|
+
frequency=self._get_fmri_frequency(),
|
|
823
|
+
)
|
|
824
|
+
bids_events_df_fp = get_bids_filepath(
|
|
825
|
+
root_path=self.path / self.BIDS_FOLDER,
|
|
826
|
+
filetype="events",
|
|
827
|
+
data_type="Fmri",
|
|
828
|
+
ses_suffix=self.SESSION_SUFFIX,
|
|
829
|
+
**timeline,
|
|
830
|
+
)
|
|
831
|
+
bids_events_df = read_bids_events(bids_events_df_fp)
|
|
832
|
+
path_to_stimuli = get_allen2022massive_common_path(self.path) / "nsd_stimuli"
|
|
833
|
+
ns_events_df = self._get_ns_img_events_df(
|
|
834
|
+
bids_events_df, path_to_stimuli, timeline
|
|
835
|
+
)
|
|
836
|
+
return pd.concat([pd.DataFrame(fmri_events), ns_events_df], axis=0)
|
|
837
|
+
|
|
838
|
+
def _get_test_image_ids(self) -> list[int]:
|
|
839
|
+
return np.load(
|
|
840
|
+
get_allen2022massive_common_path(self.path) / "test_images_ids.npy"
|
|
841
|
+
).tolist()
|
|
842
|
+
|
|
843
|
+
def _get_captions(self, image_id: int) -> str:
|
|
844
|
+
if image_id < 0:
|
|
845
|
+
msg = f"Parameter 'image_id' has value {image_id} but should be positive"
|
|
846
|
+
raise ValueError(msg)
|
|
847
|
+
npy_path = (
|
|
848
|
+
get_allen2022massive_common_path(self.path) / f"nsd_captions/{image_id}.npy"
|
|
849
|
+
)
|
|
850
|
+
captions = np.load(npy_path).tolist()
|
|
851
|
+
captions = [cap.replace(self.CAPTION_SEPARATOR, "") for cap in captions]
|
|
852
|
+
return self.CAPTION_SEPARATOR.join(captions)
|
|
853
|
+
|
|
854
|
+
def _get_ns_img_events_df(
|
|
855
|
+
self,
|
|
856
|
+
bids_events_df: pd.DataFrame,
|
|
857
|
+
stimuli_path: str | Path,
|
|
858
|
+
timeline: dict[str, tp.Any],
|
|
859
|
+
) -> pd.DataFrame:
|
|
860
|
+
bids_events = bids_events_df.to_dict("records")
|
|
861
|
+
ns_events = []
|
|
862
|
+
for bids_event in bids_events:
|
|
863
|
+
image_id = bids_event["73k_id"] # 1-based
|
|
864
|
+
ns_event = dict(
|
|
865
|
+
type="Image",
|
|
866
|
+
start=bids_event["onset"],
|
|
867
|
+
duration=bids_event["duration"],
|
|
868
|
+
filepath=str(Path(stimuli_path) / f"{image_id - 1}.png"),
|
|
869
|
+
split="test" if image_id in self._get_test_image_ids() else "train",
|
|
870
|
+
caption=self._get_captions(image_id - 1),
|
|
871
|
+
)
|
|
872
|
+
ns_events.append(ns_event)
|
|
873
|
+
return pd.DataFrame(ns_events)
|
|
874
|
+
|
|
875
|
+
@classmethod
|
|
876
|
+
def _iter_subject_session_run(cls) -> tp.Iterator[dict[str, tp.Any]]:
|
|
877
|
+
for subject in cls.SESSIONS_PER_SUBJECT.keys():
|
|
878
|
+
for session in range(1, cls.SESSIONS_PER_SUBJECT[subject] + 1):
|
|
879
|
+
for run in range(1, cls.RUNS_PER_SESSION + 1):
|
|
880
|
+
yield dict(subject=subject, session=session, task=cls.TASK, run=run)
|
|
881
|
+
|
|
882
|
+
def _get_fmri_frequency(self) -> float:
|
|
883
|
+
return 1.0 / self.TR_FMRI_S
|
|
884
|
+
|
|
885
|
+
|
|
615
886
|
# # # # # mini dataset # # # # #
|
|
616
887
|
|
|
617
888
|
|