petpal 0.5.8__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.8 → petpal-0.5.9}/PKG-INFO +1 -1
  2. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_graphical_analysis.py +1 -2
  3. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/regional_tac_extraction.py +66 -7
  4. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/segmentation_tools.py +28 -1
  5. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/time_activity_curve.py +46 -12
  6. {petpal-0.5.8 → petpal-0.5.9}/pyproject.toml +1 -1
  7. petpal-0.5.9/tests/test_graphical_analysis.py +90 -0
  8. petpal-0.5.9/tests/test_time_activity_curve.py +33 -0
  9. petpal-0.5.9/tests/test_write_tacs.py +90 -0
  10. {petpal-0.5.8 → petpal-0.5.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {petpal-0.5.8 → petpal-0.5.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {petpal-0.5.8 → petpal-0.5.9}/.github/workflows/publish-to-pypi.yml +0 -0
  13. {petpal-0.5.8 → petpal-0.5.9}/.github/workflows/python-package.yml +0 -0
  14. {petpal-0.5.8 → petpal-0.5.9}/.gitignore +0 -0
  15. {petpal-0.5.8 → petpal-0.5.9}/.readthedocs.yaml +0 -0
  16. {petpal-0.5.8 → petpal-0.5.9}/LICENSE +0 -0
  17. {petpal-0.5.8 → petpal-0.5.9}/README.md +0 -0
  18. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-00.txt +0 -0
  19. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-01.txt +0 -0
  20. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-02.txt +0 -0
  21. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tacs.pdf +0 -0
  22. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tacs.png +0 -0
  23. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-00.txt +0 -0
  24. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-01.txt +0 -0
  25. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-02.txt +0 -0
  26. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tacs.pdf +0 -0
  27. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tacs.png +0 -0
  28. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-00.json +0 -0
  29. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-01.json +0 -0
  30. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-02.json +0 -0
  31. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled.txt +0 -0
  32. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled_woMax.txt +0 -0
  33. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_tacs.pdf +0 -0
  34. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_tacs.png +0 -0
  35. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/gen_tcms_data.ipynb +0 -0
  36. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/readme.md +0 -0
  37. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-00.txt +0 -0
  38. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-01.txt +0 -0
  39. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-02.txt +0 -0
  40. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.pdf +0 -0
  41. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.png +0 -0
  42. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-00.txt +0 -0
  43. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-01.txt +0 -0
  44. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-02.txt +0 -0
  45. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tacs.pdf +0 -0
  46. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tacs.png +0 -0
  47. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-00.json +0 -0
  48. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-01.json +0 -0
  49. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-02.json +0 -0
  50. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-00.txt +0 -0
  51. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-01.txt +0 -0
  52. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.pdf +0 -0
  53. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.png +0 -0
  54. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-00.txt +0 -0
  55. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-01.txt +0 -0
  56. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.pdf +0 -0
  57. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.png +0 -0
  58. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-00.json +0 -0
  59. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-01.json +0 -0
  60. {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/turku_pet_center_fdg_plasma_clamp.txt +0 -0
  61. {petpal-0.5.8 → petpal-0.5.9}/docs/Makefile +0 -0
  62. {petpal-0.5.8 → petpal-0.5.9}/docs/PETPAL_Logo.png +0 -0
  63. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/index.rst +0 -0
  64. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/attribute.rst +0 -0
  65. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/class.rst +0 -0
  66. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/data.rst +0 -0
  67. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/exception.rst +0 -0
  68. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/function.rst +0 -0
  69. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/method.rst +0 -0
  70. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/module.rst +0 -0
  71. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/package.rst +0 -0
  72. {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/property.rst +0 -0
  73. {petpal-0.5.8 → petpal-0.5.9}/docs/conf.py +0 -0
  74. {petpal-0.5.8 → petpal-0.5.9}/docs/index.rst +0 -0
  75. {petpal-0.5.8 → petpal-0.5.9}/docs/make.bat +0 -0
  76. {petpal-0.5.8 → petpal-0.5.9}/docs/requirements.txt +0 -0
  77. {petpal-0.5.8 → petpal-0.5.9}/docs/tutorials/index.rst +0 -0
  78. {petpal-0.5.8 → petpal-0.5.9}/docs/tutorials/pib_example.rst +0 -0
  79. {petpal-0.5.8 → petpal-0.5.9}/petpal/__init__.py +0 -0
  80. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/__init__.py +0 -0
  81. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_graphical_plots.py +0 -0
  82. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_idif.py +0 -0
  83. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_parametric_images.py +0 -0
  84. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_pib_processing.py +0 -0
  85. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_plot_tacs.py +0 -0
  86. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_preproc.py +0 -0
  87. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_pvc.py +0 -0
  88. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_reference_tissue_models.py +0 -0
  89. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_stats.py +0 -0
  90. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_tac_fitting.py +0 -0
  91. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_tac_interpolation.py +0 -0
  92. {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_vat_processing.py +0 -0
  93. {petpal-0.5.8 → petpal-0.5.9}/petpal/input_function/__init__.py +0 -0
  94. {petpal-0.5.8 → petpal-0.5.9}/petpal/input_function/blood_input.py +0 -0
  95. {petpal-0.5.8 → petpal-0.5.9}/petpal/input_function/idif_necktangle.py +0 -0
  96. {petpal-0.5.8 → petpal-0.5.9}/petpal/input_function/pca_guided_idif.py +0 -0
  97. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/__init__.py +0 -0
  98. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/fit_tac_with_rtms.py +0 -0
  99. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/graphical_analysis.py +2 -2
  100. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/parametric_images.py +0 -0
  101. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/reference_tissue_models.py +0 -0
  102. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/rtm_analysis.py +0 -0
  103. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/tac_fitting.py +0 -0
  104. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/tac_interpolation.py +0 -0
  105. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/tac_uncertainty.py +0 -0
  106. {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/tcms_as_convolutions.py +0 -0
  107. {petpal-0.5.8 → petpal-0.5.9}/petpal/meta/__init__.py +0 -0
  108. {petpal-0.5.8 → petpal-0.5.9}/petpal/meta/label_maps.py +0 -0
  109. {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/__init__.py +0 -0
  110. {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/kinetic_modeling_steps.py +0 -0
  111. {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/pca_guided_idif_steps.py +0 -0
  112. {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/pipelines.py +0 -0
  113. {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/preproc_steps.py +0 -0
  114. {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/steps_base.py +0 -0
  115. {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/steps_containers.py +0 -0
  116. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/__init__.py +0 -0
  117. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/decay_correction.py +0 -0
  118. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/image_operations_4d.py +0 -0
  119. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/motion_corr.py +0 -0
  120. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/motion_target.py +0 -0
  121. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/partial_volume_corrections.py +0 -0
  122. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/register.py +0 -0
  123. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/standard_uptake_value.py +0 -0
  124. {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/symmetric_geometric_transfer_matrix.py +0 -0
  125. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/__init__.py +0 -0
  126. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/bids_utils.py +0 -0
  127. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/constants.py +0 -0
  128. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/data_driven_image_analyses.py +0 -0
  129. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/decorators.py +0 -0
  130. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/image_io.py +0 -0
  131. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/math_lib.py +0 -0
  132. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/metadata.py +0 -0
  133. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/scan_timing.py +0 -0
  134. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/stats.py +0 -0
  135. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/testing_utils.py +0 -0
  136. {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/useful_functions.py +0 -0
  137. {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/__init__.py +0 -0
  138. {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/graphical_plots.py +0 -0
  139. {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/image_visualization.py +0 -0
  140. {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/qc_plots.py +0 -0
  141. {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/tac_plots.py +0 -0
  142. {petpal-0.5.8 → petpal-0.5.9}/shared/dseg.tsv +0 -0
  143. {petpal-0.5.8 → petpal-0.5.9}/shared/freesurfer_lmap.json +0 -0
  144. {petpal-0.5.8 → petpal-0.5.9}/shared/freesurfer_lmap_lr.json +0 -0
  145. {petpal-0.5.8 → petpal-0.5.9}/shared/perl_cyno_lmap.json +0 -0
  146. {petpal-0.5.8 → petpal-0.5.9}/shared/perl_cyno_lmap_lr.json +0 -0
  147. {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/explicit_tac_fitting/01_fitting_TCMs.ipynb +0 -0
  148. {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/testing_RTMs/01_testing_RTMs.ipynb +0 -0
  149. {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/01_testing_on_tcms_database.ipynb +0 -0
  150. {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/02_testing_parametric_images.ipynb +0 -0
  151. {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/03_plotting_graphical_anlayses_testbed.ipynb +0 -0
  152. {petpal-0.5.8 → petpal-0.5.9}/tests/test_importpetpal.py +0 -0
  153. {petpal-0.5.8 → petpal-0.5.9}/tests/test_register.py +0 -0
  154. {petpal-0.5.8 → petpal-0.5.9}/tests/test_scan_timing_decay.py +0 -0
  155. {petpal-0.5.8 → petpal-0.5.9}/tests/test_weighted_sum.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: petpal
3
- Version: 0.5.8
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():
@@ -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
@@ -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.8"
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)
@@ -0,0 +1,33 @@
1
+ import pytest
2
+ import numpy as np
3
+ from petpal.utils.time_activity_curve import TimeActivityCurve
4
+
5
+ def test_post_init_sets_uncertainty_when_missing_and_converts_activity_to_float():
6
+ times = np.array([0.0, 10.0, 20.0])
7
+ activity = [1, 2, 3] # list input should be converted to 1D float array
8
+ tac = TimeActivityCurve(times=times, activity=activity)
9
+ assert isinstance(tac.uncertainty, np.ndarray)
10
+ assert tac.uncertainty.shape == times.shape
11
+ assert np.all(np.isnan(tac.uncertainty))
12
+ assert tac.activity.dtype == float
13
+ assert tac.activity.shape == times.shape
14
+
15
+ def test_post_init_raises_when_activity_is_none():
16
+ times = np.array([0.0, 5.0])
17
+ with pytest.raises(ValueError):
18
+ TimeActivityCurve(times=times, activity=None)
19
+
20
+ def test_post_init_asserts_on_shape_mismatch_between_fields():
21
+ times = np.array([0.0, 10.0, 20.0])
22
+ activity = np.array([1.0, 2.0]) # different shape -> should trigger assertion
23
+ with pytest.raises(AssertionError):
24
+ TimeActivityCurve(times=times, activity=activity)
25
+
26
+ def test_post_init_flattens_multidimensional_activity_to_1d_float():
27
+ times = np.array([0.0, 10.0, 20.0])
28
+ activity = np.array([[1, 2, 3]], dtype=int) # 2D input should be ravelled and cast to float
29
+ tac = TimeActivityCurve(times=times, activity=activity)
30
+ assert tac.activity.ndim == 1
31
+ assert tac.activity.shape == times.shape
32
+ assert tac.activity.dtype == float
33
+ assert np.allclose(tac.activity, np.array([1.0, 2.0, 3.0]))
@@ -0,0 +1,90 @@
1
+ import os
2
+ import numpy as np
3
+ import builtins
4
+ import types
5
+ import pytest
6
+ import pathlib
7
+
8
+ import petpal.preproc.regional_tac_extraction as rtx
9
+
10
+ class FakeLabelMapLoader:
11
+ def __init__(self, label_map_option):
12
+ self._label_map = {'R1': 1, 'R2': 2}
13
+ @property
14
+ def label_map(self):
15
+ return self._label_map
16
+
17
+ class FakeImg:
18
+ def __init__(self, arr):
19
+ self._arr = arr
20
+ def numpy(self):
21
+ return self._arr
22
+
23
+ class FakeScanTiming:
24
+ def __init__(self):
25
+ self.start_in_mins = [0.0, 1.0, 2.0]
26
+ self.end_in_mins = [1.0, 2.0, 3.0]
27
+ self.center_in_mins = [0.5, 1.5, 2.5]
28
+
29
+ def fake_to_tsv(self, filename):
30
+ # simple TSV writer for the TimeActivityCurve instances used in tests
31
+ with open(filename, 'w') as fh:
32
+ fh.write("time\tactivity\tuncertainty\n")
33
+ for t, a, u in zip(self.times, self.activity, self.uncertainty):
34
+ fh.write(f"{t}\t{a}\t{u}\n")
35
+
36
+ @pytest.fixture(autouse=True)
37
+ def patch_dependencies(monkeypatch):
38
+ # Patch LabelMapLoader used in module
39
+ monkeypatch.setattr(rtx, "LabelMapLoader", FakeLabelMapLoader)
40
+ # Patch ants.image_read to return dummy arrays (not used because apply_mask_4d is patched)
41
+ monkeypatch.setattr(rtx.ants, "image_read", lambda filename=None: FakeImg(np.zeros((2,2,2,3))))
42
+ # Patch ScanTimingInfo.from_nifti
43
+ monkeypatch.setattr(rtx.ScanTimingInfo, "from_nifti", lambda image_path=None: FakeScanTiming())
44
+ # combine_regions_as_mask returns the label passed through so apply_mask_4d can distinguish
45
+ monkeypatch.setattr(rtx, "combine_regions_as_mask", lambda segmentation_img, label: label)
46
+ # Patch TimeActivityCurve.to_tsv to write simple TSV so tests can assert file creation
47
+ monkeypatch.setattr(rtx.TimeActivityCurve, "to_tsv", fake_to_tsv)
48
+ yield
49
+
50
+ def test_write_tacs_one_tsv_per_region_writes_only_non_nan_region(tmp_path, monkeypatch):
51
+ # apply_mask_4d returns non-NaN voxels for label 1 and all-NaN voxels for label 2
52
+ def fake_apply_mask_4d(input_arr, mask_arr, verbose=False):
53
+ if mask_arr == 1:
54
+ return np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # mean is finite
55
+ else:
56
+ return np.array([[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan]]) # mean is nan
57
+ monkeypatch.setattr(rtx, "apply_mask_4d", fake_apply_mask_4d)
58
+
59
+ wr = rtx.WriteRegionalTacs(input_image_path="in.nii", segmentation_path="seg.nii", label_map="dummy")
60
+ out_dir = tmp_path
61
+ wr.write_tacs(out_tac_prefix="sub-01", out_tac_dir=str(out_dir), one_tsv_per_region=True)
62
+ # Expect file for R1 only
63
+ f_r1 = out_dir / "sub-01_seg-R1_tac.tsv"
64
+ f_r2 = out_dir / "sub-01_seg-R2_tac.tsv"
65
+ assert f_r1.exists()
66
+ assert not f_r2.exists()
67
+ # Basic content check
68
+ content = f_r1.read_text()
69
+ assert "time\tactivity\tuncertainty" in content
70
+ assert "0.5\t" in content # time present
71
+
72
+ def test_write_tacs_multitac_writes_combined_file_and_skips_nan_regions(tmp_path, monkeypatch):
73
+ # same masking behavior as previous test
74
+ def fake_apply_mask_4d(input_arr, mask_arr, verbose=False):
75
+ if mask_arr == 1:
76
+ return np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
77
+ else:
78
+ return np.array([[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan]])
79
+ monkeypatch.setattr(rtx, "apply_mask_4d", fake_apply_mask_4d)
80
+
81
+ wr = rtx.WriteRegionalTacs(input_image_path="in.nii", segmentation_path="seg.nii", label_map="dummy")
82
+ out_dir = tmp_path
83
+ wr.write_tacs(out_tac_prefix="sub-01", out_tac_dir=str(out_dir), one_tsv_per_region=False)
84
+ combined = out_dir / "sub-01_multitacs.tsv"
85
+ assert combined.exists()
86
+ txt = combined.read_text()
87
+ # Should contain frame_start(min) and R1 column but not R2 (R2 was NaN and skipped)
88
+ assert "frame_start(min)" in txt
89
+ assert "R1" in txt
90
+ assert "R2" not in txt
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
@@ -1103,13 +1103,13 @@ class MultiTACGraphicalAnalysis(GraphicalAnalysis, MultiTACAnalysisMixin):
1103
1103
  if self.analysis_props[0]['RSquared'] is None:
1104
1104
  raise RuntimeError("'run_analysis' method must be called before 'save_analysis'.")
1105
1105
 
1106
- if output_as_tsv:
1106
+ if output_as_json:
1107
1107
  km_multifit_analysis_to_jsons(analysis_props=self.analysis_props,
1108
1108
  output_directory=self.output_directory,
1109
1109
  output_filename_prefix=self.output_filename_prefix,
1110
1110
  method=self.method,
1111
1111
  inferred_seg_labels=self.inferred_seg_labels)
1112
- if output_as_json:
1112
+ if output_as_tsv:
1113
1113
  km_multifit_analysis_to_tsv(analysis_props=self.analysis_props,
1114
1114
  output_directory=self.output_directory,
1115
1115
  output_filename_prefix=self.output_filename_prefix,
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