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
16 changes: 7 additions & 9 deletions .github/workflows/run_pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ jobs:
test_with_pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- uses: actions/setup-python@v4
with:
python-version: 3.11
cache: poetry
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Set up Python
run: uv python install 3.11
- name: Install dependencies
run: poetry install
run: uv sync --extra dev
- name: Run Tests
run: poetry run pytest tests/
run: uv run pytest tests/
2 changes: 1 addition & 1 deletion petutils/petutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def collect_anat_and_pet(bids_data: Union[pathlib.Path, BIDSLayout], suffixes=["
mapped_pet_to_anat[subject] = {}
for subject in subjects:
pet_files = bids_data.get(subject=subject, suffix="pet")
anat_files = [a.path for a in bids_data.get("anat",suffix=suffixes, subject=subject, extension=["nii", "nii.gz"])]
anat_files = [a.path for a in bids_data.get(suffix=suffixes, subject=subject, extension=["nii", "nii.gz"])]
# for each pet image file we create an entry our mapping dictionary
for entry in pet_files:
if type(entry) is BIDSImageFile:
Expand Down
1,637 changes: 0 additions & 1,637 deletions poetry.lock

This file was deleted.

35 changes: 20 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
[tool.poetry]
[project]
name = "petutils"
version = "0.1.0"
version = "0.2.0"
description = ""
authors = ["Anthony Galassi <28850131+bendhouseart@users.noreply.github.com>"]
readme = "README.md"
packages = [{include = "petutils"}]
requires-python = ">=3.9"
authors = [
{ name = "Anthony Galassi", email = "28850131+bendhouseart@users.noreply.github.com" }
]
dependencies = [
"pybids>=0.16.3",
"nipype>=1.8.6",
]

[tool.poetry.dependencies]
python = "^3.9"
pybids = "^0.16.3"
nipype = "^1.8.6"


[tool.poetry.group.dev.dependencies]
ipython = "^8.16.1"
pytest = "^7.4.2"
[project.optional-dependencies]
dev = [
"ipython>=8.16.1",
"pytest>=7.4.2",
]

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["petutils"]
164 changes: 164 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest
import shutil
import tempfile
import os

# our first steps to testing are to build the different types of bids datasets that we expect to encounter
# these are:
Expand Down Expand Up @@ -114,3 +115,166 @@ def anat_in_each_session_folder(tmpdir):
/ file.name.replace("ses-baseline_", "ses-second_"),
)
return dest_dir

#5 - test dataset with multi run pet scans
@pytest.fixture
def multi_run_pet_scans(tmpdir):
dest_dir = pathlib.Path(tmpdir) / "multi_run_pet_scans"
shutil.copytree(data_dir, dest_dir)

runs = ["01", "02"]

for run in runs:
shutil.copy(
dest_dir / "sub-01" / "ses-baseline" / "pet" / "sub-01_ses-baseline_pet.nii.gz",
dest_dir / "sub-01" / "ses-baseline" / "pet" / f"sub-01_ses-baseline_run-{run}_pet.nii.gz",
)
shutil.copy(
dest_dir / "sub-01" / "ses-baseline" / "pet" / "sub-01_ses-baseline_pet.json",
dest_dir / "sub-01" / "ses-baseline" / "pet" / f"sub-01_ses-baseline_run-{run}_pet.json",
)

os.remove(dest_dir / "sub-01" / "ses-baseline" / "pet" / "sub-01_ses-baseline_pet.nii.gz")
os.remove(dest_dir/ "sub-01" / "ses-baseline" / "pet" / "sub-01_ses-baseline_pet.json")

return dest_dir

# Helper function to convert single run PET scans to multi-run
def convert_to_multi_run(dest_dir):
"""Convert all single-run PET scans in a directory to multi-run (run-01, run-02)"""
runs = ["01", "02"]

# Find all PET files without run entities
for pet_file in dest_dir.glob("**/pet/*_pet.nii.gz"):
if "run-" not in pet_file.name:
# Create run-01 and run-02 versions
for run in runs:
new_name = pet_file.name.replace("_pet.nii.gz", f"_run-{run}_pet.nii.gz")
shutil.copy(pet_file, pet_file.parent / new_name)
# Remove original
os.remove(pet_file)

# Do the same for JSON sidecars
for json_file in dest_dir.glob("**/pet/*_pet.json"):
if "run-" not in json_file.name:
for run in runs:
new_name = json_file.name.replace("_pet.json", f"_run-{run}_pet.json")
shutil.copy(json_file, json_file.parent / new_name)
os.remove(json_file)

# 1 - Multi-run version
@pytest.fixture
def anat_in_no_session_folder_multi_run(tmpdir):
dest_dir = pathlib.Path(tmpdir) / "anat_in_subject_folder_multi_run"
shutil.copytree(data_dir, dest_dir)

original_anat_folder = (
dest_dir / "sub-01" / "ses-baseline" / "anat"
)
subject_folder = dest_dir / "sub-01"
# now we move the anatomical folder in the first session of our test data into the subject level folder
shutil.move(original_anat_folder, subject_folder)

inherited_anat_folder = subject_folder / "anat"

# and next remove the ses- entities from the files in the newly created anat folder
for file in inherited_anat_folder.glob("sub-01_ses-baseline_*"):
shutil.move(
file,
pathlib.Path(tmpdir)
/ "anat_in_subject_folder_multi_run"
/ "sub-01"
/ "anat"
/ file.name.replace("ses-baseline_", ""),
)

# Convert to multi-run
convert_to_multi_run(dest_dir)

return dest_dir

# 2 - Multi-run version
@pytest.fixture
def anat_in_first_session_folder_multi_run(tmpdir):
dest_dir = pathlib.Path(tmpdir) / "anat_in_first_session_folder_multi_run"
shutil.copytree(data_dir, dest_dir)

# Convert to multi-run
convert_to_multi_run(dest_dir)

return dest_dir

# 3 - Multi-run version
@pytest.fixture
def anat_in_first_session_folder_multi_sessions_multi_run(tmpdir):
dest_dir = pathlib.Path (tmpdir) / "anat_in_first_session_folder_multi_sessions_multi_run"
shutil.copytree(data_dir, dest_dir)

# create a second session
second_session_folder = (
dest_dir / "sub-01" / "ses-second"
)
second_session_folder.mkdir(parents=True, exist_ok=True)

shutil.copytree(
dest_dir / "sub-01" / "ses-baseline",
second_session_folder,
dirs_exist_ok=True,
)

# replace the ses- entities in the files in the newly created second session folder
for file in second_session_folder.glob("pet/*"):
shutil.move(
file,
second_session_folder
/ "pet"
/ file.name.replace("ses-baseline_", "ses-second_"),
)

# remove anat in second session folder
shutil.rmtree(second_session_folder / "anat")

# Convert to multi-run
convert_to_multi_run(dest_dir)

return dest_dir

# 4 - Multi-run version
@pytest.fixture
def anat_in_each_session_folder_multi_run(tmpdir):
dest_dir = pathlib.Path(tmpdir) / "anat_in_each_session_folder_multi_run"
shutil.copytree(data_dir, dest_dir)

# create a second session
second_session_folder = (
dest_dir / "sub-01" / "ses-second"
)
second_session_folder.mkdir(parents=True, exist_ok=True)

shutil.copytree(
dest_dir / "sub-01" / "ses-baseline",
second_session_folder,
dirs_exist_ok=True,
)

# replace the ses- entities in the files in the newly created second session folder
for file in second_session_folder.glob("pet/*"):
shutil.move(
file,
second_session_folder
/ "pet"
/ file.name.replace("ses-baseline_", "ses-second_"),
)

for file in second_session_folder.glob("anat/*"):
shutil.move(
file,
second_session_folder
/ "anat"
/ file.name.replace("ses-baseline_", "ses-second_"),
)

# Convert to multi-run
convert_to_multi_run(dest_dir)

return dest_dir
125 changes: 124 additions & 1 deletion tests/test_pet_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,127 @@ def test_anat_in_each_session_folder(anat_in_each_session_folder):
assert len(pet_image.parts) == len(anat_image.parts)
assert re.search(r"ses-[^_|\/]*", str(anat_image))[0] == re.search(r"ses-[^_|\/]*", str(pet_image))[0]
assert re.search(r"nii|(.gz)", str(anat_image)) is not None


def test_multi_run_pet_scans(multi_run_pet_scans):
subprocess.run(["tree", multi_run_pet_scans])
a_and_p = collect_anat_and_pet(multi_run_pet_scans)
for subject in a_and_p.keys():
for pet_image, anat_image in a_and_p[subject].items():
pet_image = pathlib.Path(pet_image)
anat_image = pathlib.Path(anat_image)
assert len(pet_image.parts) == len(anat_image.parts)
assert re.search(r"ses-[^_|\/]*", str(anat_image))[0] == re.search(r"ses-[^_|\/]*", str(pet_image))[0]
assert re.search(r"nii|(.gz)", str(anat_image)) is not None

# Multi-run versions of all tests
def test_anat_in_no_session_folder_multi_run(anat_in_no_session_folder_multi_run):
subprocess.run(["tree", anat_in_no_session_folder_multi_run])
a_and_p = collect_anat_and_pet(anat_in_no_session_folder_multi_run)

# Track number of runs detected
run_count = 0

# check that for each pet file, there is a corresponding inherited anat file from the subject level
for subject in a_and_p.keys():
for pet_image, anat_image in a_and_p[subject].items():
pet_image = pathlib.Path(pet_image)
anat_image = pathlib.Path(anat_image)
# check that the pet image is at least a subfolder deeper than the anat image
assert len(pet_image.parts) > len(anat_image.parts)
assert re.search(r"ses-", str(anat_image)) is None
assert re.search(r"ses-", str(pet_image)) is not None
assert re.search(r"nii|(.gz)", str(anat_image)) is not None
# Verify run entity exists in PET filename
assert re.search(r"run-\d+", str(pet_image)) is not None
run_count += 1

# Should have at least 2 runs (run-01 and run-02)
assert run_count >= 2

def test_anat_in_first_session_folder_multi_run(anat_in_first_session_folder_multi_run):
subprocess.run(["tree", anat_in_first_session_folder_multi_run])
a_and_p = collect_anat_and_pet(anat_in_first_session_folder_multi_run)

for subject in a_and_p.keys():
set_of_anat_images = set()
set_of_pet_images = set()
anat_exists_in_one_session_folder = False
run_count = 0

for pet_image, anat_image in a_and_p[subject].items():
pet_image = pathlib.Path(pet_image)
anat_image = pathlib.Path(anat_image)

# add images to the set
set_of_anat_images.add(str(anat_image))
set_of_pet_images.add(str(pet_image))

# check that at least one session contains both a pet and an anat image
if re.search(r"ses-[^_|\/]*", str(anat_image))[0] == re.search(r"ses-[^_|\/]*", str(pet_image))[0]:
anat_exists_in_one_session_folder = True

# Verify run entity exists in PET filename
assert re.search(r"run-\d+", str(pet_image)) is not None
run_count += 1

assert anat_exists_in_one_session_folder is True
assert len(set_of_anat_images) == 1
# Should have multiple PET images due to runs
assert len(set_of_pet_images) >= 2
assert run_count >= 2

def test_anat_in_first_session_folder_multi_sessions_multi_run(anat_in_first_session_folder_multi_sessions_multi_run):
subprocess.run(["tree", anat_in_first_session_folder_multi_sessions_multi_run])
a_and_p = collect_anat_and_pet(anat_in_first_session_folder_multi_sessions_multi_run)

for subject in a_and_p.keys():
set_of_anat_images = set()
set_of_pet_images = set()
anat_exists_in_one_session_folder = False
run_count = 0

for pet_image, anat_image in a_and_p[subject].items():
pet_image = pathlib.Path(pet_image)
anat_image = pathlib.Path(anat_image)

# add images to the set
set_of_anat_images.add(str(anat_image))
set_of_pet_images.add(str(pet_image))

# check that at least one session contains both a pet and an anat image
if re.search(r"ses-[^_|\/]*", str(anat_image))[0] == re.search(r"ses-[^_|\/]*", str(pet_image))[0]:
anat_exists_in_one_session_folder = True

# Verify run entity exists in PET filename
assert re.search(r"run-\d+", str(pet_image)) is not None
run_count += 1

print("!"*100)
print(set_of_pet_images)
assert anat_exists_in_one_session_folder is True
assert len(set_of_anat_images) == 1
# Should have even more PET images (multiple sessions × multiple runs)
assert len(set_of_pet_images) >= 4 # 2 sessions × 2 runs
assert run_count >= 4

def test_anat_in_each_session_folder_multi_run(anat_in_each_session_folder_multi_run):
subprocess.run(["tree", anat_in_each_session_folder_multi_run])
a_and_p = collect_anat_and_pet(anat_in_each_session_folder_multi_run)

run_count = 0

# check that for each pet file, there is a corresponding anat file in the same session
for subject in a_and_p.keys():
for pet_image, anat_image in a_and_p[subject].items():
pet_image = pathlib.Path(pet_image)
anat_image = pathlib.Path(anat_image)
# check that the pet image is at the same folder depth as the anat image
assert len(pet_image.parts) == len(anat_image.parts)
assert re.search(r"ses-[^_|\/]*", str(anat_image))[0] == re.search(r"ses-[^_|\/]*", str(pet_image))[0]
assert re.search(r"nii|(.gz)", str(anat_image)) is not None
# Verify run entity exists in PET filename
assert re.search(r"run-\d+", str(pet_image)) is not None
run_count += 1

# Should have multiple runs across sessions
assert run_count >= 4 # 2 sessions × 2 runs
Loading