Skip to content

Commit

Permalink
Update dependencies and ensure Windows compatibility (#105)
Browse files Browse the repository at this point in the history
* remove kinfitr extra from nox

* skip kinfitr tests if dependencies are not available

* resolve numpy errors (windows) and warnings

* add windows and mac tests on ci

* reduce coverage threshold for check
  • Loading branch information
bilgelm authored Aug 29, 2024
1 parent a91416e commit aa32385
Show file tree
Hide file tree
Showing 19 changed files with 662 additions and 574 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ jobs:
- { python: "3.11", os: "ubuntu-latest", session: "mypy" }
- { python: "3.11", os: "ubuntu-latest", session: "tests" }
- { python: "3.11", os: "ubuntu-latest", session: "xdoctest" }
- { python: "3.12", os: "windows-latest", session: "pre-commit" }
- { python: "3.12", os: "windows-latest", session: "safety" }
- { python: "3.12", os: "windows-latest", session: "mypy" }
- { python: "3.12", os: "windows-latest", session: "tests" }
- { python: "3.12", os: "windows-latest", session: "typeguard" }
- { python: "3.12", os: "windows-latest", session: "xdoctest" }
- { python: "3.12", os: "windows-latest", session: "docs-build" }
- { python: "3.12", os: "macos-latest", session: "pre-commit" }
- { python: "3.12", os: "macos-latest", session: "safety" }
- { python: "3.12", os: "macos-latest", session: "mypy" }
- { python: "3.12", os: "macos-latest", session: "tests" }
- { python: "3.12", os: "macos-latest", session: "typeguard" }
- { python: "3.12", os: "macos-latest", session: "xdoctest" }
- { python: "3.12", os: "macos-latest", session: "docs-build" }

env:
NOXSESSION: ${{ matrix.session }}
Expand Down
12 changes: 6 additions & 6 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def safety(session: Session) -> None:
def mypy(session: Session) -> None:
"""Type-check using mypy."""
args = session.posargs or ["src", "tests", "docs/conf.py"]
session.install(".[kinfitr]")
session.install(".")
session.install("mypy", "pytest", "pytest-mock", "requests", "types-requests")
session.run("mypy", *args)
if not session.posargs:
Expand All @@ -160,7 +160,7 @@ def mypy(session: Session) -> None:
@session(python=python_versions)
def tests(session: Session) -> None:
"""Run the test suite."""
session.install(".[kinfitr]")
session.install(".")
session.install("coverage[toml]", "pytest", "pytest-mock", "pygments", "requests")
try:
session.run("coverage", "run", "--parallel", "-m", "pytest", *session.posargs)
Expand All @@ -185,7 +185,7 @@ def coverage(session: Session) -> None:
@session(python=python_versions[0])
def typeguard(session: Session) -> None:
"""Runtime type checking using Typeguard."""
session.install(".[kinfitr]")
session.install(".")
session.install("pytest", "pytest-mock", "typeguard", "pygments", "requests")
session.run("pytest", f"--typeguard-packages={package}", *session.posargs)

Expand All @@ -200,7 +200,7 @@ def xdoctest(session: Session) -> None:
if "FORCE_COLOR" in os.environ:
args.append("--colored=1")

session.install(".[kinfitr]")
session.install(".")
session.install("xdoctest[colors]")
session.run("python", "-m", "xdoctest", *args)

Expand All @@ -212,7 +212,7 @@ def docs_build(session: Session) -> None:
if not session.posargs and "FORCE_COLOR" in os.environ:
args.insert(0, "--color")

session.install(".[kinfitr]")
session.install(".")
session.install("sphinx", "sphinx-click", "furo", "myst-parser")

build_dir = Path("docs", "_build")
Expand All @@ -226,7 +226,7 @@ def docs_build(session: Session) -> None:
def docs(session: Session) -> None:
"""Build and serve the documentation with live reloading on file changes."""
args = session.posargs or ["--open-browser", "docs", "docs/_build"]
session.install(".[kinfitr]")
session.install(".")
session.install("sphinx", "sphinx-autobuild", "sphinx-click", "furo", "myst-parser")

build_dir = Path("docs", "_build")
Expand Down
984 changes: 538 additions & 446 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ tests = ["tests", "*/tests"]
[tool.coverage.run]
branch = true
source = ["dynamicpet", "tests"]
omit = ["src/dynamicpet/kineticmodel/kinfitr.py"]

[tool.coverage.report]
show_missing = true
fail_under = 80
fail_under = 75

[tool.isort]
profile = "black"
Expand Down
10 changes: 5 additions & 5 deletions src/dynamicpet/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from dynamicpet.temporalobject import TemporalMatrix
from dynamicpet.temporalobject.temporalobject import INTEGRATION_TYPE_OPTS
from dynamicpet.temporalobject.temporalobject import WEIGHT_OPTS
from dynamicpet.typing_utils import NumpyRealNumberArray
from dynamicpet.typing_utils import NumpyNumberArray


IMPLEMENTED_KMS = [
Expand Down Expand Up @@ -120,7 +120,7 @@ def denoise(
# check that mask is in the same space as pet
if not np.all(pet_img.img.affine == mask_img.affine):
raise ValueError("PET and mask are not in the same space")
mask_img_mat: NumpyRealNumberArray = mask_img.get_fdata().astype("bool")
mask_img_mat: NumpyNumberArray = mask_img.get_fdata().astype("bool")

if window_half_size:
if thresh:
Expand Down Expand Up @@ -329,7 +329,7 @@ def parse_kineticmodel_inputs(
refroi: str | None = None,
refmask: str | None = None,
petmask: str | None = None,
) -> tuple[PETBIDSImage | PETBIDSMatrix, TemporalMatrix, NumpyRealNumberArray | None]:
) -> tuple[PETBIDSImage | PETBIDSMatrix, TemporalMatrix, NumpyNumberArray | None]:
"""Parse kinetic model inputs.
Args:
Expand Down Expand Up @@ -374,9 +374,9 @@ def parse_kineticmodel_inputs(
# check that refmask is in the same space as pet
if not np.all(tac_object.img.affine == refmask_img.affine):
raise ValueError("PET and refmask are not in the same space")
refmask_img_mat: NumpyRealNumberArray = refmask_img.get_fdata().astype("bool")
refmask_img_mat: NumpyNumberArray = refmask_img.get_fdata().astype("bool")

petmask_img_mat: NumpyRealNumberArray | None
petmask_img_mat: NumpyNumberArray | None
if petmask:
petmask_img: SpatialImage = nib_load(petmask) # type: ignore
# check that petmask is in the same space as pet
Expand Down
6 changes: 3 additions & 3 deletions src/dynamicpet/denoise/nesma.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

from ..petbids.petbidsimage import PETBIDSImage
from ..temporalobject.temporalimage import image_maker
from ..typing_utils import NumpyRealNumberArray
from ..typing_utils import NumpyNumberArray


def nesma_semiadaptive(
ti: PETBIDSImage,
mask: NumpyRealNumberArray,
mask: NumpyNumberArray,
window_half_size: tuple[int, int, int],
thresh: float = 0.05,
) -> tuple[PETBIDSImage, NumpyRealNumberArray]:
) -> tuple[PETBIDSImage, NumpyNumberArray]:
"""NESMA denoising.
NESMA is short for Nonlocal EStimation of multispectral MAgnitudes.
Expand Down
16 changes: 8 additions & 8 deletions src/dynamicpet/kineticmodel/kineticmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ..temporalobject.temporalimage import TemporalImage
from ..temporalobject.temporalimage import image_maker
from ..temporalobject.temporalmatrix import TemporalMatrix
from ..typing_utils import NumpyRealNumberArray
from ..typing_utils import NumpyNumberArray


# rename to ReferenceTissueKineticModel ??
Expand All @@ -29,7 +29,7 @@ class KineticModel(ABC):

reftac: TemporalMatrix
tacs: TemporalMatrix | TemporalImage
parameters: dict[str, NumpyRealNumberArray]
parameters: dict[str, NumpyNumberArray]

@classmethod
@abstractmethod
Expand Down Expand Up @@ -67,15 +67,15 @@ def __init__(

self.reftac: TemporalMatrix = reftac
self.tacs: TemporalMatrix | TemporalImage = tacs
self.parameters: dict[str, NumpyRealNumberArray] = {}
self.parameters: dict[str, NumpyNumberArray] = {}

@abstractmethod
def fit(self, mask: NumpyRealNumberArray | None = None) -> None:
def fit(self, mask: NumpyNumberArray | None = None) -> None:
"""Estimate model parameters."""
# implementation should update self.parameters
raise NotImplementedError

def get_parameter(self, param_name: str) -> SpatialImage | NumpyRealNumberArray:
def get_parameter(self, param_name: str) -> SpatialImage | NumpyNumberArray:
"""Get a fitted parameter.
If the input (tacs) is an image, parameter will be returned as an image.
Expand Down Expand Up @@ -108,7 +108,7 @@ def get_parameter(self, param_name: str) -> SpatialImage | NumpyRealNumberArray:
)
return param_img
else:
param_vector: NumpyRealNumberArray = self.parameters[param_name]
param_vector: NumpyNumberArray = self.parameters[param_name]
return param_vector
elif param_name == "bp" and "dvr" in self.parameters:
self.parameters[param_name] = self.parameters["dvr"] - 1
Expand All @@ -124,8 +124,8 @@ def get_parameter(self, param_name: str) -> SpatialImage | NumpyRealNumberArray:
def set_parameter(
self,
param_name: str,
param: NumpyRealNumberArray,
mask: NumpyRealNumberArray | None = None,
param: NumpyNumberArray,
mask: NumpyNumberArray | None = None,
) -> None:
"""Set kinetic model parameter.
Expand Down
6 changes: 2 additions & 4 deletions src/dynamicpet/kineticmodel/kinfitr.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from ..temporalobject.temporalimage import TemporalImage
from ..temporalobject.temporalmatrix import TemporalMatrix
from ..typing_utils import NumpyRealNumberArray
from ..typing_utils import NumpyNumberArray
from .kineticmodel import KineticModel


Expand All @@ -34,9 +34,7 @@ def get_param_names(cls) -> list[str]:
return ["bp", "R1", "k2"]

def fit(
self,
mask: NumpyRealNumberArray | None = None,
**kwargs: NumpyRealNumberArray | float
self, mask: NumpyNumberArray | None = None, **kwargs: NumpyNumberArray | float
) -> None:
"""Estimate model parameters.
Expand Down
38 changes: 20 additions & 18 deletions src/dynamicpet/kineticmodel/srtm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..temporalobject.temporalmatrix import TemporalMatrix
from ..temporalobject.temporalobject import INTEGRATION_TYPE_OPTS
from ..temporalobject.temporalobject import WEIGHT_OPTS
from ..typing_utils import NumpyRealNumberArray
from ..typing_utils import NumpyNumberArray
from ..typing_utils import RealNumber
from .kineticmodel import KineticModel

Expand All @@ -34,8 +34,8 @@ def get_param_names(cls) -> list[str]:

def fit(
self,
mask: NumpyRealNumberArray | None = None,
weight_by: WEIGHT_OPTS | NumpyRealNumberArray | None = None,
mask: NumpyNumberArray | None = None,
weight_by: WEIGHT_OPTS | NumpyNumberArray | None = None,
) -> None:
"""Estimate model parameters.
Expand Down Expand Up @@ -101,7 +101,7 @@ def fitted_tacs(self) -> TemporalMatrix | TemporalImage:

def srtm_model(
reftac: TemporalMatrix, bp: float, r1: float, k2: float
) -> NumpyRealNumberArray:
) -> NumpyNumberArray:
"""SRTM model to generate a target TAC.
Args:
Expand All @@ -118,15 +118,17 @@ def srtm_model(
# to a higher frequency uniform timing grid, convolve, and then
# downsample to original timing grid

t = reftac.frame_mid
t = reftac.frame_mid.astype("float")
# find smallest time interval in data
# step = np.min(reftac.frame_duration)
# t_upsampled = np.arange(t[0], t[-1], step)
# if t_upsampled[-1] < t[-1]:
# t_upsampled = np.append(t_upsampled, t_upsampled[-1] + step)
t_upsampled, step = np.linspace(np.min(t), np.max(t), 1024, retstep=True)

reftac_upsampled = np.interp(t_upsampled, t, reftac.dataobj.flatten())
reftac_upsampled = np.interp(
t_upsampled, t, reftac.dataobj.astype("float").flatten()
)

k2a = k2 / (1 + bp)
conv_res_upsampled = (
Expand Down Expand Up @@ -168,9 +170,9 @@ def get_param_names(cls) -> list[str]:

def fit( # noqa: max-complexity: 12
self,
mask: NumpyRealNumberArray | None = None,
mask: NumpyNumberArray | None = None,
integration_type: INTEGRATION_TYPE_OPTS = "trapz",
weight_by: WEIGHT_OPTS | NumpyRealNumberArray | None = "frame_duration",
weight_by: WEIGHT_OPTS | NumpyNumberArray | None = "frame_duration",
fwhm: RealNumber | list[RealNumber] | None = None,
) -> None:
"""Estimate model parameters.
Expand All @@ -191,18 +193,18 @@ def fit( # noqa: max-complexity: 12
fwhm: scalar or length 3 sequence, FWHM in mm over which to smooth
"""
# get reference TAC as a 1-D vector
reftac: NumpyRealNumberArray = self.reftac.dataobj.flatten()[:, np.newaxis]
reftac: NumpyNumberArray = self.reftac.dataobj.flatten()[:, np.newaxis]
# numerical integration of reference TAC
int_reftac: NumpyRealNumberArray = self.reftac.cumulative_integral(
int_reftac: NumpyNumberArray = self.reftac.cumulative_integral(
integration_type
).flatten()

tacs: TemporalMatrix = self.tacs.timeseries_in_mask(mask)
n = tacs.num_frames
m = 3
num_elements = tacs.num_elements
tacs_mat: NumpyRealNumberArray = tacs.dataobj
int_tacs_mat: NumpyRealNumberArray = tacs.cumulative_integral(integration_type)
tacs_mat: NumpyNumberArray = tacs.dataobj
int_tacs_mat: NumpyNumberArray = tacs.cumulative_integral(integration_type)

weights = tacs.get_weights(weight_by)
w = np.diag(weights)
Expand Down Expand Up @@ -247,7 +249,7 @@ def fit( # noqa: max-complexity: 12
# a smoothing FWHM is provided.
x = np.column_stack((int_reftac, reftac, -tac))

b: NumpyRealNumberArray
b: NumpyNumberArray
try:
b = solve(x.T @ w @ x, x.T @ w @ int_tac, assume_a="sym")
except LinAlgError:
Expand Down Expand Up @@ -319,9 +321,9 @@ def fit( # noqa: max-complexity: 12

def prep_refine_r1(
self,
mask: NumpyRealNumberArray | None = None,
mask: NumpyNumberArray | None = None,
fwhm: RealNumber | list[RealNumber] | None = None,
) -> tuple[NumpyRealNumberArray, ...]:
) -> tuple[NumpyNumberArray, ...]:
"""Refine R1.
Args:
Expand Down Expand Up @@ -376,9 +378,9 @@ def prep_refine_r1(
h1_img = image_maker(h1, smooth_k2_img)
h2_img = image_maker(h2, smooth_k2a_img)

smooth_r1_mat: NumpyRealNumberArray
smooth_k2_mat: NumpyRealNumberArray
smooth_k2a_mat: NumpyRealNumberArray
smooth_r1_mat: NumpyNumberArray
smooth_k2_mat: NumpyNumberArray
smooth_k2a_mat: NumpyNumberArray
if mask is None:
smooth_r1_mat = smooth_r1_img.get_fdata().flatten()
smooth_k2_mat = smooth_k2_img.get_fdata().flatten()
Expand Down
6 changes: 3 additions & 3 deletions src/dynamicpet/kineticmodel/suvr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

from ..temporalobject.temporalimage import TemporalImage
from ..temporalobject.temporalmatrix import TemporalMatrix
from ..typing_utils import NumpyNumberArray
from ..typing_utils import NumpyRealNumber
from ..typing_utils import NumpyRealNumberArray
from .kineticmodel import KineticModel


Expand All @@ -22,7 +22,7 @@ def get_param_names(cls) -> list[str]:
"""Get names of kinetic model parameters."""
return ["suvr"]

def fit(self, mask: NumpyRealNumberArray | None = None) -> None:
def fit(self, mask: NumpyNumberArray | None = None) -> None:
"""Calculate SUVR.
Args:
Expand Down Expand Up @@ -50,7 +50,7 @@ def fit(self, mask: NumpyRealNumberArray | None = None) -> None:
"""
tacs: TemporalMatrix = self.tacs.timeseries_in_mask(mask)

numerator: NumpyRealNumberArray = np.sum(
numerator: NumpyNumberArray = np.sum(
tacs.dataobj * tacs.frame_duration, axis=-1
)
denominator: NumpyRealNumber = np.sum(
Expand Down
Loading

0 comments on commit aa32385

Please sign in to comment.