Skip to content

Commit fe3abf4

Browse files
committed
include number of distinct shells in reportlet
1 parent 38746ee commit fe3abf4

File tree

6 files changed

+78
-5
lines changed

6 files changed

+78
-5
lines changed

dmriprep/config/reports-spec.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ sections:
7272
- name: Diffusion
7373
ordering: session,acquisition,run
7474
reportlets:
75+
- bids: {datatype: figures, desc: summary, suffix: dwi}
7576
- bids: {datatype: figures, desc: validation, suffix: dwi}
7677
- bids: {datatype: figures, desc: brain, suffix: mask}
7778
caption: Average <em>b=0</em> that serves for reference in early preprocessing steps.

dmriprep/interfaces/reports.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@
3030
\t</ul>
3131
"""
3232

33+
DIFFUSION_TEMPLATE = """\
34+
\t\t<details open>
35+
\t\t<summary>Summary</summary>
36+
\t\t<ul class="elem-desc">
37+
\t\t\t<li>Phase-encoding (PE) direction: {pe_direction}</li>
38+
\t\t\t<li>Distinct shells: {shells_dist}</li>
39+
\t\t</ul>
40+
\t\t</details>
41+
"""
42+
3343
ABOUT_TEMPLATE = """\t<ul>
3444
\t\t<li>dMRIPrep version: {version}</li>
3545
\t\t<li>dMRIPrep command: <code>{command}</code></li>
@@ -119,6 +129,25 @@ def _generate_segment(self):
119129
)
120130

121131

132+
class DiffusionSummaryInputSpec(BaseInterfaceInputSpec):
133+
pe_direction = traits.Enum(
134+
None, "i", "i-", "j", "j-", "k", "k-", desc="Phase-encoding direction detected"
135+
)
136+
shells_dist = traits.Dict(mandatory=True, desc="Number of distinct shells")
137+
138+
139+
class DiffusionSummary(SummaryInterface):
140+
input_spec = DiffusionSummaryInputSpec
141+
142+
def _generate_segment(self):
143+
pe_direction = self.inputs.pe_direction
144+
shells_dist = self.inputs.shells_dist
145+
146+
return DIFFUSION_TEMPLATE.format(
147+
pe_direction=pe_direction, shells_dist=shells_dist
148+
)
149+
150+
122151
class AboutSummaryInputSpec(BaseInterfaceInputSpec):
123152
version = Str(desc="dMRIPrep version")
124153
command = Str(desc="dMRIPrep command")

dmriprep/interfaces/vectors.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class _CheckGradientTableOutputSpec(TraitedSpec):
2929
out_bvec = File(exists=True)
3030
full_sphere = traits.Bool()
3131
pole = traits.Tuple(traits.Float, traits.Float, traits.Float)
32+
num_shells = traits.Int
33+
shells_dist = traits.Dict
3234
b0_ixs = traits.List(traits.Int)
3335
b0_mask = traits.List(traits.Bool)
3436

@@ -48,6 +50,10 @@ class CheckGradientTable(SimpleInterface):
4850
(0.0, 0.0, 0.0)
4951
>>> check.outputs.full_sphere
5052
True
53+
>>> check.outputs.num_shells
54+
3
55+
>>> check.outputs.shells_dist
56+
{0.0: 12, 1200.0: 32, 2500.0: 61}
5157
5258
>>> check = CheckGradientTable(
5359
... dwi_file=str(data_dir / 'dwi.nii.gz'),
@@ -57,6 +63,10 @@ class CheckGradientTable(SimpleInterface):
5763
(0.0, 0.0, 0.0)
5864
>>> check.outputs.full_sphere
5965
True
66+
>>> check.outputs.num_shells
67+
3
68+
>>> check.outputs.shells_dist
69+
{0: 12, 1200: 32, 2500: 61}
6070
>>> newrasb = np.loadtxt(check.outputs.out_rasb, skiprows=1)
6171
>>> oldrasb = np.loadtxt(str(data_dir / 'dwi.tsv'), skiprows=1)
6272
>>> np.allclose(newrasb, oldrasb, rtol=1.e-3)
@@ -82,7 +92,8 @@ def _run_interface(self, runtime):
8292
pole = table.pole
8393
self._results["pole"] = tuple(pole)
8494
self._results["full_sphere"] = np.all(pole == 0.0)
85-
self._results["b0_mask"] = table.b0mask.tolist()
95+
self._results["num_shells"] = len(table.count_shells)
96+
self._results["shells_dist"] = table.count_shells
8697
self._results["b0_ixs"] = np.where(table.b0mask)[0].tolist()
8798

8899
cwd = Path(runtime.cwd).absolute()

dmriprep/utils/vectors.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Utilities to operate on diffusion gradients."""
22
from .. import config
3+
from collections import Counter
34
from pathlib import Path
45
from itertools import permutations
56
import nibabel as nb
@@ -174,6 +175,11 @@ def bvals(self, value):
174175
raise ValueError("The number of b-vectors and b-values do not match")
175176
self._bvals = np.array(value)
176177

178+
@property
179+
def count_shells(self):
180+
"""Count the number of volumes per b-value."""
181+
return Counter(sorted(self._bvals))
182+
177183
@property
178184
def b0mask(self):
179185
"""Get a mask of low-b frames."""
@@ -291,7 +297,7 @@ def normalize_gradients(
291297
Normalize b-vectors and b-values.
292298
293299
The resulting b-vectors will be of unit length for the non-zero b-values.
294-
The resultinb b-values will be normalized by the square of the
300+
The resulting b-values will be normalized by the square of the
295301
corresponding vector amplitude.
296302
297303
Parameters

dmriprep/workflows/dwi/base.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
88
from ...interfaces import DerivativesDataSink
9+
from ...interfaces.reports import DiffusionSummary
910

1011

1112
def init_dwi_preproc_wf(dwi_file, has_fieldmap=False):
@@ -41,6 +42,8 @@ def init_dwi_preproc_wf(dwi_file, has_fieldmap=False):
4142
File path of the b-vectors
4243
in_bval
4344
File path of the b-values
45+
metadata
46+
dwi metadata
4447
fmap
4548
File path of the fieldmap
4649
fmap_ref
@@ -82,6 +85,7 @@ def init_dwi_preproc_wf(dwi_file, has_fieldmap=False):
8285
config.loggers.workflow.debug(
8386
f"Creating DWI preprocessing workflow for <{dwi_file.name}>"
8487
)
88+
metadata = layout.get_metadata(str(dwi_file))
8589

8690
if has_fieldmap:
8791
import re
@@ -107,6 +111,7 @@ def init_dwi_preproc_wf(dwi_file, has_fieldmap=False):
107111
"dwi_file",
108112
"in_bvec",
109113
"in_bval",
114+
"metadata",
110115
# From SDCFlows
111116
"fmap",
112117
"fmap_ref",
@@ -134,12 +139,19 @@ def init_dwi_preproc_wf(dwi_file, has_fieldmap=False):
134139
inputnode.inputs.dwi_file = str(dwi_file.absolute())
135140
inputnode.inputs.in_bvec = str(layout.get_bvec(dwi_file))
136141
inputnode.inputs.in_bval = str(layout.get_bval(dwi_file))
142+
inputnode.metadata = metadata
137143

138144
outputnode = pe.Node(
139145
niu.IdentityInterface(fields=["dwi_reference", "dwi_mask", "gradients_rasb"]),
140146
name="outputnode",
141147
)
142148

149+
summary = pe.Node(
150+
DiffusionSummary(pe_direction=metadata.get("PhaseEncodingDirection")),
151+
name="dwi_summary",
152+
run_without_submitting=True,
153+
)
154+
143155
gradient_table = pe.Node(CheckGradientTable(), name="gradient_table")
144156

145157
dwi_reference_wf = init_dwi_reference_wf(
@@ -153,6 +165,7 @@ def init_dwi_preproc_wf(dwi_file, has_fieldmap=False):
153165
("in_bvec", "in_bvec"),
154166
("in_bval", "in_bval")]),
155167
(inputnode, dwi_reference_wf, [("dwi_file", "inputnode.dwi_file")]),
168+
(gradient_table, summary, [("shells_dist", "shells_dist")]),
156169
(gradient_table, dwi_reference_wf, [("b0_ixs", "inputnode.b0_ixs")]),
157170
(gradient_table, outputnode, [("out_rasb", "gradients_rasb")]),
158171
])
@@ -254,6 +267,7 @@ def _bold_reg_suffix(fallback):
254267
# fmt: off
255268
workflow.connect([
256269
(inputnode, reportlets_wf, [("dwi_file", "inputnode.source_file")]),
270+
(summary, reportlets_wf, [("out_report", "inputnode.summary_report")]),
257271
(dwi_reference_wf, reportlets_wf, [
258272
("outputnode.validation_report", "inputnode.validation_report"),
259273
]),
@@ -285,7 +299,6 @@ def _bold_reg_suffix(fallback):
285299
unwarp_wf = init_unwarp_wf(
286300
debug=config.execution.debug, omp_nthreads=config.nipype.omp_nthreads
287301
)
288-
unwarp_wf.inputs.inputnode.metadata = layout.get_metadata(str(dwi_file))
289302

290303
output_select = pe.Node(
291304
KeySelect(fields=["fmap", "fmap_ref", "fmap_coeff", "fmap_mask"]),
@@ -322,6 +335,7 @@ def _bold_reg_suffix(fallback):
322335
(dwi_reference_wf, coeff2epi_wf, [
323336
("outputnode.ref_image", "inputnode.target_ref"),
324337
("outputnode.dwi_mask", "inputnode.target_mask")]),
338+
(inputnode, unwarp_wf, [("metadata", "inputnode.metadata")]),
325339
(dwi_reference_wf, unwarp_wf, [("outputnode.ref_image", "inputnode.distorted")]),
326340
(coeff2epi_wf, unwarp_wf, [
327341
("outputnode.fmap_coeff", "inputnode.fmap_coeff")]),

dmriprep/workflows/dwi/outputs.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def init_reportlets_wf(output_dir, sdc_report=False, name="reportlets_wf"):
1515
niu.IdentityInterface(
1616
fields=[
1717
"source_file",
18+
"summary_report",
1819
"dwi_ref",
1920
"dwi_mask",
2021
"validation_report",
@@ -23,6 +24,15 @@ def init_reportlets_wf(output_dir, sdc_report=False, name="reportlets_wf"):
2324
),
2425
name="inputnode",
2526
)
27+
28+
ds_report_summary = pe.Node(
29+
DerivativesDataSink(
30+
base_directory=output_dir, desc="summary", datatype="figures"
31+
),
32+
name="ds_report_summary",
33+
run_without_submitting=True,
34+
)
35+
2636
mask_reportlet = pe.Node(SimpleShowMaskRPT(), name="mask_reportlet")
2737

2838
ds_report_mask = pe.Node(
@@ -42,11 +52,13 @@ def init_reportlets_wf(output_dir, sdc_report=False, name="reportlets_wf"):
4252

4353
# fmt:off
4454
workflow.connect([
55+
(inputnode, ds_report_summary, [("source_file", "source_file"),
56+
("summary_report", "in_file")]),
4557
(inputnode, mask_reportlet, [("dwi_ref", "background_file"),
4658
("dwi_mask", "mask_file")]),
47-
(inputnode, ds_report_validation, [("source_file", "source_file")]),
59+
(inputnode, ds_report_validation, [("source_file", "source_file"),
60+
("validation_report", "in_file")]),
4861
(inputnode, ds_report_mask, [("source_file", "source_file")]),
49-
(inputnode, ds_report_validation, [("validation_report", "in_file")]),
5062
(mask_reportlet, ds_report_mask, [("out_report", "in_file")]),
5163
])
5264
# fmt:on

0 commit comments

Comments
 (0)