Skip to content
Merged
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
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,29 @@ Run any command with `--help` for full options.

## Workflows

| Command | Description |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| `rbc anatomical` | Brain extraction (ANTs), tissue segmentation (FSL FAST), registration to MNI152 |
| `rbc functional` | Motion correction, slice timing, BBR coregistration, single-step resampling, nuisance regression |
| `rbc metrics` | ALFF/fALFF, ReHo, smoothing, z-scoring, atlas-based timeseries and correlation matrices |
| `rbc qc` | XCP-D format quality metrics, framewise displacement, DVARS, RBC pass/fail thresholds |
| `rbc all` | Runs all four stages in sequence, passing results in memory between stages |
| Command | Description |
| -------------------- | ------------------------------------------------------------------------------------------------ |
| `rbc anatomical` | Brain extraction (ANTs), tissue segmentation (FSL FAST), registration to MNI152 |
| `rbc functional` | Motion correction, slice timing, BBR coregistration, single-step resampling, nuisance regression |
| `rbc metrics` | ALFF/fALFF, ReHo, smoothing, z-scoring, atlas-based timeseries and correlation matrices |
| `rbc qc` | XCP-D format quality metrics, framewise displacement, DVARS, RBC pass/fail thresholds |
| `rbc all` | Runs all four stages in sequence, passing results in memory between stages |
| `rbc longitudinal …` | Multi-session workflows: `template`, `anatomical`, `functional`, `metrics`, `qc`, `all` |

Workflows must be run in order: `anatomical` → `functional` → `metrics` / `qc`. The `all` command handles this automatically.

Longitudinal workflows consume cross-sectional derivatives. Typical flow:

```bash
rbc all /data -o /data/derivatives --runner docker
rbc longitudinal template /data/derivatives -o /data/derivatives --runner docker
rbc longitudinal anatomical /data/derivatives -o /data/derivatives --runner docker
```

`rbc long` is an alias for `rbc longitudinal`. The `metrics`, `qc`, and `all`
longitudinal stages are registered but raise `NotImplementedError` until
Stage 6 of the longitudinal refactor lands (tracker: #301).

## Outputs

See the [data dictionary](docs/data_dictionary.md) for a complete description of every output file, including format details and the processing step that produces each one.
Expand Down
2 changes: 1 addition & 1 deletion docs/data_dictionary.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Produced by `rbc qc`. A single summary file per functional run containing qualit

## Longitudinal outputs

Produced by `rbc longitudinal`. Anatomical outputs aligned to a subject-specific longitudinal template built from multiple sessions. Note: longitudinal functional processing is not yet implemented.
Produced by the `rbc longitudinal` subcommand group (`template`, `anatomical`, `functional`, `metrics`, `qc`, `all`). Anatomical outputs aligned to a subject-specific longitudinal template built from multiple sessions. Note: longitudinal metrics, QC, and the combined `all` stage are not yet implemented (tracker: #301).

| File | Suffix | Description | Created by | Format |
| -------------------------------------------------- | ------ | ------------------------------------------------------------------------- | ------------------------------------------ | ---------------------------- |
Expand Down
26 changes: 20 additions & 6 deletions src/rbc/cli/longitudinal/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
"""``rbc longitudinal`` parent command and nested subcommand registration.

Stage 2 introduces the nested subcommand layout. The pre-existing
``--anatomical --functional`` flow lives under ``rbc longitudinal process``
until Stage 3 splits it into per-stage subcommands.
Stage 3 lands the full nested subcommand layout. Every stage of the
longitudinal pipeline has a dedicated subcommand; ``metrics``, ``qc``, and
``all`` are registered so they surface in ``--help`` but raise
``NotImplementedError`` until Stage 6.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from rbc.cli.longitudinal import process, template
from rbc.cli.longitudinal import (
all as all_,
)
from rbc.cli.longitudinal import (
anatomical,
functional,
metrics,
qc,
template,
)

if TYPE_CHECKING:
import argparse
Expand All @@ -26,7 +36,7 @@ def register_command(
aliases=["long"],
description="RBC longitudinal workflows",
help="Longitudinal workflows",
usage="rbc longitudinal {template,process} ...",
usage=("rbc longitudinal {template,anatomical,functional,metrics,qc,all} ..."),
)
nested = parser.add_subparsers(
title="longitudinal stages",
Expand All @@ -36,4 +46,8 @@ def register_command(
help="Stage help",
)
template.register_command(nested, parents=parents)
process.register_command(nested, parents=parents)
anatomical.register_command(nested, parents=parents)
functional.register_command(nested, parents=parents)
metrics.register_command(nested, parents=parents)
qc.register_command(nested, parents=parents)
all_.register_command(nested, parents=parents)
46 changes: 46 additions & 0 deletions src/rbc/cli/longitudinal/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Shared base arguments for ``rbc longitudinal`` subcommands."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING

from rbc.cli.base import BaseArgs

if TYPE_CHECKING:
import argparse


@dataclass(frozen=True)
class LongitudinalBaseArgs(BaseArgs):
"""Base args for longitudinal subcommands.

Adds ``--fs-license`` on top of :class:`~rbc.cli.base.BaseArgs`.
Only the template stage currently consumes the license, but the flag is
accepted across all longitudinal subcommands for a consistent surface.
"""

fs_license: Path | None

@classmethod
def validate_namespace(cls, ns: argparse.Namespace) -> LongitudinalBaseArgs:
"""Validate base args plus the optional ``--fs-license`` path."""
fs_license: Path | None = ns.fs_license
if fs_license is not None and not fs_license.exists():
raise ValueError(f"FreeSurfer license not found: {fs_license}")
return cls(**BaseArgs.validate_namespace(ns).__dict__, fs_license=fs_license)


def add_fs_license_argument(parser: argparse.ArgumentParser) -> None:
"""Attach the ``--fs-license`` argument to a subcommand parser."""
parser.add_argument(
"--fs-license",
type=Path,
default=None,
help=(
"Optional path to a FreeSurfer license file. Falls back to the "
"FS_LICENSE environment variable, then to a license-free bypass "
"if neither is set. Only the ``template`` stage consumes it."
),
)
110 changes: 110 additions & 0 deletions src/rbc/cli/longitudinal/all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""``rbc longitudinal all`` subcommand (placeholder for Stage 6)."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from rbc.cli.base import _or_default, _validate_nifti_path, _validate_positive
from rbc.cli.longitudinal._base import LongitudinalBaseArgs, add_fs_license_argument
from rbc.cli.metrics import _resolve_atlas_args
from rbc.orchestration import Filters, RunnerConfig
from rbc.orchestration.longitudinal.all import run
from rbc_resources import ATLAS_REGISTRY, REGISTRATION_TEMPLATES

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


@dataclass(frozen=True)
class AllLongArgs(LongitudinalBaseArgs):
"""Arguments for ``rbc longitudinal all``."""

registration_template: Path
atlas_files: dict[str, Path]
fwhm: float

@classmethod
def validate_namespace(cls, ns: argparse.Namespace) -> AllLongArgs:
"""Validate namespace for the full longitudinal pipeline subcommand."""
_validate_positive(ns.fwhm, "FWHM")
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,
)


def main(args: AllLongArgs) -> int:
"""Run the combined longitudinal pipeline."""
run(
input_dirs=args.input_dirs,
output_dir=args.output_dir,
filters=Filters(
participant_label=args.participant_label,
session_label=args.session_label,
),
fs_license=args.fs_license,
atlas_files=args.atlas_files,
fwhm=args.fwhm,
runner_config=RunnerConfig(
runner=args.runner,
verbose=bool(args.verbose),
tmp_dir=args.tmp_dir,
ants_threads=args.ants_threads,
),
)
return 0


def register_command(
subparsers: argparse._SubParsersAction,
parents: Sequence[argparse.ArgumentParser],
) -> None:
"""Register ``rbc longitudinal all`` (Stage 6 placeholder)."""
parser = subparsers.add_parser(
"all",
parents=parents,
description=(
"Run the full longitudinal pipeline (template → anat → func → "
"metrics → qc). Placeholder wired up by Stage 3; full "
"implementation ships in Stage 6."
),
help="Full longitudinal pipeline (Stage 6)",
usage=(
"rbc longitudinal all INPUT_DIR [INPUT_DIR ...] -o OUTPUT_DIR [options]"
),
)
add_fs_license_argument(parser)
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."
),
)
parser.add_argument(
"--fwhm",
type=float,
default=6.0,
help="Smoothing kernel FWHM in mm.",
)

templates = parser.add_argument_group("template overrides")
templates.add_argument(
"--anat-template",
type=_validate_nifti_path,
default=None,
help="Custom brain template for anatomical registration.",
)

parser.set_defaults(func=lambda args: main(AllLongArgs.validate_namespace(args)))
87 changes: 87 additions & 0 deletions src/rbc/cli/longitudinal/anatomical.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""``rbc longitudinal anatomical`` subcommand."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from rbc.cli.base import _or_default, _validate_nifti_path
from rbc.cli.longitudinal._base import LongitudinalBaseArgs, add_fs_license_argument
from rbc.orchestration import Filters, RunnerConfig
from rbc.orchestration.longitudinal.anatomical import run
from rbc_resources import REGISTRATION_TEMPLATES

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


@dataclass(frozen=True)
class AnatomicalLongArgs(LongitudinalBaseArgs):
"""Arguments for ``rbc longitudinal anatomical``."""

registration_template: Path

@classmethod
def validate_namespace(cls, ns: argparse.Namespace) -> AnatomicalLongArgs:
"""Validate namespace for the longitudinal anatomical subcommand."""
return cls(
**LongitudinalBaseArgs.validate_namespace(ns).__dict__,
registration_template=_or_default(
ns.anat_template, REGISTRATION_TEMPLATES.brain_1mm
),
)


def main(args: AnatomicalLongArgs) -> int:
"""Run the longitudinal anatomical stage."""
run(
input_dirs=args.input_dirs,
output_dir=args.output_dir,
filters=Filters(
participant_label=args.participant_label,
session_label=args.session_label,
),
registration_template=args.registration_template,
runner_config=RunnerConfig(
runner=args.runner,
verbose=bool(args.verbose),
tmp_dir=args.tmp_dir,
ants_threads=args.ants_threads,
),
)
return 0


def register_command(
subparsers: argparse._SubParsersAction,
parents: Sequence[argparse.ArgumentParser],
) -> None:
"""Register ``rbc longitudinal anatomical`` on a longitudinal subparser group."""
parser = subparsers.add_parser(
"anatomical",
parents=parents,
description=(
"Warp preprocessed anatomical derivatives into each subject's "
"longitudinal template space."
),
help="Longitudinal anatomical stage",
usage=(
"rbc longitudinal anatomical INPUT_DIR [INPUT_DIR ...] "
"-o OUTPUT_DIR [options]"
),
)
add_fs_license_argument(parser)

templates = parser.add_argument_group("template overrides")
templates.add_argument(
"--anat-template",
type=_validate_nifti_path,
default=None,
help="Custom brain template for anatomical registration.",
)

parser.set_defaults(
func=lambda args: main(AnatomicalLongArgs.validate_namespace(args))
)
Loading
Loading