diff --git a/src/dynamicpet/petbids/petbidsimage.py b/src/dynamicpet/petbids/petbidsimage.py index 2382312..f19b2d3 100644 --- a/src/dynamicpet/petbids/petbidsimage.py +++ b/src/dynamicpet/petbids/petbidsimage.py @@ -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 @@ -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 diff --git a/src/dynamicpet/petbids/petbidsjson.py b/src/dynamicpet/petbids/petbidsjson.py index f83721e..21bd1cb 100644 --- a/src/dynamicpet/petbids/petbidsjson.py +++ b/src/dynamicpet/petbids/petbidsjson.py @@ -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) @@ -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: @@ -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. @@ -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 diff --git a/src/dynamicpet/petbids/petbidsmatrix.py b/src/dynamicpet/petbids/petbidsmatrix.py index f332da6..f509fa8 100644 --- a/src/dynamicpet/petbids/petbidsmatrix.py +++ b/src/dynamicpet/petbids/petbidsmatrix.py @@ -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.""" @@ -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 @@ -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 diff --git a/src/dynamicpet/petbids/petbidsobject.py b/src/dynamicpet/petbids/petbidsobject.py index f5be873..78649f2 100644 --- a/src/dynamicpet/petbids/petbidsobject.py +++ b/src/dynamicpet/petbids/petbidsobject.py @@ -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 @@ -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. @@ -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 diff --git a/tests/test_petbidsjson.py b/tests/test_petbidsjson.py index 534fcca..1a16778 100644 --- a/tests/test_petbidsjson.py +++ b/tests/test_petbidsjson.py @@ -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: @@ -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 = { @@ -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) diff --git a/tests/test_petbidsmatrix.py b/tests/test_petbidsmatrix.py index f5d15fb..c63af10 100644 --- a/tests/test_petbidsmatrix.py +++ b/tests/test_petbidsmatrix.py @@ -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