From 2042372bdbaef2ad11fd94071595381652a020ea Mon Sep 17 00:00:00 2001 From: Tom Close Date: Tue, 28 May 2024 00:29:15 +0930 Subject: [PATCH 1/8] added file-type to in_file --- .../mriqc.workflows.anatomical.base.anat_qc_workflow.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml b/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml index 62caab2..9350679 100644 --- a/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml +++ b/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml @@ -6,6 +6,9 @@ nipype_name: anat_qc_workflow nipype_module: mriqc.workflows.anatomical.base # Name of the node that is to be considered the input of the workflow, i.e. its outputs will be the inputs of the workflow input_node: inputnode +inputs: + in_file: + type: medimage/t1w+nifti-x-gz # name of the workflow variable that is returned workflow_variable: workflow # the names of the nested workflows that are defined in other modules and need to be imported From 3e0814269f469f8f2aa26fc5952bed8d811f7b93 Mon Sep 17 00:00:00 2001 From: Tom Close Date: Tue, 28 May 2024 17:50:14 +0930 Subject: [PATCH 2/8] fixed up type of in_file --- .../mriqc.workflows.anatomical.base.anat_qc_workflow.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml b/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml index 9350679..c4fbd84 100644 --- a/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml +++ b/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml @@ -8,7 +8,7 @@ nipype_module: mriqc.workflows.anatomical.base input_node: inputnode inputs: in_file: - type: medimage/t1w+nifti-x-gz + type: medimage/t1w+nifti-gz-x # name of the workflow variable that is returned workflow_variable: workflow # the names of the nested workflows that are defined in other modules and need to be imported From 439eaad89c88973310ce227cf5edde7348db8f79 Mon Sep 17 00:00:00 2001 From: Tom Close Date: Sat, 1 Jun 2024 22:30:12 +0930 Subject: [PATCH 3/8] added spec find/replace --- .github/workflows/ci-cd.yaml | 6 ++++-- .../workflows/mriqc.workflows.shared.synthstrip_wf.yaml | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml index 12380ac..7f860d7 100644 --- a/.github/workflows/ci-cd.yaml +++ b/.github/workflows/ci-cd.yaml @@ -9,11 +9,13 @@ name: CI/CD on: push: branches: [ main, develop ] - tags: [ '*' ] pull_request: branches: [ main, develop ] + release: + types: [published] repository_dispatch: - types: [create-release] + types: [create-post-release] + jobs: nipype-conv: diff --git a/nipype-auto-conv/specs/workflows/mriqc.workflows.shared.synthstrip_wf.yaml b/nipype-auto-conv/specs/workflows/mriqc.workflows.shared.synthstrip_wf.yaml index 942477a..d2c0fd6 100644 --- a/nipype-auto-conv/specs/workflows/mriqc.workflows.shared.synthstrip_wf.yaml +++ b/nipype-auto-conv/specs/workflows/mriqc.workflows.shared.synthstrip_wf.yaml @@ -13,6 +13,7 @@ find_replace: - ["config = NipypeConfig\\(\\)", ""] - ["iflogger = logging.getLogger\\(\"nipype.interface\"\\)", ""] - ["logging = Logging\\(config\\)", ""] + - ["save_bias=True", "bias_image=True"] # name of the workflow variable that is returned workflow_variable: workflow # the names of the nested workflows that are defined in other modules and need to be imported From 28c5b67550642a2f6841550d77fca137f46c3ba9 Mon Sep 17 00:00:00 2001 From: Tom Close Date: Sat, 1 Jun 2024 23:09:23 +0930 Subject: [PATCH 4/8] added import f/r --- nipype-auto-conv/specs/package.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/nipype-auto-conv/specs/package.yaml b/nipype-auto-conv/specs/package.yaml index ab44c0c..8385210 100644 --- a/nipype-auto-conv/specs/package.yaml +++ b/nipype-auto-conv/specs/package.yaml @@ -59,5 +59,6 @@ import_find_replace: - ["from \\.\\. import config, logging", ""] - ["_ReadDWIMetadataOutputSpec,", ""] - ["from pydra.tasks.mriqc.nipype_ports.interfaces import utility as niu", ""] + - ["\\s+config,(\\s+)fname_presuffix,", "\\1fname_presuffix,"] copy_packages: - mriqc.data From 97fe4c8a8bf35fa59700e8047293f8ce32c27b2d Mon Sep 17 00:00:00 2001 From: Tom Close Date: Sun, 2 Jun 2024 01:23:30 +0930 Subject: [PATCH 5/8] added synthstrip cli to package --- nipype-auto-conv/specs/package.yaml | 1 + pyproject.toml | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/nipype-auto-conv/specs/package.yaml b/nipype-auto-conv/specs/package.yaml index 8385210..11594ae 100644 --- a/nipype-auto-conv/specs/package.yaml +++ b/nipype-auto-conv/specs/package.yaml @@ -62,3 +62,4 @@ import_find_replace: - ["\\s+config,(\\s+)fname_presuffix,", "\\1fname_presuffix,"] copy_packages: - mriqc.data + - mriqc.synthstrip diff --git a/pyproject.toml b/pyproject.toml index df379ed..32e2c29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,11 +18,11 @@ dependencies = [ "nilearn", "migas >= 0.4.0", "pandas ~=1.0", - "pydra >=0.22", + "pydra >=0.23", "pydra-ants", "pydra-afni", "pydra-fsl", - "pydra-mrtrix3 >=3.0.3a0", + "pydra-mrtrix3 >=3.0.4a5", "pydra-niworkflows", "pydra-nireports", "PyYAML", @@ -32,6 +32,7 @@ dependencies = [ "statsmodels", "templateflow", "nilearn", + "nitransforms", "torch", "toml", "tomli >= 1.1.0; python_version < '3.11'", @@ -103,3 +104,14 @@ per-file-ignores = ["__init__.py:F401,F403"] max-line-length = 88 select = "C,E,F,W,B,B950" extend-ignore = ['E203', 'E501', 'E129', 'W503'] + +[project.scripts] +# abide2bids = "mriqc.bin.abide2bids:main" +# dfcheck = "mriqc.bin.dfcheck:main" +# fs2gif = "mriqc.bin.fs2gif:main" +# mriqc = "mriqc.cli.run:main" +# mriqc_labeler = "mriqc.bin.labeler:main" +# mriqcwebapi_test = "mriqc.bin.mriqcwebapi_test:main" +# nib-hash = "mriqc.bin.nib_hash:main" +# participants = "mriqc.bin.subject_wrangler:main" +synthstrip = "pydra.tasks.mriqc.synthstrip.cli:main" From 652eee5b017cd6165316f87fa9f68a46033f1e83 Mon Sep 17 00:00:00 2001 From: Tom Close Date: Tue, 4 Jun 2024 20:28:38 +1000 Subject: [PATCH 6/8] debugging yaml specs --- .../interfaces/derivatives_data_sink.yaml | 94 ++++++++-------- .../specs/interfaces/iqm_file_sink.yaml | 1 + .../specs/interfaces/synth_strip.yaml | 100 +++++++++--------- nipype-auto-conv/specs/package.yaml | 11 +- ...lows.anatomical.base.anat_qc_workflow.yaml | 9 ++ 5 files changed, 114 insertions(+), 101 deletions(-) diff --git a/nipype-auto-conv/specs/interfaces/derivatives_data_sink.yaml b/nipype-auto-conv/specs/interfaces/derivatives_data_sink.yaml index 3272310..3963fb8 100644 --- a/nipype-auto-conv/specs/interfaces/derivatives_data_sink.yaml +++ b/nipype-auto-conv/specs/interfaces/derivatives_data_sink.yaml @@ -5,7 +5,7 @@ # # Docs # ---- -# +# task_name: DerivativesDataSink nipype_name: DerivativesDataSink nipype_module: mriqc.interfaces @@ -15,11 +15,11 @@ inputs: rename: # dict[str, str] - fields to rename in the Pydra interface types: - # dict[str, type] - override inferred types (use "mime-like" string for file-format types, - # e.g. 'medimage/nifti-gz'). For most fields the type will be correctly inferred - # from the nipype interface, but you may want to be more specific, particularly - # for file types, where specifying the format also specifies the file that will be - # passed to the field in the automatically generated unittests. + # dict[str, type] - override inferred types (use "mime-like" string for file-format types, + # e.g. 'medimage/nifti-gz'). For most fields the type will be correctly inferred + # from the nipype interface, but you may want to be more specific, particularly + # for file types, where specifying the format also specifies the file that will be + # passed to the field in the automatically generated unittests. base_directory: generic/directory # type=directory|default='': Path to the base directory for storing data. in_file: generic/file+list-of @@ -37,15 +37,15 @@ outputs: rename: # dict[str, str] - fields to rename in the Pydra interface types: - # dict[str, type] - override inferred types (use "mime-like" string for file-format types, - # e.g. 'medimage/nifti-gz'). For most fields the type will be correctly inferred - # from the nipype interface, but you may want to be more specific, particularly - # for file types, where specifying the format also specifies the file that will be - # passed to the field in the automatically generated unittests. + # dict[str, type] - override inferred types (use "mime-like" string for file-format types, + # e.g. 'medimage/nifti-gz'). For most fields the type will be correctly inferred + # from the nipype interface, but you may want to be more specific, particularly + # for file types, where specifying the format also specifies the file that will be + # passed to the field in the automatically generated unittests. out_file: generic/file+list-of - # type=outputmultiobject: + # type=outputmultiobject: out_meta: generic/file+list-of - # type=outputmultiobject: + # type=outputmultiobject: callables: # dict[str, str] - names of methods/callable classes defined in the adjacent `*_callables.py` # to set to the `callable` attribute of output fields @@ -54,38 +54,38 @@ outputs: requirements: # dict[str, list[str]] - input fields that are required to be provided for the output field to be present tests: -- inputs: - # dict[str, str] - values to provide to inputs fields in the task initialisation - # (if not specified, will try to choose a sensible value) - base_directory: - # type=directory|default='': Path to the base directory for storing data. - check_hdr: - # type=bool|default=True: fix headers of NIfTI outputs - compress: - # type=inputmultiobject|default=[]: whether ``in_file`` should be compressed (True), uncompressed (False) or left unmodified (None, default). - data_dtype: - # type=str|default='': NumPy datatype to coerce NIfTI data to, or `source` tomatch the input file dtype - dismiss_entities: - # type=inputmultiobject|default=[]: a list entities that will not be propagated from the source file - in_file: - # type=inputmultiobject|default=[]: the object to be saved - meta_dict: - # type=dict|default={}: an input dictionary containing metadata - source_file: - # type=inputmultiobject|default=[]: the source file(s) to extract entities from - imports: - # list[nipype2pydra.task.base.explicitimport] - list import statements required by the test, with each list item - # consisting of 'module', 'name', and optionally 'alias' keys - expected_outputs: - # dict[str, str] - expected values for selected outputs, noting that tests will typically - # be terminated before they complete for time-saving reasons, and therefore - # these values will be ignored, when running in CI - timeout: 10 - # int - the value to set for the timeout in the generated test, - # after which the test will be considered to have been initialised - # successfully. Set to 0 to disable the timeout (warning, this could - # lead to the unittests taking a very long time to complete) - xfail: true - # bool - whether the unittest is expected to fail or not. Set to false - # when you are satisfied with the edits you have made to this file + - inputs: + # dict[str, str] - values to provide to inputs fields in the task initialisation + # (if not specified, will try to choose a sensible value) + base_directory: + # type=directory|default='': Path to the base directory for storing data. + check_hdr: + # type=bool|default=True: fix headers of NIfTI outputs + compress: + # type=inputmultiobject|default=[]: whether ``in_file`` should be compressed (True), uncompressed (False) or left unmodified (None, default). + data_dtype: + # type=str|default='': NumPy datatype to coerce NIfTI data to, or `source` tomatch the input file dtype + dismiss_entities: + # type=inputmultiobject|default=[]: a list entities that will not be propagated from the source file + in_file: + # type=inputmultiobject|default=[]: the object to be saved + meta_dict: + # type=dict|default={}: an input dictionary containing metadata + source_file: + # type=inputmultiobject|default=[]: the source file(s) to extract entities from + imports: + # list[nipype2pydra.task.base.explicitimport] - list import statements required by the test, with each list item + # consisting of 'module', 'name', and optionally 'alias' keys + expected_outputs: + # dict[str, str] - expected values for selected outputs, noting that tests will typically + # be terminated before they complete for time-saving reasons, and therefore + # these values will be ignored, when running in CI + timeout: 10 + # int - the value to set for the timeout in the generated test, + # after which the test will be considered to have been initialised + # successfully. Set to 0 to disable the timeout (warning, this could + # lead to the unittests taking a very long time to complete) + xfail: true + # bool - whether the unittest is expected to fail or not. Set to false + # when you are satisfied with the edits you have made to this file doctests: [] diff --git a/nipype-auto-conv/specs/interfaces/iqm_file_sink.yaml b/nipype-auto-conv/specs/interfaces/iqm_file_sink.yaml index 6618818..2b13f7a 100644 --- a/nipype-auto-conv/specs/interfaces/iqm_file_sink.yaml +++ b/nipype-auto-conv/specs/interfaces/iqm_file_sink.yaml @@ -99,3 +99,4 @@ tests: doctests: [] find_replace: - [config\.loggers\.\w+\., logger.] + - ["value=Undefined", "value=attrs.NOTHING"] diff --git a/nipype-auto-conv/specs/interfaces/synth_strip.yaml b/nipype-auto-conv/specs/interfaces/synth_strip.yaml index bdbe066..1d017c1 100644 --- a/nipype-auto-conv/specs/interfaces/synth_strip.yaml +++ b/nipype-auto-conv/specs/interfaces/synth_strip.yaml @@ -5,7 +5,7 @@ # # Docs # ---- -# +# task_name: SynthStrip nipype_name: SynthStrip nipype_module: mriqc.interfaces.synthstrip @@ -15,11 +15,11 @@ inputs: rename: # dict[str, str] - fields to rename in the Pydra interface types: - # dict[str, type] - override inferred types (use "mime-like" string for file-format types, - # e.g. 'medimage/nifti-gz'). For most fields the type will be correctly inferred - # from the nipype interface, but you may want to be more specific, particularly - # for file types, where specifying the format also specifies the file that will be - # passed to the field in the automatically generated unittests. + # dict[str, type] - override inferred types (use "mime-like" string for file-format types, + # e.g. 'medimage/nifti-gz'). For most fields the type will be correctly inferred + # from the nipype interface, but you may want to be more specific, particularly + # for file types, where specifying the format also specifies the file that will be + # passed to the field in the automatically generated unittests. in_file: generic/file # type=file|default=: Input image to be brain extracted model: generic/file @@ -34,6 +34,8 @@ inputs: # dict[str, str] - names of methods/callable classes defined in the adjacent `*_callables.py` # to set as the `default` method of input fields metadata: + in_file: + copyfile: true # dict[str, dict[str, any]] - additional metadata to set on any of the input fields (e.g. out_file: position: 1) outputs: omit: @@ -41,11 +43,11 @@ outputs: rename: # dict[str, str] - fields to rename in the Pydra interface types: - # dict[str, type] - override inferred types (use "mime-like" string for file-format types, - # e.g. 'medimage/nifti-gz'). For most fields the type will be correctly inferred - # from the nipype interface, but you may want to be more specific, particularly - # for file types, where specifying the format also specifies the file that will be - # passed to the field in the automatically generated unittests. + # dict[str, type] - override inferred types (use "mime-like" string for file-format types, + # e.g. 'medimage/nifti-gz'). For most fields the type will be correctly inferred + # from the nipype interface, but you may want to be more specific, particularly + # for file types, where specifying the format also specifies the file that will be + # passed to the field in the automatically generated unittests. out_file: generic/file # type=file: brain-extracted image # type=file|default=: store brain-extracted input to file @@ -60,42 +62,42 @@ outputs: requirements: # dict[str, list[str]] - input fields that are required to be provided for the output field to be present tests: -- inputs: - # dict[str, str] - values to provide to inputs fields in the task initialisation - # (if not specified, will try to choose a sensible value) - in_file: - # type=file|default=: Input image to be brain extracted - use_gpu: - # type=bool|default=False: Use GPU - model: - # type=file|default=: file containing model's weights - border_mm: - # type=int|default=1: Mask border threshold in mm - out_file: - # type=file: brain-extracted image - # type=file|default=: store brain-extracted input to file - out_mask: - # type=file: brain mask - # type=file|default=: store brainmask to file - num_threads: - # type=int|default=0: Number of threads - args: - # type=str|default='': Additional parameters to the command - environ: - # type=dict|default={}: Environment variables - imports: - # list[nipype2pydra.task.base.explicitimport] - list import statements required by the test, with each list item - # consisting of 'module', 'name', and optionally 'alias' keys - expected_outputs: - # dict[str, str] - expected values for selected outputs, noting that tests will typically - # be terminated before they complete for time-saving reasons, and therefore - # these values will be ignored, when running in CI - timeout: 10 - # int - the value to set for the timeout in the generated test, - # after which the test will be considered to have been initialised - # successfully. Set to 0 to disable the timeout (warning, this could - # lead to the unittests taking a very long time to complete) - xfail: true - # bool - whether the unittest is expected to fail or not. Set to false - # when you are satisfied with the edits you have made to this file + - inputs: + # dict[str, str] - values to provide to inputs fields in the task initialisation + # (if not specified, will try to choose a sensible value) + in_file: + # type=file|default=: Input image to be brain extracted + use_gpu: + # type=bool|default=False: Use GPU + model: + # type=file|default=: file containing model's weights + border_mm: + # type=int|default=1: Mask border threshold in mm + out_file: + # type=file: brain-extracted image + # type=file|default=: store brain-extracted input to file + out_mask: + # type=file: brain mask + # type=file|default=: store brainmask to file + num_threads: + # type=int|default=0: Number of threads + args: + # type=str|default='': Additional parameters to the command + environ: + # type=dict|default={}: Environment variables + imports: + # list[nipype2pydra.task.base.explicitimport] - list import statements required by the test, with each list item + # consisting of 'module', 'name', and optionally 'alias' keys + expected_outputs: + # dict[str, str] - expected values for selected outputs, noting that tests will typically + # be terminated before they complete for time-saving reasons, and therefore + # these values will be ignored, when running in CI + timeout: 10 + # int - the value to set for the timeout in the generated test, + # after which the test will be considered to have been initialised + # successfully. Set to 0 to disable the timeout (warning, this could + # lead to the unittests taking a very long time to complete) + xfail: true + # bool - whether the unittest is expected to fail or not. Set to false + # when you are satisfied with the edits you have made to this file doctests: [] diff --git a/nipype-auto-conv/specs/package.yaml b/nipype-auto-conv/specs/package.yaml index 11594ae..455a55a 100644 --- a/nipype-auto-conv/specs/package.yaml +++ b/nipype-auto-conv/specs/package.yaml @@ -35,6 +35,12 @@ omit_constants: import_translations: - [nireports, pydra.tasks.nireports] - [niworkflows, pydra.tasks.niworkflows] +import_find_replace: + - ["from \\.\\. import config, logging", ""] + - ["_ReadDWIMetadataOutputSpec,", ""] + - ["from pydra.tasks.mriqc.nipype_ports.interfaces import utility as niu", ""] + - ["\\s+config,(\\s+)fname_presuffix,", "\\1fname_presuffix,"] + - ["from pydra.tasks.io.auto import add_traits\n", ""] find_replace: - [config\.loggers\.\w+\., logger.] - [config.to_filename\(\), ""] @@ -55,11 +61,6 @@ find_replace: # - ["\\bdict\\[", "ty.Dict["] omit_modules: - "mriqc.config" -import_find_replace: - - ["from \\.\\. import config, logging", ""] - - ["_ReadDWIMetadataOutputSpec,", ""] - - ["from pydra.tasks.mriqc.nipype_ports.interfaces import utility as niu", ""] - - ["\\s+config,(\\s+)fname_presuffix,", "\\1fname_presuffix,"] copy_packages: - mriqc.data - mriqc.synthstrip diff --git a/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml b/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml index c4fbd84..d02421e 100644 --- a/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml +++ b/nipype-auto-conv/specs/workflows/mriqc.workflows.anatomical.base.anat_qc_workflow.yaml @@ -9,6 +9,10 @@ input_node: inputnode inputs: in_file: type: medimage/t1w+nifti-gz-x + modality: + type: field/text + connections: + - [norm, modality] # name of the workflow variable that is returned workflow_variable: workflow # the names of the nested workflows that are defined in other modules and need to be imported @@ -24,3 +28,8 @@ find_replace: "# fmt: off\\n\\s*workflow.set_output\\(\\[\\('iqmswf_measures', workflow.iqmswf.lzout.measures\\)\\]\\)", "", ] + - [ + "modality=workflow.lzin.modality,(\\s+)name=\"norm\"", + "modality=workflow.lzin.modality,\\1name=\"spatial_norm\"", + ] + - ["workflow\\.norm\\b", "workflow.spatial_norm"] From 52551d9004a92aea500bf07de449a790a01e13e1 Mon Sep 17 00:00:00 2001 From: Tom Close Date: Tue, 4 Jun 2024 20:29:02 +1000 Subject: [PATCH 7/8] added scripts to help debug mriqc workflow --- test_scripts/run_anat_wf.py | 18 ++ test_scripts/run_anat_wf_orig.py | 364 +++++++++++++++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 test_scripts/run_anat_wf.py create mode 100644 test_scripts/run_anat_wf_orig.py diff --git a/test_scripts/run_anat_wf.py b/test_scripts/run_anat_wf.py new file mode 100644 index 0000000..e6548f2 --- /dev/null +++ b/test_scripts/run_anat_wf.py @@ -0,0 +1,18 @@ +from fileformats.medimage import NiftiGzX, T1Weighted +import logging +from pathlib import Path +from pydra.tasks.mriqc.workflows.anatomical.base import anat_qc_workflow + +log_file = Path("/Users/tclose/Data/pydra-mriqc-test.log") +log_file.unlink(missing_ok=True) + +pydra_logger = logging.getLogger("pydra") +pydra_logger.setLevel(logging.INFO) +file_handler = logging.FileHandler(str(log_file)) +pydra_logger.addHandler(file_handler) +pydra_logger.addHandler(logging.StreamHandler()) + +workflow = anat_qc_workflow(in_file=NiftiGzX[T1Weighted].sample(), modality="T1w") +workflow.cache_dir = "/Users/tclose/Data/pydra-mriqc-test-cache" +result = workflow(plugin="serial") +print(result.out) diff --git a/test_scripts/run_anat_wf_orig.py b/test_scripts/run_anat_wf_orig.py new file mode 100644 index 0000000..f6a719f --- /dev/null +++ b/test_scripts/run_anat_wf_orig.py @@ -0,0 +1,364 @@ +from fileformats.medimage import NiftiGzX, T1Weighted +import logging +from pathlib import Path +from logging import DEBUG, FileHandler + +# from niworkflows.utils.bids import DEFAULT_BIDS_QUERIES, collect_data +from mriqc._warnings import DATE_FMT, LOGGER_FMT, _LogFormatter +import atexit +import time +import tempfile +from mriqc import config +from mriqc.cli.parser import parse_args + +from mriqc.workflows.anatomical.base import anat_qc_workflow + +# from mriqc import config + + +# class Execution: +# log_dir = "/Users/tclose/Data/pydra-mriqc-test2.log" + + +class opts: + output_dir = "/Users/tclose/Data/pydra-mriqc-test2" + verbose = 0 + species = "human" + modalities = config.SUPPORTED_SUFFIXES + bids_database_wipe = False + testing = False + float32 = True + pdb = False + work_dir = Path("work").absolute() + verbose_reports = False + reports_only = False + write_graph = False + dry_run = False + profile = False + use_plugin = None + no_sub = False + email = "" + upload_strict = False + ants_float = False + # Diffusion workflow settings + min_dwi_length = config.workflow.min_len_dwi + min_bold_length = config.workflow.min_len_bold + fft_spikes_detector = False + fd_thres = 0.2 + deoblique = False + despike = False + verbose_count = 0 + + +config.execution.log_level = int(max(25 - 5 * opts.verbose_count, DEBUG)) + +config.loggers.init() + +_log_file = Path(opts.output_dir) / "logs" / f"mriqc-{config.execution.run_uuid}.log" +_log_file.parent.mkdir(exist_ok=True, parents=True) +_handler = FileHandler(_log_file) +_handler.setFormatter( + _LogFormatter( + fmt=LOGGER_FMT.format(color="", reset=""), + datefmt=DATE_FMT, + colored=False, + ) +) +config.loggers.default.addHandler(_handler) + +extra_messages = [""] + + +# config.loggers.cli.log( +# 26, +# PARTICIPANT_START.format( +# version=__version__, +# bids_dir=opts.bids_dir, +# output_dir=opts.output_dir, +# analysis_level=opts.analysis_level, +# extra_messages="\n".join(extra_messages), +# ), +# ) +# config.from_dict(vars(opts)) + +# Load base plugin_settings from file if --use-plugin +if opts.use_plugin is not None: + from yaml import safe_load as loadyml + + with open(opts.use_plugin) as f: + plugin_settings = loadyml(f) + _plugin = plugin_settings.get("plugin") + if _plugin: + config.nipype.plugin = _plugin + config.nipype.plugin_args = plugin_settings.get("plugin_args", {}) + config.nipype.nprocs = config.nipype.plugin_args.get( + "nprocs", config.nipype.nprocs + ) + +# # Load BIDS filters +# if opts.bids_filter_file: +# config.execution.bids_filters = loads(opts.bids_filter_file.read_text()) + +# bids_dir = config.execution.bids_dir +config.execution.output_dir = Path("/Users/tclose/Data/pydra-mriqc-test2-output") +output_dir = config.execution.output_dir +work_dir = config.execution.work_dir +version = config.environment.version + +# config.execution.bids_dir_datalad = ( +# config.execution.datalad_get +# and (bids_dir / ".git").exists() +# and (bids_dir / ".datalad").exists() +# ) + +# Setup directories +config.execution.log_dir = output_dir / "logs" +# Check and create output and working directories +config.execution.log_dir.mkdir(exist_ok=True, parents=True) +output_dir.mkdir(exist_ok=True, parents=True) +work_dir.mkdir(exist_ok=True, parents=True) + +# Force initialization of the BIDSLayout +# config.execution.init() + +# participant_label = [ +# d.name[4:] +# for d in config.execution.bids_dir.glob("sub-*") +# if d.is_dir() and d.exists() +# ] + + +config.execution.participant_label = "sub-01" + +# Handle analysis_level +analysis_level = set(config.workflow.analysis_level) +if not config.execution.participant_label: + analysis_level.add("group") +config.workflow.analysis_level = list(analysis_level) + +# List of files to be run +lc_modalities = "t1w" # [mod.lower() for mod in config.execution.modalities] +# bids_dataset, _ = collect_data( +# config.execution.layout, +# config.execution.participant_label, +# session_id=config.execution.session_id, +# task=config.execution.task_id, +# group_echos=True, +# bids_filters={ +# mod: config.execution.bids_filters.get(mod, {}) for mod in lc_modalities +# }, +# queries={mod: DEFAULT_BIDS_QUERIES[mod] for mod in lc_modalities}, +# ) + +# Drop empty queries +# bids_dataset = {mod: files for mod, files in bids_dataset.items() if files} +config.workflow.inputs = None # bids_dataset + + +# set specifics for alternative populations +if opts.species.lower() != "human": + config.workflow.species = opts.species + # TODO: add other species once rats are working + if opts.species.lower() == "rat": + config.workflow.template_id = "Fischer344" + # mean distance from the lateral edge to the center of the brain is + # ~ PA:10 mm, LR:7.5 mm, and IS:5 mm (see DOI: 10.1089/089771503770802853) + # roll movement is most likely to occur, so set to 7.5 mm + config.workflow.fd_radius = 7.5 + # block uploads for the moment; can be reversed before wider release + config.execution.no_sub = True + + +log_file = Path("/Users/tclose/Data/pydra-mriqc-test.log") +log_file.unlink(missing_ok=True) + +pydra_logger = logging.getLogger("pydra") +pydra_logger.setLevel(logging.INFO) +file_handler = logging.FileHandler(str(log_file)) +pydra_logger.addHandler(file_handler) +pydra_logger.addHandler(logging.StreamHandler()) + +tmp_dir = Path(tempfile.mkdtemp()) + +t1w = NiftiGzX[T1Weighted].sample() +t1w = t1w.copy(tmp_dir, new_stem="sub-01_T1w") + + +workflow = anat_qc_workflow(in_file=str(t1w), metadata=t1w.metadata) +workflow.run() +# workflow.cache_dir = "/Users/tclose/Data/pydra-mriqc-test-cache2" +result = workflow(plugin="serial") +print(result.out) + +atexit.register(config.restore_env) + +config.settings.start_time = time.time() + +# Run parser +parse_args() + +# if config.execution.pdb: +# from mriqc.utils.debug import setup_exceptionhook + +# setup_exceptionhook() +# config.nipype.plugin = "Linear" + +# # CRITICAL Save the config to a file. This is necessary because the execution graph +# # is built as a separate process to keep the memory footprint low. The most +# # straightforward way to communicate with the child process is via the filesystem. +# # The config file name needs to be unique, otherwise multiple mriqc instances +# # will create write conflicts. +# config_file = config.to_filename() +# config.loggers.cli.info(f"MRIQC config file: {config_file}.") + +# exitcode = 0 +# # Set up participant level +# _pool = None +# if config.nipype.plugin in ("MultiProc", "LegacyMultiProc"): +# import multiprocessing as mp +# import multiprocessing.forkserver +# from concurrent.futures import ProcessPoolExecutor +# from contextlib import suppress + +# os.environ["OMP_NUM_THREADS"] = "1" +# os.environ["NUMEXPR_MAX_THREADS"] = "1" + +# with suppress(RuntimeError): +# mp.set_start_method("fork") +# gc.collect() + +# _pool = ProcessPoolExecutor( +# max_workers=config.nipype.nprocs, +# initializer=config._process_initializer, +# initargs=(config_file,), +# ) + +# _resmon = None +# if config.execution.resource_monitor: +# from mriqc.instrumentation.resources import ResourceRecorder + +# _resmon = ResourceRecorder( +# pid=os.getpid(), +# log_file=mkstemp( +# dir=config.execution.work_dir, prefix=".resources.", suffix=".tsv" +# )[1], +# ) +# _resmon.start() + +# if not config.execution.notrack: +# from ..utils.telemetry import setup_migas + +# setup_migas() + +# with Manager() as mgr: +# from .workflow import build_workflow + +# retval = mgr.dict() +# p = Process(target=build_workflow, args=(str(config_file), retval)) +# p.start() +# p.join() + +# mriqc_wf = retval.get("workflow", None) +# exitcode = p.exitcode or retval.get("return_code", 0) + +# CRITICAL Load the config from the file. This is necessary because the ``build_workflow`` +# function executed constrained in a process may change the config (and thus the global +# state of MRIQC). +# config.load(config_file) + +# exitcode = exitcode or (mriqc_wf is None) * os.EX_SOFTWARE +# if exitcode != 0: +# sys.exit(exitcode) + +# # Initialize nipype config +# config.nipype.init() +# # Make sure loggers are started +# config.loggers.init() + +# if _resmon: +# config.loggers.cli.info(f"Started resource recording at {_resmon._logfile}.") + +# # Resource management options +# if config.nipype.plugin in ("MultiProc", "LegacyMultiProc") and ( +# 1 < config.nipype.nprocs < config.nipype.omp_nthreads +# ): +# config.loggers.cli.warning( +# "Per-process threads (--omp-nthreads=%d) exceed total " +# "threads (--nthreads/--n_cpus=%d)", +# config.nipype.omp_nthreads, +# config.nipype.nprocs, +# ) + +# # Check synthstrip is properly installed +# if not config.environment.synthstrip_path: +# config.loggers.cli.warning( +# ( +# "Please make sure FreeSurfer is installed and the FREESURFER_HOME " +# "environment variable is defined and pointing at the right directory." +# ) +# if config.environment.freesurfer_home is None +# else ( +# f"FreeSurfer seems to be installed at {config.environment.freesurfer_home}," +# " however SynthStrip's model is not found at the expected path." +# ) +# ) + +# if mriqc_wf is None: +# sys.exit(os.EX_SOFTWARE) + +# if mriqc_wf and config.execution.write_graph: +# mriqc_wf.write_graph(graph2use="colored", format="svg", simple_form=True) + +# if not config.execution.dry_run and not config.execution.reports_only: +# # Warn about submitting measures BEFORE +# if not config.execution.no_sub: +# config.loggers.cli.warning(config.DSA_MESSAGE) + +# # Clean up master process before running workflow, which may create forks +# gc.collect() +# # run MRIQC +# _plugin = config.nipype.get_plugin() +# if _pool: +# MultiProcPlugin + +# _plugin = { +# "plugin": MultiProcPlugin( +# pool=_pool, plugin_args=config.nipype.plugin_args +# ), +# } +# mriqc_wf.run(**_plugin) + +# # Warn about submitting measures AFTER +# if not config.execution.no_sub: +# config.loggers.cli.warning(config.DSA_MESSAGE) + +# if not config.execution.dry_run: +# from mriqc.reports.individual import generate_reports + +# generate_reports() + +# _subject_duration = time.gmtime( +# (time.time() - config.settings.start_time) +# / sum(len(files) for files in config.workflow.inputs.values()) +# ) +# config.loggers.cli.log( +# 25, +# messages.PARTICIPANT_FINISHED.format( +# duration=time.strftime("%Hh %Mmin %Ss", _subject_duration) +# ), +# ) + +# if _resmon is not None: +# from mriqc.instrumentation.viz import plot + +# _resmon.stop() +# plot( +# _resmon._logfile, +# param="mem_rss_mb", +# out_file=str(_resmon._logfile).replace(".tsv", ".rss.png"), +# ) +# plot( +# _resmon._logfile, +# param="mem_vsm_mb", +# out_file=str(_resmon._logfile).replace(".tsv", ".vsm.png"), +# ) From 8359ed14333c515c9d5f67f249ad46aa4e5b8c48 Mon Sep 17 00:00:00 2001 From: Tom Close Date: Fri, 27 Jun 2025 12:00:33 +1000 Subject: [PATCH 8/8] added auto-generated workflows --- .github/workflows/ci-cd.yaml | 2 +- .gitignore | 1 - docs/conf.py | 2 +- nipype-auto-conv/generate.py | 14 + nipype-auto-conv/specs/package.yaml | 4 + niworkflows/utils/bids.py | 65 + niworkflows/utils/images.py | 33 + niworkflows/utils/misc.py | 43 + pydra/tasks/mriqc/__init__.py | 2 +- pydra/tasks/mriqc/_post_release.py | 6 + pydra/tasks/mriqc/_version.py | 16 + pydra/tasks/mriqc/data/NOTICE | 17 + pydra/tasks/mriqc/data/__init__.py | 37 + pydra/tasks/mriqc/data/bootstrap-anat.yml | 104 + pydra/tasks/mriqc/data/bootstrap-dwi.yml | 82 + pydra/tasks/mriqc/data/bootstrap-func.yml | 97 + pydra/tasks/mriqc/data/config-example.toml | 60 + pydra/tasks/mriqc/data/config.py | 56 + pydra/tasks/mriqc/data/fsexport.tcl | 107 + pydra/tasks/mriqc/data/itk_identity.tfm | 5 + .../data/reports/embed_resources/boxplots.css | 157 + .../data/reports/embed_resources/boxplots.js | 1634 + .../data/reports/embed_resources/d3.min.js | 5 + pydra/tasks/mriqc/data/reports/group.html | 108 + .../reports/resources/DO_NOT_REMOVE_OR_MODIFY | 2 + .../mriqc/data/reports/resources/boxplots.css | 157 + .../mriqc/data/reports/resources/boxplots.js | 1630 + .../mriqc/data/reports/resources/d3.min.js | 5 + pydra/tasks/mriqc/data/testdata/group_T1w.tsv | 5 + .../tasks/mriqc/data/testdata/group_bold.tsv | 10 + pydra/tasks/mriqc/data/tests/ds000005/CHANGES | 12 + pydra/tasks/mriqc/data/tests/ds000005/README | 22 + .../tests/ds000005/dataset_description.json | 7 + .../data/tests/ds000005/participants.tsv | 17 + .../ds000005/sub-01/anat/sub-01_T1w.nii.gz | Bin 0 -> 96 bytes .../sub-01/anat/sub-01_inplaneT2.nii.gz | Bin 0 -> 102 bytes ...-01_task-mixedgamblestask_run-01_bold.json | 3 + ...1_task-mixedgamblestask_run-01_bold.nii.gz | Bin 0 -> 95 bytes ...01_task-mixedgamblestask_run-01_events.tsv | 0 ...-01_task-mixedgamblestask_run-02_bold.json | 3 + ...1_task-mixedgamblestask_run-02_bold.nii.gz | Bin 0 -> 95 bytes ...01_task-mixedgamblestask_run-02_events.tsv | 0 ...-01_task-mixedgamblestask_run-03_bold.json | 3 + ...1_task-mixedgamblestask_run-03_bold.nii.gz | Bin 0 -> 95 bytes ...01_task-mixedgamblestask_run-03_events.tsv | 0 .../ds000005/task-mixedgamblestask_bold.json | 4 + .../tests/ds002785/dataset_description.json | 20 + .../ds002785/sub-0017/anat/sub-0017_T1w.json | 114 + .../figures/sub-0017_desc-airmask_T1w.svg | 5634 +++ .../figures/sub-0017_desc-artifacts_T1w.svg | 934 + .../figures/sub-0017_desc-background_T1w.svg | 7206 +++ .../figures/sub-0017_desc-brainmask_T1w.svg | 4564 ++ .../figures/sub-0017_desc-head_T1w.svg | 4298 ++ .../figures/sub-0017_desc-noisefit_T1w.svg | 1324 + .../figures/sub-0017_desc-norm_T1w.svg | 19 + .../sub-0017_desc-segmentation_T1w.svg | 34802 ++++++++++++++ .../figures/sub-0017_desc-zoomed_T1w.svg | 7167 +++ .../ds002785/sub-0042/anat/sub-0042_T1w.json | 114 + .../figures/sub-0042_desc-airmask_T1w.svg | 6286 +++ .../figures/sub-0042_desc-artifacts_T1w.svg | 898 + .../figures/sub-0042_desc-background_T1w.svg | 7206 +++ .../figures/sub-0042_desc-brainmask_T1w.svg | 5280 ++ .../figures/sub-0042_desc-head_T1w.svg | 4475 ++ .../figures/sub-0042_desc-noisefit_T1w.svg | 1229 + .../figures/sub-0042_desc-norm_T1w.svg | 19 + .../sub-0042_desc-segmentation_T1w.svg | 39958 ++++++++++++++++ .../figures/sub-0042_desc-zoomed_T1w.svg | 7173 +++ .../mriqc/data/tests/gh1086-ds004134.oracle | 1 + .../data/tests/gh921-dmd-20220428-0.oracle | 1 + .../data/tests/gh921-dmd-20230319-0.oracle | 1 + pydra/tasks/mriqc/interfaces/__init__.py | 55 + .../mriqc/interfaces/anatomical/__init__.py | 52 + .../interfaces/anatomical/artifact_mask.py | 119 + .../interfaces/anatomical/compute_qi2.py | 40 + .../mriqc/interfaces/anatomical/harmonize.py | 74 + .../interfaces/anatomical/rotation_mask.py | 61 + .../interfaces/anatomical/structural_qc.py | 295 + .../interfaces/anatomical/tests/conftest.py | 25 + .../anatomical/tests/test_artifactmask.py | 21 + .../anatomical/tests/test_computeqi2.py | 17 + .../anatomical/tests/test_harmonize.py | 19 + .../anatomical/tests/test_rotationmask.py | 16 + .../anatomical/tests/test_structuralqc.py | 27 + pydra/tasks/mriqc/interfaces/bids/__init__.py | 25 + .../mriqc/interfaces/bids/iqm_file_sink.py | 197 + .../mriqc/interfaces/bids/tests/conftest.py | 25 + .../interfaces/bids/tests/test_iqmfilesink.py | 16 + .../tasks/mriqc/interfaces/common/__init__.py | 2 + .../common/conform_image/__init__.py | 29 + .../common/conform_image/conform_image.py | 126 + .../common/conform_image/tests/conftest.py | 25 + .../conform_image/tests/test_conformimage.py | 18 + .../interfaces/common/ensure_size/__init__.py | 1 + .../common/ensure_size/ensure_size.py | 139 + .../common/ensure_size/tests/conftest.py | 25 + .../ensure_size/tests/test_ensuresize.py | 18 + .../mriqc/interfaces/derivatives_data_sink.py | 300 + .../mriqc/interfaces/diffusion/__init__.py | 197 + .../interfaces/diffusion/cc_segmentation.py | 215 + .../diffusion/correct_signal_drift.py | 129 + .../interfaces/diffusion/diffusion_model.py | 182 + .../interfaces/diffusion/diffusion_qc.py | 229 + .../diffusion/extract_orientations.py | 61 + .../interfaces/diffusion/filter_shells.py | 83 + .../interfaces/diffusion/number_of_shells.py | 130 + .../mriqc/interfaces/diffusion/piesno.py | 95 + .../interfaces/diffusion/read_dwi_metadata.py | 120 + .../interfaces/diffusion/rotate_vectors.py | 70 + .../diffusion/spiking_voxels_mask.py | 111 + .../interfaces/diffusion/split_shells.py | 55 + .../interfaces/diffusion/tests/conftest.py | 25 + .../diffusion/tests/test_ccsegmentation.py | 21 + .../tests/test_correctsignaldrift.py | 22 + .../diffusion/tests/test_diffusionmodel.py | 19 + .../diffusion/tests/test_diffusionqc.py | 32 + .../tests/test_extractorientations.py | 19 + .../diffusion/tests/test_filtershells.py | 18 + .../diffusion/tests/test_numberofshells.py | 18 + .../interfaces/diffusion/tests/test_piesno.py | 17 + .../diffusion/tests/test_readdwimetadata.py | 18 + .../diffusion/tests/test_rotatevectors.py | 18 + .../diffusion/tests/test_spikingvoxelsmask.py | 18 + .../diffusion/tests/test_splitshells.py | 16 + .../diffusion/tests/test_weightedstat.py | 17 + .../interfaces/diffusion/weighted_stat.py | 57 + .../mriqc/interfaces/functional/__init__.py | 214 + .../interfaces/functional/functional_qc.py | 173 + .../functional/gather_timeseries.py | 163 + .../interfaces/functional/select_echo.py | 140 + .../mriqc/interfaces/functional/spikes.py | 124 + .../interfaces/functional/tests/conftest.py | 25 + .../functional/tests/test_functionalqc.py | 23 + .../functional/tests/test_gathertimeseries.py | 20 + .../functional/tests/test_selectecho.py | 17 + .../functional/tests/test_spikes.py | 24 + .../mriqc/interfaces/reports/__init__.py | 1 + .../interfaces/reports/add_provenance.py | 60 + .../interfaces/reports/tests/conftest.py | 25 + .../reports/tests/test_addprovenance.py | 18 + .../mriqc/interfaces/synthstrip/__init__.py | 1 + .../interfaces/synthstrip/synth_strip.py | 43 + .../interfaces/synthstrip/tests/conftest.py | 25 + .../synthstrip/tests/test_synthstrip.py | 20 + .../tasks/mriqc/interfaces/tests/conftest.py | 25 + .../tests/test_derivativesdatasink.py | 21 + .../mriqc/interfaces/transitional/__init__.py | 1 + .../mriqc/interfaces/transitional/gcor.py | 63 + .../interfaces/transitional/tests/conftest.py | 25 + .../transitional/tests/test_gcor.py | 29 + .../tasks/mriqc/interfaces/webapi/__init__.py | 182 + .../mriqc/interfaces/webapi/tests/conftest.py | 25 + .../webapi/tests/test_uploadiqms.py | 15 + .../mriqc/interfaces/webapi/upload_iq_ms.py | 269 + pydra/tasks/mriqc/messages.py | 21 + pydra/tasks/mriqc/nipype_ports/__init__.py | 27 + .../mriqc/nipype_ports/algorithms/__init__.py | 12 + .../algorithms/confounds/__init__.py | 22 + .../algorithms/confounds/compute_dvars.py | 195 + .../confounds/framewise_displacement.py | 105 + .../confounds/non_steady_state_detector.py | 38 + .../algorithms/confounds/tests/conftest.py | 25 + .../confounds/tests/test_computedvars.py | 30 + .../tests/test_framewisedisplacement.py | 25 + .../tests/test_nonsteadystatedetector.py | 18 + .../algorithms/confounds/tests/test_tsnr.py | 20 + .../nipype_ports/algorithms/confounds/tsnr.py | 97 + .../mriqc/nipype_ports/utils/__init__.py | 15 + .../mriqc/nipype_ports/utils/filemanip.py | 11 + pydra/tasks/mriqc/nipype_ports/utils/misc.py | 4 + pydra/tasks/mriqc/qc/__init__.py | 24 + pydra/tasks/mriqc/qc/anatomical.py | 417 + pydra/tasks/mriqc/qc/diffusion.py | 210 + pydra/tasks/mriqc/qc/functional.py | 62 + pydra/tasks/mriqc/synthstrip/ORIGINAL_LICENSE | 191 + pydra/tasks/mriqc/synthstrip/__init__.py | 0 pydra/tasks/mriqc/synthstrip/__main__.py | 34 + pydra/tasks/mriqc/synthstrip/cli.py | 235 + pydra/tasks/mriqc/synthstrip/model.py | 180 + pydra/tasks/mriqc/utils/__init__.py | 2 + pydra/tasks/mriqc/utils/bids.py | 95 + pydra/tasks/mriqc/utils/misc.py | 33 + pydra/tasks/mriqc/workflows/__init__.py | 31 + .../mriqc/workflows/anatomical/__init__.py | 10 + .../tasks/mriqc/workflows/anatomical/base.py | 807 + .../mriqc/workflows/anatomical/output.py | 159 + .../workflows/anatomical/tests/conftest.py | 25 + ...est_workflows_anatomical_base_airmsk_wf.py | 21 + ...kflows_anatomical_base_anat_qc_workflow.py | 22 + ..._workflows_anatomical_base_compute_iqms.py | 21 + ...st_workflows_anatomical_base_headmsk_wf.py | 21 + ...cal_base_init_brain_tissue_segmentation.py | 21 + ...s_anatomical_base_spatial_normalization.py | 21 + ...s_anatomical_output_init_anat_report_wf.py | 21 + .../mriqc/workflows/diffusion/__init__.py | 2 + pydra/tasks/mriqc/workflows/diffusion/base.py | 673 + .../tasks/mriqc/workflows/diffusion/output.py | 162 + .../workflows/diffusion/tests/conftest.py | 25 + ...t_workflows_diffusion_base_compute_iqms.py | 21 + ...rkflows_diffusion_base_dmri_qc_workflow.py | 22 + ..._workflows_diffusion_base_epi_mni_align.py | 21 + ...t_workflows_diffusion_base_hmc_workflow.py | 21 + ...ows_diffusion_output_init_dwi_report_wf.py | 21 + .../mriqc/workflows/functional/__init__.py | 2 + .../tasks/mriqc/workflows/functional/base.py | 770 + .../mriqc/workflows/functional/output.py | 276 + .../workflows/functional/tests/conftest.py | 25 + ..._workflows_functional_base_compute_iqms.py | 21 + ...workflows_functional_base_epi_mni_align.py | 21 + ...lows_functional_base_fmri_bmsk_workflow.py | 21 + ...kflows_functional_base_fmri_qc_workflow.py | 21 + .../test_workflows_functional_base_hmc.py | 21 + ...s_functional_output_init_func_report_wf.py | 21 + pydra/tasks/mriqc/workflows/shared.py | 68 + pydra/tasks/mriqc/workflows/tests/conftest.py | 25 + .../test_workflows_shared_synthstrip_wf.py | 21 + pydra/tasks/mriqc/workflows/utils.py | 176 + pyproject.toml | 2 +- test_scripts/orig-affinie-init.txt | 9 + test_scripts/orig-mriqc-registration-cmd.txt | 79 + test_scripts/pydra-affine-init.txt | 9 + test_scripts/pydra-mriqc-registration-cmd.txt | 77 + test_scripts/run_anat_wf.py | 9 +- 222 files changed, 154774 insertions(+), 6 deletions(-) create mode 100644 nipype-auto-conv/generate.py create mode 100644 niworkflows/utils/bids.py create mode 100644 niworkflows/utils/images.py create mode 100644 niworkflows/utils/misc.py create mode 100644 pydra/tasks/mriqc/_post_release.py create mode 100644 pydra/tasks/mriqc/_version.py create mode 100644 pydra/tasks/mriqc/data/NOTICE create mode 100644 pydra/tasks/mriqc/data/__init__.py create mode 100644 pydra/tasks/mriqc/data/bootstrap-anat.yml create mode 100644 pydra/tasks/mriqc/data/bootstrap-dwi.yml create mode 100644 pydra/tasks/mriqc/data/bootstrap-func.yml create mode 100644 pydra/tasks/mriqc/data/config-example.toml create mode 100644 pydra/tasks/mriqc/data/config.py create mode 100644 pydra/tasks/mriqc/data/fsexport.tcl create mode 100644 pydra/tasks/mriqc/data/itk_identity.tfm create mode 100644 pydra/tasks/mriqc/data/reports/embed_resources/boxplots.css create mode 100644 pydra/tasks/mriqc/data/reports/embed_resources/boxplots.js create mode 100644 pydra/tasks/mriqc/data/reports/embed_resources/d3.min.js create mode 100644 pydra/tasks/mriqc/data/reports/group.html create mode 100644 pydra/tasks/mriqc/data/reports/resources/DO_NOT_REMOVE_OR_MODIFY create mode 100644 pydra/tasks/mriqc/data/reports/resources/boxplots.css create mode 100644 pydra/tasks/mriqc/data/reports/resources/boxplots.js create mode 100644 pydra/tasks/mriqc/data/reports/resources/d3.min.js create mode 100644 pydra/tasks/mriqc/data/testdata/group_T1w.tsv create mode 100644 pydra/tasks/mriqc/data/testdata/group_bold.tsv create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/CHANGES create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/README create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/dataset_description.json create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/participants.tsv create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/anat/sub-01_T1w.nii.gz create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/anat/sub-01_inplaneT2.nii.gz create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.json create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.nii.gz create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_events.tsv create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.json create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.nii.gz create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_events.tsv create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_bold.json create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_bold.nii.gz create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_events.tsv create mode 100644 pydra/tasks/mriqc/data/tests/ds000005/task-mixedgamblestask_bold.json create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/dataset_description.json create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/anat/sub-0017_T1w.json create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-airmask_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-artifacts_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-background_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-brainmask_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-head_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-noisefit_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-norm_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-segmentation_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-zoomed_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/anat/sub-0042_T1w.json create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-airmask_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-artifacts_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-background_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-brainmask_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-head_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-noisefit_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-norm_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-segmentation_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-zoomed_T1w.svg create mode 100644 pydra/tasks/mriqc/data/tests/gh1086-ds004134.oracle create mode 100644 pydra/tasks/mriqc/data/tests/gh921-dmd-20220428-0.oracle create mode 100644 pydra/tasks/mriqc/data/tests/gh921-dmd-20230319-0.oracle create mode 100644 pydra/tasks/mriqc/interfaces/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/artifact_mask.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/compute_qi2.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/harmonize.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/rotation_mask.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/structural_qc.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/tests/test_artifactmask.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/tests/test_computeqi2.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/tests/test_harmonize.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/tests/test_rotationmask.py create mode 100644 pydra/tasks/mriqc/interfaces/anatomical/tests/test_structuralqc.py create mode 100644 pydra/tasks/mriqc/interfaces/bids/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/bids/iqm_file_sink.py create mode 100644 pydra/tasks/mriqc/interfaces/bids/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/bids/tests/test_iqmfilesink.py create mode 100644 pydra/tasks/mriqc/interfaces/common/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/common/conform_image/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/common/conform_image/conform_image.py create mode 100644 pydra/tasks/mriqc/interfaces/common/conform_image/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/common/conform_image/tests/test_conformimage.py create mode 100644 pydra/tasks/mriqc/interfaces/common/ensure_size/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/common/ensure_size/ensure_size.py create mode 100644 pydra/tasks/mriqc/interfaces/common/ensure_size/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/common/ensure_size/tests/test_ensuresize.py create mode 100644 pydra/tasks/mriqc/interfaces/derivatives_data_sink.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/cc_segmentation.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/correct_signal_drift.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/diffusion_model.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/diffusion_qc.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/extract_orientations.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/filter_shells.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/number_of_shells.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/piesno.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/read_dwi_metadata.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/rotate_vectors.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/spiking_voxels_mask.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/split_shells.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_ccsegmentation.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_correctsignaldrift.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_diffusionmodel.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_diffusionqc.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_extractorientations.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_filtershells.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_numberofshells.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_piesno.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_readdwimetadata.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_rotatevectors.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_spikingvoxelsmask.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_splitshells.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/tests/test_weightedstat.py create mode 100644 pydra/tasks/mriqc/interfaces/diffusion/weighted_stat.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/functional_qc.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/gather_timeseries.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/select_echo.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/spikes.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/tests/test_functionalqc.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/tests/test_gathertimeseries.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/tests/test_selectecho.py create mode 100644 pydra/tasks/mriqc/interfaces/functional/tests/test_spikes.py create mode 100644 pydra/tasks/mriqc/interfaces/reports/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/reports/add_provenance.py create mode 100644 pydra/tasks/mriqc/interfaces/reports/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/reports/tests/test_addprovenance.py create mode 100644 pydra/tasks/mriqc/interfaces/synthstrip/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/synthstrip/synth_strip.py create mode 100644 pydra/tasks/mriqc/interfaces/synthstrip/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/synthstrip/tests/test_synthstrip.py create mode 100644 pydra/tasks/mriqc/interfaces/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/tests/test_derivativesdatasink.py create mode 100644 pydra/tasks/mriqc/interfaces/transitional/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/transitional/gcor.py create mode 100644 pydra/tasks/mriqc/interfaces/transitional/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/transitional/tests/test_gcor.py create mode 100644 pydra/tasks/mriqc/interfaces/webapi/__init__.py create mode 100644 pydra/tasks/mriqc/interfaces/webapi/tests/conftest.py create mode 100644 pydra/tasks/mriqc/interfaces/webapi/tests/test_uploadiqms.py create mode 100644 pydra/tasks/mriqc/interfaces/webapi/upload_iq_ms.py create mode 100644 pydra/tasks/mriqc/messages.py create mode 100644 pydra/tasks/mriqc/nipype_ports/__init__.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/__init__.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/__init__.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/compute_dvars.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/framewise_displacement.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/non_steady_state_detector.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/conftest.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_computedvars.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_framewisedisplacement.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_nonsteadystatedetector.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_tsnr.py create mode 100644 pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tsnr.py create mode 100644 pydra/tasks/mriqc/nipype_ports/utils/__init__.py create mode 100644 pydra/tasks/mriqc/nipype_ports/utils/filemanip.py create mode 100644 pydra/tasks/mriqc/nipype_ports/utils/misc.py create mode 100644 pydra/tasks/mriqc/qc/__init__.py create mode 100644 pydra/tasks/mriqc/qc/anatomical.py create mode 100644 pydra/tasks/mriqc/qc/diffusion.py create mode 100644 pydra/tasks/mriqc/qc/functional.py create mode 100644 pydra/tasks/mriqc/synthstrip/ORIGINAL_LICENSE create mode 100644 pydra/tasks/mriqc/synthstrip/__init__.py create mode 100644 pydra/tasks/mriqc/synthstrip/__main__.py create mode 100644 pydra/tasks/mriqc/synthstrip/cli.py create mode 100644 pydra/tasks/mriqc/synthstrip/model.py create mode 100644 pydra/tasks/mriqc/utils/__init__.py create mode 100644 pydra/tasks/mriqc/utils/bids.py create mode 100644 pydra/tasks/mriqc/utils/misc.py create mode 100644 pydra/tasks/mriqc/workflows/__init__.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/__init__.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/base.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/output.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/tests/conftest.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_airmsk_wf.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_anat_qc_workflow.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_compute_iqms.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_headmsk_wf.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_init_brain_tissue_segmentation.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_spatial_normalization.py create mode 100644 pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_output_init_anat_report_wf.py create mode 100644 pydra/tasks/mriqc/workflows/diffusion/__init__.py create mode 100644 pydra/tasks/mriqc/workflows/diffusion/base.py create mode 100644 pydra/tasks/mriqc/workflows/diffusion/output.py create mode 100644 pydra/tasks/mriqc/workflows/diffusion/tests/conftest.py create mode 100644 pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_compute_iqms.py create mode 100644 pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_dmri_qc_workflow.py create mode 100644 pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_epi_mni_align.py create mode 100644 pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_hmc_workflow.py create mode 100644 pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_output_init_dwi_report_wf.py create mode 100644 pydra/tasks/mriqc/workflows/functional/__init__.py create mode 100644 pydra/tasks/mriqc/workflows/functional/base.py create mode 100644 pydra/tasks/mriqc/workflows/functional/output.py create mode 100644 pydra/tasks/mriqc/workflows/functional/tests/conftest.py create mode 100644 pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_compute_iqms.py create mode 100644 pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_epi_mni_align.py create mode 100644 pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_fmri_bmsk_workflow.py create mode 100644 pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_fmri_qc_workflow.py create mode 100644 pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_hmc.py create mode 100644 pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_output_init_func_report_wf.py create mode 100644 pydra/tasks/mriqc/workflows/shared.py create mode 100644 pydra/tasks/mriqc/workflows/tests/conftest.py create mode 100644 pydra/tasks/mriqc/workflows/tests/test_workflows_shared_synthstrip_wf.py create mode 100644 pydra/tasks/mriqc/workflows/utils.py create mode 100644 test_scripts/orig-affinie-init.txt create mode 100644 test_scripts/orig-mriqc-registration-cmd.txt create mode 100644 test_scripts/pydra-affine-init.txt create mode 100644 test_scripts/pydra-mriqc-registration-cmd.txt diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml index 7f860d7..c76733b 100644 --- a/.github/workflows/ci-cd.yaml +++ b/.github/workflows/ci-cd.yaml @@ -145,7 +145,7 @@ jobs: if: ${{ always() }} with: files: coverage.xml - name: pydra-mriqc + name: pydra-tasks-mriqc deploy: needs: [test] diff --git a/.gitignore b/.gitignore index 970d75d..8483889 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,3 @@ dmypy.json # Mac garbarge .DS_store -/pydra diff --git a/docs/conf.py b/docs/conf.py index 03a8f65..b2f81b1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,7 @@ # -- Project information ----------------------------------------------------- -project = "pydra-mriqc" +project = "pydra-tasks-mriqc" copyright = "2020, Xihe Xie" author = "Xihe Xie" diff --git a/nipype-auto-conv/generate.py b/nipype-auto-conv/generate.py new file mode 100644 index 0000000..343fa4b --- /dev/null +++ b/nipype-auto-conv/generate.py @@ -0,0 +1,14 @@ +from pathlib import Path +from click.testing import CliRunner +from nipype2pydra.cli import convert + + +spec_dir = Path(__file__).parent / "specs" +conv_dir = Path(__file__).parent.parent + +runner = CliRunner() +result = runner.invoke( + convert, + args=[str(spec_dir), str(conv_dir)], + catch_exceptions=False, +) diff --git a/nipype-auto-conv/specs/package.yaml b/nipype-auto-conv/specs/package.yaml index 455a55a..37adc7e 100644 --- a/nipype-auto-conv/specs/package.yaml +++ b/nipype-auto-conv/specs/package.yaml @@ -22,6 +22,10 @@ config_params: varname: config.environment type: struct module: mriqc + inputs_entities: + varname: config.workflow.inputs_entities + type: struct + module: mriqc omit_functions: - nipype.external.due.BibTeX omit_classes: diff --git a/niworkflows/utils/bids.py b/niworkflows/utils/bids.py new file mode 100644 index 0000000..4a2e6d4 --- /dev/null +++ b/niworkflows/utils/bids.py @@ -0,0 +1,65 @@ +from bids import BIDSLayout +import logging +from pathlib import Path + + +logger = logging.getLogger(__name__) + + +def relative_to_root(path): + """ + Calculate the BIDS root folder given one file path's. + + Examples + -------- + >>> str(relative_to_root( + ... "/sub-03/sourcedata/sub-01/anat/sub-01_T1.nii.gz" + ... )) + 'sub-01/anat/sub-01_T1.nii.gz' + + >>> str(relative_to_root( + ... "/sub-03/anat/sourcedata/sub-01/ses-preop/anat/sub-01_ses-preop_T1.nii.gz" + ... )) + 'sub-01/ses-preop/anat/sub-01_ses-preop_T1.nii.gz' + + >>> str(relative_to_root( + ... "sub-01/anat/sub-01_T1.nii.gz" + ... )) + 'sub-01/anat/sub-01_T1.nii.gz' + + >>> str(relative_to_root("anat/sub-01_T1.nii.gz")) + 'anat/sub-01_T1.nii.gz' + + """ + path = Path(path) + if path.name.startswith("sub-"): + parents = [path.name] + for p in path.parents: + parents.insert(0, p.name) + if p.name.startswith("sub-"): + return Path(*parents) + return path + raise ValueError( + f"Could not determine the BIDS root of <{path}>. " + "Only files under a subject directory are currently supported." + ) + + +def _init_layout(in_file=None, bids_dir=None, validate=True, database_path=None): + + if isinstance(bids_dir, BIDSLayout): + return bids_dir + if bids_dir is None: + in_file = Path(in_file) + for parent in in_file.parents: + if parent.name.startswith("sub-"): + bids_dir = parent.parent.resolve() + break + if bids_dir is None: + raise RuntimeError("Could not infer BIDS root") + layout = BIDSLayout( + str(bids_dir), + validate=validate, + database_path=database_path, + ) + return layout diff --git a/niworkflows/utils/images.py b/niworkflows/utils/images.py new file mode 100644 index 0000000..6236590 --- /dev/null +++ b/niworkflows/utils/images.py @@ -0,0 +1,33 @@ +from gzip import GzipFile +import logging +import nibabel as nb + + +logger = logging.getLogger(__name__) + + +def set_consumables(header, dataobj): + + header.set_slope_inter(dataobj.slope, dataobj.inter) + header.set_data_offset(dataobj.offset) + + +def unsafe_write_nifti_header_and_data(fname, header, data): + """Write header and data without any consistency checks or data munging + + This is almost always a bad idea, and you should not use this function + without a battery of tests for your specific use case. + + If you're not using this for NIfTI files specifically, you're playing + with Fortran-ordered fire. + """ + with open(fname, "wb") as fobj: + # Avoid setting fname or mtime, for deterministic outputs + if str(fname).endswith(".gz"): + fobj = GzipFile("", "wb", 9, fobj, 0.0) + header.write_to(fobj) + # This function serializes one block at a time to reduce memory usage a bit + # It assumes Fortran-ordered data. + nb.volumeutils.array_to_file(data, fobj, offset=header.get_data_offset()) + if str(fname).endswith(".gz"): + fobj.close() diff --git a/niworkflows/utils/misc.py b/niworkflows/utils/misc.py new file mode 100644 index 0000000..1a4f41f --- /dev/null +++ b/niworkflows/utils/misc.py @@ -0,0 +1,43 @@ +import logging +import os + + +logger = logging.getLogger(__name__) + + +def _copy_any(src, dst): + + import os + import gzip + from shutil import copyfileobj + from pydra.tasks.mriqc.nipype_ports.utils.filemanip import copyfile + + src_isgz = src.endswith(".gz") + dst_isgz = dst.endswith(".gz") + if not src_isgz and not dst_isgz: + copyfile(src, dst, copy=True, use_hardlink=True) + return False # Make sure we do not reuse the hardlink later + # Unlink target (should not exist) + if os.path.exists(dst): + os.unlink(dst) + src_open = gzip.open if src_isgz else open + with src_open(src, "rb") as f_in: + with open(dst, "wb") as f_out: + if dst_isgz: + # Remove FNAME header from gzip (nipreps/fmriprep#1480) + gz_out = gzip.GzipFile("", "wb", 9, f_out, 0.0) + copyfileobj(f_in, gz_out) + gz_out.close() + else: + copyfileobj(f_in, f_out) + return True + + +def unlink(pathlike, missing_ok=False): + """Backport of Path.unlink from Python 3.8+ with missing_ok keyword""" + # PY37 hack; drop when python_requires >= 3.8 + try: + os.unlink(pathlike) + except FileNotFoundError: + if not missing_ok: + raise diff --git a/pydra/tasks/mriqc/__init__.py b/pydra/tasks/mriqc/__init__.py index eda89fd..e1537e4 100644 --- a/pydra/tasks/mriqc/__init__.py +++ b/pydra/tasks/mriqc/__init__.py @@ -15,7 +15,7 @@ from ._version import __version__ except ImportError: raise RuntimeError( - "pydra-mriqc has not been properly installed, please run " + "pydra-tasks-mriqc has not been properly installed, please run " f"`pip install -e {str(pkg_path)}` to install a development version" ) if "post" not in __version__: diff --git a/pydra/tasks/mriqc/_post_release.py b/pydra/tasks/mriqc/_post_release.py new file mode 100644 index 0000000..506f6ee --- /dev/null +++ b/pydra/tasks/mriqc/_post_release.py @@ -0,0 +1,6 @@ +# Auto-generated by /Users/tclose/git/workflows/nipype2pydra/nipype2pydra/package.py, do not edit as it will be overwritten + +src_pkg_version = "25.1.0" +nipype2pydra_version = "0.4.5" +post_release = "2510045" + \ No newline at end of file diff --git a/pydra/tasks/mriqc/_version.py b/pydra/tasks/mriqc/_version.py new file mode 100644 index 0000000..f37f66b --- /dev/null +++ b/pydra/tasks/mriqc/_version.py @@ -0,0 +1,16 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '0.1.1.dev7+g52551d9.d20240929' +__version_tuple__ = version_tuple = (0, 1, 1, 'dev7', 'g52551d9.d20240929') diff --git a/pydra/tasks/mriqc/data/NOTICE b/pydra/tasks/mriqc/data/NOTICE new file mode 100644 index 0000000..939b4ca --- /dev/null +++ b/pydra/tasks/mriqc/data/NOTICE @@ -0,0 +1,17 @@ +MRIQC +Copyright © The NiPreps Developers. + +This product includes software developed by +the NiPreps Community (https://nipreps.org/). + +Portions of this software were developed at the Department of +Psychology at Stanford University, Stanford, CA, US. + +This software contains code ultimately derived from the +PCP Quality Assessment Protocol (QAP; +http://preprocessed-connectomes-project.org/quality-assessment-protocol) +by C. Craddock, S. Giavasis, D. Clark, Z. Shezhad, and J. Pellman. + +This software is also distributed as a Docker container image. +The bootstrapping file for the image ("Dockerfile") is licensed +under the MIT License. diff --git a/pydra/tasks/mriqc/data/__init__.py b/pydra/tasks/mriqc/data/__init__.py new file mode 100644 index 0000000..f4aed61 --- /dev/null +++ b/pydra/tasks/mriqc/data/__init__.py @@ -0,0 +1,37 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2024 The NiPreps 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. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +""" +MRIQC data files + +.. autofunction:: load + +.. automethod:: load.readable + +.. automethod:: load.as_path + +.. automethod:: load.cached +""" + +from acres import Loader + +load = Loader(__package__) diff --git a/pydra/tasks/mriqc/data/bootstrap-anat.yml b/pydra/tasks/mriqc/data/bootstrap-anat.yml new file mode 100644 index 0000000..048bf23 --- /dev/null +++ b/pydra/tasks/mriqc/data/bootstrap-anat.yml @@ -0,0 +1,104 @@ +# Copyright 2023 The NiPreps 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. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +########################################################################### +# Reports bootstrap file +# ====================== +# This is a YAML-formatted file specifying how the NiReports assembler +# will search for "reportlets" and compose them into a report file, +# typically in HTML format. +########################################################################### + +packagename: mriqc +title: '{filename} :: Anatomical MRI report' +sections: +- name: Summary + reportlets: + - bids: {datatype: figures, desc: summary, extension: [.html]} +- name: Basic visual report + reportlets: + - bids: {datatype: figures, desc: background} + caption: This panel shows a mosaic enhancing the background around the head. + Artifacts usually unveil themselves in the air surrounding the head, where no signal + sources are present. + subtitle: View of the background of the anatomical image + - bids: {datatype: figures, desc: zoomed} + caption: This panel shows a mosaic of the brain. This mosaic is the most suitable to + screen head-motion intensity inhomogeneities, global/local noise, signal leakage + (for example, from the eyeballs and across the phase-encoding axis), etc. + subtitle: Zoomed-in mosaic view of the brain +- name: Extended visual report + reportlets: + - bids: {datatype: figures, desc: airmask} + caption: The hat-mask calculated internally by MRIQC. Some metrics will use this + mask, for instance, to find out artifacts and estimate the spread of gaussian noise + added to the signal. This mask leaves out the air around the face to avoid measuring + noise sourcing from the eyeballs and their movement. + subtitle: '«Hat»-mask' + - bids: {datatype: figures, desc: noisefit} + caption: The noise fit internally estimated by MRIQC to calculate the QI1 index + proposed by Mortamet et al. (2009). + subtitle: Distribution of the noise within the hat mask + style: + max-width: 450px + - bids: {datatype: figures, desc: artifacts} + caption: Mask of artifactual intensities identified within the hat-mask. + subtitle: Artifactual intensities on the background + - bids: {datatype: figures, desc: brainmask} + caption: Brain mask as internally extracted by MRIQC. Defects on the brainmask could + indicate problematic aspects of the image quality-wise. + subtitle: Brain extraction performance + - bids: {datatype: figures, desc: head} + caption: A mask of the head calculated internally by MRIQC. + subtitle: Head mask + - bids: {datatype: figures, desc: segmentation} + caption: Brain tissue segmentation, as internally extracted by MRIQC. + Defects on this segmentation, as well as noisy tissue labels could + indicate problematic aspects of the image quality-wise. + subtitle: Brain tissue segmentation + - bids: {datatype: figures, desc: norm} + caption: This panel shows a quick-and-dirty nonlinear registration into + the MNI152NLin2009cAsym template accessed with + TemplateFlow. + subtitle: Spatial normalization of the anatomical image + static: false + +- name: About + nested: true + reportlets: + - custom: errors + path: '{reportlets_dir}/{run_uuid}' + captions: MRIQC may have recorded failure conditions. + title: Errors + - metadata: "input" + settings: + # By default, only the first dictionary will be expanded. + # If folded is true, all will be folded. If false all expanded. + folded: true + # If an ID is not provided, one should be generated automatically + id: 'about-metadata' + caption: | + Thanks for using MRIQC. The following information may assist in + reconstructing the provenance of the corresponding derivatives. + title: Reproducibility and provenance information + +# Rating widget +plugins: +- module: nireports.assembler + path: data/rating-widget/bootstrap.yml diff --git a/pydra/tasks/mriqc/data/bootstrap-dwi.yml b/pydra/tasks/mriqc/data/bootstrap-dwi.yml new file mode 100644 index 0000000..999cd11 --- /dev/null +++ b/pydra/tasks/mriqc/data/bootstrap-dwi.yml @@ -0,0 +1,82 @@ +# Copyright 2023 The NiPreps 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. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +########################################################################### +# Reports bootstrap file +# ====================== +# This is a YAML-formatted file specifying how the NiReports assembler +# will search for "reportlets" and compose them into a report file, +# typically in HTML format. +########################################################################### + +packagename: mriqc +title: '{filename} :: Diffusion MRI MRIQC report' +sections: +- name: Summary + reportlets: + - bids: {datatype: figures, desc: summary, extension: [.html]} + - bids: {datatype: figures, desc: heatmap} + caption: This visualization divides the data by shells, and shows the joint distribution + of SNR vs. FA. At the bottom, the distributions are marginalized for SNR. + Please note that the figures of SNR provided are calculated with a coarse estimation of + the signal variability, and therefore should be interpreted with care. + subtitle: Shell-wise joint distribution of SNR vs. FA in every voxel + - bids: {datatype: figures, desc: fa} + caption: Reconstructed FA map. + subtitle: Fractional anisotropy (FA) map + - bids: {datatype: figures, desc: md} + caption: Reconstructed MD map. + subtitle: Mean diffusivity (MD) map +- name: DWI shells + ordering: bval + reportlets: + - bids: {datatype: figures, desc: avgstd} + caption: This panel shows mosaics flickering between the voxel-wise average and standard deviation + for each shell. + subtitle: Voxel-wise average and standard deviation across volumes in this DWI shell. + static: false + - bids: {datatype: figures, desc: background} + caption: This panel shows a mosaic enhancing the background around the head. + Artifacts usually unveil themselves in the air surrounding the head, where no signal + sources are present. + subtitle: View of the background of the voxel-wise average of this DWI shell + +- name: About + nested: true + reportlets: + - custom: errors + path: '{reportlets_dir}/{run_uuid}' + captions: MRIQC may have recorded failure conditions. + title: Errors + - metadata: "input" + settings: + # By default, only the first dictionary will be expanded. + # If folded is true, all will be folded. If false all expanded. + folded: true + # If an ID is not provided, one should be generated automatically + id: 'about-metadata' + caption: | + Thanks for using MRIQC. The following information may assist in + reconstructing the provenance of the corresponding derivatives. + title: Reproducibility and provenance information + +# Rating widget +plugins: +- module: nireports.assembler + path: data/rating-widget/bootstrap.yml diff --git a/pydra/tasks/mriqc/data/bootstrap-func.yml b/pydra/tasks/mriqc/data/bootstrap-func.yml new file mode 100644 index 0000000..5e34fc1 --- /dev/null +++ b/pydra/tasks/mriqc/data/bootstrap-func.yml @@ -0,0 +1,97 @@ +# Copyright 2023 The NiPreps 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. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +########################################################################### +# Reports bootstrap file +# ====================== +# This is a YAML-formatted file specifying how the NiReports assembler +# will search for "reportlets" and compose them into a report file, +# typically in HTML format. +########################################################################### + +packagename: mriqc +title: "{filename} :: MRIQC's BOLD fMRI report" +sections: +- name: Summary + reportlets: + - bids: {datatype: figures, desc: summary, extension: [.html]} +- name: Basic echo-wise reports + ordering: echo + reportlets: + - bids: {datatype: figures, desc: stdev} + subtitle: Standard deviation of signal through time + caption: The voxel-wise standard deviation of the signal (variability along time). + - bids: {datatype: figures, desc: background} + caption: This panel shows a mosaic enhancing the background around the head. + Artifacts usually unveil themselves in the air surrounding the head, where no signal + sources are present. + subtitle: View of the background of the voxel-wise average of the BOLD timeseries + - bids: {datatype: figures, desc: zoomed} + caption: This panel shows a mosaic of the brain. This mosaic is the most suitable to + screen head-motion intensity inhomogeneities, global/local noise, signal leakage + (for example, from the eyeballs and across the phase-encoding axis), etc. + subtitle: Voxel-wise average of BOLD time-series, zoomed-in covering just the brain + - bids: {datatype: figures, desc: carpet} + subtitle: Carpetplot and nuisance signals + caption: The so-called «carpetplot» may assist in assessing head-motion + derived artifacts and respiation effects. + +- name: Extended echo-wise reports + ordering: echo + reportlets: + - bids: {datatype: figures, desc: mean} + subtitle: Voxel-wise average of BOLD time-series + caption: The average signal calculated across the last axis (time). + +- name: Extended reports shared across echos + reportlets: + - bids: {datatype: figures, desc: brainmask} + caption: Brain mask as internally extracted by MRIQC. Defects on the brainmask could + indicate problematic aspects of the image quality-wise. + subtitle: Brain extraction performance + - bids: {datatype: figures, desc: norm} + caption: This panel shows a quick-and-dirty nonlinear registration into + the MNI152NLin2009cAsym template accessed with + TemplateFlow. + subtitle: Spatial normalization of the anatomical image + static: false + +- name: About + nested: true + reportlets: + - custom: errors + path: '{reportlets_dir}/{run_uuid}' + captions: MRIQC may have recorded failure conditions. + title: Errors + - metadata: "input" + settings: + # By default, only the first dictionary will be expanded. + # If folded is true, all will be folded. If false all expanded. + folded: true + # If an ID is not provided, one should be generated automatically + id: 'about-metadata' + caption: | + Thanks for using MRIQC. The following information may assist in + reconstructing the provenance of the corresponding derivatives. + title: Reproducibility and provenance information + +# Rating widget +plugins: +- module: nireports.assembler + path: data/rating-widget/bootstrap.yml diff --git a/pydra/tasks/mriqc/data/config-example.toml b/pydra/tasks/mriqc/data/config-example.toml new file mode 100644 index 0000000..cb0519d --- /dev/null +++ b/pydra/tasks/mriqc/data/config-example.toml @@ -0,0 +1,60 @@ +[environment] +cpu_count = 8 +exec_env = "posix" +free_mem = 10.8 +overcommit_policy = "heuristic" +overcommit_limit = "50%" +nipype_version = "1.4.2" +templateflow_version = "0.5.2" +version = "0.15.2" + +[execution] +ants_float = false +bids_dir = "data/" +debug = false +dry_run = false +dsname = "ds000005" +float32 = true +layout = "BIDS Layout: data/ | Subjects: 16 | Sessions: 0 | Runs: 48" +log_dir = "derivatives/mriqc/logs" +log_level = 15 +no_sub = false +output_dir = "derivatives/" +participant_label = [ "01",] +reports_only = false +run_uuid = "20200403-185126_db5d5e64-4e98-4a75-b3d1-ab880afa0e85" +templateflow_home = "/opt/templateflow" +upload_strict = false +verbose_reports = false +webapi_url = "https://mriqc.nimh.nih.gov/api/v1" +webapi_port = 443 +work_dir = "work/" +write_graph = false + +[workflow] +analysis_level = [ "participant",] +biggest_file_gb = 0.03619009628891945 +deoblique = false +despike = false +fd_thres = 0.2 +fd_radius = 50 +fft_spikes_detector = false +ica = false +template_id = "MNI152NLin2009cAsym" + +[nipype] +crashfile_format = "txt" +get_linked_libs = false +nprocs = 8 +omp_nthreads = 8 +plugin = "MultiProc" +resource_monitor = false +stop_on_first_crash = true + +[workflow.inputs] +bold = [ "data/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.nii.gz", "data/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.nii.gz", "data/sub-01/func/sub-01_task-mixedgamblestask_run-03_bold.nii.gz",] +T1w = [ "data/sub-01/anat/sub-01_T1w.nii.gz",] + +[nipype.plugin_args] +maxtasksperchild = 1 +raise_insufficient = false diff --git a/pydra/tasks/mriqc/data/config.py b/pydra/tasks/mriqc/data/config.py new file mode 100644 index 0000000..1950d35 --- /dev/null +++ b/pydra/tasks/mriqc/data/config.py @@ -0,0 +1,56 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2021 The NiPreps 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. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Utilities: Jinja2 templates.""" + +from pathlib import Path + +from niworkflows.data import Loader + + +class GroupTemplate: + """Specific template for the individual report""" + + """ + Utility class for generating a config file from a jinja template. + https://github.com/oesteban/endofday/blob/f2e79c625d648ef45b08cc1f11fd0bd84342d604/endofday/core/template.py + """ + + def __init__(self): + import jinja2 + + self.template_str = Loader(__package__)('reports/group.html').absolute() + self.env = jinja2.Environment( + loader=jinja2.FileSystemLoader(searchpath='/'), + trim_blocks=True, + lstrip_blocks=True, + autoescape=False, # noqa: S701 + ) + + def compile(self, configs): + """Generates a string with the replacements""" + template = self.env.get_template(str(self.template_str)) + return template.render(configs) + + def generate_conf(self, configs, path): + """Saves the oucome after replacement on the template to file""" + Path(path).write_text(self.compile(configs)) diff --git a/pydra/tasks/mriqc/data/fsexport.tcl b/pydra/tasks/mriqc/data/fsexport.tcl new file mode 100644 index 0000000..3df1337 --- /dev/null +++ b/pydra/tasks/mriqc/data/fsexport.tcl @@ -0,0 +1,107 @@ +# Sample script for setting up and taking screen shots. Scripting +# reference is available at: + +# https://surfer.nmr.mgh.harvard.edu/fswiki/TkMeditGuide/TkMeditReference/TkMeditScripting + +# You can set the cursor or view center with the SetCursor command. + + + +# Alternatively you can set the slice number (in volume index +# coordinates). This will not change the in-plane center. + +# Use SetZoomLevel to zoom in and out. 1 is normal, >1 is zoomed in, +# and 0-1 is zoomed out. + +# SetZoomLevel level +SetZoomLevel 2 +# SetZoomCenter 0 0 0 + +# SetOrientation orientation +# orientation: +# 0 coronal +# 1 horizontal +# 2 sagittal +SetOrientation 0 + +# This command turns on and off various display flags. +# SetDisplayFlag flag value +# flag: +# 1 Aux Volume - set to 1 to show aux volume +# 2 Anatomical Volume - set to 0 to hide main and aux volume +# 3 Cursor +# 4 Main Surface +# 5 Original Surface +# 6 Pial Surface +# 7 Interpolate Surface Vertices +# 8 Surface Vertices +# 9 Control Points +# 10 Selection +# 11 Functional Overlay +# 12 Functional Color Scale Bar +# 13 Mask to Functional Overlay +# 14 Histogram Percent Change +# 15 Segmentation Volume Overlay +# 16 Aux Segmentation Volume +# 17 Segmentation Label Volume Count +# 18 DTI Overlay +# 20 Focus Frame +# 21 Undoable Voxels +# 22 Axes +# 23 Maximum Intensity Projection +# 24 Head Points +# 25 Verbose GCA DumpSetDisplayFlag + +# SetCursor coordinateSpace x y z +# coordinateSpace: +# 0 volume index +# 1 RAS +# 2 Talairach +SetCursor 0 128 128 128 + +# Turn cursor display off. +SetDisplayFlag 3 0 + +# Turn the axes on. +SetDisplayFlag 22 1 + +# Use this command to go to multiple views. This will copy the current +# view settings from the current view, so all the above commands will +# apply to all new views. +# SetDisplayConfig numberOfColumns numberOfRows linkPolicy +# linkPolicy: +# 0 none +# 1 linked cursors +# 2 linked slice changes +# SetDisplayConfig 2 2 1 + +# Use the RedrawScreen command to force a redraw after you have a view +# set up, before taking a picture. +# RedrawScreen + +# This command will save the actual screenshot. +# SaveTiff fileName +# SaveTIFF $::env(FS_OUTPUT_PATH)/screenshot.tif + + +# Use tcl loops to change orientations and take multiple +# screenshots. This will set the view to a single view, and take three +# screenshots, one of each orientation. +# SetDisplayConfig 1 1 0 +# foreach orientation {0 1 2} label {cor horiz sag} { + +# SetOrientation $orientation +# RedrawScreen +# SaveTIFF $::env(FS_OUTPUT_PATH)/screenshot-$label.tif +# } + +# Or take pictures of multiple slices for a movie. This goes through +# slices 0-255 and takes a shot at each one. +for { set slice 30 } { $slice < 226 } { incr slice } { + SetZoomCenter 160 80 $slice + SetSlice $slice + RedrawScreen + SaveTIFF $::env(FS_OUTPUT_PATH)[format "/screenshot-%03d.tif" $slice] +} + +QuitMedit diff --git a/pydra/tasks/mriqc/data/itk_identity.tfm b/pydra/tasks/mriqc/data/itk_identity.tfm new file mode 100644 index 0000000..2ba6b8a --- /dev/null +++ b/pydra/tasks/mriqc/data/itk_identity.tfm @@ -0,0 +1,5 @@ +#Insight Transform File V1.0 +#Transform 0 +Transform: IdentityTransform_double_3_3 +Parameters: +FixedParameters: diff --git a/pydra/tasks/mriqc/data/reports/embed_resources/boxplots.css b/pydra/tasks/mriqc/data/reports/embed_resources/boxplots.css new file mode 100644 index 0000000..0938a5e --- /dev/null +++ b/pydra/tasks/mriqc/data/reports/embed_resources/boxplots.css @@ -0,0 +1,157 @@ +/* Hide data table */ +.csvdata { + display: none; +} + +body { + font-family: helvetica; +} + +.text-warning { + font-weight: bold; + color: red; +} +/*Primary Chart*/ + +/*Nested divs for responsiveness*/ +.chart-wrapper { + max-width: 800px; /*Overwritten by the JS*/ + min-width: 160px; + margin-bottom: 20px; + font-family: helvetica; +} +.chart-wrapper .inner-wrapper { + position: relative; + padding-bottom: 50%; /*Overwritten by the JS*/ + width: 100%; +} +.chart-wrapper .outer-box { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.chart-wrapper .inner-box { + width: 100%; + height: 100%; +} + +.chart-wrapper text { + font-family: helvetica; + font-size: 13px; +} + +.chart-wrapper .axis path, +.chart-wrapper .axis line { + fill: none; + stroke: #888; + stroke-width: 2px; + shape-rendering: crispEdges; +} + +.chart-wrapper .y.axis .tick line { + stroke: lightgrey; + opacity: 0.6; + stroke-dasharray: 2,1; + stroke-width: 1; + shape-rendering: crispEdges; + +} + +.chart-wrapper .x.axis .domain { + display: none; +} + +.chart-wrapper div.tooltip { + position: absolute; + text-align: left; + padding: 3px; + font-size: 12px; + background: #eee; + border: 0px; + border-radius: 1px; + pointer-events: none; + opacity: .7; + z-index: 10; +} + +/*Box Plot*/ +.chart-wrapper .box-plot .box { + fill-opacity: .5; + stroke-width: 2; +} +.chart-wrapper .box-plot line { + stroke-width: 2px; +} +.chart-wrapper .box-plot circle { + fill: white; + stroke: black; +} + +.chart-wrapper .box-plot .median { + stroke: black; +} + +.chart-wrapper .box-plot circle.median { + /*the script makes the circles the same color as the box, you can override this in the js*/ + fill: white !important; +} + +.chart-wrapper .box-plot .mean { + stroke: white; + stroke-dasharray: 2,1; + stroke-width: 1px; +} + +@media (max-width:500px){ + .chart-wrapper .box-plot circle {display: none;} +} + +/*Violin Plot*/ + +.chart-wrapper .violin-plot .area { + shape-rendering: geometricPrecision; + opacity: 0.4; +} + +.chart-wrapper .violin-plot .line { + fill: none; + stroke-width: 2px; + shape-rendering: geometricPrecision; +} + +/*Notch Plot*/ +.chart-wrapper .notch-plot .notch { + fill-opacity: 0.4; + stroke-width: 2; +} + +/* Point Plots*/ +.chart-wrapper .points-plot .point { + /*stroke: black; + stroke-width: 1px;*/ + fill-opacity: 0.4; +} + +.chart-wrapper .metrics-lines { + stroke-width: 4px; +} + +/* Non-Chart Styles for demo*/ +.chart-options { + min-width: 200px; + font-size: 13px; + font-family: helvetica; +} +.chart-options button { + margin: 3px; + padding: 3px; + font-size: 12px; +} +.chart-options p { + display: inline; +} +@media (max-width:500px){ + .chart-options p {display: block;} +} \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/reports/embed_resources/boxplots.js b/pydra/tasks/mriqc/data/reports/embed_resources/boxplots.js new file mode 100644 index 0000000..0198b31 --- /dev/null +++ b/pydra/tasks/mriqc/data/reports/embed_resources/boxplots.js @@ -0,0 +1,1634 @@ +/** + * @fileOverview A D3 based distribution chart system. Supports: Box plots, Violin plots, Notched box plots, trend lines, beeswarm plot + * @version 3.0 + */ + + +/** + * Creates a box plot, violin plot, and or notched box plot + * @param settings Configuration options for the base plot + * @param settings.data The data for the plot + * @param settings.xName The name of the column that should be used for the x groups + * @param settings.yName The name of the column used for the y values + * @param {string} settings.selector The selector string for the main chart div + * @param [settings.axisLabels={}] Defaults to the xName and yName + * @param [settings.yTicks = 1] 1 = default ticks. 2 = double, 0.5 = half + * @param [settings.scale='linear'] 'linear' or 'log' - y scale of the chart + * @param [settings.chartSize={width:800, height:400}] The height and width of the chart itself (doesn't include the container) + * @param [settings.margin={top: 15, right: 60, bottom: 40, left: 50}] The margins around the chart (inside the main div) + * @param [settings.constrainExtremes=false] Should the y scale include outliers? + * @returns {object} chart A chart object + */ +function makeDistroChart(settings) { + + var chart = {}; + + // Defaults + chart.settings = { + data: null, + xName: null, + yName: null, + axisLabels: {xAxis: null, yAxis: null}, + labelName: "label", + unitsName: "units", + selector: null, + axisLables: null, + yTicks: 1, + scale: 'linear', + chartSize: {width: 800, height: 400}, + margin: {top: 15, right: 10, bottom: 50, left: 50}, + constrainExtremes: false, + color: d3.scale.category10(), + modality: null + }; + + for (var setting in settings) { + chart.settings[setting] = settings[setting] + } + + function formatAsFloat(d) { + if (d % 1 !== 0) { + return d3.format(".2f")(d); + } else { + return d3.format(".0f")(d); + } + } + + function logFormatNumber(d) { + var x = Math.log(d) / Math.log(10) + 1e-6; + return Math.abs(x - Math.floor(x)) < 0.6 ? formatAsFloat(d) : ""; + } + + chart.yFormatter = formatAsFloat; + + chart.data = chart.settings.data; + + iqmName = chart.data[0][chart.settings.xName] + if (iqmName.indexOf('_') > 0) { + iqmName = iqmName.substr(0, iqmName.indexOf('_')) + } + chart.settings.axisLabels.yAxis = iqmName.toUpperCase() + if (iqmName.toLowerCase().startsWith('fd') || iqmName.toLowerCase().startsWith('spikes')) { + chart.settings.constrainExtremes = true + } + + units = chart.data[0][chart.settings.unitsName] + if (units) { + chart.settings.axisLabels.yAxis += ' (' + units + ')' + } + + + chart.groupObjs = {}; //The data organized by grouping and sorted as well as any metadata for the groups + chart.objs = {mainDiv: null, chartDiv: null, g: null, xAxis: null, yAxis: null}; + chart.colorFunct = null; + + /** + * Takes an array, function, or object mapping and created a color function from it + * @param {function|[]|object} colorOptions + * @returns {function} Function to be used to determine chart colors + */ + function getColorFunct(colorOptions) { + if (typeof colorOptions == 'function') { + return colorOptions + } else if (Array.isArray(colorOptions)) { + // If an array is provided, map it to the domain + var colorMap = {}, cColor = 0; + for (var cName in chart.groupObjs) { + colorMap[cName] = colorOptions[cColor]; + cColor = (cColor + 1) % colorOptions.length; + } + return function (group) { + return colorMap[group]; + } + } else if (typeof colorOptions == 'object') { + // if an object is provided, assume it maps to the colors + return function (group) { + return colorOptions[group]; + } + } else { + return d3.scale.category10(); + } + } + + /** + * Takes a percentage as returns the values that correspond to that percentage of the group range width + * @param objWidth Percentage of range band + * @param gName The bin name to use to get the x shift + * @returns {{left: null, right: null, middle: null}} + */ + function getObjWidth(objWidth, gName) { + var objSize = {left: null, right: null, middle: null}; + var width = chart.xScale.rangeBand() * (objWidth / 100); + var padding = (chart.xScale.rangeBand() - width) / 2; + var gShift = chart.xScale(gName); + objSize.middle = chart.xScale.rangeBand() / 2 + gShift; + objSize.left = padding + gShift; + objSize.right = objSize.left + width; + return objSize; + } + + /** + * Adds jitter to the scatter point plot + * @param doJitter true or false, add jitter to the point + * @param width percent of the range band to cover with the jitter + * @returns {number} + */ + function addJitter(doJitter, width) { + if (doJitter !== true || width == 0) { + return 0 + } + return Math.floor(Math.random() * width) - width / 2; + } + + function shallowCopy(oldObj) { + var newObj = {}; + for (var i in oldObj) { + if (oldObj.hasOwnProperty(i)) { + newObj[i] = oldObj[i]; + } + } + return newObj; + } + + /** + * Closure that creates the tooltip hover function + * @param groupName Name of the x group + * @param metrics Object to use to get values for the group + * @returns {Function} A function that provides the values for the tooltip + */ + function tooltipHover(groupName, metrics) { + var tooltipString = "Group: " + groupName; + tooltipString += "Max: " + formatAsFloat(metrics.max, 0.1); + tooltipString += "Q3: " + formatAsFloat(metrics.quartile3); + tooltipString += "Median: " + formatAsFloat(metrics.median); + tooltipString += "Q1: " + formatAsFloat(metrics.quartile1); + tooltipString += "Min: " + formatAsFloat(metrics.min); + return function () { + chart.objs.tooltip.transition().duration(200).style("opacity", 0.9); + chart.objs.tooltip.html(tooltipString) + }; + } + + function axislabelHover(groupName) { + var tooltipString = "Go to definition of " + groupName; + return function () { + chart.objs.tooltip.transition().duration(200).style("opacity", 1.0); + chart.objs.tooltip.html(tooltipString) + }; + } + + /** + * Closure that creates the tooltip hover function + * @param groupName Name of the x group + * @param metrics Object to use to get values for the group + * @returns {Function} A function that provides the values for the tooltip + */ + function pointHover(label, value) { + var tooltipString = "Subject: " + label + "Measure: " + value + return function () { + chart.objs.tooltip.transition().duration(200).style("opacity", 1.0); + chart.objs.tooltip.html(tooltipString) + }; + } + + /** + * Parse the data and calculates base values for the plots + */ + !function prepareData() { + function calcMetrics(values) { + // Do not reorder in-place + values = values.slice(0).sort(d3.ascending) + + var metrics = { //These are the original non�scaled values + max: null, + upperOuterFence: null, + upperInnerFence: null, + quartile3: null, + median: null, + mean: null, + iqr: null, + quartile1: null, + lowerInnerFence: null, + lowerOuterFence: null, + min: null + }; + + metrics.min = d3.min(values); + metrics.quartile1 = d3.quantile(values, 0.25); + metrics.median = d3.median(values); + metrics.mean = d3.mean(values); + metrics.quartile3 = d3.quantile(values, 0.75); + metrics.max = d3.max(values); + metrics.iqr = metrics.quartile3 - metrics.quartile1; + + //The inner fences are the closest value to the IQR without going past it (assumes sorted lists) + var LIF = metrics.quartile1 - (1.5 * metrics.iqr); + var UIF = metrics.quartile3 + (1.5 * metrics.iqr); + for (var i = 0; i <= values.length; i++) { + if (values[i] < LIF) { + continue; + } + if (!metrics.lowerInnerFence && values[i] >= LIF) { + metrics.lowerInnerFence = values[i]; + continue; + } + if (values[i] > UIF) { + metrics.upperInnerFence = values[i - 1]; + break; + } + } + + + metrics.lowerOuterFence = metrics.quartile1 - (3 * metrics.iqr); + metrics.upperOuterFence = metrics.quartile3 + (3 * metrics.iqr); + if (!metrics.lowerInnerFence) { + metrics.lowerInnerFence = metrics.min; + } + if (!metrics.upperInnerFence) { + metrics.upperInnerFence = metrics.max; + } + return metrics + } + + var current_x = null; + var current_y = null; + var current_row; + + // Group the values + for (current_row = 0; current_row < chart.data.length; current_row++) { + current_x = chart.data[current_row][chart.settings.xName]; + current_y = chart.data[current_row][chart.settings.yName]; + current_label = chart.data[current_row][chart.settings.labelName]; + + if (chart.groupObjs.hasOwnProperty(current_x)) { + chart.groupObjs[current_x].values.push(current_y); + chart.groupObjs[current_x].labels.push(current_label); + } else { + chart.groupObjs[current_x] = {}; + chart.groupObjs[current_x].values = [current_y]; + chart.groupObjs[current_x].labels = [current_label]; + } + } + + for (var cName in chart.groupObjs) { + // chart.groupObjs[cName].values.sort(d3.ascending); + chart.groupObjs[cName].metrics = {}; + chart.groupObjs[cName].metrics = calcMetrics(chart.groupObjs[cName].values); + + } + }(); + + /** + * Prepare the chart settings and chart div and svg + */ + !function prepareSettings() { + //Set base settings + chart.margin = chart.settings.margin; + chart.divWidth = chart.settings.chartSize.width; + chart.divHeight = chart.settings.chartSize.height; + chart.width = chart.divWidth - chart.margin.left - chart.margin.right; + chart.height = chart.divHeight - chart.margin.top - chart.margin.bottom; + + if (chart.settings.axisLabels) { + chart.xAxisLable = chart.settings.axisLabels.xAxis; + chart.yAxisLable = chart.settings.axisLabels.yAxis; + } + + if (chart.settings.scale === 'log') { + chart.yScale = d3.scale.log(); + chart.yFormatter = logFormatNumber; + } else { + chart.yScale = d3.scale.linear(); + } + + if (chart.settings.constrainExtremes === true) { + var fences = []; + for (var cName in chart.groupObjs) { + fences.push(chart.groupObjs[cName].metrics.lowerInnerFence); + fences.push(chart.groupObjs[cName].metrics.upperInnerFence); + } + chart.range = d3.extent(fences); + + } else { + chart.range = d3.extent(chart.data, function (d) {return d[chart.settings.yName];}); + } + + chart.colorFunct = getColorFunct(chart.settings.colors); + + // Build Scale functions + chart.yScale.range([chart.height, 0]).domain(chart.range).nice().clamp(true); + chart.xScale = d3.scale.ordinal().domain(Object.keys(chart.groupObjs)).rangeBands([0, chart.width]); + + //Build Axes Functions + chart.objs.yAxis = d3.svg.axis() + .scale(chart.yScale) + .orient("left") + .tickFormat(chart.yFormatter) + .outerTickSize(0) + .innerTickSize(-chart.width + (chart.margin.right + chart.margin.left)); + chart.objs.yAxis.ticks(chart.objs.yAxis.ticks()*chart.settings.yTicks); + chart.objs.xAxis = d3.svg.axis().scale(chart.xScale).orient("bottom").tickSize(5); + }(); + + /** + * Updates the chart based on the current settings and window size + * @returns {*} + */ + chart.update = function () { + // Update chart size based on view port size + chart.width = parseInt(chart.objs.chartDiv.style("width"), 10) - (chart.margin.left + chart.margin.right); + chart.height = parseInt(chart.objs.chartDiv.style("height"), 10) - (chart.margin.top + chart.margin.bottom); + + // Update scale functions + chart.xScale.rangeBands([0, chart.width]); + chart.yScale.range([chart.height, 0]); + + // Update the yDomain if the Violin plot clamp is set to -1 meaning it will extend the violins to make nice points + if (chart.violinPlots && chart.violinPlots.options.show == true && chart.violinPlots.options._yDomainVP != null) { + chart.yScale.domain(chart.violinPlots.options._yDomainVP).nice().clamp(true); + } else { + chart.yScale.domain(chart.range).nice().clamp(true); + } + + //Update axes + chart.objs.g.select('.x.axis').attr("transform", "translate(0," + chart.height + ")").call(chart.objs.xAxis) + .selectAll("text") + .attr("y", 5) + .attr("x", -5) + .attr("transform", "rotate(-45)") + .style("text-anchor", "end"); + chart.objs.g.select('.x.axis .label').attr("x", chart.width / 2); + chart.objs.g.select('.y.axis').call(chart.objs.yAxis.innerTickSize(-chart.width)); + chart.objs.g.select('.y.axis .label').attr("x", -chart.height / 2); + chart.objs.chartDiv.select('svg').attr("width", chart.width + (chart.margin.left + chart.margin.right)).attr("height", chart.height + (chart.margin.top + chart.margin.bottom)); + + return chart; + }; + + /** + * Prepare the chart html elements + */ + !function prepareChart() { + // Build main div and chart div + chart.objs.mainDiv = d3.select(chart.settings.selector) + .style("width", chart.divWidth + "px") + .style("display", "inline-block"); + // Add all the divs to make it centered and responsive + chart.objs.mainDiv.append("div") + .attr("class", "inner-wrapper") + .style("padding-bottom", (chart.divHeight / chart.divWidth) * 100 + "%") + .append("div").attr("class", "outer-box") + .append("div").attr("class", "inner-box"); + // Capture the inner div for the chart (where the chart actually is) + chart.selector = chart.settings.selector + " .inner-box"; + chart.objs.chartDiv = d3.select(chart.selector); + d3.select(window).on('resize.' + chart.selector, chart.update); + + // Create the svg + chart.objs.g = chart.objs.chartDiv.append("svg") + .attr("class", "chart-area") + .attr("width", chart.width + (chart.margin.left + chart.margin.right)) + .attr("height", chart.height + (chart.margin.top + chart.margin.bottom)) + .append("g") + .attr("transform", "translate(" + chart.margin.left + "," + chart.margin.top + ")"); + + // Create axes + chart.objs.axes = chart.objs.g.append("g").attr("class", "axis"); + chart.objs.axes.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + chart.height + ")") + .call(chart.objs.xAxis); + chart.objs.axes.append("g") + .attr("class", "y axis") + .call(chart.objs.yAxis) + .append("text") + //.attr("class", "label") + .attr("transform", "rotate(-90)") + //.attr("y", -42) + .attr("y", 6) + .attr("dy", ".71em") + //.attr("x", -chart.height / 2) + .style("text-anchor", "end") + .style("font-size", "16px") + .append("a") + .attr("xlink:href", function(d) { + label = chart.yAxisLable.toLowerCase() + charidx = label.indexOf(' ') + if (charidx > 1) { label = label.substr(0, charidx); } + charidx = label.indexOf('_') + if (charidx > 1) { label = label.substr(0, charidx); } + + return "http://mriqc.readthedocs.io/en/latest/iqms/" + chart.settings.modality.toLowerCase() + ".html#iqms-" + label + }) + .text(chart.yAxisLable) + .on("mouseover", function () { + chart.objs.tooltip + .style("display", null) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); + }).on("mouseout", function () { + chart.objs.tooltip.style("display", "none"); + }).on("mousemove", axislabelHover(chart.yAxisLable)); + + // Create tooltip div + chart.objs.tooltip = chart.objs.mainDiv.append('div').attr('class', 'tooltip'); + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].g = chart.objs.g.append("g").attr("class", "group"); + } + chart.update(); + }(); + + /** + * Render a violin plot on the current chart + * @param options + * @param [options.showViolinPlot=true] True or False, show the violin plot + * @param [options.resolution=100 default] + * @param [options.bandwidth=10 default] May need higher bandwidth for larger data sets + * @param [options.width=50] The max percent of the group rangeBand that the violin can be + * @param [options.interpolation=''] How to render the violin + * @param [options.clamp=0 default] + * 0 = keep data within chart min and max, clamp once data = 0. May extend beyond data set min and max + * 1 = clamp at min and max of data set. Possibly no tails + * -1 = extend chart axis to make room for data to interpolate to 0. May extend axis and data set min and max + * @param [options.colors=chart default] The color mapping for the violin plot + * @returns {*} The chart object + */ + chart.renderViolinPlot = function (options) { + chart.violinPlots = {}; + + var defaultOptions = { + show: true, + showViolinPlot: true, + resolution: 100, + bandwidth: 20, + width: 50, + interpolation: 'cardinal', + clamp: 1, + colors: chart.colorFunct, + _yDomainVP: null // If the Violin plot is set to close all violin plots, it may need to extend the domain, that extended domain is stored here + }; + chart.violinPlots.options = shallowCopy(defaultOptions); + for (var option in options) { + chart.violinPlots.options[option] = options[option] + } + var vOpts = chart.violinPlots.options; + + // Create violin plot objects + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].violin = {}; + chart.groupObjs[cName].violin.objs = {}; + } + + /** + * Take a new set of options and redraw the violin + * @param updateOptions + */ + chart.violinPlots.change = function (updateOptions) { + if (updateOptions) { + for (var key in updateOptions) { + vOpts[key] = updateOptions[key] + } + } + + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].violin.objs.g.remove() + } + + chart.violinPlots.prepareViolin(); + chart.violinPlots.update(); + }; + + chart.violinPlots.reset = function () { + chart.violinPlots.change(defaultOptions) + }; + chart.violinPlots.show = function (opts) { + if (opts !== undefined) { + opts.show = true; + if (opts.reset) { + chart.violinPlots.reset() + } + } else { + opts = {show: true}; + } + chart.violinPlots.change(opts); + + }; + + chart.violinPlots.hide = function (opts) { + if (opts !== undefined) { + opts.show = false; + if (opts.reset) { + chart.violinPlots.reset() + } + } else { + opts = {show: false}; + } + chart.violinPlots.change(opts); + + }; + + /** + * Update the violin obj values + */ + chart.violinPlots.update = function () { + var cName, cViolinPlot; + + for (cName in chart.groupObjs) { + cViolinPlot = chart.groupObjs[cName].violin; + + // Build the violins sideways, so use the yScale for the xScale and make a new yScale + var xVScale = chart.yScale.copy(); + + + // Create the Kernel Density Estimator Function + cViolinPlot.kde = kernelDensityEstimator(eKernel(vOpts.bandwidth), xVScale.ticks(vOpts.resolution)); + cViolinPlot.kdedata = cViolinPlot.kde(chart.groupObjs[cName].values); + + var interpolateMax = chart.groupObjs[cName].metrics.max, + interpolateMin = chart.groupObjs[cName].metrics.min; + + if (vOpts.clamp == 0 || vOpts.clamp == -1) { // + // When clamp is 0, calculate the min and max that is needed to bring the violin plot to a point + // interpolateMax = the Minimum value greater than the max where y = 0 + interpolateMax = d3.min(cViolinPlot.kdedata.filter(function (d) { + return (d.x > chart.groupObjs[cName].metrics.max && d.y == 0) + }), function (d) { + return d.x; + }); + // interpolateMin = the Maximum value less than the min where y = 0 + interpolateMin = d3.max(cViolinPlot.kdedata.filter(function (d) { + return (d.x < chart.groupObjs[cName].metrics.min && d.y == 0) + }), function (d) { + return d.x; + }); + // If clamp is -1 we need to extend the axes so that the violins come to a point + if (vOpts.clamp == -1) { + kdeTester = eKernelTest(eKernel(vOpts.bandwidth), chart.groupObjs[cName].values); + if (!interpolateMax) { + var interMaxY = kdeTester(chart.groupObjs[cName].metrics.max); + var interMaxX = chart.groupObjs[cName].metrics.max; + var count = 25; // Arbitrary limit to make sure we don't get an infinite loop + while (count > 0 && interMaxY != 0) { + interMaxY = kdeTester(interMaxX); + interMaxX += 1; + count -= 1; + } + interpolateMax = interMaxX; + } + if (!interpolateMin) { + var interMinY = kdeTester(chart.groupObjs[cName].metrics.min); + var interMinX = chart.groupObjs[cName].metrics.min; + var count = 25; // Arbitrary limit to make sure we don't get an infinite loop + while (count > 0 && interMinY != 0) { + interMinY = kdeTester(interMinX); + interMinX -= 1; + count -= 1; + } + interpolateMin = interMinX; + } + + } + // Check to see if the new values are outside the existing chart range + // If they are assign them to the master _yDomainVP + if (!vOpts._yDomainVP) vOpts._yDomainVP = chart.range.slice(0); + if (interpolateMin && interpolateMin < vOpts._yDomainVP[0]) { + vOpts._yDomainVP[0] = interpolateMin; + } + if (interpolateMax && interpolateMax > vOpts._yDomainVP[1]) { + vOpts._yDomainVP[1] = interpolateMax; + } + + + } + + + if (vOpts.showViolinPlot) { + chart.update(); + xVScale = chart.yScale.copy(); + + // Need to recalculate the KDE because the xVScale changed + cViolinPlot.kde = kernelDensityEstimator(eKernel(vOpts.bandwidth), xVScale.ticks(vOpts.resolution)); + cViolinPlot.kdedata = cViolinPlot.kde(chart.groupObjs[cName].values); + } + + cViolinPlot.kdedata = cViolinPlot.kdedata + .filter(function (d) { + return (!interpolateMin || d.x >= interpolateMin) + }) + .filter(function (d) { + return (!interpolateMax || d.x <= interpolateMax) + }); + } + for (cName in chart.groupObjs) { + cViolinPlot = chart.groupObjs[cName].violin; + + // Get the violin width + var objBounds = getObjWidth(vOpts.width, cName); + var width = (objBounds.right - objBounds.left) / 2; + + var yVScale = d3.scale.linear() + .range([width, 0]) + .domain([0, d3.max(cViolinPlot.kdedata, function (d) {return d.y;})]) + .clamp(true); + + var area = d3.svg.area() + .interpolate(vOpts.interpolation) + .x(function (d) {return xVScale(d.x);}) + .y0(width) + .y1(function (d) {return yVScale(d.y);}); + + var line = d3.svg.line() + .interpolate(vOpts.interpolation) + .x(function (d) {return xVScale(d.x);}) + .y(function (d) {return yVScale(d.y)}); + + if (cViolinPlot.objs.left.area) { + cViolinPlot.objs.left.area + .datum(cViolinPlot.kdedata) + .attr("d", area); + cViolinPlot.objs.left.line + .datum(cViolinPlot.kdedata) + .attr("d", line); + + cViolinPlot.objs.right.area + .datum(cViolinPlot.kdedata) + .attr("d", area); + cViolinPlot.objs.right.line + .datum(cViolinPlot.kdedata) + .attr("d", line); + } + + // Rotate the violins + cViolinPlot.objs.left.g.attr("transform", "rotate(90,0,0) translate(0,-" + objBounds.left + ") scale(1,-1)"); + cViolinPlot.objs.right.g.attr("transform", "rotate(90,0,0) translate(0,-" + objBounds.right + ")"); + } + }; + + /** + * Create the svg elements for the violin plot + */ + chart.violinPlots.prepareViolin = function () { + var cName, cViolinPlot; + + if (vOpts.colors) { + chart.violinPlots.color = getColorFunct(vOpts.colors); + } else { + chart.violinPlots.color = chart.colorFunct + } + + if (vOpts.show == false) {return} + + for (cName in chart.groupObjs) { + cViolinPlot = chart.groupObjs[cName].violin; + + cViolinPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "violin-plot"); + cViolinPlot.objs.left = {area: null, line: null, g: null}; + cViolinPlot.objs.right = {area: null, line: null, g: null}; + + cViolinPlot.objs.left.g = cViolinPlot.objs.g.append("g"); + cViolinPlot.objs.right.g = cViolinPlot.objs.g.append("g"); + + if (vOpts.showViolinPlot !== false) { + //Area + cViolinPlot.objs.left.area = cViolinPlot.objs.left.g.append("path") + .attr("class", "area") + .style("fill", chart.violinPlots.color(cName)); + cViolinPlot.objs.right.area = cViolinPlot.objs.right.g.append("path") + .attr("class", "area") + .style("fill", chart.violinPlots.color(cName)); + + //Lines + cViolinPlot.objs.left.line = cViolinPlot.objs.left.g.append("path") + .attr("class", "line") + .attr("fill", 'none') + .style("stroke", chart.violinPlots.color(cName)); + cViolinPlot.objs.right.line = cViolinPlot.objs.right.g.append("path") + .attr("class", "line") + .attr("fill", 'none') + .style("stroke", chart.violinPlots.color(cName)); + } + + } + + }; + + + function kernelDensityEstimator(kernel, x) { + return function (sample) { + return x.map(function (x) { + return {x:x, y:d3.mean(sample, function (v) {return kernel(x - v);})}; + }); + }; + } + + function eKernel(scale) { + return function (u) { + return Math.abs(u /= scale) <= 1 ? .75 * (1 - u * u) / scale : 0; + }; + } + + // Used to find the roots for adjusting violin axis + // Given an array, find the value for a single point, even if it is not in the domain + function eKernelTest(kernel, array) { + return function (testX) { + return d3.mean(array, function (v) {return kernel(testX - v);}) + } + } + + chart.violinPlots.prepareViolin(); + + d3.select(window).on('resize.' + chart.selector + '.violinPlot', chart.violinPlots.update); + chart.violinPlots.update(); + return chart; + }; + + /** + * Render a box plot on the current chart + * @param options + * @param [options.show=true] Toggle the whole plot on and off + * @param [options.showBox=true] Show the box part of the box plot + * @param [options.showWhiskers=true] Show the whiskers + * @param [options.showMedian=true] Show the median line + * @param [options.showMean=false] Show the mean line + * @param [options.medianCSize=3] The size of the circle on the median + * @param [options.showOutliers=true] Plot outliers + * @param [options.boxwidth=30] The max percent of the group rangeBand that the box can be + * @param [options.lineWidth=boxWidth] The max percent of the group rangeBand that the line can be + * @param [options.outlierScatter=false] Spread out the outliers so they don't all overlap (in development) + * @param [options.outlierCSize=2] Size of the outliers + * @param [options.colors=chart default] The color mapping for the box plot + * @returns {*} The chart object + */ + chart.renderBoxPlot = function (options) { + chart.boxPlots = {}; + + // Defaults + var defaultOptions = { + show: true, + showBox: true, + showWhiskers: true, + showMedian: true, + showMean: false, + medianCSize: 3.5, + showOutliers: true, + boxWidth: 30, + lineWidth: null, + scatterOutliers: false, + outlierCSize: 2.5, + colors: chart.colorFunct, + padding: 0 + }; + chart.boxPlots.options = shallowCopy(defaultOptions); + for (var option in options) { + chart.boxPlots.options[option] = options[option] + } + var bOpts = chart.boxPlots.options; + + //Create box plot objects + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].boxPlot = {}; + chart.groupObjs[cName].boxPlot.objs = {}; + } + + + /** + * Calculates all the outlier points for each group + */ + !function calcAllOutliers() { + + /** + * Create lists of the outliers for each content group + * @param cGroup The object to modify + * @return null Modifies the object in place + */ + function calcOutliers(cGroup) { + var cExtremes = []; + var cOutliers = []; + var cOut, idx; + for (idx = 0; idx <= cGroup.values.length; idx++) { + cOut = {value: cGroup.values[idx]}; + + if (cOut.value < cGroup.metrics.lowerInnerFence) { + if (cOut.value < cGroup.metrics.lowerOuterFence) { + cExtremes.push(cOut); + } else { + cOutliers.push(cOut); + } + } else if (cOut.value > cGroup.metrics.upperInnerFence) { + if (cOut.value > cGroup.metrics.upperOuterFence) { + cExtremes.push(cOut); + } else { + cOutliers.push(cOut); + } + } + } + cGroup.boxPlot.objs.outliers = cOutliers; + cGroup.boxPlot.objs.extremes = cExtremes; + } + + for (var cName in chart.groupObjs) { + calcOutliers(chart.groupObjs[cName]); + } + }(); + + /** + * Take updated options and redraw the box plot + * @param updateOptions + */ + chart.boxPlots.change = function (updateOptions) { + if (updateOptions) { + for (var key in updateOptions) { + bOpts[key] = updateOptions[key] + } + } + + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].boxPlot.objs.g.remove() + } + chart.boxPlots.prepareBoxPlot(); + chart.boxPlots.update() + }; + + chart.boxPlots.reset = function () { + chart.boxPlots.change(defaultOptions) + }; + chart.boxPlots.show = function (opts) { + if (opts !== undefined) { + opts.show = true; + if (opts.reset) { + chart.boxPlots.reset() + } + } else { + opts = {show: true}; + } + chart.boxPlots.change(opts) + + }; + chart.boxPlots.hide = function (opts) { + if (opts !== undefined) { + opts.show = false; + if (opts.reset) { + chart.boxPlots.reset() + } + } else { + opts = {show: false}; + } + chart.boxPlots.change(opts) + }; + + /** + * Update the box plot obj values + */ + chart.boxPlots.update = function () { + var cName, cBoxPlot; + + for (cName in chart.groupObjs) { + cBoxPlot = chart.groupObjs[cName].boxPlot; + + // Get the box width + var objBounds = getObjWidth(bOpts.boxWidth, cName); + objBounds.middle += chart.boxPlots.options.padding + objBounds.right += chart.boxPlots.options.padding + objBounds.left += chart.boxPlots.options.padding + var width = (objBounds.right - objBounds.left); + + var sMetrics = {}; //temp var for scaled (plottable) metric values + for (var attr in chart.groupObjs[cName].metrics) { + sMetrics[attr] = null; + sMetrics[attr] = chart.yScale(chart.groupObjs[cName].metrics[attr]); + } + + // Box + if (cBoxPlot.objs.box) { + cBoxPlot.objs.box + .attr("x", objBounds.left) + .attr('width', width) + .attr("y", sMetrics.quartile3) + .attr("rx", 1) + .attr("ry", 1) + .attr("height", -sMetrics.quartile3 + sMetrics.quartile1) + } + + // Lines + var lineBounds = null; + if (bOpts.lineWidth) { + lineBounds = getObjWidth(bOpts.lineWidth, cName) + } else { + lineBounds = objBounds + } + + // Apply padding + lineBounds.middle += chart.boxPlots.options.padding + lineBounds.right += chart.boxPlots.options.padding + lineBounds.left += chart.boxPlots.options.padding + + // --Whiskers + if (cBoxPlot.objs.upperWhisker) { + cBoxPlot.objs.upperWhisker.fence + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', sMetrics.upperInnerFence) + .attr("y2", sMetrics.upperInnerFence); + cBoxPlot.objs.upperWhisker.line + .attr("x1", lineBounds.middle) + .attr("x2", lineBounds.middle) + .attr('y1', sMetrics.quartile3) + .attr("y2", sMetrics.upperInnerFence); + + cBoxPlot.objs.lowerWhisker.fence + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', sMetrics.lowerInnerFence) + .attr("y2", sMetrics.lowerInnerFence); + cBoxPlot.objs.lowerWhisker.line + .attr("x1", lineBounds.middle) + .attr("x2", lineBounds.middle) + .attr('y1', sMetrics.quartile1) + .attr("y2", sMetrics.lowerInnerFence); + } + + // --Median + if (cBoxPlot.objs.median) { + cBoxPlot.objs.median.line + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', sMetrics.median) + .attr("y2", sMetrics.median); + cBoxPlot.objs.median.circle + .attr("cx", lineBounds.middle) + .attr("cy", sMetrics.median) + } + + // --Mean + if (cBoxPlot.objs.mean) { + cBoxPlot.objs.mean.line + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', sMetrics.mean) + .attr("y2", sMetrics.mean); + cBoxPlot.objs.mean.circle + .attr("cx", lineBounds.middle) + .attr("cy", sMetrics.mean); + } + + // Outliers + + var pt; + if (cBoxPlot.objs.outliers) { + for (pt in cBoxPlot.objs.outliers) { + cBoxPlot.objs.outliers[pt].point + .attr("cx", objBounds.middle + addJitter(bOpts.scatterOutliers, width)) + .attr("cy", chart.yScale(cBoxPlot.objs.outliers[pt].value)); + } + } + if (cBoxPlot.objs.extremes) { + for (pt in cBoxPlot.objs.extremes) { + cBoxPlot.objs.extremes[pt].point + .attr("cx", objBounds.middle + addJitter(bOpts.scatterOutliers, width)) + .attr("cy", chart.yScale(cBoxPlot.objs.extremes[pt].value)); + } + } + } + }; + + /** + * Create the svg elements for the box plot + */ + chart.boxPlots.prepareBoxPlot = function () { + var cName, cBoxPlot; + + if (bOpts.colors) { + chart.boxPlots.colorFunct = getColorFunct(bOpts.colors); + } else { + chart.boxPlots.colorFunct = chart.colorFunct + } + + if (bOpts.show == false) { + return + } + + for (cName in chart.groupObjs) { + cBoxPlot = chart.groupObjs[cName].boxPlot; + + cBoxPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "box-plot"); + + //Plot Box (default show) + if (bOpts.showBox) { + cBoxPlot.objs.box = cBoxPlot.objs.g.append("rect") + .attr("class", "box") + .style("fill", chart.boxPlots.colorFunct(cName)) + .style("stroke", chart.boxPlots.colorFunct(cName)) + .on("mouseover", function () { + chart.objs.tooltip + .style("display", null) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); + }).on("mouseout", function () { + chart.objs.tooltip.style("display", "none"); + }).on("mousemove", tooltipHover(cName, chart.groupObjs[cName].metrics)); + //A stroke is added to the box with the group color, it is + // hidden by default and can be shown through css with stroke-width + } + + //Plot Median (default show) + if (bOpts.showMedian) { + cBoxPlot.objs.median = {line: null, circle: null}; + cBoxPlot.objs.median.line = cBoxPlot.objs.g.append("line") + .attr("class", "median"); + cBoxPlot.objs.median.circle = cBoxPlot.objs.g.append("circle") + .attr("class", "median") + .attr('r', bOpts.medianCSize) + .style("fill", chart.boxPlots.colorFunct(cName)); + } + + // Plot Mean (default no plot) + if (bOpts.showMean) { + cBoxPlot.objs.mean = {line: null, circle: null}; + cBoxPlot.objs.mean.line = cBoxPlot.objs.g.append("line") + .attr("class", "mean"); + cBoxPlot.objs.mean.circle = cBoxPlot.objs.g.append("circle") + .attr("class", "mean") + .attr('r', bOpts.medianCSize) + .style("fill", chart.boxPlots.colorFunct(cName)); + } + + // Plot Whiskers (default show) + if (bOpts.showWhiskers) { + cBoxPlot.objs.upperWhisker = {fence: null, line: null}; + cBoxPlot.objs.lowerWhisker = {fence: null, line: null}; + cBoxPlot.objs.upperWhisker.fence = cBoxPlot.objs.g.append("line") + .attr("class", "upper whisker") + .style("stroke", chart.boxPlots.colorFunct(cName)); + cBoxPlot.objs.upperWhisker.line = cBoxPlot.objs.g.append("line") + .attr("class", "upper whisker") + .style("stroke", chart.boxPlots.colorFunct(cName)); + + cBoxPlot.objs.lowerWhisker.fence = cBoxPlot.objs.g.append("line") + .attr("class", "lower whisker") + .style("stroke", chart.boxPlots.colorFunct(cName)); + cBoxPlot.objs.lowerWhisker.line = cBoxPlot.objs.g.append("line") + .attr("class", "lower whisker") + .style("stroke", chart.boxPlots.colorFunct(cName)); + } + + // Plot outliers (default show) + if (bOpts.showOutliers) { + if (!cBoxPlot.objs.outliers) calcAllOutliers(); + var pt; + if (cBoxPlot.objs.outliers.length) { + var outDiv = cBoxPlot.objs.g.append("g").attr("class", "boxplot outliers"); + for (pt in cBoxPlot.objs.outliers) { + cBoxPlot.objs.outliers[pt].point = outDiv.append("circle") + .attr("class", "outlier") + .attr('r', bOpts.outlierCSize) + .style("fill", chart.boxPlots.colorFunct(cName)); + } + } + + if (cBoxPlot.objs.extremes.length) { + var extDiv = cBoxPlot.objs.g.append("g").attr("class", "boxplot extremes"); + for (pt in cBoxPlot.objs.extremes) { + cBoxPlot.objs.extremes[pt].point = extDiv.append("circle") + .attr("class", "extreme") + .attr('r', bOpts.outlierCSize) + .style("stroke", chart.boxPlots.colorFunct(cName)); + } + } + } + + + } + }; + chart.boxPlots.prepareBoxPlot(); + + d3.select(window).on('resize.' + chart.selector + '.boxPlot', chart.boxPlots.update); + chart.boxPlots.update(); + return chart; + + }; + + /** + * Render a notched box on the current chart + * @param options + * @param [options.show=true] Toggle the whole plot on and off + * @param [options.showNotchBox=true] Show the notch box + * @param [options.showLines=false] Show lines at the confidence intervals + * @param [options.boxWidth=35] The width of the widest part of the box + * @param [options.medianWidth=20] The width of the narrowist part of the box + * @param [options.lineWidth=50] The width of the confidence interval lines + * @param [options.notchStyle=null] null=traditional style, 'box' cuts out the whole notch in right angles + * @param [options.colors=chart default] The color mapping for the notch boxes + * @returns {*} The chart object + */ + chart.renderNotchBoxes = function (options) { + chart.notchBoxes = {}; + + //Defaults + var defaultOptions = { + show: true, + showNotchBox: true, + showLines: false, + boxWidth: 35, + medianWidth: 20, + lineWidth: 50, + notchStyle: null, + colors: null + }; + chart.notchBoxes.options = shallowCopy(defaultOptions); + for (var option in options) { + chart.notchBoxes.options[option] = options[option] + } + var nOpts = chart.notchBoxes.options; + + //Create notch objects + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].notchBox = {}; + chart.groupObjs[cName].notchBox.objs = {}; + } + + /** + * Makes the svg path string for a notched box + * @param cNotch Current notch box object + * @param notchBounds objBound object + * @returns {string} A string in the proper format for a svg polygon + */ + function makeNotchBox(cNotch, notchBounds) { + var scaledValues = []; + if (nOpts.notchStyle == 'box') { + scaledValues = [ + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile1)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.medianLeft, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.medianLeft, chart.yScale(cNotch.metrics.median)], + [notchBounds.medianLeft, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile3)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile3)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.medianRight, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.medianRight, chart.yScale(cNotch.metrics.median)], + [notchBounds.medianRight, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile1)] + ]; + } else { + scaledValues = [ + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile1)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.medianLeft, chart.yScale(cNotch.metrics.median)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile3)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile3)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.medianRight, chart.yScale(cNotch.metrics.median)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile1)] + ]; + } + return scaledValues.map(function (d) { + return [d[0], d[1]].join(","); + }).join(" "); + } + + /** + * Calculate the confidence intervals + */ + !function calcNotches() { + var cNotch, modifier; + for (var cName in chart.groupObjs) { + cNotch = chart.groupObjs[cName]; + modifier = (1.57 * (cNotch.metrics.iqr / Math.sqrt(cNotch.values.length))); + cNotch.metrics.upperNotch = cNotch.metrics.median + modifier; + cNotch.metrics.lowerNotch = cNotch.metrics.median - modifier; + } + }(); + + /** + * Take a new set of options and redraw the notch boxes + * @param updateOptions + */ + chart.notchBoxes.change = function (updateOptions) { + if (updateOptions) { + for (var key in updateOptions) { + nOpts[key] = updateOptions[key] + } + } + + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].notchBox.objs.g.remove() + } + chart.notchBoxes.prepareNotchBoxes(); + chart.notchBoxes.update(); + }; + + chart.notchBoxes.reset = function () { + chart.notchBoxes.change(defaultOptions) + }; + chart.notchBoxes.show = function (opts) { + if (opts !== undefined) { + opts.show = true; + if (opts.reset) { + chart.notchBoxes.reset() + } + } else { + opts = {show: true}; + } + chart.notchBoxes.change(opts) + }; + chart.notchBoxes.hide = function (opts) { + if (opts !== undefined) { + opts.show = false; + if (opts.reset) { + chart.notchBoxes.reset() + } + } else { + opts = {show: false}; + } + chart.notchBoxes.change(opts) + }; + + /** + * Update the notch box obj values + */ + chart.notchBoxes.update = function () { + var cName, cGroup; + + for (cName in chart.groupObjs) { + cGroup = chart.groupObjs[cName]; + + // Get the box size + var boxBounds = getObjWidth(nOpts.boxWidth, cName); + var medianBounds = getObjWidth(nOpts.medianWidth, cName); + + var notchBounds = { + boxLeft: boxBounds.left, + boxRight: boxBounds.right, + middle: boxBounds.middle, + medianLeft: medianBounds.left, + medianRight: medianBounds.right + }; + + // Notch Box + if (cGroup.notchBox.objs.notch) { + cGroup.notchBox.objs.notch + .attr("points", makeNotchBox(cGroup, notchBounds)); + } + if (cGroup.notchBox.objs.upperLine) { + var lineBounds = null; + if (nOpts.lineWidth) { + lineBounds = getObjWidth(nOpts.lineWidth, cName) + } else { + lineBounds = objBounds + } + + var confidenceLines = { + upper: chart.yScale(cGroup.metrics.upperNotch), + lower: chart.yScale(cGroup.metrics.lowerNotch) + }; + cGroup.notchBox.objs.upperLine + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', confidenceLines.upper) + .attr("y2", confidenceLines.upper); + cGroup.notchBox.objs.lowerLine + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', confidenceLines.lower) + .attr("y2", confidenceLines.lower); + } + } + }; + + /** + * Create the svg elements for the notch boxes + */ + chart.notchBoxes.prepareNotchBoxes = function () { + var cName, cNotch; + + if (nOpts && nOpts.colors) { + chart.notchBoxes.colorFunct = getColorFunct(nOpts.colors); + } else { + chart.notchBoxes.colorFunct = chart.colorFunct + } + + if (nOpts.show == false) { + return + } + + for (cName in chart.groupObjs) { + cNotch = chart.groupObjs[cName].notchBox; + + cNotch.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "notch-plot"); + + // Plot Box (default show) + if (nOpts.showNotchBox) { + cNotch.objs.notch = cNotch.objs.g.append("polygon") + .attr("class", "notch") + .style("fill", chart.notchBoxes.colorFunct(cName)) + .style("stroke", chart.notchBoxes.colorFunct(cName)); + //A stroke is added to the notch with the group color, it is + // hidden by default and can be shown through css with stroke-width + } + + //Plot Confidence Lines (default hide) + if (nOpts.showLines) { + cNotch.objs.upperLine = cNotch.objs.g.append("line") + .attr("class", "upper confidence line") + .style("stroke", chart.notchBoxes.colorFunct(cName)); + + cNotch.objs.lowerLine = cNotch.objs.g.append("line") + .attr("class", "lower confidence line") + .style("stroke", chart.notchBoxes.colorFunct(cName)); + } + } + }; + chart.notchBoxes.prepareNotchBoxes(); + + d3.select(window).on('resize.' + chart.selector + '.notchBox', chart.notchBoxes.update); + chart.notchBoxes.update(); + return chart; + }; + + /** + * Render a raw data in various forms + * @param options + * @param [options.show=true] Toggle the whole plot on and off + * @param [options.showPlot=false] True or false, show points + * @param [options.plotType='none'] Options: no scatter = (false or 'none'); scatter points= (true or [amount=% of width (default=10)]); beeswarm points = ('beeswarm') + * @param [options.pointSize=6] Diameter of the circle in pizels (not the radius) + * @param [options.showLines=['median']] Can equal any of the metrics lines + * @param [options.showbeanLines=false] Options: no lines = false + * @param [options.beanWidth=20] % width + * @param [options.colors=chart default] + * @returns {*} The chart object + * + */ + chart.renderDataPlots = function (options) { + chart.dataPlots = {}; + + + //Defaults + var defaultOptions = { + show: true, + showPlot: false, + plotType: 'none', + pointSize: 6, + showLines: false,//['median'], + showBeanLines: false, + beanWidth: 20, + colors: null, + padding: 0 + }; + chart.dataPlots.options = shallowCopy(defaultOptions); + for (var option in options) { + chart.dataPlots.options[option] = options[option] + } + var dOpts = chart.dataPlots.options; + + //Create notch objects + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].dataPlots = {}; + chart.groupObjs[cName].dataPlots.objs = {}; + } + // The lines don't fit into a group bucket so they live under the dataPlot object + chart.dataPlots.objs = {}; + + /** + * Take updated options and redraw the data plots + * @param updateOptions + */ + chart.dataPlots.change = function (updateOptions) { + if (updateOptions) { + for (var key in updateOptions) { + dOpts[key] = updateOptions[key] + } + } + + chart.dataPlots.objs.g.remove(); + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].dataPlots.objs.g.remove() + } + chart.dataPlots.preparePlots(); + chart.dataPlots.update() + }; + + chart.dataPlots.reset = function () { + chart.dataPlots.change(defaultOptions) + }; + chart.dataPlots.show = function (opts) { + if (opts !== undefined) { + opts.show = true; + if (opts.reset) { + chart.dataPlots.reset() + } + } else { + opts = {show: true}; + } + chart.dataPlots.change(opts) + }; + chart.dataPlots.hide = function (opts) { + if (opts !== undefined) { + opts.show = false; + if (opts.reset) { + chart.dataPlots.reset() + } + } else { + opts = {show: false}; + } + chart.dataPlots.change(opts) + }; + + /** + * Update the data plot obj values + */ + chart.dataPlots.update = function () { + var cName, cGroup, cPlot; + + // Metrics lines + if (chart.dataPlots.objs.g) { + var halfBand = chart.xScale.rangeBand() / 2; // find the middle of each band + for (var cMetric in chart.dataPlots.objs.lines) { + chart.dataPlots.objs.lines[cMetric].line + .x(function (d) { + return chart.xScale(d.x) + halfBand + }); + chart.dataPlots.objs.lines[cMetric].g + .datum(chart.dataPlots.objs.lines[cMetric].values) + .attr('d', chart.dataPlots.objs.lines[cMetric].line); + } + } + + + for (cName in chart.groupObjs) { + cGroup = chart.groupObjs[cName]; + cPlot = cGroup.dataPlots; + + if (cPlot.objs.points) { + if (dOpts.plotType == 'beeswarm') { + var swarmBounds = getObjWidth(100, cName); + var yPtScale = chart.yScale.copy() + .range([Math.floor(chart.yScale.range()[0] / dOpts.pointSize), 0]) + .interpolate(d3.interpolateRound) + .domain(chart.yScale.domain()); + var maxWidth = Math.floor(chart.xScale.rangeBand() / dOpts.pointSize); + var ptsObj = {}; + var cYBucket = null; + // Bucket points + for (var pt = 0; pt < cGroup.values.length; pt++) { + cYBucket = yPtScale(cGroup.values[pt]); + if (ptsObj.hasOwnProperty(cYBucket) !== true) { + ptsObj[cYBucket] = []; + } + ptsObj[cYBucket].push(cPlot.objs.points.pts[pt] + .attr("cx", swarmBounds.middle) + .attr("cy", yPtScale(cGroup.values[pt]) * dOpts.pointSize)); + } + // Plot buckets + var rightMax = Math.min(swarmBounds.right - dOpts.pointSize); + for (var row in ptsObj) { + var leftMin = swarmBounds.left + (Math.max((maxWidth - ptsObj[row].length) / 2, 0) * dOpts.pointSize); + var col = 0; + for (pt in ptsObj[row]) { + ptsObj[row][pt].attr("cx", Math.min(leftMin + col * dOpts.pointSize, rightMax) + dOpts.pointSize / 2); + col++ + } + } + } else { // For scatter points and points with no scatter + var plotBounds = null, + scatterWidth = 0, + width = 0; + if (dOpts.plotType == 'scatter' || typeof dOpts.plotType == 'number') { + //Default scatter percentage is 20% of box width + scatterWidth = typeof dOpts.plotType == 'number' ? dOpts.plotType : 20; + } + + plotBounds = getObjWidth(scatterWidth, cName); + plotBounds.middle += chart.dataPlots.options.padding + plotBounds.right += chart.dataPlots.options.padding + plotBounds.left += chart.dataPlots.options.padding + width = plotBounds.right - plotBounds.left; + + for (var pt = 0; pt < cGroup.values.length; pt++) { + cPlot.objs.points.pts[pt] + .attr("cx", plotBounds.middle + addJitter(true, width)) + .attr("cy", chart.yScale(cGroup.values[pt])); + } + } + } + + + if (cPlot.objs.bean) { + var beanBounds = getObjWidth(dOpts.beanWidth, cName); + for (var pt = 0; pt < cGroup.values.length; pt++) { + cPlot.objs.bean.lines[pt] + .attr("x1", beanBounds.left) + .attr("x2", beanBounds.right) + .attr('y1', chart.yScale(cGroup.values[pt])) + .attr("y2", chart.yScale(cGroup.values[pt])); + } + } + } + }; + + /** + * Create the svg elements for the data plots + */ + chart.dataPlots.preparePlots = function () { + var cName, cPlot; + + if (dOpts && dOpts.colors) { + chart.dataPlots.colorFunct = getColorFunct(dOpts.colors); + } else { + chart.dataPlots.colorFunct = chart.colorFunct + } + + if (dOpts.show == false) { + return + } + + // Metrics lines + chart.dataPlots.objs.g = chart.objs.g.append("g").attr("class", "metrics-lines"); + if (dOpts.showLines && dOpts.showLines.length > 0) { + chart.dataPlots.objs.lines = {}; + var cMetric; + for (var line in dOpts.showLines) { + cMetric = dOpts.showLines[line]; + chart.dataPlots.objs.lines[cMetric] = {}; + chart.dataPlots.objs.lines[cMetric].values = []; + for (var cGroup in chart.groupObjs) { + chart.dataPlots.objs.lines[cMetric].values.push({ + x: cGroup, + y: chart.groupObjs[cGroup].metrics[cMetric] + }) + } + chart.dataPlots.objs.lines[cMetric].line = d3.svg.line() + .interpolate("cardinal") + .y(function (d) { + return chart.yScale(d.y) + }); + chart.dataPlots.objs.lines[cMetric].g = chart.dataPlots.objs.g.append("path") + .attr("class", "line " + cMetric) + .attr("data-metric", cMetric) + .style("fill", 'none') + .style("stroke", chart.colorFunct(cMetric)); + } + + } + + for (cName in chart.groupObjs) { + + cPlot = chart.groupObjs[cName].dataPlots; + cPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "data-plot"); + + // Points Plot + if (dOpts.showPlot) { + cPlot.objs.points = {g: null, pts: []}; + cPlot.objs.points.g = cPlot.objs.g.append("g").attr("class", "points-plot"); + for (var pt = 0; pt < chart.groupObjs[cName].values.length; pt++) { + cPlot.objs.points.pts.push(cPlot.objs.points.g + .append("a") + .attr("xlink:href", function(d) { + return chart.groupObjs[cName].labels[pt] + ".html" + }) + .append("circle") + .attr("class", "point") + .attr('r', dOpts.pointSize / 2)// Options is diameter, r takes radius so divide by 2 + .style("fill", chart.dataPlots.colorFunct(cName)) + .on("mouseover", function() { + chart.objs.tooltip + .style("display", null) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); + }) + .on("mouseout", function () { + chart.objs.tooltip.style("display", "none"); + }) + .on("mousemove", pointHover(chart.groupObjs[cName].labels[pt], chart.groupObjs[cName].values[pt])) + ); + } + } + + + // Bean lines + if (dOpts.showBeanLines) { + cPlot.objs.bean = {g: null, lines: []}; + cPlot.objs.bean.g = cPlot.objs.g.append("g").attr("class", "bean-plot"); + for (var pt = 0; pt < chart.groupObjs[cName].values.length; pt++) { + cPlot.objs.bean.lines.push(cPlot.objs.bean.g.append("line") + .attr("class", "bean line") + .style("stroke-width", '1') + .style("stroke", chart.dataPlots.colorFunct(cName))); + } + } + } + + }; + chart.dataPlots.preparePlots(); + + d3.select(window).on('resize.' + chart.selector + '.dataPlot', chart.dataPlots.update); + chart.dataPlots.update(); + return chart; + }; + + return chart; +} diff --git a/pydra/tasks/mriqc/data/reports/embed_resources/d3.min.js b/pydra/tasks/mriqc/data/reports/embed_resources/d3.min.js new file mode 100644 index 0000000..1664873 --- /dev/null +++ b/pydra/tasks/mriqc/data/reports/embed_resources/d3.min.js @@ -0,0 +1,5 @@ +!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)>0?i=u:r=u+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function f(n){return(n+="")===bo||n[0]===_o?_o+n:n}function s(n){return(n+="")[0]===_o?n.slice(1):n}function h(n){return f(n)in this._}function p(n){return(n=f(n))in this._&&delete this._[n]}function g(){var n=[];for(var t in this._)n.push(s(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function y(){this._=Object.create(null)}function m(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=wo.length;r>e;++e){var i=wo[e]+t;if(i in n)return i}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,i=-1,u=r.length;++ie;e++)for(var i,u=n[e],o=0,a=u.length;a>o;o++)(i=u[o])&&t(i,o,e);return n}function Z(n){return ko(n,qo),n}function V(n){var t,e;return function(r,i,u){var o,a=n[u].update,l=a.length;for(u!=e&&(e=u,t=0),i>=t&&(t=i+1);!(o=a[t])&&++t0&&(n=n.slice(0,a));var c=To.get(n);return c&&(n=c,l=B),a?t?i:r:t?b:u}function $(n,t){return function(e){var r=ao.event;ao.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{ao.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Do,i="click"+r,u=ao.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==Ro&&(Ro="onselectstart"in e?!1:x(e.style,"userSelect")),Ro){var o=n(e).style,a=o[Ro];o[Ro]="none"}return function(n){if(u.on(r,null),Ro&&(o[Ro]=a),n){var t=function(){u.on(i,null)};u.on(i,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var i=r.createSVGPoint();if(0>Po){var u=t(n);if(u.scrollX||u.scrollY){r=ao.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var o=r[0][0].getScreenCTM();Po=!(o.f||o.e),r.remove()}}return Po?(i.x=e.pageX,i.y=e.pageY):(i.x=e.clientX,i.y=e.clientY),i=i.matrixTransform(n.getScreenCTM().inverse()),[i.x,i.y]}var a=n.getBoundingClientRect();return[e.clientX-a.left-n.clientLeft,e.clientY-a.top-n.clientTop]}function G(){return ao.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?Fo:Math.acos(n)}function tn(n){return n>1?Io:-1>n?-Io:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function on(n){return(n=Math.sin(n/2))*n}function an(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?u+(o-u)*n/60:180>n?o:240>n?u+(o-u)*(240-n)/60:u}function i(n){return Math.round(255*r(n))}var u,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,u=2*e-o,new mn(i(n+120),i(n),i(n-120))}function fn(n,t,e){return this instanceof fn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof fn?new fn(n.h,n.c,n.l):n instanceof hn?gn(n.l,n.a,n.b):gn((n=Sn((n=ao.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new fn(n,t,e)}function sn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Yo)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof fn?sn(n.h,n.c,n.l):Sn((n=mn(n)).r,n.g,n.b):new hn(n,t,e)}function pn(n,t,e){var r=(n+16)/116,i=r+t/500,u=r-e/200;return i=vn(i)*na,r=vn(r)*ta,u=vn(u)*ea,new mn(yn(3.2404542*i-1.5371385*r-.4985314*u),yn(-.969266*i+1.8760108*r+.041556*u),yn(.0556434*i-.2040259*r+1.0572252*u))}function gn(n,t,e){return n>0?new fn(Math.atan2(e,t)*Zo,Math.sqrt(t*t+e*e),n):new fn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function yn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function mn(n,t,e){return this instanceof mn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof mn?new mn(n.r,n.g,n.b):_n(""+n,mn,cn):new mn(n,t,e)}function Mn(n){return new mn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,i,u,o=0,a=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(i=r[2].split(","),r[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(Nn(i[0]),Nn(i[1]),Nn(i[2]))}return(u=ua.get(n))?t(u.r,u.g,u.b):(null==n||"#"!==n.charAt(0)||isNaN(u=parseInt(n.slice(1),16))||(4===n.length?(o=(3840&u)>>4,o=o>>4|o,a=240&u,a=a>>4|a,l=15&u,l=l<<4|l):7===n.length&&(o=(16711680&u)>>16,a=(65280&u)>>8,l=255&u)),t(o,a,l))}function wn(n,t,e){var r,i,u=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-u,l=(o+u)/2;return a?(i=.5>l?a/(o+u):a/(2-o-u),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=NaN,i=l>0&&1>l?0:r),new ln(r,i,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/na),i=dn((.2126729*n+.7151522*t+.072175*e)/ta),u=dn((.0193339*n+.119192*t+.9503041*e)/ea);return hn(116*i-16,500*(r-i),200*(i-u))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function i(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(u,l)}catch(r){return void o.error.call(u,r)}o.load.call(u,n)}else o.error.call(u,l)}var u={},o=ao.dispatch("beforesend","progress","load","error"),a={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=i:l.onreadystatechange=function(){l.readyState>3&&i()},l.onprogress=function(n){var t=ao.event;ao.event=n;try{o.progress.call(u,l)}finally{ao.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.responseType=function(n){return arguments.length?(c=n,u):c},u.response=function(n){return e=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(co(arguments)))}}),u.send=function(e,r,i){if(2===arguments.length&&"function"==typeof r&&(i=r,r=null),l.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),l.setRequestHeader)for(var f in a)l.setRequestHeader(f,a[f]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),o.beforesend.call(u,l),l.send(null==r?null:r),u},u.abort=function(){return l.abort(),u},ao.rebind(u,o,"on"),null==r?u:u.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var i=e+t,u={c:n,t:i,n:null};return aa?aa.n=u:oa=u,aa=u,la||(ca=clearTimeout(ca),la=1,fa(Tn)),u}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(ca),ca=setTimeout(Tn,t)),la=0):(la=1,fa(Tn))}function Rn(){for(var n=Date.now(),t=oa;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=oa,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,i=n.currency,u=r&&e?function(n,t){for(var i=n.length,u=[],o=0,a=r[0],l=0;i>0&&a>0&&(l+a+1>t&&(a=Math.max(1,t-l)),u.push(n.substring(i-=a,i+a)),!((l+=a+1)>t));)a=r[o=(o+1)%r.length];return u.reverse().join(e)}:m;return function(n){var e=ha.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"-",l=e[4]||"",c=e[5],f=+e[6],s=e[7],h=e[8],p=e[9],g=1,v="",d="",y=!1,m=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===o)&&(c=r="0",o="="),p){case"n":s=!0,p="g";break;case"%":g=100,d="%",p="f";break;case"p":g=100,d="%",p="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+p.toLowerCase());case"c":m=!1;case"d":y=!0,h=0;break;case"s":g=-1,p="r"}"$"===l&&(v=i[0],d=i[1]),"r"!=p||h||(p="g"),null!=h&&("g"==p?h=Math.max(1,Math.min(21,h)):"e"!=p&&"f"!=p||(h=Math.max(0,Math.min(20,h)))),p=pa.get(p)||Fn;var M=c&&s;return function(n){var e=d;if(y&&n%1)return"";var i=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===a?"":a;if(0>g){var l=ao.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=g;n=p(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=m?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&s&&(x=u(x,1/0));var S=v.length+x.length+b.length+(M?0:i.length),k=f>S?new Array(S=f-S+1).join(r):"";return M&&(x=u(k+x,k.length?f-b.length:1/0)),i+=v,n=x+b,("<"===o?i+n+k:">"===o?k+i+n:"^"===o?k.substring(0,S>>=1)+i+n+k.substring(S):i+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=u(e,1);return r-t>t-e?e:r}function i(e){return t(e=n(new va(e-1)),1),e}function u(n,e){return t(n=new va(+n),e),n}function o(n,r,u){var o=i(n),a=[];if(u>1)for(;r>o;)e(o)%u||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{va=Hn;var r=new Hn;return r._=n,o(r,t,e)}finally{va=Date}}n.floor=n,n.round=r,n.ceil=i,n.offset=u,n.range=o;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(i),l.offset=In(u),l.range=a,n}function In(n){return function(t,e){try{va=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{va=Date}}}function Yn(n){function t(n){function t(t){for(var e,i,u,o=[],a=-1,l=0;++aa;){if(r>=c)return-1;if(i=t.charCodeAt(a++),37===i){if(o=t.charAt(a++),u=C[o in ya?t.charAt(a++):o],!u||(r=u(n,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function f(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var s=n.dateTime,h=n.date,p=n.time,g=n.periods,v=n.days,d=n.shortDays,y=n.months,m=n.shortMonths;t.utc=function(n){function e(n){try{va=Hn;var t=new va;return t._=n,r(t)}finally{va=Date}}var r=t(n);return e.parse=function(n){try{va=Hn;var t=r.parse(n);return t&&t._}finally{va=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=ao.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(y),k=Xn(y),N=Vn(m),E=Xn(m);g.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return m[n.getMonth()]},B:function(n){return y[n.getMonth()]},c:t(s),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ga.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return g[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ga.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ga.mondayOfYear(n),t,2)},x:t(h),X:t(p),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:at,"%":function(){return"%"}},C={a:r,A:i,b:u,B:o,c:a,d:tt,e:tt,H:rt,I:rt,j:et,L:ot,m:nt,M:it,p:f,S:ut,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",i=(r?-n:n)+"",u=i.length;return r+(e>u?new Array(e-u+1).join(t)+i:i)}function Vn(n){return new RegExp("^(?:"+n.map(ao.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function it(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function ut(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ot(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function at(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=xo(t)/60|0,i=xo(t)%60;return e+Zn(r,"0",2)+Zn(i,"0",2)}function lt(n,t,e){Ma.lastIndex=0;var r=Ma.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,a=o*e,l=Math.cos(t),c=Math.sin(t),f=u*c,s=i*l+f*Math.cos(a),h=f*o*Math.sin(a);ka.add(Math.atan2(h,s)),r=n,i=l,u=c}var t,e,r,i,u;Na.point=function(o,a){Na.point=n,r=(t=o)*Yo,i=Math.cos(a=(e=a)*Yo/2+Fo/4),u=Math.sin(a)},Na.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function yt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function mt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return xo(n[0]-t[0])a;++a)i.point((e=n[a])[0],e[1]);return void i.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,u.push(l),o.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,u.push(l),o.push(c)}}),o.sort(t),qt(u),qt(o),u.length){for(var a=0,l=e,c=o.length;c>a;++a)o[a].e=l=!l;for(var f,s,h=u[0];;){for(var p=h,g=!0;p.v;)if((p=p.n)===h)return;f=p.z,i.lineStart();do{if(p.v=p.o.v=!0,p.e){if(g)for(var a=0,c=f.length;c>a;++a)i.point((s=f[a])[0],s[1]);else r(p.x,p.n.x,1,i);p=p.n}else{if(g){f=p.p.z;for(var a=f.length-1;a>=0;--a)i.point((s=f[a])[0],s[1])}else r(p.x,p.p.x,-1,i);p=p.p}p=p.o,f=p.z,g=!g}while(!p.v);i.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,i=n[0];++r0){for(b||(u.polygonStart(),b=!0),u.lineStart();++o1&&2&t&&e.push(e.pop().concat(e.shift())),p.push(e.filter(Dt))}var p,g,v,d=t(u),y=i.invert(r[0],r[1]),m={point:o,lineStart:l,lineEnd:c,polygonStart:function(){m.point=f,m.lineStart=s,m.lineEnd=h,p=[],g=[]},polygonEnd:function(){m.point=o,m.lineStart=l,m.lineEnd=c,p=ao.merge(p);var n=Ot(y,g);p.length?(b||(u.polygonStart(),b=!0),Lt(p,Ut,n,e,u)):n&&(b||(u.polygonStart(),b=!0),u.lineStart(),e(null,null,1,u),u.lineEnd()),b&&(u.polygonEnd(),b=!1),p=g=null},sphere:function(){u.polygonStart(),u.lineStart(),e(null,null,1,u),u.lineEnd(),u.polygonEnd()}},M=Pt(),x=t(M),b=!1;return m}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Io-Uo:Io-n[1])-((t=t.x)[0]<0?t[1]-Io-Uo:Io-t[1])}function jt(n){var t,e=NaN,r=NaN,i=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(u,o){var a=u>0?Fo:-Fo,l=xo(u-e);xo(l-Fo)0?Io:-Io),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(u,r),t=0):i!==a&&l>=Fo&&(xo(e-i)Uo?Math.atan((Math.sin(t)*(u=Math.cos(r))*Math.sin(e)-Math.sin(r)*(i=Math.cos(t))*Math.sin(n))/(i*u*o)):(t+r)/2}function Ht(n,t,e,r){var i;if(null==n)i=e*Io,r.point(-Fo,i),r.point(0,i),r.point(Fo,i),r.point(Fo,0),r.point(Fo,-i),r.point(0,-i),r.point(-Fo,-i),r.point(-Fo,0),r.point(-Fo,i);else if(xo(n[0]-t[0])>Uo){var u=n[0]a;++a){var c=t[a],f=c.length;if(f)for(var s=c[0],h=s[0],p=s[1]/2+Fo/4,g=Math.sin(p),v=Math.cos(p),d=1;;){d===f&&(d=0),n=c[d];var y=n[0],m=n[1]/2+Fo/4,M=Math.sin(m),x=Math.cos(m),b=y-h,_=b>=0?1:-1,w=_*b,S=w>Fo,k=g*M;if(ka.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),u+=S?b+_*Ho:b,S^h>=e^y>=e){var N=mt(dt(s),dt(n));bt(N);var E=mt(i,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(o+=S^b>=0?1:-1)}if(!d++)break;h=y,g=M,v=x,s=n}}return(-Uo>u||Uo>u&&-Uo>ka)^1&o}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>u}function e(n){var e,u,l,c,f;return{lineStart:function(){c=l=!1,f=1},point:function(s,h){var p,g=[s,h],v=t(s,h),d=o?v?0:i(s,h):v?i(s+(0>s?Fo:-Fo),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(p=r(e,g),(wt(e,p)||wt(g,p))&&(g[0]+=Uo,g[1]+=Uo,v=t(g[0],g[1]))),v!==l)f=0,v?(n.lineStart(),p=r(g,e),n.point(p[0],p[1])):(p=r(e,g),n.point(p[0],p[1]),n.lineEnd()),e=p;else if(a&&e&&o^v){var y;d&u||!(y=r(g,e,!0))||(f=0,o?(n.lineStart(),n.point(y[0][0],y[0][1]),n.point(y[1][0],y[1][1]),n.lineEnd()):(n.point(y[1][0],y[1][1]),n.lineEnd(),n.lineStart(),n.point(y[0][0],y[0][1])))}!v||e&&wt(e,g)||n.point(g[0],g[1]),e=g,l=v,u=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return f|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),i=dt(t),o=[1,0,0],a=mt(r,i),l=yt(a,a),c=a[0],f=l-c*c;if(!f)return!e&&n;var s=u*l/f,h=-u*c/f,p=mt(o,a),g=xt(o,s),v=xt(a,h);Mt(g,v);var d=p,y=yt(g,d),m=yt(d,d),M=y*y-m*(yt(g,g)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-y-x)/m);if(Mt(b,g),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=xo(E-Fo)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(xo(b[0]-w)Fo^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-y+x)/m);return Mt(z,g),[b,_t(z)]}}}function i(t,e){var r=o?n:Fo-n,i=0;return-r>t?i|=1:t>r&&(i|=2),-r>e?i|=4:e>r&&(i|=8),i}var u=Math.cos(n),o=u>0,a=xo(u)>Uo,l=ve(n,6*Yo);return Rt(t,e,l,o?[0,-n]:[-Fo,n-Fo])}function Yt(n,t,e,r){return function(i){var u,o=i.a,a=i.b,l=o.x,c=o.y,f=a.x,s=a.y,h=0,p=1,g=f-l,v=s-c;if(u=n-l,g||!(u>0)){if(u/=g,0>g){if(h>u)return;p>u&&(p=u)}else if(g>0){if(u>p)return;u>h&&(h=u)}if(u=e-l,g||!(0>u)){if(u/=g,0>g){if(u>p)return;u>h&&(h=u)}else if(g>0){if(h>u)return;p>u&&(p=u)}if(u=t-c,v||!(u>0)){if(u/=v,0>v){if(h>u)return;p>u&&(p=u)}else if(v>0){if(u>p)return;u>h&&(h=u)}if(u=r-c,v||!(0>u)){if(u/=v,0>v){if(u>p)return;u>h&&(h=u)}else if(v>0){if(h>u)return;p>u&&(p=u)}return h>0&&(i.a={x:l+h*g,y:c+h*v}),1>p&&(i.b={x:l+p*g,y:c+p*v}),i}}}}}}function Zt(n,t,e,r){function i(r,i){return xo(r[0]-n)0?0:3:xo(r[0]-e)0?2:1:xo(r[1]-t)0?1:0:i>0?3:2}function u(n,t){return o(n.x,t.x)}function o(n,t){var e=i(n,1),r=i(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function l(n){for(var t=0,e=d.length,r=n[1],i=0;e>i;++i)for(var u,o=1,a=d[i],l=a.length,c=a[0];l>o;++o)u=a[o],c[1]<=r?u[1]>r&&Q(c,u,n)>0&&++t:u[1]<=r&&Q(c,u,n)<0&&--t,c=u;return 0!==t}function c(u,a,l,c){var f=0,s=0;if(null==u||(f=i(u,l))!==(s=i(a,l))||o(u,a)<0^l>0){do c.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+l+4)%4)!==s)}else c.point(a[0],a[1])}function f(i,u){return i>=n&&e>=i&&u>=t&&r>=u}function s(n,t){f(n,t)&&a.point(n,t)}function h(){C.point=g,d&&d.push(y=[]),S=!0,w=!1,b=_=NaN}function p(){v&&(g(m,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=s,w&&a.lineEnd()}function g(n,t){n=Math.max(-Ha,Math.min(Ha,n)),t=Math.max(-Ha,Math.min(Ha,t));var e=f(n,t);if(d&&y.push([n,t]),S)m=n,M=t,x=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,y,m,M,x,b,_,w,S,k,N=a,E=Pt(),A=Yt(n,t,e,r),C={point:s,lineStart:h,lineEnd:p,polygonStart:function(){a=E,v=[],d=[],k=!0},polygonEnd:function(){a=N,v=ao.merge(v);var t=l([n,r]),e=k&&t,i=v.length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),c(null,null,1,a),a.lineEnd()),i&&Lt(v,u,t,c,a),a.polygonEnd()),v=d=y=null}};return C}}function Vt(n){var t=0,e=Fo/3,r=ae(n),i=r(t,e);return i.parallels=function(n){return arguments.length?r(t=n[0]*Fo/180,e=n[1]*Fo/180):[t/Fo*180,e/Fo*180]},i}function Xt(n,t){function e(n,t){var e=Math.sqrt(u-2*i*Math.sin(t))/i;return[e*Math.sin(n*=i),o-e*Math.cos(n)]}var r=Math.sin(n),i=(r+Math.sin(t))/2,u=1+r*(2*i-r),o=Math.sqrt(u)/i;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/i,tn((u-(n*n+e*e)*i*i)/(2*i))]},e}function $t(){function n(n,t){Ia+=i*n-r*t,r=n,i=t}var t,e,r,i;$a.point=function(u,o){$a.point=n,t=r=u,e=i=o},$a.lineEnd=function(){n(t,e)}}function Bt(n,t){Ya>n&&(Ya=n),n>Va&&(Va=n),Za>t&&(Za=t),t>Xa&&(Xa=t)}function Wt(){function n(n,t){o.push("M",n,",",t,u)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function i(){o.push("Z")}var u=Jt(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return u=Jt(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ca+=n,za+=t,++La}function Kt(){function n(n,r){var i=n-t,u=r-e,o=Math.sqrt(i*i+u*u);qa+=o*(t+n)/2,Ta+=o*(e+r)/2,Ra+=o,Gt(t=n,e=r)}var t,e;Wa.point=function(r,i){Wa.point=n,Gt(t=r,e=i)}}function Qt(){Wa.point=Gt}function ne(){function n(n,t){var e=n-r,u=t-i,o=Math.sqrt(e*e+u*u);qa+=o*(r+n)/2,Ta+=o*(i+t)/2,Ra+=o,o=i*n-r*t,Da+=o*(r+n),Pa+=o*(i+t),Ua+=3*o,Gt(r=n,i=t)}var t,e,r,i;Wa.point=function(u,o){Wa.point=n,Gt(t=r=u,e=i=o)},Wa.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+o,e),n.arc(t,e,o,0,Ho)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function i(){a.point=t}function u(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:i,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=i,a.point=t},pointRadius:function(n){return o=n,a},result:b};return a}function ee(n){function t(n){return(a?r:e)(n)}function e(t){return ue(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=u,t.lineStart()}function u(e,r){var u=dt([e,r]),o=n(e,r);i(M,x,m,b,_,w,M=o[0],x=o[1],m=e,b=u[0],_=u[1],w=u[2],a,t),t.point(M,x)}function o(){S.point=e,t.lineEnd()}function l(){ +r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)u||xo((m*z+M*L)/x-.5)>.3||o>a*p+l*g+c*v)&&(i(t,e,r,a,l,c,A,C,N,b/=S,_/=S,w,d,y),y.point(A,C),i(A,C,N,b,_,w,f,s,h,p,g,v,d,y))}}var u=.5,o=Math.cos(30*Yo),a=16;return t.precision=function(n){return arguments.length?(a=(u=n*n)>0&&16,t):Math.sqrt(u)},t}function re(n){var t=ee(function(t,e){return n([t*Zo,e*Zo])});return function(n){return le(t(n))}}function ie(n){this.stream=n}function ue(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function oe(n){return ae(function(){return n})()}function ae(n){function t(n){return n=a(n[0]*Yo,n[1]*Yo),[n[0]*h+l,c-n[1]*h]}function e(n){return n=a.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Zo,n[1]*Zo]}function r(){a=Ct(o=se(y,M,x),u);var n=u(v,d);return l=p-n[0]*h,c=g+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,o,a,l,c,f,s=ee(function(n,t){return n=u(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,p=480,g=250,v=0,d=0,y=0,M=0,x=0,b=Fa,_=m,w=null,S=null;return t.stream=function(n){return f&&(f.valid=!1),f=le(b(o,s(_(n)))),f.valid=!0,f},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Fa):It((w=+n)*Yo),i()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):m,i()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(p=+n[0],g=+n[1],r()):[p,g]},t.center=function(n){return arguments.length?(v=n[0]%360*Yo,d=n[1]%360*Yo,r()):[v*Zo,d*Zo]},t.rotate=function(n){return arguments.length?(y=n[0]%360*Yo,M=n[1]%360*Yo,x=n.length>2?n[2]%360*Yo:0,r()):[y*Zo,M*Zo,x*Zo]},ao.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function le(n){return ue(n,function(t,e){n.point(t*Yo,e*Yo)})}function ce(n,t){return[n,t]}function fe(n,t){return[n>Fo?n-Ho:-Fo>n?n+Ho:n,t]}function se(n,t,e){return n?t||e?Ct(pe(n),ge(t,e)):pe(n):t||e?ge(t,e):fe}function he(n){return function(t,e){return t+=n,[t>Fo?t-Ho:-Fo>t?t+Ho:t,e]}}function pe(n){var t=he(n);return t.invert=he(-n),t}function ge(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*r+a*i;return[Math.atan2(l*u-f*o,a*r-c*i),tn(f*u+l*o)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*u-l*o;return[Math.atan2(l*u+c*o,a*r+f*i),tn(f*r-a*i)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,o,a){var l=o*t;null!=i?(i=de(e,i),u=de(e,u),(o>0?u>i:i>u)&&(i+=o*Ho)):(i=n+o*Ho,u=n-.5*l);for(var c,f=i;o>0?f>u:u>f;f-=l)a.point((c=_t([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Uo)%(2*Math.PI)}function ye(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function me(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),o=Math.cos(r),a=Math.sin(r),l=i*Math.cos(n),c=i*Math.sin(n),f=o*Math.cos(e),s=o*Math.sin(e),h=2*Math.asin(Math.sqrt(on(r-t)+i*o*on(e-n))),p=1/Math.sin(h),g=h?function(n){var t=Math.sin(n*=h)*p,e=Math.sin(h-n)*p,r=e*l+t*f,i=e*c+t*s,o=e*u+t*a;return[Math.atan2(i,r)*Zo,Math.atan2(o,Math.sqrt(r*r+i*i))*Zo]}:function(){return[n*Zo,t*Zo]};return g.distance=h,g}function _e(){function n(n,i){var u=Math.sin(i*=Yo),o=Math.cos(i),a=xo((n*=Yo)-t),l=Math.cos(a);Ja+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*u-e*o*l)*a),e*u+r*o*l),t=n,e=u,r=o}var t,e,r;Ga.point=function(i,u){t=i*Yo,e=Math.sin(u*=Yo),r=Math.cos(u),Ga.point=n},Ga.lineEnd=function(){Ga.point=Ga.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);return[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),o=Math.cos(i);return[Math.atan2(n*u,r*o),Math.asin(r&&e*u/r)]},e}function Se(n,t){function e(n,t){o>0?-Io+Uo>t&&(t=-Io+Uo):t>Io-Uo&&(t=Io-Uo);var e=o/Math.pow(i(t),u);return[e*Math.sin(u*n),o-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan(Fo/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),o=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,t){var e=o-t,r=K(u)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/u,2*Math.atan(Math.pow(o/r,1/u))-Io]},e):Ne}function ke(n,t){function e(n,t){var e=u-t;return[e*Math.sin(i*n),u-e*Math.cos(i*n)]}var r=Math.cos(n),i=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),u=r/i+n;return xo(i)i;i++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var i=n[0],u=e[0],o=t[0]-i,a=r[0]-u,l=n[1],c=e[1],f=t[1]-l,s=r[1]-c,h=(a*(l-c)-s*(i-u))/(s*o-a*f);return[i+h*o,l+h*f]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=cl.pop()||new Pe;return t.site=n,t}function je(n){Be(n),ol.remove(n),cl.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,i={x:e,y:r},u=n.P,o=n.N,a=[n];je(n);for(var l=u;l.circle&&xo(e-l.circle.x)f;++f)c=a[f],l=a[f-1],nr(c.edge,l.site,c.site,i);l=a[0],c=a[s-1],c.edge=Ke(l.site,c.site,null,i),$e(l),$e(c)}function He(n){for(var t,e,r,i,u=n.x,o=n.y,a=ol._;a;)if(r=Oe(a,o)-u,r>Uo)a=a.L;else{if(i=u-Ie(a,o),!(i>Uo)){r>-Uo?(t=a.P,e=a):i>-Uo?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var l=Ue(n);if(ol.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),ol.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,f=c.x,s=c.y,h=n.x-f,p=n.y-s,g=e.site,v=g.x-f,d=g.y-s,y=2*(h*d-p*v),m=h*h+p*p,M=v*v+d*d,x={x:(d*m-p*M)/y+f,y:(h*M-v*m)/y+s};nr(e.edge,c,g,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,g,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,i=e.y,u=i-t;if(!u)return r;var o=n.P;if(!o)return-(1/0);e=o.site;var a=e.x,l=e.y,c=l-t;if(!c)return a;var f=a-r,s=1/u-1/c,h=f/c;return s?(-h+Math.sqrt(h*h-2*s*(f*f/(-2*c)-l+c/2+i-u/2)))/s+r:(r+a)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,i,u,o,a,l,c,f,s=n[0][0],h=n[1][0],p=n[0][1],g=n[1][1],v=ul,d=v.length;d--;)if(u=v[d],u&&u.prepare())for(a=u.edges,l=a.length,o=0;l>o;)f=a[o].end(),r=f.x,i=f.y,c=a[++o%l].start(),t=c.x,e=c.y,(xo(r-t)>Uo||xo(i-e)>Uo)&&(a.splice(o,0,new tr(Qe(u.site,f,xo(r-s)Uo?{x:s,y:xo(t-s)Uo?{x:xo(e-g)Uo?{x:h,y:xo(t-h)Uo?{x:xo(e-p)=-jo)){var p=l*l+c*c,g=f*f+s*s,v=(s*p-c*g)/h,d=(l*g-f*p)/h,s=d+a,y=fl.pop()||new Xe;y.arc=n,y.site=i,y.x=v+o,y.y=s+Math.sqrt(v*v+d*d),y.cy=s,n.circle=y;for(var m=null,M=ll._;M;)if(y.yd||d>=a)return;if(h>g){if(u){if(u.y>=c)return}else u={x:d,y:l};e={x:d,y:c}}else{if(u){if(u.yr||r>1)if(h>g){if(u){if(u.y>=c)return}else u={x:(l-i)/r,y:l};e={x:(c-i)/r,y:c}}else{if(u){if(u.yp){if(u){if(u.x>=a)return}else u={x:o,y:r*o+i};e={x:a,y:r*a+i}}else{if(u){if(u.xu||s>o||r>h||i>p)){if(g=n.point){var g,v=t-n.x,d=e-n.y,y=v*v+d*d;if(l>y){var m=Math.sqrt(l=y);r=t-m,i=e-m,u=t+m,o=e+m,a=g}}for(var M=n.nodes,x=.5*(f+h),b=.5*(s+p),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,f,s,x,b);break;case 1:c(n,x,s,h,b);break;case 2:c(n,f,b,x,p);break;case 3:c(n,x,b,h,p)}}}(n,r,i,u,o),a}function vr(n,t){n=ao.rgb(n),t=ao.rgb(t);var e=n.r,r=n.g,i=n.b,u=t.r-e,o=t.g-r,a=t.b-i;return function(n){return"#"+bn(Math.round(e+u*n))+bn(Math.round(r+o*n))+bn(Math.round(i+a*n))}}function dr(n,t){var e,r={},i={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):i[e]=n[e];for(e in t)e in n||(i[e]=t[e]);return function(n){for(e in r)i[e]=r[e](n);return i}}function yr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function mr(n,t){var e,r,i,u=hl.lastIndex=pl.lastIndex=0,o=-1,a=[],l=[];for(n+="",t+="";(e=hl.exec(n))&&(r=pl.exec(t));)(i=r.index)>u&&(i=t.slice(u,i),a[o]?a[o]+=i:a[++o]=i),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,l.push({i:o,x:yr(e,r)})),u=pl.lastIndex;return ur;++r)a[(e=l[r]).i]=e.x(n);return a.join("")})}function Mr(n,t){for(var e,r=ao.interpolators.length;--r>=0&&!(e=ao.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],i=[],u=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(Mr(n[e],t[e]));for(;u>e;++e)i[e]=n[e];for(;o>e;++e)i[e]=t[e];return function(n){for(e=0;a>e;++e)i[e]=r[e](n);return i}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Io)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Ho*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Ho/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=ao.hcl(n),t=ao.hcl(t);var e=n.h,r=n.c,i=n.l,u=t.h-e,o=t.c-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return sn(e+u*n,r+o*n,i+a*n)+""}}function Dr(n,t){n=ao.hsl(n),t=ao.hsl(t);var e=n.h,r=n.s,i=n.l,u=t.h-e,o=t.s-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return cn(e+u*n,r+o*n,i+a*n)+""}}function Pr(n,t){n=ao.lab(n),t=ao.lab(t);var e=n.l,r=n.a,i=n.b,u=t.l-e,o=t.a-r,a=t.b-i;return function(n){return pn(e+u*n,r+o*n,i+a*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),i=Fr(t,e),u=Hr(Or(e,t,-i))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:yr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:yr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var i=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:i-4,x:yr(n[0],t[0])},{i:i-2,x:yr(n[1],t[1])})}else 1===t[0]&&1===t[1]||e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=ao.transform(n),t=ao.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,i=-1,u=r.length;++i=0;)e.push(i[r])}function oi(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(u=n.children)&&(i=u.length))for(var i,u,o=-1;++oe;++e)(t=n[e][1])>i&&(r=e,i=t);return r}function yi(n){return n.reduce(mi,0)}function mi(n,t){return n+t[1]}function Mi(n,t){return xi(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xi(n,t){for(var e=-1,r=+n[0],i=(n[1]-r)/t,u=[];++e<=t;)u[e]=i*e+r;return u}function bi(n){return[ao.min(n),ao.max(n)]}function _i(n,t){return n.value-t.value}function wi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Si(n,t){n._pack_next=t,t._pack_prev=n}function ki(n,t){var e=t.x-n.x,r=t.y-n.y,i=n.r+t.r;return.999*i*i>e*e+r*r}function Ni(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),p=Math.max(n.y+n.r,p)}if((e=n.children)&&(c=e.length)){var e,r,i,u,o,a,l,c,f=1/0,s=-(1/0),h=1/0,p=-(1/0);if(e.forEach(Ei),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(i=e[1],i.x=i.r,i.y=0,t(i),c>2))for(u=e[2],zi(r,i,u),t(u),wi(r,u),r._pack_prev=u,wi(u,i),i=r._pack_next,o=3;c>o;o++){zi(r,i,u=e[o]);var g=0,v=1,d=1;for(a=i._pack_next;a!==i;a=a._pack_next,v++)if(ki(a,u)){g=1;break}if(1==g)for(l=r._pack_prev;l!==a._pack_prev&&!ki(l,u);l=l._pack_prev,d++);g?(d>v||v==d&&i.ro;o++)u=e[o],u.x-=y,u.y-=m,M=Math.max(M,u.r+Math.sqrt(u.x*u.x+u.y*u.y));n.r=M,e.forEach(Ai)}}function Ei(n){n._pack_next=n._pack_prev=n}function Ai(n){delete n._pack_next,delete n._pack_prev}function Ci(n,t,e,r){var i=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,i)for(var u=-1,o=i.length;++u=0;)t=i[u],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pi(n,t,e){return n.a.parent===t.parent?n.a:e}function Ui(n){return 1+ao.max(n,function(n){return n.y})}function ji(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fi(n){var t=n.children;return t&&t.length?Fi(t[0]):n}function Hi(n){var t,e=n.children;return e&&(t=e.length)?Hi(e[t-1]):n}function Oi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ii(n,t){var e=n.x+t[3],r=n.y+t[0],i=n.dx-t[1]-t[3],u=n.dy-t[0]-t[2];return 0>i&&(e+=i/2,i=0),0>u&&(r+=u/2,u=0),{x:e,y:r,dx:i,dy:u}}function Yi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zi(n){return n.rangeExtent?n.rangeExtent():Yi(n.range())}function Vi(n,t,e,r){var i=e(n[0],n[1]),u=r(t[0],t[1]);return function(n){return u(i(n))}}function Xi(n,t){var e,r=0,i=n.length-1,u=n[r],o=n[i];return u>o&&(e=r,r=i,i=e,e=u,u=o,o=e),n[r]=t.floor(u),n[i]=t.ceil(o),n}function $i(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Sl}function Bi(n,t,e,r){var i=[],u=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]2?Bi:Vi,l=r?Wr:Br;return o=i(n,t,l,e),a=i(t,n,l,Mr),u}function u(n){return o(n)}var o,a;return u.invert=function(n){return a(n)},u.domain=function(t){return arguments.length?(n=t.map(Number),i()):n},u.range=function(n){return arguments.length?(t=n,i()):t},u.rangeRound=function(n){return u.range(n).interpolate(Ur)},u.clamp=function(n){return arguments.length?(r=n,i()):r},u.interpolate=function(n){return arguments.length?(e=n,i()):e},u.ticks=function(t){return Qi(n,t)},u.tickFormat=function(t,e){return nu(n,t,e)},u.nice=function(t){return Gi(n,t),i()},u.copy=function(){return Wi(n,t,e,r)},i()}function Ji(n,t){return ao.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gi(n,t){return Xi(n,$i(Ki(n,t)[2])),Xi(n,$i(Ki(n,t)[2])),n}function Ki(n,t){null==t&&(t=10);var e=Yi(n),r=e[1]-e[0],i=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),u=t/r*i;return.15>=u?i*=10:.35>=u?i*=5:.75>=u&&(i*=2),e[0]=Math.ceil(e[0]/i)*i,e[1]=Math.floor(e[1]/i)*i+.5*i,e[2]=i,e}function Qi(n,t){return ao.range.apply(ao,Ki(n,t))}function nu(n,t,e){var r=Ki(n,t);if(e){var i=ha.exec(e);if(i.shift(),"s"===i[8]){var u=ao.formatPrefix(Math.max(xo(r[0]),xo(r[1])));return i[7]||(i[7]="."+tu(u.scale(r[2]))),i[8]="f",e=ao.format(i.join("")),function(n){return e(u.scale(n))+u.symbol}}i[7]||(i[7]="."+eu(i[8],r)),e=i.join("")}else e=",."+tu(r[2])+"f";return ao.format(e)}function tu(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function eu(n,t){var e=tu(t[2]);return n in kl?Math.abs(e-tu(Math.max(xo(t[0]),xo(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ru(n,t,e,r){function i(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function u(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(i(t))}return o.invert=function(t){return u(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(i)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(i)),o):t},o.nice=function(){var t=Xi(r.map(i),e?Math:El);return n.domain(t),r=t.map(u),o},o.ticks=function(){var n=Yi(r),o=[],a=n[0],l=n[1],c=Math.floor(i(a)),f=Math.ceil(i(l)),s=t%1?2:t;if(isFinite(f-c)){if(e){for(;f>c;c++)for(var h=1;s>h;h++)o.push(u(c)*h);o.push(u(c))}else for(o.push(u(c));c++0;h--)o.push(u(c)*h);for(c=0;o[c]l;f--);o=o.slice(c,f)}return o},o.tickFormat=function(n,e){if(!arguments.length)return Nl;arguments.length<2?e=Nl:"function"!=typeof e&&(e=ao.format(e));var r=Math.max(1,t*n/o.ticks().length);return function(n){var o=n/u(Math.round(i(n)));return t-.5>o*t&&(o*=t),r>=o?e(n):""}},o.copy=function(){return ru(n.copy(),t,e,r)},Ji(o,n)}function iu(n,t,e){function r(t){return n(i(t))}var i=uu(t),u=uu(1/t);return r.invert=function(t){return u(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(i)),r):e},r.ticks=function(n){return Qi(e,n)},r.tickFormat=function(n,t){return nu(e,n,t)},r.nice=function(n){return r.domain(Gi(e,n))},r.exponent=function(o){return arguments.length?(i=uu(t=o),u=uu(1/t),n.domain(e.map(i)),r):t},r.copy=function(){return iu(n.copy(),t,e)},Ji(r,n)}function uu(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ou(n,t){function e(e){return u[((i.get(e)||("range"===t.t?i.set(e,n.push(e)):NaN))-1)%u.length]}function r(t,e){return ao.range(n.length).map(function(n){return t+e*n})}var i,u,o;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new c;for(var u,o=-1,a=r.length;++oe?[NaN,NaN]:[e>0?a[e-1]:n[0],et?NaN:t/u+n,[t,t+1/u]},r.copy=function(){return lu(n,t,e)},i()}function cu(n,t){function e(e){return e>=e?t[ao.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return cu(n,t)},e}function fu(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qi(n,t)},t.tickFormat=function(t,e){return nu(n,t,e)},t.copy=function(){return fu(n)},t}function su(){return 0}function hu(n){return n.innerRadius}function pu(n){return n.outerRadius}function gu(n){return n.startAngle}function vu(n){return n.endAngle}function du(n){return n&&n.padAngle}function yu(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function mu(n,t,e,r,i){var u=n[0]-t[0],o=n[1]-t[1],a=(i?r:-r)/Math.sqrt(u*u+o*o),l=a*o,c=-a*u,f=n[0]+l,s=n[1]+c,h=t[0]+l,p=t[1]+c,g=(f+h)/2,v=(s+p)/2,d=h-f,y=p-s,m=d*d+y*y,M=e-r,x=f*p-h*s,b=(0>y?-1:1)*Math.sqrt(Math.max(0,M*M*m-x*x)),_=(x*y-d*b)/m,w=(-x*d-y*b)/m,S=(x*y+d*b)/m,k=(-x*d+y*b)/m,N=_-g,E=w-v,A=S-g,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mu(n){function t(t){function o(){c.push("M",u(n(f),a))}for(var l,c=[],f=[],s=-1,h=t.length,p=En(e),g=En(r);++s1?n.join("L"):n+"Z"}function bu(n){return n.join("L")+"Z"}function _u(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1&&i.push("H",r[0]),i.join("")}function wu(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1){a=t[1],u=n[l],l++,r+="C"+(i[0]+o[0])+","+(i[1]+o[1])+","+(u[0]-a[0])+","+(u[1]-a[1])+","+u[0]+","+u[1];for(var c=2;c9&&(i=3*t/Math.sqrt(i),o[a]=i*e,o[a+1]=i*r));for(a=-1;++a<=l;)i=(n[Math.min(l,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),u.push([i||0,o[a]*i||0]);return u}function Fu(n){return n.length<3?xu(n):n[0]+Au(n,ju(n))}function Hu(n){for(var t,e,r,i=-1,u=n.length;++i=t?o(n-t):void(f.c=o)}function o(e){var i=g.active,u=g[i];u&&(u.timer.c=null,u.timer.t=NaN,--g.count,delete g[i],u.event&&u.event.interrupt.call(n,n.__data__,u.index));for(var o in g)if(r>+o){var c=g[o];c.timer.c=null,c.timer.t=NaN,--g.count,delete g[o]}f.c=a,qn(function(){return f.c&&a(e||1)&&(f.c=null,f.t=NaN),1},0,l),g.active=r,v.event&&v.event.start.call(n,n.__data__,t),p=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&p.push(r)}),h=v.ease,s=v.duration}function a(i){for(var u=i/s,o=h(u),a=p.length;a>0;)p[--a].call(n,o);return u>=1?(v.event&&v.event.end.call(n,n.__data__,t),--g.count?delete g[r]:delete n[e],1):void 0}var l,f,s,h,p,g=n[e]||(n[e]={active:0,count:0}),v=g[r];v||(l=i.time,f=qn(u,0,l),v=g[r]={tween:new c,time:l,timer:f,delay:i.delay,duration:i.duration,ease:i.ease,index:t},i=null,++g.count)}function no(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function to(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function eo(n){return n.toISOString()}function ro(n,t,e){function r(t){return n(t)}function i(n,e){var r=n[1]-n[0],i=r/e,u=ao.bisect(Kl,i);return u==Kl.length?[t.year,Ki(n.map(function(n){return n/31536e6}),e)[2]]:u?t[i/Kl[u-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yi(r.domain()),u=null==n?i(e,10):"number"==typeof n?i(e,n):!n.range&&[{range:n},t];return u&&(n=u[0],t=u[1]),n.range(e[0],io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ro(n.copy(),t,e)},Ji(r,n)}function io(n){return new Date(n)}function uo(n){return JSON.parse(n.responseText)}function oo(n){var t=fo.createRange();return t.selectNode(fo.body),t.createContextualFragment(n.responseText)}var ao={version:"3.5.17"},lo=[].slice,co=function(n){return lo.call(n)},fo=this.document;if(fo)try{co(fo.documentElement.childNodes)[0].nodeType}catch(so){co=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),fo)try{fo.createElement("DIV").style.setProperty("opacity",0,"")}catch(ho){var po=this.Element.prototype,go=po.setAttribute,vo=po.setAttributeNS,yo=this.CSSStyleDeclaration.prototype,mo=yo.setProperty;po.setAttribute=function(n,t){go.call(this,n,t+"")},po.setAttributeNS=function(n,t,e){vo.call(this,n,t,e+"")},yo.setProperty=function(n,t,e){mo.call(this,n,t+"",e)}}ao.ascending=e,ao.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},ao.min=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},ao.max=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},ao.extent=function(n,t){var e,r,i,u=-1,o=n.length;if(1===arguments.length){for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}else{for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}return[e,i]},ao.sum=function(n,t){var e,r=0,u=n.length,o=-1;if(1===arguments.length)for(;++o1?l/(f-1):void 0},ao.deviation=function(){var n=ao.variance.apply(this,arguments);return n?Math.sqrt(n):n};var Mo=u(e);ao.bisectLeft=Mo.left,ao.bisect=ao.bisectRight=Mo.right,ao.bisector=function(n){return u(1===n.length?function(t,r){return e(n(t),r)}:n)},ao.shuffle=function(n,t,e){(u=arguments.length)<3&&(e=n.length,2>u&&(t=0));for(var r,i,u=e-t;u;)i=Math.random()*u--|0,r=n[u+t],n[u+t]=n[i+t],n[i+t]=r;return n},ao.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},ao.pairs=function(n){for(var t,e=0,r=n.length-1,i=n[0],u=new Array(0>r?0:r);r>e;)u[e]=[t=i,i=n[++e]];return u},ao.transpose=function(n){if(!(i=n.length))return[];for(var t=-1,e=ao.min(n,o),r=new Array(e);++t=0;)for(r=n[i],t=r.length;--t>=0;)e[--o]=r[t];return e};var xo=Math.abs;ao.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,i=[],u=a(xo(e)),o=-1;if(n*=u,t*=u,e*=u,0>e)for(;(r=n+e*++o)>t;)i.push(r/u);else for(;(r=n+e*++o)=u.length)return r?r.call(i,o):e?o.sort(e):o;for(var l,f,s,h,p=-1,g=o.length,v=u[a++],d=new c;++p=u.length)return n;var r=[],i=o[e++];return n.forEach(function(n,i){r.push({key:n,values:t(i,e)})}),i?r.sort(function(n,t){return i(n.key,t.key)}):r}var e,r,i={},u=[],o=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(ao.map,e,0),0)},i.key=function(n){return u.push(n),i},i.sortKeys=function(n){return o[u.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},ao.set=function(n){var t=new y;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(y,{has:h,add:function(n){return this._[f(n+="")]=!0,n},remove:p,values:g,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,s(t))}}),ao.behavior={},ao.rebind=function(n,t){for(var e,r=1,i=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},ao.event=null,ao.requote=function(n){return n.replace(So,"\\$&")};var So=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,ko={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},No=function(n,t){return t.querySelector(n)},Eo=function(n,t){return t.querySelectorAll(n)},Ao=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ao=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(No=function(n,t){return Sizzle(n,t)[0]||null},Eo=Sizzle,Ao=Sizzle.matchesSelector),ao.selection=function(){return ao.select(fo.documentElement)};var Co=ao.selection.prototype=[];Co.select=function(n){var t,e,r,i,u=[];n=A(n);for(var o=-1,a=this.length;++o=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Lo.hasOwnProperty(e)?{space:Lo[e],local:n}:n}},Co.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=ao.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Co.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,i=-1;if(t=e.classList){for(;++ii){if("string"!=typeof n){2>i&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>i){var u=this.node();return t(u).getComputedStyle(u,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Co.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Co.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Co.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Co.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Co.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Co.remove=function(){return this.each(F)},Co.data=function(n,t){function e(n,e){var r,i,u,o=n.length,s=e.length,h=Math.min(o,s),p=new Array(s),g=new Array(s),v=new Array(o);if(t){var d,y=new c,m=new Array(o);for(r=-1;++rr;++r)g[r]=H(e[r]);for(;o>r;++r)v[r]=n[r]}g.update=p,g.parentNode=p.parentNode=v.parentNode=n.parentNode,a.push(g),l.push(p),f.push(v)}var r,i,u=-1,o=this.length;if(!arguments.length){for(n=new Array(o=(r=this[0]).length);++uu;u++){i.push(t=[]),t.parentNode=(e=this[u]).parentNode;for(var a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return E(i)},Co.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[i])&&(u&&u!==e.nextSibling&&u.parentNode.insertBefore(e,u),u=e);return this},Co.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,i=e.length;i>r;r++){var u=e[r];if(u)return u}return null},Co.size=function(){var n=0;return Y(this,function(){++n}),n};var qo=[];ao.selection.enter=Z,ao.selection.enter.prototype=qo,qo.append=Co.append,qo.empty=Co.empty,qo.node=Co.node,qo.call=Co.call,qo.size=Co.size,qo.select=function(n){for(var t,e,r,i,u,o=[],a=-1,l=this.length;++ar){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var To=ao.map({mouseenter:"mouseover",mouseleave:"mouseout"});fo&&To.forEach(function(n){"on"+n in fo&&To.remove(n)});var Ro,Do=0;ao.mouse=function(n){return J(n,k())};var Po=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;ao.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,i=0,u=t.length;u>i;++i)if((r=t[i]).identifier===e)return J(n,r)},ao.behavior.drag=function(){function n(){this.on("mousedown.drag",u).on("touchstart.drag",o)}function e(n,t,e,u,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],g|=n|e,M=r,p({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(y.on(u+d,null).on(o+d,null),m(g),p({type:"dragend"}))}var c,f=this,s=ao.event.target.correspondingElement||ao.event.target,h=f.parentNode,p=r.of(f,arguments),g=0,v=n(),d=".drag"+(null==v?"":"-"+v),y=ao.select(e(s)).on(u+d,a).on(o+d,l),m=W(s),M=t(h,v);i?(c=i.apply(f,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],p({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),i=null,u=e(b,ao.mouse,t,"mousemove","mouseup"),o=e(G,ao.touch,m,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},ao.rebind(n,r,"on")},ao.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?co(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Uo=1e-6,jo=Uo*Uo,Fo=Math.PI,Ho=2*Fo,Oo=Ho-Uo,Io=Fo/2,Yo=Fo/180,Zo=180/Fo,Vo=Math.SQRT2,Xo=2,$o=4;ao.interpolateZoom=function(n,t){var e,r,i=n[0],u=n[1],o=n[2],a=t[0],l=t[1],c=t[2],f=a-i,s=l-u,h=f*f+s*s;if(jo>h)r=Math.log(c/o)/Vo,e=function(n){return[i+n*f,u+n*s,o*Math.exp(Vo*n*r)]};else{var p=Math.sqrt(h),g=(c*c-o*o+$o*h)/(2*o*Xo*p),v=(c*c-o*o-$o*h)/(2*c*Xo*p),d=Math.log(Math.sqrt(g*g+1)-g),y=Math.log(Math.sqrt(v*v+1)-v);r=(y-d)/Vo,e=function(n){var t=n*r,e=rn(d),a=o/(Xo*p)*(e*un(Vo*t+d)-en(d));return[i+a*f,u+a*s,o*e/rn(Vo*t+d)]}}return e.duration=1e3*r,e},ao.behavior.zoom=function(){function n(n){n.on(L,s).on(Wo+".zoom",p).on("dblclick.zoom",g).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function i(n){k.k=Math.max(A[0],Math.min(A[1],n))}function u(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function o(t,e,r,o){t.__chart__={x:k.x,y:k.y,k:k.k},i(Math.pow(2,o)),u(d=e,r),t=ao.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function a(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){a(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function f(n){--z||(n({type:"zoomend"}),d=null)}function s(){function n(){a=1,u(ao.mouse(i),h),c(o)}function r(){s.on(q,null).on(T,null),p(a),f(o)}var i=this,o=D.of(i,arguments),a=0,s=ao.select(t(i)).on(q,n).on(T,r),h=e(ao.mouse(i)),p=W(i);Il.call(i),l(o)}function h(){function n(){var n=ao.touches(g);return p=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=ao.event.target;ao.select(t).on(x,r).on(b,a),_.push(t);for(var e=ao.event.changedTouches,i=0,u=e.length;u>i;++i)d[e[i].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var f=l[0];o(g,f,d[f.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var f=l[0],s=l[1],h=f[0]-s[0],p=f[1]-s[1];y=h*h+p*p}}function r(){var n,t,e,r,o=ao.touches(g);Il.call(g);for(var a=0,l=o.length;l>a;++a,r=null)if(e=o[a],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var f=(f=e[0]-n[0])*f+(f=e[1]-n[1])*f,s=y&&Math.sqrt(f/y);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],i(s*p)}M=null,u(n,t),c(v)}function a(){if(ao.event.touches.length){for(var t=ao.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var i in d)return void n()}ao.selectAll(_).on(m,null),w.on(L,s).on(R,h),N(),f(v)}var p,g=this,v=D.of(g,arguments),d={},y=0,m=".zoom-"+ao.event.changedTouches[0].identifier,x="touchmove"+m,b="touchend"+m,_=[],w=ao.select(g),N=W(g);t(),l(v),w.on(L,null).on(R,t)}function p(){var n=D.of(this,arguments);m?clearTimeout(m):(Il.call(this),v=e(d=y||ao.mouse(this)),l(n)),m=setTimeout(function(){m=null,f(n)},50),S(),i(Math.pow(2,.002*Bo())*k.k),u(d,v),c(n)}function g(){var n=ao.mouse(this),t=Math.log(k.k)/Math.LN2;o(this,n,e(n),ao.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,y,m,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Jo,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return Wo||(Wo="onwheel"in fo?(Bo=function(){return-ao.event.deltaY*(ao.event.deltaMode?120:1)},"wheel"):"onmousewheel"in fo?(Bo=function(){return ao.event.wheelDelta},"mousewheel"):(Bo=function(){return-ao.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Hl?ao.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],i=d?d[0]:e/2,u=d?d[1]:r/2,o=ao.interpolateZoom([(i-k.x)/k.k,(u-k.y)/k.k,e/k.k],[(i-t.x)/t.k,(u-t.y)/t.k,e/t.k]);return function(t){var r=o(t),a=e/r[2];this.__chart__=k={x:i-r[0]*a,y:u-r[1]*a,k:a},c(n)}}).each("interrupt.zoom",function(){f(n)}).each("end.zoom",function(){f(n)}):(this.__chart__=k,l(n),c(n),f(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},a(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},i(+t),a(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Jo:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(y=t&&[+t[0],+t[1]],n):y},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},ao.rebind(n,D,"on")};var Bo,Wo,Jo=[0,1/0];ao.color=an,an.prototype.toString=function(){return this.rgb()+""},ao.hsl=ln;var Go=ln.prototype=new an;Go.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Go.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Go.rgb=function(){return cn(this.h,this.s,this.l)},ao.hcl=fn;var Ko=fn.prototype=new an;Ko.brighter=function(n){return new fn(this.h,this.c,Math.min(100,this.l+Qo*(arguments.length?n:1)))},Ko.darker=function(n){return new fn(this.h,this.c,Math.max(0,this.l-Qo*(arguments.length?n:1)))},Ko.rgb=function(){return sn(this.h,this.c,this.l).rgb()},ao.lab=hn;var Qo=18,na=.95047,ta=1,ea=1.08883,ra=hn.prototype=new an;ra.brighter=function(n){return new hn(Math.min(100,this.l+Qo*(arguments.length?n:1)),this.a,this.b)},ra.darker=function(n){return new hn(Math.max(0,this.l-Qo*(arguments.length?n:1)),this.a,this.b)},ra.rgb=function(){return pn(this.l,this.a,this.b)},ao.rgb=mn;var ia=mn.prototype=new an;ia.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,i=30;return t||e||r?(t&&i>t&&(t=i),e&&i>e&&(e=i),r&&i>r&&(r=i),new mn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new mn(i,i,i)},ia.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new mn(n*this.r,n*this.g,n*this.b)},ia.hsl=function(){return wn(this.r,this.g,this.b)},ia.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ua=ao.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ua.forEach(function(n,t){ua.set(n,Mn(t))}),ao.functor=En,ao.xhr=An(m),ao.dsv=function(n,t){function e(n,e,u){arguments.length<3&&(u=e,e=null);var o=Cn(n,t,null==e?r:i(e),u);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:i(n)):e},o}function r(n){return e.parse(n.responseText)}function i(n){return function(t){return e.parse(t.responseText,n)}}function u(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var i=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(i(n),e)}:i})},e.parseRows=function(n,t){function e(){if(f>=c)return o;if(i)return i=!1,u;var t=f;if(34===n.charCodeAt(t)){for(var e=t;e++f;){var r=n.charCodeAt(f++),a=1;if(10===r)i=!0;else if(13===r)i=!0,10===n.charCodeAt(f)&&(++f,++a);else if(r!==l)continue;return n.slice(t,f-a)}return n.slice(t)}for(var r,i,u={},o={},a=[],c=n.length,f=0,s=0;(r=e())!==o;){for(var h=[];r!==u&&r!==o;)h.push(r),r=e();t&&null==(h=t(h,s++))||a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new y,i=[];return t.forEach(function(n){for(var t in n)r.has(t)||i.push(r.add(t))}),[i.map(o).join(n)].concat(t.map(function(t){return i.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(u).join("\n")},e},ao.csv=ao.dsv(",","text/csv"),ao.tsv=ao.dsv(" ","text/tab-separated-values");var oa,aa,la,ca,fa=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};ao.timer=function(){qn.apply(this,arguments)},ao.timer.flush=function(){Rn(),Dn()},ao.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var sa=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);ao.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=ao.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),sa[8+e/3]};var ha=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,pa=ao.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=ao.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ga=ao.time={},va=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){da.setUTCDate.apply(this._,arguments)},setDay:function(){da.setUTCDay.apply(this._,arguments)},setFullYear:function(){da.setUTCFullYear.apply(this._,arguments)},setHours:function(){da.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){da.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){da.setUTCMinutes.apply(this._,arguments)},setMonth:function(){da.setUTCMonth.apply(this._,arguments)},setSeconds:function(){da.setUTCSeconds.apply(this._,arguments)},setTime:function(){da.setTime.apply(this._,arguments)}};var da=Date.prototype;ga.year=On(function(n){return n=ga.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ga.years=ga.year.range,ga.years.utc=ga.year.utc.range,ga.day=On(function(n){var t=new va(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ga.days=ga.day.range,ga.days.utc=ga.day.utc.range,ga.dayOfYear=function(n){var t=ga.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ga[n]=On(function(n){return(n=ga.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ga[n+"s"]=e.range,ga[n+"s"].utc=e.utc.range,ga[n+"OfYear"]=function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)}}),ga.week=ga.sunday,ga.weeks=ga.sunday.range,ga.weeks.utc=ga.sunday.utc.range,ga.weekOfYear=ga.sundayOfYear;var ya={"-":"",_:" ",0:"0"},ma=/^\s*\d+/,Ma=/^%/;ao.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var xa=ao.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], +shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.geo.stream=function(n,t){n&&_a.hasOwnProperty(n.type)?_a[n.type](n,t):ht(n,t)};var _a={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++rn?4*Fo+n:n,Na.lineStart=Na.lineEnd=Na.point=b}};ao.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>p&&(p=t)}function t(t,e){var r=dt([t*Yo,e*Yo]);if(y){var i=mt(y,r),u=[i[1],-i[0],0],o=mt(u,i);bt(o),o=_t(o);var l=t-g,c=l>0?1:-1,v=o[0]*Zo*c,d=xo(l)>180;if(d^(v>c*g&&c*t>v)){var m=o[1]*Zo;m>p&&(p=m)}else if(v=(v+360)%360-180,d^(v>c*g&&c*t>v)){var m=-o[1]*Zo;s>m&&(s=m)}else s>e&&(s=e),e>p&&(p=e);d?g>t?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t)):t>g?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t)}else n(t,e);y=r,g=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,y=null}function i(n,e){if(y){var r=n-g;m+=xo(r)>180?r+(r>0?360:-360):r}else v=n,d=e;Na.point(n,e),t(n,e)}function u(){Na.lineStart()}function o(){i(v,d),Na.lineEnd(),xo(m)>Uo&&(f=-(h=180)),x[0]=f,x[1]=h,y=null}function a(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nka?(f=-(h=180),s=-(p=90)):m>Uo?p=90:-Uo>m&&(s=-90),x[0]=f,x[1]=h}};return function(n){p=h=-(f=s=1/0),M=[],ao.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],c(e[0],i)||c(e[1],i)?(a(i[0],e[1])>a(i[0],i[1])&&(i[1]=e[1]),a(e[0],i[1])>a(i[0],i[1])&&(i[0]=e[0])):u.push(i=e);for(var o,e,g=-(1/0),t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(o=a(i[1],e[0]))>g&&(g=o,f=e[0],h=i[1])}return M=x=null,f===1/0||s===1/0?[[NaN,NaN],[NaN,NaN]]:[[f,s],[h,p]]}}(),ao.geo.centroid=function(n){Ea=Aa=Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,ja);var t=Da,e=Pa,r=Ua,i=t*t+e*e+r*r;return jo>i&&(t=qa,e=Ta,r=Ra,Uo>Aa&&(t=Ca,e=za,r=La),i=t*t+e*e+r*r,jo>i)?[NaN,NaN]:[Math.atan2(e,t)*Zo,tn(r/Math.sqrt(i))*Zo]};var Ea,Aa,Ca,za,La,qa,Ta,Ra,Da,Pa,Ua,ja={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){ja.lineStart=At},polygonEnd:function(){ja.lineStart=Nt}},Fa=Rt(zt,jt,Ht,[-Fo,-Fo/2]),Ha=1e9;ao.geo.clipExtent=function(){var n,t,e,r,i,u,o={stream:function(n){return i&&(i.valid=!1),i=u(n),i.valid=!0,i},extent:function(a){return arguments.length?(u=Zt(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),i&&(i.valid=!1,i=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(ao.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,ao.geo.albers=function(){return ao.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},ao.geo.albersUsa=function(){function n(n){var u=n[0],o=n[1];return t=null,e(u,o),t||(r(u,o),t)||i(u,o),t}var t,e,r,i,u=ao.geo.albers(),o=ao.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=ao.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?o:i>=.166&&.234>i&&r>=-.214&&-.115>r?a:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(u.precision(t),o.precision(t),a.precision(t),n):u.precision()},n.scale=function(t){return arguments.length?(u.scale(t),o.scale(.35*t),a.scale(t),n.translate(u.translate())):u.scale()},n.translate=function(t){if(!arguments.length)return u.translate();var c=u.scale(),f=+t[0],s=+t[1];return e=u.translate(t).clipExtent([[f-.455*c,s-.238*c],[f+.455*c,s+.238*c]]).stream(l).point,r=o.translate([f-.307*c,s+.201*c]).clipExtent([[f-.425*c+Uo,s+.12*c+Uo],[f-.214*c-Uo,s+.234*c-Uo]]).stream(l).point,i=a.translate([f-.205*c,s+.212*c]).clipExtent([[f-.214*c+Uo,s+.166*c+Uo],[f-.115*c-Uo,s+.234*c-Uo]]).stream(l).point,n},n.scale(1070)};var Oa,Ia,Ya,Za,Va,Xa,$a={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Ia=0,$a.lineStart=$t},polygonEnd:function(){$a.lineStart=$a.lineEnd=$a.point=b,Oa+=xo(Ia/2)}},Ba={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Wa={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Wa.lineStart=ne},polygonEnd:function(){Wa.point=Gt,Wa.lineStart=Kt,Wa.lineEnd=Qt}};ao.geo.path=function(){function n(n){return n&&("function"==typeof a&&u.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=i(u)),ao.geo.stream(n,o)),u.result()}function t(){return o=null,n}var e,r,i,u,o,a=4.5;return n.area=function(n){return Oa=0,ao.geo.stream(n,i($a)),Oa},n.centroid=function(n){return Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,i(Wa)),Ua?[Da/Ua,Pa/Ua]:Ra?[qa/Ra,Ta/Ra]:La?[Ca/La,za/La]:[NaN,NaN]},n.bounds=function(n){return Va=Xa=-(Ya=Za=1/0),ao.geo.stream(n,i(Ba)),[[Ya,Za],[Va,Xa]]},n.projection=function(n){return arguments.length?(i=(e=n)?n.stream||re(n):m,t()):e},n.context=function(n){return arguments.length?(u=null==(r=n)?new Wt:new te(n),"function"!=typeof a&&u.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(u.pointRadius(+t),+t),n):a},n.projection(ao.geo.albersUsa()).context(null)},ao.geo.transform=function(n){return{stream:function(t){var e=new ie(t);for(var r in n)e[r]=n[r];return e}}},ie.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},ao.geo.projection=oe,ao.geo.projectionMutator=ae,(ao.geo.equirectangular=function(){return oe(ce)}).raw=ce.invert=ce,ao.geo.rotation=function(n){function t(t){return t=n(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t}return n=se(n[0]%360*Yo,n[1]*Yo,n.length>2?n[2]*Yo:0),t.invert=function(t){return t=n.invert(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t},t},fe.invert=ce,ao.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=se(-n[0]*Yo,-n[1]*Yo,0).invert,i=[];return e(null,null,1,{point:function(n,e){i.push(n=t(n,e)),n[0]*=Zo,n[1]*=Zo}}),{type:"Polygon",coordinates:[i]}}var t,e,r=[0,0],i=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Yo,i*Yo),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Yo,(i=+r)*Yo),n):i},n.angle(90)},ao.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Yo,i=n[1]*Yo,u=t[1]*Yo,o=Math.sin(r),a=Math.cos(r),l=Math.sin(i),c=Math.cos(i),f=Math.sin(u),s=Math.cos(u);return Math.atan2(Math.sqrt((e=s*o)*e+(e=c*f-l*s*a)*e),l*f+c*s*a)},ao.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return ao.range(Math.ceil(u/d)*d,i,d).map(h).concat(ao.range(Math.ceil(c/y)*y,l,y).map(p)).concat(ao.range(Math.ceil(r/g)*g,e,g).filter(function(n){return xo(n%d)>Uo}).map(f)).concat(ao.range(Math.ceil(a/v)*v,o,v).filter(function(n){return xo(n%y)>Uo}).map(s))}var e,r,i,u,o,a,l,c,f,s,h,p,g=10,v=g,d=90,y=360,m=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(u).concat(p(l).slice(1),h(i).reverse().slice(1),p(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(u=+t[0][0],i=+t[1][0],c=+t[0][1],l=+t[1][1],u>i&&(t=u,u=i,i=t),c>l&&(t=c,c=l,l=t),n.precision(m)):[[u,c],[i,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(m)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],y=+t[1],n):[d,y]},n.minorStep=function(t){return arguments.length?(g=+t[0],v=+t[1],n):[g,v]},n.precision=function(t){return arguments.length?(m=+t,f=ye(a,o,90),s=me(r,e,m),h=ye(c,l,90),p=me(u,i,m),n):m},n.majorExtent([[-180,-90+Uo],[180,90-Uo]]).minorExtent([[-180,-80-Uo],[180,80+Uo]])},ao.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||i.apply(this,arguments)]}}var t,e,r=Me,i=xe;return n.distance=function(){return ao.geo.distance(t||r.apply(this,arguments),e||i.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(i=t,e="function"==typeof t?null:t,n):i},n.precision=function(){return arguments.length?n:0},n},ao.geo.interpolate=function(n,t){return be(n[0]*Yo,n[1]*Yo,t[0]*Yo,t[1]*Yo)},ao.geo.length=function(n){return Ja=0,ao.geo.stream(n,Ga),Ja};var Ja,Ga={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Ka=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(ao.geo.azimuthalEqualArea=function(){return oe(Ka)}).raw=Ka;var Qa=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},m);(ao.geo.azimuthalEquidistant=function(){return oe(Qa)}).raw=Qa,(ao.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(ao.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var nl=we(function(n){return 1/n},Math.atan);(ao.geo.gnomonic=function(){return oe(nl)}).raw=nl,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Io]},(ao.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var tl=we(function(){return 1},Math.asin);(ao.geo.orthographic=function(){return oe(tl)}).raw=tl;var el=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(ao.geo.stereographic=function(){return oe(el)}).raw=el,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Io]},(ao.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,ao.geom={},ao.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,i=En(e),u=En(r),o=n.length,a=[],l=[];for(t=0;o>t;t++)a.push([+i.call(this,n[t],t),+u.call(this,n[t],t),t]);for(a.sort(qe),t=0;o>t;t++)l.push([a[t][0],-a[t][1]]);var c=Le(a),f=Le(l),s=f[0]===c[0],h=f[f.length-1]===c[c.length-1],p=[];for(t=c.length-1;t>=0;--t)p.push(n[a[c[t]][2]]);for(t=+s;t=r&&c.x<=u&&c.y>=i&&c.y<=o?[[r,o],[u,o],[u,i],[r,i]]:[];f.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(u(n,t)/Uo)*Uo,y:Math.round(o(n,t)/Uo)*Uo,i:t}})}var r=Ce,i=ze,u=r,o=i,a=sl;return n?t(n):(t.links=function(n){return ar(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return ar(e(n)).cells.forEach(function(e,r){for(var i,u,o=e.site,a=e.edges.sort(Ve),l=-1,c=a.length,f=a[c-1].edge,s=f.l===o?f.r:f.l;++l=c,h=r>=f,p=h<<1|s;n.leaf=!1,n=n.nodes[p]||(n.nodes[p]=hr()),s?i=c:a=c,h?o=f:l=f,u(n,t,e,r,i,o,a,l)}var f,s,h,p,g,v,d,y,m,M=En(a),x=En(l);if(null!=t)v=t,d=e,y=r,m=i;else if(y=m=-(v=d=1/0),s=[],h=[],g=n.length,o)for(p=0;g>p;++p)f=n[p],f.xy&&(y=f.x),f.y>m&&(m=f.y),s.push(f.x),h.push(f.y);else for(p=0;g>p;++p){var b=+M(f=n[p],p),_=+x(f,p);v>b&&(v=b),d>_&&(d=_),b>y&&(y=b),_>m&&(m=_),s.push(b),h.push(_)}var w=y-v,S=m-d;w>S?m=d+w:y=v+S;var k=hr();if(k.add=function(n){u(k,n,+M(n,++p),+x(n,p),v,d,y,m)},k.visit=function(n){pr(n,k,v,d,y,m)},k.find=function(n){return gr(k,n[0],n[1],v,d,y,m)},p=-1,null==t){for(;++p=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=vl.get(e)||gl,r=dl.get(r)||m,br(r(e.apply(null,lo.call(arguments,1))))},ao.interpolateHcl=Rr,ao.interpolateHsl=Dr,ao.interpolateLab=Pr,ao.interpolateRound=Ur,ao.transform=function(n){var t=fo.createElementNS(ao.ns.prefix.svg,"g");return(ao.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:yl)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var yl={a:1,b:0,c:0,d:1,e:0,f:0};ao.interpolateTransform=$r,ao.layout={},ao.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++ea*a/y){if(v>l){var c=t.charge/l;n.px-=u*c,n.py-=o*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=u*c,n.py-=o*c}}return!t.charge}}function t(n){n.px=ao.event.x,n.py=ao.event.y,l.resume()}var e,r,i,u,o,a,l={},c=ao.dispatch("start","tick","end"),f=[1,1],s=.9,h=ml,p=Ml,g=-30,v=xl,d=.1,y=.64,M=[],x=[];return l.tick=function(){if((i*=.99)<.005)return e=null,c.end({type:"end",alpha:i=0}),!0;var t,r,l,h,p,v,y,m,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,p=l.target,m=p.x-h.x,b=p.y-h.y,(v=m*m+b*b)&&(v=i*o[r]*((v=Math.sqrt(v))-u[r])/v,m*=v,b*=v,p.x-=m*(y=h.weight+p.weight?h.weight/(h.weight+p.weight):.5),p.y-=b*y,h.x+=m*(y=1-y),h.y+=b*y);if((y=i*d)&&(m=f[0]/2,b=f[1]/2,r=-1,y))for(;++r<_;)l=M[r],l.x+=(m-l.x)*y,l.y+=(b-l.y)*y;if(g)for(ri(t=ao.geom.quadtree(M),i,a),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*s,l.y-=(l.py-(l.py=l.y))*s);c.tick({type:"tick",alpha:i})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(f=n,l):f},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.friction=function(n){return arguments.length?(s=+n,l):s},l.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(y=n*n,l):Math.sqrt(y)},l.alpha=function(n){return arguments.length?(n=+n,i?n>0?i=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:i=0})):n>0&&(c.start({type:"start",alpha:i=n}),e=qn(l.tick)),l):i},l.start=function(){function n(n,r){if(!e){for(e=new Array(i),l=0;i>l;++l)e[l]=[];for(l=0;c>l;++l){var u=x[l];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var o,a=e[t],l=-1,f=a.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;i>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",s)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof h)for(t=0;c>t;++t)u[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)u[t]=h;if(o=[],"function"==typeof p)for(t=0;c>t;++t)o[t]=+p.call(this,x[t],t);else for(t=0;c>t;++t)o[t]=p;if(a=[],"function"==typeof g)for(t=0;i>t;++t)a[t]=+g.call(this,M[t],t);else for(t=0;i>t;++t)a[t]=g;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=ao.behavior.drag().origin(m).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",ni)),arguments.length?void this.on("mouseover.force",ti).on("mouseout.force",ei).call(r):r},ao.rebind(l,c,"on")};var ml=20,Ml=1,xl=1/0;ao.layout.hierarchy=function(){function n(i){var u,o=[i],a=[];for(i.depth=0;null!=(u=o.pop());)if(a.push(u),(c=e.call(n,u,u.depth))&&(l=c.length)){for(var l,c,f;--l>=0;)o.push(f=c[l]),f.parent=u,f.depth=u.depth+1;r&&(u.value=0),u.children=c}else r&&(u.value=+r.call(n,u,u.depth)||0),delete u.children;return oi(i,function(n){var e,i;t&&(e=n.children)&&e.sort(t),r&&(i=n.parent)&&(i.value+=n.value)}),a}var t=ci,e=ai,r=li;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(ui(t,function(n){n.children&&(n.value=0)}),oi(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},ao.layout.partition=function(){function n(t,e,r,i){var u=t.children;if(t.x=e,t.y=t.depth*i,t.dx=r,t.dy=i,u&&(o=u.length)){var o,a,l,c=-1;for(r=t.value?r/t.value:0;++cs?-1:1),g=ao.sum(c),v=g?(s-l*p)/g:0,d=ao.range(l),y=[];return null!=e&&d.sort(e===bl?function(n,t){return c[t]-c[n]}:function(n,t){return e(o[n],o[t])}),d.forEach(function(n){y[n]={data:o[n],value:a=c[n],startAngle:f,endAngle:f+=a*v+p,padAngle:h}}),y}var t=Number,e=bl,r=0,i=Ho,u=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(i=t,n):i},n.padAngle=function(t){return arguments.length?(u=t,n):u},n};var bl={};ao.layout.stack=function(){function n(a,l){if(!(h=a.length))return a;var c=a.map(function(e,r){return t.call(n,e,r)}),f=c.map(function(t){return t.map(function(t,e){return[u.call(n,t,e),o.call(n,t,e)]})}),s=e.call(n,f,l);c=ao.permute(c,s),f=ao.permute(f,s);var h,p,g,v,d=r.call(n,f,l),y=c[0].length;for(g=0;y>g;++g)for(i.call(n,c[0][g],v=d[g],f[0][g][1]),p=1;h>p;++p)i.call(n,c[p][g],v+=f[p-1][g][1],f[p][g][1]);return a}var t=m,e=gi,r=vi,i=pi,u=si,o=hi;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:_l.get(t)||gi,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:wl.get(t)||vi,n):r},n.x=function(t){return arguments.length?(u=t,n):u},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(i=t,n):i},n};var _l=ao.map({"inside-out":function(n){var t,e,r=n.length,i=n.map(di),u=n.map(yi),o=ao.range(r).sort(function(n,t){return i[n]-i[t]}),a=0,l=0,c=[],f=[];for(t=0;r>t;++t)e=o[t],l>a?(a+=u[e],c.push(e)):(l+=u[e],f.push(e));return f.reverse().concat(c)},reverse:function(n){return ao.range(n.length).reverse()},"default":gi}),wl=ao.map({silhouette:function(n){var t,e,r,i=n.length,u=n[0].length,o=[],a=0,l=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;u>e;++e)l[e]=(a-o[e])/2;return l},wiggle:function(n){var t,e,r,i,u,o,a,l,c,f=n.length,s=n[0],h=s.length,p=[];for(p[0]=l=c=0,e=1;h>e;++e){for(t=0,i=0;f>t;++t)i+=n[t][e][1];for(t=0,u=0,a=s[e][0]-s[e-1][0];f>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;u+=o*n[t][e][1]}p[e]=l-=i?u/i*a:0,c>l&&(c=l)}for(e=0;h>e;++e)p[e]-=c;return p},expand:function(n){var t,e,r,i=n.length,u=n[0].length,o=1/i,a=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];if(r)for(t=0;i>t;t++)n[t][e][1]/=r;else for(t=0;i>t;t++)n[t][e][1]=o}for(e=0;u>e;++e)a[e]=0;return a},zero:vi});ao.layout.histogram=function(){function n(n,u){for(var o,a,l=[],c=n.map(e,this),f=r.call(this,c,u),s=i.call(this,f,c,u),u=-1,h=c.length,p=s.length-1,g=t?1:1/h;++u0)for(u=-1;++u=f[0]&&a<=f[1]&&(o=l[ao.bisect(s,a,1,p)-1],o.y+=g,o.push(n[u]));return l}var t=!0,e=Number,r=bi,i=Mi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(i="number"==typeof t?function(n){return xi(n,t)}:En(t),n):i},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},ao.layout.pack=function(){function n(n,u){var o=e.call(this,n,u),a=o[0],l=i[0],c=i[1],f=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,oi(a,function(n){n.r=+f(n.value)}),oi(a,Ni),r){var s=r*(t?1:Math.max(2*a.r/l,2*a.r/c))/2;oi(a,function(n){n.r+=s}),oi(a,Ni),oi(a,function(n){n.r-=s})}return Ci(a,l/2,c/2,t?1:1/Math.max(2*a.r/l,2*a.r/c)),o}var t,e=ao.layout.hierarchy().sort(_i),r=0,i=[1,1];return n.size=function(t){return arguments.length?(i=t,n):i},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},ii(n,e)},ao.layout.tree=function(){function n(n,i){var f=o.call(this,n,i),s=f[0],h=t(s);if(oi(h,e),h.parent.m=-h.z,ui(h,r),c)ui(s,u);else{var p=s,g=s,v=s;ui(s,function(n){n.xg.x&&(g=n),n.depth>v.depth&&(v=n)});var d=a(p,g)/2-p.x,y=l[0]/(g.x+a(g,p)/2+d),m=l[1]/(v.depth||1);ui(s,function(n){n.x=(n.x+d)*y,n.y=n.depth*m})}return f}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var i,u=t.children,o=0,a=u.length;a>o;++o)r.push((u[o]=i={_:u[o],parent:t,children:(i=u[o].children)&&i.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=i);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Di(n);var u=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-u):n.z=u}else r&&(n.z=r.z+a(n._,r._));n.parent.A=i(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function i(n,t,e){if(t){for(var r,i=n,u=n,o=t,l=i.parent.children[0],c=i.m,f=u.m,s=o.m,h=l.m;o=Ti(o),i=qi(i),o&&i;)l=qi(l),u=Ti(u),u.a=n,r=o.z+s-i.z-c+a(o._,i._),r>0&&(Ri(Pi(o,n,e),n,r),c+=r,f+=r),s+=o.m,c+=i.m,h+=l.m,f+=u.m;o&&!Ti(u)&&(u.t=o,u.m+=s-f),i&&!qi(l)&&(l.t=i,l.m+=c-h,e=n)}return e}function u(n){n.x*=l[0],n.y=n.depth*l[1]}var o=ao.layout.hierarchy().sort(null).value(null),a=Li,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(c=null==(l=t)?u:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:u,n):c?l:null},ii(n,o)},ao.layout.cluster=function(){function n(n,u){var o,a=t.call(this,n,u),l=a[0],c=0;oi(l,function(n){var t=n.children;t&&t.length?(n.x=ji(t),n.y=Ui(t)):(n.x=o?c+=e(n,o):0,n.y=0,o=n)});var f=Fi(l),s=Hi(l),h=f.x-e(f,s)/2,p=s.x+e(s,f)/2;return oi(l,i?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(p-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),a}var t=ao.layout.hierarchy().sort(null).value(null),e=Li,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=null==(r=t),n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=null!=(r=t),n):i?r:null},ii(n,t)},ao.layout.treemap=function(){function n(n,t){for(var e,r,i=-1,u=n.length;++it?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var u=e.children;if(u&&u.length){var o,a,l,c=s(e),f=[],h=u.slice(),g=1/0,v="slice"===p?c.dx:"dice"===p?c.dy:"slice-dice"===p?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),f.area=0;(l=h.length)>0;)f.push(o=h[l-1]),f.area+=o.area,"squarify"!==p||(a=r(f,v))<=g?(h.pop(),g=a):(f.area-=f.pop().area,i(f,v,c,!1),v=Math.min(c.dx,c.dy),f.length=f.area=0,g=1/0);f.length&&(i(f,v,c,!0),f.length=f.area=0),u.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var u,o=s(t),a=r.slice(),l=[];for(n(a,o.dx*o.dy/t.value),l.area=0;u=a.pop();)l.push(u),l.area+=u.area,null!=u.z&&(i(l,u.z?o.dx:o.dy,o,!a.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,i=0,u=1/0,o=-1,a=n.length;++oe&&(u=e),e>i&&(i=e));return r*=r,t*=t,r?Math.max(t*i*g/r,r/(t*u*g)):1/0}function i(n,t,e,r){var i,u=-1,o=n.length,a=e.x,c=e.y,f=t?l(n.area/t):0; +if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++ue.dx)&&(f=e.dx);++ue&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=2*Math.random()-1,r=2*Math.random()-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=ao.random.normal.apply(ao,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=ao.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},ao.scale={};var Sl={floor:m,ceil:m};ao.scale.linear=function(){return Wi([0,1],[0,1],Mr,!1)};var kl={s:1,g:1,p:1,r:1,e:1};ao.scale.log=function(){return ru(ao.scale.linear().domain([0,1]),10,!0,[1,10])};var Nl=ao.format(".0e"),El={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};ao.scale.pow=function(){return iu(ao.scale.linear(),1,[0,1])},ao.scale.sqrt=function(){return ao.scale.pow().exponent(.5)},ao.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},ao.scale.category10=function(){return ao.scale.ordinal().range(Al)},ao.scale.category20=function(){return ao.scale.ordinal().range(Cl)},ao.scale.category20b=function(){return ao.scale.ordinal().range(zl)},ao.scale.category20c=function(){return ao.scale.ordinal().range(Ll)};var Al=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Cl=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),zl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),Ll=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);ao.scale.quantile=function(){return au([],[])},ao.scale.quantize=function(){return lu(0,1,[0,1])},ao.scale.threshold=function(){return cu([.5],[0,1])},ao.scale.identity=function(){return fu([0,1])},ao.svg={},ao.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),f=o.apply(this,arguments)-Io,s=a.apply(this,arguments)-Io,h=Math.abs(s-f),p=f>s?0:1;if(n>c&&(g=c,c=n,n=g),h>=Oo)return t(c,p)+(n?t(n,1-p):"")+"Z";var g,v,d,y,m,M,x,b,_,w,S,k,N=0,E=0,A=[];if((y=(+l.apply(this,arguments)||0)/2)&&(d=u===ql?Math.sqrt(n*n+c*c):+u.apply(this,arguments),p||(E*=-1),c&&(E=tn(d/c*Math.sin(y))),n&&(N=tn(d/n*Math.sin(y)))),c){m=c*Math.cos(f+E),M=c*Math.sin(f+E),x=c*Math.cos(s-E),b=c*Math.sin(s-E);var C=Math.abs(s-f-2*E)<=Fo?0:1;if(E&&yu(m,M,x,b)===p^C){var z=(f+s)/2;m=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else m=M=0;if(n){_=n*Math.cos(s-N),w=n*Math.sin(s-N),S=n*Math.cos(f+N),k=n*Math.sin(f+N);var L=Math.abs(f-s+2*N)<=Fo?0:1;if(N&&yu(_,w,S,k)===1-p^L){var q=(f+s)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Uo&&(g=Math.min(Math.abs(c-n)/2,+i.apply(this,arguments)))>.001){v=c>n^p?0:1;var T=g,R=g;if(Fo>h){var D=null==S?[_,w]:null==x?[m,M]:Re([m,M],[S,k],[x,b],[_,w]),P=m-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(g,(n-O)/(H-1)),T=Math.min(g,(c-O)/(H+1))}if(null!=x){var I=mu(null==S?[_,w]:[S,k],[m,M],c,T,p),Y=mu([x,b],[_,w],c,T,p);g===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-p^yu(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",p," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",m,",",M);if(null!=S){var Z=mu([m,M],[S,k],n,-R,p),V=mu([_,w],null==x?[m,M]:[x,b],n,-R,p);g===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",p^yu(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-p," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",m,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",p," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-p," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hu,r=pu,i=su,u=ql,o=gu,a=vu,l=du;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(i=En(t),n):i},n.padRadius=function(t){return arguments.length?(u=t==ql?ql:En(t),n):u},n.startAngle=function(t){return arguments.length?(o=En(t),n):o},n.endAngle=function(t){return arguments.length?(a=En(t),n):a},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+o.apply(this,arguments)+ +a.apply(this,arguments))/2-Io;return[Math.cos(t)*n,Math.sin(t)*n]},n};var ql="auto";ao.svg.line=function(){return Mu(m)};var Tl=ao.map({linear:xu,"linear-closed":bu,step:_u,"step-before":wu,"step-after":Su,basis:zu,"basis-open":Lu,"basis-closed":qu,bundle:Tu,cardinal:Eu,"cardinal-open":ku,"cardinal-closed":Nu,monotone:Fu});Tl.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Rl=[0,2/3,1/3,0],Dl=[0,1/3,2/3,0],Pl=[0,1/6,2/3,1/6];ao.svg.line.radial=function(){var n=Mu(Hu);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wu.reverse=Su,Su.reverse=wu,ao.svg.area=function(){return Ou(m)},ao.svg.area.radial=function(){var n=Ou(Hu);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},ao.svg.chord=function(){function n(n,a){var l=t(this,u,n,a),c=t(this,o,n,a);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?i(l.r,l.p1,l.r,l.p0):i(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+i(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var i=t.call(n,e,r),u=a.call(n,i,r),o=l.call(n,i,r)-Io,f=c.call(n,i,r)-Io;return{r:u,a0:o,a1:f,p0:[u*Math.cos(o),u*Math.sin(o)],p1:[u*Math.cos(f),u*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Fo)+",1 "+t}function i(n,t,e,r){return"Q 0,0 "+r}var u=Me,o=xe,a=Iu,l=gu,c=vu;return n.radius=function(t){return arguments.length?(a=En(t),n):a},n.source=function(t){return arguments.length?(u=En(t),n):u},n.target=function(t){return arguments.length?(o=En(t),n):o},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},ao.svg.diagonal=function(){function n(n,i){var u=t.call(this,n,i),o=e.call(this,n,i),a=(u.y+o.y)/2,l=[u,{x:u.x,y:a},{x:o.x,y:a},o];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yu;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},ao.svg.diagonal.radial=function(){var n=ao.svg.diagonal(),t=Yu,e=n.projection;return n.projection=function(n){return arguments.length?e(Zu(t=n)):t},n},ao.svg.symbol=function(){function n(n,r){return(Ul.get(t.call(this,n,r))||$u)(e.call(this,n,r))}var t=Xu,e=Vu;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Ul=ao.map({circle:$u,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Fl)),e=t*Fl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});ao.svg.symbolTypes=Ul.keys();var jl=Math.sqrt(3),Fl=Math.tan(30*Yo);Co.transition=function(n){for(var t,e,r=Hl||++Zl,i=Ku(n),u=[],o=Ol||{time:Date.now(),ease:Nr,delay:0,duration:250},a=-1,l=this.length;++au;u++){i.push(t=[]);for(var e=this[u],a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return Wu(i,this.namespace,this.id)},Yl.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(i){i[r][e].tween.set(n,t)})},Yl.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function i(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function u(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?$r:Mr,a=ao.ns.qualify(n);return Ju(this,"attr."+n,t,a.local?u:i)},Yl.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(i));return r&&function(n){this.setAttribute(i,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(i.space,i.local));return r&&function(n){this.setAttributeNS(i.space,i.local,r(n))}}var i=ao.ns.qualify(n);return this.tween("attr."+n,i.local?r:e)},Yl.style=function(n,e,r){function i(){this.style.removeProperty(n)}function u(e){return null==e?i:(e+="",function(){var i,u=t(this).getComputedStyle(this,null).getPropertyValue(n);return u!==e&&(i=Mr(u,e),function(t){this.style.setProperty(n,i(t),r)})})}var o=arguments.length;if(3>o){if("string"!=typeof n){2>o&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ju(this,"style."+n,e,u)},Yl.styleTween=function(n,e,r){function i(i,u){var o=e.call(this,i,u,t(this).getComputedStyle(this,null).getPropertyValue(n));return o&&function(t){this.style.setProperty(n,o(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,i)},Yl.text=function(n){return Ju(this,"text",n,Gu)},Yl.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Yl.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=ao.ease.apply(ao,arguments)),Y(this,function(r){r[e][t].ease=n}))},Yl.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,i,u){r[e][t].delay=+n.call(r,r.__data__,i,u)}:(n=+n,function(r){r[e][t].delay=n}))},Yl.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,i,u){r[e][t].duration=Math.max(1,n.call(r,r.__data__,i,u))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Yl.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var i=Ol,u=Hl;try{Hl=e,Y(this,function(t,i,u){Ol=t[r][e],n.call(t,t.__data__,i,u)})}finally{Ol=i,Hl=u}}else Y(this,function(i){var u=i[r][e];(u.event||(u.event=ao.dispatch("start","end","interrupt"))).on(n,t)});return this},Yl.transition=function(){for(var n,t,e,r,i=this.id,u=++Zl,o=this.namespace,a=[],l=0,c=this.length;c>l;l++){a.push(n=[]);for(var t=this[l],f=0,s=t.length;s>f;f++)(e=t[f])&&(r=e[o][i],Qu(e,f,o,u,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wu(a,o,u)},ao.svg.axis=function(){function n(n){n.each(function(){var n,c=ao.select(this),f=this.__chart__||e,s=this.__chart__=e.copy(),h=null==l?s.ticks?s.ticks.apply(s,a):s.domain():l,p=null==t?s.tickFormat?s.tickFormat.apply(s,a):m:t,g=c.selectAll(".tick").data(h,s),v=g.enter().insert("g",".domain").attr("class","tick").style("opacity",Uo),d=ao.transition(g.exit()).style("opacity",Uo).remove(),y=ao.transition(g.order()).style("opacity",1),M=Math.max(i,0)+o,x=Zi(s),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),ao.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=y.select("line"),C=g.select("text").text(p),z=v.select("text"),L=y.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=no,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*u+"V0H"+x[1]+"V"+q*u)):(n=to,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*u+","+x[0]+"H0V"+x[1]+"H"+q*u)),E.attr(N,q*i),z.attr(k,q*M),A.attr(S,0).attr(N,q*i),L.attr(w,0).attr(k,q*M),s.rangeBand){var T=s,R=T.rangeBand()/2;f=s=function(n){return T(n)+R}}else f.rangeBand?f=s:d.call(n,s,f);v.call(n,f,s),y.call(n,s,s)})}var t,e=ao.scale.linear(),r=Vl,i=6,u=6,o=3,a=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Xl?t+"":Vl,n):r},n.ticks=function(){return arguments.length?(a=co(arguments),n):a},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(i=+t,u=+arguments[e-1],n):i},n.innerTickSize=function(t){return arguments.length?(i=+t,n):i},n.outerTickSize=function(t){return arguments.length?(u=+t,n):u},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var Vl="bottom",Xl={top:1,right:1,bottom:1,left:1};ao.svg.brush=function(){function n(t){t.each(function(){var t=ao.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=t.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=t.selectAll(".resize").data(v,m);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return $l[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,s=ao.transition(t),h=ao.transition(o);c&&(l=Zi(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(s)),f&&(l=Zi(f),h.attr("y",l[0]).attr("height",l[1]-l[0]),i(s)),e(s)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",s[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1]-s[0])}function i(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function u(){function u(){32==ao.event.keyCode&&(C||(M=null,L[0]-=s[1],L[1]-=h[1],C=2),S())}function v(){32==ao.event.keyCode&&2==C&&(L[0]+=s[1],L[1]+=h[1],C=0,S())}function d(){var n=ao.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(ao.event.altKey?(M||(M=[(s[0]+s[1])/2,(h[0]+h[1])/2]),L[0]=s[+(n[0]f?(i=r,r=f):i=f),v[0]!=r||v[1]!=i?(e?a=null:o=null,v[0]=r,v[1]=i,!0):void 0}function m(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),ao.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=ao.select(ao.event.target),w=l.of(b,arguments),k=ao.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&f,C=_.classed("extent"),z=W(b),L=ao.mouse(b),q=ao.select(t(b)).on("keydown.brush",u).on("keyup.brush",v);if(ao.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",m):q.on("mousemove.brush",d).on("mouseup.brush",m),k.interrupt().selectAll("*").interrupt(),C)L[0]=s[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[s[1-T]-L[0],h[1-R]-L[1]],L[0]=s[T],L[1]=h[R]}else ao.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),ao.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var o,a,l=N(n,"brushstart","brush","brushend"),c=null,f=null,s=[0,0],h=[0,0],p=!0,g=!0,v=Bl[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:s,y:h,i:o,j:a},e=this.__chart__||t;this.__chart__=t,Hl?ao.select(this).transition().each("start.brush",function(){o=e.i,a=e.j,s=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(s,t.x),r=xr(h,t.y);return o=a=null,function(i){s=t.x=e(i),h=t.y=r(i),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){o=t.i,a=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=Bl[!c<<1|!f],n):c},n.y=function(t){return arguments.length?(f=t,v=Bl[!c<<1|!f],n):f},n.clamp=function(t){return arguments.length?(c&&f?(p=!!t[0],g=!!t[1]):c?p=!!t:f&&(g=!!t),n):c&&f?[p,g]:c?p:f?g:null},n.extent=function(t){var e,r,i,u,l;return arguments.length?(c&&(e=t[0],r=t[1],f&&(e=e[0],r=r[0]),o=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),e==s[0]&&r==s[1]||(s=[e,r])),f&&(i=t[0],u=t[1],c&&(i=i[1],u=u[1]),a=[i,u],f.invert&&(i=f(i),u=f(u)),i>u&&(l=i,i=u,u=l),i==h[0]&&u==h[1]||(h=[i,u])),n):(c&&(o?(e=o[0],r=o[1]):(e=s[0],r=s[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),f&&(a?(i=a[0],u=a[1]):(i=h[0],u=h[1],f.invert&&(i=f.invert(i),u=f.invert(u)),i>u&&(l=i,i=u,u=l))),c&&f?[[e,i],[r,u]]:c?[e,r]:f&&[i,u])},n.clear=function(){return n.empty()||(s=[0,0],h=[0,0],o=a=null),n},n.empty=function(){return!!c&&s[0]==s[1]||!!f&&h[0]==h[1]},ao.rebind(n,l,"on")};var $l={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Bl=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Wl=ga.format=xa.timeFormat,Jl=Wl.utc,Gl=Jl("%Y-%m-%dT%H:%M:%S.%LZ");Wl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?eo:Gl,eo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},eo.toString=Gl.toString,ga.second=On(function(n){return new va(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ga.seconds=ga.second.range,ga.seconds.utc=ga.second.utc.range,ga.minute=On(function(n){return new va(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ga.minutes=ga.minute.range,ga.minutes.utc=ga.minute.utc.range,ga.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new va(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ga.hours=ga.hour.range,ga.hours.utc=ga.hour.utc.range,ga.month=On(function(n){return n=ga.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ga.months=ga.month.range,ga.months.utc=ga.month.utc.range;var Kl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Ql=[[ga.second,1],[ga.second,5],[ga.second,15],[ga.second,30],[ga.minute,1],[ga.minute,5],[ga.minute,15],[ga.minute,30],[ga.hour,1],[ga.hour,3],[ga.hour,6],[ga.hour,12],[ga.day,1],[ga.day,2],[ga.week,1],[ga.month,1],[ga.month,3],[ga.year,1]],nc=Wl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),tc={range:function(n,t,e){return ao.range(Math.ceil(n/e)*e,+t,e).map(io)},floor:m,ceil:m};Ql.year=ga.year,ga.scale=function(){return ro(ao.scale.linear(),Ql,nc)};var ec=Ql.map(function(n){return[n[0].utc,n[1]]}),rc=Jl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);ec.year=ga.year.utc,ga.scale.utc=function(){return ro(ao.scale.linear(),ec,rc)},ao.text=An(function(n){return n.responseText}),ao.json=function(n,t){return Cn(n,"application/json",uo,t)},ao.html=function(n,t){return Cn(n,"text/html",oo,t)},ao.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=ao,define(ao)):"object"==typeof module&&module.exports?module.exports=ao:this.d3=ao}(); \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/reports/group.html b/pydra/tasks/mriqc/data/reports/group.html new file mode 100644 index 0000000..85f74fc --- /dev/null +++ b/pydra/tasks/mriqc/data/reports/group.html @@ -0,0 +1,108 @@ + + + + + + + + + + MRIQC: group {{ modality }} +report + + +

MRIQC: group {{ modality }} +report

+

Summary

+
    +
  • Date and time: {{ timestamp }}.
  • +
  • MRIQC version: {{ version }}.
  • +{% if failed %} +
  • Some individual reports failed: +{% for f in failed %} +{{ f }}{% if loop.last %}.{% else %}, {% endif %} +{% endfor %} +
  • +{% endif %} +
+{% for data_csv in csv_groups %} +
+ +{% endfor %} + + + + + diff --git a/pydra/tasks/mriqc/data/reports/resources/DO_NOT_REMOVE_OR_MODIFY b/pydra/tasks/mriqc/data/reports/resources/DO_NOT_REMOVE_OR_MODIFY new file mode 100644 index 0000000..b6ae8b7 --- /dev/null +++ b/pydra/tasks/mriqc/data/reports/resources/DO_NOT_REMOVE_OR_MODIFY @@ -0,0 +1,2 @@ +Do not remove or modify the files in this folder. They are being served from +github to render group level reports in a v0.9.0. \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/reports/resources/boxplots.css b/pydra/tasks/mriqc/data/reports/resources/boxplots.css new file mode 100644 index 0000000..0938a5e --- /dev/null +++ b/pydra/tasks/mriqc/data/reports/resources/boxplots.css @@ -0,0 +1,157 @@ +/* Hide data table */ +.csvdata { + display: none; +} + +body { + font-family: helvetica; +} + +.text-warning { + font-weight: bold; + color: red; +} +/*Primary Chart*/ + +/*Nested divs for responsiveness*/ +.chart-wrapper { + max-width: 800px; /*Overwritten by the JS*/ + min-width: 160px; + margin-bottom: 20px; + font-family: helvetica; +} +.chart-wrapper .inner-wrapper { + position: relative; + padding-bottom: 50%; /*Overwritten by the JS*/ + width: 100%; +} +.chart-wrapper .outer-box { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.chart-wrapper .inner-box { + width: 100%; + height: 100%; +} + +.chart-wrapper text { + font-family: helvetica; + font-size: 13px; +} + +.chart-wrapper .axis path, +.chart-wrapper .axis line { + fill: none; + stroke: #888; + stroke-width: 2px; + shape-rendering: crispEdges; +} + +.chart-wrapper .y.axis .tick line { + stroke: lightgrey; + opacity: 0.6; + stroke-dasharray: 2,1; + stroke-width: 1; + shape-rendering: crispEdges; + +} + +.chart-wrapper .x.axis .domain { + display: none; +} + +.chart-wrapper div.tooltip { + position: absolute; + text-align: left; + padding: 3px; + font-size: 12px; + background: #eee; + border: 0px; + border-radius: 1px; + pointer-events: none; + opacity: .7; + z-index: 10; +} + +/*Box Plot*/ +.chart-wrapper .box-plot .box { + fill-opacity: .5; + stroke-width: 2; +} +.chart-wrapper .box-plot line { + stroke-width: 2px; +} +.chart-wrapper .box-plot circle { + fill: white; + stroke: black; +} + +.chart-wrapper .box-plot .median { + stroke: black; +} + +.chart-wrapper .box-plot circle.median { + /*the script makes the circles the same color as the box, you can override this in the js*/ + fill: white !important; +} + +.chart-wrapper .box-plot .mean { + stroke: white; + stroke-dasharray: 2,1; + stroke-width: 1px; +} + +@media (max-width:500px){ + .chart-wrapper .box-plot circle {display: none;} +} + +/*Violin Plot*/ + +.chart-wrapper .violin-plot .area { + shape-rendering: geometricPrecision; + opacity: 0.4; +} + +.chart-wrapper .violin-plot .line { + fill: none; + stroke-width: 2px; + shape-rendering: geometricPrecision; +} + +/*Notch Plot*/ +.chart-wrapper .notch-plot .notch { + fill-opacity: 0.4; + stroke-width: 2; +} + +/* Point Plots*/ +.chart-wrapper .points-plot .point { + /*stroke: black; + stroke-width: 1px;*/ + fill-opacity: 0.4; +} + +.chart-wrapper .metrics-lines { + stroke-width: 4px; +} + +/* Non-Chart Styles for demo*/ +.chart-options { + min-width: 200px; + font-size: 13px; + font-family: helvetica; +} +.chart-options button { + margin: 3px; + padding: 3px; + font-size: 12px; +} +.chart-options p { + display: inline; +} +@media (max-width:500px){ + .chart-options p {display: block;} +} \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/reports/resources/boxplots.js b/pydra/tasks/mriqc/data/reports/resources/boxplots.js new file mode 100644 index 0000000..a846efa --- /dev/null +++ b/pydra/tasks/mriqc/data/reports/resources/boxplots.js @@ -0,0 +1,1630 @@ +/** + * @fileOverview A D3 based distribution chart system. Supports: Box plots, Violin plots, Notched box plots, trend lines, beeswarm plot + * @version 3.0 + */ + + +/** + * Creates a box plot, violin plot, and or notched box plot + * @param settings Configuration options for the base plot + * @param settings.data The data for the plot + * @param settings.xName The name of the column that should be used for the x groups + * @param settings.yName The name of the column used for the y values + * @param {string} settings.selector The selector string for the main chart div + * @param [settings.axisLabels={}] Defaults to the xName and yName + * @param [settings.yTicks = 1] 1 = default ticks. 2 = double, 0.5 = half + * @param [settings.scale='linear'] 'linear' or 'log' - y scale of the chart + * @param [settings.chartSize={width:800, height:400}] The height and width of the chart itself (doesn't include the container) + * @param [settings.margin={top: 15, right: 60, bottom: 40, left: 50}] The margins around the chart (inside the main div) + * @param [settings.constrainExtremes=false] Should the y scale include outliers? + * @returns {object} chart A chart object + */ +function makeDistroChart(settings) { + + var chart = {}; + + // Defaults + chart.settings = { + data: null, + xName: null, + yName: null, + axisLabels: {xAxis: null, yAxis: null}, + labelName: "label", + unitsName: "units", + selector: null, + axisLables: null, + yTicks: 1, + scale: 'linear', + chartSize: {width: 800, height: 400}, + margin: {top: 15, right: 10, bottom: 50, left: 50}, + constrainExtremes: false, + color: d3.scale.category10(), + qctype: null + }; + + for (var setting in settings) { + chart.settings[setting] = settings[setting] + } + + function formatAsFloat(d) { + if (d % 1 !== 0) { + return d3.format(".2f")(d); + } else { + return d3.format(".0f")(d); + } + } + + function logFormatNumber(d) { + var x = Math.log(d) / Math.log(10) + 1e-6; + return Math.abs(x - Math.floor(x)) < 0.6 ? formatAsFloat(d) : ""; + } + + chart.yFormatter = formatAsFloat; + + chart.data = chart.settings.data; + + iqmName = chart.data[0][chart.settings.xName] + if (iqmName.lastIndexOf('_') > 0) { + iqmName = iqmName.substr(0, iqmName.lastIndexOf('_')) + } + chart.settings.axisLabels.yAxis = iqmName.toUpperCase() + units = chart.data[0][chart.settings.unitsName] + if (units) { + chart.settings.axisLabels.yAxis += ' (' + units + ')' + } + + + chart.groupObjs = {}; //The data organized by grouping and sorted as well as any metadata for the groups + chart.objs = {mainDiv: null, chartDiv: null, g: null, xAxis: null, yAxis: null}; + chart.colorFunct = null; + + /** + * Takes an array, function, or object mapping and created a color function from it + * @param {function|[]|object} colorOptions + * @returns {function} Function to be used to determine chart colors + */ + function getColorFunct(colorOptions) { + if (typeof colorOptions == 'function') { + return colorOptions + } else if (Array.isArray(colorOptions)) { + // If an array is provided, map it to the domain + var colorMap = {}, cColor = 0; + for (var cName in chart.groupObjs) { + colorMap[cName] = colorOptions[cColor]; + cColor = (cColor + 1) % colorOptions.length; + } + return function (group) { + return colorMap[group]; + } + } else if (typeof colorOptions == 'object') { + // if an object is provided, assume it maps to the colors + return function (group) { + return colorOptions[group]; + } + } else { + return d3.scale.category10(); + } + } + + /** + * Takes a percentage as returns the values that correspond to that percentage of the group range width + * @param objWidth Percentage of range band + * @param gName The bin name to use to get the x shift + * @returns {{left: null, right: null, middle: null}} + */ + function getObjWidth(objWidth, gName) { + var objSize = {left: null, right: null, middle: null}; + var width = chart.xScale.rangeBand() * (objWidth / 100); + var padding = (chart.xScale.rangeBand() - width) / 2; + var gShift = chart.xScale(gName); + objSize.middle = chart.xScale.rangeBand() / 2 + gShift; + objSize.left = padding + gShift; + objSize.right = objSize.left + width; + return objSize; + } + + /** + * Adds jitter to the scatter point plot + * @param doJitter true or false, add jitter to the point + * @param width percent of the range band to cover with the jitter + * @returns {number} + */ + function addJitter(doJitter, width) { + if (doJitter !== true || width == 0) { + return 0 + } + return Math.floor(Math.random() * width) - width / 2; + } + + function shallowCopy(oldObj) { + var newObj = {}; + for (var i in oldObj) { + if (oldObj.hasOwnProperty(i)) { + newObj[i] = oldObj[i]; + } + } + return newObj; + } + + /** + * Closure that creates the tooltip hover function + * @param groupName Name of the x group + * @param metrics Object to use to get values for the group + * @returns {Function} A function that provides the values for the tooltip + */ + function tooltipHover(groupName, metrics) { + var tooltipString = "Group: " + groupName; + tooltipString += "Max: " + formatAsFloat(metrics.max, 0.1); + tooltipString += "Q3: " + formatAsFloat(metrics.quartile3); + tooltipString += "Median: " + formatAsFloat(metrics.median); + tooltipString += "Q1: " + formatAsFloat(metrics.quartile1); + tooltipString += "Min: " + formatAsFloat(metrics.min); + return function () { + chart.objs.tooltip.transition().duration(200).style("opacity", 0.9); + chart.objs.tooltip.html(tooltipString) + }; + } + + function axislabelHover(groupName) { + var tooltipString = "Go to definition of " + groupName; + return function () { + chart.objs.tooltip.transition().duration(200).style("opacity", 1.0); + chart.objs.tooltip.html(tooltipString) + }; + } + + /** + * Closure that creates the tooltip hover function + * @param groupName Name of the x group + * @param metrics Object to use to get values for the group + * @returns {Function} A function that provides the values for the tooltip + */ + function pointHover(label, value) { + var tooltipString = "Subject: " + label + "Measure: " + value + return function () { + chart.objs.tooltip.transition().duration(200).style("opacity", 1.0); + chart.objs.tooltip.html(tooltipString) + }; + } + + /** + * Parse the data and calculates base values for the plots + */ + !function prepareData() { + function calcMetrics(values) { + // Do not reorder in-place + values = values.slice(0).sort(d3.ascending) + + var metrics = { //These are the original non�scaled values + max: null, + upperOuterFence: null, + upperInnerFence: null, + quartile3: null, + median: null, + mean: null, + iqr: null, + quartile1: null, + lowerInnerFence: null, + lowerOuterFence: null, + min: null + }; + + metrics.min = d3.min(values); + metrics.quartile1 = d3.quantile(values, 0.25); + metrics.median = d3.median(values); + metrics.mean = d3.mean(values); + metrics.quartile3 = d3.quantile(values, 0.75); + metrics.max = d3.max(values); + metrics.iqr = metrics.quartile3 - metrics.quartile1; + + //The inner fences are the closest value to the IQR without going past it (assumes sorted lists) + var LIF = metrics.quartile1 - (1.5 * metrics.iqr); + var UIF = metrics.quartile3 + (1.5 * metrics.iqr); + for (var i = 0; i <= values.length; i++) { + if (values[i] < LIF) { + continue; + } + if (!metrics.lowerInnerFence && values[i] >= LIF) { + metrics.lowerInnerFence = values[i]; + continue; + } + if (values[i] > UIF) { + metrics.upperInnerFence = values[i - 1]; + break; + } + } + + + metrics.lowerOuterFence = metrics.quartile1 - (3 * metrics.iqr); + metrics.upperOuterFence = metrics.quartile3 + (3 * metrics.iqr); + if (!metrics.lowerInnerFence) { + metrics.lowerInnerFence = metrics.min; + } + if (!metrics.upperInnerFence) { + metrics.upperInnerFence = metrics.max; + } + return metrics + } + + var current_x = null; + var current_y = null; + var current_row; + + // Group the values + for (current_row = 0; current_row < chart.data.length; current_row++) { + current_x = chart.data[current_row][chart.settings.xName]; + current_y = chart.data[current_row][chart.settings.yName]; + current_label = chart.data[current_row][chart.settings.labelName]; + + if (chart.groupObjs.hasOwnProperty(current_x)) { + chart.groupObjs[current_x].values.push(current_y); + chart.groupObjs[current_x].labels.push(current_label); + } else { + chart.groupObjs[current_x] = {}; + chart.groupObjs[current_x].values = [current_y]; + chart.groupObjs[current_x].labels = [current_label]; + } + } + + for (var cName in chart.groupObjs) { + // chart.groupObjs[cName].values.sort(d3.ascending); + chart.groupObjs[cName].metrics = {}; + chart.groupObjs[cName].metrics = calcMetrics(chart.groupObjs[cName].values); + + } + }(); + + /** + * Prepare the chart settings and chart div and svg + */ + !function prepareSettings() { + //Set base settings + chart.margin = chart.settings.margin; + chart.divWidth = chart.settings.chartSize.width; + chart.divHeight = chart.settings.chartSize.height; + chart.width = chart.divWidth - chart.margin.left - chart.margin.right; + chart.height = chart.divHeight - chart.margin.top - chart.margin.bottom; + + if (chart.settings.axisLabels) { + chart.xAxisLable = chart.settings.axisLabels.xAxis; + chart.yAxisLable = chart.settings.axisLabels.yAxis; + } + + if (chart.settings.scale === 'log') { + chart.yScale = d3.scale.log(); + chart.yFormatter = logFormatNumber; + } else { + chart.yScale = d3.scale.linear(); + } + + if (chart.settings.constrainExtremes === true) { + var fences = []; + for (var cName in chart.groupObjs) { + fences.push(chart.groupObjs[cName].metrics.lowerInnerFence); + fences.push(chart.groupObjs[cName].metrics.upperInnerFence); + } + chart.range = d3.extent(fences); + + } else { + chart.range = d3.extent(chart.data, function (d) {return d[chart.settings.yName];}); + } + + chart.colorFunct = getColorFunct(chart.settings.colors); + + // Build Scale functions + chart.yScale.range([chart.height, 0]).domain(chart.range).nice().clamp(true); + chart.xScale = d3.scale.ordinal().domain(Object.keys(chart.groupObjs)).rangeBands([0, chart.width]); + + //Build Axes Functions + chart.objs.yAxis = d3.svg.axis() + .scale(chart.yScale) + .orient("left") + .tickFormat(chart.yFormatter) + .outerTickSize(0) + .innerTickSize(-chart.width + (chart.margin.right + chart.margin.left)); + chart.objs.yAxis.ticks(chart.objs.yAxis.ticks()*chart.settings.yTicks); + chart.objs.xAxis = d3.svg.axis().scale(chart.xScale).orient("bottom").tickSize(5); + }(); + + /** + * Updates the chart based on the current settings and window size + * @returns {*} + */ + chart.update = function () { + // Update chart size based on view port size + chart.width = parseInt(chart.objs.chartDiv.style("width"), 10) - (chart.margin.left + chart.margin.right); + chart.height = parseInt(chart.objs.chartDiv.style("height"), 10) - (chart.margin.top + chart.margin.bottom); + + // Update scale functions + chart.xScale.rangeBands([0, chart.width]); + chart.yScale.range([chart.height, 0]); + + // Update the yDomain if the Violin plot clamp is set to -1 meaning it will extend the violins to make nice points + if (chart.violinPlots && chart.violinPlots.options.show == true && chart.violinPlots.options._yDomainVP != null) { + chart.yScale.domain(chart.violinPlots.options._yDomainVP).nice().clamp(true); + } else { + chart.yScale.domain(chart.range).nice().clamp(true); + } + + //Update axes + chart.objs.g.select('.x.axis').attr("transform", "translate(0," + chart.height + ")").call(chart.objs.xAxis) + .selectAll("text") + .attr("y", 5) + .attr("x", -5) + .attr("transform", "rotate(-45)") + .style("text-anchor", "end"); + chart.objs.g.select('.x.axis .label').attr("x", chart.width / 2); + chart.objs.g.select('.y.axis').call(chart.objs.yAxis.innerTickSize(-chart.width)); + chart.objs.g.select('.y.axis .label').attr("x", -chart.height / 2); + chart.objs.chartDiv.select('svg').attr("width", chart.width + (chart.margin.left + chart.margin.right)).attr("height", chart.height + (chart.margin.top + chart.margin.bottom)); + + return chart; + }; + + /** + * Prepare the chart html elements + */ + !function prepareChart() { + // Build main div and chart div + chart.objs.mainDiv = d3.select(chart.settings.selector) + .style("width", chart.divWidth + "px") + .style("display", "inline-block"); + // Add all the divs to make it centered and responsive + chart.objs.mainDiv.append("div") + .attr("class", "inner-wrapper") + .style("padding-bottom", (chart.divHeight / chart.divWidth) * 100 + "%") + .append("div").attr("class", "outer-box") + .append("div").attr("class", "inner-box"); + // Capture the inner div for the chart (where the chart actually is) + chart.selector = chart.settings.selector + " .inner-box"; + chart.objs.chartDiv = d3.select(chart.selector); + d3.select(window).on('resize.' + chart.selector, chart.update); + + // Create the svg + chart.objs.g = chart.objs.chartDiv.append("svg") + .attr("class", "chart-area") + .attr("width", chart.width + (chart.margin.left + chart.margin.right)) + .attr("height", chart.height + (chart.margin.top + chart.margin.bottom)) + .append("g") + .attr("transform", "translate(" + chart.margin.left + "," + chart.margin.top + ")"); + + // Create axes + chart.objs.axes = chart.objs.g.append("g").attr("class", "axis"); + chart.objs.axes.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + chart.height + ")") + .call(chart.objs.xAxis); + chart.objs.axes.append("g") + .attr("class", "y axis") + .call(chart.objs.yAxis) + .append("text") + //.attr("class", "label") + .attr("transform", "rotate(-90)") + //.attr("y", -42) + .attr("y", 6) + .attr("dy", ".71em") + //.attr("x", -chart.height / 2) + .style("text-anchor", "end") + .style("font-size", "16px") + .append("a") + .attr("xlink:href", function(d) { + return "http://mriqc.readthedocs.io/en/latest/measures.html" + }) + .text(chart.yAxisLable) + .on("mouseover", function () { + chart.objs.tooltip + .style("display", null) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); + }).on("mouseout", function () { + chart.objs.tooltip.style("display", "none"); + }).on("mousemove", axislabelHover(chart.yAxisLable)); + + // Create tooltip div + chart.objs.tooltip = chart.objs.mainDiv.append('div').attr('class', 'tooltip'); + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].g = chart.objs.g.append("g").attr("class", "group"); + } + chart.update(); + }(); + + /** + * Render a violin plot on the current chart + * @param options + * @param [options.showViolinPlot=true] True or False, show the violin plot + * @param [options.resolution=100 default] + * @param [options.bandwidth=10 default] May need higher bandwidth for larger data sets + * @param [options.width=50] The max percent of the group rangeBand that the violin can be + * @param [options.interpolation=''] How to render the violin + * @param [options.clamp=0 default] + * 0 = keep data within chart min and max, clamp once data = 0. May extend beyond data set min and max + * 1 = clamp at min and max of data set. Possibly no tails + * -1 = extend chart axis to make room for data to interpolate to 0. May extend axis and data set min and max + * @param [options.colors=chart default] The color mapping for the violin plot + * @returns {*} The chart object + */ + chart.renderViolinPlot = function (options) { + chart.violinPlots = {}; + + var defaultOptions = { + show: true, + showViolinPlot: true, + resolution: 100, + bandwidth: 20, + width: 50, + interpolation: 'cardinal', + clamp: 1, + colors: chart.colorFunct, + _yDomainVP: null // If the Violin plot is set to close all violin plots, it may need to extend the domain, that extended domain is stored here + }; + chart.violinPlots.options = shallowCopy(defaultOptions); + for (var option in options) { + chart.violinPlots.options[option] = options[option] + } + var vOpts = chart.violinPlots.options; + + // Create violin plot objects + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].violin = {}; + chart.groupObjs[cName].violin.objs = {}; + } + + /** + * Take a new set of options and redraw the violin + * @param updateOptions + */ + chart.violinPlots.change = function (updateOptions) { + if (updateOptions) { + for (var key in updateOptions) { + vOpts[key] = updateOptions[key] + } + } + + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].violin.objs.g.remove() + } + + chart.violinPlots.prepareViolin(); + chart.violinPlots.update(); + }; + + chart.violinPlots.reset = function () { + chart.violinPlots.change(defaultOptions) + }; + chart.violinPlots.show = function (opts) { + if (opts !== undefined) { + opts.show = true; + if (opts.reset) { + chart.violinPlots.reset() + } + } else { + opts = {show: true}; + } + chart.violinPlots.change(opts); + + }; + + chart.violinPlots.hide = function (opts) { + if (opts !== undefined) { + opts.show = false; + if (opts.reset) { + chart.violinPlots.reset() + } + } else { + opts = {show: false}; + } + chart.violinPlots.change(opts); + + }; + + /** + * Update the violin obj values + */ + chart.violinPlots.update = function () { + var cName, cViolinPlot; + + for (cName in chart.groupObjs) { + cViolinPlot = chart.groupObjs[cName].violin; + + // Build the violins sideways, so use the yScale for the xScale and make a new yScale + var xVScale = chart.yScale.copy(); + + + // Create the Kernel Density Estimator Function + cViolinPlot.kde = kernelDensityEstimator(eKernel(vOpts.bandwidth), xVScale.ticks(vOpts.resolution)); + cViolinPlot.kdedata = cViolinPlot.kde(chart.groupObjs[cName].values); + + var interpolateMax = chart.groupObjs[cName].metrics.max, + interpolateMin = chart.groupObjs[cName].metrics.min; + + if (vOpts.clamp == 0 || vOpts.clamp == -1) { // + // When clamp is 0, calculate the min and max that is needed to bring the violin plot to a point + // interpolateMax = the Minimum value greater than the max where y = 0 + interpolateMax = d3.min(cViolinPlot.kdedata.filter(function (d) { + return (d.x > chart.groupObjs[cName].metrics.max && d.y == 0) + }), function (d) { + return d.x; + }); + // interpolateMin = the Maximum value less than the min where y = 0 + interpolateMin = d3.max(cViolinPlot.kdedata.filter(function (d) { + return (d.x < chart.groupObjs[cName].metrics.min && d.y == 0) + }), function (d) { + return d.x; + }); + // If clamp is -1 we need to extend the axes so that the violins come to a point + if (vOpts.clamp == -1) { + kdeTester = eKernelTest(eKernel(vOpts.bandwidth), chart.groupObjs[cName].values); + if (!interpolateMax) { + var interMaxY = kdeTester(chart.groupObjs[cName].metrics.max); + var interMaxX = chart.groupObjs[cName].metrics.max; + var count = 25; // Arbitrary limit to make sure we don't get an infinite loop + while (count > 0 && interMaxY != 0) { + interMaxY = kdeTester(interMaxX); + interMaxX += 1; + count -= 1; + } + interpolateMax = interMaxX; + } + if (!interpolateMin) { + var interMinY = kdeTester(chart.groupObjs[cName].metrics.min); + var interMinX = chart.groupObjs[cName].metrics.min; + var count = 25; // Arbitrary limit to make sure we don't get an infinite loop + while (count > 0 && interMinY != 0) { + interMinY = kdeTester(interMinX); + interMinX -= 1; + count -= 1; + } + interpolateMin = interMinX; + } + + } + // Check to see if the new values are outside the existing chart range + // If they are assign them to the master _yDomainVP + if (!vOpts._yDomainVP) vOpts._yDomainVP = chart.range.slice(0); + if (interpolateMin && interpolateMin < vOpts._yDomainVP[0]) { + vOpts._yDomainVP[0] = interpolateMin; + } + if (interpolateMax && interpolateMax > vOpts._yDomainVP[1]) { + vOpts._yDomainVP[1] = interpolateMax; + } + + + } + + + if (vOpts.showViolinPlot) { + chart.update(); + xVScale = chart.yScale.copy(); + + // Need to recalculate the KDE because the xVScale changed + cViolinPlot.kde = kernelDensityEstimator(eKernel(vOpts.bandwidth), xVScale.ticks(vOpts.resolution)); + cViolinPlot.kdedata = cViolinPlot.kde(chart.groupObjs[cName].values); + } + + cViolinPlot.kdedata = cViolinPlot.kdedata + .filter(function (d) { + return (!interpolateMin || d.x >= interpolateMin) + }) + .filter(function (d) { + return (!interpolateMax || d.x <= interpolateMax) + }); + } + for (cName in chart.groupObjs) { + cViolinPlot = chart.groupObjs[cName].violin; + + // Get the violin width + var objBounds = getObjWidth(vOpts.width, cName); + var width = (objBounds.right - objBounds.left) / 2; + + var yVScale = d3.scale.linear() + .range([width, 0]) + .domain([0, d3.max(cViolinPlot.kdedata, function (d) {return d.y;})]) + .clamp(true); + + var area = d3.svg.area() + .interpolate(vOpts.interpolation) + .x(function (d) {return xVScale(d.x);}) + .y0(width) + .y1(function (d) {return yVScale(d.y);}); + + var line = d3.svg.line() + .interpolate(vOpts.interpolation) + .x(function (d) {return xVScale(d.x);}) + .y(function (d) {return yVScale(d.y)}); + + if (cViolinPlot.objs.left.area) { + cViolinPlot.objs.left.area + .datum(cViolinPlot.kdedata) + .attr("d", area); + cViolinPlot.objs.left.line + .datum(cViolinPlot.kdedata) + .attr("d", line); + + cViolinPlot.objs.right.area + .datum(cViolinPlot.kdedata) + .attr("d", area); + cViolinPlot.objs.right.line + .datum(cViolinPlot.kdedata) + .attr("d", line); + } + + // Rotate the violins + cViolinPlot.objs.left.g.attr("transform", "rotate(90,0,0) translate(0,-" + objBounds.left + ") scale(1,-1)"); + cViolinPlot.objs.right.g.attr("transform", "rotate(90,0,0) translate(0,-" + objBounds.right + ")"); + } + }; + + /** + * Create the svg elements for the violin plot + */ + chart.violinPlots.prepareViolin = function () { + var cName, cViolinPlot; + + if (vOpts.colors) { + chart.violinPlots.color = getColorFunct(vOpts.colors); + } else { + chart.violinPlots.color = chart.colorFunct + } + + if (vOpts.show == false) {return} + + for (cName in chart.groupObjs) { + cViolinPlot = chart.groupObjs[cName].violin; + + cViolinPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "violin-plot"); + cViolinPlot.objs.left = {area: null, line: null, g: null}; + cViolinPlot.objs.right = {area: null, line: null, g: null}; + + cViolinPlot.objs.left.g = cViolinPlot.objs.g.append("g"); + cViolinPlot.objs.right.g = cViolinPlot.objs.g.append("g"); + + if (vOpts.showViolinPlot !== false) { + //Area + cViolinPlot.objs.left.area = cViolinPlot.objs.left.g.append("path") + .attr("class", "area") + .style("fill", chart.violinPlots.color(cName)); + cViolinPlot.objs.right.area = cViolinPlot.objs.right.g.append("path") + .attr("class", "area") + .style("fill", chart.violinPlots.color(cName)); + + //Lines + cViolinPlot.objs.left.line = cViolinPlot.objs.left.g.append("path") + .attr("class", "line") + .attr("fill", 'none') + .style("stroke", chart.violinPlots.color(cName)); + cViolinPlot.objs.right.line = cViolinPlot.objs.right.g.append("path") + .attr("class", "line") + .attr("fill", 'none') + .style("stroke", chart.violinPlots.color(cName)); + } + + } + + }; + + + function kernelDensityEstimator(kernel, x) { + return function (sample) { + return x.map(function (x) { + return {x:x, y:d3.mean(sample, function (v) {return kernel(x - v);})}; + }); + }; + } + + function eKernel(scale) { + return function (u) { + return Math.abs(u /= scale) <= 1 ? .75 * (1 - u * u) / scale : 0; + }; + } + + // Used to find the roots for adjusting violin axis + // Given an array, find the value for a single point, even if it is not in the domain + function eKernelTest(kernel, array) { + return function (testX) { + return d3.mean(array, function (v) {return kernel(testX - v);}) + } + } + + chart.violinPlots.prepareViolin(); + + d3.select(window).on('resize.' + chart.selector + '.violinPlot', chart.violinPlots.update); + chart.violinPlots.update(); + return chart; + }; + + /** + * Render a box plot on the current chart + * @param options + * @param [options.show=true] Toggle the whole plot on and off + * @param [options.showBox=true] Show the box part of the box plot + * @param [options.showWhiskers=true] Show the whiskers + * @param [options.showMedian=true] Show the median line + * @param [options.showMean=false] Show the mean line + * @param [options.medianCSize=3] The size of the circle on the median + * @param [options.showOutliers=true] Plot outliers + * @param [options.boxwidth=30] The max percent of the group rangeBand that the box can be + * @param [options.lineWidth=boxWidth] The max percent of the group rangeBand that the line can be + * @param [options.outlierScatter=false] Spread out the outliers so they don't all overlap (in development) + * @param [options.outlierCSize=2] Size of the outliers + * @param [options.colors=chart default] The color mapping for the box plot + * @returns {*} The chart object + */ + chart.renderBoxPlot = function (options) { + chart.boxPlots = {}; + + // Defaults + var defaultOptions = { + show: true, + showBox: true, + showWhiskers: true, + showMedian: true, + showMean: false, + medianCSize: 3.5, + showOutliers: true, + boxWidth: 30, + lineWidth: null, + scatterOutliers: false, + outlierCSize: 2.5, + colors: chart.colorFunct, + padding: 0 + }; + chart.boxPlots.options = shallowCopy(defaultOptions); + for (var option in options) { + chart.boxPlots.options[option] = options[option] + } + var bOpts = chart.boxPlots.options; + + //Create box plot objects + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].boxPlot = {}; + chart.groupObjs[cName].boxPlot.objs = {}; + } + + + /** + * Calculates all the outlier points for each group + */ + !function calcAllOutliers() { + + /** + * Create lists of the outliers for each content group + * @param cGroup The object to modify + * @return null Modifies the object in place + */ + function calcOutliers(cGroup) { + var cExtremes = []; + var cOutliers = []; + var cOut, idx; + for (idx = 0; idx <= cGroup.values.length; idx++) { + cOut = {value: cGroup.values[idx]}; + + if (cOut.value < cGroup.metrics.lowerInnerFence) { + if (cOut.value < cGroup.metrics.lowerOuterFence) { + cExtremes.push(cOut); + } else { + cOutliers.push(cOut); + } + } else if (cOut.value > cGroup.metrics.upperInnerFence) { + if (cOut.value > cGroup.metrics.upperOuterFence) { + cExtremes.push(cOut); + } else { + cOutliers.push(cOut); + } + } + } + cGroup.boxPlot.objs.outliers = cOutliers; + cGroup.boxPlot.objs.extremes = cExtremes; + } + + for (var cName in chart.groupObjs) { + calcOutliers(chart.groupObjs[cName]); + } + }(); + + /** + * Take updated options and redraw the box plot + * @param updateOptions + */ + chart.boxPlots.change = function (updateOptions) { + if (updateOptions) { + for (var key in updateOptions) { + bOpts[key] = updateOptions[key] + } + } + + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].boxPlot.objs.g.remove() + } + chart.boxPlots.prepareBoxPlot(); + chart.boxPlots.update() + }; + + chart.boxPlots.reset = function () { + chart.boxPlots.change(defaultOptions) + }; + chart.boxPlots.show = function (opts) { + if (opts !== undefined) { + opts.show = true; + if (opts.reset) { + chart.boxPlots.reset() + } + } else { + opts = {show: true}; + } + chart.boxPlots.change(opts) + + }; + chart.boxPlots.hide = function (opts) { + if (opts !== undefined) { + opts.show = false; + if (opts.reset) { + chart.boxPlots.reset() + } + } else { + opts = {show: false}; + } + chart.boxPlots.change(opts) + }; + + /** + * Update the box plot obj values + */ + chart.boxPlots.update = function () { + var cName, cBoxPlot; + + for (cName in chart.groupObjs) { + cBoxPlot = chart.groupObjs[cName].boxPlot; + + // Get the box width + var objBounds = getObjWidth(bOpts.boxWidth, cName); + objBounds.middle += chart.boxPlots.options.padding + objBounds.right += chart.boxPlots.options.padding + objBounds.left += chart.boxPlots.options.padding + var width = (objBounds.right - objBounds.left); + + var sMetrics = {}; //temp var for scaled (plottable) metric values + for (var attr in chart.groupObjs[cName].metrics) { + sMetrics[attr] = null; + sMetrics[attr] = chart.yScale(chart.groupObjs[cName].metrics[attr]); + } + + // Box + if (cBoxPlot.objs.box) { + cBoxPlot.objs.box + .attr("x", objBounds.left) + .attr('width', width) + .attr("y", sMetrics.quartile3) + .attr("rx", 1) + .attr("ry", 1) + .attr("height", -sMetrics.quartile3 + sMetrics.quartile1) + } + + // Lines + var lineBounds = null; + if (bOpts.lineWidth) { + lineBounds = getObjWidth(bOpts.lineWidth, cName) + } else { + lineBounds = objBounds + } + + // Apply padding + lineBounds.middle += chart.boxPlots.options.padding + lineBounds.right += chart.boxPlots.options.padding + lineBounds.left += chart.boxPlots.options.padding + + // --Whiskers + if (cBoxPlot.objs.upperWhisker) { + cBoxPlot.objs.upperWhisker.fence + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', sMetrics.upperInnerFence) + .attr("y2", sMetrics.upperInnerFence); + cBoxPlot.objs.upperWhisker.line + .attr("x1", lineBounds.middle) + .attr("x2", lineBounds.middle) + .attr('y1', sMetrics.quartile3) + .attr("y2", sMetrics.upperInnerFence); + + cBoxPlot.objs.lowerWhisker.fence + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', sMetrics.lowerInnerFence) + .attr("y2", sMetrics.lowerInnerFence); + cBoxPlot.objs.lowerWhisker.line + .attr("x1", lineBounds.middle) + .attr("x2", lineBounds.middle) + .attr('y1', sMetrics.quartile1) + .attr("y2", sMetrics.lowerInnerFence); + } + + // --Median + if (cBoxPlot.objs.median) { + cBoxPlot.objs.median.line + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', sMetrics.median) + .attr("y2", sMetrics.median); + cBoxPlot.objs.median.circle + .attr("cx", lineBounds.middle) + .attr("cy", sMetrics.median) + } + + // --Mean + if (cBoxPlot.objs.mean) { + cBoxPlot.objs.mean.line + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', sMetrics.mean) + .attr("y2", sMetrics.mean); + cBoxPlot.objs.mean.circle + .attr("cx", lineBounds.middle) + .attr("cy", sMetrics.mean); + } + + // Outliers + + var pt; + if (cBoxPlot.objs.outliers) { + for (pt in cBoxPlot.objs.outliers) { + cBoxPlot.objs.outliers[pt].point + .attr("cx", objBounds.middle + addJitter(bOpts.scatterOutliers, width)) + .attr("cy", chart.yScale(cBoxPlot.objs.outliers[pt].value)); + } + } + if (cBoxPlot.objs.extremes) { + for (pt in cBoxPlot.objs.extremes) { + cBoxPlot.objs.extremes[pt].point + .attr("cx", objBounds.middle + addJitter(bOpts.scatterOutliers, width)) + .attr("cy", chart.yScale(cBoxPlot.objs.extremes[pt].value)); + } + } + } + }; + + /** + * Create the svg elements for the box plot + */ + chart.boxPlots.prepareBoxPlot = function () { + var cName, cBoxPlot; + + if (bOpts.colors) { + chart.boxPlots.colorFunct = getColorFunct(bOpts.colors); + } else { + chart.boxPlots.colorFunct = chart.colorFunct + } + + if (bOpts.show == false) { + return + } + + for (cName in chart.groupObjs) { + cBoxPlot = chart.groupObjs[cName].boxPlot; + + cBoxPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "box-plot"); + + //Plot Box (default show) + if (bOpts.showBox) { + cBoxPlot.objs.box = cBoxPlot.objs.g.append("rect") + .attr("class", "box") + .style("fill", chart.boxPlots.colorFunct(cName)) + .style("stroke", chart.boxPlots.colorFunct(cName)) + .on("mouseover", function () { + chart.objs.tooltip + .style("display", null) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); + }).on("mouseout", function () { + chart.objs.tooltip.style("display", "none"); + }).on("mousemove", tooltipHover(cName, chart.groupObjs[cName].metrics)); + //A stroke is added to the box with the group color, it is + // hidden by default and can be shown through css with stroke-width + } + + //Plot Median (default show) + if (bOpts.showMedian) { + cBoxPlot.objs.median = {line: null, circle: null}; + cBoxPlot.objs.median.line = cBoxPlot.objs.g.append("line") + .attr("class", "median"); + cBoxPlot.objs.median.circle = cBoxPlot.objs.g.append("circle") + .attr("class", "median") + .attr('r', bOpts.medianCSize) + .style("fill", chart.boxPlots.colorFunct(cName)); + } + + // Plot Mean (default no plot) + if (bOpts.showMean) { + cBoxPlot.objs.mean = {line: null, circle: null}; + cBoxPlot.objs.mean.line = cBoxPlot.objs.g.append("line") + .attr("class", "mean"); + cBoxPlot.objs.mean.circle = cBoxPlot.objs.g.append("circle") + .attr("class", "mean") + .attr('r', bOpts.medianCSize) + .style("fill", chart.boxPlots.colorFunct(cName)); + } + + // Plot Whiskers (default show) + if (bOpts.showWhiskers) { + cBoxPlot.objs.upperWhisker = {fence: null, line: null}; + cBoxPlot.objs.lowerWhisker = {fence: null, line: null}; + cBoxPlot.objs.upperWhisker.fence = cBoxPlot.objs.g.append("line") + .attr("class", "upper whisker") + .style("stroke", chart.boxPlots.colorFunct(cName)); + cBoxPlot.objs.upperWhisker.line = cBoxPlot.objs.g.append("line") + .attr("class", "upper whisker") + .style("stroke", chart.boxPlots.colorFunct(cName)); + + cBoxPlot.objs.lowerWhisker.fence = cBoxPlot.objs.g.append("line") + .attr("class", "lower whisker") + .style("stroke", chart.boxPlots.colorFunct(cName)); + cBoxPlot.objs.lowerWhisker.line = cBoxPlot.objs.g.append("line") + .attr("class", "lower whisker") + .style("stroke", chart.boxPlots.colorFunct(cName)); + } + + // Plot outliers (default show) + if (bOpts.showOutliers) { + if (!cBoxPlot.objs.outliers) calcAllOutliers(); + var pt; + if (cBoxPlot.objs.outliers.length) { + var outDiv = cBoxPlot.objs.g.append("g").attr("class", "boxplot outliers"); + for (pt in cBoxPlot.objs.outliers) { + cBoxPlot.objs.outliers[pt].point = outDiv.append("circle") + .attr("class", "outlier") + .attr('r', bOpts.outlierCSize) + .style("fill", chart.boxPlots.colorFunct(cName)); + } + } + + if (cBoxPlot.objs.extremes.length) { + var extDiv = cBoxPlot.objs.g.append("g").attr("class", "boxplot extremes"); + for (pt in cBoxPlot.objs.extremes) { + cBoxPlot.objs.extremes[pt].point = extDiv.append("circle") + .attr("class", "extreme") + .attr('r', bOpts.outlierCSize) + .style("stroke", chart.boxPlots.colorFunct(cName)); + } + } + } + + + } + }; + chart.boxPlots.prepareBoxPlot(); + + d3.select(window).on('resize.' + chart.selector + '.boxPlot', chart.boxPlots.update); + chart.boxPlots.update(); + return chart; + + }; + + /** + * Render a notched box on the current chart + * @param options + * @param [options.show=true] Toggle the whole plot on and off + * @param [options.showNotchBox=true] Show the notch box + * @param [options.showLines=false] Show lines at the confidence intervals + * @param [options.boxWidth=35] The width of the widest part of the box + * @param [options.medianWidth=20] The width of the narrowist part of the box + * @param [options.lineWidth=50] The width of the confidence interval lines + * @param [options.notchStyle=null] null=traditional style, 'box' cuts out the whole notch in right angles + * @param [options.colors=chart default] The color mapping for the notch boxes + * @returns {*} The chart object + */ + chart.renderNotchBoxes = function (options) { + chart.notchBoxes = {}; + + //Defaults + var defaultOptions = { + show: true, + showNotchBox: true, + showLines: false, + boxWidth: 35, + medianWidth: 20, + lineWidth: 50, + notchStyle: null, + colors: null + }; + chart.notchBoxes.options = shallowCopy(defaultOptions); + for (var option in options) { + chart.notchBoxes.options[option] = options[option] + } + var nOpts = chart.notchBoxes.options; + + //Create notch objects + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].notchBox = {}; + chart.groupObjs[cName].notchBox.objs = {}; + } + + /** + * Makes the svg path string for a notched box + * @param cNotch Current notch box object + * @param notchBounds objBound object + * @returns {string} A string in the proper format for a svg polygon + */ + function makeNotchBox(cNotch, notchBounds) { + var scaledValues = []; + if (nOpts.notchStyle == 'box') { + scaledValues = [ + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile1)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.medianLeft, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.medianLeft, chart.yScale(cNotch.metrics.median)], + [notchBounds.medianLeft, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile3)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile3)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.medianRight, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.medianRight, chart.yScale(cNotch.metrics.median)], + [notchBounds.medianRight, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile1)] + ]; + } else { + scaledValues = [ + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile1)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.medianLeft, chart.yScale(cNotch.metrics.median)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile3)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile3)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.upperNotch)], + [notchBounds.medianRight, chart.yScale(cNotch.metrics.median)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.lowerNotch)], + [notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile1)] + ]; + } + return scaledValues.map(function (d) { + return [d[0], d[1]].join(","); + }).join(" "); + } + + /** + * Calculate the confidence intervals + */ + !function calcNotches() { + var cNotch, modifier; + for (var cName in chart.groupObjs) { + cNotch = chart.groupObjs[cName]; + modifier = (1.57 * (cNotch.metrics.iqr / Math.sqrt(cNotch.values.length))); + cNotch.metrics.upperNotch = cNotch.metrics.median + modifier; + cNotch.metrics.lowerNotch = cNotch.metrics.median - modifier; + } + }(); + + /** + * Take a new set of options and redraw the notch boxes + * @param updateOptions + */ + chart.notchBoxes.change = function (updateOptions) { + if (updateOptions) { + for (var key in updateOptions) { + nOpts[key] = updateOptions[key] + } + } + + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].notchBox.objs.g.remove() + } + chart.notchBoxes.prepareNotchBoxes(); + chart.notchBoxes.update(); + }; + + chart.notchBoxes.reset = function () { + chart.notchBoxes.change(defaultOptions) + }; + chart.notchBoxes.show = function (opts) { + if (opts !== undefined) { + opts.show = true; + if (opts.reset) { + chart.notchBoxes.reset() + } + } else { + opts = {show: true}; + } + chart.notchBoxes.change(opts) + }; + chart.notchBoxes.hide = function (opts) { + if (opts !== undefined) { + opts.show = false; + if (opts.reset) { + chart.notchBoxes.reset() + } + } else { + opts = {show: false}; + } + chart.notchBoxes.change(opts) + }; + + /** + * Update the notch box obj values + */ + chart.notchBoxes.update = function () { + var cName, cGroup; + + for (cName in chart.groupObjs) { + cGroup = chart.groupObjs[cName]; + + // Get the box size + var boxBounds = getObjWidth(nOpts.boxWidth, cName); + var medianBounds = getObjWidth(nOpts.medianWidth, cName); + + var notchBounds = { + boxLeft: boxBounds.left, + boxRight: boxBounds.right, + middle: boxBounds.middle, + medianLeft: medianBounds.left, + medianRight: medianBounds.right + }; + + // Notch Box + if (cGroup.notchBox.objs.notch) { + cGroup.notchBox.objs.notch + .attr("points", makeNotchBox(cGroup, notchBounds)); + } + if (cGroup.notchBox.objs.upperLine) { + var lineBounds = null; + if (nOpts.lineWidth) { + lineBounds = getObjWidth(nOpts.lineWidth, cName) + } else { + lineBounds = objBounds + } + + var confidenceLines = { + upper: chart.yScale(cGroup.metrics.upperNotch), + lower: chart.yScale(cGroup.metrics.lowerNotch) + }; + cGroup.notchBox.objs.upperLine + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', confidenceLines.upper) + .attr("y2", confidenceLines.upper); + cGroup.notchBox.objs.lowerLine + .attr("x1", lineBounds.left) + .attr("x2", lineBounds.right) + .attr('y1', confidenceLines.lower) + .attr("y2", confidenceLines.lower); + } + } + }; + + /** + * Create the svg elements for the notch boxes + */ + chart.notchBoxes.prepareNotchBoxes = function () { + var cName, cNotch; + + if (nOpts && nOpts.colors) { + chart.notchBoxes.colorFunct = getColorFunct(nOpts.colors); + } else { + chart.notchBoxes.colorFunct = chart.colorFunct + } + + if (nOpts.show == false) { + return + } + + for (cName in chart.groupObjs) { + cNotch = chart.groupObjs[cName].notchBox; + + cNotch.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "notch-plot"); + + // Plot Box (default show) + if (nOpts.showNotchBox) { + cNotch.objs.notch = cNotch.objs.g.append("polygon") + .attr("class", "notch") + .style("fill", chart.notchBoxes.colorFunct(cName)) + .style("stroke", chart.notchBoxes.colorFunct(cName)); + //A stroke is added to the notch with the group color, it is + // hidden by default and can be shown through css with stroke-width + } + + //Plot Confidence Lines (default hide) + if (nOpts.showLines) { + cNotch.objs.upperLine = cNotch.objs.g.append("line") + .attr("class", "upper confidence line") + .style("stroke", chart.notchBoxes.colorFunct(cName)); + + cNotch.objs.lowerLine = cNotch.objs.g.append("line") + .attr("class", "lower confidence line") + .style("stroke", chart.notchBoxes.colorFunct(cName)); + } + } + }; + chart.notchBoxes.prepareNotchBoxes(); + + d3.select(window).on('resize.' + chart.selector + '.notchBox', chart.notchBoxes.update); + chart.notchBoxes.update(); + return chart; + }; + + /** + * Render a raw data in various forms + * @param options + * @param [options.show=true] Toggle the whole plot on and off + * @param [options.showPlot=false] True or false, show points + * @param [options.plotType='none'] Options: no scatter = (false or 'none'); scatter points= (true or [amount=% of width (default=10)]); beeswarm points = ('beeswarm') + * @param [options.pointSize=6] Diameter of the circle in pizels (not the radius) + * @param [options.showLines=['median']] Can equal any of the metrics lines + * @param [options.showbeanLines=false] Options: no lines = false + * @param [options.beanWidth=20] % width + * @param [options.colors=chart default] + * @returns {*} The chart object + * + */ + chart.renderDataPlots = function (options) { + chart.dataPlots = {}; + + + //Defaults + var defaultOptions = { + show: true, + showPlot: false, + plotType: 'none', + pointSize: 6, + showLines: false,//['median'], + showBeanLines: false, + beanWidth: 20, + colors: null, + padding: 0 + }; + chart.dataPlots.options = shallowCopy(defaultOptions); + for (var option in options) { + chart.dataPlots.options[option] = options[option] + } + var dOpts = chart.dataPlots.options; + + //Create notch objects + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].dataPlots = {}; + chart.groupObjs[cName].dataPlots.objs = {}; + } + // The lines don't fit into a group bucket so they live under the dataPlot object + chart.dataPlots.objs = {}; + + /** + * Take updated options and redraw the data plots + * @param updateOptions + */ + chart.dataPlots.change = function (updateOptions) { + if (updateOptions) { + for (var key in updateOptions) { + dOpts[key] = updateOptions[key] + } + } + + chart.dataPlots.objs.g.remove(); + for (var cName in chart.groupObjs) { + chart.groupObjs[cName].dataPlots.objs.g.remove() + } + chart.dataPlots.preparePlots(); + chart.dataPlots.update() + }; + + chart.dataPlots.reset = function () { + chart.dataPlots.change(defaultOptions) + }; + chart.dataPlots.show = function (opts) { + if (opts !== undefined) { + opts.show = true; + if (opts.reset) { + chart.dataPlots.reset() + } + } else { + opts = {show: true}; + } + chart.dataPlots.change(opts) + }; + chart.dataPlots.hide = function (opts) { + if (opts !== undefined) { + opts.show = false; + if (opts.reset) { + chart.dataPlots.reset() + } + } else { + opts = {show: false}; + } + chart.dataPlots.change(opts) + }; + + /** + * Update the data plot obj values + */ + chart.dataPlots.update = function () { + var cName, cGroup, cPlot; + + // Metrics lines + if (chart.dataPlots.objs.g) { + var halfBand = chart.xScale.rangeBand() / 2; // find the middle of each band + for (var cMetric in chart.dataPlots.objs.lines) { + chart.dataPlots.objs.lines[cMetric].line + .x(function (d) { + return chart.xScale(d.x) + halfBand + }); + chart.dataPlots.objs.lines[cMetric].g + .datum(chart.dataPlots.objs.lines[cMetric].values) + .attr('d', chart.dataPlots.objs.lines[cMetric].line); + } + } + + + for (cName in chart.groupObjs) { + cGroup = chart.groupObjs[cName]; + cPlot = cGroup.dataPlots; + + if (cPlot.objs.points) { + if (dOpts.plotType == 'beeswarm') { + var swarmBounds = getObjWidth(100, cName); + var yPtScale = chart.yScale.copy() + .range([Math.floor(chart.yScale.range()[0] / dOpts.pointSize), 0]) + .interpolate(d3.interpolateRound) + .domain(chart.yScale.domain()); + var maxWidth = Math.floor(chart.xScale.rangeBand() / dOpts.pointSize); + var ptsObj = {}; + var cYBucket = null; + // Bucket points + for (var pt = 0; pt < cGroup.values.length; pt++) { + cYBucket = yPtScale(cGroup.values[pt]); + if (ptsObj.hasOwnProperty(cYBucket) !== true) { + ptsObj[cYBucket] = []; + } + ptsObj[cYBucket].push(cPlot.objs.points.pts[pt] + .attr("cx", swarmBounds.middle) + .attr("cy", yPtScale(cGroup.values[pt]) * dOpts.pointSize)); + } + // Plot buckets + var rightMax = Math.min(swarmBounds.right - dOpts.pointSize); + for (var row in ptsObj) { + var leftMin = swarmBounds.left + (Math.max((maxWidth - ptsObj[row].length) / 2, 0) * dOpts.pointSize); + var col = 0; + for (pt in ptsObj[row]) { + ptsObj[row][pt].attr("cx", Math.min(leftMin + col * dOpts.pointSize, rightMax) + dOpts.pointSize / 2); + col++ + } + } + } else { // For scatter points and points with no scatter + var plotBounds = null, + scatterWidth = 0, + width = 0; + if (dOpts.plotType == 'scatter' || typeof dOpts.plotType == 'number') { + //Default scatter percentage is 20% of box width + scatterWidth = typeof dOpts.plotType == 'number' ? dOpts.plotType : 20; + } + + plotBounds = getObjWidth(scatterWidth, cName); + plotBounds.middle += chart.dataPlots.options.padding + plotBounds.right += chart.dataPlots.options.padding + plotBounds.left += chart.dataPlots.options.padding + width = plotBounds.right - plotBounds.left; + + for (var pt = 0; pt < cGroup.values.length; pt++) { + cPlot.objs.points.pts[pt] + .attr("cx", plotBounds.middle + addJitter(true, width)) + .attr("cy", chart.yScale(cGroup.values[pt])); + } + } + } + + + if (cPlot.objs.bean) { + var beanBounds = getObjWidth(dOpts.beanWidth, cName); + for (var pt = 0; pt < cGroup.values.length; pt++) { + cPlot.objs.bean.lines[pt] + .attr("x1", beanBounds.left) + .attr("x2", beanBounds.right) + .attr('y1', chart.yScale(cGroup.values[pt])) + .attr("y2", chart.yScale(cGroup.values[pt])); + } + } + } + }; + + /** + * Create the svg elements for the data plots + */ + chart.dataPlots.preparePlots = function () { + var cName, cPlot; + + if (dOpts && dOpts.colors) { + chart.dataPlots.colorFunct = getColorFunct(dOpts.colors); + } else { + chart.dataPlots.colorFunct = chart.colorFunct + } + + if (dOpts.show == false) { + return + } + + // Metrics lines + chart.dataPlots.objs.g = chart.objs.g.append("g").attr("class", "metrics-lines"); + if (dOpts.showLines && dOpts.showLines.length > 0) { + chart.dataPlots.objs.lines = {}; + var cMetric; + for (var line in dOpts.showLines) { + cMetric = dOpts.showLines[line]; + chart.dataPlots.objs.lines[cMetric] = {}; + chart.dataPlots.objs.lines[cMetric].values = []; + for (var cGroup in chart.groupObjs) { + chart.dataPlots.objs.lines[cMetric].values.push({ + x: cGroup, + y: chart.groupObjs[cGroup].metrics[cMetric] + }) + } + chart.dataPlots.objs.lines[cMetric].line = d3.svg.line() + .interpolate("cardinal") + .y(function (d) { + return chart.yScale(d.y) + }); + chart.dataPlots.objs.lines[cMetric].g = chart.dataPlots.objs.g.append("path") + .attr("class", "line " + cMetric) + .attr("data-metric", cMetric) + .style("fill", 'none') + .style("stroke", chart.colorFunct(cMetric)); + } + + } + + + for (cName in chart.groupObjs) { + + cPlot = chart.groupObjs[cName].dataPlots; + cPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "data-plot"); + + // Points Plot + if (dOpts.showPlot) { + cPlot.objs.points = {g: null, pts: []}; + cPlot.objs.points.g = cPlot.objs.g.append("g").attr("class", "points-plot"); + for (var pt = 0; pt < chart.groupObjs[cName].values.length; pt++) { + cPlot.objs.points.pts.push(cPlot.objs.points.g + .append("a") + .attr("xlink:href", function(d) { + if (chart.settings["qctype"].startsWith('anat')) { + return chart.groupObjs[cName].labels[pt] + "_T1w.html" + } else if (chart.settings["qctype"].startsWith('func')) { + return chart.groupObjs[cName].labels[pt] + "_bold.html" + } + + }) + .append("circle") + .attr("class", "point") + .attr('r', dOpts.pointSize / 2)// Options is diameter, r takes radius so divide by 2 + .style("fill", chart.dataPlots.colorFunct(cName)) + .on("mouseover", function() { + chart.objs.tooltip + .style("display", null) + .style("left", (d3.event.pageX) + "px") + .style("top", (d3.event.pageY - 28) + "px"); + }) + .on("mouseout", function () { + chart.objs.tooltip.style("display", "none"); + }) + .on("mousemove", pointHover(chart.groupObjs[cName].labels[pt], chart.groupObjs[cName].values[pt])) + ); + } + } + + + // Bean lines + if (dOpts.showBeanLines) { + cPlot.objs.bean = {g: null, lines: []}; + cPlot.objs.bean.g = cPlot.objs.g.append("g").attr("class", "bean-plot"); + for (var pt = 0; pt < chart.groupObjs[cName].values.length; pt++) { + cPlot.objs.bean.lines.push(cPlot.objs.bean.g.append("line") + .attr("class", "bean line") + .style("stroke-width", '1') + .style("stroke", chart.dataPlots.colorFunct(cName))); + } + } + } + + }; + chart.dataPlots.preparePlots(); + + d3.select(window).on('resize.' + chart.selector + '.dataPlot', chart.dataPlots.update); + chart.dataPlots.update(); + return chart; + }; + + return chart; +} diff --git a/pydra/tasks/mriqc/data/reports/resources/d3.min.js b/pydra/tasks/mriqc/data/reports/resources/d3.min.js new file mode 100644 index 0000000..1664873 --- /dev/null +++ b/pydra/tasks/mriqc/data/reports/resources/d3.min.js @@ -0,0 +1,5 @@ +!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function i(n){return!isNaN(n)}function u(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)<0?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n(t[u],e)>0?i=u:r=u+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function f(n){return(n+="")===bo||n[0]===_o?_o+n:n}function s(n){return(n+="")[0]===_o?n.slice(1):n}function h(n){return f(n)in this._}function p(n){return(n=f(n))in this._&&delete this._[n]}function g(){var n=[];for(var t in this._)n.push(s(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function y(){this._=Object.create(null)}function m(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=wo.length;r>e;++e){var i=wo[e]+t;if(i in n)return i}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,i=-1,u=r.length;++ie;e++)for(var i,u=n[e],o=0,a=u.length;a>o;o++)(i=u[o])&&t(i,o,e);return n}function Z(n){return ko(n,qo),n}function V(n){var t,e;return function(r,i,u){var o,a=n[u].update,l=a.length;for(u!=e&&(e=u,t=0),i>=t&&(t=i+1);!(o=a[t])&&++t0&&(n=n.slice(0,a));var c=To.get(n);return c&&(n=c,l=B),a?t?i:r:t?b:u}function $(n,t){return function(e){var r=ao.event;ao.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{ao.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Do,i="click"+r,u=ao.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==Ro&&(Ro="onselectstart"in e?!1:x(e.style,"userSelect")),Ro){var o=n(e).style,a=o[Ro];o[Ro]="none"}return function(n){if(u.on(r,null),Ro&&(o[Ro]=a),n){var t=function(){u.on(i,null)};u.on(i,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var i=r.createSVGPoint();if(0>Po){var u=t(n);if(u.scrollX||u.scrollY){r=ao.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var o=r[0][0].getScreenCTM();Po=!(o.f||o.e),r.remove()}}return Po?(i.x=e.pageX,i.y=e.pageY):(i.x=e.clientX,i.y=e.clientY),i=i.matrixTransform(n.getScreenCTM().inverse()),[i.x,i.y]}var a=n.getBoundingClientRect();return[e.clientX-a.left-n.clientLeft,e.clientY-a.top-n.clientTop]}function G(){return ao.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?Fo:Math.acos(n)}function tn(n){return n>1?Io:-1>n?-Io:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function on(n){return(n=Math.sin(n/2))*n}function an(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?u+(o-u)*n/60:180>n?o:240>n?u+(o-u)*(240-n)/60:u}function i(n){return Math.round(255*r(n))}var u,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,u=2*e-o,new mn(i(n+120),i(n),i(n-120))}function fn(n,t,e){return this instanceof fn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof fn?new fn(n.h,n.c,n.l):n instanceof hn?gn(n.l,n.a,n.b):gn((n=Sn((n=ao.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new fn(n,t,e)}function sn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Yo)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof fn?sn(n.h,n.c,n.l):Sn((n=mn(n)).r,n.g,n.b):new hn(n,t,e)}function pn(n,t,e){var r=(n+16)/116,i=r+t/500,u=r-e/200;return i=vn(i)*na,r=vn(r)*ta,u=vn(u)*ea,new mn(yn(3.2404542*i-1.5371385*r-.4985314*u),yn(-.969266*i+1.8760108*r+.041556*u),yn(.0556434*i-.2040259*r+1.0572252*u))}function gn(n,t,e){return n>0?new fn(Math.atan2(e,t)*Zo,Math.sqrt(t*t+e*e),n):new fn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function yn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function mn(n,t,e){return this instanceof mn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof mn?new mn(n.r,n.g,n.b):_n(""+n,mn,cn):new mn(n,t,e)}function Mn(n){return new mn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,i,u,o=0,a=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(i=r[2].split(","),r[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(Nn(i[0]),Nn(i[1]),Nn(i[2]))}return(u=ua.get(n))?t(u.r,u.g,u.b):(null==n||"#"!==n.charAt(0)||isNaN(u=parseInt(n.slice(1),16))||(4===n.length?(o=(3840&u)>>4,o=o>>4|o,a=240&u,a=a>>4|a,l=15&u,l=l<<4|l):7===n.length&&(o=(16711680&u)>>16,a=(65280&u)>>8,l=255&u)),t(o,a,l))}function wn(n,t,e){var r,i,u=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-u,l=(o+u)/2;return a?(i=.5>l?a/(o+u):a/(2-o-u),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=NaN,i=l>0&&1>l?0:r),new ln(r,i,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/na),i=dn((.2126729*n+.7151522*t+.072175*e)/ta),u=dn((.0193339*n+.119192*t+.9503041*e)/ea);return hn(116*i-16,500*(r-i),200*(i-u))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function i(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(u,l)}catch(r){return void o.error.call(u,r)}o.load.call(u,n)}else o.error.call(u,l)}var u={},o=ao.dispatch("beforesend","progress","load","error"),a={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=i:l.onreadystatechange=function(){l.readyState>3&&i()},l.onprogress=function(n){var t=ao.event;ao.event=n;try{o.progress.call(u,l)}finally{ao.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.responseType=function(n){return arguments.length?(c=n,u):c},u.response=function(n){return e=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(co(arguments)))}}),u.send=function(e,r,i){if(2===arguments.length&&"function"==typeof r&&(i=r,r=null),l.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),l.setRequestHeader)for(var f in a)l.setRequestHeader(f,a[f]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),o.beforesend.call(u,l),l.send(null==r?null:r),u},u.abort=function(){return l.abort(),u},ao.rebind(u,o,"on"),null==r?u:u.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var i=e+t,u={c:n,t:i,n:null};return aa?aa.n=u:oa=u,aa=u,la||(ca=clearTimeout(ca),la=1,fa(Tn)),u}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(ca),ca=setTimeout(Tn,t)),la=0):(la=1,fa(Tn))}function Rn(){for(var n=Date.now(),t=oa;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=oa,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,i=n.currency,u=r&&e?function(n,t){for(var i=n.length,u=[],o=0,a=r[0],l=0;i>0&&a>0&&(l+a+1>t&&(a=Math.max(1,t-l)),u.push(n.substring(i-=a,i+a)),!((l+=a+1)>t));)a=r[o=(o+1)%r.length];return u.reverse().join(e)}:m;return function(n){var e=ha.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"-",l=e[4]||"",c=e[5],f=+e[6],s=e[7],h=e[8],p=e[9],g=1,v="",d="",y=!1,m=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===o)&&(c=r="0",o="="),p){case"n":s=!0,p="g";break;case"%":g=100,d="%",p="f";break;case"p":g=100,d="%",p="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+p.toLowerCase());case"c":m=!1;case"d":y=!0,h=0;break;case"s":g=-1,p="r"}"$"===l&&(v=i[0],d=i[1]),"r"!=p||h||(p="g"),null!=h&&("g"==p?h=Math.max(1,Math.min(21,h)):"e"!=p&&"f"!=p||(h=Math.max(0,Math.min(20,h)))),p=pa.get(p)||Fn;var M=c&&s;return function(n){var e=d;if(y&&n%1)return"";var i=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===a?"":a;if(0>g){var l=ao.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=g;n=p(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=m?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&s&&(x=u(x,1/0));var S=v.length+x.length+b.length+(M?0:i.length),k=f>S?new Array(S=f-S+1).join(r):"";return M&&(x=u(k+x,k.length?f-b.length:1/0)),i+=v,n=x+b,("<"===o?i+n+k:">"===o?k+i+n:"^"===o?k.substring(0,S>>=1)+i+n+k.substring(S):i+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=u(e,1);return r-t>t-e?e:r}function i(e){return t(e=n(new va(e-1)),1),e}function u(n,e){return t(n=new va(+n),e),n}function o(n,r,u){var o=i(n),a=[];if(u>1)for(;r>o;)e(o)%u||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{va=Hn;var r=new Hn;return r._=n,o(r,t,e)}finally{va=Date}}n.floor=n,n.round=r,n.ceil=i,n.offset=u,n.range=o;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(i),l.offset=In(u),l.range=a,n}function In(n){return function(t,e){try{va=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{va=Date}}}function Yn(n){function t(n){function t(t){for(var e,i,u,o=[],a=-1,l=0;++aa;){if(r>=c)return-1;if(i=t.charCodeAt(a++),37===i){if(o=t.charAt(a++),u=C[o in ya?t.charAt(a++):o],!u||(r=u(n,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function f(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var s=n.dateTime,h=n.date,p=n.time,g=n.periods,v=n.days,d=n.shortDays,y=n.months,m=n.shortMonths;t.utc=function(n){function e(n){try{va=Hn;var t=new va;return t._=n,r(t)}finally{va=Date}}var r=t(n);return e.parse=function(n){try{va=Hn;var t=r.parse(n);return t&&t._}finally{va=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=ao.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(y),k=Xn(y),N=Vn(m),E=Xn(m);g.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return m[n.getMonth()]},B:function(n){return y[n.getMonth()]},c:t(s),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ga.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return g[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ga.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ga.mondayOfYear(n),t,2)},x:t(h),X:t(p),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:at,"%":function(){return"%"}},C={a:r,A:i,b:u,B:o,c:a,d:tt,e:tt,H:rt,I:rt,j:et,L:ot,m:nt,M:it,p:f,S:ut,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",i=(r?-n:n)+"",u=i.length;return r+(e>u?new Array(e-u+1).join(t)+i:i)}function Vn(n){return new RegExp("^(?:"+n.map(ao.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function it(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function ut(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ot(n,t,e){ma.lastIndex=0;var r=ma.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function at(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=xo(t)/60|0,i=xo(t)%60;return e+Zn(r,"0",2)+Zn(i,"0",2)}function lt(n,t,e){Ma.lastIndex=0;var r=Ma.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,a=o*e,l=Math.cos(t),c=Math.sin(t),f=u*c,s=i*l+f*Math.cos(a),h=f*o*Math.sin(a);ka.add(Math.atan2(h,s)),r=n,i=l,u=c}var t,e,r,i,u;Na.point=function(o,a){Na.point=n,r=(t=o)*Yo,i=Math.cos(a=(e=a)*Yo/2+Fo/4),u=Math.sin(a)},Na.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function yt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function mt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return xo(n[0]-t[0])a;++a)i.point((e=n[a])[0],e[1]);return void i.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,u.push(l),o.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,u.push(l),o.push(c)}}),o.sort(t),qt(u),qt(o),u.length){for(var a=0,l=e,c=o.length;c>a;++a)o[a].e=l=!l;for(var f,s,h=u[0];;){for(var p=h,g=!0;p.v;)if((p=p.n)===h)return;f=p.z,i.lineStart();do{if(p.v=p.o.v=!0,p.e){if(g)for(var a=0,c=f.length;c>a;++a)i.point((s=f[a])[0],s[1]);else r(p.x,p.n.x,1,i);p=p.n}else{if(g){f=p.p.z;for(var a=f.length-1;a>=0;--a)i.point((s=f[a])[0],s[1])}else r(p.x,p.p.x,-1,i);p=p.p}p=p.o,f=p.z,g=!g}while(!p.v);i.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,i=n[0];++r0){for(b||(u.polygonStart(),b=!0),u.lineStart();++o1&&2&t&&e.push(e.pop().concat(e.shift())),p.push(e.filter(Dt))}var p,g,v,d=t(u),y=i.invert(r[0],r[1]),m={point:o,lineStart:l,lineEnd:c,polygonStart:function(){m.point=f,m.lineStart=s,m.lineEnd=h,p=[],g=[]},polygonEnd:function(){m.point=o,m.lineStart=l,m.lineEnd=c,p=ao.merge(p);var n=Ot(y,g);p.length?(b||(u.polygonStart(),b=!0),Lt(p,Ut,n,e,u)):n&&(b||(u.polygonStart(),b=!0),u.lineStart(),e(null,null,1,u),u.lineEnd()),b&&(u.polygonEnd(),b=!1),p=g=null},sphere:function(){u.polygonStart(),u.lineStart(),e(null,null,1,u),u.lineEnd(),u.polygonEnd()}},M=Pt(),x=t(M),b=!1;return m}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Io-Uo:Io-n[1])-((t=t.x)[0]<0?t[1]-Io-Uo:Io-t[1])}function jt(n){var t,e=NaN,r=NaN,i=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(u,o){var a=u>0?Fo:-Fo,l=xo(u-e);xo(l-Fo)0?Io:-Io),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(u,r),t=0):i!==a&&l>=Fo&&(xo(e-i)Uo?Math.atan((Math.sin(t)*(u=Math.cos(r))*Math.sin(e)-Math.sin(r)*(i=Math.cos(t))*Math.sin(n))/(i*u*o)):(t+r)/2}function Ht(n,t,e,r){var i;if(null==n)i=e*Io,r.point(-Fo,i),r.point(0,i),r.point(Fo,i),r.point(Fo,0),r.point(Fo,-i),r.point(0,-i),r.point(-Fo,-i),r.point(-Fo,0),r.point(-Fo,i);else if(xo(n[0]-t[0])>Uo){var u=n[0]a;++a){var c=t[a],f=c.length;if(f)for(var s=c[0],h=s[0],p=s[1]/2+Fo/4,g=Math.sin(p),v=Math.cos(p),d=1;;){d===f&&(d=0),n=c[d];var y=n[0],m=n[1]/2+Fo/4,M=Math.sin(m),x=Math.cos(m),b=y-h,_=b>=0?1:-1,w=_*b,S=w>Fo,k=g*M;if(ka.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),u+=S?b+_*Ho:b,S^h>=e^y>=e){var N=mt(dt(s),dt(n));bt(N);var E=mt(i,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(o+=S^b>=0?1:-1)}if(!d++)break;h=y,g=M,v=x,s=n}}return(-Uo>u||Uo>u&&-Uo>ka)^1&o}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>u}function e(n){var e,u,l,c,f;return{lineStart:function(){c=l=!1,f=1},point:function(s,h){var p,g=[s,h],v=t(s,h),d=o?v?0:i(s,h):v?i(s+(0>s?Fo:-Fo),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(p=r(e,g),(wt(e,p)||wt(g,p))&&(g[0]+=Uo,g[1]+=Uo,v=t(g[0],g[1]))),v!==l)f=0,v?(n.lineStart(),p=r(g,e),n.point(p[0],p[1])):(p=r(e,g),n.point(p[0],p[1]),n.lineEnd()),e=p;else if(a&&e&&o^v){var y;d&u||!(y=r(g,e,!0))||(f=0,o?(n.lineStart(),n.point(y[0][0],y[0][1]),n.point(y[1][0],y[1][1]),n.lineEnd()):(n.point(y[1][0],y[1][1]),n.lineEnd(),n.lineStart(),n.point(y[0][0],y[0][1])))}!v||e&&wt(e,g)||n.point(g[0],g[1]),e=g,l=v,u=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return f|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),i=dt(t),o=[1,0,0],a=mt(r,i),l=yt(a,a),c=a[0],f=l-c*c;if(!f)return!e&&n;var s=u*l/f,h=-u*c/f,p=mt(o,a),g=xt(o,s),v=xt(a,h);Mt(g,v);var d=p,y=yt(g,d),m=yt(d,d),M=y*y-m*(yt(g,g)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-y-x)/m);if(Mt(b,g),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=xo(E-Fo)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(xo(b[0]-w)Fo^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-y+x)/m);return Mt(z,g),[b,_t(z)]}}}function i(t,e){var r=o?n:Fo-n,i=0;return-r>t?i|=1:t>r&&(i|=2),-r>e?i|=4:e>r&&(i|=8),i}var u=Math.cos(n),o=u>0,a=xo(u)>Uo,l=ve(n,6*Yo);return Rt(t,e,l,o?[0,-n]:[-Fo,n-Fo])}function Yt(n,t,e,r){return function(i){var u,o=i.a,a=i.b,l=o.x,c=o.y,f=a.x,s=a.y,h=0,p=1,g=f-l,v=s-c;if(u=n-l,g||!(u>0)){if(u/=g,0>g){if(h>u)return;p>u&&(p=u)}else if(g>0){if(u>p)return;u>h&&(h=u)}if(u=e-l,g||!(0>u)){if(u/=g,0>g){if(u>p)return;u>h&&(h=u)}else if(g>0){if(h>u)return;p>u&&(p=u)}if(u=t-c,v||!(u>0)){if(u/=v,0>v){if(h>u)return;p>u&&(p=u)}else if(v>0){if(u>p)return;u>h&&(h=u)}if(u=r-c,v||!(0>u)){if(u/=v,0>v){if(u>p)return;u>h&&(h=u)}else if(v>0){if(h>u)return;p>u&&(p=u)}return h>0&&(i.a={x:l+h*g,y:c+h*v}),1>p&&(i.b={x:l+p*g,y:c+p*v}),i}}}}}}function Zt(n,t,e,r){function i(r,i){return xo(r[0]-n)0?0:3:xo(r[0]-e)0?2:1:xo(r[1]-t)0?1:0:i>0?3:2}function u(n,t){return o(n.x,t.x)}function o(n,t){var e=i(n,1),r=i(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function l(n){for(var t=0,e=d.length,r=n[1],i=0;e>i;++i)for(var u,o=1,a=d[i],l=a.length,c=a[0];l>o;++o)u=a[o],c[1]<=r?u[1]>r&&Q(c,u,n)>0&&++t:u[1]<=r&&Q(c,u,n)<0&&--t,c=u;return 0!==t}function c(u,a,l,c){var f=0,s=0;if(null==u||(f=i(u,l))!==(s=i(a,l))||o(u,a)<0^l>0){do c.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+l+4)%4)!==s)}else c.point(a[0],a[1])}function f(i,u){return i>=n&&e>=i&&u>=t&&r>=u}function s(n,t){f(n,t)&&a.point(n,t)}function h(){C.point=g,d&&d.push(y=[]),S=!0,w=!1,b=_=NaN}function p(){v&&(g(m,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=s,w&&a.lineEnd()}function g(n,t){n=Math.max(-Ha,Math.min(Ha,n)),t=Math.max(-Ha,Math.min(Ha,t));var e=f(n,t);if(d&&y.push([n,t]),S)m=n,M=t,x=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,y,m,M,x,b,_,w,S,k,N=a,E=Pt(),A=Yt(n,t,e,r),C={point:s,lineStart:h,lineEnd:p,polygonStart:function(){a=E,v=[],d=[],k=!0},polygonEnd:function(){a=N,v=ao.merge(v);var t=l([n,r]),e=k&&t,i=v.length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),c(null,null,1,a),a.lineEnd()),i&&Lt(v,u,t,c,a),a.polygonEnd()),v=d=y=null}};return C}}function Vt(n){var t=0,e=Fo/3,r=ae(n),i=r(t,e);return i.parallels=function(n){return arguments.length?r(t=n[0]*Fo/180,e=n[1]*Fo/180):[t/Fo*180,e/Fo*180]},i}function Xt(n,t){function e(n,t){var e=Math.sqrt(u-2*i*Math.sin(t))/i;return[e*Math.sin(n*=i),o-e*Math.cos(n)]}var r=Math.sin(n),i=(r+Math.sin(t))/2,u=1+r*(2*i-r),o=Math.sqrt(u)/i;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/i,tn((u-(n*n+e*e)*i*i)/(2*i))]},e}function $t(){function n(n,t){Ia+=i*n-r*t,r=n,i=t}var t,e,r,i;$a.point=function(u,o){$a.point=n,t=r=u,e=i=o},$a.lineEnd=function(){n(t,e)}}function Bt(n,t){Ya>n&&(Ya=n),n>Va&&(Va=n),Za>t&&(Za=t),t>Xa&&(Xa=t)}function Wt(){function n(n,t){o.push("M",n,",",t,u)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function i(){o.push("Z")}var u=Jt(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return u=Jt(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ca+=n,za+=t,++La}function Kt(){function n(n,r){var i=n-t,u=r-e,o=Math.sqrt(i*i+u*u);qa+=o*(t+n)/2,Ta+=o*(e+r)/2,Ra+=o,Gt(t=n,e=r)}var t,e;Wa.point=function(r,i){Wa.point=n,Gt(t=r,e=i)}}function Qt(){Wa.point=Gt}function ne(){function n(n,t){var e=n-r,u=t-i,o=Math.sqrt(e*e+u*u);qa+=o*(r+n)/2,Ta+=o*(i+t)/2,Ra+=o,o=i*n-r*t,Da+=o*(r+n),Pa+=o*(i+t),Ua+=3*o,Gt(r=n,i=t)}var t,e,r,i;Wa.point=function(u,o){Wa.point=n,Gt(t=r=u,e=i=o)},Wa.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+o,e),n.arc(t,e,o,0,Ho)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function i(){a.point=t}function u(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:i,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=i,a.point=t},pointRadius:function(n){return o=n,a},result:b};return a}function ee(n){function t(n){return(a?r:e)(n)}function e(t){return ue(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=u,t.lineStart()}function u(e,r){var u=dt([e,r]),o=n(e,r);i(M,x,m,b,_,w,M=o[0],x=o[1],m=e,b=u[0],_=u[1],w=u[2],a,t),t.point(M,x)}function o(){S.point=e,t.lineEnd()}function l(){ +r(),S.point=c,S.lineEnd=f}function c(n,t){u(s=n,h=t),p=M,g=x,v=b,d=_,y=w,S.point=u}function f(){i(M,x,m,b,_,w,p,g,s,v,d,y,a,t),S.lineEnd=o,o()}var s,h,p,g,v,d,y,m,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function i(t,e,r,a,l,c,f,s,h,p,g,v,d,y){var m=f-t,M=s-e,x=m*m+M*M;if(x>4*u&&d--){var b=a+p,_=l+g,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=xo(xo(w)-1)u||xo((m*z+M*L)/x-.5)>.3||o>a*p+l*g+c*v)&&(i(t,e,r,a,l,c,A,C,N,b/=S,_/=S,w,d,y),y.point(A,C),i(A,C,N,b,_,w,f,s,h,p,g,v,d,y))}}var u=.5,o=Math.cos(30*Yo),a=16;return t.precision=function(n){return arguments.length?(a=(u=n*n)>0&&16,t):Math.sqrt(u)},t}function re(n){var t=ee(function(t,e){return n([t*Zo,e*Zo])});return function(n){return le(t(n))}}function ie(n){this.stream=n}function ue(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function oe(n){return ae(function(){return n})()}function ae(n){function t(n){return n=a(n[0]*Yo,n[1]*Yo),[n[0]*h+l,c-n[1]*h]}function e(n){return n=a.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Zo,n[1]*Zo]}function r(){a=Ct(o=se(y,M,x),u);var n=u(v,d);return l=p-n[0]*h,c=g+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,o,a,l,c,f,s=ee(function(n,t){return n=u(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,p=480,g=250,v=0,d=0,y=0,M=0,x=0,b=Fa,_=m,w=null,S=null;return t.stream=function(n){return f&&(f.valid=!1),f=le(b(o,s(_(n)))),f.valid=!0,f},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Fa):It((w=+n)*Yo),i()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):m,i()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(p=+n[0],g=+n[1],r()):[p,g]},t.center=function(n){return arguments.length?(v=n[0]%360*Yo,d=n[1]%360*Yo,r()):[v*Zo,d*Zo]},t.rotate=function(n){return arguments.length?(y=n[0]%360*Yo,M=n[1]%360*Yo,x=n.length>2?n[2]%360*Yo:0,r()):[y*Zo,M*Zo,x*Zo]},ao.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function le(n){return ue(n,function(t,e){n.point(t*Yo,e*Yo)})}function ce(n,t){return[n,t]}function fe(n,t){return[n>Fo?n-Ho:-Fo>n?n+Ho:n,t]}function se(n,t,e){return n?t||e?Ct(pe(n),ge(t,e)):pe(n):t||e?ge(t,e):fe}function he(n){return function(t,e){return t+=n,[t>Fo?t-Ho:-Fo>t?t+Ho:t,e]}}function pe(n){var t=he(n);return t.invert=he(-n),t}function ge(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*r+a*i;return[Math.atan2(l*u-f*o,a*r-c*i),tn(f*u+l*o)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),f=c*u-l*o;return[Math.atan2(l*u+c*o,a*r+f*i),tn(f*r-a*i)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,o,a){var l=o*t;null!=i?(i=de(e,i),u=de(e,u),(o>0?u>i:i>u)&&(i+=o*Ho)):(i=n+o*Ho,u=n-.5*l);for(var c,f=i;o>0?f>u:u>f;f-=l)a.point((c=_t([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Uo)%(2*Math.PI)}function ye(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function me(n,t,e){var r=ao.range(n,t-Uo,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),o=Math.cos(r),a=Math.sin(r),l=i*Math.cos(n),c=i*Math.sin(n),f=o*Math.cos(e),s=o*Math.sin(e),h=2*Math.asin(Math.sqrt(on(r-t)+i*o*on(e-n))),p=1/Math.sin(h),g=h?function(n){var t=Math.sin(n*=h)*p,e=Math.sin(h-n)*p,r=e*l+t*f,i=e*c+t*s,o=e*u+t*a;return[Math.atan2(i,r)*Zo,Math.atan2(o,Math.sqrt(r*r+i*i))*Zo]}:function(){return[n*Zo,t*Zo]};return g.distance=h,g}function _e(){function n(n,i){var u=Math.sin(i*=Yo),o=Math.cos(i),a=xo((n*=Yo)-t),l=Math.cos(a);Ja+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*u-e*o*l)*a),e*u+r*o*l),t=n,e=u,r=o}var t,e,r;Ga.point=function(i,u){t=i*Yo,e=Math.sin(u*=Yo),r=Math.cos(u),Ga.point=n},Ga.lineEnd=function(){Ga.point=Ga.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);return[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),o=Math.cos(i);return[Math.atan2(n*u,r*o),Math.asin(r&&e*u/r)]},e}function Se(n,t){function e(n,t){o>0?-Io+Uo>t&&(t=-Io+Uo):t>Io-Uo&&(t=Io-Uo);var e=o/Math.pow(i(t),u);return[e*Math.sin(u*n),o-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan(Fo/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),o=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,t){var e=o-t,r=K(u)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/u,2*Math.atan(Math.pow(o/r,1/u))-Io]},e):Ne}function ke(n,t){function e(n,t){var e=u-t;return[e*Math.sin(i*n),u-e*Math.cos(i*n)]}var r=Math.cos(n),i=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),u=r/i+n;return xo(i)i;i++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var i=n[0],u=e[0],o=t[0]-i,a=r[0]-u,l=n[1],c=e[1],f=t[1]-l,s=r[1]-c,h=(a*(l-c)-s*(i-u))/(s*o-a*f);return[i+h*o,l+h*f]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=cl.pop()||new Pe;return t.site=n,t}function je(n){Be(n),ol.remove(n),cl.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,i={x:e,y:r},u=n.P,o=n.N,a=[n];je(n);for(var l=u;l.circle&&xo(e-l.circle.x)f;++f)c=a[f],l=a[f-1],nr(c.edge,l.site,c.site,i);l=a[0],c=a[s-1],c.edge=Ke(l.site,c.site,null,i),$e(l),$e(c)}function He(n){for(var t,e,r,i,u=n.x,o=n.y,a=ol._;a;)if(r=Oe(a,o)-u,r>Uo)a=a.L;else{if(i=u-Ie(a,o),!(i>Uo)){r>-Uo?(t=a.P,e=a):i>-Uo?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var l=Ue(n);if(ol.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),ol.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,f=c.x,s=c.y,h=n.x-f,p=n.y-s,g=e.site,v=g.x-f,d=g.y-s,y=2*(h*d-p*v),m=h*h+p*p,M=v*v+d*d,x={x:(d*m-p*M)/y+f,y:(h*M-v*m)/y+s};nr(e.edge,c,g,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,g,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,i=e.y,u=i-t;if(!u)return r;var o=n.P;if(!o)return-(1/0);e=o.site;var a=e.x,l=e.y,c=l-t;if(!c)return a;var f=a-r,s=1/u-1/c,h=f/c;return s?(-h+Math.sqrt(h*h-2*s*(f*f/(-2*c)-l+c/2+i-u/2)))/s+r:(r+a)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,i,u,o,a,l,c,f,s=n[0][0],h=n[1][0],p=n[0][1],g=n[1][1],v=ul,d=v.length;d--;)if(u=v[d],u&&u.prepare())for(a=u.edges,l=a.length,o=0;l>o;)f=a[o].end(),r=f.x,i=f.y,c=a[++o%l].start(),t=c.x,e=c.y,(xo(r-t)>Uo||xo(i-e)>Uo)&&(a.splice(o,0,new tr(Qe(u.site,f,xo(r-s)Uo?{x:s,y:xo(t-s)Uo?{x:xo(e-g)Uo?{x:h,y:xo(t-h)Uo?{x:xo(e-p)=-jo)){var p=l*l+c*c,g=f*f+s*s,v=(s*p-c*g)/h,d=(l*g-f*p)/h,s=d+a,y=fl.pop()||new Xe;y.arc=n,y.site=i,y.x=v+o,y.y=s+Math.sqrt(v*v+d*d),y.cy=s,n.circle=y;for(var m=null,M=ll._;M;)if(y.yd||d>=a)return;if(h>g){if(u){if(u.y>=c)return}else u={x:d,y:l};e={x:d,y:c}}else{if(u){if(u.yr||r>1)if(h>g){if(u){if(u.y>=c)return}else u={x:(l-i)/r,y:l};e={x:(c-i)/r,y:c}}else{if(u){if(u.yp){if(u){if(u.x>=a)return}else u={x:o,y:r*o+i};e={x:a,y:r*a+i}}else{if(u){if(u.xu||s>o||r>h||i>p)){if(g=n.point){var g,v=t-n.x,d=e-n.y,y=v*v+d*d;if(l>y){var m=Math.sqrt(l=y);r=t-m,i=e-m,u=t+m,o=e+m,a=g}}for(var M=n.nodes,x=.5*(f+h),b=.5*(s+p),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,f,s,x,b);break;case 1:c(n,x,s,h,b);break;case 2:c(n,f,b,x,p);break;case 3:c(n,x,b,h,p)}}}(n,r,i,u,o),a}function vr(n,t){n=ao.rgb(n),t=ao.rgb(t);var e=n.r,r=n.g,i=n.b,u=t.r-e,o=t.g-r,a=t.b-i;return function(n){return"#"+bn(Math.round(e+u*n))+bn(Math.round(r+o*n))+bn(Math.round(i+a*n))}}function dr(n,t){var e,r={},i={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):i[e]=n[e];for(e in t)e in n||(i[e]=t[e]);return function(n){for(e in r)i[e]=r[e](n);return i}}function yr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function mr(n,t){var e,r,i,u=hl.lastIndex=pl.lastIndex=0,o=-1,a=[],l=[];for(n+="",t+="";(e=hl.exec(n))&&(r=pl.exec(t));)(i=r.index)>u&&(i=t.slice(u,i),a[o]?a[o]+=i:a[++o]=i),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,l.push({i:o,x:yr(e,r)})),u=pl.lastIndex;return ur;++r)a[(e=l[r]).i]=e.x(n);return a.join("")})}function Mr(n,t){for(var e,r=ao.interpolators.length;--r>=0&&!(e=ao.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],i=[],u=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(Mr(n[e],t[e]));for(;u>e;++e)i[e]=n[e];for(;o>e;++e)i[e]=t[e];return function(n){for(e=0;a>e;++e)i[e]=r[e](n);return i}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Io)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Ho*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Ho/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=ao.hcl(n),t=ao.hcl(t);var e=n.h,r=n.c,i=n.l,u=t.h-e,o=t.c-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return sn(e+u*n,r+o*n,i+a*n)+""}}function Dr(n,t){n=ao.hsl(n),t=ao.hsl(t);var e=n.h,r=n.s,i=n.l,u=t.h-e,o=t.s-r,a=t.l-i;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return cn(e+u*n,r+o*n,i+a*n)+""}}function Pr(n,t){n=ao.lab(n),t=ao.lab(t);var e=n.l,r=n.a,i=n.b,u=t.l-e,o=t.a-r,a=t.b-i;return function(n){return pn(e+u*n,r+o*n,i+a*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),i=Fr(t,e),u=Hr(Or(e,t,-i))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:yr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:yr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var i=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:i-4,x:yr(n[0],t[0])},{i:i-2,x:yr(n[1],t[1])})}else 1===t[0]&&1===t[1]||e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=ao.transform(n),t=ao.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,i=-1,u=r.length;++i=0;)e.push(i[r])}function oi(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(u=n.children)&&(i=u.length))for(var i,u,o=-1;++oe;++e)(t=n[e][1])>i&&(r=e,i=t);return r}function yi(n){return n.reduce(mi,0)}function mi(n,t){return n+t[1]}function Mi(n,t){return xi(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xi(n,t){for(var e=-1,r=+n[0],i=(n[1]-r)/t,u=[];++e<=t;)u[e]=i*e+r;return u}function bi(n){return[ao.min(n),ao.max(n)]}function _i(n,t){return n.value-t.value}function wi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Si(n,t){n._pack_next=t,t._pack_prev=n}function ki(n,t){var e=t.x-n.x,r=t.y-n.y,i=n.r+t.r;return.999*i*i>e*e+r*r}function Ni(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),p=Math.max(n.y+n.r,p)}if((e=n.children)&&(c=e.length)){var e,r,i,u,o,a,l,c,f=1/0,s=-(1/0),h=1/0,p=-(1/0);if(e.forEach(Ei),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(i=e[1],i.x=i.r,i.y=0,t(i),c>2))for(u=e[2],zi(r,i,u),t(u),wi(r,u),r._pack_prev=u,wi(u,i),i=r._pack_next,o=3;c>o;o++){zi(r,i,u=e[o]);var g=0,v=1,d=1;for(a=i._pack_next;a!==i;a=a._pack_next,v++)if(ki(a,u)){g=1;break}if(1==g)for(l=r._pack_prev;l!==a._pack_prev&&!ki(l,u);l=l._pack_prev,d++);g?(d>v||v==d&&i.ro;o++)u=e[o],u.x-=y,u.y-=m,M=Math.max(M,u.r+Math.sqrt(u.x*u.x+u.y*u.y));n.r=M,e.forEach(Ai)}}function Ei(n){n._pack_next=n._pack_prev=n}function Ai(n){delete n._pack_next,delete n._pack_prev}function Ci(n,t,e,r){var i=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,i)for(var u=-1,o=i.length;++u=0;)t=i[u],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pi(n,t,e){return n.a.parent===t.parent?n.a:e}function Ui(n){return 1+ao.max(n,function(n){return n.y})}function ji(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fi(n){var t=n.children;return t&&t.length?Fi(t[0]):n}function Hi(n){var t,e=n.children;return e&&(t=e.length)?Hi(e[t-1]):n}function Oi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ii(n,t){var e=n.x+t[3],r=n.y+t[0],i=n.dx-t[1]-t[3],u=n.dy-t[0]-t[2];return 0>i&&(e+=i/2,i=0),0>u&&(r+=u/2,u=0),{x:e,y:r,dx:i,dy:u}}function Yi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zi(n){return n.rangeExtent?n.rangeExtent():Yi(n.range())}function Vi(n,t,e,r){var i=e(n[0],n[1]),u=r(t[0],t[1]);return function(n){return u(i(n))}}function Xi(n,t){var e,r=0,i=n.length-1,u=n[r],o=n[i];return u>o&&(e=r,r=i,i=e,e=u,u=o,o=e),n[r]=t.floor(u),n[i]=t.ceil(o),n}function $i(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Sl}function Bi(n,t,e,r){var i=[],u=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]2?Bi:Vi,l=r?Wr:Br;return o=i(n,t,l,e),a=i(t,n,l,Mr),u}function u(n){return o(n)}var o,a;return u.invert=function(n){return a(n)},u.domain=function(t){return arguments.length?(n=t.map(Number),i()):n},u.range=function(n){return arguments.length?(t=n,i()):t},u.rangeRound=function(n){return u.range(n).interpolate(Ur)},u.clamp=function(n){return arguments.length?(r=n,i()):r},u.interpolate=function(n){return arguments.length?(e=n,i()):e},u.ticks=function(t){return Qi(n,t)},u.tickFormat=function(t,e){return nu(n,t,e)},u.nice=function(t){return Gi(n,t),i()},u.copy=function(){return Wi(n,t,e,r)},i()}function Ji(n,t){return ao.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gi(n,t){return Xi(n,$i(Ki(n,t)[2])),Xi(n,$i(Ki(n,t)[2])),n}function Ki(n,t){null==t&&(t=10);var e=Yi(n),r=e[1]-e[0],i=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),u=t/r*i;return.15>=u?i*=10:.35>=u?i*=5:.75>=u&&(i*=2),e[0]=Math.ceil(e[0]/i)*i,e[1]=Math.floor(e[1]/i)*i+.5*i,e[2]=i,e}function Qi(n,t){return ao.range.apply(ao,Ki(n,t))}function nu(n,t,e){var r=Ki(n,t);if(e){var i=ha.exec(e);if(i.shift(),"s"===i[8]){var u=ao.formatPrefix(Math.max(xo(r[0]),xo(r[1])));return i[7]||(i[7]="."+tu(u.scale(r[2]))),i[8]="f",e=ao.format(i.join("")),function(n){return e(u.scale(n))+u.symbol}}i[7]||(i[7]="."+eu(i[8],r)),e=i.join("")}else e=",."+tu(r[2])+"f";return ao.format(e)}function tu(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function eu(n,t){var e=tu(t[2]);return n in kl?Math.abs(e-tu(Math.max(xo(t[0]),xo(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ru(n,t,e,r){function i(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function u(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(i(t))}return o.invert=function(t){return u(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(i)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(i)),o):t},o.nice=function(){var t=Xi(r.map(i),e?Math:El);return n.domain(t),r=t.map(u),o},o.ticks=function(){var n=Yi(r),o=[],a=n[0],l=n[1],c=Math.floor(i(a)),f=Math.ceil(i(l)),s=t%1?2:t;if(isFinite(f-c)){if(e){for(;f>c;c++)for(var h=1;s>h;h++)o.push(u(c)*h);o.push(u(c))}else for(o.push(u(c));c++0;h--)o.push(u(c)*h);for(c=0;o[c]l;f--);o=o.slice(c,f)}return o},o.tickFormat=function(n,e){if(!arguments.length)return Nl;arguments.length<2?e=Nl:"function"!=typeof e&&(e=ao.format(e));var r=Math.max(1,t*n/o.ticks().length);return function(n){var o=n/u(Math.round(i(n)));return t-.5>o*t&&(o*=t),r>=o?e(n):""}},o.copy=function(){return ru(n.copy(),t,e,r)},Ji(o,n)}function iu(n,t,e){function r(t){return n(i(t))}var i=uu(t),u=uu(1/t);return r.invert=function(t){return u(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(i)),r):e},r.ticks=function(n){return Qi(e,n)},r.tickFormat=function(n,t){return nu(e,n,t)},r.nice=function(n){return r.domain(Gi(e,n))},r.exponent=function(o){return arguments.length?(i=uu(t=o),u=uu(1/t),n.domain(e.map(i)),r):t},r.copy=function(){return iu(n.copy(),t,e)},Ji(r,n)}function uu(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ou(n,t){function e(e){return u[((i.get(e)||("range"===t.t?i.set(e,n.push(e)):NaN))-1)%u.length]}function r(t,e){return ao.range(n.length).map(function(n){return t+e*n})}var i,u,o;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new c;for(var u,o=-1,a=r.length;++oe?[NaN,NaN]:[e>0?a[e-1]:n[0],et?NaN:t/u+n,[t,t+1/u]},r.copy=function(){return lu(n,t,e)},i()}function cu(n,t){function e(e){return e>=e?t[ao.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return cu(n,t)},e}function fu(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qi(n,t)},t.tickFormat=function(t,e){return nu(n,t,e)},t.copy=function(){return fu(n)},t}function su(){return 0}function hu(n){return n.innerRadius}function pu(n){return n.outerRadius}function gu(n){return n.startAngle}function vu(n){return n.endAngle}function du(n){return n&&n.padAngle}function yu(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function mu(n,t,e,r,i){var u=n[0]-t[0],o=n[1]-t[1],a=(i?r:-r)/Math.sqrt(u*u+o*o),l=a*o,c=-a*u,f=n[0]+l,s=n[1]+c,h=t[0]+l,p=t[1]+c,g=(f+h)/2,v=(s+p)/2,d=h-f,y=p-s,m=d*d+y*y,M=e-r,x=f*p-h*s,b=(0>y?-1:1)*Math.sqrt(Math.max(0,M*M*m-x*x)),_=(x*y-d*b)/m,w=(-x*d-y*b)/m,S=(x*y+d*b)/m,k=(-x*d+y*b)/m,N=_-g,E=w-v,A=S-g,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mu(n){function t(t){function o(){c.push("M",u(n(f),a))}for(var l,c=[],f=[],s=-1,h=t.length,p=En(e),g=En(r);++s1?n.join("L"):n+"Z"}function bu(n){return n.join("L")+"Z"}function _u(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1&&i.push("H",r[0]),i.join("")}function wu(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t1){a=t[1],u=n[l],l++,r+="C"+(i[0]+o[0])+","+(i[1]+o[1])+","+(u[0]-a[0])+","+(u[1]-a[1])+","+u[0]+","+u[1];for(var c=2;c9&&(i=3*t/Math.sqrt(i),o[a]=i*e,o[a+1]=i*r));for(a=-1;++a<=l;)i=(n[Math.min(l,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),u.push([i||0,o[a]*i||0]);return u}function Fu(n){return n.length<3?xu(n):n[0]+Au(n,ju(n))}function Hu(n){for(var t,e,r,i=-1,u=n.length;++i=t?o(n-t):void(f.c=o)}function o(e){var i=g.active,u=g[i];u&&(u.timer.c=null,u.timer.t=NaN,--g.count,delete g[i],u.event&&u.event.interrupt.call(n,n.__data__,u.index));for(var o in g)if(r>+o){var c=g[o];c.timer.c=null,c.timer.t=NaN,--g.count,delete g[o]}f.c=a,qn(function(){return f.c&&a(e||1)&&(f.c=null,f.t=NaN),1},0,l),g.active=r,v.event&&v.event.start.call(n,n.__data__,t),p=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&p.push(r)}),h=v.ease,s=v.duration}function a(i){for(var u=i/s,o=h(u),a=p.length;a>0;)p[--a].call(n,o);return u>=1?(v.event&&v.event.end.call(n,n.__data__,t),--g.count?delete g[r]:delete n[e],1):void 0}var l,f,s,h,p,g=n[e]||(n[e]={active:0,count:0}),v=g[r];v||(l=i.time,f=qn(u,0,l),v=g[r]={tween:new c,time:l,timer:f,delay:i.delay,duration:i.duration,ease:i.ease,index:t},i=null,++g.count)}function no(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function to(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function eo(n){return n.toISOString()}function ro(n,t,e){function r(t){return n(t)}function i(n,e){var r=n[1]-n[0],i=r/e,u=ao.bisect(Kl,i);return u==Kl.length?[t.year,Ki(n.map(function(n){return n/31536e6}),e)[2]]:u?t[i/Kl[u-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yi(r.domain()),u=null==n?i(e,10):"number"==typeof n?i(e,n):!n.range&&[{range:n},t];return u&&(n=u[0],t=u[1]),n.range(e[0],io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ro(n.copy(),t,e)},Ji(r,n)}function io(n){return new Date(n)}function uo(n){return JSON.parse(n.responseText)}function oo(n){var t=fo.createRange();return t.selectNode(fo.body),t.createContextualFragment(n.responseText)}var ao={version:"3.5.17"},lo=[].slice,co=function(n){return lo.call(n)},fo=this.document;if(fo)try{co(fo.documentElement.childNodes)[0].nodeType}catch(so){co=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),fo)try{fo.createElement("DIV").style.setProperty("opacity",0,"")}catch(ho){var po=this.Element.prototype,go=po.setAttribute,vo=po.setAttributeNS,yo=this.CSSStyleDeclaration.prototype,mo=yo.setProperty;po.setAttribute=function(n,t){go.call(this,n,t+"")},po.setAttributeNS=function(n,t,e){vo.call(this,n,t,e+"")},yo.setProperty=function(n,t,e){mo.call(this,n,t+"",e)}}ao.ascending=e,ao.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},ao.min=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ir&&(e=r)}else{for(;++i=r){e=r;break}for(;++ir&&(e=r)}return e},ao.max=function(n,t){var e,r,i=-1,u=n.length;if(1===arguments.length){for(;++i=r){e=r;break}for(;++ie&&(e=r)}else{for(;++i=r){e=r;break}for(;++ie&&(e=r)}return e},ao.extent=function(n,t){var e,r,i,u=-1,o=n.length;if(1===arguments.length){for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}else{for(;++u=r){e=i=r;break}for(;++ur&&(e=r),r>i&&(i=r))}return[e,i]},ao.sum=function(n,t){var e,r=0,u=n.length,o=-1;if(1===arguments.length)for(;++o1?l/(f-1):void 0},ao.deviation=function(){var n=ao.variance.apply(this,arguments);return n?Math.sqrt(n):n};var Mo=u(e);ao.bisectLeft=Mo.left,ao.bisect=ao.bisectRight=Mo.right,ao.bisector=function(n){return u(1===n.length?function(t,r){return e(n(t),r)}:n)},ao.shuffle=function(n,t,e){(u=arguments.length)<3&&(e=n.length,2>u&&(t=0));for(var r,i,u=e-t;u;)i=Math.random()*u--|0,r=n[u+t],n[u+t]=n[i+t],n[i+t]=r;return n},ao.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},ao.pairs=function(n){for(var t,e=0,r=n.length-1,i=n[0],u=new Array(0>r?0:r);r>e;)u[e]=[t=i,i=n[++e]];return u},ao.transpose=function(n){if(!(i=n.length))return[];for(var t=-1,e=ao.min(n,o),r=new Array(e);++t=0;)for(r=n[i],t=r.length;--t>=0;)e[--o]=r[t];return e};var xo=Math.abs;ao.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,i=[],u=a(xo(e)),o=-1;if(n*=u,t*=u,e*=u,0>e)for(;(r=n+e*++o)>t;)i.push(r/u);else for(;(r=n+e*++o)=u.length)return r?r.call(i,o):e?o.sort(e):o;for(var l,f,s,h,p=-1,g=o.length,v=u[a++],d=new c;++p=u.length)return n;var r=[],i=o[e++];return n.forEach(function(n,i){r.push({key:n,values:t(i,e)})}),i?r.sort(function(n,t){return i(n.key,t.key)}):r}var e,r,i={},u=[],o=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(ao.map,e,0),0)},i.key=function(n){return u.push(n),i},i.sortKeys=function(n){return o[u.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},ao.set=function(n){var t=new y;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(y,{has:h,add:function(n){return this._[f(n+="")]=!0,n},remove:p,values:g,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,s(t))}}),ao.behavior={},ao.rebind=function(n,t){for(var e,r=1,i=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},ao.event=null,ao.requote=function(n){return n.replace(So,"\\$&")};var So=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,ko={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},No=function(n,t){return t.querySelector(n)},Eo=function(n,t){return t.querySelectorAll(n)},Ao=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ao=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(No=function(n,t){return Sizzle(n,t)[0]||null},Eo=Sizzle,Ao=Sizzle.matchesSelector),ao.selection=function(){return ao.select(fo.documentElement)};var Co=ao.selection.prototype=[];Co.select=function(n){var t,e,r,i,u=[];n=A(n);for(var o=-1,a=this.length;++o=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Lo.hasOwnProperty(e)?{space:Lo[e],local:n}:n}},Co.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=ao.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Co.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,i=-1;if(t=e.classList){for(;++ii){if("string"!=typeof n){2>i&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>i){var u=this.node();return t(u).getComputedStyle(u,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Co.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Co.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Co.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Co.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Co.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Co.remove=function(){return this.each(F)},Co.data=function(n,t){function e(n,e){var r,i,u,o=n.length,s=e.length,h=Math.min(o,s),p=new Array(s),g=new Array(s),v=new Array(o);if(t){var d,y=new c,m=new Array(o);for(r=-1;++rr;++r)g[r]=H(e[r]);for(;o>r;++r)v[r]=n[r]}g.update=p,g.parentNode=p.parentNode=v.parentNode=n.parentNode,a.push(g),l.push(p),f.push(v)}var r,i,u=-1,o=this.length;if(!arguments.length){for(n=new Array(o=(r=this[0]).length);++uu;u++){i.push(t=[]),t.parentNode=(e=this[u]).parentNode;for(var a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return E(i)},Co.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[i])&&(u&&u!==e.nextSibling&&u.parentNode.insertBefore(e,u),u=e);return this},Co.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,i=e.length;i>r;r++){var u=e[r];if(u)return u}return null},Co.size=function(){var n=0;return Y(this,function(){++n}),n};var qo=[];ao.selection.enter=Z,ao.selection.enter.prototype=qo,qo.append=Co.append,qo.empty=Co.empty,qo.node=Co.node,qo.call=Co.call,qo.size=Co.size,qo.select=function(n){for(var t,e,r,i,u,o=[],a=-1,l=this.length;++ar){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var To=ao.map({mouseenter:"mouseover",mouseleave:"mouseout"});fo&&To.forEach(function(n){"on"+n in fo&&To.remove(n)});var Ro,Do=0;ao.mouse=function(n){return J(n,k())};var Po=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;ao.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,i=0,u=t.length;u>i;++i)if((r=t[i]).identifier===e)return J(n,r)},ao.behavior.drag=function(){function n(){this.on("mousedown.drag",u).on("touchstart.drag",o)}function e(n,t,e,u,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],g|=n|e,M=r,p({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(y.on(u+d,null).on(o+d,null),m(g),p({type:"dragend"}))}var c,f=this,s=ao.event.target.correspondingElement||ao.event.target,h=f.parentNode,p=r.of(f,arguments),g=0,v=n(),d=".drag"+(null==v?"":"-"+v),y=ao.select(e(s)).on(u+d,a).on(o+d,l),m=W(s),M=t(h,v);i?(c=i.apply(f,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],p({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),i=null,u=e(b,ao.mouse,t,"mousemove","mouseup"),o=e(G,ao.touch,m,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},ao.rebind(n,r,"on")},ao.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?co(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Uo=1e-6,jo=Uo*Uo,Fo=Math.PI,Ho=2*Fo,Oo=Ho-Uo,Io=Fo/2,Yo=Fo/180,Zo=180/Fo,Vo=Math.SQRT2,Xo=2,$o=4;ao.interpolateZoom=function(n,t){var e,r,i=n[0],u=n[1],o=n[2],a=t[0],l=t[1],c=t[2],f=a-i,s=l-u,h=f*f+s*s;if(jo>h)r=Math.log(c/o)/Vo,e=function(n){return[i+n*f,u+n*s,o*Math.exp(Vo*n*r)]};else{var p=Math.sqrt(h),g=(c*c-o*o+$o*h)/(2*o*Xo*p),v=(c*c-o*o-$o*h)/(2*c*Xo*p),d=Math.log(Math.sqrt(g*g+1)-g),y=Math.log(Math.sqrt(v*v+1)-v);r=(y-d)/Vo,e=function(n){var t=n*r,e=rn(d),a=o/(Xo*p)*(e*un(Vo*t+d)-en(d));return[i+a*f,u+a*s,o*e/rn(Vo*t+d)]}}return e.duration=1e3*r,e},ao.behavior.zoom=function(){function n(n){n.on(L,s).on(Wo+".zoom",p).on("dblclick.zoom",g).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function i(n){k.k=Math.max(A[0],Math.min(A[1],n))}function u(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function o(t,e,r,o){t.__chart__={x:k.x,y:k.y,k:k.k},i(Math.pow(2,o)),u(d=e,r),t=ao.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function a(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){a(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function f(n){--z||(n({type:"zoomend"}),d=null)}function s(){function n(){a=1,u(ao.mouse(i),h),c(o)}function r(){s.on(q,null).on(T,null),p(a),f(o)}var i=this,o=D.of(i,arguments),a=0,s=ao.select(t(i)).on(q,n).on(T,r),h=e(ao.mouse(i)),p=W(i);Il.call(i),l(o)}function h(){function n(){var n=ao.touches(g);return p=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=ao.event.target;ao.select(t).on(x,r).on(b,a),_.push(t);for(var e=ao.event.changedTouches,i=0,u=e.length;u>i;++i)d[e[i].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var f=l[0];o(g,f,d[f.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var f=l[0],s=l[1],h=f[0]-s[0],p=f[1]-s[1];y=h*h+p*p}}function r(){var n,t,e,r,o=ao.touches(g);Il.call(g);for(var a=0,l=o.length;l>a;++a,r=null)if(e=o[a],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var f=(f=e[0]-n[0])*f+(f=e[1]-n[1])*f,s=y&&Math.sqrt(f/y);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],i(s*p)}M=null,u(n,t),c(v)}function a(){if(ao.event.touches.length){for(var t=ao.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var i in d)return void n()}ao.selectAll(_).on(m,null),w.on(L,s).on(R,h),N(),f(v)}var p,g=this,v=D.of(g,arguments),d={},y=0,m=".zoom-"+ao.event.changedTouches[0].identifier,x="touchmove"+m,b="touchend"+m,_=[],w=ao.select(g),N=W(g);t(),l(v),w.on(L,null).on(R,t)}function p(){var n=D.of(this,arguments);m?clearTimeout(m):(Il.call(this),v=e(d=y||ao.mouse(this)),l(n)),m=setTimeout(function(){m=null,f(n)},50),S(),i(Math.pow(2,.002*Bo())*k.k),u(d,v),c(n)}function g(){var n=ao.mouse(this),t=Math.log(k.k)/Math.LN2;o(this,n,e(n),ao.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,y,m,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Jo,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return Wo||(Wo="onwheel"in fo?(Bo=function(){return-ao.event.deltaY*(ao.event.deltaMode?120:1)},"wheel"):"onmousewheel"in fo?(Bo=function(){return ao.event.wheelDelta},"mousewheel"):(Bo=function(){return-ao.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Hl?ao.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],i=d?d[0]:e/2,u=d?d[1]:r/2,o=ao.interpolateZoom([(i-k.x)/k.k,(u-k.y)/k.k,e/k.k],[(i-t.x)/t.k,(u-t.y)/t.k,e/t.k]);return function(t){var r=o(t),a=e/r[2];this.__chart__=k={x:i-r[0]*a,y:u-r[1]*a,k:a},c(n)}}).each("interrupt.zoom",function(){f(n)}).each("end.zoom",function(){f(n)}):(this.__chart__=k,l(n),c(n),f(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},a(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},i(+t),a(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Jo:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(y=t&&[+t[0],+t[1]],n):y},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},ao.rebind(n,D,"on")};var Bo,Wo,Jo=[0,1/0];ao.color=an,an.prototype.toString=function(){return this.rgb()+""},ao.hsl=ln;var Go=ln.prototype=new an;Go.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Go.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Go.rgb=function(){return cn(this.h,this.s,this.l)},ao.hcl=fn;var Ko=fn.prototype=new an;Ko.brighter=function(n){return new fn(this.h,this.c,Math.min(100,this.l+Qo*(arguments.length?n:1)))},Ko.darker=function(n){return new fn(this.h,this.c,Math.max(0,this.l-Qo*(arguments.length?n:1)))},Ko.rgb=function(){return sn(this.h,this.c,this.l).rgb()},ao.lab=hn;var Qo=18,na=.95047,ta=1,ea=1.08883,ra=hn.prototype=new an;ra.brighter=function(n){return new hn(Math.min(100,this.l+Qo*(arguments.length?n:1)),this.a,this.b)},ra.darker=function(n){return new hn(Math.max(0,this.l-Qo*(arguments.length?n:1)),this.a,this.b)},ra.rgb=function(){return pn(this.l,this.a,this.b)},ao.rgb=mn;var ia=mn.prototype=new an;ia.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,i=30;return t||e||r?(t&&i>t&&(t=i),e&&i>e&&(e=i),r&&i>r&&(r=i),new mn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new mn(i,i,i)},ia.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new mn(n*this.r,n*this.g,n*this.b)},ia.hsl=function(){return wn(this.r,this.g,this.b)},ia.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ua=ao.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ua.forEach(function(n,t){ua.set(n,Mn(t))}),ao.functor=En,ao.xhr=An(m),ao.dsv=function(n,t){function e(n,e,u){arguments.length<3&&(u=e,e=null);var o=Cn(n,t,null==e?r:i(e),u);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:i(n)):e},o}function r(n){return e.parse(n.responseText)}function i(n){return function(t){return e.parse(t.responseText,n)}}function u(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var i=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(i(n),e)}:i})},e.parseRows=function(n,t){function e(){if(f>=c)return o;if(i)return i=!1,u;var t=f;if(34===n.charCodeAt(t)){for(var e=t;e++f;){var r=n.charCodeAt(f++),a=1;if(10===r)i=!0;else if(13===r)i=!0,10===n.charCodeAt(f)&&(++f,++a);else if(r!==l)continue;return n.slice(t,f-a)}return n.slice(t)}for(var r,i,u={},o={},a=[],c=n.length,f=0,s=0;(r=e())!==o;){for(var h=[];r!==u&&r!==o;)h.push(r),r=e();t&&null==(h=t(h,s++))||a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new y,i=[];return t.forEach(function(n){for(var t in n)r.has(t)||i.push(r.add(t))}),[i.map(o).join(n)].concat(t.map(function(t){return i.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(u).join("\n")},e},ao.csv=ao.dsv(",","text/csv"),ao.tsv=ao.dsv(" ","text/tab-separated-values");var oa,aa,la,ca,fa=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};ao.timer=function(){qn.apply(this,arguments)},ao.timer.flush=function(){Rn(),Dn()},ao.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var sa=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);ao.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=ao.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),sa[8+e/3]};var ha=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,pa=ao.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=ao.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ga=ao.time={},va=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){da.setUTCDate.apply(this._,arguments)},setDay:function(){da.setUTCDay.apply(this._,arguments)},setFullYear:function(){da.setUTCFullYear.apply(this._,arguments)},setHours:function(){da.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){da.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){da.setUTCMinutes.apply(this._,arguments)},setMonth:function(){da.setUTCMonth.apply(this._,arguments)},setSeconds:function(){da.setUTCSeconds.apply(this._,arguments)},setTime:function(){da.setTime.apply(this._,arguments)}};var da=Date.prototype;ga.year=On(function(n){return n=ga.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ga.years=ga.year.range,ga.years.utc=ga.year.utc.range,ga.day=On(function(n){var t=new va(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ga.days=ga.day.range,ga.days.utc=ga.day.utc.range,ga.dayOfYear=function(n){var t=ga.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ga[n]=On(function(n){return(n=ga.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ga[n+"s"]=e.range,ga[n+"s"].utc=e.utc.range,ga[n+"OfYear"]=function(n){var e=ga.year(n).getDay();return Math.floor((ga.dayOfYear(n)+(e+t)%7)/7)}}),ga.week=ga.sunday,ga.weeks=ga.sunday.range,ga.weeks.utc=ga.sunday.utc.range,ga.weekOfYear=ga.sundayOfYear;var ya={"-":"",_:" ",0:"0"},ma=/^\s*\d+/,Ma=/^%/;ao.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var xa=ao.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], +shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ao.format=xa.numberFormat,ao.geo={},ft.prototype={s:0,t:0,add:function(n){st(n,this.t,ba),st(ba.s,this.s,this),this.s?this.t+=ba.t:this.s=ba.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var ba=new ft;ao.geo.stream=function(n,t){n&&_a.hasOwnProperty(n.type)?_a[n.type](n,t):ht(n,t)};var _a={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++rn?4*Fo+n:n,Na.lineStart=Na.lineEnd=Na.point=b}};ao.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>p&&(p=t)}function t(t,e){var r=dt([t*Yo,e*Yo]);if(y){var i=mt(y,r),u=[i[1],-i[0],0],o=mt(u,i);bt(o),o=_t(o);var l=t-g,c=l>0?1:-1,v=o[0]*Zo*c,d=xo(l)>180;if(d^(v>c*g&&c*t>v)){var m=o[1]*Zo;m>p&&(p=m)}else if(v=(v+360)%360-180,d^(v>c*g&&c*t>v)){var m=-o[1]*Zo;s>m&&(s=m)}else s>e&&(s=e),e>p&&(p=e);d?g>t?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t)):t>g?a(f,t)>a(f,h)&&(h=t):a(t,h)>a(f,h)&&(f=t)}else n(t,e);y=r,g=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,y=null}function i(n,e){if(y){var r=n-g;m+=xo(r)>180?r+(r>0?360:-360):r}else v=n,d=e;Na.point(n,e),t(n,e)}function u(){Na.lineStart()}function o(){i(v,d),Na.lineEnd(),xo(m)>Uo&&(f=-(h=180)),x[0]=f,x[1]=h,y=null}function a(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nka?(f=-(h=180),s=-(p=90)):m>Uo?p=90:-Uo>m&&(s=-90),x[0]=f,x[1]=h}};return function(n){p=h=-(f=s=1/0),M=[],ao.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],c(e[0],i)||c(e[1],i)?(a(i[0],e[1])>a(i[0],i[1])&&(i[1]=e[1]),a(e[0],i[1])>a(i[0],i[1])&&(i[0]=e[0])):u.push(i=e);for(var o,e,g=-(1/0),t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(o=a(i[1],e[0]))>g&&(g=o,f=e[0],h=i[1])}return M=x=null,f===1/0||s===1/0?[[NaN,NaN],[NaN,NaN]]:[[f,s],[h,p]]}}(),ao.geo.centroid=function(n){Ea=Aa=Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,ja);var t=Da,e=Pa,r=Ua,i=t*t+e*e+r*r;return jo>i&&(t=qa,e=Ta,r=Ra,Uo>Aa&&(t=Ca,e=za,r=La),i=t*t+e*e+r*r,jo>i)?[NaN,NaN]:[Math.atan2(e,t)*Zo,tn(r/Math.sqrt(i))*Zo]};var Ea,Aa,Ca,za,La,qa,Ta,Ra,Da,Pa,Ua,ja={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){ja.lineStart=At},polygonEnd:function(){ja.lineStart=Nt}},Fa=Rt(zt,jt,Ht,[-Fo,-Fo/2]),Ha=1e9;ao.geo.clipExtent=function(){var n,t,e,r,i,u,o={stream:function(n){return i&&(i.valid=!1),i=u(n),i.valid=!0,i},extent:function(a){return arguments.length?(u=Zt(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),i&&(i.valid=!1,i=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(ao.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,ao.geo.albers=function(){return ao.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},ao.geo.albersUsa=function(){function n(n){var u=n[0],o=n[1];return t=null,e(u,o),t||(r(u,o),t)||i(u,o),t}var t,e,r,i,u=ao.geo.albers(),o=ao.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=ao.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?o:i>=.166&&.234>i&&r>=-.214&&-.115>r?a:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(u.precision(t),o.precision(t),a.precision(t),n):u.precision()},n.scale=function(t){return arguments.length?(u.scale(t),o.scale(.35*t),a.scale(t),n.translate(u.translate())):u.scale()},n.translate=function(t){if(!arguments.length)return u.translate();var c=u.scale(),f=+t[0],s=+t[1];return e=u.translate(t).clipExtent([[f-.455*c,s-.238*c],[f+.455*c,s+.238*c]]).stream(l).point,r=o.translate([f-.307*c,s+.201*c]).clipExtent([[f-.425*c+Uo,s+.12*c+Uo],[f-.214*c-Uo,s+.234*c-Uo]]).stream(l).point,i=a.translate([f-.205*c,s+.212*c]).clipExtent([[f-.214*c+Uo,s+.166*c+Uo],[f-.115*c-Uo,s+.234*c-Uo]]).stream(l).point,n},n.scale(1070)};var Oa,Ia,Ya,Za,Va,Xa,$a={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Ia=0,$a.lineStart=$t},polygonEnd:function(){$a.lineStart=$a.lineEnd=$a.point=b,Oa+=xo(Ia/2)}},Ba={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Wa={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Wa.lineStart=ne},polygonEnd:function(){Wa.point=Gt,Wa.lineStart=Kt,Wa.lineEnd=Qt}};ao.geo.path=function(){function n(n){return n&&("function"==typeof a&&u.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=i(u)),ao.geo.stream(n,o)),u.result()}function t(){return o=null,n}var e,r,i,u,o,a=4.5;return n.area=function(n){return Oa=0,ao.geo.stream(n,i($a)),Oa},n.centroid=function(n){return Ca=za=La=qa=Ta=Ra=Da=Pa=Ua=0,ao.geo.stream(n,i(Wa)),Ua?[Da/Ua,Pa/Ua]:Ra?[qa/Ra,Ta/Ra]:La?[Ca/La,za/La]:[NaN,NaN]},n.bounds=function(n){return Va=Xa=-(Ya=Za=1/0),ao.geo.stream(n,i(Ba)),[[Ya,Za],[Va,Xa]]},n.projection=function(n){return arguments.length?(i=(e=n)?n.stream||re(n):m,t()):e},n.context=function(n){return arguments.length?(u=null==(r=n)?new Wt:new te(n),"function"!=typeof a&&u.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(u.pointRadius(+t),+t),n):a},n.projection(ao.geo.albersUsa()).context(null)},ao.geo.transform=function(n){return{stream:function(t){var e=new ie(t);for(var r in n)e[r]=n[r];return e}}},ie.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},ao.geo.projection=oe,ao.geo.projectionMutator=ae,(ao.geo.equirectangular=function(){return oe(ce)}).raw=ce.invert=ce,ao.geo.rotation=function(n){function t(t){return t=n(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t}return n=se(n[0]%360*Yo,n[1]*Yo,n.length>2?n[2]*Yo:0),t.invert=function(t){return t=n.invert(t[0]*Yo,t[1]*Yo),t[0]*=Zo,t[1]*=Zo,t},t},fe.invert=ce,ao.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=se(-n[0]*Yo,-n[1]*Yo,0).invert,i=[];return e(null,null,1,{point:function(n,e){i.push(n=t(n,e)),n[0]*=Zo,n[1]*=Zo}}),{type:"Polygon",coordinates:[i]}}var t,e,r=[0,0],i=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Yo,i*Yo),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Yo,(i=+r)*Yo),n):i},n.angle(90)},ao.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Yo,i=n[1]*Yo,u=t[1]*Yo,o=Math.sin(r),a=Math.cos(r),l=Math.sin(i),c=Math.cos(i),f=Math.sin(u),s=Math.cos(u);return Math.atan2(Math.sqrt((e=s*o)*e+(e=c*f-l*s*a)*e),l*f+c*s*a)},ao.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return ao.range(Math.ceil(u/d)*d,i,d).map(h).concat(ao.range(Math.ceil(c/y)*y,l,y).map(p)).concat(ao.range(Math.ceil(r/g)*g,e,g).filter(function(n){return xo(n%d)>Uo}).map(f)).concat(ao.range(Math.ceil(a/v)*v,o,v).filter(function(n){return xo(n%y)>Uo}).map(s))}var e,r,i,u,o,a,l,c,f,s,h,p,g=10,v=g,d=90,y=360,m=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(u).concat(p(l).slice(1),h(i).reverse().slice(1),p(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(u=+t[0][0],i=+t[1][0],c=+t[0][1],l=+t[1][1],u>i&&(t=u,u=i,i=t),c>l&&(t=c,c=l,l=t),n.precision(m)):[[u,c],[i,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(m)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],y=+t[1],n):[d,y]},n.minorStep=function(t){return arguments.length?(g=+t[0],v=+t[1],n):[g,v]},n.precision=function(t){return arguments.length?(m=+t,f=ye(a,o,90),s=me(r,e,m),h=ye(c,l,90),p=me(u,i,m),n):m},n.majorExtent([[-180,-90+Uo],[180,90-Uo]]).minorExtent([[-180,-80-Uo],[180,80+Uo]])},ao.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||i.apply(this,arguments)]}}var t,e,r=Me,i=xe;return n.distance=function(){return ao.geo.distance(t||r.apply(this,arguments),e||i.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(i=t,e="function"==typeof t?null:t,n):i},n.precision=function(){return arguments.length?n:0},n},ao.geo.interpolate=function(n,t){return be(n[0]*Yo,n[1]*Yo,t[0]*Yo,t[1]*Yo)},ao.geo.length=function(n){return Ja=0,ao.geo.stream(n,Ga),Ja};var Ja,Ga={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Ka=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(ao.geo.azimuthalEqualArea=function(){return oe(Ka)}).raw=Ka;var Qa=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},m);(ao.geo.azimuthalEquidistant=function(){return oe(Qa)}).raw=Qa,(ao.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(ao.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var nl=we(function(n){return 1/n},Math.atan);(ao.geo.gnomonic=function(){return oe(nl)}).raw=nl,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Io]},(ao.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var tl=we(function(){return 1},Math.asin);(ao.geo.orthographic=function(){return oe(tl)}).raw=tl;var el=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(ao.geo.stereographic=function(){return oe(el)}).raw=el,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Io]},(ao.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,ao.geom={},ao.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,i=En(e),u=En(r),o=n.length,a=[],l=[];for(t=0;o>t;t++)a.push([+i.call(this,n[t],t),+u.call(this,n[t],t),t]);for(a.sort(qe),t=0;o>t;t++)l.push([a[t][0],-a[t][1]]);var c=Le(a),f=Le(l),s=f[0]===c[0],h=f[f.length-1]===c[c.length-1],p=[];for(t=c.length-1;t>=0;--t)p.push(n[a[c[t]][2]]);for(t=+s;t=r&&c.x<=u&&c.y>=i&&c.y<=o?[[r,o],[u,o],[u,i],[r,i]]:[];f.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(u(n,t)/Uo)*Uo,y:Math.round(o(n,t)/Uo)*Uo,i:t}})}var r=Ce,i=ze,u=r,o=i,a=sl;return n?t(n):(t.links=function(n){return ar(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return ar(e(n)).cells.forEach(function(e,r){for(var i,u,o=e.site,a=e.edges.sort(Ve),l=-1,c=a.length,f=a[c-1].edge,s=f.l===o?f.r:f.l;++l=c,h=r>=f,p=h<<1|s;n.leaf=!1,n=n.nodes[p]||(n.nodes[p]=hr()),s?i=c:a=c,h?o=f:l=f,u(n,t,e,r,i,o,a,l)}var f,s,h,p,g,v,d,y,m,M=En(a),x=En(l);if(null!=t)v=t,d=e,y=r,m=i;else if(y=m=-(v=d=1/0),s=[],h=[],g=n.length,o)for(p=0;g>p;++p)f=n[p],f.xy&&(y=f.x),f.y>m&&(m=f.y),s.push(f.x),h.push(f.y);else for(p=0;g>p;++p){var b=+M(f=n[p],p),_=+x(f,p);v>b&&(v=b),d>_&&(d=_),b>y&&(y=b),_>m&&(m=_),s.push(b),h.push(_)}var w=y-v,S=m-d;w>S?m=d+w:y=v+S;var k=hr();if(k.add=function(n){u(k,n,+M(n,++p),+x(n,p),v,d,y,m)},k.visit=function(n){pr(n,k,v,d,y,m)},k.find=function(n){return gr(k,n[0],n[1],v,d,y,m)},p=-1,null==t){for(;++p=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=vl.get(e)||gl,r=dl.get(r)||m,br(r(e.apply(null,lo.call(arguments,1))))},ao.interpolateHcl=Rr,ao.interpolateHsl=Dr,ao.interpolateLab=Pr,ao.interpolateRound=Ur,ao.transform=function(n){var t=fo.createElementNS(ao.ns.prefix.svg,"g");return(ao.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:yl)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var yl={a:1,b:0,c:0,d:1,e:0,f:0};ao.interpolateTransform=$r,ao.layout={},ao.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++ea*a/y){if(v>l){var c=t.charge/l;n.px-=u*c,n.py-=o*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=u*c,n.py-=o*c}}return!t.charge}}function t(n){n.px=ao.event.x,n.py=ao.event.y,l.resume()}var e,r,i,u,o,a,l={},c=ao.dispatch("start","tick","end"),f=[1,1],s=.9,h=ml,p=Ml,g=-30,v=xl,d=.1,y=.64,M=[],x=[];return l.tick=function(){if((i*=.99)<.005)return e=null,c.end({type:"end",alpha:i=0}),!0;var t,r,l,h,p,v,y,m,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,p=l.target,m=p.x-h.x,b=p.y-h.y,(v=m*m+b*b)&&(v=i*o[r]*((v=Math.sqrt(v))-u[r])/v,m*=v,b*=v,p.x-=m*(y=h.weight+p.weight?h.weight/(h.weight+p.weight):.5),p.y-=b*y,h.x+=m*(y=1-y),h.y+=b*y);if((y=i*d)&&(m=f[0]/2,b=f[1]/2,r=-1,y))for(;++r<_;)l=M[r],l.x+=(m-l.x)*y,l.y+=(b-l.y)*y;if(g)for(ri(t=ao.geom.quadtree(M),i,a),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*s,l.y-=(l.py-(l.py=l.y))*s);c.tick({type:"tick",alpha:i})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(f=n,l):f},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.friction=function(n){return arguments.length?(s=+n,l):s},l.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(y=n*n,l):Math.sqrt(y)},l.alpha=function(n){return arguments.length?(n=+n,i?n>0?i=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:i=0})):n>0&&(c.start({type:"start",alpha:i=n}),e=qn(l.tick)),l):i},l.start=function(){function n(n,r){if(!e){for(e=new Array(i),l=0;i>l;++l)e[l]=[];for(l=0;c>l;++l){var u=x[l];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var o,a=e[t],l=-1,f=a.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;i>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",s)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof h)for(t=0;c>t;++t)u[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)u[t]=h;if(o=[],"function"==typeof p)for(t=0;c>t;++t)o[t]=+p.call(this,x[t],t);else for(t=0;c>t;++t)o[t]=p;if(a=[],"function"==typeof g)for(t=0;i>t;++t)a[t]=+g.call(this,M[t],t);else for(t=0;i>t;++t)a[t]=g;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=ao.behavior.drag().origin(m).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",ni)),arguments.length?void this.on("mouseover.force",ti).on("mouseout.force",ei).call(r):r},ao.rebind(l,c,"on")};var ml=20,Ml=1,xl=1/0;ao.layout.hierarchy=function(){function n(i){var u,o=[i],a=[];for(i.depth=0;null!=(u=o.pop());)if(a.push(u),(c=e.call(n,u,u.depth))&&(l=c.length)){for(var l,c,f;--l>=0;)o.push(f=c[l]),f.parent=u,f.depth=u.depth+1;r&&(u.value=0),u.children=c}else r&&(u.value=+r.call(n,u,u.depth)||0),delete u.children;return oi(i,function(n){var e,i;t&&(e=n.children)&&e.sort(t),r&&(i=n.parent)&&(i.value+=n.value)}),a}var t=ci,e=ai,r=li;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(ui(t,function(n){n.children&&(n.value=0)}),oi(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},ao.layout.partition=function(){function n(t,e,r,i){var u=t.children;if(t.x=e,t.y=t.depth*i,t.dx=r,t.dy=i,u&&(o=u.length)){var o,a,l,c=-1;for(r=t.value?r/t.value:0;++cs?-1:1),g=ao.sum(c),v=g?(s-l*p)/g:0,d=ao.range(l),y=[];return null!=e&&d.sort(e===bl?function(n,t){return c[t]-c[n]}:function(n,t){return e(o[n],o[t])}),d.forEach(function(n){y[n]={data:o[n],value:a=c[n],startAngle:f,endAngle:f+=a*v+p,padAngle:h}}),y}var t=Number,e=bl,r=0,i=Ho,u=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(i=t,n):i},n.padAngle=function(t){return arguments.length?(u=t,n):u},n};var bl={};ao.layout.stack=function(){function n(a,l){if(!(h=a.length))return a;var c=a.map(function(e,r){return t.call(n,e,r)}),f=c.map(function(t){return t.map(function(t,e){return[u.call(n,t,e),o.call(n,t,e)]})}),s=e.call(n,f,l);c=ao.permute(c,s),f=ao.permute(f,s);var h,p,g,v,d=r.call(n,f,l),y=c[0].length;for(g=0;y>g;++g)for(i.call(n,c[0][g],v=d[g],f[0][g][1]),p=1;h>p;++p)i.call(n,c[p][g],v+=f[p-1][g][1],f[p][g][1]);return a}var t=m,e=gi,r=vi,i=pi,u=si,o=hi;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:_l.get(t)||gi,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:wl.get(t)||vi,n):r},n.x=function(t){return arguments.length?(u=t,n):u},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(i=t,n):i},n};var _l=ao.map({"inside-out":function(n){var t,e,r=n.length,i=n.map(di),u=n.map(yi),o=ao.range(r).sort(function(n,t){return i[n]-i[t]}),a=0,l=0,c=[],f=[];for(t=0;r>t;++t)e=o[t],l>a?(a+=u[e],c.push(e)):(l+=u[e],f.push(e));return f.reverse().concat(c)},reverse:function(n){return ao.range(n.length).reverse()},"default":gi}),wl=ao.map({silhouette:function(n){var t,e,r,i=n.length,u=n[0].length,o=[],a=0,l=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;u>e;++e)l[e]=(a-o[e])/2;return l},wiggle:function(n){var t,e,r,i,u,o,a,l,c,f=n.length,s=n[0],h=s.length,p=[];for(p[0]=l=c=0,e=1;h>e;++e){for(t=0,i=0;f>t;++t)i+=n[t][e][1];for(t=0,u=0,a=s[e][0]-s[e-1][0];f>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;u+=o*n[t][e][1]}p[e]=l-=i?u/i*a:0,c>l&&(c=l)}for(e=0;h>e;++e)p[e]-=c;return p},expand:function(n){var t,e,r,i=n.length,u=n[0].length,o=1/i,a=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];if(r)for(t=0;i>t;t++)n[t][e][1]/=r;else for(t=0;i>t;t++)n[t][e][1]=o}for(e=0;u>e;++e)a[e]=0;return a},zero:vi});ao.layout.histogram=function(){function n(n,u){for(var o,a,l=[],c=n.map(e,this),f=r.call(this,c,u),s=i.call(this,f,c,u),u=-1,h=c.length,p=s.length-1,g=t?1:1/h;++u0)for(u=-1;++u=f[0]&&a<=f[1]&&(o=l[ao.bisect(s,a,1,p)-1],o.y+=g,o.push(n[u]));return l}var t=!0,e=Number,r=bi,i=Mi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(i="number"==typeof t?function(n){return xi(n,t)}:En(t),n):i},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},ao.layout.pack=function(){function n(n,u){var o=e.call(this,n,u),a=o[0],l=i[0],c=i[1],f=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,oi(a,function(n){n.r=+f(n.value)}),oi(a,Ni),r){var s=r*(t?1:Math.max(2*a.r/l,2*a.r/c))/2;oi(a,function(n){n.r+=s}),oi(a,Ni),oi(a,function(n){n.r-=s})}return Ci(a,l/2,c/2,t?1:1/Math.max(2*a.r/l,2*a.r/c)),o}var t,e=ao.layout.hierarchy().sort(_i),r=0,i=[1,1];return n.size=function(t){return arguments.length?(i=t,n):i},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},ii(n,e)},ao.layout.tree=function(){function n(n,i){var f=o.call(this,n,i),s=f[0],h=t(s);if(oi(h,e),h.parent.m=-h.z,ui(h,r),c)ui(s,u);else{var p=s,g=s,v=s;ui(s,function(n){n.xg.x&&(g=n),n.depth>v.depth&&(v=n)});var d=a(p,g)/2-p.x,y=l[0]/(g.x+a(g,p)/2+d),m=l[1]/(v.depth||1);ui(s,function(n){n.x=(n.x+d)*y,n.y=n.depth*m})}return f}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var i,u=t.children,o=0,a=u.length;a>o;++o)r.push((u[o]=i={_:u[o],parent:t,children:(i=u[o].children)&&i.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=i);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Di(n);var u=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-u):n.z=u}else r&&(n.z=r.z+a(n._,r._));n.parent.A=i(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function i(n,t,e){if(t){for(var r,i=n,u=n,o=t,l=i.parent.children[0],c=i.m,f=u.m,s=o.m,h=l.m;o=Ti(o),i=qi(i),o&&i;)l=qi(l),u=Ti(u),u.a=n,r=o.z+s-i.z-c+a(o._,i._),r>0&&(Ri(Pi(o,n,e),n,r),c+=r,f+=r),s+=o.m,c+=i.m,h+=l.m,f+=u.m;o&&!Ti(u)&&(u.t=o,u.m+=s-f),i&&!qi(l)&&(l.t=i,l.m+=c-h,e=n)}return e}function u(n){n.x*=l[0],n.y=n.depth*l[1]}var o=ao.layout.hierarchy().sort(null).value(null),a=Li,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(c=null==(l=t)?u:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:u,n):c?l:null},ii(n,o)},ao.layout.cluster=function(){function n(n,u){var o,a=t.call(this,n,u),l=a[0],c=0;oi(l,function(n){var t=n.children;t&&t.length?(n.x=ji(t),n.y=Ui(t)):(n.x=o?c+=e(n,o):0,n.y=0,o=n)});var f=Fi(l),s=Hi(l),h=f.x-e(f,s)/2,p=s.x+e(s,f)/2;return oi(l,i?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(p-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),a}var t=ao.layout.hierarchy().sort(null).value(null),e=Li,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=null==(r=t),n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=null!=(r=t),n):i?r:null},ii(n,t)},ao.layout.treemap=function(){function n(n,t){for(var e,r,i=-1,u=n.length;++it?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var u=e.children;if(u&&u.length){var o,a,l,c=s(e),f=[],h=u.slice(),g=1/0,v="slice"===p?c.dx:"dice"===p?c.dy:"slice-dice"===p?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),f.area=0;(l=h.length)>0;)f.push(o=h[l-1]),f.area+=o.area,"squarify"!==p||(a=r(f,v))<=g?(h.pop(),g=a):(f.area-=f.pop().area,i(f,v,c,!1),v=Math.min(c.dx,c.dy),f.length=f.area=0,g=1/0);f.length&&(i(f,v,c,!0),f.length=f.area=0),u.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var u,o=s(t),a=r.slice(),l=[];for(n(a,o.dx*o.dy/t.value),l.area=0;u=a.pop();)l.push(u),l.area+=u.area,null!=u.z&&(i(l,u.z?o.dx:o.dy,o,!a.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,i=0,u=1/0,o=-1,a=n.length;++oe&&(u=e),e>i&&(i=e));return r*=r,t*=t,r?Math.max(t*i*g/r,r/(t*u*g)):1/0}function i(n,t,e,r){var i,u=-1,o=n.length,a=e.x,c=e.y,f=t?l(n.area/t):0; +if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++ue.dx)&&(f=e.dx);++ue&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=2*Math.random()-1,r=2*Math.random()-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=ao.random.normal.apply(ao,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=ao.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},ao.scale={};var Sl={floor:m,ceil:m};ao.scale.linear=function(){return Wi([0,1],[0,1],Mr,!1)};var kl={s:1,g:1,p:1,r:1,e:1};ao.scale.log=function(){return ru(ao.scale.linear().domain([0,1]),10,!0,[1,10])};var Nl=ao.format(".0e"),El={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};ao.scale.pow=function(){return iu(ao.scale.linear(),1,[0,1])},ao.scale.sqrt=function(){return ao.scale.pow().exponent(.5)},ao.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},ao.scale.category10=function(){return ao.scale.ordinal().range(Al)},ao.scale.category20=function(){return ao.scale.ordinal().range(Cl)},ao.scale.category20b=function(){return ao.scale.ordinal().range(zl)},ao.scale.category20c=function(){return ao.scale.ordinal().range(Ll)};var Al=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Cl=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),zl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),Ll=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);ao.scale.quantile=function(){return au([],[])},ao.scale.quantize=function(){return lu(0,1,[0,1])},ao.scale.threshold=function(){return cu([.5],[0,1])},ao.scale.identity=function(){return fu([0,1])},ao.svg={},ao.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),f=o.apply(this,arguments)-Io,s=a.apply(this,arguments)-Io,h=Math.abs(s-f),p=f>s?0:1;if(n>c&&(g=c,c=n,n=g),h>=Oo)return t(c,p)+(n?t(n,1-p):"")+"Z";var g,v,d,y,m,M,x,b,_,w,S,k,N=0,E=0,A=[];if((y=(+l.apply(this,arguments)||0)/2)&&(d=u===ql?Math.sqrt(n*n+c*c):+u.apply(this,arguments),p||(E*=-1),c&&(E=tn(d/c*Math.sin(y))),n&&(N=tn(d/n*Math.sin(y)))),c){m=c*Math.cos(f+E),M=c*Math.sin(f+E),x=c*Math.cos(s-E),b=c*Math.sin(s-E);var C=Math.abs(s-f-2*E)<=Fo?0:1;if(E&&yu(m,M,x,b)===p^C){var z=(f+s)/2;m=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else m=M=0;if(n){_=n*Math.cos(s-N),w=n*Math.sin(s-N),S=n*Math.cos(f+N),k=n*Math.sin(f+N);var L=Math.abs(f-s+2*N)<=Fo?0:1;if(N&&yu(_,w,S,k)===1-p^L){var q=(f+s)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Uo&&(g=Math.min(Math.abs(c-n)/2,+i.apply(this,arguments)))>.001){v=c>n^p?0:1;var T=g,R=g;if(Fo>h){var D=null==S?[_,w]:null==x?[m,M]:Re([m,M],[S,k],[x,b],[_,w]),P=m-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(g,(n-O)/(H-1)),T=Math.min(g,(c-O)/(H+1))}if(null!=x){var I=mu(null==S?[_,w]:[S,k],[m,M],c,T,p),Y=mu([x,b],[_,w],c,T,p);g===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-p^yu(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",p," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",m,",",M);if(null!=S){var Z=mu([m,M],[S,k],n,-R,p),V=mu([_,w],null==x?[m,M]:[x,b],n,-R,p);g===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",p^yu(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-p," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",m,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",p," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-p," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hu,r=pu,i=su,u=ql,o=gu,a=vu,l=du;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(i=En(t),n):i},n.padRadius=function(t){return arguments.length?(u=t==ql?ql:En(t),n):u},n.startAngle=function(t){return arguments.length?(o=En(t),n):o},n.endAngle=function(t){return arguments.length?(a=En(t),n):a},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+o.apply(this,arguments)+ +a.apply(this,arguments))/2-Io;return[Math.cos(t)*n,Math.sin(t)*n]},n};var ql="auto";ao.svg.line=function(){return Mu(m)};var Tl=ao.map({linear:xu,"linear-closed":bu,step:_u,"step-before":wu,"step-after":Su,basis:zu,"basis-open":Lu,"basis-closed":qu,bundle:Tu,cardinal:Eu,"cardinal-open":ku,"cardinal-closed":Nu,monotone:Fu});Tl.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Rl=[0,2/3,1/3,0],Dl=[0,1/3,2/3,0],Pl=[0,1/6,2/3,1/6];ao.svg.line.radial=function(){var n=Mu(Hu);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wu.reverse=Su,Su.reverse=wu,ao.svg.area=function(){return Ou(m)},ao.svg.area.radial=function(){var n=Ou(Hu);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},ao.svg.chord=function(){function n(n,a){var l=t(this,u,n,a),c=t(this,o,n,a);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?i(l.r,l.p1,l.r,l.p0):i(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+i(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var i=t.call(n,e,r),u=a.call(n,i,r),o=l.call(n,i,r)-Io,f=c.call(n,i,r)-Io;return{r:u,a0:o,a1:f,p0:[u*Math.cos(o),u*Math.sin(o)],p1:[u*Math.cos(f),u*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Fo)+",1 "+t}function i(n,t,e,r){return"Q 0,0 "+r}var u=Me,o=xe,a=Iu,l=gu,c=vu;return n.radius=function(t){return arguments.length?(a=En(t),n):a},n.source=function(t){return arguments.length?(u=En(t),n):u},n.target=function(t){return arguments.length?(o=En(t),n):o},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},ao.svg.diagonal=function(){function n(n,i){var u=t.call(this,n,i),o=e.call(this,n,i),a=(u.y+o.y)/2,l=[u,{x:u.x,y:a},{x:o.x,y:a},o];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yu;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},ao.svg.diagonal.radial=function(){var n=ao.svg.diagonal(),t=Yu,e=n.projection;return n.projection=function(n){return arguments.length?e(Zu(t=n)):t},n},ao.svg.symbol=function(){function n(n,r){return(Ul.get(t.call(this,n,r))||$u)(e.call(this,n,r))}var t=Xu,e=Vu;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Ul=ao.map({circle:$u,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Fl)),e=t*Fl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});ao.svg.symbolTypes=Ul.keys();var jl=Math.sqrt(3),Fl=Math.tan(30*Yo);Co.transition=function(n){for(var t,e,r=Hl||++Zl,i=Ku(n),u=[],o=Ol||{time:Date.now(),ease:Nr,delay:0,duration:250},a=-1,l=this.length;++au;u++){i.push(t=[]);for(var e=this[u],a=0,l=e.length;l>a;a++)(r=e[a])&&n.call(r,r.__data__,a,u)&&t.push(r)}return Wu(i,this.namespace,this.id)},Yl.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(i){i[r][e].tween.set(n,t)})},Yl.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function i(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function u(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?$r:Mr,a=ao.ns.qualify(n);return Ju(this,"attr."+n,t,a.local?u:i)},Yl.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(i));return r&&function(n){this.setAttribute(i,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(i.space,i.local));return r&&function(n){this.setAttributeNS(i.space,i.local,r(n))}}var i=ao.ns.qualify(n);return this.tween("attr."+n,i.local?r:e)},Yl.style=function(n,e,r){function i(){this.style.removeProperty(n)}function u(e){return null==e?i:(e+="",function(){var i,u=t(this).getComputedStyle(this,null).getPropertyValue(n);return u!==e&&(i=Mr(u,e),function(t){this.style.setProperty(n,i(t),r)})})}var o=arguments.length;if(3>o){if("string"!=typeof n){2>o&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ju(this,"style."+n,e,u)},Yl.styleTween=function(n,e,r){function i(i,u){var o=e.call(this,i,u,t(this).getComputedStyle(this,null).getPropertyValue(n));return o&&function(t){this.style.setProperty(n,o(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,i)},Yl.text=function(n){return Ju(this,"text",n,Gu)},Yl.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Yl.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=ao.ease.apply(ao,arguments)),Y(this,function(r){r[e][t].ease=n}))},Yl.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,i,u){r[e][t].delay=+n.call(r,r.__data__,i,u)}:(n=+n,function(r){r[e][t].delay=n}))},Yl.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,i,u){r[e][t].duration=Math.max(1,n.call(r,r.__data__,i,u))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Yl.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var i=Ol,u=Hl;try{Hl=e,Y(this,function(t,i,u){Ol=t[r][e],n.call(t,t.__data__,i,u)})}finally{Ol=i,Hl=u}}else Y(this,function(i){var u=i[r][e];(u.event||(u.event=ao.dispatch("start","end","interrupt"))).on(n,t)});return this},Yl.transition=function(){for(var n,t,e,r,i=this.id,u=++Zl,o=this.namespace,a=[],l=0,c=this.length;c>l;l++){a.push(n=[]);for(var t=this[l],f=0,s=t.length;s>f;f++)(e=t[f])&&(r=e[o][i],Qu(e,f,o,u,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wu(a,o,u)},ao.svg.axis=function(){function n(n){n.each(function(){var n,c=ao.select(this),f=this.__chart__||e,s=this.__chart__=e.copy(),h=null==l?s.ticks?s.ticks.apply(s,a):s.domain():l,p=null==t?s.tickFormat?s.tickFormat.apply(s,a):m:t,g=c.selectAll(".tick").data(h,s),v=g.enter().insert("g",".domain").attr("class","tick").style("opacity",Uo),d=ao.transition(g.exit()).style("opacity",Uo).remove(),y=ao.transition(g.order()).style("opacity",1),M=Math.max(i,0)+o,x=Zi(s),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),ao.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=y.select("line"),C=g.select("text").text(p),z=v.select("text"),L=y.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=no,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*u+"V0H"+x[1]+"V"+q*u)):(n=to,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*u+","+x[0]+"H0V"+x[1]+"H"+q*u)),E.attr(N,q*i),z.attr(k,q*M),A.attr(S,0).attr(N,q*i),L.attr(w,0).attr(k,q*M),s.rangeBand){var T=s,R=T.rangeBand()/2;f=s=function(n){return T(n)+R}}else f.rangeBand?f=s:d.call(n,s,f);v.call(n,f,s),y.call(n,s,s)})}var t,e=ao.scale.linear(),r=Vl,i=6,u=6,o=3,a=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Xl?t+"":Vl,n):r},n.ticks=function(){return arguments.length?(a=co(arguments),n):a},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(i=+t,u=+arguments[e-1],n):i},n.innerTickSize=function(t){return arguments.length?(i=+t,n):i},n.outerTickSize=function(t){return arguments.length?(u=+t,n):u},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var Vl="bottom",Xl={top:1,right:1,bottom:1,left:1};ao.svg.brush=function(){function n(t){t.each(function(){var t=ao.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=t.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=t.selectAll(".resize").data(v,m);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return $l[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,s=ao.transition(t),h=ao.transition(o);c&&(l=Zi(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(s)),f&&(l=Zi(f),h.attr("y",l[0]).attr("height",l[1]-l[0]),i(s)),e(s)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",s[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1]-s[0])}function i(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function u(){function u(){32==ao.event.keyCode&&(C||(M=null,L[0]-=s[1],L[1]-=h[1],C=2),S())}function v(){32==ao.event.keyCode&&2==C&&(L[0]+=s[1],L[1]+=h[1],C=0,S())}function d(){var n=ao.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(ao.event.altKey?(M||(M=[(s[0]+s[1])/2,(h[0]+h[1])/2]),L[0]=s[+(n[0]f?(i=r,r=f):i=f),v[0]!=r||v[1]!=i?(e?a=null:o=null,v[0]=r,v[1]=i,!0):void 0}function m(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),ao.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=ao.select(ao.event.target),w=l.of(b,arguments),k=ao.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&f,C=_.classed("extent"),z=W(b),L=ao.mouse(b),q=ao.select(t(b)).on("keydown.brush",u).on("keyup.brush",v);if(ao.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",m):q.on("mousemove.brush",d).on("mouseup.brush",m),k.interrupt().selectAll("*").interrupt(),C)L[0]=s[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[s[1-T]-L[0],h[1-R]-L[1]],L[0]=s[T],L[1]=h[R]}else ao.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),ao.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var o,a,l=N(n,"brushstart","brush","brushend"),c=null,f=null,s=[0,0],h=[0,0],p=!0,g=!0,v=Bl[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:s,y:h,i:o,j:a},e=this.__chart__||t;this.__chart__=t,Hl?ao.select(this).transition().each("start.brush",function(){o=e.i,a=e.j,s=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(s,t.x),r=xr(h,t.y);return o=a=null,function(i){s=t.x=e(i),h=t.y=r(i),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){o=t.i,a=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=Bl[!c<<1|!f],n):c},n.y=function(t){return arguments.length?(f=t,v=Bl[!c<<1|!f],n):f},n.clamp=function(t){return arguments.length?(c&&f?(p=!!t[0],g=!!t[1]):c?p=!!t:f&&(g=!!t),n):c&&f?[p,g]:c?p:f?g:null},n.extent=function(t){var e,r,i,u,l;return arguments.length?(c&&(e=t[0],r=t[1],f&&(e=e[0],r=r[0]),o=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),e==s[0]&&r==s[1]||(s=[e,r])),f&&(i=t[0],u=t[1],c&&(i=i[1],u=u[1]),a=[i,u],f.invert&&(i=f(i),u=f(u)),i>u&&(l=i,i=u,u=l),i==h[0]&&u==h[1]||(h=[i,u])),n):(c&&(o?(e=o[0],r=o[1]):(e=s[0],r=s[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),f&&(a?(i=a[0],u=a[1]):(i=h[0],u=h[1],f.invert&&(i=f.invert(i),u=f.invert(u)),i>u&&(l=i,i=u,u=l))),c&&f?[[e,i],[r,u]]:c?[e,r]:f&&[i,u])},n.clear=function(){return n.empty()||(s=[0,0],h=[0,0],o=a=null),n},n.empty=function(){return!!c&&s[0]==s[1]||!!f&&h[0]==h[1]},ao.rebind(n,l,"on")};var $l={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Bl=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Wl=ga.format=xa.timeFormat,Jl=Wl.utc,Gl=Jl("%Y-%m-%dT%H:%M:%S.%LZ");Wl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?eo:Gl,eo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},eo.toString=Gl.toString,ga.second=On(function(n){return new va(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ga.seconds=ga.second.range,ga.seconds.utc=ga.second.utc.range,ga.minute=On(function(n){return new va(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ga.minutes=ga.minute.range,ga.minutes.utc=ga.minute.utc.range,ga.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new va(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ga.hours=ga.hour.range,ga.hours.utc=ga.hour.utc.range,ga.month=On(function(n){return n=ga.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ga.months=ga.month.range,ga.months.utc=ga.month.utc.range;var Kl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Ql=[[ga.second,1],[ga.second,5],[ga.second,15],[ga.second,30],[ga.minute,1],[ga.minute,5],[ga.minute,15],[ga.minute,30],[ga.hour,1],[ga.hour,3],[ga.hour,6],[ga.hour,12],[ga.day,1],[ga.day,2],[ga.week,1],[ga.month,1],[ga.month,3],[ga.year,1]],nc=Wl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),tc={range:function(n,t,e){return ao.range(Math.ceil(n/e)*e,+t,e).map(io)},floor:m,ceil:m};Ql.year=ga.year,ga.scale=function(){return ro(ao.scale.linear(),Ql,nc)};var ec=Ql.map(function(n){return[n[0].utc,n[1]]}),rc=Jl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);ec.year=ga.year.utc,ga.scale.utc=function(){return ro(ao.scale.linear(),ec,rc)},ao.text=An(function(n){return n.responseText}),ao.json=function(n,t){return Cn(n,"application/json",uo,t)},ao.html=function(n,t){return Cn(n,"text/html",oo,t)},ao.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=ao,define(ao)):"object"==typeof module&&module.exports?module.exports=ao:this.d3=ao}(); \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/testdata/group_T1w.tsv b/pydra/tasks/mriqc/data/testdata/group_T1w.tsv new file mode 100644 index 0000000..48ffc78 --- /dev/null +++ b/pydra/tasks/mriqc/data/testdata/group_T1w.tsv @@ -0,0 +1,5 @@ +bids_name cjv cnr efc fber fwhm_avg fwhm_x fwhm_y fwhm_z icvs_csf icvs_gm icvs_wm inu_med inu_range qi_1 qi_2 rpve_csf rpve_gm rpve_wm size_x size_y size_z snr_csf snr_gm snr_total snr_wm snrd_csf snrd_gm snrd_total snrd_wm spacing_x spacing_y spacing_z summary_bg_k summary_bg_mad summary_bg_mean summary_bg_median summary_bg_n summary_bg_p05 summary_bg_p95 summary_bg_stdv summary_csf_k summary_csf_mad summary_csf_mean summary_csf_median summary_csf_n summary_csf_p05 summary_csf_p95 summary_csf_stdv summary_gm_k summary_gm_mad summary_gm_mean summary_gm_median summary_gm_n summary_gm_p05 summary_gm_p95 summary_gm_stdv summary_wm_k summary_wm_mad summary_wm_mean summary_wm_median summary_wm_n summary_wm_p05 summary_wm_p95 summary_wm_stdv tpm_overlap_csf tpm_overlap_gm tpm_overlap_wm wm2max +sub-50137_T1w 0.5954143604542232 1.6638579105552882 0.696013425223169 7632.404958677687 4.202703333333333 4.04437 4.57298 3.99076 0.26024785652744636 0.38408797703315845 0.35566416643939525 0.6877774000167847 0.38117922544479355 0.0 0.002295668304182044 5.081276724413724 4.990756260234296 5.113134802367116 208 256 176 2.2937107725306363 4.917917779273032 5.414127031616626 9.030752543046212 10.197292417890393 14.569689441911478 15.229464400494718 20.921411341682283 1.0 1.0 1.0 338.43701049804054 0.0 7.293577983070605 0.0 2905640.0 0.0 43.53645095974207 29.419580835323824 1.8447784153324047 148.1785165093192 442.67347918309935 457.92001267150044 467343.9921919382 108.17194144427776 741.1431271880865 199.64135332045623 -0.35434565463924894 93.55264354645082 663.4221231418237 654.2670446671546 689731.7462465676 459.0615651831031 912.2579122893512 133.037313704897 2.017341484779524 76.27779483169759 922.4739051832804 939.4977170489728 638689.2620031061 723.2325619198382 1061.9193829484284 104.03307776196522 0.24142527697392707 0.5351267249526063 0.5492788818473785 0.4052328641526733 +sub-50152_T1w 0.539219705574777 1.9474863519286423 0.6407575000929341 6400.0 3.538223004967018 3.5068090149010525 3.72369 3.38417 0.24603987215843207 0.3997439898531474 0.3542161379884206 0.6055886149406433 0.29949661493301377 0.0 0.007373185025089607 5.331492928503499 5.259937594008643 5.372550867202498 160 239 200 2.8606218342450362 7.6336456345923995 6.389366594664442 8.673832315155893 25.101236197040226 31.375238888160823 33.55424426478266 44.186257709146936 1.100000023841858 1.0 1.0 6.414540030448752 0.0 7.885408184004529 0.0 2096437.0 0.0 38.87942571938038 14.239021165535616 0.04906040355413355 177.41835136541522 525.5333175241614 545.5612502843142 344069.29238630924 191.59568993747234 808.0636241286993 190.7139388176008 -0.5716823144664929 70.32748948461413 686.3957146724873 681.9231698960066 559013.587992302 545.7505366802216 840.583026945591 89.3311783915161 -0.37782611205030214 79.81300083569138 933.452586370995 960.363458275795 495346.1196362858 711.8304204493761 1075.9795888960361 110.71951289754341 0.24415403242557018 0.5240272610492305 0.5342199967075586 0.4752163650668765 +sub-50785_T1w 0.7461341169008633 1.3397828655659076 0.7530480076783983 -1.0 3.499472057939973 3.46213 3.7065461738199192 3.32974 0.2670508857178007 0.3771657734932426 0.35578334078895674 0.9645677804946899 0.27349820435047145 0.0 0.0008989212653515643 7.277978198643724 7.159746223301595 7.305039410577692 256 180 256 2.3324453804558374 3.7071785736699265 3.8942598735683753 5.643155666579362 6.224456192836066 8.970749163248403 9.610299796084766 13.63569403216983 1.0 0.9999875426292419 1.0 579.4582204165845 0.0 2.7989928403802895 0.0 5080237.0 0.0 0.0 41.37895519541185 0.4214879703475489 118.70755566859489 386.1200298164586 393.141785312444 423407.57698828075 97.19213696569204 657.170887414366 168.55328075295242 -0.22318958644717712 105.67941658296553 572.463234916038 566.5999136902392 597994.0708619782 330.8538051880896 835.5335811562836 152.83845347074578 0.8425022802147404 114.1628559875614 847.082864043662 861.2416779398918 564092.3521048883 579.310912258923 1077.3695249855518 152.6168983168376 0.24019729935470005 0.5355084318272595 0.5571234404197298 0.3494179683613571 +sub-51187_T1w 0.6999440574229334 1.229109740105771 0.6068733970705132 4668.127902703063 3.5394978356516567 4.25443345257453 2.965211919410108 3.398848134970332 0.2632767380846025 0.38411410306597654 0.352609158849421 0.8645452558994293 0.659174335002899 0.0 0.002621822353053779 6.232708393405123 6.104914216952696 6.241270182398587 256 132 256 1.9045372860088077 3.542878319529499 3.517728319831832 5.105769353957191 5.3904165105611055 7.623761622201767 8.185965301979987 11.543717773177091 0.8593999743461609 1.5000007152557373 0.8593999743461609 643.4203151583965 0.0 12.526359964354464 0.0 2453296.0 0.0 63.92100214585662 49.63941944985638 19.631055549488405 198.10879423073186 395.1546922405064 408.42967384681106 355951.2030053217 38.246628262102604 723.3140679895878 214.45056661846712 18.305382974842022 102.98229317978202 587.9801981000472 577.6493276059628 519323.80381326505 337.8612728342414 879.2389240153134 163.04504963273772 10.92217792214386 104.91053306594992 844.5959841770807 874.6628160998225 476728.99321174936 544.9421983994544 1059.6339149996638 171.30854100624651 0.23533804206716807 0.5463730577598676 0.5594191519575634 0.31519791549296117 diff --git a/pydra/tasks/mriqc/data/testdata/group_bold.tsv b/pydra/tasks/mriqc/data/testdata/group_bold.tsv new file mode 100644 index 0000000..5f91355 --- /dev/null +++ b/pydra/tasks/mriqc/data/testdata/group_bold.tsv @@ -0,0 +1,10 @@ +bids_name aor aqi dummy_trs dvars_nstd dvars_std dvars_vstd efc fber fd_mean fd_num fd_perc fwhm_avg fwhm_x fwhm_y fwhm_z gcor gsr_x gsr_y size_t size_x size_y size_z snr spacing_tr spacing_x spacing_y spacing_z summary_bg_k summary_bg_mad summary_bg_mean summary_bg_median summary_bg_n summary_bg_p05 summary_bg_p95 summary_bg_stdv summary_fg_k summary_fg_mad summary_fg_mean summary_fg_median summary_fg_n summary_fg_p05 summary_fg_p95 summary_fg_stdv tsnr +sub-ds205s03_task-functionallocalizer_run-01_bold 0.006865975609756098 0.01166174756097561 0 25.30130857259259 1.10068032382716 1.0422690707407405 0.5578 899.5551 0.26118389619646404 32 39.02439024390244 2.43764 2.3422275 2.692425 2.2782675 0.0529285 -0.012628363445401192 0.055389221757650375 82 53 53 27 4.266440439452425 2.200000047683716 4.0 4.0 4.0 35.3128 6.4186 50.9335 25.0 54995.0 19.0 173.0 77.1159 1.403 141.8145 759.3992 764.0 20848.0 428.0 1042.0 179.0677 53.9325022965204 +sub-ds205s03_task-view_run-01_bold 0.0055223913043478245 0.013993418152173915 0 21.883337964175826 1.0119163635164834 0.9670224290109894 0.5552 948.5766 0.16520736608810047 18 19.565217391304348 2.3244433333333334 2.2259025 2.592175 2.1552525 0.0183055 -0.008478646166622639 0.04804554581642151 92 53 53 27 4.254223958782439 2.200000047683716 4.0 4.0 4.0 34.6153 6.4461 47.5813 25.0 54686.0 19.0 157.0 67.2752 1.6046 136.8667 771.5826 779.0 21157.0 419.0 1054.0 183.1078 52.836231373017654 +sub-ds205s03_task-view_run-02_bold 0.0006475 0.01044403706521739 0 24.326057035714285 0.9611447131868133 0.8934934421978024 0.5573 902.4783 0.22854716046970905 44 47.82608695652174 2.3925758333333333 2.3057225 2.642175 2.22983 0.329435 -0.011322595179080963 0.053197648376226425 92 53 53 27 4.259133013458754 2.200000047683716 4.0 4.0 4.0 35.3124 7.4936 49.9201 26.0 54896.0 19.0 170.0 73.8075 1.461 140.8794 762.1778 767.0 20947.0 428.0 1044.0 180.0793 42.17791306972504 +sub-ds205s07_task-functionallocalizer_run-01_bold 0.00972736111111111 0.008194284305555556 0 22.879393483661968 1.1479378432394367 1.0417766205633798 0.541 962.3474 0.1550487344317312 15 20.833333333333332 2.2521649999999998 2.1681675 2.39802 2.1903075 0.0291203 -0.007456877268850803 0.03371790051460266 72 53 53 27 4.285941229350169 2.200000047683716 4.0 4.0 4.0 40.9527 6.1157 42.9513 23.0 55908.0 17.0 145.0 67.5711 1.7787 137.3466 708.532 714.0 19935.0 387.0 951.0 166.587 64.71407310082577 +sub-ds205s07_task-view_run-01_bold 0.00585054347826087 0.015204289673913043 0 22.382260123296703 0.9959491859340658 0.9369370696703297 0.5386 972.983 0.1520993052375479 15 16.304347826086957 2.2514825000000003 2.174985 2.38338 2.1960825 0.0282586 -0.006972935516387224 0.032188184559345245 92 53 53 27 4.313751528981998 2.200000047683716 4.0 4.0 4.0 41.5907 6.156 41.8577 23.0 56005.0 17.0 139.0 65.5813 1.8801 136.4316 706.7871 713.0 19838.0 394.0 946.0 165.2812 50.42334433761425 +sub-ds205s07_task-view_run-02_bold 0.0019870652173913047 0.014770980326086956 0 22.60577535087912 1.0123932324175822 0.9372968517582421 0.5404 965.007 0.15451170465747024 18 19.565217391304348 2.2389508333333334 2.1558375 2.3787425 2.1822725 0.0275085 -0.007005929946899414 0.03411094844341278 92 53 53 27 4.365633569269246 2.200000047683716 4.0 4.0 4.0 38.59 6.1883 42.5556 23.0 55958.0 17.0 143.0 65.669 1.7675 138.1076 711.3101 717.0 19885.0 400.0 952.0 164.2332 54.05953362467699 +sub-ds205s09_task-view_acq-LR_run-01_bold 0.005607945205479453 0.02232912082191781 0 23.14330828208334 1.043644975 0.9879293138888892 0.5131 1196.7195 0.3258302280827085 45 61.64383561643836 2.0490825 2.0376875 2.24239 1.86717 0.0367789 -0.004032289143651724 0.020073510706424713 73 53 53 27 5.010099661656575 2.200000047683716 4.0 4.0 4.0 42.9673 5.1654 39.4401 23.0 57775.0 18.0 124.0 58.7775 2.6347 119.7322 783.8782 805.0 18068.0 448.0 993.0 160.671 54.77862358093262 +sub-ds205s09_task-view_acq-LR_run-02_bold 0.006578767123287671 0.023950454794520546 0 35.81903538416666 1.5608953040277778 1.6171545502777782 0.5136 1186.884 0.3345943377548133 47 64.38356164383562 2.054238333333333 2.038455 2.2515525 1.8727075 0.0322103 -0.0038295735139399767 0.020550260320305824 73 53 53 27 5.029289310984106 2.200000047683716 4.0 4.0 4.0 44.2484 5.2449 39.9385 23.0 57802.0 18.0 126.0 59.851 2.6451 118.8398 785.5312 807.0 18041.0 451.0 994.0 160.4556 50.310420989990234 +sub-ds205s09_task-view_acq-RL_run-01_bold 0.04132 0.05151956945205479 0 34.941844608194444 1.1900752381944446 1.11412176375 0.5122 1214.6777 0.5699059645824405 53 72.6027397260274 2.103911666666667 2.0650525 2.28995 1.9567325 0.0259984 -0.003200419247150421 0.01928347535431385 73 53 53 27 5.178320854531057 2.200000047683716 4.0 4.0 4.0 42.1191 5.0721 39.7567 23.0 57912.0 18.0 126.0 60.2804 2.5073 116.7328 789.3764 810.0 17931.0 460.0 993.0 156.417 36.2927360534668 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/CHANGES b/pydra/tasks/mriqc/data/tests/ds000005/CHANGES new file mode 100644 index 0000000..b5f26f4 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds000005/CHANGES @@ -0,0 +1,12 @@ +2.0.1 2016-10-21 + + - Added authors to dataset_discription.json + +1.0.1 2016-02-18 + + - Update orientation information in nifti header for improved left-right determination + + +1.0.0 2011-10-06 + + - initial release diff --git a/pydra/tasks/mriqc/data/tests/ds000005/README b/pydra/tasks/mriqc/data/tests/ds000005/README new file mode 100644 index 0000000..d9684e7 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds000005/README @@ -0,0 +1,22 @@ +This dataset was obtained from the OpenfMRI project (http://www.openfmri.org). +Accession #: ds005 +Description: Mixed-gambles task + +Please cite the following references if you use these data: + +Tom, S.M., Fox, C.R., Trepel, C., Poldrack, R.A. (2007). The neural basis of loss aversion in decision-making under risk. Science, 315(5811):515-8 + + +Release history: +10/06/2011: initial release +3/21/2013: Updated release with QA information +2/18/2016: Update orientation information in nifti header for improved left-right determination + +This dataset is made available under the Public Domain Dedication and License +v1.0, whose full text can be found at +https://opendatacommons.org/licenses/pddl/1-0/. +We hope that all users will follow the ODC Attribution/Share-Alike +Community Norms (https://opendatacommons.org/norms/odc-by-sa/); +in particular, while not legally required, we hope that all users +of the data will acknowledge the OpenfMRI project and NSF Grant +OCI-1131441 (R. Poldrack, PI) in any publications. diff --git a/pydra/tasks/mriqc/data/tests/ds000005/dataset_description.json b/pydra/tasks/mriqc/data/tests/ds000005/dataset_description.json new file mode 100644 index 0000000..1892444 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds000005/dataset_description.json @@ -0,0 +1,7 @@ +{ + "BIDSVersion": "1.0.0rc4", + "License": "This dataset is made available under the Public Domain Dedication and License \nv1.0, whose full text can be found at \nhttp://www.opendatacommons.org/licenses/pddl/1.0/. \n We hope that all users will follow the ODC Attribution/Share-Alike \nCommunity Norms (http://www.opendatacommons.org/norms/odc-by-sa/); \n in particular, while not legally required, we hope that all users \nof the data will acknowledge the OpenfMRI project and NSF Grant \nOCI-1131441 (R. Poldrack, PI) in any publications.", + "Name": "Mixed-gambles task", + "Authors": ["Tom, S.M.", "Fox, C.R.", "Trepel, C.", "Poldrack, R.A."], + "ReferencesAndLinks": "Tom, S.M., Fox, C.R., Trepel, C., Poldrack, R.A. (2007). The neural basis of loss aversion in decision-making under risk. Science, 315(5811):515-8" +} diff --git a/pydra/tasks/mriqc/data/tests/ds000005/participants.tsv b/pydra/tasks/mriqc/data/tests/ds000005/participants.tsv new file mode 100644 index 0000000..48a1c92 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds000005/participants.tsv @@ -0,0 +1,17 @@ +participant_id sex age +sub-01 M 28 +sub-02 F 21 +sub-03 F 27 +sub-04 M 25 +sub-05 F 20 +sub-06 M 20 +sub-07 F 24 +sub-08 M 25 +sub-09 F 19 +sub-10 M 20 +sub-11 M 20 +sub-12 M 21 +sub-13 F 22 +sub-14 F 19 +sub-15 F 20 +sub-16 M 22 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/anat/sub-01_T1w.nii.gz b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/anat/sub-01_T1w.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..58f50bb603c9350e9d23e670c51d2e14ed34d66d GIT binary patch literal 96 zcmV-m0H6OKiwFn-xnf=d|8sR>Eif@(R55ohZfR)%i(zCS3NSOUfFUCT1fptkU~m8n zG}tpR;KLi7A@ZoY2ad;t$7+y2&;hYx3=DbNhDe$}K+PJZA%QY-!T|t|_2nmE0{{SC C?IU9V literal 0 HcmV?d00001 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/anat/sub-01_inplaneT2.nii.gz b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/anat/sub-01_inplaneT2.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..b35a0911446fee7170321b8e943e6d8dc931957c GIT binary patch literal 102 zcmV-s0GaEif@(X>M?AVQytqGA?duX#k61WFQJKGq8XmBLf7Y zYI0z301GtOGce%88=N8XsJaJ^$ArgfkU!7?v0@AidD@0Znm<6z8l@qDGIGKJ0FL$L ICtw2r07;-F)Bpeg literal 0 HcmV?d00001 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.json b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.json new file mode 100644 index 0000000..3a93d7c --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.json @@ -0,0 +1,3 @@ +{ + "EchoTime": 0.030 +} \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.nii.gz b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_bold.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..44d478a9c2ab67eb481730c9e1c5e674067149bb GIT binary patch literal 95 zcmb2|=3oE;mjB&}DGFR03>*}+nUy6LWfdh9FmRjPFgAE_SbC1)rxm-{`;NC9&zfL= lfpKFJ4^QQ-wimP9e=b%ukYYk3loePQ>iN&|CvY<`007wQ9zy^C literal 0 HcmV?d00001 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_events.tsv b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-01_events.tsv new file mode 100644 index 0000000..e69de29 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.json b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.json new file mode 100644 index 0000000..3a93d7c --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.json @@ -0,0 +1,3 @@ +{ + "EchoTime": 0.030 +} \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.nii.gz b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_bold.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..44d478a9c2ab67eb481730c9e1c5e674067149bb GIT binary patch literal 95 zcmb2|=3oE;mjB&}DGFR03>*}+nUy6LWfdh9FmRjPFgAE_SbC1)rxm-{`;NC9&zfL= lfpKFJ4^QQ-wimP9e=b%ukYYk3loePQ>iN&|CvY<`007wQ9zy^C literal 0 HcmV?d00001 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_events.tsv b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-02_events.tsv new file mode 100644 index 0000000..e69de29 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_bold.json b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_bold.json new file mode 100644 index 0000000..3a93d7c --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_bold.json @@ -0,0 +1,3 @@ +{ + "EchoTime": 0.030 +} \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_bold.nii.gz b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_bold.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..44d478a9c2ab67eb481730c9e1c5e674067149bb GIT binary patch literal 95 zcmb2|=3oE;mjB&}DGFR03>*}+nUy6LWfdh9FmRjPFgAE_SbC1)rxm-{`;NC9&zfL= lfpKFJ4^QQ-wimP9e=b%ukYYk3loePQ>iN&|CvY<`007wQ9zy^C literal 0 HcmV?d00001 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_events.tsv b/pydra/tasks/mriqc/data/tests/ds000005/sub-01/func/sub-01_task-mixedgamblestask_run-03_events.tsv new file mode 100644 index 0000000..e69de29 diff --git a/pydra/tasks/mriqc/data/tests/ds000005/task-mixedgamblestask_bold.json b/pydra/tasks/mriqc/data/tests/ds000005/task-mixedgamblestask_bold.json new file mode 100644 index 0000000..e1e8666 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds000005/task-mixedgamblestask_bold.json @@ -0,0 +1,4 @@ +{ + "RepetitionTime": 2.0, + "TaskName": "mixed-gambles task" +} \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/ds002785/dataset_description.json b/pydra/tasks/mriqc/data/tests/ds002785/dataset_description.json new file mode 100644 index 0000000..97312a8 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/dataset_description.json @@ -0,0 +1,20 @@ +{ + "Name": "MRIQC - MRI Quality Control", + "BIDSVersion": "1.4.0", + "DatasetType": "derivative", + "GeneratedBy": [ + { + "Name": "MRIQC", + "Version": "23.1.0.dev9+g936aae7", + "CodeURL": "https://github.com/nipreps/mriqc/archive/23.1.0.dev9+g936aae7.tar.gz" + } + ], + "HowToAcknowledge": "Please cite our paper (https://doi.org/10.1371/journal.pone.0184661).", + "SourceDatasets": [ + { + "URL": "https://doi.org/10.18112/openneuro.ds002785.v2.0.0", + "DOI": "10.18112/openneuro.ds002785.v2.0.0" + } + ], + "License": "CC0" +} \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/anat/sub-0017_T1w.json b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/anat/sub-0017_T1w.json new file mode 100644 index 0000000..d37af77 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/anat/sub-0017_T1w.json @@ -0,0 +1,114 @@ +{ + "bids_meta": { + "AcquisitionNumber": 4, + "BidsifyVersion": "0.3.5", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20181125 GCC6.3.0", + "EchoTime": 0.00386, + "ImageComments": "PIop", + "ImageOrientationPatientDICOM": [ + 0.997822, + -0.0345204, + 0.056213, + 0.0465325, + 0.972345, + -0.228868 + ], + "Manufacturer": "Philips", + "PatientPosition": "HFS", + "PhilipsRescaleIntercept": 0, + "PhilipsRescaleSlope": 9.97753, + "PhilipsScaleSlope": 0.00757456, + "ProtocolName": "WIPt13dSENSE", + "ReconMatrixPE": 240, + "RepetitionTime": 0.008384, + "SeriesDescription": "ImageMRSERIES", + "SeriesNumber": 4, + "UsePhilipsFloatNotDisplayScaling": 1, + "dataset": "", + "modality": "T1w", + "subject_id": "0017" + }, + "cjv": 0.2984226700776387, + "cnr": 4.416750094393364, + "efc": 0.7655080888081706, + "fber": -1.0, + "fwhm_avg": 3.8418100772965005, + "fwhm_x": 3.8904602318895, + "fwhm_y": 3.87533, + "fwhm_z": 3.75964, + "icvs_csf": 0.21427377046371318, + "icvs_gm": 0.45182687552401934, + "icvs_wm": 0.33389935401226745, + "inu_med": 0.6728797554969788, + "inu_range": 0.2371561408042907, + "provenance": { + "md5sum": "488fdd8468ee6f229187a832f75df8d3", + "settings": { + "testing": false + }, + "software": "mriqc", + "version": "23.1.0.dev9+g936aae7", + "warnings": { + "large_rot_frame": true, + "small_air_mask": true + }, + "webapi_port": null, + "webapi_url": "https://mriqc.nimh.nih.gov/api/v1" + }, + "qi_1": 0.0, + "qi_2": 0.00017117314810884092, + "rpve_csf": 29.107574622378852, + "rpve_gm": 15.038577634337337, + "rpve_wm": 23.878723605719156, + "size_x": 240, + "size_y": 240, + "size_z": 220, + "snr_csf": 1.2982109108596243, + "snr_gm": 7.2667769595632645, + "snr_total": 9.787154109403472, + "snr_wm": 20.796474457787525, + "snrd_csf": 16.969258904499053, + "snrd_gm": 67.7554225958473, + "snrd_total": 66.84473903463918, + "snrd_wm": 115.80953560357119, + "spacing_x": 0.9999999403953552, + "spacing_y": 1.0, + "spacing_z": 1.0, + "summary_bg_k": 80.2823504456352, + "summary_bg_mad": 0.0, + "summary_bg_mean": 1.4847520366725901, + "summary_bg_median": 0.0, + "summary_bg_n": 94563.0, + "summary_bg_p05": 0.0, + "summary_bg_p95": 13.871251210570335, + "summary_bg_stdv": 5.657015981655371, + "summary_csf_k": 0.8963167168111004, + "summary_csf_mad": 108.8018859045977, + "summary_csf_mean": 173.34180474016256, + "summary_csf_median": 146.52730152010918, + "summary_csf_n": 13872.0, + "summary_csf_p05": 32.577903371304274, + "summary_csf_p95": 380.9685418305918, + "summary_csf_stdv": 112.8645729243335, + "summary_gm_k": 0.05707251778228173, + "summary_gm_mad": 78.93115183761164, + "summary_gm_mean": 586.1216061525913, + "summary_gm_median": 585.0590937528759, + "summary_gm_n": 31773.0, + "summary_gm_p05": 454.25905592925847, + "summary_gm_p95": 721.002239859849, + "summary_gm_stdv": 80.51023033684723, + "summary_wm_k": 0.7997106523111079, + "summary_wm_mad": 44.89661847643965, + "summary_wm_mean": 998.5774157832561, + "summary_wm_median": 999.9999904409051, + "summary_wm_n": 162663.0, + "summary_wm_p05": 917.7981742881238, + "summary_wm_p95": 1074.8754415176809, + "summary_wm_stdv": 48.084925097579145, + "tpm_overlap_csf": 0.16996463260514408, + "tpm_overlap_gm": 0.47678360376698237, + "tpm_overlap_wm": 0.5159290584796369, + "wm2max": 0.7163876204972095 +} \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-airmask_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-airmask_T1w.svg new file mode 100644 index 0000000..135aac7 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-airmask_T1w.svg @@ -0,0 +1,5634 @@ + + + + + + + + 2023-03-14T13:44:20.451451 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-artifacts_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-artifacts_T1w.svg new file mode 100644 index 0000000..0be4db1 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-artifacts_T1w.svg @@ -0,0 +1,934 @@ + + + + + + + + 2023-03-14T13:44:23.278788 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-background_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-background_T1w.svg new file mode 100644 index 0000000..b8c3e4a --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-background_T1w.svg @@ -0,0 +1,7206 @@ + + + + + + + + 2023-03-14T13:41:06.653847 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-brainmask_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-brainmask_T1w.svg new file mode 100644 index 0000000..ba0372e --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-brainmask_T1w.svg @@ -0,0 +1,4564 @@ + + + + + + + + 2023-03-14T13:42:19.686912 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-head_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-head_T1w.svg new file mode 100644 index 0000000..e3a048e --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-head_T1w.svg @@ -0,0 +1,4298 @@ + + + + + + + + 2023-03-14T13:43:26.629493 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-noisefit_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-noisefit_T1w.svg new file mode 100644 index 0000000..6dfd683 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-noisefit_T1w.svg @@ -0,0 +1,1324 @@ + + + + + + + + 2023-03-14T13:44:24.318335 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-norm_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-norm_T1w.svg new file mode 100644 index 0000000..0687dc3 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-norm_T1w.svg @@ -0,0 +1,19 @@ + + + + + 2023-03-14T13:42:43.338780 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + 2023-03-14T13:42:44.498417 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + 2023-03-14T13:42:45.747623 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + + + 2023-03-14T13:42:46.934786 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + 2023-03-14T13:42:48.087183 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + 2023-03-14T13:42:49.358157 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + + + \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-segmentation_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-segmentation_T1w.svg new file mode 100644 index 0000000..f2b9a80 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-segmentation_T1w.svg @@ -0,0 +1,34802 @@ + + + + + + + + 2023-03-14T13:45:18.986079 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-zoomed_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-zoomed_T1w.svg new file mode 100644 index 0000000..a26118a --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0017/figures/sub-0017_desc-zoomed_T1w.svg @@ -0,0 +1,7167 @@ + + + + + + + + 2023-03-14T13:42:20.713015 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/anat/sub-0042_T1w.json b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/anat/sub-0042_T1w.json new file mode 100644 index 0000000..2ea89b3 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/anat/sub-0042_T1w.json @@ -0,0 +1,114 @@ +{ + "bids_meta": { + "AcquisitionNumber": 4, + "BidsifyVersion": "0.3.5", + "ConversionSoftware": "dcm2niix", + "ConversionSoftwareVersion": "v1.0.20181125 GCC6.3.0", + "EchoTime": 0.00374, + "ImageComments": "Piop", + "ImageOrientationPatientDICOM": [ + 0.999933, + 0.0107105, + -0.00429963, + -0.0108207, + 0.999591, + -0.0264809 + ], + "Manufacturer": "Philips", + "PatientPosition": "HFS", + "PhilipsRescaleIntercept": 0, + "PhilipsRescaleSlope": 9.6315, + "PhilipsScaleSlope": 0.00738806, + "ProtocolName": "WIPt13dSENSE", + "ReconMatrixPE": 240, + "RepetitionTime": 0.008172, + "SeriesDescription": "ImageMRSERIES", + "SeriesNumber": 4, + "UsePhilipsFloatNotDisplayScaling": 1, + "dataset": "", + "modality": "T1w", + "subject_id": "0042" + }, + "cjv": 0.32579099188384514, + "cnr": 4.02557353023444, + "efc": 0.7552281551403719, + "fber": -1.0, + "fwhm_avg": 3.8609668158203454, + "fwhm_x": 3.8525202296281, + "fwhm_y": 4.07575, + "fwhm_z": 3.654630217832936, + "icvs_csf": 0.21199491574747537, + "icvs_gm": 0.42802186419053817, + "icvs_wm": 0.35998322006198646, + "inu_med": 0.6003086566925049, + "inu_range": 0.2056518077850341, + "provenance": { + "md5sum": "f60ae921c83c8c7c9d37a4f1c24a8414", + "settings": { + "testing": false + }, + "software": "mriqc", + "version": "23.1.0.dev9+g936aae7", + "warnings": { + "large_rot_frame": true, + "small_air_mask": true + }, + "webapi_port": null, + "webapi_url": "https://mriqc.nimh.nih.gov/api/v1" + }, + "qi_1": 0.0, + "qi_2": 0.00011642773748205091, + "rpve_csf": 24.61195525783125, + "rpve_gm": 12.89228853719876, + "rpve_wm": 18.582825144785794, + "size_x": 240, + "size_y": 240, + "size_z": 220, + "snr_csf": 1.551612537932502, + "snr_gm": 6.919063648216913, + "snr_total": 10.14424221136297, + "snr_wm": 21.962050447939493, + "snrd_csf": 14.07344638138718, + "snrd_gm": 47.16267312978192, + "snrd_total": 46.49048740518072, + "snrd_wm": 78.23534270437307, + "spacing_x": 0.9999999403953552, + "spacing_y": 1.0, + "spacing_z": 0.9999999403953552, + "summary_bg_k": 107.19628229499303, + "summary_bg_mad": 0.0, + "summary_bg_mean": 1.733096121297343, + "summary_bg_median": 0.0, + "summary_bg_n": 60935.0, + "summary_bg_p05": 0.0, + "summary_bg_p95": 12.45789003930986, + "summary_bg_stdv": 8.373729660228735, + "summary_csf_k": 0.44779374386824866, + "summary_csf_mad": 124.78208741269273, + "summary_csf_mean": 198.5499556123926, + "summary_csf_median": 179.88198394328356, + "summary_csf_n": 16060.0, + "summary_csf_p05": 38.84103320967406, + "summary_csf_p95": 399.40312000438564, + "summary_csf_stdv": 115.92867365260699, + "summary_gm_k": -0.0073664596272493554, + "summary_gm_mad": 87.15471518415075, + "summary_gm_mean": 603.3587115234336, + "summary_gm_median": 602.8171764574945, + "summary_gm_n": 32923.0, + "summary_gm_p05": 459.3140401970595, + "summary_gm_p95": 747.6067990032956, + "summary_gm_stdv": 87.12277442167746, + "summary_wm_k": 0.6531345168735507, + "summary_wm_mad": 42.23651582001151, + "summary_wm_mean": 998.386644906707, + "summary_wm_median": 999.9774240627885, + "summary_wm_n": 224894.0, + "summary_wm_p05": 921.4768900945783, + "summary_wm_p95": 1070.5193785503507, + "summary_wm_stdv": 45.53195992383758, + "tpm_overlap_csf": 0.1480490429482059, + "tpm_overlap_gm": 0.45370841555632335, + "tpm_overlap_wm": 0.504539439915526, + "wm2max": 0.8015368092794501 +} \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-airmask_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-airmask_T1w.svg new file mode 100644 index 0000000..fced6d7 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-airmask_T1w.svg @@ -0,0 +1,6286 @@ + + + + + + + + 2023-03-14T13:44:43.518422 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-artifacts_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-artifacts_T1w.svg new file mode 100644 index 0000000..75f1825 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-artifacts_T1w.svg @@ -0,0 +1,898 @@ + + + + + + + + 2023-03-14T13:44:43.510122 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-background_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-background_T1w.svg new file mode 100644 index 0000000..4777384 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-background_T1w.svg @@ -0,0 +1,7206 @@ + + + + + + + + 2023-03-14T13:41:06.124575 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-brainmask_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-brainmask_T1w.svg new file mode 100644 index 0000000..9421a0b --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-brainmask_T1w.svg @@ -0,0 +1,5280 @@ + + + + + + + + 2023-03-14T13:42:06.021134 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-head_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-head_T1w.svg new file mode 100644 index 0000000..6f270f3 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-head_T1w.svg @@ -0,0 +1,4475 @@ + + + + + + + + 2023-03-14T13:43:19.256196 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-noisefit_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-noisefit_T1w.svg new file mode 100644 index 0000000..da1767c --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-noisefit_T1w.svg @@ -0,0 +1,1229 @@ + + + + + + + + 2023-03-14T13:44:45.165271 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-norm_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-norm_T1w.svg new file mode 100644 index 0000000..32944bb --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-norm_T1w.svg @@ -0,0 +1,19 @@ + + + + + 2023-03-14T13:43:09.395849 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + 2023-03-14T13:43:10.523007 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + 2023-03-14T13:43:11.740858 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + + + 2023-03-14T13:43:12.944093 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + 2023-03-14T13:43:14.110292 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + 2023-03-14T13:43:15.371150 image/svg+xml Matplotlib v3.5.2, https://matplotlib.org/ + + + \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-segmentation_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-segmentation_T1w.svg new file mode 100644 index 0000000..2c31315 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-segmentation_T1w.svg @@ -0,0 +1,39958 @@ + + + + + + + + 2023-03-14T13:45:22.585329 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-zoomed_T1w.svg b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-zoomed_T1w.svg new file mode 100644 index 0000000..84c8ae1 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/ds002785/sub-0042/figures/sub-0042_desc-zoomed_T1w.svg @@ -0,0 +1,7173 @@ + + + + + + + + 2023-03-14T13:42:06.582536 + image/svg+xml + + + Matplotlib v3.5.2, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pydra/tasks/mriqc/data/tests/gh1086-ds004134.oracle b/pydra/tasks/mriqc/data/tests/gh1086-ds004134.oracle new file mode 100644 index 0000000..960e7a8 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/gh1086-ds004134.oracle @@ -0,0 +1 @@ +182 diff --git a/pydra/tasks/mriqc/data/tests/gh921-dmd-20220428-0.oracle b/pydra/tasks/mriqc/data/tests/gh921-dmd-20220428-0.oracle new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/gh921-dmd-20220428-0.oracle @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/pydra/tasks/mriqc/data/tests/gh921-dmd-20230319-0.oracle b/pydra/tasks/mriqc/data/tests/gh921-dmd-20230319-0.oracle new file mode 100644 index 0000000..ca7bf83 --- /dev/null +++ b/pydra/tasks/mriqc/data/tests/gh921-dmd-20230319-0.oracle @@ -0,0 +1 @@ +13 \ No newline at end of file diff --git a/pydra/tasks/mriqc/interfaces/__init__.py b/pydra/tasks/mriqc/interfaces/__init__.py new file mode 100644 index 0000000..c69a565 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/__init__.py @@ -0,0 +1,55 @@ +from .anatomical import ( + ArtifactMask, + ComputeQI2, + Harmonize, + RotationMask, + StructuralQC, + artifact_mask, + fuzzy_jaccard, +) +from .bids import IQMFileSink, _process_name +from .common import ConformImage, EnsureSize, NUMPY_DTYPE, OUT_FILE +from .derivatives_data_sink import DerivativesDataSink +from .diffusion import ( + CCSegmentation, + CorrectSignalDrift, + DiffusionModel, + DiffusionQC, + ExtractOrientations, + FilterShells, + NumberOfShells, + PIESNO, + ReadDWIMetadata, + RotateVectors, + SpikingVoxelsMask, + SplitShells, + WeightedStat, + _exp_func, + _rms, + get_spike_mask, + noise_piesno, + segment_corpus_callosum, +) +from .functional import ( + FunctionalQC, + GatherTimeseries, + SelectEcho, + Spikes, + _build_timeseries_metadata, + _get_echotime, + _robust_zscore, + find_peaks, + find_spikes, + select_echo, +) +from .reports import AddProvenance +from .synthstrip import SynthStrip +from .transitional import GCOR +from .webapi import ( + HASH_BIDS, + META_WHITELIST, + PROV_WHITELIST, + UploadIQMs, + _hashfields, + upload_qc_metrics, +) diff --git a/pydra/tasks/mriqc/interfaces/anatomical/__init__.py b/pydra/tasks/mriqc/interfaces/anatomical/__init__.py new file mode 100644 index 0000000..e2f3ff4 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/__init__.py @@ -0,0 +1,52 @@ +import attrs +from fileformats.generic import Directory, File +import json +import logging +import numpy as np +from pathlib import Path +from pydra.compose import python, shell, workflow +from .artifact_mask import ArtifactMask +from .compute_qi2 import ComputeQI2 +from .harmonize import Harmonize +from .rotation_mask import RotationMask +from .structural_qc import StructuralQC +from pydra.utils.typing import MultiInputObj +import scipy.ndimage as nd +import typing as ty +import yaml + + +logger = logging.getLogger(__name__) + + +def artifact_mask(imdata, airdata, distance, zscore=10.0): + """Compute a mask of artifacts found in the air region.""" + from statsmodels.robust.scale import mad + + qi1_msk = np.zeros(imdata.shape, dtype=bool) + bg_data = imdata[airdata] + if (bg_data > 0).sum() < 10: + return qi1_msk + # Standardize the distribution of the background + bg_spread = mad(bg_data[bg_data > 0]) + bg_data[bg_data > 0] = bg_data[bg_data > 0] / bg_spread + # Apply this threshold to the background voxels to identify voxels + # contributing artifacts. + qi1_msk[airdata] = bg_data > zscore + qi1_msk[distance < 0.10] = False + # Create a structural element to be used in an opening operation. + struct = nd.generate_binary_structure(3, 1) + qi1_msk = nd.binary_opening(qi1_msk, struct).astype(np.uint8) + return qi1_msk + + +def fuzzy_jaccard(in_tpms, in_mni_tpms): + + overlaps = [] + for tpm, mni_tpm in zip(in_tpms, in_mni_tpms): + tpm = tpm.reshape(-1) + mni_tpm = mni_tpm.reshape(-1) + num = np.min([tpm, mni_tpm], axis=0).sum() + den = np.max([tpm, mni_tpm], axis=0).sum() + overlaps.append(float(num / den)) + return overlaps diff --git a/pydra/tasks/mriqc/interfaces/anatomical/artifact_mask.py b/pydra/tasks/mriqc/interfaces/anatomical/artifact_mask.py new file mode 100644 index 0000000..789a770 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/artifact_mask.py @@ -0,0 +1,119 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +import numpy as np +import os +from pathlib import Path +from pydra.compose import python +import scipy.ndimage as nd + + +logger = logging.getLogger(__name__) + + +@python.define +class ArtifactMask(python.Task["ArtifactMask.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.anatomical.artifact_mask import ArtifactMask + + """ + + in_file: File + head_mask: File + glabella_xyz: tuple = (0.0, 90.0, -14.0) + inion_xyz: tuple = (0.0, -120.0, -14.0) + ind2std_xfm: File + zscore: float = 10.0 + + class Outputs(python.Outputs): + out_hat_msk: File + out_art_msk: File + out_air_msk: File + + @staticmethod + def function( + in_file: File, + head_mask: File, + glabella_xyz: tuple, + inion_xyz: tuple, + ind2std_xfm: File, + zscore: float, + ) -> tuples[File, File, File]: + out_hat_msk = attrs.NOTHING + out_art_msk = attrs.NOTHING + out_air_msk = attrs.NOTHING + from nibabel.affines import apply_affine + from nitransforms.linear import Affine + + in_file = Path(in_file) + imnii = nb.as_closest_canonical(nb.load(in_file)) + imdata = np.nan_to_num(imnii.get_fdata().astype(np.float32)) + + xfm = Affine.from_filename(ind2std_xfm, fmt="itk") + + ras2ijk = np.linalg.inv(imnii.affine) + glabella_ijk, inion_ijk = apply_affine( + ras2ijk, xfm.map([glabella_xyz, inion_xyz]) + ) + + hmdata = np.bool_(nb.load(head_mask).dataobj) + + dist = nd.morphology.distance_transform_edt(~hmdata) + + hmdata[:, :, : int(inion_ijk[2])] = 1 + hmdata[:, (hmdata.shape[1] // 2) :, : int(glabella_ijk[2])] = 1 + + dist[~hmdata] = 0 + dist /= dist.max() + + qi1_img = artifact_mask(imdata, (~hmdata), dist, zscore=zscore) + + fname = in_file.relative_to(in_file.parent).stem + ext = "".join(in_file.suffixes) + + outdir = Path(os.getcwd()).absolute() + out_hat_msk = str(outdir / f"{fname}_hat{ext}") + out_art_msk = str(outdir / f"{fname}_art{ext}") + out_air_msk = str(outdir / f"{fname}_air{ext}") + + hdr = imnii.header.copy() + hdr.set_data_dtype(np.uint8) + imnii.__class__(qi1_img.astype(np.uint8), imnii.affine, hdr).to_filename( + out_art_msk + ) + + airdata = (~hmdata).astype(np.uint8) + imnii.__class__(airdata, imnii.affine, hdr).to_filename(out_hat_msk) + + airdata[qi1_img > 0] = 0 + imnii.__class__(airdata.astype(np.uint8), imnii.affine, hdr).to_filename( + out_air_msk + ) + + return out_hat_msk, out_art_msk, out_air_msk + + +def artifact_mask(imdata, airdata, distance, zscore=10.0): + """Compute a mask of artifacts found in the air region.""" + from statsmodels.robust.scale import mad + + qi1_msk = np.zeros(imdata.shape, dtype=bool) + bg_data = imdata[airdata] + if (bg_data > 0).sum() < 10: + return qi1_msk + # Standardize the distribution of the background + bg_spread = mad(bg_data[bg_data > 0]) + bg_data[bg_data > 0] = bg_data[bg_data > 0] / bg_spread + # Apply this threshold to the background voxels to identify voxels + # contributing artifacts. + qi1_msk[airdata] = bg_data > zscore + qi1_msk[distance < 0.10] = False + # Create a structural element to be used in an opening operation. + struct = nd.generate_binary_structure(3, 1) + qi1_msk = nd.binary_opening(qi1_msk, struct).astype(np.uint8) + return qi1_msk diff --git a/pydra/tasks/mriqc/interfaces/anatomical/compute_qi2.py b/pydra/tasks/mriqc/interfaces/anatomical/compute_qi2.py new file mode 100644 index 0000000..a0a2cb6 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/compute_qi2.py @@ -0,0 +1,40 @@ +import attrs +from fileformats.generic import File +import logging +from pydra.tasks.mriqc.qc.anatomical import art_qi2 +import nibabel as nb +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class ComputeQI2(python.Task["ComputeQI2.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.anatomical.compute_qi2 import ComputeQI2 + + """ + + in_file: File + air_msk: File + + class Outputs(python.Outputs): + qi2: float + out_file: File + + @staticmethod + def function(in_file: File, air_msk: File) -> tuples[float, File]: + qi2 = attrs.NOTHING + out_file = attrs.NOTHING + imdata = nb.load(in_file).get_fdata() + airdata = nb.load(air_msk).get_fdata() + qi2, out_file = art_qi2(imdata, airdata) + qi2 = qi2 + out_file = out_file + + return qi2, out_file diff --git a/pydra/tasks/mriqc/interfaces/anatomical/harmonize.py b/pydra/tasks/mriqc/interfaces/anatomical/harmonize.py new file mode 100644 index 0000000..dd25f49 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/harmonize.py @@ -0,0 +1,74 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +from pydra.compose import python +import scipy.ndimage as nd + + +logger = logging.getLogger(__name__) + + +@python.define +class Harmonize(python.Task["Harmonize.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.anatomical.harmonize import Harmonize + + """ + + in_file: File + wm_mask: File + brain_mask: File + erodemsk: bool = True + thresh: float = 0.9 + min_size: int = 30 + + class Outputs(python.Outputs): + out_file: File + + @staticmethod + def function( + in_file: File, + wm_mask: File, + brain_mask: File, + erodemsk: bool, + thresh: float, + min_size: int, + ) -> File: + out_file = attrs.NOTHING + in_file = nb.load(in_file) + data = in_file.get_fdata() + + wm_mask = nb.load(wm_mask).get_fdata() + wm_mask[wm_mask < thresh] = 0 + wm_mask[wm_mask > 0] = 1 + wm_mask = wm_mask.astype(bool) + wm_mask_size = wm_mask.sum() + + if wm_mask_size < min_size: + brain_mask = nb.load(brain_mask).get_fdata() > 0.5 + wm_mask = brain_mask.copy() + wm_mask[data < np.percentile(data[brain_mask], 75)] = False + wm_mask[data > np.percentile(data[brain_mask], 95)] = False + elif erodemsk: + + struct = nd.generate_binary_structure(3, 2) + + wm_mask = nd.binary_erosion( + wm_mask.astype(np.uint8), structure=struct + ).astype(bool) + + data *= 1000.0 / np.median(data[wm_mask]) + + out_file = fname_presuffix(in_file, suffix="_harmonized", newpath=".") + in_file.__class__(data, in_file.affine, in_file.header).to_filename(out_file) + + out_file = out_file + + return out_file diff --git a/pydra/tasks/mriqc/interfaces/anatomical/rotation_mask.py b/pydra/tasks/mriqc/interfaces/anatomical/rotation_mask.py new file mode 100644 index 0000000..9cd148c --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/rotation_mask.py @@ -0,0 +1,61 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +from pydra.compose import python +import scipy.ndimage as nd + + +logger = logging.getLogger(__name__) + + +@python.define +class RotationMask(python.Task["RotationMask.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.anatomical.rotation_mask import RotationMask + + """ + + in_file: File + + class Outputs(python.Outputs): + out_file: File + + @staticmethod + def function(in_file: File) -> File: + out_file = attrs.NOTHING + in_file = nb.load(in_file) + data = in_file.get_fdata() + mask = data <= 0 + + mask = np.pad(mask, pad_width=(1,), mode="constant", constant_values=1) + + struct = nd.generate_binary_structure(3, 2) + mask = nd.binary_opening(mask, structure=struct).astype(np.uint8) + + label_im, nb_labels = nd.label(mask) + if nb_labels > 2: + sizes = nd.sum(mask, label_im, list(range(nb_labels + 1))) + ordered = sorted(zip(sizes, list(range(nb_labels + 1))), reverse=True) + for _, label in ordered[2:]: + mask[label_im == label] = 0 + + mask = mask[1:-1, 1:-1, 1:-1] + + if mask.sum() < 500: + mask = np.zeros_like(mask, dtype=np.uint8) + + out_img = in_file.__class__(mask, in_file.affine, in_file.header) + out_img.header.set_data_dtype(np.uint8) + + out_file = fname_presuffix(in_file, suffix="_rotmask", newpath=".") + out_img.to_filename(out_file) + out_file = out_file + + return out_file diff --git a/pydra/tasks/mriqc/interfaces/anatomical/structural_qc.py b/pydra/tasks/mriqc/interfaces/anatomical/structural_qc.py new file mode 100644 index 0000000..b7e9268 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/structural_qc.py @@ -0,0 +1,295 @@ +import attrs +from fileformats.generic import File +import logging +from pydra.tasks.mriqc.qc.anatomical import ( + art_qi1, + cjv, + cnr, + efc, + fber, + rpve, + snr, + snr_dietrich, + summary_stats, + volume_fraction, + wm2max, +) +from pydra.tasks.mriqc.utils.misc import _flatten_dict +import nibabel as nb +import numpy as np +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class StructuralQC(python.Task["StructuralQC.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.anatomical.structural_qc import StructuralQC + + """ + + in_file: File + in_noinu: File + in_segm: File + in_bias: File + head_msk: File + air_msk: File + rot_msk: File + artifact_msk: File + in_pvms: List + in_tpms: List + mni_tpms: List + in_fwhm: list + human: bool = True + + class Outputs(python.Outputs): + summary: dict + icvs: dict + rpve: dict + size: dict + spacing: dict + fwhm: dict + inu: dict + snr: dict + snrd: dict + cnr: float + fber: float + efc: float + qi_1: float + wm2max: float + cjv: float + out_qc: dict + out_noisefit: File + tpm_overlap: dict + + @staticmethod + def function( + in_file: File, + in_noinu: File, + in_segm: File, + in_bias: File, + head_msk: File, + air_msk: File, + rot_msk: File, + artifact_msk: File, + in_pvms: List, + in_tpms: List, + mni_tpms: List, + in_fwhm: list, + human: bool, + ) -> tuples[ + dict, + dict, + dict, + dict, + dict, + dict, + dict, + dict, + dict, + float, + float, + float, + float, + float, + float, + dict, + File, + dict, + ]: + summary = attrs.NOTHING + icvs = attrs.NOTHING + rpve = attrs.NOTHING + size = attrs.NOTHING + spacing = attrs.NOTHING + fwhm = attrs.NOTHING + inu = attrs.NOTHING + snr = attrs.NOTHING + snrd = attrs.NOTHING + cnr = attrs.NOTHING + fber = attrs.NOTHING + efc = attrs.NOTHING + qi_1 = attrs.NOTHING + wm2max = attrs.NOTHING + cjv = attrs.NOTHING + out_qc = attrs.NOTHING + out_noisefit = attrs.NOTHING + tpm_overlap = attrs.NOTHING + self_dict = {} + imnii = nb.load(in_noinu) + + inudata = np.nan_to_num(imnii.get_fdata()) + inudata[inudata < 0] = 0 + + if np.all(inudata < 1e-5): + raise RuntimeError( + "Input inhomogeneity-corrected data seem empty. " + "MRIQC failed to process this dataset." + ) + + segnii = nb.load(in_segm) + segdata = np.asanyarray(segnii.dataobj).astype(np.uint8) + + if np.sum(segdata > 0) < 1e3: + raise RuntimeError( + "Input segmentation data is likely corrupt. MRIQC failed to process this dataset." + ) + + airdata = np.asanyarray(nb.load(air_msk).dataobj).astype(np.uint8) + artdata = np.asanyarray(nb.load(artifact_msk).dataobj).astype(np.uint8) + + headdata = np.asanyarray(nb.load(head_msk).dataobj).astype(np.uint8) + if np.sum(headdata > 0) < 100: + raise RuntimeError( + "Detected less than 100 voxels belonging to the head mask. " + "MRIQC failed to process this dataset." + ) + + rotdata = np.asanyarray(nb.load(rot_msk).dataobj).astype(np.uint8) + + pvms = { + label: nb.load(fname).get_fdata() + for label, fname in zip(("csf", "gm", "wm"), in_pvms) + } + pvmdata = list(pvms.values()) + + pvms["bg"] = airdata + + stats = summary_stats(inudata, pvms) + summary = stats + + snrvals = [] + snr = {} + for tlabel in ("csf", "wm", "gm"): + snrvals.append( + snr( + stats[tlabel]["median"], + stats[tlabel]["stdv"], + stats[tlabel]["n"], + ) + ) + snr[tlabel] = snrvals[-1] + snr["total"] = float(np.mean(snrvals)) + + snrvals = [] + snrd = { + tlabel: snr_dietrich( + stats[tlabel]["median"], + mad_air=stats["bg"]["mad"], + sigma_air=stats["bg"]["stdv"], + ) + for tlabel in ["csf", "wm", "gm"] + } + snrd["total"] = float(np.mean([val for _, val in list(snrd.items())])) + + cnr = cnr( + stats["wm"]["median"], + stats["gm"]["median"], + stats["bg"]["stdv"], + stats["wm"]["stdv"], + stats["gm"]["stdv"], + ) + + fber = fber(inudata, headdata, rotdata) + + efc = efc(inudata, rotdata) + + wm2max = wm2max(inudata, stats["wm"]["median"]) + + qi_1 = art_qi1(airdata, artdata) + + cjv = cjv( + stats["wm"]["median"], + stats["gm"]["median"], + stats["wm"]["mad"], + stats["gm"]["mad"], + ) + + fwhm = np.array(in_fwhm[:3]) / np.array(imnii.header.get_zooms()[:3]) + fwhm = { + "x": float(fwhm[0]), + "y": float(fwhm[1]), + "z": float(fwhm[2]), + "avg": float(np.average(fwhm)), + } + + icvs = volume_fraction(pvmdata) + + rpve = rpve(pvmdata, segdata) + + size = { + "x": int(inudata.shape[0]), + "y": int(inudata.shape[1]), + "z": int(inudata.shape[2]), + } + spacing = { + i: float(v) for i, v in zip(["x", "y", "z"], imnii.header.get_zooms()[:3]) + } + + try: + size["t"] = int(inudata.shape[3]) + except IndexError: + pass + + try: + spacing["tr"] = float(imnii.header.get_zooms()[3]) + except IndexError: + pass + + bias = nb.load(in_bias).get_fdata()[segdata > 0] + inu = { + "range": float( + np.abs(np.percentile(bias, 95.0) - np.percentile(bias, 5.0)) + ), + "med": float(np.median(bias)), + } # pylint: disable=E1101 + + mni_tpms = [nb.load(tpm).get_fdata() for tpm in mni_tpms] + in_tpms = [nb.load(tpm).get_fdata() for tpm in in_pvms] + overlap = fuzzy_jaccard(in_tpms, mni_tpms) + tpm_overlap = { + "csf": overlap[0], + "gm": overlap[1], + "wm": overlap[2], + } + + out_qc = _flatten_dict(self_dict["_results"]) + + return ( + summary, + icvs, + rpve, + size, + spacing, + fwhm, + inu, + snr, + snrd, + cnr, + fber, + efc, + qi_1, + wm2max, + cjv, + out_qc, + # out_noisefit, + tpm_overlap, + ) + + +def fuzzy_jaccard(in_tpms, in_mni_tpms): + + overlaps = [] + for tpm, mni_tpm in zip(in_tpms, in_mni_tpms): + tpm = tpm.reshape(-1) + mni_tpm = mni_tpm.reshape(-1) + num = np.min([tpm, mni_tpm], axis=0).sum() + den = np.max([tpm, mni_tpm], axis=0).sum() + overlaps.append(float(num / den)) + return overlaps diff --git a/pydra/tasks/mriqc/interfaces/anatomical/tests/conftest.py b/pydra/tasks/mriqc/interfaces/anatomical/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/anatomical/tests/test_artifactmask.py b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_artifactmask.py new file mode 100644 index 0000000..4b624a4 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_artifactmask.py @@ -0,0 +1,21 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.anatomical.artifact_mask import ArtifactMask +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_artifactmask_1(): + task = ArtifactMask() + task.inputs.in_file = File.sample(seed=0) + task.inputs.head_mask = File.sample(seed=1) + task.inputs.glabella_xyz = [0.0, 90.0, -14.0] + task.inputs.inion_xyz = [0.0, -120.0, -14.0] + task.inputs.ind2std_xfm = File.sample(seed=4) + task.inputs.zscore = 10.0 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/anatomical/tests/test_computeqi2.py b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_computeqi2.py new file mode 100644 index 0000000..c8bb604 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_computeqi2.py @@ -0,0 +1,17 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.anatomical.compute_qi2 import ComputeQI2 +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_computeqi2_1(): + task = ComputeQI2() + task.inputs.in_file = File.sample(seed=0) + task.inputs.air_msk = File.sample(seed=1) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/anatomical/tests/test_harmonize.py b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_harmonize.py new file mode 100644 index 0000000..da36f9c --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_harmonize.py @@ -0,0 +1,19 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.anatomical.harmonize import Harmonize +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_harmonize_1(): + task = Harmonize() + task.inputs.in_file = File.sample(seed=0) + task.inputs.wm_mask = File.sample(seed=1) + task.inputs.erodemsk = True + task.inputs.thresh = 0.9 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/anatomical/tests/test_rotationmask.py b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_rotationmask.py new file mode 100644 index 0000000..e3a88db --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_rotationmask.py @@ -0,0 +1,16 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.anatomical.rotation_mask import RotationMask +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_rotationmask_1(): + task = RotationMask() + task.inputs.in_file = File.sample(seed=0) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/anatomical/tests/test_structuralqc.py b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_structuralqc.py new file mode 100644 index 0000000..37b3207 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/anatomical/tests/test_structuralqc.py @@ -0,0 +1,27 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.anatomical.structural_qc import StructuralQC +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_structuralqc_1(): + task = StructuralQC() + task.inputs.in_file = File.sample(seed=0) + task.inputs.in_noinu = File.sample(seed=1) + task.inputs.in_segm = File.sample(seed=2) + task.inputs.in_bias = File.sample(seed=3) + task.inputs.head_msk = File.sample(seed=4) + task.inputs.air_msk = File.sample(seed=5) + task.inputs.rot_msk = File.sample(seed=6) + task.inputs.artifact_msk = File.sample(seed=7) + task.inputs.in_pvms = [File.sample(seed=8)] + task.inputs.in_tpms = [File.sample(seed=9)] + task.inputs.mni_tpms = [File.sample(seed=10)] + task.inputs.human = True + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/bids/__init__.py b/pydra/tasks/mriqc/interfaces/bids/__init__.py new file mode 100644 index 0000000..e6d1c44 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/bids/__init__.py @@ -0,0 +1,25 @@ +import attrs +from fileformats.generic import Directory, File +import json +import logging +from pathlib import Path +from pydra.compose import python, shell, workflow +from .iqm_file_sink import IQMFileSink +from pydra.utils.typing import MultiInputObj +import typing as ty +import yaml + + +logger = logging.getLogger(__name__) + + +def _process_name(name, val): + + if "." in name: + newkeys = name.split(".") + name = newkeys.pop(0) + nested_dict = {newkeys.pop(): val} + for nk in reversed(newkeys): + nested_dict = {nk: nested_dict} + val = nested_dict + return name, val diff --git a/pydra/tasks/mriqc/interfaces/bids/iqm_file_sink.py b/pydra/tasks/mriqc/interfaces/bids/iqm_file_sink.py new file mode 100644 index 0000000..10dce50 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/bids/iqm_file_sink.py @@ -0,0 +1,197 @@ +import attrs +from fileformats.generic import File +import json +import logging +from pydra.tasks.mriqc.utils.misc import BIDS_COMP +import orjson as json +from pathlib import Path +from pydra.compose import python +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class IQMFileSink(python.Task["IQMFileSink.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.bids.iqm_file_sink import IQMFileSink + + """ + + in_file: str + modality: str + entities: dict + subject_id: str + session_id: ty.Any + task_id: ty.Any + acq_id: ty.Any + rec_id: ty.Any + run_id: ty.Any + dataset: str + dismiss_entities: list = ["datatype", "part", "echo", "extension", "suffix"] + metadata: dict + provenance: dict + root: dict + out_dir: Path + _outputs: dict = {} + + class Outputs(python.Outputs): + out_file: File + + @staticmethod + def function( + in_file: str, + modality: str, + entities: dict, + subject_id: str, + session_id: ty.Any, + task_id: ty.Any, + acq_id: ty.Any, + rec_id: ty.Any, + run_id: ty.Any, + dataset: str, + dismiss_entities: list, + metadata: dict, + provenance: dict, + root: dict, + out_dir: Path, + _outputs: dict, + ) -> File: + out_file = attrs.NOTHING + self_dict = {} + + if fields is None: + fields = [] + + self_dict["_out_dict"] = {} + + fields = list(set(fields) - set(self_dict["inputs"].copyable_trait_names())) + self_dict["_input_names"] = fields + undefined_traits = { + key: _add_field(key, add_trait=add_trait, _outputs=_outputs) + for key in fields + } + self_dict["inputs"].trait_set(trait_change_notify=False, **undefined_traits) + + if force_run: + self_dict["_always_run"] = True + self_dict = {} + out_file = _gen_outfile( + in_file=in_file, out_dir=out_dir, dismiss_entities=dismiss_entities + ) + + if root is not attrs.NOTHING: + self_dict["_out_dict"] = root + + root_adds = [] + for key, val in list(_outputs.items()): + if (val is attrs.NOTHING) or key == "trait_added": + continue + + if self_dict["expr"].match(key) is not None: + root_adds.append(key) + continue + + key, val = _process_name(key, val) + self_dict["_out_dict"][key] = val + + for root_key in root_adds: + val = _outputs.get(root_key, None) + if isinstance(val, dict): + self_dict["_out_dict"].update(val) + else: + logger.warning( + 'Output "%s" is not a dictionary (value="%s"), discarding output.', + root_key, + str(val), + ) + + id_dict = entities if (entities is not attrs.NOTHING) else {} + for comp in BIDS_COMP: + comp_val = getattr(self_dict["inputs"], comp, None) + if (comp_val is not attrs.NOTHING) and comp_val is not None: + id_dict[comp] = comp_val + id_dict["modality"] = modality + + if (metadata is not attrs.NOTHING) and metadata: + id_dict.update(metadata) + + if self_dict["_out_dict"].get("bids_meta") is None: + self_dict["_out_dict"]["bids_meta"] = {} + self_dict["_out_dict"]["bids_meta"].update(id_dict) + + if dataset is not attrs.NOTHING: + self_dict["_out_dict"]["bids_meta"]["dataset"] = dataset + + prov_dict = {} + if (provenance is not attrs.NOTHING) and provenance: + prov_dict.update(provenance) + + if self_dict["_out_dict"].get("provenance") is None: + self_dict["_out_dict"]["provenance"] = {} + self_dict["_out_dict"]["provenance"].update(prov_dict) + + Path(out_file).write_bytes( + json.dumps( + self_dict["_out_dict"], + option=( + json.OPT_SORT_KEYS + | json.OPT_INDENT_2 + | json.OPT_APPEND_NEWLINE + | json.OPT_SERIALIZE_NUMPY + ), + ) + ) + + return out_file + + +def _add_field(name, value=attrs.NOTHING, add_trait=None, _outputs=None): + self_dict = {} + self_dict["inputs"].add_trait(name, traits.Any) + _outputs[name] = value + return value + + +def _gen_outfile(in_file=None, out_dir=None, dismiss_entities=None): + out_dir = Path() + if out_dir is not attrs.NOTHING: + out_dir = Path(out_dir) + + path = Path(in_file) + for i in range(1, 4): + if str(path.parents[i].name).startswith("sub-"): + bids_root = path.parents[i + 1] + break + in_file = str(path.relative_to(bids_root)) + + if (dismiss_entities is not attrs.NOTHING) and (dismiss := dismiss_entities): + for entity in dismiss: + bids_chunks = [ + chunk + for chunk in path.name.split("_") + if not chunk.startswith(f"{entity}-") + ] + path = path.parent / "_".join(bids_chunks) + + bids_path = out_dir / in_file.replace("".join(Path(in_file).suffixes), ".json") + bids_path.parent.mkdir(parents=True, exist_ok=True) + out_file = str(bids_path) + return out_file + + +def _process_name(name, val): + + if "." in name: + newkeys = name.split(".") + name = newkeys.pop(0) + nested_dict = {newkeys.pop(): val} + for nk in reversed(newkeys): + nested_dict = {nk: nested_dict} + val = nested_dict + return name, val diff --git a/pydra/tasks/mriqc/interfaces/bids/tests/conftest.py b/pydra/tasks/mriqc/interfaces/bids/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/bids/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/bids/tests/test_iqmfilesink.py b/pydra/tasks/mriqc/interfaces/bids/tests/test_iqmfilesink.py new file mode 100644 index 0000000..98d9473 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/bids/tests/test_iqmfilesink.py @@ -0,0 +1,16 @@ +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.bids.iqm_file_sink import IQMFileSink +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_iqmfilesink_1(): + task = IQMFileSink() + task.inputs.dismiss_entities = ["datatype", "part", "echo", "extension", "suffix"] + task.inputs._outputs = {} + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/common/__init__.py b/pydra/tasks/mriqc/interfaces/common/__init__.py new file mode 100644 index 0000000..c76fa8e --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/common/__init__.py @@ -0,0 +1,2 @@ +from .conform_image import ConformImage, NUMPY_DTYPE, OUT_FILE +from .ensure_size import EnsureSize diff --git a/pydra/tasks/mriqc/interfaces/common/conform_image/__init__.py b/pydra/tasks/mriqc/interfaces/common/conform_image/__init__.py new file mode 100644 index 0000000..a8c2e47 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/common/conform_image/__init__.py @@ -0,0 +1,29 @@ +import attrs +from fileformats.generic import Directory, File +import json +import logging +import numpy as np +from pathlib import Path +from pydra.compose import python, shell, workflow +from .conform_image import ConformImage +from pydra.utils.typing import MultiInputObj +import typing as ty +import yaml + + +logger = logging.getLogger(__name__) + + +NUMPY_DTYPE = { + 1: np.uint8, + 2: np.uint8, + 4: np.uint16, + 8: np.uint32, + 64: np.float32, + 256: np.uint8, + 1024: np.uint32, + 1280: np.uint32, + 1536: np.float32, +} + +OUT_FILE = "{prefix}_conformed{ext}" diff --git a/pydra/tasks/mriqc/interfaces/common/conform_image/conform_image.py b/pydra/tasks/mriqc/interfaces/common/conform_image/conform_image.py new file mode 100644 index 0000000..ff75f7a --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/common/conform_image/conform_image.py @@ -0,0 +1,126 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nib +import numpy as np +from os import path as op +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class ConformImage(python.Task["ConformImage.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.common.conform_image.conform_image import ConformImage + + """ + + in_file: File + check_ras: bool = True + check_dtype: bool = True + + class Outputs(python.Outputs): + out_file: File + + @staticmethod + def function(in_file: File, check_ras: bool, check_dtype: bool) -> File: + out_file = attrs.NOTHING + """ + Execute this interface with the provided runtime. + + TODO: Is the *runtime* argument required? It doesn't seem to be used + anywhere. + + Parameters + ---------- + runtime : Any + Execution runtime ? + + Returns + ------- + Any + Execution runtime ? + """ + + nii = nib.squeeze_image(nib.load(in_file)) + + if check_ras: + nii = nib.as_closest_canonical(nii) + + if check_dtype: + nii = _check_dtype(nii, in_file=in_file) + + out_file, ext = op.splitext(op.basename(in_file)) + if ext == ".gz": + out_file, ext2 = op.splitext(out_file) + ext = ext2 + ext + out_file_name = OUT_FILE.format(prefix=out_file, ext=ext) + out_file = op.abspath(out_file_name) + nii.to_filename(out_file) + + return out_file + + +def _check_dtype(nii: nib.Nifti1Image, in_file=None) -> nib.Nifti1Image: + """ + Checks the NIfTI header datatype and converts the data to the matching + numpy dtype. + + Parameters + ---------- + nii : nib.Nifti1Image + Input image + + Returns + ------- + nib.Nifti1Image + Converted input image + """ + header = nii.header.copy() + datatype = int(header["datatype"]) + _warn_suspicious_dtype(datatype, in_file=in_file) + try: + dtype = NUMPY_DTYPE[datatype] + except KeyError: + return nii + else: + header.set_data_dtype(dtype) + converted = np.asanyarray(nii.dataobj, dtype=dtype) + return nib.Nifti1Image(converted, nii.affine, header) + + +def _warn_suspicious_dtype(dtype: int, in_file=None) -> None: + """ + Warns about binary type *nii* images. + + Parameters + ---------- + dtype : int + NIfTI header datatype + """ + if dtype == 1: + dtype_message = "Input image {in_file} has a suspicious data type: '{dtype}'".format( + in_file=in_file, dtype=dtype + ) + logger.warning(dtype_message) + + +NUMPY_DTYPE = { + 1: np.uint8, + 2: np.uint8, + 4: np.uint16, + 8: np.uint32, + 64: np.float32, + 256: np.uint8, + 1024: np.uint32, + 1280: np.uint32, + 1536: np.float32, +} + +OUT_FILE = "{prefix}_conformed{ext}" diff --git a/pydra/tasks/mriqc/interfaces/common/conform_image/tests/conftest.py b/pydra/tasks/mriqc/interfaces/common/conform_image/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/common/conform_image/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/common/conform_image/tests/test_conformimage.py b/pydra/tasks/mriqc/interfaces/common/conform_image/tests/test_conformimage.py new file mode 100644 index 0000000..2cf3a42 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/common/conform_image/tests/test_conformimage.py @@ -0,0 +1,18 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.common.conform_image.conform_image import ConformImage +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_conformimage_1(): + task = ConformImage() + task.inputs.in_file = File.sample(seed=0) + task.inputs.check_ras = True + task.inputs.check_dtype = True + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/common/ensure_size/__init__.py b/pydra/tasks/mriqc/interfaces/common/ensure_size/__init__.py new file mode 100644 index 0000000..c423b12 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/common/ensure_size/__init__.py @@ -0,0 +1 @@ +from .ensure_size import EnsureSize diff --git a/pydra/tasks/mriqc/interfaces/common/ensure_size/ensure_size.py b/pydra/tasks/mriqc/interfaces/common/ensure_size/ensure_size.py new file mode 100644 index 0000000..1987546 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/common/ensure_size/ensure_size.py @@ -0,0 +1,139 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nib +from pydra.tasks.ants.auto import ApplyTransforms +from pydra.tasks.niworkflows.data import Loader +import numpy as np +from os import path as op +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class EnsureSize(python.Task["EnsureSize.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.common.ensure_size.ensure_size import EnsureSize + + """ + + in_file: File + in_mask: File + pixel_size: float = 2.0 + + class Outputs(python.Outputs): + out_file: File + out_mask: File + + @staticmethod + def function(in_file: File, in_mask: File, pixel_size: float) -> tuples[File, File]: + out_file = attrs.NOTHING + out_mask = attrs.NOTHING + nii = nib.load(in_file) + size_ok = _check_size(nii, pixel_size=pixel_size) + if size_ok: + out_file = in_file + if in_mask is not attrs.NOTHING: + out_mask = in_mask + else: + + aff_base = nii.header.get_base_affine() + aff_base_inv = np.linalg.inv(aff_base) + + center_idx = (np.array(nii.shape[:3]) - 1) * 0.5 + center_mm = aff_base.dot(center_idx.tolist() + [1]) + + min_mm = aff_base.dot([-0.5, -0.5, -0.5, 1]) + max_mm = aff_base.dot((np.array(nii.shape[:3]) - 0.5).tolist() + [1]) + extent_mm = np.abs(max_mm - min_mm)[:3] + + new_size = np.array(extent_mm / pixel_size, dtype=int) + + new_base = aff_base[:3, :3] * np.abs(aff_base_inv[:3, :3]) * pixel_size + + new_center_idx = (new_size - 1) * 0.5 + new_affine_base = np.eye(4) + new_affine_base[:3, :3] = new_base + new_affine_base[:3, 3] = center_mm[:3] - new_base.dot(new_center_idx) + + rotation = nii.affine.dot(aff_base_inv) + new_affine = rotation.dot(new_affine_base) + + hdr = nii.header.copy() + hdr.set_data_shape(new_size) + nib.Nifti1Image( + np.zeros(new_size, dtype=nii.get_data_dtype()), new_affine, hdr + ).to_filename(REF_FILE_NAME) + + out_prefix, ext = op.splitext(op.basename(in_file)) + if ext == ".gz": + out_prefix, ext2 = op.splitext(out_prefix) + ext = ext2 + ext + + out_file_name = OUT_FILE_NAME.format(prefix=out_prefix, ext=ext) + out_file = op.abspath(out_file_name) + + ApplyTransforms( + dimension=3, + input_image=in_file, + reference_image=REF_FILE_NAME, + interpolation="LanczosWindowedSinc", + transforms=[str(load_data("data/itk_identity.tfm").absolute())], + output_image=out_file, + ).run() + + out_file = out_file + + if in_mask is not attrs.NOTHING: + hdr = nii.header.copy() + hdr.set_data_shape(new_size) + hdr.set_data_dtype(np.uint8) + nib.Nifti1Image( + np.zeros(new_size, dtype=np.uint8), new_affine, hdr + ).to_filename(REF_MASK_NAME) + + out_mask_name = OUT_MASK_NAME.format(prefix=out_prefix, ext=ext) + out_mask = op.abspath(out_mask_name) + ApplyTransforms( + dimension=3, + input_image=in_mask, + reference_image=REF_MASK_NAME, + interpolation="NearestNeighbor", + transforms=[str(load_data("data/itk_identity.tfm").absolute())], + output_image=out_mask, + ).run() + + out_mask = out_mask + + return out_file, out_mask + + +def _check_size(nii: nib.Nifti1Image, pixel_size=None) -> bool: + zooms = nii.header.get_zooms() + size_diff = np.array(zooms[:3]) - (pixel_size - 0.1) + if np.all(size_diff >= -1e-3): + logger.info('Voxel size is large enough.') + return True + else: + small_voxel_message = 'One or more voxel dimensions (%f, %f, %f) are smaller than the requested voxel size (%f) - diff=(%f, %f, %f)'.format( + *zooms[:3], pixel_size, *size_diff + ) + logger.info(small_voxel_message) + return False + + +OUT_FILE_NAME = "{prefix}_resampled{ext}" + +OUT_MASK_NAME = "{prefix}_resmask{ext}" + +REF_FILE_NAME = "resample_ref.nii.gz" + +REF_MASK_NAME = "mask_ref.nii.gz" + +load_data = Loader("pydra.tasks.mriqc") diff --git a/pydra/tasks/mriqc/interfaces/common/ensure_size/tests/conftest.py b/pydra/tasks/mriqc/interfaces/common/ensure_size/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/common/ensure_size/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/common/ensure_size/tests/test_ensuresize.py b/pydra/tasks/mriqc/interfaces/common/ensure_size/tests/test_ensuresize.py new file mode 100644 index 0000000..2502465 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/common/ensure_size/tests/test_ensuresize.py @@ -0,0 +1,18 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.common.ensure_size.ensure_size import EnsureSize +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_ensuresize_1(): + task = EnsureSize() + task.inputs.in_file = File.sample(seed=0) + task.inputs.in_mask = File.sample(seed=1) + task.inputs.pixel_size = 2.0 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/derivatives_data_sink.py b/pydra/tasks/mriqc/interfaces/derivatives_data_sink.py new file mode 100644 index 0000000..77cd450 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/derivatives_data_sink.py @@ -0,0 +1,300 @@ +import attrs +from contextlib import suppress +from fileformats.generic import Directory +from json import dumps +import logging +import nibabel as nb +from pydra.tasks.niworkflows import data +from pydra.tasks.niworkflows.utils.bids import relative_to_root +from pydra.tasks.niworkflows.utils.images import ( + set_consumables, + unsafe_write_nifti_header_and_data, +) +from pydra.tasks.niworkflows.utils.misc import _copy_any, unlink +import numpy as np +import os +from pathlib import Path +from pydra.compose import python +from pydra.utils.typing import MultiInputObj +import re + + +logger = logging.getLogger(__name__) + + +@python.define +class DerivativesDataSink(python.Task["DerivativesDataSink.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import Directory, File + >>> from pydra.tasks.mriqc.interfaces.derivatives_data_sink import DerivativesDataSink + >>> from pydra.utils.typing import MultiInputObj, MultiOutputType + + """ + + base_directory: Directory + check_hdr: bool = True + compress: MultiInputObj = [] + data_dtype: str + dismiss_entities: MultiInputObj = [] + in_file: List + meta_dict: dict + source_file: List + + class Outputs(python.Outputs): + out_file: List + out_meta: List + compression: Union + fixed_hdr: list + + @staticmethod + def function( + base_directory: Directory, + check_hdr: bool, + compress: MultiInputObj, + data_dtype: str, + dismiss_entities: MultiInputObj, + in_file: List, + meta_dict: dict, + source_file: List, + ) -> tuples[List, List, Union, list]: + out_file = attrs.NOTHING + out_meta = attrs.NOTHING + compression = attrs.NOTHING + fixed_hdr = attrs.NOTHING + self_dict = {} + """Initialize the SimpleInterface and extend inputs with custom entities.""" + self_dict["_allowed_entities"] = set(allowed_entities or []).union( + set(self_dict["_config_entities"]) + ) + if out_path_base: + self_dict["out_path_base"] = out_path_base + + self_dict["_metadata"] = {} + self_dict["_static_traits"] = self_dict[ + "input_spec" + ].class_editable_traits() + sorted(self_dict["_allowed_entities"]) + for dynamic_input in set(inputs) - set(self_dict["_static_traits"]): + self_dict["_metadata"][dynamic_input] = inputs.pop(dynamic_input) + + add_traits(self_dict["inputs"], self_dict["_allowed_entities"]) + for k in self_dict["_allowed_entities"].intersection(list(inputs.keys())): + + setattr(self_dict["inputs"], k, inputs[k]) + self_dict = {} + from bids.layout import parse_file_entities, Config + from bids.layout.writing import build_path + from bids.utils import listify + + base_directory = os.getcwd() + if base_directory is not attrs.NOTHING: + base_directory = base_directory + base_directory = Path(base_directory).absolute() + out_path = base_directory / self_dict["out_path_base"] + out_path.mkdir(exist_ok=True, parents=True) + + in_file = listify(in_file) + + if meta_dict is not attrs.NOTHING: + meta = meta_dict + + meta.update(self_dict["_metadata"]) + self_dict["_metadata"] = meta + + custom_config = Config( + name="custom", + entities=self_dict["_config_entities_dict"], + default_path_patterns=self_dict["_file_patterns"], + ) + in_entities = [ + parse_file_entities( + str(relative_to_root(source_file)), + config=["bids", "derivatives", custom_config], + ) + for source_file in source_file + ] + out_entities = { + k: v + for k, v in in_entities[0].items() + if all(ent.get(k) == v for ent in in_entities[1:]) + } + for drop_entity in listify(dismiss_entities or []): + out_entities.pop(drop_entity, None) + + out_entities["extension"] = [ + "".join(Path(orig_file).suffixes).lstrip(".") for orig_file in in_file + ] + + compress = listify(compress) or [None] + if len(compress) == 1: + compress = compress * len(in_file) + for i, ext in enumerate(out_entities["extension"]): + if compress[i] is not None: + ext = regz.sub("", ext) + out_entities["extension"][i] = f"{ext}.gz" if compress[i] else ext + + for key in self_dict["_allowed_entities"]: + value = getattr(self_dict["inputs"], key) + if value is not None and (value is not attrs.NOTHING): + out_entities[key] = value + + if out_entities.get("resolution") == "native" and out_entities.get("space"): + out_entities.pop("resolution", None) + + resolution = out_entities.get("resolution") + space = out_entities.get("space") + if resolution: + + if space in self_dict["_standard_spaces"]: + res = _get_tf_resolution(space, resolution) + else: # TODO: Nonstandard? + res = "Unknown" + self_dict["_metadata"]["Resolution"] = res + + if len(set(out_entities["extension"])) == 1: + out_entities["extension"] = out_entities["extension"][0] + + custom_entities = set(out_entities) - set(self_dict["_config_entities"]) + patterns = self_dict["_file_patterns"] + if custom_entities: + + custom_pat = "_".join(f"{key}-{{{key}}}" for key in sorted(custom_entities)) + patterns = [ + pat.replace("_{suffix", "_".join(("", custom_pat, "{suffix"))) + for pat in patterns + ] + + out_file = [] + compression = [] + fixed_hdr = [False] * len(in_file) + + dest_files = build_path(out_entities, path_patterns=patterns) + if not dest_files: + raise ValueError(f"Could not build path with entities {out_entities}.") + + dest_files = listify(dest_files) + if len(in_file) != len(dest_files): + raise ValueError( + f"Input files ({len(in_file)}) not matched " + f"by interpolated patterns ({len(dest_files)})." + ) + + for i, (orig_file, dest_file) in enumerate(zip(in_file, dest_files)): + out_file = out_path / dest_file + out_file.parent.mkdir(exist_ok=True, parents=True) + out_file.append(str(out_file)) + compression.append(str(dest_file).endswith(".gz")) + + try: + if os.path.samefile(orig_file, out_file): + continue + except FileNotFoundError: + pass + + new_data, new_header = None, None + + is_nifti = False + with suppress(nb.filebasedimages.ImageFileError): + is_nifti = isinstance(nb.load(orig_file), nb.Nifti1Image) + + data_dtype = data_dtype or self_dict["_default_dtypes"][suffix] + if is_nifti and any((check_hdr, data_dtype)): + nii = nb.load(orig_file) + + if check_hdr: + hdr = nii.header + curr_units = tuple( + [None if u == "unknown" else u for u in hdr.get_xyzt_units()] + ) + curr_codes = (int(hdr["qform_code"]), int(hdr["sform_code"])) + + units = ( + curr_units[0] or "mm", + "sec" if out_entities["suffix"] == "bold" else None, + ) + xcodes = (1, 1) # Derivative in its original scanner space + if space: + xcodes = ( + (4, 4) if space in self_dict["_standard_spaces"] else (2, 2) + ) + + curr_zooms = zooms = hdr.get_zooms() + if "RepetitionTime" in self_dict["inputs"].get(): + zooms = curr_zooms[:3] + (RepetitionTime,) + + if (curr_codes, curr_units, curr_zooms) != (xcodes, units, zooms): + fixed_hdr[i] = True + new_header = hdr.copy() + new_header.set_qform(nii.affine, xcodes[0]) + new_header.set_sform(nii.affine, xcodes[1]) + new_header.set_xyzt_units(*units) + new_header.set_zooms(zooms) + + if data_dtype == "source": # match source dtype + try: + data_dtype = nb.load(source_file[0]).get_data_dtype() + except Exception: + LOGGER.warning( + f"Could not get data type of file {source_file[0]}" + ) + data_dtype = None + + if data_dtype: + data_dtype = np.dtype(data_dtype) + orig_dtype = nii.get_data_dtype() + if orig_dtype != data_dtype: + LOGGER.warning( + f"Changing {out_file} dtype from {orig_dtype} to {data_dtype}" + ) + + if np.issubdtype(data_dtype, np.integer): + new_data = np.rint(nii.dataobj).astype(data_dtype) + else: + new_data = np.asanyarray(nii.dataobj, dtype=data_dtype) + + if new_header is None: + new_header = nii.header.copy() + new_header.set_data_dtype(data_dtype) + del nii + + unlink(out_file, missing_ok=True) + if new_data is new_header is None: + _copy_any(orig_file, str(out_file)) + else: + orig_img = nb.load(orig_file) + if new_data is None: + set_consumables(new_header, orig_img.dataobj) + new_data = orig_img.dataobj.get_unscaled() + else: + + new_header.set_slope_inter(slope=1.0, inter=0.0) + unsafe_write_nifti_header_and_data( + fname=out_file, header=new_header, data=new_data + ) + del orig_img + + if len(out_file) == 1: + meta_fields = self_dict["inputs"].copyable_trait_names() + self_dict["_metadata"].update( + { + k: getattr(self_dict["inputs"], k) + for k in meta_fields + if k not in self_dict["_static_traits"] + } + ) + if self_dict["_metadata"]: + sidecar = out_file.parent / f"{out_file.name.split('.', 1)[0]}.json" + unlink(sidecar, missing_ok=True) + sidecar.write_text( + dumps(self_dict["_metadata"], sort_keys=True, indent=2) + ) + out_meta = str(sidecar) + + return out_file, out_meta, compression, fixed_hdr + + +LOGGER = logging.getLogger("nipype.interface") + +regz = re.compile(r"\.gz$") diff --git a/pydra/tasks/mriqc/interfaces/diffusion/__init__.py b/pydra/tasks/mriqc/interfaces/diffusion/__init__.py new file mode 100644 index 0000000..3e80db9 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/__init__.py @@ -0,0 +1,197 @@ +import attrs +from fileformats.generic import Directory, File +import json +import logging +import numpy as np +from pathlib import Path +from pydra.compose import python, shell, workflow +from .cc_segmentation import CCSegmentation +from .correct_signal_drift import CorrectSignalDrift +from .diffusion_model import DiffusionModel +from .diffusion_qc import DiffusionQC +from .extract_orientations import ExtractOrientations +from .filter_shells import FilterShells +from .number_of_shells import NumberOfShells +from .piesno import PIESNO +from .read_dwi_metadata import ReadDWIMetadata +from .rotate_vectors import RotateVectors +from .spiking_voxels_mask import SpikingVoxelsMask +from .split_shells import SplitShells +from .weighted_stat import WeightedStat +from pydra.utils.typing import MultiInputObj +import scipy.ndimage as nd +import typing as ty +import yaml + + +logger = logging.getLogger(__name__) + + +def _exp_func(t, A, K, C): + + return A * np.exp(K * t) + C + + +def _rms(estimator, X): + """ + Callable to pass to GridSearchCV that will calculate a distance score. + + To consider: using `MDL + `__ + + """ + if len(np.unique(estimator.cluster_centers_)) < estimator.n_clusters: + return -np.inf + # Calculate distance from assigned shell centroid + distance = X - estimator.cluster_centers_[estimator.predict(X)] + # Make negative so CV optimizes minimizes the error + return -np.sqrt(distance**2).sum() + + +def get_spike_mask( + data: np.ndarray, shell_masks: list, brainmask: np.ndarray, z_threshold: float = 3.0 +) -> np.ndarray: + """ + Creates a binary mask classifying voxels in the data array as spike or non-spike. + + This function identifies voxels with signal intensities exceeding a threshold based + on standard deviations above the mean. The threshold can be applied globally to + the entire data array, or it can be calculated for groups of voxels defined by + the ``grouping_vals`` parameter. + + Parameters + ---------- + data : :obj:`~numpy.ndarray` + The data array to be thresholded. + z_threshold : :obj:`float`, optional (default=3.0) + The number of standard deviations to use above the mean as the threshold + multiplier. + brainmask : :obj:`~numpy.ndarray` + The brain mask. + shell_masks : :obj:`list` + A list of :obj:`~numpy.ndarray` objects + + Returns: + ------- + spike_mask : :obj:`~numpy.ndarray` + A binary mask where ``True`` values indicate voxels classified as spikes and + ``False`` values indicate non-spikes. The mask has the same shape as the input + data array. + + """ + spike_mask = np.zeros_like(data, dtype=bool) + brainmask = brainmask >= 0.5 + for b_mask in shell_masks: + shelldata = data[..., b_mask] + a_thres = z_threshold * shelldata[brainmask].std() + shelldata[brainmask].mean() + spike_mask[..., b_mask] = shelldata > a_thres + return spike_mask + + +def noise_piesno(data: np.ndarray, n_channels: int = 4) -> (np.ndarray, np.ndarray): + """ + Estimates noise in raw diffusion MRI (dMRI) data using the PIESNO algorithm. + + This function implements the PIESNO (Probabilistic Identification and Estimation + of Noise) algorithm [Koay2009]_ to estimate the standard deviation (sigma) of the + noise in each voxel of a 4D dMRI data array. The PIESNO algorithm assumes Rician + distributed signal and exploits the statistical properties of the noise to + separate it from the underlying signal. + + Parameters + ---------- + data : :obj:`~numpy.ndarray` + The 4D raw dMRI data array. + n_channels : :obj:`int`, optional (default=4) + The number of diffusion-encoding channels in the data. This value is used + internally by the PIESNO algorithm. + + Returns + ------- + sigma : :obj:`~numpy.ndarray` + The estimated noise standard deviation for each voxel in the data array. + mask : :obj:`~numpy.ndarray` + A brain mask estimated by PIESNO. This mask identifies voxels containing + mostly noise and can be used for further processing. + + """ + from dipy.denoise.noise_estimate import piesno + + sigma, mask = piesno(data, N=n_channels, return_mask=True) + return sigma, mask + + +def segment_corpus_callosum( + in_cfa: np.ndarray, + mask: np.ndarray, + min_rgb: tuple[float, float, float] = (0.6, 0.0, 0.0), + max_rgb: tuple[float, float, float] = (1.0, 0.1, 0.1), + clean_mask: bool = False, +) -> tuple[np.ndarray, np.ndarray]: + """ + Segments the corpus callosum (CC) from a color FA map. + + Parameters + ---------- + in_cfa : :obj:`~numpy.ndarray` + The color FA (cFA) map. + mask : :obj:`~numpy.ndarray` (bool, 3D) + A white matter mask used to define the initial bounding box. + min_rgb : :obj:`tuple`, optional + Minimum RGB values. + max_rgb : :obj:`tuple`, optional + Maximum RGB values. + clean_mask : :obj:`bool`, optional + Whether the CC mask is finally cleaned-up for spurious off voxels with + :obj:`dipy.segment.mask.clean_cc_mask` + + Returns + ------- + cc_mask: :obj:`~numpy.ndarray` + The final binary mask of the segmented CC. + + Notes + ----- + This implementation was derived from + :obj:`dipy.segment.mask.segment_from_cfa`. + + """ + from dipy.segment.mask import bounding_box + + # Prepare a bounding box of the CC + cc_box = np.zeros_like(mask, dtype=bool) + mins, maxs = bounding_box(mask) # mask needs to be volume + mins = np.array(mins) + maxs = np.array(maxs) + diff = (maxs - mins) // 5 + bounds_min = mins + diff + bounds_max = maxs - diff + cc_box[ + bounds_min[0] : bounds_max[0], + bounds_min[1] : bounds_max[1], + bounds_min[2] : bounds_max[2], + ] = True + min_rgb = np.array(min_rgb) + max_rgb = np.array(max_rgb) + # Threshold color FA + cc_mask = np.all( + (in_cfa >= min_rgb[None, :]) & (in_cfa <= max_rgb[None, :]), + axis=-1, + ) + # Apply bounding box and WM mask + cc_mask *= cc_box & mask + struct = nd.generate_binary_structure(cc_mask.ndim, cc_mask.ndim - 1) + # Perform a closing followed by opening operations on the FA. + cc_mask = nd.binary_closing( + cc_mask, + structure=struct, + ) + cc_mask = nd.binary_opening( + cc_mask, + structure=struct, + ) + if clean_mask: + from dipy.segment.mask import clean_cc_mask + + cc_mask = clean_cc_mask(cc_mask) + return cc_mask diff --git a/pydra/tasks/mriqc/interfaces/diffusion/cc_segmentation.py b/pydra/tasks/mriqc/interfaces/diffusion/cc_segmentation.py new file mode 100644 index 0000000..22818e8 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/cc_segmentation.py @@ -0,0 +1,215 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +import os +from pydra.compose import python +import scipy.ndimage as nd + + +logger = logging.getLogger(__name__) + + +@python.define +class CCSegmentation(python.Task["CCSegmentation.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.cc_segmentation import CCSegmentation + + """ + + in_fa: File + in_cfa: File + min_rgb: tuple = (0.4, 0.008, 0.008) + max_rgb: tuple = (1.1, 0.25, 0.25) + wm_threshold: float = 0.35 + clean_mask: bool = False + + class Outputs(python.Outputs): + out_mask: File + wm_mask: File + wm_finalmask: File + + @staticmethod + def function( + in_fa: File, + in_cfa: File, + min_rgb: tuple, + max_rgb: tuple, + wm_threshold: float, + clean_mask: bool, + ) -> tuples[File, File, File]: + out_mask = attrs.NOTHING + wm_mask = attrs.NOTHING + wm_finalmask = attrs.NOTHING + from skimage.measure import label + + out_mask = fname_presuffix( + in_cfa, + suffix="ccmask", + newpath=os.getcwd(), + ) + wm_mask = fname_presuffix( + in_cfa, + suffix="wmmask", + newpath=os.getcwd(), + ) + wm_finalmask = fname_presuffix( + in_cfa, + suffix="wmfinalmask", + newpath=os.getcwd(), + ) + + fa_nii = nb.load(in_fa) + fa_data = np.round(fa_nii.get_fdata(dtype="float32"), 4) + fa_labels = label((fa_data > wm_threshold).astype(np.uint8)) + wm_mask = fa_labels == np.argmax(np.bincount(fa_labels.flat)[1:]) + 1 + + wm_mask_nii = nb.Nifti1Image( + wm_mask.astype(np.uint8), + fa_nii.affine, + None, + ) + wm_mask_nii.header.set_xyzt_units("mm") + wm_mask_nii.header.set_intent( + "estimate", name="white-matter mask (FA thresholded)" + ) + wm_mask_nii.header["cal_max"] = 1 + wm_mask_nii.header["cal_min"] = 0 + wm_mask_nii.to_filename(wm_mask) + + struct = nd.generate_binary_structure(wm_mask.ndim, wm_mask.ndim - 1) + + wm_mask = nd.grey_closing( + fa_data, + structure=struct, + ) + wm_mask = nd.grey_opening( + wm_mask, + structure=struct, + ) + + fa_labels = label((np.round(wm_mask, 4) > wm_threshold).astype(np.uint8)) + wm_mask = fa_labels == np.argmax(np.bincount(fa_labels.flat)[1:]) + 1 + + wm_mask_nii = nb.Nifti1Image( + wm_mask.astype(np.uint8), + fa_nii.affine, + wm_mask_nii.header, + ) + wm_mask_nii.header.set_intent( + "estimate", name="white-matter mask after binary opening" + ) + wm_mask_nii.to_filename(wm_finalmask) + + cfa_data = np.round(nb.load(in_cfa).get_fdata(dtype="float32"), 4) + for i in range(cfa_data.shape[-1]): + cfa_data[..., i] = nd.grey_closing( + cfa_data[..., i], + structure=struct, + ) + cfa_data[..., i] = nd.grey_opening( + cfa_data[..., i], + structure=struct, + ) + + cc_mask = segment_corpus_callosum( + in_cfa=cfa_data, + mask=wm_mask, + min_rgb=min_rgb, + max_rgb=max_rgb, + clean_mask=clean_mask, + ) + cc_mask_nii = nb.Nifti1Image( + cc_mask.astype(np.uint8), + fa_nii.affine, + None, + ) + cc_mask_nii.header.set_xyzt_units("mm") + cc_mask_nii.header.set_intent("estimate", name="corpus callosum mask") + cc_mask_nii.header["cal_max"] = 1 + cc_mask_nii.header["cal_min"] = 0 + cc_mask_nii.to_filename(out_mask) + + return out_mask, wm_mask, wm_finalmask + + +def segment_corpus_callosum( + in_cfa: np.ndarray, + mask: np.ndarray, + min_rgb: tuple[float, float, float] = (0.6, 0.0, 0.0), + max_rgb: tuple[float, float, float] = (1.0, 0.1, 0.1), + clean_mask: bool = False, +) -> tuple[np.ndarray, np.ndarray]: + """ + Segments the corpus callosum (CC) from a color FA map. + + Parameters + ---------- + in_cfa : :obj:`~numpy.ndarray` + The color FA (cFA) map. + mask : :obj:`~numpy.ndarray` (bool, 3D) + A white matter mask used to define the initial bounding box. + min_rgb : :obj:`tuple`, optional + Minimum RGB values. + max_rgb : :obj:`tuple`, optional + Maximum RGB values. + clean_mask : :obj:`bool`, optional + Whether the CC mask is finally cleaned-up for spurious off voxels with + :obj:`dipy.segment.mask.clean_cc_mask` + + Returns + ------- + cc_mask: :obj:`~numpy.ndarray` + The final binary mask of the segmented CC. + + Notes + ----- + This implementation was derived from + :obj:`dipy.segment.mask.segment_from_cfa`. + + """ + from dipy.segment.mask import bounding_box + + # Prepare a bounding box of the CC + cc_box = np.zeros_like(mask, dtype=bool) + mins, maxs = bounding_box(mask) # mask needs to be volume + mins = np.array(mins) + maxs = np.array(maxs) + diff = (maxs - mins) // 5 + bounds_min = mins + diff + bounds_max = maxs - diff + cc_box[ + bounds_min[0] : bounds_max[0], + bounds_min[1] : bounds_max[1], + bounds_min[2] : bounds_max[2], + ] = True + min_rgb = np.array(min_rgb) + max_rgb = np.array(max_rgb) + # Threshold color FA + cc_mask = np.all( + (in_cfa >= min_rgb[None, :]) & (in_cfa <= max_rgb[None, :]), + axis=-1, + ) + # Apply bounding box and WM mask + cc_mask *= cc_box & mask + struct = nd.generate_binary_structure(cc_mask.ndim, cc_mask.ndim - 1) + # Perform a closing followed by opening operations on the FA. + cc_mask = nd.binary_closing( + cc_mask, + structure=struct, + ) + cc_mask = nd.binary_opening( + cc_mask, + structure=struct, + ) + if clean_mask: + from dipy.segment.mask import clean_cc_mask + + cc_mask = clean_cc_mask(cc_mask) + return cc_mask diff --git a/pydra/tasks/mriqc/interfaces/diffusion/correct_signal_drift.py b/pydra/tasks/mriqc/interfaces/diffusion/correct_signal_drift.py new file mode 100644 index 0000000..f3a45e8 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/correct_signal_drift.py @@ -0,0 +1,129 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +import os +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class CorrectSignalDrift(python.Task["CorrectSignalDrift.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.correct_signal_drift import CorrectSignalDrift + + """ + + in_file: File + bias_file: File + brainmask_file: File + b0_ixs: list + bval_file: File + full_epi: File + + class Outputs(python.Outputs): + out_file: File + out_full_file: File + b0_drift: list + signal_drift: list + + @staticmethod + def function( + in_file: File, + bias_file: File, + brainmask_file: File, + b0_ixs: list, + bval_file: File, + full_epi: File, + ) -> tuples[File, File, list, list]: + out_file = attrs.NOTHING + out_full_file = attrs.NOTHING + b0_drift = attrs.NOTHING + signal_drift = attrs.NOTHING + from mriqc import config + + bvals = np.loadtxt(bval_file) + len_dmri = bvals.size + + img = nb.load(in_file) + data = img.get_fdata() + bmask = np.ones_like(data[..., 0], dtype=bool) + + if bias_file is not attrs.NOTHING: + data *= nb.load(bias_file).get_fdata()[..., np.newaxis] + + if brainmask_file is not attrs.NOTHING: + bmask = np.round(nb.load(brainmask_file).get_fdata(), 2) > 0.5 + + out_file = fname_presuffix(in_file, suffix="_nodrift", newpath=os.getcwd()) + + if (b0len := int(data.ndim < 4)) or (b0len := data.shape[3]) < 3: + logger.warn( + f"Insufficient number of low-b orientations ({b0len}) " + "to safely calculate signal drift." + ) + + img.__class__( + np.round(data.astype("float32"), 4), + img.affine, + img.header, + ).to_filename(out_file) + + if full_epi is not attrs.NOTHING: + out_full_file = full_epi + + b0_drift = [1.0] * b0len + signal_drift = [1.0] * len_dmri + + global_signal = np.array( + [np.median(data[..., n_b0][bmask]) for n_b0 in range(img.shape[-1])] + ).astype("float32") + + global_signal /= global_signal[0] + b0_drift = [round(float(gs), 4) for gs in global_signal] + + logger.info( + f"Correcting drift with {len(global_signal)} b=0 volumes, with " + "global signal estimated at " + f'{", ".join([str(v) for v in b0_drift])}.' + ) + + data *= 1.0 / global_signal[np.newaxis, np.newaxis, np.newaxis, :] + + img.__class__( + data.astype(img.header.get_data_dtype()), + img.affine, + img.header, + ).to_filename(out_file) + + K, A_log = np.polyfit(b0_ixs, np.log(global_signal), 1) + + t_points = np.arange(len_dmri, dtype=int) + fitted = np.squeeze(_exp_func(t_points, np.exp(A_log), K, 0)) + signal_drift = fitted.astype(float).tolist() + + if full_epi is not attrs.NOTHING: + out_full_file = fname_presuffix( + full_epi, suffix="_nodriftfull", newpath=os.getcwd() + ) + full_img = nb.load(full_epi) + full_img.__class__( + full_img.get_fdata() * fitted[np.newaxis, np.newaxis, np.newaxis, :], + full_img.affine, + full_img.header, + ).to_filename(out_full_file) + + return out_file, out_full_file, b0_drift, signal_drift + + +def _exp_func(t, A, K, C): + + return A * np.exp(K * t) + C diff --git a/pydra/tasks/mriqc/interfaces/diffusion/diffusion_model.py b/pydra/tasks/mriqc/interfaces/diffusion/diffusion_model.py new file mode 100644 index 0000000..3d04e96 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/diffusion_model.py @@ -0,0 +1,182 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +import os +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class DiffusionModel(python.Task["DiffusionModel.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.diffusion_model import DiffusionModel + + """ + + in_file: File + bvals: list + bvec_file: File + brain_mask: File + decimals: int = 3 + n_shells: int + + class Outputs(python.Outputs): + out_fa: File + out_fa_nans: File + out_fa_degenerate: File + out_cfa: File + out_md: File + + @staticmethod + def function( + in_file: File, + bvals: list, + bvec_file: File, + brain_mask: File, + decimals: int, + n_shells: int, + ) -> tuples[File, File, File, File, File]: + out_fa = attrs.NOTHING + out_fa_nans = attrs.NOTHING + out_fa_degenerate = attrs.NOTHING + out_cfa = attrs.NOTHING + out_md = attrs.NOTHING + from dipy.core.gradients import gradient_table_from_bvals_bvecs + from nipype.utils.filemanip import fname_presuffix + + bvals = np.array(bvals) + + gtab = gradient_table_from_bvals_bvecs( + bvals=bvals, + bvecs=np.loadtxt(bvec_file).T, + ) + + img = nb.load(in_file) + data = img.get_fdata(dtype="float32") + + brainmask = np.ones_like(data[..., 0], dtype=bool) + + if brain_mask is not attrs.NOTHING: + brainmask = ( + np.round( + nb.load(brain_mask).get_fdata(), + 3, + ) + > 0.5 + ) + + if n_shells == 1: + from dipy.reconst.dti import TensorModel as Model + else: + from dipy.reconst.dki import DiffusionKurtosisModel as Model + + fwdtifit = Model(gtab).fit( + data, + mask=brainmask, + ) + + fa_data = fwdtifit.fa + fa_nan_msk = np.isnan(fa_data) + fa_data[fa_nan_msk] = 0 + + fa_data = np.round(fa_data, decimals) + degenerate_msk = (fa_data < 0) | (fa_data > 1.0) + + fa_data = np.clip(fa_data, 0, 1) + + fa_nii = nb.Nifti1Image( + fa_data, + img.affine, + None, + ) + + fa_nii.header.set_xyzt_units("mm") + fa_nii.header.set_intent("estimate", name="Fractional Anisotropy (FA)") + + out_fa = fname_presuffix( + in_file, + suffix="fa", + newpath=os.getcwd(), + ) + + fa_nii.to_filename(out_fa) + + fa_nan_nii = nb.Nifti1Image( + fa_nan_msk.astype(np.uint8), + img.affine, + None, + ) + + fa_nan_nii.header.set_xyzt_units("mm") + fa_nan_nii.header.set_intent("estimate", name="NaNs in the FA map mask") + fa_nan_nii.header["cal_max"] = 1 + fa_nan_nii.header["cal_min"] = 0 + + out_fa_nans = fname_presuffix( + in_file, + suffix="desc-fanans_mask", + newpath=os.getcwd(), + ) + fa_nan_nii.to_filename(out_fa_nans) + + fa_degenerate_nii = nb.Nifti1Image( + degenerate_msk.astype(np.uint8), + img.affine, + None, + ) + + fa_degenerate_nii.header.set_xyzt_units("mm") + fa_degenerate_nii.header.set_intent( + "estimate", name="degenerate vectors in the FA map mask" + ) + fa_degenerate_nii.header["cal_max"] = 1 + fa_degenerate_nii.header["cal_min"] = 0 + + out_fa_degenerate = fname_presuffix( + in_file, + suffix="desc-fadegenerate_mask", + newpath=os.getcwd(), + ) + fa_degenerate_nii.to_filename(out_fa_degenerate) + + cfa_data = fwdtifit.color_fa + cfa_nii = nb.Nifti1Image( + np.clip(cfa_data, a_min=0.0, a_max=1.0), + img.affine, + None, + ) + + cfa_nii.header.set_xyzt_units("mm") + cfa_nii.header.set_intent("estimate", name="Fractional Anisotropy (FA)") + cfa_nii.header["cal_max"] = 1.0 + cfa_nii.header["cal_min"] = 0.0 + + out_cfa = fname_presuffix( + in_file, + suffix="cfa", + newpath=os.getcwd(), + ) + cfa_nii.to_filename(out_cfa) + + out_md = fname_presuffix( + in_file, + suffix="md", + newpath=os.getcwd(), + ) + md_data = np.array(fwdtifit.md, dtype="float32") + md_data[np.isnan(md_data)] = 0 + md_data = np.clip(md_data, 0, 1) + md_hdr = fa_nii.header.copy() + md_hdr.set_intent("estimate", name="Mean diffusivity (MD)") + nb.Nifti1Image(md_data, img.affine, md_hdr).to_filename(out_md) + + return out_fa, out_fa_nans, out_fa_degenerate, out_cfa, out_md diff --git a/pydra/tasks/mriqc/interfaces/diffusion/diffusion_qc.py b/pydra/tasks/mriqc/interfaces/diffusion/diffusion_qc.py new file mode 100644 index 0000000..09cfe87 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/diffusion_qc.py @@ -0,0 +1,229 @@ +import attrs +from fileformats.generic import File +import logging +from pydra.tasks.mriqc.utils.misc import _flatten_dict +import nibabel as nb +import numpy as np +from pydra.compose import python +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class DiffusionQC(python.Task["DiffusionQC.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.diffusion_qc import DiffusionQC + + """ + + in_file: File + in_b0: File + in_shells: List + in_shells_bval: list + in_bval_file: File + in_bvec: list + in_bvec_rotated: list + in_bvec_diff: list + in_fa: File + in_fa_nans: File + in_fa_degenerate: File + in_cfa: File + in_md: File + brain_mask: File + wm_mask: File + cc_mask: File + spikes_mask: File + noise_floor: float + direction: ty.Any = all + in_fd: File + fd_thres: float = 0.2 + in_fwhm: list + qspace_neighbors: list + piesno_sigma: float = -1.0 + + class Outputs(python.Outputs): + bdiffs: dict + efc: dict + fa_degenerate: float + fa_nans: float + fber: dict + fd: dict + ndc: float + sigma: dict + spikes: dict + snr_cc: dict + summary: dict + out_qc: dict + + @staticmethod + def function( + in_file: File, + in_b0: File, + in_shells: List, + in_shells_bval: list, + in_bval_file: File, + in_bvec: list, + in_bvec_rotated: list, + in_bvec_diff: list, + in_fa: File, + in_fa_nans: File, + in_fa_degenerate: File, + in_cfa: File, + in_md: File, + brain_mask: File, + wm_mask: File, + cc_mask: File, + spikes_mask: File, + noise_floor: float, + direction: ty.Any, + in_fd: File, + fd_thres: float, + in_fwhm: list, + qspace_neighbors: list, + piesno_sigma: float, + ) -> tuples[ + dict, dict, float, float, dict, dict, float, dict, dict, dict, dict, dict + ]: + bdiffs = attrs.NOTHING + efc = attrs.NOTHING + fa_degenerate = attrs.NOTHING + fa_nans = attrs.NOTHING + fber = attrs.NOTHING + fd = attrs.NOTHING + ndc = attrs.NOTHING + sigma = attrs.NOTHING + spikes = attrs.NOTHING + snr_cc = attrs.NOTHING + summary = attrs.NOTHING + out_qc = attrs.NOTHING + self_dict = {} + from mriqc.qc import anatomical as aqc + from mriqc.qc import diffusion as dqc + + b0nii = nb.load(in_b0) + b0data = np.round( + np.nan_to_num(np.asanyarray(b0nii.dataobj)), + 3, + ) + b0data[b0data < 0] = 0 + + msknii = nb.load(brain_mask) + mskdata = np.round( # Protect the thresholding with a rounding for stability + msknii.get_fdata(), + 3, + ) + if np.sum(mskdata) < 100: + raise RuntimeError( + "Detected less than 100 voxels belonging to the brain mask. " + "MRIQC failed to process this dataset." + ) + + wmnii = nb.load(wm_mask) + wmdata = np.round( # Protect the thresholding with a rounding for stability + np.asanyarray(wmnii.dataobj), + 3, + ) + + ccnii = nb.load(cc_mask) + ccdata = np.round( # Protect the thresholding with a rounding for stability + np.asanyarray(ccnii.dataobj), + 3, + ) + + shelldata = [ + np.round( + np.asanyarray(nb.load(s).dataobj), + 4, + ) + for s in in_shells + ] + + rois = { + "fg": mskdata, + "bg": 1.0 - mskdata, + "wm": wmdata, + } + stats = aqc.summary_stats(b0data, rois) + summary = stats + + snr_cc, cc_sigma = dqc.cc_snr( + in_b0=b0data, + dwi_shells=shelldata, + cc_mask=ccdata, + b_values=in_shells_bval, + b_vectors=in_bvec, + ) + + fa_nans_mask = np.asanyarray(nb.load(in_fa_nans).dataobj) > 0.0 + fa_nans = round(float(1e6 * fa_nans_mask[mskdata > 0.5].mean()), 2) + + fa_degenerate_mask = np.asanyarray(nb.load(in_fa_degenerate).dataobj) > 0.0 + fa_degenerate = round( + float(1e6 * fa_degenerate_mask[mskdata > 0.5].mean()), + 2, + ) + + spmask = np.asanyarray(nb.load(spikes_mask).dataobj) > 0.0 + spikes = dqc.spike_ppm(spmask) + + fber = { + f"shell{i + 1:02d}": aqc.fber(bdata, mskdata.astype(np.uint8)) + for i, bdata in enumerate(shelldata) + } + + efc = {f"shell{i + 1:02d}": aqc.efc(bdata) for i, bdata in enumerate(shelldata)} + + fd_data = np.loadtxt(in_fd, skiprows=1) + num_fd = (fd_data > fd_thres).sum() + fd = { + "mean": round(float(fd_data.mean()), 4), + "num": int(num_fd), + "perc": float(num_fd * 100 / (len(fd_data) + 1)), + } + + dwidata = np.round( + np.nan_to_num(nb.load(in_file).get_fdata()), + 3, + ) + ndc = dqc.neighboring_dwi_correlation( + dwidata, + neighbor_indices=qspace_neighbors, + mask=mskdata > 0.5, + ) + + sigma = { + "cc": round(float(cc_sigma), 4), + "piesno": round(piesno_sigma, 4), + "pca": round(noise_floor, 4), + } + + diffs = np.array(in_bvec_diff) + bdiffs = { + "mean": round(float(diffs[diffs > 1e-4].mean()), 4), + "median": round(float(np.median(diffs[diffs > 1e-4])), 4), + "max": round(float(diffs[diffs > 1e-4].max()), 4), + "min": round(float(diffs[diffs > 1e-4].min()), 4), + } + + out_qc = _flatten_dict(self_dict["_results"]) + + return ( + bdiffs, + efc, + fa_degenerate, + fa_nans, + fber, + fd, + ndc, + sigma, + spikes, + snr_cc, + summary, + out_qc, + ) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/extract_orientations.py b/pydra/tasks/mriqc/interfaces/diffusion/extract_orientations.py new file mode 100644 index 0000000..4152ed7 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/extract_orientations.py @@ -0,0 +1,61 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +import os +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class ExtractOrientations(python.Task["ExtractOrientations.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.extract_orientations import ExtractOrientations + + """ + + in_file: File + indices: list + in_bvec_file: File + + class Outputs(python.Outputs): + out_file: File + out_bvec: list + + @staticmethod + def function( + in_file: File, indices: list, in_bvec_file: File + ) -> tuples[File, list]: + out_file = attrs.NOTHING + out_bvec = attrs.NOTHING + from nipype.utils.filemanip import fname_presuffix + + out_file = fname_presuffix( + in_file, + suffix="_subset", + newpath=os.getcwd(), + ) + + out_file = out_file + + img = nb.load(in_file) + bzeros = np.squeeze(np.asanyarray(img.dataobj)[..., indices]) + + hdr = img.header.copy() + hdr.set_data_shape(bzeros.shape) + hdr.set_xyzt_units("mm") + nb.Nifti1Image(bzeros, img.affine, hdr).to_filename(out_file) + + if in_bvec_file is not attrs.NOTHING: + bvecs = np.loadtxt(in_bvec_file)[:, indices].T + out_bvec = [tuple(row) for row in bvecs] + + return out_file, out_bvec diff --git a/pydra/tasks/mriqc/interfaces/diffusion/filter_shells.py b/pydra/tasks/mriqc/interfaces/diffusion/filter_shells.py new file mode 100644 index 0000000..cb308aa --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/filter_shells.py @@ -0,0 +1,83 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +import os +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class FilterShells(python.Task["FilterShells.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.filter_shells import FilterShells + + """ + + in_file: File + bvals: list + bvec_file: File + b_threshold: float = 1100 + + class Outputs(python.Outputs): + out_file: File + out_bvals: list + out_bvec_file: File + out_bval_file: File + + @staticmethod + def function( + in_file: File, bvals: list, bvec_file: File, b_threshold: float + ) -> tuples[File, list, File, File]: + out_file = attrs.NOTHING + out_bvals = attrs.NOTHING + out_bvec_file = attrs.NOTHING + out_bval_file = attrs.NOTHING + from nipype.utils.filemanip import fname_presuffix + + bvals = np.array(bvals) + bval_mask = bvals < b_threshold + bvecs = np.loadtxt(bvec_file)[:, bval_mask] + + out_bvals = bvals[bval_mask].astype(float).tolist() + out_bvec_file = fname_presuffix( + in_file, + suffix="_dti.bvec", + newpath=os.getcwd(), + use_ext=False, + ) + np.savetxt(out_bvec_file, bvecs) + + out_bval_file = fname_presuffix( + in_file, + suffix="_dti.bval", + newpath=os.getcwd(), + use_ext=False, + ) + np.savetxt(out_bval_file, bvals) + + out_file = fname_presuffix( + in_file, + suffix="_dti", + newpath=os.getcwd(), + ) + + dwi_img = nb.load(in_file) + data = np.array(dwi_img.dataobj, dtype=dwi_img.header.get_data_dtype())[ + ..., bval_mask + ] + dwi_img.__class__( + data, + dwi_img.affine, + dwi_img.header, + ).to_filename(out_file) + + return out_file, out_bvals, out_bvec_file, out_bval_file diff --git a/pydra/tasks/mriqc/interfaces/diffusion/number_of_shells.py b/pydra/tasks/mriqc/interfaces/diffusion/number_of_shells.py new file mode 100644 index 0000000..fae13e1 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/number_of_shells.py @@ -0,0 +1,130 @@ +import attrs +from fileformats.generic import File +import logging +import numpy as np +from pydra.compose import python +from sklearn.cluster import KMeans +from sklearn.model_selection import GridSearchCV + + +logger = logging.getLogger(__name__) + + +@python.define +class NumberOfShells(python.Task["NumberOfShells.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.number_of_shells import NumberOfShells + + """ + + in_bvals: File + b0_threshold: float = 50 + dsi_threshold: int = 11 + + class Outputs(python.Outputs): + models: list + n_shells: int + out_data: list + b_values: list + b_masks: list + b_indices: list + b_dict: dict + + @staticmethod + def function( + in_bvals: File, b0_threshold: float, dsi_threshold: int + ) -> tuples[list, int, list, list, list, list, dict]: + models = attrs.NOTHING + n_shells = attrs.NOTHING + out_data = attrs.NOTHING + b_values = attrs.NOTHING + b_masks = attrs.NOTHING + b_indices = attrs.NOTHING + b_dict = attrs.NOTHING + in_data = np.squeeze(np.loadtxt(in_bvals)) + highb_mask = in_data > b0_threshold + + original_bvals = sorted(set(np.rint(in_data[highb_mask]).astype(int))) + round_bvals = np.round(in_data, -2).astype(int) + shell_bvals = sorted(set(round_bvals[highb_mask])) + + if len(shell_bvals) <= dsi_threshold: + n_shells = len(shell_bvals) + models = [n_shells] + out_data = round_bvals.tolist() + b_values = shell_bvals + else: + + grid_search = GridSearchCV( + KMeans(), param_grid={"n_clusters": range(1, 10)}, scoring=_rms + ).fit(in_data[highb_mask].reshape(-1, 1)) + + results = np.array( + sorted( + zip( + grid_search.cv_results_["mean_test_score"] * -1.0, + grid_search.cv_results_["param_n_clusters"], + ) + ) + ) + + models = results[:, 1].astype(int).tolist() + n_shells = int(grid_search.best_params_["n_clusters"]) + + out_data = np.zeros_like(in_data) + predicted_shell = np.rint( + np.squeeze( + grid_search.best_estimator_.cluster_centers_[ + grid_search.best_estimator_.predict( + in_data[highb_mask].reshape(-1, 1) + ) + ], + ) + ).astype(int) + + if len(original_bvals) == n_shells: + + indices = np.abs( + predicted_shell[:, np.newaxis] - original_bvals + ).argmin(axis=1) + predicted_shell = original_bvals[indices] + + out_data[highb_mask] = predicted_shell + out_data = np.round(out_data.astype(float), 2).tolist() + b_values = sorted( + np.unique(np.round(predicted_shell.astype(float), 2)).tolist() + ) + + b_masks = [(~highb_mask).tolist()] + [ + np.isclose(out_data, bvalue).tolist() for bvalue in b_values + ] + b_indices = [ + np.atleast_1d(np.squeeze(np.argwhere(b_mask)).astype(int)).tolist() + for b_mask in b_masks + ] + + b_dict = { + int(round(k, 0)): value for k, value in zip([0] + b_values, b_indices) + } + + return models, n_shells, out_data, b_values, b_masks, b_indices, b_dict + + +def _rms(estimator, X): + """ + Callable to pass to GridSearchCV that will calculate a distance score. + + To consider: using `MDL + `__ + + """ + if len(np.unique(estimator.cluster_centers_)) < estimator.n_clusters: + return -np.inf + # Calculate distance from assigned shell centroid + distance = X - estimator.cluster_centers_[estimator.predict(X)] + # Make negative so CV optimizes minimizes the error + return -np.sqrt(distance**2).sum() diff --git a/pydra/tasks/mriqc/interfaces/diffusion/piesno.py b/pydra/tasks/mriqc/interfaces/diffusion/piesno.py new file mode 100644 index 0000000..cdfb238 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/piesno.py @@ -0,0 +1,95 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +import os +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class PIESNO(python.Task["PIESNO.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.piesno import PIESNO + + """ + + in_file: File + n_channels: int = 4 + + class Outputs(python.Outputs): + sigma: float + out_mask: File + + @staticmethod + def function(in_file: File, n_channels: int) -> tuples[float, File]: + sigma = attrs.NOTHING + out_mask = attrs.NOTHING + out_mask = fname_presuffix( + in_file, + suffix="piesno", + newpath=os.getcwd(), + ) + + in_nii = nb.load(in_file) + data = np.round(in_nii.get_fdata(), 4).astype("float32") + + sigma, maskdata = noise_piesno(data) + + header = in_nii.header.copy() + header.set_data_dtype(np.uint8) + header.set_xyzt_units("mm") + header.set_intent("estimate", name="PIESNO noise voxels mask") + header["cal_max"] = 1 + header["cal_min"] = 0 + + nb.Nifti1Image( + maskdata.astype(np.uint8), + in_nii.affine, + header, + ).to_filename(out_mask) + + sigma = round(float(np.median(sigma)), 5) + + return sigma, out_mask + + +def noise_piesno(data: np.ndarray, n_channels: int = 4) -> (np.ndarray, np.ndarray): + """ + Estimates noise in raw diffusion MRI (dMRI) data using the PIESNO algorithm. + + This function implements the PIESNO (Probabilistic Identification and Estimation + of Noise) algorithm [Koay2009]_ to estimate the standard deviation (sigma) of the + noise in each voxel of a 4D dMRI data array. The PIESNO algorithm assumes Rician + distributed signal and exploits the statistical properties of the noise to + separate it from the underlying signal. + + Parameters + ---------- + data : :obj:`~numpy.ndarray` + The 4D raw dMRI data array. + n_channels : :obj:`int`, optional (default=4) + The number of diffusion-encoding channels in the data. This value is used + internally by the PIESNO algorithm. + + Returns + ------- + sigma : :obj:`~numpy.ndarray` + The estimated noise standard deviation for each voxel in the data array. + mask : :obj:`~numpy.ndarray` + A brain mask estimated by PIESNO. This mask identifies voxels containing + mostly noise and can be used for further processing. + + """ + from dipy.denoise.noise_estimate import piesno + + sigma, mask = piesno(data, N=n_channels, return_mask=True) + return sigma, mask diff --git a/pydra/tasks/mriqc/interfaces/diffusion/read_dwi_metadata.py b/pydra/tasks/mriqc/interfaces/diffusion/read_dwi_metadata.py new file mode 100644 index 0000000..844a54d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/read_dwi_metadata.py @@ -0,0 +1,120 @@ +import attrs +from dipy.core.gradients import gradient_table +from dipy.stats.qc import find_qspace_neighbors +from fileformats.generic import Directory, File +import logging +from pydra.tasks.niworkflows.utils.bids import _init_layout +import numpy as np +from pydra.compose import python +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class ReadDWIMetadata(python.Task["ReadDWIMetadata.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import Directory, File + >>> from pydra.tasks.mriqc.interfaces.diffusion.read_dwi_metadata import ReadDWIMetadata + + """ + + in_file: File + bids_dir: ty.Any + bids_validate: bool = True + index_db: Directory + + class Outputs(python.Outputs): + out_bvec_file: File + out_bval_file: File + out_bmatrix: list + qspace_neighbors: list + out_dict: dict + subject: str + session: str + task: str + acquisition: str + reconstruction: str + run: int + suffix: str + + @staticmethod + def function( + in_file: File, bids_dir: ty.Any, bids_validate: bool, index_db: Directory + ) -> tuples[File, File, list, list, dict, str, str, str, str, str, int, str]: + out_bvec_file = attrs.NOTHING + out_bval_file = attrs.NOTHING + out_bmatrix = attrs.NOTHING + qspace_neighbors = attrs.NOTHING + out_dict = attrs.NOTHING + subject = attrs.NOTHING + session = attrs.NOTHING + task = attrs.NOTHING + acquisition = attrs.NOTHING + reconstruction = attrs.NOTHING + run = attrs.NOTHING + suffix = attrs.NOTHING + self_dict = {} + from bids.utils import listify + + self_dict["_fields"] = listify(fields or []) + self_dict["_undef_fields"] = undef_fields + self_dict = {} + runtime = niworkflows_interfaces_bids__ReadSidecarJSON___run_interface(runtime) + + out_bvec_file = str(self_dict["layout"].get_bvec(in_file)) + out_bval_file = str(self_dict["layout"].get_bval(in_file)) + + bvecs = np.loadtxt(out_bvec_file).T + bvals = np.loadtxt(out_bval_file) + + gtab = gradient_table(bvals, bvecs=bvecs) + + qspace_neighbors = find_qspace_neighbors(gtab) + out_bmatrix = np.hstack((bvecs, bvals[:, np.newaxis])).tolist() + + return ( + out_bvec_file, + out_bval_file, + out_bmatrix, + qspace_neighbors, + out_dict, + subject, + session, + task, + acquisition, + reconstruction, + run, + suffix, + ) + + +def niworkflows_interfaces_bids__ReadSidecarJSON___run_interface(): + self_dict = {} + self_dict["layout"] = bids_dir or self_dict["layout"] + self_dict["layout"] = _init_layout( + in_file, + self_dict["layout"], + bids_validate, + database_path=(index_db if (index_db is not attrs.NOTHING) else None), + ) + + output_keys = list(_BIDSInfoOutputSpec().get().keys()) + params = self_dict["layout"].parse_file_entities(in_file) + self_dict["_results"] = { + key: params.get(key.split("_")[0], type(attrs.NOTHING)) for key in output_keys + } + + metadata = self_dict["layout"].get_metadata(in_file) + out_dict = metadata + + for fname in self_dict["_fields"]: + if not self_dict["_undef_fields"] and fname not in metadata: + raise KeyError( + 'Metadata field "%s" not found for file %s' % (fname, in_file) + ) + self_dict["_results"][fname] = metadata.get(fname, type(attrs.NOTHING)) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/rotate_vectors.py b/pydra/tasks/mriqc/interfaces/diffusion/rotate_vectors.py new file mode 100644 index 0000000..743d65e --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/rotate_vectors.py @@ -0,0 +1,70 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +import numpy as np +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class RotateVectors(python.Task["RotateVectors.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.rotate_vectors import RotateVectors + + """ + + in_file: File + reference: File + transforms: File + + class Outputs(python.Outputs): + out_bvec: list + out_diff: list + + @staticmethod + def function( + in_file: File, reference: File, transforms: File + ) -> tuples[list, list]: + out_bvec = attrs.NOTHING + out_diff = attrs.NOTHING + from nitransforms.linear import load + + vox2ras = nb.load(reference).affine + ras2vox = np.linalg.inv(vox2ras) + + ijk = np.loadtxt(in_file).T + nonzero = np.linalg.norm(ijk, axis=1) > 1e-3 + + xyz = (vox2ras[:3, :3] @ ijk.T).T + + xyz_norms = np.linalg.norm(xyz, axis=1) + xyz[nonzero] = xyz[nonzero] / xyz_norms[nonzero, np.newaxis] + + hmc_rot = load(transforms).matrix[:, :3, :3] + ijk_rotated = ( + ras2vox[:3, :3] @ np.einsum("ijk,ik->ij", hmc_rot, xyz).T + ).T.astype("float32") + ijk_rotated_norm = np.linalg.norm(ijk_rotated, axis=1) + ijk_rotated[nonzero] = ( + ijk_rotated[nonzero] / ijk_rotated_norm[nonzero, np.newaxis] + ) + ijk_rotated[~nonzero] = ijk[~nonzero] + + out_bvec = list(zip(ijk_rotated[:, 0], ijk_rotated[:, 1], ijk_rotated[:, 2])) + + diffs = np.zeros_like(ijk[:, 0]) + diffs[nonzero] = np.arccos( + np.clip( + np.einsum("ij, ij->i", ijk[nonzero], ijk_rotated[nonzero]), -1.0, 1.0 + ) + ) + out_diff = [round(float(v), 6) for v in diffs] + + return out_bvec, out_diff diff --git a/pydra/tasks/mriqc/interfaces/diffusion/spiking_voxels_mask.py b/pydra/tasks/mriqc/interfaces/diffusion/spiking_voxels_mask.py new file mode 100644 index 0000000..58664a3 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/spiking_voxels_mask.py @@ -0,0 +1,111 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +import os +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class SpikingVoxelsMask(python.Task["SpikingVoxelsMask.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.spiking_voxels_mask import SpikingVoxelsMask + + """ + + in_file: File + brain_mask: File + z_threshold: float = 3.0 + b_masks: list + + class Outputs(python.Outputs): + out_mask: File + + @staticmethod + def function( + in_file: File, brain_mask: File, z_threshold: float, b_masks: list + ) -> File: + out_mask = attrs.NOTHING + out_mask = fname_presuffix( + in_file, + suffix="spikesmask", + newpath=os.getcwd(), + ) + + in_nii = nb.load(in_file) + data = np.round(in_nii.get_fdata(), 4).astype("float32") + + bmask_nii = nb.load(brain_mask) + brainmask = np.round(bmask_nii.get_fdata(), 2).astype("float32") + + spikes_mask = get_spike_mask( + data, + shell_masks=b_masks, + brainmask=brainmask, + z_threshold=z_threshold, + ) + + header = bmask_nii.header.copy() + header.set_data_dtype(np.uint8) + header.set_xyzt_units("mm") + header.set_intent("estimate", name="spiking voxels mask") + header["cal_max"] = 1 + header["cal_min"] = 0 + + spikes_mask_nii = nb.Nifti1Image( + spikes_mask.astype(np.uint8), + bmask_nii.affine, + header, + ) + spikes_mask_nii.to_filename(out_mask) + + return out_mask + + +def get_spike_mask( + data: np.ndarray, shell_masks: list, brainmask: np.ndarray, z_threshold: float = 3.0 +) -> np.ndarray: + """ + Creates a binary mask classifying voxels in the data array as spike or non-spike. + + This function identifies voxels with signal intensities exceeding a threshold based + on standard deviations above the mean. The threshold can be applied globally to + the entire data array, or it can be calculated for groups of voxels defined by + the ``grouping_vals`` parameter. + + Parameters + ---------- + data : :obj:`~numpy.ndarray` + The data array to be thresholded. + z_threshold : :obj:`float`, optional (default=3.0) + The number of standard deviations to use above the mean as the threshold + multiplier. + brainmask : :obj:`~numpy.ndarray` + The brain mask. + shell_masks : :obj:`list` + A list of :obj:`~numpy.ndarray` objects + + Returns: + ------- + spike_mask : :obj:`~numpy.ndarray` + A binary mask where ``True`` values indicate voxels classified as spikes and + ``False`` values indicate non-spikes. The mask has the same shape as the input + data array. + + """ + spike_mask = np.zeros_like(data, dtype=bool) + brainmask = brainmask >= 0.5 + for b_mask in shell_masks: + shelldata = data[..., b_mask] + a_thres = z_threshold * shelldata[brainmask].std() + shelldata[brainmask].mean() + spike_mask[..., b_mask] = shelldata > a_thres + return spike_mask diff --git a/pydra/tasks/mriqc/interfaces/diffusion/split_shells.py b/pydra/tasks/mriqc/interfaces/diffusion/split_shells.py new file mode 100644 index 0000000..d38e2b5 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/split_shells.py @@ -0,0 +1,55 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +import os +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class SplitShells(python.Task["SplitShells.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.split_shells import SplitShells + + """ + + in_file: File + bvals: list + + class Outputs(python.Outputs): + out_file: List + + @staticmethod + def function(in_file: File, bvals: list) -> List: + out_file = attrs.NOTHING + from nipype.utils.filemanip import fname_presuffix + + bval_list = np.rint(bvals).astype(int) + bvals = np.unique(bval_list) + img = nb.load(in_file) + data = np.asanyarray(img.dataobj) + + out_file = [] + + for bval in bvals: + fname = fname_presuffix( + in_file, suffix=f"_b{bval:05d}", newpath=os.getcwd() + ) + out_file.append(fname) + + img.__class__( + data[..., np.argwhere(bval_list == bval)], + img.affine, + img.header, + ).to_filename(fname) + + return out_file diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/conftest.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_ccsegmentation.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_ccsegmentation.py new file mode 100644 index 0000000..4cd0c0d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_ccsegmentation.py @@ -0,0 +1,21 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.cc_segmentation import CCSegmentation +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_ccsegmentation_1(): + task = CCSegmentation() + task.inputs.in_fa = File.sample(seed=0) + task.inputs.in_cfa = File.sample(seed=1) + task.inputs.min_rgb = [0.4, 0.008, 0.008] + task.inputs.max_rgb = [1.1, 0.25, 0.25] + task.inputs.wm_threshold = 0.35 + task.inputs.clean_mask = False + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_correctsignaldrift.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_correctsignaldrift.py new file mode 100644 index 0000000..9500900 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_correctsignaldrift.py @@ -0,0 +1,22 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.correct_signal_drift import ( + CorrectSignalDrift, +) +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_correctsignaldrift_1(): + task = CorrectSignalDrift() + task.inputs.in_file = File.sample(seed=0) + task.inputs.bias_file = File.sample(seed=1) + task.inputs.brainmask_file = File.sample(seed=2) + task.inputs.bval_file = File.sample(seed=4) + task.inputs.full_epi = File.sample(seed=5) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_diffusionmodel.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_diffusionmodel.py new file mode 100644 index 0000000..ffa0b8c --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_diffusionmodel.py @@ -0,0 +1,19 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.diffusion_model import DiffusionModel +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_diffusionmodel_1(): + task = DiffusionModel() + task.inputs.in_file = File.sample(seed=0) + task.inputs.bvec_file = File.sample(seed=2) + task.inputs.brain_mask = File.sample(seed=3) + task.inputs.decimals = 3 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_diffusionqc.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_diffusionqc.py new file mode 100644 index 0000000..3a57024 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_diffusionqc.py @@ -0,0 +1,32 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.diffusion_qc import DiffusionQC +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_diffusionqc_1(): + task = DiffusionQC() + task.inputs.in_file = File.sample(seed=0) + task.inputs.in_b0 = File.sample(seed=1) + task.inputs.in_shells = [File.sample(seed=2)] + task.inputs.in_bval_file = File.sample(seed=4) + task.inputs.in_fa = File.sample(seed=8) + task.inputs.in_fa_nans = File.sample(seed=9) + task.inputs.in_fa_degenerate = File.sample(seed=10) + task.inputs.in_cfa = File.sample(seed=11) + task.inputs.in_md = File.sample(seed=12) + task.inputs.brain_mask = File.sample(seed=13) + task.inputs.wm_mask = File.sample(seed=14) + task.inputs.cc_mask = File.sample(seed=15) + task.inputs.spikes_mask = File.sample(seed=16) + task.inputs.direction = "all" + task.inputs.in_fd = File.sample(seed=19) + task.inputs.fd_thres = 0.2 + task.inputs.piesno_sigma = -1.0 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_extractorientations.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_extractorientations.py new file mode 100644 index 0000000..c720475 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_extractorientations.py @@ -0,0 +1,19 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.extract_orientations import ( + ExtractOrientations, +) +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_extractorientations_1(): + task = ExtractOrientations() + task.inputs.in_file = File.sample(seed=0) + task.inputs.in_bvec_file = File.sample(seed=2) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_filtershells.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_filtershells.py new file mode 100644 index 0000000..2c63852 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_filtershells.py @@ -0,0 +1,18 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.filter_shells import FilterShells +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_filtershells_1(): + task = FilterShells() + task.inputs.in_file = File.sample(seed=0) + task.inputs.bvec_file = File.sample(seed=2) + task.inputs.b_threshold = 1100 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_numberofshells.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_numberofshells.py new file mode 100644 index 0000000..b5b8fff --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_numberofshells.py @@ -0,0 +1,18 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.number_of_shells import NumberOfShells +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_numberofshells_1(): + task = NumberOfShells() + task.inputs.in_bvals = File.sample(seed=0) + task.inputs.b0_threshold = 50 + task.inputs.dsi_threshold = 11 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_piesno.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_piesno.py new file mode 100644 index 0000000..eff5f4b --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_piesno.py @@ -0,0 +1,17 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.piesno import PIESNO +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_piesno_1(): + task = PIESNO() + task.inputs.in_file = File.sample(seed=0) + task.inputs.n_channels = 4 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_readdwimetadata.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_readdwimetadata.py new file mode 100644 index 0000000..68cb47c --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_readdwimetadata.py @@ -0,0 +1,18 @@ +from fileformats.generic import Directory, File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.read_dwi_metadata import ReadDWIMetadata +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_readdwimetadata_1(): + task = ReadDWIMetadata() + task.inputs.in_file = File.sample(seed=0) + task.inputs.bids_validate = True + task.inputs.index_db = Directory.sample(seed=3) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_rotatevectors.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_rotatevectors.py new file mode 100644 index 0000000..d951a5e --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_rotatevectors.py @@ -0,0 +1,18 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.rotate_vectors import RotateVectors +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_rotatevectors_1(): + task = RotateVectors() + task.inputs.in_file = File.sample(seed=0) + task.inputs.reference = File.sample(seed=1) + task.inputs.transforms = File.sample(seed=2) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_spikingvoxelsmask.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_spikingvoxelsmask.py new file mode 100644 index 0000000..cfb1fff --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_spikingvoxelsmask.py @@ -0,0 +1,18 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.spiking_voxels_mask import SpikingVoxelsMask +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_spikingvoxelsmask_1(): + task = SpikingVoxelsMask() + task.inputs.in_file = File.sample(seed=0) + task.inputs.brain_mask = File.sample(seed=1) + task.inputs.z_threshold = 3.0 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_splitshells.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_splitshells.py new file mode 100644 index 0000000..a9cbc5c --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_splitshells.py @@ -0,0 +1,16 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.split_shells import SplitShells +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_splitshells_1(): + task = SplitShells() + task.inputs.in_file = File.sample(seed=0) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/tests/test_weightedstat.py b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_weightedstat.py new file mode 100644 index 0000000..c0b9266 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/tests/test_weightedstat.py @@ -0,0 +1,17 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.diffusion.weighted_stat import WeightedStat +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_weightedstat_1(): + task = WeightedStat() + task.inputs.in_file = File.sample(seed=0) + task.inputs.stat = "mean" + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/diffusion/weighted_stat.py b/pydra/tasks/mriqc/interfaces/diffusion/weighted_stat.py new file mode 100644 index 0000000..5d2dc07 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/diffusion/weighted_stat.py @@ -0,0 +1,57 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix +import numpy as np +import os +from pydra.compose import python +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class WeightedStat(python.Task["WeightedStat.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.diffusion.weighted_stat import WeightedStat + + """ + + in_file: File + in_weights: list + stat: ty.Any = mean + + class Outputs(python.Outputs): + out_file: File + + @staticmethod + def function(in_file: File, in_weights: list, stat: ty.Any) -> File: + out_file = attrs.NOTHING + img = nb.load(in_file) + weights = [float(w) for w in in_weights] + data = np.asanyarray(img.dataobj) + statmap = np.average(data, weights=weights, axis=-1) + + out_file = fname_presuffix(in_file, suffix=f"_{stat}", newpath=os.getcwd()) + + if stat == "std": + statmap = np.sqrt( + np.average( + (data - statmap[..., np.newaxis]) ** 2, weights=weights, axis=-1 + ) + ) + + hdr = img.header.copy() + img.__class__( + statmap.astype(hdr.get_data_dtype()), + img.affine, + hdr, + ).to_filename(out_file) + + return out_file diff --git a/pydra/tasks/mriqc/interfaces/functional/__init__.py b/pydra/tasks/mriqc/interfaces/functional/__init__.py new file mode 100644 index 0000000..293bd8b --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/__init__.py @@ -0,0 +1,214 @@ +import attrs +from fileformats.generic import Directory, File +import json +import logging +import numpy as np +from pathlib import Path +from pydra.compose import python, shell, workflow +from .functional_qc import FunctionalQC +from .gather_timeseries import GatherTimeseries +from .select_echo import SelectEcho +from .spikes import Spikes +from pydra.utils.typing import MultiInputObj +import typing as ty +import yaml + + +logger = logging.getLogger(__name__) + + +def _build_timeseries_metadata(): + + return { + "trans_x": { + "LongName": "Translation Along X Axis", + "Description": "Estimated Motion Parameter", + "Units": "mm", + }, + "trans_y": { + "LongName": "Translation Along Y Axis", + "Description": "Estimated Motion Parameter", + "Units": "mm", + }, + "trans_z": { + "LongName": "Translation Along Z Axis", + "Description": "Estimated Motion Parameter", + "Units": "mm", + }, + "rot_x": { + "LongName": "Rotation Around X Axis", + "Description": "Estimated Motion Parameter", + "Units": "rad", + }, + "rot_y": { + "LongName": "Rotation Around X Axis", + "Description": "Estimated Motion Parameter", + "Units": "rad", + }, + "rot_z": { + "LongName": "Rotation Around X Axis", + "Description": "Estimated Motion Parameter", + "Units": "rad", + }, + "dvars_std": { + "LongName": "Derivative of RMS Variance over Voxels, Standardized", + "Description": ( + "Indexes the rate of change of BOLD signal across" + "the entire brain at each frame of data, normalized with the" + "standard deviation of the temporal difference time series" + ), + }, + "dvars_nstd": { + "LongName": ("Derivative of RMS Variance over Voxels, Non-Standardized"), + "Description": ( + "Indexes the rate of change of BOLD signal across" + "the entire brain at each frame of data, not normalized." + ), + }, + "dvars_vstd": { + "LongName": "Derivative of RMS Variance over Voxels, Standardized", + "Description": ( + "Indexes the rate of change of BOLD signal across" + "the entire brain at each frame of data, normalized across" + "time by that voxel standard deviation across time," + "before computing the RMS of the temporal difference" + ), + }, + "framewise_displacement": { + "LongName": "Framewise Displacement", + "Description": ( + "A quantification of the estimated bulk-head" + "motion calculated using formula proposed by Power (2012)" + ), + "Units": "mm", + }, + "aqi": { + "LongName": "AFNI's Quality Index", + "Description": "Mean quality index as computed by AFNI's 3dTqual", + }, + "aor": { + "LongName": "AFNI's Fraction of Outliers per Volume", + "Description": ( + "Mean fraction of outliers per fMRI volume as given by AFNI's 3dToutcount" + ), + }, + } + + +def _get_echotime(inlist): + + if isinstance(inlist, list): + retval = [_get_echotime(el) for el in inlist] + return retval[0] if len(retval) == 1 else retval + echo_time = inlist.get("EchoTime", None) if inlist else None + if echo_time: + return float(echo_time) + + +def _robust_zscore(data): + + return (data - np.atleast_2d(np.median(data, axis=1)).T) / np.atleast_2d( + data.std(axis=1) + ).T + + +def find_peaks(data): + + t_z = [data[:, :, i, :].mean(axis=0).mean(axis=0) for i in range(data.shape[2])] + return t_z + + +def find_spikes(data, spike_thresh): + + data -= np.median(np.median(np.median(data, axis=0), axis=0), axis=0) + slice_mean = np.median(np.median(data, axis=0), axis=0) + t_z = _robust_zscore(slice_mean) + spikes = np.abs(t_z) > spike_thresh + spike_inds = np.transpose(spikes.nonzero()) + # mask out the spikes and recompute z-scores using variance uncontaminated with spikes. + # This will catch smaller spikes that may have been swamped by big + # ones. + data.mask[:, :, spike_inds[:, 0], spike_inds[:, 1]] = True + slice_mean2 = np.median(np.median(data, axis=0), axis=0) + t_z = _robust_zscore(slice_mean2) + spikes = np.logical_or(spikes, np.abs(t_z) > spike_thresh) + spike_inds = [tuple(i) for i in np.transpose(spikes.nonzero())] + return spike_inds, t_z + + +def select_echo( + in_files: str | list[str], + te_echos: list[float | type(attrs.NOTHING) | None] | None = None, + te_reference: float = 0.030, +) -> str: + """ + Select the echo file with the closest echo time to the reference echo time. + + Used to grab the echo file when processing multi-echo data through workflows + that only accept a single file. + + Parameters + ---------- + in_files : :obj:`str` or :obj:`list` + A single filename or a list of filenames. + te_echos : :obj:`list` of :obj:`float` + List of echo times corresponding to each file. + If not a number (typically, a :obj:`~nipype.interfaces.base.type(attrs.NOTHING)`), + the function selects the second echo. + te_reference : float, optional + Reference echo time used to find the closest echo time. + + Returns + ------- + str + The selected echo file. + + Examples + -------- + >>> select_echo("single-echo.nii.gz") + ('single-echo.nii.gz', -1) + + >>> select_echo(["single-echo.nii.gz"]) + ('single-echo.nii.gz', -1) + + >>> select_echo( + ... [f"echo{n}.nii.gz" for n in range(1,7)], + ... ) + ('echo2.nii.gz', 1) + + >>> select_echo( + ... [f"echo{n}.nii.gz" for n in range(1,7)], + ... te_echos=[12.5, 28.5, 34.2, 45.0, 56.1, 68.4], + ... te_reference=33.1, + ... ) + ('echo3.nii.gz', 2) + + >>> select_echo( + ... [f"echo{n}.nii.gz" for n in range(1,7)], + ... te_echos=[12.5, 28.5, 34.2, 45.0, 56.1], + ... te_reference=33.1, + ... ) + ('echo2.nii.gz', 1) + + >>> select_echo( + ... [f"echo{n}.nii.gz" for n in range(1,7)], + ... te_echos=[12.5, 28.5, 34.2, 45.0, 56.1, None], + ... te_reference=33.1, + ... ) + ('echo2.nii.gz', 1) + + """ + if not isinstance(in_files, (list, tuple)): + return in_files, -1 + if len(in_files) == 1: + return in_files[0], -1 + import numpy as np + + n_echos = len(in_files) + if te_echos is not None and len(te_echos) == n_echos: + try: + index = np.argmin(np.abs(np.array(te_echos) - te_reference)) + return in_files[index], index + except TypeError: + pass + return in_files[1], 1 diff --git a/pydra/tasks/mriqc/interfaces/functional/functional_qc.py b/pydra/tasks/mriqc/interfaces/functional/functional_qc.py new file mode 100644 index 0000000..ec079d0 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/functional_qc.py @@ -0,0 +1,173 @@ +import attrs +from fileformats.generic import File +import logging +from pydra.tasks.mriqc.qc.anatomical import efc, fber, snr, summary_stats +from pydra.tasks.mriqc.qc.functional import gsr +from pydra.tasks.mriqc.utils.misc import _flatten_dict +import nibabel as nb +import numpy as np +from pydra.compose import python +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class FunctionalQC(python.Task["FunctionalQC.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.functional.functional_qc import FunctionalQC + + """ + + in_epi: File + in_hmc: File + in_tsnr: File + in_mask: File + direction: ty.Any = all + in_fd: File + fd_thres: float = 0.2 + in_dvars: File + in_fwhm: list + + class Outputs(python.Outputs): + fber: float + efc: float + snr: float + gsr: dict + tsnr: float + dvars: dict + fd: dict + fwhm: dict + size: dict + spacing: dict + summary: dict + out_qc: dict + + @staticmethod + def function( + in_epi: File, + in_hmc: File, + in_tsnr: File, + in_mask: File, + direction: ty.Any, + in_fd: File, + fd_thres: float, + in_dvars: File, + in_fwhm: list, + ) -> tuples[ + float, float, float, dict, float, dict, dict, dict, dict, dict, dict, dict + ]: + fber = attrs.NOTHING + efc = attrs.NOTHING + snr = attrs.NOTHING + gsr = attrs.NOTHING + tsnr = attrs.NOTHING + dvars = attrs.NOTHING + fd = attrs.NOTHING + fwhm = attrs.NOTHING + size = attrs.NOTHING + spacing = attrs.NOTHING + summary = attrs.NOTHING + out_qc = attrs.NOTHING + self_dict = {} + + epinii = nb.load(in_epi) + epidata = np.nan_to_num(np.float32(epinii.dataobj)) + epidata[epidata < 0] = 0 + + hmcnii = nb.load(in_hmc) + hmcdata = np.nan_to_num(np.float32(hmcnii.dataobj)) + hmcdata[hmcdata < 0] = 0 + + msknii = nb.load(in_mask) + mskdata = np.asanyarray(msknii.dataobj) > 0 + if np.sum(mskdata) < 100: + raise RuntimeError( + "Detected less than 100 voxels belonging to the brain mask. " + "MRIQC failed to process this dataset." + ) + + rois = {"fg": mskdata.astype(np.uint8), "bg": (~mskdata).astype(np.uint8)} + stats = summary_stats(epidata, rois) + summary = stats + + snr = snr(stats["fg"]["median"], stats["fg"]["stdv"], stats["fg"]["n"]) + + fber = fber(epidata, mskdata.astype(np.uint8)) + + efc = efc(epidata) + + gsr = {} + if direction == "all": + epidir = ["x", "y"] + else: + epidir = [direction] + + for axis in epidir: + gsr[axis] = gsr(epidata, mskdata.astype(np.uint8), direction=axis) + + dvars_avg = np.loadtxt(in_dvars, skiprows=1, usecols=list(range(3))).mean( + axis=0 + ) + dvars_col = ["std", "nstd", "vstd"] + dvars = {key: float(val) for key, val in zip(dvars_col, dvars_avg)} + + tsnr_data = nb.load(in_tsnr).get_fdata() + tsnr = float(np.median(tsnr_data[mskdata])) + + fd_data = np.loadtxt(in_fd, skiprows=1) + num_fd = (fd_data > fd_thres).sum() + fd = { + "mean": float(fd_data.mean()), + "num": int(num_fd), + "perc": float(num_fd * 100 / (len(fd_data) + 1)), + } + + fwhm = np.array(in_fwhm[:3]) / np.array(hmcnii.header.get_zooms()[:3]) + fwhm = { + "x": float(fwhm[0]), + "y": float(fwhm[1]), + "z": float(fwhm[2]), + "avg": float(np.average(fwhm)), + } + + size = { + "x": int(hmcdata.shape[0]), + "y": int(hmcdata.shape[1]), + "z": int(hmcdata.shape[2]), + } + spacing = { + i: float(v) for i, v in zip(["x", "y", "z"], hmcnii.header.get_zooms()[:3]) + } + + try: + size["t"] = int(hmcdata.shape[3]) + except IndexError: + pass + + try: + spacing["tr"] = float(hmcnii.header.get_zooms()[3]) + except IndexError: + pass + + out_qc = _flatten_dict(self_dict["_results"]) + + return ( + fber, + efc, + snr, + gsr, + tsnr, + dvars, + fd, + fwhm, + size, + spacing, + summary, + out_qc, + ) diff --git a/pydra/tasks/mriqc/interfaces/functional/gather_timeseries.py b/pydra/tasks/mriqc/interfaces/functional/gather_timeseries.py new file mode 100644 index 0000000..d1d2aed --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/gather_timeseries.py @@ -0,0 +1,163 @@ +import attrs +from fileformats.generic import File +import logging +from pydra.tasks.mriqc.nipype_ports.utils.misc import normalize_mc_params +import numpy as np +from os import path as op +import os +import pandas as pd +from pydra.compose import python +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class GatherTimeseries(python.Task["GatherTimeseries.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.functional.gather_timeseries import GatherTimeseries + + """ + + dvars: File + fd: File + mpars: File + mpars_source: ty.Any + outliers: File + quality: File + + class Outputs(python.Outputs): + timeseries_file: File + timeseries_metadata: dict + + @staticmethod + def function( + dvars: File, + fd: File, + mpars: File, + mpars_source: ty.Any, + outliers: File, + quality: File, + ) -> tuples[File, dict]: + timeseries_file = attrs.NOTHING + timeseries_metadata = attrs.NOTHING + + mpars = np.apply_along_axis( + func1d=normalize_mc_params, + axis=1, + arr=np.loadtxt(mpars), # mpars is N_t x 6 + source=mpars_source, + ) + timeseries = pd.DataFrame( + mpars, columns=["trans_x", "trans_y", "trans_z", "rot_x", "rot_y", "rot_z"] + ) + + dvars = pd.read_csv( + dvars, + sep=r"\s+", + skiprows=1, # column names have spaces + header=None, + names=["dvars_std", "dvars_nstd", "dvars_vstd"], + ) + dvars.index = pd.RangeIndex(1, timeseries.index.max() + 1) + + fd = pd.read_csv(fd, sep=r"\s+", header=0, names=["framewise_displacement"]) + fd.index = pd.RangeIndex(1, timeseries.index.max() + 1) + + aqi = pd.read_csv(quality, sep=r"\s+", header=None, names=["aqi"]) + + aor = pd.read_csv(outliers, sep=r"\s+", header=None, names=["aor"]) + + timeseries = pd.concat((timeseries, dvars, fd, aqi, aor), axis=1) + + timeseries_file = op.join(os.getcwd(), "timeseries.tsv") + + timeseries.to_csv(timeseries_file, sep="\t", index=False, na_rep="n/a") + + timeseries_file = timeseries_file + timeseries_metadata = _build_timeseries_metadata() + + return timeseries_file, timeseries_metadata + + +def _build_timeseries_metadata(): + + return { + "trans_x": { + "LongName": "Translation Along X Axis", + "Description": "Estimated Motion Parameter", + "Units": "mm", + }, + "trans_y": { + "LongName": "Translation Along Y Axis", + "Description": "Estimated Motion Parameter", + "Units": "mm", + }, + "trans_z": { + "LongName": "Translation Along Z Axis", + "Description": "Estimated Motion Parameter", + "Units": "mm", + }, + "rot_x": { + "LongName": "Rotation Around X Axis", + "Description": "Estimated Motion Parameter", + "Units": "rad", + }, + "rot_y": { + "LongName": "Rotation Around X Axis", + "Description": "Estimated Motion Parameter", + "Units": "rad", + }, + "rot_z": { + "LongName": "Rotation Around X Axis", + "Description": "Estimated Motion Parameter", + "Units": "rad", + }, + "dvars_std": { + "LongName": "Derivative of RMS Variance over Voxels, Standardized", + "Description": ( + "Indexes the rate of change of BOLD signal across" + "the entire brain at each frame of data, normalized with the" + "standard deviation of the temporal difference time series" + ), + }, + "dvars_nstd": { + "LongName": ("Derivative of RMS Variance over Voxels, Non-Standardized"), + "Description": ( + "Indexes the rate of change of BOLD signal across" + "the entire brain at each frame of data, not normalized." + ), + }, + "dvars_vstd": { + "LongName": "Derivative of RMS Variance over Voxels, Standardized", + "Description": ( + "Indexes the rate of change of BOLD signal across" + "the entire brain at each frame of data, normalized across" + "time by that voxel standard deviation across time," + "before computing the RMS of the temporal difference" + ), + }, + "framewise_displacement": { + "LongName": "Framewise Displacement", + "Description": ( + "A quantification of the estimated bulk-head" + "motion calculated using formula proposed by Power (2012)" + ), + "Units": "mm", + }, + "aqi": { + "LongName": "AFNI's Quality Index", + "Description": "Mean quality index as computed by AFNI's 3dTqual", + }, + "aor": { + "LongName": "AFNI's Fraction of Outliers per Volume", + "Description": ( + "Mean fraction of outliers per fMRI volume as given by AFNI's 3dToutcount" + ), + }, + } diff --git a/pydra/tasks/mriqc/interfaces/functional/select_echo.py b/pydra/tasks/mriqc/interfaces/functional/select_echo.py new file mode 100644 index 0000000..91b4a2c --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/select_echo.py @@ -0,0 +1,140 @@ +import attrs +from fileformats.generic import File +import logging +import numpy as np +from pydra.compose import python +from pydra.utils.typing import MultiInputObj + + +logger = logging.getLogger(__name__) + + +@python.define +class SelectEcho(python.Task["SelectEcho.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.functional.select_echo import SelectEcho + >>> from pydra.utils.typing import MultiInputObj + + """ + + in_files: List + metadata: MultiInputObj + te_reference: float = 0.03 + + class Outputs(python.Outputs): + out_file: File + echo_index: int + is_multiecho: bool + + @staticmethod + def function( + in_files: List, metadata: MultiInputObj, te_reference: float + ) -> tuples[File, int, bool]: + out_file = attrs.NOTHING + echo_index = attrs.NOTHING + is_multiecho = attrs.NOTHING + ( + out_file, + echo_index, + ) = select_echo( + in_files, + te_echos=( + _get_echotime(metadata) if (metadata is not attrs.NOTHING) else None + ), + te_reference=te_reference, + ) + is_multiecho = echo_index != -1 + + return out_file, echo_index, is_multiecho + + +def _get_echotime(inlist): + + if isinstance(inlist, list): + retval = [_get_echotime(el) for el in inlist] + return retval[0] if len(retval) == 1 else retval + echo_time = inlist.get("EchoTime", None) if inlist else None + if echo_time: + return float(echo_time) + + +def select_echo( + in_files: str | list[str], + te_echos: list[float | type(attrs.NOTHING) | None] | None = None, + te_reference: float = 0.030, +) -> str: + """ + Select the echo file with the closest echo time to the reference echo time. + + Used to grab the echo file when processing multi-echo data through workflows + that only accept a single file. + + Parameters + ---------- + in_files : :obj:`str` or :obj:`list` + A single filename or a list of filenames. + te_echos : :obj:`list` of :obj:`float` + List of echo times corresponding to each file. + If not a number (typically, a :obj:`~nipype.interfaces.base.type(attrs.NOTHING)`), + the function selects the second echo. + te_reference : float, optional + Reference echo time used to find the closest echo time. + + Returns + ------- + str + The selected echo file. + + Examples + -------- + >>> select_echo("single-echo.nii.gz") + ('single-echo.nii.gz', -1) + + >>> select_echo(["single-echo.nii.gz"]) + ('single-echo.nii.gz', -1) + + >>> select_echo( + ... [f"echo{n}.nii.gz" for n in range(1,7)], + ... ) + ('echo2.nii.gz', 1) + + >>> select_echo( + ... [f"echo{n}.nii.gz" for n in range(1,7)], + ... te_echos=[12.5, 28.5, 34.2, 45.0, 56.1, 68.4], + ... te_reference=33.1, + ... ) + ('echo3.nii.gz', 2) + + >>> select_echo( + ... [f"echo{n}.nii.gz" for n in range(1,7)], + ... te_echos=[12.5, 28.5, 34.2, 45.0, 56.1], + ... te_reference=33.1, + ... ) + ('echo2.nii.gz', 1) + + >>> select_echo( + ... [f"echo{n}.nii.gz" for n in range(1,7)], + ... te_echos=[12.5, 28.5, 34.2, 45.0, 56.1, None], + ... te_reference=33.1, + ... ) + ('echo2.nii.gz', 1) + + """ + if not isinstance(in_files, (list, tuple)): + return in_files, -1 + if len(in_files) == 1: + return in_files[0], -1 + import numpy as np + + n_echos = len(in_files) + if te_echos is not None and len(te_echos) == n_echos: + try: + index = np.argmin(np.abs(np.array(te_echos) - te_reference)) + return in_files[index], index + except TypeError: + pass + return in_files[1], 1 diff --git a/pydra/tasks/mriqc/interfaces/functional/spikes.py b/pydra/tasks/mriqc/interfaces/functional/spikes.py new file mode 100644 index 0000000..2a09ae7 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/spikes.py @@ -0,0 +1,124 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +import numpy as np +from os import path as op +from pathlib import Path +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class Spikes(python.Task["Spikes.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.functional.spikes import Spikes + + """ + + in_file: File + in_mask: File + invert_mask: bool = False + no_zscore: bool = False + detrend: bool = True + spike_thresh: float = 6.0 + skip_frames: int = 0 + out_tsz: Path = spikes_tsz.txt + out_spikes: Path = spikes_idx.txt + + class Outputs(python.Outputs): + out_tsz: File + out_spikes: File + num_spikes: int + + @staticmethod + def function( + in_file: File, + in_mask: File, + invert_mask: bool, + no_zscore: bool, + detrend: bool, + spike_thresh: float, + skip_frames: int, + out_tsz: Path, + out_spikes: Path, + ) -> tuples[File, File, int]: + out_tsz = attrs.NOTHING + out_spikes = attrs.NOTHING + num_spikes = attrs.NOTHING + func_nii = nb.load(in_file) + func_data = func_nii.get_fdata(dtype="float32") + func_shape = func_data.shape + ntsteps = func_shape[-1] + tr = func_nii.header.get_zooms()[-1] + nskip = skip_frames + + mask_data = np.bool_(nb.load(in_mask).dataobj) + mask_data[..., :nskip] = 0 + mask_data = np.stack([mask_data] * ntsteps, axis=-1) + + if not invert_mask: + brain = np.ma.array(func_data, mask=(mask_data != 1)) + else: + mask_data[..., :skip_frames] = 1 + brain = np.ma.array(func_data, mask=(mask_data == 1)) + + if detrend: + from nilearn.signal import clean + + brain = clean(brain[:, nskip:].T, t_r=tr, standardize=False).T + + if no_zscore: + ts_z = find_peaks(brain) + total_spikes = [] + else: + total_spikes, ts_z = find_spikes(brain, spike_thresh) + total_spikes = list(set(total_spikes)) + + out_tsz = op.abspath(out_tsz) + out_tsz = out_tsz + np.savetxt(out_tsz, ts_z) + + out_spikes = op.abspath(out_spikes) + out_spikes = out_spikes + np.savetxt(out_spikes, total_spikes) + num_spikes = len(total_spikes) + + return out_tsz, out_spikes, num_spikes + + +def _robust_zscore(data): + + return (data - np.atleast_2d(np.median(data, axis=1)).T) / np.atleast_2d( + data.std(axis=1) + ).T + + +def find_peaks(data): + + t_z = [data[:, :, i, :].mean(axis=0).mean(axis=0) for i in range(data.shape[2])] + return t_z + + +def find_spikes(data, spike_thresh): + + data -= np.median(np.median(np.median(data, axis=0), axis=0), axis=0) + slice_mean = np.median(np.median(data, axis=0), axis=0) + t_z = _robust_zscore(slice_mean) + spikes = np.abs(t_z) > spike_thresh + spike_inds = np.transpose(spikes.nonzero()) + # mask out the spikes and recompute z-scores using variance uncontaminated with spikes. + # This will catch smaller spikes that may have been swamped by big + # ones. + data.mask[:, :, spike_inds[:, 0], spike_inds[:, 1]] = True + slice_mean2 = np.median(np.median(data, axis=0), axis=0) + t_z = _robust_zscore(slice_mean2) + spikes = np.logical_or(spikes, np.abs(t_z) > spike_thresh) + spike_inds = [tuple(i) for i in np.transpose(spikes.nonzero())] + return spike_inds, t_z diff --git a/pydra/tasks/mriqc/interfaces/functional/tests/conftest.py b/pydra/tasks/mriqc/interfaces/functional/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/functional/tests/test_functionalqc.py b/pydra/tasks/mriqc/interfaces/functional/tests/test_functionalqc.py new file mode 100644 index 0000000..85839bf --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/tests/test_functionalqc.py @@ -0,0 +1,23 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.functional.functional_qc import FunctionalQC +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_functionalqc_1(): + task = FunctionalQC() + task.inputs.in_epi = File.sample(seed=0) + task.inputs.in_hmc = File.sample(seed=1) + task.inputs.in_tsnr = File.sample(seed=2) + task.inputs.in_mask = File.sample(seed=3) + task.inputs.direction = "all" + task.inputs.in_fd = File.sample(seed=5) + task.inputs.fd_thres = 0.2 + task.inputs.in_dvars = File.sample(seed=7) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/functional/tests/test_gathertimeseries.py b/pydra/tasks/mriqc/interfaces/functional/tests/test_gathertimeseries.py new file mode 100644 index 0000000..677c5a7 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/tests/test_gathertimeseries.py @@ -0,0 +1,20 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.functional.gather_timeseries import GatherTimeseries +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_gathertimeseries_1(): + task = GatherTimeseries() + task.inputs.dvars = File.sample(seed=0) + task.inputs.fd = File.sample(seed=1) + task.inputs.mpars = File.sample(seed=2) + task.inputs.outliers = File.sample(seed=4) + task.inputs.quality = File.sample(seed=5) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/functional/tests/test_selectecho.py b/pydra/tasks/mriqc/interfaces/functional/tests/test_selectecho.py new file mode 100644 index 0000000..08d3602 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/tests/test_selectecho.py @@ -0,0 +1,17 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.functional.select_echo import SelectEcho +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_selectecho_1(): + task = SelectEcho() + task.inputs.in_files = [File.sample(seed=0)] + task.inputs.te_reference = 0.03 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/functional/tests/test_spikes.py b/pydra/tasks/mriqc/interfaces/functional/tests/test_spikes.py new file mode 100644 index 0000000..6394182 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/functional/tests/test_spikes.py @@ -0,0 +1,24 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.functional.spikes import Spikes +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_spikes_1(): + task = Spikes() + task.inputs.in_file = File.sample(seed=0) + task.inputs.in_mask = File.sample(seed=1) + task.inputs.invert_mask = False + task.inputs.no_zscore = False + task.inputs.detrend = True + task.inputs.spike_thresh = 6.0 + task.inputs.skip_frames = 0 + task.inputs.out_tsz = "spikes_tsz.txt" + task.inputs.out_spikes = "spikes_idx.txt" + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/reports/__init__.py b/pydra/tasks/mriqc/interfaces/reports/__init__.py new file mode 100644 index 0000000..8d9323e --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/reports/__init__.py @@ -0,0 +1 @@ +from .add_provenance import AddProvenance diff --git a/pydra/tasks/mriqc/interfaces/reports/add_provenance.py b/pydra/tasks/mriqc/interfaces/reports/add_provenance.py new file mode 100644 index 0000000..65e9143 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/reports/add_provenance.py @@ -0,0 +1,60 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +import numpy as np +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class AddProvenance(python.Task["AddProvenance.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.reports.add_provenance import AddProvenance + + """ + + in_file: File + air_msk: File + rot_msk: File + modality: str + + class Outputs(python.Outputs): + out_prov: dict + + @staticmethod + def function(in_file: File, air_msk: File, rot_msk: File, modality: str) -> dict: + out_prov = attrs.NOTHING + from nipype.utils.filemanip import hash_infile + + out_prov = { + "md5sum": hash_infile(in_file), + "version": '', + "software": "mriqc", + "settings": { + "testing": False, + }, + } + + if modality in ("T1w", "T2w"): + air_msk_size = np.asanyarray(nb.load(air_msk).dataobj).astype(bool).sum() + rot_msk_size = np.asanyarray(nb.load(rot_msk).dataobj).astype(bool).sum() + out_prov["warnings"] = { + "small_air_mask": bool(air_msk_size < 5e5), + "large_rot_frame": bool(rot_msk_size > 500), + } + + if modality == "bold": + out_prov["settings"].update( + { + "fd_thres": 0.2, # .fd_thres + } + ) + + return out_prov diff --git a/pydra/tasks/mriqc/interfaces/reports/tests/conftest.py b/pydra/tasks/mriqc/interfaces/reports/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/reports/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/reports/tests/test_addprovenance.py b/pydra/tasks/mriqc/interfaces/reports/tests/test_addprovenance.py new file mode 100644 index 0000000..61e3ea5 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/reports/tests/test_addprovenance.py @@ -0,0 +1,18 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.reports.add_provenance import AddProvenance +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_addprovenance_1(): + task = AddProvenance() + task.inputs.in_file = File.sample(seed=0) + task.inputs.air_msk = File.sample(seed=1) + task.inputs.rot_msk = File.sample(seed=2) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/synthstrip/__init__.py b/pydra/tasks/mriqc/interfaces/synthstrip/__init__.py new file mode 100644 index 0000000..63e821d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/synthstrip/__init__.py @@ -0,0 +1 @@ +from .synth_strip import SynthStrip diff --git a/pydra/tasks/mriqc/interfaces/synthstrip/synth_strip.py b/pydra/tasks/mriqc/interfaces/synthstrip/synth_strip.py new file mode 100644 index 0000000..cbeab14 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/synthstrip/synth_strip.py @@ -0,0 +1,43 @@ +from fileformats.generic import File +import logging +from pathlib import Path +from pydra.compose import shell + + +logger = logging.getLogger(__name__) + + +@shell.define +class SynthStrip(shell.Task["SynthStrip.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.synthstrip.synth_strip import SynthStrip + + """ + + executable = "synthstrip" + in_file: File = shell.arg( + help="Input image to be brain extracted", + argstr="-i {in_file}", + copy_mode="File.CopyMode.copy", + ) + use_gpu: bool = shell.arg(help="Use GPU", argstr="-g", default=False) + model: File = shell.arg( + help="file containing model's weights", + argstr="--model {model}", + default="/Applications/freesurfer/7.4.1/models/synthstrip.1.pt", + ) + border_mm: int = shell.arg( + help="Mask border threshold in mm", argstr="-b {border_mm}", default=1 + ) + num_threads: int = shell.arg(help="Number of threads", argstr="-n {num_threads}") + + class Outputs(shell.Outputs): + out_mask: Path = shell.outarg( + help="store brainmask to file", + argstr="-m {out_mask}", + path_template="{in_file}_desc-brain_mask.nii.gz", + ) diff --git a/pydra/tasks/mriqc/interfaces/synthstrip/tests/conftest.py b/pydra/tasks/mriqc/interfaces/synthstrip/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/synthstrip/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/synthstrip/tests/test_synthstrip.py b/pydra/tasks/mriqc/interfaces/synthstrip/tests/test_synthstrip.py new file mode 100644 index 0000000..ab311e4 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/synthstrip/tests/test_synthstrip.py @@ -0,0 +1,20 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.synthstrip.synth_strip import SynthStrip +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_synthstrip_1(): + task = SynthStrip() + task.inputs.in_file = File.sample(seed=0) + task.inputs.use_gpu = False + task.inputs.model = File.sample(seed=2) + task.inputs.border_mm = 1 + print(f"CMDLINE: {task.cmdline}\n\n") + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/tests/conftest.py b/pydra/tasks/mriqc/interfaces/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/tests/test_derivativesdatasink.py b/pydra/tasks/mriqc/interfaces/tests/test_derivativesdatasink.py new file mode 100644 index 0000000..1e144d1 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/tests/test_derivativesdatasink.py @@ -0,0 +1,21 @@ +from fileformats.generic import Directory, File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.derivatives_data_sink import DerivativesDataSink +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_derivativesdatasink_1(): + task = DerivativesDataSink() + task.inputs.base_directory = Directory.sample(seed=0) + task.inputs.check_hdr = True + task.inputs.compress = [] + task.inputs.dismiss_entities = [] + task.inputs.in_file = [File.sample(seed=5)] + task.inputs.source_file = [File.sample(seed=7)] + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/transitional/__init__.py b/pydra/tasks/mriqc/interfaces/transitional/__init__.py new file mode 100644 index 0000000..0ef3ac2 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/transitional/__init__.py @@ -0,0 +1 @@ +from .gcor import GCOR diff --git a/pydra/tasks/mriqc/interfaces/transitional/gcor.py b/pydra/tasks/mriqc/interfaces/transitional/gcor.py new file mode 100644 index 0000000..53f6aa2 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/transitional/gcor.py @@ -0,0 +1,63 @@ +import attrs +from fileformats.generic import File +from fileformats.medimage import Nifti1 +import logging +from pydra.compose import shell + + +logger = logging.getLogger(__name__) + + +def _list_outputs(inputs=None, stdout=None, stderr=None, output_dir=None): + inputs = attrs.asdict(inputs) + + return {"out": parsed_inputs["_gcor"]} + + +def out_callable(output_dir, inputs, stdout, stderr): + outputs = _list_outputs( + output_dir=output_dir, inputs=inputs, stdout=stdout, stderr=stderr + ) + return outputs.get("out") + + +@shell.define +class GCOR(shell.Task["GCOR.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from fileformats.medimage import Nifti1 + >>> from pydra.tasks.mriqc.interfaces.transitional.gcor import GCOR + + >>> task = GCOR() + >>> task.inputs.in_file = Nifti1.mock("func.nii") + >>> task.inputs.mask = File.mock() + >>> task.inputs.nfirst = 4 + >>> task.cmdline + '@compute_gcor -nfirst 4 -input func.nii' + + + """ + + executable = "@compute_gcor" + in_file: Nifti1 = shell.arg( + help="input dataset to compute the GCOR over", + argstr="-input {in_file}", + position=-1, + ) + mask: File = shell.arg( + help="mask dataset, for restricting the computation", argstr="-mask {mask}" + ) + nfirst: int = shell.arg( + help="specify number of initial TRs to ignore", argstr="-nfirst {nfirst}" + ) + no_demean: bool = shell.arg( + help="do not (need to) demean as first step", argstr="-no_demean" + ) + + class Outputs(shell.Outputs): + out: float | None = shell.out( + help="global correlation value", callable=out_callable + ) diff --git a/pydra/tasks/mriqc/interfaces/transitional/tests/conftest.py b/pydra/tasks/mriqc/interfaces/transitional/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/transitional/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/transitional/tests/test_gcor.py b/pydra/tasks/mriqc/interfaces/transitional/tests/test_gcor.py new file mode 100644 index 0000000..5d72b01 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/transitional/tests/test_gcor.py @@ -0,0 +1,29 @@ +from fileformats.generic import File +from fileformats.medimage import Nifti1 +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.transitional.gcor import GCOR +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_gcor_1(): + task = GCOR() + task.inputs.in_file = Nifti1.sample(seed=0) + task.inputs.mask = File.sample(seed=1) + print(f"CMDLINE: {task.cmdline}\n\n") + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) + + +@pytest.mark.xfail +def test_gcor_2(): + task = GCOR() + task.inputs.in_file = Nifti1.sample(seed=0) + task.inputs.nfirst = 4 + print(f"CMDLINE: {task.cmdline}\n\n") + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/webapi/__init__.py b/pydra/tasks/mriqc/interfaces/webapi/__init__.py new file mode 100644 index 0000000..c813019 --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/webapi/__init__.py @@ -0,0 +1,182 @@ +import attrs +from fileformats.generic import Directory, File +import json +import logging +from pydra.tasks.mriqc import messages +import orjson +from pathlib import Path +from pydra.compose import python, shell, workflow +from .upload_iq_ms import UploadIQMs +from pydra.utils.typing import MultiInputObj +import typing as ty +import yaml + + +logger = logging.getLogger(__name__) + + +def _hashfields(data): + + from hashlib import sha256 + + for name in HASH_BIDS: + if name in data: + data[name] = sha256(data[name].encode()).hexdigest() + return data + + +def upload_qc_metrics( + in_iqms, endpoint=None, email=None, auth_token=None, modality=None +): + """ + Upload qc metrics to remote repository. + + :param str in_iqms: Path to the qc metric json file as a string + :param str webapi_url: the protocol (either http or https) + :param str email: email address to be included with the metric submission + :param str auth_token: authentication token + + :return: either the response object if a response was successfully sent + or it returns the string "No Response" + :rtype: object + + + """ + from copy import deepcopy + import requests + + if not endpoint or not auth_token: + # If endpoint unknown, do not even report what happens to the token. + errmsg = "Unknown API endpoint" if not endpoint else "Authentication failed." + return Bunch(status_code=1, text=errmsg) + in_data = orjson.loads(Path(in_iqms).read_bytes()) + # Extract metadata and provenance + meta = in_data.pop("bids_meta") + prov = in_data.pop("provenance") + # At this point, data should contain only IQMs + data = deepcopy(in_data) + # Check modality + modality = meta.get("modality", None) or meta.get("suffix", None) or modality + if modality not in ("T1w", "bold", "T2w"): + errmsg = ( + 'Submitting to MRIQCWebAPI: image modality should be "bold", "T1w", or "T2w", ' + f'(found "{modality}")' + ) + return Bunch(status_code=1, text=errmsg) + # Filter metadata values that aren't in whitelist + data["bids_meta"] = {k: meta[k] for k in META_WHITELIST if k in meta} + # Check for fields with appended _id + bids_meta_names = { + k: k.replace("_id", "") for k in META_WHITELIST if k.endswith("_id") + } + data["bids_meta"].update( + {k: meta[v] for k, v in bids_meta_names.items() if v in meta} + ) + # For compatibility with WebAPI. Should be rolled back to int + if (run_id := data["bids_meta"].get("run_id", None)) is not None: + data["bids_meta"]["run_id"] = f"{run_id}" + # One more chance for spelled-out BIDS entity acquisition + if (acq_id := meta.get("acquisition", None)) is not None: + data["bids_meta"]["acq_id"] = acq_id + # Filter provenance values that aren't in whitelist + data["provenance"] = {k: prov[k] for k in PROV_WHITELIST if k in prov} + # Hash fields that may contain personal information + data["bids_meta"] = _hashfields(data["bids_meta"]) + data["bids_meta"]["modality"] = modality + if email: + data["provenance"]["email"] = email + headers = {"Authorization": auth_token, "Content-Type": "application/json"} + start_message = messages.QC_UPLOAD_START.format(url=endpoint) + logger.info(start_message) + errmsg = None + try: + # if the modality is bold, call "bold" endpoint + response = requests.post( + f"{endpoint}/{modality}", + headers=headers, + data=orjson.dumps(data, option=orjson.OPT_SERIALIZE_NUMPY), + timeout=15, + ) + except requests.ConnectionError as err: + errmsg = ("Error uploading IQMs: Connection error:", f"{err}") + except requests.exceptions.ReadTimeout as err: + errmsg = (f"Error uploading IQMs: Server {endpoint} is down.", f"{err}") + if errmsg is not None: + response = Bunch(status_code=1, text="\n".join(errmsg)) + return response, data + + +HASH_BIDS = ["subject_id", "session_id"] + +META_WHITELIST = [ + "AccelNumReferenceLines", + "AccelerationFactorPE", + "AcquisitionMatrix", + "CogAtlasID", + "CogPOID", + "CoilCombinationMethod", + "ContrastBolusIngredient", + "ConversionSoftware", + "ConversionSoftwareVersion", + "DelayTime", + "DeviceSerialNumber", + "EchoTime", + "EchoTrainLength", + "EffectiveEchoSpacing", + "FlipAngle", + "GradientSetType", + "HardcopyDeviceSoftwareVersion", + "ImageType", + "ImagingFrequency", + "InPlanePhaseEncodingDirection", + "InstitutionAddress", + "InstitutionName", + "Instructions", + "InversionTime", + "MRAcquisitionType", + "MRTransmitCoilSequence", + "MagneticFieldStrength", + "Manufacturer", + "ManufacturersModelName", + "MatrixCoilMode", + "MultibandAccelerationFactor", + "NumberOfAverages", + "NumberOfPhaseEncodingSteps", + "NumberOfVolumesDiscardedByScanner", + "NumberOfVolumesDiscardedByUser", + "NumberShots", + "ParallelAcquisitionTechnique", + "ParallelReductionFactorInPlane", + "PartialFourier", + "PartialFourierDirection", + "PatientPosition", + "PercentPhaseFieldOfView", + "PercentSampling", + "PhaseEncodingDirection", + "PixelBandwidth", + "ProtocolName", + "PulseSequenceDetails", + "PulseSequenceType", + "ReceiveCoilName", + "RepetitionTime", + "ScanOptions", + "ScanningSequence", + "SequenceName", + "SequenceVariant", + "SliceEncodingDirection", + "SoftwareVersions", + "TaskDescription", + "TaskName", + "TotalReadoutTime", + "TotalScanTimeSec", + "TransmitCoilName", + "VariableFlipAngleFlag", + "acq_id", + "modality", + "run_id", + "subject_id", + "task_id", + "session_id", +] + +PROV_WHITELIST = ["version", "md5sum", "software", "settings"] diff --git a/pydra/tasks/mriqc/interfaces/webapi/tests/conftest.py b/pydra/tasks/mriqc/interfaces/webapi/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/webapi/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/interfaces/webapi/tests/test_uploadiqms.py b/pydra/tasks/mriqc/interfaces/webapi/tests/test_uploadiqms.py new file mode 100644 index 0000000..828e34c --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/webapi/tests/test_uploadiqms.py @@ -0,0 +1,15 @@ +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.interfaces.webapi.upload_iq_ms import UploadIQMs +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_uploadiqms_1(): + task = UploadIQMs() + task.inputs.strict = False + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/interfaces/webapi/upload_iq_ms.py b/pydra/tasks/mriqc/interfaces/webapi/upload_iq_ms.py new file mode 100644 index 0000000..2a8e78f --- /dev/null +++ b/pydra/tasks/mriqc/interfaces/webapi/upload_iq_ms.py @@ -0,0 +1,269 @@ +import attrs +from fileformats.generic import File +import logging +import orjson +from pathlib import Path +from pydra.compose import python +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class UploadIQMs(python.Task["UploadIQMs.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.interfaces.webapi.upload_iq_ms import UploadIQMs + + """ + + in_iqms: dict + endpoint: str + auth_token: str + email: str + strict: bool = False + modality: str = undefined + + class Outputs(python.Outputs): + api_id: ty.Any + payload_file: File + + @staticmethod + def function( + in_iqms: dict, + endpoint: str, + auth_token: str, + email: str, + strict: bool, + modality: str, + ) -> tuples[ty.Any, File]: + api_id = attrs.NOTHING + payload_file = attrs.NOTHING + email = None + if email is not attrs.NOTHING: + email = email + + api_id = None + + response, payload = upload_qc_metrics( + in_iqms, + endpoint=endpoint, + auth_token=auth_token, + email=email, + modality=modality, + ) + + payload_str = orjson.dumps( + payload, + option=( + orjson.OPT_SORT_KEYS + | orjson.OPT_INDENT_2 + | orjson.OPT_APPEND_NEWLINE + | orjson.OPT_SERIALIZE_NUMPY + ), + ).decode("utf-8") + Path("payload.json").write_text(payload_str) + payload_file = str(Path("payload.json").absolute()) + + try: + api_id = response.json()["_id"] + except (AttributeError, KeyError, ValueError): + + errmsg = ( + "QC metrics upload failed to create an ID for the record " + f"uploaded. Response from server follows: {response.text}" + "\n\nPayload:\n" + f"{payload_str}" + ) + logger.warning(errmsg) + + if response.status_code == 201: + logger.info('QC metrics successfully uploaded.') + + errmsg = "\n".join( + [ + "Unsuccessful upload.", + f"Server response status {response.status_code}:", + response.text, + "", + "", + "Payload:", + f"{payload_str}", + ] + ) + logger.warning(errmsg) + if strict: + raise RuntimeError(errmsg) + + return api_id, payload_file + + +def _hashfields(data): + + from hashlib import sha256 + + for name in HASH_BIDS: + if name in data: + data[name] = sha256(data[name].encode()).hexdigest() + return data + + +def upload_qc_metrics( + in_iqms, endpoint=None, email=None, auth_token=None, modality=None +): + """ + Upload qc metrics to remote repository. + + :param str in_iqms: Path to the qc metric json file as a string + :param str webapi_url: the protocol (either http or https) + :param str email: email address to be included with the metric submission + :param str auth_token: authentication token + + :return: either the response object if a response was successfully sent + or it returns the string "No Response" + :rtype: object + + + """ + from copy import deepcopy + import requests + + if not endpoint or not auth_token: + # If endpoint unknown, do not even report what happens to the token. + errmsg = "Unknown API endpoint" if not endpoint else "Authentication failed." + return Bunch(status_code=1, text=errmsg) + in_data = orjson.loads(Path(in_iqms).read_bytes()) + # Extract metadata and provenance + meta = in_data.pop("bids_meta") + prov = in_data.pop("provenance") + # At this point, data should contain only IQMs + data = deepcopy(in_data) + # Check modality + modality = meta.get("modality", None) or meta.get("suffix", None) or modality + if modality not in ("T1w", "bold", "T2w"): + errmsg = ( + 'Submitting to MRIQCWebAPI: image modality should be "bold", "T1w", or "T2w", ' + f'(found "{modality}")' + ) + return Bunch(status_code=1, text=errmsg) + # Filter metadata values that aren't in whitelist + data["bids_meta"] = {k: meta[k] for k in META_WHITELIST if k in meta} + # Check for fields with appended _id + bids_meta_names = { + k: k.replace("_id", "") for k in META_WHITELIST if k.endswith("_id") + } + data["bids_meta"].update( + {k: meta[v] for k, v in bids_meta_names.items() if v in meta} + ) + # For compatibility with WebAPI. Should be rolled back to int + if (run_id := data["bids_meta"].get("run_id", None)) is not None: + data["bids_meta"]["run_id"] = f"{run_id}" + # One more chance for spelled-out BIDS entity acquisition + if (acq_id := meta.get("acquisition", None)) is not None: + data["bids_meta"]["acq_id"] = acq_id + # Filter provenance values that aren't in whitelist + data["provenance"] = {k: prov[k] for k in PROV_WHITELIST if k in prov} + # Hash fields that may contain personal information + data["bids_meta"] = _hashfields(data["bids_meta"]) + data["bids_meta"]["modality"] = modality + if email: + data["provenance"]["email"] = email + headers = {"Authorization": auth_token, "Content-Type": "application/json"} + start_message = 'MRIQC Web API: submitting to <{url}>'.format(url=endpoint) + logger.info(start_message) + errmsg = None + try: + # if the modality is bold, call "bold" endpoint + response = requests.post( + f"{endpoint}/{modality}", + headers=headers, + data=orjson.dumps(data, option=orjson.OPT_SERIALIZE_NUMPY), + timeout=15, + ) + except requests.ConnectionError as err: + errmsg = ("Error uploading IQMs: Connection error:", f"{err}") + except requests.exceptions.ReadTimeout as err: + errmsg = (f"Error uploading IQMs: Server {endpoint} is down.", f"{err}") + if errmsg is not None: + response = Bunch(status_code=1, text="\n".join(errmsg)) + return response, data + + +HASH_BIDS = ["subject_id", "session_id"] + +META_WHITELIST = [ + "AccelNumReferenceLines", + "AccelerationFactorPE", + "AcquisitionMatrix", + "CogAtlasID", + "CogPOID", + "CoilCombinationMethod", + "ContrastBolusIngredient", + "ConversionSoftware", + "ConversionSoftwareVersion", + "DelayTime", + "DeviceSerialNumber", + "EchoTime", + "EchoTrainLength", + "EffectiveEchoSpacing", + "FlipAngle", + "GradientSetType", + "HardcopyDeviceSoftwareVersion", + "ImageType", + "ImagingFrequency", + "InPlanePhaseEncodingDirection", + "InstitutionAddress", + "InstitutionName", + "Instructions", + "InversionTime", + "MRAcquisitionType", + "MRTransmitCoilSequence", + "MagneticFieldStrength", + "Manufacturer", + "ManufacturersModelName", + "MatrixCoilMode", + "MultibandAccelerationFactor", + "NumberOfAverages", + "NumberOfPhaseEncodingSteps", + "NumberOfVolumesDiscardedByScanner", + "NumberOfVolumesDiscardedByUser", + "NumberShots", + "ParallelAcquisitionTechnique", + "ParallelReductionFactorInPlane", + "PartialFourier", + "PartialFourierDirection", + "PatientPosition", + "PercentPhaseFieldOfView", + "PercentSampling", + "PhaseEncodingDirection", + "PixelBandwidth", + "ProtocolName", + "PulseSequenceDetails", + "PulseSequenceType", + "ReceiveCoilName", + "RepetitionTime", + "ScanOptions", + "ScanningSequence", + "SequenceName", + "SequenceVariant", + "SliceEncodingDirection", + "SoftwareVersions", + "TaskDescription", + "TaskName", + "TotalReadoutTime", + "TotalScanTimeSec", + "TransmitCoilName", + "VariableFlipAngleFlag", + "acq_id", + "modality", + "run_id", + "subject_id", + "task_id", + "session_id", +] + +PROV_WHITELIST = ["version", "md5sum", "software", "settings"] diff --git a/pydra/tasks/mriqc/messages.py b/pydra/tasks/mriqc/messages.py new file mode 100644 index 0000000..0574250 --- /dev/null +++ b/pydra/tasks/mriqc/messages.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow + + +logger = logging.getLogger(__name__) + + +BUILDING_WORKFLOW = "Building {modality} MRIQC workflow {detail}." + +QC_UPLOAD_COMPLETE = "QC metrics successfully uploaded." + +QC_UPLOAD_START = "MRIQC Web API: submitting to <{url}>" + +SUSPICIOUS_DATA_TYPE = "Input image {in_file} has a suspicious data type: '{dtype}'" + +VOXEL_SIZE_OK = "Voxel size is large enough." + +VOXEL_SIZE_SMALL = ( + "One or more voxel dimensions (%f, %f, %f) are smaller than the " + "requested voxel size (%f) - diff=(%f, %f, %f)" +) diff --git a/pydra/tasks/mriqc/nipype_ports/__init__.py b/pydra/tasks/mriqc/nipype_ports/__init__.py new file mode 100644 index 0000000..3b6ef55 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/__init__.py @@ -0,0 +1,27 @@ +from .algorithms import ( + ComputeDVARS, + FramewiseDisplacement, + IFLOGGER, + NonSteadyStateDetector, + TSNR, + _AR_est_YW, + compute_dvars, + is_outlier, + plot_confound, + regress_poly, +) +from .utils import ( + _cifs_table, + _generate_cifs_table, + _parse_mount_table, + copyfile, + fmlogger, + fname_presuffix, + get_related_files, + hash_infile, + hash_timestamp, + normalize_mc_params, + on_cifs, + related_filetype_sets, + split_filename, +) diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/__init__.py b/pydra/tasks/mriqc/nipype_ports/algorithms/__init__.py new file mode 100644 index 0000000..fb9df24 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/__init__.py @@ -0,0 +1,12 @@ +from .confounds import ( + ComputeDVARS, + FramewiseDisplacement, + IFLOGGER, + NonSteadyStateDetector, + TSNR, + _AR_est_YW, + compute_dvars, + is_outlier, + plot_confound, + regress_poly, +) diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/__init__.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/__init__.py new file mode 100644 index 0000000..49214ce --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/__init__.py @@ -0,0 +1,22 @@ +import attrs +from fileformats.generic import Directory, File +import json +import logging +import nibabel as nb +import numpy as np +from numpy.polynomial import Legendre +from pathlib import Path +from pydra.compose import python, shell, workflow +from .compute_dvars import ComputeDVARS +from .framewise_displacement import FramewiseDisplacement +from .non_steady_state_detector import NonSteadyStateDetector +from .tsnr import TSNR +from pydra.utils.typing import MultiInputObj +import typing as ty +import yaml + + +logger = logging.getLogger(__name__) + + +IFLOGGER = logging.getLogger("nipype.interface") diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/compute_dvars.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/compute_dvars.py new file mode 100644 index 0000000..804deb1 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/compute_dvars.py @@ -0,0 +1,195 @@ +import attrs +from fileformats.generic import File +import logging +import numpy as np +import os.path as op +from pydra.compose import python +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class ComputeDVARS(python.Task["ComputeDVARS.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.nipype_ports.algorithms.confounds.compute_dvars import ComputeDVARS + + """ + + in_file: File + in_mask: File + remove_zerovariance: bool = True + variance_tol: float = 1e-07 + save_std: bool = True + save_nstd: bool = False + save_vxstd: bool = False + save_all: bool = False + series_tr: float + save_plot: bool = False + figdpi: int = 100 + figsize: ty.Any = (11.7, 2.3) + figformat: ty.Any = png + intensity_normalization: float = 1000.0 + + class Outputs(python.Outputs): + out_std: File + out_nstd: File + out_vxstd: File + out_all: File + avg_std: float + avg_nstd: float + avg_vxstd: float + fig_std: File + fig_nstd: File + fig_vxstd: File + + @staticmethod + def function( + in_file: File, + in_mask: File, + remove_zerovariance: bool, + variance_tol: float, + save_std: bool, + save_nstd: bool, + save_vxstd: bool, + save_all: bool, + series_tr: float, + save_plot: bool, + figdpi: int, + figsize: ty.Any, + figformat: ty.Any, + intensity_normalization: float, + ) -> tuples[File, File, File, File, float, float, float, File, File, File]: + out_std = attrs.NOTHING + out_nstd = attrs.NOTHING + out_vxstd = attrs.NOTHING + out_all = attrs.NOTHING + avg_std = attrs.NOTHING + avg_nstd = attrs.NOTHING + avg_vxstd = attrs.NOTHING + fig_std = attrs.NOTHING + fig_nstd = attrs.NOTHING + fig_vxstd = attrs.NOTHING + self_dict = {} + self_dict["_results"] = {} + + dvars = compute_dvars( + in_file, + in_mask, + remove_zerovariance=remove_zerovariance, + variance_tol=variance_tol, + intensity_normalization=intensity_normalization, + ) + + ( + avg_std, + avg_nstd, + avg_vxstd, + ) = np.mean( + dvars, axis=1 + ).astype(float) + + tr = None + if series_tr is not attrs.NOTHING: + tr = series_tr + + if save_std: + out_file = _gen_fname("dvars_std", ext="tsv", in_file=in_file) + np.savetxt(out_file, dvars[0], fmt="%0.6f") + out_std = out_file + + if save_plot: + fig_std = _gen_fname("dvars_std", ext=figformat, in_file=in_file) + fig = plot_confound( + dvars[0], figsize, "Standardized DVARS", series_tr=tr + ) + fig.savefig( + fig_std, + dpi=float(figdpi), + format=figformat, + bbox_inches="tight", + ) + fig.clf() + + if save_nstd: + out_file = _gen_fname("dvars_nstd", ext="tsv", in_file=in_file) + np.savetxt(out_file, dvars[1], fmt="%0.6f") + out_nstd = out_file + + if save_plot: + fig_nstd = _gen_fname("dvars_nstd", ext=figformat, in_file=in_file) + fig = plot_confound(dvars[1], figsize, "DVARS", series_tr=tr) + fig.savefig( + fig_nstd, + dpi=float(figdpi), + format=figformat, + bbox_inches="tight", + ) + fig.clf() + + if save_vxstd: + out_file = _gen_fname("dvars_vxstd", ext="tsv", in_file=in_file) + np.savetxt(out_file, dvars[2], fmt="%0.6f") + out_vxstd = out_file + + if save_plot: + fig_vxstd = _gen_fname("dvars_vxstd", ext=figformat, in_file=in_file) + fig = plot_confound( + dvars[2], figsize, "Voxelwise std DVARS", series_tr=tr + ) + fig.savefig( + fig_vxstd, + dpi=float(figdpi), + format=figformat, + bbox_inches="tight", + ) + fig.clf() + + if save_all: + out_file = _gen_fname("dvars", ext="tsv", in_file=in_file) + np.savetxt( + out_file, + np.vstack(dvars).T, + fmt="%0.8f", + delimiter="\t", + header="std DVARS\tnon-std DVARS\tvx-wise std DVARS", + comments="", + ) + out_all = out_file + + return ( + out_std, + out_nstd, + out_vxstd, + out_all, + avg_std, + avg_nstd, + avg_vxstd, + fig_std, + fig_nstd, + fig_vxstd, + ) + + +def _gen_fname(suffix, ext=None, in_file=None): + fname, in_ext = op.splitext(op.basename(in_file)) + + if in_ext == ".gz": + fname, in_ext2 = op.splitext(fname) + in_ext = in_ext2 + in_ext + + if ext is None: + ext = in_ext + + if ext.startswith("."): + ext = ext[1:] + + return op.abspath(f"{fname}_{suffix}.{ext}") + + +IFLOGGER = logging.getLogger("nipype.interface") diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/framewise_displacement.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/framewise_displacement.py new file mode 100644 index 0000000..779dbab --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/framewise_displacement.py @@ -0,0 +1,105 @@ +import attrs +from fileformats.generic import File +import logging +from pydra.tasks.mriqc.nipype_ports.utils.misc import normalize_mc_params +import numpy as np +import os.path as op +from pathlib import Path +from pydra.compose import python +from pydra.tasks.mriqc.nipype_ports.utils.misc import normalize_mc_params +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class FramewiseDisplacement(python.Task["FramewiseDisplacement.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.nipype_ports.algorithms.confounds.framewise_displacement import FramewiseDisplacement + + """ + + in_file: File + parameter_source: ty.Any + radius: float = 50 + out_file: Path = fd_power_2012.txt + out_figure: Path = fd_power_2012.pdf + series_tr: float + save_plot: bool = False + normalize: bool = False + figdpi: int = 100 + figsize: ty.Any = (11.7, 2.3) + + class Outputs(python.Outputs): + out_file: File + out_figure: File + fd_average: float + + @staticmethod + def function( + in_file: File, + parameter_source: ty.Any, + radius: float, + out_file: Path, + out_figure: Path, + series_tr: float, + save_plot: bool, + normalize: bool, + figdpi: int, + figsize: ty.Any, + ) -> tuples[File, File, float]: + out_file = attrs.NOTHING + out_figure = attrs.NOTHING + fd_average = attrs.NOTHING + self_dict = {} + mpars = np.loadtxt(in_file) # mpars is N_t x 6 + mpars = np.apply_along_axis( + func1d=normalize_mc_params, + axis=1, + arr=mpars, + source=parameter_source, + ) + diff = mpars[:-1, :6] - mpars[1:, :6] + diff[:, 3:6] *= radius + fd_res = np.abs(diff).sum(axis=1) + + self_dict["_results"] = { + "out_file": op.abspath(out_file), + "fd_average": float(fd_res.mean()), + } + np.savetxt(out_file, fd_res, header="FramewiseDisplacement", comments="") + + if save_plot: + tr = None + if series_tr is not attrs.NOTHING: + tr = series_tr + + if normalize and tr is None: + IFLOGGER.warning("FD plot cannot be normalized if TR is not set") + + out_figure = op.abspath(out_figure) + fig = plot_confound( + fd_res, + figsize, + "FD", + units="mm", + series_tr=tr, + normalize=normalize, + ) + fig.savefig( + out_figure, + dpi=float(figdpi), + format=out_figure[-3:], + bbox_inches="tight", + ) + fig.clf() + + return out_file, out_figure, fd_average + + +IFLOGGER = logging.getLogger("nipype.interface") diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/non_steady_state_detector.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/non_steady_state_detector.py new file mode 100644 index 0000000..99b1b43 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/non_steady_state_detector.py @@ -0,0 +1,38 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +from pydra.compose import python + + +logger = logging.getLogger(__name__) + + +@python.define +class NonSteadyStateDetector(python.Task["NonSteadyStateDetector.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.nipype_ports.algorithms.confounds.non_steady_state_detector import NonSteadyStateDetector + + """ + + in_file: File + + class Outputs(python.Outputs): + n_volumes_to_discard: int + + @staticmethod + def function(in_file: File) -> int: + n_volumes_to_discard = attrs.NOTHING + self_dict = {} + in_nii = nb.load(in_file) + global_signal = ( + in_nii.dataobj[:, :, :, :50].mean(axis=0).mean(axis=0).mean(axis=0) + ) + + self_dict["_results"] = {"n_volumes_to_discard": is_outlier(global_signal)} + + return n_volumes_to_discard diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/conftest.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_computedvars.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_computedvars.py new file mode 100644 index 0000000..7f92a0e --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_computedvars.py @@ -0,0 +1,30 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.nipype_ports.algorithms.confounds.compute_dvars import ( + ComputeDVARS, +) +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_computedvars_1(): + task = ComputeDVARS() + task.inputs.in_file = File.sample(seed=0) + task.inputs.in_mask = File.sample(seed=1) + task.inputs.remove_zerovariance = True + task.inputs.variance_tol = 1e-07 + task.inputs.save_std = True + task.inputs.save_nstd = False + task.inputs.save_vxstd = False + task.inputs.save_all = False + task.inputs.save_plot = False + task.inputs.figdpi = 100 + task.inputs.figsize = [11.7, 2.3] + task.inputs.figformat = "png" + task.inputs.intensity_normalization = 1000.0 + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_framewisedisplacement.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_framewisedisplacement.py new file mode 100644 index 0000000..0ca9a28 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_framewisedisplacement.py @@ -0,0 +1,25 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.nipype_ports.algorithms.confounds.framewise_displacement import ( + FramewiseDisplacement, +) +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_framewisedisplacement_1(): + task = FramewiseDisplacement() + task.inputs.in_file = File.sample(seed=0) + task.inputs.radius = 50 + task.inputs.out_file = "fd_power_2012.txt" + task.inputs.out_figure = "fd_power_2012.pdf" + task.inputs.save_plot = False + task.inputs.normalize = False + task.inputs.figdpi = 100 + task.inputs.figsize = [11.7, 2.3] + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_nonsteadystatedetector.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_nonsteadystatedetector.py new file mode 100644 index 0000000..74ffbc5 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_nonsteadystatedetector.py @@ -0,0 +1,18 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.nipype_ports.algorithms.confounds.non_steady_state_detector import ( + NonSteadyStateDetector, +) +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_nonsteadystatedetector_1(): + task = NonSteadyStateDetector() + task.inputs.in_file = File.sample(seed=0) + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_tsnr.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_tsnr.py new file mode 100644 index 0000000..49f6e47 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tests/test_tsnr.py @@ -0,0 +1,20 @@ +from fileformats.generic import File +import logging +from nipype2pydra.testing import PassAfterTimeoutWorker +from pydra.tasks.mriqc.nipype_ports.algorithms.confounds.tsnr import TSNR +import pytest + + +logger = logging.getLogger(__name__) + + +@pytest.mark.xfail +def test_tsnr_1(): + task = TSNR() + task.inputs.in_file = [File.sample(seed=0)] + task.inputs.tsnr_file = "tsnr.nii.gz" + task.inputs.mean_file = "mean.nii.gz" + task.inputs.stddev_file = "stdev.nii.gz" + task.inputs.detrended_file = "detrend.nii.gz" + res = task(worker=PassAfterTimeoutWorker) + print("RESULT: ", res) diff --git a/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tsnr.py b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tsnr.py new file mode 100644 index 0000000..a9eff44 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/algorithms/confounds/tsnr.py @@ -0,0 +1,97 @@ +import attrs +from fileformats.generic import File +import logging +import nibabel as nb +import numpy as np +import os.path as op +from pathlib import Path +from pydra.compose import python +import typing as ty + + +logger = logging.getLogger(__name__) + + +@python.define +class TSNR(python.Task["TSNR.Outputs"]): + """ + Examples + ------- + + >>> from fileformats.generic import File + >>> from pydra.tasks.mriqc.nipype_ports.algorithms.confounds.tsnr import TSNR + + """ + + in_file: List + regress_poly: ty.Any + tsnr_file: Path = tsnr.nii.gz + mean_file: Path = mean.nii.gz + stddev_file: Path = stdev.nii.gz + detrended_file: Path = detrend.nii.gz + + class Outputs(python.Outputs): + tsnr_file: File + mean_file: File + stddev_file: File + detrended_file: File + + @staticmethod + def function( + in_file: List, + regress_poly: ty.Any, + tsnr_file: Path, + mean_file: Path, + stddev_file: Path, + detrended_file: Path, + ) -> tuples[File, File, File, File]: + tsnr_file = attrs.NOTHING + mean_file = attrs.NOTHING + stddev_file = attrs.NOTHING + detrended_file = attrs.NOTHING + img = nb.load(in_file[0]) + header = img.header.copy() + vollist = [nb.load(filename) for filename in in_file] + data = np.concatenate( + [ + vol.get_fdata(dtype=np.float32).reshape(vol.shape[:3] + (-1,)) + for vol in vollist + ], + axis=3, + ) + data = np.nan_to_num(data) + + if data.dtype.kind == "i": + header.set_data_dtype(np.float32) + data = data.astype(np.float32) + + if regress_poly is not attrs.NOTHING: + data = regress_poly(regress_poly, data, remove_mean=False)[0] + img = nb.Nifti1Image(data, img.affine, header) + nb.save(img, op.abspath(detrended_file)) + + meanimg = np.mean(data, axis=3) + stddevimg = np.std(data, axis=3) + tsnr = np.zeros_like(meanimg) + stddevimg_nonzero = stddevimg > 1.0e-3 + tsnr[stddevimg_nonzero] = ( + meanimg[stddevimg_nonzero] / stddevimg[stddevimg_nonzero] + ) + img = nb.Nifti1Image(tsnr, img.affine, header) + nb.save(img, op.abspath(tsnr_file)) + img = nb.Nifti1Image(meanimg, img.affine, header) + nb.save(img, op.abspath(mean_file)) + img = nb.Nifti1Image(stddevimg, img.affine, header) + nb.save(img, op.abspath(stddev_file)) + self_dict = {} + outputs = {} + for k in ["tsnr_file", "mean_file", "stddev_file"]: + outputs[k] = op.abspath(getattr(self_dict["inputs"], k)) + + if regress_poly is not attrs.NOTHING: + detrended_file = op.abspath(detrended_file) + + return tsnr_file, mean_file, stddev_file, detrended_file + + +IFLOGGER = logging.getLogger("nipype.interface") diff --git a/pydra/tasks/mriqc/nipype_ports/utils/__init__.py b/pydra/tasks/mriqc/nipype_ports/utils/__init__.py new file mode 100644 index 0000000..ea49722 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/utils/__init__.py @@ -0,0 +1,15 @@ +from .filemanip import ( + _cifs_table, + _generate_cifs_table, + _parse_mount_table, + copyfile, + fmlogger, + fname_presuffix, + get_related_files, + hash_infile, + hash_timestamp, + on_cifs, + related_filetype_sets, + split_filename, +) +from .misc import normalize_mc_params diff --git a/pydra/tasks/mriqc/nipype_ports/utils/filemanip.py b/pydra/tasks/mriqc/nipype_ports/utils/filemanip.py new file mode 100644 index 0000000..5c2271e --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/utils/filemanip.py @@ -0,0 +1,11 @@ +import logging + + +logger = logging.getLogger(__name__) + + +_cifs_table = _generate_cifs_table() + +fmlogger = logging.getLogger("nipype.utils") + +related_filetype_sets = [(".hdr", ".img", ".mat"), (".nii", ".mat"), (".BRIK", ".HEAD")] diff --git a/pydra/tasks/mriqc/nipype_ports/utils/misc.py b/pydra/tasks/mriqc/nipype_ports/utils/misc.py new file mode 100644 index 0000000..f29d7d9 --- /dev/null +++ b/pydra/tasks/mriqc/nipype_ports/utils/misc.py @@ -0,0 +1,4 @@ +import logging + + +logger = logging.getLogger(__name__) diff --git a/pydra/tasks/mriqc/qc/__init__.py b/pydra/tasks/mriqc/qc/__init__.py new file mode 100644 index 0000000..bcbb7a6 --- /dev/null +++ b/pydra/tasks/mriqc/qc/__init__.py @@ -0,0 +1,24 @@ +from .anatomical import ( + DIETRICH_FACTOR, + FSL_FAST_LABELS, + art_qi1, + art_qi2, + cjv, + cnr, + efc, + fber, + rpve, + snr, + snr_dietrich, + summary_stats, + volume_fraction, + wm2max, +) +from .diffusion import ( + ExtremeValueWarning, + MIN_NUM_CC_MASK, + cc_snr, + neighboring_dwi_correlation, + spike_ppm, +) +from .functional import RAS_AXIS_ORDER, gsr diff --git a/pydra/tasks/mriqc/qc/anatomical.py b/pydra/tasks/mriqc/qc/anatomical.py new file mode 100644 index 0000000..71b2a9d --- /dev/null +++ b/pydra/tasks/mriqc/qc/anatomical.py @@ -0,0 +1,417 @@ +import logging +from math import sqrt +import numpy as np +import os.path as op +from scipy.stats import kurtosis + + +logger = logging.getLogger(__name__) + + +def art_qi1(airmask, artmask): + r""" + Detect artifacts in the image using the method described in [Mortamet2009]_. + Calculates :math:`\text{QI}_1`, as the proportion of voxels with intensity + corrupted by artifacts normalized by the number of voxels in the "*hat*" + mask (i.e., the background region above the nasio-occipital plane): + + .. math :: + + \text{QI}_1 = \frac{1}{N} \sum\limits_{x\in X_\text{art}} 1 + + Near-zero values are better. + If :math:`\text{QI}_1 = -1`, then the "*hat*" mask (background) was empty + and the dataset is likely a skull-stripped image or has been heavily + post-processed. + + :param numpy.ndarray airmask: input air mask, without artifacts + :param numpy.ndarray artmask: input artifacts mask + + """ + if airmask.sum() < 1: + return -1.0 + # Count the ratio between artifacts and the total voxels in "hat" mask + return float(artmask.sum() / (airmask.sum() + artmask.sum())) + + +def art_qi2( + img, + airmask, + min_voxels=int(1e3), + max_voxels=int(3e5), + save_plot=True, + coil_elements=32, +): + r""" + Calculates :math:`\text{QI}_2`, based on the goodness-of-fit of a centered + :math:`\chi^2` distribution onto the intensity distribution of + non-artifactual background (within the "hat" mask): + + + .. math :: + + \chi^2_n = \frac{2}{(\sigma \sqrt{2})^{2n} \, (n - 1)!}x^{2n - 1}\, e^{-\frac{x}{2}} + + where :math:`n` is the number of coil elements. + + :param numpy.ndarray img: input data + :param numpy.ndarray airmask: input air mask without artifacts + + """ + from pydra.tasks.nireports.reportlets.nuisance import plot_qi2 + from scipy.stats import chi2 + from sklearn.neighbors import KernelDensity + + # S. Ogawa was born + np.random.seed(1191935) + data = np.nan_to_num(img[airmask > 0], posinf=0.0) + data[data < 0] = 0 + # Write out figure of the fitting + out_file = op.abspath("error.svg") + with open(out_file, "w") as ofh: + ofh.write("

Background noise fitting could not be plotted.

") + if (data > 0).sum() < min_voxels: + return 0.0, out_file + data *= 100 / np.percentile(data, 99) + modelx = data if len(data) < max_voxels else np.random.choice(data, size=max_voxels) + x_grid = np.linspace(0.0, 110, 1000) + # Estimate data pdf with KDE on a random subsample + kde_skl = KernelDensity(kernel="gaussian", bandwidth=4.0).fit(modelx[:, np.newaxis]) + kde = np.exp(kde_skl.score_samples(x_grid[:, np.newaxis])) + # Find cutoff + kdethi = np.argmax(kde[::-1] > kde.max() * 0.5) + # Fit X^2 + param = chi2.fit(modelx, coil_elements) + chi_pdf = chi2.pdf(x_grid, *param[:-2], loc=param[-2], scale=param[-1]) + # Compute goodness-of-fit (gof) + gof = float(np.abs(kde[-kdethi:] - chi_pdf[-kdethi:]).mean()) + if save_plot: + out_file = plot_qi2(x_grid, kde, chi_pdf, modelx, kdethi) + return gof, out_file + + +def cjv(mu_wm, mu_gm, sigma_wm, sigma_gm): + r""" + Calculate the :abbr:`CJV (coefficient of joint variation)`, a measure + related to :abbr:`SNR (Signal-to-Noise Ratio)` and + :abbr:`CNR (Contrast-to-Noise Ratio)` that is presented as a proxy for + the :abbr:`INU (intensity non-uniformity)` artifact [Ganzetti2016]_. + Lower is better. + + .. math:: + + \text{CJV} = \frac{\sigma_\text{WM} + \sigma_\text{GM}}{|\mu_\text{WM} - \mu_\text{GM}|}. + + :param float mu_wm: mean of signal within white-matter mask. + :param float mu_gm: mean of signal within gray-matter mask. + :param float sigma_wm: standard deviation of signal within white-matter mask. + :param float sigma_gm: standard deviation of signal within gray-matter mask. + + :return: the computed CJV + + + """ + return float((sigma_wm + sigma_gm) / abs(mu_wm - mu_gm)) + + +def cnr(mu_wm, mu_gm, sigma_air, sigma_wm, sigma_gm): + r""" + Calculate the :abbr:`CNR (Contrast-to-Noise Ratio)` [Magnota2006]_. + Higher values are better. + + .. math:: + + \text{CNR} = \frac{|\mu_\text{GM} - \mu_\text{WM} |}{\sqrt{\sigma_B^2 + + \sigma_\text{WM}^2 + \sigma_\text{GM}^2}}, + + where :math:`\sigma_B` is the standard deviation of the noise distribution within + the air (background) mask. + + + :param float mu_wm: mean of signal within white-matter mask. + :param float mu_gm: mean of signal within gray-matter mask. + :param float sigma_air: standard deviation of the air surrounding the head ("hat" mask). + :param float sigma_wm: standard deviation within white-matter mask. + :param float sigma_gm: standard within gray-matter mask. + + :return: the computed CNR + + """ + return float(abs(mu_wm - mu_gm) / sqrt(sigma_air**2 + sigma_gm**2 + sigma_wm**2)) + + +def efc(img, framemask=None, decimals=4): + r""" + Calculate the :abbr:`EFC (Entropy Focus Criterion)` [Atkinson1997]_. + Uses the Shannon entropy of voxel intensities as an indication of ghosting + and blurring induced by head motion. A range of low values is better, + with EFC = 0 for all the energy concentrated in one pixel. + + .. math:: + + \text{E} = - \sum_{j=1}^N \frac{x_j}{x_\text{max}} + \ln \left[\frac{x_j}{x_\text{max}}\right] + + with :math:`x_\text{max} = \sqrt{\sum_{j=1}^N x^2_j}`. + + The original equation is normalized by the maximum entropy, so that the + :abbr:`EFC (Entropy Focus Criterion)` can be compared across images with + different dimensions: + + .. math:: + + \text{EFC} = \left( \frac{N}{\sqrt{N}} \, \log{\sqrt{N}^{-1}} \right) \text{E} + + :param numpy.ndarray img: input data + :param numpy.ndarray framemask: a mask of empty voxels inserted after a rotation of + data + + """ + if framemask is None: + framemask = np.zeros_like(img, dtype=np.uint8) + n_vox = np.sum(1 - framemask) + # Calculate the maximum value of the EFC (which occurs any time all + # voxels have the same value) + efc_max = 1.0 * n_vox * (1.0 / np.sqrt(n_vox)) * np.log(1.0 / np.sqrt(n_vox)) + # Calculate the total image energy + b_max = np.sqrt((img[framemask == 0] ** 2).sum()) + # Calculate EFC (add 1e-16 to the image data to keep log happy) + return round( + float( + (1.0 / efc_max) + * np.sum( + (img[framemask == 0] / b_max) + * np.log((img[framemask == 0] + 1e-16) / b_max) + ), + ), + decimals, + ) + + +def fber(img, headmask, rotmask=None, decimals=4): + r""" + Calculate the :abbr:`FBER (Foreground-Background Energy Ratio)` [Shehzad2015]_, + defined as the mean energy of image values within the head relative + to outside the head. + Higher values are better, and an FBER=-1.0 indicates that there is no signal + outside the head mask (e.g., a skull-stripped dataset). + + .. math:: + + \text{FBER} = \frac{E[|F|^2]}{E[|B|^2]} + + + :param numpy.ndarray img: input data + :param numpy.ndarray headmask: a mask of the head (including skull, skin, etc.) + :param numpy.ndarray rotmask: a mask of empty voxels inserted after a rotation of + data + + """ + fg_mu = np.median(np.abs(img[headmask > 0]) ** 2) + airmask = np.ones_like(headmask, dtype=np.uint8) + airmask[headmask > 0] = 0 + if rotmask is not None: + airmask[rotmask > 0] = 0 + bg_mu = np.median(np.abs(img[airmask == 1]) ** 2) + if bg_mu < 1.0e-3: + return -1.0 + return round(float(fg_mu / bg_mu), decimals) + + +def rpve(pvms, seg): + """ + Computes the :abbr:`rPVe (residual partial voluming error)` + of each tissue class. + + .. math :: + + \\text{rPVE}^k = \\frac{1}{N} \\left[ \\sum\\limits_{p^k_i \\in [0.5, P_{98}]} p^k_i + \\sum\\limits_{p^k_i \\in [P_{2}, 0.5)} 1 - p^k_i \\right] + + """ + pvfs = {} + for k, lid in list(FSL_FAST_LABELS.items()): + if lid == 0: + continue + pvmap = pvms[lid - 1] + pvmap[pvmap < 0.0] = 0.0 + pvmap[pvmap >= 1.0] = 1.0 + totalvol = np.sum(pvmap > 0.0) + upth = np.percentile(pvmap[pvmap > 0], 98) + loth = np.percentile(pvmap[pvmap > 0], 2) + pvmap[pvmap < loth] = 0 + pvmap[pvmap > upth] = 0 + pvfs[k] = ( + pvmap[pvmap > 0.5].sum() + (1.0 - pvmap[pvmap <= 0.5]).sum() + ) / totalvol + return {k: float(v) for k, v in list(pvfs.items())} + + +def snr(mu_fg, sigma_fg, n): + r""" + Calculate the :abbr:`SNR (Signal-to-Noise Ratio)`. + The estimation may be provided with only one foreground region in + which the noise is computed as follows: + + .. math:: + + \text{SNR} = \frac{\mu_F}{\sigma_F\sqrt{n/(n-1)}}, + + where :math:`\mu_F` is the mean intensity of the foreground and + :math:`\sigma_F` is the standard deviation of the same region. + + :param float mu_fg: mean of foreground. + :param float sigma_fg: standard deviation of foreground. + :param int n: number of voxels in foreground mask. + + :return: the computed SNR + + """ + return float(mu_fg / (sigma_fg * sqrt(n / (n - 1)))) + + +def snr_dietrich(mu_fg, mad_air=0.0, sigma_air=1.0): + r""" + Calculate the :abbr:`SNR (Signal-to-Noise Ratio)`. + + This must be an air mask around the head, and it should not contain artifacts. + The computation is done following the eq. A.12 of [Dietrich2007]_, which + includes a correction factor in the estimation of the standard deviation of + air and its Rayleigh distribution: + + .. math:: + + \text{SNR} = \frac{\mu_F}{\sqrt{\frac{2}{4-\pi}}\,\sigma_\text{air}}. + + + :param float mu_fg: mean of foreground. + :param float sigma_air: standard deviation of the air surrounding the head ("hat" mask). + + :return: the computed SNR for the foreground segmentation + + """ + if mad_air > 1.0: + return float(DIETRICH_FACTOR * mu_fg / mad_air) + logger.warning( + "Estimated signal variation in the background was too small " + f"(MAD={mad_air}, sigma={sigma_air})", + ) + return float(DIETRICH_FACTOR * mu_fg / sigma_air) if sigma_air > 1e-3 else -1.0 + + +def summary_stats( + data: np.ndarray, + pvms: dict[str, np.ndarray], + rprec_data: int = 0, + rprec_prob: int = 3, + decimals: int = 4, +) -> dict[str, dict[str, float]]: + """ + Estimates weighted summary statistics for each tissue distribution in the data. + + This function calculates the mean, median, standard deviation, kurtosis, median + absolute deviation (MAD), the 95th and 5th percentiles, and the number of voxels for + each tissue distribution defined by a label in the provided partial volume maps (pvms). + + Parameters + ---------- + data : :obj:`~numpy.ndarray` (float, 3D) + A three-dimensional array of data from which summary statistics will be extracted. + pvms : :obj:`dict` of :obj:`str` keys and :obj:`~numpy.ndarray` (float, 3D) values + A dictionary of partial volume maps where the key indicates the label of a + region-of-interest (ROI) and the values are three-dimensional arrays matched in size + with `data` and containing the probability/fraction of the voxel containing the given + label. + rprec_data : :obj:`int`, optional (default=0) + Number of decimal places to round the data array before calculation. Rounding + alleviates floating-point error variability by explicitly rounding before + quantification operations. + rprec_prob : :obj:`int`, optional (default=3) + Number of decimal places to round the probability maps before calculation. Rounding + alleviates floating-point error variability by explicitly rounding before + quantification operations. + + Returns + ------- + :obj:`dict` + A dictionary where the keys are labels from the ``pvms`` dictionary and the values + are dictionaries containing the following keys for each tissue distribution: + + * ``'mean'``: :obj:`float` - Mean value + * ``'median'``: :obj:`float` - Median value + * ``'p95'``: :obj:`float` - 95th percentile + * ``'p05'``: :obj:`float` - 5th percentile + * ``'k'``: :obj:`float` - Kurtosis + * ``'stdv'``: :obj:`float` - Standard deviation + * ``'mad'``: :obj:`float` - Median absolute deviation + * ``'n'``: :obj:`int` - Number of voxels in the tissue distribution + + """ + from statsmodels.robust.scale import mad + from statsmodels.stats.weightstats import DescrStatsW + + output = {} + for label, probmap in pvms.items(): + wstats = DescrStatsW( + data=np.round(data.reshape(-1), rprec_data), + weights=np.round(probmap.astype(np.float32).reshape(-1), rprec_prob), + ) + nvox = probmap.sum() + p05, median, p95 = wstats.quantile( + np.array([0.05, 0.50, 0.95]), return_pandas=False + ) + thresholded = data[probmap > (0.5 * probmap.max())] + output[label] = { + "mean": round(float(wstats.mean), decimals), + "median": round(float(median), decimals), + "p95": round(float(p95), decimals), + "p05": round(float(p05), decimals), + "k": round(float(kurtosis(thresholded)), decimals), + "stdv": round(float(wstats.std), decimals), + "mad": round(float(mad(thresholded, center=median)), decimals), + "n": float(nvox), + } + return output + + +def volume_fraction(pvms): + r""" + Computes the :abbr:`ICV (intracranial volume)` fractions + corresponding to the (partial volume maps). + + .. math :: + + \text{ICV}^k = \frac{\sum_i p^k_i}{\sum\limits_{x \in X_\text{brain}} 1} + + :param list pvms: list of :code:`numpy.ndarray` of partial volume maps. + + """ + tissue_vfs = {} + total = 0 + for k, lid in list(FSL_FAST_LABELS.items()): + if lid == 0: + continue + tissue_vfs[k] = pvms[lid - 1].sum() + total += tissue_vfs[k] + for k in list(tissue_vfs.keys()): + tissue_vfs[k] /= total + return {k: float(v) for k, v in list(tissue_vfs.items())} + + +def wm2max(img, mu_wm): + r""" + Calculate the :abbr:`WM2MAX (white-matter-to-max ratio)`, + defined as the maximum intensity found in the volume w.r.t. the + mean value of the white matter tissue. Values close to 1.0 are + better: + + .. math :: + + \text{WM2MAX} = \frac{\mu_\text{WM}}{P_{99.95}(X)} + + """ + return float(mu_wm / np.percentile(img.reshape(-1), 99.95)) + + +DIETRICH_FACTOR = 0.6551364 # 1.0 / sqrt(2 / (4 - pi)) + +FSL_FAST_LABELS = {"csf": 1, "gm": 2, "wm": 3, "bg": 0} diff --git a/pydra/tasks/mriqc/qc/diffusion.py b/pydra/tasks/mriqc/qc/diffusion.py new file mode 100644 index 0000000..ec81a69 --- /dev/null +++ b/pydra/tasks/mriqc/qc/diffusion.py @@ -0,0 +1,210 @@ +from contextlib import suppress +import logging +import numpy as np +from statsmodels.robust.scale import mad +from warnings import warn + + +logger = logging.getLogger(__name__) + + +class ExtremeValueWarning(UserWarning): + """A warning type for dubious metric values.""" + + +def cc_snr( + in_b0: np.ndarray, + dwi_shells: list[np.ndarray], + cc_mask: np.ndarray, + b_values: np.ndarray, + b_vectors: np.ndarray, + bval_thres: int = 50, + decimals: int = 2, +) -> dict[int, (float, float)]: + """ + Calculates the worst-case and best-case signal-to-noise ratio (SNR) within the corpus callosum. + + This function estimates the SNR in the corpus callosum (CC) by comparing the + mean signal intensity within the CC mask to the standard deviation of the background + signal (extracted from the b0 image). It performs separate calculations for + each diffusion-weighted imaging (DWI) shell. + + **Worst-case SNR:** The mean signal intensity along the diffusion direction with the + lowest signal is considered the worst-case scenario. + + **Best-case SNR:** The mean signal intensity averaged across the two diffusion + directions with the highest signal is considered the best-case scenario. + + Parameters + ---------- + in_b0 : :obj:`~numpy.ndarray` (float, 3D) + T1-weighted or b0 image used for background signal estimation. + dwi_shells : list[:obj:`~numpy.ndarray` (float, 4D)] + List of DWI data for each diffusion shell. + cc_mask : :obj:`~numpy.ndarray` (bool, 3D) + Boolean mask of the corpus callosum. + b_values : :obj:`~numpy.ndarray` (int) + Array of b-values for each DWI volume in ``dwi_shells``. + b_vectors : :obj:`~numpy.ndarray` (float) + Array of diffusion-encoding vectors for each DWI volume in ``dwi_shells``. + + Returns + ------- + cc_snr_estimates : :obj:`dict` + Dictionary containing SNR estimates for each b-value. Keys are the b-values + (integers), and values are tuples containing two elements: + + * The first element is the worst-case SNR (float). + * The second element is the best-case SNR (float). + + The SNR estimates are zero if there are no sufficient voxels to calculate them + (may occur if the number of orientations in the file is very low). + + """ + cc_mask = cc_mask > 0 # Ensure it's a boolean mask + b_values = np.rint(b_values).astype(np.uint16) + n_shells = len(b_values) + if (nvox_cc := cc_mask.sum()) < MIN_NUM_CC_MASK: + warn( + f"CC mask is too small ({nvox_cc} voxels)", + ExtremeValueWarning, + stacklevel=1, + ) + cc_snr_estimates = {"shell0": 0} + cc_snr_estimates = cc_snr_estimates | { + f"shell{shell_index:d}_worst": 0 for shell_index in range(1, n_shells + 1) + } + cc_snr_estimates = cc_snr_estimates | { + f"shell{shell_index:d}_best": 0 for shell_index in range(1, n_shells + 1) + } + return cc_snr_estimates, 0 + std_signal = mad(in_b0[cc_mask]) + xyz = np.eye(3) + cc_snr_estimates = {} + cc_snr_estimates["shell0"] = round( + float(in_b0[cc_mask].mean() / std_signal), decimals + ) + # Shell-wise calculation + for shell_index, bvecs, shell_data in zip( + range(1, n_shells + 1), b_vectors, dwi_shells + ): + shell_data = shell_data[cc_mask] + # Find main directions of diffusion + axis_X = np.argmin(np.sum((bvecs - xyz[0, :]) ** 2, axis=-1)) + axis_Y = np.argmin(np.sum((bvecs - xyz[1, :]) ** 2, axis=-1)) + axis_Z = np.argmin(np.sum((bvecs - xyz[2, :]) ** 2, axis=-1)) + mean_signal_worst = 0 + with suppress(IndexError): + data_X = shell_data[..., axis_X] + mean_signal_worst = np.mean(data_X) + mean_signal_best = 0 + with suppress(IndexError): + data_Y = shell_data[..., axis_Y] + data_Z = shell_data[..., axis_Z] + mean_signal_best = 0.5 * (np.mean(data_Y) + np.mean(data_Z)) + cc_snr_estimates[f"shell{shell_index:d}_worst"] = round( + float(np.mean(mean_signal_worst / std_signal)), decimals + ) + cc_snr_estimates[f"shell{shell_index:d}_best"] = round( + float(np.mean(mean_signal_best / std_signal)), decimals + ) + return cc_snr_estimates, std_signal + + +def neighboring_dwi_correlation( + dwi_data: np.ndarray, + neighbor_indices: list[tuple[int, int]], + mask: np.ndarray | None = None, + decimals: int = 4, +) -> float: + """ + Calculates the Neighboring DWI Correlation (NDC) from diffusion MRI (dMRI) data. + + The NDC is a measure of the correlation between signal intensities in neighboring + diffusion-weighted images (DWIs) within a mask. A low NDC (typically below 0.4) + can indicate poor image quality, according to Yeh et al. [Yeh2019]_. + + Parameters + ---------- + dwi_data : 4D :obj:`~numpy.ndarray` + DWI data on which to calculate NDC + neighbor_indices : :obj:`list` of :obj:`tuple` + List of (from, to) index neighbors. + mask : 3D :obj:`~numpy.ndarray`, optional + optional mask of voxels to include in the NDC calculation + + Returns + ------- + :obj:`float` + The NDC value. + + References + ---------- + .. [Yeh2019] Yeh, Fang-Cheng, et al. "Differential tractography as a + track-based biomarker for neuronal injury." + NeuroImage 202 (2019): 116131. + + Notes + ----- + This is a copy of DIPY's code to be removed (and just imported) as soon as + a new release of DIPY is cut including + `dipy/dipy#3156 `__. + + """ + neighbor_correlations = [] + mask = np.ones_like(dwi_data[..., 0], dtype=bool) if mask is None else mask + dwi_data = dwi_data[mask] + for from_index, to_index in neighbor_indices: + flat_from_image = dwi_data[from_index] + flat_to_image = dwi_data[to_index] + neighbor_correlations.append(np.corrcoef(flat_from_image, flat_to_image)[0, 1]) + return round(float(np.mean(neighbor_correlations)), decimals) + + +def spike_ppm( + spike_mask: np.ndarray, slice_threshold: float = 0.05, decimals: int = 2 +) -> dict[str, float | np.ndarray]: + """ + Calculates fractions (global and slice-wise) of voxels classified as spikes in ppm. + + This function computes two metrics: + + * Global spike parts-per-million [ppm]: Fraction of voxels exceeding the spike + threshold across the entire data array. + * Slice-wise spiking [ppm]: The fraction of slices along each dimension of + the data array where the average fraction of spiking voxels within the slice + exceeds a user-defined threshold (``slice_threshold``). + + Parameters + ---------- + spike_mask : :obj:`~numpy.ndarray` (bool, same shape as data) + The binary mask indicating spike voxels (True) and non-spike voxels (False). + slice_threshold : :obj:`float`, optional (default=0.05) + The minimum fraction of voxels in a slice that must be classified as spikes + for the slice to be considered spiking. + decimals : :obj:`int` + The number of decimals to round the fractions. + + Returns + ------- + :obj:`dict` + A dictionary containing the calculated spike percentages: + + * 'global': :obj:`float` - global spiking voxels ppm. + * 'slice_{i,j,k,t}': :obj:`float` - Slice-wise spiking voxel + fractions in ppm for each dimension of the data array. + + """ + axisnames = "ijkt" + spike_global = round(float(1e6 * np.mean(np.ravel(spike_mask))), decimals) + spike_slice = { + f"slice_{axisnames[axis]}": round( + float(1e6 * np.mean(np.mean(spike_mask, axis=axis) > slice_threshold)), + decimals, + ) + for axis in range(min(spike_mask.ndim, 3)) + } + return {"global": spike_global} | spike_slice + + +MIN_NUM_CC_MASK = 5 diff --git a/pydra/tasks/mriqc/qc/functional.py b/pydra/tasks/mriqc/qc/functional.py new file mode 100644 index 0000000..4100a00 --- /dev/null +++ b/pydra/tasks/mriqc/qc/functional.py @@ -0,0 +1,62 @@ +import logging +import numpy as np +import os.path as op + + +logger = logging.getLogger(__name__) + + +def gsr(epi_data, mask, direction="y", ref_file=None, out_file=None): + """ + Compute the :abbr:`GSR (ghost to signal ratio)` [Giannelli2010]_. + + The procedure is as follows: + + + + + + + + .. warning :: + + This should be used with EPI images for which the phase + encoding direction is known. + + :param str epi_file: path to epi file + :param str mask_file: path to brain mask + :param str direction: the direction of phase encoding (x, y, all) + :return: the computed gsr + + """ + direction = direction.lower() + if direction[-1] not in ("x", "y", "all"): + raise Exception( + f"Unknown direction {direction}, should be one of x, -x, y, -y, all" + ) + if direction == "all": + result = [] + for newdir in ("x", "y"): + ofile = None + if out_file is not None: + fname, ext = op.splitext(ofile) + if ext == ".gz": + fname, ext2 = op.splitext(fname) + ext = ext2 + ext + ofile = f"{fname}_{newdir}{ext}" + result += [gsr(epi_data, mask, newdir, ref_file=ref_file, out_file=ofile)] + return result + # Roll data of mask through the appropriate axis + axis = RAS_AXIS_ORDER[direction] + n2_mask = np.roll(mask, mask.shape[axis] // 2, axis=axis) + # Step 3: remove from n2_mask pixels inside the brain + n2_mask = n2_mask * (1 - mask) + # Step 4: non-ghost background region is labeled as 2 + n2_mask = n2_mask + 2 * (1 - n2_mask - mask) + # Step 5: signal is the entire foreground image + ghost = np.mean(epi_data[n2_mask == 1]) - np.mean(epi_data[n2_mask == 2]) + signal = np.median(epi_data[n2_mask == 0]) + return float(ghost / signal) + + +RAS_AXIS_ORDER = {"x": 0, "y": 1, "z": 2} diff --git a/pydra/tasks/mriqc/synthstrip/ORIGINAL_LICENSE b/pydra/tasks/mriqc/synthstrip/ORIGINAL_LICENSE new file mode 100644 index 0000000..2a8c7f8 --- /dev/null +++ b/pydra/tasks/mriqc/synthstrip/ORIGINAL_LICENSE @@ -0,0 +1,191 @@ + FreeSurfer Software License Agreement ("Agreement") + Version 1.0 (February 2011) + +This Agreement covers contributions to and downloads from the +FreeSurfer project ("FreeSurfer") maintained by The General Hospital +Corporation, Boston MA, USA ("MGH"). Part A of this Agreement applies to +contributions of software and/or data to FreeSurfer (including making +revisions of or additions to code and/or data already in FreeSurfer). Part +B of this Agreement applies to downloads of software and/or data from +FreeSurfer. Part C of this Agreement applies to all transactions with +FreeSurfer. If you distribute Software (as defined below) downloaded from +FreeSurfer, all of the paragraphs of Part B of this Agreement must be +included with and apply to such Software. + +Your contribution of software and/or data to FreeSurfer (including prior +to the date of the first publication of this Agreement, each a +"Contribution") and/or downloading, copying, modifying, displaying, +distributing or use of any software and/or data from FreeSurfer +(collectively, the "Software") constitutes acceptance of all of the +terms and conditions of this Agreement. If you do not agree to such +terms and conditions, you have no right to contribute your +Contribution, or to download, copy, modify, display, distribute or use +the Software. + +PART A. CONTRIBUTION AGREEMENT - License to MGH with Right to Sublicense +("Contribution Agreement"). + +1. As used in this Contribution Agreement, "you" means the individual + contributing the Contribution to FreeSurfer and the institution or + entity which employs or is otherwise affiliated with such + individual in connection with such Contribution. + +2. This Contribution Agreement applies to all Contributions made to + FreeSurfer, including without limitation Contributions made prior to + the date of first publication of this Agreement. If at any time you + make a Contribution to FreeSurfer, you represent that (i) you are + legally authorized and entitled to make such Contribution and to + grant all licenses granted in this Contribution Agreement with + respect to such Contribution; (ii) if your Contribution includes + any patient data, all such data is de-identified in accordance with + U.S. confidentiality and security laws and requirements, including + but not limited to the Health Insurance Portability and + Accountability Act (HIPAA) and its regulations, and your disclosure + of such data for the purposes contemplated by this Agreement is + properly authorized and in compliance with all applicable laws and + regulations; and (iii) you have preserved in the Contribution all + applicable attributions, copyright notices and licenses for any + third party software or data included in the Contribution. + +3. Except for the licenses granted in this Agreement, you reserve all + right, title and interest in your Contribution. + +4. You hereby grant to MGH, with the right to sublicense, a + perpetual, worldwide, non-exclusive, no charge, royalty-free, + irrevocable license to use, reproduce, make derivative works of, + display and distribute the Contribution. If your Contribution is + protected by patent, you hereby grant to MGH, with the right to + sublicense, a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable license under your interest in patent + rights covering the Contribution, to make, have made, use, sell and + otherwise transfer your Contribution, alone or in combination with + any other code. + +5. You acknowledge and agree that MGH may incorporate your + Contribution into FreeSurfer and may make FreeSurfer available to members + of the public on an open source basis under terms substantially in + accordance with the Software License set forth in Part B of this + Agreement. You further acknowledge and agree that MGH shall + have no liability arising in connection with claims resulting from + your breach of any of the terms of this Agreement. + +6. YOU WARRANT THAT TO THE BEST OF YOUR KNOWLEDGE YOUR CONTRIBUTION + DOES NOT CONTAIN ANY CODE THAT REQURES OR PRESCRIBES AN "OPEN + SOURCE LICENSE" FOR DERIVATIVE WORKS (by way of non-limiting + example, the GNU General Public License or other so-called + "reciprocal" license that requires any derived work to be licensed + under the GNU General Public License or other "open source + license"). + +PART B. DOWNLOADING AGREEMENT - License from MGH with Right to Sublicense +("Software License"). + +1. As used in this Software License, "you" means the individual + downloading and/or using, reproducing, modifying, displaying and/or + distributing the Software and the institution or entity which + employs or is otherwise affiliated with such individual in + connection therewith. The General Hospital Corporation ("MGH") + hereby grants you, with right to sublicense, with + respect to MGH's rights in the software, and data, if any, + which is the subject of this Software License (collectively, the + "Software"), a royalty-free, non-exclusive license to use, + reproduce, make derivative works of, display and distribute the + Software, provided that: + +(a) you accept and adhere to all of the terms and conditions of this +Software License; + +(b) in connection with any copy of or sublicense of all or any portion +of the Software, all of the terms and conditions in this Software +License shall appear in and shall apply to such copy and such +sublicense, including without limitation all source and executable +forms and on any user documentation, prefaced with the following +words: "All or portions of this licensed product (such portions are +the "Software") have been obtained under license from The General Hospital +Corporation "MGH" and are subject to the following terms and +conditions:" + +(c) you preserve and maintain all applicable attributions, copyright +notices and licenses included in or applicable to the Software; + +(d) modified versions of the Software must be clearly identified and +marked as such, and must not be misrepresented as being the original +Software; and + +(e) you consider making, but are under no obligation to make, the +source code of any of your modifications to the Software freely +available to others on an open source basis. + +2. The license granted in this Software License includes without + limitation the right to (i) incorporate the Software into + proprietary programs (subject to any restrictions applicable to + such programs), (ii) add your own copyright statement to your + modifications of the Software, and (iii) provide additional or + different license terms and conditions in your sublicenses of + modifications of the Software; provided that in each case your use, + reproduction or distribution of such modifications otherwise + complies with the conditions stated in this Software License. + +3. This Software License does not grant any rights with respect to + third party software, except those rights that MGH has been + authorized by a third party to grant to you, and accordingly you + are solely responsible for (i) obtaining any permissions from third + parties that you need to use, reproduce, make derivative works of, + display and distribute the Software, and (ii) informing your + sublicensees, including without limitation your end-users, of their + obligations to secure any such required permissions. + +4. The Software has been designed for research purposes only and has + not been reviewed or approved by the Food and Drug Administration + or by any other agency. YOU ACKNOWLEDGE AND AGREE THAT CLINICAL + APPLICATIONS ARE NEITHER RECOMMENDED NOR ADVISED. Any + commercialization of the Software is at the sole risk of the party + or parties engaged in such commercialization. You further agree to + use, reproduce, make derivative works of, display and distribute + the Software in compliance with all applicable governmental laws, + regulations and orders, including without limitation those relating + to export and import control. + +5. The Software is provided "AS IS" and neither MGH nor any + contributor to the software (each a "Contributor") shall have any + obligation to provide maintenance, support, updates, enhancements + or modifications thereto. MGH AND ALL CONTRIBUTORS SPECIFICALLY + DISCLAIM ALL EXPRESS AND IMPLIED WARRANTIES OF ANY KIND INCLUDING, + BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR + A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL + MGH OR ANY CONTRIBUTOR BE LIABLE TO ANY PARTY FOR DIRECT, + INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY ARISING IN ANY WAY + RELATED TO THE SOFTWARE, EVEN IF MGH OR ANY CONTRIBUTOR HAS + BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. TO THE MAXIMUM + EXTENT NOT PROHIBITED BY LAW OR REGULATION, YOU FURTHER ASSUME ALL + LIABILITY FOR YOUR USE, REPRODUCTION, MAKING OF DERIVATIVE WORKS, + DISPLAY, LICENSE OR DISTRIBUTION OF THE SOFTWARE AND AGREE TO + INDEMNIFY AND HOLD HARMLESS MGH AND ALL CONTRIBUTORS FROM AND + AGAINST ANY AND ALL CLAIMS, SUITS, ACTIONS, DEMANDS AND JUDGMENTS + ARISING THEREFROM. + +6. None of the names, logos or trademarks of MGH or any of + MGH's affiliates or any of the Contributors, or any funding + agency, may be used to endorse or promote products produced in + whole or in part by operation of the Software or derived from or + based on the Software without specific prior written permission + from the applicable party. + +7. Any use, reproduction or distribution of the Software which is not + in accordance with this Software License shall automatically revoke + all rights granted to you under this Software License and render + Paragraphs 1 and 2 of this Software License null and void. + +8. This Software License does not grant any rights in or to any + intellectual property owned by MGH or any Contributor except + those rights expressly granted hereunder. + +PART C. MISCELLANEOUS + +This Agreement shall be governed by and construed in accordance with +the laws of The Commonwealth of Massachusetts without regard to +principles of conflicts of law. This Agreement shall supercede and +replace any license terms that you may have agreed to previously with +respect to FreeSurfer. + diff --git a/pydra/tasks/mriqc/synthstrip/__init__.py b/pydra/tasks/mriqc/synthstrip/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pydra/tasks/mriqc/synthstrip/__main__.py b/pydra/tasks/mriqc/synthstrip/__main__.py new file mode 100644 index 0000000..8c08b16 --- /dev/null +++ b/pydra/tasks/mriqc/synthstrip/__main__.py @@ -0,0 +1,34 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2022 The NiPreps 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. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# + +if __name__ == '__main__': + import sys + + from mriqc.synthstrip.cli import main + + from . import __name__ as module + + # `python -m ` typically displays the command as __main__.py + if '__main__.py' in sys.argv[0]: + sys.argv[0] = f'{sys.executable} -m {module}' + main() diff --git a/pydra/tasks/mriqc/synthstrip/cli.py b/pydra/tasks/mriqc/synthstrip/cli.py new file mode 100644 index 0000000..ee42b7d --- /dev/null +++ b/pydra/tasks/mriqc/synthstrip/cli.py @@ -0,0 +1,235 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2022 The NiPreps 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. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +# STATEMENT OF CHANGES: This file is derived from sources licensed under the FreeSurfer 1.0 license +# terms, and this file has been changed. +# The full licensing terms of the original work are found at: +# https://github.com/freesurfer/freesurfer/blob/2995ded957961a7f3704de57eee88eb6cc30d52d/LICENSE.txt +# A copy of the license has been archived in the ORIGINAL_LICENSE file +# found within this redistribution. +# +# The original file this work derives from is found at: +# https://github.com/freesurfer/freesurfer/blob/2995ded957961a7f3704de57eee88eb6cc30d52d/mri_synthstrip/mri_synthstrip +# +# [April 2022] CHANGES: +# * MAINT: Split the monolithic file into model and CLI submodules +# * ENH: Replace freesurfer Python bundle with in-house code. +# +""" +Robust, universal skull-stripping for brain images of any type. +If you use SynthStrip in your analysis, please cite: + + A Hoopes, JS Mora, AV Dalca, B Fischl, M Hoffmann. + SynthStrip: Skull-Stripping for Any Brain Image. + https://arxiv.org/abs/2203.09974 + +""" + + +def main(): + """Entry point to SynthStrip.""" + import os + from argparse import ArgumentParser + + import nibabel as nb + import numpy as np + import scipy + import torch + + from .model import StripModel + + # parse command line + parser = ArgumentParser(description=__doc__) + parser.add_argument( + '-i', + '--image', + metavar='file', + required=True, + help='Input image to skullstrip.', + ) + parser.add_argument('-o', '--out', metavar='file', help='Save stripped image to path.') + parser.add_argument('-m', '--mask', metavar='file', help='Save binary brain mask to path.') + parser.add_argument('-g', '--gpu', action='store_true', help='Use the GPU.') + parser.add_argument('-n', '--num-threads', action='store', type=int, help='number of threads') + parser.add_argument( + '-b', + '--border', + default=1, + type=int, + help='Mask border threshold in mm. Default is 1.', + ) + parser.add_argument('--model', metavar='file', help='Alternative model weights.') + args = parser.parse_args() + + # sanity check on the inputs + if not args.out and not args.mask: + parser.fatal('Must provide at least --out or --mask output flags.') + + # necessary for speed gains (I think) + torch.backends.cudnn.benchmark = True + torch.backends.cudnn.deterministic = True + + # configure GPU device + if args.gpu: + os.environ['CUDA_VISIBLE_DEVICES'] = '0' + device = torch.device('cuda') + device_name = 'GPU' + else: + os.environ['CUDA_VISIBLE_DEVICES'] = '-1' + device = torch.device('cpu') + device_name = 'CPU' + + if args.num_threads and args.num_threads > 0: + torch.set_num_threads(args.num_threads) + + # configure model + print(f'Configuring model on the {device_name}') + + with torch.no_grad(): + model = StripModel() + model.to(device) + model.eval() + + # load model weights + if args.model is not None: + modelfile = args.model + print('Using custom model weights') + else: + raise RuntimeError('A model must be provided.') + + checkpoint = torch.load(modelfile, map_location=device) + model.load_state_dict(checkpoint['model_state_dict']) + + # load input volume + print(f'Input image read from: {args.image}') + + # normalize intensities + image = nb.load(args.image) + conformed = conform(image) + in_data = conformed.get_fdata(dtype='float32') + in_data -= in_data.min() + in_data = np.clip(in_data / np.percentile(in_data, 99), 0, 1) + in_data = in_data[np.newaxis, np.newaxis] + + # predict the surface distance transform + input_tensor = torch.from_numpy(in_data).to(device) + with torch.no_grad(): + sdt = model(input_tensor).cpu().numpy().squeeze() + + # unconform the sdt and extract mask + sdt_target = resample_like( + nb.Nifti1Image(sdt, conformed.affine, None), + image, + output_dtype='int16', + cval=100, + ) + sdt_data = np.asanyarray(sdt_target.dataobj).astype('int16') + + # find largest CC (just do this to be safe for now) + components = scipy.ndimage.label(sdt_data.squeeze() < args.border)[0] + bincount = np.bincount(components.flatten())[1:] + mask = components == (np.argmax(bincount) + 1) + mask = scipy.ndimage.morphology.binary_fill_holes(mask) + + # write the masked output + if args.out: + img_data = image.get_fdata() + bg = np.min([0, img_data.min()]) + img_data[mask == 0] = bg + nb.Nifti1Image(img_data, image.affine, image.header).to_filename( + args.out, + ) + print(f'Masked image saved to: {args.out}') + + # write the brain mask + if args.mask: + hdr = image.header.copy() + hdr.set_data_dtype('uint8') + nb.Nifti1Image(mask, image.affine, hdr).to_filename(args.mask) + print(f'Binary brain mask saved to: {args.mask}') + + print('If you use SynthStrip in your analysis, please cite:') + print('----------------------------------------------------') + print('SynthStrip: Skull-Stripping for Any Brain Image.') + print('A Hoopes, JS Mora, AV Dalca, B Fischl, M Hoffmann.') + + +def conform(input_nii): + """Resample image as SynthStrip likes it.""" + import nibabel as nb + import numpy as np + from nitransforms.linear import Affine + + shape = np.array(input_nii.shape[:3]) + affine = input_nii.affine + + # Get corner voxel centers in index coords + corner_centers_ijk = ( + np.array( + [ + (i, j, k) + for k in (0, shape[2] - 1) + for j in (0, shape[1] - 1) + for i in (0, shape[0] - 1) + ] + ) + + 0.5 + ) + + # Get corner voxel centers in mm + corners_xyz = affine @ np.hstack((corner_centers_ijk, np.ones((len(corner_centers_ijk), 1)))).T + + # Target affine is 1mm voxels in LIA orientation + target_affine = np.diag([-1.0, 1.0, -1.0, 1.0])[:, (0, 2, 1, 3)] + + # Target shape + extent = corners_xyz.min(1)[:3], corners_xyz.max(1)[:3] + target_shape = ((extent[1] - extent[0]) / 1.0 + 0.999).astype(int) + + # SynthStrip likes dimensions be multiple of 64 (192, 256, or 320) + target_shape = np.clip(np.ceil(np.array(target_shape) / 64).astype(int) * 64, 192, 320) + + # Ensure shape ordering is LIA too + target_shape[2], target_shape[1] = target_shape[1:3] + + # Coordinates of center voxel do not change + input_c = affine @ np.hstack((0.5 * (shape - 1), 1.0)) + target_c = target_affine @ np.hstack((0.5 * (target_shape - 1), 1.0)) + + # Rebase the origin of the new, plumb affine + target_affine[:3, 3] -= target_c[:3] - input_c[:3] + + nii = Affine( + reference=nb.Nifti1Image(np.zeros(target_shape), target_affine, None), + ).apply(input_nii) + return nii + + +def resample_like(image, target, output_dtype=None, cval=0): + """Resample the input image to be in the target's grid via identity transform.""" + from nitransforms.linear import Affine + + return Affine(reference=target).apply(image, output_dtype=output_dtype, cval=cval) + + +if __name__ == '__main__': + main() diff --git a/pydra/tasks/mriqc/synthstrip/model.py b/pydra/tasks/mriqc/synthstrip/model.py new file mode 100644 index 0000000..3081d2d --- /dev/null +++ b/pydra/tasks/mriqc/synthstrip/model.py @@ -0,0 +1,180 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2022 The NiPreps 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. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +# STATEMENT OF CHANGES: This file is derived from sources licensed under the FreeSurfer 1.0 license +# terms, and this file has been changed. +# The full licensing terms of the original work are found at: +# https://github.com/freesurfer/freesurfer/blob/2995ded957961a7f3704de57eee88eb6cc30d52d/LICENSE.txt +# A copy of the license has been archived in the ORIGINAL_LICENSE file +# found within this redistribution. +# +# The original file this work derives from is found at: +# https://github.com/freesurfer/freesurfer/blob/2995ded957961a7f3704de57eee88eb6cc30d52d/mri_synthstrip/mri_synthstrip +# +# [April 2022] CHANGES: +# * MAINT: Split the monolithic file into model and CLI submodules +# * ENH: Replace freesurfer Python bundle with in-house code. +# +""" +Robust, universal skull-stripping for brain images of any type. +If you use SynthStrip in your analysis, please cite: + + A Hoopes, JS Mora, AV Dalca, B Fischl, M Hoffmann. + SynthStrip: Skull-Stripping for Any Brain Image. + https://arxiv.org/abs/2203.09974 + +""" + +import numpy as np +import torch +import torch.nn as nn + + +class StripModel(nn.Module): + def __init__( + self, + nb_features=16, + nb_levels=7, + feat_mult=2, + max_features=64, + nb_conv_per_level=2, + max_pool=2, + return_mask=False, + ): + super().__init__() + + # dimensionality + ndims = 3 + + # build feature list automatically + if isinstance(nb_features, int): + if nb_levels is None: + raise ValueError('must provide unet nb_levels if nb_features is an integer') + feats = np.round(nb_features * feat_mult ** np.arange(nb_levels)).astype(int) + feats = np.clip(feats, 1, max_features) + nb_features = [ + np.repeat(feats[:-1], nb_conv_per_level), + np.repeat(np.flip(feats), nb_conv_per_level), + ] + elif nb_levels is not None: + raise ValueError('cannot use nb_levels if nb_features is not an integer') + + # extract any surplus (full resolution) decoder convolutions + enc_nf, dec_nf = nb_features + nb_dec_convs = len(enc_nf) + final_convs = dec_nf[nb_dec_convs:] + dec_nf = dec_nf[:nb_dec_convs] + self.nb_levels = int(nb_dec_convs / nb_conv_per_level) + 1 + + if isinstance(max_pool, int): + max_pool = [max_pool] * self.nb_levels + + # cache downsampling / upsampling operations + MaxPooling = getattr(nn, 'MaxPool%dd' % ndims) + self.pooling = [MaxPooling(s) for s in max_pool] + self.upsampling = [nn.Upsample(scale_factor=s, mode='nearest') for s in max_pool] + + # configure encoder (down-sampling path) + prev_nf = 1 + encoder_nfs = [prev_nf] + self.encoder = nn.ModuleList() + for level in range(self.nb_levels - 1): + convs = nn.ModuleList() + for conv in range(nb_conv_per_level): + nf = enc_nf[level * nb_conv_per_level + conv] + convs.append(ConvBlock(ndims, prev_nf, nf)) + prev_nf = nf + self.encoder.append(convs) + encoder_nfs.append(prev_nf) + + # configure decoder (up-sampling path) + encoder_nfs = np.flip(encoder_nfs) + self.decoder = nn.ModuleList() + for level in range(self.nb_levels - 1): + convs = nn.ModuleList() + for conv in range(nb_conv_per_level): + nf = dec_nf[level * nb_conv_per_level + conv] + convs.append(ConvBlock(ndims, prev_nf, nf)) + prev_nf = nf + self.decoder.append(convs) + if level < (self.nb_levels - 1): + prev_nf += encoder_nfs[level] + + # now we take care of any remaining convolutions + self.remaining = nn.ModuleList() + for nf in final_convs: + self.remaining.append(ConvBlock(ndims, prev_nf, nf)) + prev_nf = nf + + # final convolutions + if return_mask: + self.remaining.append(ConvBlock(ndims, prev_nf, 2, activation=None)) + self.remaining.append(nn.Softmax(dim=1)) + else: + self.remaining.append(ConvBlock(ndims, prev_nf, 1, activation=None)) + + def forward(self, x): + # encoder forward pass + x_history = [x] + for level, convs in enumerate(self.encoder): + for conv in convs: + x = conv(x) + x_history.append(x) + x = self.pooling[level](x) + + # decoder forward pass with upsampling and concatenation + for level, convs in enumerate(self.decoder): + for conv in convs: + x = conv(x) + if level < (self.nb_levels - 1): + x = self.upsampling[level](x) + x = torch.cat([x, x_history.pop()], dim=1) + + # remaining convs at full resolution + for conv in self.remaining: + x = conv(x) + + return x + + +class ConvBlock(nn.Module): + """ + Specific convolutional block followed by leakyrelu for unet. + """ + + def __init__(self, ndims, in_channels, out_channels, stride=1, activation='leaky'): + super().__init__() + + Conv = getattr(nn, 'Conv%dd' % ndims) + self.conv = Conv(in_channels, out_channels, 3, stride, 1) + if activation == 'leaky': + self.activation = nn.LeakyReLU(0.2) + elif activation is None: + self.activation = None + else: + raise ValueError(f'Unknown activation: {activation}') + + def forward(self, x): + out = self.conv(x) + if self.activation is not None: + out = self.activation(out) + return out diff --git a/pydra/tasks/mriqc/utils/__init__.py b/pydra/tasks/mriqc/utils/__init__.py new file mode 100644 index 0000000..f0c40d8 --- /dev/null +++ b/pydra/tasks/mriqc/utils/__init__.py @@ -0,0 +1,2 @@ +from .bids import derive_bids_fname +from .misc import BIDS_COMP, _flatten_dict diff --git a/pydra/tasks/mriqc/utils/bids.py b/pydra/tasks/mriqc/utils/bids.py new file mode 100644 index 0000000..2bba31c --- /dev/null +++ b/pydra/tasks/mriqc/utils/bids.py @@ -0,0 +1,95 @@ +import logging +from pathlib import Path + + +logger = logging.getLogger(__name__) + + +def derive_bids_fname( + orig_path: str | Path, + entity: str | None = None, + newsuffix: str | None = None, + newpath: str | Path | None = None, + newext: str | None = None, + position: int = -1, + absolute: bool = True, +) -> Path | str: + """ + Derive a new file name from a BIDS-formatted path. + + Parameters + ---------- + orig_path : :obj:`str` or :obj:`os.pathlike` + A filename (may or may not include path). + entity : :obj:`str`, optional + A new BIDS-like key-value pair. + newsuffix : :obj:`str`, optional + Replace the BIDS suffix. + newpath : :obj:`str` or :obj:`os.pathlike`, optional + Path to replace the path of the input orig_path. + newext : :obj:`str`, optional + Replace the extension of the file. + position : :obj:`int`, optional + Position to insert the entity in the filename. + absolute : :obj:`bool`, optional + If True (default), returns the absolute path of the modified filename. + + Returns + ------- + Absolute path of the modified filename + + Examples + -------- + >>> derive_bids_fname( + ... 'sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz', + ... entity='desc-preproc', + ... absolute=False, + ... ) + PosixPath('sub-001/ses-01/anat/sub-001_ses-01_desc-preproc_T1w.nii.gz') + + >>> derive_bids_fname( + ... 'sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz', + ... entity='desc-brain', + ... newsuffix='mask', + ... newext=".nii", + ... absolute=False, + ... ) # doctest: +ELLIPSIS + PosixPath('sub-001/ses-01/anat/sub-001_ses-01_desc-brain_mask.nii') + + >>> derive_bids_fname( + ... 'sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz', + ... entity='desc-brain', + ... newsuffix='mask', + ... newext=".nii", + ... newpath="/output/node", + ... absolute=True, + ... ) # doctest: +ELLIPSIS + PosixPath('/output/node/sub-001_ses-01_desc-brain_mask.nii') + + >>> derive_bids_fname( + ... 'sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz', + ... entity='desc-brain', + ... newsuffix='mask', + ... newext=".nii", + ... newpath=".", + ... absolute=False, + ... ) # doctest: +ELLIPSIS + PosixPath('sub-001_ses-01_desc-brain_mask.nii') + + """ + orig_path = Path(orig_path) + newpath = orig_path.parent if newpath is None else Path(newpath) + ext = "".join(orig_path.suffixes) + newext = newext if newext is not None else ext + orig_stem = orig_path.name.replace(ext, "") + suffix = orig_stem.rsplit("_", maxsplit=1)[-1].strip("_") + newsuffix = newsuffix.strip("_") if newsuffix is not None else suffix + orig_stem = orig_stem.replace(suffix, "").strip("_") + bidts = [bit for bit in orig_stem.split("_") if bit] + if entity: + if position == -1: + bidts.append(entity) + else: + bidts.insert(position, entity.strip("_")) + retval = newpath / f'{"_".join(bidts)}_{newsuffix}.{newext.strip(".")}' + return retval.absolute() if absolute else retval diff --git a/pydra/tasks/mriqc/utils/misc.py b/pydra/tasks/mriqc/utils/misc.py new file mode 100644 index 0000000..4b4f475 --- /dev/null +++ b/pydra/tasks/mriqc/utils/misc.py @@ -0,0 +1,33 @@ +from collections import OrderedDict +import logging + + +logger = logging.getLogger(__name__) + + +def _flatten_dict(indict): + + out_qc = {} + for k, value in list(indict.items()): + if not isinstance(value, dict): + out_qc[k] = value + else: + for subk, subval in list(value.items()): + if not isinstance(subval, dict): + out_qc["_".join([k, subk])] = subval + else: + for ssubk, ssubval in list(subval.items()): + out_qc["_".join([k, subk, ssubk])] = ssubval + return out_qc + + +BIDS_COMP = OrderedDict( + [ + ("subject_id", "sub"), + ("session_id", "ses"), + ("task_id", "task"), + ("acq_id", "acq"), + ("rec_id", "rec"), + ("run_id", "run"), + ] +) diff --git a/pydra/tasks/mriqc/workflows/__init__.py b/pydra/tasks/mriqc/workflows/__init__.py new file mode 100644 index 0000000..e5b76cf --- /dev/null +++ b/pydra/tasks/mriqc/workflows/__init__.py @@ -0,0 +1,31 @@ +from .anatomical import ( + _binarize, + airmsk_wf, + anat_qc_workflow, + compute_iqms, + headmsk_wf, + init_anat_report_wf, + init_brain_tissue_segmentation, + spatial_normalization, +) +from .diffusion import ( + _get_wm, + compute_iqms, + dmri_qc_workflow, + epi_mni_align, + hmc_workflow, + init_dwi_report_wf, +) +from .functional import ( + _carpet_parcellation, + _get_tr, + compute_iqms, + epi_mni_align, + fmri_bmsk_workflow, + fmri_qc_workflow, + hmc, + init_func_report_wf, + spikes_mask, +) +from .shared import synthstrip_wf +from .utils import _tofloat, generate_filename, get_fwhmx, slice_wise_fft, spectrum_mask diff --git a/pydra/tasks/mriqc/workflows/anatomical/__init__.py b/pydra/tasks/mriqc/workflows/anatomical/__init__.py new file mode 100644 index 0000000..c82ce0a --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/__init__.py @@ -0,0 +1,10 @@ +from .base import ( + _binarize, + airmsk_wf, + anat_qc_workflow, + compute_iqms, + headmsk_wf, + init_brain_tissue_segmentation, + spatial_normalization, +) +from .output import init_anat_report_wf diff --git a/pydra/tasks/mriqc/workflows/anatomical/base.py b/pydra/tasks/mriqc/workflows/anatomical/base.py new file mode 100644 index 0000000..1795675 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/base.py @@ -0,0 +1,807 @@ +import attrs +from itertools import chain +import logging +from pathlib import Path +from pydra.compose import python, workflow +from pydra.tasks.mriqc.interfaces import ( + ArtifactMask, + ComputeQI2, + ConformImage, + RotationMask, + StructuralQC, +) +from pydra.tasks.mriqc.workflows.anatomical.output import init_anat_report_wf +from pydra.tasks.mriqc.workflows.utils import get_fwhmx +from pydra.tasks.niworkflows.interfaces.fixes import ( + FixHeaderApplyTransforms as ApplyTransforms, +) +from templateflow.api import get as get_template +import typing as ty + + +logger = logging.getLogger(__name__) + + +@workflow.define( + outputs=[ + "norm_report", + "iqmswf_noise_report", + "anat_report_wf_zoom_report", + "anat_report_wf_bg_report", + "anat_report_wf_segm_report", + "anat_report_wf_bmask_report", + "anat_report_wf_artmask_report", + "anat_report_wf_airmask_report", + "anat_report_wf_headmask_report", + ] +) +def anat_qc_workflow( + exec_ants_float=False, + exec_debug=False, + exec_no_sub=False, + exec_verbose_reports=False, + exec_work_dir=None, + in_file: ty.Any = attrs.NOTHING, + modality: ty.Any = attrs.NOTHING, + name="anatMRIQC", + nipype_omp_nthreads=12, + wf_biggest_file_gb=1, + wf_inputs=None, + wf_inputs_entities={}, + wf_inputs_metadata=None, + wf_species="human", + wf_template_id="MNI152NLin2009cAsym", +) -> [ + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", +]: + """ + One-subject-one-session-one-run pipeline to extract the NR-IQMs from + anatomical images + + .. workflow:: + + import os.path as op + from mriqc.workflows.anatomical.base import anat_qc_workflow + from mriqc.testing import mock_config + with mock_config(): + wf = anat_qc_workflow() + + """ + from pydra.tasks.mriqc.workflows.shared import synthstrip_wf + + if exec_work_dir is None: + exec_work_dir = Path.cwd() + + outputs_ = { + "norm_report": attrs.NOTHING, + "iqmswf_noise_report": attrs.NOTHING, + "anat_report_wf_zoom_report": attrs.NOTHING, + "anat_report_wf_bg_report": attrs.NOTHING, + "anat_report_wf_segm_report": attrs.NOTHING, + "anat_report_wf_bmask_report": attrs.NOTHING, + "anat_report_wf_artmask_report": attrs.NOTHING, + "anat_report_wf_airmask_report": attrs.NOTHING, + "anat_report_wf_headmask_report": attrs.NOTHING, + } + + # Enable if necessary + # mem_gb = max( + # wf_biggest_file_gb['t1w'], + # wf_biggest_file_gb['t2w'], + # ) + dataset = list( + chain( + wf_inputs.get("t1w", []), + wf_inputs.get("t2w", []), + ) + ) + metadata = list( + chain( + wf_inputs_metadata.get("t1w", []), + wf_inputs_metadata.get("t2w", []), + ) + ) + entities = list( + chain( + wf_inputs_entities.get("t1w", []), + wf_inputs_entities.get("t2w", []), + ) + ) + message = "Building {modality} MRIQC workflow {detail}.".format( + modality="anatomical", + detail=f"for {len(dataset)} NIfTI files.", + ) + logger.info(message) + # Initialize workflow + # Define workflow, inputs and outputs + # 0. Get data + + # 1. Reorient anatomical image + to_ras = workflow.add( + ConformImage(check_dtype=False, in_file=in_file), name="to_ras" + ) + # 2. species specific skull-stripping + if wf_species.lower() == "human": + skull_stripping = workflow.add( + synthstrip_wf( + omp_nthreads=nipype_omp_nthreads, + in_files=to_ras.out_file, + name="skull_stripping", + ) + ) + ss_bias_field = "outputnode.bias_image" + else: + from nirodents.workflows.brainextraction import init_rodent_brain_extraction_wf + + skull_stripping = init_rodent_brain_extraction_wf(template_id=wf_template_id) + ss_bias_field = "final_n4.bias_image" + # 3. Head mask + hmsk = workflow.add( + headmsk_wf(omp_nthreads=nipype_omp_nthreads, wf_species=wf_species, name="hmsk") + ) + # 4. Spatial Normalization, using ANTs + norm = workflow.add( + spatial_normalization( + wf_species=wf_species, + wf_template_id=wf_template_id, + exec_ants_float=exec_ants_float, + exec_debug=exec_debug, + nipype_omp_nthreads=nipype_omp_nthreads, + modality=modality, + name="norm", + ) + ) + # 5. Air mask (with and without artifacts) + amw = workflow.add( + airmsk_wf( + ind2std_xfm=norm.ind2std_xfm, + in_file=to_ras.out_file, + head_mask=hmsk.out_file, + name="amw", + ) + ) + # 6. Brain tissue segmentation + bts = workflow.add( + init_brain_tissue_segmentation( + nipype_omp_nthreads=nipype_omp_nthreads, + std_tpms=norm.out_tpms, + in_file=hmsk.out_denoised, + name="bts", + ) + ) + # 7. Compute IQMs + iqmswf = workflow.add( + compute_iqms( + wf_species=wf_species, + std_tpms=norm.out_tpms, + in_ras=to_ras.out_file, + airmask=amw.air_mask, + hatmask=amw.hat_mask, + artmask=amw.art_mask, + rotmask=amw.rot_mask, + segmentation=bts.out_segm, + pvms=bts.out_pvms, + headmask=hmsk.out_file, + name="iqmswf", + ) + ) + # Reports + anat_report_wf = workflow.add( + init_anat_report_wf( + wf_species=wf_species, + exec_verbose_reports=exec_verbose_reports, + exec_work_dir=exec_work_dir, + in_ras=to_ras.out_file, + headmask=hmsk.out_file, + airmask=amw.air_mask, + artmask=amw.art_mask, + segmentation=bts.out_segm, + name="anat_report_wf", + ) + ) + # Connect all nodes + # fmt: off + + hmsk.inputs.in_file = skull_stripping.out_corrected + hmsk.inputs.brainmask = skull_stripping.out_mask + bts.inputs.brainmask = skull_stripping.out_mask + norm.inputs.moving_image = skull_stripping.out_corrected + norm.inputs.moving_mask = skull_stripping.out_mask + hmsk.inputs.in_tpms = norm.out_tpms + + iqmswf.inputs.inu_corrected = skull_stripping.out_corrected + iqmswf.inputs.in_inu = skull_stripping.bias_image + iqmswf.inputs.brainmask = skull_stripping.out_mask + + anat_report_wf.inputs.brainmask = skull_stripping.out_mask + + # fmt: on + # Upload metrics + if not exec_no_sub: + from pydra.tasks.mriqc.interfaces.webapi import UploadIQMs + + pass + # fmt: off + pass + pass + # fmt: on + outputs_["norm_report"] = norm.report + outputs_["iqmswf_noise_report"] = iqmswf.noise_report + outputs_["anat_report_wf_bmask_report"] = anat_report_wf.bmask_report + outputs_["anat_report_wf_artmask_report"] = anat_report_wf.artmask_report + outputs_["anat_report_wf_headmask_report"] = anat_report_wf.headmask_report + outputs_["anat_report_wf_bg_report"] = anat_report_wf.bg_report + outputs_["anat_report_wf_airmask_report"] = anat_report_wf.airmask_report + outputs_["anat_report_wf_zoom_report"] = anat_report_wf.zoom_report + outputs_["anat_report_wf_segm_report"] = anat_report_wf.segm_report + + return tuple(outputs_) + + +@workflow.define(outputs=["hat_mask", "air_mask", "art_mask", "rot_mask"]) +def airmsk_wf( + head_mask: ty.Any = attrs.NOTHING, + in_file: ty.Any = attrs.NOTHING, + ind2std_xfm: ty.Any = attrs.NOTHING, + name="AirMaskWorkflow", +) -> ["ty.Any", "ty.Any", "ty.Any", "ty.Any"]: + """ + Calculate air, artifacts and "hat" masks to evaluate noise in the background. + + This workflow mostly addresses the implementation of Step 1 in [Mortamet2009]_. + This work proposes to look at the signal distribution in the background, where + no signals are expected, to evaluate the spread of the noise. + It is in the background where [Mortamet2009]_ proposed to also look at the presence + of ghosts and artifacts, where they are very easy to isolate. + + However, [Mortamet2009]_ proposes not to look at the background around the face + because of the likely signal leakage through the phase-encoding axis sourcing from + eyeballs (and their motion). + To avoid that, [Mortamet2009]_ proposed atlas-based identification of two landmarks + (nasion and cerebellar projection on to the occipital bone). + MRIQC, for simplicity, used a such a mask created in MNI152NLin2009cAsym space and + projected it on to the individual. + Such a solution is inadequate because it doesn't drop full in-plane slices as there + will be a large rotation of the individual's tilt of the head with respect to the + template. + The new implementation (23.1.x series) follows [Mortamet2009]_ more closely, + projecting the two landmarks from the template space and leveraging + *NiTransforms* to do that. + + .. workflow:: + + from mriqc.testing import mock_config + from mriqc.workflows.anatomical.base import airmsk_wf + with mock_config(): + wf = airmsk_wf() + + """ + outputs_ = { + "hat_mask": attrs.NOTHING, + "air_mask": attrs.NOTHING, + "art_mask": attrs.NOTHING, + "rot_mask": attrs.NOTHING, + } + + rotmsk = workflow.add(RotationMask(in_file=in_file), name="rotmsk") + qi1 = workflow.add( + ArtifactMask(head_mask=head_mask, in_file=in_file, ind2std_xfm=ind2std_xfm), + name="qi1", + ) + # fmt: off + outputs_['hat_mask'] = qi1.out_hat_msk + outputs_['air_mask'] = qi1.out_air_msk + outputs_['art_mask'] = qi1.out_art_msk + outputs_['rot_mask'] = rotmsk.out_file + # fmt: on + + return tuple(outputs_) + + +@workflow.define(outputs=["out_file", "out_denoised"]) +def headmsk_wf( + brainmask: ty.Any = attrs.NOTHING, + in_file: ty.Any = attrs.NOTHING, + in_tpms: ty.Any = attrs.NOTHING, + name="HeadMaskWorkflow", + omp_nthreads=1, + wf_species="human", +) -> ["ty.Any", "ty.Any"]: + """ + Computes a head mask as in [Mortamet2009]_. + + .. workflow:: + + from mriqc.testing import mock_config + from mriqc.workflows.anatomical.base import headmsk_wf + with mock_config(): + wf = headmsk_wf() + + """ + from pydra.tasks.niworkflows.interfaces.nibabel import ApplyMask + + outputs_ = { + "out_file": attrs.NOTHING, + "out_denoised": attrs.NOTHING, + } + + def _select_wm(inlist): + return [f for f in inlist if "WM" in f][0] + + enhance = workflow.add( + FunctionTask( + func=_enhance, + input_spec=SpecInfo( + name="FunctionIn", + bases=(BaseSpec,), + fields=[("in_file", ty.Any), ("wm_tpm", ty.Any)], + ), + output_spec=SpecInfo( + name="FunctionOut", bases=(BaseSpec,), fields=[("out_file", ty.Any)] + ), + in_file=in_file, + wm_tpm=in_tpms, + ), + name="enhance", + ) + gradient = workflow.add( + FunctionTask( + func=image_gradient, + input_spec=SpecInfo( + name="FunctionIn", + bases=(BaseSpec,), + fields=[("in_file", ty.Any), ("brainmask", ty.Any), ("sigma", ty.Any)], + ), + output_spec=SpecInfo( + name="FunctionOut", bases=(BaseSpec,), fields=[("out_file", ty.Any)] + ), + brainmask=brainmask, + in_file=enhance.out_file, + ), + name="gradient", + ) + thresh = workflow.add( + FunctionTask( + func=gradient_threshold, + input_spec=SpecInfo( + name="FunctionIn", + bases=(BaseSpec,), + fields=[ + ("in_file", ty.Any), + ("brainmask", ty.Any), + ("aniso", ty.Any), + ("thresh", ty.Any), + ], + ), + output_spec=SpecInfo( + name="FunctionOut", bases=(BaseSpec,), fields=[("out_file", ty.Any)] + ), + brainmask=brainmask, + in_file=gradient.out_file, + ), + name="thresh", + ) + if wf_species != "human": + gradient.inputs.inputs.sigma = 3.0 + thresh.inputs.inputs.aniso = True + thresh.inputs.inputs.thresh = 4.0 + apply_mask = workflow.add( + ApplyMask(in_file=enhance.out_file, in_mask=brainmask), name="apply_mask" + ) + # fmt: off + enhance.inputs.wm_tpm = in_tpms + outputs_['out_file'] = thresh.out_file + outputs_['out_denoised'] = apply_mask.out_file + # fmt: on + + return tuple(outputs_) + + +@workflow.define(outputs=["out_segm", "out_pvms"]) +def init_brain_tissue_segmentation( + brainmask: ty.Any = attrs.NOTHING, + in_file: ty.Any = attrs.NOTHING, + name="brain_tissue_segmentation", + nipype_omp_nthreads=12, + std_tpms: ty.Any = attrs.NOTHING, +) -> ["ty.Any", "ty.Any"]: + """ + Setup a workflow for brain tissue segmentation. + + .. workflow:: + + from mriqc.workflows.anatomical.base import init_brain_tissue_segmentation + from mriqc.testing import mock_config + with mock_config(): + wf = init_brain_tissue_segmentation() + + """ + from pydra.tasks.ants.auto import Atropos + + outputs_ = { + "out_segm": attrs.NOTHING, + "out_pvms": attrs.NOTHING, + } + + def _format_tpm_names(in_files, fname_string=None): + import glob + from pathlib import Path + import nibabel as nb + + out_path = Path.cwd().absolute() + # copy files to cwd and rename iteratively + for count, fname in enumerate(in_files): + img = nb.load(fname) + extension = "".join(Path(fname).suffixes) + out_fname = f"priors_{1 + count:02}{extension}" + nb.save(img, Path(out_path, out_fname)) + if fname_string is None: + fname_string = f"priors_%02d{extension}" + out_files = [ + str(prior) + for prior in glob.glob(str(Path(out_path, f"priors*{extension}"))) + ] + # return path with c-style format string for Atropos + file_format = str(Path(out_path, fname_string)) + return file_format, out_files + + format_tpm_names = workflow.add( + FunctionTask( + execution={"keep_inputs": True, "remove_unnecessary_outputs": False}, + func=_format_tpm_names, + input_spec=SpecInfo( + name="FunctionIn", bases=(BaseSpec,), fields=[("in_files", ty.Any)] + ), + output_spec=SpecInfo( + name="FunctionOut", bases=(BaseSpec,), fields=[("file_format", ty.Any)] + ), + in_files=std_tpms, + ), + name="format_tpm_names", + ) + segment = workflow.add( + Atropos( + initialization="PriorProbabilityImages", + mrf_radius=[1, 1, 1], + mrf_smoothing_factor=0.01, + num_threads=nipype_omp_nthreads, + number_of_tissue_classes=3, + out_classified_image_name="segment.nii.gz", + output_posteriors_name_template="segment_%02d.nii.gz", + prior_weighting=0.1, + save_posteriors=True, + intensity_images=in_file, + mask_image=brainmask, + ), + name="segment", + ) + # fmt: off + + @python.define + def format_tpm_names_file_format_to_segment_prior_image_callable(in_: ty.Any) -> ty.Any: + return _pop(in_) + + format_tpm_names_file_format_to_segment_prior_image_callable = workflow.add(format_tpm_names_file_format_to_segment_prior_image_callable(in_=format_tpm_names.file_format), name="format_tpm_names_file_format_to_segment_prior_image_callable") + + segment.inputs.prior_image = format_tpm_names_file_format_to_segment_prior_image_callable.out + outputs_['out_segm'] = segment.classified_image + outputs_['out_pvms'] = segment.posteriors + # fmt: on + + return tuple(outputs_) + + +@workflow.define(outputs=["report", "ind2std_xfm", "out_tpms"]) +def spatial_normalization( + exec_ants_float=False, + exec_debug=False, + modality: ty.Any = attrs.NOTHING, + moving_image: ty.Any = attrs.NOTHING, + moving_mask: ty.Any = attrs.NOTHING, + name="SpatialNormalization", + nipype_omp_nthreads=12, + wf_species="human", + wf_template_id="MNI152NLin2009cAsym", +) -> ["ty.Any", "ty.Any", "ty.Any"]: + """Create a simplified workflow to perform fast spatial normalization.""" + from pydra.tasks.niworkflows.interfaces.reportlets.registration import ( + SpatialNormalizationRPT as RobustMNINormalization, + ) + + outputs_ = { + "report": attrs.NOTHING, + "ind2std_xfm": attrs.NOTHING, + "out_tpms": attrs.NOTHING, + } + + # Have the template id handy + tpl_id = wf_template_id + # Define workflow interface + + # Spatial normalization + norm = workflow.add( + RobustMNINormalization( + flavor=["testing", "fast"][exec_debug], + float=exec_ants_float, + generate_report=True, + num_threads=nipype_omp_nthreads, + template=tpl_id, + moving_image=moving_image, + moving_mask=moving_mask, + reference=modality, + ), + name="norm", + ) + if wf_species.lower() == "human": + norm.inputs.inputs.reference_mask = str( + get_template(tpl_id, resolution=2, desc="brain", suffix="mask") + ) + else: + norm.inputs.inputs.reference_image = str(get_template(tpl_id, suffix="T2w")) + norm.inputs.inputs.reference_mask = str( + get_template(tpl_id, desc="brain", suffix="mask")[0] + ) + # Project standard TPMs into T1w space + tpms_std2t1w = workflow.add( + ApplyTransforms( + default_value=0, + dimension=3, + float=exec_ants_float, + interpolation="Gaussian", + reference_image=moving_image, + transforms=norm.inverse_composite_transform, + ), + name="tpms_std2t1w", + ) + tpms_std2t1w.inputs.inputs.input_image = [ + str(p) + for p in get_template( + wf_template_id, + suffix="probseg", + resolution=(1 if wf_species.lower() == "human" else None), + label=["CSF", "GM", "WM"], + ) + ] + # fmt: off + outputs_['ind2std_xfm'] = norm.composite_transform + outputs_['report'] = norm.out_report + outputs_['out_tpms'] = tpms_std2t1w.output_image + # fmt: on + + return tuple(outputs_) + + +@workflow.define(outputs=["measures", "noise_report"]) +def compute_iqms( + airmask: ty.Any = attrs.NOTHING, + artmask: ty.Any = attrs.NOTHING, + brainmask: ty.Any = attrs.NOTHING, + hatmask: ty.Any = attrs.NOTHING, + headmask: ty.Any = attrs.NOTHING, + in_inu: ty.Any = attrs.NOTHING, + in_ras: ty.Any = attrs.NOTHING, + inu_corrected: ty.Any = attrs.NOTHING, + name="ComputeIQMs", + pvms: ty.Any = attrs.NOTHING, + rotmask: ty.Any = attrs.NOTHING, + segmentation: ty.Any = attrs.NOTHING, + std_tpms: ty.Any = attrs.NOTHING, + wf_species="human", +) -> ["ty.Any", "ty.Any"]: + """ + Setup the workflow that actually computes the IQMs. + + .. workflow:: + + from mriqc.workflows.anatomical.base import compute_iqms + from mriqc.testing import mock_config + with mock_config(): + wf = compute_iqms() + + """ + from pydra.tasks.mriqc.interfaces.anatomical import Harmonize + + outputs_ = { + "measures": attrs.NOTHING, + "noise_report": attrs.NOTHING, + } + + from pydra.tasks.mriqc.workflows.utils import _tofloat + + # Add provenance + + # AFNI check smoothing + fwhm_interface = get_fwhmx() + fwhm = workflow.add(fwhm_task, name="fwhm") + # Harmonize + homog = workflow.add( + Harmonize(brain_mask=brainmask, in_file=inu_corrected, wm_mask=pvms), + name="homog", + ) + if wf_species.lower() != "human": + homog.inputs.inputs.erodemsk = False + homog.inputs.inputs.thresh = 0.8 + # Mortamet's QI2 + getqi2 = workflow.add(ComputeQI2(air_msk=hatmask, in_file=in_ras), name="getqi2") + # Compute python-coded measures + measures = workflow.add( + StructuralQC( + human=wf_species.lower() == "human", + air_msk=airmask, + artifact_msk=artmask, + head_msk=headmask, + in_bias=in_inu, + in_file=in_ras, + in_noinu=homog.out_file, + in_pvms=pvms, + in_segm=segmentation, + mni_tpms=std_tpms, + rot_msk=rotmask, + ), + name="measures", + ) + + def _getwm(inlist): + return inlist[-1] + + # fmt: off + + + homog.inputs.wm_mask = pvms + + @python.define + def fwhm_fwhm_to_measures_in_fwhm_callable(in_: ty.Any) -> ty.Any: + return _tofloat(in_) + + fwhm_fwhm_to_measures_in_fwhm_callable = workflow.add(fwhm_fwhm_to_measures_in_fwhm_callable(in_=fwhm.fwhm), name="fwhm_fwhm_to_measures_in_fwhm_callable") + + measures.inputs.in_fwhm = fwhm_fwhm_to_measures_in_fwhm_callable.out + outputs_['measures'] = measures.out_qc + outputs_['noise_report'] = getqi2.out_file + + # fmt: on + + return tuple(outputs_) + + +def _enhance(in_file, wm_tpm, out_file=None): + + import nibabel as nb + import numpy as np + from pydra.tasks.mriqc.workflows.utils import generate_filename + + imnii = nb.load(in_file) + data = imnii.get_fdata(dtype=np.float32) + range_max = np.percentile(data[data > 0], 99.98) + excess = data > range_max + wm_prob = nb.load(wm_tpm).get_fdata() + wm_prob[wm_prob < 0] = 0 # Ensure no negative values + wm_prob[excess] = 0 # Ensure no outliers are considered + # Calculate weighted mean and standard deviation + wm_mu = np.average(data, weights=wm_prob) + wm_sigma = np.sqrt(np.average((data - wm_mu) ** 2, weights=wm_prob)) + # Resample signal excess pixels + data[excess] = np.random.normal(loc=wm_mu, scale=wm_sigma, size=excess.sum()) + out_file = out_file or str(generate_filename(in_file, suffix="enhanced").absolute()) + nb.Nifti1Image(data, imnii.affine, imnii.header).to_filename(out_file) + return out_file + + +def _get_mod(in_file): + + from pathlib import Path + + in_file = Path(in_file) + extension = "".join(in_file.suffixes) + return in_file.name.replace(extension, "").split("_")[-1] + + +def _pop(inlist): + + if isinstance(inlist, (list, tuple)): + return inlist[0] + return inlist + + +def gradient_threshold(in_file, brainmask, thresh=15.0, out_file=None, aniso=False): + """Compute a threshold from the histogram of the magnitude gradient image""" + import nibabel as nb + import numpy as np + from scipy import ndimage as sim + from pydra.tasks.mriqc.workflows.utils import generate_filename + + if not aniso: + struct = sim.iterate_structure(sim.generate_binary_structure(3, 2), 2) + else: + # Generate an anisotropic binary structure, taking into account slice thickness + img = nb.load(in_file) + zooms = img.header.get_zooms() + dist = max(zooms) + dim = img.header["dim"][0] + x = np.ones((5) * np.ones(dim, dtype=np.int8)) + np.put(x, x.size // 2, 0) + dist_matrix = np.round(sim.distance_transform_edt(x, sampling=zooms), 5) + struct = dist_matrix <= dist + imnii = nb.load(in_file) + hdr = imnii.header.copy() + hdr.set_data_dtype(np.uint8) + data = imnii.get_fdata(dtype=np.float32) + mask = np.zeros_like(data, dtype=np.uint8) + mask[data > thresh] = 1 + mask = sim.binary_closing(mask, struct, iterations=2).astype(np.uint8) + mask = sim.binary_erosion(mask, sim.generate_binary_structure(3, 2)).astype( + np.uint8 + ) + segdata = np.asanyarray(nb.load(brainmask).dataobj) > 0 + segdata = sim.binary_dilation(segdata, struct, iterations=2, border_value=1).astype( + np.uint8 + ) + mask[segdata] = 1 + # Remove small objects + label_im, nb_labels = sim.label(mask) + artmsk = np.zeros_like(mask) + if nb_labels > 2: + sizes = sim.sum(mask, label_im, list(range(nb_labels + 1))) + ordered = sorted(zip(sizes, list(range(nb_labels + 1))), reverse=True) + for _, label in ordered[2:]: + mask[label_im == label] = 0 + artmsk[label_im == label] = 1 + mask = sim.binary_fill_holes(mask, struct).astype( + np.uint8 + ) # pylint: disable=no-member + out_file = out_file or str(generate_filename(in_file, suffix="gradmask").absolute()) + nb.Nifti1Image(mask, imnii.affine, hdr).to_filename(out_file) + return out_file + + +def image_gradient(in_file, brainmask, sigma=4.0, out_file=None): + """Computes the magnitude gradient of an image using numpy""" + import nibabel as nb + import numpy as np + from scipy.ndimage import gaussian_gradient_magnitude as gradient + from pydra.tasks.mriqc.workflows.utils import generate_filename + + imnii = nb.load(in_file) + mask = np.bool_(nb.load(brainmask).dataobj) + data = imnii.get_fdata(dtype=np.float32) + datamax = np.percentile(data.reshape(-1), 99.5) + data *= 100 / datamax + data[mask] = 100 + zooms = np.array(imnii.header.get_zooms()[:3]) + sigma_xyz = 2 - zooms / min(zooms) + grad = gradient(data, sigma * sigma_xyz) + gradmax = np.percentile(grad.reshape(-1), 99.5) + grad *= 100.0 + grad /= gradmax + grad[mask] = 100 + out_file = out_file or str(generate_filename(in_file, suffix="grad").absolute()) + nb.Nifti1Image(grad, imnii.affine, imnii.header).to_filename(out_file) + return out_file + + +def _binarize(in_file, threshold=0.5, out_file=None): + + import os.path as op + import nibabel as nb + import numpy as np + + if out_file is None: + fname, ext = op.splitext(op.basename(in_file)) + if ext == ".gz": + fname, ext2 = op.splitext(fname) + ext = ext2 + ext + out_file = op.abspath(f"{fname}_bin{ext}") + nii = nb.load(in_file) + data = nii.get_fdata() > threshold + hdr = nii.header.copy() + hdr.set_data_dtype(np.uint8) + nb.Nifti1Image(data.astype(np.uint8), nii.affine, hdr).to_filename(out_file) + return out_file diff --git a/pydra/tasks/mriqc/workflows/anatomical/output.py b/pydra/tasks/mriqc/workflows/anatomical/output.py new file mode 100644 index 0000000..e2ba900 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/output.py @@ -0,0 +1,159 @@ +import attrs +import logging +from pathlib import Path +from pydra.compose import workflow +import typing as ty + + +logger = logging.getLogger(__name__) + + +@workflow.define( + outputs=[ + "zoom_report", + "bg_report", + "segm_report", + "bmask_report", + "artmask_report", + "airmask_report", + "headmask_report", + ] +) +def init_anat_report_wf( + airmask: ty.Any = attrs.NOTHING, + artmask: ty.Any = attrs.NOTHING, + brainmask: ty.Any = attrs.NOTHING, + exec_verbose_reports=False, + exec_work_dir=None, + headmask: ty.Any = attrs.NOTHING, + in_ras: ty.Any = attrs.NOTHING, + name: str = "anat_report_wf", + segmentation: ty.Any = attrs.NOTHING, + wf_species="human", +) -> ["ty.Any", "ty.Any", "ty.Any", "ty.Any", "ty.Any", "ty.Any", "ty.Any"]: + """ + Generate the components of the individual report. + + .. workflow:: + + from mriqc.workflows.anatomical.output import init_anat_report_wf + from mriqc.testing import mock_config + with mock_config(): + wf = init_anat_report_wf() + + """ + from pydra.tasks.nireports.interfaces import PlotMosaic + + if exec_work_dir is None: + exec_work_dir = Path.cwd() + + outputs_ = { + "zoom_report": attrs.NOTHING, + "bg_report": attrs.NOTHING, + "segm_report": attrs.NOTHING, + "bmask_report": attrs.NOTHING, + "artmask_report": attrs.NOTHING, + "airmask_report": attrs.NOTHING, + "headmask_report": attrs.NOTHING, + } + + # from mriqc.interfaces.reports import IndividualReport + verbose = exec_verbose_reports + reportlets_dir = exec_work_dir / "reportlets" + + mosaic_zoom = workflow.add( + PlotMosaic(cmap="Greys_r", bbox_mask_file=brainmask, in_file=in_ras), + name="mosaic_zoom", + ) + mosaic_noise = workflow.add( + PlotMosaic(cmap="viridis_r", only_noise=True, in_file=in_ras), + name="mosaic_noise", + ) + if wf_species.lower() in ("rat", "mouse"): + mosaic_zoom.inputs.inputs.view = ["coronal", "axial"] + mosaic_noise.inputs.inputs.view = ["coronal", "axial"] + + # fmt: off + outputs_['zoom_report'] = mosaic_zoom.out_file + outputs_['bg_report'] = mosaic_noise.out_file + # fmt: on + + from pydra.tasks.nireports.interfaces import PlotContours + + display_mode = "y" if wf_species.lower() in ("rat", "mouse") else "z" + plot_segm = workflow.add( + PlotContours( + colors=["r", "g", "b"], + cut_coords=10, + display_mode=display_mode, + levels=[0.5, 1.5, 2.5], + in_contours=segmentation, + in_file=in_ras, + ), + name="plot_segm", + ) + + plot_bmask = workflow.add( + PlotContours( + colors=["r"], + cut_coords=10, + display_mode=display_mode, + levels=[0.5], + out_file="bmask", + in_contours=brainmask, + in_file=in_ras, + ), + name="plot_bmask", + ) + + plot_artmask = workflow.add( + PlotContours( + colors=["r"], + cut_coords=10, + display_mode=display_mode, + levels=[0.5], + out_file="artmask", + saturate=True, + in_contours=artmask, + in_file=in_ras, + ), + name="plot_artmask", + ) + + # NOTE: humans switch on these two to coronal view. + display_mode = "y" if wf_species.lower() in ("rat", "mouse") else "x" + plot_airmask = workflow.add( + PlotContours( + colors=["r"], + cut_coords=6, + display_mode=display_mode, + levels=[0.5], + out_file="airmask", + in_contours=airmask, + in_file=in_ras, + ), + name="plot_airmask", + ) + + plot_headmask = workflow.add( + PlotContours( + colors=["r"], + cut_coords=6, + display_mode=display_mode, + levels=[0.5], + out_file="headmask", + in_contours=headmask, + in_file=in_ras, + ), + name="plot_headmask", + ) + + # fmt: off + outputs_['bmask_report'] = plot_bmask.out_file + outputs_['segm_report'] = plot_segm.out_file + outputs_['artmask_report'] = plot_artmask.out_file + outputs_['headmask_report'] = plot_headmask.out_file + outputs_['airmask_report'] = plot_airmask.out_file + # fmt: on + + return tuple(outputs_) diff --git a/pydra/tasks/mriqc/workflows/anatomical/tests/conftest.py b/pydra/tasks/mriqc/workflows/anatomical/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_airmsk_wf.py b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_airmsk_wf.py new file mode 100644 index 0000000..2bba00d --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_airmsk_wf.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.anatomical.base import airmsk_wf +import pytest + + +logger = logging.getLogger(__name__) + + +def test_airmsk_wf_build(): + workflow = airmsk_wf() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_airmsk_wf_run(): + workflow = airmsk_wf() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_anat_qc_workflow.py b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_anat_qc_workflow.py new file mode 100644 index 0000000..4a722fe --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_anat_qc_workflow.py @@ -0,0 +1,22 @@ +from fileformats.medimage import NiftiGzX, T1Weighted +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.anatomical.base import anat_qc_workflow +import pytest + + +logger = logging.getLogger(__name__) + + +def test_anat_qc_workflow_build(): + workflow = anat_qc_workflow() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_anat_qc_workflow_run(): + workflow = anat_qc_workflow(in_file=NiftiGzX[T1Weighted].sample()) + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_compute_iqms.py b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_compute_iqms.py new file mode 100644 index 0000000..507dc77 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_compute_iqms.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.anatomical.base import compute_iqms +import pytest + + +logger = logging.getLogger(__name__) + + +def test_compute_iqms_build(): + workflow = compute_iqms() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_compute_iqms_run(): + workflow = compute_iqms() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_headmsk_wf.py b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_headmsk_wf.py new file mode 100644 index 0000000..26b8172 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_headmsk_wf.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.anatomical.base import headmsk_wf +import pytest + + +logger = logging.getLogger(__name__) + + +def test_headmsk_wf_build(): + workflow = headmsk_wf() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_headmsk_wf_run(): + workflow = headmsk_wf() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_init_brain_tissue_segmentation.py b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_init_brain_tissue_segmentation.py new file mode 100644 index 0000000..94bbea9 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_init_brain_tissue_segmentation.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.anatomical.base import init_brain_tissue_segmentation +import pytest + + +logger = logging.getLogger(__name__) + + +def test_init_brain_tissue_segmentation_build(): + workflow = init_brain_tissue_segmentation() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_init_brain_tissue_segmentation_run(): + workflow = init_brain_tissue_segmentation() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_spatial_normalization.py b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_spatial_normalization.py new file mode 100644 index 0000000..6b6c3ed --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_base_spatial_normalization.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.anatomical.base import spatial_normalization +import pytest + + +logger = logging.getLogger(__name__) + + +def test_spatial_normalization_build(): + workflow = spatial_normalization() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_spatial_normalization_run(): + workflow = spatial_normalization() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_output_init_anat_report_wf.py b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_output_init_anat_report_wf.py new file mode 100644 index 0000000..c4ebae4 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/anatomical/tests/test_workflows_anatomical_output_init_anat_report_wf.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.anatomical.output import init_anat_report_wf +import pytest + + +logger = logging.getLogger(__name__) + + +def test_init_anat_report_wf_build(): + workflow = init_anat_report_wf() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_init_anat_report_wf_run(): + workflow = init_anat_report_wf() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/diffusion/__init__.py b/pydra/tasks/mriqc/workflows/diffusion/__init__.py new file mode 100644 index 0000000..e23fd8a --- /dev/null +++ b/pydra/tasks/mriqc/workflows/diffusion/__init__.py @@ -0,0 +1,2 @@ +from .base import compute_iqms, dmri_qc_workflow, epi_mni_align, hmc_workflow +from .output import _get_wm, init_dwi_report_wf diff --git a/pydra/tasks/mriqc/workflows/diffusion/base.py b/pydra/tasks/mriqc/workflows/diffusion/base.py new file mode 100644 index 0000000..4a67a5e --- /dev/null +++ b/pydra/tasks/mriqc/workflows/diffusion/base.py @@ -0,0 +1,673 @@ +import attrs +import logging +from pathlib import Path +from pydra.compose import python, workflow +from pydra.tasks.mriqc.workflows.diffusion.output import init_dwi_report_wf +import typing as ty + + +logger = logging.getLogger(__name__) + + +@workflow.define( + outputs=[ + "iqms_wf_out_file", + "iqms_wf_noise_floor", + "dwi_report_wf_snr_report", + "dwi_report_wf_noise_report", + "dwi_report_wf_fa_report", + "dwi_report_wf_md_report", + "dwi_report_wf_heatmap_report", + "dwi_report_wf_spikes_report", + "dwi_report_wf_carpet_report", + "dwi_report_wf_bmask_report", + ] +) +def dmri_qc_workflow( + bvals: ty.Any = attrs.NOTHING, + bvecs: ty.Any = attrs.NOTHING, + exec_ants_float=False, + exec_debug=False, + exec_float32=True, + exec_verbose_reports=False, + exec_work_dir=None, + in_file: ty.Any = attrs.NOTHING, + name="dwiMRIQC", + nipype_nprocs=12, + nipype_omp_nthreads=12, + qspace_neighbors: ty.Any = attrs.NOTHING, + wf_biggest_file_gb=1, + wf_fd_radius=50, + wf_fd_thres=0.2, + wf_inputs=None, + wf_inputs_entities={}, + wf_inputs_metadata=None, + wf_species="human", + wf_template_id="MNI152NLin2009cAsym", +) -> [ + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", +]: + """ + Initialize the dMRI-QC workflow. + + .. workflow:: + + import os.path as op + from mriqc.workflows.diffusion.base import dmri_qc_workflow + from mriqc.testing import mock_config + with mock_config(): + wf = dmri_qc_workflow() + + """ + from pydra.tasks.afni.auto import Volreg + + if exec_work_dir is None: + exec_work_dir = Path.cwd() + + outputs_ = { + "iqms_wf_out_file": attrs.NOTHING, + "iqms_wf_noise_floor": attrs.NOTHING, + "dwi_report_wf_snr_report": attrs.NOTHING, + "dwi_report_wf_noise_report": attrs.NOTHING, + "dwi_report_wf_fa_report": attrs.NOTHING, + "dwi_report_wf_md_report": attrs.NOTHING, + "dwi_report_wf_heatmap_report": attrs.NOTHING, + "dwi_report_wf_spikes_report": attrs.NOTHING, + "dwi_report_wf_carpet_report": attrs.NOTHING, + "dwi_report_wf_bmask_report": attrs.NOTHING, + } + + from pydra.tasks.mrtrix3.v3_0 import DwiDenoise + from pydra.tasks.niworkflows.interfaces.header import SanitizeImage + from pydra.tasks.niworkflows.interfaces.images import RobustAverage + from pydra.tasks.mriqc.interfaces.diffusion import ( + CCSegmentation, + CorrectSignalDrift, + DiffusionModel, + ExtractOrientations, + NumberOfShells, + PIESNO, + ReadDWIMetadata, + SpikingVoxelsMask, + WeightedStat, + ) + + from pydra.tasks.mriqc.workflows.shared import synthstrip_wf as dmri_bmsk_workflow + + # Enable if necessary + # mem_gb = wf_biggest_file_gb['dwi'] + dataset = wf_inputs["dwi"] + metadata = wf_inputs_metadata["dwi"] + entities = wf_inputs_entities["dwi"] + message = "Building {modality} MRIQC workflow {detail}.".format( + modality="diffusion", + detail=f"for {len(dataset)} NIfTI files.", + ) + logger.info(message) + # Define workflow, inputs and outputs + # 0. Get data, put it in RAS orientation + + sanitize = workflow.add( + SanitizeImage(max_32bit=exec_float32, n_volumes_to_discard=0, in_file=in_file), + name="sanitize", + ) + # Workflow -------------------------------------------------------- + # Read metadata & bvec/bval, estimate number of shells, extract and split B0s + + shells = workflow.add(NumberOfShells(in_bvals=bvals), name="shells") + get_lowb = workflow.add( + ExtractOrientations(in_file=sanitize.out_file), name="get_lowb" + ) + # Generate B0 reference + dwi_ref = workflow.add( + RobustAverage(mc_method=None, in_file=sanitize.out_file), name="dwi_ref" + ) + hmc_b0 = workflow.add( + Volreg( + args="-Fourier -twopass", + outputtype="NIFTI_GZ", + zpad=4, + basefile=dwi_ref.out_file, + in_file=get_lowb.out_file, + ), + name="hmc_b0", + ) + # Calculate brainmask + dmri_bmsk = workflow.add( + dmri_bmsk_workflow( + omp_nthreads=nipype_omp_nthreads, + in_files=dwi_ref.out_file, + name="dmri_bmsk", + ) + ) + # HMC: head motion correct + hmcwf = workflow.add( + hmc_workflow(wf_fd_radius=wf_fd_radius, in_bvec=bvecs, name="hmcwf") + ) + get_hmc_shells = workflow.add( + ExtractOrientations( + in_bvec_file=bvecs, in_file=hmcwf.out_file, indices=shells.b_indices + ), + name="get_hmc_shells", + ) + # Split shells and compute some stats + averages = workflow.add(WeightedStat(in_weights=shells.b_masks), name="averages") + stddev = workflow.add( + WeightedStat(stat="std", in_weights=shells.b_masks), name="stddev" + ) + dwidenoise = workflow.add( + DwiDenoise( + noise="noisemap.nii.gz", + nthreads=nipype_omp_nthreads, + mask=dmri_bmsk.out_mask, + ), + name="dwidenoise", + ) + drift = workflow.add( + CorrectSignalDrift( + brainmask_file=dmri_bmsk.out_mask, + bval_file=bvals, + full_epi=sanitize.out_file, + in_file=hmc_b0.out_file, + ), + name="drift", + ) + sp_mask = workflow.add( + SpikingVoxelsMask( + b_masks=shells.b_masks, + brain_mask=dmri_bmsk.out_mask, + in_file=sanitize.out_file, + ), + name="sp_mask", + ) + # Fit DTI/DKI model + dwimodel = workflow.add( + DiffusionModel( + brain_mask=dmri_bmsk.out_mask, + bvals=shells.out_data, + bvec_file=bvecs, + in_file=dwidenoise.out_file, + n_shells=shells.n_shells, + ), + name="dwimodel", + ) + # Calculate CC mask + cc_mask = workflow.add( + CCSegmentation(in_cfa=dwimodel.out_cfa, in_fa=dwimodel.out_fa), name="cc_mask" + ) + # Run PIESNO noise estimation + piesno = workflow.add(PIESNO(in_file=sanitize.out_file), name="piesno") + # EPI to MNI registration + spatial_norm = workflow.add( + epi_mni_align( + nipype_omp_nthreads=nipype_omp_nthreads, + wf_species=wf_species, + wf_template_id=wf_template_id, + nipype_nprocs=nipype_nprocs, + exec_debug=exec_debug, + exec_ants_float=exec_ants_float, + epi_mean=dwi_ref.out_file, + epi_mask=dmri_bmsk.out_mask, + name="spatial_norm", + ) + ) + # Compute IQMs + iqms_wf = workflow.add( + compute_iqms( + in_file=in_file, + b_values_file=bvals, + qspace_neighbors=qspace_neighbors, + spikes_mask=sp_mask.out_mask, + piesno_sigma=piesno.sigma, + framewise_displacement=hmcwf.out_fd, + in_bvec_rotated=hmcwf.out_bvec, + in_bvec_diff=hmcwf.out_bvec_diff, + in_fa=dwimodel.out_fa, + in_cfa=dwimodel.out_cfa, + in_fa_nans=dwimodel.out_fa_nans, + in_fa_degenerate=dwimodel.out_fa_degenerate, + in_md=dwimodel.out_md, + brain_mask=dmri_bmsk.out_mask, + cc_mask=cc_mask.out_mask, + wm_mask=cc_mask.wm_finalmask, + b_values_shells=shells.b_values, + in_shells=get_hmc_shells.out_file, + in_bvec=get_hmc_shells.out_bvec, + in_noise=dwidenoise.noise, + name="iqms_wf", + ) + ) + # Generate outputs + dwi_report_wf = workflow.add( + init_dwi_report_wf( + wf_species=wf_species, + wf_biggest_file_gb=wf_biggest_file_gb, + exec_verbose_reports=exec_verbose_reports, + wf_fd_thres=wf_fd_thres, + exec_work_dir=exec_work_dir, + in_bdict=shells.b_dict, + brain_mask=dmri_bmsk.out_mask, + in_avgmap=averages.out_file, + in_stdmap=stddev.out_file, + in_epi=drift.out_full_file, + in_fa=dwimodel.out_fa, + in_md=dwimodel.out_md, + in_parcellation=spatial_norm.epi_parc, + name="dwi_report_wf", + ) + ) + # fmt: off + + @python.define + def shells_b_masks_to_dwi_ref_t_mask_callable(in_: ty.Any) -> ty.Any: + return _first(in_) + + shells_b_masks_to_dwi_ref_t_mask_callable = workflow.add(shells_b_masks_to_dwi_ref_t_mask_callable(in_=shells.b_masks), name="shells_b_masks_to_dwi_ref_t_mask_callable") + + dwi_ref.inputs.t_mask = shells_b_masks_to_dwi_ref_t_mask_callable.out + + @python.define + def shells_b_indices_to_get_lowb_indices_callable(in_: ty.Any) -> ty.Any: + return _first(in_) + + shells_b_indices_to_get_lowb_indices_callable = workflow.add(shells_b_indices_to_get_lowb_indices_callable(in_=shells.b_indices), name="shells_b_indices_to_get_lowb_indices_callable") + + get_lowb.inputs.indices = shells_b_indices_to_get_lowb_indices_callable.out + + @python.define + def shells_b_indices_to_drift_b0_ixs_callable(in_: ty.Any) -> ty.Any: + return _first(in_) + + shells_b_indices_to_drift_b0_ixs_callable = workflow.add(shells_b_indices_to_drift_b0_ixs_callable(in_=shells.b_indices), name="shells_b_indices_to_drift_b0_ixs_callable") + + drift.inputs.b0_ixs = shells_b_indices_to_drift_b0_ixs_callable.out + hmcwf.inputs.in_file = drift.out_full_file + averages.inputs.in_file = drift.out_full_file + stddev.inputs.in_file = drift.out_full_file + + @python.define + def averages_out_file_to_hmcwf_reference_callable(in_: ty.Any) -> ty.Any: + return _first(in_) + + averages_out_file_to_hmcwf_reference_callable = workflow.add(averages_out_file_to_hmcwf_reference_callable(in_=averages.out_file), name="averages_out_file_to_hmcwf_reference_callable") + + hmcwf.inputs.reference = averages_out_file_to_hmcwf_reference_callable.out + dwidenoise.inputs.in_file = drift.out_full_file + + @python.define + def averages_out_file_to_iqms_wf_in_b0_callable(in_: ty.Any) -> ty.Any: + return _first(in_) + + averages_out_file_to_iqms_wf_in_b0_callable = workflow.add(averages_out_file_to_iqms_wf_in_b0_callable(in_=averages.out_file), name="averages_out_file_to_iqms_wf_in_b0_callable") + + iqms_wf.inputs.in_b0 = averages_out_file_to_iqms_wf_in_b0_callable.out + # fmt: on + outputs_["iqms_wf_out_file"] = iqms_wf.out_file + outputs_["iqms_wf_noise_floor"] = iqms_wf.noise_floor + outputs_["dwi_report_wf_noise_report"] = dwi_report_wf.noise_report + outputs_["dwi_report_wf_md_report"] = dwi_report_wf.md_report + outputs_["dwi_report_wf_bmask_report"] = dwi_report_wf.bmask_report + outputs_["dwi_report_wf_snr_report"] = dwi_report_wf.snr_report + outputs_["dwi_report_wf_carpet_report"] = dwi_report_wf.carpet_report + outputs_["dwi_report_wf_fa_report"] = dwi_report_wf.fa_report + outputs_["dwi_report_wf_spikes_report"] = dwi_report_wf.spikes_report + outputs_["dwi_report_wf_heatmap_report"] = dwi_report_wf.heatmap_report + + return tuple(outputs_) + + +@workflow.define(outputs=["out_file", "out_fd", "out_bvec", "out_bvec_diff"]) +def hmc_workflow( + in_bvec: ty.Any = attrs.NOTHING, + in_file: ty.Any = attrs.NOTHING, + name="dMRI_HMC", + reference: ty.Any = attrs.NOTHING, + wf_fd_radius=50, +) -> ["ty.Any", "ty.Any", "ty.Any", "ty.Any"]: + """ + Create a :abbr:`HMC (head motion correction)` workflow for dMRI. + + .. workflow:: + + from mriqc.workflows.diffusion.base import hmc + from mriqc.testing import mock_config + with mock_config(): + wf = hmc() + + """ + from pydra.tasks.mriqc.nipype_ports.algorithms.confounds import ( + FramewiseDisplacement, + ) + + outputs_ = { + "out_file": attrs.NOTHING, + "out_fd": attrs.NOTHING, + "out_bvec": attrs.NOTHING, + "out_bvec_diff": attrs.NOTHING, + } + + from pydra.tasks.afni.auto import Volreg + from pydra.tasks.mriqc.interfaces.diffusion import RotateVectors + + # calculate hmc parameters + hmc = workflow.add( + Volreg( + args="-Fourier -twopass", + outputtype="NIFTI_GZ", + zpad=4, + basefile=reference, + in_file=in_file, + ), + name="hmc", + ) + bvec_rot = workflow.add( + RotateVectors( + in_file=in_bvec, reference=reference, transforms=hmc.oned_matrix_save + ), + name="bvec_rot", + ) + # Compute the frame-wise displacement + fdnode = workflow.add( + FramewiseDisplacement( + normalize=False, + parameter_source="AFNI", + radius=wf_fd_radius, + in_file=hmc.oned_file, + ), + name="fdnode", + ) + # fmt: off + outputs_['out_file'] = hmc.out_file + outputs_['out_fd'] = fdnode.out_file + outputs_['out_bvec'] = bvec_rot.out_bvec + outputs_['out_bvec_diff'] = bvec_rot.out_diff + # fmt: on + + return tuple(outputs_) + + +@workflow.define(outputs=["epi_parc", "epi_mni", "report"]) +def epi_mni_align( + epi_mask: ty.Any = attrs.NOTHING, + epi_mean: ty.Any = attrs.NOTHING, + exec_ants_float=False, + exec_debug=False, + name="SpatialNormalization", + nipype_nprocs=12, + nipype_omp_nthreads=12, + wf_species="human", + wf_template_id="MNI152NLin2009cAsym", +) -> ["ty.Any", "ty.Any", "ty.Any"]: + """ + Estimate the transform that maps the EPI space into MNI152NLin2009cAsym. + + The input epi_mean is the averaged and brain-masked EPI timeseries + + Returns the EPI mean resampled in MNI space (for checking out registration) and + the associated "lobe" parcellation in EPI space. + + .. workflow:: + + from mriqc.workflows.diffusion.base import epi_mni_align + from mriqc.testing import mock_config + with mock_config(): + wf = epi_mni_align() + + """ + from pydra.tasks.ants.auto import ApplyTransforms, N4BiasFieldCorrection + + outputs_ = { + "epi_parc": attrs.NOTHING, + "epi_mni": attrs.NOTHING, + "report": attrs.NOTHING, + } + + from pydra.tasks.niworkflows.interfaces.reportlets.registration import ( + SpatialNormalizationRPT as RobustMNINormalization, + ) + from templateflow.api import get as get_template + + # Get settings + testing = exec_debug + n_procs = nipype_nprocs + ants_nthreads = nipype_omp_nthreads + + n4itk = workflow.add( + N4BiasFieldCorrection(copy_header=True, dimension=3, input_image=epi_mean), + name="n4itk", + ) + norm = workflow.add( + RobustMNINormalization( + explicit_masking=False, + flavor="testing" if testing else "precise", + float=exec_ants_float, + generate_report=True, + moving="boldref", + num_threads=ants_nthreads, + reference="boldref", + template=wf_template_id, + moving_image=n4itk.output_image, + ), + name="norm", + ) + if wf_species.lower() == "human": + norm.inputs.inputs.reference_image = str( + get_template(wf_template_id, resolution=2, suffix="boldref") + ) + norm.inputs.inputs.reference_mask = str( + get_template( + wf_template_id, + resolution=2, + desc="brain", + suffix="mask", + ) + ) + # adapt some population-specific settings + else: + from nirodents.workflows.brainextraction import _bspline_grid + + n4itk.inputs.inputs.shrink_factor = 1 + n4itk.inputs.inputs.n_iterations = [50] * 4 + norm.inputs.inputs.reference_image = str( + get_template(wf_template_id, suffix="T2w") + ) + norm.inputs.inputs.reference_mask = str( + get_template( + wf_template_id, + desc="brain", + suffix="mask", + )[0] + ) + bspline_grid = workflow.add( + FunctionTask(func=_bspline_grid), name="bspline_grid" + ) + # fmt: off + bspline_grid.inputs.in_file = epi_mean + n4itk.inputs.args = bspline_grid.out + # fmt: on + # Warp segmentation into EPI space + invt = workflow.add( + ApplyTransforms( + default_value=0, + dimension=3, + float=True, + interpolation="MultiLabel", + reference_image=epi_mean, + transforms=norm.inverse_composite_transform, + ), + name="invt", + ) + if wf_species.lower() == "human": + invt.inputs.inputs.input_image = str( + get_template( + wf_template_id, + resolution=1, + desc="carpet", + suffix="dseg", + ) + ) + else: + invt.inputs.inputs.input_image = str( + get_template( + wf_template_id, + suffix="dseg", + )[-1] + ) + # fmt: off + outputs_['epi_parc'] = invt.output_image + outputs_['epi_mni'] = norm.warped_image + outputs_['report'] = norm.out_report + # fmt: on + if wf_species.lower() == "human": + norm.inputs.moving_mask = epi_mask + + return tuple(outputs_) + + +@workflow.define(outputs=["out_file", "noise_floor"]) +def compute_iqms( + b_values_file: ty.Any = attrs.NOTHING, + b_values_shells: ty.Any = attrs.NOTHING, + brain_mask: ty.Any = attrs.NOTHING, + cc_mask: ty.Any = attrs.NOTHING, + framewise_displacement: ty.Any = attrs.NOTHING, + in_b0: ty.Any = attrs.NOTHING, + in_bvec: ty.Any = attrs.NOTHING, + in_bvec_diff: ty.Any = attrs.NOTHING, + in_bvec_rotated: ty.Any = attrs.NOTHING, + in_cfa: ty.Any = attrs.NOTHING, + in_fa: ty.Any = attrs.NOTHING, + in_fa_degenerate: ty.Any = attrs.NOTHING, + in_fa_nans: ty.Any = attrs.NOTHING, + in_file: ty.Any = attrs.NOTHING, + in_md: ty.Any = attrs.NOTHING, + in_noise: ty.Any = attrs.NOTHING, + in_shells: ty.Any = attrs.NOTHING, + name="ComputeIQMs", + piesno_sigma: ty.Any = attrs.NOTHING, + qspace_neighbors: ty.Any = attrs.NOTHING, + spikes_mask: ty.Any = attrs.NOTHING, + wm_mask: ty.Any = attrs.NOTHING, +) -> ["ty.Any", "ty.Any"]: + """ + Initialize the workflow that actually computes the IQMs. + + .. workflow:: + + from mriqc.workflows.diffusion.base import compute_iqms + from mriqc.testing import mock_config + with mock_config(): + wf = compute_iqms() + + """ + from pydra.tasks.mriqc.interfaces import IQMFileSink + + outputs_ = { + "out_file": attrs.NOTHING, + "noise_floor": attrs.NOTHING, + } + + from pydra.tasks.mriqc.interfaces.diffusion import DiffusionQC + from pydra.tasks.mriqc.interfaces.reports import AddProvenance + + # from mriqc.workflows.utils import _tofloat, get_fwhmx + + estimate_sigma = workflow.add( + FunctionTask(func=_estimate_sigma, in_file=in_noise, mask=brain_mask), + name="estimate_sigma", + ) + measures = workflow.add( + DiffusionQC( + brain_mask=brain_mask, + cc_mask=cc_mask, + in_b0=in_b0, + in_bval_file=b_values_file, + in_bvec=in_bvec, + in_bvec_diff=in_bvec_diff, + in_bvec_rotated=in_bvec_rotated, + in_cfa=in_cfa, + in_fa=in_fa, + in_fa_degenerate=in_fa_degenerate, + in_fa_nans=in_fa_nans, + in_fd=framewise_displacement, + in_file=in_file, + in_md=in_md, + in_shells=in_shells, + in_shells_bval=b_values_shells, + piesno_sigma=piesno_sigma, + qspace_neighbors=qspace_neighbors, + spikes_mask=spikes_mask, + wm_mask=wm_mask, + ), + name="measures", + ) + + # Save to JSON file + + # fmt: off + + + + outputs_['out_file'] = measures.out_qc + outputs_['noise_floor'] = estimate_sigma.out + # fmt: on + + return tuple(outputs_) + + +def _bvals_report(in_file): + + import numpy as np + + bvals = [ + round(float(val), 2) for val in np.unique(np.round(np.loadtxt(in_file), 2)) + ] + if len(bvals) > 10: + return "Likely DSI" + return bvals + + +def _estimate_sigma(in_file, mask): + + import nibabel as nb + from numpy import median + + msk = nb.load(mask).get_fdata() > 0.5 + return round( + float(median(nb.load(in_file).get_fdata()[msk])), + 6, + ) + + +def _filter_metadata( + in_dict, + keys=( + "global", + "dcmmeta_affine", + "dcmmeta_reorient_transform", + "dcmmeta_shape", + "dcmmeta_slice_dim", + "dcmmeta_version", + "time", + ), +): + """Drop large and partially redundant objects generated by dcm2niix.""" + for key in keys: + in_dict.pop(key, None) + return in_dict + + +def _first(inlist): + + if isinstance(inlist, (list, tuple)): + return inlist[0] + return inlist diff --git a/pydra/tasks/mriqc/workflows/diffusion/output.py b/pydra/tasks/mriqc/workflows/diffusion/output.py new file mode 100644 index 0000000..59d5766 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/diffusion/output.py @@ -0,0 +1,162 @@ +import attrs +import logging +from pathlib import Path +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.diffusion.output import init_dwi_report_wf +from pydra.tasks.nireports.interfaces.dmri import DWIHeatmap +from pydra.tasks.nireports.interfaces.reporting.base import ( + SimpleBeforeAfterRPT as SimpleBeforeAfter, +) +import typing as ty + + +logger = logging.getLogger(__name__) + + +@workflow.define( + outputs=[ + "snr_report", + "noise_report", + "fa_report", + "md_report", + "heatmap_report", + "spikes_report", + "carpet_report", + "bmask_report", + ] +) +def init_dwi_report_wf( + brain_mask: ty.Any = attrs.NOTHING, + exec_verbose_reports=False, + exec_work_dir=None, + in_avgmap: ty.Any = attrs.NOTHING, + in_bdict: ty.Any = attrs.NOTHING, + in_epi: ty.Any = attrs.NOTHING, + in_fa: ty.Any = attrs.NOTHING, + in_md: ty.Any = attrs.NOTHING, + in_parcellation: ty.Any = attrs.NOTHING, + in_stdmap: ty.Any = attrs.NOTHING, + name="dwi_report_wf", + noise_floor: ty.Any = attrs.NOTHING, + wf_biggest_file_gb=1, + wf_fd_thres=0.2, + wf_species="human", +) -> ["ty.Any", "ty.Any", "ty.Any", "ty.Any", "ty.Any", "ty.Any", "ty.Any", "ty.Any"]: + """ + Write out individual reportlets. + + .. workflow:: + + from mriqc.workflows.diffusion.output import init_dwi_report_wf + from mriqc.testing import mock_config + with mock_config(): + wf = init_dwi_report_wf() + + """ + from pydra.tasks.nireports.interfaces import PlotMosaic + + if exec_work_dir is None: + exec_work_dir = Path.cwd() + + outputs_ = { + "snr_report": attrs.NOTHING, + "noise_report": attrs.NOTHING, + "fa_report": attrs.NOTHING, + "md_report": attrs.NOTHING, + "heatmap_report": attrs.NOTHING, + "spikes_report": attrs.NOTHING, + "carpet_report": attrs.NOTHING, + "bmask_report": attrs.NOTHING, + } + + # from mriqc.interfaces.reports import IndividualReport + verbose = exec_verbose_reports + mem_gb = wf_biggest_file_gb + reportlets_dir = exec_work_dir / "reportlets" + + # Set FD threshold + # inputnode.inputs.fd_thres = wf_fd_thres + mosaic_fa = workflow.add( + PlotMosaic(cmap="Greys_r", bbox_mask_file=brain_mask, in_file=in_fa), + name="mosaic_fa", + ) + mosaic_md = workflow.add( + PlotMosaic(cmap="Greys_r", bbox_mask_file=brain_mask, in_file=in_md), + name="mosaic_md", + ) + mosaic_snr = workflow.add( + SimpleBeforeAfter( + after_label="Standard Deviation", + before_label="Average", + dismiss_affine=True, + fixed_params={"cmap": "viridis"}, + moving_params={"cmap": "Greys_r"}, + after=in_stdmap, + before=in_avgmap, + wm_seg=brain_mask, + ), + name="mosaic_snr", + ) + mosaic_noise = workflow.add( + PlotMosaic(cmap="viridis_r", only_noise=True, in_file=in_avgmap), + name="mosaic_noise", + ) + if wf_species.lower() in ("rat", "mouse"): + mosaic_noise.inputs.inputs.view = ["coronal", "axial"] + mosaic_fa.inputs.inputs.view = ["coronal", "axial"] + mosaic_md.inputs.inputs.view = ["coronal", "axial"] + + def _gen_entity(inlist): + return ["00000"] + [f"{int(round(bval, 0)):05d}" for bval in inlist] + + # fmt: off + + + outputs_['snr_report'] = mosaic_snr.out_report + outputs_['noise_report'] = mosaic_noise.out_file + outputs_['fa_report'] = mosaic_fa.out_file + outputs_['md_report'] = mosaic_md.out_file + # fmt: on + get_wm = workflow.add( + FunctionTask(func=_get_wm, in_file=in_parcellation), name="get_wm" + ) + plot_heatmap = workflow.add( + DWIHeatmap( + scalarmap_label="Shell-wise Fractional Anisotropy (FA)", + b_indices=in_bdict, + in_file=in_epi, + mask_file=get_wm.out, + scalarmap=in_fa, + sigma=noise_floor, + ), + name="plot_heatmap", + ) + + # fmt: off + outputs_['heatmap_report'] = plot_heatmap.out_file + # fmt: on + + return tuple(outputs_) + + +def _get_wm(in_file, radius=2): + + from pathlib import Path + import nibabel as nb + import numpy as np + from pydra.tasks.mriqc.nipype_ports.utils.filemanip import fname_presuffix + from scipy import ndimage as ndi + from skimage.morphology import ball + + parc = nb.load(in_file) + hdr = parc.header.copy() + data = np.array(parc.dataobj, dtype=hdr.get_data_dtype()) + wm_mask = ndi.binary_erosion((data == 1) | (data == 2), ball(radius)) + hdr.set_data_dtype(np.uint8) + out_wm = fname_presuffix(in_file, suffix="wm", newpath=str(Path.cwd())) + parc.__class__( + wm_mask.astype(np.uint8), + parc.affine, + hdr, + ).to_filename(out_wm) + return out_wm diff --git a/pydra/tasks/mriqc/workflows/diffusion/tests/conftest.py b/pydra/tasks/mriqc/workflows/diffusion/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/workflows/diffusion/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_compute_iqms.py b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_compute_iqms.py new file mode 100644 index 0000000..38501b2 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_compute_iqms.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.diffusion.base import compute_iqms +import pytest + + +logger = logging.getLogger(__name__) + + +def test_compute_iqms_build(): + workflow = compute_iqms() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_compute_iqms_run(): + workflow = compute_iqms() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_dmri_qc_workflow.py b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_dmri_qc_workflow.py new file mode 100644 index 0000000..9c1be77 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_dmri_qc_workflow.py @@ -0,0 +1,22 @@ +from fileformats.medimage import Bval, Bvec +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.diffusion.base import dmri_qc_workflow +import pytest + + +logger = logging.getLogger(__name__) + + +def test_dmri_qc_workflow_build(): + workflow = dmri_qc_workflow() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_dmri_qc_workflow_run(): + workflow = dmri_qc_workflow(bvals=Bval.sample(), bvecs=Bvec.sample()) + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_epi_mni_align.py b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_epi_mni_align.py new file mode 100644 index 0000000..110d050 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_epi_mni_align.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.diffusion.base import epi_mni_align +import pytest + + +logger = logging.getLogger(__name__) + + +def test_epi_mni_align_build(): + workflow = epi_mni_align() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_epi_mni_align_run(): + workflow = epi_mni_align() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_hmc_workflow.py b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_hmc_workflow.py new file mode 100644 index 0000000..fca72de --- /dev/null +++ b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_base_hmc_workflow.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.diffusion.base import hmc_workflow +import pytest + + +logger = logging.getLogger(__name__) + + +def test_hmc_workflow_build(): + workflow = hmc_workflow() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_hmc_workflow_run(): + workflow = hmc_workflow() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_output_init_dwi_report_wf.py b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_output_init_dwi_report_wf.py new file mode 100644 index 0000000..83711c8 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/diffusion/tests/test_workflows_diffusion_output_init_dwi_report_wf.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.diffusion.output import init_dwi_report_wf +import pytest + + +logger = logging.getLogger(__name__) + + +def test_init_dwi_report_wf_build(): + workflow = init_dwi_report_wf() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_init_dwi_report_wf_run(): + workflow = init_dwi_report_wf() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/functional/__init__.py b/pydra/tasks/mriqc/workflows/functional/__init__.py new file mode 100644 index 0000000..81e4f91 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/__init__.py @@ -0,0 +1,2 @@ +from .base import compute_iqms, epi_mni_align, fmri_bmsk_workflow, fmri_qc_workflow, hmc +from .output import _carpet_parcellation, _get_tr, init_func_report_wf, spikes_mask diff --git a/pydra/tasks/mriqc/workflows/functional/base.py b/pydra/tasks/mriqc/workflows/functional/base.py new file mode 100644 index 0000000..89290b5 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/base.py @@ -0,0 +1,770 @@ +import attrs +import logging +from pydra.tasks.mriqc.workflows.functional.output import init_func_report_wf +from pydra.tasks.niworkflows.utils.connections import pop_file as _pop +from pathlib import Path +from pydra.compose import python, workflow +from pydra.tasks.niworkflows.utils.connections import pop_file as _pop +import typing as ty + + +logger = logging.getLogger(__name__) + + +@workflow.define(outputs=["out_file"]) +def fmri_bmsk_workflow( + in_file: ty.Any = attrs.NOTHING, name="fMRIBrainMask" +) -> ["ty.Any"]: + """ + Compute a brain mask for the input :abbr:`fMRI (functional MRI)` dataset. + + .. workflow:: + + from mriqc.workflows.functional.base import fmri_bmsk_workflow + from mriqc.testing import mock_config + with mock_config(): + wf = fmri_bmsk_workflow() + + + """ + from pydra.tasks.afni.auto import Automask + + outputs_ = { + "out_file": attrs.NOTHING, + } + + afni_msk = workflow.add( + Automask(outputtype="NIFTI_GZ", in_file=in_file), name="afni_msk" + ) + # Connect brain mask extraction + # fmt: off + outputs_['out_file'] = afni_msk.out_file + # fmt: on + + return tuple(outputs_) + + +@workflow.define(outputs=["epi_parc", "epi_mni", "report"]) +def epi_mni_align( + epi_mask: ty.Any = attrs.NOTHING, + epi_mean: ty.Any = attrs.NOTHING, + exec_ants_float=False, + exec_debug=False, + name="SpatialNormalization", + nipype_nprocs=12, + nipype_omp_nthreads=12, + wf_species="human", + wf_template_id="MNI152NLin2009cAsym", +) -> ["ty.Any", "ty.Any", "ty.Any"]: + """ + Estimate the transform that maps the EPI space into MNI152NLin2009cAsym. + + The input epi_mean is the averaged and brain-masked EPI timeseries + + Returns the EPI mean resampled in MNI space (for checking out registration) and + the associated "lobe" parcellation in EPI space. + + .. workflow:: + + from mriqc.workflows.functional.base import epi_mni_align + from mriqc.testing import mock_config + with mock_config(): + wf = epi_mni_align() + + """ + from pydra.tasks.ants.auto import ApplyTransforms, N4BiasFieldCorrection + + outputs_ = { + "epi_parc": attrs.NOTHING, + "epi_mni": attrs.NOTHING, + "report": attrs.NOTHING, + } + + from pydra.tasks.niworkflows.interfaces.reportlets.registration import ( + SpatialNormalizationRPT as RobustMNINormalization, + ) + from templateflow.api import get as get_template + + # Get settings + testing = exec_debug + n_procs = nipype_nprocs + ants_nthreads = nipype_omp_nthreads + + n4itk = workflow.add( + N4BiasFieldCorrection(copy_header=True, dimension=3, input_image=epi_mean), + name="n4itk", + ) + norm = workflow.add( + RobustMNINormalization( + explicit_masking=False, + flavor="testing" if testing else "precise", + float=exec_ants_float, + generate_report=True, + moving="boldref", + num_threads=ants_nthreads, + reference="boldref", + template=wf_template_id, + moving_image=n4itk.output_image, + ), + name="norm", + ) + if wf_species.lower() == "human": + norm.inputs.inputs.reference_image = str( + get_template(wf_template_id, resolution=2, suffix="boldref") + ) + norm.inputs.inputs.reference_mask = str( + get_template( + wf_template_id, + resolution=2, + desc="brain", + suffix="mask", + ) + ) + # adapt some population-specific settings + else: + from nirodents.workflows.brainextraction import _bspline_grid + + n4itk.inputs.inputs.shrink_factor = 1 + n4itk.inputs.inputs.n_iterations = [50] * 4 + norm.inputs.inputs.reference_image = str( + get_template(wf_template_id, suffix="T2w") + ) + norm.inputs.inputs.reference_mask = str( + get_template( + wf_template_id, + desc="brain", + suffix="mask", + )[0] + ) + bspline_grid = workflow.add( + FunctionTask(func=_bspline_grid), name="bspline_grid" + ) + # fmt: off + bspline_grid.inputs.in_file = epi_mean + n4itk.inputs.args = bspline_grid.out + # fmt: on + # Warp segmentation into EPI space + invt = workflow.add( + ApplyTransforms( + default_value=0, + dimension=3, + float=True, + interpolation="MultiLabel", + reference_image=epi_mean, + transforms=norm.inverse_composite_transform, + ), + name="invt", + ) + if wf_species.lower() == "human": + invt.inputs.inputs.input_image = str( + get_template( + wf_template_id, + resolution=1, + desc="carpet", + suffix="dseg", + ) + ) + else: + invt.inputs.inputs.input_image = str( + get_template( + wf_template_id, + suffix="dseg", + )[-1] + ) + # fmt: off + outputs_['epi_parc'] = invt.output_image + outputs_['epi_mni'] = norm.warped_image + outputs_['report'] = norm.out_report + # fmt: on + if wf_species.lower() == "human": + norm.inputs.moving_mask = epi_mask + + return tuple(outputs_) + + +@workflow.define(outputs=["out_file", "mpars", "out_fd"]) +def hmc( + fd_radius: ty.Any = attrs.NOTHING, + in_file: ty.Any = attrs.NOTHING, + name="fMRI_HMC", + omp_nthreads=None, + wf_biggest_file_gb=1, + wf_deoblique=False, + wf_despike=False, +) -> ["ty.Any", "ty.Any", "ty.Any"]: + """ + Create a :abbr:`HMC (head motion correction)` workflow for fMRI. + + .. workflow:: + + from mriqc.workflows.functional.base import hmc + from mriqc.testing import mock_config + with mock_config(): + wf = hmc() + + """ + from pydra.tasks.mriqc.nipype_ports.algorithms.confounds import ( + FramewiseDisplacement, + ) + + outputs_ = { + "out_file": attrs.NOTHING, + "mpars": attrs.NOTHING, + "out_fd": attrs.NOTHING, + } + + from pydra.tasks.afni.auto import Despike, Refit, Volreg + + mem_gb = wf_biggest_file_gb["bold"] + + # calculate hmc parameters + estimate_hm = workflow.add( + Volreg(args="-Fourier -twopass", outputtype="NIFTI_GZ", zpad=4), + name="estimate_hm", + ) + # Compute the frame-wise displacement + fdnode = workflow.add( + FramewiseDisplacement( + normalize=False, + parameter_source="AFNI", + in_file=estimate_hm.oned_file, + radius=fd_radius, + ), + name="fdnode", + ) + # Apply transforms to other echos + apply_hmc = workflow.add( + FunctionTask( + func=_apply_transforms, + input_spec=SpecInfo( + name="FunctionIn", + bases=(BaseSpec,), + fields=[ + ("in_file", ty.Any), + ("in_xfm", ty.Any), + ("max_concurrent", ty.Any), + ], + ), + in_xfm=estimate_hm.oned_matrix_save, + ), + name="apply_hmc", + ) + apply_hmc.inputs.inputs.max_concurrent = 4 + # fmt: off + outputs_['out_file'] = apply_hmc.out + outputs_['mpars'] = estimate_hm.oned_file + outputs_['out_fd'] = fdnode.out_file + # fmt: on + if not (wf_despike or wf_deoblique): + # fmt: off + estimate_hm.inputs.in_file = in_file + apply_hmc.inputs.in_file = in_file + # fmt: on + return workflow + # despiking, and deoblique + deoblique_node = workflow.add(Refit(deoblique=True), name="deoblique_node") + despike_node = workflow.add(Despike(outputtype="NIFTI_GZ"), name="despike_node") + if wf_despike and wf_deoblique: + # fmt: off + despike_node.inputs.in_file = in_file + deoblique_node.inputs.in_file = despike_node.out_file + + @python.define + def deoblique_node_out_file_to_estimate_hm_in_file_callable(in_: ty.Any) -> ty.Any: + return _pop(in_) + + deoblique_node_out_file_to_estimate_hm_in_file_callable = workflow.add(deoblique_node_out_file_to_estimate_hm_in_file_callable(in_=deoblique_node.out_file), name="deoblique_node_out_file_to_estimate_hm_in_file_callable") + + estimate_hm.inputs.in_file = deoblique_node_out_file_to_estimate_hm_in_file_callable.out + apply_hmc.inputs.in_file = deoblique_node.out_file + # fmt: on + elif wf_despike: + # fmt: off + despike_node.inputs.in_file = in_file + + @python.define + def despike_node_out_file_to_estimate_hm_in_file_callable(in_: ty.Any) -> ty.Any: + return _pop(in_) + + despike_node_out_file_to_estimate_hm_in_file_callable = workflow.add(despike_node_out_file_to_estimate_hm_in_file_callable(in_=despike_node.out_file), name="despike_node_out_file_to_estimate_hm_in_file_callable") + + estimate_hm.inputs.in_file = despike_node_out_file_to_estimate_hm_in_file_callable.out + apply_hmc.inputs.in_file = despike_node.out_file + # fmt: on + elif wf_deoblique: + # fmt: off + deoblique_node.inputs.in_file = in_file + + @python.define + def deoblique_node_out_file_to_estimate_hm_in_file_callable(in_: ty.Any) -> ty.Any: + return _pop(in_) + + deoblique_node_out_file_to_estimate_hm_in_file_callable = workflow.add(deoblique_node_out_file_to_estimate_hm_in_file_callable(in_=deoblique_node.out_file), name="deoblique_node_out_file_to_estimate_hm_in_file_callable") + + estimate_hm.inputs.in_file = deoblique_node_out_file_to_estimate_hm_in_file_callable.out + apply_hmc.inputs.in_file = deoblique_node.out_file + # fmt: on + else: + raise NotImplementedError + + return tuple(outputs_) + + +def _apply_transforms(in_file, in_xfm, max_concurrent): + + from pathlib import Path + from nitransforms.linear import load + from nitransforms.resampling import apply + from pydra.tasks.mriqc.utils.bids import derive_bids_fname + + realigned = apply( + load(in_xfm, fmt="afni", reference=in_file, moving=in_file), + in_file, + dtype_width=4, + serialize_nvols=2, + max_concurrent=max_concurrent, + mode="reflect", + ) + out_file = derive_bids_fname( + in_file, + entity="desc-realigned", + newpath=Path.cwd(), + absolute=True, + ) + realigned.to_filename(out_file) + return str(out_file) + + +@workflow.define( + outputs=["out_file", "spikes", "fft", "spikes_num", "outliers", "dvars"] +) +def compute_iqms( + brainmask: ty.Any = attrs.NOTHING, + epi_mean: ty.Any = attrs.NOTHING, + fd_thres: ty.Any = attrs.NOTHING, + hmc_epi: ty.Any = attrs.NOTHING, + hmc_fd: ty.Any = attrs.NOTHING, + in_ras: ty.Any = attrs.NOTHING, + in_tsnr: ty.Any = attrs.NOTHING, + name="ComputeIQMs", + wf_biggest_file_gb=1, + wf_fft_spikes_detector=False, +) -> ["ty.Any", "ty.Any", "ty.Any", "Integer", "ty.Any", "ty.Any"]: + """ + Initialize the workflow that actually computes the IQMs. + + .. workflow:: + + from mriqc.workflows.functional.base import compute_iqms + from mriqc.testing import mock_config + with mock_config(): + wf = compute_iqms() + + """ + from pydra.tasks.mriqc.nipype_ports.algorithms.confounds import ComputeDVARS + + outputs_ = { + "out_file": attrs.NOTHING, + "spikes": attrs.NOTHING, + "fft": attrs.NOTHING, + "spikes_num": attrs.NOTHING, + "outliers": attrs.NOTHING, + "dvars": attrs.NOTHING, + } + + from pydra.tasks.afni.auto import OutlierCount, QualityIndex + from pydra.tasks.mriqc.interfaces import ( + DerivativesDataSink, + FunctionalQC, + GatherTimeseries, + IQMFileSink, + ) + from pydra.tasks.mriqc.interfaces.reports import AddProvenance + from pydra.tasks.mriqc.interfaces.transitional import GCOR + from pydra.tasks.mriqc.workflows.utils import _tofloat, get_fwhmx + + mem_gb = wf_biggest_file_gb["bold"] + + # Set FD threshold + + # Compute DVARS + dvnode = workflow.add( + ComputeDVARS( + save_all=True, save_plot=False, in_file=hmc_epi, in_mask=brainmask + ), + name="dvnode", + ) + # AFNI quality measures + fwhm = workflow.add(fwhm_task, name="fwhm") + fwhm.inputs.inputs.acf = True # Only AFNI >= 16 + outliers = workflow.add( + OutlierCount( + fraction=True, out_file="outliers.out", in_file=hmc_epi, mask=brainmask + ), + name="outliers", + ) + + measures = workflow.add( + FunctionalQC( + fd_thres=fd_thres, + in_epi=epi_mean, + in_fd=hmc_fd, + in_hmc=hmc_epi, + in_mask=brainmask, + in_tsnr=in_tsnr, + ), + name="measures", + ) + + # fmt: off + outputs_['dvars'] = dvnode.out_all + + @python.define + def fwhm_fwhm_to_measures_in_fwhm_callable(in_: ty.Any) -> ty.Any: + return _tofloat(in_) + + fwhm_fwhm_to_measures_in_fwhm_callable = workflow.add(fwhm_fwhm_to_measures_in_fwhm_callable(in_=fwhm.fwhm), name="fwhm_fwhm_to_measures_in_fwhm_callable") + + measures.inputs.in_fwhm = fwhm_fwhm_to_measures_in_fwhm_callable.out + outputs_['outliers'] = outliers.out_file + # fmt: on + + # Save to JSON file + + # Save timeseries TSV file + + # fmt: off + + + outputs_['out_file'] = measures.out_qc + + # fmt: on + # FFT spikes finder + if True: # wf_fft_spikes_detector: - disabled to ensure all outputs are generated + from pydra.tasks.mriqc.workflows.utils import slice_wise_fft + + spikes_fft = workflow.add( + FunctionTask( + func=slice_wise_fft, + input_spec=SpecInfo( + name="FunctionIn", bases=(BaseSpec,), fields=[("in_file", ty.Any)] + ), + output_spec=SpecInfo( + name="FunctionOut", + bases=(BaseSpec,), + fields=[ + ("n_spikes", ty.Any), + ("out_spikes", ty.Any), + ("out_fft", ty.Any), + ], + ), + ), + name="spikes_fft", + ) + # fmt: off + spikes_fft.inputs.in_file = in_ras + outputs_['spikes'] = spikes_fft.out_spikes + outputs_['fft'] = spikes_fft.out_fft + outputs_['spikes_num'] = spikes_fft.n_spikes + # fmt: on + + return tuple(outputs_) + + +def _parse_tout(in_file): + + if isinstance(in_file, (list, tuple)): + return ( + [_parse_tout(f) for f in in_file] + if len(in_file) > 1 + else _parse_tout(in_file[0]) + ) + import numpy as np + + data = np.loadtxt(in_file) # pylint: disable=no-member + return data.mean() + + +def _parse_tqual(in_file): + + if isinstance(in_file, (list, tuple)): + return ( + [_parse_tqual(f) for f in in_file] + if len(in_file) > 1 + else _parse_tqual(in_file[0]) + ) + import numpy as np + + with open(in_file) as fin: + lines = fin.readlines() + return np.mean([float(line.strip()) for line in lines if not line.startswith("++")]) + + +@workflow.define( + outputs=[ + "ema_report", + "iqmswf_out_file", + "iqmswf_spikes", + "iqmswf_fft", + "iqmswf_spikes_num", + "iqmswf_outliers", + "iqmswf_dvars", + "func_report_wf_mean_report", + "func_report_wf_stdev_report", + "func_report_wf_background_report", + "func_report_wf_zoomed_report", + "func_report_wf_carpet_report", + "func_report_wf_spikes_report", + ] +) +def fmri_qc_workflow( + exec_ants_float=False, + exec_debug=False, + exec_float32=True, + exec_no_sub=False, + exec_verbose_reports=False, + exec_work_dir=None, + in_file: ty.Any = attrs.NOTHING, + metadata: ty.Any = attrs.NOTHING, + name="funcMRIQC", + nipype_nprocs=12, + nipype_omp_nthreads=12, + wf_biggest_file_gb=1, + wf_deoblique=False, + wf_despike=False, + wf_fd_radius=50, + wf_fft_spikes_detector=False, + wf_inputs=None, + wf_inputs_entities={}, + wf_inputs_metadata=None, + wf_species="human", + wf_template_id="MNI152NLin2009cAsym", +) -> [ + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", + "ty.Any", +]: + """ + Initialize the (f)MRIQC workflow. + + .. workflow:: + + import os.path as op + from mriqc.workflows.functional.base import fmri_qc_workflow + from mriqc.testing import mock_config + with mock_config(): + wf = fmri_qc_workflow() + + """ + from pydra.tasks.mriqc.nipype_ports.algorithms.confounds import ( + NonSteadyStateDetector, + TSNR, + ) + + if exec_work_dir is None: + exec_work_dir = Path.cwd() + + outputs_ = { + "ema_report": attrs.NOTHING, + "iqmswf_out_file": attrs.NOTHING, + "iqmswf_spikes": attrs.NOTHING, + "iqmswf_fft": attrs.NOTHING, + "iqmswf_spikes_num": attrs.NOTHING, + "iqmswf_outliers": attrs.NOTHING, + "iqmswf_dvars": attrs.NOTHING, + "func_report_wf_mean_report": attrs.NOTHING, + "func_report_wf_stdev_report": attrs.NOTHING, + "func_report_wf_background_report": attrs.NOTHING, + "func_report_wf_zoomed_report": attrs.NOTHING, + "func_report_wf_carpet_report": attrs.NOTHING, + "func_report_wf_spikes_report": attrs.NOTHING, + } + + from pydra.tasks.afni.auto import TStat + from pydra.tasks.niworkflows.interfaces.header import SanitizeImage + from pydra.tasks.mriqc.interfaces.functional import SelectEcho + + mem_gb = wf_biggest_file_gb["bold"] + dataset = wf_inputs["bold"] + metadata = wf_inputs_metadata["bold"] + entities = wf_inputs_entities["bold"] + message = "Building {modality} MRIQC workflow {detail}.".format( + modality="functional", + detail=f"for {len(dataset)} BOLD runs.", + ) + logger.info(message) + # Define workflow, inputs and outputs + # 0. Get data, put it in RAS orientation + + pick_echo = workflow.add( + SelectEcho(in_files=in_file, metadata=metadata), name="pick_echo" + ) + non_steady_state_detector = workflow.add( + NonSteadyStateDetector(in_file=pick_echo.out_file), + name="non_steady_state_detector", + ) + sanitize = workflow.add( + SanitizeImage( + max_32bit=exec_float32, + in_file=in_file, + n_volumes_to_discard=non_steady_state_detector.n_volumes_to_discard, + ), + name="sanitize", + ) + # Workflow -------------------------------------------------------- + # 1. HMC: head motion correct + hmcwf = workflow.add( + hmc( + omp_nthreads=nipype_omp_nthreads, + wf_biggest_file_gb=wf_biggest_file_gb, + wf_deoblique=wf_deoblique, + wf_despike=wf_despike, + in_file=sanitize.out_file, + name="hmcwf", + ) + ) + # Set HMC settings + hmcwf.inputs.inputs.inputnode.fd_radius = wf_fd_radius + # 2. Compute mean fmri + mean = workflow.add( + TStat(options="-mean", outputtype="NIFTI_GZ", in_file=hmcwf.out_file), + name="mean", + ) + # Compute TSNR using nipype implementation + tsnr = workflow.add(TSNR(in_file=hmcwf.out_file), name="tsnr") + # EPI to MNI registration + ema = workflow.add( + epi_mni_align( + nipype_omp_nthreads=nipype_omp_nthreads, + wf_species=wf_species, + wf_template_id=wf_template_id, + nipype_nprocs=nipype_nprocs, + exec_debug=exec_debug, + exec_ants_float=exec_ants_float, + name="ema", + ) + ) + # 7. Compute IQMs + iqmswf = workflow.add( + compute_iqms( + wf_fft_spikes_detector=wf_fft_spikes_detector, + wf_biggest_file_gb=wf_biggest_file_gb, + in_ras=sanitize.out_file, + epi_mean=mean.out_file, + hmc_epi=hmcwf.out_file, + hmc_fd=hmcwf.out_fd, + in_tsnr=tsnr.tsnr_file, + name="iqmswf", + ) + ) + # Reports + func_report_wf = workflow.add( + init_func_report_wf( + wf_fft_spikes_detector=wf_fft_spikes_detector, + wf_species=wf_species, + wf_biggest_file_gb=wf_biggest_file_gb, + exec_verbose_reports=exec_verbose_reports, + exec_work_dir=exec_work_dir, + meta_sidecar=metadata, + in_ras=sanitize.out_file, + epi_mean=mean.out_file, + in_stddev=tsnr.stddev_file, + hmc_fd=hmcwf.out_fd, + hmc_epi=hmcwf.out_file, + epi_parc=ema.epi_parc, + name="func_report_wf", + ) + ) + # fmt: off + + @python.define + def mean_out_file_to_ema_epi_mean_callable(in_: ty.Any) -> ty.Any: + return _pop(in_) + + mean_out_file_to_ema_epi_mean_callable = workflow.add(mean_out_file_to_ema_epi_mean_callable(in_=mean.out_file), name="mean_out_file_to_ema_epi_mean_callable") + + ema.inputs.epi_mean = mean_out_file_to_ema_epi_mean_callable.out + + # fmt: on + if wf_fft_spikes_detector: + # fmt: off + outputs_['iqmswf_spikes'] = iqmswf.spikes + outputs_['iqmswf_fft'] = iqmswf.fft + # fmt: on + # population specific changes to brain masking + if wf_species == "human": + from pydra.tasks.mriqc.workflows.shared import ( + synthstrip_wf as fmri_bmsk_workflow, + ) + + skullstrip_epi = workflow.add( + fmri_bmsk_workflow(omp_nthreads=nipype_omp_nthreads, name="skullstrip_epi") + ) + # fmt: off + + @python.define + def mean_out_file_to_skullstrip_epi_in_files_callable(in_: ty.Any) -> ty.Any: + return _pop(in_) + + mean_out_file_to_skullstrip_epi_in_files_callable = workflow.add(mean_out_file_to_skullstrip_epi_in_files_callable(in_=mean.out_file), name="mean_out_file_to_skullstrip_epi_in_files_callable") + + skullstrip_epi.inputs.in_files = mean_out_file_to_skullstrip_epi_in_files_callable.out + ema.inputs.epi_mask = skullstrip_epi.out_mask + iqmswf.inputs.brainmask = skullstrip_epi.out_mask + func_report_wf.inputs.brainmask = skullstrip_epi.out_mask + # fmt: on + else: + from pydra.tasks.mriqc.workflows.anatomical.base import _binarize + + binarise_labels = workflow.add( + FunctionTask( + func=_binarize, + input_spec=SpecInfo( + name="FunctionIn", + bases=(BaseSpec,), + fields=[("in_file", ty.Any), ("threshold", ty.Any)], + ), + output_spec=SpecInfo( + name="FunctionOut", bases=(BaseSpec,), fields=[("out_file", ty.Any)] + ), + ), + name="binarise_labels", + ) + # fmt: off + binarise_labels.inputs.in_file = ema.epi_parc + iqmswf.inputs.brainmask = binarise_labels.out_file + func_report_wf.inputs.brainmask = binarise_labels.out_file + # fmt: on + # Upload metrics + if not exec_no_sub: + from pydra.tasks.mriqc.interfaces.webapi import UploadIQMs + + pass + # fmt: off + outputs_['iqmswf_out_file'] = iqmswf.out_file + # fmt: on + outputs_["ema_report"] = ema.report + outputs_["iqmswf_spikes_num"] = iqmswf.spikes_num + outputs_["iqmswf_fft"] = iqmswf.fft + outputs_["iqmswf_dvars"] = iqmswf.dvars + outputs_["iqmswf_spikes"] = iqmswf.spikes + outputs_["iqmswf_out_file"] = iqmswf.out_file + outputs_["iqmswf_outliers"] = iqmswf.outliers + outputs_["func_report_wf_carpet_report"] = func_report_wf.carpet_report + outputs_["func_report_wf_zoomed_report"] = func_report_wf.zoomed_report + outputs_["func_report_wf_mean_report"] = func_report_wf.mean_report + outputs_["func_report_wf_spikes_report"] = func_report_wf.spikes_report + outputs_["func_report_wf_background_report"] = func_report_wf.background_report + outputs_["func_report_wf_stdev_report"] = func_report_wf.stdev_report + + return tuple(outputs_) diff --git a/pydra/tasks/mriqc/workflows/functional/output.py b/pydra/tasks/mriqc/workflows/functional/output.py new file mode 100644 index 0000000..dfc51d0 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/output.py @@ -0,0 +1,276 @@ +import attrs +import logging +from pathlib import Path +from pydra.compose import workflow +import typing as ty + + +logger = logging.getLogger(__name__) + + +@workflow.define( + outputs=[ + "mean_report", + "stdev_report", + "background_report", + "zoomed_report", + "carpet_report", + "spikes_report", + ] +) +def init_func_report_wf( + brainmask: ty.Any = attrs.NOTHING, + epi_mean: ty.Any = attrs.NOTHING, + epi_parc: ty.Any = attrs.NOTHING, + exec_verbose_reports=False, + exec_work_dir=None, + fd_thres: ty.Any = attrs.NOTHING, + hmc_epi: ty.Any = attrs.NOTHING, + hmc_fd: ty.Any = attrs.NOTHING, + in_dvars: ty.Any = attrs.NOTHING, + in_fft: ty.Any = attrs.NOTHING, + in_ras: ty.Any = attrs.NOTHING, + in_spikes: ty.Any = attrs.NOTHING, + in_stddev: ty.Any = attrs.NOTHING, + meta_sidecar: ty.Any = attrs.NOTHING, + name="func_report_wf", + outliers: ty.Any = attrs.NOTHING, + wf_biggest_file_gb=1, + wf_fft_spikes_detector=False, + wf_species="human", +) -> ["ty.Any", "ty.Any", "ty.Any", "ty.Any", "ty.Any", "ty.Any"]: + """ + Write out individual reportlets. + + .. workflow:: + + from mriqc.workflows.functional.output import init_func_report_wf + from mriqc.testing import mock_config + with mock_config(): + wf = init_func_report_wf() + + """ + from pydra.tasks.nireports.interfaces import FMRISummary, PlotMosaic, PlotSpikes + + if exec_work_dir is None: + exec_work_dir = Path.cwd() + + outputs_ = { + "mean_report": attrs.NOTHING, + "stdev_report": attrs.NOTHING, + "background_report": attrs.NOTHING, + "zoomed_report": attrs.NOTHING, + "carpet_report": attrs.NOTHING, + "spikes_report": attrs.NOTHING, + } + + from pydra.tasks.niworkflows.interfaces.morphology import ( + BinaryDilation, + BinarySubtraction, + ) + from pydra.tasks.mriqc.interfaces.functional import Spikes + + # from mriqc.interfaces.reports import IndividualReport + verbose = exec_verbose_reports + mem_gb = wf_biggest_file_gb["bold"] + reportlets_dir = exec_work_dir / "reportlets" + + # Set FD threshold + + spmask = workflow.add( + FunctionTask( + func=spikes_mask, + input_spec=SpecInfo( + name="FunctionIn", + bases=(BaseSpec,), + fields=[("in_file", ty.Any), ("in_mask", ty.Any)], + ), + output_spec=SpecInfo( + name="FunctionOut", + bases=(BaseSpec,), + fields=[("out_file", ty.Any), ("out_plot", ty.Any)], + ), + in_file=in_ras, + ), + name="spmask", + ) + spikes_bg = workflow.add( + Spikes(detrend=False, no_zscore=True, in_file=in_ras, in_mask=spmask.out_file), + name="spikes_bg", + ) + # Generate crown mask + # Create the crown mask + dilated_mask = workflow.add(BinaryDilation(in_mask=brainmask), name="dilated_mask") + subtract_mask = workflow.add( + BinarySubtraction(in_base=dilated_mask.out_mask, in_subtract=brainmask), + name="subtract_mask", + ) + parcels = workflow.add( + FunctionTask( + func=_carpet_parcellation, + crown_mask=subtract_mask.out_mask, + segmentation=epi_parc, + ), + name="parcels", + ) + bigplot = workflow.add( + FMRISummary( + dvars=in_dvars, + fd=hmc_fd, + fd_thres=fd_thres, + in_func=hmc_epi, + in_segm=parcels.out, + in_spikes_bg=spikes_bg.out_tsz, + outliers=outliers, + tr=meta_sidecar, + ), + name="bigplot", + ) + # fmt: off + bigplot.inputs.tr = meta_sidecar + # fmt: on + mosaic_mean = workflow.add( + PlotMosaic( + cmap="Greys_r", out_file="plot_func_mean_mosaic1.svg", in_file=epi_mean + ), + name="mosaic_mean", + ) + mosaic_stddev = workflow.add( + PlotMosaic( + cmap="viridis", + out_file="plot_func_stddev_mosaic2_stddev.svg", + in_file=in_stddev, + ), + name="mosaic_stddev", + ) + mosaic_zoom = workflow.add( + PlotMosaic(cmap="Greys_r", bbox_mask_file=brainmask, in_file=epi_mean), + name="mosaic_zoom", + ) + mosaic_noise = workflow.add( + PlotMosaic(cmap="viridis_r", only_noise=True, in_file=epi_mean), + name="mosaic_noise", + ) + if wf_species.lower() in ("rat", "mouse"): + mosaic_mean.inputs.inputs.view = ["coronal", "axial"] + mosaic_stddev.inputs.inputs.view = ["coronal", "axial"] + mosaic_zoom.inputs.inputs.view = ["coronal", "axial"] + mosaic_noise.inputs.inputs.view = ["coronal", "axial"] + + # fmt: off + outputs_['mean_report'] = mosaic_mean.out_file + outputs_['stdev_report'] = mosaic_stddev.out_file + outputs_['background_report'] = mosaic_noise.out_file + outputs_['zoomed_report'] = mosaic_zoom.out_file + outputs_['carpet_report'] = bigplot.out_file + # fmt: on + if True: # wf_fft_spikes_detector: - disabled so output is always created + mosaic_spikes = workflow.add( + PlotSpikes( + cmap="viridis", + out_file="plot_spikes.svg", + title="High-Frequency spikes", + ), + name="mosaic_spikes", + ) + pass + # fmt: off + pass + mosaic_spikes.inputs.in_file = in_ras + mosaic_spikes.inputs.in_spikes = in_spikes + mosaic_spikes.inputs.in_fft = in_fft + outputs_['spikes_report'] = mosaic_spikes.out_file + # fmt: on + if not verbose: + return workflow + # Verbose-reporting goes here + from pydra.tasks.nireports.interfaces import PlotContours + from pydra.tasks.niworkflows.utils.connections import pop_file as _pop + + # fmt: off + + # fmt: on + + return tuple(outputs_) + + +def _carpet_parcellation(segmentation, crown_mask): + """Generate the union of two masks.""" + from pathlib import Path + import nibabel as nb + import numpy as np + + img = nb.load(segmentation) + lut = np.zeros((256,), dtype="uint8") + lut[100:201] = 1 # Ctx GM + lut[30:99] = 2 # dGM + lut[1:11] = 3 # WM+CSF + lut[255] = 4 # Cerebellum + # Apply lookup table + seg = lut[np.asanyarray(img.dataobj, dtype="uint16")] + seg[np.asanyarray(nb.load(crown_mask).dataobj, dtype=int) > 0] = 5 + outimg = img.__class__(seg.astype("uint8"), img.affine, img.header) + outimg.set_data_dtype("uint8") + out_file = Path("segments.nii.gz").absolute() + outimg.to_filename(out_file) + return str(out_file) + + +def _get_tr(meta_dict): + + if isinstance(meta_dict, (list, tuple)): + meta_dict = meta_dict[0] + return meta_dict.get("RepetitionTime", None) + + +def spikes_mask(in_file, in_mask=None, out_file=None): + """Calculate a mask in which check for :abbr:`EM (electromagnetic)` spikes.""" + import os.path as op + import nibabel as nb + import numpy as np + from nilearn.image import mean_img + from nilearn.plotting import plot_roi + from scipy import ndimage as nd + + if out_file is None: + fname, ext = op.splitext(op.basename(in_file)) + if ext == ".gz": + fname, ext2 = op.splitext(fname) + ext = ext2 + ext + out_file = op.abspath(f"{fname}_spmask{ext}") + out_plot = op.abspath(f"{fname}_spmask.pdf") + in_4d_nii = nb.load(in_file) + orientation = nb.aff2axcodes(in_4d_nii.affine) + if in_mask: + mask_data = np.asanyarray(nb.load(in_mask).dataobj) + a = np.where(mask_data != 0) + bbox = ( + np.max(a[0]) - np.min(a[0]), + np.max(a[1]) - np.min(a[1]), + np.max(a[2]) - np.min(a[2]), + ) + longest_axis = np.argmax(bbox) + # Input here is a binarized and intersected mask data from previous section + dil_mask = nd.binary_dilation( + mask_data, iterations=int(mask_data.shape[longest_axis] / 9) + ) + rep = list(mask_data.shape) + rep[longest_axis] = -1 + new_mask_2d = dil_mask.max(axis=longest_axis).reshape(rep) + rep = [1, 1, 1] + rep[longest_axis] = mask_data.shape[longest_axis] + new_mask_3d = np.logical_not(np.tile(new_mask_2d, rep)) + else: + new_mask_3d = np.zeros(in_4d_nii.shape[:3]) == 1 + if orientation[0] in ("L", "R"): + new_mask_3d[0:2, :, :] = True + new_mask_3d[-3:-1, :, :] = True + else: + new_mask_3d[:, 0:2, :] = True + new_mask_3d[:, -3:-1, :] = True + mask_nii = nb.Nifti1Image( + new_mask_3d.astype(np.uint8), in_4d_nii.affine, in_4d_nii.header + ) + mask_nii.to_filename(out_file) + plot_roi(mask_nii, mean_img(in_4d_nii), output_file=out_plot) + return out_file, out_plot diff --git a/pydra/tasks/mriqc/workflows/functional/tests/conftest.py b/pydra/tasks/mriqc/workflows/functional/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_compute_iqms.py b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_compute_iqms.py new file mode 100644 index 0000000..10c63c3 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_compute_iqms.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.functional.base import compute_iqms +import pytest + + +logger = logging.getLogger(__name__) + + +def test_compute_iqms_build(): + workflow = compute_iqms() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_compute_iqms_run(): + workflow = compute_iqms() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_epi_mni_align.py b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_epi_mni_align.py new file mode 100644 index 0000000..08e538a --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_epi_mni_align.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.functional.base import epi_mni_align +import pytest + + +logger = logging.getLogger(__name__) + + +def test_epi_mni_align_build(): + workflow = epi_mni_align() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_epi_mni_align_run(): + workflow = epi_mni_align() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_fmri_bmsk_workflow.py b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_fmri_bmsk_workflow.py new file mode 100644 index 0000000..e77c84b --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_fmri_bmsk_workflow.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.functional.base import fmri_bmsk_workflow +import pytest + + +logger = logging.getLogger(__name__) + + +def test_fmri_bmsk_workflow_build(): + workflow = fmri_bmsk_workflow() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_fmri_bmsk_workflow_run(): + workflow = fmri_bmsk_workflow() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_fmri_qc_workflow.py b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_fmri_qc_workflow.py new file mode 100644 index 0000000..13211d7 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_fmri_qc_workflow.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.functional.base import fmri_qc_workflow +import pytest + + +logger = logging.getLogger(__name__) + + +def test_fmri_qc_workflow_build(): + workflow = fmri_qc_workflow() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_fmri_qc_workflow_run(): + workflow = fmri_qc_workflow() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_hmc.py b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_hmc.py new file mode 100644 index 0000000..bff3d78 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_base_hmc.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.functional.base import hmc +import pytest + + +logger = logging.getLogger(__name__) + + +def test_hmc_build(): + workflow = hmc() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_hmc_run(): + workflow = hmc() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_output_init_func_report_wf.py b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_output_init_func_report_wf.py new file mode 100644 index 0000000..9b0b756 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/functional/tests/test_workflows_functional_output_init_func_report_wf.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.functional.output import init_func_report_wf +import pytest + + +logger = logging.getLogger(__name__) + + +def test_init_func_report_wf_build(): + workflow = init_func_report_wf() + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_init_func_report_wf_run(): + workflow = init_func_report_wf() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/shared.py b/pydra/tasks/mriqc/workflows/shared.py new file mode 100644 index 0000000..c8e199c --- /dev/null +++ b/pydra/tasks/mriqc/workflows/shared.py @@ -0,0 +1,68 @@ +import attrs +import logging +from pydra.compose import workflow +import typing as ty + + +logger = logging.getLogger(__name__) + + +@workflow.define(outputs=["out_brain", "bias_image", "out_mask", "out_corrected"]) +def synthstrip_wf( + in_files: ty.Any = attrs.NOTHING, name="synthstrip_wf", omp_nthreads=None +) -> ["ty.Any", "ty.Any", "ty.Any", "ty.Any"]: + """Create a brain-extraction workflow using SynthStrip.""" + from pydra.tasks.ants.auto import N4BiasFieldCorrection + + outputs_ = { + "out_brain": attrs.NOTHING, + "bias_image": attrs.NOTHING, + "out_mask": attrs.NOTHING, + "out_corrected": attrs.NOTHING, + } + + from pydra.tasks.niworkflows.interfaces.nibabel import ApplyMask, IntensityClip + from pydra.tasks.mriqc.interfaces.synthstrip import SynthStrip + + # truncate target intensity for N4 correction + pre_clip = workflow.add( + IntensityClip(p_max=99.9, p_min=10, in_file=in_files), name="pre_clip" + ) + pre_n4 = workflow.add( + N4BiasFieldCorrection( + copy_header=True, + dimension=3, + num_threads=omp_nthreads, + rescale_intensities=True, + input_image=pre_clip.out_file, + ), + name="pre_n4", + ) + post_n4 = workflow.add( + N4BiasFieldCorrection( + copy_header=True, + dimension=3, + n_iterations=[50] * 4, + num_threads=omp_nthreads, + bias_image=True, + input_image=pre_clip.out_file, + ), + name="post_n4", + ) + synthstrip = workflow.add( + SynthStrip(num_threads=omp_nthreads, in_file=pre_n4.output_image), + name="synthstrip", + ) + final_masked = workflow.add( + ApplyMask(in_file=post_n4.output_image, in_mask=synthstrip.out_mask), + name="final_masked", + ) + # fmt: off + post_n4.inputs.weight_image = synthstrip.out_mask + outputs_['out_brain'] = final_masked.out_file + outputs_['bias_image'] = post_n4.bias_image + outputs_['out_mask'] = synthstrip.out_mask + outputs_['out_corrected'] = post_n4.output_image + # fmt: on + + return tuple(outputs_) diff --git a/pydra/tasks/mriqc/workflows/tests/conftest.py b/pydra/tasks/mriqc/workflows/tests/conftest.py new file mode 100644 index 0000000..751042d --- /dev/null +++ b/pydra/tasks/mriqc/workflows/tests/conftest.py @@ -0,0 +1,25 @@ + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +import os +import pytest + + +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value # raise internal errors instead of capturing them + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value # raise internal errors instead of capturing them + + def pytest_configure(config): + config.option.capture = 'no' # allow print statements to show up in the console + config.option.log_cli = True # show log messages in the console + config.option.log_level = "INFO" # set the log level to INFO + + CATCH_CLI_EXCEPTIONS = False +else: + CATCH_CLI_EXCEPTIONS = True diff --git a/pydra/tasks/mriqc/workflows/tests/test_workflows_shared_synthstrip_wf.py b/pydra/tasks/mriqc/workflows/tests/test_workflows_shared_synthstrip_wf.py new file mode 100644 index 0000000..6e5b5c0 --- /dev/null +++ b/pydra/tasks/mriqc/workflows/tests/test_workflows_shared_synthstrip_wf.py @@ -0,0 +1,21 @@ +import logging +from pydra.compose import workflow +from pydra.tasks.mriqc.workflows.shared import synthstrip_wf +import pytest + + +logger = logging.getLogger(__name__) + + +def test_synthstrip_wf_build(): + workflow = synthstrip_wf(omp_nthreads=1) + assert isinstance(workflow, Workflow) + + +@pytest.mark.skip( + reason="Appropriate inputs for this workflow haven't been specified yet" +) +def test_synthstrip_wf_run(): + workflow = synthstrip_wf() + result = workflow(worker="debug") + print(result.out) diff --git a/pydra/tasks/mriqc/workflows/utils.py b/pydra/tasks/mriqc/workflows/utils.py new file mode 100644 index 0000000..193cb2f --- /dev/null +++ b/pydra/tasks/mriqc/workflows/utils.py @@ -0,0 +1,176 @@ +import logging +from pathlib import Path + + +logger = logging.getLogger(__name__) + + +def _tofloat(inlist): + + if isinstance(inlist, (list, tuple)): + return ( + [_tofloat(el) for el in inlist] if len(inlist) > 1 else _tofloat(inlist[0]) + ) + return float(inlist) + + +def generate_filename(in_file, dirname=None, suffix="", extension=None): + """ + Generate a nipype-like filename. + + >>> str(generate_filename("/path/to/input.nii.gz").relative_to(Path.cwd())) + 'input.nii.gz' + + >>> str(generate_filename( + ... "/path/to/input.nii.gz", dirname="/other/path", + ... )) + '/other/path/input.nii.gz' + + >>> str(generate_filename( + ... "/path/to/input.nii.gz", dirname="/other/path", extension="tsv", + ... )) + '/other/path/input.tsv' + + >>> str(generate_filename( + ... "/path/to/input.nii.gz", dirname="/other/path", extension=".tsv", + ... )) + '/other/path/input.tsv' + + >>> str(generate_filename( + ... "/path/to/input.nii.gz", dirname="/other/path", extension="", + ... )) + '/other/path/input' + + >>> str(generate_filename( + ... "/path/to/input.nii.gz", dirname="/other/path", extension="", suffix="_mod", + ... )) + '/other/path/input_mod' + + >>> str(generate_filename( + ... "/path/to/input.nii.gz", dirname="/other/path", extension="", suffix="mod", + ... )) + '/other/path/input_mod' + + >>> str(generate_filename( + ... "/path/to/input", dirname="/other/path", extension="tsv", suffix="mod", + ... )) + '/other/path/input_mod.tsv' + + """ + from pathlib import Path + + in_file = Path(in_file) + in_ext = "".join(in_file.suffixes) + dirname = Path.cwd() if dirname is None else Path(dirname) + if extension is not None: + extension = ( + extension if not extension or extension.startswith(".") else f".{extension}" + ) + else: + extension = in_ext + stem = in_file.name[: -len(in_ext)] if in_ext else in_file.name + if suffix and not suffix.startswith("_"): + suffix = f"_{suffix}" + return dirname / f"{stem}{suffix}{extension}" + + +def get_fwhmx(): + + from pydra.tasks.afni.auto import FWHMx, Info + + fwhm_args = {"combine": True, "detrend": True} + afni_version = Info.version() + if afni_version and afni_version >= (2017, 2, 3): + fwhm_args["args"] = "-ShowMeClassicFWHM" + fwhm_interface = FWHMx(**fwhm_args) + return fwhm_interface + + +def slice_wise_fft(in_file, ftmask=None, spike_thres=3.0, out_prefix=None): + """Search for spikes in slices using the 2D FFT""" + import os.path as op + import nibabel as nb + import numpy as np + from scipy.ndimage import binary_erosion, generate_binary_structure + from scipy.ndimage.filters import median_filter + from statsmodels.robust.scale import mad + from pydra.tasks.mriqc.workflows.utils import spectrum_mask + + if out_prefix is None: + fname, ext = op.splitext(op.basename(in_file)) + if ext == ".gz": + fname, _ = op.splitext(fname) + out_prefix = op.abspath(fname) + func_data = nb.load(in_file).get_fdata() + if ftmask is None: + ftmask = spectrum_mask(tuple(func_data.shape[:2])) + fft_data = [] + for t in range(func_data.shape[-1]): + func_frame = func_data[..., t] + fft_slices = [] + for z in range(func_frame.shape[2]): + sl = func_frame[..., z] + fftsl = ( + median_filter( + np.real(np.fft.fft2(sl)).astype(np.float32), + size=(5, 5), + mode="constant", + ) + * ftmask + ) + fft_slices.append(fftsl) + fft_data.append(np.stack(fft_slices, axis=-1)) + # Recompose the 4D FFT timeseries + fft_data = np.stack(fft_data, -1) + # Z-score across t, using robust statistics + mu = np.median(fft_data, axis=3) + sigma = np.stack([mad(fft_data, axis=3)] * fft_data.shape[-1], -1) + idxs = np.where(np.abs(sigma) > 1e-4) + fft_zscored = fft_data - mu[..., np.newaxis] + fft_zscored[idxs] /= sigma[idxs] + # save fft z-scored + out_fft = op.abspath(out_prefix + "_zsfft.nii.gz") + nii = nb.Nifti1Image(fft_zscored.astype(np.float32), np.eye(4), None) + nii.to_filename(out_fft) + # Find peaks + spikes_list = [] + for t in range(fft_zscored.shape[-1]): + fft_frame = fft_zscored[..., t] + for z in range(fft_frame.shape[-1]): + sl = fft_frame[..., z] + if np.all(sl < spike_thres): + continue + # Any zscore over spike_thres will be called a spike + sl[sl <= spike_thres] = 0 + sl[sl > 0] = 1 + # Erode peaks and see how many survive + struct = generate_binary_structure(2, 2) + sl = binary_erosion(sl.astype(np.uint8), structure=struct).astype(np.uint8) + if sl.sum() > 10: + spikes_list.append((t, z)) + out_spikes = op.abspath(out_prefix + "_spikes.tsv") + np.savetxt(out_spikes, spikes_list, fmt=b"%d", delimiter=b"\t", header="TR\tZ") + return len(spikes_list), out_spikes, out_fft + + +def spectrum_mask(size): + """Creates a mask to filter the image of size size""" + import numpy as np + from scipy.ndimage.morphology import distance_transform_edt as distance + + ftmask = np.ones(size) + # Set zeros on corners + # ftmask[0, 0] = 0 + # ftmask[size[0] - 1, size[1] - 1] = 0 + # ftmask[0, size[1] - 1] = 0 + # ftmask[size[0] - 1, 0] = 0 + ftmask[size[0] // 2, size[1] // 2] = 0 + # Distance transform + ftmask = distance(ftmask) + ftmask /= ftmask.max() + # Keep this just in case we want to switch to the opposite filter + ftmask *= -1.0 + ftmask += 1.0 + ftmask[ftmask >= 0.4] = 1 + ftmask[ftmask < 1] = 0 + return ftmask diff --git a/pyproject.toml b/pyproject.toml index 32e2c29..31fd154 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [project] -name = "pydra-mriqc" +name = "pydra-tasks-mriqc" description = "Pydra tasks package for mriqc" readme = "README.rst" requires-python = ">=3.10" diff --git a/test_scripts/orig-affinie-init.txt b/test_scripts/orig-affinie-init.txt new file mode 100644 index 0000000..43dfcea --- /dev/null +++ b/test_scripts/orig-affinie-init.txt @@ -0,0 +1,9 @@ +antsAffineInitializer +3 +/Users/tclose/git/workflows/mriqc/work/mriqc_wf/anatMRIQC/SpatialNormalization/_in_file_..Users..tclose..Data..openneuro..ds000114..sub-03..ses-retest..anat..sub-03_ses-retest_T1w.nii.gz/SpatialNormalization/fixed_masked.nii.gz +/Users/tclose/git/workflows/mriqc/work/mriqc_wf/anatMRIQC/SpatialNormalization/_in_file_..Users..tclose..Data..openneuro..ds000114..sub-03..ses-retest..anat..sub-03_ses-retest_T1w.nii.gz/SpatialNormalization/moving_masked.nii.gz +transform.mat +15.000000 +0.100000 +0 +10 \ No newline at end of file diff --git a/test_scripts/orig-mriqc-registration-cmd.txt b/test_scripts/orig-mriqc-registration-cmd.txt new file mode 100644 index 0000000..bee114a --- /dev/null +++ b/test_scripts/orig-mriqc-registration-cmd.txt @@ -0,0 +1,79 @@ +antsRegistration +--dimensionality +3 +--float +0 +--initial-moving-transform +[ +/Users/tclose/git/workflows/mriqc/work/mriqc_wf/anatMRIQC/SpatialNormalization/_in_file_..Users..tclose..Data..openneuro..ds000114..sub-02..ses-test..anat..sub-02_ses-test_T1w.nii.gz/SpatialNormalization/transform.mat, +0 +] +--initialize-transforms-per-stage +0 +--interpolation +LanczosWindowedSinc +--output +[ +ants_t1_to_mni, +ants_t1_to_mni_Warped.nii.gz +] +--transform +Rigid[ +1.0 +] +--metric +Mattes[ +/Users/tclose/git/workflows/mriqc/work/mriqc_wf/anatMRIQC/SpatialNormalization/_in_file_..Users..tclose..Data..openneuro..ds000114..sub-02..ses-test..anat..sub-02_ses-test_T1w.nii.gz/SpatialNormalization/fixed_masked.nii.gz, +/Users/tclose/git/workflows/mriqc/work/mriqc_wf/anatMRIQC/SpatialNormalization/_in_file_..Users..tclose..Data..openneuro..ds000114..sub-02..ses-test..anat..sub-02_ses-test_T1w.nii.gz/SpatialNormalization/moving_masked.nii.gz, +1, +56, +Random, +0.2 +] +--convergence +[ +20, +1e-07, +15 +] +--smoothing-sigmas +4.0vox +--shrink-factors +2 +--use-histogram-matching +0 +--transform +Affine[ +1.0 +] +--metric +Mattes[ +/Users/tclose/git/workflows/mriqc/work/mriqc_wf/anatMRIQC/SpatialNormalization/_in_file_..Users..tclose..Data..openneuro..ds000114..sub-02..ses-test..anat..sub-02_ses-test_T1w.nii.gz/SpatialNormalization/fixed_masked.nii.gz, +/Users/tclose/git/workflows/mriqc/work/mriqc_wf/anatMRIQC/SpatialNormalization/_in_file_..Users..tclose..Data..openneuro..ds000114..sub-02..ses-test..anat..sub-02_ses-test_T1w.nii.gz/SpatialNormalization/moving_masked.nii.gz, +1, +56, +Random, +0.1 +] +--convergence +[ +15, +1e-08, +5 +] +--smoothing-sigmas +2.0vox +--shrink-factors +1 +--use-histogram-matching +1 +--winsorize-image-intensities +[ +0.005, +0.995 +] + +--write-composite-transform +1 +--collapse-output-transforms +1 diff --git a/test_scripts/pydra-affine-init.txt b/test_scripts/pydra-affine-init.txt new file mode 100644 index 0000000..b967dd5 --- /dev/null +++ b/test_scripts/pydra-affine-init.txt @@ -0,0 +1,9 @@ +antsAffineInitializer +3 +/Users/tclose/Data/pydra-mriqc-test-cache/FunctionTask_9e6afe7c28f8064822f15f48c4fc3aa8/fixed_masked.nii.gz +/Users/tclose/Data/pydra-mriqc-test-cache/FunctionTask_9e6afe7c28f8064822f15f48c4fc3aa8/moving_masked.nii.gz +transform.mat +15.0 +0.1 +False +10 \ No newline at end of file diff --git a/test_scripts/pydra-mriqc-registration-cmd.txt b/test_scripts/pydra-mriqc-registration-cmd.txt new file mode 100644 index 0000000..c4f3488 --- /dev/null +++ b/test_scripts/pydra-mriqc-registration-cmd.txt @@ -0,0 +1,77 @@ +antsRegistration +--dimensionality +3 +--initial-moving-transform +[ +/private/var/folders/mz/yn83q2fd3s758w1j75d2nnw80000gn/T/tmpl8nw1lip/AffineInitializer_2208bf8e3d355eaa5ae846eb0cfb6c1b/transform.mat, +0 +] +--interpolation +LanczosWindowedSinc +--output +[ +ants_t1_to_mni, +ants_t1_to_mni_Warped.nii.gz +] +--transform +Rigid[ +1.0 +] +--metric +Mattes[ +/Users/tclose/Data/pydra-mriqc-test-cache/FunctionTask_c9201346e6b94a8d35e4d8c1b43adb53/fixed_masked.nii.gz, +/Users/tclose/Data/pydra-mriqc-test-cache/FunctionTask_c9201346e6b94a8d35e4d8c1b43adb53/moving_masked.nii.gz, +1, +56, +Random, +0.2 +] +--convergence +[ +20, +1e-07, +15 +] +--smoothing-sigmas +4vox +--shrink-factors +2 +--use-histogram-matching +0 +--transform +Affine[ +1.0 +] +--metric +a[ +/Users/tclose/Data/pydra-mriqc-test-cache/FunctionTask_c9201346e6b94a8d35e4d8c1b43adb53/fixed_masked.nii.gz, +/Users/tclose/Data/pydra-mriqc-test-cache/FunctionTask_c9201346e6b94a8d35e4d8c1b43adb53/moving_masked.nii.gz, +1, +56, +Random, +0.1 +] +--convergence +[ +15, +1e-08, +5 +] +--smoothing-sigmas +2vox +--shrink-factors +1 +--use-histogram-matching +1 +--winsorize-image-intensities +[ +0.005, +0.995 +] +--winsorize-image-intensities +[ +0.005, +0.995 +] +--write-composite-transform +--collapse-output-transforms diff --git a/test_scripts/run_anat_wf.py b/test_scripts/run_anat_wf.py index e6548f2..bde3b2b 100644 --- a/test_scripts/run_anat_wf.py +++ b/test_scripts/run_anat_wf.py @@ -1,5 +1,6 @@ from fileformats.medimage import NiftiGzX, T1Weighted import logging +import tempfile from pathlib import Path from pydra.tasks.mriqc.workflows.anatomical.base import anat_qc_workflow @@ -12,7 +13,13 @@ pydra_logger.addHandler(file_handler) pydra_logger.addHandler(logging.StreamHandler()) -workflow = anat_qc_workflow(in_file=NiftiGzX[T1Weighted].sample(), modality="T1w") +in_file = NiftiGzX[T1Weighted].sample() + +tmp_dir = Path(tempfile.mkdtemp()) + +in_file = in_file.copy(tmp_dir, new_stem="sub-01_T1w") + +workflow = anat_qc_workflow(in_file=in_file, modality="T1w") workflow.cache_dir = "/Users/tclose/Data/pydra-mriqc-test-cache" result = workflow(plugin="serial") print(result.out)