Skip to content

Commit ac8903f

Browse files
authored
Split longitudinal code into dedicated submodules (#305)
Pure file-move refactor. No behavior change. - workflows/{anatomical,functional}.py::longitudinal_process and *LongOutputs move to workflows/longitudinal/{anatomical,functional}.py - bids/longitudinal.py splits into bids/longitudinal/{anatomical,functional}.py - orchestration/longitudinal.py splits into orchestration/longitudinal/{__init__,anatomical,functional}.py (run() stays in __init__; process_anat / process_func move out) All callers updated atomically; no re-export shims. Test patch paths updated to reference the new submodule locations. Stage 1 of longitudinal refactor (tracker #301).
1 parent 100c00a commit ac8903f

12 files changed

Lines changed: 385 additions & 303 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""BIDS resolve and export helpers for the longitudinal workflow."""
Lines changed: 2 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""BIDS resolve and export for the longitudinal workflow."""
1+
"""BIDS resolve and export for the longitudinal anatomical workflow."""
22

33
from __future__ import annotations
44

@@ -12,8 +12,7 @@
1212
import polars as pl
1313

1414
from rbc.bids import Bids
15-
from rbc.workflows.anatomical import AnatomicalLongOutputs
16-
from rbc.workflows.functional import FunctionalLongOutputs
15+
from rbc.workflows.longitudinal.anatomical import AnatomicalLongOutputs
1716

1817

1918
def _require_file(path: Path | None, field: str) -> Path:
@@ -105,67 +104,3 @@ def export_longitudinal_anat(aex: Bids, outputs: AnatomicalLongOutputs) -> None:
105104
"mode": "image",
106105
},
107106
)
108-
109-
110-
def resolve_longitudinal_func(
111-
func_q: Bids,
112-
tpl_q: Bids,
113-
func_df: pl.DataFrame,
114-
tpl_df: pl.DataFrame,
115-
*,
116-
ses: str,
117-
) -> dict[str, Path | None]:
118-
"""Resolve inputs for longitudinal functional processing.
119-
120-
Args:
121-
func_q: Bids builder for functional datatype queries.
122-
tpl_q: Bids builder for longitudinal template queries.
123-
func_df: DataFrame of functional derivatives.
124-
tpl_df: DataFrame of longitudinal template files.
125-
ses: Session label (used for template xfm lookup).
126-
127-
Returns:
128-
Dict with keys matching ``longitudinal_process`` parameters.
129-
"""
130-
return {
131-
"template": tpl_q.expect(tpl_df, suffix="T1w"),
132-
"anat_to_template_xfm": tpl_q.expect(
133-
tpl_df,
134-
suffix="xfm",
135-
extension=".txt",
136-
extra={"from": ses},
137-
),
138-
"bold_to_anat_itk": func_q.expect(
139-
func_df,
140-
suffix="xfm",
141-
desc="linearITK",
142-
extension=".txt",
143-
extra={"from": "bold", "to": "T1w", "mode": "image"},
144-
),
145-
"sbref": func_q.expect(func_df, suffix=Suffix.SBREF, without=["space"]),
146-
"bold": func_q.expect(
147-
func_df, suffix=Suffix.BOLD, desc="preproc", without=["space"]
148-
),
149-
"bold_mask": func_q.find(
150-
func_df, suffix=Suffix.MASK, desc="brain", without=["space"]
151-
),
152-
}
153-
154-
155-
def export_longitudinal_func(fex: Bids, outputs: FunctionalLongOutputs) -> None:
156-
"""Export longitudinal functional outputs.
157-
158-
Args:
159-
fex: Bids builder with ``space="longitudinal"`` and identity entities.
160-
outputs: Results from the longitudinal functional workflow.
161-
"""
162-
fex.save(outputs.sbref, suffix=Suffix.SBREF)
163-
fex.save(outputs.bold, suffix=Suffix.BOLD, desc="preproc")
164-
fex.save(
165-
outputs.bold_to_long_xfm,
166-
suffix="xfm",
167-
desc="composite",
168-
extra={"from": "bold", "to": "longitudinal", "mode": "image"},
169-
)
170-
if outputs.bold_mask:
171-
fex.save(outputs.bold_mask, suffix=Suffix.MASK, desc="brain")
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""BIDS resolve and export for the longitudinal functional workflow."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from rbc.bids import Suffix
8+
9+
if TYPE_CHECKING:
10+
from pathlib import Path
11+
12+
import polars as pl
13+
14+
from rbc.bids import Bids
15+
from rbc.workflows.longitudinal.functional import FunctionalLongOutputs
16+
17+
18+
def resolve_longitudinal_func(
19+
func_q: Bids,
20+
tpl_q: Bids,
21+
func_df: pl.DataFrame,
22+
tpl_df: pl.DataFrame,
23+
*,
24+
ses: str,
25+
) -> dict[str, Path | None]:
26+
"""Resolve inputs for longitudinal functional processing.
27+
28+
Args:
29+
func_q: Bids builder for functional datatype queries.
30+
tpl_q: Bids builder for longitudinal template queries.
31+
func_df: DataFrame of functional derivatives.
32+
tpl_df: DataFrame of longitudinal template files.
33+
ses: Session label (used for template xfm lookup).
34+
35+
Returns:
36+
Dict with keys matching ``longitudinal_process`` parameters.
37+
"""
38+
return {
39+
"template": tpl_q.expect(tpl_df, suffix="T1w"),
40+
"anat_to_template_xfm": tpl_q.expect(
41+
tpl_df,
42+
suffix="xfm",
43+
extension=".txt",
44+
extra={"from": ses},
45+
),
46+
"bold_to_anat_itk": func_q.expect(
47+
func_df,
48+
suffix="xfm",
49+
desc="linearITK",
50+
extension=".txt",
51+
extra={"from": "bold", "to": "T1w", "mode": "image"},
52+
),
53+
"sbref": func_q.expect(func_df, suffix=Suffix.SBREF, without=["space"]),
54+
"bold": func_q.expect(
55+
func_df, suffix=Suffix.BOLD, desc="preproc", without=["space"]
56+
),
57+
"bold_mask": func_q.find(
58+
func_df, suffix=Suffix.MASK, desc="brain", without=["space"]
59+
),
60+
}
61+
62+
63+
def export_longitudinal_func(fex: Bids, outputs: FunctionalLongOutputs) -> None:
64+
"""Export longitudinal functional outputs.
65+
66+
Args:
67+
fex: Bids builder with ``space="longitudinal"`` and identity entities.
68+
outputs: Results from the longitudinal functional workflow.
69+
"""
70+
fex.save(outputs.sbref, suffix=Suffix.SBREF)
71+
fex.save(outputs.bold, suffix=Suffix.BOLD, desc="preproc")
72+
fex.save(
73+
outputs.bold_to_long_xfm,
74+
suffix="xfm",
75+
desc="composite",
76+
extra={"from": "bold", "to": "longitudinal", "mode": "image"},
77+
)
78+
if outputs.bold_mask:
79+
fex.save(outputs.bold_mask, suffix=Suffix.MASK, desc="brain")

src/rbc/orchestration/longitudinal.py renamed to src/rbc/orchestration/longitudinal/__init__.py

Lines changed: 4 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -11,94 +11,22 @@
1111
from rbc.bids import (
1212
FUNC_GROUP_ENTITIES,
1313
SUB_SES_QUERY,
14-
Datatype,
15-
Suffix,
16-
extract_entities,
1714
load_table,
1815
)
19-
from rbc.bids.longitudinal import (
20-
export_longitudinal_anat,
21-
export_longitudinal_func,
22-
resolve_longitudinal_anat,
23-
resolve_longitudinal_func,
24-
)
2516
from rbc.bids.session import iter_session_files, load_session
2617
from rbc.context import RunContext
2718
from rbc.orchestration import Filters, RunnerConfig, init_runner
28-
from rbc.workflows.anatomical import longitudinal_process as anatomical_longitudinal
29-
from rbc.workflows.functional import longitudinal_process as functional_longitudinal
19+
from rbc.orchestration.longitudinal.anatomical import process_anat
20+
from rbc.orchestration.longitudinal.functional import process_func
3021
from rbc_resources import REGISTRATION_TEMPLATES
3122

3223
if TYPE_CHECKING:
3324
from collections.abc import Sequence
3425
from pathlib import Path
3526

36-
_logger = logging.getLogger(__name__)
37-
38-
39-
def process_anat(
40-
pipe_ctx: RunContext,
41-
anat_df: pl.DataFrame,
42-
tpl_df: pl.DataFrame,
43-
registration_template: Path = REGISTRATION_TEMPLATES.brain_1mm,
44-
) -> None:
45-
"""Handle anatomical longitudinal processing for one anat group.
27+
__all__ = ["process_anat", "process_func", "run"]
4628

47-
Args:
48-
pipe_ctx: RunContext bound to this subject/session.
49-
anat_df: Anatomical derivative DataFrame for this group.
50-
tpl_df: Longitudinal template DataFrame.
51-
registration_template: Brain template for ANTs registration.
52-
"""
53-
anat_df = anat_df.filter(pl.col("space").is_null())
54-
ents = extract_entities(anat_df.row(0, named=True), ["run"])
55-
56-
anat_q = pipe_ctx.bids(datatype=Datatype.ANAT)
57-
tpl_q = anat_q.derive(ses="longitudinal")
58-
59-
resolved = resolve_longitudinal_anat(
60-
anat_q,
61-
tpl_q,
62-
anat_df,
63-
tpl_df,
64-
ses=pipe_ctx.ses, # type: ignore[arg-type]
65-
)
66-
outputs = anatomical_longitudinal(
67-
**resolved, # type: ignore[arg-type]
68-
registration_template=registration_template,
69-
)
70-
aex = anat_q.derive(entities=ents, space="longitudinal")
71-
export_longitudinal_anat(aex, outputs)
72-
73-
74-
def process_func(
75-
pipe_ctx: RunContext,
76-
func_df: pl.DataFrame,
77-
tpl_df: pl.DataFrame,
78-
) -> None:
79-
"""Handle functional longitudinal processing for one BOLD run.
80-
81-
Args:
82-
pipe_ctx: RunContext bound to this subject/session.
83-
func_df: Functional derivative DataFrame for this run.
84-
tpl_df: Longitudinal template DataFrame.
85-
"""
86-
row = func_df.filter(suffix=Suffix.BOLD).row(0, named=True)
87-
ents = extract_entities(row, ["task", "run"])
88-
89-
func_q = pipe_ctx.bids(datatype=Datatype.FUNC, entities=ents)
90-
tpl_q = pipe_ctx.bids(datatype=Datatype.ANAT).derive(ses="longitudinal")
91-
92-
resolved = resolve_longitudinal_func(
93-
func_q,
94-
tpl_q,
95-
func_df,
96-
tpl_df,
97-
ses=pipe_ctx.ses, # type: ignore[arg-type]
98-
)
99-
func_outputs = functional_longitudinal(**resolved) # type: ignore[arg-type]
100-
fex = func_q.derive(space="longitudinal")
101-
export_longitudinal_func(fex, func_outputs)
29+
_logger = logging.getLogger(__name__)
10230

10331

10432
def run(
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Per-group anatomical longitudinal processing."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
import polars as pl
8+
9+
from rbc.bids import Datatype, extract_entities
10+
from rbc.bids.longitudinal.anatomical import (
11+
export_longitudinal_anat,
12+
resolve_longitudinal_anat,
13+
)
14+
from rbc.workflows.longitudinal.anatomical import (
15+
longitudinal_process as anatomical_longitudinal,
16+
)
17+
from rbc_resources import REGISTRATION_TEMPLATES
18+
19+
if TYPE_CHECKING:
20+
from pathlib import Path
21+
22+
from rbc.context import RunContext
23+
24+
25+
def process_anat(
26+
pipe_ctx: RunContext,
27+
anat_df: pl.DataFrame,
28+
tpl_df: pl.DataFrame,
29+
registration_template: Path = REGISTRATION_TEMPLATES.brain_1mm,
30+
) -> None:
31+
"""Handle anatomical longitudinal processing for one anat group.
32+
33+
Args:
34+
pipe_ctx: RunContext bound to this subject/session.
35+
anat_df: Anatomical derivative DataFrame for this group.
36+
tpl_df: Longitudinal template DataFrame.
37+
registration_template: Brain template for ANTs registration.
38+
"""
39+
anat_df = anat_df.filter(pl.col("space").is_null())
40+
ents = extract_entities(anat_df.row(0, named=True), ["run"])
41+
42+
anat_q = pipe_ctx.bids(datatype=Datatype.ANAT)
43+
tpl_q = anat_q.derive(ses="longitudinal")
44+
45+
resolved = resolve_longitudinal_anat(
46+
anat_q,
47+
tpl_q,
48+
anat_df,
49+
tpl_df,
50+
ses=pipe_ctx.ses, # type: ignore[arg-type]
51+
)
52+
outputs = anatomical_longitudinal(
53+
**resolved, # type: ignore[arg-type]
54+
registration_template=registration_template,
55+
)
56+
aex = anat_q.derive(entities=ents, space="longitudinal")
57+
export_longitudinal_anat(aex, outputs)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Per-run functional longitudinal processing."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from rbc.bids import Datatype, Suffix, extract_entities
8+
from rbc.bids.longitudinal.functional import (
9+
export_longitudinal_func,
10+
resolve_longitudinal_func,
11+
)
12+
from rbc.workflows.longitudinal.functional import (
13+
longitudinal_process as functional_longitudinal,
14+
)
15+
16+
if TYPE_CHECKING:
17+
import polars as pl
18+
19+
from rbc.context import RunContext
20+
21+
22+
def process_func(
23+
pipe_ctx: RunContext,
24+
func_df: pl.DataFrame,
25+
tpl_df: pl.DataFrame,
26+
) -> None:
27+
"""Handle functional longitudinal processing for one BOLD run.
28+
29+
Args:
30+
pipe_ctx: RunContext bound to this subject/session.
31+
func_df: Functional derivative DataFrame for this run.
32+
tpl_df: Longitudinal template DataFrame.
33+
"""
34+
row = func_df.filter(suffix=Suffix.BOLD).row(0, named=True)
35+
ents = extract_entities(row, ["task", "run"])
36+
37+
func_q = pipe_ctx.bids(datatype=Datatype.FUNC, entities=ents)
38+
tpl_q = pipe_ctx.bids(datatype=Datatype.ANAT).derive(ses="longitudinal")
39+
40+
resolved = resolve_longitudinal_func(
41+
func_q,
42+
tpl_q,
43+
func_df,
44+
tpl_df,
45+
ses=pipe_ctx.ses, # type: ignore[arg-type]
46+
)
47+
func_outputs = functional_longitudinal(**resolved) # type: ignore[arg-type]
48+
fex = func_q.derive(space="longitudinal")
49+
export_longitudinal_func(fex, func_outputs)

0 commit comments

Comments
 (0)