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.
Files changed (53) hide show
  1. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/PKG-INFO +2 -2
  2. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/download.py +69 -77
  3. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/allen2022massive.py +271 -0
  4. neuralfetch-0.2.3/neuralfetch/studies/alvarez2022haaglanden.py +163 -0
  5. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/bel2026petit.py +2 -2
  6. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/brennan2019hierarchical.py +38 -15
  7. neuralfetch-0.2.3/neuralfetch/studies/chang2019bold5000.py +278 -0
  8. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/ghassemi2018you.py +9 -4
  9. neuralfetch-0.2.3/neuralfetch/studies/hebart2023things.py +534 -0
  10. neuralfetch-0.2.3/neuralfetch/studies/levy2026noninvasive.py +1061 -0
  11. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/li2022petit.py +1 -1
  12. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/moabb2025.py +5 -6
  13. neuralfetch-0.2.3/neuralfetch/studies/sivakumar2024emg2qwerty.py +189 -0
  14. neuralfetch-0.2.3/neuralfetch/studies/xu2024alljoined.py +163 -0
  15. neuralfetch-0.2.3/neuralfetch/studies/xu2025alljoined.py +210 -0
  16. neuralfetch-0.2.2/neuralfetch/studies/zyma2019eeg.py → neuralfetch-0.2.3/neuralfetch/studies/zyma2019electroencephalograms.py +10 -4
  17. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/test_download.py +120 -71
  18. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/test_moabb2025.py +6 -6
  19. neuralfetch-0.2.3/neuralfetch/test_sivakumar2024emg2qwerty.py +124 -0
  20. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/test_studies.py +44 -3
  21. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/test_utils.py +1 -2
  22. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/utils.py +8 -4
  23. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/PKG-INFO +2 -2
  24. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/SOURCES.txt +10 -3
  25. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/requires.txt +1 -1
  26. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/pyproject.toml +3 -3
  27. neuralfetch-0.2.2/neuralfetch/studies/hebart2023thingsmeg.py +0 -293
  28. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/LICENSE +0 -0
  29. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/README.md +0 -0
  30. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/__init__.py +0 -0
  31. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/py.typed +0 -0
  32. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/__init__.py +0 -0
  33. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/albrecht2019increased.py +0 -0
  34. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/chen2023large.py +0 -0
  35. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/dan2023bids.py +0 -0
  36. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/gifford2022large.py +0 -0
  37. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/grootswagers2022human.py +0 -0
  38. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/hinss2023open.py +0 -0
  39. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/hollenstein2018zuco.py +0 -0
  40. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/kemp2000analysis.py +0 -0
  41. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/kueper2024eeg.py +0 -0
  42. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/liu2024eeg2video.py +0 -0
  43. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/miltiadous2023dice.py +0 -0
  44. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/mumtaz2018machine.py +0 -0
  45. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/nieuwland2018large.py +0 -0
  46. /neuralfetch-0.2.2/neuralfetch/studies/schalk2004bci.py → /neuralfetch-0.2.3/neuralfetch/studies/schalk2004bci2000.py +0 -0
  47. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/shirazi2024hbn.py +0 -0
  48. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/singh2021timing.py +0 -0
  49. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch/studies/tuh_eeg.py +0 -0
  50. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/dependency_links.txt +0 -0
  51. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/entry_points.txt +0 -0
  52. {neuralfetch-0.2.2 → neuralfetch-0.2.3}/neuralfetch.egg-info/top_level.txt +0 -0
  53. {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.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.16
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 f" -J {self.threads}"
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
- Uses the ``eegdash`` library for record discovery and the :class:`S3`
823
- downloader for the actual file transfer.
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 EEGDash # type: ignore[import-not-found]
875
+ from eegdash import EEGDashDataset # type: ignore[import-not-found]
847
876
 
848
- client = EEGDash(database=self.database)
849
- records = client.find(dataset=self.study)
850
- if not records:
851
- raise RuntimeError(
852
- f"No records found for dataset '{self.study}' "
853
- f"in database '{self.database}'"
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>=2025.2.0",)
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(dataset=self.study, target_dir=self._dl_dir, include=self.include)
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 flattened into ``_dl_dir``.
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
- temp_folder = self._dl_dir.parent / "temp"
1217
- self.output_dir = temp_folder
1214
+ self.output_dir = self._dl_dir / self.study / self.version
1218
1215
 
1219
- # Download into a temp directory first because Physionet stores
1220
- # objects under <study>/<version>/, which the S3 base class
1221
- # mirrors on disk. After downloading we flatten by renaming
1222
- # temp/<study>/<version>/ directly into _dl_dir.
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