petpal 0.5.7__tar.gz → 0.5.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {petpal-0.5.7 → petpal-0.5.9}/.gitignore +4 -1
- {petpal-0.5.7 → petpal-0.5.9}/PKG-INFO +1 -1
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_graphical_analysis.py +1 -2
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/decay_correction.py +11 -10
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/regional_tac_extraction.py +66 -7
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/segmentation_tools.py +28 -1
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/scan_timing.py +66 -19
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/time_activity_curve.py +46 -12
- {petpal-0.5.7 → 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_register.py +82 -0
- petpal-0.5.9/tests/test_scan_timing_decay.py +146 -0
- petpal-0.5.9/tests/test_time_activity_curve.py +33 -0
- petpal-0.5.9/tests/test_weighted_sum.py +51 -0
- petpal-0.5.9/tests/test_write_tacs.py +90 -0
- {petpal-0.5.7 → petpal-0.5.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/.github/workflows/publish-to-pypi.yml +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/.github/workflows/python-package.yml +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/.readthedocs.yaml +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/LICENSE +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/README.md +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-00.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-01.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tac_1tcm_set-02.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tacs.pdf +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/gaussian_noise/tacs.png +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-00.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-01.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tac_1tcm_set-02.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tacs.pdf +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/noise_free/tacs.png +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-00.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-01.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/1tcm/params_1tcm_set-02.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_evenly_resampled_woMax.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_tacs.pdf +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/fdg_plasma_clamp_tacs.png +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/gen_tcms_data.ipynb +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/readme.md +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-00.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-01.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tac_2tcm_set-02.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.pdf +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/gaussian_noise/tacs.png +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-00.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-01.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tac_2tcm_set-02.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tacs.pdf +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/noise_free/tacs.png +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-00.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-01.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm/params_serial_2tcm_set-02.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-00.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tac_2tcm_k4zero_set-01.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.pdf +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/gaussian_noise/tacs.png +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-00.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tac_2tcm_k4zero_set-01.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.pdf +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/noise_free/tacs.png +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-00.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/serial_2tcm_k4zero/params_serial_2tcm_k4zero_set-01.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/data/tcm_tacs/turku_pet_center_fdg_plasma_clamp.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/Makefile +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/PETPAL_Logo.png +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/index.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/attribute.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/class.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/data.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/exception.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/function.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/method.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/module.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/package.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/_templates/python/property.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/conf.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/index.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/make.bat +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/requirements.txt +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/tutorials/index.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/docs/tutorials/pib_example.rst +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/__init__.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/__init__.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_graphical_plots.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_idif.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_parametric_images.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_pib_processing.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_plot_tacs.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_preproc.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_pvc.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_reference_tissue_models.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_stats.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_tac_fitting.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_tac_interpolation.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/cli/cli_vat_processing.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/input_function/__init__.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/input_function/blood_input.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/input_function/idif_necktangle.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/input_function/pca_guided_idif.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/__init__.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/fit_tac_with_rtms.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/graphical_analysis.py +2 -2
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/parametric_images.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/reference_tissue_models.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/rtm_analysis.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/tac_fitting.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/tac_interpolation.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/tac_uncertainty.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/kinetic_modeling/tcms_as_convolutions.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/meta/__init__.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/meta/label_maps.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/__init__.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/kinetic_modeling_steps.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/pca_guided_idif_steps.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/pipelines.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/preproc_steps.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/steps_base.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/pipelines/steps_containers.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/__init__.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/image_operations_4d.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/motion_corr.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/motion_target.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/partial_volume_corrections.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/register.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/standard_uptake_value.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/preproc/symmetric_geometric_transfer_matrix.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/__init__.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/bids_utils.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/constants.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/data_driven_image_analyses.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/decorators.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/image_io.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/math_lib.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/metadata.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/stats.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/testing_utils.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/utils/useful_functions.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/__init__.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/graphical_plots.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/image_visualization.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/qc_plots.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/petpal/visualizations/tac_plots.py +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/shared/dseg.tsv +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/shared/freesurfer_lmap.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/shared/freesurfer_lmap_lr.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/shared/perl_cyno_lmap.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/shared/perl_cyno_lmap_lr.json +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/explicit_tac_fitting/01_fitting_TCMs.ipynb +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/testing_RTMs/01_testing_RTMs.ipynb +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/01_testing_on_tcms_database.ipynb +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/02_testing_parametric_images.ipynb +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/test_notebooks/testing_graphical_analyses/03_plotting_graphical_anlayses_testbed.ipynb +0 -0
- {petpal-0.5.7 → petpal-0.5.9}/tests/test_importpetpal.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():
|
|
@@ -129,21 +129,22 @@ def decay_correct(input_image_path: str,
|
|
|
129
129
|
return corrected_image
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
def calculate_frame_decay_factor(frame_reference_time:
|
|
133
|
-
half_life: float) ->
|
|
134
|
-
"""Calculate decay
|
|
132
|
+
def calculate_frame_decay_factor(frame_reference_time: np.ndarray,
|
|
133
|
+
half_life: float) -> np.ndarray:
|
|
134
|
+
"""Calculate decay correction factors for a scan given the frame reference time and half life.
|
|
135
135
|
|
|
136
136
|
Important:
|
|
137
137
|
The frame reference time should be the time at which average activity occurs,
|
|
138
|
-
not simply the midpoint. See
|
|
139
|
-
|
|
138
|
+
not simply the midpoint. See
|
|
139
|
+
:meth:`~petpal.utils.scan_timing.calculate_frame_reference_time` for more info.
|
|
140
|
+
|
|
140
141
|
Args:
|
|
141
|
-
frame_reference_time (
|
|
142
|
+
frame_reference_time (np.ndarray): Time at which the average activity occurs for the frame.
|
|
142
143
|
half_life (float): Radionuclide half life.
|
|
143
144
|
|
|
144
145
|
Returns:
|
|
145
|
-
|
|
146
|
+
np.ndarray: Decay Correction Factors for each frame in the scan.
|
|
146
147
|
"""
|
|
147
|
-
decay_constant =
|
|
148
|
-
frame_decay_factor =
|
|
149
|
-
return frame_decay_factor
|
|
148
|
+
decay_constant = np.log(2)/half_life
|
|
149
|
+
frame_decay_factor = np.exp((decay_constant)*frame_reference_time)
|
|
150
|
+
return frame_decay_factor
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Regional TAC extraction
|
|
3
3
|
"""
|
|
4
|
+
from warnings import warn
|
|
4
5
|
import os
|
|
5
6
|
from collections.abc import Callable
|
|
6
7
|
import pathlib
|
|
@@ -345,6 +346,20 @@ class WriteRegionalTacs:
|
|
|
345
346
|
region_name = f'UNK{label:>04}'
|
|
346
347
|
return region_name
|
|
347
348
|
|
|
349
|
+
def is_empty_region(self,pet_masked_region: np.ndarray) -> bool:
|
|
350
|
+
"""Check if masked PET region has zero matched voxels, or is all NaNs. In either case,
|
|
351
|
+
return True, otherwise return False.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
pet_masked_region (np.ndarray): Array of PET voxels masked to a specific region.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
pet_masked_region_is_empty (bool): If True, input region is empty."""
|
|
358
|
+
if pet_masked_region.size==0:
|
|
359
|
+
return True
|
|
360
|
+
if np.all(np.isnan(pet_masked_region)):
|
|
361
|
+
return True
|
|
362
|
+
return False
|
|
348
363
|
|
|
349
364
|
def extract_tac(self,region_mapping: int | list[int], **tac_calc_kwargs) -> TimeActivityCurve:
|
|
350
365
|
"""
|
|
@@ -359,15 +374,46 @@ class WriteRegionalTacs:
|
|
|
359
374
|
"""
|
|
360
375
|
region_mask = combine_regions_as_mask(segmentation_img=self.seg_arr,
|
|
361
376
|
label=region_mapping)
|
|
377
|
+
|
|
362
378
|
pet_masked_region = apply_mask_4d(input_arr=self.pet_arr,
|
|
363
379
|
mask_arr=region_mask)
|
|
364
|
-
|
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Module to handle timing information of PET scans.
|
|
3
3
|
"""
|
|
4
|
-
import
|
|
4
|
+
from typing import Self
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
@@ -82,8 +82,8 @@ class ScanTimingInfo:
|
|
|
82
82
|
|
|
83
83
|
"""
|
|
84
84
|
duration: np.ndarray[float]
|
|
85
|
-
end: np.ndarray[float]
|
|
86
85
|
start: np.ndarray[float]
|
|
86
|
+
end: np.ndarray[float]
|
|
87
87
|
center: np.ndarray[float]
|
|
88
88
|
decay: np.ndarray[float]
|
|
89
89
|
|
|
@@ -130,7 +130,7 @@ class ScanTimingInfo:
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
@classmethod
|
|
133
|
-
def from_metadata(cls, metadata_dict: dict):
|
|
133
|
+
def from_metadata(cls, metadata_dict: dict) -> Self:
|
|
134
134
|
r"""
|
|
135
135
|
Extracts frame timing information and decay factors from a json metadata.
|
|
136
136
|
Expects that the JSON metadata has ``FrameDuration`` and ``DecayFactor`` or
|
|
@@ -155,13 +155,13 @@ class ScanTimingInfo:
|
|
|
155
155
|
"""
|
|
156
156
|
frm_dur = np.asarray(metadata_dict['FrameDuration'], float)
|
|
157
157
|
try:
|
|
158
|
-
|
|
158
|
+
frm_starts = np.asarray(metadata_dict['FrameTimesStart'], float)
|
|
159
159
|
except KeyError:
|
|
160
|
-
|
|
160
|
+
frm_starts = np.cumsum(frm_dur)-frm_dur
|
|
161
161
|
try:
|
|
162
|
-
|
|
162
|
+
frm_ends = np.asarray(metadata_dict['FrameTimesEnd'], float)
|
|
163
163
|
except KeyError:
|
|
164
|
-
|
|
164
|
+
frm_ends = frm_starts+frm_dur
|
|
165
165
|
try:
|
|
166
166
|
decay = np.asarray(metadata_dict['DecayCorrectionFactor'], float)
|
|
167
167
|
except KeyError:
|
|
@@ -178,7 +178,7 @@ class ScanTimingInfo:
|
|
|
178
178
|
decay=decay)
|
|
179
179
|
|
|
180
180
|
@classmethod
|
|
181
|
-
def from_nifti(cls, image_path: str):
|
|
181
|
+
def from_nifti(cls, image_path: str) -> Self:
|
|
182
182
|
r"""
|
|
183
183
|
Extracts frame timing information and decay factors from a NIfTI image metadata.
|
|
184
184
|
Expects that the JSON metadata file has ``FrameDuration`` and ``DecayFactor`` or
|
|
@@ -204,6 +204,49 @@ class ScanTimingInfo:
|
|
|
204
204
|
_meta_data = load_metadata_for_nifti_with_same_filename(image_path=image_path)
|
|
205
205
|
return cls.from_metadata(metadata_dict=_meta_data)
|
|
206
206
|
|
|
207
|
+
@classmethod
|
|
208
|
+
def from_start_end(cls,
|
|
209
|
+
frame_starts: np.ndarray,
|
|
210
|
+
frame_ends: np.ndarray,
|
|
211
|
+
decay_correction_factor: np.ndarray | None=None) -> Self:
|
|
212
|
+
"""Infer timing properties based on start and end time.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
frame_starts (np.ndarray): Start time of each frame.
|
|
216
|
+
frame_ends (np.ndarray): End time of each frame.
|
|
217
|
+
decay_correction_factor (np.ndarray | None): Decay correction factor, which can be
|
|
218
|
+
optionally provided based on the type of analysis being done. If None, frame decay
|
|
219
|
+
will be set to ones. Default None.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
scan_timing_info (ScanTimingInfo): ScanTimingInfo object with the correct start, end,
|
|
223
|
+
duration, midpoint, and (optionally) decay correction for each frame.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
ValueError: If frame_starts, frame_ends, and decay_correction_factor (if provided) are
|
|
227
|
+
not of identical shape.
|
|
228
|
+
|
|
229
|
+
"""
|
|
230
|
+
if frame_starts.shape != frame_ends.shape:
|
|
231
|
+
raise ValueError("frame_ends must have the same shape as frame_starts")
|
|
232
|
+
|
|
233
|
+
frame_duration = frame_ends - frame_starts
|
|
234
|
+
frame_midpoint = frame_starts + frame_duration / 2
|
|
235
|
+
frame_decay = np.ones_like(frame_starts)
|
|
236
|
+
|
|
237
|
+
if decay_correction_factor is None:
|
|
238
|
+
frame_decay = np.ones_like(frame_starts, dtype=float)
|
|
239
|
+
else:
|
|
240
|
+
frame_decay = np.asarray(decay_correction_factor, dtype=float)
|
|
241
|
+
if frame_decay.shape != frame_starts.shape:
|
|
242
|
+
raise ValueError("decay_correction_factor must have the same shape as frame_starts")
|
|
243
|
+
|
|
244
|
+
return cls(duration=frame_duration,
|
|
245
|
+
start=frame_starts,
|
|
246
|
+
end=frame_ends,
|
|
247
|
+
center=frame_midpoint,
|
|
248
|
+
decay=frame_decay)
|
|
249
|
+
|
|
207
250
|
|
|
208
251
|
def get_window_index_pairs_from_durations(frame_durations: np.ndarray, w_size: float):
|
|
209
252
|
r"""
|
|
@@ -263,25 +306,29 @@ def get_window_index_pairs_for_image(image_path: str, w_size: float):
|
|
|
263
306
|
w_size=w_size)
|
|
264
307
|
|
|
265
308
|
|
|
266
|
-
def calculate_frame_reference_time(frame_duration:
|
|
267
|
-
frame_start:
|
|
268
|
-
half_life: float) ->
|
|
309
|
+
def calculate_frame_reference_time(frame_duration: np.ndarray,
|
|
310
|
+
frame_start: np.ndarray,
|
|
311
|
+
half_life: float) -> np.ndarray:
|
|
269
312
|
r"""Compute frame reference time as the time at which the average activity occurs.
|
|
270
313
|
|
|
271
|
-
Equation comes from the `DICOM standard documentation
|
|
314
|
+
Equation comes from the `DICOM standard documentation
|
|
315
|
+
<https://dicom.innolitics.com/ciods/positron-emission-tomography-image/pet-image/00541300>`_
|
|
272
316
|
|
|
273
317
|
:math:`T_{ave}=\frac{1}{\lambda}ln\frac{\lambda T}{1-e^{-\lambda T}}`
|
|
274
318
|
|
|
275
|
-
where lambda is the decay constant, :math:`\frac{ln2}{T_{1/2}}`, :math:`T_{1/2}` is the half
|
|
319
|
+
where lambda is the decay constant, :math:`\frac{ln2}{T_{1/2}}`, :math:`T_{1/2}` is the half
|
|
320
|
+
life, and :math:`T` is the frame duration.
|
|
276
321
|
|
|
277
322
|
Args:
|
|
278
|
-
frame_duration (
|
|
279
|
-
frame_start (
|
|
280
|
-
half_life (float): Radionuclide half life
|
|
323
|
+
frame_duration (np.ndarray): Duration of each frame in seconds.
|
|
324
|
+
frame_start (np.ndarray): Start time of each frame relative to scan start, in seconds.
|
|
325
|
+
half_life (float): Radionuclide half life in seconds.
|
|
281
326
|
|
|
282
327
|
Returns:
|
|
283
|
-
|
|
328
|
+
np.ndarray: Frame reference time for each frame in the scan in seconds.
|
|
284
329
|
"""
|
|
285
|
-
decay_constant =
|
|
286
|
-
|
|
330
|
+
decay_constant = np.log(2)/half_life
|
|
331
|
+
decay_over_frame = decay_constant*frame_duration
|
|
332
|
+
reference_time_delay = np.log((decay_over_frame)/(1-np.exp(-decay_over_frame)))/decay_constant
|
|
333
|
+
frame_reference_time = frame_start + reference_time_delay
|
|
287
334
|
return frame_reference_time
|
|
@@ -75,6 +75,7 @@ class TimeActivityCurve:
|
|
|
75
75
|
return len(self.times)
|
|
76
76
|
|
|
77
77
|
def __post_init__(self):
|
|
78
|
+
self.validate_activity()
|
|
78
79
|
if self.uncertainty.size == 0:
|
|
79
80
|
self.uncertainty = np.empty_like(self.times)
|
|
80
81
|
self.uncertainty[:] = np.nan
|
|
@@ -82,6 +83,33 @@ class TimeActivityCurve:
|
|
|
82
83
|
f"TAC fields must have the same shapes.\ntimes:{self.times.shape}"
|
|
83
84
|
"activity:{self.activity.shape} uncertainty:{self.uncertainty.shape}")
|
|
84
85
|
|
|
86
|
+
def validate_activity(self):
|
|
87
|
+
"""Validates that the activity attribute is defined correctly.
|
|
88
|
+
|
|
89
|
+
`self.activity` must have the following properties:
|
|
90
|
+
1) It must exist and not be None
|
|
91
|
+
2) It must be a numpy array
|
|
92
|
+
3) It must have dtype float
|
|
93
|
+
4) It must be 1D
|
|
94
|
+
|
|
95
|
+
This function raises a ValueError if self.activity does not meet the first criteria, and
|
|
96
|
+
attempts to coerce self.activity into a 1D, numeric numpy array with dtype float if
|
|
97
|
+
criteria 2-4 are not met.
|
|
98
|
+
"""
|
|
99
|
+
if not hasattr(self, "activity") or self.activity is None:
|
|
100
|
+
raise ValueError("TimeActivityCurve.activity must be provided and not be None")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
arr = np.asarray(self.activity, dtype=float)
|
|
104
|
+
except (TypeError, ValueError) as exc:
|
|
105
|
+
error_message = "TimeActivityCurve.activity must be numeric or convertible to numeric"
|
|
106
|
+
raise TypeError(error_message) from exc
|
|
107
|
+
|
|
108
|
+
if arr.ndim != 1:
|
|
109
|
+
arr = arr.ravel()
|
|
110
|
+
|
|
111
|
+
self.activity = arr
|
|
112
|
+
|
|
85
113
|
@classmethod
|
|
86
114
|
def from_tsv(cls, filename: str):
|
|
87
115
|
"""
|
|
@@ -509,29 +537,29 @@ class TimeActivityCurve:
|
|
|
509
537
|
kind='linear',
|
|
510
538
|
fill_value='extrapolate')(tac.times)
|
|
511
539
|
return TimeActivityCurve(tac.times, shifted_vals_on_tac_times)
|
|
512
|
-
|
|
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)
|