petpal 0.5.7__tar.gz → 0.5.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {petpal-0.5.7 → petpal-0.5.8}/.gitignore +4 -1
  2. {petpal-0.5.7 → petpal-0.5.8}/PKG-INFO +1 -1
  3. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/decay_correction.py +11 -10
  4. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/scan_timing.py +66 -19
  5. {petpal-0.5.7 → petpal-0.5.8}/pyproject.toml +1 -1
  6. petpal-0.5.8/tests/test_register.py +82 -0
  7. petpal-0.5.8/tests/test_scan_timing_decay.py +146 -0
  8. petpal-0.5.8/tests/test_weighted_sum.py +51 -0
  9. {petpal-0.5.7 → petpal-0.5.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {petpal-0.5.7 → petpal-0.5.8}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {petpal-0.5.7 → petpal-0.5.8}/.github/workflows/publish-to-pypi.yml +0 -0
  12. {petpal-0.5.7 → petpal-0.5.8}/.github/workflows/python-package.yml +0 -0
  13. {petpal-0.5.7 → petpal-0.5.8}/.readthedocs.yaml +0 -0
  14. {petpal-0.5.7 → petpal-0.5.8}/LICENSE +0 -0
  15. {petpal-0.5.7 → petpal-0.5.8}/README.md +0 -0
  16. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-00.txt +0 -0
  17. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-01.txt +0 -0
  18. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-02.txt +0 -0
  19. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/gaussian_noise/tacs.pdf +0 -0
  20. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/gaussian_noise/tacs.png +0 -0
  21. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-00.txt +0 -0
  22. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-01.txt +0 -0
  23. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-02.txt +0 -0
  24. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/noise_free/tacs.pdf +0 -0
  25. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/noise_free/tacs.png +0 -0
  26. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/params_1tcm_set-00.json +0 -0
  27. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/params_1tcm_set-01.json +0 -0
  28. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/1tcm/params_1tcm_set-02.json +0 -0
  29. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled.txt +0 -0
  30. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled_woMax.txt +0 -0
  31. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/fdg_plasma_clamp_tacs.pdf +0 -0
  32. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/fdg_plasma_clamp_tacs.png +0 -0
  33. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/gen_tcms_data.ipynb +0 -0
  34. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/readme.md +0 -0
  35. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-00.txt +0 -0
  36. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-01.txt +0 -0
  37. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-02.txt +0 -0
  38. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.pdf +0 -0
  39. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.png +0 -0
  40. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-00.txt +0 -0
  41. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-01.txt +0 -0
  42. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-02.txt +0 -0
  43. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/noise_free/tacs.pdf +0 -0
  44. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/noise_free/tacs.png +0 -0
  45. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-00.json +0 -0
  46. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-01.json +0 -0
  47. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-02.json +0 -0
  48. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-00.txt +0 -0
  49. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-01.txt +0 -0
  50. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.pdf +0 -0
  51. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.png +0 -0
  52. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-00.txt +0 -0
  53. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-01.txt +0 -0
  54. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.pdf +0 -0
  55. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.png +0 -0
  56. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-00.json +0 -0
  57. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-01.json +0 -0
  58. {petpal-0.5.7 → petpal-0.5.8}/data/tcm_tacs/turku_pet_center_fdg_plasma_clamp.txt +0 -0
  59. {petpal-0.5.7 → petpal-0.5.8}/docs/Makefile +0 -0
  60. {petpal-0.5.7 → petpal-0.5.8}/docs/PETPAL_Logo.png +0 -0
  61. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/index.rst +0 -0
  62. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/python/attribute.rst +0 -0
  63. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/python/class.rst +0 -0
  64. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/python/data.rst +0 -0
  65. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/python/exception.rst +0 -0
  66. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/python/function.rst +0 -0
  67. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/python/method.rst +0 -0
  68. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/python/module.rst +0 -0
  69. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/python/package.rst +0 -0
  70. {petpal-0.5.7 → petpal-0.5.8}/docs/_templates/python/property.rst +0 -0
  71. {petpal-0.5.7 → petpal-0.5.8}/docs/conf.py +0 -0
  72. {petpal-0.5.7 → petpal-0.5.8}/docs/index.rst +0 -0
  73. {petpal-0.5.7 → petpal-0.5.8}/docs/make.bat +0 -0
  74. {petpal-0.5.7 → petpal-0.5.8}/docs/requirements.txt +0 -0
  75. {petpal-0.5.7 → petpal-0.5.8}/docs/tutorials/index.rst +0 -0
  76. {petpal-0.5.7 → petpal-0.5.8}/docs/tutorials/pib_example.rst +0 -0
  77. {petpal-0.5.7 → petpal-0.5.8}/petpal/__init__.py +0 -0
  78. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/__init__.py +0 -0
  79. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_graphical_analysis.py +0 -0
  80. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_graphical_plots.py +0 -0
  81. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_idif.py +0 -0
  82. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_parametric_images.py +0 -0
  83. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_pib_processing.py +0 -0
  84. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_plot_tacs.py +0 -0
  85. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_preproc.py +0 -0
  86. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_pvc.py +0 -0
  87. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_reference_tissue_models.py +0 -0
  88. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_stats.py +0 -0
  89. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_tac_fitting.py +0 -0
  90. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_tac_interpolation.py +0 -0
  91. {petpal-0.5.7 → petpal-0.5.8}/petpal/cli/cli_vat_processing.py +0 -0
  92. {petpal-0.5.7 → petpal-0.5.8}/petpal/input_function/__init__.py +0 -0
  93. {petpal-0.5.7 → petpal-0.5.8}/petpal/input_function/blood_input.py +0 -0
  94. {petpal-0.5.7 → petpal-0.5.8}/petpal/input_function/idif_necktangle.py +0 -0
  95. {petpal-0.5.7 → petpal-0.5.8}/petpal/input_function/pca_guided_idif.py +0 -0
  96. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/__init__.py +0 -0
  97. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/fit_tac_with_rtms.py +0 -0
  98. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/graphical_analysis.py +0 -0
  99. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/parametric_images.py +0 -0
  100. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/reference_tissue_models.py +0 -0
  101. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/rtm_analysis.py +0 -0
  102. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/tac_fitting.py +0 -0
  103. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/tac_interpolation.py +0 -0
  104. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/tac_uncertainty.py +0 -0
  105. {petpal-0.5.7 → petpal-0.5.8}/petpal/kinetic_modeling/tcms_as_convolutions.py +0 -0
  106. {petpal-0.5.7 → petpal-0.5.8}/petpal/meta/__init__.py +0 -0
  107. {petpal-0.5.7 → petpal-0.5.8}/petpal/meta/label_maps.py +0 -0
  108. {petpal-0.5.7 → petpal-0.5.8}/petpal/pipelines/__init__.py +0 -0
  109. {petpal-0.5.7 → petpal-0.5.8}/petpal/pipelines/kinetic_modeling_steps.py +0 -0
  110. {petpal-0.5.7 → petpal-0.5.8}/petpal/pipelines/pca_guided_idif_steps.py +0 -0
  111. {petpal-0.5.7 → petpal-0.5.8}/petpal/pipelines/pipelines.py +0 -0
  112. {petpal-0.5.7 → petpal-0.5.8}/petpal/pipelines/preproc_steps.py +0 -0
  113. {petpal-0.5.7 → petpal-0.5.8}/petpal/pipelines/steps_base.py +0 -0
  114. {petpal-0.5.7 → petpal-0.5.8}/petpal/pipelines/steps_containers.py +0 -0
  115. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/__init__.py +0 -0
  116. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/image_operations_4d.py +0 -0
  117. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/motion_corr.py +0 -0
  118. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/motion_target.py +0 -0
  119. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/partial_volume_corrections.py +0 -0
  120. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/regional_tac_extraction.py +0 -0
  121. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/register.py +0 -0
  122. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/segmentation_tools.py +0 -0
  123. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/standard_uptake_value.py +0 -0
  124. {petpal-0.5.7 → petpal-0.5.8}/petpal/preproc/symmetric_geometric_transfer_matrix.py +0 -0
  125. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/__init__.py +0 -0
  126. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/bids_utils.py +0 -0
  127. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/constants.py +0 -0
  128. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/data_driven_image_analyses.py +0 -0
  129. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/decorators.py +0 -0
  130. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/image_io.py +0 -0
  131. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/math_lib.py +0 -0
  132. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/metadata.py +0 -0
  133. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/stats.py +0 -0
  134. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/testing_utils.py +0 -0
  135. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/time_activity_curve.py +0 -0
  136. {petpal-0.5.7 → petpal-0.5.8}/petpal/utils/useful_functions.py +0 -0
  137. {petpal-0.5.7 → petpal-0.5.8}/petpal/visualizations/__init__.py +0 -0
  138. {petpal-0.5.7 → petpal-0.5.8}/petpal/visualizations/graphical_plots.py +0 -0
  139. {petpal-0.5.7 → petpal-0.5.8}/petpal/visualizations/image_visualization.py +0 -0
  140. {petpal-0.5.7 → petpal-0.5.8}/petpal/visualizations/qc_plots.py +0 -0
  141. {petpal-0.5.7 → petpal-0.5.8}/petpal/visualizations/tac_plots.py +0 -0
  142. {petpal-0.5.7 → petpal-0.5.8}/shared/dseg.tsv +0 -0
  143. {petpal-0.5.7 → petpal-0.5.8}/shared/freesurfer_lmap.json +0 -0
  144. {petpal-0.5.7 → petpal-0.5.8}/shared/freesurfer_lmap_lr.json +0 -0
  145. {petpal-0.5.7 → petpal-0.5.8}/shared/perl_cyno_lmap.json +0 -0
  146. {petpal-0.5.7 → petpal-0.5.8}/shared/perl_cyno_lmap_lr.json +0 -0
  147. {petpal-0.5.7 → petpal-0.5.8}/test_notebooks/explicit_tac_fitting/01_fitting_TCMs.ipynb +0 -0
  148. {petpal-0.5.7 → petpal-0.5.8}/test_notebooks/testing_RTMs/01_testing_RTMs.ipynb +0 -0
  149. {petpal-0.5.7 → petpal-0.5.8}/test_notebooks/testing_graphical_analyses/01_testing_on_tcms_database.ipynb +0 -0
  150. {petpal-0.5.7 → petpal-0.5.8}/test_notebooks/testing_graphical_analyses/02_testing_parametric_images.ipynb +0 -0
  151. {petpal-0.5.7 → petpal-0.5.8}/test_notebooks/testing_graphical_analyses/03_plotting_graphical_anlayses_testbed.ipynb +0 -0
  152. {petpal-0.5.7 → petpal-0.5.8}/tests/test_importpetpal.py +0 -0
@@ -236,4 +236,7 @@ fabric.properties
236
236
  .idea/caches/build_file_checksums.ser
237
237
 
238
238
  # notebooks for testing new things
239
- debug_notebooks
239
+ debug_notebooks
240
+
241
+ # vscode
242
+ .vscode
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: petpal
3
- Version: 0.5.7
3
+ Version: 0.5.8
4
4
  Summary: PET-PAL (Positron Emission Tomography Processing and Analysis Library)
5
5
  Project-URL: Repository, https://github.com/PETPAL-WUSM/PETPAL.git
6
6
  Author-email: Noah Goldman <noahg@wustl.edu>, Bradley Judge <bjudge@wustl.edu>, Furqan Dar <dar@wustl.edu>, Kenan Oestreich <kenan.oestreich@wustl.edu>
@@ -129,21 +129,22 @@ def decay_correct(input_image_path: str,
129
129
  return corrected_image
130
130
 
131
131
 
132
- def calculate_frame_decay_factor(frame_reference_time: float,
133
- half_life: float) -> float:
134
- """Calculate decay factor for a single frame, given the frame reference time and half life.
132
+ def calculate_frame_decay_factor(frame_reference_time: np.ndarray,
133
+ half_life: float) -> np.ndarray:
134
+ """Calculate decay correction factors for a scan given the frame reference time and half life.
135
135
 
136
136
  Important:
137
137
  The frame reference time should be the time at which average activity occurs,
138
- not simply the midpoint. See preproc.scan_timing.calculate_frame_reference_time for more info.
139
-
138
+ not simply the midpoint. See
139
+ :meth:`~petpal.utils.scan_timing.calculate_frame_reference_time` for more info.
140
+
140
141
  Args:
141
- frame_reference_time (float): Time at which the average activity occurs for the frame.
142
+ frame_reference_time (np.ndarray): Time at which the average activity occurs for the frame.
142
143
  half_life (float): Radionuclide half life.
143
144
 
144
145
  Returns:
145
- float: Decay Correction Factor for the frame.
146
+ np.ndarray: Decay Correction Factors for each frame in the scan.
146
147
  """
147
- decay_constant = math.log(2)/half_life
148
- frame_decay_factor = math.exp((decay_constant)*frame_reference_time)
149
- return frame_decay_factor
148
+ decay_constant = np.log(2)/half_life
149
+ frame_decay_factor = np.exp((decay_constant)*frame_reference_time)
150
+ return frame_decay_factor
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Module to handle timing information of PET scans.
3
3
  """
4
- import math
4
+ from typing import Self
5
5
  from dataclasses import dataclass
6
6
  import numpy as np
7
7
 
@@ -82,8 +82,8 @@ class ScanTimingInfo:
82
82
 
83
83
  """
84
84
  duration: np.ndarray[float]
85
- end: np.ndarray[float]
86
85
  start: np.ndarray[float]
86
+ end: np.ndarray[float]
87
87
  center: np.ndarray[float]
88
88
  decay: np.ndarray[float]
89
89
 
@@ -130,7 +130,7 @@ class ScanTimingInfo:
130
130
 
131
131
 
132
132
  @classmethod
133
- def from_metadata(cls, metadata_dict: dict):
133
+ def from_metadata(cls, metadata_dict: dict) -> Self:
134
134
  r"""
135
135
  Extracts frame timing information and decay factors from a json metadata.
136
136
  Expects that the JSON metadata has ``FrameDuration`` and ``DecayFactor`` or
@@ -155,13 +155,13 @@ class ScanTimingInfo:
155
155
  """
156
156
  frm_dur = np.asarray(metadata_dict['FrameDuration'], float)
157
157
  try:
158
- frm_ends = np.asarray(metadata_dict['FrameTimesEnd'], float)
158
+ frm_starts = np.asarray(metadata_dict['FrameTimesStart'], float)
159
159
  except KeyError:
160
- frm_ends = np.cumsum(frm_dur)
160
+ frm_starts = np.cumsum(frm_dur)-frm_dur
161
161
  try:
162
- frm_starts = np.asarray(metadata_dict['FrameTimesStart'], float)
162
+ frm_ends = np.asarray(metadata_dict['FrameTimesEnd'], float)
163
163
  except KeyError:
164
- frm_starts = np.diff(frm_ends)
164
+ frm_ends = frm_starts+frm_dur
165
165
  try:
166
166
  decay = np.asarray(metadata_dict['DecayCorrectionFactor'], float)
167
167
  except KeyError:
@@ -178,7 +178,7 @@ class ScanTimingInfo:
178
178
  decay=decay)
179
179
 
180
180
  @classmethod
181
- def from_nifti(cls, image_path: str):
181
+ def from_nifti(cls, image_path: str) -> Self:
182
182
  r"""
183
183
  Extracts frame timing information and decay factors from a NIfTI image metadata.
184
184
  Expects that the JSON metadata file has ``FrameDuration`` and ``DecayFactor`` or
@@ -204,6 +204,49 @@ class ScanTimingInfo:
204
204
  _meta_data = load_metadata_for_nifti_with_same_filename(image_path=image_path)
205
205
  return cls.from_metadata(metadata_dict=_meta_data)
206
206
 
207
+ @classmethod
208
+ def from_start_end(cls,
209
+ frame_starts: np.ndarray,
210
+ frame_ends: np.ndarray,
211
+ decay_correction_factor: np.ndarray | None=None) -> Self:
212
+ """Infer timing properties based on start and end time.
213
+
214
+ Args:
215
+ frame_starts (np.ndarray): Start time of each frame.
216
+ frame_ends (np.ndarray): End time of each frame.
217
+ decay_correction_factor (np.ndarray | None): Decay correction factor, which can be
218
+ optionally provided based on the type of analysis being done. If None, frame decay
219
+ will be set to ones. Default None.
220
+
221
+ Returns:
222
+ scan_timing_info (ScanTimingInfo): ScanTimingInfo object with the correct start, end,
223
+ duration, midpoint, and (optionally) decay correction for each frame.
224
+
225
+ Raises:
226
+ ValueError: If frame_starts, frame_ends, and decay_correction_factor (if provided) are
227
+ not of identical shape.
228
+
229
+ """
230
+ if frame_starts.shape != frame_ends.shape:
231
+ raise ValueError("frame_ends must have the same shape as frame_starts")
232
+
233
+ frame_duration = frame_ends - frame_starts
234
+ frame_midpoint = frame_starts + frame_duration / 2
235
+ frame_decay = np.ones_like(frame_starts)
236
+
237
+ if decay_correction_factor is None:
238
+ frame_decay = np.ones_like(frame_starts, dtype=float)
239
+ else:
240
+ frame_decay = np.asarray(decay_correction_factor, dtype=float)
241
+ if frame_decay.shape != frame_starts.shape:
242
+ raise ValueError("decay_correction_factor must have the same shape as frame_starts")
243
+
244
+ return cls(duration=frame_duration,
245
+ start=frame_starts,
246
+ end=frame_ends,
247
+ center=frame_midpoint,
248
+ decay=frame_decay)
249
+
207
250
 
208
251
  def get_window_index_pairs_from_durations(frame_durations: np.ndarray, w_size: float):
209
252
  r"""
@@ -263,25 +306,29 @@ def get_window_index_pairs_for_image(image_path: str, w_size: float):
263
306
  w_size=w_size)
264
307
 
265
308
 
266
- def calculate_frame_reference_time(frame_duration: float,
267
- frame_start: float,
268
- half_life: float) -> float:
309
+ def calculate_frame_reference_time(frame_duration: np.ndarray,
310
+ frame_start: np.ndarray,
311
+ half_life: float) -> np.ndarray:
269
312
  r"""Compute frame reference time as the time at which the average activity occurs.
270
313
 
271
- Equation comes from the `DICOM standard documentation <https://dicom.innolitics.com/ciods/positron-emission-tomography-image/pet-image/00541300>`_
314
+ Equation comes from the `DICOM standard documentation
315
+ <https://dicom.innolitics.com/ciods/positron-emission-tomography-image/pet-image/00541300>`_
272
316
 
273
317
  :math:`T_{ave}=\frac{1}{\lambda}ln\frac{\lambda T}{1-e^{-\lambda T}}`
274
318
 
275
- where lambda is the decay constant, :math:`\frac{ln2}{T_{1/2}}`, :math:`T_{1/2}` is the half life, and :math:`T` is the frame duration.
319
+ where lambda is the decay constant, :math:`\frac{ln2}{T_{1/2}}`, :math:`T_{1/2}` is the half
320
+ life, and :math:`T` is the frame duration.
276
321
 
277
322
  Args:
278
- frame_duration (float): Frame Duration in seconds
279
- frame_start (float): Start time of frame relative to scan start, in seconds
280
- half_life (float): Radionuclide half life
323
+ frame_duration (np.ndarray): Duration of each frame in seconds.
324
+ frame_start (np.ndarray): Start time of each frame relative to scan start, in seconds.
325
+ half_life (float): Radionuclide half life in seconds.
281
326
 
282
327
  Returns:
283
- float: Frame reference time
328
+ np.ndarray: Frame reference time for each frame in the scan in seconds.
284
329
  """
285
- decay_constant = math.log(2)/half_life
286
- frame_reference_time = frame_start + math.log((decay_constant*frame_duration)/(1-math.exp(-decay_constant*frame_duration)))/decay_constant
330
+ decay_constant = np.log(2)/half_life
331
+ decay_over_frame = decay_constant*frame_duration
332
+ reference_time_delay = np.log((decay_over_frame)/(1-np.exp(-decay_over_frame)))/decay_constant
333
+ frame_reference_time = frame_start + reference_time_delay
287
334
  return frame_reference_time
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "petpal"
7
- version = "0.5.7"
7
+ version = "0.5.8"
8
8
  description = "PET-PAL (Positron Emission Tomography Processing and Analysis Library)"
9
9
  authors = [
10
10
  {name = "Noah Goldman", email = "noahg@wustl.edu"},
@@ -0,0 +1,82 @@
1
+ import pytest
2
+ from types import SimpleNamespace
3
+ from petpal.preproc.register import register_pet
4
+
5
+ import petpal.preproc.register as reg_mod
6
+
7
+ class DummyImage:
8
+ def __init__(self, dimension, name=None):
9
+ self.dimension = dimension
10
+ self.name = name
11
+
12
+ def make_mocks(monkeypatch, pet_dim, recorded):
13
+ # Mock determine_motion_target to return a path string
14
+ monkeypatch.setattr(reg_mod, "determine_motion_target", lambda motion_target_option, input_image_path: "motion_target.nii")
15
+ # Mock ants.image_read to return DummyImage instances depending on path
16
+ def mock_image_read(path):
17
+ if path == "motion_target.nii":
18
+ img = DummyImage(dimension=3, name="motion_target")
19
+ elif path == "mri.nii":
20
+ img = DummyImage(dimension=3, name="mri")
21
+ elif path == "pet.nii":
22
+ img = DummyImage(dimension=pet_dim, name="pet")
23
+ else:
24
+ img = DummyImage(dimension=0, name=path)
25
+ recorded['image_reads'].append((path, img))
26
+ return img
27
+ monkeypatch.setattr(reg_mod.ants, "image_read", mock_image_read)
28
+ # Mock ants.registration to record args and return transforms
29
+ def mock_registration(*, moving, fixed, type_of_transform, write_composite_transform=True, **kwargs):
30
+ recorded['registration_calls'].append({
31
+ 'moving': moving, 'fixed': fixed, 'type_of_transform': type_of_transform, 'kwargs': kwargs
32
+ })
33
+ return {'fwdtransforms': ['/fake/xfm']}
34
+ monkeypatch.setattr(reg_mod.ants, "registration", mock_registration)
35
+ # Mock ants.apply_transforms to record imagetype and return a dummy transformed image
36
+ def mock_apply_transforms(*, moving, fixed, transformlist, interpolator=None, imagetype=None, **kwargs):
37
+ recorded['apply_calls'].append({'moving': moving, 'fixed': fixed, 'transformlist': transformlist, 'imagetype': imagetype})
38
+ return SimpleNamespace(name="transformed_image", imagetype=imagetype)
39
+ monkeypatch.setattr(reg_mod.ants, "apply_transforms", mock_apply_transforms)
40
+ # Mock ants.image_write to record output path
41
+ monkeypatch.setattr(reg_mod.ants, "image_write", lambda img, out: recorded['written'].append((img, out)))
42
+ # Mock image_io.safe_copy_meta
43
+ monkeypatch.setattr(reg_mod.image_io, "safe_copy_meta", lambda input_image_path, out_image_path: recorded['meta_copied'].append((input_image_path, out_image_path)))
44
+
45
+ @pytest.mark.parametrize("pet_dim, expected_imagetype", [
46
+ (4, 3),
47
+ (3, 0),
48
+ ])
49
+ def test_register_pet_sets_imagetype_and_writes(monkeypatch, capsys, pet_dim, expected_imagetype):
50
+ recorded = {'image_reads': [], 'registration_calls': [], 'apply_calls': [], 'written': [], 'meta_copied': []}
51
+ make_mocks(monkeypatch, pet_dim=pet_dim, recorded=recorded)
52
+
53
+ # Call function under test
54
+ register_pet(input_reg_image_path="pet.nii",
55
+ out_image_path="out.nii",
56
+ reference_image_path="mri.nii",
57
+ motion_target_option="some_option",
58
+ verbose=True,
59
+ type_of_transform="DenseRigid")
60
+
61
+ # Check registration was called with motion target as moving and mri as fixed
62
+ assert recorded['registration_calls'], "ants.registration was not called"
63
+ reg_call = recorded['registration_calls'][-1]
64
+ assert getattr(reg_call['moving'], "name", None) == "motion_target"
65
+ assert getattr(reg_call['fixed'], "name", None) == "mri"
66
+ assert reg_call['type_of_transform'] == "DenseRigid"
67
+
68
+ # Check apply_transforms was called with correct imagetype based on input dimension
69
+ assert recorded['apply_calls'], "ants.apply_transforms was not called"
70
+ apply_call = recorded['apply_calls'][-1]
71
+ assert apply_call['imagetype'] == expected_imagetype
72
+
73
+ # Check image was written and metadata copied
74
+ assert recorded['written'] and recorded['written'][-1][1] == "out.nii"
75
+ assert recorded['meta_copied'] and recorded['meta_copied'][-1] == ("pet.nii", "out.nii")
76
+
77
+ # Check verbose prints
78
+ captured = capsys.readouterr().out
79
+ print(captured)
80
+ assert "Registration computed transforming image motion_target.nii to mri.nii space" in captured
81
+ assert "Registration applied to pet.nii" in captured
82
+ assert "Transformed image saved to out.nii" in captured
@@ -0,0 +1,146 @@
1
+ import numpy as np
2
+ import pytest
3
+ from petpal.utils.scan_timing import calculate_frame_reference_time
4
+ from petpal.preproc.decay_correction import calculate_frame_decay_factor
5
+ from petpal.utils.scan_timing import ScanTimingInfo
6
+
7
+
8
+ def test_from_metadata_with_all_keys():
9
+ metadata = {
10
+ "FrameDuration": [60, 120, 180],
11
+ "FrameTimesStart": [0, 60, 180],
12
+ "FrameTimesEnd": [60, 180, 360],
13
+ "FrameReferenceTime": [30, 120, 270],
14
+ "DecayCorrectionFactor": [1.0, 0.95, 0.9],
15
+ }
16
+ sti = ScanTimingInfo.from_metadata(metadata)
17
+ np.testing.assert_allclose(sti.duration, np.array([60.0, 120.0, 180.0]))
18
+ np.testing.assert_allclose(sti.start, np.array([0.0, 60.0, 180.0]))
19
+ np.testing.assert_allclose(sti.end, np.array([60.0, 180.0, 360.0]))
20
+ np.testing.assert_allclose(sti.center, np.array([30.0, 120.0, 270.0]))
21
+ np.testing.assert_allclose(sti.decay, np.array([1.0, 0.95, 0.9]))
22
+ assert sti.duration.dtype.kind == "f"
23
+ assert sti.start.dtype.kind == "f"
24
+ assert sti.end.dtype.kind == "f"
25
+ assert sti.center.dtype.kind == "f"
26
+ assert sti.decay.dtype.kind == "f"
27
+
28
+
29
+ def test_from_metadata_infers_start_end_and_center_and_uses_decayfactor():
30
+ metadata = {
31
+ "FrameDuration": [30, 30, 60],
32
+ # no FrameTimesStart, no FrameTimesEnd, no FrameReferenceTime
33
+ "DecayFactor": [1.0, 0.99, 0.98],
34
+ }
35
+ sti = ScanTimingInfo.from_metadata(metadata)
36
+ expected_starts = np.array([0.0, 30.0, 60.0])
37
+ expected_ends = np.array([30.0, 60.0, 120.0])
38
+ expected_centers = expected_starts + np.array([30.0, 30.0, 60.0]) / 2.0
39
+ np.testing.assert_allclose(sti.start, expected_starts)
40
+ np.testing.assert_allclose(sti.end, expected_ends)
41
+ np.testing.assert_allclose(sti.center, expected_centers)
42
+ np.testing.assert_allclose(sti.duration, np.array([30.0, 30.0, 60.0]))
43
+ np.testing.assert_allclose(sti.decay, np.array([1.0, 0.99, 0.98]))
44
+
45
+
46
+ def test_from_metadata_prefers_decay_correction_over_decayfactor():
47
+ metadata = {
48
+ "FrameDuration": [10, 20],
49
+ "FrameTimesStart": [0, 10],
50
+ "FrameTimesEnd": [10, 30],
51
+ "DecayFactor": [0.5, 0.6],
52
+ "DecayCorrectionFactor": [0.9, 0.8],
53
+ }
54
+ sti = ScanTimingInfo.from_metadata(metadata)
55
+ # DecayCorrectionFactor should be used when present
56
+ np.testing.assert_allclose(sti.decay, np.array([0.9, 0.8]))
57
+
58
+
59
+ def test_from_start_end_computes_duration_center_and_default_decay():
60
+ starts = np.array([0.0, 60.0, 180.0])
61
+ ends = np.array([60.0, 180.0, 360.0])
62
+ info = ScanTimingInfo.from_start_end(frame_starts=starts, frame_ends=ends)
63
+ np.testing.assert_allclose(info.duration, np.array([60.0, 120.0, 180.0]))
64
+ np.testing.assert_allclose(info.center, np.array([30.0, 120.0, 270.0]))
65
+ np.testing.assert_allclose(info.decay, np.ones_like(starts))
66
+
67
+ def test_from_start_end_uses_provided_decay_list():
68
+ starts = np.array([0.0, 50.0])
69
+ ends = np.array([25.0, 100.0])
70
+ decay_list = [1.0, 0.9]
71
+ info = ScanTimingInfo.from_start_end(frame_starts=starts, frame_ends=ends, decay_correction_factor=decay_list)
72
+ np.testing.assert_allclose(info.duration, np.array([25.0, 50.0]))
73
+ np.testing.assert_allclose(info.center, np.array([12.5, 75.0]))
74
+ np.testing.assert_allclose(info.decay, np.array(decay_list))
75
+
76
+ def test_ref_time_no_decay_returns_midpoint():
77
+ durations = np.array([5.0, 10.0])
78
+ starts = np.array([0.0, 5.0])
79
+ half_life = 1.0e8 # effectively no decay
80
+ res = calculate_frame_reference_time(durations, starts, half_life)
81
+ expected = starts + durations / 2.0
82
+ np.testing.assert_allclose(res, expected, rtol=1e-2, atol=1e-3)
83
+
84
+
85
+ def test_ref_time_fast_decay_concentrates_near_start():
86
+ durations = np.array([60.0, 30.0, 15.0])
87
+ starts = np.array([0.0, 5.0, 10.0])
88
+ half_life = 1e-6 # very fast decay
89
+ res = calculate_frame_reference_time(durations, starts, half_life)
90
+ delays = res - starts
91
+ # For very fast decay, the reference time should be very close to frame start
92
+ assert np.all(delays < durations * 0.01)
93
+
94
+
95
+ def test_ref_time_numeric_integration_agrees():
96
+ # compare against numeric integral definition of weighted average time
97
+ durations = np.array([5.0, 10.0, 60.0])
98
+ starts = np.array([0.0, 5.0, 15.0])
99
+ half_life = 1e4
100
+ res = calculate_frame_reference_time(durations, starts, half_life)
101
+
102
+ expected = []
103
+ for T, s in zip(durations, starts):
104
+ lam = np.log(2) / half_life
105
+ t = np.linspace(0.0, T, 20001)
106
+ w = np.exp(-lam * t)
107
+ num = np.trapezoid(t * w, t)
108
+ den = np.trapezoid(w, t)
109
+ delay = num / den
110
+ expected.append(s + delay)
111
+ expected = np.asarray(expected)
112
+
113
+ np.testing.assert_allclose(res, expected, rtol=1e-3, atol=1e-9)
114
+
115
+
116
+ def test_ref_time_vectorized_shape_and_broadcast():
117
+ durations = np.array([10.0])
118
+ starts = np.array([2.0])
119
+ half_life = 1.0e3
120
+ res = calculate_frame_reference_time(durations, starts, half_life)
121
+ assert isinstance(res, np.ndarray)
122
+ assert res.shape == (1,)
123
+
124
+
125
+ def test_basic_powers_of_two():
126
+ half_life = 2.0
127
+ times = np.array([0.0, half_life, 2 * half_life])
128
+ out = calculate_frame_decay_factor(times, half_life)
129
+ expected = np.array([1.0, 2.0, 4.0])
130
+ np.testing.assert_allclose(out, expected, rtol=1e-12, atol=0)
131
+
132
+
133
+ def test_preserves_shape_and_dtype():
134
+ times = np.array([0.5])
135
+ out = calculate_frame_decay_factor(times, 1.0)
136
+ assert isinstance(out, np.ndarray)
137
+ assert out.shape == times.shape
138
+ np.testing.assert_allclose(out[0], 2 ** 0.5, rtol=1e-12)
139
+
140
+
141
+ def test_negative_time_and_float_half_life():
142
+ half_life = 1.5
143
+ times = np.array([-half_life, 0.0, half_life])
144
+ out = calculate_frame_decay_factor(times, half_life)
145
+ expected = np.array([0.5, 1.0, 2.0])
146
+ np.testing.assert_allclose(out, expected, rtol=1e-12, atol=0)
@@ -0,0 +1,51 @@
1
+ import numpy as np
2
+ import pytest
3
+ from petpal.utils.math_lib import weighted_sum_computation
4
+ from petpal.utils.scan_timing import calculate_frame_reference_time
5
+ from petpal.preproc.decay_correction import calculate_frame_decay_factor
6
+
7
+ def test_weighted_sum_computation_all_ones_simple():
8
+ # simple case: all voxels = 1, decay_correction = 1, frame_start[0]=0
9
+ pet_series = np.ones((2, 2, 2, 3), dtype=float)
10
+ frame_duration = np.array([10.0, 20.0, 30.0])
11
+ half_life = 100.0
12
+ frame_start = np.array([0.0, 10.0, 30.0])
13
+ decay_correction = np.ones(3)
14
+
15
+ decay_constant = np.log(2.0) / half_life
16
+ image_total_duration = frame_duration.sum()
17
+ total_decay = decay_constant * image_total_duration
18
+ total_decay /= 1.0 - np.exp(-decay_constant * image_total_duration)
19
+ total_decay /= np.exp(-decay_constant * frame_start[0])
20
+
21
+ expected = np.full((2, 2, 2), total_decay)
22
+ out = weighted_sum_computation(pet_series, frame_duration, half_life, frame_start, decay_correction)
23
+ np.testing.assert_allclose(out, expected)
24
+
25
+ def test_weighted_sum_computation_manual_computation():
26
+ # randomized small example, validate against manual numpy computation
27
+ rng = np.random.default_rng(0)
28
+ pet_series = rng.random((3, 2, 1, 4)).astype(float)
29
+ frame_duration = np.array([5.0, 5.0, 10.0, 300.0])
30
+ half_life = 50.0
31
+ frame_start = np.array([1.0, 6.0, 11.0, 21.0])
32
+ frame_ref_time = calculate_frame_reference_time(frame_duration=frame_duration,
33
+ frame_start=frame_start,
34
+ half_life=half_life)
35
+ decay_correction = calculate_frame_decay_factor(frame_reference_time=frame_ref_time,
36
+ half_life=half_life)
37
+
38
+ # manual expected computation following function logic
39
+ decay_constant = np.log(2.0) / half_life
40
+ image_total_duration = frame_duration.sum()
41
+ total_decay = decay_constant * image_total_duration
42
+ total_decay /= 1.0 - np.exp(-decay_constant * image_total_duration)
43
+ total_decay /= np.exp(-decay_constant * frame_start[0])
44
+
45
+ # compute weighted sum: sum_t(pet[...,t] * frame_duration[t] / decay_correction[t])
46
+ scaled = pet_series * frame_duration / decay_correction # broadcasting over last axis
47
+ pet_series_sum_scaled = scaled.sum(axis=3)
48
+ expected = pet_series_sum_scaled * total_decay / image_total_duration
49
+
50
+ out = weighted_sum_computation(pet_series, frame_duration, half_life, frame_start, decay_correction)
51
+ np.testing.assert_allclose(out, expected)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes