petpal 0.5.7__tar.gz → 0.5.9__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 (155) hide show
  1. {petpal-0.5.7 → petpal-0.5.9}/.gitignore +4 -1
  2. {petpal-0.5.7 → petpal-0.5.9}/PKG-INFO +1 -1
  3. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_graphical_analysis.py +1 -2
  4. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/decay_correction.py +11 -10
  5. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/regional_tac_extraction.py +66 -7
  6. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/segmentation_tools.py +28 -1
  7. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/scan_timing.py +66 -19
  8. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/time_activity_curve.py +46 -12
  9. {petpal-0.5.7 → petpal-0.5.9}/pyproject.toml +1 -1
  10. petpal-0.5.9/tests/test_graphical_analysis.py +90 -0
  11. petpal-0.5.9/tests/test_register.py +82 -0
  12. petpal-0.5.9/tests/test_scan_timing_decay.py +146 -0
  13. petpal-0.5.9/tests/test_time_activity_curve.py +33 -0
  14. petpal-0.5.9/tests/test_weighted_sum.py +51 -0
  15. petpal-0.5.9/tests/test_write_tacs.py +90 -0
  16. {petpal-0.5.7 → petpal-0.5.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {petpal-0.5.7 → petpal-0.5.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  18. {petpal-0.5.7 → petpal-0.5.9}/.github/workflows/publish-to-pypi.yml +0 -0
  19. {petpal-0.5.7 → petpal-0.5.9}/.github/workflows/python-package.yml +0 -0
  20. {petpal-0.5.7 → petpal-0.5.9}/.readthedocs.yaml +0 -0
  21. {petpal-0.5.7 → petpal-0.5.9}/LICENSE +0 -0
  22. {petpal-0.5.7 → petpal-0.5.9}/README.md +0 -0
  23. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-00.txt +0 -0
  24. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-01.txt +0 -0
  25. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-02.txt +0 -0
  26. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tacs.pdf +0 -0
  27. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tacs.png +0 -0
  28. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-00.txt +0 -0
  29. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-01.txt +0 -0
  30. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-02.txt +0 -0
  31. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tacs.pdf +0 -0
  32. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tacs.png +0 -0
  33. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-00.json +0 -0
  34. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-01.json +0 -0
  35. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-02.json +0 -0
  36. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled.txt +0 -0
  37. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled_woMax.txt +0 -0
  38. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_tacs.pdf +0 -0
  39. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_tacs.png +0 -0
  40. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/gen_tcms_data.ipynb +0 -0
  41. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/readme.md +0 -0
  42. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-00.txt +0 -0
  43. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-01.txt +0 -0
  44. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-02.txt +0 -0
  45. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.pdf +0 -0
  46. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.png +0 -0
  47. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-00.txt +0 -0
  48. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-01.txt +0 -0
  49. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-02.txt +0 -0
  50. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tacs.pdf +0 -0
  51. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tacs.png +0 -0
  52. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-00.json +0 -0
  53. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-01.json +0 -0
  54. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-02.json +0 -0
  55. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-00.txt +0 -0
  56. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-01.txt +0 -0
  57. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.pdf +0 -0
  58. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.png +0 -0
  59. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-00.txt +0 -0
  60. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-01.txt +0 -0
  61. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.pdf +0 -0
  62. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.png +0 -0
  63. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-00.json +0 -0
  64. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-01.json +0 -0
  65. {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/turku_pet_center_fdg_plasma_clamp.txt +0 -0
  66. {petpal-0.5.7 → petpal-0.5.9}/docs/Makefile +0 -0
  67. {petpal-0.5.7 → petpal-0.5.9}/docs/PETPAL_Logo.png +0 -0
  68. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/index.rst +0 -0
  69. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/attribute.rst +0 -0
  70. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/class.rst +0 -0
  71. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/data.rst +0 -0
  72. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/exception.rst +0 -0
  73. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/function.rst +0 -0
  74. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/method.rst +0 -0
  75. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/module.rst +0 -0
  76. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/package.rst +0 -0
  77. {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/property.rst +0 -0
  78. {petpal-0.5.7 → petpal-0.5.9}/docs/conf.py +0 -0
  79. {petpal-0.5.7 → petpal-0.5.9}/docs/index.rst +0 -0
  80. {petpal-0.5.7 → petpal-0.5.9}/docs/make.bat +0 -0
  81. {petpal-0.5.7 → petpal-0.5.9}/docs/requirements.txt +0 -0
  82. {petpal-0.5.7 → petpal-0.5.9}/docs/tutorials/index.rst +0 -0
  83. {petpal-0.5.7 → petpal-0.5.9}/docs/tutorials/pib_example.rst +0 -0
  84. {petpal-0.5.7 → petpal-0.5.9}/petpal/__init__.py +0 -0
  85. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/__init__.py +0 -0
  86. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_graphical_plots.py +0 -0
  87. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_idif.py +0 -0
  88. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_parametric_images.py +0 -0
  89. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_pib_processing.py +0 -0
  90. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_plot_tacs.py +0 -0
  91. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_preproc.py +0 -0
  92. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_pvc.py +0 -0
  93. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_reference_tissue_models.py +0 -0
  94. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_stats.py +0 -0
  95. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_tac_fitting.py +0 -0
  96. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_tac_interpolation.py +0 -0
  97. {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_vat_processing.py +0 -0
  98. {petpal-0.5.7 → petpal-0.5.9}/petpal/input_function/__init__.py +0 -0
  99. {petpal-0.5.7 → petpal-0.5.9}/petpal/input_function/blood_input.py +0 -0
  100. {petpal-0.5.7 → petpal-0.5.9}/petpal/input_function/idif_necktangle.py +0 -0
  101. {petpal-0.5.7 → petpal-0.5.9}/petpal/input_function/pca_guided_idif.py +0 -0
  102. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/__init__.py +0 -0
  103. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/fit_tac_with_rtms.py +0 -0
  104. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/graphical_analysis.py +2 -2
  105. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/parametric_images.py +0 -0
  106. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/reference_tissue_models.py +0 -0
  107. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/rtm_analysis.py +0 -0
  108. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/tac_fitting.py +0 -0
  109. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/tac_interpolation.py +0 -0
  110. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/tac_uncertainty.py +0 -0
  111. {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/tcms_as_convolutions.py +0 -0
  112. {petpal-0.5.7 → petpal-0.5.9}/petpal/meta/__init__.py +0 -0
  113. {petpal-0.5.7 → petpal-0.5.9}/petpal/meta/label_maps.py +0 -0
  114. {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/__init__.py +0 -0
  115. {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/kinetic_modeling_steps.py +0 -0
  116. {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/pca_guided_idif_steps.py +0 -0
  117. {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/pipelines.py +0 -0
  118. {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/preproc_steps.py +0 -0
  119. {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/steps_base.py +0 -0
  120. {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/steps_containers.py +0 -0
  121. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/__init__.py +0 -0
  122. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/image_operations_4d.py +0 -0
  123. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/motion_corr.py +0 -0
  124. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/motion_target.py +0 -0
  125. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/partial_volume_corrections.py +0 -0
  126. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/register.py +0 -0
  127. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/standard_uptake_value.py +0 -0
  128. {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/symmetric_geometric_transfer_matrix.py +0 -0
  129. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/__init__.py +0 -0
  130. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/bids_utils.py +0 -0
  131. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/constants.py +0 -0
  132. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/data_driven_image_analyses.py +0 -0
  133. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/decorators.py +0 -0
  134. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/image_io.py +0 -0
  135. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/math_lib.py +0 -0
  136. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/metadata.py +0 -0
  137. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/stats.py +0 -0
  138. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/testing_utils.py +0 -0
  139. {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/useful_functions.py +0 -0
  140. {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/__init__.py +0 -0
  141. {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/graphical_plots.py +0 -0
  142. {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/image_visualization.py +0 -0
  143. {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/qc_plots.py +0 -0
  144. {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/tac_plots.py +0 -0
  145. {petpal-0.5.7 → petpal-0.5.9}/shared/dseg.tsv +0 -0
  146. {petpal-0.5.7 → petpal-0.5.9}/shared/freesurfer_lmap.json +0 -0
  147. {petpal-0.5.7 → petpal-0.5.9}/shared/freesurfer_lmap_lr.json +0 -0
  148. {petpal-0.5.7 → petpal-0.5.9}/shared/perl_cyno_lmap.json +0 -0
  149. {petpal-0.5.7 → petpal-0.5.9}/shared/perl_cyno_lmap_lr.json +0 -0
  150. {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/explicit_tac_fitting/01_fitting_TCMs.ipynb +0 -0
  151. {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/testing_RTMs/01_testing_RTMs.ipynb +0 -0
  152. {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/01_testing_on_tcms_database.ipynb +0 -0
  153. {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/02_testing_parametric_images.ipynb +0 -0
  154. {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/03_plotting_graphical_anlayses_testbed.ipynb +0 -0
  155. {petpal-0.5.7 → petpal-0.5.9}/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.9
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>
@@ -59,7 +59,6 @@ def main():
59
59
  parser_multitac = subparsers.add_parser('graphical-analysis-multitac')
60
60
  _add_common_args(parser_multitac)
61
61
  parser_multitac.add_argument("-r", "--roi-tacs-dir", required=True, help="Path to directory containing ROI TTACs")
62
- parser_multitac.add_argument("-x","--excel", action='store_true',help='Set to output an excel-compatible table in a single file.',default=False)
63
62
 
64
63
  args = parser.parse_args()
65
64
  command = str(args.command).replace('-','_')
@@ -90,7 +89,7 @@ def main():
90
89
  output_filename_prefix=args.output_filename_prefix,
91
90
  method=method,
92
91
  fit_thresh_in_mins=args.threshold_in_mins)
93
- graphical_analysis(one_file_per_region=not args.excel, **run_kwargs)
92
+ graphical_analysis(output_as_tsv=True, output_as_json=False, **run_kwargs)
94
93
 
95
94
  if args.print:
96
95
  for key, val in graphical_analysis.analysis_props.items():
@@ -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,6 +1,7 @@
1
1
  """
2
2
  Regional TAC extraction
3
3
  """
4
+ from warnings import warn
4
5
  import os
5
6
  from collections.abc import Callable
6
7
  import pathlib
@@ -345,6 +346,20 @@ class WriteRegionalTacs:
345
346
  region_name = f'UNK{label:>04}'
346
347
  return region_name
347
348
 
349
+ def is_empty_region(self,pet_masked_region: np.ndarray) -> bool:
350
+ """Check if masked PET region has zero matched voxels, or is all NaNs. In either case,
351
+ return True, otherwise return False.
352
+
353
+ Args:
354
+ pet_masked_region (np.ndarray): Array of PET voxels masked to a specific region.
355
+
356
+ Returns:
357
+ pet_masked_region_is_empty (bool): If True, input region is empty."""
358
+ if pet_masked_region.size==0:
359
+ return True
360
+ if np.all(np.isnan(pet_masked_region)):
361
+ return True
362
+ return False
348
363
 
349
364
  def extract_tac(self,region_mapping: int | list[int], **tac_calc_kwargs) -> TimeActivityCurve:
350
365
  """
@@ -359,15 +374,46 @@ class WriteRegionalTacs:
359
374
  """
360
375
  region_mask = combine_regions_as_mask(segmentation_img=self.seg_arr,
361
376
  label=region_mapping)
377
+
362
378
  pet_masked_region = apply_mask_4d(input_arr=self.pet_arr,
363
379
  mask_arr=region_mask)
364
- extracted_tac, uncertainty = self.tac_extraction_func(pet_voxels=pet_masked_region,
365
- **tac_calc_kwargs)
380
+
381
+ is_region_empty = self.is_empty_region(pet_masked_region=pet_masked_region)
382
+ if is_region_empty:
383
+ extracted_tac = np.empty_like(self.scan_timing.center_in_mins)
384
+ extracted_tac.fill(np.nan)
385
+ uncertainty = extracted_tac.copy()
386
+ else:
387
+ extracted_tac, uncertainty = self.tac_extraction_func(pet_voxels=pet_masked_region,
388
+ **tac_calc_kwargs)
366
389
  region_tac = TimeActivityCurve(times=self.scan_timing.center_in_mins,
367
390
  activity=extracted_tac,
368
391
  uncertainty=uncertainty)
369
392
  return region_tac
370
393
 
394
+ def gen_tacs_data_frame(self) -> pd.DataFrame:
395
+ """Get empty data frame to store TACs. Sets first two columns to frame start and end
396
+ times, and remaining columns are named by region activity and uncertainty, based on the
397
+ regions included in the label map.
398
+
399
+ Returns:
400
+ tacs_data (pd.DataFrame): Data frame with columns set for frame start and end time,
401
+ and activity and uncertainty for each included region. Frame start and end time
402
+ columns filled with scan timing data.
403
+ """
404
+ activity_uncertainty_column_names = []
405
+ for region_name in self.region_names:
406
+ activity_uncertainty_column_names.append(region_name)
407
+ activity_uncertainty_column_names.append(f'{region_name}_unc')
408
+ cols_list = ['frame_start(min)','frame_end(min)'] + activity_uncertainty_column_names
409
+ tacs_data = pd.DataFrame(columns=cols_list)
410
+
411
+ tacs_data['frame_start(min)'] = self.scan_timing.start_in_mins
412
+ tacs_data['frame_end(min)'] = self.scan_timing.end_in_mins
413
+
414
+ return tacs_data
415
+
416
+
371
417
 
372
418
  def write_tacs(self,
373
419
  out_tac_prefix: str,
@@ -378,7 +424,8 @@ class WriteRegionalTacs:
378
424
  Function to write Tissue Activity Curves for each region, given a segmentation,
379
425
  4D PET image, and label map. Computes the average of the PET image within each
380
426
  region. Writes TACs in TSV format with region name, frame start time, frame end time, and
381
- activity and uncertainty within each region.
427
+ activity and uncertainty within each region. Skips writing regions without any matched
428
+ voxels.
382
429
 
383
430
  Args:
384
431
  out_tac_prefix (str): Prefix for the output files, usually the BIDS subject and
@@ -387,22 +434,34 @@ class WriteRegionalTacs:
387
434
  one_tsv_per_region (bool): If True, write one TSV TAC file for each region in the
388
435
  image. If False, write one TSV file with all TACs in the image.
389
436
  **tac_calc_kwargs: Additional keywords passed onto tac_extraction_func.
390
- """
391
- tacs_data = pd.DataFrame()
392
437
 
393
- tacs_data['frame_start(min)'] = self.scan_timing.start_in_mins
394
- tacs_data['frame_end(min)'] = self.scan_timing.end_in_mins
438
+ Raises:
439
+ Warning: for each region without any matched voxels, warn user that TAC is skipped.
440
+ """
441
+ tacs_data = self.gen_tacs_data_frame()
395
442
 
443
+ empty_regions = []
396
444
  for i,region_name in enumerate(self.region_names):
397
445
  mappings = self.region_maps[i]
398
446
  tac = self.extract_tac(region_mapping=mappings, **tac_calc_kwargs)
447
+ if tac.contains_any_nan:
448
+ empty_regions.append(region_name)
449
+ continue
399
450
  if one_tsv_per_region:
451
+ os.makedirs(out_tac_dir, exist_ok=True)
400
452
  tac.to_tsv(filename=f'{out_tac_dir}/{out_tac_prefix}_seg-{region_name}_tac.tsv')
401
453
  else:
402
454
  tacs_data[region_name] = tac.activity
403
455
  tacs_data[f'{region_name}_unc'] = tac.uncertainty
404
456
 
457
+ if len(empty_regions)>0:
458
+ warn("Empty regions were found during tac extraction. TACs for the following regions "
459
+ f"were not saved: {empty_regions}")
460
+ tacs_data.drop(empty_regions,axis=1,inplace=True)
461
+ tacs_data.drop([f'{region}_unc' for region in empty_regions],axis=1,inplace=True)
462
+
405
463
  if not one_tsv_per_region:
464
+ os.makedirs(out_tac_dir, exist_ok=True)
406
465
  tacs_data.to_csv(f'{out_tac_dir}/{out_tac_prefix}_multitacs.tsv', sep='\t', index=False)
407
466
 
408
467
  def __call__(self,
@@ -14,7 +14,9 @@ import nibabel
14
14
  from nibabel import processing
15
15
  import pandas as pd
16
16
 
17
- from ..utils.useful_functions import gen_nd_image_based_on_image_list
17
+ from ..utils.useful_functions import (gen_nd_image_based_on_image_list,
18
+ check_physical_space_for_ants_image_pair,
19
+ get_average_of_timeseries)
18
20
  from ..utils import math_lib
19
21
 
20
22
 
@@ -571,3 +573,28 @@ def unique_segmentation_labels(segmentation_img: ants.core.ANTsImage | np.ndarra
571
573
  if not zeroth_roi:
572
574
  labels = labels[labels != 0]
573
575
  return labels
576
+
577
+ def seg_crop_to_pet_fov(pet_img: ants.ANTsImage,
578
+ segmentation_img: ants.ANTsImage,
579
+ pet_thresh_value: float=np.finfo(float).eps) -> ants.ANTsImage:
580
+ """Zero out segmentation values that lie outside of the PET FOV.
581
+
582
+ Especially applicable to scanners with limited FOV (field of view). PET voxels with values less
583
+ than 1e-36 are considered outside of the FOV.
584
+
585
+ Args:
586
+ pet_img (ants.ANTsImage): PET image in anatomical space used to crop segmentation
587
+ segmentation_img (ants.ANTsImage): Segmentation image in anatomical space such as
588
+ FreeSurfer to which FOV cropping is applied.
589
+ pet_thresh_value (float): Lower threshold for the PET image by which the segmentation image
590
+ is masked. Should be <<1. Default machine epsilon for `float`.
591
+
592
+ Returns:
593
+ segmentation_masked_img (ants.ANTsImage): Segmentation image masked to PET FOV.
594
+ """
595
+ if not check_physical_space_for_ants_image_pair(pet_img, segmentation_img):
596
+ raise ValueError("PET and segmentation image must share physical space.")
597
+ pet_mean_img = get_average_of_timeseries(input_image=pet_img)
598
+ pet_mask = ants.threshold_image(pet_mean_img, pet_thresh_value)
599
+ seg_masked = ants.mask_image(segmentation_img, pet_mask)
600
+ return seg_masked
@@ -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
@@ -75,6 +75,7 @@ class TimeActivityCurve:
75
75
  return len(self.times)
76
76
 
77
77
  def __post_init__(self):
78
+ self.validate_activity()
78
79
  if self.uncertainty.size == 0:
79
80
  self.uncertainty = np.empty_like(self.times)
80
81
  self.uncertainty[:] = np.nan
@@ -82,6 +83,33 @@ class TimeActivityCurve:
82
83
  f"TAC fields must have the same shapes.\ntimes:{self.times.shape}"
83
84
  "activity:{self.activity.shape} uncertainty:{self.uncertainty.shape}")
84
85
 
86
+ def validate_activity(self):
87
+ """Validates that the activity attribute is defined correctly.
88
+
89
+ `self.activity` must have the following properties:
90
+ 1) It must exist and not be None
91
+ 2) It must be a numpy array
92
+ 3) It must have dtype float
93
+ 4) It must be 1D
94
+
95
+ This function raises a ValueError if self.activity does not meet the first criteria, and
96
+ attempts to coerce self.activity into a 1D, numeric numpy array with dtype float if
97
+ criteria 2-4 are not met.
98
+ """
99
+ if not hasattr(self, "activity") or self.activity is None:
100
+ raise ValueError("TimeActivityCurve.activity must be provided and not be None")
101
+
102
+ try:
103
+ arr = np.asarray(self.activity, dtype=float)
104
+ except (TypeError, ValueError) as exc:
105
+ error_message = "TimeActivityCurve.activity must be numeric or convertible to numeric"
106
+ raise TypeError(error_message) from exc
107
+
108
+ if arr.ndim != 1:
109
+ arr = arr.ravel()
110
+
111
+ self.activity = arr
112
+
85
113
  @classmethod
86
114
  def from_tsv(cls, filename: str):
87
115
  """
@@ -509,29 +537,29 @@ class TimeActivityCurve:
509
537
  kind='linear',
510
538
  fill_value='extrapolate')(tac.times)
511
539
  return TimeActivityCurve(tac.times, shifted_vals_on_tac_times)
512
- else:
513
- return shifted_tac
540
+ return shifted_tac
514
541
 
515
542
  @staticmethod
516
543
  def tac_dispersion(tac: 'TimeActivityCurve',
517
- disp_func: Callable[[np.ndarray, ...], np.ndarray],
544
+ disp_func: Callable[..., np.ndarray],
518
545
  disp_kwargs: dict,
519
546
  num_samples: int = 4096):
520
547
  r"""
521
548
  Applies a dispersion function to a time-activity curve (TAC) and returns the convolved TAC.
522
549
 
523
- This method evaluates the specified dispersion function `disp_func` at supersampled time points.
524
- It performs convolution (using :func:`scipy.signal.convolve`)of the supersampled TAC with
525
- the dispersion function, and the result is sampled back at the original TAC time points
526
- to form the new convolved TAC.
550
+ This method evaluates the specified dispersion function `disp_func` at supersampled time
551
+ points. It performs convolution (using :func:`scipy.signal.convolve`) of the supersampled
552
+ TAC with the dispersion function, and the result is sampled back at the original TAC time
553
+ points to form the new convolved TAC.
527
554
 
528
555
  .. note::
529
- We perform the supersampling to ensure that the TACs are sampled evenly before performing
530
- the convolution. Convolving non-evenly sampled arrays produces nonsense values.
556
+ We perform the supersampling to ensure that the TACs are sampled evenly before
557
+ performing the convolution. Convolving non-evenly sampled arrays produces nonsense
558
+ values.
531
559
 
532
560
  Args:
533
561
  tac (TimeActivityCurve): The original time-activity curve to be convolved.
534
- disp_func (Callable[[np.ndarray, ...], np.ndarray]):
562
+ disp_func (Callable[..., np.ndarray]):
535
563
  The dispersion function to be applied. This function must accept an array of
536
564
  times as its first argument, followed by any additional arguments specified
537
565
  in `disp_kwargs`.
@@ -541,8 +569,8 @@ class TimeActivityCurve:
541
569
  Defaults to 4096.
542
570
 
543
571
  Returns:
544
- TimeActivityCurve: A new `TimeActivityCurve` instance with the convolved activity values,
545
- resampled at the original TAC time points.
572
+ TimeActivityCurve: A new `TimeActivityCurve` instance with the convolved activity
573
+ values, resampled at the original TAC time points.
546
574
 
547
575
  Example:
548
576
  .. code-block:: python
@@ -592,6 +620,12 @@ class TimeActivityCurve:
592
620
 
593
621
  return disp_tac.set_activity_non_negative()
594
622
 
623
+ @property
624
+ def contains_any_nan(self):
625
+ """Return True if TAC has any NaN activity values."""
626
+ any_nan = np.isnan(self.activity).any()
627
+ return any_nan
628
+
595
629
  def safe_load_tac(filename: str,
596
630
  with_uncertainty: bool = False,
597
631
  **kwargs) -> np.ndarray:
@@ -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.9"
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,90 @@
1
+ import os
2
+ import warnings
3
+ import pytest
4
+ import pandas as pd
5
+
6
+ from petpal.utils.image_io import flatten_metadata
7
+ import petpal.kinetic_modeling.graphical_analysis as ga
8
+
9
+ def _make_instance(rsquared):
10
+ inst = ga.MultiTACGraphicalAnalysis.__new__(ga.MultiTACGraphicalAnalysis)
11
+ inst.analysis_props = [{'RSquared': rsquared}]
12
+ inst.output_directory = "/tmp"
13
+ inst.output_filename_prefix = "prefix"
14
+ inst.method = "patlak"
15
+ inst.inferred_seg_labels = ["roi1", "roi2"]
16
+ return inst
17
+
18
+
19
+ def test_save_analysis_raises_if_run_not_called():
20
+ inst = _make_instance(rsquared=None)
21
+ with pytest.raises(RuntimeError):
22
+ inst.save_analysis()
23
+
24
+ def test_save_analysis_calls_tsv_and_json(monkeypatch):
25
+ inst = _make_instance(rsquared=0.95)
26
+ calls = {"tsv": 0, "json": 0}
27
+
28
+ def km_multifit_analysis_to_tsv_without_save_file(analysis_props: list[dict],
29
+ output_directory: str,
30
+ output_filename_prefix: str,
31
+ method: str,
32
+ inferred_seg_labels: list[str]):
33
+ filename = f'{output_filename_prefix}_desc-{method}_fitprops.tsv'
34
+ filepath = os.path.join(output_directory, filename)
35
+ fit_table = pd.DataFrame()
36
+ for seg_name, fit_props in zip(inferred_seg_labels, analysis_props):
37
+ tmp_table = pd.DataFrame(flatten_metadata(fit_props),index=[seg_name])
38
+ fit_table = pd.concat([fit_table,tmp_table])
39
+ calls["tsv"] += 1
40
+ assert isinstance(analysis_props, list)
41
+ assert isinstance(output_directory, str)
42
+ assert isinstance(output_filename_prefix, str)
43
+ assert isinstance(method, str)
44
+ assert isinstance(inferred_seg_labels, list)
45
+
46
+ def km_multifit_analysis_to_jsons_without_save_file(analysis_props: list[dict],
47
+ output_directory: str,
48
+ output_filename_prefix: str,
49
+ method: str,
50
+ inferred_seg_labels: list[str]):
51
+ for seg_name, fit_props in zip(inferred_seg_labels, analysis_props):
52
+ filename = [output_filename_prefix,
53
+ f'desc-{method}',
54
+ f'seg-{seg_name}',
55
+ 'fitprops.json']
56
+ filename = '_'.join(filename)
57
+ filepath = os.path.join(output_directory, filename)
58
+ calls["json"] += 1
59
+ assert isinstance(analysis_props, list)
60
+ assert isinstance(output_directory, str)
61
+ assert isinstance(output_filename_prefix, str)
62
+ assert isinstance(method, str)
63
+ assert isinstance(inferred_seg_labels, list)
64
+
65
+ monkeypatch.setattr(ga, "km_multifit_analysis_to_tsv", km_multifit_analysis_to_tsv_without_save_file)
66
+ monkeypatch.setattr(ga, "km_multifit_analysis_to_jsons", km_multifit_analysis_to_jsons_without_save_file)
67
+
68
+ # Default behavior: TSV only
69
+ calls["tsv"] = calls["json"] = 0
70
+ inst.save_analysis(output_as_tsv=True, output_as_json=False)
71
+ assert calls["tsv"] == 1 and calls["json"] == 0
72
+
73
+ # JSON only
74
+ calls["tsv"] = calls["json"] = 0
75
+ inst.save_analysis(output_as_tsv=False, output_as_json=True)
76
+ assert calls["tsv"] == 0 and calls["json"] == 1
77
+
78
+ # Both
79
+ calls["tsv"] = calls["json"] = 0
80
+ inst.save_analysis(output_as_tsv=True, output_as_json=True)
81
+ assert calls["tsv"] == 1 and calls["json"] == 1
82
+
83
+ def test_save_analysis_warns_when_no_output_requested(monkeypatch):
84
+ inst = _make_instance(rsquared=0.5)
85
+ # prevent actual functions being called if mistakenly invoked
86
+ monkeypatch.setattr(ga, "km_multifit_analysis_to_tsv", lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not be called")))
87
+ monkeypatch.setattr(ga, "km_multifit_analysis_to_jsons", lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not be called")))
88
+
89
+ with pytest.warns(UserWarning):
90
+ inst.save_analysis(output_as_tsv=False, output_as_json=False)