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.
- {petpal-0.5.8 → petpal-0.5.9}/PKG-INFO +1 -1
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_graphical_analysis.py +1 -2
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/regional_tac_extraction.py +66 -7
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/segmentation_tools.py +28 -1
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/time_activity_curve.py +46 -12
- {petpal-0.5.8 → petpal-0.5.9}/pyproject.toml +1 -1
- petpal-0.5.9/tests/test_graphical_analysis.py +90 -0
- petpal-0.5.9/tests/test_time_activity_curve.py +33 -0
- petpal-0.5.9/tests/test_write_tacs.py +90 -0
- {petpal-0.5.8 → petpal-0.5.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/.github/workflows/publish-to-pypi.yml +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/.github/workflows/python-package.yml +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/.gitignore +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/.readthedocs.yaml +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/LICENSE +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/README.md +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-02.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-02.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-00.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-01.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-02.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled_woMax.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/gen_tcms_data.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/readme.md +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-02.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-02.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-00.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-01.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-02.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-00.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-01.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.pdf +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.png +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-00.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-01.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/turku_pet_center_fdg_plasma_clamp.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/Makefile +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/PETPAL_Logo.png +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/index.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/attribute.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/class.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/data.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/exception.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/function.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/method.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/module.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/package.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/_templates/python/property.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/conf.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/index.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/make.bat +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/requirements.txt +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/tutorials/index.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/docs/tutorials/pib_example.rst +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_graphical_plots.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_idif.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_parametric_images.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_pib_processing.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_plot_tacs.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_preproc.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_pvc.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_reference_tissue_models.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_stats.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_tac_fitting.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_tac_interpolation.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/cli/cli_vat_processing.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/input_function/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/input_function/blood_input.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/input_function/idif_necktangle.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/input_function/pca_guided_idif.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/fit_tac_with_rtms.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/graphical_analysis.py +2 -2
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/parametric_images.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/reference_tissue_models.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/rtm_analysis.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/tac_fitting.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/tac_interpolation.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/tac_uncertainty.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/kinetic_modeling/tcms_as_convolutions.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/meta/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/meta/label_maps.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/kinetic_modeling_steps.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/pca_guided_idif_steps.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/pipelines.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/preproc_steps.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/steps_base.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/pipelines/steps_containers.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/decay_correction.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/image_operations_4d.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/motion_corr.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/motion_target.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/partial_volume_corrections.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/register.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/standard_uptake_value.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/preproc/symmetric_geometric_transfer_matrix.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/bids_utils.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/constants.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/data_driven_image_analyses.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/decorators.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/image_io.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/math_lib.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/metadata.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/scan_timing.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/stats.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/testing_utils.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/utils/useful_functions.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/__init__.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/graphical_plots.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/image_visualization.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/qc_plots.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/petpal/visualizations/tac_plots.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/shared/dseg.tsv +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/shared/freesurfer_lmap.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/shared/freesurfer_lmap_lr.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/shared/perl_cyno_lmap.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/shared/perl_cyno_lmap_lr.json +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/explicit_tac_fitting/01_fitting_TCMs.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/testing_RTMs/01_testing_RTMs.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/01_testing_on_tcms_database.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/02_testing_parametric_images.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/03_plotting_graphical_anlayses_testbed.ipynb +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/tests/test_importpetpal.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/tests/test_register.py +0 -0
- {petpal-0.5.8 → petpal-0.5.9}/tests/test_scan_timing_decay.py +0 -0
- {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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-00.txt
RENAMED
|
File without changes
|
{petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-01.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-00.json
RENAMED
|
File without changes
|
{petpal-0.5.8 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-01.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|