Skip to content

Commit

Permalink
remove internal storing with InjectionStart anchor; move anchoring to…
Browse files Browse the repository at this point in the history
… concat
  • Loading branch information
bilgelm committed Sep 13, 2024
1 parent 2b78340 commit efa128b
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 58 deletions.
8 changes: 4 additions & 4 deletions src/dynamicpet/petbids/petbidsimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,13 @@ def __init__(self, img: SpatialImage, json_dict: PetBidsJson) -> None:

# need to make a copy of json_dict before storing
self.json_dict: PetBidsJson = deepcopy(json_dict)
self.set_timezero(anchor="InjectionStart")

def extract(self, start_time: RealNumber, end_time: RealNumber) -> "PETBIDSImage":
"""Extract a temporally shorter PETBIDSImage from a PETBIDSImage.
Args:
start_time: time at which to begin, inclusive (minute post-injection)
end_time: time at which to stop, inclusive (minute post-injection)
start_time: time (min) at which to begin relative to TimeZero, incl.
end_time: time (min) at which to stop relative to TimeZero, incl.
Returns:
extracted_img: extracted PETBIDSImage
Expand All @@ -75,13 +74,14 @@ def concatenate(self, other: "PETBIDSImage") -> "PETBIDSImage": # type: ignore
Returns:
concatenated PETBIDSImage
"""
offset = self._decay_correct_offset(other)
offset, original_anchor = self._decay_correct_offset(other)
other = other.decay_correct(decaycorrecttime=offset)

concat_img = super().concatenate(other)
json_dict = update_frametiming_from(self.json_dict, concat_img)

concat_res = PETBIDSImage(concat_img.img, json_dict)
concat_res.set_timezero(anchor=original_anchor)

return concat_res

Expand Down
32 changes: 7 additions & 25 deletions src/dynamicpet/petbids/petbidsjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def get_hhmmss(
return scanstart.time()


def _timediff(firsttime: datetime.time, secondtime: datetime.time) -> float:
def timediff(firsttime: datetime.time, secondtime: datetime.time) -> float:
"""Get difference in seconds between two datetime.time objects HH:MM:SS."""
td = (
3600 * (firsttime.hour - secondtime.hour)
Expand All @@ -176,21 +176,6 @@ def _timediff(firsttime: datetime.time, secondtime: datetime.time) -> float:
return td


# def get_decaycorr_rel_to_scanstart(json_dict: PetBidsJson) -> float:
# """Get an anchor time point for decay correction relative to scan start.

# Args:
# json_dict: json dictionary to be updated

# Raises:
# ValueError: image is not decay corrected
# """
# if json_dict["ImageDecayCorrected"]:
# return json_dict["ImageDecayCorrectionTime"] - json_dict["ScanStart"]
# else:
# raise ValueError("Image is not decay corrected")


def update_frametiming_from(
json_dict: PetBidsJson, temporal_object: TemporalObject[Any]
) -> PetBidsJson:
Expand Down Expand Up @@ -223,8 +208,6 @@ def get_frametiming_in_mins(
seconds. This corresponds to DICOM Tag (0018,1042) converted to seconds
relative to TimeZero.
At least one of ScanStart and InjectionStart should be 0.
If ScanStart is 0, FrameTimesStart are shifted so that outputs are relative
to injection start.
This method does not check if the FrameTimesStart and FrameDuration entries
in the json file are sensible.
Expand All @@ -247,16 +230,15 @@ def get_frametiming_in_mins(

inj_start: float = json_dict["InjectionStart"]
scan_start: float = json_dict["ScanStart"]
if inj_start == 0:
pass
elif scan_start == 0:
if frame_start[-1] + frame_duration[-1] < inj_start:
warnings.warn("No data acquired after injection", stacklevel=2)
frame_start -= inj_start
else:
if not (inj_start == 0 or scan_start == 0):
# invalid PET BIDS json
raise ValueError("Neither InjectionStart nor ScanStart is 0")

if frame_start[0] < scan_start:
raise ValueError("First time frame starts before ScanStart")
if frame_start[-1] + frame_duration[-1] < inj_start:
warnings.warn("No data acquired after injection", stacklevel=2)

# convert seconds to minutes
return frame_start / 60, frame_duration / 60

Expand Down
8 changes: 4 additions & 4 deletions src/dynamicpet/petbids/petbidsmatrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ def __init__(

# need to make a copy of json_dict before storing
self.json_dict: PetBidsJson = deepcopy(json_dict)
self.set_timezero(anchor="InjectionStart")

# def get_elem(self, elem: str) -> "PETBIDSMatrix":
# """Get timeseries data for a specific element."""
Expand All @@ -65,8 +64,8 @@ def extract(self, start_time: RealNumber, end_time: RealNumber) -> "PETBIDSMatri
"""Extract a temporally shorter PETBIDSMatrix from a PETBIDSMatrix.
Args:
start_time: time at which to begin, inclusive (minute post-injection)
end_time: time at which to stop, inclusive (minute post-injection)
start_time: time (min) at which to begin relative to TimeZero, incl.
end_time: time (min) at which to stop relative to TimeZero, incl.
Returns:
extracted_img: extracted PETBIDSMatrix
Expand All @@ -86,13 +85,14 @@ def concatenate(self, other: "PETBIDSMatrix") -> "PETBIDSMatrix": # type: ignor
Returns:
concatenated PETBIDSMatrix
"""
offset = self._decay_correct_offset(other)
offset, original_anchor = self._decay_correct_offset(other)
other = other.decay_correct(decaycorrecttime=offset)

concat_mat = super().concatenate(other)
json_dict = update_frametiming_from(self.json_dict, concat_mat)

concat_res = PETBIDSMatrix(concat_mat.dataobj, json_dict)
concat_res.set_timezero(anchor=original_anchor)

return concat_res

Expand Down
47 changes: 23 additions & 24 deletions src/dynamicpet/petbids/petbidsobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,21 @@

from abc import ABC
from typing import Literal
from typing import Tuple

import numpy as np

from ..temporalobject.temporalobject import TemporalObject
from ..typing_utils import NumpyNumberArray
from .petbidsjson import PetBidsJson
from .petbidsjson import _timediff
from .petbidsjson import get_hhmmss
from .petbidsjson import get_radionuclide_halflife
from .petbidsjson import timediff


class PETBIDSObject(TemporalObject["PETBIDSObject"], ABC):
"""PETBIDSObject abstract base class.
PETBIDSObject stores all times relative to injection start. That is, the
InjectionStart tag in the PET-BIDS json is 0 in its internal representation,
with necessary time shifts applied to other relevant tags, and TimeZero
corresponds to the time of injection.
This is done to facilitate extract and concatenate functions, which need
an anchor time point that is not relative to the imaging data.
Attributes:
frame_start: vector containing the start times of each frame
frame_duration: vector containing the durations of each frame
Expand Down Expand Up @@ -112,7 +106,9 @@ def get_decay_uncorrected_tacs(self) -> NumpyNumberArray:
)
return self.dataobj / factor

def _decay_correct_offset(self, other: "PETBIDSObject") -> float:
def _decay_correct_offset(
self, other: "PETBIDSObject"
) -> Tuple[float, Literal["InjectionStart", "ScanStart"]]:
"""Calculate ImageDecayCorrectionTime offset needed to match other to self.
This is a helper function for concatenate.
Expand All @@ -121,40 +117,43 @@ def _decay_correct_offset(self, other: "PETBIDSObject") -> float:
other: PETBIDSObject to be adjusted, if needed
Returns:
offset
offset: time offset in seconds
original_anchor: anchor time of self
Raises:
ValueError: radionuclides or injection/scan times are incompatible
"""
# check if scans are combineable
# - verify same radionuclide
if (
self.json_dict["TracerRadionuclide"]
!= other.json_dict["TracerRadionuclide"]
):
raise ValueError("Radionuclides are incompatible")

# check injection times
# - verify same injection time
if get_hhmmss(self.json_dict, "InjectionStart") != get_hhmmss(
other.json_dict, "InjectionStart"
):
raise ValueError("Injection times are incompatible")

# check scan start times
# - check scan timing
this_scanstart = get_hhmmss(self.json_dict, "ScanStart")
other_scanstart = get_hhmmss(other.json_dict, "ScanStart")
if _timediff(other_scanstart, this_scanstart) <= self.total_duration:
if timediff(other_scanstart, this_scanstart) <= self.total_duration:
raise ValueError("Scan times are incompatible")

# check decay correction
# this_decaycorrtime = get_decaycorr_rel_to_scanstart(self.json_dict)
# other_decaycorrtime = get_decaycorr_rel_to_scanstart(other.json_dict)
# if this_decaycorrtime != other_decaycorrtime + self.total_duration:
# # need to change other's decay correction to match this one's
# other.decay_correct(
# decaycorrecttime=-(other_decaycorrtime + self.total_duration)
# )

this_decaycorrtime = get_hhmmss(self.json_dict, "ImageDecayCorrectionTime")
other_decaycorrtime = get_hhmmss(other.json_dict, "ImageDecayCorrectionTime")
offset = _timediff(this_decaycorrtime, other_decaycorrtime)
offset = timediff(this_decaycorrtime, other_decaycorrtime)

original_anchor: Literal["InjectionStart", "ScanStart"]
if self.json_dict["InjectionStart"] == 0:
original_anchor = "InjectionStart"
else:
original_anchor = "ScanStart"
self.set_timezero(anchor="InjectionStart")

other.set_timezero(anchor="InjectionStart")

return offset
return offset, original_anchor
31 changes: 30 additions & 1 deletion tests/test_petbidsjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dynamicpet.petbids.petbidsjson import get_frametiming_in_mins
from dynamicpet.petbids.petbidsjson import get_hhmmss
from dynamicpet.petbids.petbidsjson import get_radionuclide_halflife
from dynamicpet.petbids.petbidsjson import timediff


def test_get_hhmmss_scanstart0() -> None:
Expand Down Expand Up @@ -47,6 +48,34 @@ def test_get_hhmmss_injstart0() -> None:
)


def test_timediff() -> None:
"""Test timediff."""
my_json_dict: PetBidsJson = {
"TimeZero": "10:00:00",
"InjectionStart": -120,
"ScanStart": 0,
"FrameTimesStart": [0, 120, 240],
"FrameDuration": [120, 120, 120],
"TracerRadionuclide": "C11",
"ImageDecayCorrected": True,
"ImageDecayCorrectionTime": -60,
}
assert (
timediff(
get_hhmmss(my_json_dict, "ScanStart"),
get_hhmmss(my_json_dict, "InjectionStart"),
)
== my_json_dict["ScanStart"] - my_json_dict["InjectionStart"]
)
assert (
timediff(
get_hhmmss(my_json_dict, "ImageDecayCorrectionTime"),
get_hhmmss(my_json_dict, "InjectionStart"),
)
== my_json_dict["ImageDecayCorrectionTime"] - my_json_dict["InjectionStart"]
)


def test_get_frametiming_from_json_scanstart0() -> None:
"""Test getting frame_start and frame_end from json when scan start is 0."""
my_json_dict: PetBidsJson = {
Expand All @@ -59,7 +88,7 @@ def test_get_frametiming_from_json_scanstart0() -> None:
"ImageDecayCorrected": True,
}
frame_start, frame_duration = get_frametiming_in_mins(my_json_dict)
assert np.all(frame_start == np.array([120, 240, 360]) / 60)
assert np.all(frame_start == np.array([0, 120, 240]) / 60)
assert np.all(frame_duration == np.array([120, 120, 120]) / 60)


Expand Down
15 changes: 15 additions & 0 deletions tests/test_petbidsmatrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,18 @@ def test_decay_uncorrect_correct(pm: PETBIDSMatrix) -> None:
assert np.allclose(pm.dataobj, pm2.dataobj)
assert np.all(pm.frame_start == pm2.frame_start)
assert np.all(pm.frame_end == pm2.frame_end)


def test_set_timezero(pm: PETBIDSMatrix) -> None:
"""Test setting time zero to InjectionStart then back to ScanStart."""
pm.json_dict["InjectionStart"] = -3600

pm.set_timezero("InjectionStart")
assert pm.json_dict["InjectionStart"] == 0
assert pm.json_dict["ScanStart"] == 3600
assert pm.frame_start[0] == 60

pm.set_timezero("ScanStart")
assert pm.json_dict["ScanStart"] == 0
assert pm.json_dict["InjectionStart"] == -3600
assert pm.frame_start[0] == 0

0 comments on commit efa128b

Please sign in to comment.