TimeActivityCurve ================================================== .. py:class:: petpal.utils.time_activity_curve.TimeActivityCurve Class to store time activity curve (TAC) data. :ivar times: Frame times for the TAC stored in an array. :vartype times: np.ndarray :ivar activity: Activity values at each frame time stored in an array. :vartype activity: np.ndarray :ivar uncertainty: Uncertainty in the measurement of activity values stored in an array. :vartype uncertainty: np.ndarray .. rubric:: Example .. code-block:: python from petpal.utils.time_activity_curve import TimeActivityCurve my_tac = TimeActivityCurve.from_tsv('/path/to/tac.tsv') print(f"Frame times: {my_tac.times}") print(f"Activity: {my_tac.activity}") print(f"Uncertainty: {my_tac.uncertainty}") my_tac.times = my_tac.times / 60 # convert time units to hours my_tac.to_tsv(filename='/path/to/new_tac.tsv') # save as new file .. py:method:: __len__() -> int Returns the number of time points in the time-activity curve (TAC). This method provides the length of the `times` attribute, representing the number of discrete time points associated with the TAC. :returns: *int* -- The number of time points in the TAC. .. rubric:: Example .. code-block:: python from petpal.utils.time_activity_curve import TimeActivityCurve # Create a TimeActivityCurve object my_tac = TimeActivityCurve( times=np.array([0, 10, 20, 30]), activity=np.array([1.2, 2.3, 3.4, 4.5]) ) # Get the number of time points print(len(my_tac)) # Output: 4 .. py:method:: __post_init__() .. py:method:: validate_activity() Validates that the activity attribute is defined correctly. `self.activity` must have the following properties: 1) It must exist and not be None 2) It must be a numpy array 3) It must have dtype float 4) It must be 1D This function raises a ValueError if self.activity does not meet the first criteria, and attempts to coerce self.activity into a 1D, numeric numpy array with dtype float if criteria 2-4 are not met. .. py:method:: from_tsv(filename: str) :classmethod: Load an instance of TimeActivityCurve object from a TSV TAC file. Reads a TSV file using :func:`~.safe_load_tac` and instantiates a TimeActivityCurve. :param filename: Path to the TSV TAC file. :type filename: str :returns: *(TimeActivityCurve)* -- A TimeActivityCurve object loaded from a TSV TAC file. .. py:property:: tac :type: numpy.ndarray Get the TAC array, not including uncertainties. :returns: *(np.ndarray)* -- The TAC as a contiguous array, with the first index being time and the second index being activity. .. py:property:: tac_werr :type: numpy.ndarray Get the TAC array, including uncertainties. :returns: *(np.ndarray)* -- The TAC as a contiguous array, with the first index being time and the second index being activity, and the third index being uncertainty. .. py:property:: times_in_mins :type: numpy.ndarray[float] Returns the TAC measured times in minutes. Validates values by checking if the final frame value is greater than 200: if so, then assumes values are in seconds and divides by 60. .. py:method:: to_tsv(filename: str, col_names: list[str] = None) Writes the TAC object to file, including measurement times, activity, and uncertainty. :param filename: Path to the file that will be written to. :type filename: str :param col_names: Custom names for time, activity, and uncertainty columns respectively. See :func:`~.safe_write_tac`. Default None. :type col_names: list[str] .. py:method:: set_activity_non_negative() -> TimeActivityCurve Ensures that the time-activity curve (TAC) data is physically consistent. This method modifies the TAC object in place by setting `uncertainty` to `NaN` and `activity` to `0` for time points where the `activity` values are negative. Such adjustments help maintain data consistency for downstream analysis. :returns: *TimeActivityCurve* -- The updated TAC object with sanitized data. .. note:: The method returns self to allow for `.`-chaining. .. rubric:: Example .. code-block:: python from petpal.utils.time_activity_curve import TimeActivityCurve # Create a TimeActivityCurve object with negative activity my_tac = TimeActivityCurve( times=np.array([0, 10, 20, 30]), activity=np.array([1.2, -2.3, 3.4, -4.5]), uncertainty=np.array([0.1, 0.2, 0.3, 0.4]) ) # Sanitize the TAC my_tac.set_activity_non_negative() print(my_tac.activity) # Output: [1.2, 0.0, 3.4, 0.0] print(my_tac.uncertainty) # Output: [0.1, NaN, 0.3, NaN] .. py:method:: evenly_resampled_tac(num_samples: int = 8192) -> TimeActivityCurve Generates a time-activity curve (TAC) resampled at evenly spaced time points. This method uses linear interpolation to recreate the TAC with a specified number of evenly spaced samples between the initial and final time points. The resulting TAC is sanitized to ensure physical consistency. Uses :class:`scipy.interpolate.interp1d` for the interpolation with ``kind=='linear'`` and ``fill_value='extrapolate'``. .. important:: If the TAC will be used for convolution later, prefer powers of two for the number of samples. :param num_samples: The number of time points in the resampled TAC. Must be greater than 2. Defaults to 4096. :type num_samples: int, optional :returns: *TimeActivityCurve* -- A new `TimeActivityCurve` instance with evenly spaced time points and resampled activity values. :raises AssertionError: If `num_samples` is less than or equal to 2. .. rubric:: Example .. code-block:: python from petpal.utils.time_activity_curve import TimeActivityCurve # Create a TimeActivityCurve object my_tac = TimeActivityCurve( times=np.array([0, 10, 20, 30]), activity=np.array([1.0, 2.0, 3.0, 4.0]) ) # Resample the TAC with evenly spaced time points resampled_tac = my_tac.evenly_resampled_tac(num_samples=10) print(resampled_tac.times) # Output: Array of 10 evenly spaced times print(resampled_tac.activity) # Output: Interpolated activity values .. py:method:: evenly_resampled_tac_given_dt(dt: float = 0.1 / 60.0) -> TimeActivityCurve Generates a time-activity curve (TAC) resampled at evenly spaced time intervals. This method calculates the number of samples required to achieve the specified time interval (`dt`) and then resamples the TAC using linear interpolation. The resulting TAC is sanitized to ensure physical consistency. :param dt: The desired time interval between consecutive resampled time points (in the same unit as `times`). Must be greater than 0. Defaults to 0.1 / 60.0 (approximately 0.00167). :type dt: float, optional :returns: *TimeActivityCurve* -- A new `TimeActivityCurve` instance with evenly spaced time intervals and resampled activity values. :raises AssertionError: If `dt` is less than or equal to 0. .. rubric:: Example .. code-block:: python from petpal.utils.time_activity_curve import TimeActivityCurve # Create a TimeActivityCurve object my_tac = TimeActivityCurve( times=np.array([0, 10, 20, 30]), activity=np.array([1.0, 2.0, 3.0, 4.0]) ) # Resample the TAC with a given time interval (dt) resampled_tac = my_tac.evenly_resampled_tac_given_dt(dt=0.1) print(resampled_tac.times) # Output: Evenly spaced time points with interval dt print(resampled_tac.activity) # Output: Interpolated activity values .. py:method:: resampled_tac_on_times(new_times: numpy.ndarray) -> TimeActivityCurve Resamples the time-activity curve (TAC) on specified time points. This method uses linear interpolation to compute activity values at the provided time points (`new_times`). The resulting TAC is sanitized to ensure physical consistency. :param new_times: An array of time points where the TAC should be resampled. Must be a 1D array of monotonically increasing values. :type new_times: np.ndarray :returns: *TimeActivityCurve* -- A new `TimeActivityCurve` instance with the specified time points and interpolated activity values. .. rubric:: Example .. code-block:: python from petpal.utils.time_activity_curve import TimeActivityCurve import numpy as np # Create a TimeActivityCurve object my_tac = TimeActivityCurve( times=np.array([0, 10, 20, 30]), activity=np.array([1.0, 2.0, 3.0, 4.0]) ) # New time points for resampling new_times = np.array([5, 15, 25]) # Resample TAC on new time points resampled_tac = my_tac.resampled_tac_on_times(new_times=new_times) print(resampled_tac.times) # Output: [5, 15, 25] print(resampled_tac.activity) # Output: Interpolated activity values at [5, 15, 25] .. py:method:: add_zero_time_and_activity() Ensures the time-activity curve (TAC) starts at time 0 with zero activity. If the first time point in the TAC is not 0.0, this method prepends a time point of 0.0 and assigns it an activity value of 0. The associated uncertainty for this time point is set to `NaN`. The method modifies the TAC in place and returns the updated instance. :returns: *TimeActivityCurve* -- The updated `TimeActivityCurve` instance with 0.0 prepended to time, activity, and uncertainty arrays (if needed). .. rubric:: Example .. code-block:: python from petpal.utils.time_activity_curve import TimeActivityCurve import numpy as np # Create a TimeActivityCurve object my_tac = TimeActivityCurve( times=np.array([10, 20, 30]), activity=np.array([2.0, 3.0, 4.0]), uncertainty=np.array([0.1, 0.2, 0.3]) ) # Add 0 time and activity if missing my_tac = my_tac.add_zero_time_and_activity() print(my_tac.times) # Output: [ 0, 10, 20, 30 ] print(my_tac.activity) # Output: [ 0, 2.0, 3.0, 4.0 ] print(my_tac.uncertainty) # Output: [ NaN, 0.1, 0.2, 0.3 ] .. py:method:: shifted_tac(shift_in_mins: float = 10.0 / 60.0, dt: float | None = None) -> TimeActivityCurve Returns a time-activity curve (TAC) shifted in time by a specified amount. If the shift is positive, the TAC is left-shifted; if negative, the TAC is right-shifted. The shift can be applied at the existing time points of the TAC or at oversampled points, depending on the value of `dt`. :param shift_in_mins: The amount (in minutes) to shift the TAC. Positive values indicate a left shift; negative values indicate a right shift. Defaults to 10.0 / 60.0 (10 seconds). :type shift_in_mins: float, optional :param dt: The time interval for oversampling. If `None`, the TAC is resampled at its original time points. If a value is provided, the TAC is resampled at an evenly spaced interval of `dt`. :type dt: float | None, optional :returns: *TimeActivityCurve* -- A new `TimeActivityCurve` instance with the shifted activity values. :raises AssertionError: If `dt` is equal to 0. .. rubric:: Example .. code-block:: python # Left-shifting the TAC shifted_tac = my_tac.shifted_tac(shift_in_mins=5.0 / 60.0) # Right-shifting the TAC with oversampling shifted_tac = my_tac.shifted_tac(shift_in_mins=-2.0 / 60.0, dt=0.1 / 60.0) .. seealso:: * :meth:`left_shifted_tac` * :meth:`right_shifted_tac` .. py:method:: left_shifted_tac(tac: TimeActivityCurve, shift_in_mins: float = 10.0 / 60.0, dt: float | None = 0.1 / 60.0) -> TimeActivityCurve :staticmethod: Produces a TAC left-shifted by a specified amount of time. The left shift moves activity values earlier in time and fills the trailing values with interpolated data. The shift can either be applied on the original time points or on a time grid determined by `dt`. :param tac: The original TAC to be shifted. :type tac: TimeActivityCurve :param shift_in_mins: The amount (in minutes) to left-shift the TAC. Must be larger than 0. Defaults to 10.0 / 60.0 (10 seconds). :type shift_in_mins: float, optional :param dt: The time interval for oversampling. If `None`, the TAC is resampled at its original time points. If a value is provided, the TAC is resampled at an evenly spaced interval of `dt`. :type dt: float | None, optional :returns: *TimeActivityCurve* -- A new `TimeActivityCurve` instance with the left-shifted activity values. :raises AssertionError: If `shift_in_mins` is not greater than 0. .. rubric:: Example .. code-block:: python # Perform a left shift on the TAC shifted_tac = TimeActivityCurve.left_shifted_tac(tac=my_tac, shift_in_mins=5.0 / 60.0) # Perform a left shift with oversampling shifted_tac = TimeActivityCurve.left_shifted_tac(tac=my_tac, shift_in_mins=2.0 / 60.0, dt=0.05 / 60.0) .. py:method:: right_shifted_tac(tac: TimeActivityCurve, shift_in_mins: float = 10.0 / 60.0, dt: float | None = 0.1 / 60.0) -> TimeActivityCurve :staticmethod: Produces a TAC right-shifted by a specified amount of time. The right shift moves activity values later in time and fills the leading values with interpolated data. The shift can either be applied on the original time points or on a time grid determined by `dt`. :param tac: The original TAC to be shifted. :type tac: TimeActivityCurve :param shift_in_mins: The amount (in minutes) to right-shift the TAC. Must be larger than 0. Defaults to 10.0 / 60.0 (10 seconds). :type shift_in_mins: float, optional :param dt: The time interval for oversampling. If `None`, the TAC is resampled at its original time points. If a value is provided, the TAC is resampled at an evenly spaced interval of `dt`. :type dt: float | None, optional :returns: *TimeActivityCurve* -- A new `TimeActivityCurve` instance with the right-shifted activity values. :raises AssertionError: If `shift_in_mins` is not greater than 0. .. rubric:: Example .. code-block:: python # Perform a right shift on the TAC shifted_tac = TimeActivityCurve.right_shifted_tac(tac=my_tac, shift_in_mins=5.0 / 60.0) # Perform a right shift with oversampling shifted_tac = TimeActivityCurve.right_shifted_tac(tac=my_tac, shift_in_mins=2.0 / 60.0, dt=0.05 / 60.0) .. py:method:: tac_dispersion(tac: TimeActivityCurve, disp_func: Callable[Ellipsis, numpy.ndarray], disp_kwargs: dict, num_samples: int = 4096) :staticmethod: Applies a dispersion function to a time-activity curve (TAC) and returns the convolved TAC. This method evaluates the specified dispersion function `disp_func` at supersampled time points. It performs convolution (using :func:`scipy.signal.convolve`) of the supersampled TAC with the dispersion function, and the result is sampled back at the original TAC time points to form the new convolved TAC. .. note:: We perform the supersampling to ensure that the TACs are sampled evenly before performing the convolution. Convolving non-evenly sampled arrays produces nonsense values. :param tac: The original time-activity curve to be convolved. :type tac: TimeActivityCurve :param disp_func: The dispersion function to be applied. This function must accept an array of times as its first argument, followed by any additional arguments specified in `disp_kwargs`. :type disp_func: Callable[..., np.ndarray] :param disp_kwargs: Additional keyword arguments to pass to `disp_func`. :type disp_kwargs: dict :param num_samples: The number of evenly spaced samples for supersampling the TAC before convolution. Defaults to 4096. :type num_samples: int, optional :returns: *TimeActivityCurve* -- A new `TimeActivityCurve` instance with the convolved activity values, resampled at the original TAC time points. .. rubric:: Example .. code-block:: python from petpal.utils.time_activity_curve import TimeActivityCurve import numpy as np # Define a sample dispersion function def monoexponential_kernel(t:np.ndarray, tau:float) -> np.ndarray: return (1.0/tau) * np.exp(-t / tau) # Original TAC my_tac = TimeActivityCurve( times=np.array([0, 10, 20, 30]), activity=np.array([1.0, 2.0, 3.0, 4.0]) ) # Apply dispersion convolved_tac = TimeActivityCurve.tac_dispersion( tac=my_tac, disp_func=monoexponential_kernel, disp_kwargs={'tau': 5}, num_samples=1024 ) print(convolved_tac.times) # Output: Original TAC time points print(convolved_tac.activity) # Output: Activity after applying dispersion .. seealso:: * :meth:`evenly_resample_tac` .. py:property:: contains_any_nan Return True if TAC has any NaN activity values.