petpal 0.5.8__tar.gz → 0.5.10__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.
- {petpal-0.5.8 → petpal-0.5.10}/PKG-INFO +1 -1
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_graphical_analysis.py +1 -2
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/image_operations_4d.py +3 -4
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/regional_tac_extraction.py +66 -7
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/segmentation_tools.py +28 -1
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/symmetric_geometric_transfer_matrix.py +63 -17
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/time_activity_curve.py +46 -12
- {petpal-0.5.8 → petpal-0.5.10}/pyproject.toml +1 -1
- petpal-0.5.10/tests/test_graphical_analysis.py +90 -0
- petpal-0.5.10/tests/test_sgtm.py +53 -0
- petpal-0.5.10/tests/test_time_activity_curve.py +33 -0
- petpal-0.5.10/tests/test_write_tacs.py +90 -0
- {petpal-0.5.8 → petpal-0.5.10}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/.github/workflows/publish-to-pypi.yml +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/.github/workflows/python-package.yml +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/.gitignore +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/.readthedocs.yaml +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/LICENSE +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/README.md +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-02.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/gaussian_noise/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/gaussian_noise/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-02.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/noise_free/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/noise_free/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/params_1tcm_set-00.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/params_1tcm_set-01.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/1tcm/params_1tcm_set-02.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled_woMax.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/fdg_plasma_clamp_tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/fdg_plasma_clamp_tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/gen_tcms_data.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/readme.md +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-02.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-02.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/noise_free/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/noise_free/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-00.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-01.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-02.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-00.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-01.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/data/tcm_tacs/turku_pet_center_fdg_plasma_clamp.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/Makefile +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/PETPAL_Logo.png +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/index.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/python/attribute.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/python/class.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/python/data.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/python/exception.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/python/function.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/python/method.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/python/module.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/python/package.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/_templates/python/property.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/conf.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/index.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/make.bat +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/requirements.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/tutorials/index.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/docs/tutorials/pib_example.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_graphical_plots.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_idif.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_parametric_images.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_pib_processing.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_plot_tacs.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_preproc.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_pvc.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_reference_tissue_models.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_stats.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_tac_fitting.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_tac_interpolation.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/cli/cli_vat_processing.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/input_function/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/input_function/blood_input.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/input_function/idif_necktangle.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/input_function/pca_guided_idif.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/fit_tac_with_rtms.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/graphical_analysis.py +2 -2
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/parametric_images.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/reference_tissue_models.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/rtm_analysis.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/tac_fitting.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/tac_interpolation.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/tac_uncertainty.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/kinetic_modeling/tcms_as_convolutions.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/meta/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/meta/label_maps.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/pipelines/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/pipelines/kinetic_modeling_steps.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/pipelines/pca_guided_idif_steps.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/pipelines/pipelines.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/pipelines/preproc_steps.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/pipelines/steps_base.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/pipelines/steps_containers.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/decay_correction.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/motion_corr.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/motion_target.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/partial_volume_corrections.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/register.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/preproc/standard_uptake_value.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/bids_utils.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/constants.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/data_driven_image_analyses.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/decorators.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/image_io.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/math_lib.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/metadata.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/scan_timing.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/stats.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/testing_utils.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/utils/useful_functions.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/visualizations/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/visualizations/graphical_plots.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/visualizations/image_visualization.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/visualizations/qc_plots.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/petpal/visualizations/tac_plots.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/shared/dseg.tsv +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/shared/freesurfer_lmap.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/shared/freesurfer_lmap_lr.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/shared/perl_cyno_lmap.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/shared/perl_cyno_lmap_lr.json +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/test_notebooks/explicit_tac_fitting/01_fitting_TCMs.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/test_notebooks/testing_RTMs/01_testing_RTMs.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/test_notebooks/testing_graphical_analyses/01_testing_on_tcms_database.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/test_notebooks/testing_graphical_analyses/02_testing_parametric_images.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/test_notebooks/testing_graphical_analyses/03_plotting_graphical_anlayses_testbed.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/tests/test_importpetpal.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/tests/test_register.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/tests/test_scan_timing_decay.py +0 -0
- {petpal-0.5.8 → petpal-0.5.10}/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.
|
|
3
|
+
Version: 0.5.10
|
|
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(
|
|
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():
|
|
@@ -64,10 +64,9 @@ def stitch_broken_scans(input_image_path: str,
|
|
|
64
64
|
try:
|
|
65
65
|
noninitial_time_zeroes = [meta['TimeZero'] for meta in noninitial_image_metadata_dicts]
|
|
66
66
|
actual_time_zero = initial_image_metadata['TimeZero']
|
|
67
|
-
except KeyError:
|
|
68
|
-
raise KeyError(
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
except KeyError as exc:
|
|
68
|
+
raise KeyError('.json sidecar for one of your input images does not contain required BIDS '
|
|
69
|
+
'key "TimeZero".') from exc
|
|
71
70
|
initial_scan_time = datetime.time.fromisoformat(actual_time_zero)
|
|
72
71
|
placeholder_date = datetime.date.today()
|
|
73
72
|
initial_scan_datetime = datetime.datetime.combine(date=placeholder_date,
|
|
@@ -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
|
-
|
|
365
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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
|
|
@@ -9,6 +9,7 @@ import warnings
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
from scipy.ndimage import gaussian_filter
|
|
11
11
|
import ants
|
|
12
|
+
import pandas as pd
|
|
12
13
|
|
|
13
14
|
from ..meta.label_maps import LabelMapLoader
|
|
14
15
|
from ..utils.useful_functions import check_physical_space_for_ants_image_pair
|
|
@@ -92,24 +93,33 @@ class Sgtm:
|
|
|
92
93
|
elif self.input_image.dimension == 4:
|
|
93
94
|
self.sgtm_result = self.run_sgtm_4d()
|
|
94
95
|
|
|
95
|
-
def save(self, output_path: str, out_tac_prefix: str | None = None):
|
|
96
|
+
def save(self, output_path: str, out_tac_prefix: str | None = None, one_tsv_per_region: bool = False):
|
|
96
97
|
r"""Save sGTM results by writing the resulting array to one or more files.
|
|
97
98
|
|
|
98
|
-
The behavior depends on the input
|
|
99
|
+
The behavior depends on the input image provided. If input image is 3D, saves the average sGTM value for each
|
|
99
100
|
region in a TSV with one row per region. If input image is 4D, saves time series average values for each frame
|
|
100
|
-
within each region
|
|
101
|
+
within each region. 4D operation saves a single file unless `one_tsv_per_region` is set to True.
|
|
101
102
|
|
|
102
103
|
Args:
|
|
103
|
-
output_path (str): Path to save sGTM results. For 3D images, this should typically be
|
|
104
|
-
.tsv file. For 4D images, this is the directory where the sGTM
|
|
104
|
+
output_path (str): Path to save sGTM results. For 3D images, this should typically be
|
|
105
|
+
the full path to a .tsv file. For 4D images, this is the directory where the sGTM
|
|
106
|
+
TACs will be saved.
|
|
105
107
|
out_tac_prefix (Optional, str): Prefix of the TAC files. Typically, something like
|
|
106
108
|
``'sub-001_ses-001_desc-sGTM'``. Defaults to None.
|
|
109
|
+
one_tsv_per_region (bool): If True, saves one tsv file for each unique region, as
|
|
110
|
+
opposed to one file containing all TACs if False. Default False.
|
|
107
111
|
"""
|
|
108
112
|
if self.input_image.dimension == 3:
|
|
109
113
|
self.save_results_3d(sgtm_result=self.sgtm_result, out_tsv_path=output_path)
|
|
110
114
|
elif self.input_image.dimension == 4:
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
if one_tsv_per_region:
|
|
116
|
+
self.save_results_4d_tacs(sgtm_result=self.sgtm_result,
|
|
117
|
+
out_tac_dir=output_path,
|
|
118
|
+
out_tac_prefix=out_tac_prefix)
|
|
119
|
+
else:
|
|
120
|
+
self.save_results_4d_multitacs(sgtm_result=self.sgtm_result,
|
|
121
|
+
out_tac_dir=output_path,
|
|
122
|
+
out_tac_prefix=out_tac_prefix)
|
|
113
123
|
|
|
114
124
|
def __call__(self, output_path: str, out_tac_prefix: str | None = None):
|
|
115
125
|
r"""Run sGTM and save results.
|
|
@@ -162,8 +172,20 @@ class Sgtm:
|
|
|
162
172
|
"segmentation to ensure this criteria is met, or use sGTM without "
|
|
163
173
|
"label map for automated complete region mapping.")
|
|
164
174
|
seg_label_map = LabelMapLoader(label_map_option=self.label_map_option).label_map
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
unique_mappings = unique_segmentation_labels(segmentation_img=self.segmentation_image,
|
|
176
|
+
zeroth_roi=self.zeroth_roi)
|
|
177
|
+
region_index_map = []
|
|
178
|
+
region_short_names = []
|
|
179
|
+
label_map_labels = list(seg_label_map.keys())
|
|
180
|
+
label_map_mappings = list(seg_label_map.values())
|
|
181
|
+
for mapping in unique_mappings:
|
|
182
|
+
if mapping in label_map_mappings:
|
|
183
|
+
id_mapping_index = label_map_mappings.index(mapping)
|
|
184
|
+
region_index_map.append(label_map_mappings[id_mapping_index])
|
|
185
|
+
region_short_names.append(label_map_labels[id_mapping_index])
|
|
186
|
+
else:
|
|
187
|
+
region_index_map.append(mapping)
|
|
188
|
+
region_short_names.append(f'UNK{mapping:05d}')
|
|
167
189
|
return (region_index_map, region_short_names)
|
|
168
190
|
|
|
169
191
|
|
|
@@ -291,7 +313,6 @@ class Sgtm:
|
|
|
291
313
|
|
|
292
314
|
return unique_labels, t_corrected, condition_number
|
|
293
315
|
|
|
294
|
-
|
|
295
316
|
def run_sgtm_4d(self) -> np.ndarray:
|
|
296
317
|
r"""Calculated partial volume corrected TACs on a 4D image by running sGTM on each frame in
|
|
297
318
|
the 4D image.
|
|
@@ -326,7 +347,6 @@ class Sgtm:
|
|
|
326
347
|
|
|
327
348
|
return np.asarray(frame_results)
|
|
328
349
|
|
|
329
|
-
|
|
330
350
|
def save_results_3d(self, sgtm_result: tuple, out_tsv_path: str):
|
|
331
351
|
r"""Saves the result of an sGTM calculation.
|
|
332
352
|
|
|
@@ -337,12 +357,10 @@ class Sgtm:
|
|
|
337
357
|
sgtm_result (tuple): Output of :meth:`run_sgtm_3d`
|
|
338
358
|
out_tsv_path (str): File path to which results are saved.
|
|
339
359
|
"""
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
comments='')
|
|
345
|
-
|
|
360
|
+
sgtm_result_to_write = pd.DataFrame(columns=['Region','Mean'])
|
|
361
|
+
sgtm_result_to_write['Region'] = self.unique_labels[1]
|
|
362
|
+
sgtm_result_to_write['Mean'] = sgtm_result[1]
|
|
363
|
+
sgtm_result_to_write.to_csv(out_tsv_path,sep='\t',index=False)
|
|
346
364
|
|
|
347
365
|
def save_results_4d_tacs(self,
|
|
348
366
|
sgtm_result: np.ndarray,
|
|
@@ -368,3 +386,31 @@ class Sgtm:
|
|
|
368
386
|
activity=tac_array[i,:])
|
|
369
387
|
out_tac_path = os.path.join(f'{out_tac_dir}', f'{out_tac_prefix}_seg-{name}_tac.tsv')
|
|
370
388
|
pvc_tac.to_tsv(filename=out_tac_path)
|
|
389
|
+
|
|
390
|
+
def save_results_4d_multitacs(self,
|
|
391
|
+
sgtm_result: np.ndarray,
|
|
392
|
+
out_tac_dir: str,
|
|
393
|
+
out_tac_prefix: str):
|
|
394
|
+
"""Like :meth:`save_results_4d_tacs`, but saves all TACs to a single file.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
sgtm_result (np.ndarray): Array of results from :meth:`run_sgtm_4d`
|
|
398
|
+
out_tac_dir (str): Path to folder where regional TACs will be saved.
|
|
399
|
+
out_tac_prefix (str): Prefix of the TAC files.
|
|
400
|
+
"""
|
|
401
|
+
os.makedirs(out_tac_dir, exist_ok=True)
|
|
402
|
+
input_image_path = self.input_image_path
|
|
403
|
+
scan_timing = ScanTimingInfo.from_nifti(image_path=input_image_path)
|
|
404
|
+
tac_time_starts = scan_timing.start_in_mins
|
|
405
|
+
tac_time_ends = scan_timing.end_in_mins
|
|
406
|
+
|
|
407
|
+
tac_array = np.asarray(sgtm_result).T
|
|
408
|
+
tacs_data_columns = ['frame_start(min)','frame_end(min)']+self.unique_labels[1]
|
|
409
|
+
tacs_data = pd.DataFrame(columns=tacs_data_columns)
|
|
410
|
+
|
|
411
|
+
tacs_data['frame_start(min)'] = tac_time_starts
|
|
412
|
+
tacs_data['frame_end(min)'] = tac_time_ends
|
|
413
|
+
for i, (_label, name) in enumerate(zip(*self.unique_labels)):
|
|
414
|
+
tacs_data[name] = tac_array[i,:]
|
|
415
|
+
tacs_data[f'{name}_unc'] = np.full(tac_array.shape[1],np.nan)
|
|
416
|
+
tacs_data.to_csv(f'{out_tac_dir}/{out_tac_prefix}_multitacs.tsv', sep='\t', index=False)
|
|
@@ -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
|
-
|
|
513
|
-
return shifted_tac
|
|
540
|
+
return shifted_tac
|
|
514
541
|
|
|
515
542
|
@staticmethod
|
|
516
543
|
def tac_dispersion(tac: 'TimeActivityCurve',
|
|
517
|
-
disp_func: Callable[
|
|
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
|
|
524
|
-
It performs convolution (using :func:`scipy.signal.convolve`)of the supersampled
|
|
525
|
-
the dispersion function, and the result is sampled back at the original TAC time
|
|
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
|
|
530
|
-
the convolution. Convolving non-evenly sampled arrays produces nonsense
|
|
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[
|
|
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
|
|
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:
|
|
@@ -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,53 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from petpal.preproc.symmetric_geometric_transfer_matrix import Sgtm
|
|
3
|
+
|
|
4
|
+
class DummyImage:
|
|
5
|
+
def __init__(self, dimension):
|
|
6
|
+
self.dimension = dimension
|
|
7
|
+
|
|
8
|
+
def make_sgtm_with_dimension(dim, sgtm_result=None):
|
|
9
|
+
sgtm = object.__new__(Sgtm)
|
|
10
|
+
sgtm.input_image = DummyImage(dimension=dim)
|
|
11
|
+
sgtm.sgtm_result = sgtm_result
|
|
12
|
+
return sgtm
|
|
13
|
+
|
|
14
|
+
def test_save_calls_save_results_3d_for_3d_image():
|
|
15
|
+
sgtm = make_sgtm_with_dimension(3, sgtm_result=("labels", "vals", 1.0))
|
|
16
|
+
called = {}
|
|
17
|
+
def fake_save_results_3d(sgtm_result, out_tsv_path):
|
|
18
|
+
called['args'] = (sgtm_result, out_tsv_path)
|
|
19
|
+
sgtm.save_results_3d = fake_save_results_3d
|
|
20
|
+
|
|
21
|
+
sgtm.save(output_path="out.tsv")
|
|
22
|
+
|
|
23
|
+
assert 'args' in called
|
|
24
|
+
assert called['args'][0] is sgtm.sgtm_result
|
|
25
|
+
assert called['args'][1] == "out.tsv"
|
|
26
|
+
|
|
27
|
+
def test_save_calls_save_results_4d_tacs_when_one_tsv_per_region_true():
|
|
28
|
+
sgtm = make_sgtm_with_dimension(4, sgtm_result="frame_results")
|
|
29
|
+
called = {}
|
|
30
|
+
def fake_save_results_4d_tacs(sgtm_result, out_tac_dir, out_tac_prefix):
|
|
31
|
+
called['args'] = (sgtm_result, out_tac_dir, out_tac_prefix)
|
|
32
|
+
sgtm.save_results_4d_tacs = fake_save_results_4d_tacs
|
|
33
|
+
|
|
34
|
+
sgtm.save(output_path="/tmp/dir", out_tac_prefix="pref", one_tsv_per_region=True)
|
|
35
|
+
|
|
36
|
+
assert 'args' in called
|
|
37
|
+
assert called['args'][0] is sgtm.sgtm_result
|
|
38
|
+
assert called['args'][1] == "/tmp/dir"
|
|
39
|
+
assert called['args'][2] == "pref"
|
|
40
|
+
|
|
41
|
+
def test_save_calls_save_results_4d_multitacs_when_one_tsv_per_region_false():
|
|
42
|
+
sgtm = make_sgtm_with_dimension(4, sgtm_result="frame_results")
|
|
43
|
+
called = {}
|
|
44
|
+
def fake_save_results_4d_multitacs(sgtm_result, out_tac_dir, out_tac_prefix):
|
|
45
|
+
called['args'] = (sgtm_result, out_tac_dir, out_tac_prefix)
|
|
46
|
+
sgtm.save_results_4d_multitacs = fake_save_results_4d_multitacs
|
|
47
|
+
|
|
48
|
+
sgtm.save(output_path="/tmp/dir2", out_tac_prefix="pref2", one_tsv_per_region=False)
|
|
49
|
+
|
|
50
|
+
assert 'args' in called
|
|
51
|
+
assert called['args'][0] is sgtm.sgtm_result
|
|
52
|
+
assert called['args'][1] == "/tmp/dir2"
|
|
53
|
+
assert called['args'][2] == "pref2"
|
|
@@ -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]))
|