petpal 0.5.8__py3-none-any.whl → 0.5.9__py3-none-any.whl

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.
@@ -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():
@@ -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,
@@ -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:
@@ -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>
@@ -1,6 +1,6 @@
1
1
  petpal/__init__.py,sha256=rYYNkBkSkHGE18JIZPeVK8YBP7Z9c4mpQfKN0cdS75k,318
2
2
  petpal/cli/__init__.py,sha256=RiQTAOhSeqw5BTVvdancX3JQj4CG8F9Qe4qWZR9nKio,434
3
- petpal/cli/cli_graphical_analysis.py,sha256=nGb0afMAigJgvbyEj5EXOCt6WNU35LNo3h3KLC8_f84,5529
3
+ petpal/cli/cli_graphical_analysis.py,sha256=L-YhkKkjmf6WLaAt8GDt1VmXaJXOsjMrbVvPhiTViYM,5386
4
4
  petpal/cli/cli_graphical_plots.py,sha256=_2tlGtZ0hIVyEYtGviEzGZMNhFymUPg4ZvSVyMtT_dA,3211
5
5
  petpal/cli/cli_idif.py,sha256=6lh_kJHcGjlHDXZOvbiuHrNqpk5FovVV5_j7_dPHTHU,5145
6
6
  petpal/cli/cli_parametric_images.py,sha256=JBFb8QlxZoGOzqvCJPFuZ7czzGWntJP5ZcfeM5-QF4Y,7385
@@ -19,7 +19,7 @@ petpal/input_function/idif_necktangle.py,sha256=o5kyAqyT4C6o7zELY4EjyHrkJyX1BWcx
19
19
  petpal/input_function/pca_guided_idif.py,sha256=MPB59K5Z5oyIunIWFqFQts61z647xawLNkv8wICrKYM,44821
20
20
  petpal/kinetic_modeling/__init__.py,sha256=tW4yRH3TwaXPwKPqdkrbQmSk9hjrF1yRkV_C59PPboQ,382
21
21
  petpal/kinetic_modeling/fit_tac_with_rtms.py,sha256=HpK7VWVCCNoSQABY9i28vYpZsMRmvgs4vdcM_ZbdaYE,20971
22
- petpal/kinetic_modeling/graphical_analysis.py,sha256=a7IOwYnG3Wao2XTjFgsPK563txm7s4lpMbaqUMQ5wUQ,51003
22
+ petpal/kinetic_modeling/graphical_analysis.py,sha256=e3ZXP8jA3ZgvC2T718-gukeBPlxCmOedCb2KlcGTUp8,51003
23
23
  petpal/kinetic_modeling/parametric_images.py,sha256=sXYracBFUtFyttO-6oiDAldnU8hPN6Y4vKOD1V-DnlE,47301
24
24
  petpal/kinetic_modeling/reference_tissue_models.py,sha256=FkLziIgtpA8tOL2gZJFg_nB8VPEBs40T7RsDAJ3nJ-A,39510
25
25
  petpal/kinetic_modeling/rtm_analysis.py,sha256=e3EuaHXml4PDALEczwyOPpnThINAGh41UKNlOQHAPqc,25945
@@ -42,9 +42,9 @@ petpal/preproc/image_operations_4d.py,sha256=IqzwxaWxoWC1gmK00uuHIwlhx8e_eQ44C6y
42
42
  petpal/preproc/motion_corr.py,sha256=dz10qjXBVTF_RH5RPZ68drUVX2qyj-MnZ674_Ccwz2Y,28670
43
43
  petpal/preproc/motion_target.py,sha256=_OJp3NoYcyD3Ke3wl2KbfOhbJ6dp6ZduR9LLz0rIaC0,3945
44
44
  petpal/preproc/partial_volume_corrections.py,sha256=J06j_Y_lhj3b3b9M5FbB2r2EPWQvoymG3GRUffSlYdE,6799
45
- petpal/preproc/regional_tac_extraction.py,sha256=qQDD9Z9p21DVUKokh_en2chOGP7F01wnDN156_74X8Q,19704
45
+ petpal/preproc/regional_tac_extraction.py,sha256=ZXo2u-EAUg5wZj7GGYLMEaOAfLv8OCOR-Gd0xvih6Y4,22358
46
46
  petpal/preproc/register.py,sha256=NKg8mt_XMGa5HBdxYZh3sMu_KMJ0W41VHlX4Zl8wlyE,14171
47
- petpal/preproc/segmentation_tools.py,sha256=BUy8ij45mmetenvWzODVwNIThDkYiEtY6gTAqI8sIak,25703
47
+ petpal/preproc/segmentation_tools.py,sha256=Xi1ZnBs3sp23MHWPPOLjuXi6qp4-igwIPXFJ4B_Yzsk,27186
48
48
  petpal/preproc/standard_uptake_value.py,sha256=YJIt0fl3fwMLl0tRYHpPPprMTaN4Q5JjQ5dx_CQX1nI,7494
49
49
  petpal/preproc/symmetric_geometric_transfer_matrix.py,sha256=ELkr7Mo233to1Rwml5YJ-aBvmTSk3LHNSdRhnX0WBDw,17575
50
50
  petpal/utils/__init__.py,sha256=PlxBIKUtNvtSFnNZqz8myszOysaYzS8nSILMK4haVGg,412
@@ -58,15 +58,15 @@ petpal/utils/metadata.py,sha256=O9exRDlqAmPAEcO9v7dsqzkYcSVLgRA207owEvNXXJ8,6129
58
58
  petpal/utils/scan_timing.py,sha256=CYtYuFquAnOQ2QfjXdeLjWrBDPM_k4vBI9oHQdpmVZ0,13908
59
59
  petpal/utils/stats.py,sha256=paFdwVPIjlAi0wh5xU4x5WeydjKsEHuwzMLcDG_WzPc,6449
60
60
  petpal/utils/testing_utils.py,sha256=eMt1kklxK3rl8tm74I3yVNDotKh1CnYWLINDT7rzboM,9557
61
- petpal/utils/time_activity_curve.py,sha256=gX3PDYbeWblycvtvyiuFtnv1mBml_-93sIXKh2EmglM,39137
61
+ petpal/utils/time_activity_curve.py,sha256=ZjirIVy6rxG1cEZhYzFhbi9FEixlgdBpVqcZXXz6c3U,40379
62
62
  petpal/utils/useful_functions.py,sha256=md2kTLbs45MhrjdMhvDYcbflPTRNPspRSIHiOeIxEqY,21361
63
63
  petpal/visualizations/__init__.py,sha256=bd0NHDVl6Z2BDhisEcob2iIcqfxUfgKJ4DEmlrXJRP4,205
64
64
  petpal/visualizations/graphical_plots.py,sha256=ZCKUeLX2TAQscuHjA4bzlFm1bACHIyCwDuNnjCakVWU,47297
65
65
  petpal/visualizations/image_visualization.py,sha256=Ob6TD4Q0pIrxi0m9SznK1TRWbX1Ea9Pt4wNMdRrTfTs,9124
66
66
  petpal/visualizations/qc_plots.py,sha256=iaCPe-LWWyM3OZzDPZodHZhP-z5fRdpUgaH7QS9VxPM,1243
67
67
  petpal/visualizations/tac_plots.py,sha256=zSGdptL-EnqhfDViAX8LFunln5a1b-NJ5ft7ZDcxQ38,15116
68
- petpal-0.5.8.dist-info/METADATA,sha256=p2q6Rl726zTvyMW39Uw3j4jw0dXYKFhIDvkGG9nXqKk,2617
69
- petpal-0.5.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
70
- petpal-0.5.8.dist-info/entry_points.txt,sha256=0SZmyXqBxKzQg2eerDA16n2BdUEXyixEm0_AUo2dFns,653
71
- petpal-0.5.8.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
72
- petpal-0.5.8.dist-info/RECORD,,
68
+ petpal-0.5.9.dist-info/METADATA,sha256=BVL0zucYR-sLQU16D61fSFD0_ADn97mcXz6Qva6a9UQ,2617
69
+ petpal-0.5.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
70
+ petpal-0.5.9.dist-info/entry_points.txt,sha256=0SZmyXqBxKzQg2eerDA16n2BdUEXyixEm0_AUo2dFns,653
71
+ petpal-0.5.9.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
72
+ petpal-0.5.9.dist-info/RECORD,,
File without changes