Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 0 deletions .circleci/continue_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ jobs:
echo "export DSCSDSI_FMAP_URL=$DSCSDSI_FMAP_URL" >> "$BASH_ENV"
echo "export MATERNAL_BRAIN_PROJECT_URL=$MATERNAL_BRAIN_PROJECT_URL" >> "$BASH_ENV"
echo "export FORREST_GUMP_URL=$FORREST_GUMP_URL" >> "$BASH_ENV"
echo "export NIBS_URL=$NIBS_URL" >> "$BASH_ENV"
- run:
name: Download integration datasets
command: |
Expand Down Expand Up @@ -425,6 +426,7 @@ jobs:
download_one DSCSDSI_fmap "$DSCSDSI_FMAP_URL"
download_one maternal_brain_project "$MATERNAL_BRAIN_PROJECT_URL"
download_one forrest_gump "$FORREST_GUMP_URL"
download_one nibs "$NIBS_URL"
- save_cache:
key: data-v2-{{ checksum ".circleci/data_versions.txt" }}
paths:
Expand Down
1 change: 1 addition & 0 deletions .circleci/data_versions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ DSDTI_FMAP_URL=https://upenn.box.com/shared/static/rxr6qbi6ezku9gw3esfpnvqlcxaw7
DSCSDSI_FMAP_URL=https://upenn.box.com/shared/static/l561psez1ojzi4p3a12eidaw9vbizwdc.gz
MATERNAL_BRAIN_PROJECT_URL=https://upenn.box.com/shared/static/tkahg1ctipmfihvpa1gmibvcv0gb721h.xz
FORREST_GUMP_URL=https://upenn.box.com/shared/static/qat58an322bzzyixrrsk7cmf52q3bepq.xz
NIBS_URL=https://upenn.box.com/shared/static/bkllff4ik51jy9ju6nben2r5zrq4a5me.xz
48 changes: 48 additions & 0 deletions .circleci/get_data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,42 @@ Contents:
- data/singleshell_output/qsiprep/sub-PNC/figures/sub-PNC_t1_2_mni.svg
- data/singleshell_output/qsiprep/sub-PNC.html


nibs:
-----

Downsampled NIBS data acquired with the HBCD protocol, with both magnitude and
phase parts, used to exercise the complex-valued denoising workflows.

Contents:
^^^^^^^^^

- data/nibs/dataset_description.json
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_dwi.bval
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_dwi.bvec
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-mag_dwi.bval
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-mag_dwi.bvec
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-mag_dwi.json
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-mag_dwi.nii.gz
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-mag_sbref.json
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-mag_sbref.nii.gz
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-phase_dwi.json
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-phase_dwi.nii.gz
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-phase_sbref.json
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-AP_part-phase_sbref.nii.gz
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_dwi.bval
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_dwi.bvec
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-mag_dwi.bval
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-mag_dwi.bvec
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-mag_dwi.json
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-mag_dwi.nii.gz
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-mag_sbref.json
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-mag_sbref.nii.gz
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-phase_dwi.json
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-phase_dwi.nii.gz
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-phase_sbref.json
- data/nibs/sub-22449/ses-01/dwi/sub-22449_ses-01_dir-PA_part-phase_sbref.nii.gz

DOC


Expand Down Expand Up @@ -440,6 +476,18 @@ get_bids_data() {
rm tinytensors_epi.tar.xz
fi

# Downsampled NIBS (complex-valued HBCD protocol)
# Unlike the other archives, this one has no top-level directory, so it is extracted
# into a directory of its own rather than into data/ directly.
if [[ ${DS} = nibs ]]; then
mkdir -p ${WORKDIR}/data/nibs
${WGET} \
-O nibs.tar.xz \
"https://upenn.box.com/shared/static/bkllff4ik51jy9ju6nben2r5zrq4a5me.xz"
tar xvfJ nibs.tar.xz -C ${WORKDIR}/data/nibs
rm nibs.tar.xz
fi

# name: Get data for fieldmap tests
if [[ ${DS} = fmaps ]]; then
mkdir -p ${WORKDIR}/data/fmaptests
Expand Down
61 changes: 61 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,44 @@
ARG BASE_IMAGE=pennlinc/qsiprep-base:20260809
ARG DWIDENOISE2_COMMIT=cd08ec1a0f5eb1dbc9962f80c20c2bb3428c4f93
# MRtrix3 "dev" as at 2026-06-22, the commit dwidenoise2 is developed against
ARG MRTRIX3_DWIDENOISE2_COMMIT=b98b54e9ae8168eeb9af23322a07011d4754456d

FROM buildpack-deps:bookworm AS dwidenoise2-build
ARG DWIDENOISE2_COMMIT
ARG MRTRIX3_DWIDENOISE2_COMMIT
RUN apt-get update && \
apt-get install -y --no-install-recommends \
cmake \
libfftw3-dev \
ninja-build \
pkg-config \
zlib1g-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

WORKDIR /src/dwidenoise2
RUN git init . && \
git remote add origin https://github.com/tsalo/dwidenoise2.git && \
git fetch --depth 1 origin ${DWIDENOISE2_COMMIT} && \
git checkout --detach FETCH_HEAD

WORKDIR /src/mrtrix3
RUN git clone --filter=blob:none --no-checkout https://github.com/MRtrix3/mrtrix3.git . && \
git checkout --detach ${MRTRIX3_DWIDENOISE2_COMMIT}
# dwidenoise2 has no external-module build, so its sources are dropped into the MRtrix3
# tree before configuring. The per-command noise estimation schedules must land in
# share/mrtrix3/<command>/, where the built commands look for them relative to the
# executable; "copy-share-data" imports them into the build tree and has to be named
# explicitly, because building named executable targets does not run MRtrix3's ALL targets.
RUN cp /src/dwidenoise2/cpp/cmd/dwidenoise2.cpp cpp/cmd/dwidenoise2.cpp && \
cp /src/dwidenoise2/cpp/cmd/dwi2noise.cpp cpp/cmd/dwi2noise.cpp && \
cp -r /src/dwidenoise2/cpp/core/denoise cpp/core/denoise && \
cp -r /src/dwidenoise2/share/dwidenoise2/. share/mrtrix3/ && \
cmake -B build -GNinja \
-DMRTRIX_BUILD_GUI=OFF \
-DMRTRIX_ENABLE_GPU=OFF \
-DCMAKE_COMPILE_WARNING_AS_ERROR=ON \
--preset=release && \
cmake --build build --target dwidenoise2 dwi2noise copy-share-data

FROM ghcr.io/prefix-dev/pixi:0.58.0 AS build
RUN apt-get update && \
Expand Down Expand Up @@ -37,6 +77,27 @@ FROM ${BASE_IMAGE} AS base
WORKDIR /home/qsiprep
ENV HOME="/home/qsiprep"

COPY --from=dwidenoise2-build \
/src/mrtrix3/build/bin/dwidenoise2 \
/opt/dwidenoise2/bin/dwidenoise2
COPY --from=dwidenoise2-build \
/src/mrtrix3/build/bin/dwi2noise \
/opt/dwidenoise2/bin/dwi2noise
COPY --from=dwidenoise2-build \
/src/mrtrix3/build/cpp/core/libmrtrix-core.so \
/opt/dwidenoise2/lib/libmrtrix-core.so
# The bundled schedules are found relative to the executable, at ../share/mrtrix3/<command>/
COPY --from=dwidenoise2-build \
/src/mrtrix3/build/share/mrtrix3 \
/opt/dwidenoise2/share/mrtrix3
COPY --from=dwidenoise2-build \
/src/dwidenoise2/LICENSE \
/opt/dwidenoise2/LICENSE
ENV PATH="/opt/dwidenoise2/bin:$PATH" \
LD_LIBRARY_PATH="/opt/dwidenoise2/lib:$LD_LIBRARY_PATH"
RUN dwidenoise2 -version && \
test -d /opt/dwidenoise2/share/mrtrix3/dwidenoise2

RUN chmod -R go=u $HOME
WORKDIR /tmp

Expand Down
79 changes: 63 additions & 16 deletions qsiprep/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

from .. import config
from ..utils.gpu import GPU_ALIASES, GPU_TASKS
from ..utils.misc import parse_denoise_method


def _build_parser(**kwargs):
Expand Down Expand Up @@ -122,6 +123,13 @@ def _int_or_auto(value, parser):

return value

def _denoise_method(value, parser):
try:
parse_denoise_method(value)
except ValueError as exc:
parser.error(f'Invalid --denoise-method specification: {exc}')
return value

def _to_gb(value):
scale = {'G': 1, 'T': 10**3, 'M': 1e-3, 'K': 1e-6, 'B': 1e-9}
digits = ''.join([c for c in value if c.isdigit()])
Expand Down Expand Up @@ -178,6 +186,7 @@ def _bids_filter(value, parser):
IsFile = partial(_is_file, parser=parser)
PositiveInt = partial(_min_one, parser=parser)
IntOrAuto = partial(_int_or_auto, parser=parser)
DenoiseMethod = partial(_denoise_method, parser=parser)
BIDSFilter = partial(_bids_filter, parser=parser)

# Arguments as specified by BIDS-Apps
Expand Down Expand Up @@ -408,20 +417,27 @@ def _bids_filter(value, parser):
help=(
'Window size in voxels for image-based denoising: odd integer or "auto". '
'Any non-"auto" value must be an odd, positive integer. '
'If using the "dwidenoise" denoising method, '
'the "auto" option will calculate a window size '
'This argument only applies to the "dwidenoise" denoising method, '
'where the "auto" option will calculate a window size '
'based on the number of volumes according to the method described by the '
'dwidenoise documentation. '
'If using the "patch2self" denoising method, this argument will not be used.'
'It is not used by the "patch2self" or "dwidenoise2" methods: dwidenoise2 sizes '
'its patches per iteration from its multi-resolution schedule, which is selected '
'with "dwidenoise2;schedule:<name>" instead.'
),
)
g_conf.add_argument(
'--denoise-method',
action='store',
choices=['dwidenoise', 'patch2self', 'none'],
type=DenoiseMethod,
default='dwidenoise',
help='Image-based denoising method. Either "dwidenoise" (MRtrix), '
'"patch2self" (DIPY) or "none". (default: dwidenoise)',
help=(
'Image-based denoising method: "dwidenoise" (MRtrix), "dwidenoise2", '
'"patch2self" (DIPY), or "none".\n'
'dwidenoise2 parameters may follow the method as semicolon-delimited '
'name:value pairs, for example '
'"dwidenoise2;demodulate:linear;decomposition:bdcsvd".'
),
)
g_conf.add_argument(
'--unringing-method',
Expand Down Expand Up @@ -475,7 +491,7 @@ def _bids_filter(value, parser):
g_conf.add_argument(
'--denoise-after-combining',
action='store_true',
help='run ``dwidenoise`` after combining dwis, but before motion correction',
help='run denoising after combining dwis, but before motion correction',
)
g_conf.add_argument(
'--separate-all-dwis',
Expand Down Expand Up @@ -739,6 +755,32 @@ def _bids_filter(value, parser):
return parser


def check_denoise_window(denoise_method, dwi_denoise_window):
"""Report a ``--dwi-denoise-window`` that the selected denoising method will ignore.

Only ``dwidenoise`` takes a window size. Leaving the others to silently ignore it would
hide a request that never took effect.
"""
if dwi_denoise_window == 'auto':
# The default, so an unused value is not a sign that anything was misunderstood
return

if denoise_method == 'patch2self':
config.loggers.cli.error(
'The --dwi-denoise-window option is not used when --denoise-method=patch2self'
)
elif denoise_method == 'dwidenoise2':
config.loggers.cli.warning(
'The --dwi-denoise-window option is not used when --denoise-method=dwidenoise2. '
'dwidenoise2 sizes its patches per iteration from its multi-resolution schedule, '
'which can be selected with "dwidenoise2;schedule:<name>" instead.'
)
elif denoise_method == 'none':
config.loggers.cli.warning(
'The --dwi-denoise-window option is not used when --denoise-method=none'
)


def parse_args(args=None, namespace=None):
"""Parse args and run further checks on the command line."""
import logging
Expand Down Expand Up @@ -843,15 +885,20 @@ def parse_args(args=None, namespace=None):
)

# Validate the tricky options here
if config.workflow.dwi_denoise_window != 'auto':
if config.workflow.denoise_method == 'patch2self':
config.loggers.cli.error(
'The --dwi-denoise-window option is not used when --denoise-method=patch2self'
)
elif config.workflow.denoise_method == 'none':
config.loggers.cli.warning(
'The --dwi-denoise-window option is not used when --denoise-method=none'
)
denoise_method, denoise_params = parse_denoise_method(config.workflow.denoise_method)
check_denoise_window(denoise_method, config.workflow.dwi_denoise_window)
if (
config.workflow.denoise_after_combining
and denoise_params.get('demodulate', 'none') != 'none'
):
# Temporary workaround for a bug in dwidenoise2: the concatenated series
# cannot be denoised with phase data.
parser.error(
'--denoise-after-combining cannot be used with phase demodulation '
f'("demodulate:{denoise_params["demodulate"]}"). '
'Remove the demodulate parameter and use "--ignore phase" to denoise '
'the magnitude data only.'
)

bids_dir = config.execution.bids_dir
output_dir = config.execution.output_dir
Expand Down
3 changes: 2 additions & 1 deletion qsiprep/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,8 @@ class workflow(_Config):
"""Run ``dwidenoise`` after combining dwis, but before motion correction."""
denoise_method = None
"""Image-based denoising method. Either "dwidenoise" (MRtrix), "patch2self" (DIPY)
or "none"."""
or "none". DWIDenoise parameters may be appended as semicolon-delimited name:value
pairs."""
distortion_group_merge = None
"""How to combine images across distortion groups (concatenate, average or none)."""
dwi_denoise_window = None
Expand Down
Loading