Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,5 @@ Information on specific functions, classes, and methods.
api/niworkflows.engine
api/niworkflows.func
api/niworkflows.interfaces
api/niworkflows.reports
api/niworkflows.utils
api/niworkflows.viz
api/niworkflows.workflows
12 changes: 11 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,17 @@

apidoc_module_dir = '../niworkflows'
apidoc_output_dir = 'api'
apidoc_excluded_paths = ['conftest.py', '*/tests/*', 'tests/*', 'testing.py']
apidoc_excluded_paths = [
'conftest.py',
'*/tests/*',
'interfaces/plotting.py',
'interfaces/reportlets/masks.py',
'interfaces/reportlets/segmentation.py',
'reports/*',
'tests/*',
'testing.py',
'viz/*',
]
apidoc_separate_modules = True
apidoc_extra_args = ['--module-first', '-d 1', '-T']

Expand Down
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ sphinx <8.2
sphinxcontrib-apidoc
templateflow
nitransforms
nireports
bidsschematools!=1.1.4
33 changes: 33 additions & 0 deletions niworkflows/_deprecated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
"""Helpers for hard deprecations."""

from collections.abc import Mapping, Sequence


class DeprecationError(ImportError):
"""Raised when a niworkflows API has moved to NiReports."""


def _format_replacements(replacements):
if isinstance(replacements, str):
return [replacements]
if isinstance(replacements, Mapping):
return [f'{name} -> {target}' for name, target in replacements.items()]
if isinstance(replacements, Sequence):
return list(replacements)

raise TypeError('replacements must be a string, mapping, or sequence')


def moved_to_nireports_message(module, replacements):
lines = [
f'{module} has been removed from niworkflows and moved to nireports.',
'Import from one of the following locations instead:',
]
lines.extend(f' - {replacement}' for replacement in _format_replacements(replacements))
return '\n'.join(lines)


def raise_moved_to_nireports(module, replacements):
raise DeprecationError(moved_to_nireports_message(module, replacements))
9 changes: 9 additions & 0 deletions niworkflows/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
# disable ET
os.environ['NO_ET'] = '1'

# Modules intentionally raising DeprecationError are excluded from doctest collection.
collect_ignore_glob = [
'interfaces/plotting.py',
'interfaces/reportlets/masks.py',
'interfaces/reportlets/segmentation.py',
'reports/*.py',
'viz/*.py',
]


def find_resource_or_skip(resource):
pathlike = load_resource(resource)
Expand Down
2 changes: 1 addition & 1 deletion niworkflows/func/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
import pytest
from nipype.pipeline import engine as pe
from nipype.utils.filemanip import copyfile, fname_presuffix
from nireports.interfaces.reporting.masks import ROIsPlot

from ...interfaces.reportlets.masks import ROIsPlot
from ...workflows.epi.refmap import init_epi_reference_wf
from ..util import init_enhance_and_skullstrip_bold_wf

Expand Down
2 changes: 1 addition & 1 deletion niworkflows/func/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from nipype.interfaces import afni, fsl
from nipype.interfaces import utility as niu
from nipype.pipeline import engine as pe
from nireports.interfaces.reporting.masks import SimpleShowMaskRPT
from packaging.version import Version
from packaging.version import parse as parseversion
from templateflow.api import get as get_template
Expand All @@ -41,7 +42,6 @@
FixN4BiasFieldCorrection as N4BiasFieldCorrection,
)
from ..interfaces.header import CopyHeader, CopyXForm, ValidateImage
from ..interfaces.reportlets.masks import SimpleShowMaskRPT
from ..utils.connections import listify
from ..utils.misc import pass_dummy_scans as _pass_dummy_scans

Expand Down
3 changes: 1 addition & 2 deletions niworkflows/interfaces/freesurfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@
from nipype.interfaces.freesurfer.preprocess import ConcatenateLTA, RobustRegister
from nipype.interfaces.freesurfer.utils import LTAConvert
from nipype.utils.filemanip import copyfile, filename_to_list, fname_presuffix

from .reportlets.registration import BBRegisterRPT, MRICoregRPT
from nireports.interfaces.reporting.registration import BBRegisterRPT, MRICoregRPT


class StructuralReference(fs.RobustTemplate):
Expand Down
254 changes: 9 additions & 245 deletions niworkflows/interfaces/plotting.py
Original file line number Diff line number Diff line change
@@ -1,248 +1,12 @@
# 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 <nipreps@gmail.com>
#
# 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/
#
"""Visualization tools."""

import nibabel as nb
import numpy as np
from nipype.interfaces.base import (
BaseInterfaceInputSpec,
File,
SimpleInterface,
TraitedSpec,
isdefined,
traits,
)
from nipype.utils.filemanip import fname_presuffix

from niworkflows.utils.timeseries import _cifti_timeseries, _nifti_timeseries
from niworkflows.viz.plots import (
compcor_variance_plot,
confounds_correlation_plot,
fMRIPlot,
from .._deprecated import raise_moved_to_nireports

raise_moved_to_nireports(
'niworkflows.interfaces.plotting',
(
'nireports.interfaces.FMRISummary',
'nireports.interfaces.CompCorVariancePlot',
'nireports.interfaces.ConfoundsCorrelationPlot',
),
)


class _FMRISummaryInputSpec(BaseInterfaceInputSpec):
in_func = File(exists=True, mandatory=True, desc='')
in_spikes_bg = File(exists=True, desc='')
fd = File(exists=True, desc='')
dvars = File(exists=True, desc='')
outliers = File(exists=True, desc='')
in_segm = File(exists=True, desc='')
tr = traits.Either(None, traits.Float, usedefault=True, desc='the TR')
fd_thres = traits.Float(0.2, usedefault=True, desc='')
drop_trs = traits.Int(0, usedefault=True, desc='dummy scans')


class _FMRISummaryOutputSpec(TraitedSpec):
out_file = File(exists=True, desc='written file path')


class FMRISummary(SimpleInterface):
"""Prepare an fMRI summary plot for the report."""

input_spec = _FMRISummaryInputSpec
output_spec = _FMRISummaryOutputSpec

def _run_interface(self, runtime):
import pandas as pd

self._results['out_file'] = fname_presuffix(
self.inputs.in_func,
suffix='_fmriplot.svg',
use_ext=False,
newpath=runtime.cwd,
)

dataframe = (
pd.DataFrame(
{
'outliers': np.loadtxt(self.inputs.outliers, usecols=[0]).tolist(),
# Pick non-standardize dvars (col 1)
# First timepoint is NaN (difference)
'DVARS': [np.nan]
+ np.loadtxt(self.inputs.dvars, skiprows=1, usecols=[1]).tolist(),
# First timepoint is zero (reference volume)
'FD': [0.0] + np.loadtxt(self.inputs.fd, skiprows=1, usecols=[0]).tolist(),
}
)
if (
isdefined(self.inputs.outliers)
and isdefined(self.inputs.dvars)
and isdefined(self.inputs.fd)
)
else None
)

input_data = nb.load(self.inputs.in_func)
seg_file = self.inputs.in_segm if isdefined(self.inputs.in_segm) else None
dataset, segments = (
_cifti_timeseries(input_data)
if isinstance(input_data, nb.Cifti2Image)
else _nifti_timeseries(input_data, seg_file)
)

fig = fMRIPlot(
dataset,
segments=segments,
spikes_files=(
[self.inputs.in_spikes_bg] if isdefined(self.inputs.in_spikes_bg) else None
),
tr=(self.inputs.tr if isdefined(self.inputs.tr) else _get_tr(input_data)),
confounds=dataframe,
units={'outliers': '%', 'FD': 'mm'},
vlines={'FD': [self.inputs.fd_thres]},
nskip=self.inputs.drop_trs,
).plot()
fig.savefig(self._results['out_file'], bbox_inches='tight')
return runtime


class _CompCorVariancePlotInputSpec(BaseInterfaceInputSpec):
metadata_files = traits.List(
File(exists=True),
mandatory=True,
desc='List of files containing component metadata',
)
metadata_sources = traits.List(
traits.Str,
desc='List of names of decompositions '
'(e.g., aCompCor, tCompCor) yielding '
'the arguments in `metadata_files`',
)
variance_thresholds = traits.Tuple(
traits.Float(0.5),
traits.Float(0.7),
traits.Float(0.9),
usedefault=True,
desc='Levels of explained variance to include in plot',
)
out_file = traits.Either(None, File, value=None, usedefault=True, desc='Path to save plot')


class _CompCorVariancePlotOutputSpec(TraitedSpec):
out_file = File(exists=True, desc='Path to saved plot')


class CompCorVariancePlot(SimpleInterface):
"""Plot the number of components necessary to explain the specified levels of variance."""

input_spec = _CompCorVariancePlotInputSpec
output_spec = _CompCorVariancePlotOutputSpec

def _run_interface(self, runtime):
if self.inputs.out_file is None:
self._results['out_file'] = fname_presuffix(
self.inputs.metadata_files[0],
suffix='_compcor.svg',
use_ext=False,
newpath=runtime.cwd,
)
else:
self._results['out_file'] = self.inputs.out_file
compcor_variance_plot(
metadata_files=self.inputs.metadata_files,
metadata_sources=self.inputs.metadata_sources,
output_file=self._results['out_file'],
varexp_thresh=self.inputs.variance_thresholds,
)
return runtime


class _ConfoundsCorrelationPlotInputSpec(BaseInterfaceInputSpec):
confounds_file = File(exists=True, mandatory=True, desc='File containing confound regressors')
out_file = traits.Either(None, File, value=None, usedefault=True, desc='Path to save plot')
reference_column = traits.Str(
'global_signal',
usedefault=True,
desc='Column in the confound file for '
'which all correlation magnitudes '
'should be ranked and plotted',
)
columns = traits.List(traits.Str, desc='Filter out all regressors not found in this list.')
max_dim = traits.Int(
20,
usedefault=True,
desc='Maximum number of regressors to include in '
'plot. Regressors with highest magnitude of '
'correlation with `reference_column` will be '
'selected.',
)
ignore_initial_volumes = traits.Int(
0,
usedefault=True,
desc='Number of non-steady-state volumes at the beginning of the scan to ignore.',
)


class _ConfoundsCorrelationPlotOutputSpec(TraitedSpec):
out_file = File(exists=True, desc='Path to saved plot')


class ConfoundsCorrelationPlot(SimpleInterface):
"""Plot the correlation among confound regressors."""

input_spec = _ConfoundsCorrelationPlotInputSpec
output_spec = _ConfoundsCorrelationPlotOutputSpec

def _run_interface(self, runtime):
if self.inputs.out_file is None:
self._results['out_file'] = fname_presuffix(
self.inputs.confounds_file,
suffix='_confoundCorrelation.svg',
use_ext=False,
newpath=runtime.cwd,
)
else:
self._results['out_file'] = self.inputs.out_file
confounds_correlation_plot(
confounds_file=self.inputs.confounds_file,
columns=self.inputs.columns if isdefined(self.inputs.columns) else None,
max_dim=self.inputs.max_dim,
output_file=self._results['out_file'],
reference=self.inputs.reference_column,
ignore_initial_volumes=self.inputs.ignore_initial_volumes,
)
return runtime


def _get_tr(img):
"""
Attempt to extract repetition time from NIfTI/CIFTI header

Examples
--------
>>> _get_tr(nb.load(Path(test_data) /
... 'sub-ds205s03_task-functionallocalizer_run-01_bold_volreg.nii.gz'))
2.2
>>> _get_tr(nb.load(Path(test_data) /
... 'sub-01_task-mixedgamblestask_run-02_space-fsLR_den-91k_bold.dtseries.nii'))
2.0

"""

try:
return img.header.matrix.get_index_map(0).series_step
except AttributeError:
return img.header.get_zooms()[-1]
raise RuntimeError('Could not extract TR - unknown data structure type')
Loading
Loading