├── .gitignore ├── LICENSE ├── MEG ├── README_MEG.txt ├── environment_THINGS-MEG.yml ├── eyetracking │ ├── step1_eyetracking_preprocess.py │ ├── step1a_eyetracking_plot_preprocessing.py │ └── step2_eyetracking_plot.py ├── step1_preprocessing.py ├── step2a_data_quality-head_position.py ├── step2b_data_quality-ERFs.py ├── step2c_data_quality-noiseceiling.py ├── step3a_validation_animacy_size.py ├── step3aa_plot_validation_size_animacy.m ├── step3b_validation-fmri_meg_combo.py ├── step3bb_plot_validation_fmri_meg_combo.m ├── step3c_validation_pairwise_decoding_mne_to_cosmo.m ├── step3d_validation_pairwise_decoding200.m ├── step3e_validation_pairwise_decoding1854.m ├── step3f_validation_pairwise_decoding_stack_plot.m ├── step3g_validation_pairwise_decoding_mds.m ├── step3h_validation_rsa_behaviour.m ├── swarm100_categorywise_decoding.sh └── swarm50_imagewisedecoding.sh ├── MRI ├── README.md ├── data │ ├── Categories_final_20200131.tsv │ ├── animacy.csv │ └── size_fixed.csv ├── notebooks │ ├── animacy_size.ipynb │ ├── fmri_usage.ipynb │ └── working_with_rois.ipynb ├── requirements.txt ├── scripts │ ├── filtercfg.json │ ├── neurodocker.sh │ ├── reconall.sh │ └── run_fmriprep.sh ├── setup.py └── thingsmri │ ├── __init__.py │ ├── anc.py │ ├── betas.py │ ├── dataset.py │ ├── glm.py │ ├── localizerGLM_FSL.py │ ├── mds_betas.py │ ├── melodic.py │ ├── prf.py │ ├── reconall.py │ └── utils.py ├── README.md └── assets └── download_button.png /.gitignore: -------------------------------------------------------------------------------- 1 | *scenePRF_Fix.py 2 | __pycache__ 3 | *.egg-info 4 | *.vscode/ 5 | .* 6 | MRI/build 7 | !/.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /MEG/README_MEG.txt: -------------------------------------------------------------------------------- 1 | README MEG 2 | 3 | We collected MEG data along with eyetracking data from four participants in twelve sessions. The BIDS-formatted data can be downloaded from OpenNeuro. 4 | This repository contains all codes for the MEG analysis and the eyetracking analyses. 5 | Running the scripts in order creates all the outputs necessary to recreate the plots shown in the paper. 6 | All codes are command-line executable (tested on Mac and Linux) and are written in Python (find full list of dependencies in environment_THINGS-MEG.yml file) & MATLAB (tested on MATLAB R2022a). 7 | In addition to standard toolboxes, the MATLAB toolbox CoSMoMVPA (https://www.cosmomvpa.org) and mne-matlab(https://github.com/mne-tools/mne-matlab) are required. 8 | 9 | Description of MEG codes: 10 | 1) PREPROCESSING 11 | - step1_preprocessing.py: preprocess the BIDS-formatted data. [call from command line with inputs -participant and -bids_dir] 12 | 2) DATA QUALITY 13 | - step2a_data_quality-head_position.py: extract the head position measurements from the raw meg data and calculate displacement across runs and sessions. [call from command line with inputs -bids_dir] 14 | - step2b_data_quality-ERPs.py: calculate ERFs for the repeat images in every session to see how consistent the data is across sessions. [call from command line with inputs -bids_dir] 15 | - step2c_data_quality-noiseceiling.py: calculate noise ceilings for the repeat images to assess how good the signal can be expected to be in each sensor group. [call from command line with inputs -bids_dir] 16 | 3) VALIDATION 17 | - step3a_validation-animacy_size.py: run a cross-validated linear regression to predict animacy and size ratings from the MEG sensor activation patterns. [call from command line with inputs -bids_dir]. 18 | - step3aa_plot_validation_size_animacy.m: plots the results of the a cross-validated linear regression (step 3a) using MATLAB. [call from command line with inputs bids_dir] 19 | - step3b_validation-fmri_meg_combo.py: run a cross-validated linear regression to predict the univariate response in fMRI ROIs from the MEG sensor activation patterns over time. [call from command line with inputs -bids_dir] 20 | - step_3bb_plot_validation_fmri_meg_combo.m plots the results of the a cross-validated linear regression (step 3b) using MATLAB). [call from command line with inputs bids_dir] 21 | - Step3c_validation_pairwise_decoding_mne_to_cosmo.m: helper script to transform the MNE-preprocessed data into a cosmo struct. [call from command line with inputs bids_dir and toolbox_dir] 22 | - Step3d_validation_pairwise_decoding200.m: function to run the pairwise decoding for the 200 repeat image trials (image-level decoding). [call from command line with inputs bids_dir, toolbox_dir, participant, blocknr, n_blocks. Variable n_blocks is used to parallelize the process. ~20GB memory is needed for each process] 23 | - Step3e_validation_pairwise_decoding1854.m: function to run the pairwise decoding for the 1854 image concepts (concept-level decoding). [call from command line with inputs bids_dir, toolbox_dir, participant, blocknr, n_blocks. Variable n_blocks is used to parallelize the process. ~240GB memory is needed for each process] 24 | - Step3f_validation_pairwise_decoding_stack_plot.m: pairwise decoding in step 3d and 3e was run in parallel chunks. This script combines the chunks together so we can plot, for example, an RDM. [call from command line with inputs bids_dir, toolbox_dir, imagewise_nblocks, sessionwise_nblocks] 25 | - Step3g_validation_pairwise_decoding_mds.m: plotting the results of the pairwise decoding analyses alongside snapshot MDS plots that show distinction between high-level categories. [call from command line with inputs bids_dir and toolbox_dir] 26 | - Step3h_validation_rsa_behaviour.m: correlate the behavioural pairwise similarities with the MEG decoding RDMs and plot. [call from command line with inputs bids_dir and toolbox_dir] 27 | 28 | Descriptions of eyetracking codes (can be found in the eyetracking subfolder) 29 | 1) step1_eyetracking_preprocess.py: extract the x-, y-, and pupil-data from the raw MEG data, preprocess, and epoch. [call from command line with inputs -bids_dir] 30 | 2) step1a_eyetracking_plot_preprocessing.py: plot overview of the preprocessing steps. [call from command line with inputs -bids_dir] 31 | 3) step2_eyetracking_plot.py: load epoched eyetracking data and make the plots shown in the supplementary materials. [call from command line with inputs -bids_dir] 32 | 33 | 34 | 35 | 36 | If you find any errors or if something does not work, please reach out to lina.teichmann@nih.gov 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /MEG/environment_THINGS-MEG.yml: -------------------------------------------------------------------------------- 1 | name: THINGS-MEG 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - aom=3.3.0 7 | - argon2-cffi=21.3.0 8 | - argon2-cffi-bindings=21.2.0 9 | - asttokens=2.0.5 10 | - backcall=0.2.0 11 | - beautifulsoup4=4.11.1 12 | - blas=1.0 13 | - bleach=4.1.0 14 | - blosc=1.21.0 15 | - bottleneck=1.3.4 16 | - brotli=1.0.9 17 | - brotlipy=0.7.0 18 | - brunsli=0.1 19 | - bzip2=1.0.8 20 | - c-ares=1.18.1 21 | - c-blosc2=2.0.4 22 | - ca-certificates=2022.4.26 23 | - cached-property=1.5.2 24 | - cffi=1.15.0 25 | - cfitsio=4.1.0 26 | - charls=2.3.4 27 | - cloudpickle=2.0.0 28 | - colorama=0.4.4 29 | - colorspacious=1.1.2 30 | - cryptography=36.0.0 31 | - curl=7.82.0 32 | - cycler=0.11.0 33 | - cytoolz=0.11.0 34 | - darkdetect=0.5.1 35 | - dask-core=2022.2.1 36 | - dbus=1.13.18 37 | - debugpy=1.5.1 38 | - decorator=5.1.1 39 | - defusedxml=0.7.1 40 | - deprecated=1.2.12 41 | - dipy=1.5.0 42 | - double-conversion=3.2.0 43 | - eigen=3.3.7 44 | - entrypoints=0.4 45 | - executing=0.8.3 46 | - expat=2.4.4 47 | - ffmpeg=4.3.2 48 | - fontconfig=2.13.1 49 | - freetype=2.11.0 50 | - fsspec=2022.2.0 51 | - future=0.18.2 52 | - gettext=0.21.0 53 | - giflib=5.2.1 54 | - gl2ps=1.4.2 55 | - glew=2.1.0 56 | - glib=2.68.4 57 | - glib-tools=2.68.4 58 | - gmp=6.2.1 59 | - gnutls=3.6.15 60 | - gst-plugins-base=1.18.5 61 | - gstreamer=1.18.5 62 | - h5io=0.1.7 63 | - h5py=3.6.0 64 | - hdf4=4.2.15 65 | - hdf5=1.12.1 66 | - icu=68.1 67 | - imagecodecs=2022.2.22 68 | - imageio=2.9.0 69 | - imageio-ffmpeg=0.4.7 70 | - ipycanvas=0.11.0 71 | - ipyevents=2.0.1 72 | - ipykernel=6.9.1 73 | - ipython=8.2.0 74 | - ipython_genutils=0.2.0 75 | - ipyvtklink=0.2.2 76 | - ipywidgets=7.6.5 77 | - jbig=2.1 78 | - jedi=0.18.1 79 | - joblib=1.1.0 80 | - jpeg=9e 81 | - jsoncpp=1.9.5 82 | - jsonschema=4.4.0 83 | - jupyter=1.0.0 84 | - jupyter_client=7.2.2 85 | - jupyter_console=6.4.3 86 | - jupyter_core=4.9.2 87 | - jupyterlab_pygments=0.1.2 88 | - jupyterlab_widgets=1.0.0 89 | - jxrlib=1.1 90 | - krb5=1.19.2 91 | - lame=3.100 92 | - lcms2=2.12 93 | - ld_impl_linux-64=2.35.1 94 | - lerc=3.0 95 | - libaec=1.0.6 96 | - libavif=0.10.0 97 | - libblas=3.9.0 98 | - libbrotlicommon=1.0.9 99 | - libbrotlidec=1.0.9 100 | - libbrotlienc=1.0.9 101 | - libcblas=3.9.0 102 | - libclang=11.1.0 103 | - libcurl=7.82.0 104 | - libdeflate=1.10 105 | - libedit=3.1.20210910 106 | - libev=4.33 107 | - libevent=2.1.10 108 | - libffi=3.3 109 | - libgfortran5=11.2.0 110 | - libglib=2.68.4 111 | - libiconv=1.16 112 | - libidn2=2.3.2 113 | - liblapack=3.9.0 114 | - libllvm11=11.1.0 115 | - libnetcdf=4.8.1 116 | - libnghttp2=1.46.0 117 | - libogg=1.3.5 118 | - libopenblas=0.3.20 119 | - libopus=1.3.1 120 | - libpng=1.6.37 121 | - libpq=13.5 122 | - libsodium=1.0.18 123 | - libssh2=1.9.0 124 | - libtasn1=4.16.0 125 | - libtheora=1.1.1 126 | - libtiff=4.3.0 127 | - libunistring=0.9.10 128 | - libuuid=1.0.3 129 | - libvorbis=1.3.7 130 | - libwebp=1.2.2 131 | - libwebp-base=1.2.2 132 | - libxml2=2.9.12 133 | - libxslt=1.1.33 134 | - libzip=1.8.0 135 | - libzlib=1.2.11 136 | - libzopfli=1.0.3 137 | - llvmlite=0.38.0 138 | - locket=0.2.1 139 | - loguru=0.5.3 140 | - lxml=4.8.0 141 | - lz4-c=1.9.3 142 | - lzo=2.10 143 | - matplotlib-base=3.5.1 144 | - matplotlib-inline=0.1.2 145 | - mffpy=0.7.1 146 | - mistune=0.8.4 147 | - mne-base=1.0.0 148 | - mne-qt-browser=0.3.0 149 | - mock=4.0.3 150 | - munkres=1.1.4 151 | - mysql-common=8.0.28 152 | - mysql-libs=8.0.28 153 | - nbclient=0.5.13 154 | - nbconvert=6.4.4 155 | - nbformat=5.3.0 156 | - ncurses=6.3 157 | - nest-asyncio=1.5.5 158 | - nettle=3.7.3 159 | - networkx=2.7.1 160 | - nibabel=3.2.2 161 | - nilearn=0.9.1 162 | - notebook=6.4.8 163 | - nspr=4.33 164 | - nss=3.74 165 | - numba=0.55.1 166 | - numexpr=2.7.3 167 | - numpy-base=1.21.5 168 | - openh264=2.1.1 169 | - openjpeg=2.4.0 170 | - openssl=1.1.1o 171 | - orjson=3.6.8 172 | - packaging=21.3 173 | - pandocfilters=1.5.0 174 | - parso=0.8.3 175 | - partd=1.2.0 176 | - patsy=0.5.2 177 | - pcre=8.45 178 | - pexpect=4.8.0 179 | - pickleshare=0.7.5 180 | - pip=21.2.4 181 | - pooch=1.6.0 182 | - proj=8.2.1 183 | - prometheus_client=0.13.1 184 | - prompt-toolkit=3.0.20 185 | - prompt_toolkit=3.0.20 186 | - psutil=5.8.0 187 | - ptyprocess=0.7.0 188 | - pugixml=1.11.4 189 | - pure_eval=0.2.2 190 | - pycparser=2.21 191 | - pydicom=2.3.0 192 | - pygments=2.11.2 193 | - pymatreader=0.0.30 194 | - pyopenssl=22.0.0 195 | - pyqt=5.12.3 196 | - pyqt-impl=5.12.3 197 | - pyqt5-sip=4.19.18 198 | - pyqtchart=5.12 199 | - pyqtgraph=0.12.4 200 | - pyqtwebengine=5.12.1 201 | - pyrsistent=0.18.0 202 | - pysocks=1.7.1 203 | - pytables=3.7.0 204 | - python=3.10.4 205 | - python-dateutil=2.8.2 206 | - python-fastjsonschema=2.15.1 207 | - python-picard=0.7 208 | - python_abi=3.10 209 | - pyvista=0.34.0 210 | - pyvistaqt=0.9.0 211 | - pywavelets=1.3.0 212 | - pyzmq=22.3.0 213 | - qdarkstyle=3.0.2 214 | - qt=5.12.9 215 | - qtconsole=5.3.0 216 | - qtpy=2.0.1 217 | - readline=8.1.2 218 | - scikit-image=0.19.2 219 | - scikit-learn=1.0.1 220 | - scooby=0.5.12 221 | - send2trash=1.8.0 222 | - six=1.16.0 223 | - snappy=1.1.9 224 | - soupsieve=2.3.1 225 | - sqlite=3.38.2 226 | - stack_data=0.2.0 227 | - statsmodels=0.13.2 228 | - tbb=2021.5.0 229 | - tbb-devel=2021.5.0 230 | - terminado=0.13.1 231 | - testpath=0.5.0 232 | - threadpoolctl=2.2.0 233 | - tifffile=2021.7.2 234 | - tk=8.6.11 235 | - toolz=0.11.2 236 | - tornado=6.1 237 | - traitlets=5.1.1 238 | - typing-extensions=4.1.1 239 | - typing_extensions=4.1.1 240 | - tzdata=2022a 241 | - utfcpp=3.2.1 242 | - vtk=9.1.0 243 | - wcwidth=0.2.5 244 | - webencodings=0.5.1 245 | - wheel=0.37.1 246 | - widgetsnbextension=3.5.2 247 | - wrapt=1.13.3 248 | - x264=1!161.3030 249 | - xlrd=2.0.1 250 | - xmltodict=0.12.0 251 | - xorg-kbproto=1.0.7 252 | - xorg-libice=1.0.10 253 | - xorg-libsm=1.2.2 254 | - xorg-libx11=1.7.2 255 | - xorg-libxt=1.2.1 256 | - xorg-xproto=7.0.31 257 | - xz=5.2.5 258 | - yaml=0.2.5 259 | - zeromq=4.3.4 260 | - zfp=0.5.5 261 | - zlib=1.2.11 262 | - zstd=1.5.2 263 | - pip: 264 | - appdirs==1.4.4 265 | - attrs==22.1.0 266 | - certifi==2021.10.8 267 | - charset-normalizer==2.1.1 268 | - contourpy==1.0.5 269 | - et-xmlfile==1.1.0 270 | - fonttools==4.37.4 271 | - hv-proc==0.1.0 272 | - idna==3.4 273 | - importlib-resources==5.7.1 274 | - iniconfig==1.1.1 275 | - install==1.3.5 276 | - jinja2==3.1.2 277 | - kiwisolver==1.4.4 278 | - markupsafe==2.1.1 279 | - matplotlib==3.6.0 280 | - mne==1.2.1 281 | - mne-bids==0.10 282 | - numpy==1.23.4 283 | - openpyxl==3.0.9 284 | - pandas==1.5.1 285 | - pillow==9.2.0 286 | - pluggy==1.0.0 287 | - py==1.11.0 288 | - pyctf-lite==1.0 289 | - pyparsing==3.0.9 290 | - pytest==7.1.3 291 | - pytz==2022.5 292 | - pyyaml==6.0 293 | - requests==2.28.1 294 | - scipy==1.9.3 295 | - seaborn==0.12.1 296 | - setuptools==62.3.1 297 | - tomli==2.0.1 298 | - tqdm==4.64.1 299 | - urllib3==1.26.12 300 | -------------------------------------------------------------------------------- /MEG/eyetracking/step1_eyetracking_preprocess.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | @ Lina Teichmann 5 | 6 | INPUTS: 7 | call from command line with following inputs: 8 | -bids_dir 9 | 10 | OUTPUTS: 11 | epoched and cleaned data will be written into the preprocessing directory plus the metadata 12 | 13 | NOTES: 14 | Preprocessing is done for every run separately and is based on Kret et al., 2019: 15 | # - removing invalid samples 16 | # - removing based on dilation speeds 17 | # - removing based on deviation from a fitted line 18 | # - detrending 19 | 20 | """ 21 | 22 | import pandas as pd 23 | import numpy as np 24 | import math, mne, os 25 | from scipy import stats 26 | from scipy.signal import butter,filtfilt 27 | from scipy.interpolate import interp1d 28 | import matplotlib 29 | from matplotlib import gridspec 30 | import matplotlib.pyplot as plt 31 | plt.rcParams["font.family"] = "Helvetica" 32 | matplotlib.rcParams.update({'font.size': 12}) 33 | 34 | 35 | #*****************************# 36 | ### PARAMETERS ### 37 | #*****************************# 38 | n_participants = 4 39 | n_sessions = 12 40 | n_runs = 10 41 | trigger_amplitude = 64 42 | 43 | # MEG info 44 | et_refreshrate = 1200 # eye-tracker signal is recorded with MEG -- so it's the same resolution 45 | meg_refreshrate = 1200 46 | trigger_channel = 'UPPT001' 47 | pd_channel = 'UADC016-2104' 48 | eye_channel = ['UADC009-2104','UADC010-2104','UADC013-2104'] # x, y, pupil 49 | 50 | 51 | # settings for eyetracker to volts conversion 52 | minvoltage = -5 53 | maxvoltage = 5 54 | minrange = -0.2 55 | maxrange = 1.2 56 | screenleft = 0 57 | screenright = 1023 58 | screentop = 0 59 | screenbottom = 767 60 | 61 | # parameters to transform from pixels to degrees 62 | screenwidth_cm = 42 63 | screenheight_cm = 32 64 | screendistance_cm = 75 65 | 66 | screensize_pix = [1024, 768] 67 | pix_per_deg = screensize_pix[0]/(math.degrees(math.atan(screenwidth_cm/2/screendistance_cm)*2)) 68 | stim_size_deg = 10 69 | stim_width = stim_size_deg*pix_per_deg 70 | stim_height = stim_size_deg*pix_per_deg 71 | 72 | 73 | #*****************************# 74 | ### HELPER FUNCTIONS ### 75 | #*****************************# 76 | def pix_to_deg(full_size_pix,screensize_pix,screenwidth_cm,screendistance_cm): 77 | pix_per_cm = screensize_pix[0]/screenwidth_cm 78 | size_cm = full_size_pix/pix_per_cm 79 | dva = math.atan(size_cm/2/screendistance_cm)*2 80 | return np.rad2deg(dva) 81 | 82 | # volts_to_pixels: converts voltages recorded by the MEG to pixels - (0,0) is the middle of the screen 83 | def volts_to_pixels(x,y,pupil,minvoltage,maxvoltage,minrange,maxrange,screenbottom,screenleft,screenright,screentop,scaling_factor): 84 | S_x = ((x-minvoltage)/(maxvoltage-minvoltage))*(maxrange-minrange)+minrange 85 | S_y = ((y-minvoltage)/(maxvoltage-minvoltage))*(maxrange-minrange)+minrange 86 | Xgaze = S_x*(screenright-screenleft+1)+screenleft 87 | Ygaze = S_y*(screenbottom-screentop+1)+screentop 88 | # pupil_n = (pupil-pupil.min())*scaling_factor 89 | return(Xgaze,Ygaze) 90 | 91 | # deviation_calculator: fits a smooth line over the samples and checks how much each sample deviates from it 92 | def deviation_calculator(tv,dia,is_valid,t_interp,smooth_filt_a,smooth_filt_b): 93 | dia_valid = dia[[x and y for x,y in zip(is_valid,~np.isnan(dia))]] 94 | t_valid = tv[[x and y for x,y in zip(is_valid,~np.isnan(dia))]] 95 | interp_f_lin = interp1d(t_valid,dia_valid,kind='linear',bounds_error=False) 96 | interp_f_near = interp1d(t_valid,dia_valid,kind='nearest',fill_value='extrapolate') 97 | extrapolated = interp_f_near(t_interp) 98 | uniform_baseline = interp_f_lin(t_interp) 99 | uniform_baseline[np.isnan(uniform_baseline)] = extrapolated[np.isnan(uniform_baseline)] 100 | smooth_uniform_baseline = filtfilt(smooth_filt_b,smooth_filt_a,uniform_baseline) 101 | interp_f_baseline = interp1d(t_interp,smooth_uniform_baseline,kind='linear',bounds_error=False) 102 | smooth_baseline = interp_f_baseline(tv) 103 | dev = np.abs(dia-smooth_baseline) 104 | 105 | return(dev,smooth_baseline) 106 | 107 | # expand_gap: this pads significanly large gaps (>75ms). Before the gap we padded 100ms, after the gap for 150ms (based on Matthias Nau pipeline in NSD paper) 108 | def expand_gap(tv,is_valid): 109 | min_gap_width = 75 110 | max_gap_width = 2000 111 | pad_back = 100 112 | pad_forward = 150 113 | valid_t = tv[is_valid] 114 | valid_idx = np.where(is_valid)[0] 115 | gaps = np.diff(valid_t) 116 | needs_padding = [x and y for x,y in zip(gaps>min_gap_width,gaps 500: 124 | pb = pad_back * 2 125 | pf = pad_forward * 2 126 | else: 127 | pb = pad_back 128 | pf = pad_forward 129 | remove_idx.extend(np.where([x and y for x,y in zip(valid_t>(i_start-pb),valid_t<(i_end+pf))])[0]) 130 | remove_idx = np.unique(remove_idx) 131 | 132 | if remove_idx.any(): 133 | is_valid[valid_idx[remove_idx]] = False 134 | return is_valid 135 | 136 | # remove_loners: see whether there are any chunks of data that are temporally isolated and relatively short. If yes, exclude them. 137 | def remove_loners(is_valid,et_refreshrate): 138 | lonely_sample_max_length = 100 #in ms 139 | time_separation = 40 #in ms 140 | valid_idx = np.where(is_valid)[0] 141 | gap_start = valid_idx[np.where(np.pad(np.diff(valid_idx),(0,1),constant_values=1)>1)] 142 | gap_end = valid_idx[np.where(np.pad(np.diff(valid_idx),(1,0),constant_values=1)>1)] 143 | start_valid_idx = [valid_idx[0]] 144 | end_valid_idx = [valid_idx[-1]] 145 | valid_data_chunks = np.reshape(np.sort(np.concatenate([start_valid_idx,gap_start,gap_end,end_valid_idx])),[-1,2]) 146 | size_valid_data_chunks = np.diff(valid_data_chunks,axis=1) 147 | size_idx = np.where((size_valid_data_chunks/et_refreshrate*1000)(time_separation*1/et_refreshrate) for i in separation],(1,0)))[0] 150 | data_chunks_to_delete = valid_data_chunks[np.intersect1d(sep_idx,size_idx)] 151 | 152 | valid_out = is_valid.copy() 153 | for i in data_chunks_to_delete: 154 | valid_out[np.arange(i[0],i[1]+1)] = 0 155 | 156 | print('removed ' + str(is_valid.sum()-valid_out.sum()) + ' samples') 157 | 158 | return valid_out.astype(bool) 159 | 160 | 161 | #*****************************# 162 | ### DATA LOADING FUNCTIONS ### 163 | #*****************************# 164 | # This function loads the data, if "iseyes" is true it will return the raw data for the eyetracking channels, otherwise it will return the raw data for the optical sensor to make the epochs 165 | def load_raw_data(rootdir,p,s,r,trigger_channel,pd_channel,eye_channel,iseyes): 166 | sess_num = str(s+1) 167 | run_num = str(r+1) 168 | data_dir = rootdir + '/sub-BIGMEG' + str(p) 169 | data_ses_dir = data_dir + '/ses-' + sess_num.zfill(2) + '/meg' 170 | meg_fn = data_ses_dir + '/sub-BIGMEG' + str(p) + '_ses-' + sess_num.zfill(2) + '_task-main_run-' + run_num.zfill(2) + '_meg.ds' 171 | print('loading participant ' + str(p) + ' session ' + sess_num + ' run ' + run_num + '...') 172 | 173 | raw = mne.io.read_raw_ctf(meg_fn,preload=True) 174 | if iseyes: 175 | raw_eyes = raw.copy().pick_channels([eye_channel[0],eye_channel[1],eye_channel[2]]) 176 | raw_eyes.get_data() 177 | print(raw_eyes.ch_names) 178 | return raw_eyes 179 | 180 | else: 181 | raw_triggers = raw.copy().pick_channels([pd_channel]) 182 | raw_triggers.get_data() 183 | return raw_triggers 184 | 185 | # The MEG saves the data in volts. Here we convert the volts to pixels for x/y and return a dataframe for easier handling 186 | # I'm also median centering the data in case there was some drift over the session 187 | def raw2df(raw_et,minvoltage,maxvoltage,minrange,maxrange,screenbottom,screenleft,screenright,screentop,screensize_pix): 188 | raw_et_df = pd.DataFrame(raw_et._data.T,columns=['x_volts','y_volts','pupil']) 189 | # not scaling the pupil anymore 190 | raw_et_df['x'],raw_et_df['y'] = volts_to_pixels(raw_et_df['x_volts'],raw_et_df['y_volts'],raw_et_df['pupil'],minvoltage,maxvoltage,minrange,maxrange,screenbottom,screenleft,screenright,screentop,scaling_factor=978.982673828819) 191 | raw_et_df['x'] = raw_et_df['x']-screensize_pix[0]/2 192 | raw_et_df['x'] = raw_et_df['x']-np.median(raw_et_df['x']) 193 | raw_et_df['y'] = raw_et_df['y']-screensize_pix[1]/2 194 | raw_et_df['y'] = raw_et_df['y']-np.median(raw_et_df['y']) 195 | raw_et_df['pupil'] = raw_et_df['pupil']-np.median(raw_et_df['pupil']) 196 | return raw_et_df 197 | 198 | 199 | #*****************************# 200 | ### PREPROC FUNCTIONS ### 201 | #*****************************# 202 | # Step 1: We are removing all samples that either go beyond the stimulus height or width (10 degrees), or where the recorded pupil size is outside of the recording range (+/-5 volts) 203 | def remove_invalid_samples(eyes,tv): 204 | withinwidth = np.abs(eyes['x'])<(stim_width/2) 205 | withinheight = np.abs(eyes['y'])<(stim_height/2) 206 | is_valid = np.array([x and y for x,y in zip(withinwidth,withinheight)]).astype(bool) 207 | if not any(is_valid): 208 | is_valid = remove_loners(is_valid,et_refreshrate) 209 | is_valid = expand_gap(tv,is_valid) 210 | 211 | return is_valid.astype(bool) 212 | 213 | # Step 2: Checking how much the pupil dliation changes from timepoint to timepoint and exclude timepoints where the dilation change is large 214 | def madspeedfilter(tv,dia,is_valid): 215 | max_gap = 200 216 | dilation = dia[is_valid] 217 | cur_tv = tv[is_valid] 218 | cur_dia_speed = np.diff(dilation)/np.diff(cur_tv) 219 | cur_dia_speed[np.diff(cur_tv)>max_gap] = np.nan 220 | 221 | back_dilation = np.pad(cur_dia_speed,(1,0),constant_values=np.nan) 222 | fwd_dilation = np.pad(cur_dia_speed,(0,1),constant_values=np.nan) 223 | back_fwd_dilation = np.vstack([back_dilation,fwd_dilation]) 224 | 225 | max_dilation_speed = np.empty_like(dia) 226 | max_dilation_speed[is_valid] = np.nanmax(np.abs(back_fwd_dilation),axis=0) 227 | max_dilation_speed 228 | 229 | mad = np.nanmedian(np.abs(max_dilation_speed-np.nanmedian(max_dilation_speed))) 230 | mad_multiplier = 16 # as defined in Kret et al., 2019 231 | if mad == 0: 232 | print('mad is 0, using dilation speed plus constant as threshold') 233 | threshold = np.nanmedian(max_dilation_speed)+mad_multiplier 234 | else: 235 | threshold = np.nanmedian(max_dilation_speed)+mad_multiplier*mad 236 | print('threshold: ' + str(threshold)) 237 | 238 | 239 | valid_out = is_valid.copy() 240 | 241 | valid_out[max_dilation_speed>=threshold] = False 242 | valid_out = remove_loners(valid_out.astype(bool),et_refreshrate) 243 | valid_out = expand_gap(tv,valid_out) 244 | valid_out = remove_loners(valid_out.astype(bool),et_refreshrate) 245 | 246 | return valid_out.astype(bool) 247 | 248 | # Step 3: Fitting a smooth line and exclude samples that deviate from that fitted line 249 | def mad_deviation(tv,dia,is_valid): 250 | n_passes = 4 251 | mad_multiplier = 16 252 | interp_fs = 100 253 | lowpass_cf = 16 254 | [smooth_filt_b,smooth_filt_a] = butter(1,lowpass_cf/(interp_fs/2)) 255 | t_interp = np.arange(tv[0],tv[-1],1000/lowpass_cf) 256 | dia[~is_valid] = np.nan 257 | is_valid_running = is_valid.copy() 258 | residuals_per_pass = np.empty([len(is_valid),n_passes]) 259 | smooth_baseline_per_pass = np.empty([len(is_valid),n_passes]) 260 | 261 | is_done = False 262 | for pass_id in range(n_passes): 263 | if is_done: 264 | break 265 | is_valid_start = is_valid_running.copy() 266 | 267 | residuals_per_pass[:,pass_id], smooth_baseline_per_pass[:,pass_id] = deviation_calculator(tv,dia,[x and y for x, y in zip(is_valid_running, is_valid)],t_interp,smooth_filt_a,smooth_filt_b) 268 | 269 | mad = np.nanmedian(np.abs(residuals_per_pass[:,pass_id]-np.nanmedian(residuals_per_pass[:,pass_id]))) 270 | threshold = np.nanmedian(residuals_per_pass[:,pass_id])+mad_multiplier*mad 271 | 272 | is_valid_running = [x and y for x,y in zip((residuals_per_pass[:,pass_id] <= threshold), is_valid)] 273 | is_valid_running = remove_loners(np.array(is_valid_running).astype(bool),et_refreshrate) 274 | is_valid_running = expand_gap(tv,np.array(is_valid_running).astype(bool)) 275 | 276 | if (pass_id>0 and np.all(is_valid_start==is_valid_running)): 277 | is_done = True 278 | valid_out = is_valid_running 279 | return valid_out.astype(bool) 280 | 281 | 282 | # This is the last step of the preocessing, all invalid samples are removed and the data is detrended 283 | def remove_invalid_detrend(eyes_in,is_valid,isdetrend): 284 | all_tp = np.arange(len(eyes_in)) 285 | eyes_in[~is_valid] = np.nan 286 | if isdetrend: 287 | m, b, _, _, _ = stats.linregress(all_tp[is_valid],eyes_in[is_valid]) 288 | eyes_in = eyes_in - (m*all_tp + b) 289 | return eyes_in 290 | 291 | 292 | #*****************************# 293 | ### COMMAND LINE INPUTS ### 294 | #*****************************# 295 | if __name__=='__main__': 296 | import argparse 297 | parser = argparse.ArgumentParser() 298 | 299 | parser.add_argument( 300 | "-bids_dir", 301 | required=True, 302 | help='path to bids root', 303 | ) 304 | 305 | args = parser.parse_args() 306 | 307 | bids_dir = args.bids_dir 308 | preprocdir = f'{bids_dir}/derivatives/preprocessed/' 309 | figdir = f'{bids_dir}/derivatives/figures/' 310 | resdir = f'{bids_dir}/derivatives/output/' 311 | 312 | if not os.path.exists(preprocdir): 313 | os.makedirs(preprocdir) 314 | if not os.path.exists(figdir): 315 | os.makedirs(figdir) 316 | if not os.path.exists(resdir): 317 | os.makedirs(resdir) 318 | 319 | ####### Run preprocessing ######## 320 | for p in range(1,n_participants+1): 321 | sa = pd.read_csv(f'{bids_dir}/sourcedata/sample_attributes_P{str(p)}.csv') 322 | sessions = [0]*n_sessions 323 | 324 | for s in range(n_sessions): 325 | for r in range(n_runs): 326 | # load raw eye-tracking data from the MEG 327 | raw_eyes = load_raw_data(rootdir=bids_dir,p=p,s=s,r=r,trigger_channel=trigger_channel,pd_channel=pd_channel,eye_channel=eye_channel,iseyes=True) 328 | 329 | # now we are getting the onsets from the photodiode channel 330 | raw_photodiode = load_raw_data(bids_dir,p,s,r,trigger_channel,pd_channel,eye_channel,False) 331 | photo_d = np.where(np.diff([0]+raw_photodiode._data[0])>1.5) 332 | pd_new= photo_d[0][np.where(np.diff([0]+photo_d[0])>1000)] 333 | 334 | # cut raw-eyes so that you don't keep all the data after the end of the run 335 | raw_eyes_cut = raw_eyes.copy() 336 | start = pd_new[0]*1/meg_refreshrate-0.2 337 | end = pd_new[-1]*1/meg_refreshrate+2 338 | raw_eyes_cut.crop(tmin=start, tmax=end, include_tmax=True) 339 | pd_new= pd_new-raw_eyes_cut.first_samp 340 | 341 | # transform MNE-struct to pandas and change from volts to degrees (x,y) and area (pupil) 342 | eyes = raw2df(raw_eyes_cut,minvoltage,maxvoltage,minrange,maxrange,screenbottom,screenleft,screenright,screentop,screensize_pix) 343 | 344 | # Define parameters 345 | tv=(eyes.index.to_numpy()*1/meg_refreshrate)*1000 346 | dia = eyes['pupil'].copy().to_numpy() 347 | 348 | # PREPROCESSING 349 | # Step 1: remove out of bounds 350 | isvalid1 = remove_invalid_samples(eyes,tv) 351 | 352 | # Step 2: speed dilation exclusion 353 | isvalid2 = madspeedfilter(tv,dia,isvalid1) 354 | 355 | # Step 3: deviation from smooth line 356 | isvalid3 = mad_deviation(tv,dia,isvalid2) 357 | 358 | # remove invalid and detrend 359 | eyes_preproc_meg = eyes.copy() 360 | eyes_preproc_meg['x'] = remove_invalid_detrend(eyes_preproc_meg['x'].to_numpy(),isvalid3,True) 361 | eyes_preproc_meg['x'] = [pix_to_deg(i,screensize_pix,screenwidth_cm,screendistance_cm) for i in eyes_preproc_meg['x']] 362 | 363 | eyes_preproc_meg['y'] = remove_invalid_detrend(eyes_preproc_meg['y'].to_numpy(),isvalid3,True) 364 | eyes_preproc_meg['y'] = [pix_to_deg(i,screensize_pix,screenwidth_cm,screendistance_cm) for i in eyes_preproc_meg['y']] 365 | 366 | eyes_preproc_meg['pupil'] = remove_invalid_detrend(eyes_preproc_meg['pupil'].to_numpy(),isvalid3,True) 367 | 368 | # Replace data with preprocessed data 369 | preprocessed_eyes = raw_eyes.copy() 370 | preprocessed_eyes._data = eyes_preproc_meg.loc[:,['x','y','pupil']].to_numpy().T 371 | 372 | 373 | # make epochs based on photodiode 374 | event_dict = {'onset_pd':4} 375 | ev_pd = np.empty(shape=(len(pd_new),3),dtype=int) 376 | for i,ev in enumerate(pd_new): 377 | ev_pd[i]=([int(ev),0,4]) 378 | 379 | if r == 0: 380 | epochs = mne.Epochs(preprocessed_eyes,ev_pd,event_id = event_dict, tmin = -0.1, tmax = 1.3, baseline=None,preload=False) 381 | if r > 0: 382 | epochs_1 = mne.Epochs(preprocessed_eyes,ev_pd,event_id = event_dict, tmin = -0.1, tmax = 1.3, baseline=None,preload=False) 383 | epochs_1.info['dev_head_t'] = epochs.info['dev_head_t'] 384 | epochs = mne.concatenate_epochs([epochs,epochs_1]) 385 | 386 | 387 | # add metadata and get rid of catch trials 388 | epochs.metadata = sa.loc[sa.session_nr==s+1,:] 389 | epochs = epochs[(epochs.metadata['trial_type']!='catch')] 390 | # save as dataframe 391 | tmp = pd.DataFrame(np.repeat(epochs.metadata.values,len(epochs.times), axis=0)) 392 | tmp.columns = epochs.metadata.columns 393 | tosave = pd.concat([epochs.to_data_frame(),tmp],axis=1) 394 | tosave.to_csv(preprocdir + '/eyes_epoched_cleaned_P' + str(p) + '_S' + str(s+1) + '.csv') 395 | -------------------------------------------------------------------------------- /MEG/eyetracking/step1a_eyetracking_plot_preprocessing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | @ Lina Teichmann 5 | 6 | INPUTS: 7 | call from command line with following inputs: 8 | -bids_dir 9 | 10 | OUTPUTS: 11 | makes an overview plot of the eyetracking preprocessing steps 12 | 13 | """ 14 | # step1_eyetracking_preprocess.py> 15 | from step1_eyetracking_preprocess import * 16 | from matplotlib import gridspec 17 | import pandas as pd 18 | import numpy as np 19 | import mne 20 | 21 | def make_overview_plot(bids_dir, figdir,p,s,r): 22 | sa = pd.read_csv(f'{bids_dir}/sourcedata/sample_attributes_P{str(p)}.csv') 23 | raw_eyes = load_raw_data(rootdir=bids_dir,p=p,s=s,r=r,trigger_channel=trigger_channel,pd_channel=pd_channel,eye_channel=eye_channel,iseyes=True) 24 | 25 | # now we are getting the onsets from the photodiode channel 26 | raw_photodiode = load_raw_data(bids_dir,p,s,r,trigger_channel,pd_channel,eye_channel,False) 27 | photo_d = np.where(np.diff([0]+raw_photodiode._data[0])>1.5) 28 | pd_new= photo_d[0][np.where(np.diff([0]+photo_d[0])>1000)] 29 | 30 | # cut raw-eyes so that you don't keep all the data after the end of the run 31 | raw_eyes_cut = raw_eyes.copy() 32 | start = pd_new[0]*1/1200-0.2 33 | end = pd_new[-1]*1/1200+2 34 | raw_eyes_cut.crop(tmin=start, tmax=end, include_tmax=True) 35 | pd_new= pd_new-raw_eyes_cut.first_samp 36 | 37 | # transform MNE-struct to pandas and change from volts to degrees (x,y) and area (pupil) 38 | eyes = raw2df(raw_eyes_cut,minvoltage,maxvoltage,minrange,maxrange,screenbottom,screenleft,screenright,screentop,screensize_pix) 39 | 40 | # Define parameters 41 | tv=(eyes.index.to_numpy()*1/1200)*1000 42 | dia = eyes['pupil'].copy().to_numpy() 43 | 44 | # PREPROCESSING 45 | # Step 1: remove out of bounds 46 | isvalid1 = remove_invalid_samples(eyes,tv) 47 | 48 | # Step 2: speed dilation exclusion 49 | isvalid2 = madspeedfilter(tv,dia,isvalid1) 50 | 51 | # Step 3: deviation from smooth line 52 | isvalid3 = mad_deviation(tv,dia,isvalid2) 53 | 54 | # remove invalid and detrend 55 | eyes_preproc_meg = eyes.copy() 56 | eyes_preproc_meg['x'] = remove_invalid_detrend(eyes_preproc_meg['x'].to_numpy(),isvalid3,True) 57 | eyes_preproc_meg['x'] = [pix_to_deg(i,screensize_pix,screenwidth_cm,screendistance_cm) for i in eyes_preproc_meg['x']] 58 | 59 | eyes_preproc_meg['y'] = remove_invalid_detrend(eyes_preproc_meg['y'].to_numpy(),isvalid3,True) 60 | eyes_preproc_meg['y'] = [pix_to_deg(i,screensize_pix,screenwidth_cm,screendistance_cm) for i in eyes_preproc_meg['y']] 61 | 62 | eyes_preproc_meg['pupil'] = remove_invalid_detrend(eyes_preproc_meg['pupil'].to_numpy(),isvalid3,True) 63 | 64 | # Replace data with preprocessed data 65 | preprocessed_eyes = raw_eyes.copy() 66 | preprocessed_eyes._data = eyes_preproc_meg.loc[:,['x','y','pupil']].to_numpy().T 67 | 68 | 69 | # make epochs based on photodiode 70 | event_dict = {'onset_pd':4} 71 | ev_pd = np.empty(shape=(len(pd_new),3),dtype=int) 72 | for i,ev in enumerate(pd_new): 73 | ev_pd[i]=([int(ev),0,4]) 74 | 75 | epochs = mne.Epochs(preprocessed_eyes,ev_pd,event_id = event_dict, tmin = -0.1, tmax = 1.3, baseline=None,preload=False) 76 | 77 | epochs.metadata = sa.loc[(sa.session_nr==s+1)&(sa.run_nr==r+1),:] 78 | epochs = epochs[(epochs.metadata['trial_type']!='catch')] 79 | 80 | # save as dataframe 81 | tmp = pd.DataFrame(np.repeat(epochs.metadata.values,len(epochs.times), axis=0)) 82 | tmp.columns = epochs.metadata.columns 83 | tosave = pd.concat([epochs.to_data_frame(),tmp],axis=1) 84 | 85 | ### FIGURE 1 ##### 86 | fig = plt.figure() 87 | fig.set_figheight(8) 88 | fig.set_figwidth(15) 89 | spec = gridspec.GridSpec(ncols=2, nrows=5,width_ratios=[3, 1], wspace=0.1,hspace=0.7) 90 | 91 | def plot_run(toplot,ax,ylabel,xlabel,title,n_samples,is_preprocessed): 92 | print(is_preprocessed) 93 | if is_preprocessed==0: 94 | toplot = [pix_to_deg(i,screensize_pix,screenwidth_cm,screendistance_cm) for i in toplot] 95 | 96 | ax.plot(np.take(toplot,np.arange(n_samples)),'grey') 97 | ax.set_xlabel(xlabel) 98 | ax.set_ylabel(ylabel) 99 | ax.legend(frameon=False) 100 | ax.spines['right'].set_visible(False) 101 | ax.spines['top'].set_visible(False) 102 | ax.set_title(title) 103 | ax.set_ylim([-3,3]) 104 | 105 | 106 | selection = np.repeat([None, isvalid1, isvalid2, isvalid3],2) 107 | titles = np.repeat(['raw','step1: invalid samples exclusion', 'step2: dilation speed exclusion', 'step3: deviation exclusion'],2) 108 | for i,v in enumerate(range(len(titles))): 109 | print(i,v) 110 | ax = fig.add_subplot(spec[v]) 111 | tmp = eyes.copy() 112 | if (selection[i] is not None): 113 | empt = np.ones(len(eyes)) 114 | empt[selection[i]] = 0 115 | tmp.loc[empt.astype(bool),:] = np.nan 116 | 117 | if i % 2 == 0: 118 | plot_run(tmp.y,ax,'y (\N{DEGREE SIGN})','',titles[i],len(tmp),False) 119 | else: 120 | plot_run(tmp.y,ax,'y (\N{DEGREE SIGN})','',titles[i],20000,False) 121 | ax.axes.yaxis.set_visible(False) 122 | ax.axes.xaxis.set_visible(False) 123 | 124 | 125 | ax = fig.add_subplot(spec[8]) 126 | plot_run(eyes_preproc_meg['y'],ax,'y (\N{DEGREE SIGN})','samples (1200 Hz)','step4: linear detrending',len(eyes_preproc_meg),True) 127 | 128 | ax = fig.add_subplot(spec[9]) 129 | plot_run(eyes_preproc_meg['y'],ax,'y (\N{DEGREE SIGN})','samples (1200 Hz)','step4: linear detrending',20000,True) 130 | ax.axes.yaxis.set_visible(False) 131 | 132 | fig.savefig(f'{figdir}/ET_preprocess_overview.png',dpi=600) 133 | 134 | 135 | 136 | 137 | ### FIGURE 2 ##### 138 | # only plot pre-processing and post-processsing example 139 | eyes = raw2df(raw_eyes_cut,minvoltage,maxvoltage,minrange,maxrange,screenbottom,screenleft,screenright,screentop,screensize_pix) 140 | 141 | fig = plt.figure() 142 | fig.set_figheight(2) 143 | fig.set_figwidth(5) 144 | spec = gridspec.GridSpec(ncols=2, nrows=2,width_ratios=[1, 1], wspace=0.1,hspace=0.25,bottom=0.22,left=0.14) 145 | 146 | samples = 30000 147 | 148 | def plot_run(toplot,ax,ylabel,xlabel,title,n_samples,is_preprocessed): 149 | y = np.take(toplot,np.arange(n_samples)) 150 | x = np.arange(len(y))*(1/1200) 151 | if is_preprocessed==1: 152 | ax.plot(x,y,'darkgrey',lw=1) 153 | else: 154 | y= [pix_to_deg(i,screensize_pix,screenwidth_cm,screendistance_cm) for i in y] 155 | ax.plot(x,y,'lightgrey',lw=1) 156 | ax.set_xlabel(xlabel) 157 | ax.set_ylabel(ylabel) 158 | ax.legend(frameon=False) 159 | ax.spines['right'].set_visible(False) 160 | ax.spines['top'].set_visible(False) 161 | ax.set_title(title) 162 | 163 | ax = fig.add_subplot(spec[0]) 164 | tmp = eyes.copy() 165 | plot_run(tmp.y,ax,'','','',len(tmp),False) 166 | ax.axes.xaxis.set_visible(False) 167 | ax.set_ylim([-10,10]) 168 | 169 | ax = fig.add_subplot(spec[1]) 170 | plot_run(tmp.y,ax,'y (\N{DEGREE SIGN})','','',samples,False) 171 | ax.axes.yaxis.set_visible(False) 172 | ax.axes.xaxis.set_visible(False) 173 | ax.set_ylim([-10,10]) 174 | 175 | ax = fig.add_subplot(spec[2]) 176 | plot_run(eyes_preproc_meg['y'],ax,'','time (s)','',len(eyes_preproc_meg),True) 177 | ax.set_ylim([-1,1]) 178 | 179 | ax = fig.add_subplot(spec[3]) 180 | plot_run(eyes_preproc_meg['y'],ax,'y (\N{DEGREE SIGN})','time (s)','',samples,True) 181 | ax.axes.yaxis.set_visible(False) 182 | ax.set_ylim([-1,1]) 183 | 184 | fig.supylabel(' y (\N{DEGREE SIGN})') 185 | 186 | fig.savefig(f'{figdir}/supplementary_ET_preprocess.png',dpi=600) 187 | 188 | 189 | 190 | 191 | #*****************************# 192 | ### COMMAND LINE INPUTS ### 193 | #*****************************# 194 | if __name__=='__main__': 195 | import argparse 196 | parser = argparse.ArgumentParser() 197 | 198 | parser.add_argument( 199 | "-bids_dir", 200 | required=True, 201 | help='path to bids root', 202 | ) 203 | 204 | args = parser.parse_args() 205 | 206 | bids_dir = args.bids_dir 207 | figdir = f'{bids_dir}/derivatives/figures/' 208 | 209 | if not os.path.exists(figdir): 210 | os.makedirs(figdir) 211 | 212 | 213 | make_overview_plot(bids_dir,figdir,p=1,s=0,r=0) 214 | -------------------------------------------------------------------------------- /MEG/eyetracking/step2_eyetracking_plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | @ Lina Teichmann 5 | 6 | INPUTS: 7 | call from command line with following inputs: 8 | -bids_dir 9 | 10 | OUTPUTS: 11 | - plot showing percentage of data we lost after preprocessing 12 | - plot showing the time-resolved euclidean x/y coordinates from the central fixation 13 | - plot showing the time-resolved pupil size across all sessions 14 | - plot showing the gaze position along with a threshold inlet 15 | 16 | """ 17 | 18 | import pandas as pd 19 | import numpy as np 20 | import matplotlib.pyplot as plt 21 | import seaborn as sns 22 | import os 23 | 24 | n_participants = 4 25 | n_sessions = 12 26 | colors = ['mediumseagreen','steelblue','goldenrod','indianred'] 27 | plt.rcParams['font.size'] = '14' 28 | plt.rcParams['font.family'] = 'Helvetica' 29 | 30 | def lighten_color(color, amount=0.5): 31 | import matplotlib.colors as mc 32 | import colorsys 33 | try: 34 | c = mc.cnames[color] 35 | except: 36 | c = color 37 | c = colorsys.rgb_to_hls(*mc.to_rgb(c)) 38 | return colorsys.hls_to_rgb(c[0], 1 - amount * (1 - c[1]), c[2]) 39 | 40 | 41 | def load_data(preprocdir): 42 | invalid_samples = np.zeros([n_participants,n_sessions]) 43 | data = [[[] for i in range(n_sessions)] for j in range(n_participants)] 44 | for p in range(n_participants): 45 | print('participant ' + str(p+1)) 46 | for s in range(n_sessions): 47 | print('loading session ' + str(s+1)) 48 | sess_dat = pd.read_csv(preprocdir + '/eyes_epoched_cleaned_P' + str(p+1) + '_S' + str(s+1) + '.csv') 49 | sess_dat.rename(columns={"UADC009-2104": "x", "UADC010-2104": "y","UADC013-2104":"pupil"},inplace=True) 50 | 51 | invalid_samples[p,s] = (np.mean([np.isnan(sess_dat.loc[sess_dat.run_nr==i,'x']).sum()/len(sess_dat.loc[sess_dat.run_nr==i,'x']) for i in range(1,11)])) 52 | 53 | tv = np.linspace(-100,1300,len(np.where(sess_dat.image_nr==sess_dat.image_nr.iloc[0])[0])) 54 | sess_dat['time_samples'] =np.concatenate([np.arange(len(tv))]*sum(sess_dat.time==-100)) 55 | 56 | data[p][s]=sess_dat 57 | 58 | return data,invalid_samples 59 | 60 | 61 | def make_plots(data,invalid_samples,figdir): 62 | # plot invalid data 63 | fig,axs = plt.subplots(ncols=1, nrows=1,num=10,tight_layout=True,figsize = (4,3)) 64 | 65 | [plt.plot(np.arange(n_sessions),np.sort(invalid_samples[i])*100,color=colors[i],marker='o',lw=1,label='M'+ str(i+1)) for i in range(n_participants)] 66 | plt.ylabel('proportion removed (%)') 67 | plt.legend(loc='upper left',ncol=2,frameon=False) 68 | 69 | ax=plt.gca() 70 | ax.spines['top'].set_visible(False) 71 | ax.spines['right'].set_visible(False) 72 | ax.set_xticks(np.arange(12)) 73 | ax.set_xticklabels([]) 74 | ax.set_xlabel('sessions (sorted)') 75 | 76 | ax.set_ylim([0,80]) 77 | fig.set_size_inches(5,3) 78 | 79 | fig.savefig(figdir + '/supplementary_ET_invalid.pdf',dpi=600) 80 | 81 | 82 | # plot time-resolved euclidean distance of the x/y position from the central fixation 83 | euc_dist_all,pupil_size_all = [],[] 84 | for p in range(n_participants): 85 | curr_dat = pd.concat(data[p],axis=0) 86 | euc_dist = pd.DataFrame(columns = np.unique(curr_dat.time_samples)) 87 | pupil_size = pd.DataFrame(columns = np.unique(curr_dat.time_samples)) 88 | 89 | for t in np.unique(curr_dat.time_samples): 90 | print(t) 91 | x = curr_dat.loc[curr_dat.time_samples==t].x.to_numpy() 92 | y = curr_dat.loc[curr_dat.time_samples==t].y.to_numpy() 93 | euc_dist[t] = np.sqrt((x-0)**2+(y-0)**2) 94 | euc_dist.reset_index(drop=True,inplace=True) 95 | pupil_size[t] = curr_dat.loc[curr_dat.time_samples==t].pupil.to_numpy() 96 | 97 | euc_dist_all.append(euc_dist) 98 | pupil_size_all.append(pupil_size) 99 | 100 | tv = ((np.unique(data[0][0].time_samples.to_numpy())*1/1200)-0.1)*1000 101 | 102 | plt.close('all') 103 | fig,ax = plt.subplots(ncols=1, nrows=1,num=1,tight_layout=True,figsize = (6,4),sharex=True,sharey=True) 104 | 105 | for p in range(n_participants): 106 | toplot = euc_dist_all[p] 107 | baseline = toplot[np.where(tv<=0)[0]].mean(axis=1) 108 | toplot = toplot.sub(baseline,axis=0) 109 | ax.plot(tv,np.mean(toplot,axis=0),color=colors[p],label='M'+str(p+1)) 110 | ax.fill_between(tv, np.mean(toplot,axis=0)-np.std(toplot,axis=0)/np.sqrt(len(toplot)), np.mean(toplot,axis=0)+np.std(toplot,axis=0)/np.sqrt(len(toplot)),color=colors[p],alpha=0.2,lw=0) 111 | ax.hlines(0,tv[0],tv[-1],'grey',linestyles='--') 112 | 113 | ax.set_xlabel('time (ms)') 114 | ax.set_ylabel('Euclidean distance (\N{DEGREE SIGN})\nbaseline corrected') 115 | ax.legend(frameon=False,ncol=1,loc='upper left',borderpad=0.1,labelspacing=0.1) 116 | ax.spines['right'].set_visible(False) 117 | ax.spines['top'].set_visible(False) 118 | 119 | 120 | fig.set_size_inches(5,3) 121 | fig.savefig(figdir + '/supplementary_ET_timeresolved.pdf',dpi=500) 122 | 123 | # plot time-resolved pupil size 124 | tv = ((np.unique(data[0][0].time_samples.to_numpy())*1/1200)-0.1)*1000 125 | 126 | plt.close('all') 127 | fig,ax = plt.subplots(ncols=1, nrows=1,num=1,tight_layout=True,figsize = (6,4),sharex=True,sharey=True) 128 | 129 | for p in range(n_participants): 130 | toplot = pupil_size_all[p] 131 | ax.plot(tv,np.mean(toplot,axis=0),color=colors[p],label='M'+str(p+1)) 132 | ax.fill_between(tv, np.mean(toplot,axis=0)-np.std(toplot,axis=0)/np.sqrt(len(toplot)), np.mean(toplot,axis=0)+np.std(toplot,axis=0)/np.sqrt(len(toplot)),color=colors[p],alpha=0.2,lw=0) 133 | 134 | ax.set_xlabel('time (ms)') 135 | ax.set_ylabel('Pupil size (a.u.)') 136 | ax.legend(frameon=False,ncol=1,loc='lower left',borderpad=0.1,labelspacing=0.1) 137 | ax.spines['right'].set_visible(False) 138 | ax.spines['top'].set_visible(False) 139 | 140 | fig.set_size_inches(5,3) 141 | fig.savefig(figdir + '/supplementary_ET_pupil_timeresolved.pdf',dpi=500) 142 | 143 | #plot gaze-position with threshold inlets 144 | # (note that data is downsampled to 100 Hz here because otherwise the plotting takes too long) 145 | plt.close('all') 146 | print('making gaze position KDE-plots') 147 | fig,axs = plt.subplots(ncols=4, nrows=1,num=4,tight_layout=True,figsize = (10,3),sharex=True,sharey=True) 148 | downsample = 1 149 | thresholds = np.linspace(0,5,50) 150 | for p,ax in enumerate(axs.flatten()): 151 | allxs,allys = [],[] 152 | for s in range(n_sessions): 153 | if downsample: 154 | allxs.extend(data[p][s].x[0::12]) 155 | allys.extend(data[p][s].y[0::12]) 156 | else: 157 | allxs.extend(data[p][s].x) 158 | allys.extend(data[p][s].y) 159 | 160 | sns.histplot(x=allxs,y=allys,ax=ax,color=colors[p]) 161 | if downsample: 162 | sns.kdeplot(x=allxs,y=allys,ax=ax,cmap='Greys',levels=[0.25,0.5,0.75],linewidths=0.3) 163 | else: 164 | circle1 = plt.Circle((0, 0), 1, edgecolor='white',fill=False,linestyle='--') 165 | ax.add_patch(circle1) 166 | 167 | ax.set_xlim([-5,5]) 168 | ax.set_ylim([-5,5]) 169 | ax.set_xlabel('x-gaze (\N{DEGREE SIGN})') 170 | ax.set_ylabel('y-gaze (\N{DEGREE SIGN})') 171 | ax.spines['right'].set_visible(False) 172 | ax.spines['top'].set_visible(False) 173 | ax.set_aspect('equal','box') 174 | 175 | # add threshold 176 | xx = np.ma.array(allxs,mask=np.isnan(allxs)) 177 | yy = np.ma.array(allys,mask=np.isnan(allys)) 178 | outx = [np.mean(xx<=thresh)*100 for thresh in thresholds] 179 | outy = [np.mean(yy<=thresh)*100 for thresh in thresholds] 180 | 181 | 182 | # inlet showing % of data below thresholds 183 | ins = ax.inset_axes([0.85,0.85,0.45,0.45]) 184 | ins.plot(thresholds,outy,color=lighten_color(colors[p],1),label='y',lw=1) 185 | ins.plot(thresholds,outx,color=lighten_color(colors[p],0.5),label='x',lw=1) 186 | ins.vlines(1,0,100,'grey',linestyles='--',lw=1) 187 | ins.hlines(outy[np.argmin(np.abs(1-thresholds))],0,5,color=lighten_color(colors[p],1),linestyles='--',lw=1) 188 | ins.hlines(outx[np.argmin(np.abs(1-thresholds))],0,5,color=lighten_color(colors[p],0.5),linestyles='--',lw=1) 189 | ins.set_xlabel('degrees',fontsize=9) 190 | ins.set_ylabel('Prop. (%)',fontsize=9) 191 | ins.spines['right'].set_visible(False) 192 | ins.spines['top'].set_visible(False) 193 | ins.set_yticks([0,50,100]) 194 | ins.set_yticklabels([0,50,100],fontsize=9) 195 | ins.set_xticks(np.arange(5)) 196 | ins.set_xticklabels(np.arange(5),fontsize=9) 197 | 198 | fig.set_size_inches(9,5) 199 | fig.savefig(figdir + '/supplementary_ET_gazepos_withrings.png',dpi=500) 200 | 201 | 202 | 203 | #*****************************# 204 | ### COMMAND LINE INPUTS ### 205 | #*****************************# 206 | if __name__=='__main__': 207 | import argparse 208 | parser = argparse.ArgumentParser() 209 | 210 | parser.add_argument( 211 | "-bids_dir", 212 | required=True, 213 | help='path to bids root', 214 | ) 215 | 216 | args = parser.parse_args() 217 | 218 | bids_dir = args.bids_dir 219 | figdir = f'{bids_dir}/derivatives/figures/' 220 | preprocdir = f'{bids_dir}/derivatives/preprocessed/' 221 | 222 | if not os.path.exists(figdir): 223 | os.makedirs(figdir) 224 | 225 | 226 | data,invalid_samples = load_data(preprocdir) 227 | make_plots(data,invalid_samples,figdir) 228 | 229 | -------------------------------------------------------------------------------- /MEG/step1_preprocessing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | @ Lina Teichmann 5 | 6 | INPUTS: 7 | call from command line with following inputs: 8 | -participant 9 | -bids_dir 10 | 11 | OUTPUTS: 12 | epoched and cleaned data will be written into the preprocessing directory 13 | 14 | NOTES: 15 | This script contains the following preprocessing steps: 16 | - channel exclusion (one malfunctioning channel ('MRO11-1609'), based on experimenter notes) 17 | - filtering (0.1 - 40Hz) 18 | - epoching (-100 - 1300ms) --> based on onsets of the optical sensor 19 | - baseline correction (zscore) 20 | - downsampling (200Hz) 21 | 22 | Use preprocessed data ("preprocessed_P{participant}-epo.fif") saved in preprocessing directory for the next steps 23 | 24 | """ 25 | 26 | import mne, os 27 | import numpy as np 28 | import pandas as pd 29 | from joblib import Parallel, delayed 30 | 31 | 32 | #*****************************# 33 | ### PARAMETERS ### 34 | #*****************************# 35 | 36 | n_sessions = 12 37 | trigger_amplitude = 64 38 | l_freq = 0.1 39 | h_freq = 40 40 | pre_stim_time = -0.1 41 | post_stim_time = 1.3 42 | std_deviations_above_below = 4 43 | output_resolution = 200 44 | trigger_channel = 'UPPT001' 45 | 46 | 47 | #*****************************# 48 | ### HELPER FUNCTIONS ### 49 | #*****************************# 50 | def setup_paths(meg_dir, session): 51 | run_paths,event_paths = [],[] 52 | for file in os.listdir(f'{meg_dir}/ses-{str(session).zfill(2)}/meg/'): 53 | if file.endswith(".ds") and file.startswith("sub"): 54 | run_paths.append(os.path.join(f'{meg_dir}/ses-{str(session).zfill(2)}/meg/', file)) 55 | if file.endswith("events.tsv") and file.startswith("sub"): 56 | event_paths.append(os.path.join(f'{meg_dir}/ses-{str(session).zfill(2)}/meg/', file)) 57 | run_paths.sort() 58 | event_paths.sort() 59 | 60 | return run_paths, event_paths 61 | 62 | def read_raw(curr_path,session,run,participant): 63 | raw = mne.io.read_raw_ctf(curr_path,preload=True) 64 | # signal dropout in one run -- replacing values with median 65 | if participant == '1' and session == 11 and run == 4: 66 | n_samples_exclude = int(0.2/(1/raw.info['sfreq'])) 67 | raw._data[:,np.argmin(np.abs(raw.times-13.4)):np.argmin(np.abs(raw.times-13.4))+n_samples_exclude] = np.repeat(np.median(raw._data,axis=1)[np.newaxis,...], n_samples_exclude, axis=0).T 68 | elif participant == '2' and session == 10 and run == 2: 69 | n_samples_exclude = int(0.2/(1/raw.info['sfreq'])) 70 | raw._data[:,np.argmin(np.abs(raw.times-59.8)):np.argmin(np.abs(raw.times-59.8))+n_samples_exclude] = np.repeat(np.median(raw._data,axis=1)[np.newaxis,...], n_samples_exclude, axis=0).T 71 | 72 | raw.drop_channels('MRO11-1609') 73 | 74 | return raw 75 | 76 | def read_events(event_paths,run,raw): 77 | # load event file that has the corrected onset times (based on optical sensor and replace in the events variable) 78 | event_file = pd.read_csv(event_paths[run],sep='\t') 79 | event_file.value.fillna(999999,inplace=True) 80 | events = mne.find_events(raw, stim_channel=trigger_channel,initial_event=True) 81 | events = events[events[:,2]==trigger_amplitude] 82 | events[:,0] = event_file['sample'] 83 | events[:,2] = event_file['value'] 84 | return events 85 | 86 | def concat_epochs(raw, events, epochs): 87 | if epochs: 88 | epochs_1 = mne.Epochs(raw, events, tmin = pre_stim_time, tmax = post_stim_time, picks = 'mag',baseline=None) 89 | epochs_1.info['dev_head_t'] = epochs.info['dev_head_t'] 90 | epochs = mne.concatenate_epochs([epochs,epochs_1]) 91 | else: 92 | epochs = mne.Epochs(raw, events, tmin = pre_stim_time, tmax = post_stim_time, picks = 'mag',baseline=None) 93 | return epochs 94 | 95 | def baseline_correction(epochs): 96 | baselined_epochs = mne.baseline.rescale(data=epochs.get_data(),times=epochs.times,baseline=(None,0),mode='zscore',copy=False) 97 | epochs = mne.EpochsArray(baselined_epochs, epochs.info, epochs.events, epochs.tmin,event_id=epochs.event_id) 98 | return epochs 99 | 100 | def stack_sessions(sourcedata_dir,preproc_dir,participant,session_epochs,output_resolution): 101 | for epochs in session_epochs: 102 | epochs.info['dev_head_t'] = session_epochs[0].info['dev_head_t'] 103 | all_epochs = mne.concatenate_epochs(epochs_list = session_epochs, add_offset=True) 104 | all_epochs.metadata = pd.read_csv(f'{sourcedata_dir}/sample_attributes_P{str(participant)}.csv') 105 | all_epochs.decimate(decim=(1200/output_resolution)) 106 | all_epochs.save(f'{preproc_dir}/preprocessed_P{str(participant)}-epo.fif', overwrite=True) 107 | print(all_epochs.info) 108 | 109 | 110 | #*****************************# 111 | ### FUNCTION TO RUN PREPROCESSING ### 112 | #*****************************# 113 | def run_preprocessing(meg_dir,session,participant): 114 | epochs = [] 115 | run_paths, event_paths = setup_paths(meg_dir, session) 116 | for run, curr_path in enumerate(run_paths): 117 | raw = read_raw(curr_path,session,run, participant) 118 | events = read_events(event_paths,run,raw) 119 | raw.filter(l_freq=l_freq,h_freq=h_freq) 120 | epochs = concat_epochs(raw, events, epochs) 121 | epochs.drop_bad() 122 | print(epochs.info) 123 | epochs = baseline_correction(epochs) 124 | return epochs 125 | 126 | 127 | #*****************************# 128 | ### COMMAND LINE INPUTS ### 129 | #*****************************# 130 | if __name__=='__main__': 131 | import argparse 132 | parser = argparse.ArgumentParser() 133 | parser.add_argument( 134 | "-participant", 135 | required=True, 136 | help='participant bids ID (e.g., 1)', 137 | ) 138 | 139 | parser.add_argument( 140 | "-bids_dir", 141 | required=True, 142 | help='path to bids root', 143 | ) 144 | 145 | args = parser.parse_args() 146 | 147 | bids_dir = args.bids_dir 148 | participant = args.participant 149 | meg_dir = f'{bids_dir}/sub-BIGMEG{participant}/' 150 | sourcedata_dir = f'{bids_dir}/sourcedata/' 151 | preproc_dir = f'{bids_dir}/derivatives/preprocessed/' 152 | if not os.path.exists(preproc_dir): 153 | os.makedirs(preproc_dir) 154 | 155 | ####### Run preprocessing ######## 156 | session_epochs = Parallel(n_jobs=12, backend="multiprocessing")(delayed(run_preprocessing)(meg_dir,session,participant) for session in range(1,n_sessions+1)) 157 | stack_sessions(sourcedata_dir,preproc_dir,participant,session_epochs,output_resolution) 158 | -------------------------------------------------------------------------------- /MEG/step2a_data_quality-head_position.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | @ Lina Teichmann 5 | 6 | INPUTS: 7 | call from command line with following inputs: 8 | -bids_dir 9 | 10 | OUTPUTS: 11 | Plots for head motion data. 12 | If it doesn't exist, the script makes a figures folder in the BIDS derivatives folder 13 | 14 | 15 | NOTES: 16 | This script is using the pyctf toolbox to extract electrode positions from the three sensors (nasion, left, right) that were mounted to the participants head. 17 | (the pyctf toolbox can be downloaded from the nih-megcore github: https://github.com/nih-megcore/pyctf) 18 | 19 | Once the electrode positions are extracted over time, we calculated the distance between the measured head positions. We then report the average distance within the session and across the session. 20 | Note that extra care should be taken when the head coil measurements are being used for source localization: it seems that for participant #4 there are a few sessions where the position recording mal-functioned or where the coil wasn't attached to the same position. 21 | 22 | 23 | """ 24 | 25 | from pyctf import dsopen 26 | import matplotlib.pyplot as plt 27 | from matplotlib.patches import Patch 28 | import numpy as np 29 | import pandas as pd 30 | import itertools, os, sys 31 | import seaborn as sns 32 | 33 | #*****************************# 34 | ### PARAMETERS ### 35 | #*****************************# 36 | n_participants = 4 37 | n_runs = 10 38 | n_sessions = 12 39 | colors = ['mediumseagreen','steelblue','goldenrod','indianred','grey'] 40 | electrodes = ['nas','lpa','rpa'] 41 | electrode_labels = ['Nasion','LPA','RPA'] 42 | ppt_labels = ['M1','M2','M3','M4'] 43 | plt.rcParams['font.size'] = '16' 44 | plt.rcParams['font.family'] = 'Helvetica' 45 | 46 | 47 | #*****************************# 48 | ### HELPER FUNCTIONS ### 49 | #*****************************# 50 | 51 | def lighten_color(color, amount=0.5): 52 | import matplotlib.colors as mc 53 | import colorsys 54 | try: 55 | c = mc.cnames[color] 56 | except: 57 | c = color 58 | c = colorsys.rgb_to_hls(*mc.to_rgb(c)) 59 | return colorsys.hls_to_rgb(c[0], 1 - amount * (1 - c[1]), c[2]) 60 | 61 | 62 | def load_sensor_positions(rootdir, recording_dir = ['x','y','z'], n_participants = 4, n_session = 12, n_runs = 10): 63 | # un-usable recordings of head coil position based on notes by experimenter (each row is a participant, each tuple is (session, run)) 64 | invalid_measures = [[], 65 | [(8,3)], 66 | [], 67 | [(4,4),(5,2),(7,7),(7,8),(7,9),(12,5),(12,10)]] 68 | 69 | # initialize data frame 70 | df = pd.DataFrame(columns=['participant','session','run']+[i + '_' + ii for i in electrodes for ii in recording_dir]) 71 | df.participant = np.repeat(np.arange(1,n_participants+1),n_sessions*n_runs) 72 | df.session = np.tile(np.repeat(np.arange(1,n_sessions+1),n_runs),n_participants) 73 | df.run = np.tile(np.arange(1,n_runs+1),n_sessions*n_participants) 74 | 75 | for p in range(1,n_participants+1): 76 | for s in range(1,n_sessions+1): 77 | for r in range(1,n_runs+1): 78 | meg_fn = f'{rootdir}/sub-BIGMEG{str(p)}/ses-{str(s).zfill(2)}/meg/sub-BIGMEG{str(p)}_ses-{str(s).zfill(2)}_task-main_run-{str(r).zfill(2)}_meg.ds' 79 | for i,v in enumerate(electrodes): 80 | filter_col = [col for col in df if col.startswith(v)] 81 | df.loc[(df.participant==p)&(df.session==s)&(df.run==r),filter_col] = dsopen(meg_fn).dewar[i] 82 | 83 | # deleting all entries from invalid measurements 84 | for ii in invalid_measures[p-1]: 85 | for i,v in enumerate(electrodes): 86 | filter_col = [col for col in df if col.startswith(v)] 87 | df.loc[(df['participant']==p) & (df['session']==ii[0]) & (df['run']==ii[1]),filter_col] = np.nan 88 | 89 | return df 90 | 91 | 92 | def plot_rdm(res,ax,cbar, fig): 93 | im = ax.imshow(res,interpolation='none', cmap='flare',aspect='equal',vmin=0,vmax=5) 94 | major_ticks = np.arange(-.5, len(res)-1, 10) 95 | minor_ticks = np.arange(-.5, len(res)-1) 96 | ax.tick_params(axis = 'both', which = 'major', labelsize = 10) 97 | ax.tick_params(axis = 'both', which = 'minor', labelsize = 0) 98 | ax.set_xticks(major_ticks) 99 | ax.set_xticks(minor_ticks, minor = True) 100 | ax.set_yticks(major_ticks) 101 | ax.set_yticks(minor_ticks, minor = True) 102 | 103 | ax.set_xticklabels(['S'+ str(i) for i in range(1,n_sessions+1)],rotation=45) 104 | ax.set_yticklabels(['S'+ str(i) for i in range(1,n_sessions+1)]) 105 | ax.grid(which = 'major', alpha = 0.9, color='w') 106 | ax.grid(which = 'minor', alpha = 0.2, color='w') 107 | ax.spines['right'].set_visible(False) 108 | ax.spines['top'].set_visible(False) 109 | 110 | if cbar: 111 | cbar_ax = fig.add_axes([0.92, 0.4, 0.01, 0.2]) 112 | cbar = fig.colorbar(im, cax=cbar_ax) 113 | cbar.set_label('Distance (mm)', fontsize=16) 114 | 115 | 116 | def make_supplementary_plot(df): 117 | fig, ax = plt.subplots(n_participants,len(electrodes),figsize=(10,20)) 118 | for p in range(1,n_participants+1): 119 | tmp = df.loc[df.participant==p,:].copy() 120 | tmp['id'] = ['S' + str(tmp.session.to_list()[i]) + '_' + 'R' + str(tmp.run.to_list()[i]) for i in range(len(tmp))] 121 | # get all possible pairwise comparisons 122 | combs = list(itertools.combinations(tmp['id'].to_list(), 2)) 123 | res = np.zeros((len(tmp),len(tmp))) 124 | res[res == 0.0] = np.nan 125 | res = pd.DataFrame(res,columns = tmp['id'].to_numpy(),index = tmp['id'].to_numpy()) 126 | 127 | # loop over electrodes and calculate distances and plot 128 | for i,v in enumerate(electrodes): 129 | filter_col = [col for col in df if col.startswith(v)] 130 | res1 = res.copy() 131 | for vv in combs: 132 | res1.loc[vv[1],vv[0]]=np.sqrt(((tmp.loc[tmp.id==vv[0],filter_col].to_numpy()-tmp.loc[tmp.id==vv[1],filter_col].to_numpy())**2).sum()) 133 | 134 | if (p==4) & (i==2): 135 | plot_rdm(res1,ax[p-1][i],True,fig) # plot with colorbar 136 | else: 137 | plot_rdm(res1,ax[p-1][i],False,fig) # plot without colorbar 138 | 139 | # label the rows and columns 140 | for a, col in zip(ax[0], electrode_labels): 141 | a.annotate(col, xy=(0.5, 1), xytext=(0, 5), 142 | xycoords='axes fraction', textcoords='offset points', 143 | size='large', ha='center', va='baseline') 144 | 145 | for a, row in zip(ax[:,0], ppt_labels): 146 | a.annotate(row, xy=(0, 0.5), xytext=(-a.yaxis.labelpad - 5, 0), 147 | xycoords=a.yaxis.label, textcoords='offset points', 148 | size='large', ha='right', va='center') 149 | 150 | fig.subplots_adjust(left=0.15, top=0.95,right=0.9,hspace=0) 151 | 152 | # save 153 | fig.savefig(figdir + '/supplementary_motion.pdf') 154 | 155 | return res, res1 156 | 157 | def make_boxplot(df,res, res1): 158 | # initialize 159 | cross_all = np.zeros((n_sessions*n_runs*n_sessions*n_runs,n_participants)) 160 | within_all = np.zeros((n_sessions*n_runs*n_sessions*n_runs,n_participants)) 161 | 162 | # make masks to filter the distance matrix to extract within and cross-session differences 163 | 164 | within_mask = ~res.copy().isna() 165 | cross_mask = ~res.copy().isna() 166 | for s in range(1,n_sessions+1): 167 | filter_row = [col for col in res1 if col.startswith('S'+str(s)+'_')] 168 | within_mask.loc[filter_row,filter_row] = True 169 | 170 | filter_col = [col for col in res1 if not col.startswith('S'+str(s)+'_')] 171 | cross_mask.loc[filter_row,filter_col] = True 172 | cross_mask.loc[filter_col,filter_row] = True 173 | 174 | # loop over participants and calculate the distances for cross- and within-session comparisons 175 | for p in range(1,n_participants+1): 176 | tmp = df.loc[df.participant==p,:].copy() 177 | # average three sensors to find midpoint 178 | for i,v in enumerate(['x','y','z']): 179 | filter_col = [col for col in tmp if col.endswith(v)] 180 | tmp[v] = tmp[filter_col].mean(axis=1) 181 | # label the sessions/runs 182 | tmp['id'] = ['S' + str(tmp.session.to_list()[i]) + '_' + 'R' + str(tmp.run.to_list()[i]) for i in range(len(tmp))] 183 | # find all combinations between all measurement pairs and make a matrix that has all pairwise distances 184 | combs = list(itertools.combinations(tmp['id'].to_list(), 2)) 185 | res = np.zeros((len(tmp),len(tmp))) 186 | res[res == 0.0] = np.nan 187 | res = pd.DataFrame(res,columns = tmp['id'].to_numpy(),index = tmp['id'].to_numpy()) 188 | res1 = res.copy() 189 | for vv in combs: 190 | res1.loc[vv[1],vv[0]] = np.sqrt(((tmp.loc[tmp.id==vv[0],['x','y','z']].to_numpy()-tmp.loc[tmp.id==vv[1],['x','y','z']].to_numpy())**2).sum()) 191 | 192 | # use the mask to extract the cross-session and within-session distances 193 | cross_all[:,p-1] = (res1[cross_mask].to_numpy()*10).ravel() 194 | within_all[:,p-1] = (res1[within_mask].to_numpy()*10).ravel() 195 | 196 | # make the boxplot with cross- and within-session distances 197 | fig, ax = plt.subplots(1,1) 198 | for p in range(n_participants): 199 | x = within_all[:,p] 200 | boxplot = ax.boxplot(x[~np.isnan(x)],sym='',whis=(0,90),notch=True,patch_artist=True,widths=0.25,positions = [p-0.15], 201 | boxprops=dict(facecolor=(colors[p]), color='k'), 202 | medianprops=dict(color='k',lw=1)) 203 | 204 | x = cross_all[:,p] 205 | boxplot = ax.boxplot(x[~np.isnan(x)],sym='',whis=(0,90),notch=True,patch_artist=True,widths=0.25,positions = [p+0.15], 206 | boxprops=dict(facecolor=lighten_color(colors[p],amount=0.3), color='k'), 207 | medianprops=dict(color='k',lw=1)) 208 | 209 | # make plot look pretty 210 | ax.set_xticks(np.arange(n_participants)) 211 | ax.set_xticklabels(['M' + str(p+1) for p in np.arange(n_participants)]) 212 | 213 | caps = boxplot['caps'] 214 | med = boxplot['medians'][0] 215 | xpos = med.get_xdata() 216 | xoff = 0.10 * (xpos[1] - xpos[0]) 217 | xlabel = xpos[1] + xoff 218 | capbottom = caps[0].get_ydata()[0] 219 | captop = caps[1].get_ydata()[0] 220 | 221 | ax.text(xlabel, capbottom, 222 | '5th percentile', va='center') 223 | ax.text(xlabel, captop, 224 | '95th percentile', va='center') 225 | 226 | ax.set_ylabel('Head coil movement (mm)') 227 | ax.spines['right'].set_visible(False) 228 | ax.spines['top'].set_visible(False) 229 | 230 | within_patch = Patch(facecolor=[0.5,0.5,0.5]) 231 | cross_patch = Patch(facecolor=lighten_color([0.5,0.5,0.5],amount=0.3)) 232 | 233 | ax.legend([within_patch,cross_patch],['within-session ','cross-session'],frameon=False,loc='upper left') 234 | ax.set_ylim([-0.1,10]) 235 | 236 | # save 237 | fig.subplots_adjust(right=0.8) 238 | fig.savefig(figdir+'/data_quality-motion-box.pdf') 239 | 240 | 241 | #*****************************# 242 | ### COMMAND LINE INPUTS ### 243 | #*****************************# 244 | if __name__=='__main__': 245 | import argparse 246 | parser = argparse.ArgumentParser() 247 | 248 | parser.add_argument( 249 | "-bids_dir", 250 | required=True, 251 | help='path to bids root', 252 | ) 253 | 254 | 255 | args = parser.parse_args() 256 | rootdir = args.bids_dir 257 | sourcedata_dir = f'{rootdir}/sourcedata/' 258 | 259 | figdir = f'{rootdir}/derivatives/figures/' 260 | if not os.path.exists(figdir): 261 | os.makedirs(figdir) 262 | 263 | ####### Run ######## 264 | df = load_sensor_positions(rootdir, recording_dir = ['x','y','z'], n_participants = 4, n_session = 12, n_runs = 10) 265 | res, res1 = make_supplementary_plot(df) 266 | make_boxplot(df,res, res1) 267 | -------------------------------------------------------------------------------- /MEG/step2b_data_quality-ERFs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | @ Lina Teichmann 5 | 6 | INPUTS: 7 | call from command line with following inputs: 8 | -bids_dir 9 | 10 | OUTPUTS: 11 | Plots the ERFs of the repeat trials. 12 | 13 | NOTES: 14 | If it doesn't exist, the script makes a figures folder in the BIDS derivatives folder 15 | 16 | """ 17 | 18 | import numpy as np 19 | import matplotlib.pyplot as plt 20 | from matplotlib.gridspec import GridSpec 21 | import mne, os 22 | import pandas as pd 23 | import seaborn as sns 24 | 25 | 26 | #*****************************# 27 | ### PARAMETERS ### 28 | #*****************************# 29 | n_participants = 4 30 | n_sessions = 12 31 | n_images = 200 32 | channel_picks = ['O','T','P'] 33 | title_names = ['Occipital','Temporal','Parietal'] 34 | colors = ['mediumseagreen','steelblue','goldenrod','indianred','grey'] 35 | plt.rcParams['font.size'] = '16' 36 | plt.rcParams['font.family'] = 'Helvetica' 37 | 38 | #*****************************# 39 | ### HELPER FUNCTIONS ### 40 | #*****************************# 41 | def load_epochs(preproc_dir,all_epochs = []): 42 | for p in range(1,n_participants+1): 43 | epochs = mne.read_epochs(f'{preproc_dir}/preprocessed_P{str(p)}-epo.fif', preload=False) 44 | all_epochs.append(epochs) 45 | return all_epochs 46 | 47 | # helper function 48 | def plot_erfs(epochs,n_sessions,name,color,ax,ax2,lab): 49 | ctf_layout = mne.find_layout(epochs.info) 50 | picks_epochs = [epochs.ch_names[i] for i in np.where([s[2]==name for s in epochs.ch_names])[0]] 51 | picks = np.where([i[2]==name for i in ctf_layout.names])[0] 52 | 53 | # get evoked data 54 | for s in range(n_sessions): 55 | evoked = epochs[(epochs.metadata['trial_type']=='test') & (epochs.metadata['session_nr']==s+1)].average() 56 | evoked.pick_channels(ch_names=picks_epochs) 57 | ax.plot(epochs.times*1000,np.mean(evoked.data.T,axis=1),color=color,lw=0.5,alpha=0.4) 58 | evoked = epochs[(epochs.metadata['trial_type']=='test')].average() 59 | evoked.pick_channels(ch_names=picks_epochs) 60 | 61 | # plot ERFs for selected sensor group 62 | ax.plot(epochs.times*1000,np.mean(evoked.data.T,axis=1),color=color,lw=1,label=lab) 63 | ax.set_xlim([epochs.times[0]*1000,epochs.times[len(epochs.times)-1]*1000]) 64 | ax.set_ylim([-0.6,0.6]) 65 | ax.spines['right'].set_visible(False) 66 | ax.spines['top'].set_visible(False) 67 | 68 | # plot sensor locations 69 | ax2.plot(ctf_layout.pos[:,0],ctf_layout.pos[:,1],color='gainsboro',marker='.',linestyle='',markersize=5) 70 | ax2.plot(ctf_layout.pos[picks,0],ctf_layout.pos[picks,1],color='grey',marker='.',linestyle='',markersize=5) 71 | ax2.set_aspect('equal') 72 | plt.axis('off') 73 | 74 | # Make the ERF plot 75 | def make_figure(all_epochs,fig_dir): 76 | fig = plt.figure(num=1,tight_layout=True,figsize = (11,6)) 77 | gs = GridSpec(3, 5, figure=fig) 78 | for i,ch in enumerate(channel_picks): 79 | for p in range(n_participants): 80 | ax = fig.add_subplot(gs[i, p]) 81 | if i == 0: 82 | ax.set_title('M' + str(p+1)) 83 | if i == 2: 84 | ax.set_xlabel('time (ms)') 85 | else: 86 | plt.setp(ax.get_xticklabels(), visible=False) 87 | if p == 0: 88 | ax.set_ylabel('fT') 89 | else: 90 | plt.setp(ax.get_yticklabels(), visible=False) 91 | 92 | ax2=fig.add_subplot(gs[i, -1]) 93 | 94 | plot_erfs(all_epochs[p],12,ch,colors[p],ax,ax2,'Sub' + str(p+1)) 95 | ax2.set_title(title_names[i]) 96 | plt.savefig(f'{fig_dir}/data_quality-ERFs.pdf',dpi=1000) 97 | 98 | 99 | #*****************************# 100 | ### COMMAND LINE INPUTS ### 101 | #*****************************# 102 | if __name__=='__main__': 103 | import argparse 104 | parser = argparse.ArgumentParser() 105 | 106 | parser.add_argument( 107 | "-bids_dir", 108 | required=True, 109 | help='path to bids root', 110 | ) 111 | 112 | args = parser.parse_args() 113 | bids_dir = args.bids_dir 114 | preproc_dir = f'{bids_dir}/derivatives/preprocessed/' 115 | sourcedata_dir = f'{bids_dir}/sourcedata/' 116 | fig_dir = f'{bids_dir}/derivatives/figures/' 117 | if not os.path.exists(fig_dir): 118 | os.makedirs(fig_dir) 119 | 120 | ####### Run ######## 121 | all_epochs = load_epochs(preproc_dir) 122 | make_figure(all_epochs,fig_dir) 123 | -------------------------------------------------------------------------------- /MEG/step2c_data_quality-noiseceiling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | @ Lina Teichmann 5 | 6 | INPUTS: 7 | call from command line with following inputs: 8 | -bids_dir 9 | 10 | OUTPUTS: 11 | Calculates and plots noise ceilings based on the 200 repeat images for all sensors and each sensor group 12 | 13 | NOTES: 14 | If it doesn't exist, the script makes a figures folder in the BIDS derivatives folder 15 | 16 | """ 17 | 18 | import numpy as np 19 | import matplotlib.pyplot as plt 20 | import matplotlib.gridspec as gridspec 21 | import mne,os 22 | from scipy.stats import zscore 23 | 24 | #*****************************# 25 | ### PARAMETERS ### 26 | #*****************************# 27 | n_participants = 4 28 | n_sessions = 12 29 | n_images = 200 30 | names = ['O','T','P','F','C'] 31 | labs = ['Occipital','Temporal','Parietal','Frontal','Central'] 32 | colors = ['mediumseagreen','steelblue','goldenrod','indianred','grey'] 33 | plt.rcParams['font.size'] = '16' 34 | plt.rcParams['font.family'] = 'Helvetica' 35 | 36 | #*****************************# 37 | ### HELPER FUNCTIONS ### 38 | #*****************************# 39 | def load_epochs(preproc_dir,all_epochs = []): 40 | for p in range(1,n_participants+1): 41 | epochs = mne.read_epochs(f'{preproc_dir}/preprocessed_P{str(p)}-epo.fif', preload=False) 42 | all_epochs.append(epochs) 43 | return all_epochs 44 | 45 | def kknc(data: np.ndarray, n: int or None = None): 46 | """ 47 | Calculate the noise ceiling reported in the NSD paper (Allen et al., 2021) 48 | Arguments: 49 | data: np.ndarray 50 | Should be shape (ntargets, nrepetitions, nobservations) 51 | n: int or None 52 | Number of trials averaged to calculate the noise ceiling. If None, n will be the number of repetitions. 53 | returns: 54 | nc: np.ndarray of shape (ntargets) 55 | Noise ceiling without considering trial averaging. 56 | ncav: np.ndarray of shape (ntargets) 57 | Noise ceiling considering all trials were averaged. 58 | """ 59 | if not n: 60 | n = data.shape[-2] 61 | normalized = zscore(data, axis=-1) 62 | noisesd = np.sqrt(np.mean(np.var(normalized, axis=-2, ddof=1), axis=-1)) 63 | sigsd = np.sqrt(np.clip(1 - noisesd ** 2, 0., None)) 64 | ncsnr = sigsd / noisesd 65 | nc = 100 * ((ncsnr ** 2) / ((ncsnr ** 2) + (1 / n))) 66 | return nc 67 | 68 | def calculate_noise_ceiling(all_epochs,all_nc = []): 69 | n_time = len(all_epochs[0].times) 70 | for p in range(n_participants): 71 | n_channels = len(all_epochs[p].ch_names) 72 | 73 | # load data 74 | epochs = all_epochs[p] 75 | # select repetition trials only and load epoched data into memory 76 | epochs_rep = epochs[(epochs.metadata['trial_type']=='test')] 77 | epochs_rep.load_data() 78 | 79 | # select session data and sort based on category number 80 | res_mat=np.empty([n_channels,n_sessions,n_images,n_time]) 81 | for sess in range(n_sessions): 82 | epochs_curr = epochs_rep[epochs_rep.metadata['session_nr']==sess+1] 83 | sort_order = np.argsort(epochs_curr.metadata['things_category_nr']) 84 | epochs_curr=epochs_curr[sort_order] 85 | epochs_curr = np.transpose(epochs_curr._data, (1,0,2)) 86 | 87 | res_mat[:,sess,:,:] = epochs_curr 88 | 89 | # run noise ceiling 90 | nc = np.empty([n_channels,n_time]) 91 | for t in range(n_time): 92 | dat = res_mat[:,:,:,t] 93 | nc[:,t] = kknc(data=dat,n=n_sessions) 94 | all_nc.append(nc) 95 | return all_nc 96 | 97 | def make_supplementary_plot(all_epochs,fig_dir): 98 | plt.close('all') 99 | fig = plt.figure(num=2,figsize = (12,8)) 100 | gs1 = gridspec.GridSpec(n_participants+1, len(names)) 101 | gs1.update(wspace=0.2, hspace=0.2) 102 | ctf_layout = mne.find_layout(all_epochs[1].info) 103 | counter = 0 104 | for i,n in enumerate(names): 105 | for p in range(n_participants): 106 | ax = fig.add_subplot(gs1[counter]) 107 | counter+=1 108 | ax.clear() 109 | picks_epochs = np.where([s[2]==n for s in all_epochs[p].ch_names])[0] 110 | picks = np.where([i[2]==n for i in ctf_layout.names])[0] 111 | [ax.plot(all_epochs[p].times*1000,ii,color=colors[p],label=labs[i],lw=0.1,alpha=0.2) for ii in all_nc[p][picks_epochs,:]] 112 | ax.plot(all_epochs[p].times*1000,np.mean(all_nc[p][picks_epochs,:],axis=0),color=colors[p],label=labs[i],lw=1.5) 113 | ax.set_ylim([0,100]) 114 | 115 | if i ==0: 116 | ax.set_title('M' + str(p+1)) 117 | 118 | if i < len(names)-1: 119 | plt.setp(ax.get_xticklabels(), visible=False) 120 | else: 121 | ax.set_xlabel('time (ms)') 122 | 123 | if p == 0: 124 | plt.setp(ax.get_yticklabels(), visible=True) 125 | ax.set_ylabel(labs[i]) 126 | else: 127 | plt.setp(ax.get_yticklabels(), visible=False) 128 | ax.spines['right'].set_visible(False) 129 | ax.spines['top'].set_visible(False) 130 | 131 | if i == 2 and p == 0: 132 | ax.set_ylabel('Explained Variance (%)\n' + labs[i]) 133 | 134 | # plot sensor locations 135 | ax2 = fig.add_subplot(gs1[counter]) 136 | counter+=1 137 | ax2.plot(ctf_layout.pos[:,0],ctf_layout.pos[:,1],color='gainsboro',marker='.',linestyle='',markersize=3) 138 | ax2.plot(ctf_layout.pos[picks,0],ctf_layout.pos[picks,1],color='grey',marker='.',linestyle='',markersize=3) 139 | ax2.axis('equal') 140 | ax2.axis('off') 141 | fig.savefig(f'{fig_dir}/data_quality-noiseceiling_all.pdf') 142 | 143 | def make_main_plot(all_epochs,all_nc): 144 | plt.close('all') 145 | fig = plt.figure(num=1,figsize = (12,3)) 146 | gs1 = gridspec.GridSpec(1,len(names),wspace=0.1,) 147 | ctf_layout = mne.find_layout(all_epochs[1].info) 148 | 149 | for i,n in enumerate(names): 150 | ax = fig.add_subplot(gs1[i]) 151 | 152 | # plot niose ceilings 153 | picks_epochs = [np.where([s[2]==n for s in all_epochs[p].ch_names])[0] for p in range(n_participants)] 154 | picks = np.where([i[2]==n for i in ctf_layout.names])[0] 155 | [ax.plot(all_epochs[p].times*1000,np.mean(all_nc[p][picks_epochs[p],:],axis=0),color=colors[p],label='M'+str(p+1),lw=2) for p in range(n_participants)] 156 | 157 | ax.set_ylim([0,90]) 158 | ax.set_xlim([all_epochs[1].times[0]*1000,all_epochs[1].times[len(all_epochs[1].times)-1]*1000]) 159 | ax.spines['right'].set_visible(False) 160 | ax.spines['top'].set_visible(False) 161 | if i == len(names)-1: 162 | plt.legend(frameon=False, bbox_to_anchor=(1, 0.5)) 163 | ax.set_xlabel('time (ms)') 164 | if i ==0: 165 | ax.set_ylabel('Explained variance (%)') 166 | else: 167 | plt.setp(ax.get_yticklabels(), visible=False) 168 | 169 | # plot sensor locations 170 | ax2 = ax.inset_axes([0.55, 0.55, 0.5, 0.5]) 171 | ax2.plot(ctf_layout.pos[:,0],ctf_layout.pos[:,1],color='darkgrey',marker='.',linestyle='',markersize=2) 172 | ax2.plot(ctf_layout.pos[picks,0],ctf_layout.pos[picks,1],color='k',marker='.',linestyle='',markersize=2) 173 | ax2.axis('equal') 174 | ax2.axis('off') 175 | ax2.set_title(labs[i],y=0.8,fontsize=14) 176 | 177 | fig.savefig(f'{fig_dir}/data_quality-noiseceiling_avgd.pdf') 178 | 179 | 180 | 181 | #*****************************# 182 | ### COMMAND LINE INPUTS ### 183 | #*****************************# 184 | if __name__=='__main__': 185 | import argparse 186 | parser = argparse.ArgumentParser() 187 | 188 | parser.add_argument( 189 | "-bids_dir", 190 | required=True, 191 | help='path to bids root', 192 | ) 193 | 194 | args = parser.parse_args() 195 | bids_dir = args.bids_dir 196 | preproc_dir = f'{bids_dir}/derivatives/preprocessed/' 197 | sourcedata_dir = f'{bids_dir}/sourcedata/' 198 | fig_dir = f'{bids_dir}/derivatives/figures/' 199 | if not os.path.exists(fig_dir): 200 | os.makedirs(fig_dir) 201 | 202 | ####### Run ######## 203 | all_epochs = load_epochs(preproc_dir) 204 | all_nc = calculate_noise_ceiling(all_epochs) 205 | make_supplementary_plot(all_epochs,fig_dir) 206 | make_main_plot(all_epochs,all_nc) 207 | 208 | -------------------------------------------------------------------------------- /MEG/step3a_validation_animacy_size.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | @ Lina Teichmann 5 | 6 | INPUTS: 7 | call from command line with following inputs: 8 | -bids_dir 9 | 10 | OUTPUTS: 11 | Runs a linear regression, using the MEG data at every timepoint to predict animacy and size ratings for each image. 12 | 13 | NOTES: 14 | The plot was made in matlab so it looks the same as the decoding plots (see Step3aa) 15 | If the output directory does not exist, this script makes an output folder in BIDS/derivatives 16 | 17 | """ 18 | 19 | import numpy as np 20 | import mne,os,itertools,sys 21 | import pandas as pd 22 | from sklearn.preprocessing import StandardScaler 23 | from sklearn.pipeline import Pipeline 24 | from sklearn.linear_model import LinearRegression 25 | from joblib import Parallel, delayed 26 | 27 | #*****************************# 28 | ### PARAMETERS ### 29 | #*****************************# 30 | n_participants = 4 31 | n_sessions = 12 32 | 33 | #*****************************# 34 | ### HELPER FUNCTIONS ### 35 | #*****************************# 36 | class load_data: 37 | def __init__(self,dat,sourcedata_dir,trial_type='exp'): 38 | self.dat = dat 39 | self.trial_type = trial_type 40 | self.sourcedata_dir = sourcedata_dir 41 | 42 | def load_animacy_size(self): 43 | ani_csv = f'{self.sourcedata_dir}/ratings_animacy.csv' 44 | size_csv = f'{self.sourcedata_dir}/ratings_size.csv' 45 | # load with pandas 46 | ani_df = pd.read_csv(ani_csv)[['uniqueID', 'lives_mean']] 47 | ani_df = ani_df.rename(columns={'lives_mean':'animacy'}) 48 | size_df = pd.read_csv(size_csv, sep=';')[['uniqueID', 'meanSize']] 49 | size_df = size_df.rename(columns={'meanSize':'size'}) 50 | # ani_df has "_", size_df " " as separator in multi-word concepts 51 | size_df['uniqueID'] = size_df.uniqueID.str.replace(' ', '_') 52 | # merge 53 | anisize_df = pd.merge(left=ani_df, right=size_df, on='uniqueID', how='outer') 54 | assert anisize_df.shape[0] == ani_df.shape[0] == size_df.shape[0] 55 | return anisize_df 56 | 57 | def load_meg(self): 58 | # select exp trails & sort the trials based on things_category_nr 59 | epochs_exp = self.dat[(self.dat.metadata['trial_type']=='exp')] 60 | sort_order = np.argsort(epochs_exp.metadata['things_category_nr']) 61 | dat_sorted=epochs_exp[sort_order] 62 | # getting data from each session and load it 63 | self.n_categories = len(dat_sorted.metadata.things_category_nr.unique()) 64 | self.n_sessions = len(dat_sorted.metadata.session_nr.unique()) 65 | self.n_channels = len(dat_sorted.ch_names) 66 | self.n_time = len(dat_sorted.times) 67 | self.sess_data = np.empty([self.n_categories,self.n_channels,self.n_time,self.n_sessions]) 68 | for sess in range(self.n_sessions): 69 | print('loading data for session ' + str(sess+1)) 70 | curr_data = dat_sorted[dat_sorted.metadata['session_nr']==sess+1] 71 | curr_data = curr_data.load_data() 72 | self.sess_data[:,:,:,sess]= curr_data._data 73 | return self.sess_data 74 | 75 | class linear_regression: 76 | def __init__(self,dat,label): 77 | self.dat = dat 78 | self.label = label 79 | self.n_categories = dat.shape[0] 80 | self.n_channels = dat.shape[1] 81 | self.n_time = dat.shape[2] 82 | self.n_sessions = dat.shape[3] 83 | 84 | def train_test_splits(self): 85 | self.train_splits,self.test_splits = [],[] 86 | for comb in itertools.combinations(np.arange(self.n_sessions), self.n_sessions-1): 87 | self.train_splits.append(comb) 88 | self.test_splits.append(list(set(np.arange(self.n_sessions)) - set(comb))) 89 | return self.train_splits,self.test_splits 90 | 91 | def run(self): 92 | sess_dat = self.dat 93 | train_splits,test_splits = self.train_test_splits() 94 | 95 | pipe = Pipeline([('scaler', StandardScaler()), 96 | ('regression', LinearRegression())]) 97 | 98 | corr_coef = np.empty([self.n_time,self.n_sessions]) 99 | 100 | def fit_predict(pipe,train_x,train_y,test_x,test_y): 101 | pipe.fit(train_x,train_y) 102 | y_pred = pipe.predict(test_x) 103 | return np.corrcoef(y_pred,test_y)[0,1] 104 | 105 | 106 | for split in range(self.n_sessions): 107 | print('cv-split ' + str(split)) 108 | training_x = np.take(sess_dat,train_splits[split],axis=3) 109 | training_x = np.concatenate(tuple(training_x[:,:,:,i] for i in range(training_x.shape[3])),axis=0) 110 | 111 | training_y = self.label 112 | training_y = np.tile(training_y,self.n_sessions-1) 113 | 114 | testing_x=np.take(sess_dat,test_splits[split][0],axis=3) 115 | testing_y = self.label 116 | 117 | corr_coef_time = Parallel(n_jobs=24)(delayed(fit_predict)(pipe,training_x[:,:,t],training_y,testing_x[:,:,t],testing_y) for t in range(self.n_time)) 118 | corr_coef[:,split] = corr_coef_time 119 | 120 | return corr_coef 121 | 122 | def run(p,preproc_dir): 123 | epochs = mne.read_epochs(f'{preproc_dir}/preprocessed_P{str(p)}-epo.fif', preload=False) 124 | anisize_df = load_data(epochs,sourcedata_dir,'exp').load_animacy_size() 125 | data = load_data(epochs,sourcedata_dir,'exp').load_meg() 126 | animacy_corr_coeff = linear_regression(data,anisize_df['animacy'].to_numpy()).run() 127 | size_corr_coeff = linear_regression(data,anisize_df['size'].to_numpy()).run() 128 | 129 | pd.DataFrame(animacy_corr_coeff).to_csv(f'{res_dir}/validation-animacy-P{str(p)}.csv') 130 | pd.DataFrame(size_corr_coeff).to_csv(f'{res_dir}/validation-size-P{str(p)}.csv') 131 | 132 | 133 | #*****************************# 134 | ### COMMAND LINE INPUTS ### 135 | #*****************************# 136 | if __name__=='__main__': 137 | import argparse 138 | parser = argparse.ArgumentParser() 139 | 140 | parser.add_argument( 141 | "-bids_dir", 142 | required=True, 143 | help='path to bids root', 144 | ) 145 | 146 | args = parser.parse_args() 147 | bids_dir = args.bids_dir 148 | preproc_dir = f'{bids_dir}/derivatives/preprocessed/' 149 | sourcedata_dir = f'{bids_dir}/sourcedata/' 150 | res_dir = f'{bids_dir}/derivatives/output/' 151 | if not os.path.exists(res_dir): 152 | os.makedirs(res_dir) 153 | 154 | ####### Run analysis ######## 155 | for p in range(1,n_participants+1): 156 | run(p,preproc_dir) 157 | -------------------------------------------------------------------------------- /MEG/step3aa_plot_validation_size_animacy.m: -------------------------------------------------------------------------------- 1 | function step3aa_plot_validation_size_animacy(bids_dir, varargin) 2 | %% Function that plots the results of the animacy & size validation analysis 3 | % 4 | % @ Lina Teichmann, 2022 5 | % 6 | % Usage: 7 | % step3aa_plot_validation_size_animacy(bids_dir, ...) 8 | % 9 | % Inputs: 10 | % bids_dir path to the bids root folder 11 | % 12 | % Returns: 13 | % _ Figure in BIDS/derivatives folder 14 | % 15 | 16 | 17 | %% parameters 18 | figdir = [bids_dir '/derivatives/figures/']; 19 | res_dir = [bids_dir '/derivatives/output/']; 20 | 21 | n_participants = 4; 22 | 23 | % plotting parameters 24 | col_pp = [0.21528455710115266, 0.5919540462603717, 0.3825837270552851; 25 | 0.24756252096251694, 0.43757475330612905, 0.5968141290988245; 26 | 0.7153368599631209, 0.546895038817448, 0.1270092896093349; 27 | 0.6772691643574462, 0.3168004639904812, 0.3167958318320575]; 28 | 29 | x_size = 0.19; 30 | y_size = 0.15; 31 | x_pos = linspace(0.1,0.9-x_size,4); 32 | y_pos = [0.55, 0.55-y_size*2]; 33 | fontsize = 20; 34 | 35 | %% load results 36 | % load results 37 | res_animacy = [];res_size = []; 38 | for p = 1:n_participants 39 | tmp=table2array(readtable([res_dir,'/validation-animacy-P',num2str(p),'.csv'],'ReadVariableNames',1,'PreserveVariableNames',1)); 40 | tmp=tmp(:,2:end); 41 | res_animacy(:,:,p) = mean(tmp,2); 42 | 43 | tmp=table2array(readtable([res_dir,'/validation-size-P',num2str(p),'.csv'],'ReadVariableNames',1,'PreserveVariableNames',1)); 44 | tmp=tmp(:,2:end); 45 | res_size(:,:,p) = mean(tmp,2); 46 | end 47 | 48 | % load one example output file to get the time vector 49 | load([res_dir '/pairwise_decoding/P1_pairwise_decoding_1854_block1.mat'], 'res') 50 | tv = res.a.fdim.values{1}*1000; 51 | 52 | 53 | %% plot 54 | f = figure(1);clf 55 | f.Position=[0,0,600,700]; 56 | 57 | text(0.5,0.39,'Size','FontSize',fontsize,'FontName','Helvetica','Units','normalized','HorizontalAlignment','center'); 58 | text(0.5,0.75,'Animacy','FontSize',fontsize,'FontName','Helvetica','Units','normalized','HorizontalAlignment','center'); 59 | axis off 60 | 61 | toplot = [{res_animacy},{res_size}]; 62 | for row = 1:2 63 | for p = 1:n_participants 64 | 65 | % define threshold based on pre-stimulus onset 66 | max_preonset = max(toplot{row}(tv<=0,p)); 67 | 68 | % plot data for each participant, fill when r > threshold 69 | ax1 = axes('Position',[x_pos(p),y_pos(row),x_size,y_size],'Units','normalized'); 70 | 71 | plot(tv,toplot{row}(:,p),'LineWidth',2,'Color',col_pp(p,:));hold on 72 | hf = fill([tv,tv(end)],[max(toplot{row}(:,p),max_preonset);max_preonset],col_pp(p,:),'EdgeColor','none','FaceAlpha',0.2); 73 | 74 | % make it look pretty 75 | ylim([-0.1,.3]) 76 | xlim([tv(1),tv(end)]) 77 | 78 | % find onset of the longest shaded cluster 79 | i=reshape(find(diff([0;toplot{row}(:,p)>max_preonset;0])~=0),2,[]); 80 | [~,jmax]=max(diff(i)); 81 | onset_idx=i(1,jmax); 82 | 83 | onset = tv(onset_idx); 84 | 85 | % add a marker for onsets 86 | text(onset,gca().YLim(1), char(8593),'Color',col_pp(p,:), 'FontSize', 24, 'VerticalAlignment', 'bottom', 'HorizontalAlignment','Center','FontName','DejaVu Sans') 87 | text(onset+15,gca().YLim(1), [num2str(onset) ' ms'],'Color',col_pp(p,:), 'FontSize', 14, 'VerticalAlignment', 'bottom', 'HorizontalAlignment','left') 88 | set(ax1,'FontSize',14,'box','off','FontName','Helvetica'); 89 | 90 | % add subject title 91 | ax1_title = axes('Position',[x_pos(p)+0.001,y_pos(row)+y_size-0.01,0.03,0.03]); 92 | text(0,0,['M' num2str(p)],'FontSize',12,'FontName','Helvetica'); 93 | ax1_title.Visible = 'off'; 94 | 95 | % add labels 96 | if p ==1 97 | ax1.YLabel.String = 'r'; 98 | else 99 | ax1.YTick = []; 100 | end 101 | ax1.XLabel.String = 'time (ms)'; 102 | 103 | end 104 | end 105 | 106 | % save figure 107 | fn = [figdir,'/validation_size-animacy']; 108 | tn = tempname; 109 | print(gcf,'-dpng','-r500',tn) 110 | 111 | im=imread([tn '.png']); 112 | [i,j]=find(mean(im,3)<255);margin=0; 113 | imwrite(im(min(i-margin):max(i+margin),min(j-margin):max(j+margin),:),[fn '.png'],'png'); 114 | 115 | print([fn '.pdf'],'-dpdf') 116 | 117 | end 118 | 119 | -------------------------------------------------------------------------------- /MEG/step3b_validation-fmri_meg_combo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | @ Lina Teichmann 5 | 6 | INPUTS: 7 | call from command line with following inputs: 8 | -bids_dir 9 | 10 | OUTPUTS: 11 | Using the MEG data to predict univariate fMRI activation in two ROIs 12 | 13 | NOTES: 14 | The plot was made in matlab so it looks the same as the decoding plots (see Step3bb) 15 | If the output directory does not exist, this script makes an output folder in BIDS/derivatives 16 | 17 | """ 18 | 19 | import numpy as np 20 | import mne,itertools,os, sys 21 | import pandas as pd 22 | import matplotlib.pyplot as plt 23 | from sklearn.preprocessing import StandardScaler 24 | from sklearn.pipeline import Pipeline 25 | from sklearn.linear_model import LinearRegression 26 | from joblib import Parallel, delayed 27 | 28 | 29 | 30 | # parameters 31 | n_participants = 4 32 | n_participants_fmri = 3 33 | n_sessions = 12 34 | 35 | #*****************************# 36 | ### HELPER FUNCTIONS ### 37 | #*****************************# 38 | def load_epochs(preproc_dir,all_epochs = []): 39 | for p in range(1,n_participants+1): 40 | epochs = mne.read_epochs(f'{preproc_dir}/preprocessed_P{str(p)}-epo.fif', preload=False) 41 | all_epochs.append(epochs) 42 | return all_epochs 43 | 44 | 45 | ## Helper functions 46 | 47 | 48 | class load_data: 49 | def __init__(self,dat,beta_dir,trial_type='exp'): 50 | self.dat = dat 51 | self.trial_type = trial_type 52 | self.beta_dir = beta_dir 53 | 54 | def load_meg(self): 55 | trialinfo = pd.read_csv(self.beta_dir + '/sub-01/sub-01_trialinfo.csv') 56 | trialinfo = trialinfo[trialinfo.trial_type=='exp'] 57 | fmri_concepts = np.sort(trialinfo.things_category_nr.unique()) 58 | 59 | # select exp trails & sort the trials based on things_category_nr & image name & select fMRI concepts only 60 | epochs_exp = self.dat[(self.dat.metadata['trial_type']=='exp')] 61 | sort_order = np.argsort(epochs_exp.metadata['image_path']) 62 | dat_sorted=epochs_exp[sort_order] 63 | 64 | meg_fmriconcepts = dat_sorted[dat_sorted.metadata.things_category_nr.isin(fmri_concepts)] 65 | meg_fmriconcepts.metadata['cv_split'] = np.tile(np.arange(1,n_sessions+1),720) 66 | sort_order = np.lexsort((meg_fmriconcepts.metadata['image_path'],meg_fmriconcepts.metadata['cv_split'])) 67 | meg_fmriconcepts = meg_fmriconcepts[sort_order] 68 | 69 | # getting data from each session and load it 70 | self.n_categories = len(meg_fmriconcepts.metadata.things_category_nr.unique()) 71 | self.n_sessions = len(meg_fmriconcepts.metadata.session_nr.unique()) 72 | self.n_channels = len(meg_fmriconcepts.ch_names) 73 | self.n_time = len(meg_fmriconcepts.times) 74 | sess_data = np.empty([self.n_categories,self.n_channels,self.n_time,self.n_sessions]) 75 | for split in range(self.n_sessions): 76 | print('loading data for cv-split ' + str(split+1)) 77 | curr_data = meg_fmriconcepts[meg_fmriconcepts.metadata['cv_split']==split+1] 78 | curr_data = curr_data.load_data() 79 | sess_data[:,:,:,split]= curr_data._data 80 | return sess_data 81 | 82 | def load_roi_betas(self): 83 | roi_ffa,roi_v1 = [],[] 84 | for ppt in range(1,n_participants_fmri+1): 85 | for roi_name, roi_array in zip(['ffa','v1'],[roi_ffa,roi_v1]): 86 | trialinfo = pd.read_csv(f'{self.beta_dir}/sub-{str(ppt).zfill(2)}/sub-{str(ppt).zfill(2)}_trialinfo.csv') 87 | try: 88 | roi = np.load(f'{self.beta_dir}/sub-{str(ppt).zfill(2)}/{roi_name}.npy') 89 | roi = roi.mean(axis=1) 90 | except: 91 | raise ValueError('This ROI file does not exist.') 92 | 93 | # take only experimental trials 94 | idx_exp = trialinfo.trial_type=='exp' 95 | roi_exp = roi[idx_exp] 96 | trialinfo_exp = trialinfo[idx_exp] 97 | trialinfo_exp.reset_index(drop=True,inplace=True) 98 | 99 | # sort based on things category 100 | trialinfo_exp_sorted = trialinfo_exp.sort_values('filename') 101 | trialinfo_exp_sorted['cv_split'] = np.tile(np.arange(1,n_sessions+1),720) 102 | trialinfo_exp_sorted=trialinfo_exp_sorted.sort_values(['cv_split','filename']) 103 | sort_index = trialinfo_exp_sorted.index.to_numpy() 104 | roi_exp_sorted = roi_exp[sort_index] 105 | roi_array.append(roi_exp_sorted.reshape([-1,n_sessions],order='F')) 106 | 107 | print('ROI shape: ' + str(np.array(roi_ffa).shape)) 108 | 109 | # average across people 110 | roi_ffa = np.array(roi_ffa).mean(axis=0) 111 | roi_v1 = np.array(roi_v1).mean(axis=0) 112 | 113 | return roi_ffa,roi_v1 114 | 115 | 116 | # Cross-validated linear regression 117 | class linear_regression: 118 | def __init__(self,dat,label): 119 | self.dat = dat 120 | self.label = label 121 | self.n_categories = dat.shape[0] 122 | self.n_channels = dat.shape[1] 123 | self.n_time = dat.shape[2] 124 | self.n_sessions = dat.shape[3] 125 | 126 | def train_test_splits(self): 127 | self.train_splits,self.test_splits = [],[] 128 | for comb in itertools.combinations(np.arange(self.n_sessions), self.n_sessions-1): 129 | self.train_splits.append(comb) 130 | self.test_splits.append(list(set(np.arange(self.n_sessions)) - set(comb))) 131 | return self.train_splits,self.test_splits 132 | 133 | def run(self): 134 | sess_dat = self.dat 135 | train_splits,test_splits = self.train_test_splits() 136 | 137 | pipe = Pipeline([('scaler', StandardScaler()), 138 | ('regression', LinearRegression())]) 139 | 140 | corr_coef = np.empty([self.n_time,self.n_sessions]) 141 | 142 | def fit_predict(pipe,train_x,train_y,test_x,test_y): 143 | pipe.fit(train_x,train_y) 144 | y_pred = pipe.predict(test_x) 145 | return np.corrcoef(y_pred,test_y)[0,1] 146 | 147 | 148 | for split in range(self.n_sessions): 149 | print('cv-split ' + str(split)) 150 | 151 | training_x = np.take(sess_dat,train_splits[split],axis=3) 152 | training_x = np.concatenate(tuple(training_x[:,:,:,i] for i in range(training_x.shape[3])),axis=0) 153 | 154 | training_y = np.take(self.label,train_splits[split],axis=1) 155 | training_y = np.concatenate(tuple(training_y[:,i] for i in range(training_y.shape[1])),axis=0) 156 | 157 | testing_x=np.take(sess_dat,test_splits[split][0],axis=3) 158 | testing_y = np.take(self.label,test_splits[split][0],axis=1) 159 | 160 | corr_coef_time = Parallel(n_jobs=24)(delayed(fit_predict)(pipe,training_x[:,:,t],training_y,testing_x[:,:,t],testing_y) for t in range(self.n_time)) 161 | corr_coef[:,split] = corr_coef_time 162 | 163 | return corr_coef 164 | 165 | def run(p,betas_dir,res_dir): 166 | all_epochs = load_epochs(preproc_dir) 167 | data = load_data(all_epochs[p-1],betas_dir,'exp').load_meg() 168 | Y_ffa,Y_v1 = load_data(all_epochs[p-1],betas_dir,trial_type='exp').load_roi_betas() 169 | 170 | corr_coeff_ffa = linear_regression(data,Y_ffa).run() 171 | corr_coeff_v1 = linear_regression(data,Y_v1).run() 172 | 173 | pd.DataFrame(corr_coeff_ffa).to_csv(f'{res_dir}/validation_fMRI-MEG-regression_ffa_P{str(p)}.csv') 174 | pd.DataFrame(corr_coeff_v1).to_csv(f'{res_dir}/validation_fMRI-MEG-regression_v1_P{str(p)}.csv') 175 | 176 | 177 | 178 | #*****************************# 179 | ### COMMAND LINE INPUTS ### 180 | #*****************************# 181 | if __name__=='__main__': 182 | import argparse 183 | parser = argparse.ArgumentParser() 184 | 185 | parser.add_argument( 186 | "-bids_dir", 187 | required=True, 188 | help='path to bids root', 189 | ) 190 | 191 | args = parser.parse_args() 192 | bids_dir = args.bids_dir 193 | preproc_dir = f'{bids_dir}/derivatives/preprocessed/' 194 | sourcedata_dir = f'{bids_dir}/sourcedata/' 195 | res_dir = f'{bids_dir}/derivatives/output/' 196 | betas_dir = f'{sourcedata_dir}/betas_roi/' 197 | if not os.path.exists(res_dir): 198 | os.makedirs(res_dir) 199 | 200 | ####### Run analysis ######## 201 | for p in range(1,n_participants+1): 202 | run(p,betas_dir,res_dir) 203 | 204 | -------------------------------------------------------------------------------- /MEG/step3bb_plot_validation_fmri_meg_combo.m: -------------------------------------------------------------------------------- 1 | function step3bb_plot_validation_fmri_meg_combo(bids_dir, varargin) 2 | %% Function that plots the results of the combination analysis between fMRI and MEG 3 | % we are taking the univariate activation in V1 and FFA and use the 4 | % time-resolved MEG data to predict these 5 | % 6 | % @ Lina Teichmann, 2022 7 | % 8 | % Usage: 9 | % step3bb_plot_validation_fmri_meg_combo(bids_dir, ...) 10 | % 11 | % Inputs: 12 | % bids_dir path to the bids root folder 13 | % 14 | % Returns: 15 | % _ Figure in BIDS/derivatives folder 16 | 17 | 18 | %% folders 19 | res_dir = [bids_dir '/derivatives/output/']; 20 | figdir = [bids_dir '/derivatives/figures/']; 21 | 22 | n_participants = 4; 23 | 24 | 25 | % plotting parameters 26 | col_pp = [0.21528455710115266, 0.5919540462603717, 0.3825837270552851; 27 | 0.24756252096251694, 0.43757475330612905, 0.5968141290988245; 28 | 0.7153368599631209, 0.546895038817448, 0.1270092896093349; 29 | 0.6772691643574462, 0.3168004639904812, 0.3167958318320575]; 30 | 31 | col_pp_light = [0.6020264172614653, 0.8666010337189269, 0.7198621708097467; 32 | 0.6329411764705883, 0.7552941176470587, 0.8572549019607842; 33 | 0.9347450980392157, 0.8266666666666667, 0.5554509803921569; 34 | 0.9019607843137256, 0.6803921568627451, 0.6803921568627451]; 35 | 36 | x_size = 0.19; 37 | y_size = 0.15; 38 | x_pos = linspace(0.1,0.9-x_size,4); 39 | 40 | %% load results 41 | ffa_res = [];v1_res = []; 42 | for p = 1:n_participants 43 | tmp=table2array(readtable([res_dir,'/validation_fMRI-MEG-regression_ffa_P',num2str(p),'.csv'],'ReadVariableNames',1,'PreserveVariableNames',1)); 44 | ffa_res(:,:,p) = tmp(:,2:end); 45 | 46 | tmp=table2array(readtable([res_dir,'/validation_fMRI-MEG-regression_v1_P',num2str(p),'.csv'],'ReadVariableNames',1,'PreserveVariableNames',1)); 47 | v1_res(:,:,p) = tmp(:,2:end); 48 | 49 | end 50 | 51 | % load one example output file to get the time vector 52 | load([res_dir '/pairwise_decoding/P1_pairwise_decoding_1854_block1.mat'], 'res') 53 | tv = res.a.fdim.values{1}*1000; 54 | 55 | 56 | %% plot 57 | f = figure(1);clf 58 | f.Position=[0,0,600,700]; 59 | for p = 1:n_participants 60 | [bci_ffa,~] = bootci(10000,{@mean,ffa_res(:,:,p)'},'alpha',.05,'type','per'); 61 | [bci_v1,~] = bootci(10000,{@mean,v1_res(:,:,p)'},'alpha',.05,'type','per'); 62 | 63 | % plot data with shaded bootci confidence intervals 64 | ax1 = axes('Position',[x_pos(p),0.5,x_size,y_size],'Units','normalized');hold on 65 | 66 | fill([tv,fliplr(tv)],[bci_ffa(1,:),fliplr(bci_ffa(2,:))],col_pp(p,:),'FaceAlpha',0.4,'EdgeColor',col_pp(p,:),'LineStyle','none') 67 | a(1)=plot(tv,movmean(mean(ffa_res(:,:,p),2),5),'Color',col_pp(p,:),'LineWidth',2); 68 | 69 | fill([tv,fliplr(tv)],[bci_v1(1,:),fliplr(bci_v1(2,:))],col_pp_light(p,:),'FaceAlpha',0.4,'EdgeColor',col_pp_light(p,:),'LineStyle','none') 70 | a(2)=plot(tv,movmean(mean(v1_res(:,:,p),2),5),'Color',col_pp_light(p,:),'LineWidth',2); 71 | 72 | plot(tv,tv*0,'k--') 73 | 74 | % make it look pretty 75 | xlim([tv(1),tv(end)]) 76 | ylim([-0.1,.25]) 77 | 78 | legend(a,[{'FFA'},{'V1'}],'box','off') 79 | 80 | % add subject title 81 | ax1_title = axes('Position',[x_pos(p)+0.001,0.5+y_size-0.01,0.03,0.03]); 82 | text(0,0,['M' num2str(p)],'FontSize',12,'FontName','Helvetica'); 83 | ax1_title.Visible = 'off'; 84 | 85 | % add labels 86 | if p ==1 87 | ax1.YLabel.String = 'r'; 88 | else 89 | ax1.YTick = []; 90 | end 91 | ax1.XLabel.String = 'time (ms)'; 92 | 93 | set(ax1,'FontSize',14,'box','off','FontName','Helvetica'); 94 | 95 | end 96 | 97 | 98 | % Plot differences 99 | clear('a') 100 | for p = 1:4 101 | toplot = v1_res(:,:,p)-ffa_res(:,:,p); 102 | [bci,~] = bootci(10000,{@mean,toplot'},'alpha',.05,'type','per'); 103 | 104 | % plot data with shaded bootci confidence intervals 105 | ax1 = axes('Position',[x_pos(p),0.5-y_size*1.5,x_size,y_size],'Units','normalized');hold on 106 | 107 | fill([tv,fliplr(tv)],[bci(1,:),fliplr(bci(2,:))],'k','FaceAlpha',0.4,'EdgeColor',col_pp(p,:),'LineStyle','none') 108 | a(1)=plot(tv,movmean(mean(toplot,2),5),'Color','k','LineWidth',2); 109 | 110 | plot(tv,tv*0,'k--') 111 | 112 | % make it look pretty 113 | xlim([tv(1),tv(end)]) 114 | ylim([-0.15,.25]) 115 | 116 | % add labels 117 | if p ==1 118 | ax1.YLabel.String = 'V1 - FFA'; 119 | else 120 | ax1.YTick = []; 121 | end 122 | ax1.XLabel.String = 'time (ms)'; 123 | 124 | set(ax1,'FontSize',14,'box','off','FontName','Helvetica'); 125 | 126 | [highest,idx] = max(movmean(mean(toplot,2),5)); 127 | ah = annotation('textarrow','X',[tv(idx)+200,tv(idx)+25],'Y',[highest,highest],'String',[num2str(tv(idx)),' ms'],'HorizontalAlignment','left','FontName','Helvetica','FontSize',14); 128 | set(ah,'parent',ax1); 129 | end 130 | 131 | 132 | %% save figure 133 | fn = [figdir,'/validation_fmri-meg-combo']; 134 | tn = tempname; 135 | print(gcf,'-dpng','-r500',tn) 136 | 137 | im=imread([tn '.png']); 138 | [i,j]=find(mean(im,3)<255);margin=0; 139 | imwrite(im(min(i-margin):max(i+margin),min(j-margin):max(j+margin),:),[fn '.png'],'png'); 140 | 141 | 142 | print([fn '.pdf'],'-dpdf') 143 | 144 | -------------------------------------------------------------------------------- /MEG/step3c_validation_pairwise_decoding_mne_to_cosmo.m: -------------------------------------------------------------------------------- 1 | function step3c_validation_pairwise_decoding_mne_to_cosmo(bids_dir, toolbox_dir, varargin) 2 | %% Function that takes preprocessed MNE data and transforms it to a matlab-cosmo script 3 | % Note: this script requires the MNE-matlab & cosmomvpa toolboxes 4 | % 5 | % @ Lina Teichmann, 2022 6 | % 7 | % Usage: 8 | % step3c_validation_pairwise_decoding_mne_to_cosmo(bids_dir, ...) 9 | % 10 | % Inputs: 11 | % bids_dir path to the bids root folder 12 | % toolbox_dir path to toolbox folder containtining CoSMoMVPA 13 | % 14 | % Returns: 15 | % ds Cosmo data struct, saved in BIDS/derivatives/preprocessed folder 16 | % 17 | 18 | 19 | %% parameters 20 | preprocdir = [bids_dir '/derivatives/preprocessed/']; 21 | 22 | addpath(genpath([toolbox_dir '/mne-matlab'])) 23 | addpath(genpath([toolbox_dir '/CoSMoMVPA'])) 24 | 25 | n_participants = 4; 26 | 27 | %% loop 28 | for p=1:n_participants 29 | tic 30 | tmp_filenames = dir([preprocdir '/preprocessed_P' num2str(p) '-epo*.fif']); 31 | n1 = {tmp_filenames.name}; 32 | [~,I] = sort(cellfun(@length,n1)); 33 | all_files = n1(I); 34 | 35 | %sanity check 36 | disp('stacking files in this order: ') 37 | for i = 1:length(all_files); disp(all_files{i}); end 38 | 39 | for f = 1:length(all_files) 40 | epo{f} = make_ds(fiff_read_epochs([preprocdir filesep cell2mat(all_files(f))])); 41 | end 42 | 43 | sa_tab = readtable([bids_dir '/sourcedata/sample_attributes_P' num2str(p) '.csv']); 44 | sa = table2struct(sa_tab,'toscalar',1); 45 | 46 | ds = cosmo_stack(epo); 47 | ds.sa = sa; 48 | 49 | save([preprocdir '/P' num2str(p) '_cosmofile.mat'],'ds','-v7.3') 50 | fprintf('Saving finished in %i seconds\n',ceil(toc)) 51 | 52 | end 53 | 54 | 55 | 56 | end 57 | 58 | 59 | %% helper function 60 | function ds = make_ds(part) 61 | data = reshape(part.data,[size(part.data,1),size(part.data,2)*size(part.data,3)]); 62 | chan = repmat(1:size(part.data,2),1,size(part.data,3)); 63 | time = repelem(1:size(part.data,3),1,size(part.data,2)); 64 | 65 | ds = struct(); 66 | ds.samples = data; 67 | ds.a.fdim.labels = [{'chan'};{'time'}]; 68 | ds.a.fdim.values = [{1:size(part.data,2)};{part.times}]; 69 | ds.fa.chan = chan; 70 | ds.fa.time = time; 71 | end 72 | 73 | 74 | -------------------------------------------------------------------------------- /MEG/step3d_validation_pairwise_decoding200.m: -------------------------------------------------------------------------------- 1 | function step3d_validation_pairwise_decoding200(bids_dir, toolbox_dir, participant, blocknr, n_blocks) 2 | 3 | %% Function to run pairwise decoding analysis for the 200 repeat stimuli 4 | % 5 | % @ Lina Teichmann, 2022 6 | % 7 | % Usage: 8 | % step3d_validation_pairwise_decoding200(bids_dir,participant, ...) 9 | % 10 | % Inputs: 11 | % bids_dir path to the bids root folder 12 | % toolbox_dir path to toolbox folder containtining CoSMoMVPA 13 | % participant participant number 14 | % blocknr number of the chunk you want to run this analysis for 15 | % n_blocks how many blocks you want to run this analysis in (this is to make it faster by running stuff in parallel) 16 | % 17 | % Returns: 18 | % decoding_acc file that has the decoding accuracy for each decoding block ('PX_pairwise)decoding_200_blockX.mat') 19 | % decoding_pairs file that contains which pairwise comparisons were run so it can be stacked back together ('PX_pairwise)decoding_200_blockX_pairs.mat') 20 | 21 | 22 | %% folders 23 | preprocdir = [bids_dir '/derivatives/preprocessed/']; 24 | res_dir = [bids_dir '/derivatives/output/']; 25 | 26 | addpath(genpath([toolbox_dir '/CoSMoMVPA'])) 27 | 28 | load([preprocdir '/P' num2str(participant) '_cosmofile.mat'],'ds'); 29 | 30 | % make a pairwise decoding folder if it does not exist 31 | if ~exist([res_dir '/pairwise_decoding'], 'dir') 32 | mkdir([res_dir '/pairwise_decoding']) 33 | end 34 | outfn = [res_dir '/pairwise_decoding/P' num2str(participant) '_pairwise_decoding_200_block' num2str(blocknr) '.mat']; 35 | outfn_pairs = [res_dir '/pairwise_decoding/P' num2str(participant) '_pairwise_decoding_200_block' num2str(blocknr) '_pairs.mat']; 36 | 37 | 38 | %% pairwise decoding 39 | ds = cosmo_slice(ds,strcmp(ds.sa.trial_type,'test')); 40 | ds.sa.targets = ds.sa.things_category_nr; 41 | ds.sa.chunks = ds.sa.session_nr; 42 | all_combinations = combnk(unique(ds.sa.targets),2); 43 | 44 | % split into blocks 45 | step = ceil(length(all_combinations)/n_blocks); 46 | s = 1:step:length(all_combinations); 47 | blocks = cell(length(s),1); 48 | for b = 1:length(s) 49 | blocks{b} = all_combinations(s(b):min(s(b)+step-1,length(all_combinations)),:); 50 | end 51 | 52 | combs = blocks{blocknr}; 53 | save(outfn_pairs, 'combs') 54 | nproc = cosmo_parallel_get_nproc_available; 55 | 56 | res = []; 57 | for pairs = 1:length(combs) 58 | tic 59 | disp([num2str(pairs) ' out of ' num2str(length(combs))]) 60 | ds_p = cosmo_slice(ds,ismember(ds.sa.things_category_nr,combs(pairs,:))); 61 | partitions = cosmo_nfold_partitioner(ds_p); 62 | measure_args=struct(); 63 | measure_args.classifier=@cosmo_classify_lda; 64 | measure_args.partitions=partitions; 65 | measure_args.nproc = nproc; 66 | nbrhood=cosmo_interval_neighborhood(ds_p,'time','radius',0); 67 | res{pairs}=cosmo_searchlight(ds_p,nbrhood,@cosmo_crossvalidation_measure,measure_args); 68 | toc 69 | end 70 | res_pairs = cosmo_stack(res); 71 | save(outfn, 'res_pairs','-v7.3') 72 | 73 | end 74 | 75 | 76 | -------------------------------------------------------------------------------- /MEG/step3e_validation_pairwise_decoding1854.m: -------------------------------------------------------------------------------- 1 | function step3e_validation_pairwise_decoding1854(bids_dir, toolbox_dir, participant, blocknr, n_blocks) 2 | %% Function to run pairwise decoding analysis for the 1854 object classes 3 | % 4 | % @ Lina Teichmann, 2022 5 | % 6 | % Usage: 7 | % step3d_validation_pairwise_decoding200(bids_dir,participant, ...) 8 | % 9 | % Inputs: 10 | % bids_dir path to the bids root folder 11 | % toolbox_dir path to toolbox folder containtining CoSMoMVPA 12 | % participant participant number 13 | % blocknr number of the chunk you want to run this analysis for 14 | % n_blocks how many blocks you want to run this analysis in (this is to make it faster by running stuff in parallel) 15 | % 16 | % Returns: 17 | % decoding_acc file that has the decoding accuracy for each decoding block ('PX_pairwise)decoding_1854_blockX.mat') 18 | % decoding_pairs file that contains which pairwise comparisons were run so it can be stacked back together ('PX_pairwise)decoding_1854_blockX_pairs.mat') 19 | 20 | 21 | 22 | %% folders 23 | preprocdir = [bids_dir '/derivatives/preprocessed/']; 24 | res_dir = [bids_dir '/derivatives/output/']; 25 | 26 | addpath(genpath([toolbox_dir '/CoSMoMVPA'])) 27 | 28 | load([preprocdir '/P' num2str(participant) '_cosmofile.mat'],'ds'); 29 | 30 | % make a pairwise decoding folder if it does not exist 31 | if ~exist([res_dir '/pairwise_decoding'], 'dir') 32 | mkdir([res_dir '/pairwise_decoding']) 33 | end 34 | outfn = [res_dir '/pairwise_decoding/P' num2str(participant) '_pairwise_decoding_1854_block' num2str(blocknr) '.mat']; 35 | outfn_pairs = [res_dir '/pairwise_decoding/P' num2str(participant) '_pairwise_decoding_1854_block' num2str(blocknr) '_pairs.mat']; 36 | 37 | %% pairwise decoding 38 | ds = cosmo_slice(ds,strcmp(ds.sa.trial_type,'exp')); 39 | ds.sa.targets = ds.sa.things_category_nr; 40 | ds.sa.chunks = ds.sa.session_nr; 41 | all_combinations = combnk(unique(ds.sa.targets),2); 42 | all_targets = unique(ds.sa.targets); 43 | all_chunks = unique(ds.sa.chunks); 44 | 45 | % split into blocks 46 | step = ceil(length(all_combinations)/n_blocks); 47 | s = 1:step:length(all_combinations); 48 | blocks = cell(length(s),1); 49 | for b = 1:length(s) 50 | blocks{b} = all_combinations(s(b):min(s(b)+step-1,length(all_combinations)),:); 51 | end 52 | 53 | combs = blocks{blocknr}; 54 | save(outfn_pairs, 'combs') 55 | nproc = cosmo_parallel_get_nproc_available; 56 | 57 | 58 | %% create RDM 59 | % find the items belonging to the exemplars 60 | target_idx = cell(1,length(all_targets)); 61 | for j=1:length(all_targets) 62 | target_idx{j} = find(ds.sa.targets==all_targets(j)); 63 | end 64 | % for each chunk, find items belonging to the test set 65 | test_chunk_idx = cell(1,length(all_chunks)); 66 | for j=1:length(all_chunks) 67 | test_chunk_idx{j} = find(ds.sa.chunks==all_chunks(j)); 68 | end 69 | 70 | %% make blocks for parfor loop 71 | step = ceil(length(combs)/nproc); 72 | s = 1:step:length(combs); 73 | comb_blocks = cell(1,length(s)); 74 | for b = 1:nproc 75 | comb_blocks{b} = combs(s(b):min(s(b)+step-1,length(combs)),:); 76 | end 77 | 78 | %arguments for searchlight and crossvalidation 79 | ma = struct(); 80 | ma.classifier = @cosmo_classify_lda; 81 | ma.output = 'accuracy'; 82 | ma.check_partitions = false; 83 | ma.nproc = 1; 84 | ma.progress = 0; 85 | ma.partitions = struct(); 86 | 87 | % set options for each worker process 88 | nh = cosmo_interval_neighborhood(ds,'time','radius',0); 89 | worker_opt_cell = cell(1,nproc); 90 | for procs=1:nproc 91 | worker_opt=struct(); 92 | worker_opt.ds=ds; 93 | worker_opt.ma = ma; 94 | worker_opt.uc = all_chunks; 95 | worker_opt.worker_id=procs; 96 | worker_opt.nproc=nproc; 97 | worker_opt.nh=nh; 98 | worker_opt.combs = comb_blocks{procs}; 99 | worker_opt.target_idx = target_idx; 100 | worker_opt.test_chunk_idx = test_chunk_idx; 101 | worker_opt_cell{procs}=worker_opt; 102 | end 103 | %% run the workers 104 | tic 105 | result_map_cell=cosmo_parcellfun(nproc,@run_block_with_worker,worker_opt_cell,'UniformOutput',false); 106 | toc 107 | %% cat the results 108 | res=cosmo_stack(result_map_cell); 109 | 110 | %% save 111 | fprintf('Saving...');tic 112 | save(outfn,'res','-v7.3') 113 | fprintf('Saving finished in %i seconds\n',ceil(toc)) 114 | 115 | 116 | end 117 | 118 | function res_block = run_block_with_worker(worker_opt) 119 | ds=worker_opt.ds; 120 | nh=worker_opt.nh; 121 | ma=worker_opt.ma; 122 | uc=worker_opt.uc; 123 | target_idx=worker_opt.target_idx; 124 | test_chunk_idx=worker_opt.test_chunk_idx; 125 | worker_id=worker_opt.worker_id; 126 | nproc=worker_opt.nproc; 127 | combs=worker_opt.combs; 128 | res_cell = cell(1,length(combs)); 129 | cc=clock();mm=''; 130 | for i=1:length(combs) 131 | idx_ex = [target_idx{combs(i,1)}; target_idx{combs(i,2)}]; 132 | [ma.partitions.train_indices,ma.partitions.test_indices] = deal(cell(1,length(uc))); 133 | for j=1:length(uc) 134 | ma.partitions.train_indices{j} = setdiff(idx_ex,test_chunk_idx{j}); 135 | ma.partitions.test_indices{j} = intersect(test_chunk_idx{j},idx_ex); 136 | end 137 | res_cell{i} = cosmo_searchlight(ds,nh,@cosmo_crossvalidation_measure,ma); 138 | res_cell{i}.sa.target1 = combs(i,1); 139 | res_cell{i}.sa.target2 = combs(i,2); 140 | if ~mod(i,10) 141 | mm=cosmo_show_progress(cc,i/length(combs),sprintf('%i/%i for worker %i/%i\n',i,length(combs),worker_id,nproc),mm); 142 | end 143 | end 144 | res_block = cosmo_stack(res_cell); 145 | end 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /MEG/step3f_validation_pairwise_decoding_stack_plot.m: -------------------------------------------------------------------------------- 1 | function step3f_validation_pairwise_decoding_stack_plot(bids_dir, toolbox_dir, imagewise_nblocks, sessionwise_nblocks, varargin) 2 | %% Stacking the pairwise decoding results and making a plot 3 | % 4 | % 5 | % @ Lina Teichmann, 2022 6 | % 7 | % Usage: 8 | % step3f_validation_pairwise_decoding_stack_plot(bids_dir, imagewise_nblocks, sessionwise_nblocks, ...) 9 | % 10 | % Inputs: 11 | % bids_dir path to the bids root folder 12 | % toolbox_dir path to toolbox folder containtining CoSMoMVPA 13 | % imagewise_nblocks number of blocks used to run the decoding analysis for the 200 objects in parallel 14 | % sessionwise_nblocks number of blocks used to run the decoding analysis for the 1854 objects in parallel 15 | % 16 | % Returns: 17 | % RDM200 stacked results of the decoding analysis for 200 images saved in BIDS/derivatives 18 | % RDM1854 stacked results of the decoding analysis for 1854 concepts saved in BIDS/derivatives 19 | % _ Figure in BIDS/derivatives folder 20 | 21 | 22 | %% folders 23 | res_dir = [bids_dir '/derivatives/output/']; 24 | figdir = [bids_dir '/derivatives/figures/']; 25 | 26 | addpath(genpath([toolbox_dir '/CoSMoMVPA'])) 27 | 28 | %% parameters 29 | n_participants = 4; 30 | col_pp = [0.21528455710115266, 0.5919540462603717, 0.3825837270552851; 31 | 0.24756252096251694, 0.43757475330612905, 0.5968141290988245; 32 | 0.7153368599631209, 0.546895038817448, 0.1270092896093349; 33 | 0.6772691643574462, 0.3168004639904812, 0.3167958318320575]; 34 | 35 | %% load and stack 36 | for p = 1:n_participants 37 | for blocknr = 1:imagewise_nblocks 38 | outfn = [res_dir '/pairwise_decoding/P' num2str(p) '_pairwise_decoding_200_block' num2str(blocknr) '.mat']; 39 | load(outfn, 'res_pairs') 40 | all_res{blocknr} = res_pairs; 41 | end 42 | res_200(p)= cosmo_stack(all_res); 43 | 44 | 45 | for blocknr = 1:sessionwise_nblocks 46 | outfn = [res_dir '/pairwise_decoding/P' num2str(p) '_pairwise_decoding_1854_block' num2str(blocknr) '.mat']; 47 | load(outfn, 'res') 48 | all_res_1854{blocknr} = res; 49 | end 50 | res_1854(p) = cosmo_stack(all_res_1854); 51 | end 52 | 53 | %% stack all pairwise decoding results 54 | for p = 1:n_participants 55 | all_res_samples_200(:,:,p)=res_200(p).samples; 56 | all_res_samples_1854(:,:,p)=res_1854(p).samples; 57 | end 58 | all_res_samples_200 = mean(all_res_samples_200,3); 59 | mean_rdm_200 = res_200(1); 60 | mean_rdm_200.samples = all_res_samples_200; 61 | 62 | all_res_samples_1854 = mean(all_res_samples_1854,3); 63 | mean_rdm_1854 = res_1854(1); 64 | mean_rdm_1854.samples = all_res_samples_1854; 65 | 66 | 67 | %% plot mean accuracy for 200 & 1854 object pairwise decoding over time 68 | figure(1);clf 69 | 70 | tv = res_200(1).a.fdim.values{1}*1000; 71 | 72 | plot_mean_decoding(1,res_200,'Mean Pairwise Decoding 200 Objects',tv,n_participants,col_pp,[45,80]) 73 | %% 74 | saveas(gcf,[figdir '/PairwiseDecoding_timeseries_200.pdf']) 75 | 76 | figure(2);clf 77 | tv = res_1854(1).a.fdim.values{1}*1000; 78 | plot_mean_decoding(1,res_1854,'Mean Pairwise Decoding 1854 Objects',tv,n_participants,col_pp,[48,60]) 79 | saveas(gcf,[figdir '/PairwiseDecoding_timeseries_1854.pdf']) 80 | 81 | %% plot the dissimilarity matrix 200 82 | figure(3);clf 83 | mat = nan(200,200,length(tv),n_participants); 84 | all_combinations = combnk(1:200,2); 85 | for p = 1:n_participants 86 | for t = 1:length(tv) 87 | for i = 1:size(all_combinations,1) 88 | r = all_combinations(i,1); 89 | c = all_combinations(i,2); 90 | 91 | mat(r,c,t,p) = res_200(p).samples(i,t); 92 | mat(c,r,t,p) = res_200(p).samples(i,t); 93 | 94 | end 95 | 96 | end 97 | end 98 | 99 | average_rdm = mean(mat,4); 100 | plot_rdm(2,res_200,average_rdm,'Pairwise Decoding (peak time), 200 Objects',n_participants,[45,90]) 101 | saveas(gcf,[figdir '/PairwiseDecoding_rdm_200.pdf']) 102 | 103 | save([res_dir,'/validation-pairwise_decoding_RDM200'],'mat','-v7.3') 104 | 105 | 106 | %% plot the dissimilarity matrix 1854 107 | figure(4);clf 108 | mat = nan(1854,1854,length(tv),n_participants); 109 | for p = 1:n_participants 110 | disp(p) 111 | all_combinations = [res_1854(p).sa.target1,res_1854(p).sa.target2]; 112 | 113 | for t = 1:length(tv) 114 | for i = 1:size(all_combinations,1) 115 | r = all_combinations(i,1); 116 | c = all_combinations(i,2); 117 | mat(r,c,t,p) = res_1854(p).samples(i,t); 118 | mat(c,r,t,p) = res_1854(p).samples(i,t); 119 | end 120 | 121 | end 122 | end 123 | 124 | average_rdm = mean(mat,4); 125 | plot_rdm(2,res_1854,average_rdm,'Pairwise Decoding (peak time), 1854 Objects',n_participants,[45,70]) 126 | saveas(gcf,[figdir '/PairwiseDecoding_rdm_1854.pdf']) 127 | 128 | save([res_dir,'/validation-pairwise_decoding_RDM1854'],'mat','-v7.3') 129 | 130 | 131 | end 132 | 133 | 134 | %% Helper functions for plotting 135 | function plot_mean_decoding(fignum,toplot,title_string,tv,n_participants,cols,ylimit) 136 | figure(fignum);clf; 137 | 138 | for p = 1:n_participants 139 | a(p) = plot(tv,mean(toplot(p).samples*100),'LineWidth',2,'Color',cols(p,:));hold on 140 | end 141 | 142 | plot(tv,tv*0+50,'k--') 143 | 144 | xlabel('time (ms)') 145 | ylabel('Decoding Accuracy (%)') 146 | title(title_string) 147 | set(gcf,'Units','centimeters','Position',[0,0,15,10]) 148 | set(gca,'FontSize',14,'Box','off','FontName','Helvetica') 149 | legend(a,['M1';'M2';'M3';'M4']) 150 | 151 | xlim([tv(1),tv(end)]) 152 | ylim([ylimit(1),ylimit(2)]) 153 | 154 | end 155 | 156 | 157 | function plot_rdm(fignum,toplot,average_rdm,title_string,n_participants,col_lim) 158 | figure(fignum);clf; 159 | 160 | for p = 1:n_participants 161 | all_res(p,:)=mean(toplot(p).samples); 162 | end 163 | [~,max_index] = max(mean(all_res)); 164 | imagesc(average_rdm(:,:,max_index)*100,col_lim); cb=colorbar; cb.Label.String = 'Decoding Accuracy (%)';cb.Location = 'southoutside'; 165 | axis square; 166 | set(gca,'xTick',[],'yTick',[],'FontSize',12,'FontName','Helvetica') 167 | title(title_string) 168 | set(gcf,'Units','centimeters','Position',[0,0,12,12]) 169 | 170 | end 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /MEG/step3g_validation_pairwise_decoding_mds.m: -------------------------------------------------------------------------------- 1 | function step3g_validation_pairwise_decoding_mds(bids_dir, toolbox_dir, varargin) 2 | %% Making an MDS plot for some image categories 3 | % 4 | % 5 | % @ Lina Teichmann, 2022 6 | % 7 | % Usage: 8 | % step3g_validation_pairwise_decoding_mds(bids_dir, ...) 9 | % 10 | % Inputs: 11 | % bids_dir path to the bids root folder 12 | % toolbox_dir path to toolbox folder containtining CoSMoMVPA 13 | 14 | % Returns: 15 | % _ Figure in BIDS/derivatives folder 16 | 17 | 18 | %% folders 19 | res_dir = [bids_dir '/derivatives/output/']; 20 | figdir = [bids_dir '/derivatives/figures/']; 21 | 22 | addpath(genpath([toolbox_dir '/CoSMoMVPA'])) 23 | 24 | %% parameters 25 | n_participants = 4; 26 | 27 | % plotting parameters 28 | col_pp = [0.21528455710115266, 0.5919540462603717, 0.3825837270552851; 29 | 0.24756252096251694, 0.43757475330612905, 0.5968141290988245; 30 | 0.7153368599631209, 0.546895038817448, 0.1270092896093349; 31 | 0.6772691643574462, 0.3168004639904812, 0.3167958318320575]; 32 | colour_gray = [0.75,0.75,0.75]; 33 | line_col = [0, 109, 163]./255; 34 | contrast_cols = cat(3,[[70, 225, 240]./255;[191, 82, 124]./255],... 35 | [[126, 92, 150]./255;[255, 153, 0]./255]); 36 | ylims = [41,80;45,65]; 37 | x_size = 0.19; 38 | y_size = 0.15; 39 | size_mds = 0.09; 40 | x_pos = linspace(0.1,0.9-x_size,4); 41 | y_pos = [0.8,0.54,0.29]; 42 | 43 | 44 | %% load results 45 | % load one example output file to get the time vector 46 | load([res_dir '/pairwise_decoding/P1_pairwise_decoding_1854_block1.mat'], 'res') 47 | tv = res.a.fdim.values{1}*1000; 48 | 49 | % load decoding results 50 | load([res_dir,'/validation-pairwise_decoding_RDM1854'],'mat') 51 | decoding_1854 = mat; 52 | 53 | load([res_dir,'/validation-pairwise_decoding_RDM200'],'mat') 54 | decoding_200 = mat; 55 | 56 | % load category labels 57 | labels = readtable([bids_dir '/sourcedata/category_mat_manual.tsv'],'FileType','text','PreserveVariableNames',1); 58 | 59 | 60 | %% MDS: highlighting animals vs food and vehicles vs tools 61 | colour_cat1 = zeros(1854,1); 62 | colour_cat1(labels.animal==1&labels.food==0)=1; 63 | colour_cat1(labels.animal==0&labels.food==1)=2; 64 | 65 | colour_cat2 = zeros(1854,1); 66 | colour_cat2(labels.vehicle==1&labels.tool==0)=1; 67 | colour_cat2(labels.vehicle==0&labels.tool==1)=2; 68 | % colour_cat2(labels.plant==1&labels.bodyPart==0)=1; 69 | % colour_cat2(labels.plant==0&labels.bodyPart==1)=2; 70 | 71 | 72 | mds_categories = [colour_cat1,colour_cat2]; 73 | legends = cat(3,[{'animals'},{'food'}],[{'vehicles'},{'tools'}]); 74 | % legends = cat(3,[{'animals'},{'food'}],[{'plants'},{'body parts'}]); 75 | 76 | average_rdm = mean(decoding_1854,4); 77 | 78 | % loop over the two MDS comparisons 79 | for i = 1:size(mds_categories,2) 80 | colour_cat = mds_categories(:,i); 81 | mean_mds_comp = squeeze(mean(mean(average_rdm(colour_cat==1,colour_cat==2,:)))); 82 | 83 | avg = []; 84 | for t=1:length(tv) 85 | D = average_rdm(:,:,t); 86 | D(find(eye(size(D))))=0; 87 | [Y(:,:,t),~] = cmdscale(D,2); 88 | 89 | tmp = triu(average_rdm(:,:,t),1); 90 | tmp = tmp(colour_cat==0,colour_cat==0); 91 | avg=[avg;mean(mean(tmp(tmp>0)))]; 92 | 93 | end 94 | 95 | % use procrustes to align the different MDS over time 96 | for t = length(tv):-1:2 97 | [~,z(:,:,t,i)] = procrustes(Y(:,:,t),Y(:,:,t-1)); 98 | end 99 | 100 | end 101 | 102 | %% plot timecourse together with MDS snapshots 103 | toplot = zeros(length(tv),4); 104 | 105 | all_decoding = [{decoding_200},{decoding_1854}]; 106 | 107 | for i = 1:2 108 | for p = 1:n_participants 109 | for t = 1:length(tv) 110 | tmp = triu(all_decoding{i}(:,:,t,p),1); 111 | toplot(t,p,i) = mean(mean(tmp(tmp>0)))*100; 112 | end 113 | end 114 | end 115 | 116 | f=figure(2);clf; 117 | f.Position=[0,0,600,700]; 118 | titles = [{'Object image decoding'},{'Object category decoding'}]; 119 | 120 | 121 | 122 | % decoding plots for image and category decoding 123 | for i = 1:2 124 | for p = 1:4 125 | 126 | % define threshold based on pre-stimulus onset 127 | max_preonset = max(toplot(tv<=0,p,i)); 128 | 129 | % plot data for each participant, fill when r > threshold 130 | disp(y_pos(i)) 131 | ax = axes('Position',[x_pos(p),y_pos(i),x_size,y_size],'Units','normalized'); 132 | plot(tv,toplot(:,p,i),'LineWidth',2,'Color',col_pp(p,:));hold on 133 | hf = fill([tv,tv(end)],[max(toplot(:,p,i),max_preonset);max_preonset],col_pp(p,:),'EdgeColor','none','FaceAlpha',0.2); 134 | 135 | % make it look pretty 136 | ylim(ylims(i,:)) 137 | xlim([tv(1),tv(end)]) 138 | if p ==1 139 | ax.YLabel.String = [{'Decoding'}; {'accuracy (%)'}]; 140 | else 141 | ax.YTick = []; 142 | end 143 | xlabel('time (ms)') 144 | set(gca(),'FontSize',12,'box','off','FontName','Helvetica'); 145 | 146 | % find onset of the longest shaded cluster 147 | ii=reshape(find(diff([0;toplot(:,p,i)>max_preonset;0])~=0),2,[]); 148 | [~,jmax]=max(diff(ii)); 149 | onset_idx=ii(1,jmax); 150 | 151 | onset = tv(onset_idx); 152 | 153 | % add a marker for onsets 154 | text(onset,gca().YLim(1), char(8593),'Color',col_pp(p,:), 'FontSize', 24, 'VerticalAlignment', 'bottom', 'HorizontalAlignment','Center','FontName','Helvetica') 155 | text(onset+20,gca().YLim(1), [num2str(onset) ' ms'],'Color',col_pp(p,:), 'FontSize', 14, 'VerticalAlignment', 'bottom', 'HorizontalAlignment','left') 156 | 157 | % add subject title 158 | ax1_title = axes('Position',[x_pos(p)+0.001,y_pos(i)+y_size-0.01,0.03,0.03]); 159 | text(0,0,['M' num2str(p)],'FontSize',11,'FontName','Helvetica'); 160 | ax1_title.Visible = 'off'; 161 | 162 | end 163 | 164 | end 165 | 166 | 167 | % add title 168 | row_title = axes('Position',[x_pos(1)+0.01,y_pos(1)+y_size+0.02,0.03,0.03]); 169 | text(0,0,titles{1},'FontSize',14,'FontWeight','bold','FontName','Helvetica') 170 | row_title.Visible = 'off'; 171 | 172 | row_title = axes('Position',[x_pos(1)+0.01,y_pos(2)+y_size+0.04,0.03,0.03]); 173 | text(0,0,titles{2},'FontSize',14,'FontWeight','bold','FontName','Helvetica') 174 | row_title.Visible = 'off'; 175 | 176 | row_title = axes('Position',[x_pos(1)+0.01,y_pos(2)+y_size+0.015,0.03,0.03]); 177 | text(0,0,'Single subjects','FontSize',12,'FontName','Helvetica') 178 | row_title.Visible = 'off'; 179 | 180 | 181 | % MDS 182 | group_avg = mean(toplot(:,:,2),2); 183 | ax1 = axes('Position',[x_pos(1),y_pos(3),x_pos(end)+x_size/2,y_size],'Units','normalized'); 184 | upper = group_avg' + std(toplot(:,:,2)')/sqrt(size(toplot,2)); 185 | lower = group_avg' - std(toplot(:,:,2)')/sqrt(size(toplot,2)); 186 | 187 | fill([tv,fliplr(tv)],[lower,fliplr(upper)],'k','FaceAlpha',0.1,'LineStyle','none'); hold on 188 | plot(tv, group_avg,'Color','k','LineWidth',2); 189 | plot(tv,tv*0+50,'k--') 190 | 191 | fill([tv,fliplr(tv)],[lower,fliplr(upper)],[1,1,1]/255,'FaceAlpha',0.1,'LineStyle','none'); hold on 192 | plot(tv, group_avg,'Color',[1,1,1]/255,'LineWidth',2); 193 | plot(tv,tv*0+50,'Color',[1,1,1]/255) 194 | 195 | xlim([tv(1),tv(end)]) 196 | 197 | ax1.YLabel.String = [{'Decoding'}; {'accuracy (%)'}]; 198 | ax1.XLabel.String='time (ms)'; 199 | ax1.XTick = [-80,0,120,320,520,720,920,1120]; 200 | set(ax1,'FontSize',12,'box','off','FontName','Helvetica'); 201 | 202 | % add title 203 | row_title = axes('Position',[x_pos(1)+0.01,y_pos(3)+y_size+0.02,0.03,0.03]); 204 | text(0,0,'Group Average','FontSize',12,'FontName','Helvetica') 205 | row_title.Visible = 'off'; 206 | 207 | 208 | t_idx = 5:40:length(tv)+1; 209 | t_time = tv(t_idx); 210 | tv_pix = linspace(ax1.Position(1),ax1.Position(1)+ax1.Position(3),length(tv)); 211 | 212 | % loop over MDS comparisons 213 | for i = 1:2 214 | color1 = contrast_cols(1,:,i); 215 | color2 = contrast_cols(2,:,i); 216 | colour_cat = mds_categories(:,i); 217 | 218 | % loop over time 219 | for t = 1:length(t_idx) 220 | ax2 = axes(); 221 | a=[]; 222 | a(3)=scatter(z(colour_cat==0,1,t_idx(t),i),z(colour_cat==0,2,t_idx(t),i),15,'MarkerFaceAlpha',1,'MarkerFaceColor',colour_gray,'MarkerEdgeColor','None');hold on 223 | a(2)=scatter(z(colour_cat==1,1,t_idx(t),i),z(colour_cat==1,2,t_idx(t),i),15,'MarkerFaceAlpha',0.7,'MarkerFaceColor',color1,'MarkerEdgeColor','None');hold on 224 | a(1)=scatter(z(colour_cat==2,1,t_idx(t),i),z(colour_cat==2,2,t_idx(t),i),15,'MarkerFaceAlpha',0.5,'MarkerFaceColor',color2,'MarkerEdgeColor','None');hold on 225 | 226 | ax2.XTick = []; 227 | ax2.YTick = []; 228 | ax2.XLim=[-0.3,0.3]; 229 | ax2.YLim=[-0.3,0.3]; 230 | axis square 231 | 232 | if i == 1 233 | ax2.Position = [tv_pix(t_idx(t))-size_mds/2,y_pos(3)-0.16,size_mds,size_mds]; 234 | else 235 | ax2.Position = [tv_pix(t_idx(t))-size_mds/2,y_pos(3)-0.16-size_mds-0.01,size_mds,size_mds]; 236 | end 237 | annotation('arrow',[tv_pix(t_idx(t)),tv_pix(t_idx(t))],[y_pos(3)-0.025,y_pos(3)-0.16+size_mds]) 238 | set(ax2,'XColor', 'none','YColor','none') 239 | end 240 | 241 | ax2 = axes('box','off');hold on 242 | if i == 1 243 | ax2.Position = [0.92-0.05,y_pos(3)-0.16,size_mds,size_mds]; 244 | else 245 | ax2.Position = [0.92-0.05,y_pos(3)-0.16-size_mds-0.01,size_mds,size_mds]; 246 | end 247 | plot(0.1,0.7,'k.','MarkerSize',25,'Color',color1) 248 | text(0.2,0.7,legends(:,1,i),'FontSize',12,'FontName','Helvetica','HorizontalAlignment','left','VerticalAlignment','middle') 249 | plot(0.1,0.5,'k.','MarkerSize',25,'Color',color2) 250 | text(0.2,0.5,legends(:,2,i),'FontSize',12,'FontName','Helvetica','HorizontalAlignment','left','VerticalAlignment','middle') 251 | plot(0.1,0.3,'k.','MarkerSize',25,'Color',colour_gray) 252 | text(0.2,0.3,'all other','FontSize',12,'FontName','Helvetica','HorizontalAlignment','left','VerticalAlignment','middle') 253 | ax2.Visible = 'off'; 254 | ax2.XLim = [0,1]; 255 | ax2.YLim = [0,1]; 256 | end 257 | 258 | % save figure 259 | fn = [figdir,'/validation_decoding-mds']; 260 | tn = tempname; 261 | print(gcf,'-dpng','-r500',tn) 262 | 263 | im=imread([tn '.png']); 264 | [i,j]=find(mean(im,3)<255);margin=0; 265 | imwrite(im(min(i-margin):max(i+margin),min(j-margin):max(j+margin),:),[fn '.png'],'png'); 266 | print([fn '.pdf'],'-dpdf') 267 | 268 | 269 | end 270 | -------------------------------------------------------------------------------- /MEG/step3h_validation_rsa_behaviour.m: -------------------------------------------------------------------------------- 1 | function step3h_validation_rsa_behaviour(bids_dir, toolbox_dir, varargin) 2 | %% RSA between the behavioural similarity matrix and the MEG pairwise decoding accuracies 3 | % 4 | % 5 | % @ Lina Teichmann, 2022 6 | % 7 | % Usage: 8 | % step3h_validation_rsa_behaviour(bids_dir, ...) 9 | % 10 | % Inputs: 11 | % bids_dir path to the bids root folder 12 | % toolbox_dir path to toolbox folder containtining CoSMoMVPA 13 | 14 | % Returns: 15 | % _ Figure in BIDS/derivatives folder 16 | 17 | 18 | %% folders 19 | res_dir = [bids_dir '/derivatives/output/']; 20 | figdir = [bids_dir '/derivatives/figures/']; 21 | 22 | addpath(genpath([toolbox_dir '/CoSMoMVPA'])) 23 | %% parameters 24 | n_participants = 4; 25 | 26 | % plotting parameters 27 | col_pp = [0.21528455710115266, 0.5919540462603717, 0.3825837270552851; 28 | 0.24756252096251694, 0.43757475330612905, 0.5968141290988245; 29 | 0.7153368599631209, 0.546895038817448, 0.1270092896093349; 30 | 0.6772691643574462, 0.3168004639904812, 0.3167958318320575]; 31 | 32 | x_size = 0.19; 33 | y_size = 0.15; 34 | x_pos = linspace(0.1,0.9-x_size,4); 35 | 36 | %% load stuff 37 | % load behavioural similarities 38 | load([bids_dir '/sourcedata/spose_similarity.mat'],'spose_sim') 39 | 40 | % load decoding results 41 | load([res_dir,'/validation-pairwise_decoding_RDM1854'],'mat') 42 | decoding_1854 = mat; 43 | 44 | load([res_dir,'/validation-pairwise_decoding_RDM200'],'mat') 45 | decoding_200 = mat; 46 | 47 | % load one example output file to get the time vector 48 | load([res_dir '/pairwise_decoding/P1_pairwise_decoding_1854_block1.mat'], 'res') 49 | tv = res.a.fdim.values{1}*1000; 50 | 51 | 52 | %% RSA: behaviour - MEG 53 | corr_beh = zeros(size(decoding_1854,3),4); 54 | 55 | for p = 1:4 56 | for t = 1:size(decoding_1854,3) 57 | dat = decoding_1854(:,:,t,p); 58 | corr_beh(t,p)=corr(dat(:),spose_sim(:), 'rows','complete','Type','Pearson'); 59 | end 60 | end 61 | 62 | save([res_dir '/validation_rsa-behaviour'],'corr_beh') 63 | 64 | %% plot 65 | f = figure(1);clf 66 | f.Position=[0,0,600,700]; 67 | 68 | for p = 1:n_participants 69 | % define threshold based on pre-stimulus onset 70 | max_preonset = max(corr_beh(tv<=0,p)*-1); 71 | 72 | % plot data for each participant, fill when r > threshold 73 | ax1 = axes('Position',[x_pos(p),0.5,x_size,y_size],'Units','normalized'); 74 | plot(tv,corr_beh(:,p)*-1,'LineWidth',2,'Color',col_pp(p,:));hold on 75 | hf = fill([tv,tv(end)],[max(corr_beh(:,p)*-1,max_preonset);max_preonset],col_pp(p,:),'EdgeColor','none','FaceAlpha',0.2); 76 | 77 | % make it look pretty 78 | ylim([-0.03,.11]) 79 | xlim([tv(1),tv(end)]) 80 | 81 | % find onset of the longest shaded cluster 82 | i=reshape(find(diff([0;corr_beh(:,p)*-1>max_preonset;0])~=0),2,[]); 83 | [~,jmax]=max(diff(i)); 84 | onset_idx=i(1,jmax); 85 | onset = tv(onset_idx); 86 | 87 | % add a marker for onsets 88 | text(onset,gca().YLim(1), char(8593),'Color',col_pp(p,:), 'FontSize', 20, 'VerticalAlignment', 'bottom', 'HorizontalAlignment','Center','FontName','Helvetica') 89 | text(onset+15,gca().YLim(1), [num2str(onset) ' ms'],'Color',col_pp(p,:), 'FontSize', 14, 'VerticalAlignment', 'bottom', 'HorizontalAlignment','left') 90 | set(ax1,'FontSize',14,'box','off','FontName','Helvetica'); 91 | 92 | % add subject title 93 | ax1_title = axes('Position',[x_pos(p)+0.001,0.5+y_size-0.01,0.03,0.03]); 94 | text(0,0,['M' num2str(p)],'FontSize',12,'FontName','Helvetica'); 95 | ax1_title.Visible = 'off'; 96 | 97 | % add labels 98 | if p ==1 99 | ax1.YLabel.String = 'r'; 100 | else 101 | ax1.YTick = []; 102 | end 103 | ax1.XLabel.String = 'time (ms)'; 104 | 105 | end 106 | 107 | % save figure 108 | fn = [figdir,'/validation_rsa-behaviour']; 109 | tn = tempname; 110 | print(gcf,'-dpng','-r500',tn) 111 | 112 | im=imread([tn '.png']); 113 | [i,j]=find(mean(im,3)<255);margin=0; 114 | imwrite(im(min(i-margin):max(i+margin),min(j-margin):max(j+margin),:),[fn '.png'],'png'); 115 | 116 | print([fn '.pdf'],'-dpdf') 117 | 118 | -------------------------------------------------------------------------------- /MRI/README.md: -------------------------------------------------------------------------------- 1 | # THINGS-fMRI 2 | 3 | This repository contains code for running the analyses presented in the [THINGS-data manuscript](https://doi.org/10.1101/2022.07.22.501123). 4 | 5 | ## Installation 6 | 7 | The python code can be installed from this repository with `pip`: 8 | 9 | ``` 10 | # create a new environment 11 | conda create -n thingsdata python==3.9 12 | conda activate thingsdata 13 | # install the python modules for analyzing the fMRI data 14 | pip install -e . 15 | ``` 16 | 17 | ## External requirements 18 | 19 | Some of the analyses run non-python neuroimaging software, such as [FreeSurfer](https://surfer.nmr.mgh.harvard.edu/), [ANTS](https://stnava.github.io/ANTs/), and [FSL](https://fsl.fmrib.ox.ac.uk/). 20 | 21 | ## Jupyter notebooks 22 | 23 | - [fmri_usage.ipynb](notebooks/fmri_usage.ipynb): Examples on how to interact with the fMRI data in general, such as: a) loading the single trial responses from the table or the volumetric data, b) using the brain masks to convert data between these two formats, c) plotting data on the cortical flat maps. 24 | - [animacy_size.ipynb](notebooks/animacy_size.ipynb): Demonstration for fitting an encoding model of object animacy and size to the single trial fMRI responses. 25 | - [working_with_rois.ipynb](notebooks/working_with_rois.ipynb): Demonstration for how to extract data from regions of interest. 26 | 27 | ## Python modules 28 | 29 | - [reconall.py](src/reconall.py): Python scrpit wrapping [FreeSurfer reconall](https://surfer.nmr.mgh.harvard.edu/fswiki/recon-all). 30 | - [scenePRF_Fix.py](src/scenePRF_Fix.py): Experiment code to run used to run the PRF experiment in psychopy. 31 | - [prf.py](src/prf.py): Analysis code for running a population receptive field model in [AFNI](https://afni.nimh.nih.gov/), refining the retinotopic estimates with [neuropythy](https://github.com/noahbenson/neuropythy), and generating ROIs for retinotopic brain areas. 32 | - [localizerGLM_FSL.py](src/localizerGLM_FSL.py): Analysis code for the object category functional localizer, running [FSL](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki) and [nipype](https://nipy.org/packages/nipype/index.html). 33 | - [melodic.py](src/melodic.py): Analysis code for the ICA denoising procedure. This includes: a) running ICA ([FSL MELODIC](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/MELODIC)) on the preprocessed functional MRI data, b) calculating a list of features which characterize each IC, c) generating visualizations for raters to label, d) output ICA noise regressors based on feature thresholds. 34 | - [betas.py](src/betas.py): Procedure for estimating single trial responses from the preprocessed volumetric time series data. 35 | - [anc.py](src/anc.py): Estimation of noise ceilings in single-trial response estimates. 36 | - [mds_betas.py](src/mds_betas.py): Script for visualizing similarity structure in LOC responses via multidimensional scaling (grouped by object categories). 37 | - [utils.py](src/utils.py), [glm.py](src/glm.py), [dataset](src/dataset.py): Miscellaneous helper functions used by the other modules. 38 | 39 | ## Bash scripts 40 | 41 | - [neurodocker.sh](scripts/neurodocker.sh): Recipe for a docker container running FSL and FreeSurfer. 42 | - [reconall.sh](scripts/reconall.sh): Run FreeSurfer recon-all through the neurodocker container. 43 | - [run_fmriprep.sh](scripts/run_fmriprep.sh): [fMRIPrep](https://fmriprep.org/en/stable/) command. -------------------------------------------------------------------------------- /MRI/requirements.txt: -------------------------------------------------------------------------------- 1 | nilearn==0.10.1 2 | scikit-learn==1.0.2 3 | matplotlib==3.3.1 4 | seaborn==0.12.2 5 | scipy==1.10.1 6 | pandas==2.0.2 7 | fracridge==1.3.1 8 | joblib==1.2.0 9 | tqdm==4.56.0 10 | pybids==0.8.0 11 | nipype==1.8.6 12 | cython==0.29.22 13 | h5py==3.10.0 14 | pycortex==1.2.7 15 | cmasher==1.6.3 16 | duecredit==0.9.1 17 | numexpr==2.7.3 18 | tables==3.7.0 19 | jupyter 20 | -------------------------------------------------------------------------------- /MRI/scripts/filtercfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "t1w": { 3 | "acquisition": "prescannormalized", 4 | "reconstruction": "pydeface", 5 | "suffix": "T1w" 6 | }, 7 | "t2w": { 8 | "acquisition": "prescannormalized", 9 | "reconstruction": "pydeface", 10 | "suffix": "T2w" 11 | } 12 | } -------------------------------------------------------------------------------- /MRI/scripts/neurodocker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Generate a docker container that supports FreeSurfer and FSL 4 | neurodocker generate docker --base=debian:stretch --pkg-manager=apt \ 5 | --fsl version=6.0.3 method=binaries \ 6 | --freesurfer version=6.0.0 method=binaries --copy license.txt /opt/freesurfer-6.0.0/ \ 7 | --install gcc g++ graphviz tree nano less git \ 8 | --user=root \ 9 | --miniconda miniconda_version=4.3.31 create_env="thingsmrienv" pip_install="setuptools git+https://github.com/nipy/nipype.git pybids" activate=true \ 10 | > Dockerfile 11 | 12 | docker build --rm -t preproc . 13 | -------------------------------------------------------------------------------- /MRI/scripts/reconall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ~/.bashrc 4 | conda activate thingsmri_env 5 | 6 | thingsmridir="$(pwd)/../../../../" 7 | subject=$1 8 | nprocs=50 9 | 10 | docker run -it --rm --mount \ 11 | type=bind,source="${thingsmridir}",target=/thingsmri \ 12 | preproc \ 13 | python /thingsmri/bids/code/things/mri/reconall.py "${subject}" "${nprocs}" 14 | -------------------------------------------------------------------------------- /MRI/scripts/run_fmriprep.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | subject=$1 4 | nprocs=$2 5 | maxmem=15 # 400 6 | 7 | rawdatadir="$(pwd)/../../../rawdata" 8 | derivsdir="$(pwd)/../../../../test_fprep_upsampling/derivatives" 9 | workdir="$(pwd)/../../../../fmriprep_wdir" 10 | 11 | docker run -ti --rm \ 12 | --memory="$maxmem""g" \ 13 | -v "$rawdatadir":/data:ro \ 14 | -v "$derivsdir":/out \ 15 | -v "$workdir":/work \ 16 | -v "$(pwd)":/licensedir \ 17 | poldracklab/fmriprep:20.2.0 \ 18 | --participant-label "$subject" \ 19 | --t nsd --fs-no-reconall \ 20 | --output-spaces T1w func \ 21 | --bold2t1w-dof 9 \ 22 | --nprocs "$nprocs" --mem "$maxmem""GB" \ 23 | --fs-license-file /licensedir/license.txt \ 24 | --bids-filter-file /licensedir/filtercfg.json \ 25 | -w /work /data /out participant 26 | -------------------------------------------------------------------------------- /MRI/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('requirements.txt') as f: 4 | requirements = f.read().splitlines() 5 | 6 | setup( 7 | name="thingsmri", 8 | version="0.1", 9 | packages=find_packages(), 10 | install_requires=requirements, 11 | author="Oliver Contier", 12 | author_email="contier@cbs.mpg.de", 13 | description="Python code for analyzing the things-fMRI dataset", 14 | ) -------------------------------------------------------------------------------- /MRI/thingsmri/__init__.py: -------------------------------------------------------------------------------- 1 | from . import * -------------------------------------------------------------------------------- /MRI/thingsmri/anc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculate the noise ceiling in single trial response estimates. 3 | 4 | Usage: 5 | python anc.py 6 | 7 | Example: 8 | python anc.py 01 /home/user/thingsmri /home/user/thingsmri/betas_vol /home/user/thingsmri/noiseceiling 9 | """ 10 | 11 | import os 12 | import sys 13 | from os.path import join as pjoin 14 | from tqdm import tqdm 15 | import numpy as np 16 | from scipy.stats import zscore 17 | from nilearn.image import new_img_like 18 | 19 | sys.path.append(os.getcwd()) 20 | from betas import list_stb_outputs_for_mcnc 21 | 22 | 23 | def kknc(data: np.ndarray, n: int or None = None, ignore_nans=False): 24 | """ 25 | Calculate the noise ceiling reported in the NSD paper (Allen et al., 2021) 26 | 27 | Arguments: 28 | data: np.ndarray 29 | Should be shape (ntargets, nrepetitions, nobservations) 30 | n: int or None 31 | Number of trials averaged to calculate the noise ceiling. If None, n will be the number of repetitions. 32 | ignore_nans: bool 33 | If True, ignore nans in data normalization and variance calculation. 34 | returns: 35 | nc: np.ndarray of shape (ntargets) 36 | Noise ceiling without considering trial averaging. 37 | ncav: np.ndarray of shape (ntargets) 38 | Noise ceiling considering all trials were averaged. 39 | """ 40 | if not n: 41 | n = data.shape[-2] 42 | nanpol = "omit" if ignore_nans else "propagate" 43 | normalized = zscore(data, axis=-1, nan_policy=nanpol) 44 | if ignore_nans: 45 | normalized = np.nan_to_num(normalized) 46 | noisesd = np.sqrt(np.nanmean(np.nanvar(normalized, axis=-2, ddof=1), axis=-1)) 47 | else: 48 | noisesd = np.sqrt(np.mean(np.var(normalized, axis=-2, ddof=1), axis=-1)) 49 | sigsd = np.sqrt(np.clip(1 - noisesd**2, 0.0, None)) 50 | ncsnr = sigsd / noisesd 51 | nc = 100 * ((ncsnr**2) / ((ncsnr**2) + (1 / n))) 52 | return nc 53 | 54 | 55 | def calc_kknc_singletrialbetas( 56 | sub: str, 57 | bidsroot: str, 58 | betas_basedirs: list, 59 | out_dirs: list, 60 | ns: list, 61 | ) -> None: 62 | """ 63 | Calculate KKNC for different versions of our single trial betas. 64 | Example: 65 | sub = '01' 66 | bidsroot = '/path/to/bids/dataset' 67 | ns = [1, 12] 68 | stb_derivnames = ['derivatives/betas_vol/'] 69 | out_derivnames = ['derivatives/nc/'] 70 | calc_kknc_singletrialbetas(sub, bidsroot, stb_derivnames, out_derivnames, ns) 71 | """ 72 | assert len(betas_basedirs) == len(out_dirs) 73 | for betas_basedir, outdir in tqdm( 74 | zip(betas_basedirs, out_dirs), 75 | desc="Iterating through betas versions", 76 | total=len(out_dirs), 77 | ): 78 | assert os.path.exists(betas_basedir) 79 | if not os.path.exists(outdir): 80 | os.makedirs(outdir) 81 | betas, example_img = list_stb_outputs_for_mcnc( 82 | sub, bidsroot, betas_basedir=betas_basedir 83 | ) 84 | ncs = [kknc(betas, n) for n in tqdm(ns, desc="calculating noise ceilings")] 85 | for n, nc in tqdm(zip(ns, ncs), desc="saving output files", total=len(ncs)): 86 | img = new_img_like(example_img, nc) 87 | outfile = pjoin(outdir, f"sub-{sub}_kknc_n-{n}.nii.gz") 88 | img.to_filename(outfile) 89 | 90 | 91 | if __name__ == "__main__": 92 | sub, bidsroot, betaspath, outpath = sys.argv[1], sys.argv[2] 93 | ns = [1, 12] 94 | betas_basedirs = [betaspath] 95 | out_dirs = [outpath] 96 | calc_kknc_singletrialbetas(sub, bidsroot, betas_basedirs, out_dirs, ns) 97 | -------------------------------------------------------------------------------- /MRI/thingsmri/dataset.py: -------------------------------------------------------------------------------- 1 | #! /usr/env/python 2 | 3 | import warnings 4 | from os.path import exists as pexists 5 | from os.path import join as pjoin 6 | from os.path import pardir 7 | import pandas as pd 8 | 9 | from bids import BIDSLayout 10 | 11 | 12 | class ThingsMRIdataset: 13 | """ 14 | Data loader for the THINGS-fMRI BIDS timeseries dataset. 15 | """ 16 | 17 | def __init__(self, root_path: str, validate: bool = True): 18 | # path attributes 19 | assert isinstance(root_path, str) 20 | self.root_path = root_path 21 | self.rawdata_path = pjoin(self.root_path, "rawdata") 22 | self.sourcedata_path = pjoin(self.root_path, "sourcedata") 23 | self.derivs_path = pjoin(self.root_path, "derivatives") 24 | # pybids layout 25 | self.layout = BIDSLayout(self.rawdata_path, validate=validate) 26 | self.subjects = self.layout.get(return_type="id", target="subject") 27 | self.sessions = self.layout.get(return_type="id", target="session") 28 | self.things_sessions = [ 29 | ses 30 | for ses in self.layout.get(return_type="id", target="session") 31 | if "things" in ses 32 | ] 33 | runids = self.layout.get(return_type="id", target="run") 34 | self.maxnruns = int(runids[-1]) # maximum number of runs per session 35 | 36 | def update_layout(self, validate: bool = True): 37 | self.layout = BIDSLayout(self.rawdata_path, validate=validate) 38 | self.layout.add_derivatives(self.derivs_path) 39 | return None 40 | 41 | def include_derivs(self): 42 | """Note that this only captures folders in the derivs_path which have a dataset_description.json.""" 43 | if pexists(self.derivs_path): 44 | self.layout.add_derivatives(self.derivs_path) 45 | else: 46 | warnings.warn( 47 | "Could not find derivatives directory\n{}".format(self.derivs_path) 48 | ) 49 | return None 50 | 51 | def get_reconall_anat(self, subject: str) -> dict: 52 | """Collect paths to relevant outputs of reconall""" 53 | return dict( 54 | wmseg=pjoin( 55 | self.derivs_path, "reconall", f"sub-{subject}", "mri", "wm.seg.mgz" 56 | ), 57 | t1=pjoin(self.derivs_path, "reconall", f"sub-{subject}", "mri", "nu.mgz"), 58 | t1_brain=pjoin( 59 | self.derivs_path, "reconall", f"sub-{subject}", "mri", "norm.mgz" 60 | ), 61 | ) 62 | 63 | def get_fieldmap_files(self, subject: str) -> dict: 64 | phasediff_files = self.layout.get( 65 | subject=subject, return_type="file", extension=".nii.gz", suffix="phasediff" 66 | ) 67 | mag1_files = self.layout.get( 68 | subject=subject, 69 | return_type="file", 70 | extension=".nii.gz", 71 | suffix="magnitude1", 72 | ) 73 | mag2_files = self.layout.get( 74 | subject=subject, 75 | return_type="file", 76 | extension=".nii.gz", 77 | suffix="magnitude2", 78 | ) 79 | return dict( 80 | phasediff_files=phasediff_files, 81 | mag1_files=mag1_files, 82 | mag2_files=mag2_files, 83 | ) 84 | 85 | def get_fmriprep_t1w(self, subject): 86 | return pjoin( 87 | self.root_path, 88 | "derivatives", 89 | "fmriprep", 90 | f"sub-{subject}", 91 | "anat", 92 | f"sub-{subject}_acq-prescannormalized_rec-pydeface_desc-preproc_T1w.nii.gz", 93 | ) 94 | 95 | 96 | class ThingsmriLoader: 97 | def __init__(self, thingsmri_dir): 98 | self.thingsmri_dir = thingsmri_dir 99 | self.betas_dir = pjoin(thingsmri_dir, "betas_csv") 100 | self.brainmasks_dir = pjoin(thingsmri_dir, "brainmasks") 101 | 102 | def load_responses(self, subject, drop_voxel_id_from_responses=True): 103 | stimdata = pd.read_csv( 104 | pjoin(self.betas_dir, f"sub-{subject}_StimulusMetadata.csv") 105 | ) 106 | voxdata = pd.read_csv(pjoin(self.betas_dir, f"sub-{subject}_VoxelMetadata.csv")) 107 | responses = pd.read_hdf(pjoin(self.betas_dir, f"sub-{subject}_ResponseData.h5")) 108 | if drop_voxel_id_from_responses: 109 | responses = responses.drop(columns="voxel_id") 110 | return responses, stimdata, voxdata 111 | 112 | def get_brainmask(self, subject): 113 | return pjoin(self.brainmasks_dir, f"sub-{subject}_space-T1w_brainmask.nii.gz") 114 | 115 | 116 | def load_animacy_size( 117 | ani_csv: str = pjoin(pardir, "data", "animacy.csv"), 118 | size_tsv: str = pjoin(pardir, "data", "size_fixed.csv"), 119 | ): 120 | # load with pandas 121 | ani_df = pd.read_csv(ani_csv)[["uniqueID", "lives_mean"]] 122 | ani_df = ani_df.rename(columns={"lives_mean": "animacy"}) 123 | size_df = pd.read_csv(size_tsv, sep=";")[["uniqueID", "meanSize"]] 124 | size_df = size_df.rename(columns={"meanSize": "size"}) 125 | # ani_df has "_", size_df " " as separator in multi-word concepts 126 | size_df["uniqueID"] = size_df.uniqueID.str.replace(" ", "_") 127 | # merge 128 | anisize_df = pd.merge( 129 | left=ani_df, 130 | right=size_df, 131 | on="uniqueID", 132 | how="outer", 133 | ) 134 | assert anisize_df.shape[0] == ani_df.shape[0] == size_df.shape[0] 135 | return anisize_df 136 | -------------------------------------------------------------------------------- /MRI/thingsmri/glm.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os.path import join as pjoin 3 | import nibabel as nib 4 | import numpy as np 5 | import pandas as pd 6 | from joblib import Parallel, delayed 7 | from nilearn.glm.first_level import FirstLevelModel, make_first_level_design_matrix 8 | from nilearn.image import load_img, concat_imgs 9 | from nilearn.masking import apply_mask, intersect_masks 10 | from scipy.stats import zscore 11 | from tqdm import tqdm 12 | 13 | from thingsmri.dataset import ThingsMRIdataset 14 | from thingsmri.utils import psc 15 | 16 | 17 | def get_nuisance_df(noiseregs, nuisance_tsv, include_all_aroma=False): 18 | """Make pd.DataFrame based on list of desired noise regressors and a nuisance_tsv file returned by fmriprep""" 19 | noiseregs_copy = noiseregs[:] 20 | nuisance_df = pd.read_csv(nuisance_tsv, sep="\t") 21 | if include_all_aroma: 22 | noiseregs_copy += [c for c in nuisance_df.columns if "aroma" in c] 23 | nuisance_df = nuisance_df[noiseregs_copy] 24 | if "framewise_displacement" in noiseregs_copy: 25 | nuisance_df["framewise_displacement"] = nuisance_df[ 26 | "framewise_displacement" 27 | ].fillna(0) 28 | return nuisance_df 29 | 30 | 31 | def df_to_boxcar_design( 32 | design_df: pd.DataFrame, frame_times: np.ndarray, add_constant: bool = False 33 | ) -> pd.DataFrame: 34 | """ 35 | Make boxcar design matrix from data frame with one regressor for each trial_type (and no constant). 36 | CAVEAT: nilearn sorts the conditions alphabetically, not by onset. 37 | """ 38 | dropcols = [] if add_constant else ["constant"] 39 | trialtypes = design_df["trial_type"].unique().tolist() 40 | designmat = make_first_level_design_matrix( 41 | frame_times=frame_times, 42 | events=design_df, 43 | hrf_model=None, 44 | drift_model=None, 45 | high_pass=None, 46 | drift_order=None, 47 | oversampling=1, 48 | ).drop(columns=dropcols) 49 | return designmat[trialtypes] 50 | 51 | 52 | def load_masked(bold_file, mask, rescale="psc", dtype=np.single): 53 | if rescale == "psc": 54 | return np.nan_to_num(psc(apply_mask(bold_file, mask, dtype=dtype))) 55 | elif rescale == "z": 56 | return np.nan_to_num( 57 | zscore(apply_mask(bold_file, mask, dtype=dtype), nan_policy="omit", axis=0) 58 | ) 59 | elif rescale == "center": 60 | data = np.nan_to_num(apply_mask(bold_file, mask, dtype=dtype)) 61 | data -= data.mean(axis=0) 62 | else: 63 | return apply_mask(bold_file, mask, dtype=dtype) 64 | 65 | 66 | class THINGSGLM(object): 67 | """ 68 | Parent class for different GLMs to run on the things mri dataset, 69 | mostly handling IO. 70 | """ 71 | 72 | def __init__( 73 | self, 74 | bidsroot: str, 75 | subject: str, 76 | out_deriv_name: str = "glm", 77 | noiseregs: list = [ 78 | "trans_x", 79 | "trans_y", 80 | "trans_z", 81 | "rot_x", 82 | "rot_y", 83 | "rot_z", 84 | "framewise_displacement", 85 | ], 86 | acompcors: bool or int = 10, 87 | include_all_aroma: bool = False, 88 | # include_manual_ica: bool = False, 89 | hrf_model: str or None = "spm + derivative", 90 | noise_model: str = "ols", 91 | high_pass: float = 0.01, 92 | sigscale_nilearn: bool or int or tuple = False, 93 | standardize: bool = True, 94 | verbosity: int = 3, 95 | nruns_perses: int = 10, 96 | nprocs: int = 1, 97 | lowmem=False, 98 | ntrs: int = 284, 99 | tr: float = 1.5, 100 | drift_model: str = "cosine", 101 | drift_order: int = 4, 102 | fwhm: bool or None = None, 103 | overwrite: bool = False, 104 | stc_reftime: float = 0.75, 105 | ): 106 | self.bidsroot = os.path.abspath(bidsroot) 107 | self.include_all_aroma = include_all_aroma 108 | # self.include_manual_ica = include_manual_ica 109 | self.subject = subject 110 | self.out_deriv_name = out_deriv_name 111 | self.verbosity = verbosity 112 | self.lowmem = lowmem 113 | self.nprocs = nprocs 114 | self.acompcors = acompcors 115 | self.tr = tr 116 | self.ntrs = ntrs 117 | self.nruns_perses = nruns_perses 118 | self.high_pass = high_pass 119 | self.hrf_model = hrf_model 120 | self.noise_model = noise_model 121 | self.drift_model = drift_model 122 | self.drift_order = drift_order 123 | self.sigscale_nilearn = sigscale_nilearn 124 | self.standardize = standardize 125 | self.fwhm = fwhm 126 | self.stc_reftime = ( 127 | stc_reftime # fmriprep interpolates to mean of all slice times 128 | ) 129 | self.overwrite = overwrite 130 | self.ds = ThingsMRIdataset(self.bidsroot) 131 | self.n_sessions = len(self.ds.things_sessions) 132 | self.nruns_total = self.n_sessions * self.nruns_perses 133 | self.subj_prepdir = pjoin( 134 | bidsroot, "derivatives", "fmriprep", f"sub-{self.subject}" 135 | ) 136 | self.subj_outdir = pjoin( 137 | bidsroot, "derivatives", out_deriv_name, f"sub-{self.subject}" 138 | ) 139 | self.icalabelled_dir = pjoin( 140 | bidsroot, "derivatives", "ICAlabelled", f"sub-{self.subject}" 141 | ) 142 | if not os.path.exists(self.subj_outdir): 143 | os.makedirs(self.subj_outdir) 144 | if acompcors: 145 | noiseregs += [f"a_comp_cor_{i:02}" for i in range(self.acompcors)] 146 | self.noiseregs = noiseregs 147 | self.frame_times_tr = ( 148 | np.arange(0, self.ntrs * self.tr, self.tr) + self.stc_reftime 149 | ) 150 | # get image dimensions 151 | example_img = load_img( 152 | self.ds.layout.get( 153 | session="things01", extension="nii.gz", suffix="bold", subject="01" 154 | )[0].path 155 | ) 156 | self.nx, self.ny, self.nz, self.ntrs = example_img.shape 157 | self.n_samples_total, self.nvox_masked, self.union_mask = None, None, None 158 | 159 | def _get_events_files(self): 160 | return self.ds.layout.get(task="things", subject=self.subject, suffix="events") 161 | 162 | def _get_bold_files(self): 163 | bold_files = [ 164 | pjoin( 165 | self.subj_prepdir, 166 | f"ses-{sesname}", 167 | "func", 168 | f"sub-{self.subject}_ses-{sesname}_task-things_run-{run_i + 1}_space-T1w_desc-preproc_bold.nii.gz", 169 | ) 170 | for sesname in self.ds.things_sessions 171 | for run_i in range(10) 172 | ] 173 | for boldfile in bold_files: 174 | assert os.path.exists(boldfile), f"\nboldfile not found:\n{boldfile}\n" 175 | return bold_files 176 | 177 | def _get_masks(self): 178 | masks = [ 179 | pjoin( 180 | self.subj_prepdir, 181 | f"ses-{sesname}", 182 | "func", 183 | f"sub-{self.subject}_ses-{sesname}_task-things_run-{run_i + 1}_space-T1w_desc-brain_mask.nii.gz", 184 | ) 185 | for sesname in self.ds.things_sessions 186 | for run_i in range(10) 187 | ] 188 | for mask in masks: 189 | assert os.path.exists(mask), f"\nmask not found:\n{mask}\n" 190 | return masks 191 | 192 | def _get_nuisance_tsvs(self): 193 | nuisance_tsvs = [ 194 | pjoin( 195 | self.subj_prepdir, 196 | f"ses-{sesname}", 197 | "func", 198 | f"sub-{self.subject}_ses-{sesname}_task-things_run-{run_i + 1}_desc-confounds_timeseries.tsv", 199 | ) 200 | for sesname in self.ds.things_sessions 201 | for run_i in range(10) 202 | ] 203 | for tsv in nuisance_tsvs: 204 | assert os.path.exists(tsv), f"\nnuisance tsv not found:\n{tsv}\n" 205 | return nuisance_tsvs 206 | 207 | def _get_ica_txts(self): 208 | ica_txts = [ 209 | pjoin( 210 | self.icalabelled_dir, 211 | f"ses-{sesname}", 212 | f"sub-{self.subject}_ses-{sesname}_task-things_run-{run_i + 1:02d}.txt", 213 | ) 214 | for sesname in self.ds.things_sessions 215 | for run_i in range(10) 216 | ] 217 | for txt in ica_txts: 218 | assert os.path.exists(txt), f"\nica tsv not found:\n{txt}\n" 219 | return ica_txts 220 | 221 | def get_inputs(self): 222 | event_files = self._get_events_files() 223 | bold_files = self._get_bold_files() 224 | masks = self._get_masks() 225 | nuisance_tsvs = self._get_nuisance_tsvs() 226 | assert ( 227 | len(event_files) == len(bold_files) == len(nuisance_tsvs) == len(masks) 228 | ), f"\ninputs have unequal length\n" 229 | return event_files, bold_files, nuisance_tsvs, masks 230 | 231 | def add_union_mask(self, masks): 232 | """Create a union mask based on the run-wise brain masks""" 233 | self.union_mask = intersect_masks(masks, threshold=0) 234 | 235 | def vstack_data_masked( 236 | self, bold_files, rescale_runwise="psc", rescale_global="off", dtype=np.single 237 | ): 238 | arrs = Parallel(n_jobs=20)( 239 | delayed(load_masked)(bf, self.union_mask, rescale_runwise, dtype) 240 | for bf in bold_files 241 | ) 242 | if rescale_global == "psc": 243 | data = np.nan_to_num(psc(np.vstack(arrs))) 244 | elif rescale_global == "z": 245 | data = np.nan_to_num(zscore(np.vstack(arrs), nan_policy="omit", axis=0)) 246 | elif rescale_global: 247 | data = np.vstack(arrs) 248 | data -= data.mean(axis=0) 249 | else: 250 | data = np.vstack(arrs) 251 | self.nvox_masked = data.shape[1] 252 | return data.astype(dtype) 253 | 254 | def load_data_concat_volumes(self, bold_files): 255 | print("concatinating bold files") 256 | bold_imgs = [ 257 | nib.Nifti2Image.from_image(load_img(b)) 258 | for b in tqdm(bold_files, "loading nifti files") 259 | ] 260 | return concat_imgs(bold_imgs, verbose=self.verbosity) 261 | 262 | def init_glm(self, mask): 263 | print(f"instantiating model with nprocs: {self.nprocs}") 264 | return FirstLevelModel( 265 | minimize_memory=self.lowmem, 266 | mask_img=mask, 267 | verbose=self.verbosity, 268 | noise_model=self.noise_model, 269 | t_r=self.tr, 270 | standardize=self.standardize, 271 | signal_scaling=self.sigscale_nilearn, 272 | n_jobs=self.nprocs, 273 | smoothing_fwhm=self.fwhm, 274 | ) 275 | -------------------------------------------------------------------------------- /MRI/thingsmri/localizerGLM_FSL.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code for localizing category selective regions of interests. The nipype implementation can exploit running on multiple CPU, 3 | the number of which can be specified with 4 | 5 | Usage: 6 | python localizerGLM_FSL.py 7 | 8 | Example: 9 | python betas.py 01 /home/user/thingsmri 8 10 | """ 11 | 12 | import os 13 | from os.path import join as pjoin 14 | from os.path import pardir 15 | 16 | import pandas as pd 17 | from niflow.nipype1.workflows.fmri.fsl import ( 18 | create_modelfit_workflow, 19 | create_fixed_effects_flow, 20 | ) 21 | from nilearn.image import resample_img, index_img 22 | from nilearn.masking import intersect_masks 23 | from nipype.algorithms.modelgen import SpecifyModel 24 | from nipype.interfaces.base import Bunch 25 | from nipype.interfaces.fsl.model import SmoothEstimate, Cluster 26 | from nipype.interfaces.fsl.preprocess import SUSAN 27 | from nipype.interfaces.io import DataSink 28 | from nipype.interfaces.utility import Function 29 | from nipype.pipeline.engine import Node, Workflow, MapNode 30 | 31 | from thingsmri.dataset import ThingsMRIdataset 32 | 33 | 34 | def grabdata(subject: str, bidsroot: str, sesname: str, nruns: int = 6): 35 | print("grabbing data") 36 | thingsmri = ThingsMRIdataset(bidsroot) 37 | bold_files = [ 38 | pjoin( 39 | bidsroot, 40 | "derivatives", 41 | "fmriprep", 42 | f"sub-{subject}", 43 | f"ses-{sesname}", 44 | "func", 45 | f"sub-{subject}_ses-{sesname}_task-6cat_run-{run_i + 1}_space-T1w_desc-preproc_bold.nii.gz", 46 | ) 47 | for run_i in range(nruns) 48 | ] 49 | nuisance_tsvs = [ 50 | pjoin( 51 | bidsroot, 52 | "derivatives", 53 | "fmriprep", 54 | f"sub-{subject}", 55 | f"ses-{sesname}", 56 | "func", 57 | f"sub-{subject}_ses-{sesname}_task-6cat_run-{run_i + 1}_desc-confounds_timeseries.tsv", 58 | ) 59 | for run_i in range(nruns) 60 | ] 61 | masks = [ 62 | pjoin( 63 | bidsroot, 64 | "derivatives", 65 | "fmriprep", 66 | f"sub-{subject}", 67 | f"ses-{sesname}", 68 | "func", 69 | f"sub-{subject}_ses-{sesname}_task-6cat_run-{run_i + 1}_space-T1w_desc-brain_mask.nii.gz", 70 | ) 71 | for run_i in range(nruns) 72 | ] 73 | anat_mask = pjoin( 74 | bidsroot, 75 | "derivatives", 76 | "fmriprep", 77 | f"sub-{subject}", 78 | "anat", 79 | f"sub-{subject}_acq-prescannormalized_rec-pydeface_desc-brain_mask.nii.gz", 80 | ) 81 | events_tsvs = thingsmri.layout.get( 82 | subject=subject, 83 | task="6cat", 84 | extension="tsv", 85 | session=sesname, 86 | return_type="filename", 87 | ) 88 | return bold_files, nuisance_tsvs, masks, events_tsvs, anat_mask 89 | 90 | 91 | def cat_contrasts(): 92 | """return list of category selective contrasts for use in Level1Design""" 93 | condnames = ["bodyparts", "faces", "objects", "scenes", "words", "scrambled"] 94 | contrasts = [ 95 | ["all", "T", condnames, [1] * len(condnames)], 96 | ["FFA_scr", "T", condnames, [0, 1, 0, 0, 0, -1]], 97 | ["FFA_obj", "T", condnames, [0, 2, -1, 0, 0, -1]], 98 | ["FFA_obj2", "T", condnames, [0, 1, -1, 0, 0, 0]], 99 | ["FFA_alt", "T", condnames, [0, 3, -1, -1, 0, -1]], 100 | ["FFA_alt2", "T", condnames, [0, 2, -1, -1, 0, 0]], 101 | ["FFA_all", "T", condnames, [-1, 5, -1, -1, -1, -1]], 102 | ["PPA_scr", "T", condnames, [0, 0, 0, 1, 0, -1]], 103 | ["PPA_alt", "T", condnames, [0, -1, -1, 3, 0, -1]], 104 | ["PPA_alt2", "T", condnames, [0, -1, -1, 2, 0, 0]], 105 | ["PPA_obj", "T", condnames, [0, 0, -1, 2, 0, -1]], 106 | ["PPA_obj2", "T", condnames, [0, 0, -1, 1, 0, 0]], 107 | ["PPA_all", "T", condnames, [-1, -1, -1, 5, -1, -1]], 108 | ["EBA_scr", "T", condnames, [1, 0, 0, 0, 0, -1]], 109 | ["EBA_all", "T", condnames, [5, -1, -1, -1, -1, -1]], 110 | ["EBA_obj", "T", condnames, [1, 0, -1, 0, 0, 0]], 111 | ["EBA_obj2", "T", condnames, [2, 0, -1, 0, 0, -1]], 112 | ["LOC", "T", condnames, [1, 1, 1, 1, 1, -5]], 113 | ["LOC_alt", "T", condnames, [0, 0, 1, 0, 0, -1]], 114 | ["VIS", "T", condnames, [-1, -1, -1, -1, -1, 5]], 115 | ["VIS_alt", "T", condnames, [0, 0, -1, 0, 0, 1]], 116 | ["VWF_all", "T", condnames, [-1, -1, -1, -1, 5, -1]], 117 | ["VWF_scr", "T", condnames, [0, 0, 0, 0, 1, -1]], 118 | ["VWF_obj", "T", condnames, [0, 0, -1, 0, 1, 0]], 119 | ["VWF_obj2", "T", condnames, [0, 0, -1, 0, 2, -1]], 120 | ] 121 | contrast_names = [con[0] for con in contrasts] 122 | return contrasts, contrast_names 123 | 124 | 125 | def make_runinfo( 126 | events_tsv: str, 127 | nuisance_tsv: str, 128 | noiseregs: list, 129 | stc_reftime: float, 130 | ) -> Bunch: 131 | """Create subjectinfo (bunch) for one run.""" 132 | events_df = pd.read_csv(events_tsv, sep="\t") 133 | events_df["onset"] = ( 134 | events_df["onset"] - stc_reftime 135 | ) # take slice timing reference into account 136 | # get conditions, onsets, durations as lists 137 | conditions = [] 138 | onsets = [] 139 | durations = [] 140 | for group in events_df.groupby("trial_type"): 141 | conditions.append(group[0]) 142 | onsets.append(group[1].onset.tolist()) 143 | durations.append(group[1].duration.tolist()) 144 | # add noise regressors if there are any 145 | if not noiseregs: 146 | return Bunch(conditions=conditions, onsets=onsets, durations=durations) 147 | nuisance_df = pd.read_csv(nuisance_tsv, sep="\t") 148 | nuisance_df = nuisance_df[noiseregs] 149 | if "framewise_displacement" in noiseregs: 150 | nuisance_df["framewise_displacement"] = nuisance_df[ 151 | "framewise_displacement" 152 | ].fillna(0) 153 | noiseregs_names = nuisance_df.columns.tolist() 154 | noiseregs_regressors = [ 155 | nuisance_df[noisereg].tolist() for noisereg in noiseregs_names 156 | ] 157 | return Bunch( 158 | conditions=conditions, 159 | onsets=onsets, 160 | durations=durations, 161 | regressor_names=noiseregs_names, 162 | regressors=noiseregs_regressors, 163 | ) 164 | 165 | 166 | def sort_copes(files): 167 | """ 168 | reshape copes (or varcopes) returned by create_fixed_effects_flow() 169 | for use in create_fixed_effects_flow() 170 | """ 171 | numelements = len(files[0]) 172 | outfiles = [] 173 | for i in range(numelements): 174 | outfiles.insert(i, []) 175 | for j, elements in enumerate(files): 176 | outfiles[i].append(elements[i]) 177 | return outfiles 178 | 179 | 180 | def resample_to_file(in_file: str, target_file: str, wdir: str): 181 | """Resample in_file to the resolution of target_file""" 182 | target_img = index_img(target_file, 0) 183 | resampled = resample_img( 184 | in_file, target_affine=target_img.affine, target_shape=target_img.shape 185 | ) 186 | if not os.path.exists(wdir): 187 | os.makedirs(wdir) 188 | outfile = pjoin(wdir, "resampled_file.nii.gz") 189 | resampled.to_filename(outfile) 190 | return outfile 191 | 192 | 193 | def union_masks_to_filename(masks: list, wdir: str): 194 | """Create union of list of brain masks, save to file in working directory and return the file path""" 195 | union_img = intersect_masks(masks, threshold=0) 196 | if not os.path.exists(wdir): 197 | os.makedirs(wdir) 198 | outfile = pjoin(wdir, "unionmask.nii.gz") 199 | union_img.to_filename(outfile) 200 | return outfile 201 | 202 | 203 | def make_localizerGLM_wf( 204 | subject: str, 205 | bidsroot: str, 206 | whichsession: dict = { 207 | "01": "localizer2", 208 | "02": "localizer1", 209 | "03": "localizer1", 210 | }, 211 | hrf: dict = {"dgamma": {"derivs": False}}, 212 | cluster_thr: float = 3.7, 213 | cluster_pthr: float = 0.0001, 214 | hpf: int = 60, 215 | fwhm: int = 5, 216 | ar: bool = False, 217 | tr: float = 1.5, 218 | stc_reftime: float = 0.701625, 219 | smoothing_brightness_threshold: float = 2000, 220 | noiseregs: list = [], # ['trans_x', 'trans_y', 'trans_z', 'rot_x', 'rot_y', 'rot_z', 'framewise_displacement'], 221 | ): 222 | wdir = pjoin(bidsroot, pardir, "localizerGLM_wdir", f"sub-{subject}") 223 | contrasts, contrast_names = cat_contrasts() 224 | locses = whichsession[subject] 225 | wf = Workflow(name="wf", base_dir=wdir) 226 | bold_files, nuisance_tsvs, masks, events_tsvs, anat_mask = grabdata( 227 | subject, bidsroot, locses 228 | ) 229 | union_mask = union_masks_to_filename(masks, pjoin(wdir, "unionmask")) 230 | smooth = MapNode(SUSAN(), name="smooth", iterfield=["in_file"]) 231 | smooth.inputs.in_file = bold_files 232 | smooth.inputs.fwhm = fwhm 233 | smooth.inputs.brightness_threshold = smoothing_brightness_threshold 234 | runinfos = [ 235 | make_runinfo( 236 | events_tsv, nuisance_tsv, noiseregs=noiseregs, stc_reftime=stc_reftime 237 | ) 238 | for events_tsv, nuisance_tsv in zip(events_tsvs, nuisance_tsvs) 239 | ] 240 | modelspec = Node( 241 | SpecifyModel( 242 | subject_info=runinfos, 243 | high_pass_filter_cutoff=hpf, 244 | input_units="secs", 245 | time_repetition=tr, 246 | ), 247 | name="modelspec", 248 | ) 249 | modelfit = create_modelfit_workflow() 250 | modelfit.inputs.inputspec.interscan_interval = tr 251 | modelfit.inputs.inputspec.contrasts = contrasts 252 | modelfit.inputs.inputspec.bases = hrf 253 | modelfit.inputs.inputspec.model_serial_correlations = ar 254 | if not ar: 255 | filmgls = modelfit.get_node("modelestimate") 256 | filmgls.inputs.autocorr_noestimate = True 257 | ffx = create_fixed_effects_flow() 258 | l2model = ffx.get_node("l2model") 259 | l2model.inputs.num_copes = len(bold_files) 260 | flameo = ffx.get_node("flameo") 261 | flameo.inputs.mask_file = union_mask 262 | sortcopes = Node( 263 | Function(function=sort_copes, input_names=["files"], output_names=["outfiles"]), 264 | name="sortcopes", 265 | ) 266 | sortvarcopes = Node( 267 | Function(function=sort_copes, input_names=["files"], output_names=["outfiles"]), 268 | name="sortvarcopes", 269 | ) 270 | smoothest = MapNode( 271 | SmoothEstimate(mask_file=union_mask), name="smoothest", iterfield=["zstat_file"] 272 | ) 273 | cluster = MapNode( 274 | Cluster( 275 | threshold=cluster_thr, 276 | pthreshold=cluster_pthr, 277 | out_threshold_file=True, 278 | out_pval_file=True, 279 | out_index_file=True, 280 | out_localmax_txt_file=True, 281 | out_localmax_vol_file=True, 282 | out_max_file=True, 283 | out_mean_file=True, 284 | out_size_file=True, 285 | ), 286 | name="cluster", 287 | iterfield=["in_file", "dlh", "volume"], 288 | ) 289 | sink = Node( 290 | DataSink( 291 | infields=["contasts.@con"], 292 | ), 293 | name="sink", 294 | ) 295 | sink.inputs.base_directory = pjoin( 296 | bidsroot, "derivatives", "localizer", f"sub-{subject}" 297 | ) 298 | sink.inputs.substitutions = [ 299 | (f"/{outputtype}/_cluster{i}/", f"/contrast-{contname}/") 300 | for outputtype in [ 301 | "threshold_file", 302 | "pval_file", 303 | "index_file", 304 | "localmax_txt_file", 305 | "localmax_vol_file", 306 | "max_file", 307 | "mean_file", 308 | "size_file", 309 | ] 310 | for i, contname in enumerate(contrast_names) 311 | ] 312 | wf.connect( 313 | [ 314 | (smooth, modelspec, [("smoothed_file", "functional_runs")]), 315 | (smooth, modelfit, [("smoothed_file", "inputspec.functional_data")]), 316 | (modelspec, modelfit, [("session_info", "inputspec.session_info")]), 317 | (modelfit, sortcopes, [("outputspec.copes", "files")]), 318 | (modelfit, sortvarcopes, [("outputspec.varcopes", "files")]), 319 | (sortcopes, ffx, [("outfiles", "inputspec.copes")]), 320 | (sortvarcopes, ffx, [("outfiles", "inputspec.varcopes")]), 321 | (modelfit, ffx, [("outputspec.dof_file", "inputspec.dof_files")]), 322 | (ffx, smoothest, [("outputspec.zstats", "zstat_file")]), 323 | (smoothest, cluster, [("dlh", "dlh"), ("volume", "volume")]), 324 | (ffx, cluster, [("outputspec.zstats", "in_file")]), 325 | ( 326 | cluster, 327 | sink, 328 | [ 329 | ("threshold_file", "threshold_file"), 330 | ("pval_file", "pval_file"), 331 | ("index_file", "index_file"), 332 | ("localmax_txt_file", "localmax_txt_file"), 333 | ("localmax_vol_file", "localmax_vol_file"), 334 | ("max_file", "max_file"), 335 | ("mean_file", "mean_file"), 336 | ("size_file", "size_file"), 337 | ], 338 | ), 339 | ] 340 | ) 341 | return wf 342 | 343 | 344 | if __name__ == "__main__": 345 | import sys 346 | 347 | subject, bidsroot, nprocs = sys.argv[1], sys.argv[2], sys.argv[3] 348 | wf = make_localizerGLM_wf(subject, bidsroot) 349 | wf.run(plugin="MultiProc", plugin_args=dict(n_procs=nprocs)) 350 | -------------------------------------------------------------------------------- /MRI/thingsmri/mds_betas.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run multidimensional scaling on single trial responses in category-selective brain areas. 3 | 4 | Usage: 5 | python mds_betas.py 6 | 7 | Examples: 8 | python mds_betas.py 01 /home/user/thingsmri vehicle tool lightskyblue mediumvioletred 9 | python mds_betas.py 01 /home/user/thingsmri animal food rebeccapurple mediumspringgreen 10 | """ 11 | 12 | from os.path import join as pjoin 13 | import numpy as np 14 | import os 15 | from nilearn.masking import intersect_masks 16 | import seaborn as sns 17 | import matplotlib.pyplot as plt 18 | import sys 19 | from sklearn.manifold import MDS 20 | 21 | sys.path.append(os.getcwd()) 22 | from thingsmri.utils import load_category_df, get_category_rois 23 | from thingsmri.betas import ( 24 | load_betas, 25 | load_filenames, 26 | filter_catch_trials, 27 | average_betas_per_concept, 28 | ) 29 | 30 | 31 | def run_mds_on_betas( 32 | sub, 33 | bidsroot, 34 | mask, 35 | betas_derivname="betas_loo/on_residuals/scalematched", 36 | out_derivname="mds", 37 | target_cats=["body part", "plant"], 38 | target_colors=["rebeccapurple", "mediumspringgreen"], 39 | mds_kws=dict( 40 | n_components=2, 41 | n_init=10, 42 | max_iter=5_000, 43 | n_jobs=-1, 44 | dissimilarity="precomputed", 45 | ), 46 | seed=0, 47 | ): 48 | """ 49 | Example: 50 | sub = sys.argv[1] 51 | bidsroot = pjoin(os.pardir, os.pardir, os.pardir) 52 | # get category selective areas as a mask 53 | julian_basedir = pjoin( 54 | bidsroot, 'derivatives', 'julian_parcels', 'julian_parcels_edited' 55 | ) 56 | julian_dir = pjoin(julian_basedir, f'sub-{sub}') 57 | roi_files = glob.glob(pjoin(julian_dir, '*', '*.nii.gz')) 58 | roi_files = [rf for rf in roi_files if 'RSC' not in rf] # dismiss RSC 59 | loc_files = glob.glob(pjoin(julian_dir.replace('edited', 'intersected'), 60 | 'object_parcels', '*.nii.gz')) 61 | roi_files += loc_files 62 | mask = intersect_masks(roi_files, threshold=0, connected=False) 63 | for target_cats, target_colors in tqdm(zip( 64 | [['animal', 'food'], ['body part', 'plant']], 65 | [['lightskyblue', 'mediumvioletred'], ['rebeccapurple', 'mediumspringgreen']], 66 | ), desc='target_cats', total=2): 67 | run_mds_on_betas(sub, bidsroot, mask=mask, target_cats=target_cats, target_colors=target_colors) 68 | """ 69 | np.random.seed(seed) 70 | # if colors are passed as RBG tuples, normalize to 0-1 71 | for i, color in enumerate(target_colors): 72 | if type(color) == tuple: 73 | target_colors[i] = tuple([e / 255 for e in color]) 74 | # define file names 75 | outdir = pjoin(bidsroot, "derivatives", out_derivname, f"sub-{sub}") 76 | out_npy = pjoin( 77 | outdir, 78 | f'{target_cats[0]}_{target_cats[1]}_ninit-{mds_kws["n_init"]}_maxiter-{mds_kws["max_iter"]}.npy', 79 | ) 80 | out_png = out_npy.replace(".npy", ".png") 81 | out_pdf = out_npy.replace(".npy", ".pdf") 82 | if not os.path.exists(outdir): 83 | os.makedirs(outdir) 84 | # load betas 85 | betas_ = load_betas(sub, mask, bidsroot) 86 | fnames_ = load_filenames(sub, bidsroot, betas_derivname) 87 | # exclude catch trials 88 | betas_, fnames_, noncatch_is = filter_catch_trials(betas_, fnames_) 89 | # average within concepts 90 | betas, concepts = average_betas_per_concept(betas_, fnames_) 91 | # compute correlation distance 92 | rdm = 1 - np.corrcoef(betas) 93 | # load category names 94 | cat_df = load_category_df() 95 | cats = [] 96 | for con in concepts: 97 | if con[-1] in [ 98 | "1", 99 | "2", 100 | ]: # some concepts are coded 'bracelet2' or 'bow1', we just count them as individuals 101 | con = con[:-1] 102 | row = cat_df.loc[cat_df["Word"] == con.replace("_", " ")] 103 | hits = [cat for cat in target_cats if row[cat].values[0] == 1] 104 | result = ( 105 | hits[0] if len(hits) else "Other" 106 | ) # only keep the first category found, or "Other" 107 | cats.append(result) 108 | cats = np.array(cats) 109 | sort = np.argsort(cats) 110 | cats = cats[sort] 111 | 112 | # show how many exemplars were found per category 113 | # uniquecats, counts = np.unique(cats, return_counts=True) 114 | # run MDS and save embedding 115 | mds = MDS(random_state=seed, **mds_kws) 116 | mds.fit(rdm) 117 | Y = mds.embedding_[sort] 118 | np.save(out_npy, Y) 119 | # plot and save 120 | colors_ = target_colors + ["lightgrey"] 121 | labels_ = target_cats + ["Other"] 122 | fig = plt.figure(figsize=(7, 7)) 123 | g = sns.scatterplot( 124 | x=Y[:, 0], 125 | y=Y[:, 1], 126 | hue=cats, 127 | hue_order=labels_, 128 | palette=colors_, 129 | s=300, 130 | alpha=0.7, 131 | linewidth=0, 132 | legend=False, 133 | ) 134 | plt.axis("off") 135 | fig.savefig(out_png, dpi=300) 136 | fig.savefig(out_pdf, dpi=300) 137 | return None 138 | 139 | 140 | if __name__ == "__main__": 141 | sub, bidsroot, cat1, cat2, col1, col2 = sys.argv[1:] 142 | rois = get_category_rois(sub, bidsroot, "/rois/category_localizer") 143 | for roiname, roifile in rois: 144 | if "RSC" in roiname: 145 | del rois[roiname] 146 | mask = intersect_masks(rois.values(), threshold=0, connected=False) 147 | run_mds_on_betas( 148 | sub, bidsroot, mask=mask, target_cats=[cat1, cat2], target_colors=[col1, col2] 149 | ) 150 | -------------------------------------------------------------------------------- /MRI/thingsmri/prf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code for estimating retinotopic parameters with AFNIs circular population 3 | receptive field mapping and neuropythy to further refine these estimates and 4 | delineate retinotopic regions. 5 | 6 | Requires a preprocessed version of the dataset stored in bidsdata/derivatives/fmriprep. Further non-python dependencies: FSL, AFNI 7 | 8 | Usage: 9 | python prf.py 10 | """ 11 | 12 | import os 13 | import subprocess 14 | import sys 15 | import warnings 16 | from os import pardir 17 | from os.path import abspath 18 | from os.path import join as pjoin 19 | from shutil import copyfile 20 | 21 | import nibabel as nib 22 | import numpy as np 23 | from nilearn.image import load_img, new_img_like, index_img, resample_to_img 24 | from nilearn.masking import intersect_masks, apply_mask, unmask 25 | from nipype.interfaces.fsl.maths import TemporalFilter 26 | from nipype.pipeline.engine import MapNode 27 | from scipy.stats import zscore 28 | from tqdm import tqdm 29 | 30 | from thingsmri.utils import xy_to_eccrad, xy_to_various_angles, call_vol2surf 31 | 32 | 33 | class ThingsPrfAfni: 34 | """ 35 | Runs AFNIs PRF analysis on the THINGS-fMRI data. 36 | Requires fMRIPREP output of localizer sessions and afni brik files specifying the stimulus. 37 | Preprocessing (in addition to fmriprep) includes temporal filtering of individual runs, averaging them, 38 | and zscoring the resulting average time series. This pipeline creates a temporary working directory in the parent 39 | directory of the bids dataset. The results are saved to the derivatives section of the bids dataset. 40 | 41 | Example: 42 | prfdata = ThingsPrfAfni(bidsroot=pjoin(pardir, pardir, pardir), subject='01) 43 | prfdata.run() 44 | """ 45 | 46 | def __init__( 47 | self, 48 | bidsroot, 49 | subject, 50 | stimulus_brik, 51 | stimulus_head, 52 | conv_fname, 53 | sesmap={"01": "localizer2", "02": "localizer2", "03": "localizer1"}, 54 | fmriprep_deriv_name="fmriprep", 55 | out_deriv_name="prf_afni", 56 | preproc_hpf=100.0, 57 | stimdur: float = 3.0, 58 | ): 59 | self.bidsroot = abspath(bidsroot) 60 | self.subject = subject 61 | self.sesmap = sesmap 62 | self.conv_fname = conv_fname 63 | self.stimbrik = stimulus_brik 64 | self.stimhead = stimulus_head 65 | self.wdir = pjoin(self.bidsroot, pardir, "prf_afni_wdir", f"sub-{subject}") 66 | self.outdir = pjoin( 67 | self.bidsroot, "derivatives", out_deriv_name, f"sub-{subject}" 68 | ) 69 | if not os.path.exists(self.outdir): 70 | os.makedirs(self.outdir) 71 | self.data = pjoin(self.wdir, "data.nii.gz") 72 | if not os.path.exists(self.wdir): 73 | os.makedirs(self.wdir) 74 | else: 75 | warnings.warn( 76 | f"working directory already exists. Files may be overwritten in\n{self.wdir}" 77 | ) 78 | self.copy_stimbrik() 79 | self.nruns = 4 80 | self.tr = 1.5 81 | self.stimwidth_pix = 990 82 | self.ppd = 90 83 | self.hpf = preproc_hpf 84 | self.ses = sesmap[subject] 85 | self.fmriprep_deriv_name = fmriprep_deriv_name 86 | self.fmriprep_dir = pjoin( 87 | self.bidsroot, 88 | "derivatives", 89 | fmriprep_deriv_name, 90 | f"sub-{subject}", 91 | f"ses-{self.ses}", 92 | "func", 93 | ) 94 | self.boldfiles_fprep, self.maskfiles_fprep = self.get_file_names() 95 | self.umask_f = pjoin(self.wdir, "unionmask.nii.gz") 96 | self.stmidur = stimdur 97 | self.buck_map = { 98 | "amp": 0, 99 | "x": 1, 100 | "y": 2, 101 | "sigma": 3, 102 | "rsquared": 10, 103 | } # meaning of sub bricks in afni output 104 | 105 | def get_file_names(self): 106 | boldfiles_fprep = [ 107 | pjoin( 108 | self.fmriprep_dir, 109 | f"sub-{self.subject}_ses-{self.ses}_task-pRF_run-{runi}_space-T1w_desc-preproc_bold.nii.gz", 110 | ) 111 | for runi in range(1, self.nruns + 1) 112 | ] 113 | maskfiles_fprep = [ 114 | pjoin( 115 | self.fmriprep_dir, 116 | f"sub-{self.subject}_ses-{self.ses}_task-pRF_run-{runi}_space-T1w_desc-brain_mask.nii.gz", 117 | ) 118 | for runi in range(1, self.nruns + 1) 119 | ] 120 | for l in [boldfiles_fprep, maskfiles_fprep]: 121 | for e in l: 122 | if not os.path.exists(e): 123 | raise IOError(f"Could not find file\n{e}") 124 | return boldfiles_fprep, maskfiles_fprep 125 | 126 | def preproc_data(self): 127 | # temporal filtering with fsl 128 | tf = MapNode( 129 | TemporalFilter(highpass_sigma=self.hpf / self.tr), 130 | name="TemporalFilter", 131 | iterfield=["in_file"], 132 | ) 133 | tf.inputs.in_file = self.boldfiles_fprep 134 | tf.run() 135 | filterd_boldfiles = tf.get_output("out_file") 136 | # create a brain mask 137 | umask = intersect_masks(self.maskfiles_fprep, threshold=0) 138 | umask.to_filename(self.umask_f) 139 | # load in numpy, average, z score 140 | bold_arrs = [ 141 | apply_mask(bp, umask) 142 | for bp in tqdm(filterd_boldfiles, desc="loading runs for averaging") 143 | ] 144 | data_arr = zscore(np.mean(np.stack(bold_arrs), axis=0), axis=0) 145 | # save to working directory 146 | data_img = unmask(data_arr, umask) 147 | data_img.to_filename(self.data) 148 | 149 | def copy_stimbrik(self): 150 | """to working directory""" 151 | for f in [self.stimbrik, self.stimhead]: 152 | copyfile(f, pjoin(self.wdir, os.path.basename(f))) 153 | 154 | def set_afni_environ(self): 155 | os.environ["AFNI_CONVMODEL_REF"] = self.conv_fname 156 | os.environ["AFNI_MODEL_PRF_STIM_DSET"] = self.stimhead.replace("HEAD", "") 157 | os.environ["AFNI_MODEL_PRF_ON_GRID"] = "YES" 158 | os.environ["AFNI_MODEL_DEBUG"] = "3" 159 | 160 | def run_afni(self): 161 | os.chdir(self.wdir) 162 | subprocess.run(["3dcopy", self.data, "AFNIpRF+orig."]) 163 | subprocess.run(["3dcopy", self.umask_f, "automask+orig."]) 164 | self.set_afni_environ() 165 | subprocess.run( 166 | [ 167 | "3dNLfim", 168 | "-input", 169 | "AFNIpRF+orig.", 170 | "-mask", 171 | "automask+orig.", 172 | "-noise", 173 | "Zero", 174 | "-signal", 175 | "Conv_PRF", 176 | "-sconstr", 177 | "0", 178 | "-20.0", 179 | "20.0", 180 | "-sconstr", 181 | "1", 182 | "-1.0", 183 | "1.0", 184 | "-sconstr", 185 | "2", 186 | "-1.0", 187 | "1.0", 188 | "-sconstr", 189 | "3", 190 | "0.0", 191 | "1.0", 192 | "-BOTH", 193 | "-nrand", 194 | "10000", 195 | "-nbest", 196 | "5", 197 | "-bucket", 198 | "0", 199 | "Buck.PRF", 200 | "-snfit", 201 | "snfit.PRF", 202 | "-TR", 203 | str(self.tr), 204 | "-float", 205 | ] 206 | ) 207 | for k, v in self.buck_map.items(): 208 | subprocess.run( 209 | ["3dAFNItoNIFTI", "-prefix", f"{k}.nii.gz", f"Buck.PRF+orig.[{v}]"] 210 | ) 211 | 212 | def unit_to_dva(self, in_file, out_file): 213 | """Take a nifti that encodes x,y, or sigma in unit measures (0-1) and convert to degree visual angle""" 214 | in_img = nib.load(in_file) 215 | arr = in_img.get_fdata() 216 | dva_arr = (arr * self.stimwidth_pix) / self.ppd 217 | out_img = nib.Nifti1Image(dva_arr, affine=in_img.affine) 218 | out_img.to_filename(out_file) 219 | return dva_arr 220 | 221 | def convert_afni_output(self): 222 | # convert to dva, overwriting afni output which is in unit measures 223 | os.chdir(self.wdir) 224 | for stat in ["x", "y", "sigma"]: 225 | _ = self.unit_to_dva(f"{stat}.nii.gz", f"{stat}.nii.gz") 226 | x_img, y_img = nib.load("x.nii.gz"), nib.load("y.nii.gz") 227 | x_arr, y_arr = x_img.get_fdata(), y_img.get_fdata() 228 | # save eccentricity and polar angle directly to derivatives 229 | ecc_arr, pa_arr = xy_to_eccrad(x_arr, y_arr) 230 | for arr, name in zip([ecc_arr, pa_arr], ["ecc", "pa"]): 231 | img = nib.Nifti1Image(arr, affine=x_img.affine) 232 | img.to_filename(pjoin(self.outdir, f"{name}.nii.gz")) 233 | 234 | def copy_results(self): 235 | """From working directory to bids derivatives""" 236 | for k in self.buck_map.keys(): 237 | copyfile(pjoin(self.wdir, f"{k}.nii.gz"), pjoin(self.outdir, f"{k}.nii.gz")) 238 | for prefix in ["snfit.PRF+orig", "Buck.PRF+orig"]: 239 | for suffix in ["HEAD", "BRIK"]: 240 | copyfile( 241 | pjoin(self.wdir, ".".join([prefix, suffix])), 242 | pjoin(self.outdir, ".".join([prefix, suffix])), 243 | ) 244 | 245 | def run(self): 246 | if not os.path.exists(self.data): 247 | self.preproc_data() 248 | if not os.path.exists(pjoin(self.wdir, "Buck.PRF+orig.BRIK")): 249 | self.run_afni() 250 | self.convert_afni_output() 251 | self.copy_results() 252 | 253 | 254 | def list_afni_outputs(sub, afnioutdir): 255 | """Get file names of afni_prf output""" 256 | return dict( 257 | ecc=pjoin(afnioutdir, f"sub-{sub}", "ecc.nii.gz"), 258 | rsquared=pjoin(afnioutdir, f"sub-{sub}", "rsquared.nii.gz"), 259 | sigma=pjoin(afnioutdir, f"sub-{sub}", "sigma.nii.gz"), 260 | x=pjoin(afnioutdir, f"sub-{sub}", "x.nii.gz"), 261 | y=pjoin(afnioutdir, f"sub-{sub}", "y.nii.gz"), 262 | ) 263 | 264 | 265 | def run_neuropythy( 266 | sub, 267 | afnioutir, 268 | base_outdir, 269 | base_wdir, 270 | meanruns_dir, 271 | reconall_dir, 272 | flip_y=True, 273 | ): 274 | wdir = pjoin(base_wdir, f"sub-{sub}") 275 | outdir = pjoin(base_outdir, f"sub-{sub}") 276 | for d in [wdir, outdir]: 277 | if not os.path.exists(d): 278 | os.makedirs(d) 279 | afniresults = list_afni_outputs(sub, afnioutir) 280 | x_arr = load_img(afniresults["x"]).get_fdata() 281 | y_arr = load_img(afniresults["y"]).get_fdata() 282 | if flip_y: 283 | y_arr *= -1 284 | _, _, _, _, pa_arr = xy_to_various_angles(x_arr, y_arr) 285 | pa_img = new_img_like(load_img(afniresults["x"]), pa_arr) 286 | pa_img.to_filename(pjoin(wdir, "pa.nii.gz")) 287 | afniresults["pa"] = pjoin(wdir, "pa.nii.gz") 288 | # project afni results to surface 289 | for inname, outname in zip( 290 | ["pa", "rsquared", "ecc", "sigma"], 291 | ["prf_angle", "prf_vexpl", "prf_eccen", "prf_radius"], 292 | ): 293 | call_vol2surf(sub, afniresults[inname], outname, wdir) 294 | # run neuropythy 295 | subprocess.run( 296 | [ 297 | "python", 298 | "-m", 299 | "neuropythy", 300 | "register_retinotopy", 301 | f"sub-{sub}", 302 | "--verbose", 303 | f"--lh-eccen={pjoin(wdir, 'lh.prf_eccen.mgz')}", 304 | f"--rh-eccen={pjoin(wdir, 'rh.prf_eccen.mgz')}", 305 | f"--lh-angle={pjoin(wdir, 'lh.prf_angle.mgz')}", 306 | f"--rh-angle={pjoin(wdir, 'rh.prf_angle.mgz')}", 307 | f"--lh-weight={pjoin(wdir, 'lh.prf_vexpl.mgz')}", 308 | f"--rh-weight={pjoin(wdir, 'rh.prf_vexpl.mgz')}", 309 | f"--lh-radius={pjoin(wdir, 'lh.prf_radius.mgz')}", 310 | f"--rh-radius={pjoin(wdir, 'rh.prf_radius.mgz')}", 311 | ] 312 | ) 313 | # copy and reorient the neuropythy output 314 | os.chdir(pjoin(reconall_dir, f"sub-{sub}", "mri")) 315 | for param in ["angle", "eccen", "sigma", "varea"]: 316 | subprocess.run( 317 | [ 318 | "mri_convert", 319 | f"inferred_{param}.mgz", 320 | pjoin(outdir, f"inferred_{param}_fsorient.nii.gz"), 321 | ] 322 | ) 323 | subprocess.run( 324 | [ 325 | "fslreorient2std", 326 | pjoin(outdir, f"inferred_{param}_fsorient.nii.gz"), 327 | pjoin(outdir, f"inferred_{param}.nii.gz"), 328 | ] 329 | ) 330 | # resample binary visual ROIs to functional space 331 | ref_f = pjoin(meanruns_dir, f"sub-{sub}.nii.gz") 332 | ref_img = index_img(ref_f, 0) 333 | va_img = load_img(pjoin(outdir, "inferred_varea.nii.gz")) 334 | va = va_img.get_fdata() 335 | for roival in range(1, 13): 336 | roi_arr = np.zeros(va.shape) 337 | roi_arr[np.where(va == roival)] = 1.0 338 | roi_img = new_img_like(va_img, roi_arr) 339 | res_img = resample_to_img(roi_img, ref_img, interpolation="nearest") 340 | res_img.to_filename(pjoin(outdir, f"resampled_va-{roival}_interp-nn.nii.gz")) 341 | # also save linear interpolation for convenience 342 | lres_img = resample_to_img(roi_img, ref_img, interpolation="linear") 343 | lres_img.to_filename( 344 | pjoin(outdir, f"resampled_va-{roival}_interp-linear.nii.gz") 345 | ) 346 | 347 | # resample all other outputs too 348 | for param in ["angle", "eccen", "sigma", "varea"]: 349 | interp = "nearest" if param == "varea" else "linear" 350 | res_img = resample_to_img( 351 | pjoin(outdir, f"inferred_{param}.nii.gz"), ref_img, interpolation=interp 352 | ) 353 | res_img.to_filename(pjoin(outdir, f"resampled_{param}.nii.gz")) 354 | 355 | 356 | def threshold_vareas_by_eccentricity( 357 | meanruns_dir="/Users/olivercontier/bigfri/scratch/bids/derivatives/mean_runs", 358 | max_eccen=15.55, 359 | npt_bdir="/Users/olivercontier/bigfri/scratch/bids/derivatives/prf_neuropythy", 360 | out_bdir="/Users/olivercontier/bigfri/scratch/bids/derivatives/prf_neuropythy/fixated", 361 | ): 362 | """ 363 | because neuropythy infers receptive field parameters outside the stimulated region of the visual field 364 | """ 365 | for sub in tqdm(range(1, 4), desc="subjects"): 366 | npt_outdir = pjoin(npt_bdir, f"sub-0{sub}") 367 | thresh_outdir = pjoin(out_bdir, f"sub-0{sub}") 368 | if not os.path.exists(thresh_outdir): 369 | os.makedirs(thresh_outdir) 370 | # anatomical resolution, but reoriented to standard (unlike fsorient) 371 | eccen_orig_f = pjoin(npt_outdir, "inferred_eccen.nii.gz") 372 | eccen_orig_img = load_img(eccen_orig_f) 373 | eccen = eccen_orig_img.get_fdata() 374 | periphery_mask = eccen > max_eccen 375 | # reference image for resampling 376 | ref_f = pjoin(meanruns_dir, f"sub-0{sub}.nii.gz") 377 | ref_img = index_img(ref_f, 0) 378 | # threshold vareas 379 | va_img = load_img(pjoin(npt_outdir, "inferred_varea.nii.gz")) 380 | va = va_img.get_fdata() 381 | va[periphery_mask] = 0.0 382 | va_thr_img = new_img_like(va_img, va) 383 | va_thr_img.to_filename(pjoin(thresh_outdir, "inferred_varea.nii.gz")) 384 | # resample to functional space and save rois individually 385 | for roival in tqdm(range(1, 13), desc="rois", leave=False): 386 | roi_arr = np.zeros(va.shape) 387 | roi_arr[np.where(va == roival)] = 1.0 388 | roi_img = new_img_like(va_img, roi_arr) 389 | res_img = resample_to_img(roi_img, ref_img, interpolation="nearest") 390 | res_img.to_filename( 391 | pjoin(thresh_outdir, f"resampled_va-{roival}_interp-nn.nii.gz") 392 | ) 393 | lres_img = resample_to_img(roi_img, ref_img, interpolation="linear") 394 | lres_img.to_filename( 395 | pjoin(thresh_outdir, f"resampled_va-{roival}_interp-linear.nii.gz") 396 | ) 397 | return None 398 | 399 | 400 | if __name__ == "__main__": 401 | import sys 402 | 403 | sub, bidsroot, afni_inputs_dir = sys.argv[1], sys.argv[2], sys.argv[3] 404 | 405 | # run PRF estimation with AFNI 406 | conv_fname = pjoin(afni_inputs_dir, "conv.ref.spmg1_manual.1D") 407 | stimulus_brik = pjoin(afni_inputs_dir, "stim.308.LIA.bmask.resam+orig.BRIK") 408 | stimulus_head = pjoin(afni_inputs_dir, "stim.308.LIA.bmask.resam+orig.HEAD") 409 | 410 | afniprf = ThingsPrfAfni( 411 | bidsroot, 412 | sub, 413 | stimulus_brik, 414 | stimulus_head, 415 | conv_fname, 416 | ) 417 | afniprf.run() 418 | -------------------------------------------------------------------------------- /MRI/thingsmri/reconall.py: -------------------------------------------------------------------------------- 1 | """ 2 | Run Freesurfer's recon-all on the THINGS-fMRI dataset. Mostly intended for documentation. 3 | """ 4 | 5 | import stat 6 | from distutils.dir_util import copy_tree 7 | from os import pardir, chmod 8 | from os.path import join as pjoin 9 | 10 | from nipype.interfaces.freesurfer.preprocess import ReconAll 11 | from nipype.interfaces.utility import Function 12 | from nipype.pipeline.engine import Node, Workflow 13 | 14 | 15 | def grabanat(bidsroot, subject): 16 | """Grab anatomical data for given subject""" 17 | import sys 18 | from os.path import join as pjoin 19 | from dataset import ThingsMRIdataset 20 | 21 | sys.path.insert(0, pjoin(bidsroot, "code", "things", "mri")) 22 | thingsmri = ThingsMRIdataset(bidsroot) 23 | t1files = thingsmri.layout.get( 24 | subject=subject, 25 | return_type="file", 26 | extension=".nii.gz", 27 | suffix="T1w", 28 | reconstruction="pydeface", 29 | acquisition="prescannormalized", 30 | ) 31 | t2file = thingsmri.layout.get( 32 | subject=subject, 33 | return_type="file", 34 | extension=".nii.gz", 35 | suffix="T2w", 36 | reconstruction="pydeface", 37 | acquisition="prescannormalized", 38 | )[0] 39 | return t1files, t2file 40 | 41 | 42 | def make_reconall_wf(subject, wdir, bidsroot, directive="all", nprocs=12) -> Workflow: 43 | """return very simple workflow running reconall for one subject""" 44 | wf = Workflow(name="reconall_wf", base_dir=wdir) 45 | datagrabber = Node( 46 | Function( 47 | function=grabanat, 48 | input_names=["bidsroot", "subject"], 49 | output_names=["t1files", "t2file"], 50 | ), 51 | name="datagrabber", 52 | ) 53 | datagrabber.inputs.subject = subject 54 | datagrabber.inputs.bidsroot = bidsroot 55 | reconall = Node( 56 | ReconAll(use_T2=True, directive=directive, openmp=nprocs), name="reconall" 57 | ) 58 | wf.connect( 59 | [(datagrabber, reconall, [("t1files", "T1_files"), ("t2file", "T2_file")])] 60 | ) 61 | return wf 62 | 63 | 64 | def main( 65 | subject, 66 | nprocs, 67 | bidsroot, # path within container "preproc" 68 | free_permissions=True, # free permissions for workdir and output (bc docker has different user than host) 69 | ) -> None: 70 | """Run Reconall for one subject and copy the output to the bids derivatives""" 71 | wdir = pjoin(bidsroot, pardir, "reconall_workdir", f"sub-{subject}") 72 | derivdir = pjoin(bidsroot, "derivatives", "reconall", f"sub-{subject}") 73 | # make and run workflow 74 | wf = make_reconall_wf(subject=subject, bidsroot=bidsroot, wdir=wdir, nprocs=nprocs) 75 | wf.write_graph(graph2use="colored", simple_form=True) 76 | wf.run() 77 | # copy output to derivatives manually to preserve typical reconall output structure 78 | ra_nodedir = pjoin(wdir, "reconall_wf", "reconall", "recon_all") 79 | copy_tree(ra_nodedir, derivdir, preserve_mode=False) 80 | if free_permissions: 81 | for d in [wdir, derivdir]: 82 | chmod(d, stat.S_IRWXO) 83 | return None 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THINGS-data 2 | 3 | [THINGS-data](https://elifesciences.org/articles/82580) is a collection of large-scale datasets for the study of natural object representations in brain and behavior. It includes functional magnetic resonance imaging (fMRI) data, magnetoencephalographic (MEG) recordings, and 4.70 million similarity judgments in response to thousands of images from the [THINGS object concept and image database](https://doi.org/10.1371/journal.pone.0223792). 4 | 5 | # Code Repositories 6 | 7 | This repository the scripts and notebooks for reproducing the neuroimaging analyses presented in the [THINGS-data paper](https://elifesciences.org/articles/82580). It is structured into two sub-folders reflecting the two neuroimaging data modalities: 8 | - [MRI](MRI) 9 | - [MEG](MEG) 10 | 11 | 12 | # Download 13 | 14 | ## figshare 15 | 16 | THINGS-data is hosted as a collection of data objects on figshare. 17 | 18 | > 🔗 THINGS-data on FigShare 19 | > 20 | > [https://doi.org/10.25452/figshare.plus.c.6161151](https://doi.org/10.25452/figshare.plus.c.6161151) 21 | 22 | Besides the raw data, this collection includes a data derivatives such as preprocessed versions of both the fMRI and MEG data. Additional derivatives for the fMRI data include single trial response estimates, cortical surface maps, noise ceiling estimates, and regions of interest. 23 | 24 | You can browse the collection and download individual parts which are relevant for your research. 25 | 26 | For smaller files, you can simply click the `Download` button! 27 | ![](assets/download_button.png) 28 | 29 | If you plan to download larger data objects, it might make sense to start this process in the command line. Simply right-click on the `Download`` button and copy the link address. Executing the following code in the command line to begin the download process for that file. 30 | ``` 31 | # This link downloads the fMRI single trial responses 32 | wget -O betas_csv.zip https://plus.figshare.com/ndownloader/files/36789690 33 | ``` 34 | Since downloading larger data object may take some time, it can make sense to run this process in the background with tools such as `screen` or `tmux`. 35 | 36 | 37 | ## OpenNeuro 38 | 39 | The raw fMRI and MEG datasets are available on [OpenNeuro](https://openneuro.org). 40 | 41 | > 🔗 THINGS-data on OpenNeuro 42 | > 43 | > - MRI: [https://openneuro.org/datasets/ds004192](https://openneuro.org/datasets/ds004192) 44 | > - MEG: [https://openneuro.org/datasets/ds004212](https://openneuro.org/datasets/ds004212) 45 | 46 | The official [documentation](https://docs.openneuro.org/user-guide) gives helpful explanations on how to download data from OpenNeuro. 47 | 48 | 49 | ## OSF 50 | 51 | The behavioral dataset containing 4.7 million human similarity judgements is available on OSF and can be downloaded directly via your web browser. 52 | 53 | > 🔗 THINGS-data on OSF 54 | > 55 | > [osf.io/f5rn6/](https://osf.io/f5rn6/) 56 | 57 | 58 | # How to cite 59 | ``` 60 | @article { 61 | THINGSdata, 62 | article_type = {journal}, 63 | title = {THINGS-data, a multimodal collection of large-scale datasets for investigating object representations in human brain and behavior}, 64 | author = {Hebart, Martin N and Contier, Oliver and Teichmann, Lina and Rockter, Adam H and Zheng, Charles Y and Kidder, Alexis and Corriveau, Anna and Vaziri-Pashkam, Maryam and Baker, Chris I}, 65 | editor = {Barense, Morgan}, 66 | volume = 12, 67 | year = 2023, 68 | month = {feb}, 69 | pub_date = {2023-02-27}, 70 | pages = {e82580}, 71 | citation = {eLife 2023;12:e82580}, 72 | doi = {10.7554/eLife.82580}, 73 | url = {https://doi.org/10.7554/eLife.82580}, 74 | journal = {eLife}, 75 | issn = {2050-084X}, 76 | publisher = {eLife Sciences Publications, Ltd}, 77 | } 78 | ``` -------------------------------------------------------------------------------- /assets/download_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViCCo-Group/THINGS-data/96fe386de6d65c472618b300bb98ae2ffca28d9d/assets/download_button.png --------------------------------------------------------------------------------