Skip to content

Commit

Permalink
Document M0 use in reports (#329)
Browse files Browse the repository at this point in the history
* Update util.py

* Add M0 string to main CBF description.

* Update cbf.py

* Pin to aslprep_build 0.0.2.

* Rename smoothkernel to smooth_kernel.

* Work on GE reports too.

* Update ge_utils.py

* Improve reports.

* Fix.
  • Loading branch information
tsalo authored Oct 4, 2023
1 parent d9715de commit 55c378a
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 32 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM pennlinc/aslprep_build:main
FROM pennlinc/aslprep_build:0.0.2

# Install aslprep
COPY . /src/aslprep
Expand Down
2 changes: 1 addition & 1 deletion aslprep/interfaces/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def _run_interface(self, runtime):
asl_img = nb.load(self.inputs.asl_file)
assert asl_img.shape[3] == aslcontext.shape[0]

if self.inputs.processing_target == "controllabel":
if self.inputs.processing_target == "control":
files_to_keep = ["control", "label", "m0scan"]
elif self.inputs.processing_target == "deltam":
files_to_keep = ["deltam", "m0scan"]
Expand Down
2 changes: 1 addition & 1 deletion aslprep/utils/asl.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def select_processing_target(aslcontext):
voltypes = aslcontext_df["volume_type"].tolist()

if "control" in voltypes and "label" in voltypes:
processing_target = "controllabel"
processing_target = "control"
elif "deltam" in voltypes:
processing_target = "deltam"
elif "cbf" in voltypes:
Expand Down
9 changes: 6 additions & 3 deletions aslprep/workflows/asl/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def init_asl_preproc_wf(asl_file):
spaces = config.workflow.spaces
output_dir = str(config.execution.output_dir)
dummyvols = config.workflow.dummy_vols
smoothkernel = config.workflow.smooth_kernel
smooth_kernel = config.workflow.smooth_kernel
m0_scale = config.workflow.m0_scale
scorescrub = config.workflow.scorescrub
basil = config.workflow.basil
Expand Down Expand Up @@ -280,7 +280,10 @@ def init_asl_preproc_wf(asl_file):
workflow.connect([(inputnode, validate_asl_wf, [("asl_file", "inputnode.asl_file")])])

# Generate a tentative aslref from the most appropriate available image type in the ASL file
asl_reference_wf = init_asl_reference_wf(name="asl_reference_wf")
asl_reference_wf = init_asl_reference_wf(
aslcontext=run_data["aslcontext"],
name="asl_reference_wf",
)
asl_reference_wf.inputs.inputnode.dummy_scans = 0

# fmt:off
Expand Down Expand Up @@ -432,7 +435,7 @@ def init_asl_preproc_wf(asl_file):
m0_scale=m0_scale,
scorescrub=scorescrub,
basil=basil,
smooth_kernel=smoothkernel,
smooth_kernel=smooth_kernel,
metadata=metadata,
name="compute_cbf_wf",
)
Expand Down
44 changes: 30 additions & 14 deletions aslprep/workflows/asl/cbf.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def init_compute_cbf_wf(
wf = init_compute_cbf_wf(
name_source=str(perf_dir / "sub-01_asl.nii.gz"),
processing_target="controllabel",
processing_target="control",
metadata=metadata,
dummy_vols=0,
scorescrub=True,
Expand Down Expand Up @@ -111,6 +111,7 @@ def init_compute_cbf_wf(
"""

m0type = metadata["M0Type"]
is_casl = pcasl_or_pasl(metadata=metadata)
is_multi_pld = determine_multi_pld(metadata=metadata)
if (processing_target == "cbf") and not basil:
Expand All @@ -122,6 +123,26 @@ def init_compute_cbf_wf(
)
basil = False

if m0type in ("Included", "Separate"):
m0_str = (
"Calibration (M0) volumes associated with the ASL scan were smoothed with a "
f"Gaussian kernel (FWHM={smooth_kernel} mm) and the average calibration image was "
f"calculated and scaled by {m0_scale}."
)
elif m0type == "Estimate":
m0_str = (
f"A single M0 estimate of {metadata['M0Estimate']} was used to produce a calibration "
f"'image' and was scaled by {m0_scale}."
)
else:
m0_str = (
f"As no calibration images or provided M0 estimate was available for the ASL scan, "
"the control volumes used as a substitute. "
"The control volumes in the ASL scans were smoothed with a "
f"Gaussian kernel (FWHM={smooth_kernel} mm) and the average control image was "
f"calculated and scaled by {m0_scale}."
)

if processing_target == "cbf":
workflow.__desc__ += """\
*ASLPrep* loaded pre-calculated cerebral blood flow (CBF) data from the ASL file.
Expand All @@ -134,6 +155,7 @@ def init_compute_cbf_wf(
{metadata['ArterialSpinLabelingType']} data using the following method.
First, delta-M values were averaged over time for each post-labeling delay (PLD).
{m0_str}
Next, arterial transit time (ATT) was estimated on a voxel-wise basis according to
@dai2012reduced.
Expand All @@ -151,6 +173,7 @@ def init_compute_cbf_wf(
*ASLPrep* calculated cerebral blood flow (CBF) from the single-delay
{metadata['ArterialSpinLabelingType']} using a single-compartment general kinetic model
[@buxton1998general].
{m0_str}
"""

else:
Expand All @@ -163,22 +186,25 @@ def init_compute_cbf_wf(

# Single-delay PASL data, with different bolus cut-off techniques
if bcut == "QUIPSS":
workflow.__desc__ += """\
workflow.__desc__ += f"""\
*ASLPrep* calculated cerebral blood flow (CBF) from the single-delay PASL
using a single-compartment general kinetic model [@buxton1998general]
using the QUIPSS modification, as described in @wong1998quantitative.
{m0_str}
"""
elif bcut == "QUIPSSII":
workflow.__desc__ += """\
workflow.__desc__ += f"""\
*ASLPrep* calculated cerebral blood flow (CBF) from the single-delay PASL
using a single-compartment general kinetic model [@buxton1998general]
using the QUIPSS II modification, as described in @alsop_recommended_2015.
{m0_str}
"""
elif bcut == "Q2TIPS":
workflow.__desc__ += """\
workflow.__desc__ += f"""\
*ASLPrep* calculated cerebral blood flow (CBF) from the single-delay PASL
using a single-compartment general kinetic model [@buxton1998general]
using the Q2TIPS modification, as described in @noguchi2015technical.
{m0_str}
"""
else:
# No bolus cutoff delay technique
Expand All @@ -190,11 +216,6 @@ def init_compute_cbf_wf(
"basis based on the slice timing."
)

if m0_scale != 1:
workflow.__desc__ += (
f"Prior to calculating CBF, the M0 volumes were scaled by a factor of {m0_scale}."
)

inputnode = pe.Node(
niu.IdentityInterface(
fields=[
Expand Down Expand Up @@ -533,11 +554,6 @@ def init_compute_cbf_ge_wf(
"basis based on the slice timing."
)

if m0_scale != 1:
workflow.__desc__ += (
f"Prior to calculating CBF, the M0 volumes were scaled by a factor of {m0_scale}."
)

inputnode = pe.Node(
niu.IdentityInterface(
fields=[
Expand Down
27 changes: 27 additions & 0 deletions aslprep/workflows/asl/ge_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from aslprep import config
from aslprep.interfaces import DerivativesDataSink
from aslprep.interfaces.ge import GeReferenceFile
from aslprep.utils.asl import select_processing_target
from aslprep.workflows.asl.registration import init_fsl_bbr_wf

DEFAULT_MEMORY_MIN_GB = config.DEFAULT_MEMORY_MIN_GB
Expand Down Expand Up @@ -50,6 +51,32 @@ def init_asl_reference_ge_wf(
First, a reference volume and its skull-stripped version were generated.
"""

m0type = metadata["M0Type"]
processing_target = select_processing_target(aslcontext=aslcontext)
if m0type in ("Included", "Separate"):
ref_str = (
"The reference volume was generated by extracting M0 volumes associated with the ASL "
"data, averaging the M0 volumes, and smoothing the mean image with a Gaussian kernel "
f"(FWHM={smooth_kernel} mm). This smoothed M0 volume was retained for later use in "
"CBF calculation as well. "
)
elif m0type == "Estimate":
ref_str = (
f"The reference volume was generated from {processing_target} volumes, which were "
f"averaged and smoothed with a Gaussian kernel (FWHM={smooth_kernel} mm). "
f"A single M0 estimate of {metadata['M0Estimate']} was used to produce a calibration "
"'image'. "
)
else:
ref_str = (
f"The reference volume was generated from {processing_target} volumes, which were "
f"averaged and smoothed with a Gaussian kernel (FWHM={smooth_kernel} mm). "
f"As no calibration images or provided M0 estimate was available for the ASL scan, "
"the reference volume was retained for later use in CBF calculation. "
)

workflow.__desc__ += ref_str

inputnode = pe.Node(
niu.IdentityInterface(
fields=[
Expand Down
4 changes: 2 additions & 2 deletions aslprep/workflows/asl/gecbf.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def init_asl_gepreproc_wf(asl_file):
m0_scale = config.workflow.m0_scale
scorescrub = config.workflow.scorescrub
basil = config.workflow.basil
smoothkernel = config.workflow.smooth_kernel
smooth_kernel = config.workflow.smooth_kernel

if scorescrub:
config.loggers.workflow.warning(f"SCORE/SCRUB processing will be disabled for {asl_file}")
Expand Down Expand Up @@ -270,7 +270,7 @@ def init_asl_gepreproc_wf(asl_file):
asl_reference_wf = init_asl_reference_ge_wf(
metadata=metadata,
aslcontext=run_data["aslcontext"],
smooth_kernel=smoothkernel,
smooth_kernel=smooth_kernel,
name="asl_reference_ge_wf",
)

Expand Down
8 changes: 4 additions & 4 deletions aslprep/workflows/asl/hmc.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def init_asl_hmc_wf(
from aslprep.workflows.asl.hmc import init_asl_hmc_wf
wf = init_asl_hmc_wf(
processing_target="controllabel",
processing_target="control",
m0type="Separate",
mem_gb=3,
omp_nthreads=1,
Expand All @@ -46,7 +46,7 @@ def init_asl_hmc_wf(
Parameters
----------
processing_target : {"controllabel", "deltam", "cbf"}
processing_target : {"control", "deltam", "cbf"}
m0type : {"Separate", "Included", "Absent", "Estimate"}
mem_gb : :obj:`float`
Size of ASL file in GB
Expand Down Expand Up @@ -88,7 +88,7 @@ def init_asl_hmc_wf(
workflow = Workflow(name=name)

separation_substr = ""
if processing_target == "controllabel" or m0type == "Included":
if processing_target == "control" or m0type == "Included":
separation_substr = (
"Motion correction was performed separately for each of the volume types "
"in order to account for intensity differences between different contrasts, "
Expand Down Expand Up @@ -139,7 +139,7 @@ def init_asl_hmc_wf(
if m0type == "Included":
files_to_mcflirt.append("m0scan")

if processing_target == "controllabel":
if processing_target == "control":
files_to_mcflirt += ["control", "label"]
else:
files_to_mcflirt.append(processing_target)
Expand Down
21 changes: 17 additions & 4 deletions aslprep/workflows/asl/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
"""Utility workflows."""
import pandas as pd
from nipype.interfaces import afni, fsl
from nipype.interfaces import utility as niu
from nipype.pipeline import engine as pe
Expand All @@ -15,6 +16,7 @@

from aslprep.interfaces.niworkflows import EstimateReferenceImage
from aslprep.interfaces.utility import SplitReferenceTarget
from aslprep.utils.asl import select_processing_target

DEFAULT_MEMORY_MIN_GB = 0.01

Expand Down Expand Up @@ -215,7 +217,7 @@ def init_validate_asl_wf(asl_file=None, name="validate_asl_wf"):
"""
workflow = Workflow(name=name)
workflow.__desc__ = """\
First, the middle volume of the ASL timeseries was selected as the refernce volume and
First, the middle volume of the ASL timeseries was selected as the reference volume and
brain extracted using *Nipype*'s custom brain extraction workflow.
"""

Expand Down Expand Up @@ -269,6 +271,7 @@ def init_validate_asl_wf(asl_file=None, name="validate_asl_wf"):

def init_asl_reference_wf(
asl_file=None,
aslcontext=None,
sbref_files=None,
pre_mask=False,
name="asl_reference_wf",
Expand Down Expand Up @@ -344,9 +347,19 @@ def init_asl_reference_wf(
"""
workflow = Workflow(name=name)
workflow.__desc__ = """\
First, the middle volume of the ASL timeseries was selected as the refernce volume and
brain extracted using *Nipype*'s custom brain extraction workflow.

reference_target = ""
if aslcontext is not None:
reference_target = select_processing_target(aslcontext)
aslcontext_df = pd.read_table(aslcontext)
if "m0scan" in aslcontext_df["volume_type"].values:
reference_target = "M0"

reference_target += " "

workflow.__desc__ = f"""\
First, the middle {reference_target}volume of the ASL timeseries was selected as the
reference volume and brain extracted using *Nipype*'s custom brain extraction workflow.
"""

inputnode = pe.Node(
Expand Down
4 changes: 2 additions & 2 deletions docs/workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Head-motion estimation
from aslprep.workflows.asl.hmc import init_asl_hmc_wf

wf = init_asl_hmc_wf(
processing_target="controllabel",
processing_target="control",
m0type="Included",
mem_gb=1,
omp_nthreads=1,
Expand Down Expand Up @@ -277,7 +277,7 @@ CBF Computation in native space

wf = init_compute_cbf_wf(
name_source=str(nii_file),
processing_target="controllabel",
processing_target="control",
scorescrub=False,
basil=False,
metadata=metadata,
Expand Down

0 comments on commit 55c378a

Please sign in to comment.