Skip to content
Closed
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
6 changes: 6 additions & 0 deletions src/rbc/bids/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ def export_functional(
desc=bids_safe_label(reg),
extension=".1D",
)
func.save(
outputs.bpf_regressor_file[reg],
suffix="regressors+filtered",
desc=bids_safe_label(reg),
extension=".1D",
)

mni = func.derive(space=TemplateSpace.MNI152NLIN6ASYM)
for reg in regressors:
Expand Down
109 changes: 105 additions & 4 deletions src/rbc/bids/longitudinal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypedDict

from rbc.bids import Suffix, TemplateSpace
from rbc.bids import Suffix, TemplateSpace, bids_safe_label
from rbc.bids.metrics import export_metrics

if TYPE_CHECKING:
from collections.abc import Sequence
from pathlib import Path

import polars as pl

from rbc.bids import Bids
from rbc.workflows.anatomical import AnatomicalLongOutputs
from rbc.workflows.functional import FunctionalLongOutputs
from rbc.workflows.metrics import MetricsOutputs


def _require_file(path: Path | None, field: str) -> Path:
Expand Down Expand Up @@ -107,14 +110,27 @@ def export_longitudinal_anat(aex: Bids, outputs: AnatomicalLongOutputs) -> None:
)


class LongitudinalFuncInputs(TypedDict):
"""Resolved inputs for the longitudinal functional workflow."""

template: Path
anat_to_template_xfm: Path
bold_to_anat_itk: Path
sbref: Path
bold: Path
bold_mask: Path
regressor_files: dict[str, Path]


def resolve_longitudinal_func(
func_q: Bids,
tpl_q: Bids,
func_df: pl.DataFrame,
tpl_df: pl.DataFrame,
*,
ses: str,
) -> dict[str, Path | None]:
regressors: Sequence[str],
) -> LongitudinalFuncInputs:
"""Resolve inputs for longitudinal functional processing.

Args:
Expand All @@ -123,6 +139,7 @@ def resolve_longitudinal_func(
func_df: DataFrame of functional derivatives.
tpl_df: DataFrame of longitudinal template files.
ses: Session label (used for template xfm lookup).
regressors: Regressor names (e.g. ``["36-parameter"]``).

Returns:
Dict with keys matching ``longitudinal_process`` parameters.
Expand All @@ -146,9 +163,15 @@ def resolve_longitudinal_func(
"bold": func_q.expect(
func_df, suffix=Suffix.BOLD, desc="preproc", without=["space"]
),
"bold_mask": func_q.find(
"bold_mask": func_q.expect(
func_df, suffix=Suffix.MASK, desc="brain", without=["space"]
),
"regressor_files": {
reg: func_q.expect(
func_df, suffix="regressors", desc=bids_safe_label(reg), extension=".1D"
)
for reg in regressors
},
}


Expand All @@ -167,5 +190,83 @@ def export_longitudinal_func(fex: Bids, outputs: FunctionalLongOutputs) -> None:
desc="composite",
extra={"from": "bold", "to": "longitudinal", "mode": "image"},
)
fex.save(outputs.bold_mask, suffix=Suffix.MASK, desc="brain")
for reg, path in outputs.regressed_bold.items():
fex.save(
path,
suffix=Suffix.BOLD,
desc="regressed",
extra={"reg": bids_safe_label(reg)},
)
for reg, path in outputs.cleaned_bold.items():
fex.save(
path,
suffix=Suffix.BOLD,
desc="preproc",
extra={"reg": bids_safe_label(reg)},
)
if outputs.bold_mask:
fex.save(outputs.bold_mask, suffix=Suffix.MASK, desc="brain")


class LongitudinalMetricsInputs(TypedDict):
"""Resolved inputs for longitudinal metrics."""

regressed_bold: Path
cleaned_bold: Path
template_brain_mask: Path


def resolve_longitudinal_metrics(
long_q: Bids,
func_df: pl.DataFrame,
*,
regressor: str,
) -> LongitudinalMetricsInputs:
"""Resolve inputs for longitudinal metrics.

Args:
long_q: Bids builder with ``space="longitudinal"`` and identity entities.
func_df: DataFrame of longitudinal functional derivatives.
regressor: Regressor name (e.g. ``"36-parameter"``).

Returns:
Dict with keys matching ``single_session_metrics`` parameters.
"""
return {
"regressed_bold": long_q.expect(
func_df,
suffix=Suffix.BOLD,
desc="regressed",
extra={"reg": bids_safe_label(regressor)},
),
"cleaned_bold": long_q.expect(
func_df,
suffix=Suffix.BOLD,
desc="preproc",
extra={"reg": bids_safe_label(regressor)},
),
"template_brain_mask": long_q.expect(
func_df,
suffix=Suffix.MASK,
desc="brain",
),
}


def export_longitudinal_metrics(
long_q: Bids,
outputs: MetricsOutputs,
*,
regressor: str,
atlases: list[str],
) -> None:
"""Export longitudinal metrics outputs.

Args:
long_q: Bids builder with ``space="longitudinal"`` and identity entities.
outputs: Results from the metrics workflow.
regressor: Regressor name.
atlases: Atlas labels.
"""
export_metrics(long_q, outputs, regressor=regressor, atlases=atlases)
44 changes: 39 additions & 5 deletions src/rbc/cli/longitudinal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal

from rbc.cli.base import BaseArgs, _or_default, _validate_nifti_path
from rbc.cli.metrics import _resolve_atlas_args
from rbc.orchestration import Filters, RunnerConfig
from rbc.orchestration.longitudinal import run
from rbc_resources import REGISTRATION_TEMPLATES
from rbc_resources import ATLAS_REGISTRY, REGISTRATION_TEMPLATES

if TYPE_CHECKING:
import argparse
Expand All @@ -22,22 +23,29 @@ class LongitudinalArgs(BaseArgs):

anatomical: bool
functional: bool
metrics: bool
registration_template: Path
regressor: Sequence[Literal["36-parameter", "aCompCor"]]
atlas_files: dict[str, Path]

@classmethod
def validate_namespace(cls, ns: argparse.Namespace) -> LongitudinalArgs:
"""Validation of longitudinal workflow specific arguments to NamedTuple."""
if not ns.functional and not ns.anatomical:
if not ns.functional and not ns.anatomical and not ns.metrics:
raise ValueError(
"At least one of '--anatomical' or '--functional' is required."
"At least one of '--anatomical', '--functional', "
"or '--metrics' is required."
)
return cls(
**BaseArgs.validate_namespace(ns).__dict__,
anatomical=ns.anatomical,
functional=ns.functional,
metrics=ns.metrics,
registration_template=_or_default(
ns.anat_template, REGISTRATION_TEMPLATES.brain_1mm
),
regressor=ns.regressor,
atlas_files=_resolve_atlas_args(ns.atlas),
)


Expand All @@ -52,7 +60,10 @@ def main(args: LongitudinalArgs) -> int:
),
anatomical=args.anatomical,
functional=args.functional,
metrics=args.metrics,
registration_template=args.registration_template,
regressors=args.regressor,
atlas_files=args.atlas_files,
runner_config=RunnerConfig(
runner=args.runner,
verbose=bool(args.verbose),
Expand Down Expand Up @@ -86,7 +97,30 @@ def register_command(
action="store_true",
help="Use functional longitudinal pipeline for processing",
)

parser.add_argument(
"--regressor",
nargs="+",
choices=["36-parameter", "aCompCor"],
default=["36-parameter"],
help="Space-delimited nuisance regression method(s) to apply.",
)
parser.add_argument(
"--metrics",
default=False,
action="store_true",
help="Compute longitudinal metrics (ALFF, ReHo, timeseries).",
)
parser.add_argument(
"--atlas",
nargs="+",
default=["schaefer_200"],
metavar="ATLAS",
help=(
"Atlas(es) for timeseries extraction. Accepts registry names "
f"({', '.join(sorted(ATLAS_REGISTRY))}) or paths to custom NIfTI "
"atlas files."
),
)
templates = parser.add_argument_group("template overrides")
templates.add_argument(
"--anat-template",
Expand Down
Loading