Skip to content

Commit

Permalink
Merge pull request #3 from kaitj/enh/io_functions
Browse files Browse the repository at this point in the history
Add IO functions
  • Loading branch information
kaitj authored Jun 15, 2023
2 parents 04987b8 + 8f24bbe commit 335de62
Show file tree
Hide file tree
Showing 14 changed files with 708 additions and 17 deletions.
16 changes: 5 additions & 11 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
**Proposed changes**
Describe the changes implemented in this pull request. If the changes fix a
bug or resolves a feature request, be sure to link the issue. Please explain
the issue and how the changes address the issue.
Describe the changes implemented in this pull request. If the changes fix a bug or resolves a feature request, be sure to link the issue. Please explain the issue and how the changes address the issue.

**Types of changes**
What types of changes does your code introduce? Put an `x` in the boxes that
apply
What types of changes does your code introduce? Put an `x` in the boxes that apply

- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality
to not work as expected)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Other (if none of the other choices apply)

**Checklist**
Put an `x` in the boxes that apply. You can also fill these out after creating
the PR. If you are unsure about any of the choices, don't hesitate to ask!
Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you are unsure about any of the choices, don't hesitate to ask!

- [ ] Changes have been tested to ensure that fix is effective or that a
feature works.
- [ ] Changes have been tested to ensure that fix is effective or that a feature works.
- [ ] Changes pass the unit tests
- [ ] Code has been run through the `poe quality` task
- [ ] I have included necessary documentation or comments (as necessary)
Expand Down
14 changes: 10 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ jobs:
}}
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root --with dev
# Example of test
# - name: Test minimum inputs
# run: poetry run poe test_base
run: poetry install --no-interaction --no-root --with dev
- name: Install library
run: poetry install --no-interaction --with dev
- name: Perform unit testing
run: poetry run poe test_cov
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ../cov.xml
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Directories
__pycache__/

# Testing
.hypothesis
.ruff_cache
.pytest_cache
1 change: 1 addition & 0 deletions afids_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Top-level for afids-utils."""
8 changes: 8 additions & 0 deletions afids_utils/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Custom exceptions"""


class InvalidFiducialNumberError(Exception):
"""Exception for invalid fiducial number"""

def __init__(self, fid_num: int) -> None:
super().__init__(f"Provided fiducial {fid_num} is not valid.")
70 changes: 70 additions & 0 deletions afids_utils/io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Methods for loading and saving nifti files"""
from __future__ import annotations

import csv
from importlib import resources
from os import PathLike

import numpy as np
import pandas as pd
from numpy.typing import NDArray

from afids_utils.exceptions import InvalidFiducialNumberError

FCSV_FIELDNAMES = [
"# columns = id",
"x",
"y",
"z",
"ow",
"ox",
"oy",
"oz",
"vis",
"sel",
"lock",
"label",
"desc",
"associatedNodeID",
]


def get_afid(
fcsv_path: PathLike[str] | str, fid_num: int
) -> NDArray[np.single]:
"""Extract specific fiducial's spatial coordinates"""
if fid_num < 1 or fid_num > 32:
raise InvalidFiducialNumberError(fid_num)
fcsv_df = pd.read_csv(
fcsv_path, sep=",", header=2, usecols=FCSV_FIELDNAMES
)

return fcsv_df.loc[fid_num - 1, ["x", "y", "z"]].to_numpy(dtype="single")


def afids_to_fcsv(
afid_coords: NDArray[np.single],
fcsv_output: PathLike[str] | str,
) -> None:
"""AFIDS to Slicer-compatible .fcsv file"""
# Read in fcsv template
with resources.open_text(
"afids_utils.resources", "template.fcsv"
) as template_fcsv_file:
header = [template_fcsv_file.readline() for _ in range(3)]
reader = csv.DictReader(template_fcsv_file, fieldnames=FCSV_FIELDNAMES)
fcsv = list(reader)

# Loop over fiducials and update with fiducial spatial coordinates
for idx, row in enumerate(fcsv):
row["x"] = afid_coords[idx][0]
row["y"] = afid_coords[idx][1]
row["z"] = afid_coords[idx][2]

# Write output fcsv
with open(fcsv_output, "w", encoding="utf-8", newline="") as out_fcsv_file:
for line in header:
out_fcsv_file.write(line)
writer = csv.DictWriter(out_fcsv_file, fieldnames=FCSV_FIELDNAMES)
for row in fcsv:
writer.writerow(row)
Empty file added afids_utils/py.typed
Empty file.
Empty file.
35 changes: 35 additions & 0 deletions afids_utils/resources/template.fcsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Markups fiducial file version = 4.6
# CoordinateSystem = 0
# columns = id,x,y,z,ow,ox,oy,oz,vis,sel,lock,label,desc,associatedNodeID
vtkMRMLMarkupsFiducialNode_1,0,0,0,0,0,0,1,1,1,1,1,AC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_2,0,0,0,0,0,0,1,1,1,1,2,PC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_3,0,0,0,0,0,0,1,1,1,1,3,infracollicular sulcus,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_4,0,0,0,0,0,0,1,1,1,1,4,PMJ,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_5,0,0,0,0,0,0,1,1,1,1,5,superior interpeduncular fossa,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_6,0,0,0,0,0,0,1,1,1,1,6,R superior LMS,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_7,0,0,0,0,0,0,1,1,1,1,7,L superior LMS,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_8,0,0,0,0,0,0,1,1,1,1,8,R inferior LMS,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_9,0,0,0,0,0,0,1,1,1,1,9,L inferior LMS,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_10,0,0,0,0,0,0,1,1,1,1,10,culmen,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_11,0,0,0,0,0,0,1,1,1,1,11,intermammillary sulcus,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_12,0,0,0,0,0,0,1,1,1,1,12,R MB,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_13,0,0,0,0,0,0,1,1,1,1,13,L MB,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_14,0,0,0,0,0,0,1,1,1,1,14,pineal gland,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_15,0,0,0,0,0,0,1,1,1,1,15,R LV at AC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_16,0,0,0,0,0,0,1,1,1,1,16,L LV at AC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_17,0,0,0,0,0,0,1,1,1,1,17,R LV at PC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_18,0,0,0,0,0,0,1,1,1,1,18,L LV at PC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_19,0,0,0,0,0,0,1,1,1,1,19,genu of CC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_20,0,0,0,0,0,0,1,1,1,1,20,splenium,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_21,0,0,0,0,0,0,1,1,1,1,21,R AL temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_22,0,0,0,0,0,0,1,1,1,1,22,L AL temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_23,0,0,0,0,0,0,1,1,1,1,23,R superior AM temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_24,0,0,0,0,0,0,1,1,1,1,24,L superior AM temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_25,0,0,0,0,0,0,1,1,1,1,25,R inferior AM temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_26,0,0,0,0,0,0,1,1,1,1,26,L inferior AM temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_27,0,0,0,0,0,0,1,1,1,1,27,R indusium griseum origin,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_28,0,0,0,0,0,0,1,1,1,1,28,L indusium griseum origin,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_29,0,0,0,0,0,0,1,1,1,1,29,R ventral occipital horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_30,0,0,0,0,0,0,1,1,1,1,30,L ventral occipital horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_31,0,0,0,0,0,0,1,1,1,1,31,R olfactory sulcal fundus,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_32,0,0,0,0,0,0,1,1,1,1,32,L olfactory sulcal fundus,vtkMRMLScalarVolumeNode1
35 changes: 35 additions & 0 deletions afids_utils/tests/data/tpl-MNI152NLin2009cAsym_afids.fcsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Markups fiducial file version = 4.6
# CoordinateSystem = 0
# columns = id,x,y,z,ow,ox,oy,oz,vis,sel,lock,label,desc,associatedNodeID
vtkMRMLMarkupsFiducialNode_1,-0.23099999999999998,2.93275,-4.899,0,0,0,1,1,1,0,1,AC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_2,-0.24125000000000002,-25.074749999999998,-2.169,0,0,0,1,1,1,0,2,PC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_3,0.19949999999999998,-37.268249999999995,-10.9115,0,0,0,1,1,1,0,3,infracollicular sulcus,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_4,-0.12725000000000003,-23.14825,-21.522,0,0,0,1,1,1,0,4,PMJ,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_5,-0.06675,-14.81275,-11.32025,0,0,0,1,1,1,0,5,superior interpeduncular fossa,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_6,12.67275,-26.960749999999997,-10.38225,0,0,0,1,1,1,0,6,R superior LMS,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_7,-13.004999999999999,-27.190749999999998,-10.32375,0,0,0,1,1,1,0,7,L superior LMS,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_8,10.873,-30.96475,-21.533,0,0,0,1,1,1,0,8,R inferior LMS,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_9,-11.18,-30.437000000000005,-21.537,0,0,0,1,1,1,0,9,L inferior LMS,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_10,-0.004750000000000004,-52.3695,2.06825,0,0,0,1,1,1,0,10,culmen,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_11,-0.0665,-8.131499999999999,-14.7755,0,0,0,1,1,1,0,11,intermammillary sulcus,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_12,2.0255,-8.143500000000001,-14.752749999999999,0,0,0,1,1,1,0,12,R MB,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_13,-2.3954999999999997,-8.0565,-14.8675,0,0,0,1,1,1,0,13,L MB,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_14,0.04425,-31.73675,0.76675,0,0,0,1,1,1,0,14,pineal gland,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_15,15.141750000000002,5.3445,24.358249999999998,0,0,0,1,1,1,0,15,R LV at AC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_16,-15.6095,5.22575,24.63125,0,0,0,1,1,1,0,16,L LV at AC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_17,18.4955,-22.061999999999998,27.66,0,0,0,1,1,1,0,17,R LV at PC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_18,-18.999000000000002,-22.046,27.284,0,0,0,1,1,1,0,18,L LV at PC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_19,0.17799999999999996,33.423500000000004,2.6755,0,0,0,1,1,1,0,19,genu of CC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_20,0.037000000000000005,-37.6845,6.14675,0,0,0,1,1,1,0,20,splenium of CC,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_21,34.312250000000006,-4.982,-26.87875,0,0,0,1,1,1,0,21,R AL temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_22,-34.72725,-7.2475,-25.203,0,0,0,1,1,1,0,22,L AL temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_23,18.69625,-10.56775,-17.7725,0,0,0,1,1,1,0,23,R superior AM temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_24,-20.12075,-11.62125,-17.1495,0,0,0,1,1,1,0,24,L superior AM temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_25,21.07325,-4.56975,-28.90925,0,0,0,1,1,1,0,25,R inferior AM temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_26,-21.966749999999998,-5.28875,-27.895,0,0,0,1,1,1,0,26,L inferior AM temporal horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_27,14.40775,-39.697250000000004,3.8390000000000004,0,0,0,1,1,1,0,27,R indusium griseum origin,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_28,-15.48075,-42.28375,3.9659999999999997,0,0,0,1,1,1,0,28,L indusium griseum origin,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_29,20.730249999999998,-79.47475,4.85125,0,0,0,1,1,1,0,29,R ventral occipital horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_30,-19.41825,-81.46925,3.785,0,0,0,1,1,1,0,30,L ventral occipital horn,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_31,12.108500000000001,17.02725,-12.94,0,0,0,1,1,1,0,31,R olfactory sulcal fundus,vtkMRMLScalarVolumeNode1
vtkMRMLMarkupsFiducialNode_32,-13.34325,17.0885,-13.35225,0,0,0,1,1,1,0,32,L olfactory sulcal fundus,vtkMRMLScalarVolumeNode1
122 changes: 122 additions & 0 deletions afids_utils/tests/test_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from __future__ import annotations

import csv
import tempfile
from os import PathLike, remove
from pathlib import Path

import numpy as np
import pytest
from hypothesis import HealthCheck, assume, given, settings
from hypothesis import strategies as st
from hypothesis.extra.numpy import arrays
from numpy.typing import NDArray

from afids_utils.exceptions import InvalidFiducialNumberError
from afids_utils.io import FCSV_FIELDNAMES, afids_to_fcsv, get_afid


@pytest.fixture
def valid_fcsv_file() -> PathLike[str]:
return (
Path(__file__).parent / "data" / "tpl-MNI152NLin2009cAsym_afids.fcsv"
)


class TestGetAfid:
@given(afid_num=st.integers(min_value=1, max_value=32))
@settings(
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
def test_valid_num_get_afid(
self, valid_fcsv_file: PathLike[str], afid_num: int
):
afid = get_afid(valid_fcsv_file, afid_num)

# Check array type
assert isinstance(afid, np.ndarray)
# Check array values
assert afid.dtype == np.single

@given(afid_num=st.integers(min_value=-1000, max_value=1000))
@settings(
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
def test_invalid_num_get_afid(
self,
valid_fcsv_file: PathLike[str],
afid_num: int,
):
assume(afid_num < 1 or afid_num > 32)

with pytest.raises(
InvalidFiducialNumberError, match=".*is not valid."
):
get_afid(valid_fcsv_file, afid_num)


@st.composite
def afid_coords(
draw: st.DrawFn,
min_value: float = -50.0,
max_value: float = 50.0,
width: int = 16,
) -> NDArray[np.single]:
coords = draw(
arrays(
shape=(32, 3),
dtype=np.single,
elements=st.floats(
min_value=min_value, max_value=max_value, width=width
),
)
)

return coords


class TestAfidsToFcsv:
@given(afids_coords=afid_coords())
@settings(
suppress_health_check=[HealthCheck.function_scoped_fixture],
)
def test_write_fcsv(
self, afids_coords: NDArray[np.single], valid_fcsv_file: PathLike[str]
) -> None:
out_fcsv_file = tempfile.NamedTemporaryFile(
mode="w", delete=False, prefix="sub-test_afids.fcsv"
)
out_fcsv_path = Path(out_fcsv_file.name)

afids_to_fcsv(afids_coords, out_fcsv_path)

# Check file was created
assert out_fcsv_path.exists()

# Load files
with open(
valid_fcsv_file, "r", encoding="utf-8", newline=""
) as template_fcsv_file:
template_header = [template_fcsv_file.readline() for _ in range(3)]

with open(
out_fcsv_path, "r", encoding="utf-8", newline=""
) as output_fcsv_file:
output_header = [output_fcsv_file.readline() for _ in range(3)]
reader = csv.DictReader(
output_fcsv_file, fieldnames=FCSV_FIELDNAMES
)
output_fcsv = list(reader)

# Check header
assert output_header == template_header
# Check contents
for idx, row in enumerate(output_fcsv):
assert (row["x"], row["y"], row["z"]) == (
str(afids_coords[idx][0]),
str(afids_coords[idx][1]),
str(afids_coords[idx][2]),
)

# Delete temporary file
remove(out_fcsv_path)
16 changes: 16 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
comment:
layout: "header, diff, flags, components"

coverage:
status:
project:
default:
target: 100%
threshold: 5%

component_management:
individual_components:
- component_id: afids-utils_io
name: afids-utils_io
paths:
- afids_utils/io/
Loading

0 comments on commit 335de62

Please sign in to comment.