pax_global_header00006660000000000000000000000064151741536620014524gustar00rootroot0000000000000052 comment=7e9e9a6b75b5825f2ad481716e54aa5fe0896326 nipy-heudiconv-217744b/000077500000000000000000000000001517415366200147415ustar00rootroot00000000000000nipy-heudiconv-217744b/.autorc000066400000000000000000000002571517415366200162430ustar00rootroot00000000000000{ "onlyPublishWithReleaseLabel": true, "baseBranch": "master", "author": "auto ", "noVersionPrefix": false, "plugins": ["git-tag", "released"] } nipy-heudiconv-217744b/.codespellrc000066400000000000000000000002131517415366200172350ustar00rootroot00000000000000[codespell] skip = .git,.venv,venvs,*.svg,_build,build,venv,venvs # te -- TE as codespell is case insensitive ignore-words-list = bu,nd,te nipy-heudiconv-217744b/.github/000077500000000000000000000000001517415366200163015ustar00rootroot00000000000000nipy-heudiconv-217744b/.github/ISSUE_TEMPLATE.md000066400000000000000000000011231517415366200210030ustar00rootroot00000000000000 ### Summary ### Platform details: Choose one: - [ ] Local environment - [ ] Container - Heudiconv version: nipy-heudiconv-217744b/.github/dependabot.yml000066400000000000000000000003231517415366200211270ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: weekly commit-message: prefix: "[gh-actions]" include: scope labels: - internal nipy-heudiconv-217744b/.github/workflows/000077500000000000000000000000001517415366200203365ustar00rootroot00000000000000nipy-heudiconv-217744b/.github/workflows/claude.yml000066400000000000000000000027221517415366200223210ustar00rootroot00000000000000name: Claude PR Assistant on: issue_comment: types: [created] pull_request_review_comment: types: [created] issues: types: [opened, assigned] pull_request_review: types: [submitted] jobs: claude-code-action: if: | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && contains(github.event.issue.body, '@claude')) runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read id-token: write steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Run Claude PR Action uses: anthropics/claude-code-action@beta with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} timeout_minutes: "60" # mode: tag # Default: responds to @claude mentions # Optional: Restrict network access to specific domains only # experimental_allowed_domains: | # .anthropic.com # .github.com # api.github.com # .githubusercontent.com # bun.sh # registry.npmjs.org # .blob.core.windows.net nipy-heudiconv-217744b/.github/workflows/docker.yml000066400000000000000000000022031517415366200223250ustar00rootroot00000000000000name: Build Docker image on: push: branches: - master jobs: build-docker: runs-on: ubuntu-latest steps: - name: Checkout source uses: actions/checkout@v6 with: fetch-depth: 0 - name: Generate Dockerfile run: bash gen-docker-image.sh working-directory: utils - name: Build Docker image run: | # build only if not release tag, i.e. has some "-" in describe # so we do not duplicate work with release workflow. git describe --match 'v[0-9]*' | grep -q -e - && \ docker build \ -t nipy/heudiconv:master \ -t nipy/heudiconv:unstable \ . - name: Push Docker image run: | git describe --match 'v[0-9]*' | grep -q -e - && ( docker login -u "$DOCKER_LOGIN" --password-stdin <<<"$DOCKER_TOKEN" docker push nipy/heudiconv:master docker push nipy/heudiconv:unstable ) env: DOCKER_LOGIN: ${{ secrets.DOCKER_LOGIN }} DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} # vim:set sts=2: nipy-heudiconv-217744b/.github/workflows/lint.yml000066400000000000000000000010001517415366200220160ustar00rootroot00000000000000name: Linters on: - push - pull_request jobs: lint: runs-on: ubuntu-latest steps: - name: Set up environment uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.9' - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install --upgrade tox - name: Run linters run: tox -e lint nipy-heudiconv-217744b/.github/workflows/release.yml000066400000000000000000000056701517415366200225110ustar00rootroot00000000000000name: Auto-release on PR merge on: # ATM, this is the closest trigger to a PR merging push: branches: - master jobs: auto-release: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" steps: - name: Checkout source uses: actions/checkout@v6 with: fetch-depth: 0 - name: Download auto run: | #curl -vL -o - "$(curl -fsSL https://api.github.com/repos/intuit/auto/releases/latest | jq -r '.assets[] | select(.name == "auto-linux.gz") | .browser_download_url')" | gunzip > ~/auto # Pin so we don't break if & when # is fixed. # 11.0.5 is needed for wget -O- https://github.com/intuit/auto/releases/download/v11.0.5/auto-linux.gz | gunzip > ~/auto chmod a+x ~/auto - name: Query 'auto' on type of the release id: auto-version run: | # to be able to debug if something goes wrong set -o pipefail ~/auto version -vv | tee /tmp/auto-version version="$(sed -ne '/Calculated SEMVER bump:/s,.*: *,,p' /tmp/auto-version)" echo "version=$version" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python if: steps.auto-version.outputs.version != '' uses: actions/setup-python@v6 with: python-version: '^3.9' - name: Install Python dependencies if: steps.auto-version.outputs.version != '' run: python -m pip install build twine - name: Create release if: steps.auto-version.outputs.version != '' run: ~/auto shipit env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Build & upload to PyPI if: steps.auto-version.outputs.version != '' run: | python -m build twine upload dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - name: Generate Dockerfile if: steps.auto-version.outputs.version != '' run: bash gen-docker-image.sh working-directory: utils - name: Build Docker images if: steps.auto-version.outputs.version != '' run: | docker build \ -t nipy/heudiconv:master \ -t nipy/heudiconv:unstable \ -t nipy/heudiconv:latest \ -t nipy/heudiconv:"$(git describe | sed -e 's,^v,,g')" \ . - name: Push Docker images if: steps.auto-version.outputs.version != '' run: | docker login -u "$DOCKER_LOGIN" --password-stdin <<<"$DOCKER_TOKEN" docker push --all-tags nipy/heudiconv env: DOCKER_LOGIN: ${{ secrets.DOCKER_LOGIN }} DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} # vim:set sts=2: nipy-heudiconv-217744b/.github/workflows/test.yml000066400000000000000000000033031517415366200220370ustar00rootroot00000000000000name: Test on: pull_request: push: schedule: # run weekly to ensure that we are still good - cron: '1 2 * * 3' jobs: test: runs-on: ubuntu-22.04 env: BOTO_CONFIG: /tmp/nowhere DATALAD_TESTS_SSH: '1' strategy: fail-fast: false matrix: python-version: - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' steps: - name: Check out repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install git-annex run: | # The ultimate one-liner setup for NeuroDebian repository bash <(wget -q -O- http://neuro.debian.net/_files/neurodebian-travis.sh) sudo apt-get update -qq sudo apt-get install git-annex-standalone dcm2niix - name: Install dependencies run: | python -m pip install --upgrade pip wheel pip install -r dev-requirements.txt pip install requests # below installs pyld but that assumes we have requests already pip install datalad pip install coverage pytest - name: Configure Git identity run: | git config --global user.email "test@github.land" git config --global user.name "GitHub Almighty" - name: Run tests with coverage run: coverage run `which pytest` -s -v heudiconv - name: Upload coverage to Codecov uses: codecov/codecov-action@v6 with: fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} # vim:set et sts=2: nipy-heudiconv-217744b/.github/workflows/typing.yml000066400000000000000000000010161517415366200223710ustar00rootroot00000000000000name: Type-check on: - push - pull_request jobs: typing: runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: python-version: '3.9' - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install --upgrade tox - name: Run type checker run: tox -e typing nipy-heudiconv-217744b/.gitignore000066400000000000000000000001641517415366200167320ustar00rootroot00000000000000*.egg-info/ *.pyc .cache/ .coverage .idea/ .tox/ .vscode/ _build/ _version.py build/ dist/ sample_nifti.json venvs/ nipy-heudiconv-217744b/.mailmap000066400000000000000000000040271517415366200163650ustar00rootroot00000000000000Basile Pinsard Chris Filo Gorgolewski Chris Gorgolewski Christopher J. Markiewicz Dae Houlihan Dae Isaac To Isaac To John Lee John Lee John T. Wodder II Jörg Stadler Joerg Stadler Jörg Stadler Joerg Stadler Jörg Stadler Jörg Stadler Jörg Stadler Jörg Stadler Mathias Goncalves mathiasg Mathias Goncalves mathiasg Mathias Goncalves Mathias Goncalves Mathias Goncalves Mathias Goncalves Matteo Visconti di Oleggio Castello Matteo Visconti dOC Matteo Visconti di Oleggio Castello Matteo Visconti dOC Matteo Visconti di Oleggio Castello Michael Dayan <79224807+neurorepro@users.noreply.github.com> Michael Krause Pablo Velasco Pablo Velasco pvelasco Satrajit Ghosh Satrajit Ghosh Steven Tilley Steven Tilley Steven Tilley Steven Tilley nipy-heudiconv-217744b/.pre-commit-config.yaml000066400000000000000000000013551517415366200212260ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: check-added-large-files - id: check-json - id: check-toml - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/codespell-project/codespell rev: v2.2.4 hooks: - id: codespell - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - flake8-builtins - flake8-unused-arguments nipy-heudiconv-217744b/.readthedocs.yaml000066400000000000000000000003501517415366200201660ustar00rootroot00000000000000version: 2 formats: all python: install: - requirements: docs/requirements.txt - method: pip path: . build: os: ubuntu-20.04 tools: python: "3.9" sphinx: configuration: docs/conf.py fail_on_warning: true nipy-heudiconv-217744b/.zenodo.json000066400000000000000000000147371517415366200172240ustar00rootroot00000000000000{ "creators": [ { "name": "Yaroslav O. Halchenko", "orcid": "0000-0003-3456-2493", "affiliation": "Center for Open Neuroscience, Department of Psychological and Brain Sciences, Dartmouth College, Hanover, NH, USA" }, { "name": "Mathias Goncalves", "orcid": "0000-0002-7252-7771", "affiliation": "Department of Psychology, Stanford University, CA, USA" }, { "name": "Satrajit Ghosh", "orcid": "0000-0002-5312-6729", "affiliation": "McGovern Institute, Massachusetts Institute of Technology, Cambridge, MA, USA" }, { "name": "Pablo Velasco", "orcid": "0000-0002-5749-6049", "affiliation": "Flywheel Exchange LLC, Minneapolis, MN, USA" }, { "name": "Matteo Visconti di Oleggio Castello", "orcid": "0000-0001-7931-5272", "affiliation": "University of California, Berkeley, Berkeley, CA, USA" }, { "name": "Taylor Salo", "orcid": "0000-0001-9813-3167", "affiliation": "Perelman School of Medicine, University of Pennsylvania, Philadelphia, PA, USA" }, { "name": "John T. Wodder II", "affiliation": "Center for Open Neuroscience, Department of Psychological and Brain Sciences, Dartmouth College, Hanover, NH, USA" }, { "name": "Michael Hanke", "orcid": "0000-0001-6398-6370", "affiliation": "Institute of Neuroscience and Medicine, Brain & Behaviour (INM-7), Research Center J\u00fclich, J\u00fclich, Germany and Institute of Systems Neuroscience, Medical Faculty, Heinrich Heine University D\u00fcsseldorf, D\u00fcsseldorf, Germany" }, { "name": "Patrick Sadil", "orcid": "0000-0003-4141-1343", "affiliation": "Department of Biostatistics, Johns Hopkins Bloomberg School of Public Health, Baltimore, MD, USA" }, { "name": "Krzysztof Jacek Gorgolewski", "orcid": "0000-0003-3321-7583", "affiliation": "Emeritus of Department of Psychology, Stanford University, CA, USA" }, { "name": "Horea-Ioan Ioanas", "orcid": "0000-0001-7037-2449", "affiliation": "Center for Open Neuroscience, Department of Psychological and Brain Sciences, Dartmouth College, Hanover, NH, USA" }, { "name": "Chris Rorden", "orcid": "0000-0002-7554-6142", "affiliation": "Department of Psychology, University of South Carolina, Columbia, SC, USA" }, { "name": "Timothy J. Hendrickson", "orcid": "0000-0001-6862-6526", "affiliation": "Masonic Institute\u00a0for the Developing Brain, University of Minnesota, Minneapolis, MN, USA and Minnesota Supercomputing Institute, University of Minnesota, Minneapolis, MN, USA" }, { "name": "Michael Dayan", "orcid": "0000-0002-2666-0969", "affiliation": "Human Neuroscience Platform, Fondation Campus Biotech Geneva, Geneva, Switzerland" }, { "name": "Sean Dae Houlihan", "orcid": "0000-0001-5003-9278", "affiliation": "Center for Open Neuroscience, Department of Psychological and Brain Sciences, Dartmouth College, Hanover, NH, USA and Department of Brain and Cognitive Sciences, Massachusetts Institute of Technology, Cambridge, MA, USA" }, { "name": "James Kent", "orcid": "0000-0002-4892-2659", "affiliation": "Department of Psychology, University of Texas at Austin, Austin, TX, USA" }, { "name": "Ted Strauss", "orcid": "0000-0002-1927-666X", "affiliation": "McConnell Brain Imaging Centre, McGill University, Montreal, QC, Canada" }, { "name": "John Lee", "orcid": "0000-0001-5884-4247", "affiliation": "Data Science and Sharing Team, National Institute of Mental Health, Bethesda, MD, USA" }, { "name": "Isaac To", "orcid": "0000-0002-4740-0824", "affiliation": "Center for Open Neuroscience, Department of Psychological and Brain Sciences, Dartmouth College, Hanover, NH, USA" }, { "name": "Christopher J. Markiewicz", "orcid": "0000-0002-6533-164X", "affiliation": "Department of Psychology, Stanford University, CA, USA" }, { "name": "Darren Lukas", "orcid": "0009-0003-6941-0833", "affiliation": "Institute for Glycomics, Griffith University, QLD, Australia" }, { "name": "Ellyn R. Butler", "orcid": "0000-0001-6316-6444", "affiliation": "Department of Psychology, Northwestern University, Evanston, IL, USA" }, { "name": "Todd Thompson", "affiliation": "Department of Brain and Cognitive Sciences, Massachusetts Institute of Technology, Cambridge, MA, USA" }, { "name": "Maite Termenon", "orcid": "0000-0001-8102-5135", "affiliation": "Biomedical Engineering Department, Faculty of Engineering, Mondragon University, Mondragon, Spain and BCBL, Basque center on Cognition, Brain and Language, San Sebastian, Spain" }, { "name": "David V. Smith", "orcid": "0000-0001-5754-9633", "affiliation": "Department of Psychology and Neuroscience, Temple University, Philadelphia, PA, USA" }, { "name": "Austin Macdonald", "orcid": "0000-0002-8124-807X", "affiliation": "Center for Open Neuroscience, Department of Psychological and Brain Sciences, Dartmouth College, Hanover, NH, USA" }, { "name": "David N. Kennedy", "orcid": "0000-0002-9377-0797", "affiliation": "Departments of Psychiatry and Radiology, University of Massachusetts Chan Medical School, Worcester, MA, USA" } ], "keywords": [ "Python", "neuroscience", "standardization", "DICOM", "BIDS", "open science", "FOSS" ], "access_right": "open", "license": "Apache-2.0", "upload_type": "software", "title": "HeuDiConv — flexible DICOM conversion into structured directory layouts" } nipy-heudiconv-217744b/CHANGELOG.md000066400000000000000000001460721517415366200165640ustar00rootroot00000000000000# v1.4.0 (Tue Apr 28 2026) #### 🚀 Enhancement - BF: support _ses-DATE as alternative to _ses-{date} for Siemens XA60 [#848](https://github.com/nipy/heudiconv/pull/848) ([@yarikoptic](https://github.com/yarikoptic)) - Allow for relative, up to 5% differences, while comparing numerics for fieldmap correspondence [#842](https://github.com/nipy/heudiconv/pull/842) ([@yarikoptic](https://github.com/yarikoptic)) #### 🐛 Bug Fix - Fix fmap rec dir ordering [#855](https://github.com/nipy/heudiconv/pull/855) ([@octomike](https://github.com/octomike)) #### 🏠 Internal - [gh-actions](deps): Bump codecov/codecov-action from 5 to 6 [#854](https://github.com/nipy/heudiconv/pull/854) ([@dependabot[bot]](https://github.com/dependabot[bot])) - [gh-actions](deps): Bump actions/checkout from 5 to 6 [#841](https://github.com/nipy/heudiconv/pull/841) ([@dependabot[bot]](https://github.com/dependabot[bot])) #### Authors: 3 - [@dependabot[bot]](https://github.com/dependabot[bot]) - Michael ([@octomike](https://github.com/octomike)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.3.4 (Wed Sep 24 2025) #### 🐛 Bug Fix - Sanitize "locator" value [#830](https://github.com/nipy/heudiconv/pull/830) ([@yarikoptic](https://github.com/yarikoptic)) - [FIX] remove XA partial volumes, generated by dcm2niix as derived data [#817](https://github.com/nipy/heudiconv/pull/817) ([@bpinsard](https://github.com/bpinsard)) - [ENH] filter dicoms earlier to avoid nibabel crash with XA ill-formed mutli-planar localizer dicoms [#806](https://github.com/nipy/heudiconv/pull/806) ([@bpinsard](https://github.com/bpinsard)) - Address typing issues (and avoid some minor code duplication) [#828](https://github.com/nipy/heudiconv/pull/828) ([@yarikoptic](https://github.com/yarikoptic)) - remove bval bvecs files generated for SBRef on XA60/CMRR/dcm2niix [#819](https://github.com/nipy/heudiconv/pull/819) ([@bpinsard](https://github.com/bpinsard)) #### 🏠 Internal - [gh-actions](deps): Bump actions/setup-python from 5 to 6 [#833](https://github.com/nipy/heudiconv/pull/833) ([@dependabot[bot]](https://github.com/dependabot[bot])) - [gh-actions](deps): Bump actions/checkout from 4 to 5 [#827](https://github.com/nipy/heudiconv/pull/827) ([@dependabot[bot]](https://github.com/dependabot[bot])) - Add claude workflow [#827](https://github.com/nipy/heudiconv/pull/827) ([@yarikoptic](https://github.com/yarikoptic)) - Use ubuntu-22.04 until we fixup NeuroDebian APT for debian and ubuntu [#825](https://github.com/nipy/heudiconv/pull/825) ([@yarikoptic](https://github.com/yarikoptic)) #### 📝 Documentation - Fix the name of the `podman` executable in the docs [#829](https://github.com/nipy/heudiconv/pull/829) ([@mih](https://github.com/mih)) - DOC: fix the description of filter_files [#823](https://github.com/nipy/heudiconv/pull/823) ([@mslw](https://github.com/mslw) [@yarikoptic](https://github.com/yarikoptic)) #### 🧪 Tests - Enable testing 3.13 [#831](https://github.com/nipy/heudiconv/pull/831) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 5 - [@dependabot[bot]](https://github.com/dependabot[bot]) - Basile ([@bpinsard](https://github.com/bpinsard)) - Michael Hanke ([@mih](https://github.com/mih)) - Michał Szczepanik ([@mslw](https://github.com/mslw)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.3.3 (Tue Mar 25 2025) #### 🐛 Bug Fix - Require nibabel >= 5.3.1 (fixes issues with enhanced DICOMs), and drop Python 3.8 support [#800](https://github.com/nipy/heudiconv/pull/800) ([@bpinsard](https://github.com/bpinsard)) - Do not create README if there is other allowed README.md or alike [#818](https://github.com/nipy/heudiconv/pull/818) ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - Minor non-consecuential changes (trailing ., add venv folders to ignore for codespell) [#820](https://github.com/nipy/heudiconv/pull/820) ([@yarikoptic](https://github.com/yarikoptic)) - [gh-actions](deps): Bump codecov/codecov-action from 4 to 5 [#805](https://github.com/nipy/heudiconv/pull/805) ([@dependabot[bot]](https://github.com/dependabot[bot])) #### 📝 Documentation - Improve formatting in custom_seqinfo doc + provide url to an example [#815](https://github.com/nipy/heudiconv/pull/815) ([@yarikoptic](https://github.com/yarikoptic)) - DOC: Replace gone pointer to "Usage" with "CLI Reference" [#811](https://github.com/nipy/heudiconv/pull/811) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 3 - [@dependabot[bot]](https://github.com/dependabot[bot]) - Basile ([@bpinsard](https://github.com/bpinsard)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.3.2 (Tue Nov 05 2024) #### 🐛 Bug Fix - chore!: drop support and testing for EOLed Python 3.8 [#801](https://github.com/nipy/heudiconv/pull/801) ([@yarikoptic](https://github.com/yarikoptic)) - Do specify filter="tar" when extracting tars [#796](https://github.com/nipy/heudiconv/pull/796) ([@yarikoptic](https://github.com/yarikoptic)) - Add RRID badge to README.rst [#796](https://github.com/nipy/heudiconv/pull/796) ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - Docker image: Use newer debian stable for the base and newer dcm2niix [#790](https://github.com/nipy/heudiconv/pull/790) ([@yarikoptic](https://github.com/yarikoptic)) #### 🧪 Tests - Add Python 3.12 into github testing CI [#799](https://github.com/nipy/heudiconv/pull/799) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.3.1 (Fri Oct 25 2024) #### 🐛 Bug Fix - Fix assignment of sensitive git-annex metadata data via glob patterns (regression introduced by #739) [#793](https://github.com/nipy/heudiconv/pull/793) ([@bpinsard](https://github.com/bpinsard)) #### Authors: 1 - Basile ([@bpinsard](https://github.com/bpinsard)) --- # v1.3.0 (Wed Oct 02 2024) #### 🚀 Enhancement - timezone aware [#780](https://github.com/nipy/heudiconv/pull/780) ([@AlanKuurstra](https://github.com/AlanKuurstra) [@yarikoptic](https://github.com/yarikoptic)) #### 🐛 Bug Fix - BF(workaround): if heuristic provided just a string and not list of types -- make it into a tuple [#787](https://github.com/nipy/heudiconv/pull/787) ([@yarikoptic](https://github.com/yarikoptic)) - Refactor create_seqinfo tiny bit to avoid duplication and add logging; and in tests to reuse list of dicom paths [#785](https://github.com/nipy/heudiconv/pull/785) ([@yarikoptic](https://github.com/yarikoptic)) - extract sequence_name from PulseSequenceName on Siemens XA** data [#753](https://github.com/nipy/heudiconv/pull/753) ([@bpinsard](https://github.com/bpinsard)) - Just INFO not WARNING if heuristic is missing intotoids [#784](https://github.com/nipy/heudiconv/pull/784) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 3 - [@AlanKuurstra](https://github.com/AlanKuurstra) - Basile ([@bpinsard](https://github.com/bpinsard)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.2.0 (Fri Sep 13 2024) #### 🚀 Enhancement - [ENH] add PlainAcquisitionLabel IntendedFor method [#768](https://github.com/nipy/heudiconv/pull/768) ([@octomike](https://github.com/octomike)) #### 🐛 Bug Fix - Fixup testing: kludge for pydicom 3.0.0 in dcmstack, ignore some warnings from nipype for python 3.12 [#782](https://github.com/nipy/heudiconv/pull/782) ([@yarikoptic](https://github.com/yarikoptic)) - Add JOSS paper badge to README.md (leading) ([@yarikoptic](https://github.com/yarikoptic)) - Provide title matching JOSS publication as the title in .zenodo.json ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - Rename s variable to curr_seqinfo in reproin heuristic [#779](https://github.com/nipy/heudiconv/pull/779) ([@tsalo](https://github.com/tsalo)) #### 📝 Documentation - run pre-commit on all files to avoid unrelated errors in other PRs. [#778](https://github.com/nipy/heudiconv/pull/778) ([@bpinsard](https://github.com/bpinsard)) #### 🧪 Tests - Run tests weekly to ensure we are still good [#781](https://github.com/nipy/heudiconv/pull/781) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 4 - Basile ([@bpinsard](https://github.com/bpinsard)) - Michael ([@octomike](https://github.com/octomike)) - Taylor Salo ([@tsalo](https://github.com/tsalo)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.1.6 (Thu Jun 06 2024) #### 🏠 Internal - Remove grants from .zenodo.json for now [#766](https://github.com/nipy/heudiconv/pull/766) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.1.5 (Thu Jun 06 2024) #### 🏠 Internal - Revert "Add CITATION.cff validation to lint workflow" [#765](https://github.com/nipy/heudiconv/pull/765) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.1.4 (Wed Jun 05 2024) #### 🏠 Internal - Replace CITATION.cff with .zenodo.json for correct (split) affiliations on Zenodo [#764](https://github.com/nipy/heudiconv/pull/764) ([@yarikoptic](https://github.com/yarikoptic)) - Codespell tuneup: no dedicated workflow (part of tox -e lint), and fix few freshly detected typos [#762](https://github.com/nipy/heudiconv/pull/762) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.1.3 (Thu May 16 2024) #### 📝 Documentation - Specify license in CITATION.cff [#760](https://github.com/nipy/heudiconv/pull/760) ([@yarikoptic](https://github.com/yarikoptic)) - Replace one rogue markdown [ref](url) to `ref `_ RST [#759](https://github.com/nipy/heudiconv/pull/759) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.1.2 (Wed May 15 2024) #### 🏠 Internal - Add CITATION.cff based on JOSS paper.md [#758](https://github.com/nipy/heudiconv/pull/758) ([@yarikoptic](https://github.com/yarikoptic)) - Add Jpeg2000 to Docker container [#747](https://github.com/nipy/heudiconv/pull/747) ([@jennan](https://github.com/jennan)) #### 📝 Documentation - Fix typo [#757](https://github.com/nipy/heudiconv/pull/757) ([@asmacdo](https://github.com/asmacdo)) #### Authors: 3 - Austin Macdonald ([@asmacdo](https://github.com/asmacdo)) - Maxime Rio ([@jennan](https://github.com/jennan)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.1.1 (Thu May 02 2024) #### 🐛 Bug Fix - Handle cases where dates/times in DICOM are empty strings, not Nones (e.g. after some anonymization) [#756](https://github.com/nipy/heudiconv/pull/756) ([@jennan](https://github.com/jennan) [@yarikoptic](https://github.com/yarikoptic)) #### Authors: 2 - Maxime Rio ([@jennan](https://github.com/jennan)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.1.0 (Wed Feb 28 2024) #### 🚀 Enhancement - Add support for a custom seqinfo to extract from DICOMs any additional metadata desired for a heuristic [#581](https://github.com/nipy/heudiconv/pull/581) ([@yarikoptic](https://github.com/yarikoptic) [@bpinsard](https://github.com/bpinsard)) - codespell: ignore "build" folder which might be on the system [#581](https://github.com/nipy/heudiconv/pull/581) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 2 - Basile ([@bpinsard](https://github.com/bpinsard)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.0.2 (Mon Feb 26 2024) #### 🐛 Bug Fix - properly remove GE multiecho bvals/bvecs [#728](https://github.com/nipy/heudiconv/pull/728) ([@bpinsard](https://github.com/bpinsard)) - datalad sensitive marking fixes [#739](https://github.com/nipy/heudiconv/pull/739) ([@bpinsard](https://github.com/bpinsard)) - Reject "Missing images" in sensor-dicoms [#735](https://github.com/nipy/heudiconv/pull/735) ([@chaselgrove](https://github.com/chaselgrove)) #### ⚠️ Pushed to `master` - Adding workflow figure ([@TheChymera](https://github.com/TheChymera)) - Added figures to master branch ([@TheChymera](https://github.com/TheChymera)) #### 🏠 Internal - auto 11.0.5 is needed to avoid hitting some "Error: fatal: ... not an integer" bug [#746](https://github.com/nipy/heudiconv/pull/746) ([@yarikoptic](https://github.com/yarikoptic)) - Fix - auto is in ~/, not in the PATH [#745](https://github.com/nipy/heudiconv/pull/745) ([@yarikoptic](https://github.com/yarikoptic)) - Make it possible to review auto version -v output during release + adjust that workflow step description [#743](https://github.com/nipy/heudiconv/pull/743) ([@yarikoptic](https://github.com/yarikoptic)) - [gh-actions](deps): Bump codecov/codecov-action from 3 to 4 [#736](https://github.com/nipy/heudiconv/pull/736) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@yarikoptic](https://github.com/yarikoptic)) - [gh-actions](deps): Bump actions/setup-python from 4 to 5 [#723](https://github.com/nipy/heudiconv/pull/723) ([@dependabot[bot]](https://github.com/dependabot[bot])) #### 📝 Documentation - Adjust wording on heuristics page -- do not claim creating some skeleton [#741](https://github.com/nipy/heudiconv/pull/741) ([@yarikoptic](https://github.com/yarikoptic)) - Document how to release and add changelog entries [#737](https://github.com/nipy/heudiconv/pull/737) ([@asmacdo](https://github.com/asmacdo) [@yarikoptic](https://github.com/yarikoptic)) - Add dianne tutorials [#734](https://github.com/nipy/heudiconv/pull/734) ([@asmacdo](https://github.com/asmacdo) [@yarikoptic](https://github.com/yarikoptic)) - Add documentation building instructions [#730](https://github.com/nipy/heudiconv/pull/730) ([@asmacdo](https://github.com/asmacdo)) - Allowing RTD to access images under the same path as README [#734](https://github.com/nipy/heudiconv/pull/734) ([@TheChymera](https://github.com/TheChymera)) - Using environment figure in about section [#730](https://github.com/nipy/heudiconv/pull/730) ([@TheChymera](https://github.com/TheChymera)) - Make README more concrete [#724](https://github.com/nipy/heudiconv/pull/724) ([@asmacdo](https://github.com/asmacdo)) #### Authors: 6 - [@chaselgrove](https://github.com/chaselgrove) - [@dependabot[bot]](https://github.com/dependabot[bot]) - Austin Macdonald ([@asmacdo](https://github.com/asmacdo)) - Basile ([@bpinsard](https://github.com/bpinsard)) - Horea Christian ([@TheChymera](https://github.com/TheChymera)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.0.1 (Fri Dec 08 2023) #### 🐛 Bug Fix - Drop Python 3.7 support [#722](https://github.com/nipy/heudiconv/pull/722) ([@yarikoptic](https://github.com/yarikoptic)) - ReproIn: give an informative assertion message when multiple values are found [#718](https://github.com/nipy/heudiconv/pull/718) ([@yarikoptic](https://github.com/yarikoptic)) - Convert assertion into a warning that we would not use dicom dir template option [#709](https://github.com/nipy/heudiconv/pull/709) ([@yarikoptic](https://github.com/yarikoptic)) - Do not demand --files for all commands, even those which do not care about it (like populate-intended-for) [#708](https://github.com/nipy/heudiconv/pull/708) ([@yarikoptic](https://github.com/yarikoptic)) #### ⚠️ Pushed to `master` - Add script to sensor dicoms -- for the error where dcm2niix might or might not fail but issues an Error ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - Ran pre-commit on everything, black decided to adjust some formatting [#721](https://github.com/nipy/heudiconv/pull/721) ([@yarikoptic](https://github.com/yarikoptic)) - Make sensor-dicoms use gnu-getopt if present (on OSX) [#721](https://github.com/nipy/heudiconv/pull/721) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v1.0.0 (Wed Sep 20 2023) #### 💥 Breaking Change - [gh-actions](deps): Bump actions/checkout from 3 to 4 [#703](https://github.com/nipy/heudiconv/pull/703) ([@dependabot[bot]](https://github.com/dependabot[bot])) #### 🚀 Enhancement - Fix inconsistent behavior of existing session when using -d compared to --files option: raise an AssertionError instead of just a warning [#682](https://github.com/nipy/heudiconv/pull/682) ([@neurorepro](https://github.com/neurorepro)) #### 🐛 Bug Fix - Various tiny enhancements flake etc demanded [#702](https://github.com/nipy/heudiconv/pull/702) ([@yarikoptic](https://github.com/yarikoptic)) - Boost claimed BIDS version to 1.8.0 from 1.4.1 [#699](https://github.com/nipy/heudiconv/pull/699) ([@yarikoptic](https://github.com/yarikoptic)) - Point to Courtois-neuromod heuristic [#702](https://github.com/nipy/heudiconv/pull/702) ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - Add codespell to lint tox env [#706](https://github.com/nipy/heudiconv/pull/706) ([@yarikoptic](https://github.com/yarikoptic)) - test-compare-two-versions.sh: also ignore differences in HeudiconvVersion field in jsons since we have it there now [#685](https://github.com/nipy/heudiconv/pull/685) ([@yarikoptic](https://github.com/yarikoptic)) #### 📝 Documentation - Add description of placeholders which could be used in the produced templates [#681](https://github.com/nipy/heudiconv/pull/681) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 3 - [@dependabot[bot]](https://github.com/dependabot[bot]) - Michael ([@neurorepro](https://github.com/neurorepro)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.13.1 (Tue May 23 2023) #### 🐛 Bug Fix - Make .subsecond optional in BIDS/DICOM datetime entries [#675](https://github.com/nipy/heudiconv/pull/675) ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - [gh-actions](deps): Bump codespell-project/actions-codespell from 1 to 2 [#677](https://github.com/nipy/heudiconv/pull/677) ([@dependabot[bot]](https://github.com/dependabot[bot])) - Replace (no longer used) Travis badge with GitHub action one (for test) [#677](https://github.com/nipy/heudiconv/pull/677) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 2 - [@dependabot[bot]](https://github.com/dependabot[bot]) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.13.0 (Mon May 08 2023) #### 🚀 Enhancement - Add type annotations [#656](https://github.com/nipy/heudiconv/pull/656) ([@jwodder](https://github.com/jwodder) [@yarikoptic](https://github.com/yarikoptic)) - ENH: Support extracting DICOMs from ZIP files (and possibly other archives) by switching to use shutil.unpack_archive instead of tarfile module functionality [#471](https://github.com/nipy/heudiconv/pull/471) ([@HippocampusGirl](https://github.com/HippocampusGirl) [@psadil](https://github.com/psadil)) - Allow filling of acq_time when AcquisitionDate AcquisitionTime missing [#614](https://github.com/nipy/heudiconv/pull/614) ([@psadil](https://github.com/psadil)) #### 🐛 Bug Fix - BF(?): make _setter images be taken as scouts - only DICOMs are saved [#570](https://github.com/nipy/heudiconv/pull/570) ([@yarikoptic](https://github.com/yarikoptic)) - Adjust .mailmap to account for mapping various folks with multiple emails so that git shortlog -sn -e provides entries without duplicates [#570](https://github.com/nipy/heudiconv/pull/570) ([@yarikoptic](https://github.com/yarikoptic)) - Make an `embed_dicom_and_nifti_metadata()` annotation work on Python 3.7 [#673](https://github.com/nipy/heudiconv/pull/673) ([@jwodder](https://github.com/jwodder)) - Merge branch 'feature_dicom_compresslevel' [#673](https://github.com/nipy/heudiconv/pull/673) ([@yarikoptic](https://github.com/yarikoptic)) - Update heudiconv/dicoms.py [#669](https://github.com/nipy/heudiconv/pull/669) ([@octomike](https://github.com/octomike)) - Don't call `logging.basicConfig()` in `__init__.py` [#659](https://github.com/nipy/heudiconv/pull/659) ([@jwodder](https://github.com/jwodder)) #### ⚠️ Pushed to `master` - Mailmapping more contributors ([@yarikoptic](https://github.com/yarikoptic)) - Adjust comment and remove trailing space flipping linting ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - Declare `custom_grouping` return type instead of casting [#671](https://github.com/nipy/heudiconv/pull/671) ([@jwodder](https://github.com/jwodder)) - Use `pydicom.dcmread()` instead of `pydicom.read_file()` [#668](https://github.com/nipy/heudiconv/pull/668) ([@jwodder](https://github.com/jwodder)) - Add `sample_nifti.json` to `.gitignore` [#663](https://github.com/nipy/heudiconv/pull/663) ([@jwodder](https://github.com/jwodder)) - Write command arguments as lists of strings instead of splitting strings on whitespace [#664](https://github.com/nipy/heudiconv/pull/664) ([@jwodder](https://github.com/jwodder)) - Add & apply pre-commit and lint job [#658](https://github.com/nipy/heudiconv/pull/658) ([@jwodder](https://github.com/jwodder)) - Fix some strings with \ (make them raw or double-\), improve pytest config: move to tox.ini, make unknown warnings into errors [#660](https://github.com/nipy/heudiconv/pull/660) ([@jwodder](https://github.com/jwodder)) - Replace py.path with pathlib [#654](https://github.com/nipy/heudiconv/pull/654) ([@jwodder](https://github.com/jwodder)) #### 🧪 Tests - Make `test_private_csa_header` test write to temp dir [#666](https://github.com/nipy/heudiconv/pull/666) ([@jwodder](https://github.com/jwodder)) #### 🔩 Dependency Updates - Replace third-party `mock` library with stdlib's `unittest.mock` [#661](https://github.com/nipy/heudiconv/pull/661) ([@jwodder](https://github.com/jwodder)) - Remove kludgy support for older versions of pydicom and dcmstack [#662](https://github.com/nipy/heudiconv/pull/662) ([@jwodder](https://github.com/jwodder)) - Remove use of `six` [#655](https://github.com/nipy/heudiconv/pull/655) ([@jwodder](https://github.com/jwodder)) #### Authors: 5 - John T. Wodder II ([@jwodder](https://github.com/jwodder)) - Lea Waller ([@HippocampusGirl](https://github.com/HippocampusGirl)) - Michael ([@octomike](https://github.com/octomike)) - Patrick Sadil ([@psadil](https://github.com/psadil)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.12.2 (Tue Mar 14 2023) #### 🏠 Internal - [DATALAD RUNCMD] produce updated dockerfile [#652](https://github.com/nipy/heudiconv/pull/652) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.12.1 (Tue Mar 14 2023) #### 🐛 Bug Fix - Re-add explicit instructions to install dcm2niix "manually" and remove it from install_requires [#651](https://github.com/nipy/heudiconv/pull/651) ([@yarikoptic](https://github.com/yarikoptic)) #### 📝 Documentation - Contributing guide. [#641](https://github.com/nipy/heudiconv/pull/641) ([@TheChymera](https://github.com/TheChymera)) - Reword and correct punctuation on installation.rst [#643](https://github.com/nipy/heudiconv/pull/643) ([@yarikoptic](https://github.com/yarikoptic) [@candleindark](https://github.com/candleindark)) #### Authors: 3 - Horea Christian ([@TheChymera](https://github.com/TheChymera)) - Isaac To ([@candleindark](https://github.com/candleindark)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.12.0 (Tue Feb 21 2023) #### 🚀 Enhancement - strip non-alphanumeric from session ids too [#647](https://github.com/nipy/heudiconv/pull/647) ([@keithcallenberg](https://github.com/keithcallenberg) [@yarikoptic](https://github.com/yarikoptic)) #### 🐛 Bug Fix - Docker images: tag also as "unstable", strip "v" prefix, and avoid building in non-release workflow for releases. [#642](https://github.com/nipy/heudiconv/pull/642) ([@yarikoptic](https://github.com/yarikoptic)) - add install link to README [#640](https://github.com/nipy/heudiconv/pull/640) ([@asmacdo](https://github.com/asmacdo)) - Setting git author and email in test environment [#631](https://github.com/nipy/heudiconv/pull/631) ([@TheChymera](https://github.com/TheChymera)) - Duecredit dcm2niix [#622](https://github.com/nipy/heudiconv/pull/622) ([@yarikoptic](https://github.com/yarikoptic)) - Do not issue warning if cannot parse _task entity [#621](https://github.com/nipy/heudiconv/pull/621) ([@yarikoptic](https://github.com/yarikoptic)) - Provide codespell config and workflow [#619](https://github.com/nipy/heudiconv/pull/619) ([@yarikoptic](https://github.com/yarikoptic)) - BF: Use .get in group_dicoms_into_seqinfos to not puke if SeriesDescription is missing [#622](https://github.com/nipy/heudiconv/pull/622) ([@yarikoptic](https://github.com/yarikoptic)) - DOC: do provide short version for sphinx [#609](https://github.com/nipy/heudiconv/pull/609) ([@yarikoptic](https://github.com/yarikoptic)) #### ⚠️ Pushed to `master` - DOC: add clarification on where docs/requirements.txt should be "installed" from ([@yarikoptic](https://github.com/yarikoptic)) - fix minor typo ([@yarikoptic](https://github.com/yarikoptic)) - DOC: fixed the comment. Original was copy/pasted from DataLad ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - dcm2niix explicitly noted as a (PyPI) dependency and removed from being installed via apt-get etc [#628](https://github.com/nipy/heudiconv/pull/628) ([@TheChymera](https://github.com/TheChymera) [@yarikoptic](https://github.com/yarikoptic)) #### 📝 Documentation - Reword number of intended ideas in README.rst [#639](https://github.com/nipy/heudiconv/pull/639) ([@candleindark](https://github.com/candleindark)) - Add a bash anon-cmd to be used to incrementally anonymize sids [#615](https://github.com/nipy/heudiconv/pull/615) ([@yarikoptic](https://github.com/yarikoptic)) - Reword and correct punctuation on usage.rst [#644](https://github.com/nipy/heudiconv/pull/644) ([@candleindark](https://github.com/candleindark) [@yarikoptic](https://github.com/yarikoptic)) - Clarify the infotodict function [#645](https://github.com/nipy/heudiconv/pull/645) ([@yarikoptic](https://github.com/yarikoptic)) - Added distribution badges [#632](https://github.com/nipy/heudiconv/pull/632) ([@TheChymera](https://github.com/TheChymera)) - Capitalize sentences and end sentences with period [#629](https://github.com/nipy/heudiconv/pull/629) ([@candleindark](https://github.com/candleindark)) - Tune up .mailmap to harmonize Pablo, Dae and Mathias [#629](https://github.com/nipy/heudiconv/pull/629) ([@yarikoptic](https://github.com/yarikoptic)) - Add HOWTO 101 section, with references to ReproIn to README.rst [#623](https://github.com/nipy/heudiconv/pull/623) ([@yarikoptic](https://github.com/yarikoptic)) - minor fix -- Fix use of code:: directive [#623](https://github.com/nipy/heudiconv/pull/623) ([@yarikoptic](https://github.com/yarikoptic)) #### 🧪 Tests - Add 3.11 to be tested etc [#635](https://github.com/nipy/heudiconv/pull/635) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 5 - Austin Macdonald ([@asmacdo](https://github.com/asmacdo)) - Horea Christian ([@TheChymera](https://github.com/TheChymera)) - Isaac To ([@candleindark](https://github.com/candleindark)) - Keith Callenberg ([@keithcallenberg](https://github.com/keithcallenberg)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.11.6 (Thu Nov 03 2022) #### 🏠 Internal - Delete .dockerignore [#607](https://github.com/nipy/heudiconv/pull/607) ([@jwodder](https://github.com/jwodder)) #### 📝 Documentation - DOC: Various fixes to make RTD build the docs again [#608](https://github.com/nipy/heudiconv/pull/608) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 2 - John T. Wodder II ([@jwodder](https://github.com/jwodder)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.11.5 (Thu Nov 03 2022) #### 🐛 Bug Fix - Fix certificate issue as indicated in #595 [#597](https://github.com/nipy/heudiconv/pull/597) ([@neurorepro](https://github.com/neurorepro)) - BF docker build: use python3.9 (not 3.7 which gets upgraded to 3.9) and newer dcm2niix [#596](https://github.com/nipy/heudiconv/pull/596) ([@yarikoptic](https://github.com/yarikoptic)) - Fixup miniconda spec for neurodocker so it produces dockerfile now [#596](https://github.com/nipy/heudiconv/pull/596) ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - Update GitHub Actions action versions [#601](https://github.com/nipy/heudiconv/pull/601) ([@jwodder](https://github.com/jwodder)) - Set action step outputs via $GITHUB_OUTPUT [#600](https://github.com/nipy/heudiconv/pull/600) ([@jwodder](https://github.com/jwodder)) #### 📝 Documentation - DOC: codespell fix a few typos in code comments [#605](https://github.com/nipy/heudiconv/pull/605) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 3 - John T. Wodder II ([@jwodder](https://github.com/jwodder)) - Michael ([@neurorepro](https://github.com/neurorepro)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.11.4 (Thu Sep 29 2022) #### 🐛 Bug Fix - install dcmstack straight from github until it is released [#593](https://github.com/nipy/heudiconv/pull/593) ([@yarikoptic](https://github.com/yarikoptic)) - DOC: provide rudimentary How to contribute section in README.rst ([@yarikoptic](https://github.com/yarikoptic)) #### ⚠️ Pushed to `master` - Check out a full clone when testing ([@jwodder](https://github.com/jwodder)) - Convert Travis workflow to GitHub Actions ([@jwodder](https://github.com/jwodder)) - BF(docker): replace old -tipsy with -y -all for conda clean as neurodocker does now ([@yarikoptic](https://github.com/yarikoptic)) - adjusted script for neurodocker although it does not work ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - 0.9 of dcmstack was released, no need for github version [#594](https://github.com/nipy/heudiconv/pull/594) ([@yarikoptic](https://github.com/yarikoptic)) - Minor face-lifts to ReproIn: align doc and code better to BIDS terms, address deprecation warnings etc [#569](https://github.com/nipy/heudiconv/pull/569) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 2 - John T. Wodder II ([@jwodder](https://github.com/jwodder)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.11.3 (Thu May 12 2022) #### 🏠 Internal - BF: add recently tests data missing from distribution [#567](https://github.com/nipy/heudiconv/pull/567) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.11.2 (Thu May 12 2022) #### 🏠 Internal - Make versioningit write version to file; make setup.py read version as fallback [#566](https://github.com/nipy/heudiconv/pull/566) ([@jwodder](https://github.com/jwodder)) - BF: add fetch-depth: 0 to get all tags into docker builds of master [#566](https://github.com/nipy/heudiconv/pull/566) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 2 - John T. Wodder II ([@jwodder](https://github.com/jwodder)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.11.1 (Tue May 10 2022) #### 🏠 Internal - Remove .git/ from .dockerignore so that versioning works while building docker image [#564](https://github.com/nipy/heudiconv/pull/564) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 1 - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # v0.11.0 (Tue May 10 2022) #### 🚀 Enhancement - RF: drop Python 3.6 (EOLed), fix dcm2niix version in neurodocker script [#555](https://github.com/nipy/heudiconv/pull/555) ([@yarikoptic](https://github.com/yarikoptic)) - ENH: Adds populate_intended_for for fmaps [#482](https://github.com/nipy/heudiconv/pull/482) ([@pvelasco](https://github.com/pvelasco) [@yarikoptic](https://github.com/yarikoptic) bids@dbic.dartmouth.edu [@neurorepro](https://github.com/neurorepro)) #### 🐛 Bug Fix - bids_ME heuristic: add test for the dataset that raised #541, add support for MEGRE [#547](https://github.com/nipy/heudiconv/pull/547) ([@pvelasco](https://github.com/pvelasco) [@yarikoptic](https://github.com/yarikoptic)) - reproin heuristic: specify POPULATE_INTENDED_FOR_OPTS [#546](https://github.com/nipy/heudiconv/pull/546) ([@yarikoptic](https://github.com/yarikoptic)) - FIX: Convert sets to lists for filename updaters [#461](https://github.com/nipy/heudiconv/pull/461) ([@tsalo](https://github.com/tsalo)) - Added new infofilestyle compatible with BIDS [#12](https://github.com/nipy/heudiconv/pull/12) ([@chrisgorgo](https://github.com/chrisgorgo)) - try a simple fix for wrongly ordered files in tar file [#535](https://github.com/nipy/heudiconv/pull/535) ([@bpinsard](https://github.com/bpinsard)) - BF: Fix the order of the 'echo' entity in the filename [#542](https://github.com/nipy/heudiconv/pull/542) ([@pvelasco](https://github.com/pvelasco)) - ENH: add HeudiconvVersion to sidecar .json files [#529](https://github.com/nipy/heudiconv/pull/529) ([@yarikoptic](https://github.com/yarikoptic)) - BF (TST): make anonymize_script actually output anything and map deterministically [#511](https://github.com/nipy/heudiconv/pull/511) ([@yarikoptic](https://github.com/yarikoptic)) - Rename DICOMCONVERT_README.md to README.md [#4](https://github.com/nipy/heudiconv/pull/4) ([@satra](https://github.com/satra)) #### ⚠️ Pushed to `master` - Dockerfile - use bullseye for the base and fresh dcm2niix ([@yarikoptic](https://github.com/yarikoptic)) #### 🏠 Internal - Run codespell on some obvious typos [#563](https://github.com/nipy/heudiconv/pull/563) ([@yarikoptic](https://github.com/yarikoptic)) - Set up auto [#558](https://github.com/nipy/heudiconv/pull/558) ([@jwodder](https://github.com/jwodder) [@yarikoptic](https://github.com/yarikoptic)) #### 🧪 Tests - BF(TST): use caplog to control logging level, use python3 in shebang [#553](https://github.com/nipy/heudiconv/pull/553) ([@yarikoptic](https://github.com/yarikoptic)) - BF(TST): use caplog instead of capfd for testing if we log a warning [#534](https://github.com/nipy/heudiconv/pull/534) ([@yarikoptic](https://github.com/yarikoptic)) - Travis - Use bionic for the base [#533](https://github.com/nipy/heudiconv/pull/533) ([@yarikoptic](https://github.com/yarikoptic)) #### Authors: 9 - Basile ([@bpinsard](https://github.com/bpinsard)) - Chris Gorgolewski ([@chrisgorgo](https://github.com/chrisgorgo)) - DBIC BIDS Team (bids@dbic.dartmouth.edu) - John T. Wodder II ([@jwodder](https://github.com/jwodder)) - Michael ([@neurorepro](https://github.com/neurorepro)) - Pablo Velasco ([@pvelasco](https://github.com/pvelasco)) - Satrajit Ghosh ([@satra](https://github.com/satra)) - Taylor Salo ([@tsalo](https://github.com/tsalo)) - Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic)) --- # [0.10.0] - 2021-09-16 Various improvements and compatibility/support (dcm2niix, datalad) changes. ## Added - Add "AcquisitionTime" to the seqinfo ([#487][]) - Add support for saving the Phoenix Report in the sourcedata folder ([#489][]) ## Changed - Python 3.5 EOLed, supported (tested) versions now: 3.6 - 3.9 - In reprorin heuristic, allow for having multiple accessions since now there is `-g all` grouping ([#508][]) - For BIDS, produce a singular `scans.json` at the top level, and not one per sub/ses (generates too many identical files) ([#507][]) ## Fixed - Compatibility with DataLad 0.15.0. Minimal version is 0.13.0 now. - Try to open top level BIDS .json files a number of times for adjustment, so in the case of competition across parallel processes, they just end up with the last one "winning over" ([#523][]) - Don't fail if etelemetry.get_project returns None ([#501][]) - Consistently use `n/a` for age/sex, also handle ?M for months ([#500][]) - To avoid crashing on unrelated derivatives files etc, make `find_files` to take list of topdirs (excluding `derivatives/` etc), and look for _bold only under sub-* directories ([#496][]) - Ensure bvec/bval files are only created for dwi output ([#491][]) ## Removed - In reproin heuristic, old hardcoded sequence renamings and filters ([#508][]) # [0.9.0] - 2020-12-23 Various improvements and compatibility/support (dcm2niix, datalad, duecredit) changes. Major change is placement of output files to the target output directory during conversion. ## Added - #454 zenodo referencing in README.rst and support for ducredit for heudiconv and reproin heuristic - #445 more tutorial references in README.md ## Changed - [#485][] placed files during conversion right away into the target directory (with a `_heudiconv???` suffix, renamed into ultimate target name later on), which avoids hitting file size limits of /tmp ([#481][]) and helped to avoid a regression in dcm2nixx 1.0.20201102 - [#477][] replaced `rec-` with `part-` now hat BIDSsupports the part entity - [#473][] made default for CogAtlasID to be a TODO URL - [#459][] made AcquisitionTime used for acq_time scans file field - [#451][] retained sub-second resolution in scans files - [#442][] refactored code so there is now heudiconv.main.workflow for more convenient use as a Python module ## Fixed - minimal version of nipype set to 1.2.3 to guarantee correct handling of DWI files ([#480][]) - `heudiconvDCM*` temporary directories are removed now ([#462][]) - compatibility with DataLad 0.13 ([#464][]) ## Removed - #443 pathlib as a dependency (we are Python3 only now) # [0.8.0] - 2020-04-15 ## Enhancements - Centralized saving of .json files. Indentation of some files could change now from previous versions where it could have used `3` spaces. Now indentation should be consistently `2` for .json files we produce/modify ([#436][]) (note: dcm2niix uses tabs for indentation) - ReproIn heuristic: support SBRef and phase data ([#387][]) - Set the "TaskName" field in .json sidecar files for multi-echo data ([#420][]) - Provide an informative exception if command needs heuristic to be specified ([#437][]) ## Refactored - `embed_nifti` was refactored into `embed_dicom_and_nifti_metadata` which would no longer create `.nii` file if it does not exist already ([#432][]) ## Fixed - Skip datalad-based tests if no datalad available ([#430][]) - Search heuristic file path first so we do not pick up a python module if name conflicts ([#434][]) # [0.7.0] - 2020-03-20 ## Removed - Python 2 support/testing ## Enhancement - `-g` option obtained two new modes: `all` and `custom`. In case of `all`, all provided DICOMs will be treated as coming from a single scanning session. `custom` instructs to use `.grouping` value (could be a DICOM attribute or a callable)provided by the heuristic ([#359][]). - Stop before reading pixels data while gathering metadata from DICOMs ([#404][]) - reproin heuristic: - In addition to original "md5sum of the study_description" `protocols2fix` could now have (and applied after md5sum matching ones) 1). a regular expression searched in study_description, 2). an empty string as "catch all". This features could be used to easily provide remapping into reproin naming (documentation is to come to http://github.com/ReproNim/reproin) ([#425][]) ## Fixed - Use nan, not None for absent echo value in sorting - reproin heuristic: case seqinfos into a list to be able to modify from overloaded heuristic ([#419][]) - No spurious errors from the logger upon a warning about `etelemetry` absence ([#407][]) # [0.6.0] - 2019-12-16 This is largely a bug fix. Metadata and order of `_key-value` fields in BIDS could change from the result of converting using previous versions, thus minor version boost. 14 people contributed to this release -- thanks [everyone](https://github.com/nipy/heudiconv/graphs/contributors)! ## Enhancement - Use [etelemetry](https://pypi.org/project/etelemetry) to inform about most recent available version of heudiconv. Please set `NO_ET` environment variable if you want to disable it ([#369][]) - BIDS: - `--bids` flag became an option. It can (optionally) accept `notop` value to avoid creation of top level files (`CHANGES`, `dataset_description.json`, etc) as a workaround during parallel execution to avoid race conditions etc. ([#344][]) - Generate basic `.json` files with descriptions of the fields for `participants.tsv` and `_scans.tsv` files ([#376][]) - Use `filelock` while writing top level files. Use `HEUDICONV_FILELOCK_TIMEOUT` environment to change the default timeout value ([#348][]) - `_PDT2` was added as a suffix for multi-echo (really "multi-modal") sequences ([#345][]) - Calls to `dcm2niix` would include full output path to make it easier to discern in the logs what file it is working on ([#351][]) - With recent [datalad]() (>= 0.10), created DataLad dataset will use `--fake-dates` functionality of DataLad to not leak data conversion dates, which might be close to actual data acquisition/patient visit ([#352][]) - Support multi-echo EPI `_phase` data ([#373][] fixes [#368][]) - Log location of a bad .json file to ease troubleshooting ([#379][]) - Add basic pypi classifiers for the package ([#380][]) ## Fixed - Sorting `_scans.tsv` files lacking valid dates field should not cause a crash ([#337][]) - Multi-echo files detection based number of echos ([#339][]) - BIDS - Use `EchoTimes` from the associated multi-echo files if `EchoNumber` tag is missing ([#366][] fixes [#347][]) - Tolerate empty ContentTime and/or ContentDate in DICOMs ([#372][]) and place "n/a" if value is missing ([#390][]) - Do not crash and store original .json file is "JSON pretification" fails ([#342][]) - ReproIn heuristic - tolerate WIP prefix on Philips scanners ([#343][]) - allow for use of `(...)` instead of `{...}` since `{}` are not allowed ([#343][]) - Support pipolar fieldmaps by providing them with `_epi` not `_magnitude`. "Loose" BIDS `_key-value` pairs might come now after `_dir-` even if they came first before ([#358][] fixes [#357][]) - All heuristics saved under `.heudiconv/` under `heuristic.py` name, to avoid discrepancy during reconversion ([#354][] fixes [#353][]) - Do not crash (with TypeError) while trying to sort absent file list ([#360][]) - heudiconv requires nipype >= 1.0.0 ([#364][]) and blacklists `1.2.[12]` ([#375][]) # [0.5.4] - 2019-04-29 This release includes fixes to BIDS multi-echo conversions, the re-implementation of queuing support (currently just SLURM), as well as some bugfixes. Starting today, we will (finally) push versioned releases to DockerHub. Finally, to more accurately reflect on-going development, the `latest` tag has been renamed to `unstable`. ## Added - Readthedocs documentation ([#327][]) ## Changed - Update Docker dcm2niix to v.1.0.20190410 ([#334][]) - Allow usage of `--files` with basic heuristics. This requires use of `--subject` flag, and is limited to one subject. ([#293][]) ## Deprecated ## Fixed - Improve support for multiple `--queue-args` ([#328][]) - Fixed an issue where generated BIDS sidecar files were missing additional information - treating all conversions as if the `--minmeta` flag was used ([#306][]) - Re-enable SLURM queuing support ([#304][]) - BIDS multi-echo support for EPI + T1 images ([#293][]) - Correctly handle the case when `outtype` of heuristic has "dicom" before '.nii.gz'. Previously would have lead to absent additional metadata extraction etc ([#310][]) ## Removed - `--sbargs` argument was renamed to `--queue-args` ([#304][]) ## Security # [0.5.3] - 2019-01-12 Minor hot bugfix release ## Fixed - Do not shorten spaces in the dates while pretty printing .json # [0.5.2] - 2019-01-04 A variety of bugfixes ## Changed - Reproin heuristic: `__dup` indices would now be assigned incrementally individually per each sequence, so there is a chance to properly treat associate for multi-file (e.g. `fmap`) sequences - Reproin heuristic: also split StudyDescription by space not only by ^ - `tests/` moved under `heudiconv/tests` to ease maintenance and facilitate testing of an installed heudiconv - Protocol name will also be accessed from private Siemens csa.tProtocolName header field if not present in public one - nipype>=0.12.0 is required now ## Fixed - Multiple files produced by dcm2niix are first sorted to guarantee correct order e.g. of magnitude files in fieldmaps, which otherwise resulted in incorrect according to BIDS ordering of them - Aggregated top level .json files now would contain only the fields with the same values from all scanned files. In prior versions, those files were not regenerated after an initial conversion - Unicode handling in anonimization scripts # [0.5.1] - 2018-07-05 Bugfix release ## Added - Video tutorial / updated slides - Helper to set metadata restrictions correctly - Usage is now shown when run without arguments - New fields to Seqinfo - series_uid - Reproin heuristic support for xnat ## Changed - Dockerfile updated to use `dcm2niix v1.0.20180622` - Conversion table will be regenerated if heurisic has changed - Do not touch existing BIDS files - events.tsv - task JSON ## Fixed - Python 2.7.8 and older installation - Support for updated packages - `Datalad` 0.10 - `pydicom` 1.0.2 - Later versions of `pydicom` are prioritized first - JSON pretty print should not remove spaces - Phasediff fieldmaps behavior - ensure phasediff exists - support for single magnitude acquisitions # [0.5] - 2018-03-01 The first release after major refactoring: ## Changed - Refactored into a proper `heudiconv` Python module - `heuristics` is now a `heudiconv.heuristics` submodule - you can specify shipped heuristics by name (e.g. `-f reproin`) without providing full path to their files - you need to use `--files` (not just positional argument(s)) if not using `--dicom_dir_templates` or `--subjects` to point to data files or directories with input DICOMs - `Dockerfile` is generated by [neurodocker](https://github.com/kaczmarj/neurodocker) - Logging verbosity reduced - Increased leniency with missing DICOM fields - `dbic_bids` heuristic renamed into reproin ## Added - [LICENSE](https://github.com/nipy/heudiconv/blob/master/LICENSE) with Apache 2.0 license for the project - [CHANGELOG.md](https://github.com/nipy/heudiconv/blob/master/CHANGELOG.md) - [Regression testing](https://github.com/nipy/heudiconv/blob/master/tests/test_regression.py) on real data (using datalad) - A dedicated [ReproIn](https://github.com/repronim/reproin) project with details about ReproIn setup/specification and operation using `reproin` heuristic shipped with heudiconv - [utils/test-compare-two-versions.sh](utils/test-compare-two-versions.sh) helper to compare conversions with two different versions of heudiconv ## Removed - Support for converters other than `dcm2niix`, which is now the default. Explicitly specify `-c none` to only prepare conversion specification files without performing actual conversion ## Fixed - Compatibility with Nipype 1.0, PyDicom 1.0, and upcoming DataLad 0.10 - Consistency with converted files permissions - Ensured subject id for BIDS conversions will be BIDS compliant - Re-add `seqinfo` fields as column names in generated `dicominfo` - More robust sanity check of the regex reformatted .json file to avoid numeric precision issues - Many other various issues # [0.4] - 2017-10-15 A usable release to support [DBIC][] use-case ## Added - more testing ## Changes - Dockerfile updates (added pigz, progressed forward [dcm2niix][]) ## Fixed - correct date/time in BIDS `_scans` files - sort entries in `_scans` by date and then filename # [0.3] - 2017-07-10 A somewhat working release on the way to support [DBIC][] use-case ## Added - more tests - grouping of dicoms by series if provided - many more features and fixes # [0.2] - 2016-10-20 An initial release on the way to support [DBIC][] use-case ## Added - basic Python project assets (`setup.py`, etc) - basic tests - [datalad][] support - dbic_bids heuristic - `--dbg` command line flag to enter `pdb` environment upon failure # Fixed - Better Python3 support - Better PEP8 compliance # [0.1] - 2015-09-23 Initial version --- ## References [DBIC]: http://dbic.dartmouth.edu [datalad]: http://datalad.org [dcm2niix]: https://github.com/rordenlab/dcm2niix [#301]: https://github.com/nipy/heudiconv/issues/301 [#353]: https://github.com/nipy/heudiconv/issues/353 [#354]: https://github.com/nipy/heudiconv/issues/354 [#357]: https://github.com/nipy/heudiconv/issues/357 [#358]: https://github.com/nipy/heudiconv/issues/358 [#347]: https://github.com/nipy/heudiconv/issues/347 [#366]: https://github.com/nipy/heudiconv/issues/366 [#368]: https://github.com/nipy/heudiconv/issues/368 [#373]: https://github.com/nipy/heudiconv/issues/373 [#485]: https://github.com/nipy/heudiconv/issues/485 [#442]: https://github.com/nipy/heudiconv/issues/442 [#451]: https://github.com/nipy/heudiconv/issues/451 [#459]: https://github.com/nipy/heudiconv/issues/459 [#473]: https://github.com/nipy/heudiconv/issues/473 [#477]: https://github.com/nipy/heudiconv/issues/477 [#293]: https://github.com/nipy/heudiconv/issues/293 [#304]: https://github.com/nipy/heudiconv/issues/304 [#306]: https://github.com/nipy/heudiconv/issues/306 [#310]: https://github.com/nipy/heudiconv/issues/310 [#327]: https://github.com/nipy/heudiconv/issues/327 [#328]: https://github.com/nipy/heudiconv/issues/328 [#334]: https://github.com/nipy/heudiconv/issues/334 [#337]: https://github.com/nipy/heudiconv/issues/337 [#339]: https://github.com/nipy/heudiconv/issues/339 [#342]: https://github.com/nipy/heudiconv/issues/342 [#343]: https://github.com/nipy/heudiconv/issues/343 [#344]: https://github.com/nipy/heudiconv/issues/344 [#345]: https://github.com/nipy/heudiconv/issues/345 [#348]: https://github.com/nipy/heudiconv/issues/348 [#351]: https://github.com/nipy/heudiconv/issues/351 [#352]: https://github.com/nipy/heudiconv/issues/352 [#359]: https://github.com/nipy/heudiconv/issues/359 [#360]: https://github.com/nipy/heudiconv/issues/360 [#364]: https://github.com/nipy/heudiconv/issues/364 [#369]: https://github.com/nipy/heudiconv/issues/369 [#372]: https://github.com/nipy/heudiconv/issues/372 [#375]: https://github.com/nipy/heudiconv/issues/375 [#376]: https://github.com/nipy/heudiconv/issues/376 [#379]: https://github.com/nipy/heudiconv/issues/379 [#380]: https://github.com/nipy/heudiconv/issues/380 [#387]: https://github.com/nipy/heudiconv/issues/387 [#390]: https://github.com/nipy/heudiconv/issues/390 [#404]: https://github.com/nipy/heudiconv/issues/404 [#407]: https://github.com/nipy/heudiconv/issues/407 [#419]: https://github.com/nipy/heudiconv/issues/419 [#420]: https://github.com/nipy/heudiconv/issues/420 [#425]: https://github.com/nipy/heudiconv/issues/425 [#430]: https://github.com/nipy/heudiconv/issues/430 [#432]: https://github.com/nipy/heudiconv/issues/432 [#434]: https://github.com/nipy/heudiconv/issues/434 [#436]: https://github.com/nipy/heudiconv/issues/436 [#437]: https://github.com/nipy/heudiconv/issues/437 [#462]: https://github.com/nipy/heudiconv/issues/462 [#464]: https://github.com/nipy/heudiconv/issues/464 [#480]: https://github.com/nipy/heudiconv/issues/480 [#481]: https://github.com/nipy/heudiconv/issues/481 [#487]: https://github.com/nipy/heudiconv/issues/487 [#489]: https://github.com/nipy/heudiconv/issues/489 [#491]: https://github.com/nipy/heudiconv/issues/491 [#496]: https://github.com/nipy/heudiconv/issues/496 [#500]: https://github.com/nipy/heudiconv/issues/500 [#501]: https://github.com/nipy/heudiconv/issues/501 [#507]: https://github.com/nipy/heudiconv/issues/507 [#508]: https://github.com/nipy/heudiconv/issues/508 [#523]: https://github.com/nipy/heudiconv/issues/523 nipy-heudiconv-217744b/CONTRIBUTING.rst000066400000000000000000000114371517415366200174100ustar00rootroot00000000000000========================= Contributing to HeuDiConv ========================= Files organization ------------------ * `heudiconv/ <./heudiconv>`_ is the main Python module where major development is happening, with major submodules being: - ``cli/`` - wrappers and argument parsers bringing the HeuDiConv functionality to the command line. - ``external/`` - general compatibility layers for external functions HeuDiConv depends on. - ``heuristics/`` - heuristic evaluators for workflows, pull requests here are particularly welcome. * `docs/ <./docs>`_ - documentation directory. * `utils/ <./utils>`_ - helper utilities used during development, testing, and distribution of HeuDiConv. How to contribute ----------------- The preferred way to contribute to the HeuDiConv code base is to fork the `main repository `_ on GitHub. If you are unsure what that means, here is a set-up workflow you may wish to follow: 0. Fork the `project repository `_ on GitHub, by clicking on the “Fork” button near the top of the page — this will create a copy of the repository writeable by your GitHub user. 1. Set up a clone of the repository on your local machine and connect it to both the “official” and your copy of the repository on GitHub:: git clone git://github.com/nipy/heudiconv cd heudiconv git remote rename origin official git remote add origin git://github.com/YOUR_GITHUB_USERNAME/heudiconv 2. When you wish to start a new contribution, create a new branch:: git checkout -b topic_of_your_contribution 3. When you are done making the changes you wish to contribute, record them in Git:: git add the/paths/to/files/you/modified can/be/more/than/one git commit 3. Push the changes to your copy of the code on GitHub, following which Git will provide you with a link which you can click to initiate a pull request:: git push -u origin topic_of_your_contribution (If any of the above seems overwhelming, you can look up the `Git documentation `_ on the web.) Releases and Changelog ---------------------- HeuDiConv uses the `auto `_ tool to generate the changelog and automatically release the project. `auto` is used in the HeuDiConv GitHub actions, which monitors the labels on the pull request. HeuDiConv automation can add entries to the changelog, cut releases, and push new images to `dockerhub `_. The following pull request labels are respected: * major: Increment the major version when merged * minor: Increment the minor version when merged * patch: Increment the patch version when merged * skip-release: Preserve the current version when merged * release: Create a release when this pr is merged * internal: Changes only affect the internal API * documentation: Changes only affect the documentation * tests: Add or improve existing tests * dependencies: Update one or more dependencies version * performance: Improve performance of an existing feature Development environment ----------------------- We support Python 3 only (>= 3.7). Dependencies which you will need are `listed in the repository `_. Note that you will likely have these will already be available on your system if you used a package manager (e.g. Debian's ``apt-get``, Gentoo's ``emerge``, or simply PIP) to install the software. Development work might require live access to the copy of HeuDiConv which is being developed. If a system-wide release of HeuDiConv is already installed, or likely to be, it is best to keep development work sandboxed inside a dedicated virtual environment. This is best accomplished via:: cd /path/to/your/clone/of/heudiconv mkdir -p venvs/dev python -m venv venvs/dev source venvs/dev/bin/activate pip install -e .[all] Documentation ------------- To contribute to the documentation, we recommend building the docs locally prior to submitting a patch. To build the docs locally: 1. From the root of the heudiconv repository, `pip install -r docs/requirements.txt` 2. From the `docs/` directory, run `make html` Additional Hints ---------------- It is recommended to check that your contribution complies with the following rules before submitting a pull request: * All public functions (i.e. functions whose name does not start with an underscore) should have informative docstrings with sample usage presented as doctests when appropriate. * Docstrings are formatted in `NumPy style `_. * Lines are no longer than 120 characters. * All tests still pass:: cd /path/to/your/clone/of/heudiconv pytest -vvs . * New code should be accompanied by new tests. nipy-heudiconv-217744b/Dockerfile000066400000000000000000000154221517415366200167370ustar00rootroot00000000000000# Generated by Neurodocker and Reproenv. FROM neurodebian:bookworm ENV PATH="/opt/dcm2niix-v1.0.20240202/bin:$PATH" RUN apt-get update -qq \ && apt-get install -y -q --no-install-recommends \ ca-certificates \ cmake \ g++ \ gcc \ git \ make \ pigz \ zlib1g-dev \ && rm -rf /var/lib/apt/lists/* \ && git clone https://github.com/rordenlab/dcm2niix /tmp/dcm2niix \ && cd /tmp/dcm2niix \ && git fetch --tags \ && git checkout v1.0.20240202 \ && mkdir /tmp/dcm2niix/build \ && cd /tmp/dcm2niix/build \ && cmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON -DCMAKE_INSTALL_PREFIX:PATH=/opt/dcm2niix-v1.0.20240202 .. \ && make -j1 \ && make install \ && rm -rf /tmp/dcm2niix RUN apt-get update -qq \ && apt-get install -y -q --no-install-recommends \ gcc \ git \ git-annex-standalone \ libc-dev \ liblzma-dev \ netbase \ pigz \ && rm -rf /var/lib/apt/lists/* COPY [".", \ "/src/heudiconv"] ENV CONDA_DIR="/opt/miniconda-py39_4.12.0" \ PATH="/opt/miniconda-py39_4.12.0/bin:$PATH" RUN apt-get update -qq \ && apt-get install -y -q --no-install-recommends \ bzip2 \ ca-certificates \ curl \ && rm -rf /var/lib/apt/lists/* \ # Install dependencies. && export PATH="/opt/miniconda-py39_4.12.0/bin:$PATH" \ && echo "Downloading Miniconda installer ..." \ && conda_installer="/tmp/miniconda.sh" \ && curl -fsSL -o "$conda_installer" https://repo.continuum.io/miniconda/Miniconda3-py39_4.12.0-Linux-x86_64.sh \ && bash "$conda_installer" -b -p /opt/miniconda-py39_4.12.0 \ && rm -f "$conda_installer" \ # Prefer packages in conda-forge && conda config --system --prepend channels conda-forge \ # Packages in lower-priority channels not considered if a package with the same # name exists in a higher priority channel. Can dramatically speed up installations. # Conda recommends this as a default # https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-channels.html && conda config --set channel_priority strict \ && conda config --system --set auto_update_conda false \ && conda config --system --set show_channel_urls true \ # Enable `conda activate` && conda init bash \ && conda install -y --name base \ "python=3.9" \ "traits>=4.6.0" \ "scipy" \ "numpy" \ "nomkl" \ "pandas" \ "gdcm" \ && bash -c "source activate base \ && python -m pip install --no-cache-dir --editable \ "/src/heudiconv[all]"" \ # Clean up && sync && conda clean --all --yes && sync \ && rm -rf ~/.cache/pip/* ENTRYPOINT ["heudiconv"] # Save specification to JSON. RUN printf '{ \ "pkg_manager": "apt", \ "existing_users": [ \ "root" \ ], \ "instructions": [ \ { \ "name": "from_", \ "kwds": { \ "base_image": "neurodebian:bookworm" \ } \ }, \ { \ "name": "env", \ "kwds": { \ "PATH": "/opt/dcm2niix-v1.0.20240202/bin:$PATH" \ } \ }, \ { \ "name": "run", \ "kwds": { \ "command": "apt-get update -qq\\napt-get install -y -q --no-install-recommends \\\\\\n ca-certificates \\\\\\n cmake \\\\\\n g++ \\\\\\n gcc \\\\\\n git \\\\\\n make \\\\\\n pigz \\\\\\n zlib1g-dev\\nrm -rf /var/lib/apt/lists/*\\ngit clone https://github.com/rordenlab/dcm2niix /tmp/dcm2niix\\ncd /tmp/dcm2niix\\ngit fetch --tags\\ngit checkout v1.0.20240202\\nmkdir /tmp/dcm2niix/build\\ncd /tmp/dcm2niix/build\\ncmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON -DCMAKE_INSTALL_PREFIX:PATH=/opt/dcm2niix-v1.0.20240202 ..\\nmake -j1\\nmake install\\nrm -rf /tmp/dcm2niix" \ } \ }, \ { \ "name": "install", \ "kwds": { \ "pkgs": [ \ "git", \ "gcc", \ "pigz", \ "liblzma-dev", \ "libc-dev", \ "git-annex-standalone", \ "netbase" \ ], \ "opts": null \ } \ }, \ { \ "name": "run", \ "kwds": { \ "command": "apt-get update -qq \\\\\\n && apt-get install -y -q --no-install-recommends \\\\\\n gcc \\\\\\n git \\\\\\n git-annex-standalone \\\\\\n libc-dev \\\\\\n liblzma-dev \\\\\\n netbase \\\\\\n pigz \\\\\\n && rm -rf /var/lib/apt/lists/*" \ } \ }, \ { \ "name": "copy", \ "kwds": { \ "source": [ \ ".", \ "/src/heudiconv" \ ], \ "destination": "/src/heudiconv" \ } \ }, \ { \ "name": "env", \ "kwds": { \ "CONDA_DIR": "/opt/miniconda-py39_4.12.0", \ "PATH": "/opt/miniconda-py39_4.12.0/bin:$PATH" \ } \ }, \ { \ "name": "run", \ "kwds": { \ "command": "apt-get update -qq\\napt-get install -y -q --no-install-recommends \\\\\\n bzip2 \\\\\\n ca-certificates \\\\\\n curl\\nrm -rf /var/lib/apt/lists/*\\n# Install dependencies.\\nexport PATH=\\"/opt/miniconda-py39_4.12.0/bin:$PATH\\"\\necho \\"Downloading Miniconda installer ...\\"\\nconda_installer=\\"/tmp/miniconda.sh\\"\\ncurl -fsSL -o \\"$conda_installer\\" https://repo.continuum.io/miniconda/Miniconda3-py39_4.12.0-Linux-x86_64.sh\\nbash \\"$conda_installer\\" -b -p /opt/miniconda-py39_4.12.0\\nrm -f \\"$conda_installer\\"\\n# Prefer packages in conda-forge\\nconda config --system --prepend channels conda-forge\\n# Packages in lower-priority channels not considered if a package with the same\\n# name exists in a higher priority channel. Can dramatically speed up installations.\\n# Conda recommends this as a default\\n# https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-channels.html\\nconda config --set channel_priority strict\\nconda config --system --set auto_update_conda false\\nconda config --system --set show_channel_urls true\\n# Enable `conda activate`\\nconda init bash\\nconda install -y --name base \\\\\\n \\"python=3.9\\" \\\\\\n \\"traits>=4.6.0\\" \\\\\\n \\"scipy\\" \\\\\\n \\"numpy\\" \\\\\\n \\"nomkl\\" \\\\\\n \\"pandas\\" \\\\\\n \\"gdcm\\"\\nbash -c \\"source activate base\\n python -m pip install --no-cache-dir --editable \\\\\\n \\"/src/heudiconv[all]\\"\\"\\n# Clean up\\nsync && conda clean --all --yes && sync\\nrm -rf ~/.cache/pip/*" \ } \ }, \ { \ "name": "entrypoint", \ "kwds": { \ "args": [ \ "heudiconv" \ ] \ } \ } \ ] \ }' > /.reproenv.json # End saving to specification to JSON. nipy-heudiconv-217744b/LICENSE000066400000000000000000000014251517415366200157500ustar00rootroot00000000000000Copyright [2014-2024] [HeuDiConv developers] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Some parts of the codebase/documentation are borrowed from other sources: - HeuDiConv tutorial from https://bitbucket.org/dpat/neuroimaging_core_docs/src Copyright 2023 Dianne Patterson nipy-heudiconv-217744b/NOTES000066400000000000000000000000571517415366200155560ustar00rootroot00000000000000Requires python-rdflib due to PROV generation. nipy-heudiconv-217744b/README.rst000066400000000000000000000152561517415366200164410ustar00rootroot00000000000000============= **HeuDiConv** ============= `a heuristic-centric DICOM converter` .. image:: https://joss.theoj.org/papers/10.21105/joss.05839/status.svg :target: https://doi.org/10.21105/joss.05839 :alt: JOSS Paper .. image:: https://img.shields.io/badge/docker-nipy/heudiconv:latest-brightgreen.svg?logo=docker&style=flat :target: https://hub.docker.com/r/nipy/heudiconv/tags/ :alt: Our Docker image .. image:: https://github.com/nipy/heudiconv/actions/workflows/test.yml/badge.svg?event=push :target: https://github.com/nipy/heudiconv/actions/workflows/test.yml :alt: GitHub Actions (test) .. image:: https://codecov.io/gh/nipy/heudiconv/branch/master/graph/badge.svg :target: https://codecov.io/gh/nipy/heudiconv :alt: CodeCoverage .. image:: https://readthedocs.org/projects/heudiconv/badge/?version=latest :target: http://heudiconv.readthedocs.io/en/latest/?badge=latest :alt: Readthedocs .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.1012598.svg :target: https://doi.org/10.5281/zenodo.1012598 :alt: Zenodo (latest) .. image:: https://repology.org/badge/version-for-repo/debian_unstable/heudiconv.svg?header=Debian%20Unstable :target: https://repology.org/project/heudiconv/versions :alt: Debian Unstable .. image:: https://repology.org/badge/version-for-repo/gentoo_ovl_science/python:heudiconv.svg?header=Gentoo%20%28%3A%3Ascience%29 :target: https://repology.org/project/python:heudiconv/versions :alt: Gentoo (::science) .. image:: https://repology.org/badge/version-for-repo/pypi/python:heudiconv.svg?header=PyPI :target: https://repology.org/project/python:heudiconv/versions :alt: PyPI .. image:: https://img.shields.io/badge/RRID-SCR__017427-blue :target: https://identifiers.org/RRID:SCR_017427 :alt: RRID About ----- ``heudiconv`` is a flexible DICOM converter for organizing brain imaging data into structured directory layouts. - It allows flexible directory layouts and naming schemes through customizable heuristics implementations. - It only converts the necessary DICOMs and ignores everything else in a directory. - You can keep links to DICOM files in the participant layout. - Using `dcm2niix `_ under the hood, it's fast. - It can track the provenance of the conversion from DICOM to NIfTI in W3C PROV format. - It provides assistance in converting to `BIDS `_. - It integrates with `DataLad `_ to place converted and original data under git/git-annex version control while automatically annotating files with sensitive information (e.g., non-defaced anatomicals, etc). Heudiconv can be inserted into your workflow to provide automatic conversion as part of a data acquisition pipeline, as seen in the figure below: .. image:: figs/environment.png Installation ------------ See our `installation page `_ on heudiconv.readthedocs.io . HOWTO 101 --------- In a nutshell -- ``heudiconv`` is given a file tree of DICOMs, and it produces a restructured file tree of NifTI files (conversion handled by `dcm2niix`_) with accompanying metadata files. The input and output structure is as flexible as your data, which is accomplished by using a Python file called a ``heuristic`` that knows how to read your input structure and decides how to name the resultant files. You can run your conversion automatically (which will produce a ``.heudiconv`` directory storing the used parameters), or generate the default parameters, edit them to customize file naming, and continue conversion via an additional invocation of `heudiconv`: .. image:: figs/workflow.png ``heudiconv`` comes with `existing heuristics `_ which can be used as is, or as examples. For instance, the Heuristic `convertall `_ extracts standard metadata from all matching DICOMs. ``heudiconv`` creates mapping files, ``.edit.text`` which lets researchers simply establish their own conversion mapping. In most use-cases of retrospective study data conversion, you would need to create your custom heuristic following the examples and the `"Heuristic" section `_ in the documentation. **Note** that `ReproIn heuristic `_ is generic and powerful enough to be adopted virtually for *any* study: For prospective studies, you would just need to name your sequences following the `ReproIn convention `_, and for retrospective conversions, you often would be able to create a new versatile heuristic by simply providing remappings into ReproIn as shown in `this issue (documentation is coming) `_. Having decided on a heuristic, you could use the command line:: heudiconv -f HEURISTIC-FILE-OR-NAME -o OUTPUT-PATH --files INPUT-PATHs with various additional options (see ``heudiconv --help`` or `"CLI Reference" in documentation `__) to tune its behavior to convert your data. For detailed examples and guides, please check out `ReproIn conversion invocation examples `_ and the `user tutorials `_ in the documentation. How to cite ----------- Please use `Zenodo record `_ for your specific version of HeuDiConv. We also support gathering all relevant citations via `DueCredit `_. How to contribute ----------------- For a detailed into, see our `contributing guide `_. Our releases are packaged using Intuit auto, with the corresponding workflow including Docker image preparation being found in ``.github/workflows/release.yml``. 3-rd party heuristics --------------------- - https://github.com/courtois-neuromod/ds_prep/blob/main/mri/convert/heuristics_unf.py Support ------- All bugs, concerns and enhancement requests for this software can be submitted here: https://github.com/nipy/heudiconv/issues. If you have a problem or would like to ask a question about how to use ``heudiconv``, please submit a question to `NeuroStars.org `_ with a ``heudiconv`` tag. NeuroStars.org is a platform similar to StackOverflow but dedicated to neuroinformatics. All previous ``heudiconv`` questions are available here: http://neurostars.org/tags/heudiconv/ . nipy-heudiconv-217744b/custom/000077500000000000000000000000001517415366200162535ustar00rootroot00000000000000nipy-heudiconv-217744b/custom/dbic/000077500000000000000000000000001517415366200171545ustar00rootroot00000000000000nipy-heudiconv-217744b/custom/dbic/README000066400000000000000000000003431517415366200200340ustar00rootroot00000000000000Scripts and configurations used alongside with heudiconv setup at DBIC (Dartmouth Brain Imaging Center). Might migrate to an independent repository eventually but for now placed here with hope they might come useful for some. nipy-heudiconv-217744b/custom/dbic/singularity-env.def000066400000000000000000000051361517415366200230010ustar00rootroot00000000000000# Copyright (c) 2015-2016, Gregory M. Kurtzer. All rights reserved. # # Changes for NeuroDebian/DBIC setup are Copyright (c) 2017 Yaroslav Halchenko. # # The purpose of the environment is to provide a complete suite for running # heudiconv on the INBOX server to provide conversion into BIDS layout. # ATM it does not ship heudiconv itself which would be accessed directly # from the main drive for now. # # "Singularity" Copyright (c) 2016, The Regents of the University of California, # through Lawrence Berkeley National Laboratory (subject to receipt of any # required approvals from the U.S. Dept. of Energy). All rights reserved. # # Notes: # - Due to https://github.com/singularityware/singularity/issues/471 # bootstrapping leads to non-usable/non-removable-without-reboot # image due to some rogue run away processes. # This line could help to kill them but should be used with caution # since could kill other unrelated processes # # grep -l loop /proc/*/mountinfo | sed -e 's,/proc/\(.*\)/.*,\1,g' | while read pid; do sudo kill $pid; done BootStrap: debootstrap #OSVersion: stable # needs nipype 0.12.1 but that one didn't build for stable since needs python-prov... # so trying stretch OSVersion: stretch MirrorURL: http://ftp.us.debian.org/debian/ # so if image is executed we just enter the environment %runscript echo "Welcome to the DBIC BIDS environment" /bin/bash %post echo "Configuring the environment" apt-get update apt-get -y install eatmydata eatmydata apt-get -y install vim wget strace time ncdu gnupg curl procps wget -q -O/tmp/nd-configurerepo https://raw.githubusercontent.com/neurodebian/neurodebian/4d26c8f30433145009aa3f74516da12f560a5a13/tools/nd-configurerepo bash /tmp/nd-configurerepo chmod a+r -R /etc/apt eatmydata apt-get -y install datalad python-nipype virtualenv dcm2niix python-dcmstack python-configparser python-funcsigs python-pytest dcmtk # for bids-validator curl -sL https://deb.nodesource.com/setup_6.x | bash - && \ eatmydata apt-get install -y nodejs npm install -g bids-validator@0.20.0 chmod a+rX -R /usr/lib/node_modules/ chmod a+rX -R /etc/apt/sources.list.d rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* apt-get clean # and wipe out apt lists since not to be used RW for further tuning # find /var/lib/apt/lists/ -type f -delete # /usr/bin/find /var/lib/apt/lists/ -type f -name \*Packages\* -o -name \*Contents\* # complicates later interrogation - thus disabled # Create some bind mount directories present on rolando mkdir -p /afs /inbox chmod a+rX /afs /inbox nipy-heudiconv-217744b/dev-requirements.txt000066400000000000000000000000431517415366200207760ustar00rootroot00000000000000-r requirements.txt tinydb inotify nipy-heudiconv-217744b/docs/000077500000000000000000000000001517415366200156715ustar00rootroot00000000000000nipy-heudiconv-217744b/docs/Makefile000066400000000000000000000011051517415366200173260ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) nipy-heudiconv-217744b/docs/api.rst000066400000000000000000000002321517415366200171710ustar00rootroot00000000000000============= API Reference ============= .. toctree:: :maxdepth: 1 api/bids api/convert api/dicoms api/parser api/queue api/utils nipy-heudiconv-217744b/docs/api/000077500000000000000000000000001517415366200164425ustar00rootroot00000000000000nipy-heudiconv-217744b/docs/api/bids.rst000066400000000000000000000000571517415366200201170ustar00rootroot00000000000000==== BIDS ==== .. automodule:: heudiconv.bids nipy-heudiconv-217744b/docs/api/convert.rst000066400000000000000000000001041517415366200206470ustar00rootroot00000000000000========== Conversion ========== .. automodule:: heudiconv.convert nipy-heudiconv-217744b/docs/api/dicoms.rst000066400000000000000000000000671517415366200204550ustar00rootroot00000000000000====== DICOMS ====== .. automodule:: heudiconv.dicoms nipy-heudiconv-217744b/docs/api/parser.rst000066400000000000000000000000721517415366200204670ustar00rootroot00000000000000======= Parsing ======= .. automodule:: heudiconv.parser nipy-heudiconv-217744b/docs/api/queue.rst000066400000000000000000000001131517415366200203130ustar00rootroot00000000000000============= Batch Queuing ============= .. automodule:: heudiconv.queue nipy-heudiconv-217744b/docs/api/utils.rst000066400000000000000000000000711517415366200203320ustar00rootroot00000000000000======= Utility ======= .. automodule:: heudiconv.utils nipy-heudiconv-217744b/docs/changes.rst000066400000000000000000000000751517415366200200350ustar00rootroot00000000000000======= Changes ======= .. literalinclude:: ../CHANGELOG.md nipy-heudiconv-217744b/docs/commandline.rst000066400000000000000000000003541517415366200207130ustar00rootroot00000000000000============= CLI Reference ============= ``heudiconv`` processes DICOM files and converts the output into user defined paths. .. argparse:: :ref: heudiconv.cli.run.get_parser :prog: heudiconv :nodefault: :nodefaultconst: nipy-heudiconv-217744b/docs/conf.py000066400000000000000000000126211517415366200171720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- from heudiconv import __version__ project = "heudiconv" copyright = "2014-2022, Heudiconv team" # noqa: A001 author = "Heudiconv team" # The short X.Y version version = ".".join(__version__.split(".")[:2]) # The full version, including alpha/beta/rc tags release = __version__ # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinxarg.ext", "sphinx.ext.napoleon", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = [".rst", ".md"] # source_suffix = '.rst' # The master toctree document. master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = "heudiconvdoc" # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, "heudiconv.tex", "heudiconv Documentation", "Heudiconv team", "manual", ), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "heudiconv", "heudiconv Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "heudiconv", "heudiconv Documentation", author, "heudiconv", "One line description of project.", "Miscellaneous", ), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- autodoc_default_options = {"members": None} nipy-heudiconv-217744b/docs/container.rst000066400000000000000000000035121517415366200204060ustar00rootroot00000000000000============================== Using heudiconv in a Container ============================== If heudiconv is :ref:`installed via a Docker container `, you can run the commands in the following format:: docker run nipy/heudiconv:latest [heudiconv options] So a user running via container would check the version with this command:: docker run nipy/heudiconv:latest --version Which is equivalent to the locally installed command:: heudiconv --version Bind mount ---------- Typically, users of heudiconv will be operating on data that is on their local machine. We can give heudiconv access to that data via a ``bind mount``, which is the ``-v`` syntax. Once common pattern is to share the working directory with ``-v $PWD:$PWD``, so heudiconv will behave as though it is installed on your system. However, you should be aware of how permissions work depending on your container toolset. Docker Permissions ****************** When you run a container with docker without specifying a user, it will be run as root. This isn't ideal if you are operating on data owned by your local user, so for ``Docker`` it is recommended to specify that the container will run as your user.:: docker run --user=$(id -u):$(id -g) -e "UID=$(id -u)" -e "GID=$(id -g)" --rm -t -v $PWD:$PWD nipy/heudiconv:latest --version Podman Permissions ****************** When running Podman without specifying a user, the container is run as root inside the container, but your user outside of the container. This default behavior usually works for heudiconv users:: podman run -v $PWD:PWD nipy/heudiconv:latest --version Other Common Options -------------------- We typically recommend users make use of the following flags to Docker and Podman * ``-it`` Interactive terminal * ``--rm`` Remove the changes to the container when it completes nipy-heudiconv-217744b/docs/custom-heuristic.rst000066400000000000000000000564141517415366200217440ustar00rootroot00000000000000========================= Custom Heuristics ========================= This tutorial is based on `Dianne Patterson's University of Arizona tutorials `_ In this tutorial we go more in depth, creating our own *heuristic.py* and modifying it for our needs: 1. :ref:`Step1 ` Generate a heuristic (translation) file skeleton and some associated descriptor text files. 2. :ref:`Step2 ` Modify the *heuristic.py* to specify BIDS output names and directories, and the input DICOM characteristics. 3. :ref:`Step3 ` Call HeuDiConv to run on more subjects and sessions. **Prerequisites**: 1. Ensure :ref:`heudiconv and dcm2niix ` is installed. 2. :ref:`Prepare the dataset ` used in the quickstart. .. _heudiconv_step1: Step 1: Generate Skeleton ************************* .. note:: Step 1 only needs to be completed once for each project. If repeating this step, ensure that the .heudiconv directory is removed. From the *MRIS* directory, run the following command to process the ``dcm`` files that you downloaded and unzipped for this tutorial.:: heudiconv --files dicom/219/*/*/*.dcm -o Nifti/ -f convertall -s 219 -c none * ``--files dicom/{subject}/*/*/*.dcm`` identifies the path to the DICOM files and specifies that they have the extension ``.dcm`` in this case. * ``-o Nifti/`` is the output in *Nifti*. If the output directory does not exist, it will be created. * ``-f convertall`` This creates a *heuristic.py* template from an existing heuristic module. There are `other heuristic modules `_ , but *convertall* is a good default. * ``-s 219`` specifies the subject number. * ``-c none`` indicates you are not actually doing any conversion right now. You will now have a heudiconv skeleton in the `/.heudiconv` directory, in our case `Nifti/.heudiconv` The ``.heudiconv`` hidden directory ====================================== Take a look at *MRIS/Nifti/.heudiconv/219/info/*, heudiconv has produced two files of interest: a skeleton *heuristic.py* and a *dicominfo.tsv* file. The generated heuristic file template contains comments explaining usage. .. warning:: * **The Good** Every time you run conversion to create the BIDS NIfTI files and directories, a detailed record of what you did is recorded in the *.heudiconv* directory. This includes a copy of the *heuristic.py* module that you ran for each subject and session. Keep in mind that the hidden *.heudiconv* directory gets updated every time you run heudiconv. Together your *code* and *.heudiconv* directories provide valuable provenance information that should remain with your data. * **The Bad** If you rerun *heuristic.py* for some subject and session that has already been run, heudiconv quietly uses the conversion routines it stored in *.heudiconv*. This can be really annoying if you are troubleshooting *heuristic.py*. * **More Good** You can remove subject and session information from *.heudiconv* and run it fresh. In fact, you can entirely remove the *.heudiconv* directory and still run the *heuristic.py* you put in the *code* directory. .. _heudiconv_step2: Step 2: Modify Heuristic ************************ .. TODO Lets remove heuristic1 and heuristic2 and create a 2nd example dataset? or branch? We will modify the generated *heuristic.py* so heudiconv will arrange the output in a BIDS directory structure. It is okay to rename this file, or to have several versions with different names, just be sure to pass the intended filename with `-f`. See :doc:`heuristics` docs for more info. * I provide three section labels (1, 1b and 2) to facilitate exposition here. Each of these sections should be manually modified by you for your project. Section 1 ============== * This *heuristic.py* does not import all sequences in the example *Dicom* directory. This is a feature of heudiconv: You do not need to import scouts, motion corrected images or other DICOMs of no interest. * You may wish to add, modify or remove keys from this section for your own data:: # Section 1: These key definitions should be revised by the user ################################################################### # For each sequence, define a key variables (e.g., t1w, dwi etc) and template using the create_key function: # key = create_key(output_directory_path_and_name). ###### TIPS ####### # If there are sessions, then session must be subfolder name. # Do not prepend the ses key to the session! It will be prepended automatically for the subfolder and the filename. # The final value in the filename should be the modality. It does not have a key, just a value. # Otherwise, there is a key for every value. # Filenames always start with subject, optionally followed by session, and end with modality. ###### Definitions ####### # The "data" key creates sequential numbers which can be used for naming sequences. # This is especially valuable if you run the same sequence multiple times at the scanner. data = create_key('run-{item:03d}') t1w = create_key('sub-{subject}/{session}/anat/sub-{subject}_{session}_T1w') dwi = create_key('sub-{subject}/{session}/dwi/sub-{subject}_{session}_dir-AP_dwi') # Save the RPE (reverse phase-encode) B0 image as a fieldmap (fmap). It will be used to correct # the distortion in the DWI fmap_rev_phase = create_key('sub-{subject}/{session}/fmap/sub-{subject}_{session}_dir-PA_epi') fmap_mag = create_key('sub-{subject}/{session}/fmap/sub-{subject}_{session}_magnitude') fmap_phase = create_key('sub-{subject}/{session}/fmap/sub-{subject}_{session}_phasediff') # Even if this is resting state, you still need a task key func_rest = create_key('sub-{subject}/{session}/func/sub-{subject}_{session}_task-rest_run-01_bold') func_rest_post = create_key('sub-{subject}/{session}/func/sub-{subject}_{session}_task-rest_run-02_bold') * **Key** * Define a short informative key variable name for each image sequence you wish to export. Note that you can use any key names you want (e.g. *foo* would work as well as *fmap_phase*), but you need to be consistent. * The ``key`` name is to the left of the ``=`` for each row in the above example. * **Template** * Use the variable ``{subject}`` to make the code general purpose, so you can apply it to different subjects in Step 3. * Use the variable ``{session}`` to make the code general purpose only if you have multiple sessions for each subject. * Once you use the variable ``{session}``: * Ensure that a session gets added to the **output path**, e.g., ``sub-{subject}/{session}/anat/`` AND * Session gets added to the **output filename**: ``sub-{subject}_{session}_T1w`` for every image in the session. * Otherwise you will get `bids validator errors `_ * Define the output directories and file names according to the `BIDS specification `_ * Note the output names for the fieldmap images (e.g., *sub-219_ses-itbs_dir-PA_epi.nii.gz*, *sub-219_ses-itbs_magnitude1.nii.gz*, *sub-219_ses-itbs_magnitude2.nii.gz*, *sub-219_ses-itbs_phasediff.nii.gz*). * The reverse_phase encode dwi image (e.g., *sub-219_ses-itbs_dir-PA_epi.nii.gz*) is grouped with the fieldmaps because it is used to correct other images. * Data that is not yet defined in the BIDS specification will cause the bids-validator to produce an error unless you include it in a `.bidsignore `_ file. * **data** * a key definition that creates sequential numbering * ``03d`` means *create three slots for digits* ``3d``, *and pad with zeros* ``0``. * This is useful if you have a scanner sequence with a single name but you run it repeatedly and need to generate separate files for each run. For example, you might define a single functional sequence at the scanner and then run it several times instead of creating separate names for each run. .. Note:: It is usually better to name your sequences explicitly (e.g., run-01, run-02 etc.) rather than depending on sequential numbering. There will be less confusion later. * If you have a sequence with the same name that you run repeatedly WITHOUT the sequential numbering, HeuDiConv will overwrite earlier sequences with later ones. * To ensure that a sequence includes sequential numbering, you also need to add ``run-{item:03d}`` (for example) to the key-value specification for that sequence. * Here I illustrate with the t1w key-value pair: * If you started with: * ``t1w = create_key('sub-{subject}/anat/sub-{subject}_T1w')``, * You could add sequence numbering like this: * ``t1w = create_key('sub-{subject}/anat/sub-{subject}_run-{item:03d}_T1w')``. * Now if you export several T1w images for the same subject and session, using the exact same protocol, each will get a separate run number like this: * *sub-219_ses_run-001_T1w.nii.gz, sub-219_ses_run-002_T1w.nii.gz* etc. Section 1b ==================== * Based on your chosen keys, create a data dictionary called *info*:: # Section 1b: This data dictionary (below) should be revised by the user. ########################################################################### # info is a Python dictionary containing the following keys from the infotodict defined above. # This list should contain all and only the sequences you want to export from the dicom directory. info = {t1w: [], dwi: [], fmap_rev_phase: [], fmap_mag: [], fmap_phase: [], func_rest: [], func_rest_post: []} # The following line does no harm, but it is not part of the dictionary. last_run = len(seqinfo) * Enter each key in the dictionary in this format ``key: []``, for example, ``t1w: []``. * Separate the entries with commas as illustrated above. Section 2 =============== * Define the criteria for identifying each DICOM series that corresponds to one of the keys you want to export:: # Section 2: These criteria should be revised by the user. ########################################################## # Define test criteria to check that each DICOM sequence is correct # seqinfo (s) refers to information in dicominfo.tsv. Consult that file for # available criteria. # Each sequence to export must have been defined in Section 1 and included in Section 1b. # The following illustrates the use of multiple criteria: for idx, s in enumerate(seqinfo): # Dimension 3 must equal 176 and the string 'mprage' must appear somewhere in the protocol_name if (s.dim3 == 176) and ('mprage' in s.protocol_name): info[t1w].append(s.series_id) # Dimension 3 must equal 74 and dimension 4 must equal 32, and the string 'DTI' must appear somewhere in the protocol_name if (s.dim3 == 74) and (s.dim4 == 32) and ('DTI' in s.protocol_name): info[dwi].append(s.series_id) # The string 'verify_P-A' must appear somewhere in the protocol_name if ('verify_P-A' in s.protocol_name): info[fmap_rev_phase] = [s.series_id] # Dimension 3 must equal 64, and the string 'field_mapping' must appear somewhere in the protocol_name if (s.dim3 == 64) and ('field_mapping' in s.protocol_name): info[fmap_mag] = [s.series_id] # Dimension 3 must equal 32, and the string 'field_mapping' must appear somewhere in the protocol_name if (s.dim3 == 32) and ('field_mapping' in s.protocol_name): info[fmap_phase] = [s.series_id] # The string 'resting_state' must appear somewhere in the protocol_name and the Boolean field is_motion_corrected must be False (i.e. not motion corrected) # This ensures I do NOT get the motion corrected MOCO series instead of the raw series! if ('restingstate' == s.protocol_name) and (not s.is_motion_corrected): info[func_rest].append(s.series_id) # The string 'Post_TMS_resting_state' must appear somewhere in the protocol_name and the Boolean field is_motion_corrected must be False (i.e. not motion corrected) # This ensures I do NOT get the motion corrected MOCO series instead of the raw series. if ('Post_TMS_restingstate' == s.protocol_name) and (not s.is_motion_corrected): info[func_rest_post].append(s.series_id) * To define the criteria, look at *dicominfo.tsv* in *.heudiconv/info*. This file contains tab-separated values so you can easily view it in Excel or any similar spreadsheet program. *dicominfo.tsv* is not used programmatically to run heudiconv (i.e., you could delete it with no adverse consequences), but it is very useful for defining the test criteria for Section 2 of *heuristic.py*. * Some values in *dicominfo.tsv* might be wrong. For example, my reverse phase encode sequence with two acquisitions of 74 slices each is reported as one acquisition with 148 slices (2018_12_11). Hopefully they'll fix this. Despite the error in *dicominfo.tsv*, dcm2niix reconstructed the images correctly. * You will be adding, removing or altering values in conditional statements based on the information you find in *dicominfo.tsv*. * ``seqinfo`` (s) refers to the same information you can view in *dicominfo.tsv* (although seqinfo does not rely on *dicominfo.tsv*). * Here are two types of criteria: * ``s.dim3 == 176`` is an **equivalence** (e.g., good for checking dimensions for a numerical data type). For our sample T1w image to be exported from DICOM, it must have 176 slices in the third dimension. * ``'mprage' in s.protocol_name`` says the protocol name string must **include** the word *mprage* for the *T1w* image to be exported from DICOM. This criterion string is case-sensitive. * ``info[t1w].append(s.series_id)`` Given that the criteria are satisfied, the series should be named and organized as described in *Section 1* and referenced by the info dictionary. The information about the processing steps is saved in the *.heudiconv* subdirectory. * Here I have organized each conditional statement so that the sequence protocol name comes first followed by other criteria if relevant. This is not necessary, though it does make the resulting code easier to read. .. _heudiconv_step3: Step 3: ******************* * You have now done all the hard work for your project. When you want to add a subject or session, you only need to run this third step for that subject or session (A record of each run is kept in .heudiconv for you):: heudiconv --files dicom/{subject}/*/*.dcm -o Nifti/ -f Nifti/code/heuristic.py -s 219 -ss itbs -c dcm2niix -b --minmeta --overwrite * The first time you run this step, several important text files are generated (e.g., CHANGES, dataset_description.json, participants.tsv, README etc.). On subsequent runs, information may be added (e.g., *participants.tsv* will be updated). Other files, like the *README* and *dataset_description.json* should be updated manually. * This Docker command is slightly different from the previous Docker command you ran. * ``-f Nifti/code/heuristic.py`` now tells HeuDiConv to use your revised *heuristic.py* in the *code* directory. * In this case, we specify the subject we wish to process ``-s 219`` and the name of the session ``-ss itbs``. * We could specify multiple subjects like this: ``-s 219 220 -ss itbs`` * ``-c dcm2niix -b`` indicates that we want to use the dcm2niix converter with the -b flag (which creates BIDS). * ``--minmeta`` ensures that only the minimum necessary amount of data gets added to the JSON file when created. On the off chance that there is a LOT of meta-information in the DICOM header, the JSON file will not get swamped by it. fmriprep and mriqc are very sensitive to this information overload and will crash, so *minmeta* provides a layer of protection against such corruption. * ``--overwrite`` This is a peculiar option. Without it, I have found the second run of a sequence does not get generated. But with it, everything gets written again (even if it already exists). I don't know if this is my problem or the tool...but for now, I'm using ``--overwrite``. * Step 3 should produce a tree like this:: Nifti ├── CHANGES ├── README ├── code │   ├── __pycache__ │   │   └── heuristic1.cpython-36.pyc │   ├── heuristic1.py │   └── heuristic2.py ├── dataset_description.json ├── participants.json ├── participants.tsv ├── sub-219 │   └── ses-itbs │   ├── anat │   │   ├── sub-219_ses-itbs_T1w.json │   │   └── sub-219_ses-itbs_T1w.nii.gz │   ├── dwi │   │   ├── sub-219_ses-itbs_dir-AP_dwi.bval │   │   ├── sub-219_ses-itbs_dir-AP_dwi.bvec │   │   ├── sub-219_ses-itbs_dir-AP_dwi.json │   │   └── sub-219_ses-itbs_dir-AP_dwi.nii.gz │   ├── fmap │   │   ├── sub-219_ses-itbs_dir-PA_epi.json │   │   ├── sub-219_ses-itbs_dir-PA_epi.nii.gz │   │   ├── sub-219_ses-itbs_magnitude1.json │   │   ├── sub-219_ses-itbs_magnitude1.nii.gz │   │   ├── sub-219_ses-itbs_magnitude2.json │   │   ├── sub-219_ses-itbs_magnitude2.nii.gz │   │   ├── sub-219_ses-itbs_phasediff.json │   │   └── sub-219_ses-itbs_phasediff.nii.gz │   ├── func │   │   ├── sub-219_ses-itbs_task-rest_run-01_bold.json │   │   ├── sub-219_ses-itbs_task-rest_run-01_bold.nii.gz │   │   ├── sub-219_ses-itbs_task-rest_run-01_events.tsv │   │   ├── sub-219_ses-itbs_task-rest_run-02_bold.json │   │   ├── sub-219_ses-itbs_task-rest_run-02_bold.nii.gz │   │   └── sub-219_ses-itbs_task-rest_run-02_events.tsv │   ├── sub-219_ses-itbs_scans.json │   └── sub-219_ses-itbs_scans.tsv └── task-rest_bold.json TIPS ====== * **Name Directories as you wish**: You can name the project directory (e.g., **MRIS**) and the output directory (e.g., **Nifti**) as you wish (just don't put spaces in the names!). * **Age and Sex Extraction**: Heudiconv will extract age and sex info from the DICOM header. If there is any reason to believe this information is wrong in the DICOM header (for example, it was made-up because no one knew how old the subject was, or it was considered a privacy concern), then you need to check the output. If you have Horos (or another DICOM editor), you can edit the values in the DICOM headers, otherwise you need to edit the values in the BIDS text file *participants.tsv*. * **Separating Sessions**: If you have multiple sessions at the scanner, you should create an *Exam* folder for each session. This will help you to keep the data organized and *Exam* will be reported in the *study_description* in your *dicominfo.tsv*, so that you can use it as a criterion. * **Don't manually combine DICOMS from different sessions**: If you combine multiple sessions in one subject DICOM folder, heudiconv will fail to run and will complain about ``conflicting study identifiers``. You can get around the problem by figuring out which DICOMs are from different sessions and separating them so you deal with one set at a time. This may mean you have to manually edit the BIDS output. * Why might you manually combine sessions you ask? Because you never intended to have multiple sessions, but the subject had to complete some scans the next day. Or, because the scanner had to be rebooted. * **Don't assume all your subjects' dicoms have the same names or that the sequences were always run in the same order**: If you develop a *heuristic.py* on one subject, try it and carefully evaluate the results on your other subjects. This is especially true if you already collected the data before you started thinking about automating the output. Every time you run HeuDiConv with *heuristic.py*, a new *dicominfo.tsv* file is generated. Inspect this for differences in protocol names and series descriptions etc. * **Decompressing DICOMS**: Decompress your data, heudiconv does not yet support compressed DICOM conversion. https://github.com/nipy/heudiconv/issues/287 * **Create unique DICOM protocol names at the scanner** If you have the opportunity to influence the DICOM naming strategies, then try to ensure that there is a unique protocol name for every run. For example, if you repeat the fmri protocol three times, name the first one fmri_1, the next fmri_2, and the last fmri_3 (or any variation on this theme). This will make it much easier to uniquely specify the sequences when you convert and reduce your chance of errors. Exploring Criteria ********************** *dicominfo.tsv* contains a human readable version of seqinfo. Each column of data can be used as criteria for identifying the correct DICOM image. We have already provided examples of using string types, numbers, and Booleans (True-False). Tuples (immutable lists) are also available and examples of using these are provided below. To ensure that you are extracting the images you want, you need to be very careful about creating your initial *heuristic.py*. Why Experiment? ==================== * Criteria can be tricky. Ensure the NIfTI files you create are the correct ones (for example, not the derived or motion corrected if you didn't want that). In addition to looking at the images created (which tells you whether you have a fieldmap or T1w etc.), you should look at the dimensions of the image. Not only the dimensions, but the range of intensity values and the size of the image on disk should match for dcm2niix and heudiconv's *heuristic.py*. * For really tricky cases, download and install dcm2niix on your local machine and run it for a sequence of concern (in my experience, it is usually fieldmaps that go wrong). * Although Python does not require you to use parentheses while defining criteria, parentheses are a good idea. Parentheses will help ensure that complex criteria involving multiple logical operators ``and, or, not`` make sense and behave as expected. Tuples --------- Suppose you want to use the values in the field ``image_type``? It is not a number or string or Boolean. To discover the data type of a column, you can add a statement like this ``print(type(s.image_type))`` to the for loop in Section 2 of *heuristic.py*. Then run *heuristic.py* (preferably without any actual conversions) and you should see an output like this ````. Here is an example of using a value from ``image_type`` as a criterion:: if ('ASL_3D_tra_iso' == s.protocol_name) and ('TTEST' in s.image_type): info[asl_der].append(s.series_id) Note that this differs from testing for a string because you cannot test for any substring (e.g., 'TEST' would not work). String tests will not work on a tuple datatype. .. Note:: *image_type* is described in the `DICOM specification `_ nipy-heudiconv-217744b/docs/figs000077700000000000000000000000001517415366200177042../figs/ustar00rootroot00000000000000nipy-heudiconv-217744b/docs/heuristics.rst000066400000000000000000000230501517415366200206050ustar00rootroot00000000000000=============== Heuristics File =============== The heuristic file controls how information about the DICOMs is used to convert to a file system layout (e.g., BIDS). Provided Heuristics ------------------- ``heudiconv`` provides over 10 pre-created heuristics, which can be seen `here `_ . These heuristic files are documented in their code comments. Some of them, like `convertall `_ or `ReproIn `__ could be immediately reused and represent two ends of the spectrum in heuristics: - ``convertall`` is very simple and does not automate anything -- it is for a user to modify filenames in the prepared conversion table, and then rerun with ``-c dcm2niix``. - ``reproin`` can be used fully automated, if original sequences were named according to its ReproIn convention. Discover more on their user in the :ref:`Tutorials` section. However, there is a large variety of data out there, and not all DICOMs will be covered by the existing heuristics. This section will outline what makes up a heuristic file, and some useful functions available when making one. Components ========== ------------------------ ``infotodict(seqinfos)`` ------------------------ The only required function for a heuristic, `infotodict` is used to both define the conversion outputs and specify the criteria for scan to output association. Conversion outputs are defined as keys, a `tuple` consisting of three elements: - a template path used for the basis of outputs - `tuple` of output types. Valid types include `nii`, `nii.gz`, and `dicom`. - `None` - a historical artifact (corresponds to some notion of ``annotation_class`` no living human is aware about) The following details of the sequences could also be used as a ``{detail}`` in the conversion keys: - ``item``: an index of seqinfo (e.g., ``1``), - ``subject``: a subject label (e.g., ``qa``) - ``seqitem``: sequence item, index with a sequence/protocol name (e.g., ``3-anat-scout_ses-{date}`` or ``3-anat-scout_ses-DATE`` for Siemens X60 which does not allow {} in names) - ``subindex``: an index within the ``seqinfo`` (e.g., ``1``), - ``session``: empty (no session) or a session entity (along with ``ses-``, e.g., ``ses-20191216``), - ``bids_subject_session_prefix``: shortcut for BIDS file name prefix combining subject and optional session (e.g., ``sub-qa_ses-20191216``), - ``bids_subject_session_dir``: shortcut for BIDS file path combining subject and optional session (e.g., ``sub-qa/ses-20191216``). .. note:: An example conversion key ``('sub-{subject}/func/sub-{subject}_task-test_run-{item}_bold', ('nii.gz', 'dicom'), None)`` or equivalent in `--bids` mode which would work also if there is a specified session ``('{bids_subject_session_dir}/func/{bids_subject_session_prefix}_task-test_run-{item}_bold', ('nii.gz', 'dicom'), None)`` The ``seqinfos`` parameter is a list of namedtuples which serves as a grouped and stacked record of the DICOMs passed in. Each item in `seqinfo` contains DICOM metadata that can be used to isolate the series, and assign it to a conversion key. A function ``create_key`` is commonly defined by heuristics (internally) to assist in creating the key, and to be used inside ``infotodict``. A dictionary of {``conversion key``: ``series_id``} is returned, where ``series_id`` is the 3rd (indexes as ``[2]`` or accessed as ``.series_id`` from ``seqinfo``). --------------------------------- ``create_key(template, outtype)`` --------------------------------- A common helper function used to create the conversion key in ``infotodict``. But it is not used directly by HeuDiConv. -------------------- ``filter_files(fl)`` -------------------- A utility function used to filter any input files. If this function is included, every file found will go through this filter. Any files where this function returns ``False`` will be filtered out (files where this function returns ``True`` will be kept). Note: this function's logic is opposite to ``filter_dicom`` (see below). Think of it as an input for Python's built-in ``filter()``. -------------------------- ``filter_dicom(dcm_data)`` -------------------------- A utility function used to filter any DICOMs. If this function is included, every DICOM found will go through this filter. Any DICOMs where this function returns ``True`` will be filtered out. ------------------------------- ``infotoids(seqinfos, outdir)`` ------------------------------- Further processing on ``seqinfos`` to deduce/customize subject, session, and locator. A dictionary of {"locator": locator, "session": session, "subject": subject} is returned. --------------------------------------------------------------- ``grouping`` string or ``grouping(files, dcmfilter, seqinfo)`` --------------------------------------------------------------- Whenever ``--grouping custom`` (``-g custom``) is used, this attribute or callable will be used to inform how to group the DICOMs into separate groups. From `original PR#359 `_:: grouping = 'AcquisitionDate' or:: def grouping(files, dcmfilter, seqinfo): seqinfos = collections.OrderedDict() ... return seqinfos # ordered dict containing seqinfo objects: list of DICOMs --------------------------------------------------------------- ``custom_seqinfo(wrapper, series_files)`` --------------------------------------------------------------- If present this function will be called on each group of dicoms with a sample nibabel dicom wrapper to extract additional information to be used in ``infotodict``. Importantly the return value of that function needs to be hashable. For instance the following non-hashable types can be converted to an alternative hashable type: - `list` --> `tuple` - `dict` --> `frozendict` - `arrays` --> `bytes` (`tobytes()`, or `pickle.dumps`), `str` or `tuple` of tuples. For an example see `convertall_custom.py `__ heuristic. ------------------------------- ``POPULATE_INTENDED_FOR_OPTS`` ------------------------------- Dictionary to specify options to populate the ``'IntendedFor'`` field of the ``fmap`` jsons. When a BIDS session has ``fmaps``, they can automatically be assigned to be used for susceptibility distortion correction of other non-``fmap`` images in the session (populating the ``'IntendedFor'`` field in the ``fmap`` json file). For this automated assignment, ``fmaps`` are taken as groups (``_phase`` and ``_phasediff`` images and the corresponding ``_magnitude`` images; consecutive Spin-Echo images collected with opposite phase encoding polarity (``pepolar`` case); etc.). This is achieved by checking, for every non-``fmap`` image in the session, which ``fmap`` groups are suitable candidates to correct for distortions in that image. Then, if there is more than one candidate (e.g., if there was a ``fmap`` collected at the beginning of the session and another one at the end), the user can specify which one to use. The parameters that can be specified and the allowed options are defined in ``bids.py``: - ``'matching_parameter'``: The imaging parameter that needs to match between the ``fmap`` and an image for the ``fmap`` to be considered as a suitable to correct that image. Allowed options are: * ``'Shims'``: ``heudiconv`` will check the ``ShimSetting`` in the ``.json`` files and will only assign ``fmaps`` to images if the ``ShimSettings`` are identical for both. * ``'ImagingVolume'``: both ``fmaps`` and images will need to have the same the imaging volume (the header affine transformation: position, orientation and voxel size, as well as number of voxels along each dimensions). * ``'ModalityAcquisitionLabel'``: it checks for what modality (``anat``, ``func``, ``dwi``) each ``fmap`` is intended by checking the ``_acq-`` label in the ``fmap`` filename and finding corresponding modalities (e.g. ``_acq-fmri``, ``_acq-bold`` and ``_acq-func`` will be matched with the ``func`` modality) * ``'CustomAcquisitionLabel'``: it checks for what modality images each ``fmap`` is intended by checking the ``_acq-`` custom label (e.g. ``_acq-XYZ42``) in the ``fmap`` filename, and matching it with: - the corresponding modality image ``_acq-`` label for modalities other than ``func`` (e.g. ``_acq-XYZ42`` for ``dwi`` images) - the corresponding image ``_task-`` label for the ``func`` modality (e.g. ``_task-XYZ42``) * ``'PlainAcquisitionLabel'``: similar to ``'CustomAcquisitionLabel'``, but does not change behavior for ``func`` modality and always bases decision on the ``_acq-`` label. Helps in cases when there are multiple tasks and a shared ``fmap`` for some of them. * ``'Force'``: forces ``heudiconv`` to consider any ``fmaps`` in the session to be suitable for any image, no matter what the imaging parameters are. - ``'criterion'``: Criterion to decide which of the candidate ``fmaps`` will be assigned to a given file, if there are more than one. Allowed values are: * ``'First'``: The first matching ``fmap``. * ``'Closest'``: The closest in time to the beginning of the image acquisition. .. note:: Example:: POPULATE_INTENDED_FOR_OPTS = { 'matching_parameters': ['ImagingVolume', 'Shims'], 'criterion': 'Closest' } If ``POPULATE_INTENDED_FOR_OPTS`` is not present in the heuristic file, ``IntendedFor`` will not be populated automatically. nipy-heudiconv-217744b/docs/index.rst000066400000000000000000000006431517415366200175350ustar00rootroot00000000000000.. heudiconv documentation master file, created by sphinx-quickstart on Mon Mar 25 15:42:31 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. include:: ../README.rst .. include:: ../CONTRIBUTING.rst Contents -------- .. toctree:: :maxdepth: 2 installation changes tutorials heuristics commandline container api nipy-heudiconv-217744b/docs/installation.rst000066400000000000000000000067541517415366200211400ustar00rootroot00000000000000============ Installation ============ ``Heudiconv`` is packaged and available from many different sources. .. _install_local: Local ===== Released versions of HeuDiConv are available on `PyPI `_ and `conda `_. If installing through ``PyPI``, eg:: pip install heudiconv[all] Manual installation of `dcm2niix `_ is required. You can also benefit from an installer/downloader helper ``dcm2niix`` package on ``PyPI``, so you can simply ``pip install dcm2niix`` if you are installing in user space so subsequently it would be able to download and install dcm2niix binary. On Debian-based systems, we recommend using `NeuroDebian `_, which provides the `heudiconv package `_. .. _install_container: Containers ========== Our container image releases are available on `our Docker Hub `_ If `Docker `_ is available on your system, you can pull the latest release:: $ docker pull nipy/heudiconv:latest Additionally, HeuDiConv is available through the Docker image at `repronim/reproin `_ provided by `ReproIn heuristic project `_, which develops the ``reproin`` heuristic. To maintain provenance, it is recommended that you use the ``latest`` tag only when testing out heudiconv. Otherwise, it is recommended that you use an explicit version and record that information alongside the produced data. Singularity =========== If `Singularity `_ is available on your system, you can use it to pull and convert our Docker images! For example, to pull and build the latest release, you can run:: $ singularity pull docker://nipy/heudiconv:latest Singularity YODA style using ///repronim/containers =================================================== `ReproNim `_ provides a large collection of Singularity container images of popular neuroimaging tools, e.g. all the BIDS-Apps. This collection also includes the forementioned container images for `HeuDiConv `_ and `ReproIn `_ in the Singularity image format. This collection is available as a `DataLad `_ dataset at `///repronim/containers `_ on `datasets.datalad.org `_ and as `a GitHub repo `_. The HeuDiConv and ReproIn container images are named ``nipy-heudiconv`` and ``repronim-reproin``, respectively, in this collection. To use them, you can install the DataLad dataset and then use the ``datalad containers-run`` command to run. For a more detailed example of using images from this collection while fulfilling the `YODA Principles `_, please check out `A typical YODA workflow `_ in the documentation of this collection. **Note:** With the ``datalad containers-run`` command, the images in this collection work on macOS (OSX) as well for ``repronim/containers`` helpers automagically take care of running the Singularity containers via Docker. nipy-heudiconv-217744b/docs/quickstart.rst000066400000000000000000000071761517415366200206300ustar00rootroot00000000000000Quickstart ========== This tutorial is based on `Dianne Patterson's University of Arizona tutorials `_ This guide assumes you have already :ref:`installed heudiconv and dcm2niix ` and demonstrates how to use the heudiconv tool with a provided `heuristic.py` to convert DICOMS into the BIDS data structure. .. _prepare_dataset: Prepare Dataset *************** Download and unzip `sub-219_dicom.zip `_. We will be working from a directory called MRIS. Under the MRIS directory is the *dicom* subdirectory: Under the subject number *219* the session *itbs* is nested. Each dicom sequence folder is nested under the session:: dicom └── 219 └── itbs ├── Bzero_verify_PA_17 ├── DTI_30_DIRs_AP_15 ├── Localizers_1 ├── MoCoSeries_19 ├── MoCoSeries_31 ├── Post_TMS_restingstate_30 ├── T1_mprage_1mm_13 ├── field_mapping_20 ├── field_mapping_21 └── restingstate_18 Nifti └── code └── heuristic1.py Basic Conversion **************** Next we will use heudiconv convert DICOMS into the BIDS data structure. The example dataset includes an example heuristic file, `heuristic1.py`. Typical use of heudiconv will require the creation and editing of your :doc:`heuristics file `, which we will cover in a :doc:`later tutorial `. .. note:: Heudiconv requires you to run the command from the parent directory of both the Dicom and Nifti directories, which is `MRIS` in our case. Run the following command:: heudiconv --files dicom/219/itbs/*/*.dcm -o Nifti -f Nifti/code/heuristic1.py -s 219 -ss itbs -c dcm2niix -b --minmeta --overwrite * We specify the dicom files to convert with `--files` * The heuristic file is provided with the `-f` option * We tell heudiconv to place our output in the Nifti dir with `-o` * `-b` indicates that we want to output in BIDS format * `--minmeta` guarantees that meta-information in the dcms does not get inserted into the JSON sidecar. This is good because the information is not needed but can overflow the JSON file causing some BIDS apps to crash. Output ****** The *Nifti* directory will contain a bids-compliant subject directory:: └── sub-219 └── ses-itbs ├── anat ├── dwi ├── fmap └── func The following required BIDS text files are also created in the Nifti directory. Details for filling in these skeleton text files can be found under `tabular files `_ in the BIDS specification:: CHANGES README dataset_description.json participants.json participants.tsv task-rest_bold.json Validation ********** Ensure that everything is according to spec by using `bids validator `_ Click `Choose File` and then select the *Nifti* directory. There should be no errors (though there are a couple of warnings). .. Note:: Your files are not uploaded to the BIDS validator, so there are no privacy concerns! Next **** In the following sections, you will modify *heuristic.py* yourself so you can test different options and understand how to work with your own data. nipy-heudiconv-217744b/docs/reproin.rst000066400000000000000000000204671517415366200201120ustar00rootroot00000000000000================ Reproin ================ This tutorial is based on `Dianne Patterson's University of Arizona tutorials `_ `Reproin `_ is a setup for automatic generation of sharable, version-controlled BIDS datasets from MR scanners. If you can control how your image sequences are named at the scanner, you can use the *reproin* naming convention. If you cannot control such naming, or already have collected data, you can provide your custom heuristic mapping into *reproin* and thus in effect use reproin heuristic. That will be a topic for another tutorial but meanwhile you can checkout `reproin/issues/18 `_ for a brief HOWTO. Get Example Dataset ------------------- This example uses a phantom dataset: `reproin_dicom.zip `_ generated by the University of Arizona on their Siemens Skyra 3T with Syngo MR VE11c software on 2018_02_08. The ``REPROIN`` directory is a simple reproin-compliant DICOM (.dcm) dataset without sessions. (Derived dwi images (ADC, FA etc.) that the scanner produced have been removed.:: [user@local ~/reproin_dicom/REPROIN]$ tree -I "*.dcm" REPROIN ├── data └── dicom └── 001 └── Patterson_Coben\ -\ 1 ├── Localizers_4 ├── anatT1w_acqMPRAGE_6 ├── dwi_dirAP_9 ├── fmap_acq4mm_7 ├── fmap_acq4mm_8 ├── fmap_dirPA_15 └── func_taskrest_16 Convert and organize -------------------- From the ``REPROIN`` directory:: heudiconv -f reproin --bids -o data --files dicom/001 --minmeta * ``-f reproin`` specifies the converter file to use * ``-o data/`` specifies the output directory *data*. If the output directory does not exist, it will be created. * ``--files dicom/001`` identifies the path to the DICOM files. * ``--minmeta`` ensures that only the minimum necessary amount of data gets added to the JSON file when created. On the off chance that there is a LOT of meta-information in the DICOM header, the JSON file will not get swamped by it. Rumors are that fMRIPrep and MRIQC might be sensitive to excess of metadata and might crash crash, so minmeta provides a layer of protection against such corruption. Output Directory Structure -------------------------- Heudiconv's Reproin converter produces a hierarchy of directories with the BIDS dataset (here - `Cohen`) at the bottom:: data └── Patterson └── Coben ├── sourcedata │   └── sub-001 │   ├── anat │   ├── dwi │   ├── fmap │   └── func └── sub-001 ├── anat ├── dwi ├── fmap └── func The specific value for the hierarchy can be specified to HeuDiConv via `--locator PATH` option. If not, ReproIn heuristic bases it on the value of the DICOM "Study Description" field which is populated when user selects a specific *Exam* card located within some *Region* (see `ReproIn Walkthrough "Organization" `_). * The dataset is nested under two levels in the output directory: *Region* (Patterson) and *Exam* (Coben). *Tree* is reserved for other purposes at the UA research scanner. * Although the Program *Patient* is not visible in the output hierarchy, it is important. If you have separate sessions, then each session should have its own Program name. * **sourcedata** contains tarred gzipped (`.tgz`) sets of DICOM images corresponding to NIfTI images. * **sub-001/** contains a single subject data within this BIDS dataset. * The hidden directory is generated: *REPROIN/data/Patterson/Coben/.heudiconv* to contain derived mapping data, which could potentially be inspected or adjusted/used for re-conversion. Reproin Scanner File Names **************************** * For both BIDS and *reproin*, names are composed of an ordered series of key-value pairs, called `*entities* `_. Each key and its value are joined with a dash ``-`` (e.g., ``acq-MPRAGE``, ``dir-AP``). These key-value pairs are joined to other key-value pairs with underscores ``_``. The exception is the modality label, which is discussed more below. * *Reproin* scanner sequence names are simplified relative to the final BIDS output and generally conform to this scheme (but consult the `reproin heuristics file `_ for additional options): ``sequence type-modality label`` _ ``session-session name`` _ ``task-task name`` _ ``acquisition-acquisition detail`` _ ``run-run number`` _ ``direction-direction label``:: | func-bold_ses-pre_task-faces_acq-1mm_run-01_dir-AP * Each sequence name begins with the seqtype key. The seqtype key is the modality and corresponds to the name of the BIDS directory where the sequence belongs, e.g., ``anat``, ``dwi``, ``fmap`` or ``func``. * The seqtype key is optionally followed by a dash ``-`` and a modality label value (e.g., ``anat-scout`` or ``anat-T2W``). Often, the modality label is not needed because there is a predictable default for most seqtypes: * For **anat** the default modality is ``T1W``. Thus a sequence named ``anat`` will have the same output BIDS files as a sequence named ``anat-T1w``: *sub-001_T1w.nii.gz*. * For **fmap** the default modality is ``epi``. Thus ``fmap_dir-PA`` will have the same output as ``fmap-epi_dir-PA``: *sub-001_dir-PA_epi.nii.gz*. * For **func** the default modality is ``bold``. Thus, ``func-bold_task-rest`` will have the same output as ``func_task-rest``: *sub-001_task-rest_bold.nii.gz*. * *Reproin* gets the subject number from the DICOM metadata. * If you have multiple sessions, the session name does not need to be included in every sequence name in the program (i.e., Program= *Patient* level mentioned above). Instead, the session can be added to a single sequence name, usually the scout (localizer) sequence e.g. ``anat-scout_ses-pre``, and *reproin* will propagate the session information to the other sequence names in the *Program*. Interestingly, *reproin* does not add the localizer to your BIDS output. * When our scanner exports the DICOM sequences, all dashes are removed. But don't worry, *reproin* handles this just fine. * In the UA phantom reproin data, the subject was named ``01``. Horos reports the subject number as ``01`` but exports the DICOMS into a directory ``001``. If the data are copied to an external drive at the scanner, then the subject number is reported as ``001_001`` and the images are ``*.IMA`` instead of ``*.dcm``. *Reproin* does not care, it handles all of this gracefully. Your output tree (excluding *sourcedata* and *.heudiconv*) should look like this:: . |-- CHANGES |-- README |-- dataset_description.json |-- participants.tsv |-- sub-001 | |-- anat | | |-- sub-001_acq-MPRAGE_T1w.json | | `-- sub-001_acq-MPRAGE_T1w.nii.gz | |-- dwi | | |-- sub-001_dir-AP_dwi.bval | | |-- sub-001_dir-AP_dwi.bvec | | |-- sub-001_dir-AP_dwi.json | | `-- sub-001_dir-AP_dwi.nii.gz | |-- fmap | | |-- sub-001_acq-4mm_magnitude1.json | | |-- sub-001_acq-4mm_magnitude1.nii.gz | | |-- sub-001_acq-4mm_magnitude2.json | | |-- sub-001_acq-4mm_magnitude2.nii.gz | | |-- sub-001_acq-4mm_phasediff.json | | |-- sub-001_acq-4mm_phasediff.nii.gz | | |-- sub-001_dir-PA_epi.json | | `-- sub-001_dir-PA_epi.nii.gz | |-- func | | |-- sub-001_task-rest_bold.json | | |-- sub-001_task-rest_bold.nii.gz | | `-- sub-001_task-rest_events.tsv | `-- sub-001_scans.tsv `-- task-rest_bold.json * Note that despite all the the different subject names (e.g., ``01``, ``001`` and ``001_001``), the subject is labeled ``sub-001``. nipy-heudiconv-217744b/docs/requirements.txt000066400000000000000000000002151517415366200211530ustar00rootroot00000000000000# should be "installed" from top directory since that one refers to .[all] sphinx-argparse sphinxcontrib-napoleon -r ../dev-requirements.txt nipy-heudiconv-217744b/docs/tutorials.rst000066400000000000000000000027151517415366200204560ustar00rootroot00000000000000.. _Tutorials: ========= Tutorials ========= .. toctree:: quickstart custom-heuristic reproin External Tutorials ****************** Luckily(?), we live in an era of plentiful information. Below are some links to other users' tutorials covering their experience with ``heudiconv``. - `YouTube tutorial `_ by `James Kent `_. - `Walkthrough `_ by the `Stanford Center for Reproducible Neuroscience `_. - `U of A Neuroimaging Core `_ by `Dianne Patterson `_. - `Sample Conversion: Coastal Coding 2019 `_. - `A joined DataLad and HeuDiConv tutorial for reproducible fMRI studies `_. - `The ReproIn conversion workflow overview `_. - `Slides `_ and `recording `_ of a ReproNim Webinar on ``heudiconv``. .. caution:: Some of these tutorials may not be up to date with the latest releases of ``heudiconv``. nipy-heudiconv-217744b/figs/000077500000000000000000000000001517415366200156715ustar00rootroot00000000000000nipy-heudiconv-217744b/figs/environment.png000066400000000000000000002051671517415366200207560ustar00rootroot00000000000000PNG  IHDR#$LbKGD IDATxy\T̰2l" "+hji.an~fVk^.nn+o (l ɑEPQ_/^ř88y6 -j;""""""n""""""" aMDDDDDD!L4I70&""""""&DDDDDDD¤HCtin""""""" aMDDDDDD!L4I70&""""""m@DǯadmADDDDZ >Ya<2MDr5GvDDDDWsq:!Sa9@o{|?kx\ute6{L 7ڒߎ׭svv?}=U ""zZ}Ae_">xC䇎mM{/aKX=o]A%^l.F;' Ȥea˾DLl-Zo}׶e=$ g'3Rd0;؋uu8p3s^IY%bNAWk;^PT#FvNƾcWlt)J~&=ntQPTc+eM vIƴ a09xE~zj3B7"`Ի-t~mai+3@"v4u& x9=2^R缽R^n ^{XDduA0y64ޓken)cC1{Y ]."""Qh J*1'5bNad S}\Y3c}t pcPZ^+!5(*fbsOeQYD~İdrf+x5+6FBJJ*`afP?9 c^/*e /W_*D[`NA,}5aX$RoD:bڸ06xabW'[+cHX ^_?/{,xp#Z D G~4~u8y7 ?$}^/{,pٷageGtƀ}$jyთ{ެ{9ذ;XTJ,^PQYNv0,p=NaVF}pחaxudgH`sBqy;P9-7~r߮=p!5"j5^!zwg7X`:~tV fMgpl:^lʫIxMze1Q=]Yx7g'K ݱAxeDgN_@ 9ϟ\ͱjLZﵿp,0'śO}Uϋtu8Ż{`Ӣ1xqx6K7kO4o{[Ub'P󺃍 te02aq2>Yw%` )9qC;4~4;ch'7G;0g4 `{e#ɘ2'|A|hl, g1wb@+Dnfϟ?#[3KYsIT/zr3pkC TA<kͮjZ<ħ`@dR wХ8~J˫`jvteɤb[VP\Uϣg >O<+/~?m>Ov iYE뱰27~G܅dgq2>q[%A/Wka`{el#Ƽh  դQ_oUbٺS6/{ &~߮=} H Sg Y~f} T".&3E@?~3|Z071OkNLwKY31{@7'Xo5M .V02g aΰ4¶j=;];>[QR?PT*T"cX3o8#gɢY#""ZLUls 0xlx?oq[%bK=gPY3nb_njeA8ؘ@"Vm?8oֻ55<^N%dZYCE"zwvOrZ<ܬcl?{b8}0B>xqxteG;\+c`T;%eu?N'dBQYP7qJ  ex;5X;ncieU03okqQKӝ⓳wo/m0%J0On6#:תz{YcO"=cnH{_m """&D *H2|=laf7unI ,cge;[b}Տ&Z@e׶޺n cVuI%4aDL&S PJ$pq0C߯gH FӗsC2_}8;>pXr 'O7aŝd2)L56""I7iܮdW!!%'̞j )l;nODxv7p\:\Fx7|*m-ĶDs@;' Łאx'>'=: h*LNƃ܏wEO-J+!3 AbjjatWGstǶ$;^KNry;^|vڻ1vY}ODDDLl{ FzV:ɉ{y% 2}+D\ 0̻FW -U+#_TC »Aoמ 7-sPӕadܙ+!o)Q,;Vztu0AN~Z6_GRwH .f_\uvXsCB|#jkjĜ+sCxZ5x~S(kDts8ۥuHVװ(WTcGPP\6|8@_Oۢ ϋdR 1毟W_M@ںy@_WǡBQZw sWnwUEAI-VQ[Şn"Ҩ{kP{_ݽUy]–}xwb0,+6ƻB(]_9v;05GNr,&7Ge?÷;t싻/;\(ķkOša&sexO#;+߳35: Fru xzyAQĖ}^ vVƘ6<1npq0L&KEZf&ECng* {kc_[x6҃nvL}+& !6NXaΘ28t7a/G+ز?^V~^r_pj[㝉=Hi`gD;kv^@O(-!;:⥷,Υrd02 R'#C`ci{.E{!JQ zPc Kġi+,Gemo&tG ؤ FxcunǬoc .:6}RO<;Eې8iyjIcScPS#3?bXE8`{OFYw;]>^@^&~ih4;_=]z~i$B}3q 6.Pꬭ+{Nۡ 7Pe"rT|c,zTxFm\Yv6\dWl2gaʘPmBDDHr"6,Zeg rD-g;Щ4|c,޹T$"kЫ+ ӖI%x~_eXq M<7nÇѱc{^722.]BbbbKÆ Cjj*.^Pm&;;;8;;k;RFF!A&&&?ڱ_ϟxHHLMMk555L.>CmҤI8p֬Y2ؾ};u._raaaHMMHaÆ!//"#"jSI75 LcҤIjbbbpy<3gNkrww}ړO> YҺlذ 'Nl5ѣ,P(W^y"""ŅH;;;$$$ ++K5*66Xr% ..ضm̙;B._~8}4nܸqޘ6mԮYYY/ptt'ƍ!9r$ & ##NR|ܹ3о}{5 qqqbhO^^}tr~~~4iRSSyw؁KK.ҥC9<:::?~|8СCxgЮ];8::"449 E 8]ts|̙Fvv6`Ĉŋ1h rx{{O>R? >7{Ftttk>}cƌ;r9wEATF~о}{p>|;wFpp0ڞnUxxxgɓ'DD!LI ozT,Xعs'~w\~'Oƫ1c 66oVZŋ]cҤIXhF 6 ** 4h 8y$>3\AAAuzޔJ%ƌe˖aԨQX~=ϟL >Ǐo3k֬+UVwѣGF… qe\GAXXfΜ۷GA&LTUVypƎ }}}ݻ>,pB]CŢEZ'\OO'ȑ#4h[ 8vV^]v߾}[<2d_cݺu/2zc<|0FR(̝;>|8Ο?˩5GD"⣏>P;tzXz5^~e 6 c8rxk.̞=oxG ATTVX`[lAff&.]cz###7vѣG1k,L:U<ާO`Xn͛'|^{M-wyGJR-9s&~78pC i>i3;w"??VVVkQ/"`ٰadd7|SN!$$cH$Ɗ+гgO{g+Wɓ8<~{ܹsի`ƌ-[`bboEEE-[iӦKl8FOOW^U;SYf7o?H@PPV^n?5{^GM=ㅅ{۶m9::ŋ|O[[[s~n233|]\m(]o PTTA mj{@5 0Xjn݊"q/ubnnL&~s @|n233!֭[Tmh|]u=ZADDI757a՛6mqettt0m{H IDAT4=ZΝ;cժU<|ݭ%;22Rn^{ݘGDDä7...j{x}}},,|'Utvv6rx\5\E!HPUUUJ-{jK}#YHs8n.FBByyy{Ƚ#(({Q{QTT qaF""j=nDG56NA9bHLLD@@EG0`;TWWP(_7z#0o<|(,,;bcc'j . ѣ0|?lׯᅬ & ;;| 8ٳݻwoѱ~z"((~~~Ϳ/1i$ܹ/Fxx8^9s&FÇcԩ033Þ={?bر d ݻ1w\L4 yyyꫯZʧ~HhPSS<8p\.G`` :u[J߾}憴zݎ͛~h׮fΜk/2rrrn:,]nnnx뭷PPPgϢE öm3fr/"{=HǍ;v`pttW_oplذQQQ㏡ lݺ;!"&FƎ8qpvvn8{ #۷C׮]rB.Y0nByy9aee% >+ ܼyё VV@~~rss!J$%%222 ___&ޏ;vj-%)F```s;9vܼyu7#YbnܸvL*B899uRSF'˼#-h6n { 55&&&Evn8y$222ąBH2]Yv"IIx{WSqe(//#=='OT* ={ .nMrY\r055E||<''Iv BBCyyyؿ[W&ӝYeffbϞ=pww?:vղ~L!77ٳHMMUKU $%%o~99وkWѱ/*++qXYYO>bÉǑ[ԉ'N%z% JIIƙ3gN:!??W\#wTr:tJ-Sʝw~~~]ެgee!++ w!C6Rwܝl߾;vD@@ڷo!=vtCjj*$ d2\]]:sĄmbL&CDD_% \\\qu֛t_:xxxʕ+HON:vohh?~?푗'Nڵk-~}Ҿ;Q47W\kﯶr6Mv7Ո{Q=UUU8<Ј[LP(qh8Gjj*]Zg8VT D LV;;?? e`aQ7!733Cff&jjjPRr[;JfvtM5rK&AAAL&CLLLkIDDXa݈4(JTWWC&իj=ʪQ*8|rrr`nncccdR(FϫjEڪ.sT RDHH6H&FȬ~LDTs &ݍHMM.A{UQ\\f-Lq999nL혱[_DΝ=j X[[Jr)T&&&pqqA0eрX 6 -„ QŤ kt{{\~Mi5** I())5!BCĉϝ033CyyJ///I3_q|.]pȑ'Lۦpm@2e :t())Abb"v؁UV^_|!.TW\AXXRSS4x6lVuwwǴiԎ".. .ĦMpǦW^̙zr9^{5tرU ""uLQ^^8884Zх|}}am]w$___֮jnll'| P(055Ry&LMMadd___-akk7o ::yXXwܺuՁ# jlt7&D׿:̙3o&;XXX`ͺѣG[2D:uTŜ9sj*L2E ѵGb .Z|uwwoğj~XXX׷=000l}"~wwwYL \wݩp;}}}ǧ#<==$555}6OvQnz9rr/8Ǽ>x \:`ԨQ<ݻ7Μ9~puuŸq㐞v;wbȐ!򂋋 z~ S tqq}Y>>Xx1rssQoڷo??f% 0@!*/ GGGxzzbܸqxڹO\.]`޼yjKKK駟sppp@]5v؁KK..]V֭[SONNN JX[[cݺujuNPk~ѽ1~( ٳqqq Pg0ᦇAbb"Ə0|n7qD6m֮] eeeիWԮ򟕕iӦƱc_#66GFuu5௿ /,\+V@N0}t|bƈ#T*s"??ÇǏǐ!C֭1w:CnKWW/***&H۱qf\t Ю];ؤIh"96l@TT1h $%%-7n{Ǐ϶ .˗rJ9raaa9s&o8x ^z%899aҥXz5Ǝo>:uꄭ[Fh 2Db,\zj=W[n-}?i81chh@;w;vl1JKKQYY := aM,=XjUC1p1L<'N˗/Gee"Xx1:#%%=z?,пbڵC Aqq1~wdff̙38z(f͚Sӧ0|[N\ATbb3g~Á0d8p `3>>>kkk~p5ʼn^~I^m-((޻ڵE5 ǐ'u tuu`/n'v/LaP\\ѣG k׮QԈJddd1lmm_~歹J۷ڱ=zΞ=cʔ)ud2maa!Μ9ǫ} `ggL̩S0j( {0`-[>Pҭ7eeeФ 5ԩSBwA>}W_ө xꩧԞ v/_~%ڞZ:tZqQը0C;A*ESgϞw}j4œO>h4wQh-w?it?^vgaMR#!!K,Q[ %%ΰLv8ׯǤI0fW^x'1j(UI}j^tII ,XK\#ήdffB[μ;<UM!r.QC:v/B/2t׫U}ֵkP(rz[}xֶN9D"&&MBZZ;ر#1cƨ3\MikS "aM"&GFF|||0c G|⢶(q(pq۷x뭷_cΝzTo"U N<{ſo<077D"ԩSvnEEEmQ ӌĿzqÇ-PsbۃK33:O:QQQذaF)W=36mqKEEE{UYY ]]]T 6g=Tٳgcԩسg<5k`ٲeOŹ ykkS "aMp{A^^ɓ'c֭d011v-{;*HPUU.]iXaa!V^ KKK۷ɱA%jiӦa͚5O0p@qn>lmmw> J322`jj*nxϣ?<>3ͺCwV)mm M""j\Z\P_|%''3ᦇyرcņ;wjڽzvZ `׮]jTs&CCCwݷom={q,**BPP>#"44{[Ԯ>Is1SXX'֭[==&ӣZ9=(CCC̚5 999O<`ժUjkjj0}tq3m#** q8'Y:Jff&N:fΛ7_uvt(((\5F5u&++Kj') "iS=EEE(**v%&P?~<-vSo9\ŋxbYJÇZZZbܹBNp)C`` wA~ !!!áCc=֭֭[ѣG̜9FСCꫯB__˗/quO?xo .\ 6g!{W:.++Õ+WyfwqM]Ԓ%KA5\mĈO?Cpp01`|wFP(믿 /FEEBCCq,Y< <==Ѿ}{ . ѣ0|?lV՘={6222зo_ҥKX|9BCC!QSS߿/lll/wްÇeMikS0`@""j"~tZ&=zp!//_}lmmՎ]zUpss텤ύyAlllpaݺuj׷bccpH011*$$$8 FFF gggRXr'l޼Yѣ`hh(XXXݻb믿 悾.L:U~zcƌ/===M5j9'&& ;w www!##YW||HjMBbbbi}}}[n ~A* Bvv'dff о}{AOOOÅM6{uaʔ)' ӧO2332ԩSWWWAGGG,_ԉO& C\>SSS@M&$''̝;W077>ݻW sssg.]$ ƍkV[w֭aѢEM:{pImQ ۴ƣAk"Q#~|;^ƅYرc011\.oȚ&22.]BbbCyݼyvl苉CZ'== v(DBpivmXҦt&&DDDDDDD¤HCDD7ovDDDDԆHCtin""""""" n"D7o͛H!P KOOGzzàGC "*&D1^^^vE::gq닚mAZP({n, Q=n4J!Q v())7`dd{{{mCDn""""""" aMDDDDDD!L4I70&""""""&DDDDDDD¤HCtin""""""" aMDDDDDD!L4I70&""""""&DDDDDDD¤HCti """j㑕ժuZXX $$U%rrrpܹVS&k׮066nz"&DDDDZ,8;;CWWUꬩAzz:*uQ***KKKXXXZٸpBCC!Z޶I7")) k`aa[[VApEd2A*mٿH; c IDATLLDRR|}}[޶s@T000comm +++$$$'" 99h߾}&777q in""""-HLLBUB&… A+1UHKK+ 9푘Rpx9iUBBJhUJJpwwGRRRSSᡵX|mQm())ѭ[Vqo tV@GGGk== sssmCCCXYYPۡn߾|h;133ÀD"#T*tkn""":.E6Fpwwvmh? """"ues"Zؕ+Wp "((~~~}GUI/@ B !" (JWDˮ^]{k]]u׆(` HS"-!!I ̜CC $!s|A"6ڴiDBI\r|q_#ng\<Gޜ<[u͊^/7n<}/@bbb0`]wY;̜9W_}.]?pa<<<oj:?\ÇcZ gРAp lUӯ{ų>[`ѢE/7R-"""MΚ5k(--Ƥ{|WoS""NaqF>҈k׮… 2e ;v> 11JܫWf rz-?Nyy%}oRtHԭ[76nݻPL>.]gϞ nj3.&K1"RWAB#a'mQwLo2lۙ9s&'NofѢEDGG>--ѣGSZZ|^YY1< cǎexxxw^Fjeԩ >ܥ &0j(/_NTTKyll,)))̜9￿JL/OI4I}!--iӦ׿եcDz{jۯZ4ӧc<ٳcFR<@~~>6l ==}Y={6\sM> '|¨QxO]}||k6mZIѣUN>J;w /pwoV^뮻2d&M{FBB=̚5owymNxYf cѢEZ~?K]XjM:MAN9˜t<ʏ⃅umGQԢ^2}j*mVퟏ;wRRRr^vlقiujs5װzjvR4Mnfz)M?_Y6k,v_|Kׯg{3СCɡUVu~&o߾oߞiӦU^eyնo0Mw.ׯ ;wn #==.+6jۻw/˗/`ܸq. bbӂωкu:Y&44u+9RuTϏQF_PVVVy}ƌp ʕ+]fz|J tHewy''O&++kv;cǎME2uԺ;cǘ>}:۶mĉ8ʩQNKsO|.111RW]F%St%~/ZuVev۴$֫ i]j~1*v}Ob6g뮻>}:-bٳ7W[o߾?̛7뮻2x` RtHvw/_ T_2MjZ)--o+Ã6lPgubbqGaIm6Kt#3mٲW[ޮ];ydX]UuZTlXKLL }Çgƌ1xjɓ;oꫯgsHQ-"""MZdd$^{-_|f5 cǎؠ;vӫWK,᭷ު8Gm:zw1Yrz/{8J{bX̰>|%KУGlaaa$&&|rRSSkL8oGbcce޼y8^TTļyԩ;w1;3W^yǏ3f̘sFЫWʟiii<<3ׯʹ[DDD-[{:*.&w٬t}##<4Ȩz;&|)6o\esHyvQ뽤a(&鍊n"00O>[oQQFK/ٳ),,$;;_￟ 5aeͭLg̘8W=Hol6< 6m"77yo#k;0rT/F܋ð͠`zXNO)?Zf#%%p6nȇ~ȀXj>(Æ -ƒ>k׿ڵkYz5dcG;33|nMƖ-[زe _|Æ /7M AO#))ƺׯ;ǎT{=, =z8(Hѣ㏹뮻j̙3r?O<&Lӓ1cW_ѥK&LO>ҥKyG8q"~| 2)StR6loɓO>ɠA aʔ)yΣ|:w14VU3~ 2JwQ _o<:E߻b6˙裏 /@Ϟ=y뭷_Ry0 |ಬ`ݛp)KNN>`u]wŲejiiiL<嘱HytRI2L=0럙b3_[ZZ|9aaau:nGGZZ tw(L8KgԽaibmGl[R)ʧSNubU]v5¡CHLLqG,2220 (ZhqX8ta^q_۷oeTד͛7cFuEEEGhh(XsY!//={0`7҇+Iώs4-"""""M^ϻMtt97T 5i6E~\i]v=(rB}S˥Q~$kd1aV;ܱk5@" @I\RY'/X--m-BI\,㉊h賀D)ǫ `x ãC3 L(RTTĺuXn~~~$''@XXC7sH;=ds]|>PD {Y6RJIEDDH+v0OLL$((aX_/:A{&EgAI\4QhDBB[k'b˙Ӄtܓ^4U.?JEDDy{{@BBFD.g~!-?"_wlޞGĭtE+߿?`"H[DDD.ʥ-++K\B|}}b80. YS-"""nnww5OOOHy+++c֭tܹI鉯f,%"""Vݺusw"DDD0[~~>~- ::H#2DDDDDDDꉒnzt&sPp!fI :w׿5KJ(YaVe۶Qx53[x Lu7cO͚E?c ww5KנAIz9>\ec&aTu (Gk}f%ݦiw^iӦ e’owIdƳ_?JV?g8q޽33cOM諯=tZ^N{Qiֈ ??JİVg<~KX^ÆaҜ#V+ @{ k8p= !-ߦ|n,aaXS>޻֭~8fVe۷c‘Iьx7;+((tA…x P34G.6 s]Ála /[b"%+VP{weҍL:u"0v zٳ*,-e6٫yyLDfðٰb).&pdr<7(9{H'ORlߏl;s&w#(C_|׷{יły(' x/0'Mx| ;YDDDDD9jj JJJhѢeBSӭϥ"&CV~4Vm4)Y[׮I/% 1OpgZX.I?De `mKv1fndl;{̢"ʷlô۝#gv99gT<;*Hi8p8rx4ҙSm*W|6;YXYXHY.'OQkdNݠfm 6lY^YX%%5ծ/bob5>/6[32z3e;wwMBcܔL=PDDDyZ N٭ChR]m燗@ G=qgV\[||V>;JJ*b򴴪UwrZd,6o1V11<ެOy\t|Dޙ\6*Z_W[~\_@+~*w""""v-NiXMn 5jtI7@hhh寃Y+llfG`iӦ~JOMzIdq*u}v;S` Du&ȨmU=㙻7sژItDK#Z;&#m7mrCEDDDY~~>?}=phkk@0M OǴ۫cOMllX۵s-,-qkÇi%8U5qT,*–pŵbx{SRqjYk jۉ\.dm )߱ݙZmQbϽVioX͚Uf,.dB 9چ>W޻y]we6?iݻk L""""""&^7ވڅK˖:vrƺ*|+ľ?=z`s SR(߷c?]W/[gN<{~ ;9ư IDATiccc 7t,Z9<:$sYBC:usΌ@;WK(<0-m1A]//s'8VN%̣?cbv KX%WDDDDD1k#f:]X#xa!],}uϋʱUrwI0:ނv,׼A2xiHx`tq2sGpb?pN:ތe8cP~X݃qlyp1Vm.}V}sar"g`v%F׻w1s1:܄`Xc,'at9TQ9mwbwK7#1o{O? o`#7ƕObww=0wM`\okXVo!X>6s""""""RHw h#xct 0q&LN݃͟eGXǮc_1̫n3tːc1?b;`D\q20]}&F۝:2X:/+\]H,60:`X.YFP7,#g:IX,ƈ|MXrKc͐YGw~y6S1X:ǒ1::vkMoDDDDDDP6%FpwDݟC?1lj`re{UOcse1á0\'z㇧0ň( ,]ı%̌NǷhM/?`b*x:+% gW|ϻMeT'cλݙ(#*gD|0*֢{N_K\fm87uA3˕u1a}2jv׹~m JrO}23װv0xb?fV,]nw p& 2OVA~/Mexv&{gXip|5szok(9Q;i(nX {1~s:ivꉹS71|BqC+0y|;p]QυoFuB39JlX0:s%5>ssTNc+GDvm\xH|9(ڂ=iK^sw=@!3{Y^z9VuF%;:}[߯Z~8 8FPk^tN˶x:7 [:ֶF׻1.7#W8v~vK w`s ̭c Ftzmjbhӹ3zؿc;XOmV/;:o?s!6kݟCDDDDDj^Iwqym`\Ǫ'I>ru+pa.yζF sк}B1܁(/±?;ͧsdenЪytnv`Դt|B1j~F&Y6wGF+h[YJO:}TfG9S#馽Fbn~9ݬsۮml>J43_Lr~qg`^ 7E$WlXwzڷqjkk[<lADDDDD iF#&OssX<,uYuwUq?Vt s˿0"af<.; xzǏh^ sۇ‰g=jTFb qdma~m^ybYPe6~6o(v1an{ #jK0w~yhFui̚M\}#wg VyΩj96]K ̽31ZtH6`0q-"K^0|cϗxpnƒTz i|+AŒ+cl=#7\ c8VNĒ&aq #$ki Ӭ؂hs8持ғXn]P.HS_DrϤI?~<GDfXnquVZX13Vc$tCl:s0 RBBZ.3NO7 ų6_C)6s-SΏyy˴h_Yޯm?lKoLDDDD%ލOZ~KӖ2?u>Kӗb1, _zFƷMٕ :G6bhК{2,>IFnvߎ-naxp=meb@D oW-Ƿ um 70(kw^V~II4KJݯ"Ap>?eD  `D<هtT&_'KؗaX K9i3$(-hؚva{*rQ2Odr(e2=Gӹe:s(3?u>ߧݴWn6*fm4cا&biF۫/t4RJE΃Yg,l?Z3OX4PP3£DF3$j:Fh%,?Rcn\ hӒVkَ@^Jv#'pa2s3),+$2 _MKay! i?F%ݚ^.""""Ma4hA/>5NnjZjJcEX}ͲC˰vBC։v-SOrZ 'e8f>q}λޥ_F eHԐZg(j0 v~^/`%"""ҤM<0q/`ӧ:x ӧOgذa$&&Β%lݓ{=]U olfڿ5QM BC!!"r#Ov14j(#cF_m?goӑ* Zj(&m$''טt߿W^y//MxW?dyl6 tQ\ gnSOg:ouFh_}'l&e}WctFl%d`]:ۭnoj؝OX9M^p7 l7!QC꼩^S[DDDݛs~c_{5zYt_h?"y־YSyL#IrG9돮g7cE*7By!!qSNtj݉8LT[Ϭy+Z_#=ջJV]y$Y \~5JEDDt -ZDzz:~~~e$bZȑ#޽{R^u2d̘11cPRRŋ 2-p|rKQQp lْ N͛1M_~aÆѲej;vEqQ2d-Zh(J^:Ss[tddHFD+ r R__ZRt|[5H}yMtptFl'3itlّ#5ۑwOϼyO˳k6WqG;OX?tegҥ<sQ?1vX~mVkW^y3d222xW^#,,mV֫~ nfvEbΝv>3"""ظq#eee?~[ҧOL/3g?0Ѥ2qD>c/TYk*^,#Gy#Ytp_)wBlX,-#]\6bù[zN:76A[ ̐! nr7,I[›m)i;}G\\ӧOe˖|b̙Ok˖-yWٸq#'O$$$Ů]?)sv|s#MX`6 9-;nmѭm7 J `Y2%`xpG'';&m۶U[y֭|,\Х0 v܉8uVkw1bGkuI9\_}\}9< hʍvHH@ey޲??q"mXr)Ozîfbωkݡ%"""%$$0o޼j˖,Y¸q*?{*#DGGSZZz1+yڵ+/wO>j2l0^|Et1/SvL!:8HKHlt@<>ZIHsoot9*--e˖-lٲ///HHH ** h>i7hh^:ꆇ6#!034;" #ggN~ |(&',,:SOѧOk7x#6mbѢEGbb"{vi[o1n8n݊fwqqqйsgxꩧ+]FFFSOUNgOLLd˖-,YTʈ믧UVKfΜj_~0j(… 9v am IDATx4O%%%lڴG||<ݻw'**ݡ]:t0 :t,k|hyϠOg!niZcbb8qb-Zn;g.y_UTTT^^^ >T9'CCC\(((`Hrrrox!H#[DDDDX}I֬Yʕ+ &!!$Uk\z-ᘘñ8^z8=-}뉭,<Х ÃR>;7=Z (GWgu#|"6ZZ{'w6{TtŇY:G~'63 })(/`ˉ-^ŋ1Jl܇հVN[DDDD.S>[Hm*l-[ˉ+DŽ)acF,x}TNK?VrJ((-Jw|UU9餓B$@)wVt8sWPD:H:&tH# $!9iϗO첞Zk:2i*11ckh˛^oRVU5'ٚ˒KH/M'2?  /GߘJUE?~3u>LM8(vsݞӼ9I_I'8ux9.s!͹sXd 666#GPVVܹs9rHlѣdɒ.l ___<<#rswȑ#Yb;v 55ݶի<{,[YfѣGITkٷ0OsOzBUAV2moRX@7nZkW &|mSw̽(*jí U/m鍻;FeJjmMF!ŋ;w.fj2m^`>#JJJGyw9~8b;Ջ>krܹv111A__*֔UY1i ( x^"'jƍ+ ++F<6[1nI<SX|\x#FзolBqq1/ִǏ+gϞzl߾> [[$לO?Dii)O=/_&((}}}OuZ;W^ƍf?6k.x7ꜗ 99'O9~~~ 06l`Ŋ}:9oFFFuNߜ9soٳg=sL,YիWȈd jv\J?Ǝˏ?y߾}<쳜:uqƱf8~A`` < W\Ȩdggw^ygӥz OOOyԹ/Ieu}Q<(ICNPU簒nnp U\j5okƒ\ȯtVŠBb+b~f׷~Kk'ڎB! N`ȑrJM^KKK^}U***8zhy/,_\ꀀJKKILLw~Cjn[[[^{58tP0__-| b6mɌ7*1rHrرFWZZʱc=z&Y:u*7|]k;ZˠDϞ=9ydMLNQN{!0$d%P\V|s[!xܹs ,,jrȈ>}h֫W/Mu֒Ċ+~[`Ao)jyԩf}=߾}b6j厈T*5\<3>(ӧOghz#77Z"==7n0eʔ:oQ^^g9{lŽeCe[׮]tαy3컽#G4ž.V]5E|M4NIy II$$ 8NC;V%IBֶafffmN[MCii)Z款 )(~z謰6fff5d۶m^.]l2ϟ/''Zӏ;V{[5115ϻEcŽe˽nRVVax|vܹssssCOOZ^R~Ct-Se[FF򖻅S]_%>KXⳄ̒LagNN&JllqrU:]SAw{PWNMMm4s%$$Y{bb"ׯήޞ:9q ⫯ҚTTTB`…a8Wι1LLL1b'Nji&U~Ct-Se[BBB≦cA|%ŴZ؆'<`z[hDD?t?GŽFvQv.h U$$q92TUlg9xn:&Mj0,wh=6`4~#oBٲe gΜaɚ߫puuk׮ҽ{wn޼Ivv6~!@fΜI=ˣSN,^?V-!2pM^|ang#?JUEHf#f] cw qS;j&&Lr$ITTUp1"cw#f7nh:bsrBoJ+JIM&5/d*U|LuJ:' $qϝT(8YpԸ%::IBȘ1c000ZIc=Fyy9-b\xRɨQ֌9|03ɓ's LLL4IܹsO|DR*o~{wn1#G2cSܧ`aXK J};f`V YIQ';b3u,S\eDjn*)8%0c6uODvq0 Scahh|63Oܰh*]YM(׼E!hs(OMnٲelݺK.: N*++2d֚BQUI{v W+FSpc 4o'Uoߟ31N:.^B_O{3{ہݻh]y%yս秓c\0uӺM̠7TUO=ϡC컵lmu3Oh~c\M]b傅IUETVUVK&%'"wܨ0z/jB!:B&oF ]D1&8C 66(4RrSHI>LqҨ9 G9wwSdÀ1fd4m7B!BD`VYȒQ]F1ubg1(Fu?sUGuu"/nbΡ\:b#403dTQ r̹|R ~>Nh[!B꺚uOT*pabՅUtN9w̕+ؙ=ԟ +#$[[w4i sjcwR&Ѱ4 }%mB2I?Qi} SUW|Ctw@%%H0} ޝ3&IB!ef`LޡPPPO?{{#:e{ B!B<$B!B!Z$B!B!D+[!B!h%t !B!Dn!B!'ÄB! +4WT(J~G w}}}.]a{)ڙB!B&ѣeeeRZZJYYUUU~sqq!&IB!B4*A4[!B!קgϞm$B!B&GOOaJ>}```QDn!B!h"___*++VUUoG$:IB!ҥVoj&&&xxxCT#[!B!o߾n===|}}Q*%z B!PW/敕SD#[!B!wwwffftڵ~{ Fvv6eeemZ 0)++#;;M466ҲMaR\\L^^^ijjY)DsqmjSTTԦeZZZbllܦeIF˵6{{{ۦe !ڏJ"$$6-WT 7BJ^Jaaak``AYݻ7J鵼%%%\|6-Ԕz?֚$~qu,,t~K IDAT,޽{Ott4񸺺YBC^^zSNmRJ"&&v-,""R|||044l2 44țBq022gϞcccC{T*_m\YYDFFҧO6)nҦRq* 776-'''n޼Innn-h{YYYf 7B͍r۬\!III$''f 7T^ ??6+W~ΑtLѦ95I`o&++nݺ m#BQceemo``iii$&&yB< ѱ]L066k׮ܾ}6/_Xz-CFFqqqKKKKh&3t?K.K 7Pܸq]bB.u51RٮMI,,,'22vCAee%!!!nqйsg())i8h }}}ƏOΝ;7nh쌉 !!!TVVYDL\at8۷k MZZZ0 dD RWtLr7ggg `KGtIu$tO^ڽ=+DEE=qԩC@Rq111iO) ݉ ..6)Hڴm@Ggaa!J3JxbbbaP(h'w>`llܦT*177{899Idjj!r5}K[WʪߴёK-CDB B!B<$B!B!Z$B!B!D+[!B!h%t !B!Dn!B!H-B!BIB!BV"Iw svvƦ?~g͸AAA7[nq֭ ]\js|4ٳذe˖fϫ+Wҽ{Z8rH D|i+_ciTTT`cc/xmOggg,X@HHHc+55{'''nkk˴ijmWq_/"ov˗/oihr~G(--姟~jW^lٲѣG7{^ׯgڴi9rp\w}GUU>ҒWt mqMm￳{n^}UxGپ}{+%See%sa[;wHYr%W\akƟ1cyۙ={6[lCӳ>up^_eΜ9wO>?i׿ň#~zؘnݺi֧OH@@9SNmpw!))N:鉡!py:uٳgѣDQPP@||<%%%8;;: )* --?իWyf^z%qrss~:{s$&&3uΝ;ҹsg\\\000sbbbֶsaaaqRk챣R߿?}0WWW={6gΜ! lN3nee%AAA8::ҽ{Z1::\<<<p޽;ʛ;>wcjjױҲ ̝;w055ӳrzMopuuL>>F[HOOEK9]Ck#==TKۺWZZJXX BQRs{zzz2|p}Yf̘ҥK[k/ihZ^Jxx8K.eҥmmmѣfff|gӫW/N:EAAXZZҿdgg쌓־OBBFTrM ֭VVVb֭[XYYѣGz1fff:cdd%Jezk[LL :W6vYbiiYk <lBpp0Ç oh]~R5OK/]KäIx'={6'88###bsNŎHt!WWW݉wHƍӦMcԨQٓ 6yСC̟?'ORx뭷dĈL0ooo&O̝;wjjӦMXZZ2ydΝ͛7ߵƹy&:t'|1c3˓O>IIIf+W0|pxG0`1%00>ְk׮1}t٣qpu9|0@Ji{Ihh(_|2?~\k?ԩS5k3gΤO>8q~iFÉ:GСCyG9vX @`` >>>L:#Fлwo*rԼշOn7 dΝL8S2o<|}}Yh,^PhƍG~8q"L>]k_kV}[C^?~͟'x'cZƬX3gEHH}Ҝx'(**$%%#/m_xرs68h걣T*7nNbٲe-5kXlw֮]ˉ'r ͛Z7͝^>},XP?ZR̙Cll,{!99Ə믿IvV>>wW\ɋ/HXX7odʕٳ?rtc~fDGGc"""x'ouG򒒒Xd ̿/ٞCpPA}%M9׋ビ֭믿JjZd_ͭ[pss#$$W_}GyWĞ={}6=fz1o_BddyDD{eE||</>ЊGr5Y>[pww׹nJff/)) ooo =8::rRRR8{,&&&<㤤4jYBRqaJf.lرTTTh=8}4666tYJPP7q>c.16w_$nCAAA$&&2lذ:&99>#F```=9;w櫯w7n`Μ9b``ٹsgi&T* ,ɉdz{nrrrj߯_?q?oo*]v1tPu֤9κu9r$}~~~ 8W^y~C2j(7n>6`fΜK/+Kz ݵk| >kkkF{g&ioVqt-oΝrJz쉾>Ԩi =z ++&zѲLLLغu+уq;pA9BoͪUڵ+FFF 69sVz7w\sCJCCC֭[ &&&<L6I #Gd888```s97n駟jPoܸJ;<<|nڨem͛7ٳgo|L>ӧO/:1b={VSӧ8p Ӫ9s7#:u"!!AӜiۛnoP;<}~Ԫ"} rku҅/r{MO 4V 35sh޽dff2f٣###6o\+m'࣏>'XZZb``@EEEtԉYfuVڵ+;vSN^M9Z<կ 887uVԣ5P^Phۖ1ƦP꣡*r{MO;YJҺ,Gk:u"PouG kZJJJTsiZ9ӊ?~bVZEff&\z+Vk]ɩQ1֥9Ӷ7^ HJJ%''sm6m`6)uQwBГI&qi._̺u밵孷gmզM_Yh瞣F7k׮մo!""B:/XJ۩dϞ=̘1C+lqВǎ>Æ lo߾o2Og;n[Yׯ_:/gbb9j5>ј}xQΆOz U#77en^ZmZowwTɄ2x`yL+Z)3giݬwe׮]Yxhruܬ7ܚou-.<󔖖wi\ԫWz4;K___ϓO>/RE]رc)++ŋ:u KKK|||xG022ܹsTTT4=7#G?j15gj}-I  FFF>URYttta888ЩS{͍ /pBJ!͛;ws璚Z/88BQC{իHdݻ7;wɓk:>)Ac9x s%,, kkk(--,L(ZӖҜSuRֱ֘4} 66ooʪV z q5IIIiqt-OYx*JcXJ96|46l`޼yZM3faa>?}eѢEZ-k}5j-Wrzj.\իݻw={$..ssbhw,,,_mu]gqYN:Ő!CP*8gΜ̙36gw+))ᣏ>PjskKmIdƌ|WZ=Ϟ=KXX xgܹjzzz(sҥZV~ªѣCe׮] 1I}OII믿g.'""իWӭ[76\M9ztܙÇtRj ?z(O& SSS7yk73ۧ@ ֭zڟɰa4=RwsXRRmRB}@mϞ=kNhhcz Uw`_ho+VW^A__E[]ѵ3qww'88/// ߡ,YTMMMŋ3k,x]>lLrrr ڵkddd!W^!**͛7k.ܹ3yyy\vdƎop֮]˅ ?>gO>a… 0^ʚ5kXd ~a/̯… 1c&&&=zQFq-~ru-%KReժUZü5Xz5ϟח$vM>}4NMA/7oO?eΜ9tMu6qDƌ믿Nii֗ NoIHHʕ+( ꫯj5'Nlzm-z[.QmpbRRRpttgv9z̈́ 9n^^QQQ;VSuc̘1׏TP(,X? ʹ'--b d„ smRRRpvvwtҞRSSZ*++ٳ7ny;0tFWksvtVӱcxwݻse2d5 ///Ξ=?Ç͍AJdd$VVVYBjj*<#Ihh(lܸQkR{ ܼycҽ{wO~7o666Ayy9^^^K|GiƟ5k*+WСCywt>>> 4=z4ieZmد]&Ain׹k_6l)++cʔ)[NLs ())kQv% IDAT׮ ǥKػw/Ǐ'!!AV/8՛~ci}s.;wYx1O?4.\ ..~z6i]cƌϏL>|KۺT*={6FFF$%%ԩSƵkݻw%UoOkkkϧ+++ƍǚ5kx000Ќ`ԨQKƏGUUz}} z*ȑ#kz̶ի8lllt~XǴiӘ4i:ut2dofDff&JaÆӻwo֭[#Κ5 sss>+$&&j1l0ͧ<<}}}q=}t:RB$IhW WWW\]]yG!$$H***S!bbb~~~i͝y'7B_ ۛBCDy/0$xzzRVVFdd$!!!H-hQ :CIsrr;4!D@@@999r5222;4!CN[nʕ+;Z֯_Mvvvr~^"رcϟH$;NJ+/=H$B`` ƍ>}uԱҟ֐; $G~nݺٺ9&]xQ{K.᫯!C`RWb=z4FXg݋k/FXX\.޽X|`~a t6oތQF?#77;v@۶m7isNbѢEСu/#Gرco .ذanݺGRuԁMIIAZZ{iii{.7on/ji944۷G~~>lق3gΠK.&?Aa՘5k&Ou#88 4(| mۆWA5{?֪U F``͛cO>ȑ#-jsrT%KK.OQW^ SNի5jٳgS"`РAk&M`ƌرc郴J3t&L@ݱo>ܽ{y{{cسg$Ě㏐dXh^0 ,XCb;+S7"+Vӧ7FD"} >T*m6ˤ9B_,Mڵ9 44ӧOT*m .ň#վ+'?FLLĘT*ɓѠAڵ+իO?rc޺u mڴAtt4wƍ#>>رc+/]VX???,YD{PTFL|={[n-R(((g}fqF'Oƍho~-1,e . ==}{=..fǏcΝv Vjeyĉ={6߿7nɓ'3Y& ΀,N>]s* , LЏO:___'\%yyyCڙ.]bĉ8rH7# сĉx)F"J}T&G若Ѭ,{,Av*t O>۷=z@AA?V(hР>ڵd 6 : FӾзo_|8BCCooŋ f߽п\p֭[! O5/d}Qׅ ~߿;vܹsGw1bD Ұ,:`Xr% 2:q֭#%%;wĨQ0b&Lkף^;w`رٳ'6m+Wɓ'j׃-[`ĉزe ̙FUP^=/ƍøq㐘Ν;aÆ9r$VZM6frԩ i,~͒Ws5݃^O֭N:yG=?dtѣG3(F.]`ǎn Ϟ=ÁѣGaÆfK(9ȟ={d^rԯ_Rg)/{ǚ}SyȐ!ի9.\Eh"o{z>fl޼qqq3g ==GE߾}QzusP_&oFW_YKsY+;v SNŁ,Գ{<JLL˲2|}}QPP-Z08͛7zТE 4޽{wDFFVkz0 .\瑘Caو&,J# xbvQ2$$D[AwzzAM`{)o 1z<ժU3x%KpP~}|?~E\[HI , qyT*!`Zj垉4h HKKþ}_cذa8sv&>22QQQHHHY񐐐\n eRY/ڵkRDzz:N<3gbسgOoS%,~~~3gLJyB_Qt=!{-33U # ex끏{HMM-w[>ܠR ~T%GQQQ8y$zhL<ǎCϞ=0KP/}Qh׀x<FNNΝGgϞ>qsYFm۶a޼yxl*&\f x sUzrǜ9sp-bĉF}s=zb̾ |>}._l :u*owww-uT.;YZ5Fڵ!ʵFJ3mlb|>BPoy;w`۶m 3.OOOL̄; 8zJS]-["66RSL)/ H$Ν;qu$''k 32,ˢyRѣr8[\a D?ܢ@r \4K^V* _{a HNNƸqm6dXz=#>>0##6WAw@@ BSJѱcG<Dmڈ?BBBpItgWUܹsׯ ziy(!UѡC۷~A:c0p@D" >B>իuVXhâU4h=a&F}TG若)((Mhܸ1bbbc\v\,t=!~~~uZn@lذA/XTƘ1c=zH$2 {JXtA6L믿B$i YBP`ƍ>}:0{lT'Obܹ|Dz}?`РA裏xbDDD x" 0et|Gƺu~z 8dZeR/j莑,"##W\ANN&N6mXtيgs|'ػw/l'DW6mpQHRI>+Wbر#^uxzzԩSu:kǡCжm[cȐ!HHH@>}Ю];ܾ}999hݺ-ˤI iiit\.~4?Ϟ=CӦMqF8x >=z@dd$BCC!)) nnnXzuR9,Y]"33uԱA3Z-לݚrNһAٳgi&$%%Y&mۆݻk?hԮ][ڐ!C]v͛ǡCa/֨1jBttOyR-"/cҤIׯ<==+t~ rݺu3X#cDؼy3^}(**BΝѱcGKoр7o]v7nWwiҤ R)>}}VZڵ+FW N:H$*ݻ7unݺelӦ ]}!)) Ϟ=T*EڵѧOߠ*iڴ)fΜ'ON^z6Y82((녱cɓ'0v\ j [2Ɏ(7v.FK\FF^f͚YTqt~:^jLVyM4 ۷oǣGaT7|3ׯ_GXXv2j%&&˫• )o_wζ8s  Wꋮד( z4F&ٳhРAcOѢE lV=WTTpqG" ** :uºulݜR%''ǧ쭗 C%7 *.>3ddd`ڵvuzHOO/f͚9oO>qtب[2ꋮ'$OOOL6 +\R,=zSNdϜ9DGGWr˳KOpBܼy,ѣ>S̘1;vDQQQANN>CÆ usѢ~lZ/SƎΝ;C~~~0 F ݻwǼy0i$ :k|PI&q5,[ +W:gA7L2Çǚ5k Z*""gΜСC୷™3g~n֭hذ!~GGHIԏG}XEBb ڵkR))#FÇѪU+qԩ2@S*Xf ƌ[7.nRuhM9nBtњnӬk5jG@k !B!'AA7!B!b%NOwvv68GGv!++ j mVXXHpR)-ԑBafryϩں &7'R ;;ٶnK˅2h+)ӑnf8JLx{{ۺ9NZjnx@JAbSիWGnl݌Ju%9s u˸ IDATShӦ6l؀FӱcG[7ia=z4֭k'%h,qt= P.6!B!b%tB!B!VBA7!B!b%tB!B!VBA7!B!b%tB!B!V[9:DGS( ܼySuTe!fz>4ˁ^zU6RInݺJ}6,,%ĉn;p{nsN۷oOA7!v*550x===w{w߭fB*YRR_O>X,FƍU[1k@dfX@OO̓JŖ٦`_5 #X$C25{ZFLMxp5YAK(@qdt=pr2I[[jfX]Dav)3x\ !@ ^e gY<H&4(*0 JMҾxF6Rz@aA7w[74pp"( s\4{ p;Vq!hv  e1r-5jT}!@mې Ri3tjFrt= vB aÆp'Q*VB,հaCp\܄8H7\.QQQU"Bn;a0JDŽ8>ph͚5A!-,, {JM4Bl,tۑ @(0 ͐@LMT*4m-"T6paD儸PmG\.4ibaY2BHz0ZEl)& qAt= 4mT2 ZjA,۰UKp\4mTo2Vq>pww{MR^ PmgBBBn qL%'X'p`cuԁH$q!U4ta:,۰Eh9BCC&!A7a*H 1n; 44n!Bh,"22M"XAZl D9!. 퐦˲4CFLq84j!XӬQ$S(S͚5GÆ mBH9 64 !9DDDeBz@ٺĸdffB غ) D``AԮ]aaan !z@aXemX$<$ȑ"_DZE D2ܲDBE$r/KsQ/;ϽB잂UB"+\?!BB\\%G 3=A1<`غvVRm#JV\6.˲`,˂ڟ0p[ X BE!r5A, 'xUcx E D P)3n( }ax<-X pBL%ٹ\)^ܷ,Xp"^=)IAkhdkf%< \pz[ @LA8!U@"G *9 , S<,8 ^|O}ٓOۊb*y-Ae}0<Bo(y#E\"Ea8`Y0 TI9q= ;2\o )NnsUUڙ̂eE|/LA-́Ua8al.UUz22B*2Z(6^άMwVpl4A;W&xr > " ۖF)n=Y*% BSvfm4Aߋ*dgr k#օg*ˆLiFȈ3]E)vungI;6' B ‰P*+ I-A<_$ەnQr]P"ZFPW BȈ e9v]֌<&\'趗a}{ .^KBA8))ȶ #Y`-##TSN=<] gF/)A7U E% T~ [ȳ~օ+TH 222bLcqT )> E& xnJE[s +qnJ;s 4, Kga1_}RN\U"OodS?;.TDfZFFz,] -O4tSڙH"p(wjrB.^t@^&LJSɕD&C-##dKs^, )3,tSڙk0/t*9,Ŝ (=u22REOiӡ)fΈ.c S4KyȕItl ,G7Ζ@"ӼO\-##XTEltSUBb Mn.{A,K ۔&6CA6C[ik0)&%22FXhyVݔvF*ޓpz-) "< ­JCb4Ȧlb P&@c!ŅʕN3X?nҳ++o=F4'`,Xߨ'#wdOR: =~[hJak-w5 l7, xZqUq$Q Ja:>RE7<+ϯ!9kng Vߏx0t Ѱ+bm>;mF--NUl-*2p?!gϯ"9R Ґ/ϷqK]U!GHz~ェȕKҹۖ}h,xZ[Y yC!-J,i6?3o';O R7`Jϛ xhm%#Fv5VCZ$~6h7ذŖ۰t=û*sX$<<2κ'O!޲Uf榝ٺs=XB6!! 82 _p99lTR Vg@QBeR '- UJ+ ![ B/؞zaлCgO_`H71޳uSI)t~i̢,Zf%&CKdS_hvz $)-#X`׃}mv03>>dXd.KU/=doyJ"s-.iͷ\ۋ>aƨi!Sf, B>os9; HQRgn<7 u^{ ~#H7|;o{DL q+?G./j r#pQn P{.mg* DgX o7-#= у8*K.ö8itm6B a(NfLLuqP~]>oV#ydgf=^SE(+oGΠ s>@-Mm+<%_QΌq Q,q$ O'FW׭eC 1pswu;br|&:0ed]< QYc?Shh3iLx];!a!fG@~py\1J@o!GWm{+S0iBE!$fٲ8XVJiGļ￀ SѾ+= =ş.RĠwBӊ|R3A_IbRPq}Zռqt"~Z=gXtP*>r*G?oaMHJgv8~YIIl[ yNmZmWi%tlid*#n^x=\-[L*{ð(_|50f ?}3FOǞ ó^V1'7,\׃s"H ?k!7;z Lq3&N:{.}W_c[ t}o?] bƢO~C?BmF{p;_2)Y% !(vژ0{L FТ} OR!tFV}qEtz3Sf`!zfB\Mnw`;O.JGs?қjصrϣ۴i_A潱cvpL%ó ܝ_a5z yLԂ4r`: 75G6QjVFS30I=DLѽGj_F{#ܮK{\=Gv|>OEAkݼe&]?h2%R?:}JB̬Ixc{uo@Kz3[+ bm%W EY(RoA6ۀ2"Evy[uz԰^HY'zAvIt>i_cTh-["fh޾D:mj`w^xvǃ;(N}Qmviwc(/x~c{݄fIQ#>=[}꽄9+^vHm|4ۺ}3-fֈxD wWy62\ B5a5HҬ,YE4Ď q ~?Gv}[7mN; 0cI2siP*AT7Wz2鋋GFթ8O:"ohC4jn;`틸Ϛ"x<= D{@"+ޢOwMX8W?YK>kzȮ_pe\:szknD3az,~B__ 8 C' A ᵎ/ɑ敛xm@O<>Ծk{l)I%Nӽ]a A%pڭ[ EXiƻ29|U ~nIynqF Cm5z^׎bn\7[7ʹq"1D1aΝwjoPRTd9)8*bcXp( 6O]}\L*J`]&[IX/;㗝gG-H_OKJ3r% ׭s|ͺvK+,>Ji$͸x7V~v8 cf`r , 2O"[C_?**esCOK3'3\8y'v[%R=,_??vC1Q 1LB/ȶ[$ Q_;ynVm(OgY,loBcEj x=D ^#yMhӹ-c_U+)8 Lor9yl_%w/`CkcΊFVs(tjluvaZʒA9أ{"T"z__>b4hӇN!/pFٔ#;C!W`oP3@|&Wy ^| "àv+/] \3L˕I8u$8\AF S sdgdܱ{h>^|3UBaVKQXPW&z똹gƂlGRjO\!Bᦋ,fuGMxChO&#ȝ箝3ઽt"ݺgr2Qd#x,`~׭?~WS!WZAH)hRjOK<-a ?*dd^ ^R,w, %DVoQwZX>V~~jѧGiOpU2(_Mʗ&PosRYmg^WT-Nb_zs{׃.퐓ߎ{=Yp>vSrv*r9P;>,, IDAT7ZG!zBN:j IwYFWhUһ+Nq?F^Q#d2}{~EA^>_1עh>Ywx_d<@x^ '!mܡ5w5۠+Ю[4dERۋ3жkrFLAoa2xʩJ;.ݔ0.0`Y%1 O`]0<<qeh{|)phA4ikT3l;yma>{ }cGQfoeP7,b1bSͯ6-߈l [oa hH[<%l~")922|6jk0])\YΔwq='@VF^n7N: 1hѾ%J@VmGiЉpda̛4L}aM3lYY8r;X3I [v؊f<1Ob?G5 }jWq cNeaL8`Ft EhCi{ad#,^ڢ.G",| 2#vf/\ryQU(̶J[-˱iq6Qxl ~޴N^#O/􅟠Ϳq|a|/?P֝biro9RԨY}GSjT{L_|Xj0{,fɗ% F]GSY\ݓUWehvq['v9Ez+*Fzy-p?>ض:ٙZ'Ccq'1 %6,]Lfᘻr6A$\ .]kO߬ _nqkwR1<\V`(/DÈ0|QvTEO}WamK7זrĪQׅѭ axԅ"x<)vPp@Oɕ$eȁjD뢢V<.Iɪ'σxQFy9mεm1 A6^|A6aȈubz().O!ez=-fAwI*VNȃvEvz,۔* ‹/rI22RL"\1,O!0xls]Rڭh0_vFJr潃V| ^/ IhcWQz(L<1>֭BEOxAwI%p<_tn+vF,{;3ckw -#O`ғ+bm<=z,4[i֒$'4f=vF*ua4[ [sM{kedA򔪧_X% %2 J ;[NvF&%T =edAg)-*KyrR:)vF gÂ5^ !Whb9-#3PM-(=8,m()e``^] E"ZMU)8ua*͍7 V|A22CJ\).,z-z,vnc ϢerpNFigU uYh{*\qbi΋BN>TQmc {Nig\/*GA6!jθC )jDz=z,0s祴3B  s,ms~Qz(!StTšP]eCmNigX^RMH%edPB ܆SLc ہQm zA42 3B*- y De ^FF顄اҗXg7UESSAAwU_|A8RwV|fFV 7">TPPPTT$> h4Bz AaX4M Xgp(((PT666=P(T=(ܟvƍjDK;99988ܼyuɤj{e+@PǧW6T*Q!G4mRbO(jիW{kлp0zx+𠰱!D(x<xu᰷wsst*xt J}}h왵\n[d2ـ܂R,,,x=B///"ɔk2z`EXV]&%I}PiRXX3 B(ʕ+z>=xUWWrHғ ///VYD8c*ꎎLAWQSSSS[[ۻ ---&)00靀JvD"Ŗ(ʚ!xxx,ۻCXX@ ]ΎhͳjŁӦM n?ѣ3gd> 0}-[8q"??h4⠠ٳg{yyuk׮m۶ׯ_ojj"d{lƌ4773WZ5.nW_ݰa!$66СC]Ύc:J2==/;vʕ+]]]?'Y6՝>}zʕK,yleRon߾d2YWVVVVV9r䣏>OXzÆ ]D8j>YԲ),,ܴiӄ X,Vjjjbbb^^^7Zl;g={V.WVV;wO> 0 _}K/d9JP=z֭&iȐ!k׮ͭgΜ|}}U*ҥK_}Ζvqqxb[ݽ{wkkS7D8E9997nÆ ;vJO>dcc-/ׯ'L81++^HOO4i!߶my?_XXbV\k׮)Sq8>ϟ>}z鄐 6lڴՓ!7nl{aÆ"C_*O>!DDDO|>}P766iСCǎ#O?t.!!)aeܸq]նZVV)Jccc_i5kVXXOBB’%KΜ9]}Sׯ7 ]ܵkswxu YYY5k/Bd2YJJJs8Ro޾C ݻ_ݼy3Mӏ?UҥK{Çsܸ-[?obWpCOBڲv BD"1bDr\gpiBȸq: 9s搎Uƭ[BΝ>Ns͛7B֭[w={,_7-D8^̇.B"##)䕕J2`g]zësa٧O.++lOKK 0E˗ !?妤;9FZ[[{!e:nر{;v`صk/oWFF!<qIҊk:tժUݾ`08zo'j_}g}t&+W|g=J'85۷o'xyy11$%%m޼Yܹ1tŋ'OfE >|Ϟ=hiiD] ʒ儐ѣGw-99JKKÇbi>wܪU:bŊ4cB/s82ty`0\`_*ŵ챷bgghOO[nŋJbI$8Ph?~?[{ Ժuc ,㝝kkkWX1f&iU{FqgϞݰa_Ey٣T*WXgu6d裏!2lԩ]711ĉz*!dΜ9]BQTBB͛ wލCN|gRRR:v?<000@[[[W[la˖- xǻ˝u!dƍ٣G~cջw /9w믹]J)))/EQ{S=sO?eʕ+މ'ڟs?iggGp8?!C֬Y;eʔ*=x.!$>>~…]wV;wnUUՓO>eLfGV/\ٳE1sݼy?+\/[,33_wbi޼y'N8|p^^EQs_~y̙z+66ֲb999(Xq oDFF;LKJJ\xQ,KV]]]l+VX$"<<2yLsssyy{ヲ˗Tz^Pfee/b*3fh4bxʔ)yyyk׮t޽{y<!qʕ[lِ!Cڿ,C!;;{oo~cwƎꫯZ… |ǎb*++9?\~|cǚ~.\׿u  jW]]VQQ?GDD04Mݻ7++ҝ W𐈌IIIٵk塁Ŕuqq1'L._xQ$kw~[Ϛ5+**^3?3f;w[dT*4ƍ]vmԩ3_}5MDDΝ;bŊqɒ%L rԩ>Iqlر$NZֲTW^-9ջۿW_}?㏭:888|'V'lll~b ,d2H$of„ ϟ9sOINN[o3|pooo777p &_rrrwb*g̘q rJ^^!nU.orŴ4.]aʌj}QwwZ"''ܹsFJ ?3!_a>%%eܹz>77711 ,HHH8qbCC… :\f/]˖-;yJRՖSnoxxx.!ї?Sekk3hРӧԩSmvȑBCxxѣg͚ekk~[bų>uSN477s\Lֿ#GΜ9޾ۘ?/Jod{]vCY,I̙uUsx,[O?}ƌcy۷uVsiii"&֭# 8088Rss믿ZlgXO<_|!n9yKKs=fn|Att4ssmsl6gzG9r$!`2Pu?l8|p~7޸A?~<~]#FzD"Yp-Kw`N;Mbׯ%Kھ+RRR7xcÆ ;?ϔ?55u+W$@ɴo>BȤI.';;;/Y$<<\_|?qڵtr7x<ŋ{1Zs7|c:pܿ㾘_~dyXeLS,777Æ cʉwӄ ֬Y{nD8z7L&Y,־}s31 KlmmY@n( BȠA.}/ҰaFkK.]OŜ//_>x#GY^vsCRRRyꩧɜ|>yDgϞeZ\e!ΝO Cwf{GÇ^3#..駟|͡Cvm/b*찰0vѸ~zB믿nUd#99ۺu /'dٳg[/]t˖-eeewW25KMMMeeeǎ#<+V%<== !uuu%K///lllT|7 ݇Z|n޼믿ZEE!Ã?{ VB!!$//Ov7UT\\q8qƭY_TTװbzӧX]Z6.]tܹ۷oOMM=JꫯlllfϞl2WWWܸ\^@ lmmYfmڴiƌC %I"X߸q$b`hhhpwwCMM !VGFXTT3h0=zrs#!7nHMMa(..>zHmm~HD+!Jjժy4_/-Zrʕ (csss/_N`=f999{:~}SN^:ؘiy^KMM-,,:uŋM&S~~?xuOO%K;+ʙ3gB/_4=:,,,??…)))T*ۗ,Y_[mfݺu?ϗYYoiRYXXx!Rf8LU___^^~adHttڵk- /X;w?SSSGD>w\FFdrrrqiBHXXT"D8:p;w^p7)5a„7ݻw…lllo__|_|S5bĈիW<޴iܹs >#Q"l6?77wRdd|xSd +VX,O?4lذt׮]VmllM[oZ]ڷo!<oI:b֬YsN߲ 6 LHH?qD??.=zX,ϼyFȨb..."fG1p@v􌌌\7rH@uVB˵,>bXtO?][[kHQP(tssn=x "|ڵ+WTUUT*.+Jccc;,2r.;{)M( *J233ͭpKl6{РAEiiiiiiXX@ aǎ={ի4MSe|AAAYYY|ܔ)SN<9UV^ &??߲x1ݶm˗Տ<򈇇ǎ;,ׯ_O;v=\p'OΒkAA;L^zIޫi]]]Z%K/_z.11QP8;;KjBCCkF-z{;;D8%KL47޸ϳgϦiz͚5sΝ9s?ڸ={899õsa…555/2eJooPiO(~tdZ>zUhcbbOǏ9r… >2l/d"A!d2z6±t:!B-+ IDAT;A,\>h //}}ݯjO8axR]]n:ϲr|>.^XVV6i$PXVVv=#dǏ'?2!!A E"Gl>գr^E/o@P(?ѣQ(r`00w{11[( )!):;ah4ή*ʴ4J4pn]wgnn.[p/R܁ׯ?*¡ZdbXL 3d0l N)p8<|iNǔʠ(Ie Ml6[3ù\.ǣii1LI 9r)bbL&Ncf(͛7b}5¡5eRcFiӦ'N0VZJ"ٳO<溺:愇F_xqpp@ `1)\[=D8*++_OquuMNN;J҉'Recc\pMӳgf...cǎeg0}Z=== !H$ɑXҢVB+WZ[[ !=ZxaaO?mBz}mm#3 C7jkk-|>ŜZREEN0`SR[[[[[X,6"BG;;`&A(J,4 o0$ EQ rMNgYPT,2хf^/_jkk3b &(}B4!U(v<{yyy 00m}$¡X,H$fg_jFYX,Becr^F" -OUp򌌌K.Y/!c:99ހP(*++CBB}$UVVj9`YYY*2^"4jU*ǓH$^^^AAA,h4^tI >֯_k׮GDDPe0ϝ;՝᭭*յ{nڴڵkZ}ӧOM>ArwbLpD]vmٲ%"">88bAUUUYf̘1J2=='|R"TVVnܸqĈ...EEE.] ab\.ruc-f9OVV@ ppppB]]ÑH$;Se5[~~> aoo#Gt3fԘL!..i Fuuu tÆ  (eSLNq^aڵ+&&ϯgLdddp8c*rh4zxxt?=C#,cf-KIIIIII'p8.ݯ_?3EQVc-$DliiaXn`0LV]]]]]uPL2)?I3RPP Jcbb:zر"q=@z4aoo:}EI$n.{ѣG3K7׮]cZF)--(֖b2*+++++\.fZVPH$MUVV&}}}; 7oVWW{xx09h.++kjj~@ 21M}V.RY\\V~SYT23dqZ}Uh9錌^{ еp<ٳNr0𢣣|wx}h4t:sˉ'<<<*++|>suԩ*&IUTT7.66gff:;;X9.^XSS3dȐG8F޽{틊=:x`ˍݸq#==E( \4m4Lf4^Z__5^/J]\\\n[[ZNMMouVÉs wD/Twvv1cƌ3z~iI$A0/ccc0M K||뽼# MMM}޽sСCe2YWh4E999KyWYYyqVh6LEEExJڻwRd,h4f檣q._6p@XlK\~=447RJ%M]D/:ō"Л}z\NӴmUt<E{7o J*JV755.^z5>>U)V*E7.??wȑLr-V+?~L&"hΝN7 ׯefff&h񈖖 .l6id/^bɓ'>\*CP޽rݘ,%Ixx~ EҥK>>>")u8%&&UWWlmm%1!Fi& Ukk+ǓJ!`0t]Ӳ7S =ztBBɓ'U*!$88N"8_hСCLmn:C e'33S 899A▖GFcǎGXV RVVvasysB@ `"''ׯ&GkkkYY^7WT&Nxȑj۷o7 VV\U݄*lv||uLr)cƌٴiӎ;kjj/[\\,FP(|x֬YUb!Åu\\Ų]nf fUmի111nnnV{e2P(E"7nP(dFT h4omĈ 3ڎ9A(4=fP(7mmm666dƍkhh0V @0aB!ACCRPX;ju:];6- 0tS|www> 3sa0222F`3--ƍ9lĉ-Z3,폃Xvp8V/RiUD"H$b>SeyoۼK!vvvvvvg>lD8t:׮]{ɶ6 .;w.\NcX}򪪪8qKL񌐐BOr; ư>kvvv>> htz_yFVV[[[ !ӡbb>o\1:NT @  \.Ų\Wӵj[[[H$hmmefxV0ɻi 7QKKP(ttt4ߎNh4|>ht:X&8d2i4fV7M&4٪V yTw fc|>_$ښiZF$?+9ddd#}GbbJX,#Sߗd@#^Vsͭ[vс7xcܸqL`?xnj3k,Bhپ}{II]cc#}ꩧFe niiٳgOjj*7 Rt66v޼yLTXXur>T*Nb~qo0㏖ZIƎ+^^^ޟYZZ*<=='Of2Z[[;?Ϟ=9믿;vTBy7_qFB /DFF^re˖-eee&<99999Y&);vh4j4wwɓ'ZaMYYY۷oJ%%%.../b\\'"7oޜmoo<}SZf]dyxxXjx<{o/pK}$qO4]PPpԩ$bG~~NQT`` B}}}ee  fzcȑ# !ZvΝ7n}}} !bd&v=<39θqv`2X,->LP۵ZmEEŨQlll4!h4*3B/pXi\[ ?455u=Y/((6뛟_RR´4-,EI$HEY'@r$MMM3trfKJJhvss[ddSpุ+koth48p 44)c"ի**44L&{{{;;;6t`XL=ŲbooV^p! @$ׯ_]={ ??>**Yx.))e"gϞuttdj… BFҥK*ɍX]]mkkk'f0H$r\$YH$<O1ssszMӭ4Ms8>o9t-''fu'Ohܷo_DDDHH¤z,** pwwi.J]tI׋DoD?~|ɓF*3t>Ewl6\K4ؘigg7dB` viZT2?655mݺ5///88ٳsm_3''2>> XM`ggP(T*D"immeXVu&!Bh4jZ@PYY) _~Ѣ"^0mڴ[&2L γыO}YRR\.|#""3ahnjjڹsСC}||wj(R2->oy ᆋ IDATDЧ1~$s%Ctg0qojIccۼy̅{챜Pgg.3gܬQ*7n9repݻwļׯ=~n`0 &LH$W4\֭}뭷,i:##cܸqV3&&&_~ed=17oFQRX,UT*wXO6AV 0DePQAQ+ujګZmն:Z ^aC @Bv\}q~9?<$OﻳC klllT*t:]HbV9?? p8ܬY[ZZtiP(Μ93k[lquu]`RT!@ @=<Z[[ρA-\.Suww?}f}}}|DrRW a-t"V[W&חtuuKW.` ɓ'W(++J=]]]EEEX ---MMMT*}EIII{{PFKK hllp8f\.J(k6"x<ޠ#ill,..4Ʀ+T*kkk L;( D"FS;NKR),X0hoLܬP(0x`ڴi6҂~}}}"bƏ=WEEE]]]...FKOOpss;^ #'Ovtt$3335jBxl6ӧD"BdffmJ2==++F +=zԤ'H$R||<@'O$ ;(((::>|x͚5۲/_vuu |+xMTVVD#GTeOӳΎL&+ΆsssZ.WWWd 9 ,)AX, BPX,Vcccuu3d>RNYXX555uvvO7Dghh mJJJ(RDj]G-͍455X,XYD*ؐogggmm5g|FFFW[[L=@ @ Rttt?i^^^ L&ļ6xzz۷omx<ԔJ?byxxp'OyxxXZZH$777CCCWWW[[[4TTTJӧd1c FWWWss3ñ%555Arrr[[ȑ#tzccיL&Ʉq*d2bYXXHҢ/~pqUUUvv'411144d0$Օ`xxxpx%[ZZzzz Clȑ#a ==!&&ܼ $^&̚5 kJ&%%%ݻm, A ݘr2V+N3 {vv6B ׮]Jl̘1ׯ_?tĉ---kjjd3Bgg'BRj0d2:AxxxbbիWQEEEffرc1ċ/N> //QQQ8444##k׮H >0\.FTcy Uĺ?'&&{{{ %$$ 2fffj3bjjlkk+H^R H$矞%@ @ oLfll,rP(񕕕4ŅBp&M255f</33f;::X,3ֈǵk:::,&&&::SK fX^~Ŋ0`߯  /ZZZ¨cRT,\xj-SSӠ ؾE"F333sss322|q]]]2,<<j$BJjf;::z{{cxR)͞={ݪk={fbbd2 /Q8`Lvpp>F/8uYfO0$999))JDFFZYYaơ666)));vQQQ<OT/aoo?sL3K޹sg<&..OV|p֭m۶ Byyzz`ppQjjիWαtRWW#Fennnjj 5T* 322RRR|9bt:}t&0cQFGGGCF)V:nllVcnnn|+ (A}r pPyc(>}:~x#ai=<<a}@rr̙3ߔxKV0A}}0a5jԨQXk|hY0YeP.]tR-6Z f̘v$&&f(c51XOOoƌCJD"4@ @q~̙3g?7ԫ cرcGZZg}477ϟ?ʕ+GGGkwvv}:lU@@P^yGH$wWRRjaaP($ +p111Ϟ=u>|=a„Ahiboo?{슊˗/DEEa:ǴiӾe˖l-V(,k@*Z#ooWJBI_CCC/ L׈@+ Ŏ;wر]MFF… ,TVVhaٺ7f#?cyy_`޼y}ٻȿ۷'''@ zzzPrP(cǎzzz/C*H$P]]ݝ 0d166Ce *#F?~jO(BтƌSSSprss Bxxf@ɠ8:::::WUU555߿ӦM4 `gggddT^^y'2D" BH -x}}7 XQt_ɝ;w}}}N:m6Dאd6l駟F1f̘'Of-Ң'4:d2]tվH LMMuGddd@ +4J/U1T*B=Z[[zzz8J- +xpqq[)<p8'NTS\^===OOOLv֭r,XZZZZZ޺uz7cƌ7nXYY{{{"o!4 ѣ+W;viii(lP߿?FDD={vݺupPԷN*Uɓ'obPu@ W8rȰw]]]zU.߸q^^^0uqss>OX[[k"מ*44~p8 j<iFfgg@9D.sfffRj<2,55M VC(x}ۋ֬Y}vrrr}}Rd2ӦM>}ZjMMͩS:;; رc.]Y 55599ᘚ3fŊq O8cgg7'j+<q͛7kMHHhkk۴i^ .oVXfZi~qX,?~<ҥKLǕKS@pԩ܎fgg7{PFryRRRzzzKK J^hL&;~۷|… ؾ,?{L"\Er%5UVVv᪪*CC)S,]WWW:tŋ$iĉ{%%%(J''xqe_ZZ zjii͙3gKg͛7/]bllxbN^zuϞ=K,YdɥK_fbb2hS>}D"+V888tvv8qѣG<`.ZHSNUW_}ѣO>$>>6lؐgsΊK={Ç'$i'Omr@ /X,NKKH$̬ *-,,j+W3Hf/BhHJKKkkk~ۏ?>%%ܼnА楥T*uSNyxxtvv677dS?"P9gΜދ/R('''SSԸ8!//=zt^^a]]3fLiiӧObqqq곽ٳgۧc'wR8piXgll==СC&Mz*w+W*Jfdd֖O?effbXShz'O533H$ǎ;vҥKq#ohhxB]]ݸq!77TQ4.y<C)q #B{޽+V8t|"舎.(( H,ڵk;v8p|]+**ݻx<ٳg߆}/,,lnn?✝mmm9DaS cǎ/^|i||njC&ݻCLZd>رc%n޼o߾˗:t հ R)WNP꫟X" .$&&I.hѢԐw .ܽ{ĉZ} 6( &H^/9rd… ##c}}} ں ))i˖-gΜĹa'j @ ؿP(oꜜO~]]]D"1!!b HTˆ@ xH$H$BBB> @.766b-qx<>88@ tvv+Ǐ=/XL&[[[۳X,[[[Xjر\ccc9H$J@PP= p8\dd$x555  '>>> Q,,,ƍz{{)ʜ9s,,,//yuttyyyd2K#FP.ՍP(MMM h4___.:::g͚ HR L---uuut:k*J{{ oA--3dzxK@/%KӦMH$U|4ILL 7ox葁]߿jjժ˗/?~ի/noo_jvYYY}}}uuu"h 6\vۻΝ;| _tttL>]5ݻ ;::x<ާ~ 2V:L^%::S IDATs֭ÃeX`ɩ`]Ho{t\94qƺKvuu555qbwwf޼y111彽Ν#}h޽weXMMMYYY}]OOYOχW\y… |>VLE% ZjUVV۷333-;w6ERg[nar.\:Jݾ};HljjR&kƍ7ܺu@ 1@ Y&<<5 }еn:èQN>@tvv655ռprr233/`6jFFFªc]\\LLLT;c``,,,Tegllb4| p"::ܼXZ5XѣGO9r>~;e evttD8f̘ݻwoܸQĄ`(N{lp8\ff& cJ0Ё:VogϞ)ʑ#G1B{MuMDѱ)8;gϞU۰a? 55=w\@ZZVVV"##Umt̙3g ..K#**JOI4vww?ݻ쬦0̦gϞ5aPpttwt$Cڐ!`OLL۷777 (Hh)l̓C]=!!p@(޼y@ ̙30M@ seS__٢/vtt`Ϻץ@  Tc`.u>w> t:@ tG$;w!H߽{njjւ=~vgϞ4+**tqq`wᬬ@@a\(//`1ԎU\LVo3f[nЗ0Oh}}=@Ui[۷'''Ϝ9s!!!X5ŋU ===mmm/A`Ĉ4 ,юZW8M]P*j)l >G==={s_x1jԨa5ѱf.]!URPazZQp WUU>s믯_.J޼yS DFFZZZV&B @tttC nmH8)L&`AjVAǍB 3HƤ,ˇ*MMMV@:3Б˗/x<vŋ/bgϞݹscՈDR bVZoРqܸq444=%ʇM5-0A 9YN{ԑ7ecSYYYׯ-++۶mFߴi̝͒`!MzzzL ǠA,QAkﱴtҥj"Îkc'!sƵ,~R gYsEia;`oo|)SRTѾ0cЅ@ {tss|,W@ 6yď #""bP!@ 0tvvZXXlٲ&Pܹs?@/9NRWJL&S*Af ˡP1" @.C"8h P u;"jV*qR9T4 vuB bT`j=ut&8"8zա ToG22006`z>?hrSRRfϞQہYJ\:OܯL8199Yy99:uOGm?X,~Sx{SK7v=zk׮a [W^=T);;;Fi72,qn:}^'oopG"$\p+WL2E$RԸ8..@ xKc?9@ H=j uJ׮]322O^U*[ZZB\.Duuu666 Ck/HkjjB!ɴRnB!t-FS݂*|~ss3!`߄BaSSSCCFcX&&&j4` {{{W(E"-x#ܜJR Q]]j\x- bK,K$>`bb}L&kjjb1d2X1R)Z[[MMM_A"\&t===i0[=633vvvv% 622R(Bb$7mڴuVS7PWWM&_m<%%e׮]ǏWU8`gN[[ZVAT&-1oٲEwUkiѡv#)WQm-_|}}}ϿqƖ-[;;k rNe%ɰ?tMu򥀫S,U1ImU%>>~ڵ׮];p@zzz9s{:@ @/獌VX[Q*\.7==ɓcǎϡܹsl6pYkk%K96(HnܸammM/\fΝ B.^XWW>cƌS ܸq#==JСCfr;vf477EDD` RY^^(333E"QBBرcUr]]]zzz0B dgg'&&1bnnnMMM٪Le2Ynn+bccΜ9sʕm۶EFF<L&嗞2ʕ+iiiVVVt:nٲe,륂9;&<<<>rȕ+WpG8::ZYY|/UT*;v,''͍H$۷?2ӧO߽{wԨQD"رcǏbOUUݻwU!Trq__}H$www.{Сӧ/^XwŢLs7mp1R9}tzE 77{`$z===U Jf Ղx|ӧO௤Fj )Cˢ"@YaÆO?tѢEC?XNX)7u7e+GǦZZZD"j=CCoƍAAAǏR_ZZټeAAAR.}cVѱygg'Nz?gϞ{EDDL]?~|fffQQ˗^e!@ p4&mu떓Sll,jww˗ԩSUD"1!!L}ݺudB!++K.;v,OIIY`Lf-/_L"~gͽ~ɓr5((PTTtҥBP(>ቶPQ(|tD" _r%&_]vŋvvvfdeeر&*d/̌vƍϟܹQJׯOKK Qm055@ l"H$7mdii Rߟ1c5BP8q0Ĉ&M7|DǏbbbT=tʕ+1ԩS|>Ĉ~~~Cf;ѣG;wܴiv|Νb8((Vutt,,,씕mݺԔ7OQQ?m6hx#GP eee+}:r3g,\lhh8r䈖Hcۛ2KSl6#??_5-TJy֭{իW$B~'W޷oB0a۷sQh HʧD_oٲeϞ='Oa P\\\]]d2\ElYp᯿w߅KTO?Baɒ%/^OӭrkDDD;wn``@Kĸ PT*O:j*3K"/_qp B… 6l*D *((t^ b``3fÇ_x_XX|ruu5]MUMdؕ3lSnnngNJJ=z N>M vbllo߾e˖͝;wٲeYYYW\144߃߹sرc]]]'O?s挱keek[D_7n޽{cbbD'O%ڵka]a pȑ~mOZ`Frrƍq8ܷ~::#qƫW~~~s̑d.]*--囹SΝ;… k֬=p@GGǢE^A95k֪Uݽ|rկ3WP8;;}Ǒl6;666;;{ҥnnn/O@ @CZ D"֬R WVV* WW;wTWWC_Br(J %iX,׬@*%%% ^^^R6 SoCxSWWW[[܎z&C&ro<;;ƆfI(fffN2ۢk`` ۰kVx<2<3H B)o(ENëIR6ٹBlmmD" }YYoGPU˘+)J9sX,<ݽo߾5kܾ};77&MLL222Rƍ_ӣo&7oZU +++aj͍7>,z ((ԩSjwW_2e˖˗koNNg}vɓ'Op8܄ vڅgϞ?~ڵssqʕnnn׮]KOOO_~u؇o޼UKWB wԌ7N5LʴرJ_ٳ o޼ѣ7?TԢp) rti̙3'NﱖnjO?a1K,166o>|awttFd.I&1L]*##d:::b] _BBB(Jgg͛7L%x5BaNN{ァfC.2L՚+0yiȑD"-%%eժUbD"122R7%U'ݹsTQ"(//㹸 ppov"(++kӦMp!k׮]~\.۲eݻMLLRG֯_m6ݻ`͚5666;v8N7@zgX c_.X{Iݛ<Te"o޼MM ߾ ٟaWP93fhjj444 E3b@ j\z… BP5a)@7'N8nJR@\.p8ܜ9s\c;;;.iQC.766޹s vW*hRֲ'ɋ b1h[nmnn600QZw.~pTz3fj?}ݻ _x1{찰0kk" Y,f;2?\fO;...}ُ?d2Dbiiʕ+}}}ᷚ9˅.QLŰKju5Mݻwoݺ=s;v`8w\DD ן>}|:th>>>x<LJ~G///8q߁^&/lMu1S2ǩ⠙oM KKKz,,,,,, ZflÖ  ,WWסࡠhjCZV&vLǕKS$It? @P+2l͠JPԢ"KtЦn*DKK{{;,<*tedA$DQ>{{aK^iYuZf vC @Ngg;w>}:TvEEšC|Phnn:0ASSwbL&¶Ό 77Lҋ@ @ ;CTxsb;FCu3J D"3L*ZUU NL&{ũSX,Vtt4ϊܓ'O.Zhȑ/\&999 ڪ{wkgggbbT*%IJJJfffll EFFMݽFm<Ç.\ڦjJNNڵkzpwwg:88L|-˫52KKKcccx  /^8bĈe˖400pQB!FP^laffbŊSNB녅x<i~|/Ar9FfCP0VMHwvvV(|>_ 1Ah200pT.'ON:uʔ)P& rttܸqKHH"Bxak0*<boձj:thݺuPy>QFp8*uϟ?{'H .T|LMM8qf'U155ݼy_Ud2CCáP{{{i4E ta@ CP־x]orΜ9$IP}0UT*ׯt;wBBB^':p T*xZl .g:::5-b1uvvt:JLTprKJKKΝ**T;ގȬ/HBPڈD"<̬Sqמ*ŋFPT*UmjkkX,jLqulP^^syM0A3PQ"l߾`|'tV۳Xz&ÀyqԾ=z1lHՌt`0aii=艈իW^zX37۞2W@ @D7r˹\1655nHdff׳Jz*++;;;=<$$&b122"CyÞ>>===O}˗=ӓ;9?;s}N &lA uGpjmk;Ziն EوMs~=7%'o$y711133-<<ٳ&M"L>oݿ||sQQQee 1d2JZtuu999 V ===2*b-_/NNNx999ӦMq rBd...AAA,S X͙h]1<.3|g>>> ŋ?D"6t:͛71 _Lniiij< ƍQoܸqԩ?x|?3<+sҤI?~|}}}ffD" ={ŋw5}o۶ oeim;O2Lj:$$wruu |1cFW NjkIT:::ږᓡTvKnD8\jjXZWWWSSc9`0HR Ga2IIID/NlUxx@ =z([xqhhVujjWNNO[:O>dIIIvvvOOߺu$$ H AN4 O75i$ooopѣ=<"""{˗/kZDe__[z*leXӧOǓ-mAǏ;wd2y{{?J+U*V(6m-ϟoa|MaaaII޽{Y,Vjj3,WE&wܹgŊAAARH P(?8;;O81>>.Ù7o՛c=fYpi4#hb"喪bT*8 xFqrr0̲7~. ZN۞bYJAA,+p ĿE"ь3F| ŊjL >o޼y 3ϲ-*=Db=biܸqVGrgϞ={A&iwb(H$ZxP#2eʔaED RL&fpvv~GW^\2l `F +WF-]ܹs2s̙3gZtpp6m XIKKj%JΟ?ocǎ?W8pR ;99}&LGYfZ(g2#Z*eĺ,ׯ_l;3qldgg\pf3Z(&B& VVywwO?TZZj0:ݍVKJJbbb,:uj~~~YYQ&88xF MMMd22pgAAaX}}ٳgbRqرcE" b~W!!!jEQwww3BLS]]oxx82lԩ'OuuumkkS($ig]p!&&JTpWApg 'gc0111bXzOOϠ \rRͻ~:BH$l6; ((AX㏻455T*^<2TĉJ^  V@tR|=Yf πNjii7rtt 7=D"&(ɩ(ĂBxyyyyyƁe< txp>O"0 NjP(z^:::h4;`0 l6;)) I,B! m 12EQ% Y"Hd2~O뫬?'s(b<==`'PSQQP(Tr<006F999 a9 jt07,Op-Zfo,Acfl6>JRRaf4KKKI$R@@=(9heeeMM xwtF[dǕ+WixDd27))/zoPa#>vJ gy&11p*//j^^^&L񩪪~z\\U&pW7NvC(:;;]jH4gGo</44t2a0l6fE) Fd6M&a Jڦ C"EQɄ=Ro L&L"TAb,CFL&s5d2Dg}oXZZot^?jԨ@ RCBBbʗ.]jii:;vP(DDuuuFFFx[s J^뛐G P,//okkcǎr#77WKRch4%%%xA[455]ti̙=&X32"/_O?X[}ix2L//{|jPV!ʖ-[,6Ƶvww{yy Zŋjl6[F0 o?so|W"(//ٳ< VT;{[^^޵kD"H$"6 >r??={tttJrϞ='Np8:_~dx# ***ve2}ҥ =*yĉBpժU ///;UWWWRRO2 ұcZj׮]8q">IVVVKK/ ٳ|ͬ,WWWWWW&L 2!f͚aÆlHYxW?SRRbbb~\>h>|x``ߖH$̙3gէNZx1ػw/{&Mھ}/|r.k9ac0q]v񌴴e˖SV Ena6|vO!// J4(++KLL/k{SƎoca@D ,ZAX>D6pd2988K(wttx{{ AtzJJʙ3g˓xĦ\.wrr#B~~~g``zzKLL KK'GGGqϷz|+//oܸqqqq˗/G$77pXKMM&rA~i ~ILbbb܈&LjA$''WTThZ>f$ \||nQMMMjj*#̘1ʕ+:fcvС7x\{Ҭ"'O׿u 㶙F)))CYwwNj65Moo/#Oxzajft:)^7 l6ݗ8{At:^߰b8Zkc0 kooxEMM L# 'rd2YRL&*aJ"ɶ;ԄNG4.--V;yyyMMM_Vv<QQQxk2l Î?dOnRyʕYfIRH$jkkSlx!JMMP󍍍t:VSD VKL6X  FDB7|ɲeˬ8(j0*++,Y2R1 /]]]|s=g6-gvڎ;|><[dɄ TO^ǷȬMnW+/"a7o...2L;h47) *,,QQQ$)""bг|*J^t,*"ꊢ(100`0lk[[>d2hvWWW;{}wwwWVVN2#?BBB,۹̜2e ˭*:uT| [`f[˜bL&VW15ko `|Lj%cfffRRRtt4 (ؖFVEDVS(6o#䔟a'''Lv1&dɒϝ;nϵ;p̟?2_{;v]d2gy&..eߑ>(...**'N+V-#Do}^q`B|Eo޼J.ju[[twwwFFƛo9Ldz?,..?a o&l~wo_bbSO=oG\\\Tj-](UTT|7yyyZvFuttlذ!&&(++=zmos}c6[ZZg6}Q6|:j  h4DקR:;;]\\~eǏ;vڴix"@"l+1LٌiZ2l;`L&ш HWWWee-[d2ܹs\.w.fxܸq]4q///WWW˜bZT*F/^ضVݷo˗׭[pz}SSJs&ZZZ9>O<₢Jo_j*a2> aAM4I*fgg_zyԩCJKKϟoyl#1˗7on2… fGFc~~|0yd8ΨQ\]]zVG3f `D?L&Syy,X`Y϶NP(xc𮮮l6oܸ`0dee}w3f .))7o^||<;``f[PRT>>3fH۷߸qcGee%sssa/^  `vĉ'N̚5kر,eΝK.35`4ʾɓ'?#Vy)?EM6I$[_~pիh4Z]]|pƍ˗?*jӦM2LPyxxWҥK!>>~[x<^BBBBB7nloo_jR4d2 LڊWh˫& `xyy^:&&fzwCss3JŃt7n>>c="d2DZ| \T*+R>#O\V;88X^d+裏F8F#e /=f:hljj~LeeK@@P(CKٹs||| S@ȔJeIIIww0c0 kiiLxӧO7nڴip:# 8h{N>ou#$$C. {e˖ K,뛚( W7;MMMT* +;::,g6 &D" 9^SSoo7n8::ZUչY^CGGǠ BV|=#{g}6**66>00uքٳg8`@`yY\\.ojj'"L&oeaɓ'M_R;Xvݺu"4t:^̊KeeH$vm}N0 +((P2*m ȉ'fΜy/sJȲF&J׮];0Nw%D2hxA캺Db@ee%ÿL jEBW^UǏTTDDĕ+W#""=w\ZZ* aaaݭpƍWWWA N#61 >ta6U*y/hnn&nx>쳀GƌGmxVm^l{_XcKٖ &hd~qBa&)??aƌTaa!P(b8((h˗/oTmmm=tNaaaJrϞ=/ P(":;;K$aU%777,,-rh4q|ZrJ[[oo︸8<7bĉ*j߾}l6sDAJJJ/"##\|r|Z]WWwa;ǎw,_tݻ?ÈRY^^H vuu??XZZҲp°0b~K. GG%KXP(Vn@0TJɴQYEDh4իo~ᘘWTTںjժHtttXǏZ7nU*eeeVs.^κd0_ FAAAww+1 /_^fM\\ ,zԩSrɓaaa6m"b ɡaaanrrrsss~~ٳ 6n8ydRy//ѣGX,d6dd2MT8 9>)9]E"6@,f@_;pUVV&''I${޼yxfp8XJ7rss###Ripp0GdU$33sٲeP ;A^7 x{ j3ALR͛TRRVx ???l52#N%H$0cBarrrrrCV}7~='..+8qPx<gyiж8 WC &Haaai%^ٶ x5'3zG$44tGh4fyЇT*$~0(JLLLLL̠|ٳ?h#'66666vVG"##-߳m'd2 B:h{R~Cfg& a&2555t:U"`fp,0LN(PMMMx5*2jݤt…K 1ggg6M4'D Ǐ_p6?A:K/߆۷744 \._l٭ ApWi/B*TUU5||L&rzNՎxϯYJT`?p@._~ҤICݤOdCe%=====n^ۧ9pBSSӂ S*_ᅲdqqq@׫j2i4Uy0 12 A K._ Njs0?D!++k'fff&$$X5M{ 㕪(LgT*Z=b Lt4r+Ġ( %%- x@@q!!!W\@Ql6WVV޽ΧS(K.aN:uINGjA^?00aXUU P UwVrrrKK޽{d2Bqrr dTRLU(x N⨨"FBB_][[KRnj}dq/_)m x@@!tq[M%Νبji4H$DTE,l2TOсaɔJNNNr,Νk0T'sε0"pd2f l"H rBYrX,DMMYv-y~@gxy}AC__ߟ*00ЪT*JQQQģ޶h!!!!!!3tzBBhsssDDχU*ؚ`(...,,B 2-rSOOᅬ ȴi_|pG/^ Kx$''>}ZTZ NNNx3xٳg[EY/]$\]]m:v؊+f͚ E; ەo5ļ_,z<l?y_x@{:NIu |ԩSqppk ccƬ}TmŽş~fӲ;jv L0HX!ޭ|, OOu֥ۛ冄=bpMF>chWWo7d 2$A:N1&d2(JRts1 3F0*Jl'0`0L&2LlC,F/ FVqcٌ!H4FM&h4d2N_v`0,xVON(*^^o=m IDATؑK\ۋA `0,wUf:^UD]^7L|҈ T*`u %0`У Ǯ]hΈ:8H?xW;СӀxÀbi@cܳUA!APjW`0xcggg/B8vX1whѱαܹs,+**jу6*Z}n߿? ?CeeeG!ׯʕ+ju +t7W\~JGiIԨj.h۔'Hׯ'HΝ`Xݻwwvv۞G䩧6͑Ι2Ku r|ǂ0^j!5 p)-@DTYF8T*^_dI|||OOq|ʔ)SL!1x˗/ǻV>z'Rß|I|X_H /P(<}ǎW^#xRBBBdۋ/s4y۷I!̚5Ag:88\200A˗/IR/hDv_{{{^^P(裏^SLqqqٶmۘ1cp[ţ㣢JݳgOXX؊+9,Xb/ u۹sŋR)LNIIIMMD~~~׮]&EQP0 ëiY%#:vvdF0sۀ"(] ] !H  V142mΣ 55jOw.+=r w#_>ݦ}JJ0 հApj[:r+Nˏ}mt:߶m^7f̘{ZG>|^km~oݺuđʕ+m#>BpTٳ2K/EEEY-uN?jjj,'''gVv;X~ED$ .$m؟;vl۶ͲHiBB¾}J~P(LKK_~^^^6l@x1\^^Z@l'S$-ʡr4fMHˏxǨ ,ɬinӶUoV/c>)bd+Uc8iG='G|9T.;mz/G㩌7mѶX K$&Xndܮkt畒WfIf88R;ڎ* b_jն_Vor{ |#͏ H]/t]0a}IMOY1X-ږǽw;*rn&b&Lj4Wq`[VŇxq0pi? >>:]PIR<+**R 2חUH$U&D^$544 A///oUwnDGGWVVA zƍl6۪Ç`.//p8, _ly󦏏VU(NNNT*UhB>nbm3c^n׵m;r# J7P>L"?|;L 3Qoƽ.ҹk:RvmѶx:xm9݆o$!]8eSj!ܐ*:yj\S3gz =GZMj?_Kj7w4y϶l*P!Y꒚&I3S,W;mh:Jc5f_bw/#@Vn~w;S^^D\;: _}FmcCSïͿ8$bLzoڳg޽{׬Yk0Nٲe gϞ3gH$joo߷oɓ'{9ټd|$qۿ꫗^ziԨQ{}w~mA.\駟vwwgggFÇYh5Z௎OB"XΕ' Tej'ӫHN}.H3V}بisp}VmR6xay?f;rAnH8/v <ՠWy4^^O^G% xaW|s&1lt$Iժꌦ CG;^P’vA& 3=o]IaqJꝊw4 t;kT5&MDoGo KR_v7v@{y٧M%ժm=yVky nLNKj)-m.}o%Ƌ s:)^%>@ZW*si|t#^v=? :,X8C-8 =Vz =HXU'nzEћ7oj4&7nPT<ӂ鉢h{{;P(Ŀt ðf ÆI)hnnh4, OrܹV777E""" 466+l'O^b\rɓ|G-3&99yƌK/tӧwħگ_pkk+?yfiiiV:p믿>{lqsƧ~I|KKKtBBBBCCKKK:dT*$x]C1Tޱ-@Fl@$#sS&1}6~6]~ʦMss?5u* !}zzA-v ժjO:#RtˤZu5ɦܗ^+{eB NZ倗 j w|V A[vɞt)yC}c{v۩S>͏?IҾRCcޫŽG8w춚ߝ Uʈu@:/fͯ7l\b{v{ά;17 EѺV___H JVI$"&J:::=z_~?~|OO/"l0.]@$IF:nW@ 0L"200w԰J(cn'33566V*3&&ƶu8 ,sss322b/^Z*E+++'L0hH9rdԨQd4O${zz,o R__ZXt:]OOO~~~NNΘ1c2?pUUUΝOk͛7wvviŇO|> f_~h4⋭[q)Sl2{Yg\Tg{g٥.mKK4Vb׫X|nnnMbbMb+bQEE,e brRDqs̙3˞3gc9b9s)))9y_|a><|0 %%|g#͚ߣ?J8 Vpg;ќA$y Q]Z[D#l_Bv@γCHf2m-ֈ@/םD.41JrEQ}Vi1 `>.Ϗ?xkm`]-q8 dfC5%U^hP73I=:0sãyŸH{.}u{/NsNcY^L͕{ aSE;ќۋG8S 5 \1ƚ;wO8T*aI~t`Pp=99}=1cLgd}V{k׮=s3&zP(Duj0D"aǎ;}tdddPPPUUՆ f̘1bĈ>E/// 3kjju~m۶2,22r͚5!!!~~>laaaJuVLj/^ܽ{?O(P(oo;wCUh4o*333==:Ne׮][ld|ĉ{Zzd<<`|EEEY6m+D2_8iooqF\ 6xn b`"_ VzHhG}B 0FS O>>ց?]ޤ7ЛrEwP;]aPXbX W/v}ڨvYi?L:wҫvII0K؅ԨjP FMny[._~W0yh>WVɋ$4G<ڴme~¥pqxU t}m >b TT{5{;]\\o޼9ݻHOrrr Ǎ7t:f^5g111փGGGDR\\lpXwٳgr<''gر3gWIfy+ğNvV[L: jHu,os{}q)\ b1hնg3Hvxx`I,*??olc3 #G؅t"#$o-Y'\ΣЬi6mn1V|R:Ҡ\ _=eY]bSlP(ȡKJJɓ'7"~0L[v.\=|-[؋ݻwxxx3tׯoذZ[l̙3_~՚EZe+++SSSͿY"!UWWGEE| 1vvv"`Č۪i)ľ17FihbIWPT=f t]ǒ+8n}Z S0 IDATI\eg3nR̆L `Dq^L/sDfpr ľU]jdq`S=A6`7{E )بp0H^0aDpĖX/v}}=`ԩ=b\908"i2)C"ŖK===-XwQ=+'ObX'/.ĠsΖ-LsA᪗*eUQOg-N7q n]5ZL4B`(IIPHv|Xpp,Fe2s !"ǼL{ቮO{xjEZZڞ 9m9xeI mǏH{wW迬[iiiݮ-))پ}V}<==XA Рj\rܹI&/gX0œ. P]]}vOO+V@i pd2I={tuu-[ZwXѩT** gFڲ[RQ(XF.GGG;|.;66ٳuuu+ovqq1 q1 _>|oٳg?n{A՞?ĉK.PFc]]ƍOncZ-U*˖-ۼysWWJruu3gNeer7>?iҤI&OogikkDDT>=T=,Vi{=2ԯIK`2HrDFmUЬM֛T`&m@NOꥀ-8 b_$j_ 7Cb 02 ItH זuj>a„>v Yۭ=-aa=1E٧Njٙ:i*Q +g6kCJN&X8Фzjl2<0Q4u5Ep#$H +maw /lvW8)bϷS{ 9"eׂv};@Y$\ك]bc_ {OOsvEf=J,ox`c7v5F扪tA]PA/w{:2}!XIf4iXIsN+^hǏ2x.ij e[`f͚n^Jϟ?}xSN\VjO:u5JyfPz= \LGΜ9~$HFڵkN*//߻w1xdjooi(xAرc{eϞ=odbaXNN΃f͚e-ojucFu:`*1n$J*î ,6 S{{D"1XYYI&J% ʊs[[[[҆*JZM z1 ny!ͶXW__na=B"my9qDuuu-J3fhܰaCtt%Kz(Pfʚ1c#"J ^ߍӝ@ (--p/ǽL^@D;jv>KaFD0uK&WhG ;< 6!ɢ e44J^l cptx~]͋6HOsJ/zu}]h} W;JlĖ DuVG V^ &M ~'E (on߾ ǰa,T*an hZ eT*UCCC\\ 2LPoߦzt:F`0t#477s\U*JT#6___ۥt^z p ###E[w^KKիmFcyy+W>36L2"VaXss3FVi4<*44޽{P&̃u._}ЭqcswJ||yKDٲe˯ZQQElv``%K\]]I$R^^X,6 5LZ"Gy_lOyE''cCx<Ͽx°a n,?{JskzL&[Lyzzr\//_5Ԑ;JS$'?9$R84T*QsL&SThv…#GlH ŝ:u̙3 0Do@&&r-BRO=888;;;//d2tvvz{{ׯ_͵3LhmwҤIG~zbb"DR*aaa,<bZbB!ɼr ˅zrvC,\d2N뗗d2g |A`` ah4($`j1 3%%%[n]|@%=B=@&FD"/322~w.\J"222z뭖ggg[&ʕ+ׯ_"I=7p0}_X  i:uݷ Zc>%>E$xq*s%s7}W_Ob(|S""2O o /ll^]f.cܧLúq/' &MsF!RZ鉦g[8 鍙39dN.8 b_+?+l`rs:DdZ.i> 7O;x~ ݦ`t߿%3k֬?PV;w4B޽{͚5,pu…>~x^9a„~I*۷o'Nܼ:HO?>|8>۹sg{{;.3dddj6l׿׀aF/RRRϜ9s51!lL]Vq/vw %;$+ REiiFl^lí=gt9e|hQ 7 tl:u1j:_h?ܱ#b^sS*ÄpCR)%:${NO4km٘٢m4j< `]?W|CvcqUz5 cFl@[ǚ5Tq㈀آm_RۥΛ 3=fZv_ - L2o9tV]Kދ$.1jvxM"E+7*kz7 ſ6aa 6P(ݻetuuuuud3flJ%JCZZZN>-juGGGPPĉ]]]D{Ϟ=:l0|٫T*ݿkk+LtS)ZfKOO?u8p)[k-Fٳg Ny#G-Z633311$Cݻt+Wܽ{WӵFGGϝ;vRܶmۚ5kAAAF8/ʕ+,d2>|xŊѣxa8w`A @A ǓJh4*V ˄L<V ÇϞ=d2RSSg̘ y111Fq۶m;wDbxSN1&??SӧOOMMP(d2yҥ;v;::0 [`All,^.[짟~zwݛ_|E{ADDNۿ'8Nmmmbbqc8d2̼odNNN=E3X, b~GPDªUvرj*PhggWRRj*<rV,3g:::'OZ|_z9O>"bΜ9III4 «꺺:^믋D"bB0L&$)((Ν;K,1c>IQ\\ʊ+z[q… ?{YFU4ѱ ľU)7CB '0dΥR̙?v|g}jժӧOO6mɒ%mmm'O @^yM"qH$D2n4ۗKz/!!@ HҖ ߿?j(򥥥!88N577o۶-666((L&o3wHmmmBBG}2GYё+gϞh4¡鲲ƎkqZ>z^wpphbŋF^_RR^Νs΍?bAM6XD"9::9//<D'qL&'~m{T*|ȑ "i4:;wiӬ-l6fWZY__pK?OD"gbcc;;;-Æ 3JF^{5H*T,J„z/Zfzgk ꫯ,|)0t:zzH͛7p`?p8+.ۭdYf͚5KP@SL1bDyyP(trr"?w}.k}'۷oeO~ FΝd2yk׮]v c…7n8{/nh6l ܰah4ҹs΅ w}H$]vĉP$ی;tڵkݻo6ol>X|9N 6F磻+%BhL&?Ȟ [b26 /[B/(Ŝ8,['{.E*rk@"<==t:L=t-uuupX,2gh>CO̭5p8QQQ\n\\WkpTjdduUcU'g:}|>b&X0mڴI&痖* 흒b'//ҥKeeeF}̘1l0𣜜N: > e IDATUPPp-Z2j(777L cT777arNXp9s\zBVs܈ha:Guvvh<@<-`;Pbv*UL"LJd̻w5 冢W9D!111ׯ|xܹK>Sx {yy.JJJnSy J .nwO4wq_'i@@فF[7BzEPRRRRR^"JPƌ3f󍎎L 7|nnn=5D"%&&K8=AGݻ:bŊ>@0a]I8Uh$!'@ D5osC.{|mC 2L&Ι3Z"k.H4bD}`0,3g!#99a L7`0`a+WlhhqT8_@ txcVFy= ZSGIIIs8 >ywu7zbhIMMMMM}gYt>OrYfV__tvɤVzI6Q/^$ƍx oxPTnذ]]]UUUKdd$L)He2٭[mv).k@0̬̪*Bqܹ><⼼6mTRRY\\aÆI&yzzn3OP:n999x:^~p8=ܑ#G:;; ~嗪*(f{;w...F"T=2n̡n3ą2q֓WW悹U-aJMIIV*,-%%xh ;99 8777777(r0̱c&$$466={VVKdb^|ŜZf#""Ǝk!PԤ_ H5D""#H 3f .!!!()`F):;N5@ 8::≤Tj||Q(>>j;;;l777C\ٳL&kmmU<ۻUP8eʔV8w/8Ex'e"HJde(F&b BRϟ 2sL\ᰳ;2$D-Y,… 4--- F&CBB\\\.&NX]]j|T*NP""" vvvc)}R.\m2I&kYPáAn!D_'Z'd .@ _N}Ɗ]Ϲ=7EУg,x2^ xF] đ`1±5; zeCtK-L \\\yv tzH>'(;Q-qpppppL?⬬ Zu[@ @ sDFfQ^5B///CX,.H$ܹH$:99,rY,ְaZD;@ Gܿ^07駟/^KSh$C+tqNNk#"ꆺ9@u/˿lӶ3yP7xy| 81nn\n< y."iرEEE,+11iOb{ktj P(d2gΜlZ74ťߪ}KKˎ;|- &I*kz^F ÈD"F`v,DR+7 z`0dY |nd2 DzmYnkZ^o2%_EUt:~9t: `z^Z Ah4K[&Ht:^ L&S>-EN;h4_@[߳_ a-ɤh2q,stu; ̿[(--rB|Fl2{~obX/a@ ̌p @裈_|>͛7n?4lZvʕ>>>&`ƍ4-))J>x 333--_g2UUU?Þ={-Zԯ4|o֌36ld2sssyw}7>>~ҥK}]*Z\\矯Y&22ɓ'}:$$dT*‚"]qlv|||DD"l6իPTWW_vm)))D"J]v`0D"qƍqÈ˗/S(___`4G:/Y$R*Ǐ P aؾ}=܃B!gώ3N>}h4Κ5ˋL&{zzvvvr\ۿ踸8}[ZZX,SZZ!|ݻ{7e`|>ܹd2׷iذaPǎ`0@ YYY1110J#88xĈNNND"cP(씔:Nƌxx<'Nm`24~Y_`a^,"X]DOOw kt9TH-.¯`0&@qqqa]]]FZfQQQz ~TՎt\.`t=͆>BGGǀN J pKaa/^n{`HHHpqq>ixx{/UUU!!!΁øD \̀buG0Lttt 6 jV 5NwΝ(Ryyy=+((HT{xxr^]vymBaQ`0TWW>|~˓J}իW;;;JeII/\PuuuܺukYY/ig@ ZuW_iˣ죆9bh`3䓯ðe˖)~uџ~cϒo @ .*_i0V%!9.Jlۂ@ n@1nChs\.)J%D0NR4m߼y ZZZN>gaqa*x^drM&4P(T*ڤ^p.Je4 ]]]Ǐf{t:-Hddr޽m۶M8Q {lTTEK Cyy{キp  s?.,,LNN:ujO: L&9s@ptt4>Ss),,,666..|˭[ ǝ;99"h`P@ )(eujtA @u6HÑϻ??Aׯt:]<oĉ @< cj/_>#&Nؓ4h,**:pݻΜ95}t@` Hvڥh/_(kkMx }ڶnZUUs8믿^t?(^yO>fƪ+W\xL& Bh͛7꫉Dv?|̘1&MVBCC{q*+--MOO1B30 Mdj4󐔧d@ UFc]rb`'ϝ;[0H Vb!g1c eg[b/,_<<<ܖq /bR^߼ysf1b~a0>UVW111ꫯ}]܈D>`ǎ;wP(ׯ_j-Fs .|=O0!>>~Wx²s IDATߋ |ccs/p@Ÿ{P71>Y5?un}@ %B~n[oorssϝ;W__*}apBS% Nw:St:G***^*@ `Pj2&:'r]Z[[,Xl@oi4ʼn?""֭[al6[*NT"0d@ I_>10Hͣ։9i#ghѿvhCܽ{WP2!H`n=J! #&&fٮCD/׍9w7Z@R8޽{yyyݚN`& ðϫTŋiqww/,, 7F [2J#R7̞=ۀvXln* Ud2;;;J%n8a؃l6L-%RD"1d* >z釦k[8EVGGGT*F]طo]]]?~|͚5~~d2`l 3{l<#֙3gb|`{I{{{`#33`07<Ç4@ *nnn/ij{D9Pv#Sݦj՘@?jϤD"RD"ݼyNϛ7/_D&k[n=֓EpMMMMNNN<<ݻWRiZ&ѣU;G]nyxo^7UǛ/^S @_(D#ojn{.nX,4MwttjNR:::jFqHiZ6t:bJuVggs-\VVV[[ұcdpm[T9sf۶mcϟaXTTԼyv}Q\ylOcdɒݻwKl6K'|r(i?̙3KO=?f ƒ%K233 (\hrWZs?0G5k,AAA+Vعsgff&EQ8G#N ˓@ p\.wNx駏?wt:&Ǐzj["d2[ΡFJJ˽|/uV?о__?pG}bt:݌3V\ia|Wl6ŭ[ζ sH%]]]Ok׮7|(ŢRmb~Ǐ0f/_<886`ڵ_|+pk_cǎ/Fk\7nȨgXVjʕ111EL T*СC'Md_ e˖A*H.\?>Vzח.]zn6QEFu'qMTddb4m~?L&$^nuf^{pwiFUrlX,~!IIzV}ojr3̑#G3+.\j}Oe2n޼)ˡ;C}N %/.Vx UFx:{:ɽۻwVw~*___UO(O>O}\=p85q( =h0 H$=,!jip`QO>9ȵk99 b|`eVc/KW΍* =zP3s^z^BNMMU nnRowg|jAGCxOvfr\R6 7nT}?ôiӎ=$Ж LaXNNK/pxAfk1M>m'qbX,81pL(vnM`$iu=#á_VY Ҡ_SNÇ߱cGkk+ǻ~d M?lذaA\zcŊby{{C_ "il6r 箴xwwbBE4i$4MI=MFH$Ab-F LJ[͔LDH4& k˧gd K$"\\\\\\>괴Ã﮴G+(>.=w؅*6 Ӆf0]C K.>8׸DI:K{: oVUUe6vXzcƌ3f۷oo۶-!!L&p8._W$I_^.aVZZ?Ι3wxŚ1cCjaUV B Úx \*++U*G}燲L~ᇭ[BƍݻIM4֭CQԺuBBBМ^fҠiz۶m_z\@8*<=8<nų|gmݞ٘`OOA$EDD , "wdĉQQQZ ,`XǎS*|||CCCVkAAApppzz:ZNJJKK0 kkkLHH@_\.7==ƍ(5w\%X,VZZڅ l3}j;v, o`6cƌ7LKKCLҥK?۷o۷+. SL={k V533399922`w.֭[ZvҤI(!???...!!~d2?^^^T*7or|„ )))</""%]`_Z=˗ZmJJdr͛7ǍWQQQ]]j*WXa`0ϟhѢ˗w I c]ߠ( $Y,Ͷ/Z CP1tNٌy<ȏ@ r#>H?ct\i64L6Gh h!~/c ?! BCCCםcbbn߾"$IJ$XlkZGPbV萂Hd>芊BaXaaaPP-aiiiQ*J$.# =.ٗT1bDX@Q'ZGkkk{=U\]]{pOj_|պl ÿUV566v I~z{8$VիW>ѵZ噙cƌFUrssbq@@KΝ;fNL͛7Ϝ9SYY9|ŋWl{p@ E4UezD9*Oɀ'vo[k~ij^G1 C ֓H$Ƕ bJKK=<.m#G"=suuU(FRRҶmBCCQth4 ?OOOנGI#]I@ !KI7{ljfcc{:2ᅴLT0 s^=x$IL&bfΜm۶u֍3dƌ)))ΗiZR9s& cE#AD"łJ'iZ6D.a0ŋ;::bizHt)??%tY%EQGIMMm{6lؐ}q1uKfԩS,kذaΉ)E566;vl޼y=͛7kkk IDAT̙eðҢw}4?/Y!񅢨gN>_Dm]oz뭸8ǧL2~7|X,~`0Ə?~[ >00ðUV>|8 /&LW{ oxAz"RYՔ, 5>`"";;;;;;==50LcZAF=##obO>9rHPEQ7nizΜ9luV-¡Ls7@`T* {j'xB"iNUVVl)Q5v؆]v;vԩLǏ^:((a/cܸq#Fo O?X!uƆ nҥ=466j4BvSuk׮$Fs1TRԓmrfͺpP& +W1:t0(L@M=8_%ZE?=p Zmff[/^>>sεu@'i$$ikb(rjqcѢEӦMC۷o_qq1yޝvV+{HVwܹH jjjJ_~뛙W_͜9s}iaiNywy'44!6`6/_qE;Pݡ(jΝIII!!!d ߿?***<<礖G;@ꫯJr׮]Ǐ==\2**FCe2hẂa2[i!%(3t~OL逻$]vY]$Ѕ q p' 1hniiٵk`x}cT*FQA CGAAEƌagDDqT'Y,K.:t(%%eڴi Np8hX,FEL&Ӑ!C z=jу/1bܺu'_G3gDe~o+%%%;7pα "333''_q^UYY˗|Mg6@NLfuuS{=j?3g|]VB<<<֬Yo@X,V}AQg}& gΜF#GL&ӓO>Μ9sȑ4M/3gμϦhKO(3mڴt%Nn6[[[{T*I4i  ޒPkn)ioop8...(džb}Ƕo߾s'xB$]~=--- 4iƍ)SDEEiڼ)SDDD`T*QU\]]:\.q<%%EܹS"$&&>C zӧqW([npƌC&OvSNM:UP0 G$ J:q(J___ۄ ~TX\\\|ȑ =khh3MMMy..ť~#b̘1h999mPuիW~Riee]j/:X>nll$I7dL&a]$ hx=|&=,97TS=p]xq˖- ѣG/^=)[QQq>2׮]V__׿500mquu)S[\.wٮGiܘeZkjjlY%R4)) *䟁H$Bk|>񬬬,13f@YW>>>K.=~'|b0BCC̙c_cժUNڳgOSS2d?oks3;w )SRosN@0nܸ' aaSO=u}577M:u̘1=R5k;wvv/_!K=$$>AtBB,66>!œӦM ڿk2<==322{1b"h˖-.\uرÇ; xw$uK.b$^^^ awLLC`rĉSTb8--mѢEߝ2lѶȄ 0 QlvEE-t*֯_啞>yd uwww#""pÇno HwnKKS),7|sYd+B\.F˧ QPRJZ B^PTTԤP(Fї4?߶%***""ԩSq<..N 8 ;xzhY\\\VV뛒bvGQTYYիWfsPPPRRRw &ʕ+GnEڤ$`0.\PWWjR,,,lhh񉊊BpԩsUVVJ Fub޸qC?8aI---r<&&qsojjaZ1͕E%&&kt:]cccPPE 4MvޞZZZBBBlvkkkAAx%?AAA/rx{{/]tҥ ųf͚5kV{'OeѢE nb!I\.>3`jժbFDw`8w ðO;dȐ!C8l\bE;Ubbbbb &8LJJP)SzX,~W7@a e聤b, \͏l 6ײg0577۷o^^^hKJJ D"yc-Z޾}ۡsR)zjڵ# @._p+Vŋ_x cĉ;w;w.awRZj^j? 'NxgP2̰0VsС_GĎ;P;VQD"’)߸qc@@N۾}al|#G<==oݺuٱcN:(T۷wvv SN̟?5W^bbbry{{;Fٚd2kkk^aXwk0 gΜ^^^---۷o_x1Zmnn^p!׮](GUUUff@ -++r d.[f[V^od*--H$x})f^ /^,,,29fϟ?d޽]SLG@t m[d>鷙3{:VjkkIm/((EQ޽{y晸86T*±NkoowXVTRG)))=zIAhhg}6nܸSe0PGX{aaauuu="G cԨQg|~FFsssH$ZvagΜd2bh4YVk~~~mm{MG|| b`ؽ{L&4iD"Ae`vڕ볲ƍrccc;;;Q㥤\t)!!!,, ATWW#Fxxx0LwwÇ00aBLL tׯ_?{lXXdyfKKs=겳kkkd*(( cƌdmmmΝ+((@ֆ>$Ij4Nq^G4MMMѣGmǺےuh.,,vĉuD8z6e{l/%%%%% QTTO=G}.-- ...H?hGD 6-` = t˪['% z*D"l6{gƍMMMhj̘1=#E(ĉ6mC@͉ϟ??k,$ڒљ+8KR*" 0aE) 77ӧO?3(QFq\ D"nhh^|/]P(<=== k <==Lb3b%''_둑?Wggp\10:::>/Cbxٲe˖-쉀uo4`3ρpNp΁貔%a1-a3AKZv[o3y Do+V7ud2###80{l[~qsssɑ{nAQADkAtp8Aw}Gtuuuii\.Gt:a I&u0M:nӦMSNBk=`0x<l6;66:bؿް x<ސ!CvرbŊRņdJRI@V H QQQwv>^ojjz_W46-ju^FQT \s^_PPaaG"#;?_w[O{<\\]] 2rr) ʕ+ϟ?_N2%&&/E{~zhkkmAE1LXe,  h鋶* @"(?Cjp5k\z> >}C߸rŋm3$׮]C? ˗L&j $IVWWk4_kkkQ@ 8uԆ pl6;c0)1f^EQnݺrԩS{k<"? =0`ŽI˝;waü{ W՝>}˖-pq[҆X,1c- -|Ry$m6@0aH;;;Ϝ9ckjjl" W\0 ZK.ݲe7[ưaBCCIX,T*]ԾC;dBtbb"*<㸟_FFF]]]}}}nnɓ'g͚u/n(@4Mo7\xB/=i^p!<<<>>,gmDۦM~8 |/wx9LAjܹsR(.\ؼyk\m }]YYi^PPP(EINOP*Q__ر={,]0M&… ]]]o޼=i$ p K&)ɚ;C!8{xxGDDDFF^~~ZWAף` ruuuHנ(l6 Bc9ǫunOq8U__CG\rE-ZȾ}CJ[Dn`9x(D#ssTyåwjKLM57wEEE>ۮpD3"Hj4M/kZrCEEE}RL<ɓ >!++kٲe(?@;$j D"t7oFLQܹs :>]W,+$$dǎ4M`0Axx8=|p[[}(`0PeF...۹V[VVEu>YRRb$aX ÝLW"H"TUUG8,hD&IƍEEEs̹퐻k.޾}5hq]C¾Ӄyͽnw8Zaf #%"PSSӗEL&ٳg &HPLH$pBQUUe4Q>GSSӮ]n߾=|CTi IDATÚիWK$ ôZmnn믿k_}ÇK߿jߕ777[T)"""''ǖq͛7ܼy355"Iի~a 3p=zƍx㍾χdݻwT*94~xx:Od[[j+m]֕j-))ٴiӒ%K\.EQeee/2a</888;;… #G0b\tigswwONN>w\FFϧ(_~QThVtl~~tϛ7O,+WL0$y֭]vYV4ŋW_@(f0`0|ˠA`VQp ]L@{<#s =qajþ}O=]QnƢ0Pc&`0::: >/[[[z)@aBxy<ސ!C.d2YHH9۷Ң{]/ݻ7__ߪ*^yWrx]ƍ۲eKaaaSSܹsccc}ٟ׷cҤIW^ELfqqqvvT*|Opӿ˗/:ruu]x}߿(T h4ׯr2cM63g*>^bO>E ?c}8~xNNW[[l6mZPPAbbbEET*9NhhlF CQQ^M 'O>}X,DC[vL&koowqq mnn0 k׮9r$99֭[ #**>7^sl]/\ r}ăv޽/_Fm455V;L&3,)l,Tw7=&6@6ڪK:HZP(DŲl3Z|>rAB8)lG6$+nas0L+L&ve6)B itjK3LWWW˴l6{Лp/ZVlr3c?a 0c|V+a2gϞ=|ڵk\n+ ^$tի;e68f͚.T*qU3L*Fb2٬VQѽL?E"oO p8 :TǾM&V$H$HE:D/W@GGGEEhA/btvvfwwwV[ZZ?jzxx8n2<==Q<"kq8 ea6 ;jL&RYUUE?z_!([SSP(  2* \.Jsss{{Cl6GEE_hmmmUUU վ<MF 򖖖`Po~aƌAAAQ6R>37zMVu[Ͷm s?8cT*eVL&=Xe2YwHL&wvۏ,k`Opi+TÇw72&&fܸq 0Lۺ?akw]9.q3gjXGae ñ?D0~@w6p (l63 . V[__b+G~]v)H{93Ju͛7'''k7NIIɎ;jjjPᶢe˖fsVVֱc<==B֭[ccc,Xc;yee޽{KKKjuGG̙3'M&`4~7ԵfٳfD 7nU(*mǏEAh.++۴iN #I':t}0$-[455) .k2 ?,֬YQ]]}ԩVsssWZ5sLJs={]vʔ)8={V r_cbbV믿RYY|.߸qF믿:t?2j͛/]//_l4|^7ٳX,[n=sL`` .--MHHxgQ`0//d2 ]vEEE8e###7m$^yaXggW_}p~s~uL\5̚[-/>xB[ iH)2F& @1 ϸ^\d0L>pap\>d08E 1\co\cp.`v+ɓ-'33O3i.ӕQ4=\]]^#|>?--_+H 77'vO;c26NwgϲlLR[[K%DpPIEQYYY*j޽j:==>(JQԜ9sbbbPd2}7GE9sڵk˖-1ba?cffŋټw^ >3>~~M$5˗/dڼyT*]z5=z秦CZ[['xa8pĉ|>?..r^waÆ߭[=:mIHHHHHs77m۶>2]|`պvڄT4'NXjհa j}7̙ӯo^+7V_?M& z}JJʊ+l_^ߵkJUrrrΜ9vZ4yټlٲӧOgddSN:8n0>wyg̘1c?~֭sss03fš믿k7n>|z`P.:uL`@Kc쟍ƞ)s>@Џ5pXc6̹>&]m Y<Psgց===-`X,KEGs6mĉV+EQkGG^ww+W2(;lL~'޴MtIהЅ )22:gΨ(ˈ( n, --I6{r&m)('oyIz*s!GEEc y``@ BCAAA H$3gNII͛7Q* %<DDDݼyӡs=|?AAAMc?\`c yFg)-27 oj}h@'@%M d2tzHHù~R pss#'(z?\*.X,[V \1l6Պ ^R΋dAT*@}}}vv={ٳj:uJşa˃Z\L^"8]0L*VTT1)&8 bllO>iiiy}}}X,Wݒs(555UTTDYֲd J̘1CTVTT`GRΔd0*>Q҈b\NCN8t҉I2%KN׮]kMr`>AAA <ϼd s8Al, mIBCz~߾}WNNN=r'|裏KV3f̘?>b}14 ,|l?icT*Zreff&Xp8 ,سgOcc) E*N W^ H555{Q(;wtss+**ڻwossŋnDv/;׹2?K6mD6Z-_~enn/rm۶lٲy٧씕EFFD"Z~W޾cǎ_|ѾիW^mX6o\SS9e`` ..nS bd)9   E0?oR]Ѓb ;/nnn\.v{Bt`>q=Z>>!!!z~*{&{-Zh…+6m۶mV7ihh Eȍ`Kgg' AΝЙcҥƸG===:nnnI^^^J~%a`*   Ǡ2=VJ 8a#1յ&޻~zJJʢ񤧧wcužj%:̏ ðq7~ƭks8n-??7޸e-f <<WTfsSS?_XXk. 2}tŲm۶&UVgm߾o ;LJ~^vHNkmm9G7͸KL&S(Iq{UcZ]XXh?-00P  2wݻ777_relll`p^^޽{?10^p~~~T*U$-Xѣj:==}ddԩS111)))˗/+ʘޟ~);;;>>`/ڵkׯ__r}t*lRD"r1L_?FpqqYvݻ^:k,HT[[{9sDFFN G?;=44f+Jj}Θ1cSp& }ٝ;wٳg3'xLJ8^ziƍUUUiiiׯ_tX,;w.ܹsxx8111::`0;w+9]v>S,{̙~?n4APHHyAAAg~׭[d2ϝ;kM6B8? 1 29   Cc @)Z Ϗp8a08E/7lGGG|m>30ógŋ{s ˣXxF>>>˗/Nуa2[XXXGGHRO>k FBX|}ٰaCJJŋ>b2222J(: B!;&&F,>|X ,[,::ڡԜ9s^zOO7aaa,kÆ W\Qr|ӦM}FΞEvvv@@y||s-[;w&W^ u΁kbbCC LfoܸƨG8Ȉ8uɓ'Qd~}ۡ vuܹ˗/K'|2))<7xȑ#fY.޽۾ER)y?O|~jj?o~GŒgf`… Rn+s8AAAQh= [2!gݼy 3g~`z``Eq#z^zxxدeAó8S(&IRyyyMI3gL6-11#''ԩS %55:"f2@% 0% sΗ Zm٤Ri(N~ \(;'744r-d6JT*f3͠JS:sܳD"J<22:y̌bq^o0ݝ!AattT"8Ψ,* 9   {ϊ[QE'p3f͸<`N Kv 3ϼZvƍ.]X,jO>O<ǎe;::d2?LR/^l}a3fzOǭV+\jUll--[lUG}7ȅkמ={vZ8oٲ%???!!],? /޸qclluddD"lذ(--sss[f{ٰa?@RAy+Wu'Rya@`0D"ѪU7nؾ}ݻ]]]FT* 駟6mDښa:nZΜ9CjU* . W~t...F{ٲe AD]]]EEEnnsjcEVVl67448G9`08TȦ8 Μ9SXXh0_b2ӟ&2E۷o%BO'yx|   9}aL8Li7RLLLs`zĉPPe\}QfffXXh4}}}cjiڞE>Ο?_$q8]v;+JL&s[:tvn8RڸqX,FQTR)J qzj98><<\YYYSS-J%b9sLMM͊+lnll裏y9bΝ`0jFׇΝ;whhhof544={6//Qo/ wTÇe˖l^_KAM ZZZ~G}T"LÇqZZZgwԩPBAJ2Lwtwwa2ׯ_o{9&IѣGGFF Ebb"FyfkkkooL&kr-DFF_> EGdggߛYASdɒ%K+wAAA{iIe>P#lG¤_RWWdZ3fGGG'YKX,===`yllbM/4ZdI\\x: +VpF#Nw JJ=(P( TB!y8n4ٳn: Lfbb"`666\h4~s̱,KEEo=}tpqM6S( *++KKKgΜiX7m A&LfxxkHH}Q(Ddɒ`pv< ,@QW_uwwWF$ Ι3ao###%%%dAAAA&ٳVz@ZafLbTkׂ|P \Xx>~wNX,Rxbcc EWWWQQQoo vhW8q_` `   (jkKե:T7  0˹A?S__ߋ/xˀJΞU.}:NwP D2TD5RRRJJJ.\h_ŋfͺ RSSɝOb8((`0;v D8tzbbbRRJ]h;XDh4L&xb@@#9N``OT*Fcgg'bU^^^eX\."J BP&avҥ%Kp\PՕNj#FFݍ40AAAMuIH 3)0~XX㸃 =$3 [v&b̙z=4 ]4{E䖆tVeeevv-q/[OKK#;Kwtt_ޡǝ,HX,#_0̾Jh:DT?dTUUϝ;#pQZA Gd-P6z[,7nĀ-FkpТ"V #2<<\^^7HP(nܸtNp;"3Q "ϟ?ɋH#aX___OOϸA;. 8AAA4HҢDċ5s^[*ڸo<\sÈ E>߿СC [ ͛d Ν;wM6M"ܸq[jO2" -[cǎӧO=BH$K~˖- jmmGtw0,--׷… rDLfpppYYammmMlv~~~eee\\;tNONNF7$66>7oWWWWqqkBB@-رcӦM쬯tuut:ݹsF/{zz&$$޼y0..T- kllܱcG^^kkkkyyBw7|gAE0رcuuuӧ/\9!a*wrqDʼnFxcFY̡q,)L   Ocm5ښc}hkSinik,Y,YT Սց_ini6phv;22222}ٳǡDRRRL&JzzzRԞ*#@;nf R)hMrssL&XgXK.N d0===8'''Ϟ=ۡAF1))鶮 Bqwwimmuuu r'$$$$$(DPLdjii`>6DD(@ `2 E*N4K9tOOOD\.OKK `0T*Օf\vmtt4<<| DžFx^77`-8.f͚hcccccT*]`9y`0jjj CXXجYb1aA:JDRƍEFFŹ2LEu: Cdpp677FX,WWW>}zFFsp@}溺:N)AQbA!àvC`0xyy[?PEE͛7gΜ9nlٲ%66_V(9@}|T*`8߀0 hT*N{c(ʸ{qEQ) NwSf;~AfqLc@t^iR(0%صPR@`.!8-s&B^ h4\$5t>awR )XVdr&`<\@@Yk?57j4^EQT()fZX,l6[$ft:^&\Ifahq\("R]]w`0^V+D:hi4pD"X,fqb8n?{l67449d>>Fd2rBn+djkkDў7AN) >OqRyƍ>ІEѾnww8???Zl6Ϛ5+""`56$z{{nZcccCBBh4^ 1~. .PUU522#9VFyfccb 1cyq7---7oD3gΔdSigDpÉDL844T__" 322d2y7FJ9k,////J*..X,<oضm۶dɒhtlSe IDATO-]V*8|HL'I9{vAAA/OcTh* U* A,Iģ;tn(zMc?l0M}e<y!龾[i4Xvqq8$T}xk޽ NHHX`FY,Y!9Bq~*p>[D@&9)S(I$ɴ?#RQQq…qxXF'|Cs8Ϛ`0"" !- |ԩ1Nii#G\\\O.^zܹUV[^?x`}}}RRX,pBQQ{buuHNN>tPjj+Zw}wƍ\駟&&&^,Դ~:422g/Z~JQQQ===}… 6ڵk ,G{,0 VuŊ^T"B>8駟t >qȑG}444AŲgͶzj0d2m۶M(nذ@!##c…-Ņ 566~W |rskiiٻwozzz^^Wss͝;A ~ʕ+OJJr(sW_1 @ӓc_tE悂nQ ofggϚ5d?~|Æ  oH$ZjmM:::?/]T,t1www2ŋ_:b*  /K/) A^{U*annnd￿r͛7'Oܾ};^~}jj5k&a=h14јp=8Ue캠P) ܃1cV[:( AAA=\hk,Ք^f- *#Z,IDQ)%F`f̥O R ST`fa4- ~=V}}}'JxH*UwEff&q!D"Yf GUTv'TjDDDccRtsst Ś={vAAACCCFF JR v=`LTTP(j[bT|>_׃UjD2($$$00pxxxddxr;--ʕ+}}}V,⌌O? HII 9+.. h AÇOtϝ;ٙh斕uU^[@Eff͛7M&(P/55?e~"A=z4--M&k됩7s/ܿs9s|w6 ظpBdK>.N`0,9 T3L('Ejt:.pLfX,d*UȞxz‹:b2m͛\jDX,ǎ1czGyL`0~>`H /LѴZ-[V^M^Z~}VVF3L{쩬LMMR_OOO?f-[с[o%F+ZT1dIr$UzAy~.~ɮ ꄡ,~)Zv``~ l6p@4u87J5U#U6Ʀ%ɮp^ #Aj￧h  18F!dH_8nX O?(Z[[{xAp4˵Z)WNGќȹ\bX,Dʓ'Ovvv UVMK||||||rfsUUW^پ}{~~>AAlذ!--m܋["B|ljΧoXΟ?_UU/Y`0=#%Fm۶m۶EFF#b6<888qFr۷o7ߏ8[n #(//P(111 %}u֑O>-322 /a͚5))) $JR)g\\\rssyr9O(/*/uT,,:;A= zhK5%1R$)3\g~A8op?pl^v- t_p^a2&j"BNS ulld2i4ooo7Z֢'N,\QV+Bq^@g &J;EQBT655k!!!`b_}Յ a5553gΜbO*Li_Bf L&WWW300Rz=ɼJs8L2lڵεM&Ç_z;+fwxTBg.))L\.w/hmm}w쫇YVPӓJvuu)ʀ P__ag* HpʕJHH?w///2x{{g'xދEC? T0T#ɑqn~χ ݒ5Tk H I W z8hSSӑ#G<<<֯_o_ettԹlh`iFR޽b+rb\paϞ=yyy#צFɓ'.] J , 6&N 0eM2 h4- A(ٳ?#MMM|>k*,(**R( 8'-Zݽsή5k8%3''ԼrVݵkdzF yŊtzHHHHHڵk]olݺ,ɅJz<==}]Yo3g^|X:ںe˖uٷ#""&^WW/Ʀ=Ph;DQӧO_dC)$n4e2B!^bVZ2?X,ּy,KSSSpppdd$ f߾}jzݺu1BR:`C Ü-zb@ FrY#2`f;())bBժT*wwwzV{ĉ˗gdd0M6544Oҩ.]:pO?@oذ紗PT###>>>-...66rss͛/^#8&h@;PnD =ƞ×K5_u~uX[zG>NAAAtǺ] MQ)0~Xkr8ILJ]]].\hmmu~HW,].q@ЯpܚVmlld Ad*;:{ljjE3blV*FdXT*)b$Hwi'|288y`0ٗKh ?b(Pdoo/NcRАR߳f~Θ1cVr5۽@x<^xxZbx*;GСCo?Ѹe˖e˖9W-RtºgZꫯ>ӹyBH$\ ƶd2͛7&:L&uwwiZ>U/8O rfknnDD&q8Zm6ĂF &]`6662P(t(pw8&3W|rPyɓ'}||~fxAOOO)Zǻ=<<|یq/@ 050 3|>D)l6y%88رc6ܛfZd⨨(rY=<ٵCKե`^\Ϲӄ`4AAAmKեc8\fL$y=5pŲq_tgΜA'dP(HhpzqK &$$1׆pwwwGQ~U!hAt:ŋ]\\ʊ8:Nh4P͡N7ӘK.y{{ׯ_'IIIv*++ŠuGAA VNӳ룢(fzjDDٙc\j9'ca|PPdqT*  EюFOjHnR BEDD0LyCCC*zzzh4P(Q/++*Ԭ].B Ç_@6/,, |B011NϞ=_Rv .<`ϫVzꩧΝ>vP ?kB .crM֝,YYT?=AAAQi NG %1:.z:yuuuT g21116l C0{{ձ.mWiҥ ظlcK4]4b,Wc v ""緟]Z38Ü93g3x𠟟Bhkk۳g, ]qb#>}USSۓy]}4UDx@ 񏢛ӝՕe Iќ1ӝ.OH%fc#d2Gɀ֭[4JΞ&).5&...zixG(ǀ!Ȁ >WRdgg=zŋc*U+VWKK ""+jjjƘ 744~bh4 jkkd)&USSSSSfEN133֣Y,Viiiqq=F7V3jfgg'NNjHTuwwa&.>Eqq NRūEEE...FC-,,I033~M[[m$#H噘544TUUURRܬ쬪 SFF{: eff|o߾100ʬJKKvss*))jmm}e)^rvv"===A9˻w襤Z:&Y+Z{{[(X|v!Ǐ/ۈ@ /"?wCTJH--"QqwUw]ݴˮ]n߾Eمc:r]&9]xrrr"Zϟ߽{|p:pAvvcǒ̩}˗ BHrq8*' ɀO ( w#Y80**ڒ|_WFw$WW"~D[@ #ʾ̬,6M,,]U]/!$p'p޻wOdٴᘛ+))EGGc:MMMazuuuaG VBx.** ueUU_(! #Y8J(++K2zxmğOll,ND /R[[;Əb qE1313igv^;}eW_DDD^?g}NܹGÉgmA ?̮֬v@_FEUU~ITUUh8 ~ʕE@={/_兙+N|Idff9`Lݻ@P,,,,Ybkk9۷.\033r.444tO[lQQN 񷦸NW!104P(ʄjTJDTunB 8::w]~fPTT|据.T/**pPZ8Z[[;::D"c<Ob %%%@|$18s7@ DHQ<Tž¤\}}ouog/)TTT\x &$$_- ٽ{=ZWW?v؇5uѢE4i҇DZxbWW-[Àsss]ݿ\8@  (-9DgϞM0EEEjLtz{{d2o߾]^^@<pϩS:_._vF_( BKKK^^sPTToXZZ644 ÒEEn(x4{B80@|!H,_6G{NzGzr{'Ԙ%[@-z* ,,ѣG]]]***]9222tl\\tA qիW߷˗/6lmo@ qXC܌ΌʾJ Ie&m'SGlll,bP(0xWW܄ uuu BCCH16 D @|QgfN՘Z(x<333XxT{%{ x/⚚~7nlܸQ8?uI]]ݰASSӵkZ[[<޽oϜ9{ݦ&yySY۷wޗ/_RRR'N(--p87o&%%P(ssE98848---::Fy{{/YD䳊fGEE%%%uuuN}ܹsE&fjjjtttee@ 066^`&w&555EKXԩSƍt˗/{zztuu.]*bwFŠ9w\NNԄ 6nܨ988hoouVuuQA7oްX,--3g.X@8}@ l~)4#Q(RWUWWWss@ ?6(,kQQƏOPܮ^ܬ'+;bS 3 Y8 #LP0AyB-6-)+;*LATQ @/_Xl!b(++iiiǎ:}ov@á c߾}Ǐ8q|RRRttkMEENͶm:::H$###&ѣ?ѣGa3|~PPPTT@UUH$>{,**ܹsIII0[]]݌3JKKx|||KN>3,~ڴiJJJqqq/^ȰfΜ9sLt/''t:{ڵkl6; @YYyѢE111XZ:.ޤokZXXX,Xu__͆y9w^>OєRRR>C tuĉ#GxxxtuuGFFFGG߿ҤI,ٳg7o [nֆ3f0 2s˗/XbL&b4ٳ@&I"H$ӧOϜ9aÆ~MŠFAVVH$>6W>|iii)~bOONIiBBB@@ŒR]]]o>|pxx8ֶٳg+((h4:~Ν~ӧ&LL:GCBB8@"1G_ +IQ<o֬Yqqq8JJII]t)!!AII oFFFp׭[-޽ȑ#]]ݪ7n|W͛GP۹\W&L0c``IT!wl#'/`#!ֽ{ wݮMC 괷?~H$.Y秦l{zҥK<oݺuX+W'%%=|:--E/3gl===@p’ŋud%%cǎEGGs_͛暚{{ "-ZTZZzN8uԬoF6mڴbŊfcU\FFfΜ9s3"\ r8ׯW:::999nnncǎݻɓݻw'$$477\v…*+**_^^^ì [>}ݻw111ЏU/^`ܹ`={}wݰ TpœyΝc2L&3;;[OOaaaV4:VDFF߽{dɒ1GoaXϟw cΜ9(--e2;wlkk dI&Mt:f_t H6NXIvZ\\Cuu5M &O?>nܸʖ\/9s:rL~i]]]yyyUUUIIIllGpeeeKKKϏ?eFEرcΝ^zIQQFwww_lׯ=zTZZjmm]QQq=|:a"檪fk<">o{RrxYa?ID BGG VUQQ CUU@ ==lkkk2y833@ >d.[PPpڵvZVV@ (gi:fsl*UjR[Ҟ=* RG '22STfꀋ":tS]fuWWףG<722BbCCÅ `TC<IxyysO< Sddd۟SӃ O>)əFFFaaapCVTT zzzGndddW޿_MM 3?xo-((PWW)"COHHa܅ h4ڛ7oDߏ j\P__/^L"***:<+ `wY[[H,_o*=ДI*3g۷oE@٘^ QSS~zh0.]:tP?S A"9S__ƍѫ}a6[zyya)>d +IQ999UV<.]ڳg3pcڵk'N@K '[z/ 22Rx ?;Ȋ+drCCCCCFEhhhhڵk b8t@ صkל9s`ztt4J}O{&*lg;w:ٝT;?8,Y$ձ@ Hlv[[Ç mll?~k8;;t4UBBט~ {.%% GEEűcbccySPPPꫯnݺuΝ#G<|XDxjĉO<Z)))#9p03\ E`0111.]RVVɑ|.D9W x<.+H7 X#⿌!G H$ 244K xCCCx<~ZįL& _,D" 9^#(C `lsMG"p8XG_$`].u5$ÞC|>x<ؙz CxB G  b 700@R?#LLLLj=l|Nu6E[@%|111Pj R555󨨨a"qfzzzd2fWUUA .dffl.<@‡CNܮ3_d ǻzz ޼y_H\tS"""߫@|X|yy9}Drt.|>ps:ɄC|%8vH$x}|EV:w͛7O8/JTAD"m߾~~~'O6mpbxW\NC; L: yۀ'@ 5ko&&&3fˤP("NTŶ555&aLMM;G9M+ }F a ;a̾B w\FnwnzGz@@9CcQs7 ɓ' xyyihhl'{add7xzzb TBRϟe_2G, .(**؈&x<A$'M8NyyyVVVqq1400:u<@ dX^^^ƍ^Ur---yyy|>7???''FJJˋFa-x%%%iii򮮮NNN«'???--NMMVAATVV .\ՕD"  o 3gtrr񭭭8ŸUTT͛WLfqqqJJJwwFjb%%%YYY޽466766 utt}IIII8"$xgϞh4WWWY,VVVV~~~YY&_SScllw +vZ&"==`#m4@|a}iZŷ?o}nd;Mc OA] )!b8œ›l&>\yyy6 3 @.h-.Jߏq?}o r}-3 99;wܹsKommܸqĉ";ߋQ&bbS.痘H&===]\\`\\\eeO|0x}&L`mm]XX+::ZJJ j} !!!ǏONN k֬ 1H2$M?cի;vܿ?44444@ L:uP; f{/ [ @W-Mgvu# {>#5xQHWo }&3m[/Kzzyˈ_@CCS*++kooolld2ܳ+#H~~~ /"[䜝ϟ?_nB >!Q@ m6aWwM>~-$$$$''[[[Yr3gΞ=ۯ]߿`yy/^ܸq fϞ=155]paWWWTT+W0ɓ'/XXt)l@ Ȉ533۲e }vMMͼy8lG,XPWWϚ5Kxqf?}4%%~ӦM EAAjGEEkiiyyy GjrJ?Nj{Y~~9stt޺({{{ᦰKII͚5Jo޼yĉcq킂͛7X,:o߾ݻwSNϚ5F憅566Ν;t:=222 `…---׮]ꫯߖ#G//ŋ>~x޽!!!4 +رcZZZӧO7nrO>@ !EIz$%uv{{;8I><," ---¿9 &|?d2_v;~'l7Caa~ {a Ǐ;::~@ P|? OLLҢ¯1͓k9(I*Z*88YYY5550֭[n]AAA|||bbbrrCbccҠ* .3]NI'{JR[z{{=z{ݻ0albbC@c~pF xrrr111fX}-A 2A``jd>1)Sܹs5g̘!;H 0OkY0ķ (Yxk2 IDAT}tŋEEE.u[nB{&neevZ-T*uP ̙<¶/^KIIkii;w.##c֬Ypeoĉ]]]557oBbϿsN]]7|cnn.?~~~SNtTTT ۞--- e߾}O"_ cΜ9H$իaVVV}}}YYY&&&"3KbOwvvfffRTCCß>uꔛ&y̙FJJF?~ƍVVV...zw)-- ;:y̛7@YY- c֭e@ HLLTUU544dujCaWʠ {/ { /,,,,Z[_x2&^=O,A {.ɴ=u:c---"٠rX !M bzzz4۷AO?|a##a׬Ǥ:99D"Ĉ?=z/DDD@ |ʈž G&À3[)VWW i+Zlَ;bbbD1߷mmmmmmr7oXBKKZKK {|/$t">aa'H~#)((̟?gϞs۷B}'3z'ި, T7HküCFg$A ݜ/mM͝.Ox?BCBBbcc$TAII}:> Y'T@s@`wE<}t ؘf3 --N}p877GnUUUcccjܸqʘoE[[[QQƫppfPSS333lkkUggg ˷X,:P[[[]]=7:JJJ|||䔈 && .ߴ77/^ R8nNNNp@ Ɔ׳իWXCᲸ8 ).nwwwxd2Y ܼysʔ)Pl޽{ȑacފ &iaa!'''򤿿LIIIFFB@2iii===yyy.;88RSScaa%y_ "Eɏǵ98Ԙ%5ZfͰkmmm=~x,为 rrr*...W^MMMItttB$Jy_~]Ȕ|}}ڮ^*%&&f:::u3fӧ㛚*ap8hhh/Rk<TIlQRR^常ե6 NIIT=xӧwQTT3g(y۷oAYYJ`?nܸ5kٳtss~ ͍d2PH2Dx}xI驫r x<~GfXnnnovvv \ < `Ou#VSSX6c+G  f,-Q[{"u'# L𰰰`7" A>ڡsCee%@ $󛛛ᡪ@ hmmK|>I bmnjjbX2`iiHT@KKK&lhhvy iɓ'Ǐp8Ђ"?zf"477WWW[[[c& }}N45`H 0, ''GYYy]frrr%`oo/afggW^^.%Pvv6F_}]]]g&Ƀl6{Xwe˖TAaa]=.<|wtRyyCl[!222>>6lHNNׯ_ӓe˖ϟ/X+D| 3i3[k}Ỹ/=۝Z//_ŋaJJJřXbbbb^^vXZZsY=vokj…222t:NcCCCG  4w߼yb)Jjj*ܲ }8Ν;78ׯ_~-.W^#;hjjN6mhh(223;;[uEZ}a_x&++1X8«===+WՅ0/_8<|7믹***8NŠ$Hػwo]]݂ F622p ث)ӧaXXyppȹaX>_S[nf '5O1Ǐwtt;6 _5Ǎ7qDu!4p8 !kҤIT*JFGG'$$||ħB}j懈 eL;b}d"d@ H$MMqƙ~(h&p|f|~mmmCC! K"x_ݻ7i$qeee''akr222d%H";=T@ [TUU`Рˆ &hkkK^bG޽{»\'Os窫_cASSSqqɓ}9s`{;r䈛hVWW[YYA7K.-^822nݺuΝϟUDDW^-y/!jH&(ObŊPVYYyGGDž n߾ʕ+ްa>ܷo۵kH9f cPPN?rIf___uu0D@QQqƌ/޷oZ^^޾}VXqgɒ%ǎ5k:HHHhK.L6-((ʕ+NNNׯ711ioo}vvv~$@I'?愕]vmܸ}ҥ%Rpp0܌ryӧOWTT̞=@ _rlݺh__~yeˈDb\\ܫWF]v=|0++)((~PDDDׯZ n풤WZeffUyil`we]T]\U\-@ ds244TWW)// @|Bx, .{yy)++9s~P(fZto;f?{,55 +GCFF@Z]c>yԴ"99ycxBWWJGGGaBٽ{ŋl2~69v̙34,+<^V: ,_^h;\kx8[ƈp\</D"ay:::WZ)G=z(++KCCct)6]ZZ:}t(1n8RԼ)Sx"11QQQQWW\.ٳ3fI@}9.{n8DrLMM-,,lvNN΍7dddDql6 `k.{{{ gΜ111WL$y<^hhh@@Fz]]]CRׯ'M$~`h4ZuuuSTTT>⯃Qa֬i++'5!#eKkEkouoss@,7'N%ĉ_x!0?<@w߭]bϛ-,,X;w V-jɒ%K,lmm133Y500+//oool[dIgg' ZreYYYGG[M55Q [l] F%''D40![noG"--Ma2)Sjkk{zz޸qY̆eeeEEEx< &jhhܼydlmmmq-Tq lH:40;vH$ h޽SUU֓ϟogg'C"?RRR'5D>ښH$fggwvvN2EFV33Q$ pWUWWU׊|F7::'c8o!}..ΤIJ L!Sv`nn>xLqė2pfffq1d2lP(Qqww!18ŋwppx8lɉ-iΰ[(H$Ͱ픤"ֆrϞ=K 0I000000G^^J0*0Oo60 Fܖ⨨ kFd: +3ү=34lQ"FA(( F)@q46gtfdtfr{qg$gLƏ@ ğp p8g< Fbpp::zm" ())A544.\[[[~~Յ+++]VK@JJJ hUa yy! ؽd2恪V"V&##xꕦ(,KIIIx5??ҥְ"ii 6Jp8QQQ6m27717oޜ:u*00pkD"mڴiӦM===rrrRRRaaazzz5ѣRRRWPPBb8;;gggxӧOo۶x^|>f))a##&r&&r&Ԏk6>tOQLX{@ڵ>((}TbvftfTUTH*3i3^T\ wwwn"GlR\\8^Euu&z{{9`0TUUE ;;;,+::J.YdؖSԞNVVVJIIA DpL&f *++D"lj{{{GGp, ÍNOOop*999uuu***33~-O>-++ 611ovggٳg)SG`1` Ο?~a}D M&܆999<_]]=*}Ȝ={VCCC_EEE"f9aׯ|_/FK~,%ys[%۩SFi@ NFFFffgϞ>}J`s@ ԎFOvUuuUqEjT… î h7頉@ >dׯ_xbl:::X8^~>1LL۷Pt摓آ@ 씒A\H:^YY)ݻwژCMMM]];b²zggg__k 9 3(YQQQyBMMb=422ܹn:qMdÇ-,,>yf###444wEkkkJJJhhHX3gܻw/2 %''[ZZH$ (/ʕ+w=n8Xx*ttt:::z{{, 3rم `c444$!QP(TOGIoIR[R^w^nwnD~@|F?o>۷@ihLHg83W0wUqWGjTİtvv666; u˖-p@q ǟM[[@PSSN'$ ++d֖E0aB\\\bbB T*ƟhllQRRL&èIIItss񝝝333SRR%D"5q4*) IDAT+ggg))) ++Ŕ[[[eee%t(**'HJJJxo޼;99f̴߷(d3 Hd2秦 籰&Neeekkk=<<af'''.~ss󞞞zooo<188W^^.K4gOOO"H V^388tRggg,ڵk/]~&%%˗c;sΓ'OO.G (pKF2bDaw߼y˗;CCCEEBUUՍ7J(5内ommbSSSE<F8@"*j`_јd2aQ:LP2 eeeէ ?e7x駟hq(jZA8 rrrdX}||]fX:::X,}E ^7RP(tth4mmmNNNC匒坝mи.a:K$9luY,l[}lGFp8C}Vp{!iZAU% i4Z6L#JA9}=Rym<0h4'}}}bw0;V ![z{{cbbl6x_@]RgggKKKTT!a2V8 _~S{=dUVU% yq8 :%Pt:;;"##`A h⫫C$22r;AHkL-+)MNNp DhZ[[n믃q\~9\`0{9\ ͛'O<Θ=Z?p9b^u0PvO_'Nؾ}{KKdb0,kԩӟؚ0L#B̟?qvv^n]gg !!! ?kOuo Al6/F#k C 5D"oooCʕ+_b0<󌷷ĉ.^p! C 5~kz.p^y>#d,i ֶ  ϊ[kԲZ]-N\7U&I!2A0[n-//lАJ[`rz~[ ˻===CCCAvJe~ׯWTTtuu-X`4̙c^cԩ>,B c0JnAgϞ5ͱIII e1 +,,# a&fs̙3ZڵkiiijڵkgΜ9dqxlrJhhUpokk_z{{Җʌ t%t:=88q##ذaÚ5kBQT" _,k׮OA 'sVT.VJœ2\3(r__!  Z[ f($Xa,ߺ3J0ZvϞ=s,`0 qǎj:!!!44~K.:u*Nonn pd2???OOO09 i|׮]{wddd|7k֬Ad$ADqqC@97nlڴ^8^]]OO?t+W[V?SA áCpT<\~Ȕk׮}G9_b0>LF8Ξ=+F80 kllܲe˼y㛚mwՌ Ey<X?JDŽ##& Ath(-%)%vj]%MS{s  ~4MyOyXiV"MtIL%hJd AXqwy?q۷o[,lofxx8F h4dsɤz2 ,HJJL&o۵kצL2T}p^6zoGG0lGf}W_"##Tj```DDW_} B8|rEM&6 _uP3f̐J~T* /((4i08z{{y[[[{ʕӧcVZZSOx.EZA2谛 BZm^^^dd)SF  hB`QYqJHQtEsJSpH%|AAD6uWT5 TIjsANLL9sfDDH?5.=\yyΝ>:ÈT* ӳsN2MVxxO&#A"hSxS$?<_*ygO?f΄! ߳~JSU.k7p6;EtAhUVnnnѝ555'O|BCCa=`4ɑzggUV\.dZ,rmIT>FJ1c͛/^ RQ(-[8~֭BRFq6A>|^_5M0122h-APh , AIYs.P(VX?hH$z뭷{ DP(<==׭[l6{d2x;v\pGGGϟ?_Tm(JllƍW\IK"`d>`Gd~`ܒܗ?3Lr QP׃\nNN?n:^toj[fL&3!!aѢEc9Яh>7nAffCl6?~\ dVA&9sUK$ ̜9 8/TUUL&// $&&/1 555Νkhh>>>8>}JK=X,/^,,,d2SL;wh^/,,,**RNNN3fȐJKbܹswܱZk֬9s&ɴZرcV񩨨8zhWW/Fqʕ4v˖-KLLDQt˖-W^uhkk+---))퍌1cFLLC g^xݻz'HJJB O8Q[[tzvvŋ]]]0?B`XSLyoj8tPgg'N4iҊ+Tjlll[lJJJ ?L>㄄7/Nt ϭ<-?]*.Q9efD w?qT*m6Ywuu/\ h܁lT>[aNaIxxGFKMM ZzX,OOOr'HIIqx>>L0/s8PF(~0788^0 X~ydA6qFPhL |d 6 7ްo`0RRRa1gg֮] 2)S(3$(zT*=r{=77C˖-AG-Ydʔ)a%ϙf ~WU*֭[{{{sss]]]ݳgR\xqttꍒCY,oooш HbbɓWuuu}Wh4޽v޼y g+b={b7n|뭷D"Qeeg}Ogggi Ú5k1 d_}FKNN7nؿzz/o޼uZe /rHHhܳgϗ_~ꫯSNk6ȑ#?z-OOO(ѣG ^xᅿw{ժUO=d{ 'G=d4䒔t[HQtjAʒKSũ Açj NJE"ZV*UA*MUn]AYdd'xw _3.bKR|`1(ّ݇F4 7obT@B89!$@P"##} 7:4`2vPKR ~@JaFfs}}=8J&{goNA*y<?!!!55b(:i$U`0f̘lZ###ҪT>>>*W(z'J1ͭo`OOɓ'rssY,@ 󫨨R|>(TRRBA;w\tiڴiiii )Stttiif ***ڲ( x"Nm۶F=!!!ZYt:]@@X,P(4; 8{yy-\֭[(RTr0;;;44A'OlϏNt1ES㓒\]]T*N\.W"dffvi IDATŀb800pϞ=p(6l P( #!!dz{{h4׿7ߌ][[DF2N8q7:m{K?W_}LhfPݷo?n/0  V$IVLTR(=1D''[սV'ۓMZק]]]a:>fBA%C]`00 3 +~@N Y4AXqkuo#p[KrKTX?FH$wTXX(fbwd2W\:ԛ8LjQ Qw`8Ϡ(zuىat ___0$ᄅ!RQQ1qݵZ-Xi0L7olllS HȇP(X$|>`mDeeKHH}o#""T*UGG Tj uuuD BӁB]]lNMMuwwxR}^577DR>=yyyIIId YGh###2,88ax{{XXUU,'''N2b544B777ӿ묬,fСCׯMdӁ!LZTPl6744TUU1ɓ'{yyx<ϧں䐐ZRR͛7hD~Ubxyn*4ggEb<Ǚ 1#t?d\:;;枞w hZ[ejRBII4@oG}ݍa-Y,VLLCggy *Y7b;wڵkfY(rܮ.Xp4tah0ܢV EўOTaXkkVq7obu^K:Y|||aaaO徾iZ<oV* qɀv ,X| ?~x\\͛ .DFF  bX%}q… C=KKK幻;**ʡFSll={@`;::aIvIRPA>X ð?jzΜ9]]]6mJLL4Ldw,Z﫯RX,&_ )SBCC7n8cƌ>=%%ѣ%%%_5L *NM4EW4W4U ׌Q!BQT}}}݋GC+1B! r%M@DU0+qgϖΞ&Ƨ,##|J1%FXYf%&&ZVb6|>\# ðZzJfQX, dfee8p`˖-s>qD|||bbg1L/^D` 4 + 1 zb  V0L}}}A\vC*Κ5+22r4M&Qˋ^~}Νsuww_:;;_za"=#0NPtǏOMMYRiii* šrqq1`MUUUJJJ}}+W222 {?sllX,follꫯ@+Wn߾\RQQqʕ?677իok3gμzM?++k֬Y E=X(;;M%wvS8mLe h"#W(BJ}7,~Aи2VW.p͘2͛cx "JA0 pop'vڵ144t֬Y`` b?z> xssst8pƍe˖D bE__BL傐& FO鹹MMM;vʚ3gΈqb???ryoo5(۷oӦM]]]ZPPh8(ZUUy槞z*<<V?#gg1ժݵkWOOH\>|h4DDDtvv* XLpZEEE--- Ν0lƌ7onjjZl}, Ǐ[29zh\\gs=G^~ /ؿ}qqqnnn6m",l~j kiiDIYswܑ#G^|ń#`vߓ&M͵/1sٻwSO=vܹ:D |>͚5۷oP(.\PUU%Hw _n_|Œ%KRRR7f{yy \nkk[l0얖2hvXfIhBaSٙ5ښ"EL-eL8a T?  ~z,==UUf DDQ"\F AAz>?? .6mK\.Wр 3kjj@휜0>y#GX̦0Ν;k׮F% It:]BBBRR}FÇwvvᨨvX +W̞=>[lh7nDdԩǎ۹s /=# ޽{x㍠ }Z]]}ŋ&@ kB!HMSUUE.XxD"ٱcGWW@ HJJzg *mf6zj񂂂jjj@ȱcdžYCP(pXVEV葄"h &FZ,ȶ5o3iTI*[-AA;#f{M#k5! g͞2]zCAЄ##lFq`d9ݻ6pxT 0Ll6b_t{֬YหܹsNJDMM͞={rssS8cf?ҭji4m6V% BPt:8 u*++}||$(nJ}o[[[j+@xɹtҝ;wF}֭?ӧK/%$$O9s̙3Lh_$%%%%%={BCCd2fVuww===p̙͛7h4A?P+x ȹpAy!#dLi;}{AAfѨkȮjq3BwOrIr ! 544TVVZtڞRLOOhA={gPgUUU[[}CTbF*L&H$r(JQ񆞞'O<hn7n0 P@JlNg0K_ܸqD"AjJy@#<<|`A| %+FqxUV2zȑW_}5>>,x ry]]݆ ɩ銊֯_O6 \~}Μ9dh4axyyGFFڇ@2899iQ5DwZ`ʞŪUISXkF EJ&  hH]ʞ2uڢFė*IMpN`QY# AУAնbyyyI z `cdϟh"֭[d:~~~\.]׃mǫ9Nxx8h# -Zvqq٠JJ񉊊 *:iҤAL2nrssF1$$:uիWYT*R $#Fр-A\zU 'h48bFY, hjf ,KkkkPPVS?٧w񘘘 h4~7C!1;w2eŋ+++322Znll\nz& YVq\T6~5kրf2H$$EWt R4]*NeP\  zcU2uY Agl٩T sT z|<o}}}6l~e!{H*bbN4d餤'N̞=JFDDϟ/VkSSS~~~tt4r.d2Aʣ@PVV&qq %'O.++peB0""<`NNΝ;ϟ?d2 ř3g&OLc˷\.xC!.>wwSNn3SN(@QGG+p FәL&BsΌ3p\.wl6l6kP8{lj8n6^{>جY Μ9bV_~pBooo2poooϗ_~y͚54mѢEGuV@@}͚53 VkPPSO=eb{wh4bILL\x}b+իW۷ȑ#Atlh1>///ggAoX,f=qm`DÉ'>"iii/N? l$$$^ՕNٳgϞ=;~ᇁ?KKKU*/5kC… (ЀNG|ɱc@9ٳg\ҾN sPp<OI$׿{d|k֬]\\A)^1{)+G9Eef;?AAh4vzde2U"h/ %yh*2Iu=nnn_2LATVVo裏<<<dPBARE"ğfJĝ;wFOܱu| P*4 $~P}ЂC~(TO?<;(d2zZl6^OjB?JfFN3lm6qg0cZz.`6TlEi4+3}BwIрrCWh4@AhZ''y@xc*TjrrxwzdEm  ׌Qt:;;"##AgJg!DPL&4Q!@Acb^߆ %MpN&<0J#""߱г> ȝ;wϟf/^߿_ dgg;Lr444le˖9j=}f۴iO<2- ڵk^2L;v˖-A!&> Lt5#P(T*ECAC|gF at:#Ar<TjDDĠ;ݡ3GDDׇO z8Fd2A4 ðrA?T\s ,r9933]SDS^lAAԲ>[CDI ]AЄr?oF;--?Y1._kƻ޽{{1[oٲߧR8T^{̙3xTP 1tɓloAQ6. agOAиcSٙ5ښ"EL-eL8a  ~{zURUiAw{4;%ى>A_W\ Z7oޜ4i FWW͛7WWWW(Ff# >Z[[2 jAGðybl-ZdVD4mѢE"ꪯH$fB&l6C8a2p9 @ɗp嵵...=]]]MMMT*544vpC D"Eo޼RML012 2aK@A1AL[EEYl[61S&NKr#$  &+n.Sju8٨x01Ac&@Ae˖+r+}Wx䔩ˊEyy'O&8'dJ3Y;  hp Ң\bTd ) j6¸\F>oyMMMuukaXrrrZ-&a6c߿ A}/70Mppp[[.{*j׮]6lpssZ:/_~a0ݹsA .]$f̘TUUAhg!6ͧOh+V`:Lc6 T*ڵk|>A]]]?3g@ð/rٙjdSNop@A=r;v)ͼ{tDU-(! &ESS^*QgKgOO`ZW@@qԩSZf3000 -VN3g [}G7oތrdeOP<$Iss3AS܂ |PݝVVVT*uڴi xuuڵkGiiGR :::L wرw}lqvv6~-.^811\GbZO8qz`  Mv޽  zMrYLeV!MtIL%qiA f3!͛7m4rb1a!bXݝNݽ{ɓf>x<^oo/yڴid9 0/H$Z=h1dޭA1ׯGGG^BL:U&yxx\|955uL'EQT$-]4%%͛=Vu+VJcADƤ0Sũ)F]cF[s]{݇&IK%)c[} AA È^@6*gl)Wxw GV[[O? ! /77wЗl{ppRܲe@ :uXUn߾b,a_~P7o  HTU\\@ђ>Ӆ d>, bbb⋍7bHDF}Ht:1_!4!t?Oo޼l6d1><2ʵ1a\W9l6uQ$A@4)<)\aVSuc.Bp;AнX,---X :;;!ƶ6A3AA DQ#b-t =9>%)) f f2nܸ1p1@dA1L777WW8:pjֈػ~zvv |||puM&͛7ڵks̱hPP('O޵kWSS/T,tziEy<srrtRuuٳ]\\ h$3_a0T*=h`~۷O(^z$q6l6S(`Ŗb1 Vp\TWf^ObXAXVb4Qp8L&>fgR<f;|5f`0h4t;HAAf AHD~qZf!P8f0(Z,Dvh4:,#Bp8U , x背a8CaiXz=?TfZZ-p8l6{ zzzpJvf3́weɉ` <=^ORǔp޽V ʕ{yGNlawY))l/x{a6oܸ Čp à  7p@e,S-jAx. ,D| C $H$`BܺuK$q[n%$$5"r|r>±o߾)S#(0`,Z?O>dƌd{FP.\3hq?tPYY/L6ckǏ'RQн{3PooO?rJ1R $  7ndff"󓒒:OW ɓ'gΜ 0tuuڵK(WK/$ QTǏ/** FYrss===6իFQ{l޼yT*չsΝ;999k׮>|l6[Pg}vS@OgϞRk&M*~`X,܀g AGtA,_<--d榦F^:88ˇ w0^Zx1gΜپ}믿^UUe5X,ްaCTTUUUMMMVU fggK$12e2j&i .DX3gt:!W¦3]33\3j5E"Z&Sy%ANwssCdAV?_*S51DfA2@MdJp8wɯFzf;wnqq<'Nǃ{=oҥSRRX,Vii)Xn144455Ζm(Jbbw}/-4-99OΝ;=,Af۶mK,ᨭ=ydHHHppb|X,4i \.߱c|gXls=w M0q_ 7 :СC ,ɉ\P[[r?Sݻ[n=rs=&VWW秥͛7Bܹsgewðܽ{W^ X,G9|0IIIT{lԩ 8QPPp)5m4Al6ݻzw ~ǏS(X[nuuubf۶mۑ#Grsssvܹ?yyyBOIIIIIosݻwGGGk4[ '''1q/$ 8ѭ[뜜eׯG$''gFΝ;ςݻw׿JҌ ѱ~z___ ߿m۶ݻw``4l6[]]݋/ ֲl޼hd/{wԕ6 (X7Ժujt3ڙVhmg::ӱuZkbkj"(";@ 7~oMmK{O.6n_ABU[[[fE{ 9|G:;;RSO=%˧M h4ڜ9s R111/rHH$PEQTn:]t:=%%%((HRȔ SO577P(܀I v9$O?},0qGfΜ(++kkk>4ԩS=33~WTTTkkkww?a/^/jYYYuuu׮]DDhpAX,VNNNCCCss3p(e˖`0YYY q5Œb(.Z롡 RQQ!333yuF___4Z{_p} `K.xm6ÇcbbBBB@}xxTP(DkNNǛ?EP(>>> X"#یL&E\R...EQP`9\6E @DwwlF1Lwb #D"HDv|>IGxEQۿTQ A.]Mp8}}} Q^^۱FL$ aءC[ }ͭcCK,t"<oacpe8.8QuuubbCS~)==l6o۶mڵZw}WWW陜𤆆=zE,)l޻w;::Ξ={ڵ3fX,FS\\ rw|I=A#.55{Okk·BY-~/L&Z6LT*U,!qVV+N~pgg' _Su:RD$88Z0%Gl 2Fq?88hZ T*3h*>/Hy"0O#ȁ|>! *t_^^,+0t rMOM2=5vڢ"8067ZT:>Zw4Pr(aDфB&vСGR(z* 4!ΟW3? >ÍK,t28Lηv(n#l6J%#Ht:P((ף$ A/^Zd'gW\1 "DDz{{U^~/((lL"SN=) Ozɤ$PWee%Ǜ2e o߾ 1EEESL``iw!PPRoa~J (++"ðN^mLLΝ;m6y 틎c&xWN@PhCCC---ݶmף9s߸qW_}8 h"h(-V+m3*+t .,tyz,R׮]e2YBBC|BT^rk0͞:u*(J  9q5E# XRO)((hƵZ˗z`\Qbd2?ZVySSőH$III &&8Gt 'aV/+[01ɬVkII`HKKi1IəL&N_p={}Ԟ+WΧϜ9 nqEQ|>0Ʉ ^hJ>oX1tDy楦1TVVX,wwтp?rHZZ}'**sΝqqq...ONOO5`X~gEP*?ŋ d2WVV.X`ٲexy .,\ŋk׮%zyyrFH$2Kϟ߲e˸ӧOXf+SN8p<?`O:Uѐ󩯯_d֭[M6%%%=ß~)Xx粳[Pt?<|4kw>,M2nZS[[H8ttth4k׮@GGf CggJUUU*Jd2 n޼)NEc``fIROOO:[[[juiiiZZeijjGÃdLj _s7ZZZȋO^//&٬h6߀]`]~G m}C$IX:~ .rhyRt/U g0`c2梢'Ndddq^ff`0)BhѢߛ7o a޼y3--fٺȋNrR@p@=l:P*Քv;qc$K CABx~&AAmm+ɻB7͆(b>}رcQQQ6mR*|rЃ<ð={K]1AHS֣M0rI]Vk4+V̛7DM}||/_~z'嬿tijs;hnkk۹sjݺuǡC>|0==9 f\CCC{yƍ,KMMg}dɒ+Q%֯_~駟_1 ;zw}pEرgd(J'x>Xpa```uuuaaX,!  }wwwxxhiOTPLƔsN2&*KM4TEuu4yZ$ niCC2Ba2l6x0fHSLinn6j62DAp5"Hr} i8STwuuY֞\\\/g}}}OO(yJBD&:rVZ;X[W~Ew'p.*KMys0At A}t'x.2 ,TTTX"-- A77O>bD2VkUU?;oETO=T||,&4~#۶mJ/stO?:uj͚5ykÆ k׮}+++|wߥh`ãխz IDATjlb9G2ZhA. [h@EPAh TeVQ)єic,E~u E8g0 oX,,h4jۈpĈBRJ:':3Dx<4P%/gCCb`ӃڊŕJDCiSSIQ(*PAAФYV^z&iJr׮] wHJ@D l6e\. LLL$X̟ƍ g=z4;;{իL&XNS( ը }%At:3(Hz{{, @d"W͛ Fw݈ OT*믿tR)lݻwWWW?s)2wB$''_~=!!d8qbɒ%Q]f/drcxx8Y_;.\@N^|鰰P`@ H$#a~YLryy%}%'{NNM2ˏ7ٳsJ AZwLxԈ3'r vL&9Z:|qX,H7&#===6 )wnmEEXmV#rKvM.ΧM(  h401} K{4O:d2WZ+ ZZZMF.vwtt B&wegggYYYzz9sFܤb \.P( B br򍍍nnn`&z^VۗP*t:}HJ`́N>0X|N!l6$ -Z4bĥ/Pe0R%zs pUF$̘1[ӑFZv.JU__ocȐuiZX6,*+[Ȫ+RUiUlEv(rD>wsP:RW޻Gܯ0Z&)'YSR)iӦUyݵL6卺F!DtQ"kl'{BAAi4K.;l4Jruu1 H$h4jd2!!!VAX,  |mp8AAA}'mhhhhhA]~=** D8IHH(**jllLHHR8h!ZRtwwGQð*z=JeX# Aj_V+J___'0`0\~=<<|=1LׯWPP%$a6qh4 Ll6,_ϦMZ@Nϟ?0l```۶m<LgϞ0ٳgΜH^:J% SRR@ Vkccc? Yhс~iZFFT~q(F ##5&cdi).;!  N<ϡӭrN#]N8vv%O777a *V! +C7nWWq3B$I A`@PT*Xc2,2 Peeeݻw߾}yyy&55 />sʒYf(ZfX ,|>_pF } hr?22ҥK4-00P޽; 22< >#Gٳg۷`}*R䵲L H>Ғ.99e\q\d2y<J 5S,O,GihhHNN:u*=rL&?"|{nxx8==}ʔ)?ëJ^@n aҥKƘٚp͛7h2::d2?)Adʕ=X@@@jj*a_~s Tcpp_k ʊTESK$dd&{v3000444仧s s6Ab W4'CtLl"uFDR:::]]]qrh Aݣ[_Q_1k$J$IpAO>ľ=^pO(AwF8~ nnn\.|dX6hѢA<==WZu7xd2.^8&&+W^}ԩ]vi4GFFUf77cǎ]pa&=̙3ܹs%ɑ#GvfSRRfϞm_9J<}޽{{{{E"lX$sssG&OnyryPPСC5ƶdɒ3g|W ''ger~a~~Aӗ{Ϗ|wN0,((hÆ cmJ)SSSAb L&sp$bv8pA>7tz||CbDEE9 5\l욁ނbMyAH"+B"0AL"tww  R$ߋVCaC Bo``ZooD& A!ø^T ¸80g``0\ ^ I@˫ nP;PKV3Ĺ)sA .L:566vM;v׿333s@ B s[l~UV&jl6E .f F< Bϊ~AT*599y'Aw6C['pSH0)nROAZ|&Mxd|Bן9sA }}}xNNX,Ol/_LfΜ 6X,'N DT*/^ϥ A(NJ{ܹ jj9s&D~gN'RRRȏFٳV5$$LBf|+++J%GC^䂂$'HRLAyW^E$<<|'r Zy%% nSTYj n_DssssssXX{ ̙3Gp;}ˇ~k_Н0LqPT:> ~Kuuu>/;x o|YFA+<"E33G$Kg̔2=;0q 9L)--p8&A`e2Fvf tu#rXjkkkzL&7@Vĸs6mZIIs|||LPkkjxzʕ&`0 C[[ZFQ4**N(yƍW!n5AoIiR:AyI Yss󫯾GmMkjjD"Q``E8jkkD"#ddd3?䓅 .[xQQmD8*++#6nnnRF8~/8g͚5<?<<|%s!讂Xf#06$MJ$jTAYlY@@XtMg'ї_~dɒXQc͚5~iRRFc0/(kCQT.O  {PAʬ::S)ӶY,i{Jug0%s8P(R>R2 W8bxg(J$ Rn" H@@ m.//Ք1=!$IR8V ={PEEEyy92 ).. !~\b!0̌NذaBǿmmm(0 v8~˗/򄄄hϝ;E?~yb9|ҥKqqa"bppfϜ9322A\QQa??j4+W` NAGLryy%}%'{NNM2ˏ {BA 2hXWa@č.O.zAMJ%;*cxx_LLL={Z>{oY:uJ(J$~*|KK˛oa7|ח3vׯO2HQuNÝ8;XE9rdhh<Ν;srr@Jf+--ݾ}{zz:F8tܹKl6^wfzjYYYDDDNNN/͛ RL7gϞU* ---!!!:uom?qhAwXTV";KU7XW*VUi|8>YI%;AA{AV CRIɞAнdggbc0ȖCCC-F]|yݺu _|1%%E q^R\/ӳΟWcccCCömbj x'T*ã.&&Av%K[.\0uTA0&iaظqF.kD"֭[SSStzTT+3ϐI-駟Ff/^l6s\ Feۥ@  Hadly]?t"KRdqiAA=VW~06LA)TY44  ~VU D":NɄ̌p8ǀ jkkGjX,vpdRZ cǎ4fIT*5==ܹs ADss믿c2QVV/{.J>Y___'rҥKqqq8Vu:]hh(`T*a~X kADSSoddQ,*?sAzzzzyyaFьFF\(b`0ZZZbbb@~O?"&$##,eKKK ?f4)`c8L&lfdq@A]Ofd kk·BY<AUv N\7U&OxO W_-++۴iǒ%KBBBȽ^YQQQTTD-z表(E%ɍ79F%ASJ4_ +WDDDEFF.NO2Aw^bFcqq͛HNfM6`յ̛76 M:GWWW~~~yy=vjuXX؟砠;|  Ŋccűm"UQEEATd(2Rd)L s'AAm"JmfIҤ(Q55 V((SSSoܸOBmsxx $&& T . EiicU*m@QN}GDR***233/\[ohӧfffq"( Ȓ6@jۺu@ @Ql6=mn#~OR׭[1qaRl6EG;`hF;8S(SUxFOqǢ(JLĈ3qwk߈fNN~#cð1~cam0G At<"E33?v8]:}L)C:ٳ  [++5jAO41Qȣ&{jAc'O7ᠾ0QD"uԩ;v8}XV__֋-CBB.]d+\!RVV7pGDD6Jfff~BDFFݻg}Na؍7rss,k֬YOLL,..Wj655EEE (B҂~w`ND5kH$BHRlf@GG^l|bFŅdڿ8nXt:]gg'qssckh4T B d2TN>!^q7ݽRݝ9C L`WWWUcppfD"&4CU; \]]+ld<AN*X-,b0Bdj0Fh4zzzzxxx

aAR555) ///@0y޼ys… A=IDw?uvd"UՙaD"+T:ٳ  h,&F]#"(K5C6Ó9S 1Abl6;"" 0&P(j}ό.8l _zbRϯ:11y{{b  E˗//_fڏonn6m-e0׮] 48 RV7y#Xt]]]O<Vc1 Sշpt:޽{BO> Ahڢ]vEGG[@ۦ&A^cRj=}ѣGB!kjj^l{mmm\>444<} n޼k.V`0z衘Es_X,HP((h,))+׮]t)JE>ð?O .t|[o5{l rԩW_6qƩSbvcǎ|69eʔGybv 2UUUӧO'A,W޽ť ^x!::zbww˗/[,oܾ}{[[[DDDKK a+WLII!RJ'!?~祗^ ' [nU*SNuMpu$st =I$Mjj:snv֋.OO$)0, AAwUE7)qL4i`*-$!~1Ac+**JeVVG'NdddE芊3f0 PXXX\\l2?3UV??# 'غu֭[ܴiӪUn;wvpޭ[گ"d2}||v? BRoߞ1 P*ڇ~j={,]b0ϟ\nLĻ~D􀀀APxKK˧~uO,0qG\\.76رciZmFF}^l;wndd$x0}ѣG===ѳg^xqʕ3fٹsÇ~a0bl A3g?~dggcdW^y,u…C̜9Aɴk.>eL O8fSSSCjݻ#"".\&8p ekVWWo߾{L{ޙ3g pKLLLLL BT*꫈82Pd_Eðwy'22}: HrrriS HBBBqqR?DdDEE?~̷))) HJJJMMMgg'ٓoƌ`Aŋoٲ%,, Ν?'P(.\t҈l6>>g}Y"bèT*pnyad"Bt2b>f1Lp/f1 r&cLryy%}%'{NNM2ӟ?ٳ fV55!fs]sSd) bAL*.^x֬YF.% *DM6i46b|P(ܾ};jxb4*exYI,AA6鯕_^P*U"A;m APb ֲPuwwwuu_߀+?&ԉ"jq}LA:U(:e o"C@(aEq 9\=oPij+@N. B$-?Bw9|zիl64 n?X,F2744x{{lB Ü_ .]dXHV $_ A{^rEӹQRR+J- Nx c"$++g"ߟRrroOOO`0Z-(Y('rјL3g,X===#bX֡;vP>}Ç/_.1 ;{,Nwuu/^tt֭[m6h@w:tߗH$ׯ_/U* >|ĉ[lOy^[[V-[z5kfgg;eƍsνիA Hadly]?t"KRdqiAA-6v:AyI7AFqx*(( /_3gNjj&d2ݻݾ4닊yķ:p@aaagg 5k>g}6''dnذA*{<`7 /!ٳh4>32e;[o{u̼_|E2gARI6wٲeW_}EX3g&@{Z2#TSzF}ޓ\\wd K6C .,4YZ4YƔM ; _}иL氕(:::$$dĻPq06`cŋASn6FBLԩS++++**h4Zzz4aXSSӗ_~!bX@S`VÇ@o1V{zzz?K/q\G;vlѢEcpv(7zDj?mQ\\<44b@Dk ;wvvvv"nX*++wCEGG;dLJJgppP&2 Ϟ=[RR⥥mmmr`x뭷 8Bdd$nhh` fY,-[ ;9uww XSS]^^k?/^ؾDc@@9s@2 00?KAuvɮ)-(+w!DȊFAt0՗_l#ll*;I$Iը &͎H *zp8w~=019pWTzg.6I39 uuuK.1cFm&$$СC#_t:AΕ, J!Lh4T*Gg޼y_~ecc. \\\&X ~AAA۷oΝSRR,XPk\7GI 68Od2;v`ݺuSNȈ8N^^^^^f_7PbԴaÆ>lƍ6m}V6lذm۶XꊊLR(믿o_r8779NTGp'oӄ ^ECiXqlHUT_ѠkP0Y ,AAQjJEAH$)Fàڂ Gt7IxSSӾ}bիA rAGAAAooO< >_Ff~*++{_s) 8;bhh0G烢Xc Kl6???t/BYYxW{KKKLL}#_dggϞ=9333007 HNN ׯ_߶m[FFƜ9szΝ;U*Ն &5dlT*uٲe/_xL&c'Oloo7ApE.oڴ?޳gڵkA&&&>hiiIMM}:DMWTT\v-**j.\hoow:u`ZbA>>(LJ2]/\|r alnhh@QOtvv:99yzz_AbbbjjjL&a}}}Y lnn^jJZ---&I"I,+))I.H$\[[lٲynZ,kr\C@@Jd2$b(~;{ wd6p344B7jjj駟&<+W D6ǏŁPa/lD ql6~?lM. XB*J&""(fAH$qNv(PTOdH )CJ.J\kx.; n/jFUSHXWWyfD}Ą4!3CT:D8m_dt.TTT&"סnnn---s,}D.fJU__̙_|1<<~UJrfffzz>lBB-#_ܹ?|Μ9Flߩ_Wvtј{Bڛ۠(::؈袍׸TV?[8Vxv lqb,qγ+MCT*WwLuBue&X^4WadddddX|>৆:tM())HOO_>.\033`0***zzz,[,%%ݝRחiڰtz⾾>n;ܹs:*龾={˾-;;;$$ľ &TV;]l-;dhh?00P(ξ]gΜ  N>7;nP( zw={ |,#G\xqzmnmmɉrXjeeeIIKTTwo/'N,LQJJz 4222::`0nȄn mmm-B GmL)Q1#rB|i7. -PZG 3ϟxh__߻KT=`1 ~~~~~Hp\ pb4-zN2̅zCc6K NCA`DL%3cfK 5SDmUܙnʤUՖ)F >,~B/MYhR8[YWW_QkW&+|d\T]Ba0 `2W:n'Oȴ IDATk/;w:<\sQ9͋uUc c]cyLmf:ӤidD"sK'^eZTf=Z^^k׮w<;D8 44ݲ7ma`! ֭[wWoTjūz4xs8SYM3gdJL)gg7tcC5JƸHpN8;hb/`\U' d'0ɓqqq"NZ&)!!%psy` ^F7'a ((%565__$/U#E): &rF ujeҤDć*Lwg Y\hDR433Fݨ\n``E8vJPFFF~Lݭҏ>hӦM۶mΥbVRR{ :V;;aZ!py&ʊ'<:r4-:[jeuAWk[^ UH.,( sر)z7&νp~^zݝD"MNNT*555EQk1LZ}Kq\]ۧR7n-Xb ۀ e|I~pH^T,/pepB{uDuNw+ʛ4M 3|' $yN-)o~ݻw+V rJܞݻw'%%Q(m6ooC-:Q6pemR|/X$/j46O6{҄iRJ.FfFd*Yz<"/(XsUWW#% RQ!!!/ ^f kz}iiiMM W,Z599Y[[oJf(===}ҥET*B-`vᆆH$Jcbbl/MMM7Uǎ{lGL&ÇlقaXWWlB8k4& hbկ~bV+}ҨTjttSOBDz'Ddzzk֬aXE |'8 ;v7$U@@F&no 2#U"H(5>,LQJJJZ]eJYFkђPRs'qh47pT*Je0W;Cvvvxx-cٲe8?>99yx\fG8qa?A( HMM%a䴴2"xoo]>[W^h4Znnnpp0DBJz{{Ʀ8PS__rJbar|zz:$$ð3g|Ī<==u:]yyy\\/q7h4˝9@K!NPDTAj(ثp;ј5*E $WOP{*X+Vݻo)((DWp8 EdFh4H aDHD!;===Gt:"L&ܹqSV[, 233P(lM灢(ñ/4mqqqL&3;;D`0TVVfddPT4<<@a E" b4V^mMAQr8nY*EUDc'ѓːe<ocbˌ'eJYT+cjT^߮xݷlz{aaarjԜ9sj/m߾=&&EQm_튠h\]]'ATRtqqJNNP(!!!---MMM*466 Nڷo``0333eee Vu``ŋ>\ æ% 1`||D89SYMC'M??K͍UըΩ,: J K(n5\VqqqYYrOLL8Ntq'[D8dQPJkdF<<jlӻGDRR,2k`1MUMBn#"aHyI$gb/ m@RqܖKaEQggggggP駟נ===(fBBB|M'QYYo_g bxc62$^Qꫯ0 kiiykė #77wR|ĝ!rY E$ ]JM#8f::+|5L&sDQ@oooYNNNqqq|IPPz|>?77?|7tj^lj' h4gϞR֭[~ K-¡VpټzyZ֠(:g(/q8^ R|/X$/j46O6{҄iRJ `7רjd*¨@ćON^;^y2bZsrrFG}GĈgy.F[n믿NRF#J}L 66;C(sssRԯP(|W~߈D"Vb F}uP՟pBIIIw}6 EQ*j5D l6{ڵT*g0&K[t:}ժU~-b6lpС~Z(jHL.H~Ge0z>..j9Pl&NW]]-H̢ 0&<<|sCjfqVGPtO,qd"~T*F>MB&TC q]ٌ84F9|ĘL&DR.d"wd:~1jH$aad}H,N!&Rd2&#70m7vj #jQT:n?V8>oa2 td27222::6CIƉrEyDުw8' 24כN7Vc[cw9HTrL^,Hoooooohh?\n .lٲexxx!}|dڎ8>55jf`prrb\.#( &`08訛599IhH$6- /h4NLL  ØL&ϟ]jjj k3r|zzbt755pLLL[桡!}i È5aoooLNN̐dggg@ej6GGG%v+jZӱl:bfXT*N3l6@e0/VR3dHNN&၎˗u]ARYYYYZZ:<qDgg'B QTeeeDתUx<3c6;::ZZZJ%Ŷ*Fc+srrRRR\]wk:}tuuwoJ Seee<GAh4 ? prrJKK tLRx≘^_QQޞ:00w_Nק_^$Y,Z2}&&& ϝ;766fZ7oO]FceeeeeŋᅠW0XtFkԪkό)+<;~vwE(˟ثppꐩdu: bZP 뛛[__O<;.Kԛ? wooX, (6ml6a鞞88~'r#Q $G{yyٿ)Pp ɴo BR-[06'1vE.k3Rp8O -bE8ne˖1Bqᦦ&^o7NOOccc?F9vgffRSSqԩS=l>t7li?z/_W_555=jCCCIII@@}_j4x3۷Oܹ׷믿{lDccc[ozCT|q믿f0O<IJel333G-))xD*wߝmQVV`07==m0G콽pL?O:p@||k29{ر#66v8'H${{||`A?>22RPP433s}رqXf 3R)"yQ^]e肞F u*eʤBԍB)+`g7| ]!`˽kP{{qwwF S?@̿Ϋ EK.!b26lpq^Op8<Xe˖oDCT655_Iꠠ "633sw!hۓIHHHHH]%22rÆ %%%)))TVV(~˗#m۶Ǐ766g:uJ,[͍S755uuuEDDcjW_}i&4rq[[a際ccc/\īDO􌌌$bH'O߰ayǟ{YrCz-ۗ;vx.]G6l`?{drǡfWIIIdd$wAC8!!qxDIb諒 R3EPnMz^]_ £V V_G~"|>p\? O"a]Am$899|Vk4t tzHHHKKKww7KG B$˖-QfdZXdGG  9qQ6jzzztt488o㫪gϞuwwOKKQ`Drp/..vww RDkڪ4[`U֢P($h4.<1F[|9Yd$drrd2G8VkGGGrr2|:KoڲFJQUT3'Ob1j:c j===ݡjX, bpp0??!tLqsgל?+?+SdJ??K͍bXFfFd*Yj2"h0'XʓFFI+ p0fjZG 2{L" AO>jcbb+++Lq\R555|&4Fr'''DbP0L3^&hIIIMM^ JIIv(4ӧOD,499аzj(Ζ-[>裡tAOO4nh48q"((>T:;;+**:;;ׯ_BJT!XX~}VUw.L SYk5pU&͓uje~A7[0-IħAEB=p̙3gϞʲE8p0a$$ ė(3>7MXX؎;222f'O`666_"zj"bADJlbXfff1$iv ARP(;//WRRgϞ{'66v:FM*J$+La_Oh4'|r۶m"h!^nU>{O?kW_}*_Wwh49}>,G|䱩tdc]s9bxWKO˔V"RR4K " qV?477o޼h>-a0JJJ=VWW9rð4v7pm۶ B81Qx9ǐH$[ FP\t%+aaa|I}}f'q9vM IDATFEEyzz8~ҥZڵkzzDdӦMWfq\۷^ r駟~V\\o߾= sUpP9kKTuO++LQfKQ ѯ)e5EKBIAAR45JTo@`&˿{J.4{w^ף( ㅅ2HLLDdժU28l6֭/{fq'0t: BdZ-F#(SSS999 Q#y3<< X,ϜPTwڹs W_oٲe! &&&>WWrLh4Zttttt޽{;uV[d[s`N"K|E"yQOg(=@#{pӘ5*E $WOP9$@߿Jرcv`hZhOԕ"jLi4P"AJ]]] B(ZӧO:u*777//֚lZu:ål-@8bŲ1jzrr~g׷jfffP[[m9(R??4" z']Us|| ۵kJ"":NV م9ƉrEyDG$ 24U0cfR:Պ)UJʛKp# rp\"h*@3g{ϟ[{{{___ppK khTЈX,tBmeL&\nZm ]]]^SSS' R)g||>bߎbllN)) 188rڞrP#m7ɴgϞUVu] ZV[Ѱ`HptFkԪkό)+<;~vwE(˟ A|V]k((%.KQd|>>ӳgNMM~w-<kwJ3gxAО˗`0GQZS$k6I$f2s~QԠe˖p$+ ZM8rBX2lttbP(AVkKKX<_q'''gg޴q///^^^.]R<XQd^U簾&1gxqDPl FD\pZf ]E*HMtLuɋ zO(3t@mRרj A|X> Δ}rĉǏ67>>o;wrCV/wA՘ HB!Lt:ͭ=,, AtbUVV:u*>>d444tvvx<ŢL&B DDPD*~煅ׯwrr Ɗ+l_XXGP ʼn'/_H puumhhDy{ϗ.Y,>sڐ`6==b~K%''WTTm۶]]]CCC\\.h4!!!}***)n F8f73ۧ8~jW޺u+Yv]wh?j8fnݚlf⋏>ȴpwwGR) 333Vѣ%%%۶m#?^|E133t=m۶8pĉ `0[n͚5Mi4ځfff, ؾ}{RRuL}6^sLd6m̘Df6\.wΝꩧB^Ynƍ:vl6֖;|Glo߾>?00@&zTbaaaP "fxmY뾶ZY}zc'NrŹL^l8h{dJYƈ$jk/ 焓PwVkZ`ZGFFߣR~,JJ$ׯ5罹v<..T\\\z-H4m9s楗^f$,:Z"\U{[Gii[o|( iAijZUU|r`4 'OY,''y~h42tKY,RI&\ e8 H!8>11`0l{|7pux[hoo'ɉm`dddtt4,,߸(0kj=9vGۃ ??K͍M=`0^d ը<eM[[ 7o:X;sbyPݱcΝ;gw6<;cUv\PPVkoo|o[LuuuF EQ^~ڡUwy*0`hoo?r8݃EQsU%lÇբWQJPqMPyXxsEg3 B~ $ٯ/ժj҅Ta*̺p[͚f[5*Wk[^ ED1QX8^2::ꫯ '33ˋ니 6S\M|G}a3ԩSWՕ"CCC{TjTTTNN7OM. j\9 pE>,G|䱩tdc]n ^uNwTu 3ըR`J X,4%%%]חBLo&??ׁUVWS,**&Hll&>sLxx@ ՛P(999JAA= N첲~JTpUZJ[.99CV_$$88E{ árֺ͗שN.WW(*9[gЌ͸yWA#(RaZY]2OK'䭤=\'NTVV766d2GTOMMph4+W?//\FG䘘~wvv9d]xxx/wǗ/_0swwP(tqqz $-a|n JRbQxaꂘ!N Ritc{,,SwXz^]_&Z UUD]KpHMMrrrrss qdrale8/h4D"YzuaaP( 94%.V\\_ڡDɓ'ӗLn LQV>Q~pI a+uW1Cw>.Sk[q+̔RQLd"" NGQ411Q"d29**/xpT*ggg \.WV[VGEEŧ~裏؟x]]ݽ %Z%肍׸Uמ?[8VxfL7*K_n!_B l ʣoa#3#2JQ5mF4,Ic\cn3w7޽kڵ~~~󗖚 EQ&i_l6 Qd䔔 BZ - Bq8x;wFFF:j477;du"*HMtLuɋ zO(3OF^gz6 rL'uA $M$H4w7|v cӦMqqqW> V+B#J]hbV199rmM&￯^x__1##z  "AC8!!qxDIb諒J*Cq;nln U1*H^_ Ã%''=z&&& QK*??@EQ[@ ZA &''Y,0/P(|'<<dXXh-y& hT՞fl6_m}||XSZZ:gxAZZZJ"{MB&t:Fsx~qł'jŁ) Ju8n6m(쀭m LsfٖA&)ʜbX0 #rT(h4ff WS0 #C"p7LS/BAT5#Ľ\b8~ھ};D8j|X>>cSDiDCǎǻgn:U8h{dJY`5PPJ8'\ʗFq(<p=~qOOg}vٲeīg}sNv}榦&[ߋ7ٳgbbAdSfuq  /H$bd2I$?;]t:}׮]\.,66JTp͖ZCT3j={r([n֭:l6OLLxxAp7 }}}eŊnnn1 ommP(~Kh4-[,..ݖfXT*UKKKss\.wuuMII3.\nnnAAAmZvzz711wvh4vvv?jUUUB022NֶL&{K"V^`2z{{{{{###&&&&J\.ammm2N'$$p8-[[[[ZZzzz(J||T*0L&i49+VWP9kKTuO++LQfKj1ȡ2jU  >,~B/)kfeeϼh4'''"A^zl65X,ǃlk<󌳳_RR1F18h:nf}Yp),,|ǡDlE8pb "a>/AOOg}/Lr Cee#G.^vZ"¡j744$$$[NV8q %׿hnnl>ydssm<<d28p,)))))㭷ܺu+D"x>,##c֭Dl69r>>>>99s߾}鹹qVo߾ጌ @pܹ?|ڵRbccmd2q曺?00ND@"ѱrJJuᒒ{yy1 ___.klXݽ{n6?33W_X{,h&''m600pرG}1Ϟ=$)44tvj=Pk-R|/X$/j4^ fӄiT \Mr2cGp.)L${2=obbbjjjǯxۗ(XeBHϰ8!Fװ:~ڜ0 #r\Qz}ggg__קT*{zb ޼y͛mUVXptӧOs{| /O<\===ׯG$%%E 444"ꫯX,".#kjjW\ HMMMoooAAARR bJ$),,4 > Q+**555bߟc0S("n(ʞ-[ۢ9. ᕕB_W9N\\˗/_r+  ܺu iii?<wٱͽ~wtt x”[ш5˃/5i=R`(8v,Y-`$Ik^xᡇjkks 2,:.00pw#MH8FdH$Mje:BbccCV4m0bcc|>M\ neÀl6h4Z֠ GB000# F,{uuuvHw٤%`U7nEN>RܛjӣT*)lb[)S$MMM'O\~`Ǎ7]J—LLZ]^`(brt9\>S;sj:8W@oּyxk:-}?w1^.?~q3hPv IDAThR/7 9ڜաofVr}:ՄZ0|>_R4=5|>5 ǨiҥKǏ B_(vwws/ϝ;o>\SSS%''xzyHEH$-8l6L&\ZfYO$UVV|7o^z/***::כyR9wy''''**v/^UpLHP&Z[;~kwOlMv6$lv !#4Kf3^g}S"d4Be*z*;9'?afZ2"?Mx 6@1:N˗;}tn#0 xd øyu}/RggJZrEӳgϞ+Vpf3EQrɝg10|`X&L}v^j*.JMgΜ;vlHW ϝ;l޼ݻ Ì3{s9 .رcڵe4,򤤤!g8q"==%VZtíhbtSy5b6[ .zypԩSsqdi*,.(vtSRgża} v/7n܊+Bǿ;Κ+Wdee]~իIIIcƌq_0Lgg~h0֬YmP(din EQD"hʕ+Vຆ^W_=qD@@Ы_.E"-˲%%%aaaۻϞ=k6333FcSS˗U<VXXgϞ 6joo?xڵkj N7~R4W;S;J8':Wꗊ>%O>]e*0 ~Ϛ>;=˰B^g'NBt]&;KkIH8n Wjx%%%׿-Z$>w}wSLvi͛|L&~Q;pbn݆\.gYn{Sqyh={Y\MbϞ=[]]5`SC0F1===5Zm"""(ѱt5G]vm˖-"Nޛ"ut/z\K7pN똭>?}3W?wZ4_n<1? --1[uuonr8Yk.,-K3ՙLTb4?㺺k! iv:k,z{{ !r3gĸV~d|xc2 W_ܹs̘1*("99[b?tqfi~xT P"xœO>\GgWq4?2UdIg xSGևѶ_h7r-!5ےe?m),ݖmU,aeսǵpn+]071&v[Λ;|x~_E!+U^8=rt3;~[( R {Rj-..xЖ=a„x@1rP:Mӕb8>>P(x<^OO0 tww|nѣGe2ٳLRiHHHoo/:T* !!!!"`0XVW*)))gϞmnn7ne˗/ CkkL&wOkkkRRGE fH$VPQQQ__#7!O>O>ڵkw^nS7JTgr}.t]w7c3o;db((H}pDʗJ󦢧:}uw8\.KJ6jX1BȉM&-'B\K!f|ii>=v^Qzy&!D#ϟU'ONVOTeyD4bEE^~@oxy3T-Yxa'aPa [[K֑ջߒ%~B?_ċ>J;K \1]Y!oݣ ۨh4gMj?%˦ie3c|bF\__WvOD"ygV\Ѻoz<} %$#J Xg R̙3QQQ!!!k׮UTT;VRB4eeeYYYxx8ϧi"666 avXP(B!<<##cǎGYpH$r8~)!ՍiӦx`Y#IOOrT*庆|'zޣЪBCCE"WD(z[ee˖!VOLOL/]ikk\tp҈5{n+K; ݫ3q;Rx`_Ѹ:[fEQZV1F  UƒX,NHHHHHb\.OMM8*jʔ)3D@BUBa||<סaG$d2>VpbK\UUtRW_BH8a;ck μ}}C!eGӴfۿ㽹iu۶mԩSJ%0?|XXXjj*xqLL /#Y{g=ԶX,MMMX0,-1113f(--qóFwz024MܹՁLHHX|<;!N]m6<κ.\1(?ߓ}*YW`)~)Fy(U#Uh7Ne+e+(5)rr>nۢ*:7zo༧:NꟚH}9x //;w|TEʗ~72cx+0O壘ܳ;ni0Y_ݰ;A1bkFW< ! m &I&Nne aNhu'aaaK,9vX\\lNs"HrrrN:^(r4L6܇C&mذaĉǏ7G菢츸Q G,`^'l9z.jww 詰kK,,' &Jcg99?󣒎S5Sd3d2d+_Od3!t;v 츹'Nww3T-]:~2S˕%= bv'z-޻b螫+ OǸ7l?9LLDW]. 2U"7wrս/.yut:|3$De"7k& p[s["~n^M^ G+Ͷsϳ,[__jM&l6/~׽FEEEaa]Il6;STT$ F /^^}}@GIKKs!DQTNNΎ;֯_%E !j8pࡇB*aBX,NMMe9&#*{*_.#;GZo~㻏?YO\%S|WCO>Y:K0ha@ˁ&kS:S@EA0()w5</ϟ"KvÑ{gŰk_;Q51Sigo][3KLQGgeK|@/ysyYY%KF%BAIMIMx0+!B:teB`M\ x/-j=C0\q[[͛l6[EEEQ\ 0LSSGVj0N'a_W-O~"v+h]4TWW7|\b !^n^]]]c>Ayr-** _w5>u'w椤q}_Y_o"mRRҝ$&&3Ǯ]î\vK˭[.\0##[aYDEY,~gq~?{W|ڵk֬:uRVc:t/|熾GCmٲeرĆ nݚھw/ܹsW X۱gϞիW3_neZ]s7FFWWWr/E"X,v/94ǵp풒RVVwծ]]]>>>P)nRZqqq7nܠicÇggg{,[Tuoڵk? 3gT*=GWXlXX(>hjj PqF???BT*Z xZR=b ^pf{W/_Z, U*UCp;Zs=W[[{СO?tڴiK,Q/V+M 22ߟ]+-jlpL&???|>_*JRBSO=裏Κ5˕pz)F3Ax?`C!eN'0<oAXl?}a(0 ?C"|.Ly hӟd0O׷~@빻d~ʕ+Y=yף kwg2~eeeM8fǎ=N|b駟8ek׮ n GWW׳>1q\xq֭\;teKKKW\U#s%MMMŲ{bN7Ν0L___KKH$ 6ccccccr<˲͛UUU ...((#u8)(((<<\*aVkkkkMMM{{oBBBPP{t:*** C```LLGa+**#""ÕJe;l6UUUI҉'reYVWW'˹_uuu\nJmmmzl6WWWwwwtHD²jzD"VT"h9fqFmm-KJJ'we/]d6<.鬨@_衇*++tRAAP(LJJz]M86nܸk׮AAA&LXlj-//w˛9sh"""^~刈ޥKq `[ 1ZUUUUU5ĘÇ?[l2YUU{˗?sȑ?\&={7nӦMq;O?tҤI\HKipR4<<|߾}%???''[fVTT:ujk׮={6wS钒$22̙34M\r„ c޽G QՇjVuoَ?K.'''D"NaY?޳gdfffQQю;۷n:y䞞+Wp ]_SN_oVTTz/=!n޽СCz>>>͛ j5!d2oS(_3Xh<*44KFܟr9w:u(KRl۹T\q*]l6>/E"0cժUb8(((&&nx<\>X 111**Q"?rrrNp8!"H*o߾{--_}U}}}xx8!ڵkJHHh4GE=sܖ۷߿ӦM駟Z,m۶iZBHuu|_^J`ڴik׮}Ǐ%&&Bx<޲e˂\Y|ܹбcB><} xnL[[ۉ'&M_*44(,YdVH&e_TTf) R=fD*Z5\N^Ș5kB'%%%%%7LJrԩ_'HKKs]??H8n82Mv}w)JGDD~_VZͲlWW'|RWWaÆqƹekl6>%eY@  &燅qjڴ!>QIIIPPPxxcXnܸ1{l!Nsǎ.]ϟ[TP(h?l6 Bלu:Goxa\-C0˗u:]pp׊syVћٳׯ_߸q̙3%H$Z`>xĉJob~G?7[ZZzzz r[ 1 sСcǎ-X`ܹavW h wF-kooPyyGrڵwKFFFOOOWW70 G9sGMMMf,4ͽn ˲</..˲,˺s/|>w@,/[cGTL& z{{ CttZ&DDD0 t:]KyOѣGy<^llk>IIIW\xbJJ EQNСC25&11QV nl.//" {$7nܮ] P(了8qBR k9իW `_ R)gO?]tV%455IRRz^ii/~˗/7!' "'@QP( KQ^^>,&&&11Q(yڵ_v:ǎ 7o7ϟ?O~QWWWTT0k,HGijj:p)|||N---Gmhh7oرc] ݻT*չsf]e(ZfooL<RPP0e"cQhT(&ED"E$d2TWWطoߔ)SVkIIICCc=`*..zcFDDP׿}񝝝wNKK4i׮pر(QuؿYYj۷o'YF 6W*zTM ZV$1G?NJJ=1ٽpP5ZSM82'&&Zna]<$$t҅ +W0?|rQQQII?>vXXLSL)++8˲YyƌSTTt̙͛7O^ ]6...33[h˗/9seم 7#@$%% W.w "///((}+V?xٲe'O"P*g޼y(.s x7nػwŋ}}}.\Q]SS{キqFNc,[RRzv%r|T(x}`:!"hĉ'N[%**jrR\TpryVVVVV`<$&&6X*od z{b8b%$$J>=gX,:::y睸˗{zUVV+̝;O7okjj> 6t:nK{{S__cytiiifdd6c˗ V#$%%Ž.=p㣏>[~Vu:]]]vۣu_KLLJ4M/)((XpЅǏB", 39" /p_\%eowԠ \='h4r\Tݻs:::JG+Bht%(&iΝWVTb455W[Bwv%EQ^wODV{J$}}} p/?.X ,,lYDp!ʕ+[&$$<QQQ] C.|JnM@ o~5P*al6pM6tzo߾G}ԽA:x d2ٺu-ZtW_}wڵ2E.;N>hX\oMT*LӴGz뭧zJVq.ժ뇻pM"H$Ϝ9… ֭[cccGv4kkk c[ZRUU^᪯G,SUPPpڵ^$\r駟Jjhh(EQ0$px<NOi pXr3_^UUW^ǽlٲk׮nٲE86m7nܼy^zU??g<#_ҵ3<-C=Tĉ۶mC*02H8z. @$?Vu$IppN"p\ɓ'xH8&AIDAT>H8>L& O{NY3p O`Xb1 L&>} PuwgAQP(#S: }p }p }p }p }wz00( ,01v$Rwz ^˲wz_,bӳJR} 8}p }pehIENDB`nipy-heudiconv-217744b/heudiconv/000077500000000000000000000000001517415366200167255ustar00rootroot00000000000000nipy-heudiconv-217744b/heudiconv/__init__.py000066400000000000000000000003511517415366200210350ustar00rootroot00000000000000import logging from ._version import __version__ from .info import __packagename__ __all__ = ["__packagename__", "__version__"] lgr = logging.getLogger(__name__) lgr.debug("Starting the abomination") # just to "run-test" logging nipy-heudiconv-217744b/heudiconv/bids.py000066400000000000000000001263541517415366200202330ustar00rootroot00000000000000"""Handle BIDS specific operations""" from __future__ import annotations __docformat__ = "numpy" from collections import OrderedDict import csv import errno from glob import glob import hashlib import logging import os import os.path as op from pathlib import Path import re from typing import Any, Optional import warnings import numpy as np import pydicom as dcm from . import __version__, dicoms from .parser import find_files from .utils import ( create_file_if_missing, is_readonly, json_dumps, load_json, remove_prefix, remove_suffix, save_json, set_readonly, strptime_bids, update_json, ) lgr = logging.getLogger(__name__) # Fields to be populated in _scans files. Order matters SCANS_FILE_FIELDS = OrderedDict( [ ("filename", OrderedDict([("Description", "Name of the nifti file")])), ( "acq_time", OrderedDict( [ ("LongName", "Acquisition time"), ("Description", "Acquisition time of the particular scan"), ] ), ), ("operator", OrderedDict([("Description", "Name of the operator")])), ( "randstr", OrderedDict( [("LongName", "Random string"), ("Description", "md5 hash of UIDs")] ), ), ] ) #: JSON Key where we will embed our version in the newly produced .json files HEUDICONV_VERSION_JSON_KEY = "HeudiconvVersion" class BIDSError(Exception): pass BIDS_VERSION = "1.8.0" # List defining allowed parameter matching for fmap assignment: SHIM_KEY = "ShimSetting" AllowedFmapParameterMatching = [ "Shims", "ImagingVolume", "ModalityAcquisitionLabel", "CustomAcquisitionLabel", "PlainAcquisitionLabel", "Force", ] # Key info returned by get_key_info_for_fmap_assignment when # matching_parameter = "Force" KeyInfoForForce = "Forced" # List defining allowed criteria to assign a given fmap to a non-fmap run # among the different fmaps with matching parameters: AllowedCriteriaForFmapAssignment = [ "First", "Closest", ] def maybe_na(val: Any) -> str: """Return 'n/a' if non-None value represented as str is not empty Primarily for the consistent use of lower case 'n/a' so 'N/A' and 'NA' are also treated as 'n/a' """ if val is not None: valstr = str(val).strip() return "n/a" if (not valstr or valstr in ("N/A", "NA")) else valstr else: return "n/a" def treat_age(age: str | float | None) -> str | None: """Age might encounter 'Y' suffix or be a float""" if age is None: return None # might be converted to N/A by maybe_na agestr = str(age) if agestr.endswith("M"): agestr = agestr.rstrip("M") ageflt = float(agestr) / 12 agestr = ("%.2f" if ageflt != int(ageflt) else "%d") % ageflt else: agestr = agestr.rstrip("Y") if agestr: # strip all leading 0s but allow to scan a newborn (age 0Y) agestr = "0" if not agestr.lstrip("0") else agestr.lstrip("0") if agestr.startswith("."): # we had float point value, let's prepend 0 agestr = "0" + agestr return agestr def populate_bids_templates( path: str, defaults: Optional[dict[str, Any]] = None ) -> None: """Premake BIDS text files with templates""" lgr.info("Populating template files under %s", path) descriptor = op.join(path, "dataset_description.json") if defaults is None: defaults = {} if not op.lexists(descriptor): save_json( descriptor, OrderedDict( [ ("Name", "TODO: name of the dataset"), ("BIDSVersion", BIDS_VERSION), ( "License", defaults.get( "License", "TODO: choose a license, e.g. PDDL " "(http://opendatacommons.org/licenses/pddl/)", ), ), ( "Authors", defaults.get( "Authors", ["TODO:", "First1 Last1", "First2 Last2", "..."] ), ), ( "Acknowledgements", defaults.get( "Acknowledgements", "TODO: whom you want to acknowledge" ), ), ( "HowToAcknowledge", "TODO: describe how to acknowledge -- either cite a " "corresponding paper, or just in acknowledgement " "section", ), ("Funding", ["TODO", "GRANT #1", "GRANT #2"]), ("ReferencesAndLinks", ["TODO", "List of papers or websites"]), ("DatasetDOI", "TODO: eventually a DOI for the dataset"), ] ), ) sourcedata_README = op.join(path, "sourcedata", "README") if op.exists(op.dirname(sourcedata_README)): create_file_if_missing( sourcedata_README, "TODO: Provide description about source data, e.g. \n" "Directory below contains DICOMS compressed into tarballs per " "each sequence, replicating directory hierarchy of the BIDS dataset" " itself.", # TODO: get from schema glob_suffixes=[".md", ".txt", ".rst", ""], ) create_file_if_missing( op.join(path, "CHANGES"), "0.0.1 Initial data acquired\n" "TODOs:\n\t- verify and possibly extend information in participants.tsv" " (see for example http://datasets.datalad.org/?dir=/openfmri/ds000208)" "\n\t- fill out dataset_description.json, README, sourcedata/README" " (if present)\n\t- provide _events.tsv file for each _bold.nii.gz with" " onsets of events (see '8.5 Task events' of BIDS specification)", ) create_file_if_missing( op.join(path, "README"), "TODO: Provide description for the dataset -- basic details about the " "study, possibly pointing to pre-registration (if public or embargoed)", # TODO: get from schema glob_suffixes=[".md", ".txt", ".rst", ""], ) create_file_if_missing( op.join(path, "scans.json"), json_dumps(SCANS_FILE_FIELDS, sort_keys=False) ) create_file_if_missing(op.join(path, ".bidsignore"), ".duecredit.p") if op.lexists(op.join(path, ".git")): create_file_if_missing(op.join(path, ".gitignore"), ".duecredit.p") populate_aggregated_jsons(path) def populate_aggregated_jsons(path: str) -> None: """Aggregate across the entire BIDS dataset ``.json``\\s into top level ``.json``\\s Top level .json files would contain only the fields which are common to all ``subject[/session]/type/*_modality.json``\\s. ATM aggregating only for ``*_task*_bold.json`` files. Only the task- and OPTIONAL _acq- field is retained within the aggregated filename. The other BIDS _key-value pairs are "aggregated over". Parameters ---------- path: str Path to the top of the BIDS dataset """ # TODO: collect all task- .json files for func files to tasks = {} # way too many -- let's just collect all which are the same! # FIELDS_TO_TRACK = {'RepetitionTime', 'FlipAngle', 'EchoTime', # 'Manufacturer', 'SliceTiming', ''} for fpath in find_files( r".*_task-.*\_bold\.json", topdir=glob(op.join(path, "sub-*")), exclude_vcs=True, exclude=r"/\.(datalad|heudiconv)/", ): # # According to BIDS spec I think both _task AND _acq (may be more? # _rec, _dir, ...?) should be retained? # TODO: if we are to fix it, then old ones (without _acq) should be # removed first task = re.sub(r".*_(task-[^_\.]*(_acq-[^_\.]*)?)_.*", r"\1", fpath) json_ = load_json(fpath, retry=100) if task not in tasks: tasks[task] = json_ else: rec = tasks[task] # let's retain only those fields which have the same value for field in sorted(rec): if field not in json_ or json_[field] != rec[field]: del rec[field] # create a stub onsets file for each one of those suf = "_bold.json" assert fpath.endswith(suf) # specify the name of the '_events.tsv' file: if "_echo-" in fpath: # multi-echo sequence: bids (1.1.0) specifies just one '_events.tsv' # file, common for all echoes. The name will not include _echo-. # TODO: RF to use re.match for better readability/robustness # So, find out the echo number: fpath_split = fpath.split("_echo-", 1) # split fpath using '_echo-' fpath_split_2 = fpath_split[1].split( "_", 1 ) # split the second part of fpath_split using '_' echoNo = fpath_split_2[0] # get echo number if echoNo == "1": if len(fpath_split_2) != 2: raise ValueError("Found no trailer after _echo-") # we modify fpath to exclude '_echo-' + echoNo: fpath = fpath_split[0] + "_" + fpath_split_2[1] else: # for echoNo greater than 1, don't create the events file, so go to # the next for loop iteration: continue events_file = remove_suffix(fpath, suf) + "_events.tsv" # do not touch any existing thing, it may be precious if not op.lexists(events_file): lgr.debug("Generating %s", events_file) with open(events_file, "w") as fp: fp.write( "onset\tduration\ttrial_type\tresponse_time\tstim_file" "\tTODO -- fill in rows and add more tab-separated " "columns if desired" ) # extract tasks files stubs for task_acq, fields in tasks.items(): task_file = op.join(path, task_acq + "_bold.json") # Since we are pulling all unique fields we have to possibly # rewrite this file to guarantee consistency. # See https://github.com/nipy/heudiconv/issues/277 for a usecase/bug # when we didn't touch existing one. # But the fields we enter (TaskName and CogAtlasID) might need need # to be populated from the file if it already exists placeholders = { "TaskName": ( "TODO: full task name for %s" % task_acq.split("_")[0].split("-")[1] ), "CogAtlasID": "http://www.cognitiveatlas.org/task/id/TODO", } if op.lexists(task_file): j = load_json(task_file, retry=100) # Retain possibly modified placeholder fields for f in placeholders: if f in j: placeholders[f] = j[f] act = "Regenerating" else: act = "Generating" lgr.debug("%s %s", act, task_file) fields.update(placeholders) save_json(task_file, fields, sort_keys=True, pretty=True) def tuneup_bids_json_files(json_files: list[str]) -> None: """Given a list of BIDS .json files, e.g.""" if not json_files: return # Harmonize generic .json formatting for jsonfile in json_files: json_ = load_json(jsonfile) # sanitize! for f1 in ["Acquisition", "Study", "Series"]: for f2 in ["DateTime", "Date"]: json_.pop(f1 + f2, None) # TODO: should actually be placed into series file which must # go under annex (not under git) and marked as sensitive # MG - Might want to replace with flag for data sensitivity # related - https://github.com/nipy/heudiconv/issues/92 if "Date" in str(json_): # Let's hope no word 'Date' comes within a study name or smth like # that raise ValueError("There must be no dates in .json sidecar") # Those files should not have our version field already - should have been # freshly produced assert HEUDICONV_VERSION_JSON_KEY not in json_ json_[HEUDICONV_VERSION_JSON_KEY] = str(__version__) save_json(jsonfile, json_) # Load the beast seqtype = op.basename(op.dirname(jsonfile)) # MG - want to expand this for other _epi # possibly add IntendedFor automatically as well? if seqtype == "fmap": json_basename = "_".join(jsonfile.split("_")[:-1]) # if we got by now all needed .json files -- we can fix them up # unfortunately order of "items" is not guaranteed atm json_phasediffname = json_basename + "_phasediff.json" json_mag = json_basename + "_magnitude*.json" if op.exists(json_phasediffname) and len(glob(json_mag)) >= 1: json_ = load_json(json_phasediffname) # TODO: we might want to reorder them since ATM # the one for shorter TE is the 2nd one! # For now just save truthfully by loading magnitude files lgr.debug("Placing EchoTime fields into phasediff file") for i in 1, 2: try: json_["EchoTime%d" % i] = load_json( json_basename + "_magnitude%d.json" % i )["EchoTime"] except IOError as exc: lgr.error("Failed to open magnitude file: %s", exc) # might have been made R/O already, but if not -- it will be set # only later in the pipeline, so we must not make it read-only yet was_readonly = is_readonly(json_phasediffname) if was_readonly: set_readonly(json_phasediffname, False) save_json(json_phasediffname, json_) if was_readonly: set_readonly(json_phasediffname) def add_participant_record( studydir: str, subject: str, age: str | None, sex: str | None ) -> None: participants_tsv = op.join(studydir, "participants.tsv") participant_id = "sub-%s" % subject if not create_file_if_missing( participants_tsv, "\t".join(["participant_id", "age", "sex", "group"]) + "\n" ): # check if may be subject record already exists with open(participants_tsv) as f: f.readline() known_subjects = {ln.split("\t")[0] for ln in f.readlines()} if participant_id in known_subjects: return else: # Populate participants.json (an optional file to describe column names in # participant.tsv). This auto generation will make BIDS-validator happy. participants_json = op.join(studydir, "participants.json") if not op.lexists(participants_json): save_json( participants_json, OrderedDict( [ ( "participant_id", OrderedDict([("Description", "Participant identifier")]), ), ( "age", OrderedDict( [ ( "Description", "Age in years (TODO - verify) as in the initial" " session, might not be correct for other sessions", ) ] ), ), ( "sex", OrderedDict( [ ( "Description", "self-rated by participant, M for male/F for " "female (TODO: verify)", ) ] ), ), ( "group", OrderedDict( [ ( "Description", "(TODO: adjust - by default everyone is in " "control group)", ) ] ), ), ] ), sort_keys=False, ) # Add a new participant with open(participants_tsv, "a") as f: f.write( "\t".join( map( str, [ participant_id, maybe_na(treat_age(age)), maybe_na(sex), "control", ], ) ) + "\n" ) def find_subj_ses(f_name: str) -> tuple[Optional[str], Optional[str]]: """Given a path to the bids formatted filename parse out subject/session""" # we will allow the match at either directories or within filename # assuming that bids layout is "correct" regex = re.compile("sub-(?P[a-zA-Z0-9]*)([/_]ses-(?P[a-zA-Z0-9]*))?") regex_res = regex.search(f_name) res = regex_res.groupdict() if regex_res else {} return res.get("subj", None), res.get("ses", None) def save_scans_key( item: tuple[str, tuple[str, ...], list[str]], bids_files: list[str] ) -> None: """ Parameters ---------- item: bids_files: list of str Returns ------- """ rows = {} assert bids_files, "we do expect some files since it was called" # we will need to deduce subject and session from the bids_filename # and if there is a conflict, we would just blow since this function # should be invoked only on a result of a single item conversion as far # as I see it, so should have the same subject/session subj: Optional[str] = None ses: Optional[str] = None for bids_file in bids_files: # get filenames f_name = "/".join(bids_file.split("/")[-2:]) f_name = f_name.replace("json", "nii.gz") rows[f_name] = get_formatted_scans_key_row(item[-1][0]) subj_, ses_ = find_subj_ses(f_name) if not subj_: lgr.warning( "Failed to detect fulfilled BIDS layout. " "No scans.tsv file(s) will be produced for %s", ", ".join(bids_files), ) return if subj and subj_ != subj: raise ValueError( "We found before subject %s but now deduced %s from %s" % (subj, subj_, f_name) ) subj = subj_ if ses and ses_ != ses: raise ValueError( "We found before session %s but now deduced %s from %s" % (ses, ses_, f_name) ) ses = ses_ # where should we store it? output_dir = op.dirname(op.dirname(bids_file)) # save ses = "_ses-%s" % ses if ses else "" add_rows_to_scans_keys_file( op.join(output_dir, "sub-{0}{1}_scans.tsv".format(subj, ses)), rows ) def add_rows_to_scans_keys_file(fn: str, newrows: dict[str, list[str]]) -> None: """Add new rows to the _scans file. Parameters ---------- fn: str filename newrows: dict extra rows to add (acquisition time, referring physician, random string) """ if op.lexists(fn): with open(fn, "r") as csvfile: reader = csv.reader(csvfile, delimiter="\t") existing_rows = [row for row in reader] # skip header fnames2info = {row[0]: row[1:] for row in existing_rows[1:]} newrows_key = newrows.keys() newrows_toadd = list(set(newrows_key) - set(fnames2info.keys())) for key_toadd in newrows_toadd: fnames2info[key_toadd] = newrows[key_toadd] # remove os.unlink(fn) else: fnames2info = newrows header = list(SCANS_FILE_FIELDS.keys()) # prepare all the data rows data_rows = [[k] + v for k, v in fnames2info.items()] # sort by the date/filename try: data_rows_sorted = sorted(data_rows, key=lambda x: (x[1], x[0])) except TypeError as exc: lgr.warning("Sorting scans by date failed: %s", str(exc)) data_rows_sorted = sorted(data_rows) # save with open(fn, "a") as csvfile: writer = csv.writer(csvfile, delimiter="\t") writer.writerows([header] + data_rows_sorted) def get_formatted_scans_key_row(dcm_fn: str | Path) -> list[str]: """ Parameters ---------- dcm_fn: str Returns ------- row: list [ISO acquisition time, performing physician name, random string] """ dcm_data = dcm.dcmread(dcm_fn, stop_before_pixels=True, force=True) # we need to store filenames and acquisition datetimes acq_datetime = dicoms.get_datetime_from_dcm(dcm_data=dcm_data) # add random string # But let's make it reproducible by using all UIDs # (might change across versions?) randcontent = "".join( [getattr(dcm_data, f) or "" for f in sorted(dir(dcm_data)) if f.endswith("UID")] ) randstr = hashlib.md5(randcontent.encode()).hexdigest()[:8] try: perfphys = dcm_data.PerformingPhysicianName except AttributeError: perfphys = "" row = [acq_datetime.isoformat() if acq_datetime else "", perfphys, randstr] # empty entries should be 'n/a' # https://github.com/dartmouth-pbs/heudiconv/issues/32 row = ["n/a" if not str(e) else e for e in row] return row def convert_sid_bids(subject_id: str) -> str: """Shim for stripping any non-BIDS compliant characters within subject_id Parameters ---------- subject_id : string Returns ------- sid : string New subject ID """ warnings.warn( "convert_sid_bids() is deprecated, please use sanitize_label() instead.", DeprecationWarning, stacklevel=2, ) return sanitize_label(subject_id) def get_shim_setting(json_file: str) -> Any: """ Gets the "ShimSetting" field from a json_file. If no "ShimSetting" present, return error Parameters ---------- json_file : str Returns ------- str with "ShimSetting" value """ data = load_json(json_file) try: shims = data[SHIM_KEY] except KeyError: lgr.error( 'File %s does not have "%s". ' 'Please use a different "matching_parameters" in your heuristic file', json_file, SHIM_KEY, ) raise return shims def find_fmap_groups(fmap_dir: str) -> dict[str, list[str]]: """ Finds the different fmap groups in a fmap directory. By groups here we mean fmaps that are intended to go together (with reversed PE polarity, magnitude/phase, etc.) Parameters ---------- fmap_dir : str path to the session folder (or to the subject folder, if there are no sessions). Returns ------- fmap_groups : dict key: prefix common to the group (e.g. no "dir" entity, "_phase"/"_magnitude", ...) value: list of all fmap paths in the group """ if op.basename(fmap_dir) != "fmap": lgr.error("%s is not a fieldmap folder", fmap_dir) # Get a list of all fmap json files in the session: fmap_jsons = sorted(glob(op.join(fmap_dir, "*.json"))) # RegEx to remove fmap-specific substrings from fmap file names # "_phase[1,2]", "_magnitude[1,2]", "_phasediff", "_dir-

][][__] where [PREFIX:] - leading capital letters followed by : are stripped/ignored [WIP ] - prefix is stripped/ignored (added by Philips for patch sequences) <...> - value to be entered [...] - optional -- might be nearly mandatory for some modalities (e.g., run for functional) and very optional for others *ID - alpha-numerical identifier (e.g. 01,02, pre, post, pre01) for a run, task, session. Note that makes more sense to use numerical values for RUNID (e.g., _run-01, _run-02) for obvious sorting and possibly descriptive ones for e.g. SESID (_ses-movie, _ses-localizer) a known BIDS sequence datatype which is usually a name of the folder under subject's directory. And (optional) suffix is a specific sequence type (e.g., "bold" for func, or "T1w" for "anat"), which could often (but not always) be deduced from DICOM. Known to ReproIn BIDS modalities are: anat - anatomical data. Might also be collected multiple times across runs (e.g. if subject is taken out of magnet etc), so could (optionally) have "_run" definition attached. For "standard anat" suffixes, please consult to "8.3 Anatomy imaging data" but most common are 'T1w', 'T2w', 'angio'. beh - behavioral data. known but not "treated". func - functional (AKA task, including resting state) data. Typically contains multiple runs, and might have multiple different tasks different per each run (e.g. _task-memory_run-01, _task-oddball_run-02) fmap - field maps dwi - diffusion weighted imaging (also can as well have runs) The other BIDS modalities are not known ATM and their data will not be converted and will be just skipped (with a warning). Full list of datatypes can be found at https://github.com/bids-standard/bids-specification/blob/v1.7.0/src/schema/objects/datatypes.yaml and their corresponding suffixes at https://github.com/bids-standard/bids-specification/tree/v1.7.0/src/schema/rules/datatypes _ses- (optional) a session. Having a single sequence within a study would make that study follow "multi-session" layout. A common practice to have a _ses specifier within the scout sequence name. You can either specify explicit session identifier (SESID) or just say to maintain, create (starts with 1). You can also use _ses-{date} (or _ses-DATE for Siemens X60 which does not allow {} in names) in case of scanning phantoms or non-human subjects and wanting sessions to be coded by the acquisition date. _task- (optional) a short name for a task performed during that run. If not provided and it is a func sequence, _task-UNKNOWN will be automatically added to comply with BIDS. Consult http://www.cognitiveatlas.org/tasks on known tasks. _acq- (optional) a short custom label to distinguish a different set of parameters used for acquiring the same modality (e.g. _acq-highres, _acq-lowres etc) _run- (optional) a (typically functional) run. The same idea as with SESID. _dir-[AP,PA,LR,RL,VD,DV] (optional) to be used for fmap images, whenever a pair of the SE images is collected to be used to estimate the fieldmap (optional) any other fields (e.g. _acq-) from BIDS acquisition __ (optional) after two underscores any arbitrary comment which will not matter to how layout in BIDS. But that one theoretically should not be necessary, and (ab)use of it would just signal lack of thought while preparing sequence name to start with since everything could have been expressed in BIDS fields. ## Last moment checks/FAQ: - Functional runs should have _task- field defined - Do not use "+", "_" or "-" within SESID, TASKID, ACQLABEL, RUNID, so we could detect "canceled" runs. - If run was canceled -- just copy canceled run (with the same index) and re-run it. Files with overlapping name will be considered duplicate/canceled session and only the last one would remain. The others would acquire __dup0 suffix. Although we still support "-" and "+" used within SESID and TASKID, their use is not recommended, thus not listed here ## Scanner specifics We perform following actions regardless of the type of scanner, but applied generally to accommodate limitations imposed by different manufacturers/models: ### Philips - We replace all ( with { and ) with } to be able e.g. to specify session {date} - "WIP " prefix unconditionally added by the scanner is stripped ### Siemens X60 - _ses-DATE is supported as an alternative to _ses-{date} since X60 does not allow {} """ from __future__ import annotations from collections.abc import Iterable from glob import glob import hashlib import logging import os.path import re from typing import Any, Optional, TypeVar import pydicom as dcm from heudiconv.due import Doi, due from heudiconv.utils import SeqInfo, StudySessionInfo lgr = logging.getLogger("heudiconv") T = TypeVar("T") # Terminology to harmonise and use to name variables etc # experiment # subject # [session] # exam (AKA scanning session) - currently seqinfo, unless brought together from multiple # series (AKA protocol?) # - series_spec - deduced from fields the spec (literal value) # - series_info - the dictionary with fields parsed from series_spec # Which fields in seqinfo (in this order) to check for the ReproIn spec series_spec_fields = ("protocol_name", "series_description") # dictionary from accession-number to runs that need to be marked as bad # NOTE: even if filename has number that is 0-padded, internally no padding # is done fix_accession2run: dict[str, list[str]] = { # e.g.: # 'A000035': ['^8-', '^9-'], } # A dictionary containing fixes/remapping for sequence names per study. # Keys are md5sum of study_description from DICOMs, in the form of PI-Experimenter^protocolname # You can use `heudiconv -f reproin --command ls --files PATH # to list the "study hash". # Values are list of tuples in the form (regex_pattern, substitution). # If the key is an empty string`''''`, it would apply to any study. protocols2fix: dict[str | re.Pattern[str], list[tuple[str, str]]] = { # e.g., QA: # '43b67d9139e8c7274578b7451ab21123': # [ # ('BOLD_p2_s4_3\.5mm', 'func_task-rest_acq-p2-s4-3.5mm'), # ('BOLD_', 'func_task-rest'), # ('_p2_s4', '_acq-p2-s4'), # ('_p2', '_acq-p2'), # ], # '': # for any study example with regexes used # [ # ('AAHead_Scout_.*', 'anat-scout'), # ('^dti_.*', 'dwi'), # ('^.*_distortion_corr.*_([ap]+)_([12])', r'fmap-epi_dir-\1_run-\2'), # ('^(.+)_ap.*_r(0[0-9])', r'func_task-\1_run-\2'), # ('^t1w_.*', 'anat-T1w'), # # problematic case -- multiple identically named pepolar fieldmap runs # # I guess we will just sacrifice ability to detect canceled runs here. # # And we cannot just use _run+ since it would increment independently # # for ap and then for pa. We will rely on having ap preceding pa. # # Added _acq-mb8 so they match the one in funcs # ('func_task-discorr_acq-ap', r'fmap-epi_dir-ap_acq-mb8_run+'), # ('func_task-discorr_acq-pa', r'fmap-epi_dir-pa_acq-mb8_run='), # ] } # list containing StudyInstanceUID to skip -- hopefully doesn't happen too often dicoms2skip: list[str] = [ # e.g. # '1.3.12.2.1107.5.2.43.66112.30000016110117002435700000001', ] DEFAULT_FIELDS = { # Let it just be in each json file extracted "Acknowledgements": "We thank Terry Sacket and the rest of the DBIC (Dartmouth Brain Imaging " "Center) personnel for assistance in data collection, and " "Yaroslav O. Halchenko for preparing BIDS dataset. " "TODO: adjust to your case.", } POPULATE_INTENDED_FOR_OPTS = { "matching_parameters": ["ImagingVolume", "Shims"], "criterion": "Closest", } KNOWN_DATATYPES = {"anat", "func", "dwi", "behav", "fmap"} def _delete_chars(from_str: str, deletechars: str) -> str: return from_str.translate(str.maketrans("", "", deletechars)) def filter_dicom(dcmdata: dcm.dataset.Dataset) -> bool: """Return True if a DICOM dataset should be filtered out, else False""" return True if dcmdata.StudyInstanceUID in dicoms2skip else False def filter_files(_fn: str) -> bool: """Return True if a file should be kept, else False. ATM reproin does not do any filtering. Override if you need to add some """ return True def create_key( subdir: Optional[str], file_suffix: str, outtype: tuple[str, ...] = ("nii.gz", "dicom"), annotation_classes: None = None, prefix: str = "", ) -> tuple[str, tuple[str, ...], None]: if not subdir: raise ValueError("subdir must be a valid format string") # may be even add "performing physician" if defined?? template = os.path.join( prefix, "{bids_subject_session_dir}", subdir, "{bids_subject_session_prefix}_%s" % file_suffix, ) return template, outtype, annotation_classes def md5sum(string: Optional[str]) -> str: """Computes md5sum of a string""" if not string: return "" # not None so None was not compared to strings m = hashlib.md5(string.encode()) return m.hexdigest() def get_study_description(seqinfo: list[SeqInfo]) -> str: # Centralized so we could fix/override v = get_unique(seqinfo, "study_description") assert isinstance(v, str) return v def get_study_hash(seqinfo: list[SeqInfo]) -> str: # XXX: ad hoc hack return md5sum(get_study_description(seqinfo)) def fix_canceled_runs(seqinfo: list[SeqInfo]) -> list[SeqInfo]: """Function that adds cancelme_ to known bad runs which were forgotten""" if not fix_accession2run: return seqinfo # nothing to do for i, curr_seqinfo in enumerate(seqinfo): accession_number = curr_seqinfo.accession_number if accession_number and accession_number in fix_accession2run: lgr.info( "Considering some runs possibly marked to be " "canceled for accession %s", accession_number, ) # This code is reminiscent of prior logic when operating on # a single accession, but left as is for now badruns = fix_accession2run[accession_number] badruns_pattern = "|".join(badruns) if re.match(badruns_pattern, curr_seqinfo.series_id): lgr.info("Fixing bad run {0}".format(curr_seqinfo.series_id)) fixedkwargs = dict() for key in series_spec_fields: fixedkwargs[key] = "cancelme_" + getattr(curr_seqinfo, key) seqinfo[i] = curr_seqinfo._replace(**fixedkwargs) return seqinfo def fix_dbic_protocol(seqinfo: list[SeqInfo]) -> list[SeqInfo]: """Ad-hoc fixup for existing protocols. It will operate in 3 stages on `protocols2fix` records. 1. consider a record which has md5sum of study_description 2. apply all substitutions, where key is a regular expression which successfully searches (not necessarily matches, so anchor appropriately) study_description 3. apply "catch all" substitutions in the key containing an empty string 3. is somewhat redundant since `re.compile('.*')` could match any, but is kept for simplicity of its specification. """ study_hash = get_study_hash(seqinfo) study_description = get_study_description(seqinfo) # We will consider first study specific (based on hash) if study_hash in protocols2fix: _apply_substitutions( seqinfo, protocols2fix[study_hash], "study (%s) specific" % study_hash ) # Then go through all regexps returning regex "search" result # on study_description for sub, substitutions in protocols2fix.items(): if isinstance(sub, re.Pattern) and sub.search(study_description): _apply_substitutions( seqinfo, substitutions, "%r regex matching" % sub.pattern ) # and at the end - global if "" in protocols2fix: _apply_substitutions(seqinfo, protocols2fix[""], "global") return seqinfo def _apply_substitutions( seqinfo: list[SeqInfo], substitutions: list[tuple[str, str]], subs_scope: str ) -> None: lgr.info("Considering %s substitutions", subs_scope) for i, curr_seqinfo in enumerate(seqinfo): fixed_kwargs = dict() # need to replace both protocol_name series_description for key in series_spec_fields: oldvalue = value = getattr(curr_seqinfo, key) # replace all I need to replace for substring, replacement in substitutions: value = re.sub(substring, replacement, value) if oldvalue != value: lgr.info(" %s: %r -> %r", key, oldvalue, value) fixed_kwargs[key] = value # namedtuples are immutable seqinfo[i] = curr_seqinfo._replace(**fixed_kwargs) def fix_seqinfo(seqinfo: list[SeqInfo]) -> list[SeqInfo]: """Just a helper on top of both fixers""" # add cancelme to known bad runs seqinfo = fix_canceled_runs(seqinfo) seqinfo = fix_dbic_protocol(seqinfo) return seqinfo def ls(_study_session: StudySessionInfo, seqinfo: list[SeqInfo]) -> str: """Additional ls output for a seqinfo""" # assert len(sequences) <= 1 # expecting only a single study here # seqinfo = sequences.keys()[0] return " study hash: %s" % get_study_hash(seqinfo) # XXX we killed session indicator! what should we do now?!!! # WE DON'T NEED IT -- it will be provided into conversion_info as `session` # So we just need subdir and file_suffix! @due.dcite( Doi("10.5281/zenodo.1207117"), path="heudiconv.heuristics.reproin", description="ReproIn heudiconv heuristic for turnkey conversion into BIDS", ) def infotodict( seqinfo: list[SeqInfo], ) -> dict[tuple[str, tuple[str, ...], None], list[str]]: """Heuristic evaluator for determining which runs belong where allowed template fields - follow python string module: item: index within category subject: participant id seqitem: run number during scanning subindex: sub index within group session: scan index for longitudinal acq """ seqinfo = fix_seqinfo(seqinfo) lgr.info("Processing %d seqinfo entries", len(seqinfo)) info: dict[tuple[str, tuple[str, ...], None], list[str]] = {} skipped: list[str] = [] skipped_unknown: list[str] = [] current_run = 0 run_label: Optional[str] = None # run- dcm_image_iod_spec: Optional[str] = None skip_derived = False for curr_seqinfo in seqinfo: # XXX: skip derived sequences, we don't store them to avoid polluting # the directory, unless it is the motion corrected ones # (will get _rec-moco suffix) if ( skip_derived and curr_seqinfo.is_derived and not curr_seqinfo.is_motion_corrected ): skipped.append(curr_seqinfo.series_id) lgr.debug("Ignoring derived data %s", curr_seqinfo.series_id) continue # possibly apply present formatting in the series_description or protocol name for f in "series_description", "protocol_name": curr_seqinfo = curr_seqinfo._replace( **{f: getattr(curr_seqinfo, f).format(**curr_seqinfo._asdict())} ) template = None suffix = "" # seq = [] # figure out type of image from curr_seqinfo.image_info -- just for checking ATM # since we primarily rely on encoded in the protocol name information prev_dcm_image_iod_spec = dcm_image_iod_spec if len(curr_seqinfo.image_type) > 2: # https://dicom.innolitics.com/ciods/cr-image/general-image/00080008 # 0 - ORIGINAL/DERIVED # 1 - PRIMARY/SECONDARY # 3 - Image IOD specific specialization (optional) dcm_image_iod_spec = curr_seqinfo.image_type[2] image_type_datatype = { # Note: P and M are too generic to make a decision here, could be # for different datatypes (bold, fmap, etc) "FMRI": "func", "MPR": "anat", "DIFFUSION": "dwi", "MIP_SAG": "anat", # angiography "MIP_COR": "anat", # angiography "MIP_TRA": "anat", # angiography }.get(dcm_image_iod_spec, None) else: dcm_image_iod_spec = image_type_datatype = None series_info = {} # For please lintian and its friends for sfield in series_spec_fields: svalue = getattr(curr_seqinfo, sfield) series_info = parse_series_spec(svalue) if series_info: # looks like a valid spec - we are done series_spec = svalue break else: lgr.debug("Failed to parse reproin spec in .%s=%r", sfield, svalue) if not series_info: series_spec = None # we cannot know better lgr.warning( "Could not determine the series name by looking at %s fields", ", ".join(series_spec_fields), ) skipped_unknown.append(curr_seqinfo.series_id) continue if dcm_image_iod_spec and dcm_image_iod_spec.startswith("MIP"): series_info["acq"] = series_info.get("acq", "") + sanitize_str( dcm_image_iod_spec ) datatype = series_info.pop("datatype") datatype_suffix = series_info.pop("datatype_suffix", None) if image_type_datatype and datatype != image_type_datatype: lgr.warning( "Deduced datatype to be %s from DICOM, but got %s out of %s", image_type_datatype, datatype, series_spec, ) # if curr_seqinfo.is_derived: # # Let's for now stash those close to original images # # TODO: we might want a separate tree for all of this!? # # so more of a parameter to the create_key # #datatype += '/derivative' # # just keep it lower case and without special characters # # XXXX what for??? # #seq.append(curr_seqinfo.series_description.lower()) # prefix = os.path.join('derivatives', 'scanner') # else: # prefix = '' prefix = "" # # Figure out the datatype_suffix (BIDS _suffix) # # If none was provided -- let's deduce it from the information we find: # analyze curr_seqinfo.protocol_name (series_id is based on it) for full name mapping etc if not datatype_suffix: if datatype == "func": if "_pace_" in series_spec: datatype_suffix = "pace" # or should it be part of seq- elif "P" in curr_seqinfo.image_type: datatype_suffix = "phase" elif "M" in curr_seqinfo.image_type: datatype_suffix = "bold" else: # assume bold by default datatype_suffix = "bold" elif datatype == "fmap": # TODO: support phase1 phase2 like in "Case 2: Two phase images ..." if not dcm_image_iod_spec: raise ValueError("Do not know image data type yet to make decision") datatype_suffix = { # might want explicit {file_index} ? # _epi for pepolar fieldmaps, see # https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#case-4-multiple-phase-encoded-directions-pepolar "M": "epi" if "dir" in series_info else "magnitude", "P": "phasediff", "DIFFUSION": "epi", # according to KODI those DWI are the EPIs we need }[dcm_image_iod_spec] elif datatype == "dwi": # label for dwi as well datatype_suffix = "dwi" # # Even if datatype_suffix was provided, for some data we might need to override, # since they are complementary files produced along-side with original # ones. # if curr_seqinfo.series_description.endswith("_SBRef"): datatype_suffix = "sbref" if not datatype_suffix: # Might be provided by the bids ending within series_spec, we would # just want to check if that the last element is not _key-value pair bids_ending = series_info.get("bids", None) if not bids_ending or "-" in bids_ending.split("_")[-1]: lgr.warning( "We ended up with an empty label/suffix for %r", series_spec ) run = series_info.get("run") if run is not None: # so we have an indicator for a run if run == "+": # some sequences, e.g. fmap, would generate two (or more?) # sequences -- e.g. one for magnitude(s) and other ones for # phases. In those we must not increment run! if dcm_image_iod_spec and dcm_image_iod_spec == "P": if prev_dcm_image_iod_spec != "M": # XXX if we have a known earlier study, we need to always # increase the run counter for phasediff because magnitudes # were not acquired if ( get_study_hash([curr_seqinfo]) == "9d148e2a05f782273f6343507733309d" ): current_run += 1 else: raise RuntimeError( "Was expecting phase image to follow magnitude " "image, but previous one was %r", prev_dcm_image_iod_spec, ) # else we do nothing special else: # and otherwise we go to the next run current_run += 1 elif run == "=": if not current_run: current_run = 1 elif run.isdigit(): current_run_ = int(run) if current_run_ < current_run: lgr.warning( "Previous run (%s) was larger than explicitly specified %s", current_run, current_run_, ) current_run = current_run_ else: raise ValueError( "Don't know how to deal with run specification %s" % repr(run) ) run_label = "run-%02d" % current_run else: # if there is no _run -- no run label added run_label = None def from_series_info(name: str) -> Optional[str]: """A little helper to provide _name-value if series_info knows it Returns None otherwise """ if series_info.get(name): # noqa: B023 return "%s-%s" % (name, series_info[name]) # noqa: B023 else: return None # yoh: had a wrong assumption # if curr_seqinfo.is_motion_corrected: # assert curr_seqinfo.is_derived, "Motion corrected images must be 'derived'" if curr_seqinfo.is_motion_corrected: if from_series_info("rec"): raise NotImplementedError( "want to add _rec-moco but there is _rec- already" ) # But we want to add an indicator in case it was motion corrected # in the magnet. ref sample /2017/01/03/qa series_info["rec"] = "moco" # TODO: get order from schema, do not hardcode. ATM could be checked at # https://bids-specification.readthedocs.io/en/stable/99-appendices/04-entity-table.html # https://github.com/bids-standard/bids-specification/blob/HEAD/src/schema/rules/entities.yaml # ATM we at large rely on possible (re)ordering according to schema to be done # by heudiconv, not reproin here. filename_suffix_parts = [ from_series_info("task"), from_series_info("acq"), from_series_info("rec"), from_series_info("dir"), series_info.get("bids"), run_label, datatype_suffix, ] # filter those which are None, and join with _ suffix = "_".join(filter(bool, filename_suffix_parts)) # type: ignore[arg-type] # # .series_description in case of # sdesc = curr_seqinfo.study_description # # temporary aliases for those phantoms which we already collected # # so we rename them into this # #MAPPING # # # the idea ias to have sequence names in the format like # # bids__bidsrecord # # in bids record we could have _run[+=] # # which would say to either increment run number from already encountered # # or reuse the last one # if seq: # suffix += 'seq-%s' % ('+'.join(seq)) # For scouts -- we want only dicoms # https://github.com/nipy/heudiconv/issues/145 outtype: tuple[str, ...] if ( "_Scout" in curr_seqinfo.series_description or ( datatype == "anat" and datatype_suffix and datatype_suffix.startswith("scout") ) or ( curr_seqinfo.series_description.lower() == curr_seqinfo.protocol_name.lower() + "_setter" ) ): outtype = ("dicom",) else: outtype = ("nii.gz", "dicom") template = create_key(datatype, suffix, prefix=prefix, outtype=outtype) # we wanted ordered dict for consistent demarcation of dups if template not in info: info[template] = [] info[template].append(curr_seqinfo.series_id) if skipped: lgr.info("Skipped %d sequences: %s" % (len(skipped), skipped)) if skipped_unknown: lgr.warning( "Could not figure out where to stick %d sequences: %s" % (len(skipped_unknown), skipped_unknown) ) info = get_dups_marked(info) # mark duplicate ones with __dup-0x suffix return info def get_dups_marked( info: dict[tuple[str, tuple[str, ...], None], list[T]], per_series: bool = True ) -> dict[tuple[str, tuple[str, ...], None], list[T]]: """ Parameters ---------- info per_series: bool If set to False, it would create growing index through all series. That could lead to non-desired effects if some "multi file" scans (such as fmap with magnitude{1,2} and phasediff) would not be able to associate multiple files for the same acquisition. By default (True) dup indices would be per each series (change introduced in 0.5.2) Returns ------- """ # analyze for "cancelled" runs, if run number was explicitly specified and # thus we ended up with multiple entries which would mean that older ones # were "cancelled" info = info.copy() dup_id = 0 for template, series_ids in list(info.items()): if len(series_ids) > 1: lgr.warning( "Detected %d duplicated run(s) for template %s: %s", len(series_ids) - 1, template[0], series_ids[:-1], ) # copy the duplicate ones into separate ones if per_series: dup_id = 0 # reset since declared per series for dup_series_id in series_ids[:-1]: dup_id += 1 dup_template = ("%s__dup-%02d" % (template[0], dup_id),) + template[1:] # There must have not been such a beast before! if dup_template in info: raise AssertionError( "{} is already known to info={}. " "May be a bug for per_series=True handling?" "".format(dup_template, info) ) info[dup_template] = [dup_series_id] info[template] = series_ids[-1:] assert len(info[template]) == 1 return info def get_unique(seqinfos: list[SeqInfo], attr: str) -> Any: """Given a list of seqinfos, which must have come from a single study, get specific attr, which must be unique across all of the entries If not -- fail! """ values = set(getattr(si, attr) for si in seqinfos) if len(values) != 1: raise AssertionError( f"Was expecting a single value for attribute {attr!r} " f"but got: {', '.join(sorted(values))}" ) return values.pop() # TODO: might need to do grouping per each session and return here multiple # hits, or may be we could just somehow demarkate that it will be multisession # one and so then later value parsed (again) in infotodict would be used??? def infotoids(seqinfos: Iterable[SeqInfo], outdir: str) -> dict[str, Optional[str]]: seqinfo_lst = list(seqinfos) # decide on subjid and session based on patient_id lgr.info("Processing sequence infos to deduce study/session") study_description = get_study_description(seqinfo_lst) study_description_hash = md5sum(study_description) subject = fixup_subjectid(get_unique(seqinfo_lst, "patient_id")) # TODO: fix up subject id if missing some 0s if study_description: # Generally it is a ^ but if entered manually, ppl place space in it split = re.split("[ ^]", study_description, maxsplit=1) # split first one even more, since could be PI_Student or PI-Student split = re.split("[-_]", split[0], maxsplit=1) + split[1:] # locator = study_description.replace('^', '/') locator = "/".join(split) else: locator = "unknown" # TODO: actually check if given study is study we would care about # and if not -- we should throw some ???? exception # So -- use `outdir` and locator etc to see if for a given locator/subject # and possible ses+ in the sequence names, so we would provide a sequence # So might need to go through parse_series_spec(curr_seqinfo.protocol_name) # to figure out presence of sessions. ses_markers: list[str] = [] # there might be fixups needed so we could deduce session etc # this copy is not replacing original one, so the same fix_seqinfo # might be called later seqinfo_lst = fix_seqinfo(seqinfo_lst) for s in seqinfo_lst: if s.is_derived: continue session_ = parse_series_spec(s.protocol_name).get("session", None) if session_ and "{" in session_: # there was a marker for something we could provide from our seqinfo # e.g. {date} session_ = session_.format(**s._asdict()) elif session_ == "DATE": # Siemens X60 does not allow {} in names, so also support uppercase "DATE" session_ = s.date if session_: ses_markers.append(session_) session: Optional[str] = None if ses_markers: # we have a session or possibly more than one even # let's figure out which case we have nonsign_vals = set(ses_markers).difference("+=") # although we might want an explicit '=' to note the same session as # mentioned before? if len(nonsign_vals) > 1: lgr.warning( # raise NotImplementedError( "Cannot deal with multiple sessions in the same study yet!" " We will process until the end of the first session" ) if nonsign_vals: # get only unique values ses_markers = list(set(ses_markers)) if set(ses_markers).intersection("+="): raise NotImplementedError( "Should not mix hardcoded session markers with incremental ones (+=)" ) if not len(ses_markers) == 1: raise NotImplementedError( "Should have got a single session marker. Got following: %s" % ", ".join(map(repr, ses_markers)) ) session = ses_markers[0] else: # TODO - I think we are doomed to go through the sequence and split # ... actually the same as with nonsign_vals, we just would need to figure # out initial one if sign ones, and should make use of knowing # outdir # raise NotImplementedError() # we need to look at what sessions we already have sessions_dir = os.path.join(outdir, locator, "sub-" + subject) prior_sessions = sorted(glob(os.path.join(sessions_dir, "ses-*"))) # TODO: more complicated logic # For now just increment session if + and keep the same number if = # and otherwise just give it 001 # Note: this disables our safety blanket which would refuse to process # what was already processed before since it would try to override, # BUT there is no other way besides only if heudiconv was storing # its info based on some UID if ses_markers == ["+"]: session = "%03d" % (len(prior_sessions) + 1) elif ses_markers == ["="]: session = ( os.path.basename(prior_sessions[-1])[4:] if prior_sessions else "001" ) else: session = "001" if study_description_hash == "9d148e2a05f782273f6343507733309d": session = "siemens1" lgr.info("Imposing session {0}".format(session)) return { # TODO: request info on study from the JedCap "locator": locator, # Sessions to be deduced yet from the names etc TODO "session": session, "subject": subject, } def sanitize_str(value: str) -> str: """Remove illegal characters for BIDS from task/acq/etc..""" return _delete_chars(value, "#!@$%^&.,:;_-") def parse_series_spec(series_spec: str) -> dict[str, str]: """Parse protocol name according to our convention with minimal set of fixups""" # Since Yarik didn't know better place to put it in, but could migrate outside # at some point. TODO series_spec = series_spec.replace("anat_T1w", "anat-T1w") series_spec = series_spec.replace("hardi_64", "dwi_acq-hardi64") series_spec = series_spec.replace("AAHead_Scout", "anat-scout") # Parse the name according to our convention/specification # leading or trailing spaces do not matter series_spec = series_spec.strip(" ") # Strip off leading CAPITALS: prefix to accommodate some reported usecases: # https://github.com/ReproNim/reproin/issues/14 # where PU: prefix is added by the scanner series_spec = re.sub("^[A-Z]*:", "", series_spec) series_spec = re.sub("^WIP ", "", series_spec) # remove Philips WIP prefix # Remove possible suffix we don't care about after __ series_spec = series_spec.split("__", 1)[0] bids = False # we don't know yet for sure # We need to figure out if it is a valid bids split = series_spec.split("_") prefix = split[0] # Fixups if prefix == "scout": prefix = split[0] = "anat-scout" if prefix != "bids" and "-" in prefix: prefix, _ = prefix.split("-", 1) if prefix == "bids": bids = True # for sure split = split[1:] def split2(s: str) -> tuple[str, Optional[str]]: # split on - if present, if not -- 2nd one returned None if "-" in s: a, _, b = s.partition("-") return a, b return s, None # Let's analyze first element which should tell us sequence type datatype, datatype_suffix = split2(split[0]) if datatype not in KNOWN_DATATYPES: # It is not something we don't consume if bids: lgr.warning( "It was instructed to be BIDS datatype but unknown " "%s found. Known are: %s", datatype, ", ".join(KNOWN_DATATYPES), ) return {} regd = dict(datatype=datatype) if datatype_suffix: regd["datatype_suffix"] = datatype_suffix # now go through each to see if one which we care bids_leftovers = [] for s in split[1:]: key, value = split2(s) if value is None and key[-1] in "+=": value = key[-1] key = key[:-1] # sanitize values, which must not have _ and - is undesirable ATM as well # TODO: BIDSv2.0 -- allows "-" so replace with it instead value = ( str(value) .replace("_", "X") .replace("-", "X") .replace("(", "{") .replace(")", "}") ) # for Philips if key in ["ses", "run", "task", "acq", "rec", "dir"]: # those we care about explicitly regd[{"ses": "session"}.get(key, key)] = sanitize_str(value) else: bids_leftovers.append(s) if bids_leftovers: regd["bids"] = "_".join(bids_leftovers) # TODO: might want to check for all known "standard" BIDS suffixes here # among bids_leftovers, thus serve some kind of BIDS validator # if not regd.get('datatype_suffix', None): # # might need to assign a default label for each datatype if was not # # given # regd['datatype_suffix'] = { # 'func': 'bold' # }.get(regd['datatype'], None) return regd def fixup_subjectid(subjectid: str) -> str: """Just in case someone managed to miss a zero or added an extra one""" # make it lowercase subjectid = subjectid.lower() reg = re.match(r"sid0*(\d+)$", subjectid) if not reg: # some completely other pattern # just filter out possible _- in it return re.sub("[-_]", "", subjectid) return "sid%06d" % int(reg.groups()[0]) nipy-heudiconv-217744b/heudiconv/heuristics/reproin_validator.cfg000066400000000000000000000012431517415366200253130ustar00rootroot00000000000000{ "ignore": [ "TOTAL_READOUT_TIME_NOT_DEFINED", "CUSTOM_COLUMN_WITHOUT_DESCRIPTION" ], "warn": [], "error": [], "ignoredFiles": [ "/.heudiconv/*", "/.heudiconv/*/*", "/.heudiconv/*/*/*", "/.heudiconv/*/*/*/*", "/.heudiconv/.git*", "/.heudiconv/.git/*", "/.heudiconv/.git/*/*", "/.heudiconv/.git/*/*/*", "/.heudiconv/.git/*/*/*/*", "/.heudiconv/.git/*/*/*/*/*", "/.heudiconv/.git/*/*/*/*/*/*", "/.git*", "/.datalad/*", "/.datalad/.*", "/.*/.datalad/*", "/.*/.datalad/.*", "/sub*/ses*/*/*__dup*", "/sub*/*/*__dup*" ] } nipy-heudiconv-217744b/heudiconv/heuristics/studyforrest_phase2.py000066400000000000000000000037571517415366200255140ustar00rootroot00000000000000from __future__ import annotations from typing import Optional from heudiconv.utils import SeqInfo scaninfo_suffix = ".json" def create_key( template: Optional[str], outtype: tuple[str, ...] = ("nii.gz",), annotation_classes: None = None, ) -> tuple[str, tuple[str, ...], None]: if template is None or not template: raise ValueError("Template must be a valid format string") return (template, outtype, annotation_classes) def infotodict( seqinfo: list[SeqInfo], ) -> dict[tuple[str, tuple[str, ...], None], list[str]]: """Heuristic evaluator for determining which runs belong where allowed template fields - follow python string module: item: index within category subject: participant id seqitem: run number during scanning subindex: sub index within group """ label_map = { "movie": "movielocalizer", "retmap": "retmap", "visloc": "objectcategories", } info: dict[tuple[str, tuple[str, ...], None], list[str]] = {} for s in seqinfo: if "EPI_3mm" not in s.protocol_name: continue label = s.protocol_name.split("_")[2].split()[0].strip("1234567890").lower() if label in ("movie", "retmap", "visloc"): key = create_key( "ses-localizer/func/{subject}_ses-localizer_task-%s_run-{item:01d}_bold" % label_map[label] ) elif label == "sense": # pilot retmap had different description key = create_key( "ses-localizer/func/{subject}_ses-localizer_task-retmap_run-{item:01d}_bold" ) elif label == "r": key = create_key( "ses-movie/func/{subject}_ses-movie_task-movie_run-%i_bold" % int(s.protocol_name.split("_")[2].split()[0][-1]) ) else: raise RuntimeError("YOU SHALL NOT PASS!") if key not in info: info[key] = [] info[key].append(s.series_id) return info nipy-heudiconv-217744b/heudiconv/heuristics/test_b0dwi_for_fmap.py000066400000000000000000000026761517415366200254110ustar00rootroot00000000000000"""Heuristic to extract a b-value=0 DWI image (basically, a SE-EPI) both as a fmap and as dwi It is used just to test that a 'DIFFUSION' image that the user chooses to extract as fmap (pepolar case) doesn't produce _bvecs/ _bvals json files, while it does for dwi images """ from __future__ import annotations from typing import Optional from heudiconv.utils import SeqInfo def create_key( template: Optional[str], outtype: tuple[str, ...] = ("nii.gz",), annotation_classes: None = None, ) -> tuple[str, tuple[str, ...], None]: if template is None or not template: raise ValueError("Template must be a valid format string") return (template, outtype, annotation_classes) def infotodict( seqinfo: list[SeqInfo], ) -> dict[tuple[str, tuple[str, ...], None], list[str]]: """Heuristic evaluator for determining which runs belong where allowed template fields - follow python string module: item: index within category subject: participant id seqitem: run number during scanning subindex: sub index within group """ fmap = create_key("sub-{subject}/fmap/sub-{subject}_acq-b0dwi_epi") dwi = create_key("sub-{subject}/dwi/sub-{subject}_acq-b0dwi_dwi") info: dict[tuple[str, tuple[str, ...], None], list[str]] = {fmap: [], dwi: []} for s in seqinfo: if "DIFFUSION" in s.image_type: info[fmap].append(s.series_id) info[dwi].append(s.series_id) return info nipy-heudiconv-217744b/heudiconv/heuristics/test_reproin.py000066400000000000000000000316351517415366200242060ustar00rootroot00000000000000# # Tests for reproin.py # from __future__ import annotations import re from typing import NamedTuple from unittest.mock import patch import pytest from . import reproin from .reproin import ( filter_files, fix_canceled_runs, fix_dbic_protocol, fixup_subjectid, get_dups_marked, get_unique, infotoids, md5sum, parse_series_spec, sanitize_str, ) from ..utils import SeqInfo class FakeSeqInfo(NamedTuple): accession_number: str study_description: str field1: str field2: str def test_get_dups_marked() -> None: no_dups: dict[tuple[str, tuple[str, ...], None], list[int]] = { ("some", ("foo",), None): [1] } assert get_dups_marked(no_dups) == no_dups info: dict[tuple[str, tuple[str, ...], None], list[int | str]] = { ("bu", ("du",), None): [1, 2], ("smth", (), None): [3], ("smth2", ("apple", "banana"), None): ["a", "b", "c"], } assert ( get_dups_marked(info) == get_dups_marked(info, True) == { ("bu__dup-01", ("du",), None): [1], ("bu", ("du",), None): [2], ("smth", (), None): [3], ("smth2__dup-01", ("apple", "banana"), None): ["a"], ("smth2__dup-02", ("apple", "banana"), None): ["b"], ("smth2", ("apple", "banana"), None): ["c"], } ) assert get_dups_marked(info, per_series=False) == { ("bu__dup-01", ("du",), None): [1], ("bu", ("du",), None): [2], ("smth", (), None): [3], ("smth2__dup-02", ("apple", "banana"), None): ["a"], ("smth2__dup-03", ("apple", "banana"), None): ["b"], ("smth2", ("apple", "banana"), None): ["c"], } def test_filter_files() -> None: # Filtering is currently disabled -- any sequence directory is Ok assert filter_files("/home/mvdoc/dbic/09-run_func_meh/0123432432.dcm") assert filter_files("/home/mvdoc/dbic/run_func_meh/012343143.dcm") def test_md5sum() -> None: assert md5sum("cryptonomicon") == "1cd52edfa41af887e14ae71d1db96ad1" assert md5sum("mysecretmessage") == "07989808231a0c6f522f9d8e34695794" def test_fix_canceled_runs() -> None: class FakeSeqInfo(NamedTuple): accession_number: str series_id: str protocol_name: str series_description: str seqinfo: list[FakeSeqInfo] = [] runname = "func_run+" for i in range(1, 6): seqinfo.append( FakeSeqInfo("accession1", "{0:02d}-".format(i) + runname, runname, runname) ) fake_accession2run = {"accession1": ["^01-", "^03-"]} with patch.object(reproin, "fix_accession2run", fake_accession2run): seqinfo_ = fix_canceled_runs(seqinfo) # type: ignore[arg-type] for i, s in enumerate(seqinfo_, 1): output = runname if i == 1 or i == 3: output = "cancelme_" + output for key in ["series_description", "protocol_name"]: value = getattr(s, key) assert value == output # check we didn't touch series_id assert s.series_id == "{0:02d}-".format(i) + runname def test_fix_dbic_protocol() -> None: accession_number = "A003" seq1 = FakeSeqInfo( accession_number, "mystudy", "02-anat-scout_run+_MPR_sag", "11-func_run-life2_acq-2mm692", ) seq2 = FakeSeqInfo(accession_number, "mystudy", "nochangeplease", "nochangeeither") seqinfos = [seq1, seq2] protocols2fix = { md5sum("mystudy"): [ (r"scout_run\+", "THESCOUT-runX"), ("run-life[0-9]", "run+_task-life"), ], re.compile("^my.*"): [("THESCOUT-runX", "THESCOUT")], # rely on 'catch-all' to fix up above scout "": [("THESCOUT", "scout")], } with patch.object(reproin, "protocols2fix", protocols2fix), patch.object( reproin, "series_spec_fields", ["field1"] ): seqinfos_ = fix_dbic_protocol(seqinfos) # type: ignore[arg-type] assert seqinfos[1] == seqinfos_[1] # type: ignore[comparison-overlap] # field2 shouldn't have changed since I didn't pass it assert seqinfos_[0] == FakeSeqInfo( # type: ignore[comparison-overlap] accession_number, "mystudy", "02-anat-scout_MPR_sag", seq1.field2 ) # change also field2 please with patch.object(reproin, "protocols2fix", protocols2fix), patch.object( reproin, "series_spec_fields", ["field1", "field2"] ): seqinfos_ = fix_dbic_protocol(seqinfos) # type: ignore[arg-type] assert seqinfos[1] == seqinfos_[1] # type: ignore[comparison-overlap] # now everything should have changed assert seqinfos_[0] == FakeSeqInfo( # type: ignore[comparison-overlap] accession_number, "mystudy", "02-anat-scout_MPR_sag", "11-func_run+_task-life_acq-2mm692", ) def test_sanitize_str() -> None: assert sanitize_str("super@duper.faster") == "superduperfaster" assert sanitize_str("perfect") == "perfect" assert sanitize_str("never:use:colon:!") == "neverusecolon" def test_fixupsubjectid() -> None: assert fixup_subjectid("abra") == "abra" assert fixup_subjectid("sub") == "sub" assert fixup_subjectid("sid") == "sid" assert fixup_subjectid("sid000030") == "sid000030" assert fixup_subjectid("sid0000030") == "sid000030" assert fixup_subjectid("sid00030") == "sid000030" assert fixup_subjectid("sid30") == "sid000030" assert fixup_subjectid("SID30") == "sid000030" def test_parse_series_spec() -> None: pdpn = parse_series_spec assert pdpn("nondbic_func-bold") == {} assert pdpn("cancelme_func-bold") == {} assert ( pdpn("bids_func-bold") == pdpn("func-bold") == {"datatype": "func", "datatype_suffix": "bold"} ) # pdpn("bids_func_ses+_task-boo_run+") == \ # order and PREFIX: should not matter, as well as trailing spaces assert ( pdpn(" PREFIX:bids_func_ses+_task-boo_run+ ") == pdpn("PREFIX:bids_func_ses+_task-boo_run+") == pdpn("WIP func_ses+_task-boo_run+") == pdpn("bids_func_ses+_run+_task-boo") == { "datatype": "func", # 'datatype_suffix': 'bold', "session": "+", "run": "+", "task": "boo", } ) # TODO: fix for that assert ( pdpn("bids_func-pace_ses-1_task-boo_acq-bu_bids-please_run-2__therest") == pdpn("bids_func-pace_ses-1_run-2_task-boo_acq-bu_bids-please__therest") == pdpn("func-pace_ses-1_task-boo_acq-bu_bids-please_run-2") == { "datatype": "func", "datatype_suffix": "pace", "session": "1", "run": "2", "task": "boo", "acq": "bu", "bids": "bids-please", } ) assert pdpn("bids_anat-scout_ses+") == { "datatype": "anat", "datatype_suffix": "scout", "session": "+", } assert pdpn("anat_T1w_acq-MPRAGE_run+") == { "datatype": "anat", "run": "+", "acq": "MPRAGE", "datatype_suffix": "T1w", } # Check for currently used {date}, which should also should get adjusted # from (date) since Philips does not allow for {} assert ( pdpn("func_ses-{date}") == pdpn("func_ses-(date)") == {"datatype": "func", "session": "{date}"} ) # Siemens X60 does not allow {} in names, so also support uppercase "DATE" assert pdpn("func_ses-DATE") == {"datatype": "func", "session": "DATE"} assert pdpn("fmap_dir-AP_ses-01") == { "datatype": "fmap", "session": "01", "dir": "AP", } assert pdpn("func-bold_rec-norm_dir-AP") == { "datatype": "func", "datatype_suffix": "bold", "rec": "norm", "dir": "AP", } assert pdpn("fmap_rec-norm_dir-AP_acq-3mm") == { "datatype": "fmap", "rec": "norm", "dir": "AP", "acq": "3mm", } def test_get_unique() -> None: accession_number = "A003" acqs = [ FakeSeqInfo(accession_number, "mystudy", "nochangeplease", "nochangeeither"), FakeSeqInfo(accession_number, "mystudy2", "nochangeplease", "nochangeeither"), ] assert get_unique(acqs, "accession_number") == accession_number # type: ignore[arg-type] with pytest.raises(AssertionError) as ce: get_unique(acqs, "study_description") # type: ignore[arg-type] assert ( str(ce.value) == "Was expecting a single value for attribute 'study_description' but got: mystudy, mystudy2" ) @pytest.mark.parametrize( "ses_spec,expected_session", [ # ses-DATE (Siemens X60 workaround) should resolve to acquisition date ("DATE", "20240115"), # ses-{date} (original syntax) should resolve to acquisition date ("{date}", "20240115"), # ses-date (lowercase) should be treated as session name 'date' ("date", "date"), # ses-datelike should NOT expand - only exact "DATE" is special ("datelike", "datelike"), # explicit session should pass through unchanged ("01", "01"), ], ) def test_infotoids_ses_date(ses_spec: str, expected_session: str) -> None: """Test session resolution in infotoids for various ses- specifications.""" seqinfo = SeqInfo( total_files_till_now=1, example_dcm_file="/path/to/dcm", series_id=f"1-anat-scout_ses-{ses_spec}", dcm_dir_name=f"1-anat-scout_ses-{ses_spec}", series_files=1, unspecified="", dim1=256, dim2=256, dim3=176, dim4=1, TR=2.0, TE=3.0, protocol_name=f"anat-scout_ses-{ses_spec}", is_motion_corrected=False, is_derived=False, patient_id="phantom01", study_description="PI^study", referring_physician_name="", series_description=f"anat-scout_ses-{ses_spec}", sequence_name="", image_type=("ORIGINAL", "PRIMARY"), accession_number="A001", patient_age="030Y", patient_sex="O", date="20240115", series_uid="1.2.3.4", time="120000", custom=None, ) # outdir not accessed for explicit session values (only for +/= markers) result = infotoids([seqinfo], "/dev/null/output") assert result["session"] == expected_session @pytest.mark.parametrize("moco", [True, False]) @pytest.mark.parametrize("additional_rec", ["_rec-norm", ""]) def test_infotodict_entity_ordering(moco: bool, additional_rec: str) -> None: """Test that entities in the generated filename follow BIDS ordering (task, acq, rec, dir, run, suffix) regardless of input order.""" # Deliberately scramble entity order in the protocol name scrambled = f"func-bold_dir-AP_run-1{additional_rec}_task-rest_acq-mb4" seqinfo = SeqInfo( total_files_till_now=1, example_dcm_file="/path/to/dcm", series_id=f"1-{scrambled}", dcm_dir_name=f"1-{scrambled}", series_files=1, unspecified="", dim1=64, dim2=64, dim3=40, dim4=100, TR=2.0, TE=30.0, protocol_name=scrambled, is_motion_corrected=moco, is_derived=False, patient_id="sub01", study_description="PI^study", referring_physician_name="", series_description=scrambled, sequence_name="", image_type=("ORIGINAL", "PRIMARY", "FMRI"), accession_number="A001", patient_age="030Y", patient_sex="M", date="20240115", series_uid="1.2.3.4", time="120000", custom=None, ) if moco and additional_rec == '': result = reproin.infotodict([seqinfo]) templates = [key[0] for key in result] assert len(templates) == 1 template = templates[0] # Verify rec-moco has been added in the generated filename expected_order = ["task-rest", "acq-mb4", "rec-moco", "dir-AP", "run-01", "bold"] positions = [template.index(e) for e in expected_order] assert positions == sorted(positions), ( f"Entities not in BIDS order in {template!r} or rec-moco missing" ) pass if moco and additional_rec != '': with pytest.raises(NotImplementedError) as ce: reproin.infotodict([seqinfo]) # "want to add _rec-moco but there is _rec- already" assert str(ce.value) == "want to add _rec-moco but there is _rec- already" if not moco and additional_rec == '': # nothing new to test for this combination return if not moco and additional_rec != '': result = reproin.infotodict([seqinfo]) templates = [key[0] for key in result] assert len(templates) == 1 template = templates[0] # Verify entities appear in BIDS order in the generated filename expected_order = ["task-rest", "acq-mb4", "rec-norm", "dir-AP", "run-01", "bold"] positions = [template.index(e) for e in expected_order] assert positions == sorted(positions), ( f"Entities not in BIDS order in {template!r}" ) nipy-heudiconv-217744b/heudiconv/heuristics/uc_bids.py000066400000000000000000000066441517415366200231030ustar00rootroot00000000000000from __future__ import annotations from typing import Optional from heudiconv.utils import SeqInfo def create_key( template: Optional[str], outtype: tuple[str, ...] = ("nii.gz",), annotation_classes: None = None, ) -> tuple[str, tuple[str, ...], None]: if template is None or not template: raise ValueError("Template must be a valid format string") return (template, outtype, annotation_classes) def infotodict( seqinfo: list[SeqInfo], ) -> dict[tuple[str, tuple[str, ...], None], list]: """Heuristic evaluator for determining which runs belong where allowed template fields - follow python string module: item: index within category subject: participant id seqitem: run number during scanning subindex: sub index within group """ t1w = create_key("anat/sub-{subject}_T1w") t2w = create_key("anat/sub-{subject}_acq-{acq}_T2w") flair = create_key("anat/sub-{subject}_acq-{acq}_FLAIR") rest = create_key("func/sub-{subject}_task-rest_acq-{acq}_run-{item:02d}_bold") info: dict[tuple[str, tuple[str, ...], None], list] = { t1w: [], t2w: [], flair: [], rest: [], } for seq in seqinfo: x, _, z, n_vol, protocol, dcm_dir = ( seq.dim1, seq.dim2, seq.dim3, seq.dim4, seq.protocol_name, seq.dcm_dir_name, ) # t1_mprage --> T1w if ( (z == 160) and (n_vol == 1) and ("t1_mprage" in protocol) and ("XX" not in dcm_dir) ): info[t1w] = [seq.series_id] # t2_tse --> T2w if ( (z == 35) and (n_vol == 1) and ("t2_tse" in protocol) and ("XX" not in dcm_dir) ): info[t2w].append({"item": seq.series_id, "acq": "TSE"}) # T2W --> T2w if ( (z == 192) and (n_vol == 1) and ("T2W" in protocol) and ("XX" not in dcm_dir) ): info[t2w].append({"item": seq.series_id, "acq": "highres"}) # t2_tirm --> FLAIR if ( (z == 35) and (n_vol == 1) and ("t2_tirm" in protocol) and ("XX" not in dcm_dir) ): info[flair].append({"item": seq.series_id, "acq": "TIRM"}) # t2_flair --> FLAIR if ( (z == 160) and (n_vol == 1) and ("t2_flair" in protocol) and ("XX" not in dcm_dir) ): info[flair].append({"item": seq.series_id, "acq": "highres"}) # T2FLAIR --> FLAIR if ( (z == 192) and (n_vol == 1) and ("T2-FLAIR" in protocol) and ("XX" not in dcm_dir) ): info[flair].append({"item": seq.series_id, "acq": "highres"}) # EPI (physio-matched) --> bold if ( (x == 128) and (z == 28) and (n_vol == 300) and ("EPI" in protocol) and ("XX" not in dcm_dir) ): info[rest].append({"item": seq.series_id, "acq": "128px"}) # EPI (physio-matched_NEW) --> bold if ( (x == 64) and (z == 34) and (n_vol == 300) and ("EPI" in protocol) and ("XX" not in dcm_dir) ): info[rest].append({"item": seq.series_id, "acq": "64px"}) return info nipy-heudiconv-217744b/heudiconv/info.py000066400000000000000000000027171517415366200202410ustar00rootroot00000000000000__author__ = "HeuDiConv team and contributors" __url__ = "https://github.com/nipy/heudiconv" __packagename__ = "heudiconv" __description__ = "Heuristic DICOM Converter" __license__ = "Apache 2.0" __longdesc__ = """Convert DICOM dirs based on heuristic info - HeuDiConv uses the dcmstack package and dcm2niix tool to convert DICOM directories or tarballs into collections of NIfTI files following pre-defined heuristic(s).""" CLASSIFIERS = [ "Environment :: Console", "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering", "Typing :: Typed", ] PYTHON_REQUIRES = ">=3.9" REQUIRES = [ # not usable in some use cases since might be just a downloader, not binary # 'dcm2niix', "dcmstack>=0.8", "etelemetry", "filelock>=3.0.12", "nibabel>=5.3.1", "nipype >=1.2.3", "pydicom >= 1.0.0", ] TESTS_REQUIRES = [ "pytest", "tinydb", "inotify", ] MIN_DATALAD_VERSION = "0.13.0" EXTRA_REQUIRES = { "tests": TESTS_REQUIRES, "extras": [ "duecredit", # optional dependency ], # Requires patched version ATM ['dcmstack'], "datalad": ["datalad >=%s" % MIN_DATALAD_VERSION], } # Flatten the lists EXTRA_REQUIRES["all"] = sum(EXTRA_REQUIRES.values(), []) nipy-heudiconv-217744b/heudiconv/main.py000066400000000000000000000457621517415366200202410ustar00rootroot00000000000000from __future__ import annotations from glob import glob import logging import os.path as op import sys from types import TracebackType from typing import Any, Optional from . import __packagename__, __version__ from .bids import populate_bids_templates, populate_intended_for, tuneup_bids_json_files from .convert import prep_conversion from .due import Doi, due from .parser import get_study_sessions from .queue import queue_conversion from .utils import SeqInfo, anonymize_sid, load_heuristic, sanitize_path, treat_infofile lgr = logging.getLogger(__name__) INIT_MSG = "Running {packname} version {version} latest {latest}".format def is_interactive() -> bool: """Return True if all in/outs are tty""" # TODO: check on windows if hasattr check would work correctly and add value: return sys.stdin.isatty() and sys.stdout.isatty() and sys.stderr.isatty() def setup_exceptionhook() -> None: """ Overloads default sys.excepthook with our exceptionhook handler. If interactive, our exceptionhook handler will invoke pdb.post_mortem; if not interactive, then invokes default handler. """ def _pdb_excepthook( exc_type: type[BaseException], exc_value: BaseException, tb: Optional[TracebackType], ) -> None: if is_interactive(): import pdb import traceback traceback.print_exception(exc_type, exc_value, tb) # print() pdb.post_mortem(tb) else: lgr.warning("We cannot setup exception hook since not in interactive mode") sys.excepthook = _pdb_excepthook def process_extra_commands( outdir: str, command: str, files: Optional[list[str]], heuristic: Optional[str], session: Optional[str], subjs: Optional[list[str]], grouping: str, ) -> None: """ Perform custom command instead of regular operations. Supported commands: ['treat-json', 'ls', 'populate-templates', 'populate-intended-for'] Parameters ---------- outdir : str Output directory command : {'treat-json', 'ls', 'populate-templates', 'populate-intended-for'} Heudiconv command to run files : list of str or None List of files if command needs/expects it heuristic : str or None Path to heuristic file or name of builtin heuristic. session : str or None Session identifier subjs : None or list of str List of subject identifiers grouping : {'studyUID', 'accession_number', 'all', 'custom'} How to group dicoms. """ def ensure_has_files() -> None: if files is None: raise ValueError(f"command {command} expects --files being provided") if command == "treat-jsons": ensure_has_files() assert files is not None # for mypy now for fname in files: treat_infofile(fname) elif command == "ls": ensure_heuristic_arg(heuristic) assert heuristic is not None ensure_has_files() assert files is not None # for mypy now heuristic_mod = load_heuristic(heuristic) heuristic_ls = getattr(heuristic_mod, "ls", None) for fname in files: study_sessions = get_study_sessions( None, [fname], heuristic_mod, outdir, session, subjs, grouping=grouping, ) print(fname) for study_session, sequences in study_sessions.items(): assert isinstance(sequences, dict) suf = "" if heuristic_ls: suf += heuristic_ls(study_session, list(sequences.keys())) print("\t%s %d sequences%s" % (str(study_session), len(sequences), suf)) elif command == "populate-templates": ensure_heuristic_arg(heuristic) assert heuristic is not None ensure_has_files() assert files is not None # for mypy now heuristic_mod = load_heuristic(heuristic) for fname in files: populate_bids_templates(fname, getattr(heuristic_mod, "DEFAULT_FIELDS", {})) elif command == "sanitize-jsons": ensure_has_files() assert files is not None # for mypy now tuneup_bids_json_files(files) elif command == "heuristics": from .utils import get_known_heuristics_with_descriptions for name_desc in get_known_heuristics_with_descriptions().items(): print("- %s: %s" % name_desc) elif command == "heuristic-info": ensure_heuristic_arg(heuristic) assert heuristic is not None from .utils import get_heuristic_description print(get_heuristic_description(heuristic, full=True)) elif command == "populate-intended-for": kwargs: dict[str, Any] = {} if heuristic: heuristic_mod = load_heuristic(heuristic) kwargs = getattr(heuristic_mod, "POPULATE_INTENDED_FOR_OPTS", {}) if not subjs: subjs = [ # search outdir for 'sub-*'; if it is a directory (not a regular file), remove # the initial 'sub-': op.basename(s)[len("sub-") :] for s in glob(op.join(outdir, "sub-*")) if op.isdir(s) ] # read the subjects from the participants.tsv file to compare: participants_tsv = op.join(outdir, "participants.tsv") if op.lexists(participants_tsv): with open(participants_tsv, "r") as f: # read header line and find index for 'participant_id': participant_id_index = ( f.readline().split("\t").index("participant_id") ) # read all participants, removing the initial 'sub-': known_subjects = [ ln.split("\t")[participant_id_index][len("sub-") :] for ln in f.readlines() ] if not set(subjs) == set(known_subjects): # issue a warning, but continue with the 'subjs' list (the subjects for # which there is data): lgr.warning( "'participants.tsv' contents are not identical to subjects found " "in the BIDS dataset %s", outdir, ) for subj in subjs: subject_path = op.join(outdir, "sub-" + subj) if session: session_paths = [op.join(subject_path, "ses-" + session)] else: # check to see if the data for this subject is organized by sessions; if not # just use the subject_path session_paths = [ s for s in glob(op.join(subject_path, "ses-*")) if op.isdir(s) ] or [subject_path] for session_path in session_paths: populate_intended_for(session_path, **kwargs) else: raise ValueError("Unknown command %s" % command) def ensure_heuristic_arg(heuristic: Optional[str] = None) -> None: """ Check that the heuristic argument was provided. """ from .utils import get_known_heuristic_names if not heuristic: raise ValueError( "Specify heuristic using -f. Known are: %s" % ", ".join(get_known_heuristic_names()) ) @due.dcite( Doi("10.5281/zenodo.1012598"), path="heudiconv", description="Flexible DICOM converter for organizing brain imaging data", version=__version__, cite_module=True, ) def workflow( *, dicom_dir_template: Optional[str] = None, files: Optional[list[str]] = None, subjs: Optional[list[str]] = None, converter: str = "dcm2niix", outdir: str = ".", locator: Optional[str] = None, conv_outdir: Optional[str] = None, anon_cmd: Optional[str] = None, heuristic: Optional[str] = None, with_prov: bool = False, session: Optional[str] = None, bids_options: Optional[str] = None, overwrite: bool = False, datalad: bool = False, debug: bool = False, command: Optional[str] = None, grouping: str = "studyUID", minmeta: bool = False, random_seed: Optional[int] = None, dcmconfig: Optional[str] = None, queue: Optional[str] = None, queue_args: Optional[str] = None, ) -> None: """Run the HeuDiConv conversion workflow. Parameters ---------- dicom_dir_template : str or None, optional Location of dicomdir that can be indexed with subject id {subject} and session {session}. Tarballs (can be compressed) are supported in addition to directory. All matching tarballs for a subject are extracted and their content processed in a single pass. If multiple tarballs are found, each is assumed to be a separate session and the 'session' argument is ignored. Mutually exclusive with 'files'. Default is None. files : list or None, optional Files (tarballs, dicoms) or directories containing files to process. Mutually exclusive with 'dicom_dir_template'. Default is None. subjs : list or None, optional List of subjects - required for dicom template. If not provided, DICOMS would first be "sorted" and subject IDs deduced by the heuristic. Default is None. converter : {'dcm2niix', 'none'}, optional Tool to use for DICOM conversion. Setting to 'none' disables the actual conversion step -- useful for testing heuristics. Default is 'dcm2niix'. outdir : str, optional Output directory for conversion setup (for further customization and future reference. This directory will refer to non-anonymized subject IDs. Default is '.' (current working directory). locator : str or 'unknown' or None, optional Study path under outdir. If provided, it overloads the value provided by the heuristic. If 'datalad=True', every directory within locator becomes a super-dataset thus establishing a hierarchy. Setting to "unknown" will skip that dataset. Default is None. conv_outdir : str or None, optional Output directory for converted files. By default this is identical to --outdir. This option is most useful in combination with 'anon_cmd'. Default is None. anon_cmd : str or None, optional Command to run to convert subject IDs used for DICOMs to anonymized IDs. Such command must take a single argument and return a single anonymized ID. Also see 'conv_outdir'. Default is None. heuristic : str or None, optional Name of a known heuristic or path to the Python script containing heuristic. Default is None. with_prov : bool, optional Store additional provenance information. Requires python-rdflib. Default is False. session : str or None, optional Session for longitudinal study_sessions. Default is None. bids_options : str or None, optional Flag for output into BIDS structure. Can also take BIDS- specific options, e.g., --bids notop. The only currently supported options is "notop", which skips creation of top-level BIDS files. This is useful when running in batch mode to prevent possible race conditions. Default is None. overwrite : bool, optional Overwrite existing converted files. Default is False. datalad : bool, optional Store the entire collection as DataLad dataset(s). Small files will be committed directly to git, while large to annex. New version (6) of annex repositories will be used in a "thin" mode so it would look to mortals as just any other regular directory (i.e. no symlinks to under .git/annex). For now just for BIDS mode. Default is False. debug : bool, optional Do not catch exceptions and show exception traceback. Default is False. command : {'heuristics', 'heuristic-info', 'ls', 'populate-templates', 'sanitize-jsons', 'treat-jsons', 'populate-intended-for', None}, optional Custom action to be performed on provided files instead of regular operation. Default is None. grouping : {'studyUID', 'accession_number', 'all', 'custom'}, optional How to group dicoms. Default is 'studyUID'. minmeta : bool, optional Exclude dcmstack meta information in sidecar jsons. Default is False. random_seed : int or None, optional Random seed to initialize RNG. Default is None. dcmconfig : str or None, optional JSON file for additional dcm2niix configuration. Default is None. queue : {'SLURM', None}, optional Batch system to submit jobs in parallel. Default is None. If set, will cause scheduling of conversion and return without performing any further action. queue_args : str or None, optional Additional queue arguments passed as single string of space-separated Argument=Value pairs. Default is None. Notes ----- All parameters in this function must be called as keyword arguments. """ # To be done asap so anything random is deterministic if random_seed is not None: import random random.seed(random_seed) import numpy numpy.random.seed(random_seed) # Ensure only supported bids options are passed if debug: lgr.setLevel(logging.DEBUG) # Should be possible but only with a single subject -- will be used to # override subject deduced from the DICOMs if files and subjs and len(subjs) > 1: raise ValueError("Unable to processes multiple `--subjects` with files") if debug: setup_exceptionhook() # Deal with provided files or templates # pre-process provided list of files and possibly sort into groups/sessions # Group files per each study/sid/session outdir = op.abspath(outdir) latest = None try: import etelemetry latest = etelemetry.get_project("nipy/heudiconv") except Exception as e: lgr.warning("Could not check for version updates: %s", str(e)) lgr.info( INIT_MSG( packname=__packagename__, version=__version__, latest=(latest or {}).get("version", "Unknown"), ) ) if command: if dicom_dir_template: lgr.warning( f"DICOM directory template {dicom_dir_template!r} was provided but will be ignored since " f"commands do not care about it ATM" ) process_extra_commands( outdir, command, files, heuristic, session, subjs, grouping, ) return # # Load heuristic -- better do it asap to make sure it loads correctly # if not heuristic: raise RuntimeError("No heuristic specified - add to arguments and rerun") if queue: lgr.info("Queuing %s conversion", queue) if files: iterarg = "files" iterables = len(files) elif subjs: iterarg = "subjects" iterables = len(subjs) else: raise ValueError("'queue' given but both 'files' and 'subjects' are false") queue_conversion(queue, iterarg, iterables, queue_args) return heuristic_mod = load_heuristic(heuristic) study_sessions = get_study_sessions( dicom_dir_template, files, heuristic_mod, outdir, session, subjs, grouping=grouping, ) # extract tarballs, and replace their entries with expanded lists of files # TODO: we might need to sort so sessions are ordered??? lgr.info("Need to process %d study sessions", len(study_sessions)) # processed_studydirs = set() locator_manual, session_manual = locator, session for (locator, session_, sid), files_or_seqinfo in study_sessions.items(): # Allow for session to be overloaded from command line if session_manual is not None: session_ = session_manual if locator_manual is not None: locator = locator_manual if not len(files_or_seqinfo): raise ValueError("nothing to process?") # that is how life is ATM :-/ since we don't do sorting if subj # template is provided if isinstance(files_or_seqinfo, dict): assert isinstance(list(files_or_seqinfo.keys())[0], SeqInfo) dicoms = None seqinfo = files_or_seqinfo else: dicoms = files_or_seqinfo seqinfo = None if locator == "unknown": lgr.warning("Skipping unknown locator dataset") continue if locator: locator = sanitize_path(locator, "locator") if anon_cmd and sid is not None: anon_sid = anonymize_sid(sid, anon_cmd) lgr.info("Anonymized {} to {}".format(sid, anon_sid)) else: anon_sid = None study_outdir = op.join(outdir, locator or "") anon_outdir = conv_outdir or outdir anon_study_outdir = op.join(anon_outdir, locator or "") if datalad: from .external.dlad import prepare_datalad dlad_sid = sid if not anon_sid else anon_sid dl_msg = prepare_datalad( anon_study_outdir, anon_outdir, dlad_sid, session_, seqinfo, dicoms, bids_options, ) lgr.info( "PROCESSING STARTS: {0}".format( str(dict(subject=sid, outdir=study_outdir, session=session_)) ) ) prep_conversion( sid, dicoms, study_outdir, heuristic_mod, converter=converter, anon_sid=anon_sid, anon_outdir=anon_study_outdir, with_prov=with_prov, ses=session_, bids_options=bids_options, seqinfo=seqinfo, min_meta=minmeta, overwrite=overwrite, dcmconfig=dcmconfig, grouping=grouping, ) lgr.info( "PROCESSING DONE: {0}".format( str(dict(subject=sid, outdir=study_outdir, session=session_)) ) ) if datalad: from .external.dlad import add_to_datalad msg = "Converted subject %s" % dl_msg # TODO: whenever propagate to supers work -- do just # ds.save(msg=msg) # also in batch mode might fail since we have no locking ATM # and theoretically no need actually to save entire study # we just need that add_to_datalad(outdir, study_outdir, msg, bids_options) # if bids: # # Let's populate BIDS templates for folks to take care about # for study_outdir in processed_studydirs: # populate_bids_templates(study_outdir) # # TODO: record_collection of the sid/session although that information # is pretty much present in .heudiconv/SUBJECT/info so we could just poke there nipy-heudiconv-217744b/heudiconv/parser.py000066400000000000000000000271521517415366200206020ustar00rootroot00000000000000from __future__ import annotations import atexit from collections import defaultdict from collections.abc import ItemsView, Iterable, Iterator from glob import glob import logging import os import os.path as op import re import shutil import sys from types import ModuleType from typing import Optional from .dicoms import group_dicoms_into_seqinfos from .utils import SeqInfo, StudySessionInfo, TempDirs, docstring_parameter lgr = logging.getLogger(__name__) tempdirs = TempDirs() # Ensure they are cleaned up upon exit atexit.register(tempdirs.cleanup) _VCS_REGEX = r"%s\.(?:git|gitattributes|svn|bzr|hg)(?:%s|$)" % (op.sep, op.sep) def _get_unpack_formats() -> dict[str, bool]: """For each extension return if it is a tar""" out = {} for _, exts, d in shutil.get_unpack_formats(): for e in exts: out[e] = bool(re.search(r"\btar\b", d.lower())) return out _UNPACK_FORMATS = _get_unpack_formats() _TAR_UNPACK_FORMATS = tuple(k for k, is_tar in _UNPACK_FORMATS.items() if is_tar) @docstring_parameter(_VCS_REGEX) def find_files( regex: str, topdir: list[str] | tuple[str, ...] | str = op.curdir, exclude: Optional[str] = None, exclude_vcs: bool = True, dirs: bool = False, ) -> Iterator[str]: """Generator to find files matching regex Parameters ---------- regex: string exclude: string, optional Matches to exclude exclude_vcs: If True, excludes commonly known VCS subdirectories. If string, used as regex to exclude those files (regex: `{}`) topdir: string or list, optional Directory where to search dirs: bool, optional Either to match directories as well as files """ if isinstance(topdir, (list, tuple)): for topdir_ in topdir: yield from find_files( regex, topdir=topdir_, exclude=exclude, exclude_vcs=exclude_vcs, dirs=dirs, ) return for dirpath, dirnames, filenames in os.walk(topdir): names = (dirnames + filenames) if dirs else filenames paths = (op.join(dirpath, name) for name in names) for path in filter(re.compile(regex).search, paths): path = path.rstrip(op.sep) if exclude and re.search(exclude, path): continue if exclude_vcs and re.search(_VCS_REGEX, path): continue yield path def get_extracted_dicoms(fl: Iterable[str]) -> ItemsView[Optional[str], list[str]]: """Given a collection of files and/or directories, list out and possibly extract the contents from archives. Parameters ---------- fl Files (possibly archived) to process. Returns ------- ItemsView[str | None, list[str]] The absolute paths of (possibly newly extracted) files. Notes ----- For 'classical' heudiconv, if multiple archives are provided, they correspond to different sessions, so here we would group into sessions and return pairs `sessionid`, `files` with `sessionid` being None if no "sessions" detected for that file or there was just a single tarball in the list. When contents of fl appear to be an unpackable archive, the contents are extracted into utils.TempDirs(f'heudiconvDCM') and the mode of all extracted files is set to 700. When contents of fl are a list of unarchived files, they are treated as a single session. When contents of fl are a list of unarchived and archived files, the unarchived files are grouped into a single session (key: None). If there is only one archived file, the contents of that file are grouped with the unarchived file. If there are multiple archived files, they are grouped into separate sessions. """ sessions: dict[Optional[str], list[str]] = defaultdict(list) # keep track of session manually to ensure that the variable is bound # when it is used after the loop (e.g., consider situation with # fl being empty) session = 0 # needs sorting to keep the generated "session" label deterministic for _, t in enumerate(sorted(fl)): if not t.endswith(tuple(_UNPACK_FORMATS)): sessions[None].append(t) continue # Each file extracted must be associated with the proper session, # but the high-level shutil does not have a way to list the files # contained within each archive. So, files are temporarily # extracted into unique tempdirs # cannot use TempDirs since will trigger cleanup with __del__ tmpdir = tempdirs(prefix="heudiconvDCM") # check content and sanitize permission bits before extraction os.chmod(tmpdir, mode=0o700) # For tar (only!) starting with 3.12 we should provide filter # (enforced in 3.14) on how to filter/safe-guard filenames. kws: dict[str, str] = {} if sys.version_info >= (3, 12) and t.endswith(_TAR_UNPACK_FORMATS): # Allow for a user-workaround if would be desired # see e.g. https://docs.python.org/3.12/library/tarfile.html#extraction-filters kws["filter"] = os.environ.get("HEUDICONV_TAR_FILTER", "tar") shutil.unpack_archive(t, extract_dir=tmpdir, **kws) # type: ignore[arg-type] archive_content = list(find_files(regex=".*", topdir=tmpdir)) # may be too cautious (tmpdir is already 700). for f in archive_content: os.chmod(f, mode=0o700) # store full paths to each file, so we don't need to drag along # tmpdir as some basedir sessions[str(session)] = archive_content session += 1 if session == 1: # we had only 1 session (and at least 1), so not really multiple # sessions according to classical 'heudiconv' assumptions, thus # just move them all into None sessions[None] += sessions.pop("0") return sessions.items() def get_study_sessions( dicom_dir_template: Optional[str], files_opt: Optional[list[str]], heuristic: ModuleType, outdir: str, session: Optional[str], sids: Optional[list[str]], grouping: str = "studyUID", ) -> dict[StudySessionInfo, list[str] | dict[SeqInfo, list[str]]]: """Sort files or dicom seqinfos into study_sessions. study_sessions put together files for a single session of a subject in a study. Two major possible workflows: - if dicom_dir_template provided -- doesn't pre-load DICOMs and just loads files pointed by each subject and possibly sessions as corresponding to different tarballs. - if files_opt is provided, sorts all DICOMs it can find under those paths """ study_sessions: dict[StudySessionInfo, list[str] | dict[SeqInfo, list[str]]] = {} if dicom_dir_template: dicom_dir_template = op.abspath(dicom_dir_template) # MG - should be caught by earlier checks # assert not files_opt # see above TODO assert sids # expand the input template if "{subject}" not in dicom_dir_template: raise ValueError( "dicom dir template must have {subject} as a placeholder for a " "subject id. Got %r" % dicom_dir_template ) for sid in sids: sdir = dicom_dir_template.format(subject=sid, session=session) for session_, files_ in get_extracted_dicoms(sorted(glob(sdir))): if session_ is not None and session: lgr.warning( "We had session specified (%s) but while analyzing " "files got a new value %r (using it instead)" % (session, session_) ) # in this setup we do not care about tracking "studies" so # locator would be the same None study_sessions[ StudySessionInfo( None, session_ if session_ is not None else session, sid ) ] = files_ else: # MG - should be caught on initial run # YOH - what if it is the initial run? # prep files assert files_opt files: list[str] = [] for f in files_opt: if op.isdir(f): files += sorted( find_files(".*", topdir=f, exclude_vcs=True, exclude=r"/\.datalad/") ) else: files.append(f) # in this scenario we don't care about sessions obtained this way extracted_files: list[str] = [] for _, files_ex in get_extracted_dicoms(files): extracted_files += files_ex # sort all DICOMS using heuristic seqinfo_dict = group_dicoms_into_seqinfos( extracted_files, grouping, file_filter=getattr(heuristic, "filter_files", None), dcmfilter=getattr(heuristic, "filter_dicom", None), custom_grouping=getattr(heuristic, "grouping", None), custom_seqinfo=getattr(heuristic, "custom_seqinfo", None), ) if sids: if len(sids) != 1: raise RuntimeError( "We were provided some subjects (%s) but " "we can deal only " "with overriding only 1 subject id. Got %d subjects and " "found %d sequences" % (sids, len(sids), len(seqinfo_dict)) ) sid = sids[0] else: sid = None if not getattr(heuristic, "infotoids", None): # allow bypass with subject override if not sid: raise NotImplementedError( "Cannot guarantee subject id - add " "`infotoids` to heuristic file or " "provide `--subjects` option" ) lgr.info( "Heuristic is missing an `infotoids` method, assigning " "empty method and using provided subject id %s. " "Provide `session` and `locator` fields for best results.", sid, ) def infotoids( seqinfos: Iterable[SeqInfo], outdir: str # noqa: U100 ) -> dict[str, Optional[str]]: return {"locator": None, "session": None, "subject": None} heuristic.infotoids = infotoids # type: ignore[attr-defined] for _studyUID, seqinfo in seqinfo_dict.items(): # so we have a single study, we need to figure out its # locator, session, subject # TODO: Try except to ignore those we can't handle? # actually probably there should be a dedicated exception for # heuristics to throw if they detect that the study they are given # is not the one they would be willing to work on ids = heuristic.infotoids(seqinfo.keys(), outdir=outdir) # TODO: probably infotoids is doomed to do more and possibly # split into multiple sessions!!!! but then it should be provided # full seqinfo with files which it would place into multiple groups study_session_info = StudySessionInfo( ids.get("locator"), ids.get("session", session) or session, sid or ids.get("subject", None), ) lgr.info("Study session for %r", study_session_info) if grouping != "all": assert study_session_info not in study_sessions, ( f"Existing study session {study_session_info} " f"already in analyzed sessions {study_sessions.keys()}" ) study_sessions[study_session_info] = seqinfo return study_sessions nipy-heudiconv-217744b/heudiconv/py.typed000066400000000000000000000000001517415366200204120ustar00rootroot00000000000000nipy-heudiconv-217744b/heudiconv/queue.py000066400000000000000000000064101517415366200204240ustar00rootroot00000000000000from __future__ import annotations import logging import os import subprocess import sys from typing import Optional from nipype.utils.filemanip import which lgr = logging.getLogger(__name__) def queue_conversion( queue: str, iterarg: str, iterables: int, queue_args: Optional[str] = None ) -> None: """ Write out conversion arguments to file and submit to a job scheduler. Parses `sys.argv` for heudiconv arguments. Parameters ---------- queue: string Batch scheduler to use iterarg: str Multi-argument to index (`subjects` OR `files`) iterables: int Number of `iterarg` arguments queue_args: string (optional) Additional queue arguments for job submission """ SUPPORTED_QUEUES = {"SLURM": "sbatch"} if queue not in SUPPORTED_QUEUES: raise NotImplementedError("Queuing with %s is not supported", queue) for i in range(iterables): args = clean_args(sys.argv[1:], iterarg, i) # make arguments executable heudiconv_exec = which("heudiconv") or "heudiconv" args.insert(0, heudiconv_exec) convertcmd = " ".join(args) # will overwrite across subjects queue_file = os.path.abspath("heudiconv-%s.sh" % queue) with open(queue_file, "wt") as fp: fp.write("#!/bin/bash\n") if queue_args: for qarg in queue_args.split(): fp.write("#SBATCH %s\n" % qarg) fp.write(convertcmd + "\n") cmd = [SUPPORTED_QUEUES[queue], queue_file] subprocess.call(cmd) lgr.info("Submitted %d jobs", iterables) def clean_args(hargs: list[str], iterarg: str, iteridx: int) -> list[str]: """ Filters arguments for batch submission. Parameters ---------- hargs: list Command-line arguments iterarg: str Multi-argument to index (`subjects` OR `files`) iteridx: int `iterarg` index to submit Returns ------- cmdargs : list Filtered arguments for batch submission Example -------- >>> from heudiconv.queue import clean_args >>> cmd = ['heudiconv', '-d', '/some/{subject}/path', ... '-q', 'SLURM', ... '-s', 'sub-1', 'sub-2', 'sub-3', 'sub-4'] >>> clean_args(cmd, 'subjects', 0) ['heudiconv', '-d', '/some/{subject}/path', '-s', 'sub-1'] """ if iterarg == "subjects": iterargs = ["-s", "--subjects"] elif iterarg == "files": iterargs = ["--files"] else: raise ValueError("Cannot index %s" % iterarg) # remove these or cause an infinite loop queue_args = ["-q", "--queue", "--queue-args"] # control variables for multi-argument parsing is_iterarg = False itercount = 0 indices = [] cmdargs = hargs[:] for i, arg in enumerate(hargs): if arg.startswith("-") and is_iterarg: # moving on to another argument is_iterarg = False if is_iterarg: if iteridx != itercount: indices.append(i) itercount += 1 if arg in iterargs: is_iterarg = True if arg in queue_args: indices.extend([i, i + 1]) for j in sorted(indices, reverse=True): del cmdargs[j] return cmdargs nipy-heudiconv-217744b/heudiconv/tests/000077500000000000000000000000001517415366200200675ustar00rootroot00000000000000nipy-heudiconv-217744b/heudiconv/tests/__init__.py000066400000000000000000000000001517415366200221660ustar00rootroot00000000000000nipy-heudiconv-217744b/heudiconv/tests/anonymize_script.py000077500000000000000000000006361517415366200240460ustar00rootroot00000000000000#! /usr/bin/env python3 import hashlib import re import sys def bids_id_(sid: str) -> str: m = re.compile(r"^(?:sub-|)(.+)$").search(sid) if m: parsed_id = m.group(1) return hashlib.md5(parsed_id.encode()).hexdigest()[:8] else: raise ValueError("invalid sid") def main() -> str: sid = sys.argv[1] return bids_id_(sid) if __name__ == "__main__": print(main()) nipy-heudiconv-217744b/heudiconv/tests/conftest.py000066400000000000000000000005011517415366200222620ustar00rootroot00000000000000import os import pytest @pytest.fixture(autouse=True, scope="session") def git_env() -> None: os.environ["GIT_AUTHOR_EMAIL"] = "maxm@example.com" os.environ["GIT_AUTHOR_NAME"] = "Max Mustermann" os.environ["GIT_COMMITTER_EMAIL"] = "maxm@example.com" os.environ["GIT_COMMITTER_NAME"] = "Max Mustermann" nipy-heudiconv-217744b/heudiconv/tests/data/000077500000000000000000000000001517415366200210005ustar00rootroot00000000000000nipy-heudiconv-217744b/heudiconv/tests/data/01-anat-scout/000077500000000000000000000000001517415366200232745ustar00rootroot00000000000000nipy-heudiconv-217744b/heudiconv/tests/data/01-anat-scout/0001.dcm000066400000000000000000005237661517415366200243640ustar00rootroot00000000000000DICMULOBUI1.2.840.10008.5.1.4.1.1.4UI41.3.12.2.1107.5.2.43.66112.2016101409252673867900728UI1.2.840.10008.1.2.1UI1.2.40.0.13.1.1SH dcm4che-2.0 CS ISO_IR 100CSORIGINAL\PRIMARY\M\ND\NORMDA20161014TM092530.706000 UI1.2.840.10008.5.1.4.1.1.4UI41.3.12.2.1107.5.2.43.66112.2016101409252673867900728 DA20161014!DA20161014"DA20161014#DA201610140TM092234.190000 1TM092530.687000 2TM092512.690000 3TM092530.706000 PSH phantom-1 `CSMRpLOSIEMENS LODartmouth College - PBS STMaynard 3,Hanover,NH,US,03755 PNSHAWP661120LOHalchenko_Yarik^950_bids_test4>LOanat-scout_ses-localizer@LO DepartmentPPNLOPrismaPNphantom1-sid1  LOphantom1-sid1 0DA19800801@CSO AS036Y DS 1.828803660DS 90.71848554 CSBRAIN  CSGR!CSSP"CSPFP #CS3D$SH *fl3d1_ns %CSN PDS1.6000000238419 DS3.15DS1.37DS1 DS 123.252622SH1HIS1 DS3 IS118 IS1 DS100 DS100 DS540 LO66112  LO syngo MR E110LOanat-scout_ses-localizerQSHBodyUSCSROW DS8 CSN DS0.01101367460529DS0 QCSHFS LOSIEMENS MR HEADER CS IMAGE NUM 4  LO1.0  DS12345 SHNormalSHNoSL SL IS0\0\0 FD lffYa@`@DS0.6875IS5800 UI81.3.12.2.1107.5.2.43.66112.30000016101413223420300000001 UI:1.3.12.2.1107.5.2.43.66112.2016101409252673719600723.0.0.0 SH1 IS1 IS1 IS1 2DS-101.60000151396\-140\130 7DS 0\1\0\0\0\-1 RUI41.3.12.2.1107.5.2.43.66112.1.20161014092234552.0.0.0 @LO ADS-101.60000151396(US(CS MONOCHROME2 (US(US(0DS 1.625\1.625 (US(US (US (US(US(US(PDS21(QDS50(ULOAlgo1 )LOSIEMENS CSA HEADER)LOSIEMENS MEDCOM HEADER2)CS IMAGE NUM 4 ) LO20161014)OB-SV10eMEchoLinePositionISM M 80 EchoColumnPositionISM M 80 EchoPartitionPositionISM M 64 UsedChannelMaskDateUL UsedChannelStringmeUTM!!M!XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXActual3DImaPartNumberISM M 0 ICE_DimserLOMMX_1_1_1_1_1_1_1_1_1_1_1_13B_valuerientationISFilter1eISFilter2eISProtocolSliceNumberISM M 0 RealDwellTimeberISM M 5800 PixelFilenDateUNPixelFileNameeUNSliceMeasurementDurationDSMM12345.00000000SequenceMasksitionUL M M 134217728AcquisitionMatrixTextSHM M 160p*160MeasuredFourierLinesISM M 0 FlowEncodingDirectionatioISFlowVenceCompressionMethodFDPhaseEncodingDirectionPositiveISM M 1 NumberOfImagesInMosaicUS DiffusionGradientDirectionFDImageGroupionUS SliceNormalVectoruenceRefFDDiffusionDirectionalityCSTimeAfterStartnceSQDSFlipAngleCodeSequence J`DSSequenceNameentUID2rf8zSHRepetitionTime 5*}tkZXSQQMSDSEchoTimediaFileSetIDrcVLJKGIHFGCFGEIKIDSNumberOfAveragesetUIDF=9540-.-.0/0236;?AFDSVoxelThickness))&%$#$##$###%()-029AGINDSVoxelPhaseFOV!#%(-4:AHPW_jzDSVoxelReadoutFOVing $+18AJSZ_kDSVoxelPositionSag#*19BLU]^iCDSVoxelPositionCor$)1;FQ^bbk:CnDSVoxelPositionTraER_ae}bsxDSVoxelNormalSagSae$DSVoxelNormalCorx DSVoxelNormalTraDSVoxelInPlaneRotDSImagePositionPatientDSImageOrientationPatientDSPixelSpacingDSSliceLocationgentSQDSSliceThicknessDSSpectrumTextRegionLabelSHComp_AlgorithmISComp_Blendedxtensions%[EISComp_ManualAdjustedAt}mI6.ISComp_AutoParam )tbvW;1+# LTComp_AdjustedParamF9{j?2/&   LTComp_JobID\S70,!     LTFMRIStimulInfo     #(+ISFlowEncodingDirectionString !$%)-/11579:SHRepetitionTimeEffectiven!%)+-13459?@>=BBCCDDSCsiImagePositionPatient255:=@BAADEHIILLIFGFDSCsiImageOrientationPatientEGILKJLOOKJJIFHHHFDSCsiPixelSpacingFHKIJMONOOOPJGFIJKGA?<:9DSCsiSliceLocationQSQQOLMLLMMIEB@===941-(DSCsiSliceThicknessMKOQOIDEB?>=;741+'&#DSOriginalSeriesNumberCA?>=;62-'%$  ISOriginalImageNumber=840-)'" ISImaAbsTablePosition($   %1EUSLM M 0 M 0 M -1286 NonPlanarImageo  $/BX[mUS M M 0 MoCoQMeasurePixelValue $.@V]dHUS LQAlgorithmPixelValue<T_dz-/SHSlicePosition_PCSxmD  FDMM-101.60000151M-140.00000000 M 130.00000000RBMoCoTransValueRangeFDRBMoCoRotamesFDMultistepIndexointerISM M 0 ImaRelTablePositionISM M 0 M 0 M 0 ImaCoilStringLOMMHEA;HEPRFSWDDataTypeSHM M measuredGSWDDataTypeanCE *SHM M measuredNormalizeManipulated;EISImaPATModeTextute4I!LOMMp3B_matrixolusVolume6.FDBandwidthPerPixelPhaseEncode FDFMRIStimulLevelpTime $(FDFmriConditionsDataSequence  &+049=?UTFmriResultSequence ',-189;;@IJJLUTMosaicRefAcqTimesons&*+.36:=<BCFKMRVTSYFDAutoInlineImageFilterEnabled:;AFGHNQOLPV[\WTTISM M 1 QCDatatBolusIngredientConcentrationOUTSRQPRUSPQOFDExamLandmarksgentSequenceSQQPPOONMKJLKKMJE@LTExamDataRoleAdministrationRouteSequenceFEB@>;98:2+STMkkMk Loc Head Sag MRDiffusioneLJIIJIHIGB?=:=;52.)$!UTRealWorldValueMappingor8992-)%"  UTPhaseContrastN4rsion)$!  CSMRVelocityEncoding    (/BWZZUTVelocityEncodingDirectionN4 #+3IYXhFDImageType4MFsource &-;O[Xd CSVolumetricProperties4MFdS3% CSMorphoQCThresholdcalVersionFDMorphoQCIndexensionFlagFDImageHistorydingSchemeCreatorUIDLOM""M"ChannelMixing:ND=true_CMM=1_CDM=1MACC1MNormalizeAlgo:PreScanMImplicitImageFilter:SHMR_ASLeSetExtensionCreatorUIDUTDistorcor_IntensityCorrectionCSUserDefinedImageUT)CSMR)LO20161014) OBSV10OMUsedPatientWeightpߡppߡp K``ISM M 90 NumberOfPrescanspߡppߡp K``ISM M 0 TransmitterCalibrationpߡppߡp K``yRDSM M 224.66051500PhaseGradientAmplitudepߡppߡp K``$DSM M 0.00000000ReadoutGradientAmplitudepߡppߡp K``  DSM M 0.00000000SelectionGradientAmplitudepߡppߡpK``DSM M 0.00000000GradientDelayTime!pߡppߡp K``/5DSM M 36.00000000 M 35.00000000 M 30.00000000RfWatchdogMask  pߡppߡp K``HIISM M 0 RfPowerErrorIndicatorpߡppߡpK``XWDSSarWholeBody).4pߡppߡp K``bbDSSedy257;@Hpߡppߡp K``b`DSMM1000000.00000000 M 0.39396170 M 0.39396170SequenceFileOwnerVpߡppߡp K``a`SHMMSIEMENSStim_mon_mode[\\pߡppߡp K``ZWISM M 2 Operation_mode_flagpߡppߡp K``pߡpISM M 0 dBdt_maxaabcpߡppߡp K``pߡpDSM M 0.00000000t_puls_max`b^pߡppߡp K``pߡpDSM M 0.00000000dBdt_threshRPKpߡppߡp K``pߡpDSM M 0.00000000dBdt_limit@>;pߡppߡp K``pߡpDSM M 0.00000000SW_korr_faktor' pߡppߡp K``pߡpDSM M 1.00000000Stim_max_onlinepߡppߡp K``pߡpDSM M 7.70728540 M 11.69708824 M 13.82370281Stim_max_ges_norm_onlinepߡppߡp K``pߡpDSM M 0.64360857Stim_limKpߡppߡp K``pߡpDSM M 42.70069885 M 23.97319984 M 36.48099899Stim_faktor]pߡppߡp K``pߡpDSM M 1.00000000CoilForGradientpߡppߡp K``pߡpSHMMvoidCoilForGradient2pߡppߡp K``pߡpSHMMAS82CoilTuningReflectionpߡppߡp K``pߡpDSCoilIdpߡppߡp K``pߡpIS M M 255 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 MiscSequenceParampߡppߡp K``pߡp&IS*M M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 96 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 0 M 2 M 0 M 0 MrProtocolVersionpߡppߡp K``pߡpISM M 51130001DataFileNamepߡppߡp K``pߡpLORepresentativeImagepߡppߡp K``pߡpUIPositivePCSDirectionspߡppߡp K``pߡpSHMM+LPHRelTablePositionpߡppߡp K``pߡpISM M 0 M 0 M 0 ReadoutOSpߡppߡp K``pߡpFDM M 2.00000000LongModelNamepߡppߡp K``pߡpLOM M NUMARIS/4SliceArrayConcatenationspߡppߡp K``pߡpISM M 1 SliceResolutionpߡppߡp K``pߡpDSM M 0.68750000AbsTablePositionpߡppߡp K``pߡpISM M -1286 AutoAlignMatrixpߡppߡp K``pߡpFLMeasurementIndexpߡppߡp K``pߡpFLCoilStringpߡppߡp K``pߡpLOMMHEA;HEPPATModeText pߡppߡp K``pߡpLOMMp3PatReinPatternpߡppߡp K``pߡpSTM##M#1;HFS;90.72;36.00;3;0;0;-345402656ProtocolChangeHistorypߡppߡp K``pߡpUS M M 0 Isocenteredpߡppߡp K``pߡpUS M M 0 MrPhoenixProtocolpߡppߡp K``pߡpUNM__M_ { "PhoenixMetaProtocol" 1000002 2.0 { { "true" { "false" "true" } } { "true" 1 } { "true" " { ""MultiStep Controller"" 1000001 666.0 { 60 400 ""Multistep Protocol"" 401 ""Step"" 402 ""Inline Composing"" 403 ""Composing Group"" 404 ""Last Step"" 405 ""Composing Function"" 406 ""Inline Combine"" 407 ""Enables you to set up a Multistep Protocol."" 408 ""Indicates the number of the current Step of the Multistep Protocol.\nPress the + button to add a Step at the end of the list.\nPress the - button to delete the current Step."" 409 ""Invokes Inline Composing."" 410 ""Identifies all Steps that will be composed."" 411 ""Defines the last measurement step of a composing function."" 412 ""Save all measurements of the Multistep Protocol into one series."" 413 ""Defines the composing algorithm to be used."" 414 ""Prio recon"" 415 ""Enables Prio Recon measurement"" 416 ""Auto Align Spine"" 417 ""Enables the Auto Align Spine mode in GSP when protocol is open"" 422 ""Coil Select Mode"" 423 ""If set to """"Default"""",\nglobal settings from the queue menu will be used.\nIf set to """"All Off"""",\nboth """"Auto Coil Select"""" and """"Coil Memory"""" are deactivated."" 424 ""Auto Coil Select On"" 425 ""Auto Coil Select Off"" 426 ""Default"" 429 ""Wait for user to start"" 430 ""Load images to graphic segments"" 431 ""Before measurement"" 432 ""After measurement"" 433 ""1st segment"" 434 ""2nd segment"" 435 ""3rd segment"" 436 ""All segments"" 445 ""Angio"" 446 ""Spine"" 447 ""Adaptive"" 525 ""SD???"" 526 ""SD"" 538 ""Normalize"" 539 ""Homogenize composed data to avoid unwanted local enhancements."" 540 ""Off"" 541 ""Weak"" 542 ""Medium"" 543 ""Strong"" 545 ""Diffusion"" 546 ""Coil Memory On"" 547 ""Coil Memory Off"" 548 ""All Off"" 616 ""Disable auto transfer to RIS"" 617 ""Single measurement"" 618 ""Repeated measurement"" 620 ""Auto open inline display"" 621 ""Auto close inline display"" 622 ""Load images to viewer"" 623 ""Auto store images"" 624 ""Generate inline position display"" 625 ""All orientations"" 626 ""Load images to stamp segments"" 627 ""Inline movie"" 628 ""Sag"" 629 ""Cor"" 630 ""Tra"" } { { } { { ""false"" ""true"" } } { { { } { } { } { } } { { } { } { } { } } } { { { ""false"" ""true"" } } {