Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start putting together pipeline architecture #54

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ dynamic = ["version"]
dependencies = [
"click >=8.1",
"numpy >=1.26, <=1.26.4",
"itk",
"dipy >=1.9",
"pandas",
"HD_BET @ https://github.com/brain-microstructure-exploration-tools/HD-BET/archive/refs/tags/v1.0.0.zip#sha256=d48908854207b839552f2059c9cf2a48819b847bc1eb0ea4445d1d589471a1f5",
]

Expand Down
23 changes: 23 additions & 0 deletions src/abcdmicro/dwi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

from dataclasses import dataclass

from abcdmicro.event import AbcdEvent
from abcdmicro.resource import BvalResource, BvecResource, VolumeResource


@dataclass
class Dwi:
"""An ABCD diffusion weighted image."""

event: AbcdEvent
"""The ABCD event associated with this DWI."""

volume: VolumeResource
"""The DWI image volume."""

bval: BvalResource
"""The DWI b-values"""

bvec: BvecResource
"""The DWI b-vectors"""
55 changes: 55 additions & 0 deletions src/abcdmicro/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import ClassVar

import pandas as pd


@dataclass
class AbcdEvent:
"""An ABCD event -- a particular subject and time point from a particular ABCD data release."""

subject_id: str
"""The subject GUID defined in the NIMH Data Archive, for example 'NDAR_INV00U4FTRU'"""

eventname: str
"""The ABCD Study event name, for example 'baseline_year_1_arm_1'"""

image_download_path: Path
"""Path to the ABCD image download root directory. This would be the directory that
contains `fmriresults01/abcd-mproc-release5/` with some compressed images in there"""

tabular_data_path: Path
"""Path to the extracted ABCD tabular data directory. This would contain subdirectories
like `core/mental-health/` with csv tables inside them."""

abcd_version: str
"""Version of the ABCD dataset release, for example '5.1'."""

_tables: ClassVar[dict[str, dict[str, pd.DataFrame]]] = {}
"""A mapping (ABCD version string) -> (relative table path) -> (loaded table)"""

def get_table(self, table_relative_path: str) -> pd.DataFrame:
"""Get a table, loading it from disk if it hasn't already been loaded.

Args:
table_relative_path: The relative path of the table from the table root directory.
Example: 'core/mental-health/mh_p_pss.csv'

Returns: The loaded table as a pandas DataFrame,
with subject ID and eventname as a multi-index.
"""
if self.abcd_version not in self._tables:
self._tables[self.abcd_version] = {}
path_to_table_mapping = self._tables[self.abcd_version]
if table_relative_path not in path_to_table_mapping:
table = pd.read_csv(
self.tabular_data_path / table_relative_path,
index_col=["src_subject_id", "eventname"],
)
path_to_table_mapping[table_relative_path] = table
else:
table = path_to_table_mapping[table_relative_path]
return table
66 changes: 66 additions & 0 deletions src/abcdmicro/io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any

import itk
import numpy as np
from dipy.io.gradients import read_bvals_bvecs
from numpy.typing import NDArray

from abcdmicro.resource import (
BvalResource,
BvecResource,
InMemoryBvalResource,
InMemoryBvecResource,
InMemoryVolumeResource,
VolumeResource,
)


@dataclass
class NiftiVolumeResrouce(VolumeResource):
"""A volume or volume stack that is saved to disk in the nifti file format."""

path: Path
"""Path to the underlying volume nifti file"""

def load(self) -> InMemoryVolumeResource:
return InMemoryVolumeResource(itk.imread(self.path))

def get_array(self) -> NDArray[Any]:
return self.load().get_array()

def get_metadata(self) -> dict[Any, Any]:
return self.load().get_metadata()


@dataclass
class FslBvalResource(BvalResource):
"""A b-value list that is saved to disk in the FSL text file format."""

path: Path
"""Path to the underlying bval txt file"""

def load(self) -> InMemoryBvalResource:
bvals_array, _ = read_bvals_bvecs(self.path, None)
return InMemoryBvalResource(bvals_array)

def get(self) -> NDArray[np.floating]:
return self.load().get()


@dataclass
class FslBvecResource(BvecResource):
"""A b-vector list that is saved to disk in the FSL text file format."""

path: Path
"""Path to the underlying bvec txt file"""

def load(self) -> InMemoryBvecResource:
_, bvecs_array = read_bvals_bvecs(None, self.path)
return InMemoryBvecResource(bvecs_array)

def get(self) -> NDArray[np.floating]:
return self.load().get()
79 changes: 79 additions & 0 deletions src/abcdmicro/resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any

import itk
import numpy as np
from numpy.typing import NDArray


class VolumeResource(ABC):
"""Base class for resources representing a volume or volume stack.
An n-D array where n >= 3 and where three of the dimensions are spatial
and have associated header information describing a patient coordinate system."""

@abstractmethod
def get_array(self) -> NDArray[Any]:
"""Get the underlying volume data array"""

@abstractmethod
def get_metadata(self) -> dict[Any, Any]:
"""Get the volume image metadata"""


@dataclass
class InMemoryVolumeResource(VolumeResource):
"""A volume resource that is loaded into memory.
An n-D array where n >= 3 and where three of the dimensions are spatial
and have associated header information describing a patient coordinate system."""

image: itk.Image
"""The underlying ITK image of the volume"""

def get_array(self) -> NDArray[Any]:
return itk.array_view_from_image(self.image)

def get_metadata(self) -> dict[Any, Any]:
return dict(self.image)


class BvalResource(ABC):
"""Base class for resources representing a list of b-values associated with a 4D DWI
volume stack."""

@abstractmethod
def get(self) -> NDArray[np.floating]:
"""Get the underlying array of b-values"""


@dataclass
class InMemoryBvalResource(BvalResource):
"""A b-value list that is loaded into memory."""

array: NDArray[np.floating]
"""The underlying array of b-values"""

def get(self) -> NDArray[np.floating]:
return self.array


class BvecResource(ABC):
"""Base class for resources representing a list of b-vectors associated with a 4D DWI
volume stack."""

@abstractmethod
def get(self) -> NDArray[np.floating]:
"""Get the underlying array of b-vectors"""


@dataclass
class InMemoryBvecResource(BvecResource):
"""A b-vector list that is loaded into memory."""

array: NDArray[np.floating]
"""The underlying array of b-vectors"""

def get(self) -> NDArray[np.floating]:
return self.array
15 changes: 15 additions & 0 deletions tests/test_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

from pathlib import Path

from abcdmicro.event import AbcdEvent


def test_create_event():
AbcdEvent(
subject_id="NDAR_INV00U4FTRU",
eventname="baseline_year_1_arm_1",
image_download_path=Path("/this/is/a/path/for/images"),
tabular_data_path=Path("/this/is/a/path/for/tables"),
abcd_version="5.1",
)
55 changes: 55 additions & 0 deletions tests/test_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations

import tempfile
from pathlib import Path

import numpy as np
import pytest
from dipy.io.image import save_nifti

from abcdmicro.io import NiftiVolumeResrouce


@pytest.fixture()
def bval_array():
return np.array([500.0, 1000.0, 200.0])


@pytest.fixture()
def bvec_array():
return np.array(
[
[
1.0,
1.0,
1.0,
],
[2.0, 0.0, -4.0],
]
)


@pytest.fixture()
def volume_array():
rng = np.random.default_rng(1337)
return rng.random(size=(3, 4, 5, 6), dtype=float)


@pytest.mark.filterwarnings("ignore:builtin type [sS]wig.* has no __module__ attribute")
def test_nifti_volume_resource(volume_array):
with tempfile.TemporaryDirectory() as tmpdir:
volume_file = Path(tmpdir) / "volume_file.nii"
save_nifti(
fname=volume_file,
data=volume_array,
affine=np.array(
[
[1.35, 0, 0, 0],
[0, 1.45, 0, 0],
[0, 0, 1.55, 0],
[0, 0, 0, 1],
]
),
)
volume_resource = NiftiVolumeResrouce(path=volume_file)
assert np.allclose(volume_resource.get_array(), volume_array)
77 changes: 77 additions & 0 deletions tests/test_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from __future__ import annotations

import itk
import numpy as np
import pytest

from abcdmicro.resource import (
BvalResource,
BvecResource,
InMemoryBvalResource,
InMemoryBvecResource,
InMemoryVolumeResource,
VolumeResource,
)


def test_bval_abstractness():
with pytest.raises(TypeError):
BvalResource() # type: ignore[abstract]


def test_bvec_abstractness():
with pytest.raises(TypeError):
BvecResource() # type: ignore[abstract]


def test_volume_abstractness():
with pytest.raises(TypeError):
VolumeResource() # type: ignore[abstract]


@pytest.fixture()
def bval_array():
return np.array([500.0, 1000.0, 200.0])


@pytest.fixture()
def bvec_array():
return np.array(
[
[
1.0,
1.0,
1.0,
],
[2.0, 0.0, -4.0],
]
)


@pytest.fixture()
def volume_array():
rng = np.random.default_rng(1337)
return rng.random(size=(3, 4, 5, 6), dtype=float)


def test_bval_inmemory_get(bval_array):
bval = InMemoryBvalResource(array=bval_array)
assert (bval.get() == bval_array).all()


def test_bvec_inmemory_get(bvec_array):
bvec = InMemoryBvecResource(array=bvec_array)
assert (bvec.get() == bvec_array).all()


@pytest.mark.filterwarnings("ignore:builtin type [sS]wig.* has no __module__ attribute")
def test_volume_inmemory_get_array(volume_array):
vol = InMemoryVolumeResource(image=itk.image_from_array(volume_array))
assert (vol.get_array() == volume_array).all()


def test_volume_inmemory_get_metadata(volume_array):
image = itk.image_from_array(volume_array)
image["bleh"] = "some_info"
vol = InMemoryVolumeResource(image=image)
assert vol.get_metadata()["bleh"] == "some_info"
Loading