├── .gitattributes ├── .github ├── actions │ └── openjournals-pdf │ │ └── action.yml └── workflows │ ├── joss-draft.yml │ └── python-tests.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── ansible └── install_and_run_sovabids.yaml ├── codecov.yml ├── docs ├── Makefile └── source │ ├── README.rst │ ├── _copyover │ └── _images │ │ ├── INCF_logo_with_tagline.png │ │ ├── gruneco.jpg │ │ ├── online_validator.png │ │ └── udea.png │ ├── _static │ ├── front_10_mappings.png │ ├── front_11_select_mapping.png │ ├── front_12_the_mapping.png │ ├── front_13_edit_mapping.png │ ├── front_14_save_mapping.png │ ├── front_15_check_mapping.png │ ├── front_16_mappings_ready.png │ ├── front_17_conversion_click.png │ ├── front_18_download_click.png │ ├── front_19_download_popup.png │ ├── front_1_intro.png │ ├── front_20_converted_files.png │ ├── front_21_sovabids_files.png │ ├── front_22_mapping_file.png │ ├── front_23_log_files.png │ ├── front_2_click_upload.png │ ├── front_3_upload.png │ ├── front_4_deselect.png │ ├── front_5_detected.png │ ├── front_6_upload_rules.png │ ├── front_7_inspect_rules.png │ ├── front_8_edit_rules.png │ ├── front_9_submit_rules.png │ ├── gallery.css │ └── sovabids - eResearch Australasia 2021 conference poster.pdf │ ├── _templates │ └── autosummary │ │ ├── class.rst │ │ └── function.rst │ ├── conf.py │ ├── gsoc_proposal.md │ ├── index.rst │ ├── mappings_schema.rst │ ├── rules_schema.rst │ └── validation.rst ├── examples ├── README.rst ├── bidscoin_example.py ├── example_heuristic_source_target_pair.py ├── gui_example.py ├── lemon_example.py ├── lemon_example_rules.yml └── rpc_example.py ├── front ├── app │ ├── app.py │ ├── download_zip.py │ ├── forms.py │ ├── rules.yaml │ ├── rules2.yaml │ ├── static │ │ └── upload_files.css │ ├── templates │ │ ├── base.html │ │ ├── convert.html │ │ ├── ejemplos │ │ │ ├── base.css │ │ │ └── base.html │ │ ├── error.html │ │ ├── exclude_files.html │ │ ├── files.html │ │ ├── index.html │ │ ├── individual_rules.html │ │ ├── load_rules.html │ │ ├── ready.html │ │ └── upload_files.html │ └── utils.py └── requirements.txt ├── gsoc_proposal.md ├── paper ├── arch.png ├── main-use.png ├── paper.bib ├── paper.md └── rules-mappings.png ├── requirements-dev.txt ├── requirements-gui.txt ├── requirements-user.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── sovabids ├── __init__.py ├── _version.py ├── bids.py ├── convert.py ├── datasets.py ├── dicts.py ├── errors.py ├── files.py ├── heuristics.py ├── loggers.py ├── misc.py ├── parsers.py ├── rules.py ├── schemas │ ├── __init__.py │ ├── bidsmap_sova2coin.yml │ └── rules.yml ├── settings.py └── sovarpc.py ├── tests ├── __init__.py ├── test_bids.py ├── test_heuristics.py ├── test_path_parser.py ├── test_sova2coin.py └── test_web_validator.sh └── versioneer.py /.gitattributes: -------------------------------------------------------------------------------- 1 | sovabids/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/actions/openjournals-pdf/action.yml: -------------------------------------------------------------------------------- 1 | name: Open Journals PDF Generator 2 | description: Create a draft or preprint PDF for an Open Journals paper 3 | inputs: 4 | journal: 5 | description: Target journal, must be either "joss" or "jose" 6 | required: false 7 | default: joss 8 | paper-path: 9 | description: >- 10 | Path to the main paper Markdown file, relative to the 11 | project's root directory. 12 | required: false 13 | default: 'paper.md' 14 | output-type: 15 | description: Type of output (pdf or preprint) 16 | required: false 17 | default: 'pdf' 18 | 19 | runs: 20 | using: docker 21 | image: 'docker://openjournals/inara:latest' 22 | env: 23 | GIT_SHA: $GITHUB_SHA 24 | JOURNAL: ${{ inputs.journal }} 25 | args: 26 | - -o ${{ inputs.output-type }} 27 | - ${{ inputs.paper-path }} 28 | 29 | branding: 30 | icon: file-text 31 | color: blue 32 | -------------------------------------------------------------------------------- /.github/workflows/joss-draft.yml: -------------------------------------------------------------------------------- 1 | name: Generate Paper PDFs 2 | on: [push] 3 | 4 | jobs: 5 | paper: 6 | runs-on: ubuntu-latest 7 | name: Paper Draft and Preprint 8 | steps: 9 | - name: Checkout Repository 10 | uses: actions/checkout@v4 11 | 12 | # ✅ Generate Draft PDF using inara 13 | - name: Build Draft PDF 14 | uses: ./.github/actions/openjournals-pdf 15 | with: 16 | journal: joss 17 | paper-path: paper/paper.md 18 | output-type: pdf 19 | 20 | # ✅ Upload the Draft PDF 21 | - name: Upload Draft PDF 22 | uses: actions/upload-artifact@v4 23 | with: 24 | name: draft-paper 25 | path: paper/paper.pdf 26 | 27 | # ✅ Generate Preprint LaTeX File (`paper.preprint.tex`) 28 | - name: Build Preprint LaTeX File 29 | uses: ./.github/actions/openjournals-pdf 30 | with: 31 | journal: joss 32 | paper-path: paper/paper.md 33 | output-type: preprint 34 | 35 | # ✅ Print files to verify `paper.preprint.tex` exists 36 | - name: List contents of paper/ 37 | run: ls -lah paper/ 38 | 39 | # ✅ Upload the Preprint LaTeX file (`paper.preprint.tex`) 40 | - name: Upload Preprint LaTeX File 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: preprint-tex 44 | path: paper/paper.preprint.tex 45 | -------------------------------------------------------------------------------- /.github/workflows/python-tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: python-tests 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: [3.8] 20 | node: [18] # might need to update this manually? 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Use Node.js ${{ matrix.node }} 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node }} 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | python -m pip install pytest 35 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi 36 | # pip install -e .,already inside requirements-dev.txt 37 | - name: Test with pytest 38 | run: | 39 | pytest tests/ --cov=sovabids/ --cov-report=xml --verbose -s 40 | - name: Build docs 41 | run: | 42 | cd docs 43 | make clean 44 | make html 45 | - name: Upload coverage reports to Codecov 46 | uses: codecov/codecov-action@v5 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | slug: yjmantilla/sovabids 50 | - name : Test with web-validator 51 | run: | 52 | /bin/bash tests/test_web_validator.sh 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # test files 2 | 3 | front/app/media/ 4 | front/app/_uploads/ 5 | front/app/_convert/ 6 | front/app/uploads/ 7 | 8 | 9 | src/ 10 | _data/ 11 | _temp/ 12 | _ideas/ 13 | 14 | .vscode/ 15 | 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | .Python 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | pip-wheel-metadata/ 39 | share/python-wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | MANIFEST 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .nox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | *.py,cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | 69 | # Translations 70 | *.mo 71 | *.pot 72 | 73 | # Django stuff: 74 | *.log 75 | local_settings.py 76 | db.sqlite3 77 | db.sqlite3-journal 78 | 79 | # Flask stuff: 80 | instance/ 81 | .webassets-cache 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | make.bat 89 | docs/source/generated 90 | docs/source/auto_examples 91 | # PyBuilder 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | .python-version 103 | 104 | # pipenv 105 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 106 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 107 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 108 | # install all needed dependencies. 109 | #Pipfile.lock 110 | 111 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 112 | __pypackages__/ 113 | 114 | # Celery stuff 115 | celerybeat-schedule 116 | celerybeat.pid 117 | 118 | # SageMath parsed files 119 | *.sage.py 120 | 121 | # Environments 122 | .env 123 | .venv 124 | env/ 125 | venv/ 126 | ENV/ 127 | env.bak/ 128 | venv.bak/ 129 | 130 | # Spyder project settings 131 | .spyderproject 132 | .spyproject 133 | 134 | # Rope project settings 135 | .ropeproject 136 | 137 | # mkdocs documentation 138 | /site 139 | 140 | # mypy 141 | .mypy_cache/ 142 | .dmypy.json 143 | dmypy.json 144 | 145 | # Pyre type checker 146 | .pyre/ 147 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | 12 | 13 | # Set the version of Python and other tools you might need 14 | build: 15 | os: ubuntu-22.04 16 | tools: 17 | python: "3.8" 18 | 19 | # Optionally set the version of Python and requirements required to build your docs 20 | python: 21 | install: 22 | - requirements: requirements-dev.txt 23 | # is important that "-e ." exist in requirements-dev.txt 24 | # see https://github.com/readthedocs/readthedocs.org/issues/3986 25 | # see https://github.com/readthedocs/readthedocs.org/issues/6243 26 | #- method: pip 27 | # path: . -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, sovabids developers 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include sovabids/_version.py 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/codecov/c/github/yjmantilla/sovabids 2 | :target: https://app.codecov.io/gh/yjmantilla/sovabids 3 | :alt: codecov 4 | 5 | .. image:: https://img.shields.io/github/actions/workflow/status/yjmantilla/sovabids/python-tests.yml?branch=main&label=tests 6 | :target: https://github.com/yjmantilla/sovabids/actions?query=workflow%3Apython-tests 7 | :alt: Python tests 8 | 9 | .. image:: https://readthedocs.org/projects/sovabids/badge/?version=latest 10 | :target: https://sovabids.readthedocs.io/en/latest/?badge=latest 11 | :alt: Documentation Status 12 | 13 | .. image:: https://img.shields.io/badge/Preprint-Zenodo-orange 14 | :target: https://doi.org/10.5281/zenodo.10292410 15 | 16 | sovabids 17 | ======== 18 | 19 | `Visit the documentation `_ 20 | 21 | .. after-init-label 22 | 23 | * sovabids is a python package for automating eeg2bids conversion. 24 | 25 | * sovabids can be used through (click to see the examples): 26 | a. `its python API `_ 27 | b. `its CLI entry points `_ 28 | c. `its JSON-RPC entry points (needs a server running the backend) `_ 29 | d. `its minimal web-app GUI `_ 30 | 31 | .. note:: 32 | 33 | The advantage of the JSON-RPC way is that it can be used from other programming languages. 34 | 35 | Limitation: 36 | 37 | Do notice that at the moment the files have to be on the same computer that runs the server. 38 | 39 | .. warning:: 40 | 41 | Currently meg2bids conversion is not supported, but this is a targeted feature. 42 | 43 | .. tip:: 44 | 45 | By default sovabids will skip files already converted. If you want to overwrite previous conversions currently you need to delete the output folder (by yourself) and start sovabids over again. 46 | 47 | Architecture 48 | ------------ 49 | 50 | The main elements of sovabids are: 51 | * A source path with the original dataset. 52 | * A bids path that will be the output path of the conversion. 53 | * A rules file that configures how the conversion is done from the general perspective. 54 | * A mapping file that encodes how the conversion is performed to each individual file of the dataset. 55 | 56 | .. image:: https://mermaid.ink/svg/eyJjb2RlIjoiZ3JhcGggTFJcbiAgICBTPlwiU291cmNlIHBhdGhcIl1cbiAgICBCPlwiQmlkcyBwYXRoXCJdXG4gICAgUj5cIlJ1bGVzIGZpbGVcIl1cbiAgICBBUigoXCJBcHBseSBSdWxlc1wiKSlcbiAgICBNPlwiTWFwcGluZ3MgZmlsZVwiXVxuICAgIENUKChcIkNvbnZlcnQgVGhlbVwiKSlcbiAgICBPWyhcIkNvbnZlcnRlZCBkYXRhc2V0XCIpXVxuICAgIFMgLS0-IEFSXG4gICAgQiAtLT4gQVJcbiAgICBSIC0tPiBBUlxuICAgIEFSIC0tPiBNXG4gICAgTSAtLT4gQ1RcbiAgICBDVCAtLT4gT1xuICAiLCJtZXJtYWlkIjp7InRoZW1lIjoiZm9yZXN0In0sInVwZGF0ZUVkaXRvciI6ZmFsc2UsImF1dG9TeW5jIjp0cnVlLCJ1cGRhdGVEaWFncmFtIjpmYWxzZX0 57 | 58 | Internally sovabids uses `MNE-Python `_ and `MNE-BIDS `_ to perform the conversion. In a sense is a wrapper that allows to do conversions from the command line. 59 | 60 | Installation 61 | ------------ 62 | 63 | .. code-block:: bash 64 | 65 | git clone https://github.com/yjmantilla/sovabids.git 66 | cd sovabids 67 | pip install -r requirements-user.txt 68 | 69 | Installation for GUI usage 70 | ------------------------------- 71 | 72 | This will install sovabids for usage with an experimental web gui, and as an experimental plugin for bidscoin. 73 | 74 | .. code-block:: bash 75 | 76 | git clone https://github.com/yjmantilla/sovabids.git 77 | cd sovabids 78 | pip install -r requirements-gui.txt 79 | 80 | 81 | Installation for developers 82 | --------------------------- 83 | 84 | Fork this repo and run: 85 | 86 | .. code-block:: bash 87 | 88 | git clone https://github.com//sovabids.git 89 | cd sovabids 90 | pip install -r requirements-dev.txt 91 | 92 | Notice that the requirements-dev.txt file already has the sovabids installation using editable mode. 93 | 94 | 95 | Basic Usage 96 | ----------- 97 | 98 | The easiest way is to use sovabids through its CLI entry-points as follows: 99 | 100 | sovapply 101 | ^^^^^^^^ 102 | 103 | Use the sovapply entry-point to produce a mapping file from a source path, an output bids root path and a rules filepath. 104 | 105 | 106 | .. code-block:: bash 107 | 108 | sovapply source_path bids_path rules_path 109 | 110 | By default the mapping file made will have the following filepath: 111 | 112 | .. code-block:: text 113 | 114 | bids_path/code/sovabids/mappings.yml 115 | 116 | 117 | sovaconvert 118 | ^^^^^^^^^^^ 119 | 120 | Use the sovaconvert entry-point to convert the dataset given its mapping file. 121 | 122 | .. code-block:: bash 123 | 124 | sovaconvert mapping_file 125 | 126 | Funding 127 | ------- 128 | 129 | .. raw:: html 130 | 131 |
132 | 133 |
134 | 135 | 136 | 137 | 138 | Acknowledgments 139 | --------------- 140 | 141 | sovabids is developed with the help of the following entities: 142 | 143 | .. raw:: html 144 | 145 |
146 | 147 |
148 | 149 |
150 | 151 |
152 | 153 |
154 | 155 |
156 | 157 | 158 | 159 | 160 | Academic Works 161 | --------------- 162 | 163 | * `Poster for the Big Data Neuroscience Workshop 2022 (Austin, Texas) `_ 164 | 165 | * `Poster for OHBM 2022 Anual Meeting `_ 166 | 167 | * `Video for OHBM 2022 Anual Meeting `_ 168 | 169 | * `Poster for the eResearch Australasia Conference 2021 `_ 170 | 171 | 172 | 173 | What does sova means? 174 | --------------------- 175 | 176 | sova is a contraction of 'eso va' which mean 'that goes' in spanish. 177 | 178 | Nevertheless the real usage by the original developers is just to convey the idea of : 179 | 180 | we will make it happen, we dont know how, but we will 181 | -------------------------------------------------------------------------------- /ansible/install_and_run_sovabids.yaml: -------------------------------------------------------------------------------- 1 | - name: install SOVABIDS 2 | hosts: myhosts 3 | tasks: 4 | - name: Ping my hosts 5 | ansible.builtin.ping: 6 | 7 | - name: Print message 8 | ansible.builtin.debug: 9 | msg: Hello world 10 | 11 | - name: Install python3 as python 12 | ansible.builtin.apt: 13 | name: python-is-python3 14 | update-cache: true 15 | become: true 16 | 17 | - name: Update repositories cache and install "pip" package 18 | ansible.builtin.apt: 19 | name: pip 20 | update-cache: true 21 | become: true 22 | 23 | - name: Clone the sovabids repo 24 | ansible.builtin.git: 25 | repo: https://github.com/yjmantilla/sovabids.git 26 | dest: src/sovabids 27 | 28 | - name: Install sovabids requirements 29 | ansible.builtin.pip: 30 | requirements: ~/src/sovabids/requirements-gui.txt 31 | chdir: ~/src/sovabids 32 | 33 | - name: Test sovabids 34 | ansible.builtin.pip: 35 | name: pytest 36 | 37 | - name: Run flask 38 | ansible.builtin.shell: cd /; nohup python ~/src/sovabids/front/app/app.py 1>/dev/null 2>&1 & 39 | 40 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/source/README.rst: -------------------------------------------------------------------------------- 1 | 2 | Intro to sovabids 3 | ================= 4 | 5 | .. include:: ../../README.rst 6 | :start-after: .. after-init-label -------------------------------------------------------------------------------- /docs/source/_copyover/_images/INCF_logo_with_tagline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_copyover/_images/INCF_logo_with_tagline.png -------------------------------------------------------------------------------- /docs/source/_copyover/_images/gruneco.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_copyover/_images/gruneco.jpg -------------------------------------------------------------------------------- /docs/source/_copyover/_images/online_validator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_copyover/_images/online_validator.png -------------------------------------------------------------------------------- /docs/source/_copyover/_images/udea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_copyover/_images/udea.png -------------------------------------------------------------------------------- /docs/source/_static/front_10_mappings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_10_mappings.png -------------------------------------------------------------------------------- /docs/source/_static/front_11_select_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_11_select_mapping.png -------------------------------------------------------------------------------- /docs/source/_static/front_12_the_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_12_the_mapping.png -------------------------------------------------------------------------------- /docs/source/_static/front_13_edit_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_13_edit_mapping.png -------------------------------------------------------------------------------- /docs/source/_static/front_14_save_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_14_save_mapping.png -------------------------------------------------------------------------------- /docs/source/_static/front_15_check_mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_15_check_mapping.png -------------------------------------------------------------------------------- /docs/source/_static/front_16_mappings_ready.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_16_mappings_ready.png -------------------------------------------------------------------------------- /docs/source/_static/front_17_conversion_click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_17_conversion_click.png -------------------------------------------------------------------------------- /docs/source/_static/front_18_download_click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_18_download_click.png -------------------------------------------------------------------------------- /docs/source/_static/front_19_download_popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_19_download_popup.png -------------------------------------------------------------------------------- /docs/source/_static/front_1_intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_1_intro.png -------------------------------------------------------------------------------- /docs/source/_static/front_20_converted_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_20_converted_files.png -------------------------------------------------------------------------------- /docs/source/_static/front_21_sovabids_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_21_sovabids_files.png -------------------------------------------------------------------------------- /docs/source/_static/front_22_mapping_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_22_mapping_file.png -------------------------------------------------------------------------------- /docs/source/_static/front_23_log_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_23_log_files.png -------------------------------------------------------------------------------- /docs/source/_static/front_2_click_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_2_click_upload.png -------------------------------------------------------------------------------- /docs/source/_static/front_3_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_3_upload.png -------------------------------------------------------------------------------- /docs/source/_static/front_4_deselect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_4_deselect.png -------------------------------------------------------------------------------- /docs/source/_static/front_5_detected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_5_detected.png -------------------------------------------------------------------------------- /docs/source/_static/front_6_upload_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_6_upload_rules.png -------------------------------------------------------------------------------- /docs/source/_static/front_7_inspect_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_7_inspect_rules.png -------------------------------------------------------------------------------- /docs/source/_static/front_8_edit_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_8_edit_rules.png -------------------------------------------------------------------------------- /docs/source/_static/front_9_submit_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/front_9_submit_rules.png -------------------------------------------------------------------------------- /docs/source/_static/gallery.css: -------------------------------------------------------------------------------- 1 | /* 2 | Sphinx-Gallery has compatible CSS to fix default sphinx themes 3 | Tested for Sphinx 1.3.1 for all themes: default, alabaster, sphinxdoc, 4 | scrolls, agogo, traditional, nature, haiku, pyramid 5 | Tested for Read the Docs theme 0.1.7 */ 6 | .sphx-glr-thumbcontainer { 7 | background: #fff; 8 | border: solid #fff 1px; 9 | -moz-border-radius: 5px; 10 | -webkit-border-radius: 5px; 11 | border-radius: 5px; 12 | box-shadow: none; 13 | float: left; 14 | margin: 5px; 15 | min-height: 230px; 16 | padding-top: 5px; 17 | position: relative; 18 | } 19 | .sphx-glr-thumbcontainer:hover { 20 | border: solid #b4ddfc 1px; 21 | box-shadow: 0 0 15px rgba(142, 176, 202, 0.5); 22 | } 23 | .sphx-glr-thumbcontainer a.internal { 24 | bottom: 0; 25 | display: block; 26 | left: 0; 27 | padding: 150px 10px 0; 28 | position: absolute; 29 | right: 0; 30 | top: 0; 31 | } 32 | /* Next one is to avoid Sphinx traditional theme to cover all the 33 | thumbnail with its default link Background color */ 34 | .sphx-glr-thumbcontainer a.internal:hover { 35 | background-color: transparent; 36 | } 37 | 38 | .sphx-glr-thumbcontainer p { 39 | margin: 0 0 .1em 0; 40 | } 41 | .sphx-glr-thumbcontainer .figure { 42 | margin: 10px; 43 | width: 160px; 44 | } 45 | .sphx-glr-thumbcontainer img { 46 | display: inline; 47 | max-height: 112px; 48 | max-width: 160px; 49 | } 50 | .sphx-glr-thumbcontainer[tooltip]:hover:after { 51 | background: rgba(0, 0, 0, 0.8); 52 | -webkit-border-radius: 5px; 53 | -moz-border-radius: 5px; 54 | border-radius: 5px; 55 | color: #fff; 56 | content: attr(tooltip); 57 | left: 95%; 58 | padding: 5px 15px; 59 | position: absolute; 60 | z-index: 98; 61 | width: 220px; 62 | bottom: 52%; 63 | } 64 | .sphx-glr-thumbcontainer[tooltip]:hover:before { 65 | border: solid; 66 | border-color: #333 transparent; 67 | border-width: 18px 0 0 20px; 68 | bottom: 58%; 69 | content: ''; 70 | left: 85%; 71 | position: absolute; 72 | z-index: 99; 73 | } 74 | 75 | .sphx-glr-script-out { 76 | color: #888; 77 | margin: 0; 78 | } 79 | p.sphx-glr-script-out { 80 | padding-top: 0.7em; 81 | } 82 | .sphx-glr-script-out .highlight { 83 | background-color: #1f4d61; 84 | margin-left: 2.5em; 85 | margin-top: -2.1em; 86 | } 87 | .sphx-glr-script-out .highlight pre { 88 | background-color: #1f4d61; 89 | border: 0; 90 | max-height: 30em; 91 | overflow: auto; 92 | padding-left: 1ex; 93 | margin: 0px; 94 | word-break: break-word; 95 | } 96 | .sphx-glr-script-out + p { 97 | margin-top: 1.8em; 98 | } 99 | blockquote.sphx-glr-script-out { 100 | margin-left: 0pt; 101 | } 102 | .sphx-glr-script-out.highlight-pytb .highlight pre { 103 | color: #000; 104 | background-color: #1f4d61; 105 | border: 1px solid #f66; 106 | margin-top: 10px; 107 | padding: 7px; 108 | } 109 | 110 | div.sphx-glr-footer { 111 | text-align: center; 112 | } 113 | 114 | div.sphx-glr-download { 115 | margin: 1em auto; 116 | vertical-align: middle; 117 | } 118 | 119 | div.sphx-glr-download a { 120 | background-color: #ffc; 121 | background-image: linear-gradient(to bottom, #FFC, #d5d57e); 122 | border-radius: 4px; 123 | border: 1px solid #c2c22d; 124 | color: #000; 125 | display: inline-block; 126 | font-weight: bold; 127 | padding: 1ex; 128 | text-align: center; 129 | } 130 | 131 | div.sphx-glr-download code.download { 132 | display: inline-block; 133 | white-space: normal; 134 | word-break: normal; 135 | overflow-wrap: break-word; 136 | /* border and background are given by the enclosing 'a' */ 137 | border: none; 138 | background: none; 139 | } 140 | 141 | div.sphx-glr-download a:hover { 142 | box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25); 143 | text-decoration: none; 144 | background-image: none; 145 | background-color: #d5d57e; 146 | } 147 | 148 | .sphx-glr-example-title:target::before { 149 | display: block; 150 | content: ""; 151 | margin-top: -50px; 152 | height: 50px; 153 | visibility: hidden; 154 | } 155 | 156 | ul.sphx-glr-horizontal { 157 | list-style: none; 158 | padding: 0; 159 | } 160 | ul.sphx-glr-horizontal li { 161 | display: inline; 162 | } 163 | ul.sphx-glr-horizontal img { 164 | height: auto !important; 165 | } 166 | 167 | .sphx-glr-single-img { 168 | margin: auto; 169 | display: block; 170 | max-width: 100%; 171 | } 172 | 173 | .sphx-glr-multi-img { 174 | max-width: 42%; 175 | height: auto; 176 | } 177 | 178 | div.sphx-glr-animation { 179 | margin: auto; 180 | display: block; 181 | max-width: 100%; 182 | } 183 | div.sphx-glr-animation .animation{ 184 | display: block; 185 | } 186 | 187 | p.sphx-glr-signature a.reference.external { 188 | -moz-border-radius: 5px; 189 | -webkit-border-radius: 5px; 190 | border-radius: 5px; 191 | padding: 3px; 192 | font-size: 75%; 193 | text-align: right; 194 | margin-left: auto; 195 | display: table; 196 | } 197 | 198 | .sphx-glr-clear{ 199 | clear: both; 200 | } 201 | 202 | a.sphx-glr-backref-instance { 203 | text-decoration: none; 204 | } 205 | -------------------------------------------------------------------------------- /docs/source/_static/sovabids - eResearch Australasia 2021 conference poster.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/docs/source/_static/sovabids - eResearch Australasia 2021 conference poster.pdf -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :special-members: __contains__,__getitem__,__iter__,__len__,__add__,__sub__,__mul__,__div__,__neg__,__hash__ 7 | :members: 8 | 9 | .. _sphx_glr_backreferences_{{ fullname }}: 10 | 11 | .. minigallery:: {{ fullname }} 12 | :add-heading: 13 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/function.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autofunction:: {{ objname }} 6 | 7 | .. _sphx_glr_backreferences_{{ fullname }}: 8 | 9 | .. minigallery:: {{ fullname }} 10 | :add-heading: 11 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | from datetime import date 16 | import sovabids 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | curdir = os.path.dirname(__file__) 20 | sys.path.append(os.path.abspath(os.path.join(curdir, "..", "sovabids"))) 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = 'sovabids' 25 | copyright = '2021, sovabids team' 26 | author = "sovabids developers" 27 | _today = date.today() 28 | copyright = f"2021-{_today.year}, sovabids developers. Last updated {_today.isoformat()}" 29 | 30 | # The short X.Y version 31 | version = sovabids.__version__ 32 | release = version 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | "myst_parser", 41 | "sphinx.ext.autodoc", 42 | "sphinx.ext.autosummary", 43 | # "sphinx.ext.intersphinx", 44 | #"numpydoc", 45 | "sphinx_gallery.gen_gallery", 46 | #"gh_substitutions", # custom extension, see ./sphinxext/gh_substitutions.py 47 | "sphinx_copybutton", 48 | 'sphinxcontrib.mermaid', 49 | 'sphinx.ext.napoleon', 50 | ] 51 | 52 | copybutton_prompt_text = r">>> |\.\.\. " 53 | copybutton_prompt_is_regexp = True 54 | 55 | 56 | master_doc = "index" 57 | autosummary_generate = True 58 | 59 | autodoc_default_options = { 60 | "members": True, 61 | "inherited-members": True, 62 | "show-inheritance": True, 63 | } 64 | 65 | sphinx_gallery_conf = { 66 | "doc_module": "sovabids", 67 | "reference_url": { 68 | "sovabids": None, 69 | }, 70 | "examples_dirs": "../../examples", 71 | "gallery_dirs": "auto_examples", 72 | "filename_pattern": "^((?!sgskip).)*$", 73 | "backreferences_dir": "generated", 74 | 'run_stale_examples': False, #Force (or not) re running examples 75 | } 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | # This patterns also effect to html_static_path and html_extra_path 80 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store",'_ideas'] 81 | 82 | # Add any paths that contain templates here, relative to this directory. 83 | templates_path = ['_templates'] 84 | 85 | # -- Options for HTML output ------------------------------------------------- 86 | 87 | # The theme to use for HTML and HTML Help pages. See the documentation for 88 | # a list of builtin themes. 89 | # 90 | html_theme = 'furo' 91 | 92 | # Add any paths that contain custom static files (such as style sheets) here, 93 | # relative to this directory. They are copied after the builtin static files, 94 | # so a file named "default.css" will overwrite the builtin "default.css". 95 | #html_static_path = ['_static'] #already done in the setup(app) section 96 | html_extra_path = ['_copyover'] 97 | 98 | ################################################################################################### 99 | # Seems like this is not needed anymore ########################################################### 100 | # Replace gallery.css for changing the highlight of the output cells in sphinx gallery 101 | # See: 102 | # https://github.com/sphinx-gallery/sphinx-gallery/issues/399 103 | # https://github.com/sphinx-doc/sphinx/issues/2090 104 | # https://github.com/sphinx-doc/sphinx/issues/7747 105 | # def setup(app): 106 | # app.connect('builder-inited', lambda app: app.config.html_static_path.append('_static')) 107 | # app.add_css_file('gallery.css') 108 | ################################################################################################### 109 | 110 | # Auto API 111 | extensions += ['autoapi.extension'] 112 | 113 | autoapi_type = 'python' 114 | autoapi_dirs = ["../../sovabids"] 115 | 116 | extensions += ['sphinx.ext.viewcode'] #see https://github.com/readthedocs/sphinx-autoapi/issues/422 -------------------------------------------------------------------------------- /docs/source/gsoc_proposal.md: -------------------------------------------------------------------------------- 1 | sovabids was born as a google summer of code project [here](https://neurostars.org/t/gsoc-2021-project-idea-14-1-eeg-meg-to-bids-converter-a-gui-application-in-python/18167) 2 | 3 | sovabids advanced week by week during the gsoc as narrated [here](https://yjmantilla.github.io/dirs/gsoc.html) 4 | 5 | The following is the proposal presented to INCF and google. 6 | 7 | ```{include} ../../gsoc_proposal.md 8 | ``` 9 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to sovabids' documentation! 2 | ==================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :caption: Contents: 7 | 8 | README.rst 9 | rules_schema.rst 10 | mappings_schema.rst 11 | validation.rst 12 | auto_examples/index.rst 13 | autoapi/index.rst 14 | gsoc_proposal.md 15 | GitHub Repository 16 | -------------------------------------------------------------------------------- /docs/source/mappings_schema.rst: -------------------------------------------------------------------------------- 1 | Mappings File Schema 2 | ==================== 3 | 4 | The **Mappings File** setups the way the conversion is done from an individual point of view, that is, in a per-file basis. It is intended to be produced by the **apply_rules** (in the cli **sovapply**) module, or by a GUI editor that allows the user to edit the mapping in a per-file basis. 5 | 6 | The **Mappings File** is in yaml format. As of now the purpose of this documentation is not to teach yaml (we may have a dedicated file for that in the future). For now, you can check this `guide `_ though. 7 | 8 | The Typical Mapping File 9 | ------------------------ 10 | 11 | A typical mapping file looks like this: 12 | 13 | .. code-block:: yaml 14 | 15 | 16 | General: 17 | IO: 18 | source: Y:\code\sovabids\_data\DUMMY\DUMMY_SOURCE 19 | target: Y:\code\sovabids\_data\DUMMY\DUMMY_BIDS_custom 20 | channels: 21 | name: 22 | '0': ECG_CHAN 23 | '1': EOG_CHAN 24 | type: 25 | ECG_CHAN: ECG 26 | EOG_CHAN: EOG 27 | dataset_description: 28 | Authors: 29 | - A1 30 | - A2 31 | Name: Dummy 32 | non-bids: 33 | code_execution: 34 | - print('some good code') 35 | - print(raw.info) 36 | - print(some bad code) 37 | eeg_extension: .vhdr 38 | path_analysis: 39 | pattern: T%entities.task%/S%entities.session%/sub%entities.subject%_%entities.acquisition%_%entities.run%.vhdr 40 | sidecar: 41 | EEGReference: FCz 42 | PowerLineFrequency: 50 43 | SoftwareFilters: 44 | Anti-aliasing filter: 45 | Roll-off: 6dB/Octave 46 | half-amplitude cutoff (Hz): 500 47 | Individual: 48 | - IO: 49 | source: Y:\code\sovabids\_data\DUMMY\DUMMY_SOURCE\T0\S0\sub0_0_0.vhdr 50 | target: Y:\code\sovabids\_data\DUMMY\DUMMY_BIDS_custom\sub-0\ses-0\eeg\sub-0_ses-0_task-0_acq-0_run-0_eeg.vhdr 51 | channels: 52 | name: 53 | '0': ECG_CHAN 54 | '1': EOG_CHAN 55 | type: 56 | ECG_CHAN: ECG 57 | EOG_CHAN: EOG 58 | dataset_description: 59 | Authors: 60 | - A1 61 | - A2 62 | Name: Dummy 63 | entities: 64 | acquisition: '0' 65 | run: '0' 66 | session: '0' 67 | subject: '0' 68 | task: '0' 69 | non-bids: 70 | code_execution: 71 | - print('some good code') 72 | - print(raw.info) 73 | - print(some bad code) 74 | eeg_extension: .vhdr 75 | path_analysis: 76 | pattern: T%entities.task%/S%entities.session%/sub%entities.subject%_%entities.acquisition%_%entities.run%.vhdr 77 | sidecar: 78 | EEGReference: FCz 79 | PowerLineFrequency: 50 80 | SoftwareFilters: 81 | Anti-aliasing filter: 82 | Roll-off: 6dB/Octave 83 | half-amplitude cutoff (Hz): 500 84 | - IO: 85 | source: Y:\code\sovabids\_data\DUMMY\DUMMY_SOURCE\T0\S0\sub0_0_1.vhdr 86 | target: Y:\code\sovabids\_data\DUMMY\DUMMY_BIDS_custom\sub-0\ses-0\eeg\sub-0_ses-0_task-0_acq-0_run-1_eeg.vhdr 87 | channels: 88 | name: 89 | '0': ECG_CHAN 90 | '1': EOG_CHAN 91 | type: 92 | ECG_CHAN: ECG 93 | EOG_CHAN: EOG 94 | dataset_description: 95 | Authors: 96 | - A1 97 | - A2 98 | Name: Dummy 99 | entities: 100 | acquisition: '0' 101 | run: '1' 102 | session: '0' 103 | subject: '0' 104 | task: '0' 105 | non-bids: 106 | code_execution: 107 | - print('some good code') 108 | - print(raw.info) 109 | - print(some bad code) 110 | eeg_extension: .vhdr 111 | path_analysis: 112 | pattern: T%entities.task%/S%entities.session%/sub%entities.subject%_%entities.acquisition%_%entities.run%.vhdr 113 | sidecar: 114 | EEGReference: FCz 115 | PowerLineFrequency: 50 116 | SoftwareFilters: 117 | Anti-aliasing filter: 118 | Roll-off: 6dB/Octave 119 | half-amplitude cutoff (Hz): 500 120 | 121 | 122 | Relation to the Rules File 123 | -------------------------- 124 | 125 | As you may have noticed, the **Mappings File** has a lot of similiraties with the **Rules File**. This is because the **Mappings File** is just the rules after being applied to each file. 126 | 127 | 128 | The General and Invididual Objects 129 | ---------------------------------- 130 | 131 | Essentially, the mappings file will have a **General** and an **Invididual** object at the top level. 132 | 133 | The **General** object will contain a copy of the "general" rules; the **Individual** object will hold rules for each of the files to be converted. That is, each file holds "a copy" of the rules along with the modifications that apply to that particular file. 134 | 135 | .. code-block:: yaml 136 | 137 | General : 138 | rules 139 | Individual : 140 | list of rules 141 | 142 | 143 | In the example shown above you may notice that the **General** object does not have an **entities** object, thats because the **entities** object was inferred from the **path_analysis** rule. 144 | Nevertheless, the **Individual** object does show the entities object . That is because the **entities** object was filled by applying the **path_analysis** rule. 145 | 146 | .. note:: 147 | 148 | As of now, the only object that actually shows the result of applying the rules is the **entities** object. The other ones will just show the rule applied to that particular file. 149 | 150 | The Individual object as a list 151 | ------------------------------- 152 | 153 | An important difference between the **General** object and the **Individual** object is that the **General** object holds a single set of rules, whereas the **Individual** object maintains a list of them; in other words, one set of rules for each file. As a result of this, the **Individual** object will be a list; that why it has a (``-``) at the start of every mapping it holds: 154 | 155 | .. code-block:: yaml 156 | 157 | General : 158 | rules 159 | Individual : 160 | - rules for file 1 161 | - rules for file 2 162 | ... 163 | - rules for file N 164 | 165 | THE IO object 166 | ------------- 167 | 168 | A difference you will notice between the **Rules File** and the **Mappings File** is the **IO** object. 169 | 170 | This object just holds input/output information, or more specifically, the **source** and **target**. 171 | 172 | IO in the General object 173 | ^^^^^^^^^^^^^^^^^^^^^^^^ 174 | 175 | For the **General** object we will have : 176 | 177 | .. code-block:: yaml 178 | 179 | General: 180 | IO: 181 | source: source path - root folder of the data to be converted (input) 182 | target: target path - root folder of the bids directory (output) 183 | 184 | 185 | IO in the Invididual object 186 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 187 | 188 | For one of the elements of the **Invididual** object we will have : 189 | 190 | .. code-block:: yaml 191 | 192 | Individual: 193 | IO: 194 | source: source filepath - non-bids input data to be converted 195 | target: target filepath - bids output of that file 196 | 197 | Conclusions 198 | ----------- 199 | 200 | In essence, the **Mappings File** is just the **Rules File** copied once for each file, plus one more to have the "General" perspective. The copies of the rules made for each file will also hold any modification of the rules that apply for that particular file. 201 | 202 | The **IO** object just holds input/output information from the point-of-view of files. -------------------------------------------------------------------------------- /docs/source/validation.rst: -------------------------------------------------------------------------------- 1 | Validation of bids datasets 2 | =========================== 3 | 4 | You can validate the output of sovabids using the `online bids validator `_ . 5 | 6 | You should get something like the following: 7 | 8 | .. image:: _images/online_validator.png -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | .. _general_examples: 2 | 3 | Examples Gallery 4 | ================ 5 | 6 | Here we show some exemplary use cases for sovabids. 7 | 8 | These examples assume the folder structure of the sovabids repository (ie having the "examples" directory). 9 | If you plan to run these examples you should install sovabids in the following way: 10 | 11 | Installation for using examples 12 | ------------------------------- 13 | 14 | To be able to run all examples the easiest is to install sovabids in the 'advanced-usage' mode, 15 | this helps to preserve that folder structure of the repository and install any additional dependencies needed for the examples. 16 | 17 | See the `introduction section of the docs `_ for the installation instructions. 18 | 19 | .. contents:: Contents 20 | :local: 21 | :depth: 3 22 | -------------------------------------------------------------------------------- /examples/bidscoin_example.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | =================================== 4 | LEMON dataset example with BIDSCOIN 5 | =================================== 6 | 7 | This example illustrates the use of ``sovabids`` on the `LEMON dataset `_ 8 | using bidscoin. 9 | 10 | The main elements of this example are: 11 | * A source path with the original dataset. 12 | * A bids path that will be the output path of the conversion. 13 | * A rules file that configures how the conversion is done. 14 | * A bidsmap template required by bidscoin. (Equivalent to our rules file) 15 | * A study bidsmap. (Equivalent to our mappings file) 16 | 17 | Refer to the `bidscoin documentation `_ to understand the components and workflow of bidscoin. 18 | 19 | In summary, bidscoin uses a bidsmap to encode how a conversion is done. 20 | 21 | Intuitively, the bidsmapper grabs a template to produce a study bidsmap, which may or may not be customized for each file through the bidseditor. Either way the final study bidsmap is passed to bidscoiner to perform the conversion. 22 | 23 | The connection of sovabids and bidscoin is through a plugin called **sova2coin** which helps the bidscoin modules by dealing with the EEG data. 24 | 25 | .. mermaid:: 26 | 27 | graph LR 28 | S>"Source path"] 29 | B>"Bids path"] 30 | R>"Rules file"] 31 | O[("Converted dataset")] 32 | BM((bidsmapper)) 33 | BC((bidscoiner)) 34 | BE((bidseditor)) 35 | BF>bidsmap template] 36 | SOVA((sova2coin)) --> BM 37 | B-->BM 38 | BM-->|bidsmap|BE 39 | BE-->|user-edited bidsmap|BC 40 | BC-->O 41 | R --> SOVA 42 | BF -->BM 43 | S --> BM 44 | SOVA-->BC 45 | 46 | 47 | .. warning:: 48 | If you didn't install sovabids in 'gui-usage' mode ( `see here `_ ), you will probably need to install bidscoin: 49 | 50 | Install our `bidscoin branch `_ 51 | 52 | That is, you need to run: 53 | 54 | .. code-block:: bash 55 | 56 | pip install git+https://github.com/yjmantilla/bidscoin.git@sovabids 57 | 58 | If that doesn't work try: 59 | 60 | .. code-block:: bash 61 | 62 | git clone https://github.com/yjmantilla/bidscoin/tree/sovabids 63 | cd bidscoin 64 | pip install . 65 | """ 66 | 67 | #%% 68 | # Imports 69 | # ^^^^^^^ 70 | # First we import some functions we will need: 71 | 72 | import os 73 | import shutil # File manipulation 74 | import yaml # To do yaml operations 75 | from mne_bids import print_dir_tree # To show the input/output directories structures inside this example 76 | from sovabids.datasets import lemon_bidscoin_prepare # Dataset 77 | from sovabids.schemas import get_sova2coin_bidsmap # bidsmap template schema 78 | from sovabids.settings import REPO_PATH 79 | #%% 80 | # Setting up the paths 81 | # ^^^^^^^^^^^^^^^^^^^^ 82 | # First, we will set up four paths. Because this example is intended to run relative 83 | # to the repository directory we use relative path but for real use-cases it is 84 | # easier to just input the absolute-path. We will print these paths for more clarity. 85 | 86 | dataset = 'lemon_bidscoin' # Just folder name where to save or dataset 87 | data_dir = os.path.join(REPO_PATH,'_data') 88 | data_dir = os.path.abspath(data_dir) 89 | 90 | source_path = os.path.abspath(os.path.join(data_dir,dataset+'_input')) 91 | bids_path= os.path.abspath(os.path.join(data_dir,dataset+'_output')) 92 | code_path = os.path.join(bids_path,'code','bidscoin') 93 | rules_path = os.path.join(code_path,'rules.yml') 94 | template_path = os.path.join(code_path,'template.yml') 95 | bidsmap_path = os.path.join( code_path,'bidsmap.yaml') 96 | print('source_path:',source_path.replace(data_dir,'')) 97 | print('bids_path:', bids_path.replace(data_dir,'')) 98 | print('rules_path:',rules_path.replace(data_dir,'')) 99 | print('template_path:',template_path.replace(data_dir,'')) 100 | print('bidsmap_path:',bidsmap_path.replace(data_dir,'')) 101 | 102 | #%% 103 | # Cleaning the output directory 104 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 105 | # We will clean the output path as a safety measure from previous conversions. 106 | 107 | try: 108 | shutil.rmtree(bids_path) 109 | except: 110 | pass 111 | 112 | #%% 113 | # 114 | # Make the folders if they don't exist to avoid errors 115 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 116 | for p in [source_path,bids_path,code_path]: 117 | os.makedirs(p,exist_ok=True) 118 | 119 | #%% 120 | # Getting and preparing the dataset 121 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 122 | # We have to download and decompress the dataset. We also need to fix a filename inconsistency 123 | # (without this correction the file won't be able to be opened in mne). Luckily all of that is 124 | # encapsulated in the lemon_prepare function since these issues are not properly of sovabids. 125 | # 126 | # We also need to prepare the data to the `bidscoin required source data structure `_ . 127 | # 128 | # We will save this input data to source_path . 129 | lemon_bidscoin_prepare(source_path) 130 | 131 | 132 | #%% 133 | # The input directory 134 | # ^^^^^^^^^^^^^^^^^^^ 135 | # For clarity purposes we will print here the directory we are trying to convert to BIDS. 136 | 137 | print_dir_tree(source_path) 138 | 139 | #%% 140 | # Making the rules 141 | # ^^^^^^^^^^^^^^^^ 142 | # See the Rules File Schema documentation for help regarding making this rules file. 143 | # 144 | # Here we will make the rules from a python dictionary. 145 | 146 | rules ={ 147 | 'entities':{'task':'resting'}, 148 | 'dataset_description':{'Name':dataset}, 149 | 'sidecar':{'PowerLineFrequency':50,'EEGReference':'FCz'}, 150 | 'channels':{'type':{'VEOG':'VEOG'}}, 151 | 'non-bids':{'path_analysis':{'pattern':'sub-%entities.subject%/ses-%entities.session%/%ignore%/%ignore%.vhdr'}} 152 | } 153 | with open(rules_path, 'w') as outfile: 154 | yaml.dump(rules, outfile, default_flow_style=False) 155 | 156 | #%% 157 | # Now print the rules to see how the yaml file we made from the python dictionary looks like 158 | # 159 | with open(rules_path,encoding="utf-8") as f: 160 | rules = f.read() 161 | print(rules) 162 | 163 | #%% 164 | # Making the bidsmap template 165 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 166 | # The template is equivalent to the "rules" file of sovabids. It encodes the general way of doing the conversion. 167 | # 168 | # Explaining this file is out of scope of this example (this is bidscoin territory). 169 | # 170 | # We will notice however that: 171 | # 172 | # * We input our rules file as an option to the sova2coin plugin. 173 | # * We are interested in a "EEG" dataformat with an "eeg" datatype. 174 | # * We match every file with a .* in the properties.filename section 175 | # * The attributes are basically the metadata information extracted from the files which may be used to derive bids-related information. 176 | # * In the attributes section we have the objects as they are named in our rules file schema (that is, here we deal with sovabids terminology using a dot notation for nesting) 177 | # * We populate bids-related info with the extracted attributes (see subject, session and bids sections of the file) 178 | # * We set the suffix to eeg. 179 | # * The ``<`` and ``<<`` is best explained `here `_ 180 | template = get_sova2coin_bidsmap().format(rules_path) 181 | 182 | print(template) 183 | 184 | with open(template_path,mode='w') as f: 185 | f.write(template) 186 | 187 | #%% 188 | # .. tip:: 189 | # 190 | # You can also input the rules directly (ie writing the rules instead of the path to the rules file) 191 | # 192 | # What is important is that inside the "rules" field of the sova2coin "options" 193 | # 194 | # .. note:: 195 | # The EEG:eeg hierarchy just says that there is an 'EEG' dataformat which a general 'eeg' datatype. 196 | # 197 | # This is a bit redundant but it happens because bidscoin was originally thought for DICOM (dataformat) 198 | # which holds many datatypes (anat,perf,etc). In eeg this doesnt happens. 199 | # 200 | # Some necessary code 201 | # ^^^^^^^^^^^^^^^^^^^ 202 | # To be able to run commands from this notebook and capture their outputs we need to define the following, nevertheless this is not relevant to actually running this from the command line. 203 | 204 | from subprocess import PIPE, run 205 | 206 | def out(command): 207 | result = run(command, stdout=PIPE, stderr=PIPE, universal_newlines=True, shell=True) 208 | return result.stdout 209 | 210 | my_output = out("echo hello world") 211 | print(my_output) 212 | 213 | #%% 214 | # bidsmapper 215 | # ^^^^^^^^^^ 216 | # First we execute the bidsmapper to get a study bidsmap from our bidsmap template. 217 | # 218 | # The bidsmap file is equivalent to our "mappings" file; it encodes how the conversion is done on a per-file basis. 219 | # 220 | # Lets see the help: 221 | 222 | command = "bidsmapper --help" 223 | print(command) 224 | #%% 225 | # This will give the following output 226 | # 227 | my_output= out(command) 228 | print(my_output) 229 | 230 | #%% 231 | # Now we will use the following command to get the bidsmap study file. 232 | # 233 | # Note we use -t to set the template and -a to run this without the bidseditor 234 | # 235 | # You can skip the -a option if you are able to open the bidseditor (ie you are able to give user-input in its interface) 236 | # 237 | # Just remember to save the bidsmap yaml at the end. 238 | # 239 | # See the `bidseditor documentation `_ for more info. 240 | command = 'bidsmapper '+source_path + ' '+ bids_path + ' -t ' + template_path + ' -a' 241 | print(command) 242 | 243 | #%% 244 | # This will produce the following study bidsmap: 245 | 246 | my_output= out(command) 247 | print(my_output) 248 | 249 | with open(bidsmap_path,encoding="utf8", errors='ignore') as f: 250 | bidsmap= f.read() 251 | print(bidsmap) 252 | 253 | #%% 254 | # at the following path: 255 | 256 | print(bidsmap_path) 257 | 258 | #%% 259 | # bidscoiner 260 | # ^^^^^^^^^^ 261 | # Now we are ready to perform the conversion given the study bidsmap file just made. 262 | # 263 | # Use the following command to print the help of the tool: 264 | # 265 | command = "bidscoiner --help" 266 | print(command) 267 | #%% 268 | # This will give the following output 269 | # 270 | my_output= out(command) 271 | print(my_output) 272 | 273 | #%% 274 | # Now we will use the following command to perform the conversion. 275 | # 276 | command = 'bidscoiner '+source_path + ' '+ bids_path + ' -b '+ bidsmap_path 277 | 278 | print(command) 279 | 280 | #%% 281 | # This will produce the following output: 282 | my_output= out(command) 283 | print(my_output) 284 | 285 | #%% 286 | # Checking the conversion 287 | # ^^^^^^^^^^^^^^^^^^^^^^^ 288 | # For clarity purposes we will check the output directory we got from sovabids. 289 | 290 | print_dir_tree(bids_path) 291 | 292 | print('BIDSCOIN CONVERSION FINISHED!') 293 | 294 | 295 | 296 | -------------------------------------------------------------------------------- /examples/example_heuristic_source_target_pair.py: -------------------------------------------------------------------------------- 1 | """ 2 | =================================== 3 | Usage example of source,target pair 4 | =================================== 5 | 6 | This example illustrates how does the inference of the path_pattern from a (source,target) pair example works. 7 | 8 | The main elements of this example are: 9 | * A source example path of one of your files 10 | * A target path that will be the expected mapping of your file. 11 | * The from_io_example heuristic that internally does the inference work. 12 | * A path pattern inferred from the above. 13 | 14 | 15 | Be sure to read `the Rules File Schema documentation section relating to the Paired Example `_ before doing this example for more context. 16 | 17 | 18 | .. mermaid:: 19 | 20 | graph LR 21 | S>"source path example"] 22 | B>"target path example"] 23 | AR(("from_io_example")) 24 | M>"path pattern"] 25 | S --> AR 26 | B --> AR 27 | AR --> M 28 | 29 | The Rules File 30 | -------------- 31 | The Rules File we are dealing here has the following path_analysis rule 32 | 33 | """ 34 | 35 | import json # utility 36 | from sovabids.files import _write_yaml # To print the yaml file 37 | 38 | # The rule we are dealing with 39 | rule = { 40 | 'non-bids':{ 41 | 'path_analysis': 42 | { 43 | 'source' : 'data/lemon/V001/resting/010002.vhdr', 44 | 'target' : 'data_bids/sub-010002/ses-001/eeg/sub-010002_ses-001_task-resting_eeg.vhdr' 45 | } 46 | } 47 | } 48 | 49 | yaml_file = _write_yaml(rule) 50 | 51 | print('Rules File:\n\n',yaml_file) 52 | 53 | #%% 54 | # The from_io_example function 55 | # ----------------------------- 56 | # 57 | # Although this is hidden from the user, internally sovabids uses this function to infer the pattern. 58 | # 59 | # The name of the function means "from input-output example", as one provides an input and output pair of (source,target) paths. 60 | # 61 | # Here we will illustrate how this function behaves. Lets see the documentation of the function: 62 | # 63 | 64 | from sovabids.heuristics import from_io_example # The function itself 65 | 66 | print('from_io_example:\n\n',from_io_example.__doc__) 67 | 68 | #%% 69 | # The result of the function 70 | # ----------------------------- 71 | # 72 | # The function will return the placeholder pattern as explained in `the Rules File Schema documentation section relating to the Placeholder Pattern `_ . 73 | # 74 | # 75 | sourcepath = rule['non-bids']['path_analysis']['source'] 76 | targetpath = rule['non-bids']['path_analysis']['target'] 77 | result = from_io_example(sourcepath,targetpath) 78 | 79 | print('Result:\n\n',result) 80 | 81 | 82 | #%% 83 | # Ambiguity 84 | # ---------- 85 | # 86 | # This is explained in more detail in `the warning section of the the Paired Example documentation `_ . 87 | # Be sure to read it before for fully understading what ambiguity means here. 88 | # 89 | # An ambiguous rule would be: 90 | # 91 | 92 | rule = { 93 | 'non-bids':{ 94 | 'path_analysis': 95 | { 96 | 'source':'data/lemon/session001/taskT001/010002.vhdr', 97 | 'target':'data_bids/sub-010002/ses-001/eeg/sub-010002_ses-001_task-T001_eeg.vhdr' 98 | } 99 | } 100 | } 101 | 102 | yaml_file = _write_yaml(rule) 103 | 104 | print('Ambiguous Example:\n\n',yaml_file) 105 | 106 | #%% 107 | # If your example is ambiguous, the function will raise an error. 108 | # 109 | # Notice the last bit of the message, it will hint you about what part of the example is suspected to have ambiguity. 110 | # 111 | from traceback import format_exc 112 | 113 | try: 114 | sourcepath = rule['non-bids']['path_analysis']['source'] 115 | targetpath = rule['non-bids']['path_analysis']['target'] 116 | result = from_io_example(sourcepath,targetpath) 117 | except: 118 | print('Error:\n\n',format_exc()) 119 | -------------------------------------------------------------------------------- /examples/gui_example.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | =============================== 4 | GUI example with LEMON dataset 5 | =============================== 6 | 7 | This example illustrates the use of ``sovabids`` on the `LEMON dataset `_ 8 | using the preliminary GUI tool. 9 | 10 | Install Sovabids in gui-usage mode 11 | --------------------------------------- 12 | (`see here `_) 13 | 14 | Download the Lemon dataset 15 | ----------------------------- 16 | (if you have not already done so) 17 | If you need to download the files, you can use: 18 | 19 | .. code-block:: bash 20 | 21 | python -c 'from sovabids.datasets import lemon_prepare; lemon_prepare()' 22 | 23 | This will download the lemon dataset to a '_data' subfolder that will be created in the installed packages folder. 24 | 25 | Run front/app/app.py in a terminal 26 | ---------------------------------- 27 | (the front folder is the one in the root of the cloned sovabids repository.) 28 | 29 | Assuming you are at the root of the sovabids cloned repository, you can use: 30 | 31 | .. code-block:: bash 32 | 33 | python front/app/app.py 34 | 35 | 36 | Go to your browser at http://127.0.0.1:5000/ 37 | --------------------------------------------- 38 | 39 | You will see: 40 | 41 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_1_intro.png 42 | 43 | This is an introductory page of the GUI. 44 | 45 | Click Upload Files 46 | ------------------- 47 | 48 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_2_click_upload.png 49 | 50 | Choose Files 51 | -------------- 52 | 53 | In a terminal, run the following command to find the installed packages folder: 54 | 55 | .. code-block:: bash 56 | 57 | pip show sovabids 58 | 59 | The path to the installed packages folder is given in the 'Location:' field. 60 | 61 | 62 | Click 'Choose Files', go to the installed packages folder, click on the _data subfolder, and then select the lemon subfolder. Click submit. 63 | 64 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_3_upload.png 65 | 66 | You will need to wait while the files are copied to the server. Since these are heavy eegs, it will take a bit. 67 | 68 | Deselect any files you want to skip 69 | ------------------------------------ 70 | 71 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_4_deselect.png 72 | 73 | Here we won't skip any files. Notice though that there are non-eeg files (.csv and tar.gz ) mixed in there. Sovabids will skip them automatically. 74 | 75 | Click Send. 76 | 77 | Confirm detected eegs 78 | ------------------------ 79 | 80 | Sovabids will show the individual eegs found. Notice that since this is a brainvision dataset, sovabids lists the main files of this format (the .vhdr files) since the other ones (.eeg and .vmrk) are sidecars. 81 | 82 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_5_detected.png 83 | 84 | 85 | The rules files 86 | ----------------- 87 | 88 | A suitable rules file is already at examples/lemon_example_rules.yml within the cloned sovabids repositry, you can upload that one. 89 | 90 | Click Choose File, then in the popup window select the rules file, click open and then submit. 91 | 92 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_6_upload_rules.png 93 | 94 | After that you will notice the text pane is updated: 95 | 96 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_7_inspect_rules.png 97 | 98 | You can edit the rules directly, as shown here where we modify the dataset name to "Modified Name'. 99 | 100 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_8_edit_rules.png 101 | 102 | Once ready, scroll down to find another submit and click it to continue. 103 | 104 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_9_submit_rules.png 105 | 106 | 107 | Edit individual mappings 108 | -------------------------- 109 | 110 | You will now see a list of the eeg files, for each you can edit its mapping. At first you will see an empty text pane since no eeg file is chosen. 111 | 112 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_10_mappings.png 113 | 114 | Click any of the files for editing it: 115 | 116 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_11_select_mapping.png 117 | 118 | Once clicked, you will see the corresponding mapping: 119 | 120 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_12_the_mapping.png 121 | 122 | You can edit the INDIVIDUAL mapping of this file, in example here we will change the power line frequency of this eeg to 60 Hz. 123 | 124 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_13_edit_mapping.png 125 | 126 | To save the edited individual mapping, press Send at the right: 127 | 128 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_14_save_mapping.png 129 | 130 | You will be redirected to the empty mapping, but if you click the same eeg file again you will notice the changes are saved: 131 | 132 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_15_check_mapping.png 133 | 134 | Once the mappings are ready, click next: 135 | 136 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_16_mappings_ready.png 137 | 138 | 139 | Proceed to the conversion 140 | -------------------------- 141 | 142 | Click on the button: 143 | 144 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_17_conversion_click.png 145 | 146 | Once clicked it will take a while before the program finishes (a feedback information of the progress is not yet implemented on this preliminary GUI). 147 | 148 | Save your conversion 149 | ------------------------ 150 | 151 | When the files are ready you will see: 152 | 153 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_18_download_click.png 154 | 155 | Once you click there, sovabids will begin compressing your files so it will take a bit until the download windows is shown. Select where you want to download the file and press save. 156 | 157 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_19_download_popup.png 158 | 159 | When the download is ready, navigate to the chosen folder and decompress the files. Go into the correspondent folder to see the converted files: 160 | 161 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_20_converted_files.png 162 | 163 | Inspect the sovabids logs and mappings 164 | --------------------------------------- 165 | 166 | Inside the code/sovabids subdirectory you will see the mappings and log files. 167 | 168 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_21_sovabids_files.png 169 | 170 | The mappings.yml file will hold the mappings: 171 | 172 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_22_mapping_file.png 173 | 174 | The logs will hold run-time information of the procedure: 175 | 176 | .. image:: https://github.com/yjmantilla/sovabids/raw/main/docs/source/_static/front_23_log_files.png 177 | 178 | """ 179 | -------------------------------------------------------------------------------- /examples/lemon_example.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | ====================== 4 | LEMON dataset example 5 | ====================== 6 | 7 | This example illustrates the use of ``sovabids`` on the `LEMON dataset `_ 8 | using both the python API and the CLI tool. 9 | 10 | The main elements of this example are: 11 | * A source path with the original dataset. 12 | * A bids path that will be the output path of the conversion. 13 | * A rules file that configures how the conversion is done. 14 | * A mapping file that encodes how the conversion is performed to each individual file of the dataset. 15 | 16 | .. mermaid:: 17 | 18 | graph LR 19 | S>"Source path"] 20 | B>"Bids path"] 21 | R>"Rules file"] 22 | AR(("Apply Rules")) 23 | M>"Mappings file"] 24 | CT(("Convert Them")) 25 | O[("Converted dataset")] 26 | S --> AR 27 | B --> AR 28 | R --> AR 29 | AR --> M 30 | M --> CT 31 | CT --> O 32 | """ 33 | 34 | #%% 35 | # Using the python API 36 | # -------------------- 37 | # First we will illustrate how to run the software within python. 38 | # 39 | # Imports 40 | # ^^^^^^^ 41 | # First we import some functions we will need: 42 | 43 | import os # For path manipulation 44 | import shutil # File manipulation 45 | from mne_bids import print_dir_tree # To show the input/output directories structures inside this example 46 | from sovabids.rules import apply_rules # Apply rules for conversion 47 | from sovabids.convert import convert_them # Do the conversion 48 | from sovabids.datasets import lemon_prepare # Download the dataset 49 | from sovabids.settings import REPO_PATH 50 | #%% 51 | # Getting and preparing the dataset 52 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 53 | # We have to download and decompress the dataset. We also need to fix a filename inconsistency 54 | # (without this correction the file won't be able to be opened in mne). Luckily all of that is 55 | # encapsulated in the lemon_prepare function since these issues are not properly of sovabids. 56 | # 57 | # By default the files are saved in the '_data' directory of the sovabids project. 58 | lemon_prepare() 59 | 60 | #%% 61 | # Setting up the paths 62 | # ^^^^^^^^^^^^^^^^^^^^ 63 | # Now we will set up four paths. Because this example is intended to run relative 64 | # to the repository directory we use relative path but for real use-cases it is 65 | # easier to just input the absolute-path. We will print these paths for more clarity. 66 | 67 | source_path = os.path.abspath(os.path.join(REPO_PATH,'_data','lemon')) # For the input data we will convert 68 | bids_path= os.path.abspath(os.path.join(REPO_PATH,'_data','lemon_bids')) # The output directory that will have the converted data 69 | rules_path = os.path.abspath(os.path.join(REPO_PATH,'examples','lemon_example_rules.yml')) # The rules file that setups the rule for conversion 70 | mapping_path = os.path.abspath(os.path.join(bids_path,'code','sovabids','mappings.yml')) # The mapping file that will hold the results of applying the rules to each file 71 | 72 | print('source_path:',source_path.replace(REPO_PATH,'')) 73 | print('bids_path:', bids_path.replace(REPO_PATH,'')) 74 | print('rules_path:',rules_path.replace(REPO_PATH,'')) 75 | print('mapping_path:',mapping_path.replace(REPO_PATH,'')) 76 | 77 | #%% 78 | # Cleaning the output directory 79 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 80 | # We will clean the output path as a safety measure from previous conversions. 81 | 82 | try: 83 | shutil.rmtree(bids_path) 84 | except: 85 | pass 86 | 87 | #%% 88 | # The input directory 89 | # ^^^^^^^^^^^^^^^^^^^ 90 | # For clarity purposes we will print here the directory we are trying to convert to BIDS. 91 | 92 | print_dir_tree(source_path) 93 | 94 | #%% 95 | # Making the rules 96 | # ^^^^^^^^^^^^^^^^ 97 | # The most important and complicated part of this is making the rules file, 98 | # either by hand or by the "DISCOVER_RULES" module (which is not yet implemented). 99 | # 100 | # This part is already done for you, but for clarification here are the rules 101 | # we are applying. Please read the following output as the yaml has some basic 102 | # documentation comments. 103 | # 104 | # See the Rules File Schema documentation for help regarding making this rules file. 105 | # 106 | with open(rules_path,encoding="utf-8") as f: 107 | rules = f.read() 108 | print(rules) 109 | 110 | #%% 111 | # Applying the rules 112 | # ^^^^^^^^^^^^^^^^^^ 113 | # We apply the rules to the input dataset by giving the input,ouput,rules, and mapping paths to the apply_rules function. 114 | # 115 | # This will produce by default a 'mappings.yml' file at the specified directory of 'bids_path/code/sovabids'. 116 | # 117 | # This file holds the result of applying the rules to each of the dataset files. 118 | apply_rules(source_path,bids_path,rules_path,mapping_path) 119 | 120 | #%% 121 | # Doing the conversion 122 | # ^^^^^^^^^^^^^^^^^^^^ 123 | # We now do the conversion of the dataset by reading the mapping file ('mappings.yml') with the convert them module. 124 | convert_them(mapping_path) 125 | 126 | #%% 127 | # Checking the conversion 128 | # ^^^^^^^^^^^^^^^^^^^^^^^ 129 | # For clarity purposes we will check the output directory we got from sovabids. 130 | 131 | print_dir_tree(bids_path) 132 | 133 | print('LEMON CONVERSION FINISHED!') 134 | 135 | #%% 136 | # Using the CLI tool 137 | # ------------------ 138 | # 139 | # sovabids can also be used through the command line. Here we provide an example of how to do so. 140 | # 141 | # 142 | # The overview of what we are doing is now: 143 | # 144 | # .. mermaid:: 145 | # 146 | # graph LR 147 | # S>"Source path"] 148 | # B>"Bids path"] 149 | # R>"Rules file"] 150 | # AR(("sovapply")) 151 | # M>"Mappings file"] 152 | # CT(("sovaconvert")) 153 | # O[("Converted dataset")] 154 | # S --> AR 155 | # B --> AR 156 | # R --> AR 157 | # AR --> M 158 | # M --> CT 159 | # CT --> O 160 | 161 | #%% 162 | # Same old blues 163 | # ^^^^^^^^^^^^^^ 164 | # Notice that we will run this inside of python so that the example can be run without needing configuration. 165 | # 166 | # To run this locally you will need to run lemon_prepare() function from the command line. You can do so by running: 167 | # .. code-block:: bash 168 | # 169 | # python -c "from sovabids.datasets import lemon_prepare; lemon_prepare()" 170 | # 171 | # Since we already have run lemon_prepare() inside this example, we will start from this step. 172 | # 173 | # We set up the paths again, but now we will change the output to a new path (with "_cli" at the end). We will also clean this path as we did before. 174 | # 175 | source_path = os.path.abspath(os.path.join(REPO_PATH,'_data','lemon')) # For the input data we will convert 176 | bids_path= os.path.abspath(os.path.join(REPO_PATH,'_data','lemon_bids_cli')) # The output directory that will have the converted data 177 | rules_path = os.path.abspath(os.path.join(REPO_PATH,'examples','lemon_example_rules.yml')) # The rules file that setups the rule for conversion 178 | mapping_path = os.path.abspath(os.path.join(bids_path,'code','sovabids','mappings.yml')) # The mapping file that will hold the results of applying the rules to each file 179 | 180 | print('source_path:',source_path.replace(REPO_PATH,'')) 181 | print('bids_path:', bids_path.replace(REPO_PATH,'')) 182 | print('rules_path:',rules_path.replace(REPO_PATH,'')) 183 | print('mapping_path:',mapping_path.replace(REPO_PATH,'')) 184 | 185 | try: 186 | shutil.rmtree(bids_path) 187 | except: 188 | pass 189 | 190 | # %% 191 | # Some necessary code 192 | # ^^^^^^^^^^^^^^^^^^^ 193 | # To be able to run commands from this notebook and capture their outputs we need to define the following, nevertheless this is not relevant to actually running this from the command line. 194 | 195 | from subprocess import PIPE, run 196 | 197 | def out(command): 198 | result = run(command, stdout=PIPE, stderr=PIPE, universal_newlines=True, shell=True) 199 | return result.stdout 200 | 201 | my_output = out("echo hello world") 202 | print(my_output) 203 | 204 | #%% 205 | # sovapply 206 | # ^^^^^^^^ 207 | # In this example we have already made the rules. So we will apply them using the sovapply tool. 208 | # 209 | # Use the following command to print the help of the tool: 210 | # 211 | command = "sovapply --help" 212 | print(command) 213 | #%% 214 | # This will give the following output 215 | # 216 | my_output= out(command) 217 | print(my_output) 218 | 219 | #%% 220 | # Now we will use the following command to get the mappings file. 221 | # 222 | command = 'sovapply '+source_path + ' '+ bids_path + ' ' + rules_path + ' -m ' + mapping_path 223 | print(command) 224 | 225 | #%% 226 | # This will produce the following output: 227 | my_output= out(command) 228 | print(my_output) 229 | 230 | #%% 231 | # sovaconvert 232 | # ^^^^^^^^^^^ 233 | # Now we are ready to perform the conversion given the mapping file just made. 234 | # 235 | # Use the following command to print the help of the tool: 236 | # 237 | command = "sovaconvert --help" 238 | print(command) 239 | #%% 240 | # This will give the following output 241 | # 242 | my_output= out(command) 243 | print(my_output) 244 | 245 | #%% 246 | # Now we will use the following command to perform the conversion. 247 | # 248 | command = 'sovaconvert ' + mapping_path 249 | print(command) 250 | 251 | #%% 252 | # This will produce the following output: 253 | my_output= out(command) 254 | print(my_output) 255 | 256 | #%% 257 | # Checking the conversion 258 | # ^^^^^^^^^^^^^^^^^^^^^^^ 259 | # For clarity purposes we will check the output directory we got from sovabids. 260 | 261 | print_dir_tree(bids_path) 262 | 263 | print('LEMON CLI CONVERSION FINISHED!') 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /examples/lemon_example_rules.yml: -------------------------------------------------------------------------------- 1 | entities: # Configuring the file name structure of bids 2 | task : resting # Setting the task of all files to a fixed string 3 | 4 | dataset_description: # Configuring the dataset_description.json file 5 | Name : Lemon # Name of the dataset, set up as a fixed string 6 | Authors: # Here I put the personnel involved in the acquisition of the dataset 7 | - Anahit Babayan # See http://fcon_1000.projects.nitrc.org/indi/retro/MPI_LEMON.html 8 | - Miray Erbey 9 | - Deniz Kumral 10 | - Janis D. Reinelt 11 | - Andrea M. F. Reiter 12 | - Josefin Röbbig 13 | - H. Lina Schaare 14 | - Marie Uhlig 15 | - Alfred Anwander 16 | - Pierre-Louis Bazin 17 | - Annette Horstmann 18 | - Leonie Lampe 19 | - Vadim V. Nikulin 20 | - Hadas Okon-Singer 21 | - Sven Preusser 22 | - André Pampel 23 | - Christiane S. Rohr 24 | - Julia Sacher1 25 | - Angelika Thöne-Otto 26 | - Sabrina Trapp 27 | - Till Nierhaus 28 | - Denise Altmann 29 | - Katrin Arelin 30 | - Maria Blöchl 31 | - Edith Bongartz 32 | - Patric Breig 33 | - Elena Cesnaite 34 | - Sufang Chen 35 | - Roberto Cozatl 36 | - Saskia Czerwonatis 37 | - Gabriele Dambrauskaite 38 | - Maria Dreyer 39 | - Jessica Enders 40 | - Melina Engelhardt 41 | - Marie Michele Fischer 42 | - Norman Forschack 43 | - Johannes Golchert 44 | - Laura Golz 45 | - C. Alexandrina Guran 46 | - Susanna Hedrich 47 | - Nicole Hentschel 48 | - Daria I. Hoffmann 49 | - Julia M. Huntenburg 50 | - Rebecca Jost 51 | - Anna Kosatschek 52 | - Stella Kunzendorf 53 | - Hannah Lammers 54 | - Mark E. Lauckner 55 | - Keyvan Mahjoory 56 | - Natacha Mendes 57 | - Ramona Menger 58 | - Enzo Morino 59 | - Karina Näthe 60 | - Jennifer Neubauer 61 | - Handan Noyan 62 | - Sabine Oligschläger 63 | - Patricia Panczyszyn-Trzewik 64 | - Dorothee Poehlchen 65 | - Nadine Putzke 66 | - Sabrina Roski 67 | - Marie-Catherine Schaller 68 | - Anja Schieferbein 69 | - Benito Schlaak 70 | - Hanna Maria Schmidt 71 | - Robert Schmidt 72 | - Anne Schrimpf 73 | - Sylvia Stasch 74 | - Maria Voss 75 | - Anett Wiedemann 76 | - Daniel S. Margulies 77 | - Michael Gaebler 78 | - Arno Villringer 79 | 80 | sidecar: # Configuring the sidecar eeg file 81 | PowerLineFrequency : 50 # Noted from the visual inspection of the eeg spectrum 82 | EEGReference : FCz # As mentioned in https://www.nature.com/articles/sdata2018308 83 | 84 | channels: # Configuring the channels tsv 85 | type : # This property allow us to overwrite channel types inferred by MNE 86 | VEOG : VEOG # Here the syntax is : 87 | F3 : EEG # Here we set the type of F3, it was already correctly inferred by mne but it is included to illustrate retyping of various channels. 88 | non-bids: # Additional configuration not belonging specifically to any of the previous objects 89 | eeg_extension : .vhdr # Sets which extension to read as an eeg file 90 | path_analysis: # Some bids properties can be inferred from the path of the source files 91 | pattern : RSEEG/sub-%entities.subject%.vhdr # For example here we extract from the path the "subject" child of the "entities" object 92 | -------------------------------------------------------------------------------- /examples/rpc_example.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | ======================================== 4 | RPC API example with the LEMON dataset 5 | ======================================== 6 | 7 | This example illustrates the use of ``sovabids`` on the `LEMON dataset `_ 8 | using the RPC API. 9 | 10 | .. warning:: 11 | To run this example, you need to install sovabids in 'advanced-usage' mode ( `see here `_ ). 12 | 13 | """ 14 | 15 | #%% 16 | # Sovabids uses an action-oriented API. Here we will illustrate each of the available functionalities. 17 | # 18 | # Imports 19 | # ------- 20 | # First we import some functions we will need: 21 | 22 | 23 | import os # For path manipulation 24 | import shutil # File manipulation 25 | from mne_bids import print_dir_tree # To show the input/output directories structures inside this example 26 | from sovabids.datasets import lemon_prepare # Download the dataset 27 | from sovabids.settings import REPO_PATH 28 | from sovabids.sovarpc import app as sovapp # The RPC API application 29 | import sovabids.sovarpc as sovarpc 30 | from fastapi.testclient import TestClient # This will be for simulating ourselves as a client of the RPC API 31 | import json # for making json-based requests 32 | import copy # just to make deep copies of variables 33 | 34 | #%% 35 | # Getting and preparing the dataset 36 | # --------------------------------- 37 | # We have to download and decompress the dataset. We also need to fix a filename inconsistency 38 | # (without this correction the file won't be able to be opened in mne). Luckily all of that is 39 | # encapsulated in the lemon_prepare function since these issues are not properly of sovabids. 40 | # 41 | # By default the files are saved in the '_data' directory of the sovabids project. 42 | lemon_prepare() 43 | 44 | #%% 45 | # Setting up the paths 46 | # -------------------- 47 | # Now we will set up four paths. Because this example is intended to run relative 48 | # to the repository directory we use relative path but for real use-cases it is 49 | # easier to just input the absolute-path. We will print these paths for more clarity. 50 | 51 | source_path = os.path.abspath(os.path.join(REPO_PATH,'_data','lemon')) # For the input data we will convert 52 | bids_path= os.path.abspath(os.path.join(REPO_PATH,'_data','lemon_bids_rpc')) # The output directory that will have the converted data 53 | rules_path = os.path.abspath(os.path.join(REPO_PATH,'examples','lemon_example_rules.yml')) # The rules file that setups the rule for conversion 54 | mapping_path = os.path.abspath(os.path.join(bids_path,'code','sovabids','mappings.yml')) # The mapping file that will hold the results of applying the rules to each file 55 | 56 | print('source_path:',source_path.replace(REPO_PATH,'')) 57 | print('bids_path:', bids_path.replace(REPO_PATH,'')) 58 | print('rules_path:',rules_path.replace(REPO_PATH,'')) 59 | print('mapping_path:',mapping_path.replace(REPO_PATH,'')) 60 | 61 | #%% 62 | # Cleaning the output directory 63 | # ----------------------------- 64 | # We will clean the output path as a safety measure from previous conversions. 65 | 66 | try: 67 | shutil.rmtree(bids_path) 68 | except: 69 | pass 70 | 71 | #%% 72 | # The input directory 73 | # ------------------- 74 | # For clarity purposes we will print here the directory we are trying to convert to BIDS. 75 | 76 | print_dir_tree(source_path) 77 | 78 | 79 | 80 | #%% 81 | # RPC API 82 | # ------- 83 | # Simulating ourselves as clients 84 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 85 | # We will use the TestClient class to send requests to the API (the sovapp variable) 86 | 87 | client = TestClient(sovapp) 88 | 89 | #%% 90 | # The general request 91 | # ^^^^^^^^^^^^^^^^^^^ 92 | # We will define a function to make a request to the API 93 | # given the name of the method and its parameters as a dictionary 94 | 95 | def make_request(method,params): 96 | print('Method:',method) 97 | print('');print(''); 98 | print('Parameters:') 99 | print(json.dumps(params, indent=4)) 100 | print('');print(''); 101 | # We create the complete request 102 | request= { 103 | "jsonrpc": "2.0", 104 | "id": 0, 105 | "method": method, 106 | "params": params} 107 | print('Request:') 108 | print(json.dumps(request, indent=4)) 109 | print('');print(''); 110 | # json dumps is important to avoid parsing errors in the API request 111 | request = json.dumps(request) 112 | 113 | # Send the request 114 | request_url = "/api/sovabids/" +method 115 | print('Request URL:') 116 | print(request_url) 117 | print('');print(''); 118 | response = client.post(request_url,data=request ) # POST request as common in RPC-based APIs 119 | 120 | # Get the answer 121 | result = json.loads(response.content.decode())['result'] 122 | print('Answer:') 123 | print(json.dumps(result, indent=4)) 124 | print('');print(''); 125 | return result 126 | 127 | #%% 128 | # load_rules 129 | # ^^^^^^^^^^ 130 | # For loading a yaml rules file. 131 | # Lets see the docstring of this method 132 | 133 | print(sovarpc.load_rules.__doc__) 134 | 135 | #%% 136 | # Lets define the request 137 | 138 | method = 'load_rules' # Just a variable for the method name 139 | 140 | params = { # Parameters of the method 141 | "rules_path": rules_path 142 | } 143 | 144 | #%% 145 | # And proceed with it 146 | 147 | result = make_request(method,params) 148 | 149 | rules = copy.deepcopy(result) 150 | 151 | #%% 152 | # save_rules 153 | # ^^^^^^^^^^ 154 | # We can for example use it as a way to save a backup of the already-existing rules file. 155 | # Lets see the docstring of this method 156 | print(sovarpc.save_rules.__doc__) 157 | 158 | #%% 159 | # Lets define the request 160 | method = "save_rules" # Just a variable for the method name 161 | 162 | params = { # Parameters of the method 163 | "rules": rules, 164 | "path": mapping_path.replace('mappings','rules')+'.bkp' # We will do it as if we were saving a backup of the rules 165 | # Since the rules file already exists 166 | } 167 | 168 | #%% 169 | # And proceed with it 170 | result = make_request(method,params) 171 | 172 | #%% 173 | # get_files 174 | # ^^^^^^^^^ 175 | # Useful for getting the files on a directory. 176 | # Lets see the docstring of this method 177 | print(sovarpc.get_files.__doc__) 178 | 179 | #%% 180 | # .. note:: 181 | # 182 | # get_files uses the rules because of the non-bids.eeg_extension configuration. 183 | 184 | 185 | #%% 186 | # Lets define the request 187 | method = "get_files" # Just a variable for the method name 188 | 189 | params = { # Parameters of the method 190 | "rules": rules, 191 | "path": source_path 192 | } 193 | 194 | #%% 195 | # And proceed with it 196 | result = make_request(method,params) 197 | 198 | filelist = copy.deepcopy(result) 199 | 200 | #%% 201 | # apply_rules_to_single_file 202 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^ 203 | # We can use this to get a mapping for a single mapping 204 | # and for previewing the bids files that would be written. 205 | # Lets see the docstring of this method 206 | print(sovarpc.apply_rules_to_single_file.__doc__) 207 | 208 | #%% 209 | # Lets define the request 210 | method = "apply_rules_to_single_file" # Just a variable for the method name 211 | 212 | params = { # Parameters of the method 213 | "file": filelist[0], 214 | "bids_path": bids_path+'.preview', 215 | "rules": rules, 216 | "write":False, 217 | "preview":True 218 | } 219 | 220 | #%% 221 | # And proceed with it 222 | result = make_request(method,params) 223 | 224 | 225 | #%% 226 | # apply_rules 227 | # ^^^^^^^^^^^ 228 | # We can use this to get the mappings for all the files in a list of them. 229 | # Lets see the docstring of this method 230 | print(sovarpc.apply_rules.__doc__) 231 | 232 | #%% 233 | # Lets define the request 234 | method = "apply_rules" # Just a variable for the method name 235 | 236 | params = { # Parameters of the method 237 | "file_list": filelist, 238 | "bids_path": bids_path, 239 | "rules": rules, 240 | "mapping_path":mapping_path 241 | } 242 | 243 | #%% 244 | # And proceed with it 245 | result = make_request(method,params) 246 | 247 | file_mappings=copy.deepcopy(result) 248 | 249 | #%% 250 | # save_mappings 251 | # ^^^^^^^^^^^^^ 252 | # We can use this to save a backup of the mappings. 253 | # Lets see the docstring of this method 254 | print(sovarpc.save_mappings.__doc__) 255 | 256 | #%% 257 | # Lets define the request 258 | method = "save_mappings" # Just a variable for the method name 259 | 260 | params = { # Parameters of the method 261 | "general": file_mappings['General'], 262 | "individual":file_mappings['Individual'], 263 | "path": mapping_path+'.bkp' 264 | } 265 | 266 | #%% 267 | # And proceed with it 268 | result = make_request(method,params) 269 | 270 | #%% 271 | # convert_them 272 | # ^^^^^^^^^^^^ 273 | # We can use this to perform the conversion given the mappings. 274 | # Lets see the docstring of this method 275 | print(sovarpc.convert_them.__doc__) 276 | 277 | #%% 278 | # Lets define the request 279 | method = "convert_them" # Just a variable for the method name 280 | 281 | params = { # Parameters of the method 282 | "general": file_mappings['General'], 283 | "individual":file_mappings['Individual'] 284 | } 285 | 286 | #%% 287 | # And proceed with it 288 | result = make_request(method,params) 289 | 290 | 291 | #%% 292 | # Checking the conversion 293 | # ----------------------- 294 | # For clarity purposes we will check the output directory we got from sovabids. 295 | 296 | print_dir_tree(bids_path) 297 | 298 | print('LEMON CONVERSION FINISHED!') 299 | 300 | #%% 301 | # The ideal GUI for the designed API 302 | # ---------------------------------- 303 | # Here is the GUI schematic we had in mind when we designed the API 304 | # 305 | # .. image:: https://user-images.githubusercontent.com/36543115/125894264-9e1bd421-41e2-444b-adcb-ecf11e81d1a0.png 306 | # :alt: Ideal GUI 307 | # 308 | # .. warning:: 309 | # 310 | # The only difference is that apply_rules will receive a list of the paths of the files we want to convert rather than a single input directory path 311 | # -------------------------------------------------------------------------------- /front/app/download_zip.py: -------------------------------------------------------------------------------- 1 | import os 2 | import zipfile 3 | from io import BytesIO 4 | from flask import send_file 5 | from flask import Response 6 | 7 | def download_files(list_files,filename): 8 | zip_subdir = "archivos" 9 | #s = BytesIO() 10 | zf = zipfile.ZipFile(filename, "w") 11 | 12 | for fpath in list_files: 13 | zip_path = fpath.split("_convert\\")[-1] 14 | zf.write(fpath, zip_path) 15 | 16 | # Must close zip for all contents to be written 17 | zf.close() 18 | name = 'bids-converted_dataset.zip' 19 | #response = Response(s.getvalue(), content_type = "application/x-zip-compressed") 20 | #response['Content-Disposition'] = 'attachment; filename= "%s"' % filename 21 | 22 | # See https://stackoverflow.com/a/53390515/14068216 23 | response = send_file(filename, mimetype="application/x-zip-compressed", download_name=name, as_attachment=True) 24 | response.headers["x-filename"] = name 25 | response.headers["Access-Control-Expose-Headers"] = 'x-filename' 26 | # # ..and correct content-disposition 27 | # response['Content-Disposition'] = 'attachment; filename=%s' % zip_filename 28 | 29 | return response 30 | -------------------------------------------------------------------------------- /front/app/forms.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/front/app/forms.py -------------------------------------------------------------------------------- /front/app/rules.yaml: -------------------------------------------------------------------------------- 1 | channels: 2 | name: 3 | '0': ECG_CHAN 4 | '1': EOG_CHAN 5 | type: 6 | ECG_CHAN: ECG 7 | EOG_CHAN: EOG 8 | dataset_description: 9 | Authors: 10 | - A1 11 | - A2 12 | Name: Dummy 13 | non-bids: 14 | code_execution: 15 | - print('some good code') 16 | - print(raw.info) 17 | - print(some bad code) 18 | eeg_extension: .vhdr 19 | path_analysis: 20 | fields: 21 | - entities.task 22 | - entities.session 23 | - entities.subject 24 | - entities.acquisition 25 | - entities.run 26 | pattern: T(.+)\/S(.+)\/sub(.+)_(.+)_(.+).vhdr 27 | sidecar: 28 | EEGReference: FCz 29 | PowerLineFrequency: 50 30 | SoftwareFilters: 31 | Anti-aliasing filter: 32 | Roll-off: 6dB/Octave 33 | half-amplitude cutoff (Hz): 500 -------------------------------------------------------------------------------- /front/app/rules2.yaml: -------------------------------------------------------------------------------- 1 | channels: 2 | name: 3 | '0': ECG_CHAN 4 | '1': EOG_CHAN 5 | type: 6 | ECG_CHAN: ECG 7 | EOG_CHAN: EOG 8 | dataset_description: 9 | Authors: 10 | - A1 11 | - A2 12 | Name: Dummy 13 | sidecar: 14 | EEGReference: FCz 15 | PowerLineFrequency: 50 16 | SoftwareFilters: 17 | Anti-aliasing filter: 18 | Roll-off: 6dB/Octave 19 | half-amplitude cutoff (Hz): 500 -------------------------------------------------------------------------------- /front/app/static/upload_files.css: -------------------------------------------------------------------------------- 1 | .inputfile { 2 | width: 0.1px; 3 | height: 0.1px; 4 | opacity: 0; 5 | overflow: hidden; 6 | position: absolute; 7 | z-index: -1; 8 | } 9 | 10 | .inputfile + label { 11 | cursor: pointer; /* "hand" cursor */ 12 | } -------------------------------------------------------------------------------- /front/app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 14 | {% endblock %} 15 | 16 | 17 | 30 | {% block content %} 31 | {% endblock %} 32 | 38 | 39 | -------------------------------------------------------------------------------- /front/app/templates/convert.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block nav_rules %}active{% endblock %} 4 | 5 | {% block content %} 6 |

Conversion

7 |
8 | 9 |
10 | {% endblock %} -------------------------------------------------------------------------------- /front/app/templates/ejemplos/base.css: -------------------------------------------------------------------------------- 1 | .nav-tabs .nav-item .nav-link { 2 | background-color: #0080FF; 3 | color: #FFF; 4 | } 5 | 6 | .nav-tabs .nav-item .nav-link.active { 7 | color: #0080FF; 8 | } 9 | 10 | .tab-content { 11 | border: 1px solid #dee2e6; 12 | border-top: transparent; 13 | padding: 15px; 14 | } 15 | 16 | .tab-content .tab-pane { 17 | background-color: #FFF; 18 | color: #0080FF; 19 | min-height: 200px; 20 | height: auto; 21 | } -------------------------------------------------------------------------------- /front/app/templates/ejemplos/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 30 | 31 | 37 | 38 | -------------------------------------------------------------------------------- /front/app/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block nav_rules %}active{% endblock %} 4 | 5 | {% block content %} 6 |

ERROR!

7 | 8 |

The server response was:

9 | 10 |
11 | 12 | 13 | 14 |

15 | {{ error }} 16 |

17 |
18 | 19 |

Send the following traceback to the github issues

20 | 21 |
22 |

23 | Would be helpful to also include the context where the problem arised. For example the steps you did to arrive to the error, the kind of data you are using, etc. 24 |

25 |
26 | 27 |
28 |

29 | {{ details }} 30 |

31 |
32 | 33 | {% endblock %} -------------------------------------------------------------------------------- /front/app/templates/exclude_files.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block nav_exclude %}active{% endblock %} 4 | 5 | {% block content %} 6 | 7 | UNSELECT the files you want to exclude from the conversion: 8 | 9 |
10 |
    11 | {% for file in filenames %} 12 |
    13 | {% endfor %} 14 |
15 | 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /front/app/templates/files.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block nav_exclude %}active{% endblock %} 4 | 5 | {% block content %} 6 | The following files will be converted to BIDS: 7 | 8 |
9 |
    10 | {% for file in filenames %} 11 |
  • {{ file }}
  • 12 | {% endfor %} 13 |
14 | 15 |
16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /front/app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% set active_page = "index" %} 3 | 4 | {% block content %} 5 |

sovabids

6 |

Visit the documentation

7 |
    8 |
  • sovabids is a python package for automating eeg2bids conversion.
  • 9 |
  • 10 | sovabids can be used through: 11 |
      12 |
    1. its python API
    2. 13 |
    3. its CLI entry points.
    4. 14 |
    5. its JSON-RPC entry points (needs a server running the backend).
    6. 15 |
    16 |
    17 |
  • 18 |
19 |

Architecture

20 |
21 |
The main elements of sovabids are:
22 |
    23 |
  • A source path with the original dataset.
  • 24 |
  • A bids path that will be the output path of the conversion.
  • 25 |
  • A rules file that configures how the conversion is done from the general perspective.
  • 26 |
  • A mapping file that encodes how the conversion is performed to each individual file of the dataset.
  • 27 |
28 |
29 |
30 |

image

31 | 32 | {% endblock %} -------------------------------------------------------------------------------- /front/app/templates/individual_rules.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block nav_rules %}active{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
    9 | {% for k, v in files.items() %} 10 |
    15 | {% endfor %} 16 |
17 | 18 |
19 |
20 |
21 |
22 |

Modifying individual record mapping: {{ file }}

23 |
24 | 25 | 26 | 34 | 37 | 38 | 39 |
40 | 41 |
42 | 43 | 44 |
45 | 46 |
47 |
48 |
49 |

Ready?

50 |
51 | 52 | 53 |
54 | 55 |
56 | 57 |
58 |
59 | {% endblock %} -------------------------------------------------------------------------------- /front/app/templates/load_rules.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block nav_rules %}active{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |

9 | 10 | 13 |

14 |
15 |

16 | 17 |

18 |
19 |
20 |
21 |

22 | 23 |

24 |
25 |

26 | 27 |

28 |
29 | 30 | {% endblock %} -------------------------------------------------------------------------------- /front/app/templates/ready.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block nav_rules %}active{% endblock %} 4 | 5 | {% block content %} 6 |

YOUR FILES ARE READY!

7 | Download files 8 |

NOTICE: For linux-based systems the downloaded zip will have the files stored under a nested subdirectory structure.

9 | {% endblock %} -------------------------------------------------------------------------------- /front/app/templates/upload_files.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | {% endblock %} 7 | 8 | {% block nav_upload %}active{% endblock %} 9 | 10 | {% block content %} 11 |

Select file(s) to upload

12 |

If necessary, use https://prerau.bwh.harvard.edu/edf-de-identification-tool/ for deidentification

13 |

14 | {% with messages = get_flashed_messages() %} 15 | {% if messages %} 16 |

    17 | {% for message in messages %} 18 |
  • {{ message }}
  • 19 | {% endfor %} 20 |
21 | {% endif %} 22 | {% endwith %} 23 |

24 |
25 |
26 |

27 | 28 | 29 | 30 | 34 |

35 |
36 |

37 | 38 |

39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /front/app/utils.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | def level_key(data, parent, level=0, stored=[]): 4 | if isinstance(data, dict): 5 | level+=1 6 | # print(f'{data} is dict, level: {level}') 7 | for key in data.keys(): 8 | if isinstance(data[key], dict): 9 | 10 | #print(f'{data[key]} is dict') 11 | stored = level_key(data[key], parent=key, level=level) 12 | else: 13 | datos = {'name': key, 'value': data[key], 'level': level, 'parent': parent} 14 | stored.append(datos) 15 | return stored 16 | return stored 17 | 18 | 19 | # file = open('rules.yaml') 20 | # data = yaml.load(file, Loader=yaml.FullLoader) 21 | # stored = [] 22 | # for key in data.keys(): 23 | # if isinstance(data[key], dict): 24 | # stored = level_key(data[key], parent=key, stored=stored) 25 | # print(stored) 26 | 27 | def create_file(): 28 | for i in range(20): 29 | file = open(f'./media/{i}.txt', 'w') 30 | file.close() 31 | 32 | create_file() 33 | 34 | 35 | -------------------------------------------------------------------------------- /front/requirements.txt: -------------------------------------------------------------------------------- 1 | flask -------------------------------------------------------------------------------- /gsoc_proposal.md: -------------------------------------------------------------------------------- 1 | # GSoC Proposal 2 | 3 | SOVABIDS: A python package for the automatic conversion of MEG/EEG datasets to the BIDS standard, with a focus on making the most out of metadata 4 | 5 | ## Abstract 6 | 7 | BIDS is a standard for neuroimaging datasets that helps with data sharing and reusability; as a result it has been widely adopted by the community. Although developed for MRI originally, it has gained popularity within the EEG and MEG realms. Converting raw data to the BIDS standard is not difficult but requires a lot of time if done by hand. Currently software like mne-bids and Biscuit are available to assist and facilitate this conversion but there is still no automated way to produce a valid BIDS dataset for the EEG and MEG use-cases. Mostly what is missing is metadata. Here a python package is proposed to be able to infer the missing information from files accompanying the EEG and MEG files and/or a single bids-conversion example provided by the user. The idea of having simple human-readable configuration files is also explored since this would help the sharing of common conversion parameters within similar communities. If this proposal was successfully implemented then batch conversion of BIDS datasets in the EEG and MEG cases would be realized. Moreover, since the design is constrained to simple configuration files, the software could potentially expand BIDS adoption to people not experienced with scripting. It is hoped that this package becomes the backend of an hypothetical web application that assists the conversion of MEG-EEG datasets to BIDS. 8 | 9 | ## Problem Statement 10 | 11 | Data is a fundamental asset for science, sadly it is usually stored in such a way that only the original researcher knows how to make use of it; the neuroimaging community is no stranger to this phenomenon. As the accumulation and sharing of data becomes more important following the data-intensive needs of neuroimaging, it has become critical the existence of an unified way to store data among researchers. One of the solutions to this is the BIDS standard for neuroimaging data, originally developed for MRI. As the BIDS adoption increases, it expands its range of application; in particular the MEG and EEG extensions are becoming more popular in their respective communities. 12 | 13 | In essence, BIDS is a specification that sets how data is saved on the storage medium: its folder structure, the filenames, the metadata format, etc. The BIDS standard is simple to understand but doing the conversion from a raw source may demand a lot of time. Luckily semi-automatic and automatic converters have been developed to address this, although mainly for MRI. As of now for MEG and EEG conversion some software packages have been developed: MNE-BIDS (working mainly by python scripts), Biscuit (GUI-based converter done in python), some plugins for EEGLAB, FieldTrip and SPM. and some manufacturer specific converters (BrainVision, for example). Most of these softwares will infer the technical characteristics to populate the BIDS dataset but will require the user to input the non-technical metadata (in example subjects ids, demographic characteristics,etc) through either scripting or GUI interfaces. The previous limitation makes the process of the -to BIDS- conversion complex and not completely automatic. In a lot of cases the information missing is available through metadata files, only needing a way for them to be read and incorporated into the already extracted technical information. In general the user ends up inputting a lot of information by hand or he needs to do scripts particular for his use-case. Since a lot of the MEG and EEG researchers don’t have a programming background this task will end up being manual. Moreover, the path containing the files usually encodes study design information, which is not usually used to improve the automation. MNE-BIDS for example will require inputting the subject,session and run through a python script, BISCUITS will require the user to set some of these parameters manually through a GUI and so on. 14 | 15 | Another improvement that could be made for automatic “MEG/EEG datasets to BIDS” conversion is to have a file that encodes the conversion parameters, so that they can be easily shared within a community with the same storage pattern. In addition, some individual MEG/EEG records may have particular characteristics not shared between all the records, thus requiring another file that characterizes the conversion for a particular case. The two files mentioned previously should be simple and intuitive enough so that people not experienced with scripting and programming can make use of the software. Similarly, the learning curve of the software should be low enough for people to adopt it. 16 | 17 | Motivated by the previous discussion, the proposed solution mainly addresses the following questions: 18 | 19 | - How can we infer as much as we can from the metadata files available and from a single bids conversion example provided by the user?. 20 | - How can we encode the information inferred previously in a simple and intuitive human-readable way? 21 | - How can we encode in a file the “to-bids” mapping that setups the conversion of a single record ? 22 | 23 | ## Proposed Solution 24 | 25 | The following schematic illustrates the overall design of the proposed software: 26 | 27 | ![sovabids](https://raw.githubusercontent.com/yjmantilla/yjmantilla.github.io/master/images/sovabids.png) 28 | 29 | The software is designed to work with EEG and MEG files, but because of the time constraints of the program, the GSoC project will focus on the conversion of EEG files from (at least) two different EEG system vendors. Moreover, the GUI of the “user-supervised adjustment loop”, although designed to be in the final version, won’t be developed during GSoC because of the same reason. 30 | 31 | The three main modules illustrated previously are expanded accordingly: 32 | 33 | ### “Discover Rules” Module 34 | 35 | This module is in charge of obtaining a “Translation Rules File” that describes the general rules for the conversion of the MEG/EEG dataset. For that it will infer characteristics from two sources: 36 | 37 | - A single example of a successful bids conversion provided by the user. 38 | - Metadata files available. 39 | 40 | From the structural point of view, the metadata files contemplated here are: 41 | 42 | - Manufacturer-Specific 43 | - Tabular Files 44 | - Dictionary Files 45 | 46 | Internally, this module is made out of functions that will apply different heuristics to get the information of interest. 47 | 48 | From the functional point of view two types of metadata arise: 49 | 50 | - Study Design Metadata 51 | - Technical Metadata of the Acquisition 52 | 53 | Researchers are usually accustomed to the study design details, whereas the technical metadata may be known by a technical user. It is possible that either of these two users would want to set directly some parameters rather than them to be guessed. Because of this, the module should have a way to set directly some details rather than guess them. 54 | 55 | ### “Apply Rules” Module 56 | 57 | This module interprets the “Translation Rules” file and outputs a “Individual Record Mapping” file. This is mainly applying the rules of the “Translation Rules File” to a single record. 58 | 59 | Power-users may produce the “Translation Rules” file on their own if they wish to do so; thus skipping the “Discover Rules” module. This would be possible since the file is designed to be simple and intuitive syntax. 60 | 61 | Since the “guessed” rules from the previous module are not necessarily correct in all cases, an user-supervised adjustment loop powered by a GUI is included in the design of the software. This GUI should show the user the hypothetical results of the conversion and offer a way to change the parameters so that the user gets the expected results. The changes should be applied on the “Translation Rule” if the change is a general one or on the “Individual Record Mapping” for record-wise changes. 62 | 63 | The “Apply Rules” Module should plug into the mentioned GUI through an API so as to not limit the visualization possibilities of the package. That is, during this GSoC, an effort will be made to implement the necessary API to achieve this rather than on a GUI. 64 | 65 | ### “Convert Them” Module 66 | 67 | This module grabs the “Individual Record Mapping” file (or a collection of them) and performs the conversion using the mne-bids package. A way to override the inferences done by the mne-bids package module should be applied if the user specifies to do so. 68 | 69 | ## Deliverables 70 | 71 | Python package that contains the following modules: 72 | 73 | - “Discover Rules” Module. 74 | - “Apply Rules” Module. 75 | - “Convert Them” Module. 76 | - “Translation Rules” File Schema 77 | - “Individual Record Mapping” File Schema 78 | - Use-case examples illustrating the conversion process of files from 2 different EEG System Vendors 79 | - Documentation. 80 | 81 | ## Community Impact 82 | 83 | If this software was successfully developed it would contribute to the adoption of the BIDS standard in EEG and MEG communities that don’t have a scripting/programming background. It would allow the automatic conversion of EEG/MEG datasets on servers since the conversion is controlled by configuration files. These same configurations files open the possibility for exchange of conversion mappings between communities that have a similar storage strategy. 84 | 85 | ## Suitability for the project 86 | 87 | I have some experience working with EEG data and how it is handled by BIDS. This experience was mostly acquired through an internship in a research group of the University Of Antioquia called “Neuropsychology and Behaviour Group” (GRUNECO). There I learned mostly resting EEG processing. Since the research group was (and is) trying to standardize their datasets I learned about the BIDS standard for EEG; coincidentally, I worked with a partner in this group to make a proof-of-concept software that does the conversion of a raw dataset to the BIDS standard. Although this software was a rudimentary initial design, it tackled problems similar to those explored in this proposal. I also have experience with python and in particular I contributed to some bug fixing and refactoring in the open source package [“pyprep”](https://github.com/sappelhoff/pyprep). Through these last efforts I acquired some knowledge about the mne package workings. As a result of these experiences I have knowledge of the python language, the EEG files, the BIDS ecosystem and the mne package library; all of these are essential for the execution of this project. 88 | 89 | Apart from the previous, I also collaborated with my mentor Oren Civier to build this proposal: starting from the initial conversations through the NeuroStars forum up to using google docs to collaborate. 90 | 91 | ## Timeline 92 | 93 | Note: During the course of GSoC I will be attending my university classes. 94 | 95 | This timeline provides a rough guideline of how the project will be done. 96 | 97 | ### 17 - 23 May 98 | 99 | Exploration of common EEG metadata formats. 100 | 101 | ### 24 - 30 May 102 | 103 | Learning of the BIDS specification for EEG. 104 | 105 | ### 31 May - 6 June 106 | 107 | Familiarisation with the mne-bids package to produce BIDS datasets. 108 | 109 | ### 7 - 27 June 110 | 111 | Design of the “Translation Rules” file schema that defines the conversion process of the dataset from a general point of view. 112 | 113 | Deliverables: 114 | 115 | - “Translation Rules” File Schema 116 | 117 | ### 21 June - 4 July 118 | 119 | Design of the “Individual Record Mapping” file schema and development of the “Apply Rules” module that outputs mapping files for each record. 120 | 121 | Deliverables: 122 | 123 | - “Individual Record Mapping” File Schema 124 | - “Apply Rules” Module 125 | 126 | ### 28 June - 25 July 127 | 128 | Development of the “Discover Rules” module with functions for each metadata file format explored and for the analysis of a single bids-conversion example provided by the user. 129 | 130 | Deliverables: 131 | 132 | - “Discover Rules” Module 133 | 134 | ### 19 July - 1 August 135 | 136 | Development of a module in charge of obtaining, through the mapping file, the specific parameters to be passed to the mne-bids package. This will store the data in the structure specified by the BIDS standard. 137 | 138 | Deliverables: 139 | 140 | - “Convert Records” Module 141 | 142 | ### 2 - 8 August 143 | 144 | Testing and correction of possible errors found and development conversion examples. 145 | 146 | Deliverables: 147 | 148 | - Examples illustrating the conversion process of files from 2 different EEG System Vendors 149 | 150 | ### 9 - 15 August 151 | 152 | Documentation of the software. 153 | 154 | Deliverables: 155 | 156 | - Documentation 157 | -------------------------------------------------------------------------------- /paper/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/paper/arch.png -------------------------------------------------------------------------------- /paper/main-use.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/paper/main-use.png -------------------------------------------------------------------------------- /paper/rules-mappings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/paper/rules-mappings.png -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | bids_validator 4 | sphinx>=4.0.0 5 | insegel 6 | furo 7 | sphinx_gallery 8 | sphinx-copybutton 9 | sphinxcontrib-mermaid 10 | sphinx-autoapi 11 | myst-parser 12 | flask 13 | uvicorn 14 | fastapi 15 | fastapi-jsonrpc 16 | versioneer 17 | pybv >= 0.7.3 18 | pyyaml 19 | httpx 20 | mne-bids 21 | pydantic 22 | git+https://github.com/yjmantilla/bidscoin.git@sovabids 23 | -e . 24 | -------------------------------------------------------------------------------- /requirements-gui.txt: -------------------------------------------------------------------------------- 1 | bids_validator 2 | flask 3 | uvicorn 4 | fastapi 5 | fastapi-jsonrpc 6 | pybv >= 0.7.3 7 | httpx 8 | mne-bids 9 | pyyaml 10 | git+https://github.com/yjmantilla/bidscoin.git@sovabids 11 | . 12 | -------------------------------------------------------------------------------- /requirements-user.txt: -------------------------------------------------------------------------------- 1 | bids_validator 2 | pybv >= 0.7.3 3 | mne-bids 4 | pyyaml 5 | . 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pybv >= 0.7.3 3 | pyyaml 4 | pandas 5 | fastapi 6 | fastapi-jsonrpc -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = sovabids 3 | url = https://github.com/yjmantilla/sovabids/ 4 | author = sovabids developers 5 | maintainer = Yorguin Mantilla 6 | maintainer_email = yjmantilla@gmail.com 7 | description = Automated eeg2bids conversion 8 | long-description = file: README.rst 9 | long-description-content-type = text/x-rst; charset=UTF-8 10 | keywords = EEG eeg bids mne mne-bids conversion 11 | platforms = any 12 | classifiers = 13 | Topic :: Scientific/Engineering 14 | Programming Language :: Python :: 3 15 | Intended Audience :: Science/Research 16 | Development Status :: 3 - Alpha 17 | Operating System :: POSIX :: Linux 18 | Operating System :: Unix 19 | Operating System :: MacOS 20 | Operating System :: Microsoft :: Windows 21 | project_urls = 22 | Documentation = https://sovabids.readthedocs.io/en/latest/ 23 | Bug Reports = https://github.com/yjmantilla/sovabids/issues 24 | Source = https://github.com/yjmantilla/sovabids 25 | 26 | [options] 27 | python_requires = >= 3.8 28 | packages = find: 29 | include_package_data = True 30 | 31 | [options.packages.find] 32 | exclude = 33 | tests 34 | front 35 | 36 | [coverage:run] 37 | omit = 38 | # Do not include test script in coverage report 39 | *tests* 40 | setup.py 41 | sovabids/_version.py 42 | 43 | [versioneer] 44 | VCS = git 45 | style = pep440 46 | versionfile_source = sovabids/_version.py 47 | versionfile_build = sovabids/_version.py 48 | tag_prefix = 49 | parentdir_prefix = 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import versioneer 3 | 4 | REQUIREMENTS = [i.strip() for i in open("requirements-user.txt").readlines()][:-1] # ignore last "." 5 | 6 | 7 | # Give setuptools a hint to complain if it's too old a version 8 | # 30.3.0 allows us to put most metadata in setup.cfg 9 | # 38.3.0 contains most setup.cfg bugfixes 10 | # Should match pyproject.toml 11 | SETUP_REQUIRES = ["setuptools >= 38.3.0"] 12 | 13 | 14 | setuptools.setup( 15 | setup_requires=SETUP_REQUIRES, 16 | install_requires = REQUIREMENTS, 17 | entry_points = {'console_scripts':[ 18 | 'sovapply = sovabids.rules:sovapply', 19 | 'sovaconvert = sovabids.convert:sovaconvert' 20 | ]}, 21 | version=versioneer.get_version(), 22 | cmdclass=versioneer.get_cmdclass(), 23 | ) 24 | -------------------------------------------------------------------------------- /sovabids/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from . import _version 3 | __version__ = _version.get_versions()['version'] 4 | -------------------------------------------------------------------------------- /sovabids/bids.py: -------------------------------------------------------------------------------- 1 | """Module with bids utilities.""" 2 | import os 3 | import json 4 | 5 | from mne_bids.utils import _write_json 6 | 7 | def update_dataset_description(dataset_description,bids_path,do_not_create=False): 8 | """Update the dataset_description.json located at bids_path given a dictionary. 9 | 10 | If it exist, updates with the new given values. If it doesn't exist, then creates it. 11 | 12 | Parameters 13 | ---------- 14 | 15 | dataset_description : dict 16 | The dataset_description dictionary to update with, following the schema of the dataset_description.json file of bids. 17 | bids_path : str 18 | The bids_path of the dataset description file, basically the folder where the file is. 19 | do_not_create : bool 20 | If true, does not create the file if it does not exist. 21 | """ 22 | jsonfile = os.path.join(bids_path,'dataset_description.json') 23 | os.makedirs(bids_path,exist_ok=True) 24 | if os.path.isfile(jsonfile): 25 | with open(jsonfile) as f: 26 | info = json.load(f) 27 | else: 28 | info = {} 29 | if dataset_description != {}: 30 | info.update(dataset_description) 31 | if not do_not_create and os.path.isfile(jsonfile): 32 | _write_json(jsonfile,info,overwrite=True) 33 | # Problem: Authors with strange characters are written incorrectly. -------------------------------------------------------------------------------- /sovabids/convert.py: -------------------------------------------------------------------------------- 1 | """Module to perform the conversions. 2 | """ 3 | import argparse 4 | import os 5 | 6 | import logging 7 | from sovabids.dicts import deep_get 8 | from sovabids.rules import load_rules,apply_rules_to_single_file 9 | from sovabids.bids import update_dataset_description 10 | from sovabids.loggers import setup_logging 11 | from sovabids.settings import SECTION_STRING 12 | 13 | LOGGER = logging.getLogger(__name__) 14 | 15 | def convert_them(mappings_input): 16 | """Convert eeg files to bids according to the mappings given. 17 | 18 | Parameters 19 | ---------- 20 | mappings_input : str|dict 21 | The path to the mapping file or the mapping dictionary: 22 | { 23 | 'General': dict with the general rules, 24 | 'Individual': list of dicts with the individual mappings of each file. 25 | } 26 | 27 | Returns 28 | ------- 29 | None 30 | """ 31 | 32 | # Loading Mappings 33 | mappings = load_rules(mappings_input) 34 | mapping_file = mappings_input if isinstance(mappings_input,str) else None 35 | 36 | # Verifying Mappings 37 | assert 'Individual' in mappings,f'`Individual` does not exist in the mapping dictionary' 38 | assert 'General' in mappings,f'`General` does not exist in the mapping dictionary' 39 | 40 | # Getting input,output and log path 41 | bids_path = mappings['General']['IO']['target'] 42 | source_path = mappings['General']['IO']['source'] 43 | log_file = os.path.join(bids_path,'code','sovabids','sovabids.log') 44 | 45 | # Setup the logging 46 | setup_logging(log_file) 47 | LOGGER.info('') 48 | LOGGER.info(SECTION_STRING + ' START CONVERT_THEM ' + SECTION_STRING) 49 | LOGGER.info(f"source_path={source_path} bids_path={bids_path} mapping_file={str(mapping_file)} ") 50 | 51 | LOGGER.info(f"Converting Individual Mappings") 52 | num_files = len(mappings['Individual']) 53 | for i,mapping in enumerate(mappings['Individual']): 54 | input_file=deep_get(mapping,'IO.source',None) 55 | output_file=deep_get(mapping,'IO.target',None) 56 | try: 57 | 58 | LOGGER.info(f"File {i+1} of {num_files} ({(i+1)*100/num_files}%) : {input_file}") 59 | if not os.path.isfile(output_file): 60 | apply_rules_to_single_file(input_file,mapping,bids_path,write=True) 61 | else: 62 | LOGGER.info(f'{output_file} already existed. Skipping...') 63 | except : 64 | LOGGER.exception(f'Error for {input_file}') 65 | 66 | LOGGER.info(f"Conversion Done!") 67 | 68 | LOGGER.info(f"Updating Dataset Description") 69 | 70 | # Grab the info from the last file to make the dataset description 71 | if 'dataset_description' in mappings['General']: 72 | dataset_description = mappings['General']['dataset_description'] 73 | update_dataset_description(dataset_description,bids_path) 74 | 75 | LOGGER.info(f"Dataset Description Updated!") 76 | 77 | LOGGER.info(SECTION_STRING + ' END CONVERT_THEM ' + SECTION_STRING) 78 | 79 | 80 | def sovaconvert(): 81 | """Console script usage for conversion.""" 82 | # see https://github.com/Donders-Institute/bidscoin/blob/master/bidscoin/bidsmapper.py for example of how to make this 83 | parser = argparse.ArgumentParser() 84 | subparsers = parser.add_subparsers() 85 | 86 | parser = subparsers.add_parser('convert_them') 87 | parser.add_argument('mappings',help='The mapping file of the conversion.') 88 | args = parser.parse_args() 89 | convert_them(args.mappings) 90 | 91 | if __name__ == "__main__": 92 | sovaconvert() 93 | -------------------------------------------------------------------------------- /sovabids/datasets.py: -------------------------------------------------------------------------------- 1 | """Module with dataset utilities. 2 | """ 3 | import os 4 | from pandas import read_csv 5 | import shutil 6 | from sovabids.files import download,_get_files 7 | from sovabids.misc import get_num_digits 8 | from sovabids.parsers import parse_from_regex 9 | import mne 10 | import numpy as np 11 | from mne_bids.write import _write_raw_brainvision 12 | import fileinput 13 | 14 | 15 | def lemon_prepare(): 16 | """Download and prepare a few files of the LEMON dataset. 17 | 18 | Notes 19 | ----- 20 | 21 | See the `LEMON dataset `_ . 22 | """ 23 | 24 | # Path Configuration 25 | 26 | this_dir = os.path.dirname(__file__) 27 | data_dir = os.path.join(this_dir,'..','_data') 28 | root_path = os.path.abspath(os.path.join(data_dir,'lemon')) 29 | os.makedirs(data_dir,exist_ok=True) 30 | 31 | # Download lemon Database 32 | 33 | urls = ['https://fcp-indi.s3.amazonaws.com/data/Projects/INDI/MPI-LEMON/Compressed_tar/EEG_MPILMBB_LEMON/EEG_Raw_BIDS_ID/sub-032301.tar.gz', 34 | 'https://fcp-indi.s3.amazonaws.com/data/Projects/INDI/MPI-LEMON/Compressed_tar/EEG_MPILMBB_LEMON/EEG_Raw_BIDS_ID/sub-032302.tar.gz', 35 | 'https://fcp-indi.s3.amazonaws.com/data/Projects/INDI/MPI-LEMON/Compressed_tar/EEG_MPILMBB_LEMON/EEG_Raw_BIDS_ID/sub-032303.tar.gz', 36 | 'https://fcp-indi.s3.amazonaws.com/data/Projects/INDI/MPI-LEMON/name_match.csv'] 37 | 38 | for url in urls: 39 | download(url,os.path.join(data_dir,'lemon')) 40 | 41 | # Generate all filepaths 42 | 43 | filepaths = _get_files(root_path) 44 | 45 | 46 | # Label Correction 47 | name_match = read_csv(os.path.join(root_path,'name_match.csv')) 48 | 49 | # Unpack files 50 | 51 | # TAR FILES 52 | tars = [x for x in filepaths if 'tar.gz' in x ] 53 | 54 | # SUBJECTS 55 | # ignore field so that it doesnt get rid of - or _ 56 | old_ids = [parse_from_regex(x,'(sub-.*?).tar.gz',['ignore']) for x in tars] 57 | old_ids = [x['ignore'] for x in old_ids] 58 | new_ids = [name_match.loc[(name_match.INDI_ID==x),'Initial_ID']._values[0] for x in old_ids] 59 | 60 | # EEG FILES 61 | not_tars = [x for x in filepaths if '.vhdr' in x ] 62 | not_tars_ids = [parse_from_regex(x,'RSEEG\\/(sub-.*?).vhdr',['id']) for x in not_tars] 63 | not_tars_ids = [x['id'] for x in not_tars_ids] 64 | 65 | 66 | assert len(tars) == len(old_ids) == len(new_ids) 67 | 68 | if set(new_ids) == set(not_tars_ids): # all done 69 | return 70 | else: 71 | for file,old,new in zip(tars,old_ids,new_ids): 72 | if not new in not_tars_ids: # skip already prepared files 73 | shutil.unpack_archive(file,root_path) 74 | olddir = os.path.join(root_path,old) 75 | subject_files = _get_files(olddir) 76 | for subfile in subject_files: # fix sub-id 77 | new_path = subfile.replace(old,new) 78 | dir,_ = os.path.split(new_path) 79 | os.makedirs(dir,exist_ok=True) 80 | shutil.move(subfile,new_path) 81 | shutil.rmtree(olddir) 82 | print('LEMON PREPARE DONE!') 83 | 84 | def lemon_bidscoin_prepare(src_path): 85 | """Download and prepare a few files of the LEMON dataset to be used with BIDSCOIN. 86 | 87 | Parameters 88 | ---------- 89 | src_path : str 90 | The path where the BIDSCOIN-ready LEMON files will be 91 | 92 | See Also 93 | -------- 94 | 95 | datasets.lemon_prepare 96 | """ 97 | lemon_prepare() 98 | this_dir = os.path.dirname(__file__) 99 | data_dir = os.path.join(this_dir,'..','_data') 100 | root_path = os.path.abspath(os.path.join(data_dir,'lemon')) 101 | bidscoin_input_path = src_path 102 | 103 | os.makedirs(bidscoin_input_path,exist_ok=True) 104 | 105 | files = _get_files(root_path) 106 | files = [x for x in files if x.split('.')[-1] in ['eeg','vmrk','vhdr'] ] 107 | 108 | files_out = [] 109 | for f in files: 110 | session = 'ses-001' 111 | task = 'resting' 112 | head,tail=os.path.split(f) 113 | sub = tail.split('.')[0] 114 | new_path = os.path.join(bidscoin_input_path,sub,session,task,tail) 115 | files_out.append(new_path) 116 | 117 | for old,new in zip(files,files_out): 118 | print(old,' to ',new) 119 | os.makedirs(os.path.split(new)[0], exist_ok=True) 120 | if not os.path.isfile(new): 121 | shutil.copy2(old,new) 122 | else: 123 | print('already done, skipping...') 124 | print('finish') 125 | 126 | def make_dummy_dataset(EXAMPLE, 127 | PATTERN='T%task%/S%session%/sub%subject%_%acquisition%_%run%', 128 | DATASET = 'DUMMY', 129 | NSUBS = 2, 130 | NSESSIONS = 2, 131 | NTASKS = 2, 132 | NACQS = 2, 133 | NRUNS = 2, 134 | PREFIXES = {'subject':'SU','session':'SE','task':'TA','acquisition':'AC','run':'RU'}, 135 | ROOT=None, 136 | ): 137 | """Create a dummy dataset given some parameters. 138 | 139 | Parameters 140 | ---------- 141 | EXAMPLE : str,PathLike|list , required 142 | Path of the file to replicate as each file in the dummy dataset. 143 | If a list, it is assumed each item is a file. All of these items are replicated. 144 | PATTERN : str, optional 145 | The pattern in placeholder notation using the following fields: 146 | %dataset%, %task%, %session%, %subject%, %run%, %acquisition% 147 | DATASET : str, optional 148 | Name of the dataset. 149 | NSUBS : int, optional 150 | Number of subjects. 151 | NSESSIONS : int, optional 152 | Number of sessions. 153 | NTASKS : int, optional 154 | Number of tasks. 155 | NACQS : int, optional 156 | Number of acquisitions. 157 | NRUNS : int, optional 158 | Number of runs. 159 | PREFIXES : dict, optional 160 | Dictionary with the following keys:'subject', 'session', 'task' and 'acquisition'. 161 | The values are the corresponding prefix. RUN is not present because it has to be a number. 162 | ROOT : str, optional 163 | Path where the files will be generated. 164 | If None, the _data subdir will be used. 165 | 166 | """ 167 | 168 | if ROOT is None: 169 | this_dir = os.path.dirname(__file__) 170 | data_dir = os.path.abspath(os.path.join(this_dir,'..','_data')) 171 | else: 172 | data_dir = ROOT 173 | os.makedirs(data_dir,exist_ok=True) 174 | 175 | sub_zeros = get_num_digits(NSUBS) 176 | subs = [ PREFIXES['subject']+ str(x).zfill(sub_zeros) for x in range(NSUBS)] 177 | 178 | task_zeros = get_num_digits(NTASKS) 179 | tasks = [ PREFIXES['task']+str(x).zfill(task_zeros) for x in range(NTASKS)] 180 | 181 | run_zeros = get_num_digits(NRUNS) 182 | runs = [str(x).zfill(run_zeros) for x in range(NRUNS)] 183 | 184 | ses_zeros = get_num_digits(NSESSIONS) 185 | sessions = [ PREFIXES['session']+str(x).zfill(ses_zeros) for x in range(NSESSIONS)] 186 | 187 | acq_zeros = get_num_digits(NACQS) 188 | acquisitions = [ PREFIXES['acquisition']+str(x).zfill(acq_zeros) for x in range(NACQS)] 189 | 190 | 191 | for task in tasks: 192 | for session in sessions: 193 | for run in runs: 194 | for sub in subs: 195 | for acq in acquisitions: 196 | dummy = PATTERN.replace('%dataset%',DATASET) 197 | dummy = dummy.replace('%task%',task) 198 | dummy = dummy.replace('%session%',session) 199 | dummy = dummy.replace('%subject%',sub) 200 | dummy = dummy.replace('%run%',run) 201 | dummy = dummy.replace('%acquisition%',acq) 202 | path = [data_dir] +dummy.split('/') 203 | fpath = os.path.join(*path) 204 | dirpath = os.path.join(*path[:-1]) 205 | os.makedirs(dirpath,exist_ok=True) 206 | if isinstance(EXAMPLE,list): 207 | for ff in EXAMPLE: 208 | fname, ext = os.path.splitext(ff) 209 | shutil.copyfile(ff, fpath+ext) 210 | if 'vmrk' in ext or 'vhdr' in ext: 211 | replace_brainvision_filename(fpath+ext,path[-1]) 212 | else: 213 | fname, ext = os.path.splitext(EXAMPLE) 214 | shutil.copyfile(EXAMPLE, fpath+ext) 215 | 216 | 217 | def generate_1_over_f_noise(n_channels, n_times, exponent=1.0, random_state=None): 218 | rng = np.random.default_rng(random_state) 219 | noise = np.zeros((n_channels, n_times)) 220 | 221 | freqs = np.fft.rfftfreq(n_times, d=1.0) # d=1.0 assumes unit sampling rate 222 | freqs[0] = freqs[1] # avoid division by zero at DC 223 | 224 | scale = 1.0 / np.power(freqs, exponent) 225 | 226 | for ch in range(n_channels): 227 | # Generate white noise in time domain 228 | white = rng.standard_normal(n_times) 229 | # Transform to frequency domain 230 | white_fft = np.fft.rfft(white) 231 | # Apply 1/f scaling 232 | pink_fft = white_fft * scale 233 | # Transform back to time domain 234 | pink = np.fft.irfft(pink_fft, n=n_times) 235 | # Normalize to zero mean, unit variance 236 | pink = (pink - pink.mean()) / pink.std() 237 | noise[ch, :] = pink 238 | 239 | return noise 240 | 241 | def get_dummy_raw(NCHANNELS = 5, 242 | SFREQ = 200, 243 | STOP = 10, 244 | NUMEVENTS = 10, 245 | ): 246 | """ 247 | Create a dummy MNE Raw file given some parameters. 248 | 249 | Parameters 250 | ---------- 251 | NCHANNELS : int, optional 252 | Number of channels. 253 | SFREQ : float, optional 254 | Sampling frequency of the data. 255 | STOP : float, optional 256 | Time duration of the data in seconds. 257 | NUMEVENTS : int, optional 258 | Number of events along the duration. 259 | """ 260 | # Create some dummy metadata 261 | n_channels = NCHANNELS 262 | sampling_freq = SFREQ # in Hertz 263 | info = mne.create_info(n_channels, sfreq=sampling_freq) 264 | 265 | times = np.linspace(0, STOP, STOP*sampling_freq, endpoint=False) 266 | data = generate_1_over_f_noise(NCHANNELS, times.shape[0], exponent=1.0) 267 | #np.zeros((NCHANNELS,times.shape[0])) 268 | 269 | raw = mne.io.RawArray(data, info) 270 | raw.set_channel_types({x:'eeg' for x in raw.ch_names}) 271 | new_events = mne.make_fixed_length_events(raw, duration=STOP//NUMEVENTS) 272 | 273 | return raw,new_events 274 | 275 | def save_dummy_vhdr(fpath,dummy_args={} 276 | ): 277 | """ 278 | Save a dummy vhdr file. 279 | 280 | Parameters 281 | ---------- 282 | fpath : str, required 283 | Path where to save the file. 284 | kwargs : dict, optional 285 | Dictionary with the arguments of the get_dummy_raw function. 286 | 287 | Returns 288 | ------- 289 | List with the Paths of the desired vhdr file, if those were succesfully created, 290 | None otherwise. 291 | """ 292 | 293 | raw,new_events = get_dummy_raw(**dummy_args) 294 | _write_raw_brainvision(raw,fpath,new_events,overwrite=True) 295 | eegpath =fpath.replace('.vhdr','.eeg') 296 | vmrkpath = fpath.replace('.vhdr','.vmrk') 297 | if all(os.path.isfile(x) for x in [fpath,eegpath,vmrkpath]): 298 | return [fpath,eegpath,vmrkpath] 299 | else: 300 | return None 301 | 302 | def save_dummy_cnt(fpath, 303 | ): 304 | """ 305 | Save a dummy cnt file. 306 | 307 | Parameters 308 | ---------- 309 | fpath : str, required 310 | Path where to save the file. 311 | 312 | Returns 313 | ------- 314 | Path of the desired file if the file was succesfully created, 315 | None otherwise. 316 | """ 317 | fname = 'scan41_short.cnt' 318 | cnt_dict={'dataset_name': 'cnt_sample', 319 | 'archive_name': 'scan41_short.cnt', 320 | 'hash': 'md5:7ab589254e83e001e52bee31eae859db', 321 | 'url': 'https://github.com/mne-tools/mne-testing-data/blob/master/CNT/scan41_short.cnt?raw=true', 322 | 'folder_name': 'cnt_sample', 323 | } 324 | data_path = mne.datasets.fetch_dataset(cnt_dict) 325 | shutil.copyfile(os.path.join(data_path,'scan41_short.cnt'), fpath) #copyfile overwrites by default 326 | if os.path.isfile(fpath): 327 | return fpath 328 | else: 329 | return None 330 | 331 | def replace_brainvision_filename(fpath,newname): 332 | if '.eeg' in newname: 333 | newname = newname.replace('.eeg','') 334 | if '.vmrk' in newname: 335 | newname = newname.replace('.vmrk','') 336 | for line in fileinput.input(fpath, inplace=True): 337 | if 'DataFile' in line: 338 | print(f'DataFile={newname}.eeg'.format(fileinput.filelineno(), line)) 339 | elif 'MarkerFile' in line: 340 | print(f'MarkerFile={newname}.vmrk'.format(fileinput.filelineno(), line)) 341 | else: 342 | print('{}'.format(line), end='') 343 | -------------------------------------------------------------------------------- /sovabids/dicts.py: -------------------------------------------------------------------------------- 1 | """Module with dictionary utilities.""" 2 | 3 | import collections 4 | from functools import reduce 5 | def deep_get(dictionary, keys, default=None,sep='.'): 6 | """Safe nested dictionary getter. 7 | 8 | Parameters 9 | ---------- 10 | dictionary: dict 11 | The dictionary from which to get the value. 12 | keys: str 13 | The nested keys using sep as separator. 14 | Ie: 'person.name.lastname' if `sep`='.' 15 | default: object 16 | The default value to return if the key is not found 17 | sep : str, optional 18 | The separator to indicate nesting/branching/hierarchy. 19 | 20 | Returns 21 | ------- 22 | 23 | object: 24 | The value of the required key. `default` if the key is not found. 25 | 26 | Notes 27 | ----- 28 | Taken from https://stackoverflow.com/a/46890853/14068216 29 | """ 30 | return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split(sep), dictionary) 31 | 32 | def deep_merge_N(l): 33 | """Merge the list of dictionaries, such that the latest one has the greater precedence. 34 | 35 | Parameters 36 | ---------- 37 | 38 | l : list of dict 39 | List containing the dictionaries to be merged, having precedence on the last ones. 40 | 41 | Returns 42 | ------- 43 | 44 | dict : 45 | The merged dictionary. 46 | """ 47 | d = {} 48 | while True: 49 | if len(l) == 0: 50 | return {} 51 | if len(l) == 1: 52 | return l[0] 53 | d1 = l.pop(0) 54 | d2 = l.pop(0) 55 | d = deep_merge(d1,d2) 56 | l.insert(0, d) 57 | 58 | def deep_merge(a, b): 59 | """ 60 | Merge two values, with `b` taking precedence over `a`. 61 | 62 | Semantics: 63 | - If either `a` or `b` is not a dictionary, `a` will be returned only if 64 | `b` is `None`. Otherwise `b` will be returned. 65 | - If both values are dictionaries, they are merged as follows: 66 | * Each key that is found only in `a` or only in `b` will be included in 67 | the output collection with its value intact. 68 | * For any key in common between `a` and `b`, the corresponding values 69 | will be merged with the same semantics. 70 | 71 | From David Schneider answer at https://stackoverflow.com/questions/7204805/how-to-merge-dictionaries-of-dictionaries/15836901#15836901 72 | 73 | Parameters 74 | ---------- 75 | 76 | a : object 77 | b : object 78 | 79 | Returns 80 | ------- 81 | 82 | dict : 83 | Merged dictionary. 84 | """ 85 | if not isinstance(a, dict) or not isinstance(b, dict): 86 | return a if b is None else b 87 | else: 88 | # If we're here, both a and b must be dictionaries or subtypes thereof. 89 | 90 | # Compute set of all keys in both dictionaries. 91 | keys = set(a.keys()) | set(b.keys()) 92 | 93 | # Build output dictionary, merging recursively values with common keys, 94 | # where `None` is used to mean the absence of a value. 95 | return { 96 | key: deep_merge(a.get(key), b.get(key)) 97 | for key in keys 98 | } 99 | 100 | 101 | def flatten(d, parent_key='', sep='.'): 102 | """Flatten the nested dictionary structure using the given separator. 103 | 104 | If parent_key is given, then that level is added at the start of the tree. 105 | 106 | Parameters 107 | ---------- 108 | 109 | d : dict 110 | The dictionary to flat. 111 | parent_key : str, optional 112 | The optional top-level field of the dictionary. 113 | sep : str, optional 114 | The separator to indicate nesting/branching/hierarchy. 115 | 116 | Returns 117 | ------- 118 | dict : 119 | A dictionary with only one level of fields. 120 | """ 121 | items = [] 122 | for k, v in d.items(): 123 | new_key = parent_key + sep + k if parent_key else k 124 | if isinstance(v, collections.MutableMapping): 125 | items.extend(flatten(v, new_key, sep=sep).items()) 126 | else: 127 | items.append((new_key, v)) 128 | return dict(items) 129 | 130 | 131 | def nested_notation_to_tree(key,value,leaf='.'): 132 | """Create a nested dictionary from the single (key,value) pair, with the key being branched by the leaf separator. 133 | 134 | Parameters 135 | ---------- 136 | key : str 137 | The key/field to be nested, assuming nesting is represented with the "leaf" parameters. 138 | value : object 139 | The value that it will have at the last level of nesting. 140 | leaf : str, optional 141 | The separator used to indicate nesting in "key" parameter. 142 | 143 | Returns 144 | ------- 145 | 146 | dict : 147 | Nested dictionary. 148 | """ 149 | if leaf in key: 150 | tree_list = key.split(leaf) 151 | tree_dict = value 152 | for key in reversed(tree_list): 153 | tree_dict = {key: tree_dict} 154 | return tree_dict 155 | else: 156 | return {key:value} 157 | -------------------------------------------------------------------------------- /sovabids/errors.py: -------------------------------------------------------------------------------- 1 | """Errors for the RPC sovabids API 2 | """ 3 | import fastapi_jsonrpc as jsonrpc 4 | from pydantic import BaseModel, errors 5 | 6 | class RulesError(jsonrpc.BaseError): 7 | CODE = 5000 8 | MESSAGE = 'Error loading the rules.' 9 | 10 | class DataModel(BaseModel): 11 | details: str 12 | 13 | class ApplyError(jsonrpc.BaseError): 14 | CODE = 5100 15 | MESSAGE = 'Error applying rules.' 16 | 17 | class DataModel(BaseModel): 18 | details: str 19 | 20 | class ConvertError(jsonrpc.BaseError): 21 | CODE = 5200 22 | MESSAGE = 'Error converting files.' 23 | 24 | class DataModel(BaseModel): 25 | details: str 26 | 27 | class SaveError(jsonrpc.BaseError): 28 | CODE = 5300 29 | MESSAGE = 'Error saving file.' 30 | 31 | class DataModel(BaseModel): 32 | details: str 33 | 34 | class FileListError(jsonrpc.BaseError): 35 | CODE = 5400 36 | MESSAGE = 'Error getting the filelist.' 37 | 38 | class DataModel(BaseModel): 39 | details: str 40 | -------------------------------------------------------------------------------- /sovabids/files.py: -------------------------------------------------------------------------------- 1 | """Module with file utilities.""" 2 | import os 3 | import requests 4 | import yaml 5 | def _get_files(root_path): 6 | """Recursively scan the directory for files, returning a list with the full-paths to each. 7 | 8 | Parameters 9 | ---------- 10 | 11 | root_path : str 12 | The path we want to obtain the files from. 13 | 14 | Returns 15 | ------- 16 | 17 | filepaths : list of str 18 | A list containing the path to each file in root_path. 19 | """ 20 | filepaths = [] 21 | for root, dirs, files in os.walk(root_path, topdown=False): 22 | for name in files: 23 | filepaths.append(os.path.join(root, name).replace('\\','/')) 24 | return filepaths 25 | 26 | def _write_yaml(dictionary,path=None): 27 | """Write a yaml file based on the dictionary to the specified path. 28 | 29 | Parameters 30 | ---------- 31 | dictionary : dict 32 | The dictionary to be written. 33 | path : str | None 34 | Full path to the yaml file to be written. If None, no file will be written. 35 | 36 | Returns 37 | ------- 38 | 39 | str : 40 | The dump version of the generated yaml document. 41 | """ 42 | if path is not None: 43 | outputfolder,outputname = os.path.split(path) 44 | os.makedirs(outputfolder,exist_ok=True) 45 | full_path = os.path.join(outputfolder,outputname) 46 | with open(full_path, 'w') as outfile: 47 | yaml.dump(dictionary, outfile, default_flow_style=False) 48 | return yaml.dump(dictionary, default_flow_style=False) 49 | 50 | 51 | 52 | def download(url,path): 53 | """Download in the path the file from the given url. 54 | 55 | From H S Umer farooq answer at https://stackoverflow.com/questions/22676/how-to-download-a-file-over-http 56 | 57 | Parameters 58 | ---------- 59 | 60 | url : str 61 | The url of the file to download. 62 | path : str 63 | The path where to download the file. 64 | """ 65 | get_response = requests.get(url,stream=True) 66 | file_name = url.split("/")[-1] 67 | p = os.path.abspath(os.path.join(path)) 68 | os.makedirs(p,exist_ok=True) 69 | print('Downloading',file_name,'at',p) 70 | if not os.path.isfile(os.path.join(p,file_name)): 71 | with open(os.path.join(p,file_name), 'wb') as f: 72 | for chunk in get_response.iter_content(chunk_size=1024): 73 | if chunk: # filter out keep-alive new chunks 74 | f.write(chunk) 75 | print('100') 76 | else: 77 | print("WARNING: File already existed. Skipping...") -------------------------------------------------------------------------------- /sovabids/heuristics.py: -------------------------------------------------------------------------------- 1 | """Heuristics Module 2 | 3 | Functions should return a dictionary. 4 | """ 5 | from sovabids.parsers import parse_path_pattern_from_entities 6 | from sovabids.parsers import parse_entities_from_bidspath 7 | from sovabids.parsers import find_bidsroot 8 | from bids_validator import BIDSValidator 9 | 10 | def from_io_example(sourcepath,targetpath): 11 | """Get the path pattern from a source-target mapping example. 12 | 13 | The name of the function means "from input-output example", as one provides an input and output pair of (source,target) paths. 14 | 15 | Parameters 16 | ---------- 17 | sourcepath : str 18 | The sourcepath that will be modified to get the path pattern 19 | targetpath : str 20 | The bidspath we are going to derive the information on. 21 | 22 | Returns 23 | ------- 24 | dict : 25 | { 26 | 'pattern': The path pattern in placeholder format. 27 | } 28 | """ 29 | # TODO: Should we also allow the user to just populate the bids stuff by himself 30 | # That is, instead of providing the target , directly provide the values 31 | # of the entities he expects on a dictionary 32 | # In example: source = 'data/lemon/V001/resting/010002.vhdr' 33 | # target = {'subject':'010002','task':'resting','session':'001'} 34 | # With the currently implemented functions this is rather trivial. Is just a matter of exposing it. 35 | validator = BIDSValidator() 36 | # Find the root since we need it relative to the bidspath for the validator to work 37 | try: 38 | bidsroot = find_bidsroot(targetpath) 39 | except: 40 | raise IOError(f'targetpath :{targetpath} is not a valid bidspath.') 41 | targetpath2 = targetpath.replace(bidsroot,'') # avoid side-effects explicitly, although this shouldnt affect the str as it is immutable 42 | targetpath2 = targetpath2.replace('\\','/') 43 | # add / prefix 44 | if targetpath2[0] != '/': 45 | targetpath2 = '/' + targetpath2 46 | assert validator.is_bids(targetpath2),'ERROR: The provided target-path is not a valid bids-path' 47 | bids_entities=parse_entities_from_bidspath(targetpath) 48 | pattern = parse_path_pattern_from_entities(sourcepath,bids_entities) 49 | return {'pattern':pattern} 50 | -------------------------------------------------------------------------------- /sovabids/loggers.py: -------------------------------------------------------------------------------- 1 | """Module dealing with logging related functionality and settings""" 2 | 3 | import os 4 | import logging 5 | import sys 6 | from datetime import datetime 7 | 8 | def _excepthook(*args): 9 | """Catch Exceptions to logger. 10 | 11 | Notes 12 | ----- 13 | See https://code.activestate.com/recipes/577074-logging-asserts/ 14 | """ 15 | logging.getLogger().error('Uncaught exception:', exc_info=args) 16 | 17 | sys.excepthook = _excepthook # See _excepthook documentation 18 | 19 | 20 | def setup_logging(log_file=None, debug=False): 21 | """Setup the logging 22 | 23 | Parameters 24 | ---------- 25 | log_file: str 26 | Name of the logfile 27 | debug: bool 28 | Set log level to DEBUG if debug==True 29 | 30 | Returns 31 | ------- 32 | logging.logger: 33 | The logger. 34 | 35 | 36 | Notes 37 | ----- 38 | This function is a copy of the one found in bidscoin. 39 | https://github.com/Donders-Institute/bidscoin/blob/748ea2ba537b06d8eee54ac7217b909bdf91a812/bidscoin/bidscoin.py#L41-L83 40 | """ 41 | currentDT = datetime.now() 42 | currentDT.strftime("%Y-%m-%d %H:%M:%S") 43 | 44 | noDate = True 45 | # Get the root logger 46 | logger = logging.getLogger() 47 | 48 | # Set the format and logging level 49 | if debug: 50 | fmt = '%(asctime)s - %(name)s - %(levelname)s | %(message)s' 51 | logger.setLevel(logging.DEBUG) 52 | else: 53 | fmt = '%(asctime)s - %(levelname)s | %(message)s' 54 | logger.setLevel(logging.INFO) 55 | datefmt = '%Y-%m-%d %H:%M:%S' 56 | formatter = logging.Formatter(fmt=fmt, datefmt=datefmt) 57 | 58 | # Set & add the streamhandler and add some color to those boring terminal logs! :-) 59 | #coloredlogs.install(level=logger.level, fmt=fmt, datefmt=datefmt) 60 | 61 | if not log_file: 62 | return logger 63 | 64 | # Set & add the log filehandler 65 | logdir,log_name = os.path.split(log_file) 66 | os.makedirs(logdir,exist_ok=True) # Create the log dir if it does not exist 67 | log_name = os.path.join(logdir,currentDT.strftime("%Y-%m-%d__%H_%M_%S") + '__' + log_name) 68 | if noDate: 69 | log_name=log_file 70 | loghandler = logging.FileHandler(log_name) 71 | loghandler.setLevel(logging.DEBUG) 72 | loghandler.setFormatter(formatter) 73 | loghandler.set_name('loghandler') 74 | logger.addHandler(loghandler) 75 | 76 | # Set & add the error / warnings handler 77 | error_file = log_name +'.errors' # Derive the name of the error logfile from the normal log_file 78 | errorhandler = logging.FileHandler(error_file, mode='w') 79 | errorhandler.setLevel(logging.WARNING) 80 | errorhandler.setFormatter(formatter) 81 | errorhandler.set_name('errorhandler') 82 | logger.addHandler(errorhandler) 83 | return logger 84 | -------------------------------------------------------------------------------- /sovabids/misc.py: -------------------------------------------------------------------------------- 1 | "Module with misc utilities for sovabids." 2 | 3 | import numpy as np 4 | 5 | def flat_paren_counter(string): 6 | """Count the number of non-nested balanced parentheses in the string. If parenthesis is not balanced then return -1. 7 | 8 | Parameters 9 | ---------- 10 | 11 | string : str 12 | The string we will inspect for balanced parentheses. 13 | 14 | Returns 15 | ------- 16 | 17 | int : 18 | The number of non-nested balanced parentheses or -1 if the string has unbalanced parentheses. 19 | """ 20 | #Modified from 21 | #jeremy radcliff 22 | #https://codereview.stackexchange.com/questions/153078/balanced-parentheses-checker-in-python 23 | counter = 0 24 | times = 0 25 | inside = False 26 | for c in string: 27 | if not inside and c == '(': 28 | counter += 1 29 | inside = True 30 | elif inside and c == ')': 31 | counter -= 1 32 | times +=1 33 | inside = False 34 | if counter < 0: 35 | return -1 36 | 37 | if counter == 0: 38 | return times 39 | return -1 40 | 41 | 42 | 43 | def get_num_digits(N): 44 | """Return the number of digits of the given number N. 45 | 46 | Parameters 47 | ---------- 48 | N : int 49 | The number we want to apply the function to. 50 | 51 | Returns 52 | ------- 53 | int : 54 | The numbers of digits needed to represent the number N. 55 | """ 56 | return int(np.log10(N))+1 57 | 58 | 59 | -------------------------------------------------------------------------------- /sovabids/parsers.py: -------------------------------------------------------------------------------- 1 | """Module with parser utilities.""" 2 | import re 3 | from copy import deepcopy 4 | 5 | from sovabids.misc import flat_paren_counter 6 | from sovabids.dicts import deep_merge_N,nested_notation_to_tree 7 | 8 | def placeholder_to_regex(placeholder,encloser='%',matcher='(.+)'): 9 | """Translate a placeholder pattern to a regex pattern. 10 | 11 | Parameters 12 | ---------- 13 | placeholder : str 14 | The placeholder pattern to translate. 15 | matcher : str, optional 16 | The regex pattern to use for the placeholder, ie : (.*?),(.*),(.+). 17 | encloser : str, optional 18 | The symbol which encloses the fields of the placeholder pattern. 19 | 20 | Returns 21 | ------- 22 | 23 | pattern : str 24 | The regex pattern. 25 | fields : list of str 26 | The fields as they appear in the regex pattern. 27 | """ 28 | pattern = placeholder 29 | pattern = pattern.replace('\\','/') 30 | if pattern.count('%') == 0 or pattern.count('%') % 2 != 0: 31 | return '',[] 32 | else: 33 | borders = pattern.split(encloser)[::2] 34 | fields = pattern.split(encloser)[1::2] 35 | for field in fields: 36 | pattern = pattern.replace(encloser+field+encloser, matcher, 1) 37 | pattern = pattern.replace('/','\\/') 38 | return pattern,fields 39 | 40 | def parse_from_placeholder(string,pattern,encloser='%',matcher='(.+)'): 41 | """Parse string from a placeholder pattern. 42 | 43 | Danger: It will replace underscores and hyphens with an empty character in all fields 44 | except for the ignore field. This to accomodate to the bids standard restrictions automatically. 45 | 46 | Parameters 47 | ---------- 48 | 49 | string : str 50 | The string to parse. 51 | pattern : str 52 | The placeholder pattern to use for parsing. 53 | matcher : str, optional 54 | The regex pattern to use for the placeholder, ie : (.*?),(.*),(.+). 55 | encloser : str, optional 56 | The symbol which encloses the fields of the placeholder pattern. 57 | 58 | Returns 59 | ------- 60 | 61 | dict 62 | The dictionary with the fields and values requested. 63 | """ 64 | pattern,fields = placeholder_to_regex(pattern,encloser,matcher) 65 | return parse_from_regex(string,pattern,fields) 66 | 67 | def parse_from_regex(string,pattern,fields,invalid_replace=''): 68 | """Parse string from regex pattern. 69 | 70 | Danger: It will replace underscores and hyphens with an empty character in all fields 71 | except for the ignore field. This to accomodate to the bids standard restrictions automatically. 72 | 73 | Parameters 74 | ---------- 75 | string : str 76 | The string to parse. 77 | pattern : str 78 | The regex pattern to use for parsing. 79 | fields : list of str 80 | List of fields in the same order as they appear in the regex pattern. 81 | invalid_replace: str 82 | String that will replace '-' and '_' that appear on extracted fields. 83 | Returns 84 | ------- 85 | 86 | dict 87 | The dictionary with the fields and values requested. 88 | """ 89 | 90 | string = string.replace('\\','/') # USE POSIX PLEASE 91 | num_groups = flat_paren_counter(pattern) 92 | if isinstance(fields,str): 93 | fields = [fields] 94 | num_fields = len(fields) 95 | if not num_fields == num_groups: 96 | return {} 97 | match = re.search(pattern,string) 98 | 99 | if not hasattr(match, 'groups'): 100 | raise AttributeError(f"Couldn't find fields in the string {string} using the pattern {pattern}. Recheck the pattern for errors.") 101 | 102 | if not num_groups == len(match.groups()): 103 | return {} 104 | 105 | l = [] 106 | 107 | for field,value in zip(fields,list(match.groups())): 108 | if field != 'ignore' and ('_' in value or '-' in value): 109 | value2 = value.replace('_',invalid_replace) 110 | value2 = value2.replace('-',invalid_replace) 111 | d = nested_notation_to_tree(field,value2) 112 | else: 113 | d = nested_notation_to_tree(field,value) 114 | l.append(d) 115 | return deep_merge_N(l) 116 | 117 | def parse_entity_from_bidspath(path,entity,mode='r2l'): 118 | """Get the value of a bids-entity from a path. 119 | 120 | Parameters 121 | ---------- 122 | path : str 123 | The bidspath we are going to derive the information on. 124 | Should be the complete path of file of a modality (ie an _eeg file). 125 | entity : str 126 | The entity we are going to extract. 127 | SHOULD be one of sub|ses|task|acq|run 128 | mode : str 129 | Direction of lookup. One of r2l|l2r . 130 | r2l (right to left) 131 | l2r (left to right) 132 | 133 | Returns 134 | ------- 135 | value : str 136 | The extracted value of the entity as a string. 137 | If None, it means the entity was not found on the string. 138 | """ 139 | entity = entity if '-' in entity else entity + '-' 140 | # Easier to find it from the tail of the bidspath 141 | if mode == 'r2l': 142 | entity_position = path.rfind(entity) 143 | elif mode == 'l2r': 144 | entity_position = path.find(entity) 145 | else: 146 | raise ValueError('Incorrect usage of the mode argument.') 147 | 148 | if entity_position == -1: 149 | return None 150 | 151 | little_path = path[entity_position:] 152 | 153 | value = re.search('%s(.*?)%s' % ('-', '_'), little_path,).group(1) 154 | 155 | return value 156 | 157 | def _modify_entities_of_placeholder_pattern(pattern,mode='append'): 158 | """Convert between sovabids entities pattern notation and the shorter notation. 159 | 160 | The shorter notation is: 161 | %dataset%, %task%, %session%, %subject%, %run%, %acquisition% 162 | 163 | Parameters 164 | ---------- 165 | string : str 166 | The pattern string to convert. 167 | mode : str 168 | Whether to append 'entities' or cut it. One of {'append','cut'} 169 | 170 | Returns 171 | ------- 172 | str 173 | The converted pattern string. 174 | """ 175 | if mode == 'append': 176 | for keyword in ['%task%','%session%','%subject%','%run%','%acquisition%']: 177 | pattern = pattern.replace(keyword,'%entities.'+keyword[1:]) 178 | pattern = pattern.replace('%dataset%','%dataset_description.Name%') 179 | elif mode == 'cut': 180 | for keyword in ['%task%','%session%','%subject%','%run%','%acquisition%']: 181 | pattern = pattern.replace('%entities.'+keyword[1:],keyword) 182 | pattern = pattern.replace('%dataset_description.Name%','%dataset%') 183 | return pattern 184 | 185 | def parse_entities_from_bidspath(targetpath,entities=['sub','ses','task','acq','run'],mode='r2l'): 186 | """Get the bids entities from a bidspath. 187 | 188 | Parameters 189 | ---------- 190 | targetpath : str 191 | The bidspath we are going to derive the information on. 192 | entities : list of str 193 | The entities we are going to extract. 194 | Defaults to sub,ses,task,acq,run 195 | mode : str 196 | Direction of lookup. One of r2l|l2r . 197 | r2l (right to left) 198 | l2r (left to right) 199 | 200 | Returns 201 | ------- 202 | dict 203 | A dictionary with the extracted entities. 204 | {'sub':'11','task':'resting','ses':'V1','acq':'A','run':1} 205 | """ 206 | path = deepcopy(targetpath) 207 | bids_dict = dict() 208 | for entity in entities: 209 | bids_dict[entity] = parse_entity_from_bidspath(path,entity,mode) 210 | # Clean Non Existent key 211 | bids_dict2 = {key:value for key,value in bids_dict.items() if value is not None} 212 | return bids_dict2 213 | 214 | def parse_path_pattern_from_entities(sourcepath,bids_entities): 215 | """Get the path pattern from a path and a dictionary of bids entities and their values. 216 | 217 | Parameters 218 | ---------- 219 | sourcepath : str 220 | The sourcepath that will be modified to get the path pattern 221 | bids_entities : dict 222 | Dictionary with the entities and their values on the path. 223 | Ie {'sub':'11','task':'resting','ses':'V1','acq':'A','run':1} 224 | There should be no ambiguity between the sourcepath and each of the values. 225 | Otherwise an error will be raised. 226 | 227 | Returns 228 | ------- 229 | 230 | str : 231 | The path pattern in placeholder format 232 | """ 233 | path = deepcopy(sourcepath) 234 | values = [val for key,val in bids_entities.items()] 235 | key_map={ 236 | 'sub':'%subject%', 237 | 'ses':'%session%', 238 | 'task':'%task%', 239 | 'acq':'%acquisition%', 240 | 'run':'%run%' 241 | } 242 | assert '%' not in path # otherwise it will mess up the logic 243 | for key,val in bids_entities.items(): 244 | pathcopy = deepcopy(path) 245 | # Replace all other values which are superstrings of the current one 246 | superstrings = [x for x in values if val in x and val!=x] 247 | for string in superstrings: 248 | pathcopy = pathcopy.replace(string,'*'*len(string)) 249 | # handle ambiguity 250 | if pathcopy.count(val) > 1: 251 | raise ValueError('Ambiguity: The path has multiple instances of {}'.format(val)) 252 | if pathcopy.count(val) < 1: 253 | superstrings = [x for x in bids_entities.values() if val in x and val!=x] 254 | substrings = [x for x in bids_entities.values() if x in val and val!=x] 255 | possible_ambiguity_with = set(superstrings+substrings) 256 | raise ValueError(f'{val} seems to be ambiguous with any of the following values {possible_ambiguity_with}') 257 | path = path.replace(val,key_map[key]) 258 | values[values.index(val)] = key_map[key] 259 | path = _modify_entities_of_placeholder_pattern(path) 260 | path = path.replace('\\','/') 261 | # Find first changing value and put the pattern from there 262 | first_placeholder = path.find('%') 263 | # Identify where should the pattern start 264 | start = path[:first_placeholder].rfind('/') + 1 if '/' in path[:first_placeholder] else 0 265 | path = path[start:] 266 | return path 267 | 268 | def find_bidsroot(path): 269 | """Get the bidsroot from an absolute path describing a bids file inside a subject subfolder. 270 | 271 | Parameters 272 | ---------- 273 | path : str 274 | The absolute path to any bids file inside a sub- folder. 275 | 276 | Returns 277 | ------- 278 | 279 | str : 280 | The bidsroot absolute path. 281 | """ 282 | sub = parse_entities_from_bidspath(path,entities=['sub'],mode='r2l') 283 | index = path.find(sub['sub']) 284 | #We know the bids root is the path up until that index minus some stuff 285 | bidsroot = path[:index-4] #remove sub- prefix 286 | return bidsroot 287 | -------------------------------------------------------------------------------- /sovabids/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | """Module with the schemas associated with sovabids.""" 2 | import os 3 | 4 | def get_sova2coin_bidsmap(): 5 | """Get the sova2coin bidsmap. 6 | 7 | Returns 8 | ------- 9 | 10 | str : 11 | The sova2coin bidsmap. 12 | 13 | """ 14 | this_dir = os.path.dirname(__file__) 15 | bidsmap_sova2coin_file = os.path.join(this_dir,'bidsmap_sova2coin.yml') 16 | with open(bidsmap_sova2coin_file) as f: 17 | bidsmap_sova2coin = f.read() 18 | return bidsmap_sova2coin 19 | 20 | -------------------------------------------------------------------------------- /sovabids/schemas/bidsmap_sova2coin.yml: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- 2 | # This is a bidsmap YAML file with the key-value mappings for the different BIDS 3 | # datatypes (anat, func, dwi, etc). The datatype attributes are the keys that map 4 | # onto the BIDS labels. The bidsmap data-structure should be 5 levels deep: 5 | # 6 | # dict : dict : list : dict : dict 7 | # dataformat : datatype : run-item : bidsmapping : mapping-data 8 | # 9 | # NB: 10 | # 1) Edit the bidsmap file to your needs before feeding it to bidscoiner.py 11 | # 2) (Institute) users may create their own bidsmap_[template].yaml or 12 | # bidsmap_[sample].yaml file 13 | # 14 | # For more information, see: https://bidscoin.readthedocs.io 15 | # -------------------------------------------------------------------------------- 16 | 17 | 18 | Options: 19 | # -------------------------------------------------------------------------------- 20 | # General options and plugins 21 | # -------------------------------------------------------------------------------- 22 | bidscoin: 23 | version: 3.7.0-dev # BIDScoin version (should correspond with the version in ../bidscoin/version.txt) 24 | bidsignore: extra_data/ # Semicolon-separated list of entries that are added to the .bidsignore file (for more info, see BIDS specifications), e.g. extra_data/;pet/;myfile.txt;yourfile.csv 25 | subprefix: sub- # The default subject prefix of the source data 26 | sesprefix: ses- # The default session prefix of the source data 27 | plugins: # List of plugins with plugin-specific key-value pairs (that can be used by the plugin) 28 | README: # The plugin basename that is installed in the default bidscoin/plugins folder 29 | dcm2bidsmap: # The default plugin that is used by the bidsmapper to map DICOM and PAR/REC source data 30 | dcm2niix2bids: # See dcm2niix -h and https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#General_Usage for more info 31 | path: module add dcm2niix; # Command to set the path to dcm2niix (note the semi-colon), e.g. module add dcm2niix/1.0.20180622; or PATH=/opt/dcm2niix/bin:$PATH; or /opt/dcm2niix/bin/ or '"C:\Program Files\dcm2niix\"' (note the quotes to deal with the whitespace) 32 | args: -b y -z y -i n # Argument string that is passed to dcm2niix. Tip: SPM users may want to use '-z n' (which produces unzipped nifti's, see dcm2niix -h for more information) 33 | sova2coin: # sovabids module 34 | rules : {} # 35 | 36 | 37 | EEG: 38 | # -------------------------------------------------------------------------------- 39 | # EEG key-value heuristics (sovabids fields that are mapped to the BIDS labels) 40 | # -------------------------------------------------------------------------------- 41 | subject: <> 42 | session: <> 43 | 44 | eeg: # ----------------------- All eeg runs -------------------- 45 | - provenance: # The fullpath name of the EEG file from which the attributes are read. Serves also as a look-up key to find a run in the bidsmap 46 | properties: &fileprop # This is an optional (stub) entry of filesystem matching (could be added to any run-item) 47 | filepath: # File folder, e.g. ".*Parkinson.*" or ".*(phantom|bottle).*" 48 | filename: .* # File name, e.g. ".*fmap.*" or ".*(fmap|field.?map|B0.?map).*" 49 | filesize: # File size, e.g. "2[4-6]\d MB" for matching files between 240-269 MB 50 | nrfiles: # Number of files in the folder that match the above criteria, e.g. "5/d/d" for matching a number between 500-599 51 | attributes: &eeg_attr # An empty / non-matching reference dictionary that can be derefenced in other run-items of this data type 52 | sidecar: 53 | channels.name: 54 | channels.type: 55 | channels.units: 56 | entities.subject: 57 | entities.task: 58 | entities.session: 59 | entities.run: 60 | dataset_description: 61 | bids: &eeg_bids # See: schema/datatypes/eeg.yaml 62 | task: <> # Note Dynamic values are not previewed in the bids editor but they do work, this should be fixed anyway 63 | acq: 64 | run: <> 65 | suffix: eeg 66 | meta: # This is an optional entry for meta-data that will be appended to the json sidecar files. Currently not supported in sova2coin. -------------------------------------------------------------------------------- /sovabids/schemas/rules.yml: -------------------------------------------------------------------------------- 1 | #TODO: Do a validator of this file schema 2 | # # eeg bids fields collection 3 | # # Unsupported files are commented 4 | entities : #see https://github.com/bids-standard/bids-specification/blob/66d8532065bdae6a7275efd8bbe05a8e00dc1031/src/schema/datatypes/eeg.yaml#L13-L18 5 | subject : REQUIRED # this is mostly used if you are doing a single user, better to do it from the path 6 | session : OPTIONAL 7 | task : REQUIRED 8 | acquisition : OPTIONAL 9 | run : OPTIONAL 10 | 11 | dataset_description : #https://bids-specification.readthedocs.io/en/latest/03-modality-agnostic-files.html 12 | # we for now will support only what mne_bids.make_dataset_description supports 13 | Name : REQUIRED 14 | # BIDSVersion : REQUIRED 15 | # HEDVersion : RECOMMENDED 16 | # DatasetType : RECOMMENDED 17 | # License : RECOMMENDED 18 | Authors : OPTIONAL 19 | # Acknowledgements : OPTIONAL 20 | # HowToAcknowledge : OPTIONAL 21 | # Funding : OPTIONAL 22 | # EthicsApprovals : OPTIONAL 23 | # ReferencesAndLinks : OPTIONAL 24 | # DatasetDOI : OPTIONAL 25 | 26 | # #https://bids-specification.readthedocs.io/en/latest/04-modality-specific-files/03-electroencephalography.html 27 | sidecar : 28 | # TaskName : REQUIRED 29 | # InstitutionName : RECOMMENDED 30 | # InstitutionAddress : RECOMMENDED 31 | # Manufacturer : RECOMMENDED 32 | # ManufacturersModelName : RECOMMENDED 33 | # SoftwareVersions : RECOMMENDED 34 | # TaskDescription : RECOMMENDED 35 | # Instructions : RECOMMENDED 36 | # CogAtlasID : RECOMMENDED 37 | # CogPOID : RECOMMENDED 38 | # DeviceSerialNumber : RECOMMENDED 39 | EEGReference : REQUIRED 40 | # SamplingFrequency : REQUIRED 41 | PowerLineFrequency : REQUIRED 42 | SoftwareFilters : REQUIRED # do a dictionary tree like {"Anti-aliasing filter": {"half-amplitude cutoff (Hz)": 500, "Roll-off": "6dB/Octave"}} 43 | # CapManufacturer : RECOMMENDED 44 | # CapManufacturersModelName : RECOMMENDED 45 | # EEGChannelCount : RECOMMENDED 46 | # ECGChannelCount : RECOMMENDED 47 | # EMGChannelCount : RECOMMENDED 48 | # EOGChannelCount : RECOMMENDED 49 | # MiscChannelCount : RECOMMENDED 50 | # TriggerChannelCount : RECOMMENDED 51 | # RecordingDuration : RECOMMENDED 52 | # RecordingType : RECOMMENDED 53 | # EpochLength : RECOMMENDED 54 | # EEGGround : RECOMMENDED 55 | # HeadCircumference : RECOMMENDED 56 | # EEGPlacementScheme : RECOMMENDED 57 | # HardwareFilters : RECOMMENDED 58 | # SubjectArtefactDescription : RECOMMENDED 59 | 60 | channels : 61 | name : REQUIRED #notice, this will be applied first so if other fields are changed use the new name as index 62 | type : REQUIRED #dictionary following name : bids-type 63 | # units : REQUIRED 64 | # description : OPTIONAL 65 | # sampling_frequency : OPTIONAL 66 | # reference : OPTIONAL 67 | # low_cutoff : OPTIONAL 68 | # high_cutoff : OPTIONAL 69 | # notch : OPTIONAL 70 | # status : OPTIONAL 71 | # status_description : OPTIONAL 72 | 73 | # electrodes : 74 | # name : REQUIRED 75 | # x : REQUIRED 76 | # y : REQUIRED 77 | # z : REQUIRED 78 | # type : RECOMMENDED 79 | # material : RECOMMENDED 80 | # impedance : RECOMMENDED 81 | 82 | # coordsystem : # this has special logic for the requirements but the default is implemented 83 | # # https://bids-specification.readthedocs.io/en/latest/04-modality-specific-files/03-electroencephalography.html 84 | # IntendedFor : OPTIONAL 85 | # EEGCoordinateSystem : REQUIRED 86 | # EEGCoordinateUnits : REQUIRED 87 | # EEGCoordinateSystemDescription : RECOMMENDED 88 | # FiducialsDescription : OPTIONAL 89 | # FiducialsCoordinates : RECOMMENDED 90 | # FiducialsCoordinateSystem : RECOMMENDED 91 | # FiducialsCoordinateUnits : RECOMMENDED 92 | # FiducialsCoordinateSystemDescription : RECOMMENDED 93 | # AnatomicalLandmarkCoordinates : RECOMMENDED 94 | # AnatomicalLandmarkCoordinateSystem : RECOMMENDED 95 | # AnatomicalLandmarkCoordinateUnits : RECOMMENDED 96 | # AnatomicalLandmarkCoordinateSystemDescription : RECOMMENDED 97 | 98 | # events: 99 | # onset : REQUIRED 100 | # duration : REQUIRED 101 | # sample : OPTIONAL 102 | # trial_type : OPTIONAL 103 | # response_time : OPTIONAL 104 | # value : OPTIONAL 105 | # HED : OPTIONAL 106 | # stim_file : OPTIONAL 107 | # StimulusPresentation : RECOMMENDED 108 | # OperatingSystem : RECOMMENDED 109 | # SoftwareName : RECOMMENDED 110 | # SoftwareRRID : RECOMMENDED 111 | # SoftwareVersion : RECOMMENDED 112 | # Code : RECOMMENDED 113 | 114 | non-bids : 115 | eeg_extension : OPTIONAL # to say which files to take into account when reading a directory, can be a list of extensions or a single one 116 | path_analysis : #OPTIONAL #USE POSIX REQUIRED 117 | pattern: REQUIRED-IF-PATH-ANALYSIS # Either regex or custom notation (which would have the fields already inside) 118 | fields: #REQUIRED if regex # dot notation of the other dicts, same order as they appear in the regex pattern 119 | - field1.something 120 | - field2 121 | encloser : OPTIONAL # the split symbol, only for custom notation of path pattern, defaults to % 122 | matcher : OPTIONAL # the regex matcher that replaces %field% to translate it to regex, only for custom notation.defaults to (.+) 123 | code_execution : OPTIONAL # if the code itself needs ie quotes, escape characters may be nedeed 124 | # example: raw.set_channel_mapping({\"VEOG\":\"eog\"})" # this is an example, this line of code actually doesnt work 125 | 126 | IO: # INPUT-OUTPUT 127 | source : UNUSED for RULES, REQUIRED for MAPPINGS (the source file path for individual mappings,the source data root path for general mapping) 128 | target : UNUSED for RULES, REQUIRED for MAPPINGS (the target file path for individual mappings,the target bids root path for general mapping) 129 | -------------------------------------------------------------------------------- /sovabids/settings.py: -------------------------------------------------------------------------------- 1 | """Module dealing with general settings and constants for the package.""" 2 | import os 3 | from sovabids import __path__ 4 | 5 | SUPPORTED_EXTENSIONS = ['.set' ,'.cnt' ,'.vhdr' ,'.bdf','.edf' ,'.fif'] 6 | """The current supported extensions of the package.""" 7 | 8 | NULL_VALUES = ['',None,{},[]] 9 | """The values to consider as NULL or NOT-AVAILABLE.""" 10 | 11 | SECTION_STRING = 11*'-' 12 | """A string intended to announce the start and end of a section in the logging.""" 13 | 14 | SOVABIDS_PATH = __path__[0] 15 | """The path of the package.""" 16 | 17 | REPO_PATH = os.path.realpath(os.path.join(SOVABIDS_PATH,'..')) 18 | """The path of the sovabids project, that is the root of the git repository. Needs that sovabids was installed in editable mode from the git repository. 19 | """ 20 | -------------------------------------------------------------------------------- /sovabids/sovarpc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Action Oriented RPC API for Sovabids. 3 | """ 4 | 5 | import traceback 6 | import fastapi_jsonrpc as jsonrpc 7 | from typing import List, Optional 8 | import sovabids.rules as ru 9 | import sovabids.convert as co 10 | import sovabids.files as fi 11 | from sovabids.errors import ApplyError,ConvertError,SaveError,RulesError,FileListError 12 | app = jsonrpc.API() 13 | 14 | api = jsonrpc.Entrypoint('/api/sovabids') 15 | 16 | 17 | # Sadly we will need to manage the docstring of these methods by hand since the fastapi documentation 18 | # Does not support sphinx references 19 | @api.method(errors=[ApplyError]) 20 | def apply_rules( 21 | file_list: List[str], 22 | bids_path: str, 23 | rules: dict, 24 | mapping_path: str 25 | ) -> dict: 26 | """Apply rules to a set of files. 27 | 28 | Parameters 29 | ---------- 30 | 31 | file_list : list of str 32 | List of str with the paths of the files we want to convert (ie the output of get_files). 33 | bids_path : str 34 | The path we want the converted files in. 35 | rules : dict 36 | A dictionary with the rules. 37 | mapping_path : str, optional 38 | The fullpath where we want to write the mappings file. 39 | If '', then bids_path/code/sovabids/mappings.yml will be used. 40 | 41 | Returns 42 | ------- 43 | 44 | dict : 45 | A dictionary following: { 46 | 'General': rules given, 47 | 'Individual':list of mapping dictionaries for each file 48 | } 49 | 50 | 51 | Notes 52 | ----- 53 | A wrapper of around rules.apply_rules function. 54 | See docstring of :py:func:`apply_rules() ` in :py:mod:`rules` 55 | """ 56 | try: 57 | mappings = ru.apply_rules(source_path=file_list,bids_path=bids_path,rules=rules,mapping_path=mapping_path) 58 | except: 59 | raise ApplyError(data={'details': traceback.format_exc()}) 60 | return mappings 61 | 62 | @api.method(errors=[ConvertError]) 63 | def convert_them( 64 | general : dict, 65 | individual: List[dict] 66 | ) -> None: 67 | """Convert eeg files to bids according to the mappings given. 68 | 69 | Parameters 70 | ---------- 71 | general : dict 72 | The general rules 73 | individual: list[dict] 74 | List with the individual mappings of each file. 75 | 76 | Notes 77 | ----- 78 | A wrapper of around convert.convert_them function. 79 | 80 | See docstring of :py:func:`convert_them() ` in :py:mod:`convert` 81 | 82 | Returns 83 | ------- 84 | None 85 | """ 86 | try: 87 | data = {'General':general,'Individual':individual} 88 | co.convert_them(mappings_input=data) 89 | except: 90 | raise ConvertError(data={'details': traceback.format_exc()}) 91 | 92 | @api.method(errors=[RulesError]) 93 | def load_rules( 94 | rules_path: str, 95 | ) -> dict: 96 | """Load rules from a path. 97 | 98 | Parameters 99 | ---------- 100 | 101 | rules_path : str 102 | The path to the rules file. 103 | 104 | Returns 105 | ------- 106 | 107 | dict 108 | The rules dictionary. 109 | 110 | Notes 111 | ----- 112 | A wrapper of around rules.load_rules function. 113 | 114 | See docstring of :py:func:`load_rules() ` in :py:mod:`rules` 115 | """ 116 | 117 | try: 118 | rules = ru.load_rules(rules_path) 119 | except: 120 | raise RulesError(data={'details': traceback.format_exc()}) 121 | return rules 122 | 123 | @api.method(errors=[ApplyError]) 124 | def apply_rules_to_single_file( 125 | file:str, 126 | rules:dict, 127 | bids_path:str, 128 | write:bool=False, 129 | preview:bool=False 130 | ) -> dict: 131 | """Apply rules to a single file. 132 | 133 | Parameters 134 | ---------- 135 | 136 | file : str 137 | Path to the file. 138 | rules : dict 139 | The rules dictionary. 140 | bids_path : str 141 | Path to the bids directory 142 | write : bool, optional 143 | Whether to write the converted files to disk or not. 144 | preview : bool, optional 145 | Whether to return a dictionary with a "preview" of the conversion. 146 | This dict will have the same schema as the "Mapping File Schema" but may have flat versions of its fields. 147 | *UNDER CONSTRUCTION* 148 | 149 | Returns 150 | ------- 151 | 152 | dict: 153 | { 154 | mapping : dict 155 | The mapping obtained from applying the rules to the given file 156 | preview : bool|dict 157 | If preview = False, then False. If True, then the preview dictionary. 158 | } 159 | 160 | Notes 161 | ----- 162 | A wrapper of around rules.apply_rules_to_single_file function. 163 | 164 | See docstring of :py:func:`apply_rules_to_single_file() ` in :py:mod:`rules` 165 | """ 166 | 167 | try: 168 | mapping,preview=ru.apply_rules_to_single_file(file,rules,bids_path,write,preview) 169 | except: 170 | raise ApplyError(data={'details': traceback.format_exc()}) 171 | return {'mapping':mapping,'preview':preview} 172 | 173 | @api.method(errors=[SaveError]) 174 | def save_rules( 175 | rules:dict, 176 | path:str 177 | ) -> None: 178 | """Save rules as a yaml file to a path. 179 | 180 | Parameters 181 | ---------- 182 | rules: dict 183 | The rules dictionary to save 184 | path : str 185 | The full-path (including filename) where to save the rules as yaml. 186 | 187 | Returns 188 | ------- 189 | 190 | None 191 | 192 | Notes 193 | ----- 194 | 195 | A wrapper of around files._write_yaml function. 196 | 197 | See docstring of :py:func:`_write_yaml() ` in :py:mod:`files` 198 | """ 199 | 200 | try: 201 | fi._write_yaml(rules,path) 202 | except: 203 | raise SaveError(data={'details': traceback.format_exc()}) 204 | return 205 | 206 | @api.method(errors=[SaveError]) 207 | def save_mappings( 208 | path:str, 209 | general:dict, 210 | individual:List[dict] 211 | ) -> None: 212 | """Save mappings as a yaml file to a path. 213 | 214 | Parameters 215 | ---------- 216 | 217 | path : str 218 | The full-path (including filename) where to save the mappings as yaml. 219 | general: dict 220 | The general rules dictionary. 221 | individual: list of dict 222 | A list containing the mapping dictionary of each file. 223 | 224 | Returns 225 | ------- 226 | 227 | None 228 | 229 | Notes 230 | ----- 231 | 232 | A wrapper of around files._write_yaml function. 233 | 234 | See docstring of :py:func:`_write_yaml() ` in :py:mod:`files` 235 | """ 236 | 237 | try: 238 | data = {'General':general,'Individual':individual} 239 | fi._write_yaml(data,path) 240 | except: 241 | raise SaveError(data={'details': traceback.format_exc()}) 242 | return 243 | 244 | @api.method(errors=[FileListError]) 245 | def get_files( 246 | path:str, 247 | rules:dict 248 | ) -> list: 249 | """Recursively scan the directory for valid files, returning a list with the full-paths to each. 250 | 251 | The valid files are given by the 'non-bids.eeg_extension' rule. See the "Rules File Schema". 252 | 253 | Parameters 254 | ---------- 255 | 256 | path : str 257 | The path we want to obtain the files from. 258 | rules : dict 259 | The rules dictionary. 260 | 261 | Returns 262 | ------- 263 | 264 | list[str]: 265 | A list containing the path to each valid file in the source_path. 266 | 267 | 268 | Notes 269 | ----- 270 | 271 | A wrapper of around rules.get_files function. 272 | 273 | See docstring of :py:func:`get_files() ` in :py:mod:`rules` 274 | """ 275 | 276 | try: 277 | filelist = ru.get_files(path,rules) 278 | except: 279 | raise FileListError(data={'details': traceback.format_exc()}) 280 | return filelist 281 | 282 | 283 | app.bind_entrypoint(api) 284 | 285 | def main(entry='sovarpc:app',port=5000,debug=False): 286 | import uvicorn 287 | uvicorn.run(entry, port=port, access_log=False) 288 | 289 | if __name__ == '__main__': 290 | main(port=5100) 291 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yjmantilla/sovabids/ceadd9d88d4b53826470ad48fc03745d34c0d0e5/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_bids.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import yaml 4 | from fastapi.testclient import TestClient 5 | from sovabids.sovarpc import app 6 | import json 7 | 8 | from bids_validator import BIDSValidator 9 | 10 | from sovabids.parsers import placeholder_to_regex,_modify_entities_of_placeholder_pattern 11 | from sovabids.rules import apply_rules,load_rules 12 | from sovabids.dicts import deep_merge_N 13 | from sovabids.datasets import make_dummy_dataset,save_dummy_vhdr,save_dummy_cnt 14 | from sovabids.convert import convert_them 15 | 16 | def dummy_dataset(pattern_type='placeholder',write=True,mode='python',format='.vhdr'): 17 | 18 | # Getting current file path and then going to _data directory 19 | this_dir = os.path.dirname(__file__) 20 | data_dir = os.path.join(this_dir,'..','_data') 21 | data_dir = os.path.abspath(data_dir) 22 | 23 | # Defining relevant conversion paths 24 | test_root = os.path.join(data_dir,'DUMMY') 25 | input_root = os.path.join(test_root,'DUMMY_SOURCE') 26 | mode_str = '_' + mode 27 | bids_path = os.path.join(test_root,'DUMMY_BIDS'+'_'+pattern_type+mode_str+'_'+format.replace('.','')) 28 | 29 | # Make example File 30 | if format == '.vhdr': 31 | example_fpath = save_dummy_vhdr(os.path.join(data_dir,'dummy.vhdr')) 32 | elif format == '.cnt': 33 | example_fpath = save_dummy_cnt(os.path.join(data_dir,'dummy.cnt')) 34 | 35 | # PARAMS for making the dummy dataset 36 | DATA_PARAMS ={ 'EXAMPLE':example_fpath, 37 | 'PATTERN':'T%task%/S%session%/sub%subject%_%acquisition%_%run%', 38 | 'DATASET' : 'DUMMY', 39 | 'NSUBS' : 2, 40 | 'NTASKS' : 2, 41 | 'NRUNS' : 2, 42 | 'NSESSIONS' : 2, 43 | 'ROOT' : input_root 44 | } 45 | 46 | if mode == 'rpc': 47 | client = TestClient(app) 48 | else: 49 | client = None 50 | 51 | # Preparing directories 52 | dirs = [input_root,bids_path] #dont include test_root for saving multiple conversions 53 | for dir in dirs: 54 | try: 55 | shutil.rmtree(dir) 56 | except: 57 | pass 58 | 59 | [os.makedirs(dir,exist_ok=True) for dir in dirs] 60 | 61 | # Generating the dummy dataset 62 | make_dummy_dataset(**DATA_PARAMS) 63 | 64 | # Making rules for the dummy conversion 65 | 66 | # Gotta fix the pattern that wrote the dataset to the notation of the rules file 67 | FIXED_PATTERN =DATA_PARAMS.get('PATTERN',None) 68 | 69 | FIXED_PATTERN = _modify_entities_of_placeholder_pattern(FIXED_PATTERN,'append') 70 | FIXED_PATTERN = FIXED_PATTERN + format 71 | 72 | # Making the rules dictionary 73 | data={ 74 | 'dataset_description': 75 | { 76 | 'Name':'Dummy', 77 | 'Authors':['A1','A2'], 78 | }, 79 | 'sidecar': 80 | { 81 | 'PowerLineFrequency' : 50, 82 | 'EEGReference':'FCz', 83 | 'SoftwareFilters':{"Anti-aliasing filter": {"half-amplitude cutoff (Hz)": 500, "Roll-off": "6dB/Octave"}} 84 | }, 85 | 'non-bids': 86 | { 87 | 'eeg_extension':format, 88 | 'path_analysis':{'pattern':FIXED_PATTERN}, 89 | 'code_execution':['print(\'some good code\')','print(raw.info)','print(some bad code)'] 90 | }, 91 | 'channels': 92 | {'name':{'1':'ECG_CHAN','2':'EOG_CHAN'}, #Note example vhdr and CNT have these channels 93 | 'type':{'ECG_CHAN':'ECG','EOG_CHAN':'EOG'}} # Names (keys) are after the rename of the previous line 94 | } 95 | 96 | if pattern_type == 'regex': 97 | FIXED_PATTERN_RE,fields = placeholder_to_regex(FIXED_PATTERN) 98 | dregex = {'non-bids':{'path_analysis':{'fields':fields,'pattern':FIXED_PATTERN_RE}}} 99 | data = deep_merge_N([data,dregex]) 100 | # Writing the rules file 101 | outputname = 'dummy_rules'+'_'+pattern_type+'.yml' 102 | 103 | full_rules_path = os.path.join(test_root,outputname) 104 | with open(full_rules_path, 'w') as outfile: 105 | yaml.dump(data, outfile, default_flow_style=False) 106 | 107 | mappings_path=None 108 | 109 | if mode=='python': 110 | # Loading the rules file (yes... kind of redundant but tests the io of the rules file) 111 | rules = load_rules(full_rules_path) 112 | 113 | file_mappings = apply_rules(source_path=input_root,bids_path=bids_path,rules=rules) 114 | elif mode=='cli': 115 | os.system('sovapply '+input_root + ' '+ bids_path + ' ' + full_rules_path) 116 | mappings_path = os.path.join(bids_path,'code','sovabids','mappings.yml') 117 | file_mappings = load_rules(mappings_path) 118 | elif mode=='rpc': 119 | 120 | # Load Rules 121 | request=json.dumps({ #jsondumps important to avoid parse errors 122 | "jsonrpc": "2.0", 123 | "id": 0, 124 | "method": "load_rules", 125 | "params": { 126 | "rules_path": full_rules_path 127 | } 128 | }) 129 | 130 | response = client.post("/api/sovabids/load_rules",data=request ) 131 | print(request,'response',response.content.decode()) 132 | rules = json.loads(response.content.decode())['result'] 133 | 134 | # Save Rules 135 | request = json.dumps({ 136 | "jsonrpc": "2.0", 137 | "id": 0, 138 | "method": "save_rules", 139 | "params": { 140 | "rules": rules, 141 | "path": full_rules_path+'.bkp' 142 | } 143 | }) 144 | response = client.post("/api/sovabids/save_rules",data=request ) 145 | print(request,'response',response.content.decode()) 146 | 147 | # Get Files 148 | request = json.dumps({ 149 | "jsonrpc": "2.0", 150 | "id": 0, 151 | "method": "get_files", 152 | "params": { 153 | "rules": rules, 154 | "path": input_root 155 | } 156 | }) 157 | response = client.post("/api/sovabids/get_files",data=request ) 158 | print(request,'response',response.content.decode()) 159 | filelist = json.loads(response.content.decode())['result'] 160 | 161 | # Preview Single File 162 | 163 | request=json.dumps({ #jsondumps important to avoid parse errors 164 | "jsonrpc": "2.0", 165 | "id": 0, 166 | "method": "apply_rules_to_single_file", 167 | "params": { 168 | "file": filelist[0], 169 | "bids_path": bids_path+'.preview', 170 | "rules": rules, 171 | "write":False, 172 | "preview":True 173 | } 174 | }) 175 | response = client.post("/api/sovabids/apply_rules_to_single_file",data=request ) 176 | print(request,'response',response.content.decode()) 177 | single_file = json.loads(response.content.decode()) 178 | print(single_file) 179 | 180 | # Get Mappings 181 | request=json.dumps({ #jsondumps important to avoid parse errors 182 | "jsonrpc": "2.0", 183 | "id": 0, 184 | "method": "apply_rules", 185 | "params": { 186 | "file_list": filelist, 187 | "bids_path": bids_path, 188 | "rules": rules, 189 | "mapping_path":'' 190 | } 191 | }) 192 | response = client.post("/api/sovabids/apply_rules",data=request ) 193 | print(request,'response',response.content.decode()) 194 | file_mappings = json.loads(response.content.decode()) 195 | file_mappings=file_mappings['result'] 196 | mappings_path = os.path.join(bids_path,'code','sovabids','mappings.yml') 197 | 198 | # Save Mappings 199 | request = json.dumps({ 200 | "jsonrpc": "2.0", 201 | "id": 0, 202 | "method": "save_mappings", 203 | "params": { 204 | "general": file_mappings['General'], 205 | "individual":file_mappings['Individual'], 206 | "path": mappings_path+'.bkp' 207 | } 208 | }) 209 | response = client.post("/api/sovabids/save_mappings",data=request ) 210 | print(request,'response',response.content.decode()) 211 | 212 | individuals=file_mappings['Individual'] 213 | 214 | # Testing the mappings (at the moment it only test the filepaths) 215 | validator = BIDSValidator() 216 | filepaths = [x['IO']['target'].replace(bids_path,'') for x in individuals] 217 | for filepath in filepaths: 218 | assert validator.is_bids(filepath),'{} is not a valid bids path'.format(filepath) 219 | if write: 220 | if mode=='python': 221 | convert_them(file_mappings) 222 | elif mode=='cli': 223 | os.system('sovaconvert '+mappings_path) 224 | elif mode=='rpc': 225 | request=json.dumps({ #jsondumps important to avoid parse errors 226 | "jsonrpc": "2.0", 227 | "id": 0, 228 | "method": "convert_them", 229 | "params": { 230 | "general": file_mappings['General'], 231 | "individual":file_mappings['Individual'] 232 | } 233 | }) 234 | response = client.post("/api/sovabids/convert_them",data=request) 235 | 236 | print('okrpc') 237 | return file_mappings 238 | def test_dummy_dataset(): 239 | # apparently it cannot download the cnt consistenly on the github actions machine 240 | #dummy_dataset('placeholder',write=True,format='.cnt') # Test cnt conversion 241 | dummy_dataset('placeholder',write=True) 242 | dummy_dataset('regex',write=True) 243 | dummy_dataset('placeholder',write=True,mode='cli') 244 | dummy_dataset('regex',write=True,mode='cli') 245 | dummy_dataset('placeholder',write=True,mode='rpc') 246 | dummy_dataset('regex',write=True,mode='rpc') 247 | 248 | #TODO: A test for incremental conversion 249 | if __name__ == '__main__': 250 | test_dummy_dataset() 251 | print('ok') -------------------------------------------------------------------------------- /tests/test_heuristics.py: -------------------------------------------------------------------------------- 1 | from sovabids.heuristics import from_io_example 2 | from sovabids.rules import apply_rules_to_single_file 3 | from sovabids.dicts import deep_merge_N 4 | from sovabids.rules import load_rules 5 | import os 6 | import pytest 7 | try: 8 | from test_bids import dummy_dataset 9 | except ImportError: 10 | from .test_bids import dummy_dataset 11 | 12 | 13 | def test_from_io_example(): 14 | 15 | # check if there is already an example dataset to save time 16 | 17 | pattern_type='placeholder'#defaults of dummy_dataset function 18 | mode='python'#defaults of dummy_dataset function 19 | # Getting current file path and then going to _data directory 20 | this_dir = os.path.dirname(__file__) 21 | data_dir = os.path.join(this_dir,'..','_data') 22 | data_dir = os.path.abspath(data_dir) 23 | 24 | # Defining relevant conversion paths 25 | test_root = os.path.join(data_dir,'DUMMY') 26 | input_root = os.path.join(test_root,'DUMMY_SOURCE') 27 | mode_str = '_' + mode 28 | bids_path = os.path.join(test_root,'DUMMY_BIDS'+'_'+pattern_type+mode_str) 29 | mapping_file = os.path.join(bids_path,'code','sovabids','mappings.yml') 30 | if not os.path.isfile(mapping_file): 31 | mappings = dummy_dataset() 32 | else: 33 | mappings = load_rules(mapping_file) 34 | sourcepath=mappings['Individual'][0]['IO']['source'] 35 | targetpath=mappings['Individual'][0]['IO']['target'] 36 | answer=mappings['Individual'][0]['non-bids']['path_analysis']['pattern'] 37 | pattern = from_io_example(sourcepath,targetpath)['pattern'] 38 | assert pattern == answer 39 | rules = mappings['General'] 40 | example_rule = {'non-bids':{'path_analysis':{'pattern':'None','source':sourcepath,'target':targetpath}}} 41 | rules = deep_merge_N([rules,example_rule]) 42 | ind_mapping = apply_rules_to_single_file(sourcepath,rules,rules['IO']['target']) 43 | assert ind_mapping[0]['IO']['target'] == targetpath 44 | 45 | source = 'data/lemon/V001/resting/010002.vhdr' 46 | target = 'data_bids/sub-010002/ses-001/eeg/sub-010002_ses-001_task-resting_eeg.vhdr' 47 | pattern = from_io_example(source,target)['pattern'] 48 | assert pattern == 'V%entities.session%/%entities.task%/%entities.subject%.vhdr' 49 | 50 | 51 | source = 'data/lemon/V001/resting/010002.vhdr' 52 | target = 'data_bids/sub-010002/ses-001/eeg/010002.vhdr' 53 | with pytest.raises(IOError): 54 | from_io_example(source,target) 55 | 56 | target = 'data_bids/sub-010002/ses-001/eeg/sub-010002_.vhdr' 57 | with pytest.raises(AssertionError): 58 | from_io_example(source,target) 59 | 60 | source='data/lemon/session001/taskT001/010002.vhdr' 61 | target='data_bids/sub-010002/ses-001/eeg/sub-010002_ses-001_task-T001_eeg.vhdr' 62 | 63 | 64 | with pytest.raises(ValueError): 65 | from_io_example(source,target) 66 | 67 | source='data/lemon/session009/taskT001/010002.vhdr' 68 | target='data_bids/sub-010002/ses-009/eeg/sub-010002_ses-009_task-T001_eeg.vhdr' 69 | 70 | pattern = from_io_example(source,target) 71 | assert pattern['pattern'] == 'session%entities.session%/task%entities.task%/%entities.subject%.vhdr' 72 | print('hallelujah') 73 | 74 | if __name__ == '__main__': 75 | test_from_io_example() 76 | print('ok') -------------------------------------------------------------------------------- /tests/test_path_parser.py: -------------------------------------------------------------------------------- 1 | """sovareject tests 2 | Run tests: 3 | >>> pytest 4 | 5 | Run coverage 6 | >>> coverage run -m pytest 7 | 8 | Basic coverage reports 9 | >>> coverage report 10 | 11 | HTML coverage reports 12 | >>> coverage html 13 | 14 | For debugging: 15 | Remove fixtures from functions 16 | (since fixtures cannot be called directly) 17 | and use the functions directly 18 | In example: 19 | >>> test_eegthresh(rej_matrix_tuple()) 20 | """ 21 | from sovabids.parsers import parse_from_placeholder 22 | from sovabids.parsers import parse_from_regex 23 | from sovabids.parsers import find_bidsroot 24 | from sovabids.parsers import parse_path_pattern_from_entities 25 | import pytest 26 | 27 | def test_parse_from_regex(): 28 | string = r'Y:\code\sovabids\_data\lemon2\sub-010002\ses-001\resting\sub-010002.vhdr' 29 | path_pattern = '.*?\\/.*?\\/.*?\\/.*?\\/.*?\\/sub-(.*?)\\/ses-(.*?)\\/(.*?)\\/sub-(.*?).vhdr' 30 | fields = ['ignore','entities.session','entities.task','entities.subject'] 31 | result = parse_from_regex(string,path_pattern,fields) 32 | assert result['entities']['subject'] == '010002' 33 | assert result['entities']['session']=='001' 34 | assert result['entities']['task']=='resting' 35 | assert result['ignore']=='010002' 36 | 37 | string = r'Y:\code\sovabids\_data\lemon2\sub-010004\ses-001\sub-010004.vhdr' 38 | 39 | path_pattern = 'ses-(.*?)\\/s(.*?)-(.*?).vhdr' 40 | fields = ['entities.session','ignore','entities.subject'] 41 | result = parse_from_regex(string,path_pattern,fields) 42 | assert result['entities']['session']=='001' 43 | assert result['entities']['subject']=='010004' 44 | assert result['ignore']=='ub' 45 | 46 | string = 'y:\\code\\sovabids\\_data\\lemon\\sub-010002.vhdr' 47 | path_pattern = 'sub-(.*?).vhdr' 48 | fields = 'entities.subject' 49 | result = parse_from_regex(string,path_pattern,fields) 50 | assert result['entities']['subject'] == '010002' 51 | 52 | path_pattern = '(.*?)\\/sub-(.*?).vhdr' 53 | fields = ['ignore','entities.subject'] 54 | result = parse_from_regex(string,path_pattern,fields) 55 | assert result['entities']['subject'] == '010002' 56 | assert result['ignore'] == 'y:/code/sovabids/_data/lemon' #USE POSIX 57 | 58 | path_pattern = '(.*?).vhdr' 59 | fields = 'entities.subject' 60 | result = parse_from_regex(string,path_pattern,fields) 61 | assert result['entities']['subject'] == 'y:/code/sovabids/data/lemon/sub010002' 62 | # Notice replacement of '_' to '', expected as bids does not accept _,- 63 | 64 | path_pattern = 'sub-(.*)' # notice no "?",or use .+ 65 | fields = 'entities.subject' 66 | result = parse_from_regex(string,path_pattern,fields) 67 | assert result['entities']['subject'] == '010002.vhdr' 68 | 69 | 70 | def test_parse_from_placeholder(): 71 | matcher = '(.+)' 72 | string = r'Y:\code\sovabids\_data\lemon2\sub-010002\ses-001\resting\sub-010002.vhdr' 73 | path_pattern = 'sub-%ignore%\ses-%entities.session%\%entities.task%\sub-%entities.subject%.vhdr' 74 | result = parse_from_placeholder(string,path_pattern,matcher=matcher) 75 | assert result['entities']['subject'] == '010002' 76 | assert result['entities']['session']=='001' 77 | assert result['entities']['task']=='resting' 78 | assert result['ignore']=='010002' 79 | 80 | string = r'Y:\code\sovabids\_data\lemon2\sub-010004\ses-001\sub-010004.vhdr' 81 | path_pattern = 'ses-%entities.session%/s%ignore%-%entities.subject%.vhdr' 82 | result = parse_from_placeholder(string,path_pattern,matcher=matcher) 83 | assert result['entities']['session']=='001' 84 | assert result['entities']['subject']=='010004' 85 | assert result['ignore']=='ub' 86 | 87 | string = 'y:\\code\\sovabids\\_data\\lemon\\sub-010002.vhdr' 88 | path_pattern = 'sub-%entities.subject%.vhdr' 89 | result = parse_from_placeholder(string,path_pattern,matcher=matcher) 90 | assert result['entities']['subject'] == '010002' 91 | 92 | path_pattern = '%ignore%\sub-%entities.subject%.vhdr' 93 | result = parse_from_placeholder(string,path_pattern,matcher=matcher) 94 | assert result['entities']['subject'] == '010002' 95 | assert result['ignore'] == 'y:/code/sovabids/_data/lemon' #USE POSIX 96 | 97 | path_pattern = '%entities.subject%.vhdr' 98 | result = parse_from_placeholder(string,path_pattern,matcher=matcher) 99 | assert result['entities']['subject'] == 'y:/code/sovabids/data/lemon/sub010002' 100 | # Notice replacement of '_' to '', expected as bids does not accept _,- 101 | 102 | path_pattern = 'sub-%entities.subject%' 103 | result = parse_from_placeholder(string,path_pattern,matcher=matcher) 104 | assert result['entities']['subject'] == '010002.vhdr' 105 | 106 | def test_find_bidsroot(): 107 | path = 'y:\code\sovabids\_data\DUMMY\DUMMY_BIDS_placeholder_python\sub-SU0\ses-SE0\eeg\sub-SU0_ses-SE0_task-TA0_acq-AC0_run-0_eeg.vhdr' 108 | bidsroot=find_bidsroot(path) 109 | assert bidsroot=='y:\\code\\sovabids\\_data\\DUMMY\\DUMMY_BIDS_placeholder_python\\' 110 | 111 | def test_parse_path_pattern_from_entities(): 112 | 113 | # ambiguous 114 | with pytest.raises(ValueError): 115 | source='data/lemon/sessionV001/task001/010002.vhdr' 116 | entities = {'sub':'010002','task':'001','ses':'V001'} 117 | parse_path_pattern_from_entities(source,entities) 118 | 119 | # non-ambiguous 120 | source='data/lemon/sessionV001/task009/010002.vhdr' 121 | entities = {'sub':'010002','task':'009','ses':'V001'} 122 | pattern =parse_path_pattern_from_entities(source,entities) 123 | assert pattern == 'session%entities.session%/task%entities.task%/%entities.subject%.vhdr' 124 | 125 | if __name__ == '__main__': 126 | test_parse_from_regex() 127 | test_parse_from_placeholder() 128 | test_find_bidsroot() 129 | test_parse_path_pattern_from_entities() 130 | print('ok') -------------------------------------------------------------------------------- /tests/test_sova2coin.py: -------------------------------------------------------------------------------- 1 | from bidscoin.plugins.sova2coin import is_sourcefile 2 | from pathlib import Path 3 | from bidscoin.bidscoiner import bidscoiner 4 | from bidscoin.bidsmapper import bidsmapper 5 | import os 6 | import shutil 7 | from sovabids.schemas import get_sova2coin_bidsmap 8 | from sovabids.files import _get_files 9 | from sovabids.settings import REPO_PATH 10 | from sovabids.parsers import _modify_entities_of_placeholder_pattern 11 | from sovabids.datasets import lemon_bidscoin_prepare,make_dummy_dataset,save_dummy_vhdr 12 | import yaml 13 | 14 | def test_sova2coin(dataset='dummy_bidscoin',noedit=True): 15 | data_dir = os.path.join(REPO_PATH,'_data') 16 | data_dir = os.path.abspath(data_dir) 17 | 18 | source_path = os.path.abspath(os.path.join(data_dir,dataset+'_input')) 19 | bids_path= os.path.abspath(os.path.join(data_dir,dataset+'_output')) 20 | rules_path = os.path.join(data_dir,'bidscoin_'+dataset+'_rules.yml') 21 | template_path = os.path.join(data_dir,'bidscoin_template.yml') 22 | 23 | if dataset == 'dummy_bidscoin': 24 | pat = 'sub-%entities.subject%/ses-%entities.session%/eeg-%entities.task%-%entities.run%.vhdr' 25 | rules = { 26 | 'entities': None, 27 | 'dataset_description':{'Name':dataset}, 28 | 'non-bids':{ 29 | 'path_analysis': 30 | {'pattern':pat} 31 | } 32 | } 33 | elif dataset=='lemon_bidscoin': 34 | rules ={ 35 | 'entities':{'task':'resting'}, 36 | 'dataset_description':{'Name':dataset}, 37 | 'sidecar':{'PowerLineFrequency':50,'EEGReference':'FCz'}, 38 | 'channels':{'type':{'VEOG':'VEOG'}}, 39 | 'non-bids':{'path_analysis':{'pattern':'sub-%entities.subject%/ses-%entities.session%/%ignore%/%ignore%.vhdr'}} 40 | } 41 | 42 | with open(rules_path, 'w') as outfile: 43 | yaml.dump(rules, outfile, default_flow_style=False) 44 | 45 | 46 | #CLEAN BIDS PATH 47 | for dir in [bids_path,source_path]: 48 | try: 49 | shutil.rmtree(dir) 50 | except: 51 | pass 52 | 53 | if dataset=='lemon_bidscoin': 54 | lemon_bidscoin_prepare(source_path) 55 | else: 56 | pat = _modify_entities_of_placeholder_pattern(rules['non-bids']['path_analysis']['pattern'],'cut') 57 | pat = pat.replace('.vhdr','') 58 | try: 59 | shutil.rmtree(source_path) 60 | except: 61 | pass 62 | 63 | # Make example VHDR File 64 | example_fpath = save_dummy_vhdr(os.path.join(data_dir,'dummy.vhdr')) 65 | 66 | make_dummy_dataset(EXAMPLE=example_fpath,DATASET=dataset+'_input',NSUBS=3,NTASKS=2,NSESSIONS=2,NACQS=1,NRUNS=2,PATTERN=pat,ROOT=source_path) 67 | 68 | 69 | files = _get_files(source_path) 70 | any_vhdr = Path([x for x in files if '.vhdr' in x][0]) 71 | any_not_vhdr = Path([x for x in files if '.vhdr' not in x][0]) 72 | 73 | assert is_sourcefile(any_vhdr)=='EEG' 74 | assert is_sourcefile(any_not_vhdr)=='' 75 | # if dataset=='lemon_bidscoin': 76 | # assert get_attribute('EEG',any_vhdr,'sidecar.SamplingFrequency') == 2500.0 77 | # else: 78 | # assert get_attribute('EEG',any_vhdr,'sidecar.SamplingFrequency') == 200.0 79 | 80 | bidsmap = get_sova2coin_bidsmap().format(rules_path) 81 | 82 | with open(template_path,mode='w') as f: 83 | f.write(bidsmap) 84 | 85 | bidsmapper(rawfolder=source_path,bidsfolder=bids_path,subprefix='sub-',sesprefix='ses-',bidsmapfile='bidsmap.yaml',templatefile= template_path,noedit=noedit) 86 | 87 | bidscoiner(rawfolder = source_path, 88 | bidsfolder = bids_path) 89 | 90 | if __name__ == '__main__': 91 | noedit=False 92 | test_sova2coin(noedit=noedit) 93 | #test_sova2coin('lemon_bidscoin',noedit=noedit) -------------------------------------------------------------------------------- /tests/test_web_validator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | npm install --global npm@^7 4 | npm install -g bids-validator 5 | bids-validator _data/DUMMY/DUMMY_BIDS_placeholder_cli_vhdr/ 6 | # bids-validator _data/DUMMY_BIDS_placeholder_python_cnt/ 7 | # apparently it cannot download the cnt 8 | bids-validator _data/DUMMY/DUMMY_BIDS_placeholder_python_vhdr/ 9 | bids-validator _data/DUMMY/DUMMY_BIDS_placeholder_rpc_vhdr/ 10 | bids-validator _data/DUMMY/DUMMY_BIDS_regex_cli_vhdr/ 11 | bids-validator _data/DUMMY/DUMMY_BIDS_regex_python_vhdr/ 12 | bids-validator _data/DUMMY/DUMMY_BIDS_regex_rpc_vhdr/ 13 | 14 | 15 | bids-validator _data/dummy_bidscoin_output 16 | echo $? 17 | # bids-validator _data/DUMMY/DUMMY_SOURCE --------------------------------------------------------------------------------