Skip to content

Functional initialization & motion correction#39

Merged
nx10 merged 40 commits into
add-nib-and-fix-syntaxfrom
enh/functional
Feb 10, 2026
Merged

Functional initialization & motion correction#39
nx10 merged 40 commits into
add-nib-and-fix-syntaxfrom
enh/functional

Conversation

@jpillai00
Copy link
Copy Markdown
Contributor

Starting functional workflow implementation addressing #16 & #17 -- sets up core initialization and motion reference + correction. Still need to add testing for these.

Currently, the workflow saves all intermediate outputs but I'm assuming we don't need all of that saved?

@jpillai00 jpillai00 requested review from kaitj and nx10 February 5, 2026 21:11
Copy link
Copy Markdown
Contributor

@kaitj kaitj left a comment

Choose a reason for hiding this comment

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

This will need a rebase from main to pull in all the changes + linting / formatting.

Comment thread src/rbc/core/functional/initialization.py
Comment thread src/rbc/core/functional/initialization.py
Comment thread src/rbc/core/functional/motion.py Outdated
Comment thread src/rbc/core/functional/motion.py
Comment thread src/rbc/core/functional/motion.py Outdated
)

def motion_correction(
in_file: Path, ref_file: Path, output_prefix: str
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think there might be an extra tab in this line.

Comment thread src/rbc/workflows/functional.py Outdated

func_out_dir = output_dir / bids(datatype="func", directory=True)

save_directory(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So would this save the directory as <directory>/sub-<subject>[/ses-<session>]/func/sub-<subject>_[ses-<session>]_desc-motion_mat/?

While nothing inherently wrong with this AFAIK with respect to bids derivatives, we would still want the files within the folder to be BIDS-esque for the end user (e.g. file names should be somewhat bids compliant). Alternatively, could just save the files directly into the output folder. Can chat more about this offline if this comment is confusing!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Currently, inside /func, the directory is named sub-<subject>_ses-<session>_motionMatrices. The matrices are saved inside as MAT_0000, MAT_0001, etc., with a matrix for each volume (this is just the default naming from mcflirt I think). I thought saving them to a directory would clutter the /func folder a bit less, but if saving them directly into the output is better (with something like sub-<subject>_[ses-<session>]_desc-motion_mat0000), that works too!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No I think this makes sense then. If its 1 matrix per volume having the folder is better than saving N number of matrices in the parent folder.

Comment thread test_functional.py Outdated
Comment thread test_functional.py Outdated
Comment thread test_functional.py Outdated
Comment thread test_functional.py Outdated
@kaitj
Copy link
Copy Markdown
Contributor

kaitj commented Feb 6, 2026

Also realized I missed your question:

Currently, the workflow saves all intermediate outputs but I'm assuming we don't need all of that saved?

We won't need to save everything, only what is important in the final output directory (@nx10 do we still have that list or I can find it later).

The way I've been going about it is treating runner.data_dir as a temporary working directory, with files essentially being "removed" once the workflow is completed, hence the need to save the files to a final output directory with bids naming. This just requires less overall disk space. I think we'll want an option to keep the intermediate directory for debugging purposes though.

@childmindresearch childmindresearch deleted a comment from codecov Bot Feb 7, 2026
@jpillai00
Copy link
Copy Markdown
Contributor Author

Added unit + integration tests for this

Also do we even want the transformation matrices saved after motion correction? I added it because it was in the reimplementation guide, but it's not in the output spreadsheet.

@nx10 let me know if we don't need it and I'll remove from the workflow. otherwise, this should be ready for review

@nx10
Copy link
Copy Markdown
Contributor

nx10 commented Feb 9, 2026

Amazing! I'll have a closer look at this tomorrow, but re transform matrices:

As far as I remember cpac/fmriprep save these as massive voxel-wise xfm nifti files which we want to avoid (as the affine transform matrices are much smaller). So I believe as you have them right now is exactly what we what we want.

Comment thread src/rbc/core/functional/motion.py Outdated
Comment on lines +53 to +63
motion_mat_dir = [
d for d in Path(mc_result.root).iterdir() if d.is_dir() and d.suffix == ".mat"
]

return SimpleNamespace(
bold=Path(mc_result.out_file),
par=Path(mc_result.par_file),
rms_rel=Path(mc_result.rmsrel_files),
rms_abs=Path(mc_result.rmsabs_files),
mat_dir=motion_mat_dir[0],
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
motion_mat_dir = [
d for d in Path(mc_result.root).iterdir() if d.is_dir() and d.suffix == ".mat"
]
return SimpleNamespace(
bold=Path(mc_result.out_file),
par=Path(mc_result.par_file),
rms_rel=Path(mc_result.rmsrel_files),
rms_abs=Path(mc_result.rmsabs_files),
mat_dir=motion_mat_dir[0],
)
motion_mat_dir = None
for d in Path(mc_result.root).iterdir():
if d.is_dir() and d.suffix == ".mat":
motion_mat_dir = d
break
return SimpleNamespace(
bold=Path(mc_result.out_file),
par=Path(mc_result.par_file),
rms_rel=Path(mc_result.rmsrel_files),
rms_abs=Path(mc_result.rmsabs_files),
mat_dir=motion_mat_dir,
)

Two minor suggestions here:

  1. Break out of the loop once the first matrix is found; this step is probably quite quick anyways even without the break, but 🤷‍♂️
  2. This change also ensures there is not an index error if the motion_mat_dir list is empty. That being said, we probably want to raise some sort of error if there is no corresponding directory?

Comment thread src/rbc/workflows/functional.py Outdated
Comment thread src/rbc/workflows/functional.py Outdated
@kaitj
Copy link
Copy Markdown
Contributor

kaitj commented Feb 9, 2026

As far as I remember cpac/fmriprep save these as massive voxel-wise xfm nifti files which we want to avoid (as the affine transform matrices are much smaller). So I believe as you have them right now is exactly what we what we want.

This is only true if the transforms are only linear (saving the affines only; which I think they were, but we should double check)

@nx10
Copy link
Copy Markdown
Contributor

nx10 commented Feb 9, 2026

Once you've rebased/merged main and addressed all previous issues, mark the branch as ready for review and I'll take a look.

@jpillai00 jpillai00 marked this pull request as ready for review February 9, 2026 20:42
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 9, 2026

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/rbc
   __init__.py00100% 
src/rbc/cli
   __init__.py00100% 
   anatomical.py00100% 
   derivatives.py00100% 
   functional.py00100% 
   longitudinal.py00100% 
   qc.py00100% 
src/rbc/core
   __init__.py10100% 
   bids.py3620100% 
   common.py70100% 
   utils.py33778%53, 67–68, 70–71, 73–74
src/rbc/core/anatomical
   __init__.py30100% 
   registration.py10460%25, 117, 132, 147
   segmentation.py10460%23, 45, 52, 63
src/rbc/core/functional
   __init__.py30100% 
   initialization.py6183%40
   motion.py17947%43, 53–57, 59–60, 62
src/rbc/core/longitudinal
   __init__.py00100% 
src/rbc/core/qc
   __init__.py00100% 
src/rbc/core/resources
   __init__.py80100% 
src/rbc/workflows
   anatomical.py15846%27–28, 30, 34, 37, 40, 45, 55
TOTAL4753393% 

Tests Skipped Failures Errors Time
47 0 💤 0 ❌ 0 🔥 1m 13s ⏱️

Comment thread tests/unit/test_func.py Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These tests dont actually test any functions in this codebase, please remove.

Comment thread tests/integration/test_functional.py Outdated
Comment on lines +28 to +29
new_info = afni.v_3dinfo(dataset=[truncated_bold.output_file], nv=True)
assert int(new_info.info[0]) == original_count - start_tr
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just use nibabel here to read and inspect the header

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same goes for all instances of afni.v_3dinfo below

Comment thread src/rbc/core/functional/motion.py Outdated
Comment on lines +62 to +68
return SimpleNamespace(
bold=Path(mc_result.out_file),
par=Path(mc_result.par_file),
rms_rel=Path(mc_result.rmsrel_files),
rms_abs=Path(mc_result.rmsabs_files),
mat_dir=motion_mat_dir,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use a NamedTuple - I know there are more instances of SimpleNamespace used like this in the codebase right now, its on my todo list to remove them. SimpleNamespace is really more intended for dynamic output patterns and is quite bad for static type checking

Comment thread src/rbc/core/utils.py Outdated
Comment on lines +59 to +74
def save_directory(in_dir: str | Path, out_dir: str | Path, name: str) -> Path:
"""Save a directory by copying it to a new location.

Args:
in_dir: Path to directory to copy.
out_dir: Path to destination directory.
name: BIDS compliant name for the directory.
"""
in_dir = Path(in_dir)
target = Path(out_dir) / name

if not in_dir.is_dir():
raise ValueError(f"Input path {in_dir} is not a directory.")

shutil.copytree(in_dir, target, dirs_exist_ok=True)
return target
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do I need this when I could just use shutil.copytree ? for the input directory check?

Comment thread pyproject.toml Outdated
testpaths = ["tests"]
markers = [
"unit: Fast (<1s) unit tests (e.g. utilities, BIDS parsing, file operations",
"unit: Fast (<1s) unit tests (e.g. utilities, BIDS parsing, file operations)",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"unit: Fast (<1s) unit tests (e.g. utilities, BIDS parsing, file operations)",
"unit: Fast (<1s) unit tests (e.g. utilities, BIDS parsing, file operations",

Correct, but unrelated change - separate PR

Comment thread src/rbc/core/functional/motion.py Outdated
Returns:
AFNI 3dcalc output object.
"""
total_vols = afni.v_3dinfo(dataset=[in_file], nv=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use nibabel to read the header

Comment thread src/rbc/core/functional/motion.py Outdated
Comment on lines +53 to +57
motion_mat_dir = None
for d in Path(mc_result.root).iterdir():
if d.is_dir() and d.suffix == ".mat":
motion_mat_dir = d
break
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I dont think we need to scan here, better match the output file directly with something like Path(mc_result.root) / f"{output_prefix}.mat"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(if need be hardcode the prefix it produces)

Comment thread tests/integration/test_functional.py Outdated
Comment on lines +8 to +9
from rbc.core import functional
from rbc.core.common import reorient
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

prefer absolute imports here

)


def scale(in_file: Path, scale_factor: float = 0.1) -> afni.V3drefitOutputs:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rename to something more descriptive, e.g. scale_bold

@nx10 nx10 changed the base branch from main to add-nib-and-fix-syntax February 10, 2026 18:17
@nx10 nx10 merged commit 5e9083e into add-nib-and-fix-syntax Feb 10, 2026
2 of 6 checks passed
@nx10 nx10 deleted the enh/functional branch February 10, 2026 18:18
nx10 pushed a commit that referenced this pull request Feb 10, 2026
* add nibabel and fix syntax in pyproject.toml

* parantheses in pyproject.toml

* Functional initialization & motion correction (#39)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants