Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop functions to read sidecar JSONs #760

Merged
merged 11 commits into from
Jul 13, 2024
1 change: 0 additions & 1 deletion qsiprep/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
BIDSInfo,
DerivativesDataSink,
DerivativesMaybeDataSink,
ReadSidecarJSON,
)
from .confounds import DMRISummary, GatherConfounds
from .fmap import FieldToHz, FieldToRadS, Phasediff2Fieldmap, Phases2Fieldmap
Expand Down
106 changes: 0 additions & 106 deletions qsiprep/interfaces/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import re
from shutil import copyfileobj, copytree

import simplejson as json
from bids.layout import parse_file_entities
from nipype import logging
from nipype.interfaces.base import (
Expand Down Expand Up @@ -500,111 +499,6 @@ def _run_interface(self, runtime):
return runtime


class ReadSidecarJSONInputSpec(BaseInterfaceInputSpec):
in_file = File(exists=True, mandatory=True, desc="the input nifti file")
fields = traits.List(traits.Str, desc="get only certain fields")


class ReadSidecarJSONOutputSpec(TraitedSpec):
subject_id = traits.Str()
session_id = traits.Str()
task_id = traits.Str()
acq_id = traits.Str()
rec_id = traits.Str()
run_id = traits.Str()
out_dict = traits.Dict()


class ReadSidecarJSON(SimpleInterface):
"""
A utility to find and read JSON sidecar files of a BIDS tree
"""

expr = re.compile(
"^sub-(?P<subject_id>[a-zA-Z0-9]+)(_ses-(?P<session_id>[a-zA-Z0-9]+))?"
"(_task-(?P<task_id>[a-zA-Z0-9]+))?(_acq-(?P<acq_id>[a-zA-Z0-9]+))?"
"(_rec-(?P<rec_id>[a-zA-Z0-9]+))?(_run-(?P<run_id>[a-zA-Z0-9]+))?"
)
input_spec = ReadSidecarJSONInputSpec
output_spec = ReadSidecarJSONOutputSpec
_always_run = True

def _run_interface(self, runtime):
metadata = get_metadata_for_nifti(self.inputs.in_file)
output_keys = [key for key in list(self.output_spec().get().keys()) if key.endswith("_id")]
outputs = self.expr.search(op.basename(self.inputs.in_file)).groupdict()

for key in output_keys:
id_value = outputs.get(key)
if id_value is not None:
self._results[key] = outputs.get(key)

if isdefined(self.inputs.fields) and self.inputs.fields:
for fname in self.inputs.fields:
self._results[fname] = metadata[fname]
else:
self._results["out_dict"] = metadata

return runtime


def get_metadata_for_nifti(in_file):
"""Fetch metadata for a given nifti file"""
in_file = op.abspath(in_file)

fname, ext = op.splitext(in_file)
if ext == ".gz":
fname, ext2 = op.splitext(fname)
ext = ext2 + ext

side_json = fname + ".json"
fname_comps = op.basename(side_json).split("_")

session_comp_list = []
subject_comp_list = []
top_comp_list = []
ses = None
sub = None

for comp in fname_comps:
if comp[:3] != "run":
session_comp_list.append(comp)
if comp[:3] == "ses":
ses = comp
else:
subject_comp_list.append(comp)
if comp[:3] == "sub":
sub = comp
else:
top_comp_list.append(comp)

if any([comp.startswith("ses") for comp in fname_comps]):
bids_dir = "/".join(op.dirname(in_file).split("/")[:-3])
else:
bids_dir = "/".join(op.dirname(in_file).split("/")[:-2])

top_json = op.join(bids_dir, "_".join(top_comp_list))
potential_json = [top_json]

subject_json = op.join(bids_dir, sub, "_".join(subject_comp_list))
potential_json.append(subject_json)

if ses:
session_json = op.join(bids_dir, sub, ses, "_".join(session_comp_list))
potential_json.append(session_json)

potential_json.append(side_json)

merged_param_dict = {}
for json_file_path in potential_json:
if op.isfile(json_file_path):
with open(json_file_path, "r") as jsonfile:
param_dict = json.load(jsonfile)
merged_param_dict.update(param_dict)

return merged_param_dict


def _splitext(fname):
fname, ext = op.splitext(op.basename(fname))
if ext == ".gz":
Expand Down
5 changes: 3 additions & 2 deletions qsiprep/workflows/dwi/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from ... import config
from ...engine import Workflow
from ...interfaces import ConformDwi, DerivativesDataSink
from ...interfaces.bids import get_metadata_for_nifti
from ...interfaces.dipy import Patch2Self
from ...interfaces.dwi_merge import MergeDWIs, PhaseToRad, StackConfounds
from ...interfaces.gradients import ExtractB0s
Expand Down Expand Up @@ -115,7 +114,9 @@ def init_merge_and_denoise_wf(
bids_dwi_files=raw_dwi_files,
b0_threshold=config.workflow.b0_threshold,
harmonize_b0_intensities=not config.workflow.no_b0_harmonization,
scan_metadata={scan: get_metadata_for_nifti(scan) for scan in raw_dwi_files},
scan_metadata={
scan: config.execution.layout.get_metadata(scan) for scan in raw_dwi_files
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much better!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only issue is that read_nifti_sidecar is still used in places. It looks like there are a few interfaces/functions (get_distortion_grouping and get_best_b0_topup_inputs_from) that use read_nifti_sidecar and do some complicated stuff, so I didn't try to remove it.

},
),
name="merge_dwis",
n_procs=omp_nthreads,
Expand Down
9 changes: 9 additions & 0 deletions qsiprep/workflows/fieldmap/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ def init_sdc_wf(fieldmap_info, dwi_meta):
# set inputs
if fieldmap_info["suffix"] == "phasediff":
fmap_estimator_wf.inputs.inputnode.phasediff = fieldmap_info["phasediff"]
fmap_estimator_wf.inputs.inputnode.phase_meta = (
config.execution.layout.get_metadata(fieldmap_info["phasediff"])
)
else:
# Check that fieldmap is not bipolar
fmap_polarity = fieldmap_info["metadata"].get("DiffusionScheme", None)
Expand All @@ -219,13 +222,19 @@ def init_sdc_wf(fieldmap_info, dwi_meta):
('b0_mask', 'b0_mask')]),
]) # fmt:skip
return workflow

if fmap_polarity is None:
config.loggers.workflow.warning("Assuming phase images are Monopolar")

fmap_estimator_wf.inputs.inputnode.phasediff = [
fieldmap_info["phase1"],
fieldmap_info["phase2"],
]
fmap_estimator_wf.inputs.inputnode.phase_meta = [
config.execution.layout.get_metadata(fieldmap_info["phase1"]),
config.execution.layout.get_metadata(fieldmap_info["phase2"]),
]

fmap_estimator_wf.inputs.inputnode.magnitude = [
fmap_
for key, fmap_ in sorted(fieldmap_info.items())
Expand Down
32 changes: 14 additions & 18 deletions qsiprep/workflows/fieldmap/phdiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from nipype.interfaces import utility as niu
from nipype.pipeline import engine as pe
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
from niworkflows.interfaces.bids import ReadSidecarJSON
from niworkflows.interfaces.images import IntraModalMerge
from niworkflows.interfaces.reportlets.masks import BETRPT

Expand Down Expand Up @@ -73,7 +72,16 @@ def init_phdiff_wf(phasetype="phasediff", name="phdiff_wf"):
"""Container in use does not have FSL. To use this workflow,
please download the qsiprep container with FSL installed."""
)
inputnode = pe.Node(niu.IdentityInterface(fields=["magnitude", "phasediff"]), name="inputnode")
inputnode = pe.Node(
niu.IdentityInterface(
fields=[
"magnitude",
"phasediff",
"phase_meta",
],
),
name="inputnode",
)

outputnode = pe.Node(
niu.IdentityInterface(fields=["fmap", "fmap_ref", "fmap_mask"]), name="outputnode"
Expand Down Expand Up @@ -119,15 +127,11 @@ def init_phdiff_wf(phasetype="phasediff", name="phdiff_wf"):
# rsec2hz (divide by 2pi)

if phasetype == "phasediff":
# Read phasediff echo times
meta = pe.Node(ReadSidecarJSON(), name="meta", mem_gb=0.01)

# phase diff -> radians
pha2rads = pe.Node(niu.Function(function=siemens2rads), name="pha2rads")
# Read phasediff echo times
meta = pe.Node(ReadSidecarJSON(), name="meta", mem_gb=0.01, run_without_submitting=True)

workflow.connect([
(meta, compfmap, [('out_dict', 'metadata')]),
(inputnode, compfmap, [('phase_meta', 'metadata')]),
(inputnode, pha2rads, [('phasediff', 'in_file')]),
(pha2rads, prelude, [('out', 'phase_file')]),
(inputnode, ds_report_fmap_mask, [('phasediff', 'source_file')]),
Expand All @@ -138,25 +142,17 @@ def init_phdiff_wf(phasetype="phasediff", name="phdiff_wf"):
The phase difference used for unwarping was calculated using two separate phase measurements
[@pncprocessing].
"""
# Special case for phase1, phase2 images
meta = pe.MapNode(
ReadSidecarJSON(),
name="meta",
mem_gb=0.01,
run_without_submitting=True,
iterfield=["in_file"],
)
phases2fmap = pe.Node(Phases2Fieldmap(), name="phases2fmap")

workflow.connect([
(meta, phases2fmap, [('out_dict', 'metadatas')]),
(inputnode, phases2fmap, [('phase_meta', 'metadatas')]),
(inputnode, phases2fmap, [('phasediff', 'phase_files')]),
(phases2fmap, prelude, [('out_file', 'phase_file')]),
(phases2fmap, compfmap, [('phasediff_metadata', 'metadata')]),
(phases2fmap, ds_report_fmap_mask, [('out_file', 'source_file')])
]) # fmt:skip

workflow.connect([
(inputnode, meta, [('phasediff', 'in_file')]),
(inputnode, magmrg, [('magnitude', 'in_files')]),
(magmrg, n4, [('out_avg', 'input_image')]),
(n4, prelude, [('output_image', 'magnitude_file')]),
Expand Down