Skip to content
16 changes: 16 additions & 0 deletions src/rbc/bids/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
from rbc.workflows.functional import FunctionalOutputs


def _smooth_label(fwhm: float, precision: int | None = None) -> str:
"""Format FWHM as a BIDS-safe label (e.g. 6.0 -> 'sm6', 0.1 -> 'sm0p1')."""
s = f"{fwhm:.{precision}f}" if precision is not None else str(fwhm)
return "sm" + s.rstrip("0").rstrip(".").replace(".", "p")


class FunctionalRun(NamedTuple):
"""A single functional run discovered from a BIDS session.

Expand Down Expand Up @@ -104,13 +110,16 @@ def export_functional(
outputs: FunctionalOutputs,
*,
regressors: Sequence[str],
smooth: float | None = None,
) -> Bids:
"""Export functional workflow outputs to BIDS-named derivatives.

Args:
func: Bids builder with ``datatype=FUNC`` and identity entities.
outputs: Results from the functional preprocessing workflow.
regressors: Regressor names (e.g. ``["36-parameter"]``).
smooth: Smoothing kernel FWHM in mm, or ``None`` if smoothing
was not requested.

Returns:
The MNI-space Bids builder, for use by downstream exports
Expand Down Expand Up @@ -179,6 +188,13 @@ def export_functional(
desc="preproc",
extra={"reg": bids_safe_label(reg)},
)
if outputs.cleaned_bold_smooth is not None and smooth is not None:
mni.save(
outputs.cleaned_bold_smooth[reg],
suffix=Suffix.BOLD,
desc=f"{_smooth_label(smooth)}preproc",
extra={"reg": bids_safe_label(reg)},
)
mni.save(outputs.template_bold, suffix=Suffix.BOLD, desc="preproc")
mni.save(outputs.template_brain_mask, suffix=Suffix.MASK, desc="bold")

Expand Down
15 changes: 15 additions & 0 deletions src/rbc/bids/longitudinal/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
from rbc.workflows.longitudinal.functional import FunctionalLongOutputs


def _smooth_label(fwhm: float, precision: int | None = None) -> str:
"""Format FWHM as a BIDS-safe label (e.g. 6.0 -> 'sm6', 0.1 -> 'sm0p1')."""
s = f"{fwhm:.{precision}f}" if precision is not None else str(fwhm)
return "sm" + s.rstrip("0").rstrip(".").replace(".", "p")


def resolve_longitudinal_func(
func_q: Bids,
tpl_q: Bids,
Expand Down Expand Up @@ -81,13 +87,15 @@ def export_longitudinal_func(
outputs: FunctionalLongOutputs,
*,
regressors: Sequence[str],
smooth: float | None = None,
) -> None:
"""Export longitudinal functional outputs.

Args:
fex: Bids builder with ``space="longitudinal"`` and identity entities.
outputs: Results from the longitudinal functional workflow.
regressors: Regressor strategy names that were applied.
smooth: Smoothing kernel FWHM in mm, or ``None`` if not requested.
"""
fex.save(outputs.sbref, suffix=Suffix.SBREF)
fex.save(outputs.bold, suffix=Suffix.BOLD, desc="preproc")
Expand All @@ -112,3 +120,10 @@ def export_longitudinal_func(
desc="preproc",
extra={"reg": bids_safe_label(reg)},
)
if outputs.cleaned_bold_smooth is not None and smooth is not None:
fex.save(
outputs.cleaned_bold_smooth[reg],
suffix=Suffix.BOLD,
desc=f"{_smooth_label(smooth)}preproc",
extra={"reg": bids_safe_label(reg)},
)
46 changes: 40 additions & 6 deletions src/rbc/bids/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
from rbc.workflows.metrics import MetricsOutputs


def _smooth_label(fwhm: float, precision: int | None = None) -> str:
"""Format FWHM as a BIDS-safe label (e.g. 6.0 -> 'sm6', 0.1 -> 'sm0p1')."""
s = f"{fwhm:.{precision}f}" if precision is not None else str(fwhm)
return "sm" + s.rstrip("0").rstrip(".").replace(".", "p")


class MetricsInputs(TypedDict):
"""Resolved functional inputs for the metrics workflow."""

Expand Down Expand Up @@ -64,26 +70,54 @@ def export_metrics(
*,
regressor: str,
atlases: Sequence[str],
smooth: float | None,
) -> None:
"""Export metrics for a single regressor to BIDS-named derivatives.

Raw and z-scored raw maps are always exported. Smoothed and
z-scored smoothed variants are exported only when the corresponding
fields are not None (i.e. when ``smooth`` is not ``None`` in
``single_session_metrics``).

Args:
mni: MNI-space Bids builder (typically from
:func:`~rbc.bids.functional.export_functional`).
outputs: Results from the metrics workflow.
regressor: The regressor this run used.
atlases: Atlas names used for timeseries extraction.
smooth: Smoothing kernel FWHM in mm, or ``None`` if smoothing
was not requested.
"""
mex = mni.derive(extra={"reg": bids_safe_label(regressor)})

# Raw maps
mex.save(outputs.alff, suffix="alff")
mex.save(outputs.falff, suffix="falff")
mex.save(outputs.alff_smooth, suffix="alff", desc="smooth")
mex.save(outputs.falff_smooth, suffix="falff", desc="smooth")
mex.save(outputs.alff_zscored, suffix="alff", desc="smoothZstd")
mex.save(outputs.falff_zscored, suffix="falff", desc="smoothZstd")
mex.save(outputs.reho, suffix="reho")
mex.save(outputs.reho_smooth, suffix="reho", desc="smooth")
mex.save(outputs.reho_zscored, suffix="reho", desc="smoothZstd")

# Z-scored raw maps
mex.save(outputs.alff_zscored, suffix="alff", desc="zstd")
mex.save(outputs.falff_zscored, suffix="falff", desc="zstd")
mex.save(outputs.reho_zscored, suffix="reho", desc="zstd")

# Smoothed + z-scored smoothed
if smooth is not None:
sm_desc = _smooth_label(smooth)
if outputs.alff_smooth is not None:
mex.save(outputs.alff_smooth, suffix="alff", desc=sm_desc)
assert outputs.alff_smooth_zscored is not None # noqa: S101
mex.save(outputs.alff_smooth_zscored, suffix="alff", desc=f"{sm_desc}Zstd")
Comment thread
jpillai00 marked this conversation as resolved.
if outputs.falff_smooth is not None:
mex.save(outputs.falff_smooth, suffix="falff", desc=sm_desc)
assert outputs.falff_smooth_zscored is not None # noqa: S101
mex.save(
outputs.falff_smooth_zscored, suffix="falff", desc=f"{sm_desc}Zstd"
)
if outputs.reho_smooth is not None:
mex.save(outputs.reho_smooth, suffix="reho", desc=sm_desc)
assert outputs.reho_smooth_zscored is not None # noqa: S101
mex.save(outputs.reho_smooth_zscored, suffix="reho", desc=f"{sm_desc}Zstd")

for atl in atlases:
mex.save(
outputs.timeseries[atl],
Expand Down
16 changes: 9 additions & 7 deletions src/rbc/cli/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class AllArgs(BaseArgs):
regressor: Sequence[Literal["36-parameter", "aCompCor"]]
task: str | None
atlas_files: dict[str, Path]
fwhm: float
smooth: float | None
start_tr: int
tr: float | None
brain_extraction_templates: BrainExtractionTemplates
Expand All @@ -51,7 +51,8 @@ class AllArgs(BaseArgs):
def validate_namespace(cls, ns: argparse.Namespace) -> AllArgs:
"""Validate all-workflow arguments."""
_validate_task(ns.task)
_validate_positive(ns.fwhm, "FWHM")
if ns.smooth is not None:
_validate_positive(ns.smooth, "smooth")
_validate_positive(ns.start_tr, "Start TR")
_validate_positive(ns.tr, "TR")
atlas_files = _resolve_atlas_args(ns.atlas)
Expand All @@ -60,7 +61,7 @@ def validate_namespace(cls, ns: argparse.Namespace) -> AllArgs:
regressor=ns.regressor,
task=ns.task,
atlas_files=atlas_files,
fwhm=ns.fwhm,
smooth=ns.smooth,
start_tr=ns.start_tr,
tr=ns.tr,
brain_extraction_templates=_build_brain_extraction_templates(ns),
Expand Down Expand Up @@ -91,7 +92,7 @@ def main(args: AllArgs) -> int:
),
regressors=args.regressor,
atlas_files=args.atlas_files,
fwhm=args.fwhm,
smooth=args.smooth,
start_tr=args.start_tr,
tr=args.tr,
brain_extraction_templates=args.brain_extraction_templates,
Expand Down Expand Up @@ -144,10 +145,11 @@ def register_command(
),
)
parser.add_argument(
"--fwhm",
"--smooth",
type=float,
default=6.0,
help="Smoothing kernel FWHM in mm.",
default=None,
help="Smoothing with the kernel of specified FWHM in mm (e.g. --smooth 6.0) "
"If omitted, no smoothing is applied.",
)
parser.add_argument(
"--start-tr",
Expand Down
14 changes: 14 additions & 0 deletions src/rbc/cli/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class FunctionalArgs(BaseArgs):
regressor: Sequence[Literal["36-parameter", "aCompCor"]]
task: str | None
tr: float | None
smooth: float | None
func_template: Path
func_template_mask: Path
func_template_ref: Path
Expand All @@ -44,12 +45,15 @@ class FunctionalArgs(BaseArgs):
def validate_namespace(cls, ns: argparse.Namespace) -> FunctionalArgs:
"""Validation of functional workflow specific arguments to NamedTuple."""
_validate_task(ns.task)
if ns.smooth is not None:
_validate_positive(ns.smooth, "smooth")
_validate_positive(ns.tr, "TR")
return cls(
**BaseArgs.validate_namespace(ns).__dict__,
regressor=ns.regressor,
task=ns.task,
tr=ns.tr,
smooth=ns.smooth,
func_template=_or_default(
ns.func_template, REGISTRATION_TEMPLATES.brain_2mm
),
Expand All @@ -74,6 +78,7 @@ def main(args: FunctionalArgs) -> int:
),
regressors=args.regressor,
tr=args.tr,
smooth=args.smooth,
func_template=args.func_template,
func_template_mask=args.func_template_mask,
func_template_ref=args.func_template_ref,
Expand Down Expand Up @@ -116,6 +121,15 @@ def register_command(
default=None,
help="Repetition time in seconds. Overrides BIDS sidecar and NIfTI header.",
)
parser.add_argument(
"--smooth",
type=float,
default=None,
metavar="FWHM",
help="Smooth the cleaned (post-regression, bandpass-filtered) BOLD with "
"the kernel of specified FWHM in mm (e.g. --smooth 6.0) "
"If omitted, no smoothing is applied.",
)

templates = parser.add_argument_group("template overrides")
templates.add_argument(
Expand Down
17 changes: 10 additions & 7 deletions src/rbc/cli/longitudinal/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,23 @@ class AllLongArgs(LongitudinalBaseArgs):

registration_template: Path
atlas_files: dict[str, Path]
fwhm: float
smooth: float | None
regressor: Sequence[Literal["36-parameter", "aCompCor"]]
task: str | None

@classmethod
def validate_namespace(cls, ns: argparse.Namespace) -> AllLongArgs:
"""Validate namespace for the full longitudinal pipeline subcommand."""
_validate_positive(ns.fwhm, "FWHM")
if ns.smooth is not None:
_validate_positive(ns.smooth, "smooth")
_validate_task(ns.task)
return cls(
**LongitudinalBaseArgs.validate_namespace(ns).__dict__,
registration_template=_or_default(
ns.anat_template, REGISTRATION_TEMPLATES.brain_1mm
),
atlas_files=_resolve_atlas_args(ns.atlas),
fwhm=ns.fwhm,
smooth=ns.smooth,
regressor=ns.regressor,
task=ns.task,
)
Expand All @@ -63,7 +64,7 @@ def main(args: AllLongArgs) -> int:
regressors=args.regressor,
fs_license=args.fs_license,
atlas_files=args.atlas_files,
fwhm=args.fwhm,
smooth=args.smooth,
runner_config=RunnerConfig(
runner=args.runner,
verbose=bool(args.verbose),
Expand Down Expand Up @@ -117,10 +118,12 @@ def register_command(
),
)
parser.add_argument(
"--fwhm",
"--smooth",
type=float,
default=6.0,
help="Smoothing kernel FWHM in mm.",
default=None,
metavar="FWHM",
help="Smoothing with the kernel of specified FWHM in mm (e.g. --smooth 6.0) "
"If omitted, no smoothing is applied.",
)

templates = parser.add_argument_group("template overrides")
Expand Down
15 changes: 14 additions & 1 deletion src/rbc/cli/longitudinal/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal

from rbc.cli.base import _validate_task
from rbc.cli.base import _validate_positive, _validate_task
from rbc.cli.longitudinal._base import LongitudinalBaseArgs, add_fs_license_argument
from rbc.orchestration import Filters, RunnerConfig
from rbc.orchestration.longitudinal.functional import run
Expand All @@ -21,15 +21,19 @@ class FunctionalLongArgs(LongitudinalBaseArgs):

task: str | None
regressor: Sequence[Literal["36-parameter", "aCompCor"]]
smooth: float | None

@classmethod
def validate_namespace(cls, ns: argparse.Namespace) -> FunctionalLongArgs:
"""Validate namespace for the longitudinal functional subcommand."""
_validate_task(ns.task)
if ns.smooth is not None:
_validate_positive(ns.smooth, "smooth")
return cls(
**LongitudinalBaseArgs.validate_namespace(ns).__dict__,
task=ns.task,
regressor=ns.regressor,
smooth=ns.smooth,
)


Expand All @@ -44,6 +48,7 @@ def main(args: FunctionalLongArgs) -> int:
task=args.task,
),
regressors=args.regressor,
smooth=args.smooth,
runner_config=RunnerConfig(
runner=args.runner,
verbose=bool(args.verbose),
Expand Down Expand Up @@ -78,6 +83,14 @@ def register_command(
default=None,
help="Task label to filter BOLD runs (without 'task-' prefix).",
)
parser.add_argument(
"--smooth",
type=float,
default=None,
metavar="FWHM",
help="Smoothing with the kernel of specified FWHM in mm (e.g. --smooth 6.0) "
"If omitted, no smoothing is applied.",
)
parser.add_argument(
"--regressor",
nargs="+",
Expand Down
Loading
Loading