From 9c41e80295a6a08b42dca4af950f02dab6adda29 Mon Sep 17 00:00:00 2001 From: Carlos Uribe Date: Sat, 8 Nov 2025 18:08:03 -0800 Subject: [PATCH 01/11] docs: unify tutorial assets, improve sphinx build configuration --- .readthedocs.yaml | 8 +- {pytheranostics/docs => docs}/Makefile | 0 {pytheranostics/docs => docs}/make.bat | 0 docs/source/API/modules.rst | 84 +++++++++ .../source/_static/dosimetry_workflow.png | Bin .../docs => docs}/source/_static/logo.png | Bin .../docs => docs}/source/changelog.rst | 0 {pytheranostics/docs => docs}/source/conf.py | 54 +++++- .../source/examples/data}/016/test016.dcm | Bin .../source/examples/data}/test.dcm | Bin .../source/examples/data}/test0034_2.dcm | Bin .../source/examples/data}/test016.dcm | Bin .../source/examples/data}/testimages/0034.dcm | Bin .../source/examples/data}/testimages/016.dcm | Bin .../data}/testimages/spect_counts.dcm | Bin .../data}/testimages/spect_counts_out.dcm | Bin .../extensions/sphinx_github_contributors.py | 0 .../docs => docs}/source/index.rst | 38 ++-- .../source/intro/installation.rst | 0 .../docs => docs}/source/intro/overview.rst | 0 .../Data_Ingestion_Examples.ipynb | 0 .../ROI_Mapping_Tutorial.ipynb | 0 .../tutorials/SPECT2SUV/SPECT2SUV.ipynb | 18 +- docs/source/tutorials/index.rst | 12 ++ .../source/usage/basic_usage.rst | 0 pyproject.toml | 24 +++ pytheranostics/docs/requirements.txt | 7 - pytheranostics/docs/source/API/modules.rst | 87 --------- pytheranostics/documentation/SPECT2SUV.ipynb | 170 ------------------ pytheranostics/dosimetry/organ_s_dosimetry.py | 26 +-- 30 files changed, 222 insertions(+), 306 deletions(-) rename {pytheranostics/docs => docs}/Makefile (100%) rename {pytheranostics/docs => docs}/make.bat (100%) create mode 100644 docs/source/API/modules.rst rename {pytheranostics/docs => docs}/source/_static/dosimetry_workflow.png (100%) rename {pytheranostics/docs => docs}/source/_static/logo.png (100%) rename {pytheranostics/docs => docs}/source/changelog.rst (100%) rename {pytheranostics/docs => docs}/source/conf.py (53%) rename {pytheranostics/documentation => docs/source/examples/data}/016/test016.dcm (100%) rename {pytheranostics/documentation => docs/source/examples/data}/test.dcm (100%) rename {pytheranostics/documentation => docs/source/examples/data}/test0034_2.dcm (100%) rename {pytheranostics/documentation => docs/source/examples/data}/test016.dcm (100%) rename {pytheranostics/documentation => docs/source/examples/data}/testimages/0034.dcm (100%) rename {pytheranostics/documentation => docs/source/examples/data}/testimages/016.dcm (100%) rename {pytheranostics/documentation => docs/source/examples/data}/testimages/spect_counts.dcm (100%) rename {pytheranostics/documentation => docs/source/examples/data}/testimages/spect_counts_out.dcm (100%) rename {pytheranostics/docs => docs}/source/extensions/sphinx_github_contributors.py (100%) rename {pytheranostics/docs => docs}/source/index.rst (75%) rename {pytheranostics/docs => docs}/source/intro/installation.rst (100%) rename {pytheranostics/docs => docs}/source/intro/overview.rst (100%) rename {pytheranostics/documentation => docs/source/tutorials/Data_Ingestion_Examples}/Data_Ingestion_Examples.ipynb (100%) rename {pytheranostics/documentation => docs/source/tutorials/ROI_Mapping_Tutorial}/ROI_Mapping_Tutorial.ipynb (100%) rename {pytheranostics/docs => docs}/source/tutorials/SPECT2SUV/SPECT2SUV.ipynb (87%) create mode 100644 docs/source/tutorials/index.rst rename {pytheranostics/docs => docs}/source/usage/basic_usage.rst (100%) delete mode 100644 pytheranostics/docs/requirements.txt delete mode 100644 pytheranostics/docs/source/API/modules.rst delete mode 100644 pytheranostics/documentation/SPECT2SUV.ipynb diff --git a/.readthedocs.yaml b/.readthedocs.yaml index eeb06bc..ca0cf3c 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -17,7 +17,7 @@ build: # Build documentation in the "docs/" directory with Sphinx sphinx: - configuration: pytheranostics/docs/source/conf.py + configuration: docs/source/conf.py # Optionally build your docs in additional formats such as PDF and ePub # formats: @@ -29,9 +29,7 @@ sphinx: # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - - requirements: requirements.txt - - requirements: pytheranostics/docs/requirements.txt - method: pip path: . - # - method: pip - # requirements: -r pytheranostics/docs/requirements.txt + extra_requirements: + - docs diff --git a/pytheranostics/docs/Makefile b/docs/Makefile similarity index 100% rename from pytheranostics/docs/Makefile rename to docs/Makefile diff --git a/pytheranostics/docs/make.bat b/docs/make.bat similarity index 100% rename from pytheranostics/docs/make.bat rename to docs/make.bat diff --git a/docs/source/API/modules.rst b/docs/source/API/modules.rst new file mode 100644 index 0000000..19e2f09 --- /dev/null +++ b/docs/source/API/modules.rst @@ -0,0 +1,84 @@ +DICOM Tools +=========== + +.. automodule:: pytheranostics.dicomtools.dicomtools + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.dicomtools.dicom_receiver + :members: + :undoc-members: + :show-inheritance: + + +Calibrations +============ + +.. automodule:: pytheranostics.calibrations.gamma_camera + :members: + :undoc-members: + :show-inheritance: + + +Dosimetry +========= + +.. automodule:: pytheranostics.dosimetry.base_dosimetry + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.dosimetry.bone_marrow + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.dosimetry.dosiomicsclass + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.dosimetry.dvk + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.dosimetry.image_analysis + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.dosimetry.mc + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.dosimetry.olinda + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.dosimetry.organ_s_dosimetry + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.dosimetry.voxel_s_dosimetry + :members: + :undoc-members: + :show-inheritance: + + +Fitting +======= + +.. automodule:: pytheranostics.fits.fits + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pytheranostics.fits.functions + :members: + :undoc-members: + :show-inheritance: diff --git a/pytheranostics/docs/source/_static/dosimetry_workflow.png b/docs/source/_static/dosimetry_workflow.png similarity index 100% rename from pytheranostics/docs/source/_static/dosimetry_workflow.png rename to docs/source/_static/dosimetry_workflow.png diff --git a/pytheranostics/docs/source/_static/logo.png b/docs/source/_static/logo.png similarity index 100% rename from pytheranostics/docs/source/_static/logo.png rename to docs/source/_static/logo.png diff --git a/pytheranostics/docs/source/changelog.rst b/docs/source/changelog.rst similarity index 100% rename from pytheranostics/docs/source/changelog.rst rename to docs/source/changelog.rst diff --git a/pytheranostics/docs/source/conf.py b/docs/source/conf.py similarity index 53% rename from pytheranostics/docs/source/conf.py rename to docs/source/conf.py index 186e511..735d341 100644 --- a/pytheranostics/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,6 +4,7 @@ import sys sys.path.insert(0, os.path.abspath("../..")) +sys.path.insert(0, os.path.abspath(".")) # -- Project information ----------------------------------------------------- @@ -25,10 +26,20 @@ "sphinx.ext.viewcode", "sphinx.ext.githubpages", "sphinx.ext.mathjax", + "myst_parser", + "nbsphinx", + "sphinx_copybutton", ] templates_path = ["_templates"] -exclude_patterns = [] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**/.ipynb_checkpoints"] + +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +autodoc_mock_imports = ["radiomics", "gatetools", "itk"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" @@ -52,3 +63,44 @@ napoleon_use_param = True napoleon_use_rtype = True napoleon_type_aliases = None + +nbsphinx_execute = "never" +nbsphinx_allow_errors = False +nbsphinx_codecell_lexer = "python" + +myst_enable_extensions = [ + "colon_fence", + "deflist", +] + +_CONTRIB_EXTENSION = "sphinxcontrib.contributors" +try: + __import__(_CONTRIB_EXTENSION) +except ImportError: # pragma: no cover - optional dependency + _contributors_available = False +else: + _contributors_available = True + extensions.append(_CONTRIB_EXTENSION) + + +def setup(app): + if _contributors_available: + return + + from docutils import nodes + from docutils.parsers.rst import Directive + + class _ContributorsDirective(Directive): + has_content = False + required_arguments = 1 + + def run(self): + repo = self.arguments[0] + paragraph = nodes.paragraph() + paragraph += nodes.Text( + "Install 'sphinx-contributors' to render the contributors list. " + f"In the meantime see https://github.com/{repo}/graphs/contributors." + ) + return [paragraph] + + app.add_directive("contributors", _ContributorsDirective) diff --git a/pytheranostics/documentation/016/test016.dcm b/docs/source/examples/data/016/test016.dcm similarity index 100% rename from pytheranostics/documentation/016/test016.dcm rename to docs/source/examples/data/016/test016.dcm diff --git a/pytheranostics/documentation/test.dcm b/docs/source/examples/data/test.dcm similarity index 100% rename from pytheranostics/documentation/test.dcm rename to docs/source/examples/data/test.dcm diff --git a/pytheranostics/documentation/test0034_2.dcm b/docs/source/examples/data/test0034_2.dcm similarity index 100% rename from pytheranostics/documentation/test0034_2.dcm rename to docs/source/examples/data/test0034_2.dcm diff --git a/pytheranostics/documentation/test016.dcm b/docs/source/examples/data/test016.dcm similarity index 100% rename from pytheranostics/documentation/test016.dcm rename to docs/source/examples/data/test016.dcm diff --git a/pytheranostics/documentation/testimages/0034.dcm b/docs/source/examples/data/testimages/0034.dcm similarity index 100% rename from pytheranostics/documentation/testimages/0034.dcm rename to docs/source/examples/data/testimages/0034.dcm diff --git a/pytheranostics/documentation/testimages/016.dcm b/docs/source/examples/data/testimages/016.dcm similarity index 100% rename from pytheranostics/documentation/testimages/016.dcm rename to docs/source/examples/data/testimages/016.dcm diff --git a/pytheranostics/documentation/testimages/spect_counts.dcm b/docs/source/examples/data/testimages/spect_counts.dcm similarity index 100% rename from pytheranostics/documentation/testimages/spect_counts.dcm rename to docs/source/examples/data/testimages/spect_counts.dcm diff --git a/pytheranostics/documentation/testimages/spect_counts_out.dcm b/docs/source/examples/data/testimages/spect_counts_out.dcm similarity index 100% rename from pytheranostics/documentation/testimages/spect_counts_out.dcm rename to docs/source/examples/data/testimages/spect_counts_out.dcm diff --git a/pytheranostics/docs/source/extensions/sphinx_github_contributors.py b/docs/source/extensions/sphinx_github_contributors.py similarity index 100% rename from pytheranostics/docs/source/extensions/sphinx_github_contributors.py rename to docs/source/extensions/sphinx_github_contributors.py diff --git a/pytheranostics/docs/source/index.rst b/docs/source/index.rst similarity index 75% rename from pytheranostics/docs/source/index.rst rename to docs/source/index.rst index 8a32ea6..c7199d9 100644 --- a/pytheranostics/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,20 +3,30 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to PyTheranostics's documentation! -======================================= +PyTheranostics Documentation +============================ PyTheranostics is a comprehensive Python library for nuclear medicine image processing and dosimetry calculations. It provides a complete workflow from image processing to absorbed dose calculations in target organs. .. toctree:: :maxdepth: 2 - :caption: Contents: + :caption: Getting Started - installation - quickstart - modules - api - contributing + intro/overview + intro/installation + usage/basic_usage + +.. toctree:: + :maxdepth: 1 + :caption: Tutorials + + tutorials/index + +.. toctree:: + :maxdepth: 2 + :caption: Reference + + API/modules changelog Features @@ -31,7 +41,7 @@ Features * Visualization and plotting capabilities Installation ------------ +------------ You can install PyTheranostics using pip: @@ -46,7 +56,7 @@ For development installation: pip install -e ".[dev]" Quick Start ----------- +----------- .. code-block:: python @@ -76,11 +86,9 @@ This project is licensed under the terms of the MIT license. See the `LICENSE `_ +for the up-to-date list of collaborators. .. footer:: diff --git a/pytheranostics/docs/source/intro/installation.rst b/docs/source/intro/installation.rst similarity index 100% rename from pytheranostics/docs/source/intro/installation.rst rename to docs/source/intro/installation.rst diff --git a/pytheranostics/docs/source/intro/overview.rst b/docs/source/intro/overview.rst similarity index 100% rename from pytheranostics/docs/source/intro/overview.rst rename to docs/source/intro/overview.rst diff --git a/pytheranostics/documentation/Data_Ingestion_Examples.ipynb b/docs/source/tutorials/Data_Ingestion_Examples/Data_Ingestion_Examples.ipynb similarity index 100% rename from pytheranostics/documentation/Data_Ingestion_Examples.ipynb rename to docs/source/tutorials/Data_Ingestion_Examples/Data_Ingestion_Examples.ipynb diff --git a/pytheranostics/documentation/ROI_Mapping_Tutorial.ipynb b/docs/source/tutorials/ROI_Mapping_Tutorial/ROI_Mapping_Tutorial.ipynb similarity index 100% rename from pytheranostics/documentation/ROI_Mapping_Tutorial.ipynb rename to docs/source/tutorials/ROI_Mapping_Tutorial/ROI_Mapping_Tutorial.ipynb diff --git a/pytheranostics/docs/source/tutorials/SPECT2SUV/SPECT2SUV.ipynb b/docs/source/tutorials/SPECT2SUV/SPECT2SUV.ipynb similarity index 87% rename from pytheranostics/docs/source/tutorials/SPECT2SUV/SPECT2SUV.ipynb rename to docs/source/tutorials/SPECT2SUV/SPECT2SUV.ipynb index 87195f3..478a918 100644 --- a/pytheranostics/docs/source/tutorials/SPECT2SUV/SPECT2SUV.ipynb +++ b/docs/source/tutorials/SPECT2SUV/SPECT2SUV.ipynb @@ -36,9 +36,21 @@ "metadata": {}, "outputs": [], "source": [ - "spect_counts='/mnt/c/Users/curibe/Nextcloud/BCCancer/CodeRepositories/doodle/doodle/documentation/testimages/016.dcm'\n", + "from pathlib import Path\n", "\n", - "output_path='./test016.dcm'" + "def _find_examples_dir() -> Path:\n", + " candidates = [\n", + " Path.cwd() / \"examples\" / \"data\",\n", + " Path.cwd() / \"docs\" / \"source\" / \"examples\" / \"data\",\n", + " ]\n", + " for candidate in candidates:\n", + " if candidate.exists():\n", + " return candidate\n", + " raise FileNotFoundError(\"Could not locate docs example data directory\")\n", + "\n", + "EXAMPLES_DIR = _find_examples_dir()\n", + "spect_counts = str(EXAMPLES_DIR / \"testimages\" / \"016.dcm\")\n", + "output_path = str(Path.cwd() / \"test016.dcm\")\n" ] }, { @@ -168,4 +180,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst new file mode 100644 index 0000000..58ef2a4 --- /dev/null +++ b/docs/source/tutorials/index.rst @@ -0,0 +1,12 @@ +Tutorials +========= + +Hands-on walkthroughs that demonstrate common PyTheranostics workflows. The +notebooks are rendered directly in the documentation via nbsphinx. + +.. toctree:: + :maxdepth: 1 + + SPECT2SUV/SPECT2SUV + ROI_Mapping_Tutorial/ROI_Mapping_Tutorial + Data_Ingestion_Examples/Data_Ingestion_Examples diff --git a/pytheranostics/docs/source/usage/basic_usage.rst b/docs/source/usage/basic_usage.rst similarity index 100% rename from pytheranostics/docs/source/usage/basic_usage.rst rename to docs/source/usage/basic_usage.rst diff --git a/pyproject.toml b/pyproject.toml index f93e7cc..1f47f14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,22 @@ dependencies = [ "Bug Tracker" = "https://github.com/qurit/PyTheranostics/issues" [project.optional-dependencies] +test = [ + "pytest>=7.0", + "pytest-cov>=4.0", +] +docs = [ + "sphinx>=7.0", + "sphinx-rtd-theme>=1.0", + "sphinx-autodoc-typehints>=2.0", + "nbsphinx>=0.9", + "nbconvert>=7.0", + "myst-parser>=2.0", + "sphinx-copybutton>=0.5", + "sphinx-contributors>=0.1", + "pandoc>=2.4", + "ipython>=8.0", +] dev = [ "pytest>=7.0", "pytest-cov>=4.0", @@ -50,6 +66,14 @@ dev = [ "pre-commit>=3.0.0", "sphinx>=7.0", "sphinx-rtd-theme>=1.0", + "sphinx-autodoc-typehints>=2.0", + "nbsphinx>=0.9", + "nbconvert>=7.0", + "myst-parser>=2.0", + "sphinx-copybutton>=0.5", + "sphinx-contributors>=0.1", + "pandoc>=2.4", + "ipython>=8.0", "pydocstyle>=6.0", "flake8-docstrings>=1.7" ] diff --git a/pytheranostics/docs/requirements.txt b/pytheranostics/docs/requirements.txt deleted file mode 100644 index dd5adc6..0000000 --- a/pytheranostics/docs/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -sphinx-autodoc-typehints -sphinx_rtd_theme -nbsphinx -nbconvert -pandoc -sphinx-contributors -sphinx-copybutton diff --git a/pytheranostics/docs/source/API/modules.rst b/pytheranostics/docs/source/API/modules.rst deleted file mode 100644 index 5b6cdb1..0000000 --- a/pytheranostics/docs/source/API/modules.rst +++ /dev/null @@ -1,87 +0,0 @@ -DICOM Tools -==================== - -.. autoclass:: dicomtools - :members: - :undoc-members: - .. :show-inheritance: - - -Calibrations -==================== - -.. automodule:: calibrations.gamma_camera - :members: - :undoc-members: - :show-inheritance: - -Dosimetry -==================== - -.. autoclass:: dosimetry - :members: - :undoc-members: - -.. .. automodule:: dosimetry.BaseDosimetry -.. :members: -.. :undoc-members: - - - -.. automodule:: dosimetry.BoneMarrow - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: dosimetry.dosiomicsclass - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: dosimetry.dvk - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: dosimetry.image_analysis - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: dosimetry.mc - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: dosimetry.olinda - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: dosimetry.OrganSDosimetry - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: dosimetry.patientdosimetry - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: dosimetry.VoxelSDosimetry - :members: - :undoc-members: - :show-inheritance: - -Fitting -==================== - - .. automodule:: fits.fits - :members: - :undoc-members: - :show-inheritance: - - .. automodule:: fits.functions - :members: - :undoc-members: - :show-inheritance: diff --git a/pytheranostics/documentation/SPECT2SUV.ipynb b/pytheranostics/documentation/SPECT2SUV.ipynb deleted file mode 100644 index 832972b..0000000 --- a/pytheranostics/documentation/SPECT2SUV.ipynb +++ /dev/null @@ -1,170 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "6d072b76-26c2-4789-9d4a-7be2f0170f5c", - "metadata": {}, - "outputs": [], - "source": [ - "from doodle.dicomtools.dicomtools import DicomModify\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from pydicom.dataset import Dataset\n", - "from pydicom.uid import generate_uid\n", - "\n", - "%load_ext autoreload\n", - "%autoreload 2\n", - "%matplotlib inline" - ] - }, - { - "cell_type": "markdown", - "id": "0c6f9dbd-0afa-4f6a-87ec-a81120df22ec", - "metadata": {}, - "source": [ - "### Point to the SPECT image in counts (the one that you want to make quantitative)\n", - "\n", - "### and set the output path (the location where you want the QSPECT image to be saved at the end" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "154c4542-32ab-4da3-89d1-170e10a539db", - "metadata": {}, - "outputs": [], - "source": [ - "spect_counts='/mnt/c/Users/curibe/Nextcloud/BCCancer/CodeRepositories/doodle/doodle/documentation/testimages/016.dcm'\n", - "\n", - "output_path='./test016.dcm'" - ] - }, - { - "cell_type": "markdown", - "id": "82c8ff82-32de-43f5-936a-f63597c35bc5", - "metadata": {}, - "source": [ - "### Set the calibration factor for the camera of the centre that you're using" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "14c487d1-3d64-4794-a7e2-02e0ec98ba67", - "metadata": {}, - "outputs": [], - "source": [ - "CF = 0.10800584442987242\n", - "img=DicomModify(spect_counts,CF)" - ] - }, - { - "cell_type": "markdown", - "id": "15ff709a-66de-45be-960c-c7b0ab1da03d", - "metadata": {}, - "source": [ - "### Specify the following information from the filled in form from the injection" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "3a7b4268-5548-4100-8173-95eae051ec75", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
injected_activity_MBqinjection_datetime
07395.7448952022-06-16 09:18:00
\n", - "
" - ], - "text/plain": [ - " injected_activity_MBq injection_datetime\n", - "0 7395.744895 2022-06-16 09:18:00" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "weight = 113.4\n", - "height = 1.785\n", - "injection_date = '20220616'\n", - "pre_inj_activity = 7450\n", - "pre_inj_time = '0804'\n", - "post_inj_activity = 14.4\n", - "post_inj_time = '0955'\n", - "injection_time = '0918'\n", - "\n", - "#The activity meteer scale factor is a factor to multiply activity values if the calibration setting of the dose calibrator has changed\n", - "activity_meter_scale_factor = 1\n", - "\n", - "\n", - "inj_df = img.make_bqml_suv(weight=weight,height=height,injection_date=injection_date,pre_inj_activity=pre_inj_activity,pre_inj_time=pre_inj_time,post_inj_activity=post_inj_activity,post_inj_time=post_inj_time,injection_time=injection_time,activity_meter_scale_factor=activity_meter_scale_factor)\n", - "img.ds.save_as(output_path)\n", - "inj_df" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c4cbee90-0ee5-4915-8a4b-fd875a69d804", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "DOODLE", - "language": "python", - "name": "doodle" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 0670872..60b31ba 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -136,24 +136,14 @@ def calculate_ttb(self): def prepare_data(self) -> None: """ - Prepare data for dosimetry calculations or export based on the current configuration. - - The behavior depends on the 'Level' setting: - - 1. Organ-level: - - If Output Type is 'Export': - - Generates files compatible with the selected software: - - 'Olinda' → creates a .cas file for Olinda/EXM. - - 'MirdCalc' → creates files compatible with MIRDcalc. - - If Output Type is 'Calculate': - - Performs organ-level dosimetry calculations using the specified method. - - Uses S-values from the chosen source (e.g., Olinda, MirdCalc, OpenDDose). - - Additional options like ROB, lesion dosimetry, or salivary gland handling are applied as configured. - - 2. Voxel-level: - - Prepares voxel-based dosimetry data. - - The chosen calculation method (e.g., Dose Kernel) is applied. - - Output is formatted according to the specified voxel-level output format (e.g., NIfTI). + Prepare data for dosimetry calculations or export based on the configuration. + + For organ-level workflows the method either exports data compatible with + Olinda/MIRDcalc or performs the configured calculation, sourcing S-values + from the selected tables and honoring options such as ROB, lesions, or + salivary gland handling. For voxel-level workflows it assembles the inputs + for kernel-based calculations and writes the data using the requested + voxel-level format (for example, NIfTI). """ self.results_fitting = self.results[["Volume_CT_mL", "TIA_h"]].copy() # Average Volume over time points. From ed2eafb6ca7859042d5734ae939cf2d5d1cd3012 Mon Sep 17 00:00:00 2001 From: Carlos Uribe Date: Sat, 8 Nov 2025 18:43:57 -0800 Subject: [PATCH 02/11] add tests for dosimetry/fits/imaging and set GitHub actions so tests run automatically on pushes and PRs to dev and main branches --- .github/workflows/ci.yml | 35 ++++++++++++++++ pytheranostics/imaging_tools/tools.py | 2 +- tests/conftest.py | 22 ++++++---- tests/test_dosimetry_bone_marrow.py | 26 ++++++++++++ tests/test_fits_module.py | 60 +++++++++++++++++++++++++++ tests/test_imaging_tools.py | 45 ++++++++++++++++++++ 6 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/test_dosimetry_bone_marrow.py create mode 100644 tests/test_fits_module.py create mode 100644 tests/test_imaging_tools.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..14fbd8d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: + - main + - dev + pull_request: + +jobs: + lint-and-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev,test]" + + - name: Run pre-commit + run: pre-commit run --all-files + + - name: Run pytest + run: pytest diff --git a/pytheranostics/imaging_tools/tools.py b/pytheranostics/imaging_tools/tools.py index 1f6cc47..b57dd86 100644 --- a/pytheranostics/imaging_tools/tools.py +++ b/pytheranostics/imaging_tools/tools.py @@ -92,7 +92,7 @@ def load_metadata(dir: str, modality: str) -> ImagingMetadata: f"Injected activity found in DICOM Header: {injected_activity:2.1f} MBq. Please verify." ) - except AttributeError: + except (AttributeError, IndexError): print( "Injected activity not found in DICOM header. Using default: 7400 MBq" ) diff --git a/tests/conftest.py b/tests/conftest.py index 05f7bb0..fac2793 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ """Test configuration and fixtures for PyTheranostics.""" -import os +from pathlib import Path import numpy as np import pytest @@ -29,12 +29,6 @@ def sample_image(): return np.random.rand(100, 100) -@pytest.fixture -def sample_dicom_path(): - """Return path to sample DICOM file.""" - return os.path.join(os.path.dirname(__file__), "data", "sample.dcm") - - @pytest.fixture def sample_activity(): """Create sample activity data.""" @@ -45,3 +39,17 @@ def sample_activity(): def sample_time_points(): """Create sample time points.""" return np.array([0, 1, 2, 3, 4]) + + +@pytest.fixture(scope="session") +def docs_examples_dir() -> Path: + """Return the path to the documentation example data directory.""" + return ( + Path(__file__).resolve().parent.parent / "docs" / "source" / "examples" / "data" + ) + + +@pytest.fixture(scope="session") +def spect_example_dir(docs_examples_dir: Path) -> Path: + """Directory containing sample SPECT DICOM images.""" + return docs_examples_dir / "testimages" diff --git a/tests/test_dosimetry_bone_marrow.py b/tests/test_dosimetry_bone_marrow.py new file mode 100644 index 0000000..8895aa9 --- /dev/null +++ b/tests/test_dosimetry_bone_marrow.py @@ -0,0 +1,26 @@ +"""Unit tests for bone marrow dosimetry helpers.""" + +import math + +import pytest + +from pytheranostics.dosimetry.bone_marrow import bm_scaling_factor + + +def test_bm_scaling_factor_uses_phantom_mass_by_default(): + """If no patient mass is provided, phantom data should be used.""" + result = bm_scaling_factor(gender="Female", mass_bm=None, hematocrit=None) + assert math.isclose(result, 900.0, rel_tol=1e-6) + + +@pytest.mark.parametrize( + ("gender", "mass_bm", "hematocrit", "expected"), + [ + ("Male", 1000.0, None, 1000.0), + ("Female", 900.0, 0.45, 0.19 / (1 - 0.45) * 900.0), + ], +) +def test_bm_scaling_factor_handles_custom_values(gender, mass_bm, hematocrit, expected): + """The scaling factor should respect custom masses and hematocrit.""" + result = bm_scaling_factor(gender=gender, mass_bm=mass_bm, hematocrit=hematocrit) + assert math.isclose(result, expected, rel_tol=1e-6) diff --git a/tests/test_fits_module.py b/tests/test_fits_module.py new file mode 100644 index 0000000..135a383 --- /dev/null +++ b/tests/test_fits_module.py @@ -0,0 +1,60 @@ +"""Tests for the fitting helper functions.""" + +import numpy as np +import pytest + +from pytheranostics.fits.fits import ( + calculate_r_squared, + exponential_fit_lmfit, + get_exponential, +) +from pytheranostics.fits.functions import monoexp_fun + + +def test_get_exponential_defaults(): + """Default configuration for mono-exponential fits should be stable.""" + func, params, bounds = get_exponential(order=1, param_init=None, decayconst=0.1) + assert func is monoexp_fun + assert params == (1, 1) + assert bounds[0][0] == 0 + assert pytest.approx(bounds[0][1]) == 0.1 + assert np.isinf(bounds[1]) + + +def test_calculate_r_squared_perfect_fit(): + """A perfect mono-exponential fit should have r^2 == 1.""" + x = np.linspace(0, 4, 5) + y = monoexp_fun(x, 2.0, 0.5) + r2, residuals = calculate_r_squared(x, y, (2.0, 0.5), monoexp_fun) + assert pytest.approx(r2, rel=1e-9) == 1.0 + assert np.allclose(residuals, 0.0) + + +def test_exponential_fit_lmfit_mono_handles_noise(): + """Mono-exponential fit should recover parameters from noisy data.""" + rng = np.random.default_rng(42) + x = np.linspace(0, 6, 20) + y_true = monoexp_fun(x, 5.0, 0.4) + y_noisy = y_true + rng.normal(scale=0.05, size=x.shape) + + result, fitted_model = exponential_fit_lmfit( + x_data=x, y_data=y_noisy, num_exponentials=1 + ) + + assert pytest.approx(result.params["A1"].value, rel=0.05) == 5.0 + assert pytest.approx(result.params["A2"].value, rel=0.1) == 0.4 + assert np.allclose( + fitted_model(x[:3]), + monoexp_fun(x[:3], result.params["A1"].value, result.params["A2"].value), + ) + + +def test_exponential_fit_lmfit_applies_uptake_constraint(): + """Bi-exponential fits with uptake should constrain the amplitudes.""" + x = np.linspace(0, 4, 15) + y = monoexp_fun(x, 2.0, 0.5) + monoexp_fun(x, -2.0, 1.5) + + result, _ = exponential_fit_lmfit(x, y, num_exponentials=2, with_uptake=True) + + assert result.params["B1"].expr == "-A1" + assert pytest.approx(result.params["A1"].value, rel=0.1) == 2.0 diff --git a/tests/test_imaging_tools.py b/tests/test_imaging_tools.py new file mode 100644 index 0000000..e2a55c1 --- /dev/null +++ b/tests/test_imaging_tools.py @@ -0,0 +1,45 @@ +"""Tests for imaging tools utilities.""" + +import shutil + +import numpy as np +import pytest +import SimpleITK + +from pytheranostics.imaging_tools import tools + + +def test_load_metadata_from_sample_spect_folder(spect_example_dir, tmp_path): + """Ensure metadata extraction works on bundled DICOM samples.""" + single_case_dir = tmp_path / "spect_case" + single_case_dir.mkdir() + shutil.copy(spect_example_dir / "016.dcm", single_case_dir / "case.dcm") + + meta = tools.load_metadata(str(single_case_dir), modality="Lu177_NM") + assert meta.PatientID == "PR21-CAVA-0016" + assert meta.AcquisitionDate == "20220617" + # DICOM lacks injected activity tag -> default should apply + assert meta.Injected_Activity_MBq == 7400.0 + assert meta.Radionuclide == "Lu177" + + +@pytest.mark.parametrize("is_mask", [True, False]) +def test_itk_image_from_array_preserves_metadata(is_mask): + """Array conversion should preserve spacing/origin/direction.""" + ref = SimpleITK.Image(2, 2, 2, SimpleITK.sitkFloat32) + ref.SetSpacing((2.0, 2.0, 5.0)) + ref.SetOrigin((1.0, 1.0, -3.0)) + ref.SetDirection((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) + ref.SetMetaData("0010|0010", "Test Patient") + + array = np.ones((2, 2, 2), dtype=np.uint8 if is_mask else np.float32) + image = tools.itk_image_from_array(array, ref_image=ref, is_mask=is_mask) + + assert tuple(image.GetSpacing()) == pytest.approx(ref.GetSpacing()) + assert tuple(image.GetOrigin()) == pytest.approx(ref.GetOrigin()) + assert tuple(image.GetDirection()) == tuple(ref.GetDirection()) + assert image.GetMetaData("0010|0010") == "Test Patient" + if is_mask: + assert image.GetPixelID() == SimpleITK.sitkUInt8 + else: + assert image.GetPixelID() == ref.GetPixelID() From 8d0c9d5f2d231d70bb993660ca95a1015ab9c5fd Mon Sep 17 00:00:00 2001 From: Carlos Uribe Date: Sat, 8 Nov 2025 19:17:51 -0800 Subject: [PATCH 03/11] load data via importlib.resources --- pytheranostics/data/__init__.py | 1 + pytheranostics/dosimetry/base_dosimetry.py | 26 +-- pytheranostics/dosimetry/dvk.py | 20 +- pytheranostics/dosimetry/olinda.py | 32 ++-- pytheranostics/dosimetry/organ_s_dosimetry.py | 57 ++++-- pytheranostics/dosimetry/voxel_s_dosimetry.py | 32 ++-- .../preclinical_dosimetry/biodose.py | 174 ++++++++++-------- pytheranostics/registration/phantom_to_ct.py | 28 ++- pytheranostics/shared/resources.py | 22 +++ tests/test_resource_loading.py | 19 ++ 10 files changed, 252 insertions(+), 159 deletions(-) create mode 100644 pytheranostics/data/__init__.py create mode 100644 pytheranostics/shared/resources.py create mode 100644 tests/test_resource_loading.py diff --git a/pytheranostics/data/__init__.py b/pytheranostics/data/__init__.py new file mode 100644 index 0000000..1e67676 --- /dev/null +++ b/pytheranostics/data/__init__.py @@ -0,0 +1 @@ +"""Package containing data assets distributed with PyTheranostics.""" diff --git a/pytheranostics/dosimetry/base_dosimetry.py b/pytheranostics/dosimetry/base_dosimetry.py index 3380a44..f599f86 100644 --- a/pytheranostics/dosimetry/base_dosimetry.py +++ b/pytheranostics/dosimetry/base_dosimetry.py @@ -2,7 +2,6 @@ import abc import json -from os import path from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -16,6 +15,7 @@ from pytheranostics.imaging_tools.tools import extract_masks from pytheranostics.misc_tools.tools import calculate_time_difference from pytheranostics.plots.plots import plot_tac_residuals +from pytheranostics.shared.resources import resource_path class BaseDosimetry(metaclass=abc.ABCMeta): @@ -101,10 +101,11 @@ def __init__( self.clinical_data = clinical_data - with open( - path.dirname(__file__) + "/../data/s-values/spheres.json", "r" - ) as file: - self.mass_and_s_values = json.load(file) + with resource_path( + "pytheranostics.data", "s-values/spheres.json" + ) as spheres_path: + with spheres_path.open("r", encoding="utf-8") as file: + self.mass_and_s_values = json.load(file) if ( self.clinical_data is not None @@ -322,9 +323,9 @@ def check_nm_data(self) -> Dict[str, Any]: Also verify that radionuclide data (e.g., half-life) is available in internal database. """ # Load Radionuclide data - rad_data_path = path.dirname(__file__) + "/../data/isotopes.json" - with open(rad_data_path, "r") as rad_data: - radionuclide_data = json.load(rad_data) + with resource_path("pytheranostics.data", "isotopes.json") as rad_data_path: + with rad_data_path.open("r", encoding="utf-8") as rad_data: + radionuclide_data = json.load(rad_data) if self.nm_data.meta[0].Radionuclide is None: raise ValueError("Nuclear Medicine Data missing radionuclide") @@ -742,11 +743,12 @@ def write_json_data( """ # Open empty json to load its structure: if create_new: - json_path = path.dirname(__file__) + "/../data/output.json" + with resource_path("pytheranostics.data", "output.json") as template_json: + with template_json.open("r", encoding="utf-8") as file: + data = json.load(file) else: - json_path = file_path - with open(json_path, "r") as file: - data = json.load(file) + with open(file_path, "r", encoding="utf-8") as file: + data = json.load(file) data["PatientID"] = self.config["PatientID"] data["InstitutionName"] = InstitutionName diff --git a/pytheranostics/dosimetry/dvk.py b/pytheranostics/dosimetry/dvk.py index 923fc69..96822f8 100644 --- a/pytheranostics/dosimetry/dvk.py +++ b/pytheranostics/dosimetry/dvk.py @@ -1,12 +1,12 @@ """Dose voxel kernel module for convolution-based dosimetry.""" -import os from typing import Optional import numpy from scipy import signal from pytheranostics.misc_tools.tools import hu_to_rho +from pytheranostics.shared.resources import resource_path class DoseVoxelKernel: @@ -20,22 +20,22 @@ def __init__(self, isotope: str, voxel_size_mm: float) -> None: isotope (str): The isotope name (e.g., 'Lu177'). voxel_size_mm (float): Voxel size in millimeters. """ + kernel_filename = ( + f"voxel_kernels/{isotope}-{voxel_size_mm:1.2f}-mm-mGyperMBqs-SoftICRP.img" + ) try: - self.kernel = numpy.fromfile( - os.path.dirname(__file__) - + f"/../data/voxel_kernels/{isotope}-{voxel_size_mm:1.2f}-mm-mGyperMBqs-SoftICRP.img", - dtype=numpy.float32, - ) + with resource_path("pytheranostics.data", kernel_filename) as kernel_path: + self.kernel = numpy.fromfile(kernel_path, dtype=numpy.float32) except FileNotFoundError: print( f" >> Voxel Kernel for SPECT voxel size ({voxel_size_mm:2.2f} mm) not found. Using default kernel for 4.8 mm voxels..." ) - self.kernel = numpy.fromfile( - os.path.dirname(__file__) - + f"/../data/voxel_kernels/{isotope}-4.80-mm-mGyperMBqs-SoftICRP.img", - dtype=numpy.float32, + fallback_filename = ( + f"voxel_kernels/{isotope}-4.80-mm-mGyperMBqs-SoftICRP.img" ) + with resource_path("pytheranostics.data", fallback_filename) as kernel_path: + self.kernel = numpy.fromfile(kernel_path, dtype=numpy.float32) self.kernel = self.kernel.reshape((51, 51, 51)).astype(numpy.float64) diff --git a/pytheranostics/dosimetry/olinda.py b/pytheranostics/dosimetry/olinda.py index e3c5783..7cbebc1 100644 --- a/pytheranostics/dosimetry/olinda.py +++ b/pytheranostics/dosimetry/olinda.py @@ -1,19 +1,21 @@ -from os import path -from pathlib import Path +"""Helpers for reading Olinda/EXM phantom tables shipped with PyTheranostics.""" import pandas +from pytheranostics.shared.resources import resource_path + def load_s_values(gender: str, radionuclide: str) -> pandas.DataFrame: - """Load S-values Dataframes""" - path_to_sv = Path(f"./phantomdata/{radionuclide}-{gender}-Svalues.csv") - if not path_to_sv.exists(): - raise FileExistsError( - f"S-values for {gender}, {radionuclide} not found. Please make sure" - " gender is ['Male', 'Female'] and radionuclide SymbolMass e.g., Lu177" - ) - - s_df = pandas.read_csv(path_to_sv) + """Load the S-value table for a gender/radionuclide pair.""" + relative_path = f"phantomdata/{radionuclide}-{gender}-Svalues.csv" + try: + with resource_path("pytheranostics.dosimetry", relative_path) as path_to_sv: + s_df = pandas.read_csv(path_to_sv) + except FileNotFoundError as exc: # pragma: no cover - defensive + raise FileNotFoundError( + f"S-values for {gender}, {radionuclide} not found. Ensure gender is " + "one of ['Male', 'Female'] and radionuclide uses the SymbolMass format (e.g., Lu177)." + ) from exc s_df.set_index(keys=["Target"], drop=True, inplace=True) s_df = s_df.drop(labels=["Target"], axis=1) @@ -21,9 +23,11 @@ def load_s_values(gender: str, radionuclide: str) -> pandas.DataFrame: def load_phantom_mass(gender: str, organ: str) -> float: - """Load the mass of organs in the standar ICRP Male/Female phantom""" - phantom_data_path = path.dirname(__file__) + "/phantomdata/human_phantom_masses.csv" - masses = pandas.read_csv(phantom_data_path) + """Return the ICRP phantom mass for the requested organ and gender.""" + with resource_path( + "pytheranostics.dosimetry", "phantomdata/human_phantom_masses.csv" + ) as phantom_data_path: + masses = pandas.read_csv(phantom_data_path) if organ not in masses["Organ"].to_list(): raise ValueError(f"Organ {organ} not found in phantom data.") diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 60b31ba..1e9cf31 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -15,10 +15,7 @@ from pytheranostics.dosimetry.base_dosimetry import BaseDosimetry from pytheranostics.imaging_ds.longitudinal_study import LongitudinalStudy - -parent_dir = path.dirname(path.dirname(__file__)) -SVALUES_PATH = path.join(parent_dir, "data", "s-values") -MASSES_PATH = path.join(parent_dir, "data", "ICRP_phantom_masses") +from pytheranostics.shared.resources import resource_path class OrganSDosimetry(BaseDosimetry): @@ -57,6 +54,16 @@ def check_mandatory_fields_organ(self) -> None: return None + @staticmethod + def _load_human_mass_table() -> pandas.DataFrame: + """Load the reference human phantom masses.""" + with resource_path( + "pytheranostics.preclinical_dosimetry", + "phantomdata/human_phantom_masses.csv", + ) as masses_path: + masses = pandas.read_csv(masses_path, index_col=0) + return masses + def composition_and_density_from_HU(self, density: float) -> Tuple[str, float]: """Determine composition and density for a given CT HU value.""" if density <= 100: @@ -341,12 +348,16 @@ def calculate_absorbed_dose(self) -> pandas.DataFrame: }, } - svalues_beta = self.load_svalues( - path.join(SVALUES_PATH, model_files[self.config["Gender"]]["beta"]) - ) - svalues_gamma = self.load_svalues( - path.join(SVALUES_PATH, model_files[self.config["Gender"]]["gamma"]) - ) + with resource_path( + "pytheranostics.data", + f"s-values/{model_files[self.config['Gender']]['beta']}", + ) as beta_path: + svalues_beta = self.load_svalues(beta_path) + with resource_path( + "pytheranostics.data", + f"s-values/{model_files[self.config['Gender']]['gamma']}", + ) as gamma_path: + svalues_gamma = self.load_svalues(gamma_path) print("Source organs available in the model:", svalues_beta.columns.tolist()) print("Source organs present :", self.results_fitting.index.tolist()) @@ -466,8 +477,11 @@ def redistribute_ROB_into_source_organs_missing( def apply_s_value(self, tia_df, s_values, radiation_type) -> pandas.DataFrame: """Multiply S-values by TIA to compute dose matrix for radiation type.""" # Path to organ masses - masses_path = path.join(MASSES_PATH, "ICRP_mass_male.csv") - self.organ_masses = pandas.read_csv(masses_path, index_col=0) + masses = self._load_human_mass_table() + gender = self.config.get("Gender", "Male") + if gender not in masses.columns: + raise ValueError(f"Unknown gender '{gender}' for mass table.") + self.organ_masses = masses[[gender]].rename(columns={gender: "Mass_g"}) # Handle remainder of the body # Redistribute ROB TIA into missing source organs if needed - approach consistent with MIRDcalc software @@ -596,8 +610,10 @@ def perform_mass_scaling( self, df: pandas.DataFrame, gender: str ) -> pandas.DataFrame: """Apply mass scaling to absorbed dose calculations based on patient-specific organ masses.""" - masses_path = path.join(MASSES_PATH, f"ICRP_mass_{gender.lower()}_target.csv") - model_masses_df = pandas.read_csv(masses_path, index_col=0) + masses = self._load_human_mass_table() + if gender not in masses.columns: + raise ValueError(f"Unknown gender '{gender}' for mass table.") + model_masses_df = masses[[gender]].rename(columns={gender: "Mass_g"}) print("Performing mass scaling...") @@ -636,17 +652,20 @@ def perform_mass_scaling( def create_Olinda_file(self, dirname: str, savefile: bool = False) -> None: """Create .cas file that can be exported to Olinda/EXM.""" - this_dir = path.dirname(__file__) - TEMPLATE_PATH = path.join(this_dir, "olindaTemplates") - if self.config["Gender"] == "Male": - template = pandas.read_csv(path.join(TEMPLATE_PATH, "adult_male.cas")) + template_file = "adult_male.cas" elif self.config["Gender"] == "Female": - template = pandas.read_csv(path.join(TEMPLATE_PATH, "adult_female.cas")) + template_file = "adult_female.cas" else: print( "Ensure that you correctly wrote patient gender in config file. Olinda supports: Male and Female." ) + return + + with resource_path( + "pytheranostics.dosimetry", f"olindaTemplates/{template_file}" + ) as template_path: + template = pandas.read_csv(template_path) template.columns = ["Data"] match = re.match(r"([a-zA-Z]+)([0-9]+)", self.config["Radionuclide"]) diff --git a/pytheranostics/dosimetry/voxel_s_dosimetry.py b/pytheranostics/dosimetry/voxel_s_dosimetry.py index a9d5064..3dd719e 100644 --- a/pytheranostics/dosimetry/voxel_s_dosimetry.py +++ b/pytheranostics/dosimetry/voxel_s_dosimetry.py @@ -14,6 +14,7 @@ from pytheranostics.fits.fits import get_exponential from pytheranostics.imaging_ds.longitudinal_study import LongitudinalStudy from pytheranostics.imaging_tools.tools import itk_image_from_array, resample_to_target +from pytheranostics.shared.resources import resource_path class VoxelSDosimetry(BaseDosimetry): @@ -173,14 +174,11 @@ def run_MC(self) -> None: # TODO: finish the code!!!!! # ============================================================================= n_primaries_per_mac = int(n_primaries / n_cpu) - file_path = os.path.join( - os.path.dirname(__file__), "../data/monte_carlo/main_template.mac" - ) - - mac_file = numpy.fromfile(file_path, dtype=numpy.float32) - - with open(file_path, "r") as mac_file: - filedata = mac_file.read() + with resource_path( + "pytheranostics.data", "monte_carlo/main_template.mac" + ) as template_path: + with template_path.open("r", encoding="utf-8") as mac_file: + filedata = mac_file.read() for i in range(0, n_cpu): new_mac = filedata @@ -198,17 +196,13 @@ def run_MC(self) -> None: # TODO: finish the code!!!!! os.makedirs(os.path.join(output_dir, "data"), exist_ok=True) os.makedirs(os.path.join(output_dir, "output"), exist_ok=True) - folder_path = os.path.join( - os.path.dirname(__file__), "../data/monte_carlo/data" - ) - # Copy files from the source directory to the destination directory - for file_name in os.listdir(folder_path): - full_file_name = os.path.join(folder_path, file_name) - if os.path.isfile(full_file_name): - shutil.copy(full_file_name, os.path.join(output_dir, "data")) - - # List the files in the destination directory to confirm the copy operation - os.listdir(os.path.join(output_dir, "data")) + with resource_path("pytheranostics.data", "monte_carlo/data") as folder_path: + for entry in folder_path.iterdir(): + if entry.is_file(): + shutil.copy( + entry, + os.path.join(output_dir, "data", entry.name), + ) # TODO: Below is still work in progress diff --git a/pytheranostics/preclinical_dosimetry/biodose.py b/pytheranostics/preclinical_dosimetry/biodose.py index 1edcede..f5dd9f1 100644 --- a/pytheranostics/preclinical_dosimetry/biodose.py +++ b/pytheranostics/preclinical_dosimetry/biodose.py @@ -1,3 +1,5 @@ +"""Preclinical biodistribution analysis helpers for BioDose workflows.""" + import datetime from copy import deepcopy from os import makedirs, path @@ -14,25 +16,24 @@ monoexp_fun, ) from pytheranostics.plots.plots import plot_tac_residuals +from pytheranostics.shared.resources import resource_path + + +def _preclinical_resource(relative_path: str): + return resource_path("pytheranostics.preclinical_dosimetry", relative_path) + -this_dir = path.dirname(__file__) -parent_dir = path.dirname(this_dir) -TEMPLATE_PATH = path.join(this_dir, "olindaTemplates") -PHANTOM_PATH = path.join( - this_dir, "phantomdata" -) # These variables use only lowercases so "PhantomData" is in lowercase -SVALUES_PATH = path.join(parent_dir, "data", "s-values") +def _data_resource(relative_path: str): + return resource_path("pytheranostics.data", relative_path) class BioDose: - """ - This is a class for the analysis of biodistribution data in preclinical experiments - """ + """Analyze biodistribution data gathered from preclinical experiments.""" def __init__( self, isotope, half_life, phantom, mouse_mass, sex, uptake=None, timepoints=None ): - "half_life in hours" + """Initialize the biodistribution analysis (half_life expressed in hours).""" if uptake is None: uptake = [] if timepoints is None: @@ -49,7 +50,7 @@ def __init__( self.wb_m = int(self.mouse_mass[:-1]) def read_biodi(self, biodi_file): - """This method reads a biodi file and sets the data as a pandas dataframe in self.biodi""" + """Read a biodistribution CSV and populate ``self.biodi``.""" print( "Reading biodistribution information from the file: {}".format(biodi_file) ) @@ -102,9 +103,7 @@ def read_biodi(self, biodi_file): print("Decayed biodistribution stored in self.biodi") def initialize_results_df(self): - """ - This method initializes the results dataframe with the organ list and columns for the different fits. - """ + """Initialize the results DataFrame with the expected columns.""" columns = [ "Mono-Exponential", "Bi-Exponential", @@ -128,9 +127,7 @@ def update_fit_results( area_bi=None, area_uptake=None, ): - """ - This method updates the fit results for a given organ. - """ + """Update the stored fit parameters and AUC metrics for one organ.""" area_mono_val = ( area_mono[0] if isinstance(area_mono, (tuple, list)) else area_mono ) @@ -180,10 +177,7 @@ def curve_fits( append_zero=True, tps_to_skip_fit=0, ): - """ - This method fits the curves using lmfit and stores the results in self.fit_results. - The results can be seen in self.area organized in a pandas dataframe. - """ + """Fit exponential models (with optional uptake) using lmfit.""" decayconst = log(2) / self.half_life if organlist is None: @@ -359,7 +353,7 @@ def curve_fits( print(f"Error creating fitting parameters DataFrame: {e}") def num_decays(self, fit_accepted): - """Sets the number of decays in each of the organ based on the accepted fit (e.g. exponential or bi-exponential)""" + """Compute organ decays based on the accepted fit type.""" self.disintegrations = pd.DataFrame(index=self.biodi.index, columns=["%ID/g*h"]) self.fit_accepted = fit_accepted @@ -397,6 +391,7 @@ def num_decays(self, fit_accepted): self.disintegrations.loc["Remainder Body"] = np.nan def calculate_tumor_sink_effect(self): + """Compute the tumor sink effect factor for downstream corrections.""" tumor_value = self.disintegrations["h"]["Tumor"] wb_value = self.disintegrations["h"].sum() @@ -407,6 +402,7 @@ def calculate_tumor_sink_effect(self): print(f"Tumor sink effect: {self.tumor_sink_effect_factor}") def tumor_sink_effect_correction(self, df): + """Apply the tumor sink effect factor to a biodistribution DataFrame.""" df_corrected = df.copy() for organ in df_corrected.columns: @@ -416,11 +412,12 @@ def tumor_sink_effect_correction(self, df): return df_corrected def phantom_data(self): - print(PHANTOM_PATH) + """Load the reference phantom masses and reconcile them with biodistribution organs.""" if "mouse" in self.phantom.lower(): - self.phantom_mass = pd.read_csv( - path.join(PHANTOM_PATH, "mouse_phantom_masses.csv") - ) # TODO: CHANGE PATH + with _preclinical_resource( + "phantomdata/mouse_phantom_masses.csv" + ) as phantom_path: + self.phantom_mass = pd.read_csv(phantom_path) # elif 'human' in self.phantom.lower(): # self.phantom_mass = pd.read_csv(path.join(PHANTOM_PATH,'human_phantom_masses.csv')) self.phantom_mass.set_index("Organ", inplace=True) @@ -463,7 +460,7 @@ def phantom_data(self): ) def remainder_body_uptake(self, tumor_name=None): - + """Redistribute activity from organs that are not modeled in the phantom.""" print("At this point we are ignoring the tumor") if tumor_name: self.not_inphantom_notumor = [ @@ -477,14 +474,16 @@ def remainder_body_uptake(self, tumor_name=None): # These organs that are not modelled in the phantom are now going to be scaled using mass information from the literature: if "mouse" in self.phantom.lower(): - self.literature_mass = pd.read_csv( - path.join(PHANTOM_PATH, "mouse_notinphantom_masses.csv") - ) # TODO: CHANGE PATH + with _preclinical_resource( + "phantomdata/mouse_notinphantom_masses.csv" + ) as lit_path: + self.literature_mass = pd.read_csv(lit_path) elif "human" in self.phantom.lower(): - self.literature_mass = pd.read_csv( - path.join(PHANTOM_PATH, "human_notinphantom_masses.csv") - ) # TODO: CHANGE PATH + with _preclinical_resource( + "phantomdata/human_notinphantom_masses.csv" + ) as lit_path: + self.literature_mass = pd.read_csv(lit_path) print(self.phantom.lower()) print(self.literature_mass) @@ -585,25 +584,29 @@ def remainder_body_uptake(self, tumor_name=None): ) # Only organs that are in the phantom will be kept in the disintegrations dataframe and passed to olinda def not_inphantom_notumor_fun(self): + """Drop leftover organs that the phantom model does not include.""" self.disintegrations.drop(self.not_inphantom_notumor, inplace=True) def add_tumor_mass(self, tumor_name, tumor_mass): - self.phantom_mass.loc[tumor_name] = ( - tumor_mass # grams Provided by average of biodi from Etienne - ) + """Register a tumor entry with the provided mass (grams).""" + self.phantom_mass.loc[tumor_name] = tumor_mass def calculate_absorbed_dose(self, model, disintegrations): """ - Calculate absorbed dose per target organ based on selected model and disintegration data. - - Args: - model (str): One of 'mouse25g', 'mouse30g', or 'mouse35g' - disintegrations (pd.DataFrame): DataFrame with source organ disintegrations in hours - - Returns: - pd.DataFrame: Absorbed dose in mGy and Gy for each target organ + Calculate absorbed dose per target organ based on the selected model. + + Parameters + ---------- + model : str + One of ``mouse25g``, ``mouse30g``, ``mouse35g``, ``Female``, or ``Male``. + disintegrations : pandas.DataFrame + Source-organ disintegrations expressed in hours. + + Returns + ------- + pandas.DataFrame + Absorbed dose in mGy and Gy for each target organ. """ - disintegrations = disintegrations.copy() model_files = { @@ -619,8 +622,8 @@ def calculate_absorbed_dose(self, model, disintegrations): f"Invalid model '{model}'. Expected one of: {list(model_files.keys())}" ) - svalues_path = path.join(SVALUES_PATH, model_files[model]) - svalues_df = pd.read_csv(svalues_path, index_col=0) # S-values in mSv/MBq-s + with _data_resource(f"s-values/{model_files[model]}") as svalues_path: + svalues_df = pd.read_csv(svalues_path, index_col=0) # S-values in mSv/MBq-s print(f"Loaded S-values from: {svalues_path}") print("Target organs (S-value index):", svalues_df.index.tolist()) @@ -675,16 +678,20 @@ def calculate_absorbed_dose(self, model, disintegrations): def apply_s_value(self, tia_df, s_values): """ - Multiply S-values by TIA to compute dose matrix. - - Args: - tia_df (pd.DataFrame): Disintegration times in seconds - s_values (pd.DataFrame): S-values table (target organs as index, source organs as columns) - - Returns: - pd.DataFrame: Dose matrix (target organ x source organ) + Multiply S-values by TIA to compute a dose matrix. + + Parameters + ---------- + tia_df : pandas.DataFrame + Disintegration times in seconds. + s_values : pandas.DataFrame + S-value table with target organs as rows and source organs as columns. + + Returns + ------- + pandas.DataFrame + Dose matrix indexed by target organ. """ - common_source_organs = tia_df.index.intersection(s_values.columns) print(f"{len(common_source_organs)} source organs: {common_source_organs}") @@ -708,11 +715,14 @@ def create_mousecase( savefile=False, dirname="./", ): - """This function creates a pandas dataframe that looks exactly as the case files generated by OLINDA for the g mouse. - The result can be viewed under self.mousecase, and the pandas methods can be used to finally save it if wanted. + """ + Create a pandas representation of the mouse case file used by OLINDA. + + The result is stored in ``self.mousecase`` and can optionally be persisted. """ filename = self.phantom.lower() + ".cas" - template = pd.read_csv(path.join(TEMPLATE_PATH, filename)) + with _preclinical_resource(f"olindaTemplates/{filename}") as template_path: + template = pd.read_csv(template_path) template.columns = ["Data"] # modify the isotope in the template @@ -779,6 +789,7 @@ def create_mousecase( print(f"The case file {filename} has been saved in\n{format(dirname)}") def rename_organ(self, oldname, newname): + """Rename an organ across biodistribution and disintegration tables.""" ind_list = self.disintegrations_all_organs.index.tolist() ind_pos = ind_list.index(oldname) ind_list[ind_pos] = newname @@ -790,6 +801,7 @@ def rename_organ(self, oldname, newname): self.biodi.index = ind_list def create_human(self, tumor_name=None): + """Convert the current biodistribution into the human phantom domain.""" # We are mostly using the disintegrations_all_organs dataframe, but we adjust the biodi dataframe as well to match the human phantom structure human = deepcopy(self) human.phantom = "AdultHuman" @@ -842,15 +854,17 @@ def create_human(self, tumor_name=None): "Tumor", axis=0 ) - human.phantom_mass = pd.read_csv( - path.join(PHANTOM_PATH, "human_phantom_masses.csv") - ) + with _preclinical_resource( + "phantomdata/human_phantom_masses.csv" + ) as human_mass_path: + human.phantom_mass = pd.read_csv(human_mass_path) human.phantom_mass.set_index("Organ", inplace=True) human.phantom_mass.sort_index(inplace=True) - human.literature_mass = pd.read_csv( - path.join(PHANTOM_PATH, "human_notinphantom_masses.csv") - ) + with _preclinical_resource( + "phantomdata/human_notinphantom_masses.csv" + ) as human_lit_path: + human.literature_mass = pd.read_csv(human_lit_path) human.literature_mass.set_index("Organ", inplace=True) human.disintegrations_all_organs.sort_index(inplace=True) @@ -927,9 +941,9 @@ def create_human(self, tumor_name=None): return human def apply_relative_mass_scaling(self, mouse_mass=25): - rMSF_data = pd.read_csv( - path.join(PHANTOM_PATH, "rMSF_factor.csv"), index_col=0 - ) # TODO: CHANGE PATH + """Apply relative mass scaling factors to mouse disintegrations.""" + with _preclinical_resource("phantomdata/rMSF_factor.csv") as rmsf_path: + rMSF_data = pd.read_csv(rmsf_path, index_col=0) female_mass_sum = rMSF_data.loc[self.not_inphantom_notumor, "Female"].sum() male_mass_sum = rMSF_data.loc[self.not_inphantom_notumor, "Male"].sum() @@ -964,14 +978,18 @@ def apply_relative_mass_scaling(self, mouse_mass=25): ] *= remainder_correction_male def create_humancase(self, df, method, savefile=False, dirname="./"): - """This function creates a pandas dataframe that looks exactly as the case files generated by OLINDA for the human. - The result can be viewed under self.humancas, and the pandas methods can be used to finally save it if wanted. """ + Create a pandas representation of the human case file used by OLINDA. - if self.sex == "Male": - template = pd.read_csv(path.join(TEMPLATE_PATH, "adult_male.cas")) - else: - template = pd.read_csv(path.join(TEMPLATE_PATH, "adult_female.cas")) + The result is stored in ``self.humancas`` and can optionally be saved. + """ + template_filename = ( + "adult_male.cas" if self.sex == "Male" else "adult_female.cas" + ) + with _preclinical_resource( + f"olindaTemplates/{template_filename}" + ) as template_path: + template = pd.read_csv(template_path) template.columns = ["Data"] @@ -1035,7 +1053,7 @@ def create_humancase(self, df, method, savefile=False, dirname="./"): ) def scale_biexponential_tiac(self, row, biol_lambda_SF=0.25): - + """Scale biexponential fits to enforce biological clearance constraints.""" Cm_organ_t0_1 = row["bi_exp1:%ID"] / 100 Cm_organ_t0_2 = row["bi_exp2:%ID"] / 100 @@ -1076,7 +1094,7 @@ def scale_biexponential_tiac(self, row, biol_lambda_SF=0.25): return TIAC_h_bi_male, TIAC_h_bi_female def scale_monoexponential_tiac(self, row, biol_lambda_SF=0.25): - + """Scale monoexponential fits to enforce biological clearance constraints.""" lambda_effective = row["mono_exp:lambda_effective_1/h"] lambda_physical = log(2) / self.half_life # 1/h @@ -1099,7 +1117,7 @@ def scale_monoexponential_tiac(self, row, biol_lambda_SF=0.25): return TIAC_h_mono_male, TIAC_h_mono_female def lambda_biological_scaling(self, biol_lambda_SF=0.25, tumor_name=None): - + """Apply biological lambda scaling to the stored fits and return a new BioDose instance.""" print("At this point we are ignoring the tumor") if tumor_name: self.not_inphantom_notumor = [ diff --git a/pytheranostics/registration/phantom_to_ct.py b/pytheranostics/registration/phantom_to_ct.py index c49b975..7766fb6 100644 --- a/pytheranostics/registration/phantom_to_ct.py +++ b/pytheranostics/registration/phantom_to_ct.py @@ -11,6 +11,7 @@ from SimpleITK.SimpleITK import Transform from pytheranostics.registration.demons import multiscale_demons +from pytheranostics.shared.resources import resource_path class PhantomToCTBoneReg: @@ -32,7 +33,7 @@ class PhantomToCTBoneReg: def __init__( self, CT: SimpleITK.Image, - phantom_skeleton_path: Path = Path("../data/phantom/skeleton/Skeleton.nii.gz"), + phantom_skeleton_path: Optional[Path] = None, verbose: bool = False, ) -> None: """Initialize the Phantom-to-CT registration helper. @@ -42,13 +43,19 @@ def __init__( CT : SimpleITK.Image The reference patient CT image. phantom_skeleton_path : Path, optional - Path to the XCAT phantom skeleton image (NIfTI), by default - "../data/phantom/skeleton/Skeleton.nii.gz". + Path to the XCAT phantom skeleton image (NIfTI). If omitted, the + bundled phantom skeleton distributed with PyTheranostics is used. verbose : bool, optional Enable verbose logging during registration, by default False. """ self.CT = SimpleITK.Image(CT) # Make a Copy. - self.Phantom = SimpleITK.ReadImage(fileName=phantom_skeleton_path) + if phantom_skeleton_path is None: + with resource_path( + "pytheranostics.data", "phantom/skeleton/Skeleton.nii.gz" + ) as skeleton_path: + self.Phantom = SimpleITK.ReadImage(fileName=str(skeleton_path)) + else: + self.Phantom = SimpleITK.ReadImage(fileName=str(phantom_skeleton_path)) # Set Origin for Phantom to that of reference CT. self.Phantom.SetOrigin(self.CT.GetOrigin()) @@ -267,7 +274,7 @@ def register( def register_mask( self, fixed_image: SimpleITK.Image, - mask_path: Path = Path("../data/phantom/bone_marrow/Marrow.nii.gz"), + mask_path: Optional[Path] = None, ) -> SimpleITK.Image: """Register a phantom mask (e.g., bone marrow) to patient CT. @@ -276,14 +283,21 @@ def register_mask( fixed_image : SimpleITK.Image The reference CT image. mask_path : Path, optional - Path to the phantom mask file, by default "../data/phantom/bone_marrow/Marrow.nii.gz" + Path to the phantom mask file. If omitted, the packaged bone marrow + mask is used. Returns ------- SimpleITK.Image The registered mask in CT space. """ - mask_image = SimpleITK.ReadImage(fileName=mask_path) + if mask_path is None: + with resource_path( + "pytheranostics.data", "phantom/bone_marrow/Marrow.nii.gz" + ) as default_mask: + mask_image = SimpleITK.ReadImage(fileName=str(default_mask)) + else: + mask_image = SimpleITK.ReadImage(fileName=str(mask_path)) mask_image.SetOrigin(fixed_image.GetOrigin()) mask_image = SimpleITK.Cast(mask_image, SimpleITK.sitkFloat32) diff --git a/pytheranostics/shared/resources.py b/pytheranostics/shared/resources.py new file mode 100644 index 0000000..0f37ea5 --- /dev/null +++ b/pytheranostics/shared/resources.py @@ -0,0 +1,22 @@ +"""Utility helpers for accessing package data via importlib.resources.""" + +from __future__ import annotations + +from contextlib import contextmanager +from importlib import resources +from pathlib import Path +from typing import Iterator + + +@contextmanager +def resource_path(package: str, relative_path: str) -> Iterator[Path]: + """Yield a filesystem path to a bundled resource. + + The helper works for both files and directories and hides the boilerplate + of using ``importlib.resources.as_file``. It ensures compatibility when the + package is installed as a wheel/zip where resources need to be extracted to + a temporary location before accessing them by path. + """ + resource = resources.files(package).joinpath(*relative_path.split("/")) + with resources.as_file(resource) as path_obj: + yield Path(path_obj) diff --git a/tests/test_resource_loading.py b/tests/test_resource_loading.py new file mode 100644 index 0000000..57ec9f0 --- /dev/null +++ b/tests/test_resource_loading.py @@ -0,0 +1,19 @@ +"""Tests for data access via importlib.resources-based helpers.""" + +import numpy as np + +from pytheranostics.dosimetry.dvk import DoseVoxelKernel +from pytheranostics.dosimetry.olinda import load_phantom_mass + + +def test_load_phantom_mass_returns_expected_value(): + """The packaged phantom mass table should include standard organs.""" + liver_mass = load_phantom_mass(gender="Male", organ="Liver") + assert liver_mass == 1800 # matches bundled phantom data + + +def test_dose_voxel_kernel_falls_back_to_packaged_kernel(): + """DoseVoxelKernel should load the packaged default kernel if the exact voxel size is missing.""" + kernel = DoseVoxelKernel(isotope="Lu177", voxel_size_mm=5.5) + assert kernel.kernel.shape == (51, 51, 51) + assert kernel.kernel.dtype == np.float64 From f4249c15506fe04a45d4c360f49090ef7a9009a8 Mon Sep 17 00:00:00 2001 From: Carlos Uribe Date: Sat, 8 Nov 2025 19:31:09 -0800 Subject: [PATCH 04/11] centralize dosimetry and preclinical data under the pytheranostics.data to stay consistent --- pyproject.toml | 1 + .../dosimetry/olindaTemplates/adult_female.cas | 0 .../dosimetry/olindaTemplates/adult_male.cas | 0 .../dosimetry/phantomdata/Lu177-Female-Svalues.csv | 0 .../dosimetry/phantomdata/Lu177-Male-Svalues.csv | 0 .../dosimetry/phantomdata/PhantomMasses.xlsx | Bin .../phantomdata/human_notinphantom_masses.csv | 0 .../dosimetry/phantomdata/human_phantom_masses.csv | 0 .../preclinical}/mouse25g.cas | 0 .../preclinical}/olindaTemplates/adult_female.cas | 0 .../preclinical}/olindaTemplates/adult_male.cas | 0 .../preclinical}/olindaTemplates/mouse25g.cas | 0 .../phantomdata/Organ_weights_mice.xlsx | Bin .../preclinical}/phantomdata/PhantomMasses.xlsx | Bin .../phantomdata/human_notinphantom_masses.csv | 0 .../phantomdata/human_phantom_masses.csv | 0 .../phantomdata/mouse_notinphantom_masses.csv | 0 .../phantomdata/mouse_phantom_masses.csv | 0 .../preclinical}/phantomdata/rMSF_factor.csv | 0 pytheranostics/dosimetry/olinda.py | 6 +++--- pytheranostics/dosimetry/organ_s_dosimetry.py | 5 ++--- pytheranostics/preclinical_dosimetry/biodose.py | 3 ++- 22 files changed, 8 insertions(+), 7 deletions(-) rename pytheranostics/{ => data}/dosimetry/olindaTemplates/adult_female.cas (100%) rename pytheranostics/{ => data}/dosimetry/olindaTemplates/adult_male.cas (100%) rename pytheranostics/{ => data}/dosimetry/phantomdata/Lu177-Female-Svalues.csv (100%) rename pytheranostics/{ => data}/dosimetry/phantomdata/Lu177-Male-Svalues.csv (100%) rename pytheranostics/{ => data}/dosimetry/phantomdata/PhantomMasses.xlsx (100%) rename pytheranostics/{ => data}/dosimetry/phantomdata/human_notinphantom_masses.csv (100%) rename pytheranostics/{ => data}/dosimetry/phantomdata/human_phantom_masses.csv (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/mouse25g.cas (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/olindaTemplates/adult_female.cas (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/olindaTemplates/adult_male.cas (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/olindaTemplates/mouse25g.cas (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/phantomdata/Organ_weights_mice.xlsx (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/phantomdata/PhantomMasses.xlsx (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/phantomdata/human_notinphantom_masses.csv (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/phantomdata/human_phantom_masses.csv (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/phantomdata/mouse_notinphantom_masses.csv (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/phantomdata/mouse_phantom_masses.csv (100%) rename pytheranostics/{preclinical_dosimetry => data/preclinical}/phantomdata/rMSF_factor.csv (100%) diff --git a/pyproject.toml b/pyproject.toml index 1f47f14..c3cdcfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ packages = [ "pytheranostics.calibrations", "pytheranostics.dicomtools", "pytheranostics.dosimetry", + "pytheranostics.data", "pytheranostics.fits", "pytheranostics.plots", "pytheranostics.qc", diff --git a/pytheranostics/dosimetry/olindaTemplates/adult_female.cas b/pytheranostics/data/dosimetry/olindaTemplates/adult_female.cas similarity index 100% rename from pytheranostics/dosimetry/olindaTemplates/adult_female.cas rename to pytheranostics/data/dosimetry/olindaTemplates/adult_female.cas diff --git a/pytheranostics/dosimetry/olindaTemplates/adult_male.cas b/pytheranostics/data/dosimetry/olindaTemplates/adult_male.cas similarity index 100% rename from pytheranostics/dosimetry/olindaTemplates/adult_male.cas rename to pytheranostics/data/dosimetry/olindaTemplates/adult_male.cas diff --git a/pytheranostics/dosimetry/phantomdata/Lu177-Female-Svalues.csv b/pytheranostics/data/dosimetry/phantomdata/Lu177-Female-Svalues.csv similarity index 100% rename from pytheranostics/dosimetry/phantomdata/Lu177-Female-Svalues.csv rename to pytheranostics/data/dosimetry/phantomdata/Lu177-Female-Svalues.csv diff --git a/pytheranostics/dosimetry/phantomdata/Lu177-Male-Svalues.csv b/pytheranostics/data/dosimetry/phantomdata/Lu177-Male-Svalues.csv similarity index 100% rename from pytheranostics/dosimetry/phantomdata/Lu177-Male-Svalues.csv rename to pytheranostics/data/dosimetry/phantomdata/Lu177-Male-Svalues.csv diff --git a/pytheranostics/dosimetry/phantomdata/PhantomMasses.xlsx b/pytheranostics/data/dosimetry/phantomdata/PhantomMasses.xlsx similarity index 100% rename from pytheranostics/dosimetry/phantomdata/PhantomMasses.xlsx rename to pytheranostics/data/dosimetry/phantomdata/PhantomMasses.xlsx diff --git a/pytheranostics/dosimetry/phantomdata/human_notinphantom_masses.csv b/pytheranostics/data/dosimetry/phantomdata/human_notinphantom_masses.csv similarity index 100% rename from pytheranostics/dosimetry/phantomdata/human_notinphantom_masses.csv rename to pytheranostics/data/dosimetry/phantomdata/human_notinphantom_masses.csv diff --git a/pytheranostics/dosimetry/phantomdata/human_phantom_masses.csv b/pytheranostics/data/dosimetry/phantomdata/human_phantom_masses.csv similarity index 100% rename from pytheranostics/dosimetry/phantomdata/human_phantom_masses.csv rename to pytheranostics/data/dosimetry/phantomdata/human_phantom_masses.csv diff --git a/pytheranostics/preclinical_dosimetry/mouse25g.cas b/pytheranostics/data/preclinical/mouse25g.cas similarity index 100% rename from pytheranostics/preclinical_dosimetry/mouse25g.cas rename to pytheranostics/data/preclinical/mouse25g.cas diff --git a/pytheranostics/preclinical_dosimetry/olindaTemplates/adult_female.cas b/pytheranostics/data/preclinical/olindaTemplates/adult_female.cas similarity index 100% rename from pytheranostics/preclinical_dosimetry/olindaTemplates/adult_female.cas rename to pytheranostics/data/preclinical/olindaTemplates/adult_female.cas diff --git a/pytheranostics/preclinical_dosimetry/olindaTemplates/adult_male.cas b/pytheranostics/data/preclinical/olindaTemplates/adult_male.cas similarity index 100% rename from pytheranostics/preclinical_dosimetry/olindaTemplates/adult_male.cas rename to pytheranostics/data/preclinical/olindaTemplates/adult_male.cas diff --git a/pytheranostics/preclinical_dosimetry/olindaTemplates/mouse25g.cas b/pytheranostics/data/preclinical/olindaTemplates/mouse25g.cas similarity index 100% rename from pytheranostics/preclinical_dosimetry/olindaTemplates/mouse25g.cas rename to pytheranostics/data/preclinical/olindaTemplates/mouse25g.cas diff --git a/pytheranostics/preclinical_dosimetry/phantomdata/Organ_weights_mice.xlsx b/pytheranostics/data/preclinical/phantomdata/Organ_weights_mice.xlsx similarity index 100% rename from pytheranostics/preclinical_dosimetry/phantomdata/Organ_weights_mice.xlsx rename to pytheranostics/data/preclinical/phantomdata/Organ_weights_mice.xlsx diff --git a/pytheranostics/preclinical_dosimetry/phantomdata/PhantomMasses.xlsx b/pytheranostics/data/preclinical/phantomdata/PhantomMasses.xlsx similarity index 100% rename from pytheranostics/preclinical_dosimetry/phantomdata/PhantomMasses.xlsx rename to pytheranostics/data/preclinical/phantomdata/PhantomMasses.xlsx diff --git a/pytheranostics/preclinical_dosimetry/phantomdata/human_notinphantom_masses.csv b/pytheranostics/data/preclinical/phantomdata/human_notinphantom_masses.csv similarity index 100% rename from pytheranostics/preclinical_dosimetry/phantomdata/human_notinphantom_masses.csv rename to pytheranostics/data/preclinical/phantomdata/human_notinphantom_masses.csv diff --git a/pytheranostics/preclinical_dosimetry/phantomdata/human_phantom_masses.csv b/pytheranostics/data/preclinical/phantomdata/human_phantom_masses.csv similarity index 100% rename from pytheranostics/preclinical_dosimetry/phantomdata/human_phantom_masses.csv rename to pytheranostics/data/preclinical/phantomdata/human_phantom_masses.csv diff --git a/pytheranostics/preclinical_dosimetry/phantomdata/mouse_notinphantom_masses.csv b/pytheranostics/data/preclinical/phantomdata/mouse_notinphantom_masses.csv similarity index 100% rename from pytheranostics/preclinical_dosimetry/phantomdata/mouse_notinphantom_masses.csv rename to pytheranostics/data/preclinical/phantomdata/mouse_notinphantom_masses.csv diff --git a/pytheranostics/preclinical_dosimetry/phantomdata/mouse_phantom_masses.csv b/pytheranostics/data/preclinical/phantomdata/mouse_phantom_masses.csv similarity index 100% rename from pytheranostics/preclinical_dosimetry/phantomdata/mouse_phantom_masses.csv rename to pytheranostics/data/preclinical/phantomdata/mouse_phantom_masses.csv diff --git a/pytheranostics/preclinical_dosimetry/phantomdata/rMSF_factor.csv b/pytheranostics/data/preclinical/phantomdata/rMSF_factor.csv similarity index 100% rename from pytheranostics/preclinical_dosimetry/phantomdata/rMSF_factor.csv rename to pytheranostics/data/preclinical/phantomdata/rMSF_factor.csv diff --git a/pytheranostics/dosimetry/olinda.py b/pytheranostics/dosimetry/olinda.py index 7cbebc1..063920d 100644 --- a/pytheranostics/dosimetry/olinda.py +++ b/pytheranostics/dosimetry/olinda.py @@ -7,9 +7,9 @@ def load_s_values(gender: str, radionuclide: str) -> pandas.DataFrame: """Load the S-value table for a gender/radionuclide pair.""" - relative_path = f"phantomdata/{radionuclide}-{gender}-Svalues.csv" + relative_path = f"dosimetry/phantomdata/{radionuclide}-{gender}-Svalues.csv" try: - with resource_path("pytheranostics.dosimetry", relative_path) as path_to_sv: + with resource_path("pytheranostics.data", relative_path) as path_to_sv: s_df = pandas.read_csv(path_to_sv) except FileNotFoundError as exc: # pragma: no cover - defensive raise FileNotFoundError( @@ -25,7 +25,7 @@ def load_s_values(gender: str, radionuclide: str) -> pandas.DataFrame: def load_phantom_mass(gender: str, organ: str) -> float: """Return the ICRP phantom mass for the requested organ and gender.""" with resource_path( - "pytheranostics.dosimetry", "phantomdata/human_phantom_masses.csv" + "pytheranostics.data", "dosimetry/phantomdata/human_phantom_masses.csv" ) as phantom_data_path: masses = pandas.read_csv(phantom_data_path) diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 1e9cf31..90f082a 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -58,8 +58,7 @@ def check_mandatory_fields_organ(self) -> None: def _load_human_mass_table() -> pandas.DataFrame: """Load the reference human phantom masses.""" with resource_path( - "pytheranostics.preclinical_dosimetry", - "phantomdata/human_phantom_masses.csv", + "pytheranostics.data", "dosimetry/phantomdata/human_phantom_masses.csv" ) as masses_path: masses = pandas.read_csv(masses_path, index_col=0) return masses @@ -663,7 +662,7 @@ def create_Olinda_file(self, dirname: str, savefile: bool = False) -> None: return with resource_path( - "pytheranostics.dosimetry", f"olindaTemplates/{template_file}" + "pytheranostics.data", f"dosimetry/olindaTemplates/{template_file}" ) as template_path: template = pandas.read_csv(template_path) diff --git a/pytheranostics/preclinical_dosimetry/biodose.py b/pytheranostics/preclinical_dosimetry/biodose.py index f5dd9f1..ebd90cb 100644 --- a/pytheranostics/preclinical_dosimetry/biodose.py +++ b/pytheranostics/preclinical_dosimetry/biodose.py @@ -20,7 +20,8 @@ def _preclinical_resource(relative_path: str): - return resource_path("pytheranostics.preclinical_dosimetry", relative_path) + """Resolve a path inside the packaged preclinical data directory.""" + return resource_path("pytheranostics.data", f"preclinical/{relative_path}") def _data_resource(relative_path: str): From 4a866c6b480610888d6679e5ac04b57534981192 Mon Sep 17 00:00:00 2001 From: Carlos Uribe Date: Sat, 8 Nov 2025 19:53:27 -0800 Subject: [PATCH 05/11] data: reorganize phantom and olinda data under the data folder and dedpulicate --- pyproject.toml | 3 + pytheranostics/data/README.md | 23 ++++ .../phantomdata/Lu177-Female-Svalues.csv | 27 ---- .../phantomdata/Lu177-Male-Svalues.csv | 26 ---- .../phantomdata/human_phantom_masses.csv | 30 ----- .../templates/human}/adult_female.cas | 0 .../templates/human}/adult_male.cas | 0 .../templates/mouse}/mouse25g.cas | 0 .../human}/PhantomMasses.xlsx | Bin .../human}/human_notinphantom_masses.csv | 0 .../human}/human_phantom_masses.csv | 0 .../mouse}/Organ_weights_mice.xlsx | Bin .../mouse}/mouse_notinphantom_masses.csv | 0 .../mouse}/mouse_phantom_masses.csv | 0 .../mouse}/rMSF_factor.csv | 0 .../olindaTemplates/adult_female.cas | 127 ------------------ .../olindaTemplates/adult_male.cas | 127 ------------------ .../preclinical/olindaTemplates/mouse25g.cas | 127 ------------------ .../phantomdata/PhantomMasses.xlsx | Bin 17557 -> 0 bytes .../phantomdata/human_notinphantom_masses.csv | 6 - pytheranostics/dosimetry/olinda.py | 4 +- pytheranostics/dosimetry/organ_s_dosimetry.py | 4 +- .../preclinical_dosimetry/biodose.py | 33 ++--- 23 files changed, 44 insertions(+), 493 deletions(-) create mode 100644 pytheranostics/data/README.md delete mode 100644 pytheranostics/data/dosimetry/phantomdata/Lu177-Female-Svalues.csv delete mode 100644 pytheranostics/data/dosimetry/phantomdata/Lu177-Male-Svalues.csv delete mode 100644 pytheranostics/data/dosimetry/phantomdata/human_phantom_masses.csv rename pytheranostics/data/{dosimetry/olindaTemplates => olinda/templates/human}/adult_female.cas (100%) rename pytheranostics/data/{dosimetry/olindaTemplates => olinda/templates/human}/adult_male.cas (100%) rename pytheranostics/data/{preclinical => olinda/templates/mouse}/mouse25g.cas (100%) rename pytheranostics/data/{dosimetry/phantomdata => phantom/human}/PhantomMasses.xlsx (100%) rename pytheranostics/data/{dosimetry/phantomdata => phantom/human}/human_notinphantom_masses.csv (100%) rename pytheranostics/data/{preclinical/phantomdata => phantom/human}/human_phantom_masses.csv (100%) rename pytheranostics/data/{preclinical/phantomdata => phantom/mouse}/Organ_weights_mice.xlsx (100%) rename pytheranostics/data/{preclinical/phantomdata => phantom/mouse}/mouse_notinphantom_masses.csv (100%) rename pytheranostics/data/{preclinical/phantomdata => phantom/mouse}/mouse_phantom_masses.csv (100%) rename pytheranostics/data/{preclinical/phantomdata => phantom/mouse}/rMSF_factor.csv (100%) delete mode 100644 pytheranostics/data/preclinical/olindaTemplates/adult_female.cas delete mode 100644 pytheranostics/data/preclinical/olindaTemplates/adult_male.cas delete mode 100644 pytheranostics/data/preclinical/olindaTemplates/mouse25g.cas delete mode 100644 pytheranostics/data/preclinical/phantomdata/PhantomMasses.xlsx delete mode 100644 pytheranostics/data/preclinical/phantomdata/human_notinphantom_masses.csv diff --git a/pyproject.toml b/pyproject.toml index c3cdcfa..5cbc512 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,9 @@ dev = [ "flake8-docstrings>=1.7" ] +[tool.hatch.build] +include = ["pytheranostics/data/**/*"] + [tool.hatch.build.targets.wheel] packages = [ "pytheranostics", diff --git a/pytheranostics/data/README.md b/pytheranostics/data/README.md new file mode 100644 index 0000000..83f1f19 --- /dev/null +++ b/pytheranostics/data/README.md @@ -0,0 +1,23 @@ +# PyTheranostics Data Layout + +All reference assets that ship with the library live under this directory so +they are available to both library code and tests via `importlib.resources`. + +``` +pytheranostics/data/ +├── phantom/ +│ ├── human/ # ICRP organ masses, literature tables, supporting workbooks +│ └── mouse/ # Preclinical phantom masses, scaling factors, literature data +├── olinda/ +│ └── templates/ +│ ├── human/ # Adult male/female OLINDA case templates +│ └── mouse/ # Mouse-specific case templates (e.g., mouse25g) +├── s-values/ +│ ├── organ/ # Radionuclide/sex-specific organ S-value tables +│ └── spheres.json +├── monte_carlo/ # Geant4/GATE templates used by voxel dosimetry +└── phantom/ # (additional imaging phantoms, e.g., skeleton masks) +``` + +When adding new assets, prefer extending these folders (or adding a clearly +named subdirectory) instead of nesting data inside individual subpackages. diff --git a/pytheranostics/data/dosimetry/phantomdata/Lu177-Female-Svalues.csv b/pytheranostics/data/dosimetry/phantomdata/Lu177-Female-Svalues.csv deleted file mode 100644 index f7c4665..0000000 --- a/pytheranostics/data/dosimetry/phantomdata/Lu177-Female-Svalues.csv +++ /dev/null @@ -1,27 +0,0 @@ -Target,Adrenals ,Brain,Breasts ,Esophagus,Eyes,GB Cont,LLI Cont ,SI Cont,StomCont,ULI Cont,Rectum,HeartCon,Hrt Wall,Kidneys ,Liver,Lungs,Ovaries,Pancreas,Salivary,Red Mar.,CortBone,TrabBone,Spleen,Thymus,Thyroid,UB Cont,Uterus,Tot Body -Adrenals,1.81E-03,5.72E-10,1.36E-08,1.10E-07,5.84E-10,2.83E-07,7.67E-08,6.74E-08,3.09E-07,4.45E-08,7.93E-09,5.82E-08,5.82E-08,7.16E-07,3.25E-07,5.44E-08,1.66E-08,3.21E-07,1.93E-09,7.85E-08,3.80E-08,3.80E-08,2.38E-06,2.25E-08,8.45E-09,5.97E-09,1.14E-08,4.41E-07 -Brain,5.72E-10,1.89E-05,2.74E-09,6.45E-09,2.03E-07,2.33E-10,2.17E-10,8.38E-11,7.48E-10,1.25E-10,1.52E-11,2.42E-09,2.42E-09,2.86E-10,8.10E-10,4.99E-09,1.96E-11,3.93E-10,1.72E-07,3.45E-08,3.99E-08,3.99E-08,6.75E-10,5.55E-09,1.84E-08,7.15E-12,1.92E-11,4.26E-07 -Breasts,1.36E-08,2.74E-09,4.78E-05,3.76E-08,6.99E-09,1.06E-08,5.54E-09,5.24E-09,2.83E-08,6.70E-09,5.45E-10,7.79E-08,7.79E-08,6.27E-09,2.94E-08,6.15E-08,7.10E-10,1.98E-08,9.83E-09,1.67E-08,1.08E-08,1.08E-08,1.15E-08,8.07E-08,3.18E-08,3.18E-10,1.01E-09,4.19E-07 -Esophagus,1.10E-07,6.45E-09,3.76E-08,6.71E-04,6.11E-09,4.29E-08,1.92E-08,1.24E-08,1.09E-07,1.28E-08,7.10E-10,3.53E-07,4.30E-07,4.15E-08,1.34E-07,2.09E-07,3.33E-09,1.33E-07,2.89E-08,2.50E-08,2.39E-08,2.39E-08,7.79E-08,2.98E-07,5.79E-07,1.62E-09,2.94E-09,4.27E-07 -Eyes,5.84E-10,2.18E-07,6.99E-09,6.11E-09,1.57E-03,1.91E-10,1.83E-10,1.35E-10,8.35E-10,6.17E-11,1.29E-10,3.24E-09,3.24E-09,3.16E-10,1.01E-09,4.84E-09,7.54E-11,5.67E-10,1.40E-07,3.45E-08,3.99E-08,3.99E-08,6.92E-10,6.92E-09,1.94E-08,7.23E-11,1.91E-10,4.26E-07 -Gallbladder Wall,2.87E-07,2.33E-10,1.07E-08,4.30E-08,1.91E-10,2.52E-04,1.91E-07,1.86E-07,6.41E-08,2.68E-07,1.78E-08,2.35E-08,2.35E-08,3.04E-07,2.52E-07,2.28E-08,3.76E-08,2.85E-07,7.16E-10,2.78E-08,1.49E-08,1.49E-08,4.78E-08,1.20E-08,4.52E-09,1.65E-08,3.29E-08,4.43E-07 -LLI Wall,7.67E-08,2.17E-10,5.54E-09,1.92E-08,1.83E-10,1.91E-07,1.49E-04,1.82E-07,1.67E-07,5.20E-08,7.10E-08,1.91E-07,1.91E-07,1.04E-07,3.81E-08,1.51E-08,7.27E-08,1.04E-07,2.66E-10,7.15E-08,2.48E-08,2.48E-08,2.00E-07,5.82E-09,1.96E-09,5.05E-08,8.96E-08,4.42E-07 -Small Intestine,6.74E-08,1.23E-10,5.24E-09,1.83E-08,1.98E-10,1.86E-07,1.82E-07,4.30E-05,1.09E-07,1.79E-07,7.55E-08,1.54E-08,1.54E-08,1.04E-07,4.88E-08,1.11E-08,1.45E-07,2.60E-07,4.64E-10,5.89E-08,1.95E-08,1.95E-08,7.53E-08,5.14E-09,2.04E-09,8.54E-08,2.07E-07,4.39E-07 -Stomach Wall,3.20E-07,7.48E-10,2.83E-08,1.00E-07,8.35E-10,6.51E-08,1.67E-07,1.02E-07,5.31E-05,1.81E-08,5.41E-09,1.23E-07,1.23E-07,9.67E-08,7.98E-08,9.81E-08,9.41E-09,4.40E-07,2.52E-09,2.42E-08,1.47E-08,1.47E-08,5.14E-07,2.67E-08,1.20E-08,4.29E-09,7.90E-09,4.38E-07 -ULI Wall,4.45E-08,4.45E-08,6.70E-09,1.28E-08,6.17E-11,2.68E-07,5.20E-08,1.79E-07,1.81E-08,7.54E-05,2.48E-08,1.13E-08,1.13E-08,9.46E-08,7.90E-08,8.82E-09,6.66E-08,8.87E-08,3.39E-10,5.01E-08,1.65E-08,1.65E-08,2.55E-08,4.83E-09,2.21E-09,2.52E-08,4.86E-08,4.42E-07 -Rectum,7.93E-09,1.52E-11,5.45E-10,7.10E-10,1.29E-10,1.78E-08,7.10E-08,7.35E-08,5.41E-09,2.48E-08,1.50E-04,1.31E-09,1.31E-09,1.60E-08,5.26E-09,1.03E-09,4.40E-07,1.00E-08,3.17E-11,7.15E-08,2.48E-08,2.48E-08,6.97E-09,3.68E-10,1.45E-10,5.82E-07,1.26E-06,4.42E-07 -Heart Wall,5.82E-08,2.76E-09,8.89E-08,4.12E-07,3.24E-09,2.43E-08,2.20E-08,1.42E-08,1.23E-07,1.13E-08,1.31E-09,3.32E-05,9.57E-05,2.39E-08,7.33E-08,2.58E-07,2.35E-09,6.89E-08,9.01E-09,3.01E-08,1.91E-08,1.91E-08,6.24E-08,4.00E-07,6.83E-08,1.00E-09,1.88E-09,4.39E-07 -Kidneys,6.90E-07,2.86E-10,6.86E-09,4.15E-08,3.16E-10,2.96E-07,1.04E-07,9.43E-08,9.63E-08,9.07E-08,1.60E-08,2.31E-08,2.32E-08,8.70E-05,1.51E-07,2.30E-08,3.02E-08,1.76E-07,8.82E-10,5.63E-08,2.33E-08,2.33E-08,4.19E-07,8.85E-09,4.26E-09,1.12E-08,2.31E-08,4.36E-07 -Liver,3.24E-07,8.80E-10,2.94E-08,1.34E-07,1.01E-09,2.50E-07,3.81E-08,4.55E-08,7.76E-08,7.54E-08,5.26E-09,7.24E-08,7.25E-08,1.52E-07,1.76E-05,1.07E-07,1.03E-08,2.41E-07,2.75E-09,2.74E-08,1.72E-08,1.72E-08,3.97E-08,4.19E-08,1.61E-08,4.52E-09,8.46E-09,4.37E-07 -Lungs,5.71E-08,5.02E-09,6.17E-08,2.09E-07,4.84E-09,2.28E-08,1.51E-08,1.02E-08,8.77E-08,8.81E-09,1.03E-09,2.15E-07,2.52E-07,2.30E-08,1.07E-07,2.50E-05,1.99E-09,4.71E-08,1.64E-08,3.64E-08,2.37E-08,2.37E-08,5.24E-08,3.25E-07,1.28E-07,8.33E-10,1.56E-09,4.34E-07 -Ovaries,1.66E-08,1.96E-11,7.10E-10,3.33E-09,7.54E-11,3.75E-08,7.26E-08,1.45E-07,9.41E-09,6.65E-08,4.39E-07,2.35E-09,2.35E-09,3.01E-08,1.03E-08,1.99E-09,2.14E-03,1.91E-08,2.72E-11,7.11E-08,2.21E-08,2.21E-08,1.17E-08,1.09E-09,2.04E-10,2.51E-07,9.10E-07,4.43E-07 -Pancreas,3.08E-07,3.93E-10,1.98E-08,1.33E-07,5.67E-10,2.76E-07,1.04E-07,2.14E-07,4.05E-07,8.85E-08,9.99E-09,6.56E-08,6.58E-08,1.72E-07,2.34E-07,4.64E-08,1.91E-08,2.00E-04,1.42E-09,4.19E-08,2.21E-08,2.21E-08,2.28E-07,2.10E-08,7.18E-09,7.93E-09,1.65E-08,4.44E-07 -Salivary Glands,1.93E-09,1.67E-07,9.83E-09,2.89E-08,1.41E-07,7.16E-10,2.66E-10,4.64E-10,2.52E-09,3.59E-10,3.17E-11,8.78E-09,8.78E-09,8.82E-10,2.75E-09,1.61E-08,2.72E-11,1.42E-09,3.38E-04,2.50E-08,2.39E-08,2.39E-08,1.60E-09,2.20E-08,8.60E-08,3.36E-10,1.98E-11,4.27E-07 -Red Marrow,4.73E-08,1.93E-08,1.69E-08,9.52E-08,1.49E-08,4.47E-08,3.64E-08,3.69E-08,3.27E-08,2.81E-08,5.44E-08,4.88E-08,4.88E-08,4.52E-08,3.60E-08,6.26E-08,8.62E-08,4.99E-08,2.89E-08,1.49E-05,6.81E-08,5.40E-06,3.96E-08,6.65E-08,6.58E-08,4.14E-08,5.94E-08,3.28E-07 -Osteogenic Cells,6.45E-08,1.27E-07,2.17E-08,1.02E-07,1.75E-07,6.17E-08,3.84E-08,4.57E-08,4.42E-08,3.15E-08,5.82E-08,5.80E-08,5.80E-08,5.69E-08,4.65E-08,6.93E-08,7.54E-08,7.23E-08,1.33E-07,5.64E-06,1.37E-05,1.73E-05,5.30E-08,6.93E-08,7.32E-08,4.00E-08,5.93E-08,3.75E-07 -Spleen,2.37E-06,6.75E-10,1.28E-08,7.80E-08,6.92E-10,4.78E-08,1.99E-07,6.88E-08,4.43E-07,2.55E-08,6.97E-09,6.14E-08,6.15E-08,4.09E-07,3.81E-08,5.02E-08,1.17E-08,2.31E-07,1.60E-09,2.93E-08,1.79E-08,1.79E-08,1.84E-04,1.80E-08,7.31E-09,4.45E-09,9.55E-09,4.37E-07 -Thymus,2.25E-08,5.55E-09,9.65E-08,2.98E-07,6.92E-09,1.14E-08,5.82E-09,5.13E-09,2.67E-08,4.83E-09,4.49E-10,3.65E-07,4.20E-07,4.26E-09,4.58E-08,3.54E-07,1.09E-09,2.23E-08,2.23E-08,2.48E-08,1.65E-08,1.65E-08,1.80E-08,1.18E-03,4.53E-07,3.38E-10,3.77E-10,4.33E-07 -Thyroid,8.45E-09,1.84E-08,3.18E-08,5.78E-07,1.94E-08,4.52E-09,1.96E-09,1.39E-09,1.20E-08,2.21E-09,1.45E-10,7.18E-08,7.18E-08,4.16E-09,1.61E-08,1.25E-07,2.04E-10,7.18E-09,8.56E-08,2.50E-08,2.39E-08,2.39E-08,7.31E-09,4.10E-07,1.38E-03,1.14E-10,6.41E-10,4.27E-07 -Urinary Bladder Wall,5.97E-09,1.04E-11,3.18E-10,1.62E-09,7.23E-11,1.65E-08,5.05E-08,7.02E-08,4.29E-09,2.52E-08,5.82E-07,1.00E-09,1.00E-09,1.00E-08,4.52E-09,8.33E-10,2.09E-07,7.93E-09,3.36E-10,2.82E-08,1.44E-08,1.44E-08,4.45E-09,3.38E-10,1.14E-10,7.54E-05,6.25E-07,4.40E-07 -Uterus,1.14E-08,1.92E-11,1.01E-09,2.94E-09,1.91E-10,3.19E-08,8.93E-08,1.93E-07,7.90E-09,4.83E-08,1.04E-06,1.88E-09,1.88E-09,2.37E-08,8.43E-09,1.56E-09,8.77E-07,1.65E-08,1.98E-11,5.19E-08,1.70E-08,1.70E-08,9.53E-09,3.77E-10,6.41E-10,6.09E-07,3.00E-04,4.42E-07 -Total Body,4.43E-07,4.25E-07,4.19E-07,4.18E-07,4.05E-07,4.39E-07,4.39E-07,4.38E-07,4.32E-07,4.37E-07,4.28E-07,4.32E-07,4.32E-07,4.38E-07,4.38E-07,4.34E-07,4.45E-07,4.46E-07,4.12E-07,4.35E-07,4.31E-07,4.31E-07,4.39E-07,4.33E-07,4.28E-07,4.34E-07,4.45E-07,4.30E-07 diff --git a/pytheranostics/data/dosimetry/phantomdata/Lu177-Male-Svalues.csv b/pytheranostics/data/dosimetry/phantomdata/Lu177-Male-Svalues.csv deleted file mode 100644 index a736b7b..0000000 --- a/pytheranostics/data/dosimetry/phantomdata/Lu177-Male-Svalues.csv +++ /dev/null @@ -1,26 +0,0 @@ -Target,Adrenals ,Brain,Esophagus,Eyes,GB Cont,LLI Cont ,SI Cont,StomCont,ULI Cont,Rectum,HeartCon,Hrt Wall,Kidneys ,Liver,Lungs,Pancreas,Prostate,Salivary,Red Mar.,CortBone,TrabBone,Spleen,Testes,Thymus,Thyroid,UB Cont,Tot Body -Adrenals,1.68E-03,4.12E-10,7.41E-08,1.79E-10,1.74E-07,1.92E-07,7.91E-08,1.60E-07,8.42E-08,1.24E-08,4.95E-08,4.95E-08,1.03E-06,2.87E-07,4.07E-08,2.15E-07,1.73E-08,1.47E-09,6.73E-08,2.98E-08,2.98E-08,4.42E-07,8.32E-10,1.73E-08,8.26E-09,8.10E-09,3.62E-07 -Brain,4.12E-10,1.70E-05,9.46E-09,1.57E-07,3.06E-10,3.08E-10,9.00E-11,9.63E-10,1.70E-10,1.94E-11,1.47E-09,1.47E-09,2.22E-10,6.58E-10,3.31E-09,4.69E-10,1.86E-11,1.55E-07,2.29E-08,3.34E-08,3.34E-08,5.59E-10,1.27E-12,3.14E-09,1.39E-08,6.54E-12,3.49E-07 -Esophagus,7.53E-08,9.45E-09,5.88E-04,7.98E-09,4.41E-08,3.47E-08,1.76E-08,2.11E-07,2.14E-08,8.17E-10,2.49E-07,2.68E-07,3.22E-08,9.98E-08,1.75E-07,1.07E-07,2.22E-09,4.40E-08,2.07E-08,2.10E-08,2.10E-08,6.81E-08,1.57E-10,1.61E-07,4.76E-07,1.02E-09,3.56E-07 -Eyes,1.79E-10,1.69E-07,7.98E-09,1.57E-03,6.83E-10,3.41E-10,1.61E-10,9.40E-10,8.49E-11,1.19E-10,1.95E-09,1.95E-09,2.95E-10,8.29E-10,2.75E-09,5.75E-10,6.92E-11,7.44E-08,2.29E-08,3.34E-08,3.34E-08,4.14E-10,1.35E-11,3.96E-09,1.00E-08,1.19E-11,3.49E-07 -Gallbladder Wall,1.77E-07,3.06E-10,4.41E-08,6.83E-10,2.09E-04,8.33E-08,7.29E-08,5.70E-08,4.03E-07,8.06E-09,5.65E-08,5.67E-08,9.27E-08,4.20E-07,3.31E-08,1.07E-07,1.31E-08,1.05E-09,3.09E-08,1.20E-08,1.20E-08,2.45E-08,6.67E-10,2.13E-08,7.05E-09,6.17E-09,3.64E-07 -LLI Wall,1.92E-07,3.08E-10,3.47E-08,3.41E-10,8.33E-08,1.59E-04,2.63E-07,1.64E-07,5.96E-08,7.69E-08,4.02E-08,4.03E-08,1.24E-07,3.70E-08,2.59E-08,4.20E-07,4.61E-08,6.60E-10,5.50E-08,2.02E-08,2.02E-08,1.38E-07,2.19E-09,1.08E-08,4.13E-09,2.39E-08,3.63E-07 -Small Intestine,7.92E-08,9.00E-11,1.76E-08,2.47E-10,7.29E-08,2.63E-07,3.45E-05,6.93E-08,1.97E-07,1.21E-07,2.21E-08,2.21E-08,7.10E-08,3.69E-08,1.09E-08,2.32E-07,9.27E-08,3.64E-10,5.04E-08,1.57E-08,1.57E-08,3.99E-08,6.56E-09,6.44E-09,2.24E-09,6.84E-08,3.64E-07 -Stomach Wall,1.49E-07,9.23E-10,2.24E-07,9.40E-10,5.45E-08,1.64E-07,4.50E-08,4.88E-05,1.47E-08,5.36E-09,1.89E-07,1.91E-07,6.00E-08,8.11E-08,1.23E-07,6.71E-07,5.84E-09,2.71E-09,2.25E-08,1.09E-08,1.09E-08,2.51E-07,4.01E-10,4.30E-08,1.76E-08,2.60E-09,3.59E-07 -ULI Wall,8.43E-08,1.70E-10,2.14E-08,8.49E-11,4.03E-07,5.96E-08,1.28E-07,1.47E-08,8.05E-05,2.42E-08,3.10E-08,3.10E-08,8.21E-08,9.96E-08,1.56E-08,1.20E-07,3.89E-08,4.72E-10,4.26E-08,1.38E-08,1.38E-08,2.43E-08,3.47E-09,9.04E-09,1.53E-09,2.75E-08,3.63E-07 -Rectum,1.24E-08,1.94E-11,2.38E-09,1.19E-10,8.06E-09,7.69E-08,1.21E-07,5.36E-09,2.42E-08,1.60E-04,2.03E-09,2.03E-09,2.35E-08,5.20E-09,1.34E-09,1.10E-08,4.93E-07,3.24E-11,5.50E-08,2.02E-08,2.02E-08,8.46E-09,3.48E-08,4.94E-10,3.35E-10,2.57E-07,3.63E-07 -Heart Wall,4.58E-08,1.71E-09,2.40E-07,1.95E-09,5.45E-08,4.02E-08,2.21E-08,1.99E-07,3.10E-08,2.03E-09,2.42E-05,7.27E-05,2.18E-08,1.08E-07,1.91E-07,1.36E-07,2.45E-09,5.43E-09,2.97E-08,1.57E-08,1.57E-08,3.95E-08,1.64E-10,4.65E-07,4.65E-08,1.05E-09,3.60E-07 -Kidneys,9.83E-07,2.77E-10,2.98E-08,2.92E-10,8.94E-08,1.24E-07,7.09E-08,6.24E-08,8.20E-08,2.35E-08,2.20E-08,2.21E-08,7.76E-05,1.20E-07,2.02E-08,8.98E-08,3.28E-08,6.39E-10,4.71E-08,1.73E-08,1.73E-08,2.65E-07,1.85E-09,8.05E-09,3.80E-09,1.18E-08,3.58E-07 -Liver,2.66E-07,7.89E-10,8.57E-08,8.29E-10,3.67E-07,3.63E-08,3.69E-08,8.35E-08,9.90E-08,5.20E-09,1.07E-07,1.08E-07,1.18E-07,1.37E-05,8.93E-08,1.14E-07,8.30E-09,2.16E-09,2.44E-08,1.33E-08,1.33E-08,3.12E-08,5.50E-10,3.84E-08,1.42E-08,3.78E-09,3.58E-07 -Lungs,3.93E-08,3.39E-09,1.56E-07,2.75E-09,3.26E-08,2.59E-08,1.09E-08,1.11E-07,1.56E-08,1.34E-09,1.64E-07,1.86E-07,1.94E-08,8.77E-08,1.99E-05,5.50E-08,1.72E-09,1.10E-08,3.04E-08,1.85E-08,1.85E-08,4.75E-08,1.07E-10,1.56E-07,1.12E-07,7.61E-10,3.54E-07 -Pancreas,2.06E-07,4.69E-10,9.57E-08,5.75E-10,1.08E-07,3.75E-07,2.09E-07,6.20E-07,1.34E-07,1.10E-08,1.37E-07,1.37E-07,9.16E-08,1.16E-07,5.67E-08,1.71E-04,1.21E-08,1.36E-09,4.05E-08,1.78E-08,1.78E-08,1.49E-07,8.56E-10,3.18E-08,9.58E-09,5.30E-09,3.65E-07 -Prostate,1.73E-08,1.86E-11,2.22E-09,6.92E-11,1.31E-08,4.60E-08,6.00E-08,5.84E-09,3.88E-08,4.92E-07,2.39E-09,2.39E-09,3.28E-08,8.30E-09,1.72E-09,1.21E-08,1.39E-03,2.51E-10,2.44E-08,1.08E-08,1.08E-08,1.02E-08,2.85E-08,7.20E-10,5.62E-10,3.63E-07,3.62E-07 -Salivary Glands,1.47E-09,1.53E-07,4.41E-08,7.76E-08,9.17E-10,6.60E-10,2.37E-10,2.93E-09,4.72E-10,3.40E-11,5.35E-09,5.35E-09,6.39E-10,2.16E-09,1.10E-08,1.36E-09,2.35E-11,2.78E-04,2.07E-08,2.10E-08,2.10E-08,1.80E-09,3.64E-11,1.22E-08,5.80E-08,3.17E-11,3.56E-07 -Red Marrow,3.78E-08,2.89E-08,6.03E-08,1.69E-08,1.90E-08,2.46E-08,2.14E-08,3.40E-08,1.98E-08,5.61E-08,3.60E-08,3.60E-08,3.35E-08,3.09E-08,5.53E-08,2.51E-08,7.74E-08,2.23E-08,1.15E-05,5.60E-08,4.16E-06,3.67E-08,8.38E-09,5.88E-08,4.69E-08,3.96E-08,2.74E-07 -Osteogenic Cells,6.09E-08,1.05E-07,7.57E-08,1.13E-07,2.66E-08,3.52E-08,3.21E-08,4.63E-08,2.99E-08,5.07E-08,3.77E-08,3.77E-08,5.34E-08,3.74E-08,5.47E-08,3.93E-08,7.17E-08,1.06E-07,5.62E-06,1.36E-05,1.73E-05,5.07E-08,1.77E-08,4.32E-08,5.63E-08,4.30E-08,4.00E-07 -Spleen,4.15E-07,5.59E-10,5.91E-08,4.14E-10,2.37E-08,1.38E-07,3.99E-08,2.51E-07,2.43E-08,8.46E-09,3.83E-08,3.83E-08,2.58E-07,3.03E-08,4.64E-08,1.41E-07,9.88E-09,1.53E-09,2.48E-08,1.35E-08,1.35E-08,1.60E-04,5.26E-10,1.54E-08,1.01E-08,3.47E-09,3.58E-07 -Testes,8.32E-10,1.27E-12,1.52E-10,1.35E-11,6.67E-10,2.19E-09,4.26E-09,4.01E-10,3.47E-09,3.48E-08,1.77E-10,1.77E-10,1.85E-09,5.50E-10,1.07E-10,8.56E-10,2.75E-08,3.64E-11,8.50E-09,1.12E-08,1.12E-08,5.26E-10,6.77E-04,2.43E-10,8.58E-11,7.77E-08,3.51E-07 -Thymus,1.39E-08,3.14E-09,1.41E-07,3.19E-09,1.89E-08,1.08E-08,4.18E-09,4.27E-08,9.04E-09,4.94E-10,3.47E-07,4.26E-07,7.62E-09,3.97E-08,1.49E-07,2.91E-08,7.20E-10,1.29E-08,2.35E-08,1.34E-08,1.34E-08,1.63E-08,2.43E-10,9.42E-04,2.03E-07,4.49E-10,3.55E-07 -Thyroid,8.26E-09,1.39E-08,4.24E-07,1.00E-08,7.05E-09,4.13E-09,1.46E-09,1.75E-08,1.53E-09,1.95E-10,4.45E-08,4.45E-08,3.80E-09,1.42E-08,1.18E-07,9.57E-09,1.55E-10,6.14E-08,2.07E-08,2.10E-08,2.10E-08,1.01E-08,8.58E-11,2.22E-07,1.18E-03,2.24E-10,3.56E-07 -Urinary Bladder Wall,8.10E-09,6.54E-12,1.02E-09,1.19E-10,6.17E-09,2.39E-08,4.44E-08,2.60E-09,2.75E-08,2.57E-07,1.05E-09,1.05E-09,1.18E-08,3.78E-09,7.61E-10,5.30E-09,3.83E-07,3.17E-11,2.44E-08,1.08E-08,1.08E-08,3.47E-09,7.78E-08,4.49E-10,2.24E-10,5.78E-05,3.62E-07 -Total Body,3.47E-07,3.30E-07,3.44E-07,3.32E-07,3.42E-07,3.49E-07,3.48E-07,3.43E-07,3.47E-07,3.54E-07,3.38E-07,3.38E-07,3.49E-07,3.40E-07,3.41E-07,3.45E-07,3.57E-07,3.38E-07,3.57E-07,3.54E-07,3.54E-07,3.46E-07,3.49E-07,3.41E-07,3.46E-07,3.52E-07,3.54E-07 diff --git a/pytheranostics/data/dosimetry/phantomdata/human_phantom_masses.csv b/pytheranostics/data/dosimetry/phantomdata/human_phantom_masses.csv deleted file mode 100644 index 0a67d39..0000000 --- a/pytheranostics/data/dosimetry/phantomdata/human_phantom_masses.csv +++ /dev/null @@ -1,30 +0,0 @@ -Organ,Female,Male -Adrenals,13,14 -Brain,1300,1450 -Breasts,500,25 -Esophagus,35,40 -Eyes,15,15 -Gallbladder,8,10 -Left Colon,145,150 -Small Intestine,600,650 -Stomach,140,150 -Right Colon,145,150 -Heart,250,330 -Heart Contents,370,510 -Kidneys,275,310 -Liver,1400,1800 -Lungs,950,1200 -Ovaries,11,0 -Pancreas,120,140 -Prostate,0,17 -Rectum,70,70 -Salivary Glands,70,85 -Red Marrow,900,1170 -Bone Surfaces,120,120 -Spleen,130,150 -Testes,0,35 -Thymus,20,25 -Thyroid,17,20 -Bladder,40,50 -Uterus,80,0 -Body,60000,73000 diff --git a/pytheranostics/data/dosimetry/olindaTemplates/adult_female.cas b/pytheranostics/data/olinda/templates/human/adult_female.cas similarity index 100% rename from pytheranostics/data/dosimetry/olindaTemplates/adult_female.cas rename to pytheranostics/data/olinda/templates/human/adult_female.cas diff --git a/pytheranostics/data/dosimetry/olindaTemplates/adult_male.cas b/pytheranostics/data/olinda/templates/human/adult_male.cas similarity index 100% rename from pytheranostics/data/dosimetry/olindaTemplates/adult_male.cas rename to pytheranostics/data/olinda/templates/human/adult_male.cas diff --git a/pytheranostics/data/preclinical/mouse25g.cas b/pytheranostics/data/olinda/templates/mouse/mouse25g.cas similarity index 100% rename from pytheranostics/data/preclinical/mouse25g.cas rename to pytheranostics/data/olinda/templates/mouse/mouse25g.cas diff --git a/pytheranostics/data/dosimetry/phantomdata/PhantomMasses.xlsx b/pytheranostics/data/phantom/human/PhantomMasses.xlsx similarity index 100% rename from pytheranostics/data/dosimetry/phantomdata/PhantomMasses.xlsx rename to pytheranostics/data/phantom/human/PhantomMasses.xlsx diff --git a/pytheranostics/data/dosimetry/phantomdata/human_notinphantom_masses.csv b/pytheranostics/data/phantom/human/human_notinphantom_masses.csv similarity index 100% rename from pytheranostics/data/dosimetry/phantomdata/human_notinphantom_masses.csv rename to pytheranostics/data/phantom/human/human_notinphantom_masses.csv diff --git a/pytheranostics/data/preclinical/phantomdata/human_phantom_masses.csv b/pytheranostics/data/phantom/human/human_phantom_masses.csv similarity index 100% rename from pytheranostics/data/preclinical/phantomdata/human_phantom_masses.csv rename to pytheranostics/data/phantom/human/human_phantom_masses.csv diff --git a/pytheranostics/data/preclinical/phantomdata/Organ_weights_mice.xlsx b/pytheranostics/data/phantom/mouse/Organ_weights_mice.xlsx similarity index 100% rename from pytheranostics/data/preclinical/phantomdata/Organ_weights_mice.xlsx rename to pytheranostics/data/phantom/mouse/Organ_weights_mice.xlsx diff --git a/pytheranostics/data/preclinical/phantomdata/mouse_notinphantom_masses.csv b/pytheranostics/data/phantom/mouse/mouse_notinphantom_masses.csv similarity index 100% rename from pytheranostics/data/preclinical/phantomdata/mouse_notinphantom_masses.csv rename to pytheranostics/data/phantom/mouse/mouse_notinphantom_masses.csv diff --git a/pytheranostics/data/preclinical/phantomdata/mouse_phantom_masses.csv b/pytheranostics/data/phantom/mouse/mouse_phantom_masses.csv similarity index 100% rename from pytheranostics/data/preclinical/phantomdata/mouse_phantom_masses.csv rename to pytheranostics/data/phantom/mouse/mouse_phantom_masses.csv diff --git a/pytheranostics/data/preclinical/phantomdata/rMSF_factor.csv b/pytheranostics/data/phantom/mouse/rMSF_factor.csv similarity index 100% rename from pytheranostics/data/preclinical/phantomdata/rMSF_factor.csv rename to pytheranostics/data/phantom/mouse/rMSF_factor.csv diff --git a/pytheranostics/data/preclinical/olindaTemplates/adult_female.cas b/pytheranostics/data/preclinical/olindaTemplates/adult_female.cas deleted file mode 100644 index 35fe178..0000000 --- a/pytheranostics/data/preclinical/olindaTemplates/adult_female.cas +++ /dev/null @@ -1,127 +0,0 @@ -Saved on 06.06.2017 at 09:36:07 PDT -[BEGIN CASE FILE] -[BEGIN NUCLIDE SETTINGS] -USE_DECAY_SERIES|false -USE_LEGACY_DATA|false -[END NUCLIDE SETTINGS] -[BEGIN NUCLIDES] -Lu-177| -[END NUCLIDES] -[BEGIN MODEL SETTINGS] -[END MODEL SETTINGS] -[BEGIN MODELS] -[BEGIN ICRP 89 Adult Female] -ICRP 89 Adult Female|B -[BEGIN ICRP 89 Adult Female SOURCE ORGANS] -Adrenals|13.0|0.0 -Brain|1300.0|0.0 -Breasts|500.0|0.0 -Esophagus|35.0|0.0 -Eyes|15.0|0.0 -Gallbladder Contents|48.0|0.0 -LLI Contents|80.0|0.0 -Small Intestine|280.0|0.0 -Stomach Contents|230.0|0.0 -ULI Contents|160.0|0.0 -Rectum|80.0|0.0 -Heart Contents|370.0|0.0 -Heart Wall|250.0|0.0 -Kidneys|275.5|0.0 -Liver|1400.0|0.0 -Lungs|950.0|0.0 -Muscle|0.0|0.0 -Ovaries|11.0|0.0 -Pancreas|120.0|0.0 -Prostate|0.0|0.0 -Salivary Glands|70.0|0.0 -Red Marrow|900.0|0.0 -Cortical Bone|3200.0|0.0 -Trabecular Bone|800.0|0.0 -Spleen|130.0|0.0 -Testes|0.0|0.0 -Thymus|20.0|0.0 -Thyroid|17.0|0.0 -Urinary Bladder Contents|160.0|0.0 -Uterus|80.0|0.0 -Fetus|0.0|0.0 -Placenta|0.0|0.0 -Total Body|60000.0|0.0 -[END ICRP 89 Adult Female SOURCE ORGANS] -[BEGIN ICRP 89 Adult Female MODEL OPTIONS] -IS_BONE_ACTIVITY_ON_SURFACE|true -[END ICRP 89 Adult Female MODEL OPTIONS] -[BEGIN ICRP 89 Adult Female TARGET ORGANS] -Adrenals|13.0 -Brain|1300.0 -Breasts|500.0 -Esophagus|35.0 -Eyes|15.0 -Gallbladder Wall|8.0 -LLI Wall|145.0 -Small Intestine|600.0 -Stomach|140.0 -ULI Wall|145.0 -Rectum|70.0 -Heart Wall|250.0 -Kidneys|275.5 -Liver|1400.0 -Lungs|950.0 -Muscle|0.0 -Ovaries|11.0 -Pancreas|120.0 -Prostate|0.0 -Salivary Glands|70.0 -Red Marrow|900.0 -Bone Surfaces|120.0 -Skin|0.0 -Spleen|130.0 -Testes|0.0 -Thymus|20.0 -Thyroid|17.0 -Urinary Bladder Wall|40.0 -Uterus|80.0 -Fetus|0.0 -Placenta|0.0 -Total Body|60000.0 -[END ICRP 89 Adult Female TARGET ORGANS] -[BEGIN ICRP 89 Adult Female MODEL TARGET ORGANS] -TARGET_ORGAN_MASSES_ARE_FROM_USER_INPUT|FALSE -[END ICRP 89 Adult Female MODEL TARGET ORGANS] -[BEGIN KINETIC DATA] -Adrenals|0.0 -Brain|0.0 -Breasts|0.0 -Esophagus|0.0 -Eyes|0.0 -Gallbladder Contents|0.0 -LLI Contents|0.0 -Small Intestine|0.0 -Stomach Contents|0.0 -ULI Contents|0.0 -Rectum|0.0 -Heart Contents|0.0 -Heart Wall|0.0 -Kidneys|0.0 -Liver|0.0 -Lungs|0.0 -Muscle|0.0 -Ovaries|0.0 -Pancreas|0.0 -Prostate|0.0 -Salivary Glands|0.0 -Red Marrow|0.0 -Cortical Bone|0.0 -Trabecular Bone|0.0 -Spleen|0.0 -Testes|0.0 -Thymus|0.0 -Thyroid|0.0 -Urinary Bladder Contents|0.0 -Uterus|0.0 -Fetus|0.0 -Placenta|0.0 -Total Body|0.0 -[END KINETIC DATA] -[END ICRP 89 Adult Female] -[END MODELS] -[END CASE FILE] diff --git a/pytheranostics/data/preclinical/olindaTemplates/adult_male.cas b/pytheranostics/data/preclinical/olindaTemplates/adult_male.cas deleted file mode 100644 index ae0a178..0000000 --- a/pytheranostics/data/preclinical/olindaTemplates/adult_male.cas +++ /dev/null @@ -1,127 +0,0 @@ -Saved on 11.27.2017 at 11:50:52 PST -[BEGIN CASE FILE] -[BEGIN NUCLIDE SETTINGS] -USE_DECAY_SERIES|false -USE_LEGACY_DATA|false -[END NUCLIDE SETTINGS] -[BEGIN NUCLIDES] -Lu-177| -[END NUCLIDES] -[BEGIN MODEL SETTINGS] -[END MODEL SETTINGS] -[BEGIN MODELS] -[BEGIN ICRP 89 Adult Male] -ICRP 89 Adult Male|A -[BEGIN ICRP 89 Adult Male SOURCE ORGANS] -Adrenals|14.0|0.0 -Brain|1450.0|0.0 -Breasts|0.0|0.0 -Esophagus|40.0|0.0 -Eyes|15.0|0.0 -Gallbladder Contents|58.0|0.0 -LLI Contents|75.0|0.0 -Small Intestine|350.0|0.0 -Stomach Contents|250.0|0.0 -ULI Contents|150.0|0.0 -Rectum|75.0|0.0 -Heart Contents|510.0|0.0 -Heart Wall|330.0|0.0 -Kidneys|310.0|0.0 -Liver|1800.0|0.0 -Lungs|1200.0|0.0 -Muscle|0.0|0.0 -Ovaries|0.0|0.0 -Pancreas|140.0|0.0 -Prostate|17.0|0.0 -Salivary Glands|85.0|0.0 -Red Marrow|1170.0|0.0 -Cortical Bone|4400.0|0.0 -Trabecular Bone|1100.0|0.0 -Spleen|150.0|0.0 -Testes|35.0|0.0 -Thymus|25.0|0.0 -Thyroid|20.0|0.0 -Urinary Bladder Contents|211.0|0.0 -Uterus|0.0|0.0 -Fetus|0.0|0.0 -Placenta|0.0|0.0 -Total Body|73000.0|0.0 -[END ICRP 89 Adult Male SOURCE ORGANS] -[BEGIN ICRP 89 Adult Male MODEL OPTIONS] -IS_BONE_ACTIVITY_ON_SURFACE|true -[END ICRP 89 Adult Male MODEL OPTIONS] -[BEGIN ICRP 89 Adult Male TARGET ORGANS] -Adrenals|14.0 -Brain|1450.0 -Breasts|0.0 -Esophagus|40.0 -Eyes|15.0 -Gallbladder Wall|10.0 -LLI Wall|150.0 -Small Intestine|650.0 -Stomach|150.0 -ULI Wall|150.0 -Rectum|70.0 -Heart Wall|330.0 -Kidneys|310.0 -Liver|1800.0 -Lungs|1200.0 -Muscle|0.0 -Ovaries|0.0 -Pancreas|140.0 -Prostate|17.0 -Salivary Glands|85.0 -Red Marrow|1170.0 -Bone Surfaces|120.0 -Skin|0.0 -Spleen|150.0 -Testes|35.0 -Thymus|25.0 -Thyroid|20.0 -Urinary Bladder Wall|50.0 -Uterus|0.0 -Fetus|0.0 -Placenta|0.0 -Total Body|73000.0 -[END ICRP 89 Adult Male TARGET ORGANS] -[BEGIN ICRP 89 Adult Male MODEL TARGET ORGANS] -TARGET_ORGAN_MASSES_ARE_FROM_USER_INPUT|FALSE -[END ICRP 89 Adult Male MODEL TARGET ORGANS] -[BEGIN KINETIC DATA] -Adrenals|0.0 -Brain|0.0 -Breasts|0.0 -Esophagus|0.0 -Eyes|0.0 -Gallbladder Contents|0.0 -LLI Contents|0.0 -Small Intestine|0.0 -Stomach Contents|0.0 -ULI Contents|0.0 -Rectum|0.0 -Heart Contents|0.0 -Heart Wall|0.0 -Kidneys|0.0 -Liver|0.0 -Lungs|0.0 -Muscle|0.0 -Ovaries|0.0 -Pancreas|0.0 -Prostate|0.0 -Salivary Glands|0.0 -Red Marrow|0.0 -Cortical Bone|0.0 -Trabecular Bone|0.0 -Spleen|0.0 -Testes|0.0 -Thymus|0.0 -Thyroid|0.0 -Urinary Bladder Contents|0.0 -Uterus|0.0 -Fetus|0.0 -Placenta|0.0 -Total Body|0.0 -[END KINETIC DATA] -[END ICRP 89 Adult Male] -[END MODELS] -[END CASE FILE] diff --git a/pytheranostics/data/preclinical/olindaTemplates/mouse25g.cas b/pytheranostics/data/preclinical/olindaTemplates/mouse25g.cas deleted file mode 100644 index 6bfa44b..0000000 --- a/pytheranostics/data/preclinical/olindaTemplates/mouse25g.cas +++ /dev/null @@ -1,127 +0,0 @@ -Saved on 03.08.2018 at 13:04:40 -[BEGIN CASE FILE] -[BEGIN NUCLIDE SETTINGS] -USE_DECAY_SERIES|false -USE_LEGACY_DATA|false -[END NUCLIDE SETTINGS] -[BEGIN NUCLIDES] -Y-90| -[END NUCLIDES] -[BEGIN MODEL SETTINGS] -[END MODEL SETTINGS] -[BEGIN MODELS] -[BEGIN 25g Mouse] -25g Mouse|Z0 -[BEGIN 25g Mouse SOURCE ORGANS] -Adrenals|0.0|0.0 -Brain|0.46592|0.000233 -Breasts|0.0|0.0 -Esophagus|0.0|0.0 -Eyes|0.0|0.0 -Gallbladder Contents|0.0|0.0 -LLI Contents|0.58344|0.0 -Small Intestine|1.742|0.0 -Stomach Contents|0.055328|0.0 -ULI Contents|0.0|0.0 -Rectum|0.0|0.0 -Heart Contents|0.23504|0.0 -Heart Wall|0.0|0.0 -Kidneys|0.3016|0.0 -Liver|1.7368|0.0 -Lungs|0.087024|0.0 -Muscle|0.0|0.0 -Ovaries|0.0|0.0 -Pancreas|0.304928|0.0 -Prostate|0.0|0.0 -Salivary Glands|0.0|0.0 -Red Marrow|0.0|0.0 -Cortical Bone|2.18|0.0 -Trabecular Bone|0.0|0.0 -Spleen|0.11128|0.0 -Testes|0.16016|0.0 -Thymus|0.0|0.0 -Thyroid|0.014248|0.0 -Urinary Bladder Contents|0.0601536|0.0 -Uterus|0.0|0.0 -Fetus|0.0|0.0 -Placenta|0.0|0.0 -Total Body|24.109264|0.0 -[END 25g Mouse SOURCE ORGANS] -[BEGIN 25g Mouse MODEL OPTIONS] -IS_BONE_ACTIVITY_ON_SURFACE|true -[END 25g Mouse MODEL OPTIONS] -[BEGIN 25g Mouse TARGET ORGANS] -Adrenals|0.0 -Brain|0.46592 -Breasts|0.0 -Esophagus|0.0 -Eyes|0.0 -Gallbladder Wall|0.0 -LLI Wall|0.58344 -Small Intestine|1.742 -Stomach|0.055328 -ULI Wall|0.0 -Rectum|0.0 -Heart Wall|0.23504 -Kidneys|0.3016 -Liver|1.7368 -Lungs|0.087024 -Muscle|0.0 -Ovaries|0.0 -Pancreas|0.3049 -Prostate|0.0 -Salivary Glands|0.0 -Red Marrow|0.0 -Bone Surfaces|2.18 -Skin|0.0 -Spleen|0.11128 -Testes|0.16016 -Thymus|0.0 -Thyroid|0.014248 -Urinary Bladder Wall|0.0601536 -Uterus|0.0 -Fetus|0.0 -Placenta|0.0 -Total Body|24.109264 -[END 25g Mouse TARGET ORGANS] -[BEGIN 25g Mouse MODEL TARGET ORGANS] -TARGET_ORGAN_MASSES_ARE_FROM_USER_INPUT|FALSE -[END 25g Mouse MODEL TARGET ORGANS] -[BEGIN KINETIC DATA] -Adrenals|0.0 -Brain|0.0 -Breasts|0.0 -Esophagus|0.0 -Eyes|0.0 -Gallbladder Contents|0.0 -LLI Contents|0.0 -Small Intestine|0.0 -Stomach Contents|0.0 -ULI Contents|0.0 -Rectum|0.0 -Heart Contents|0.0 -Heart Wall|0.0 -Kidneys|0.0 -Liver|0.0 -Lungs|0.0 -Muscle|0.0 -Ovaries|0.0 -Pancreas|0.0 -Prostate|0.0 -Salivary Glands|0.0 -Red Marrow|0.0 -Cortical Bone|0.0 -Trabecular Bone|0.0 -Spleen|0.0 -Testes|0.0 -Thymus|0.0 -Thyroid|0.0 -Urinary Bladder Contents|0.0 -Uterus|0.0 -Fetus|0.0 -Placenta|0.0 -Total Body|0.0 -[END KINETIC DATA] -[END 25g Mouse] -[END MODELS] -[END CASE FILE] diff --git a/pytheranostics/data/preclinical/phantomdata/PhantomMasses.xlsx b/pytheranostics/data/preclinical/phantomdata/PhantomMasses.xlsx deleted file mode 100644 index f3f6d44caa569fca47812f2e1dd10a7842d88e2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17557 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�Hn> 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 pandas.DataFrame: """Load the S-value table for a gender/radionuclide pair.""" - relative_path = f"dosimetry/phantomdata/{radionuclide}-{gender}-Svalues.csv" + relative_path = f"s-values/organ/{radionuclide}-{gender}-Svalues.csv" try: with resource_path("pytheranostics.data", relative_path) as path_to_sv: s_df = pandas.read_csv(path_to_sv) @@ -25,7 +25,7 @@ def load_s_values(gender: str, radionuclide: str) -> pandas.DataFrame: def load_phantom_mass(gender: str, organ: str) -> float: """Return the ICRP phantom mass for the requested organ and gender.""" with resource_path( - "pytheranostics.data", "dosimetry/phantomdata/human_phantom_masses.csv" + "pytheranostics.data", "phantom/human/human_phantom_masses.csv" ) as phantom_data_path: masses = pandas.read_csv(phantom_data_path) diff --git a/pytheranostics/dosimetry/organ_s_dosimetry.py b/pytheranostics/dosimetry/organ_s_dosimetry.py index 90f082a..062cbec 100644 --- a/pytheranostics/dosimetry/organ_s_dosimetry.py +++ b/pytheranostics/dosimetry/organ_s_dosimetry.py @@ -58,7 +58,7 @@ def check_mandatory_fields_organ(self) -> None: def _load_human_mass_table() -> pandas.DataFrame: """Load the reference human phantom masses.""" with resource_path( - "pytheranostics.data", "dosimetry/phantomdata/human_phantom_masses.csv" + "pytheranostics.data", "phantom/human/human_phantom_masses.csv" ) as masses_path: masses = pandas.read_csv(masses_path, index_col=0) return masses @@ -662,7 +662,7 @@ def create_Olinda_file(self, dirname: str, savefile: bool = False) -> None: return with resource_path( - "pytheranostics.data", f"dosimetry/olindaTemplates/{template_file}" + "pytheranostics.data", f"olinda/templates/human/{template_file}" ) as template_path: template = pandas.read_csv(template_path) diff --git a/pytheranostics/preclinical_dosimetry/biodose.py b/pytheranostics/preclinical_dosimetry/biodose.py index ebd90cb..1b93d57 100644 --- a/pytheranostics/preclinical_dosimetry/biodose.py +++ b/pytheranostics/preclinical_dosimetry/biodose.py @@ -19,11 +19,6 @@ from pytheranostics.shared.resources import resource_path -def _preclinical_resource(relative_path: str): - """Resolve a path inside the packaged preclinical data directory.""" - return resource_path("pytheranostics.data", f"preclinical/{relative_path}") - - def _data_resource(relative_path: str): return resource_path("pytheranostics.data", relative_path) @@ -415,8 +410,8 @@ def tumor_sink_effect_correction(self, df): def phantom_data(self): """Load the reference phantom masses and reconcile them with biodistribution organs.""" if "mouse" in self.phantom.lower(): - with _preclinical_resource( - "phantomdata/mouse_phantom_masses.csv" + with _data_resource( + "phantom/mouse/mouse_phantom_masses.csv" ) as phantom_path: self.phantom_mass = pd.read_csv(phantom_path) # elif 'human' in self.phantom.lower(): @@ -475,14 +470,14 @@ def remainder_body_uptake(self, tumor_name=None): # These organs that are not modelled in the phantom are now going to be scaled using mass information from the literature: if "mouse" in self.phantom.lower(): - with _preclinical_resource( - "phantomdata/mouse_notinphantom_masses.csv" + with _data_resource( + "phantom/mouse/mouse_notinphantom_masses.csv" ) as lit_path: self.literature_mass = pd.read_csv(lit_path) elif "human" in self.phantom.lower(): - with _preclinical_resource( - "phantomdata/human_notinphantom_masses.csv" + with _data_resource( + "phantom/human/human_notinphantom_masses.csv" ) as lit_path: self.literature_mass = pd.read_csv(lit_path) @@ -722,7 +717,7 @@ def create_mousecase( The result is stored in ``self.mousecase`` and can optionally be persisted. """ filename = self.phantom.lower() + ".cas" - with _preclinical_resource(f"olindaTemplates/{filename}") as template_path: + with _data_resource(f"olinda/templates/mouse/{filename}") as template_path: template = pd.read_csv(template_path) template.columns = ["Data"] @@ -855,15 +850,15 @@ def create_human(self, tumor_name=None): "Tumor", axis=0 ) - with _preclinical_resource( - "phantomdata/human_phantom_masses.csv" + with _data_resource( + "phantom/human/human_phantom_masses.csv" ) as human_mass_path: human.phantom_mass = pd.read_csv(human_mass_path) human.phantom_mass.set_index("Organ", inplace=True) human.phantom_mass.sort_index(inplace=True) - with _preclinical_resource( - "phantomdata/human_notinphantom_masses.csv" + with _data_resource( + "phantom/human/human_notinphantom_masses.csv" ) as human_lit_path: human.literature_mass = pd.read_csv(human_lit_path) human.literature_mass.set_index("Organ", inplace=True) @@ -943,7 +938,7 @@ def create_human(self, tumor_name=None): def apply_relative_mass_scaling(self, mouse_mass=25): """Apply relative mass scaling factors to mouse disintegrations.""" - with _preclinical_resource("phantomdata/rMSF_factor.csv") as rmsf_path: + with _data_resource("phantom/mouse/rMSF_factor.csv") as rmsf_path: rMSF_data = pd.read_csv(rmsf_path, index_col=0) female_mass_sum = rMSF_data.loc[self.not_inphantom_notumor, "Female"].sum() @@ -987,8 +982,8 @@ def create_humancase(self, df, method, savefile=False, dirname="./"): template_filename = ( "adult_male.cas" if self.sex == "Male" else "adult_female.cas" ) - with _preclinical_resource( - f"olindaTemplates/{template_filename}" + with _data_resource( + f"olinda/templates/human/{template_filename}" ) as template_path: template = pd.read_csv(template_path) From 60a3508aff9ff929bd62c6a4c3662bf3e52b7e0a Mon Sep 17 00:00:00 2001 From: Carlos Uribe Date: Sat, 8 Nov 2025 20:48:33 -0800 Subject: [PATCH 06/11] add missing docstrings to comply with the docstring checks in github actions which initially failed --- docs/source/conf.py | 1 + .../extensions/sphinx_github_contributors.py | 2 + pytheranostics/calibrations/__init__.py | 1 + pytheranostics/calibrations/gamma_camera.py | 8 +- pytheranostics/dicomtools/__init__.py | 1 + pytheranostics/dicomtools/dicomtools.py | 86 ++++--------------- pytheranostics/dosimetry/dosiomicsclass.py | 7 ++ pytheranostics/dosimetry/image_analysis.py | 13 ++- pytheranostics/dosimetry/mc.py | 7 ++ pytheranostics/fits/__init__.py | 1 + pytheranostics/fits/fits.py | 10 +-- pytheranostics/fits/functions.py | 8 ++ pytheranostics/plots/__init__.py | 1 + pytheranostics/plots/plots.py | 34 +++----- pytheranostics/qc/__init__.py | 1 + pytheranostics/qc/dosecal_qc.py | 9 +- pytheranostics/qc/planar_qc.py | 7 +- pytheranostics/qc/qc.py | 20 ++--- pytheranostics/qc/spect_qc.py | 7 +- pytheranostics/registration/demons.py | 27 +----- pytheranostics/segmentation/__init__.py | 1 + pytheranostics/segmentation/tools.py | 3 + pytheranostics/shared/__init__.py | 1 + pytheranostics/shared/corrections.py | 5 +- pytheranostics/shared/evaluation_metrics.py | 6 +- pytheranostics/shared/radioactive_decay.py | 5 +- setup_dev.py | 1 + tests/conftest.py | 1 - 28 files changed, 135 insertions(+), 139 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 735d341..e6201ff 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -84,6 +84,7 @@ def setup(app): + """Register a fallback contributors directive when the extension is missing.""" if _contributors_available: return diff --git a/docs/source/extensions/sphinx_github_contributors.py b/docs/source/extensions/sphinx_github_contributors.py index 51b3df2..b1d6952 100644 --- a/docs/source/extensions/sphinx_github_contributors.py +++ b/docs/source/extensions/sphinx_github_contributors.py @@ -5,6 +5,7 @@ def fetch_github_contributors(app): + """Fetch contributors via the GitHub API and write a simple RST list.""" username = app.config.github_username repository = app.config.github_repository output_file = app.config.contributors_output_file @@ -40,6 +41,7 @@ def fetch_github_contributors(app): def setup(app): + """Register config values and connect the fetch hook.""" app.add_config_value("github_username", None, "env") app.add_config_value("github_repository", None, "env") app.add_config_value("contributors_output_file", "../contributors.rst", "env") diff --git a/pytheranostics/calibrations/__init__.py b/pytheranostics/calibrations/__init__.py index e69de29..3731650 100644 --- a/pytheranostics/calibrations/__init__.py +++ b/pytheranostics/calibrations/__init__.py @@ -0,0 +1 @@ +"""PyTheranostics package.""" diff --git a/pytheranostics/calibrations/gamma_camera.py b/pytheranostics/calibrations/gamma_camera.py index cb44ce0..39013b3 100644 --- a/pytheranostics/calibrations/gamma_camera.py +++ b/pytheranostics/calibrations/gamma_camera.py @@ -1,3 +1,5 @@ +"""Gamma camera calibration utilities.""" + import json import math import pprint @@ -17,11 +19,14 @@ class GammaCamera(PlanarQC): + """Encapsulate gamma camera QC and sensitivity calculations.""" def __init__(self, isotope, dicomfile, db_dic, cal_type="planar"): + """Initialize the planar QC base class and load site metadata.""" super().__init__(isotope, dicomfile, db_dic=db_dic, cal_type=cal_type) def get_sensitivity(self, source_id="C", **kwargs): + """Calculate camera sensitivity for the provided calibration source.""" # ser_date = self.ds.SeriesDate # ser_time = self.ds.SeriesTime @@ -195,7 +200,7 @@ def get_sensitivity(self, source_id="C", **kwargs): pprint.pprint(self.cal_dic) def calculate_uncertainty(self, site_id, camera_model, uncertainty_activity): - + """Compute calibration factor and sensitivity uncertainties.""" u_prim_list = [] for detector in ["Detector1", "Detector2"]: u_pw = math.sqrt(self.win_check["photopeak"]["counts"][detector]) @@ -231,6 +236,7 @@ def calculate_uncertainty(self, site_id, camera_model, uncertainty_activity): return uncertainty_cf, uncertainty_sensitivity def calfactor_to_database(self, **kwargs): + """Persist calibration factors to the shared JSON database.""" if "site_id" in kwargs: site_id = kwargs["site_id"] diff --git a/pytheranostics/dicomtools/__init__.py b/pytheranostics/dicomtools/__init__.py index e69de29..1ab4f90 100644 --- a/pytheranostics/dicomtools/__init__.py +++ b/pytheranostics/dicomtools/__init__.py @@ -0,0 +1 @@ +"""DICOM utilities exposed at the package level.""" diff --git a/pytheranostics/dicomtools/dicomtools.py b/pytheranostics/dicomtools/dicomtools.py index 197f6e6..cf837d8 100644 --- a/pytheranostics/dicomtools/dicomtools.py +++ b/pytheranostics/dicomtools/dicomtools.py @@ -1,3 +1,5 @@ +"""Utility functions for reading and modifying nuclear medicine DICOM files.""" + import time from datetime import datetime from pathlib import Path @@ -14,8 +16,10 @@ class DicomModify: + """Helper that edits DICOM headers/pixel data for quantitative SPECT studies.""" def __init__(self, fname, CF): + """Load the DICOM file and store calibration info.""" self.ds = pydicom.dcmread(fname) self.CF = CF self.fname = fname @@ -35,6 +39,7 @@ def make_bqml_suv( radiopharmaceutical="Lutetium-PSMA-617", n_detectors=2, ): + """Convert raw counts to BQML/SUV units and update the header accordingly.""" # Half-life is in seconds # Siemens has an issue setting up the times. We are using the Acquisition time which is the time of the start of the last bed to harmonize. @@ -186,30 +191,18 @@ def make_bqml_suv( return inj_df def save(self): + """Persist the modified dataset alongside the original file.""" self.ds.save_as(f"{self.fname.split('.dcm')[0]}_out.dcm") def dicom_slope_intercept(img): - """This function calculates the slope and intercept for a DICOM image in the way that GE does it. - - GE PET images are stored in DICOM files that are signed int16. This allows for a maximum value of 32767. - The slope is calculated such that the maximum value in the pixel array (before multiplying by slope) is 32767. - - Parameters - ---------- - img: numpy array - contains the float values of the image (e.g. MBq/ml in our case) - - Returns - ------- - slope: float - The slope to be set in the dicom header - - intercept: float - The intercept for the dicom header + """Calculate GE-style slope/intercept for converting floats to signed int16. + GE PET images are stored as signed int16 values with magnitude limited to + 32767. The computed slope ensures the largest absolute voxel value in the + floating-point array (e.g., MBq/mL) maps to this range once quantized, while + the intercept remains zero (GE convention). """ - max_val = np.max(img) min_val = np.min(img) @@ -228,26 +221,7 @@ def generate_basic_dcm_tags( date: str, time: str, ) -> List[Any]: - """This function generates basic DICOM tags. Useful to build simple DICOM datasets from images. - - Parameters - ---------- - img_size: - - slice_thickness: - - name: - - direction: - - date: - - time: - - Returns - ------- - series_tag_values: - """ + """Generate the minimal tag set needed for a synthetic DICOM series.""" series_tag_values = [ ("0008|0031", time), # Series Time ("0008|0021", date), # Series Date @@ -293,26 +267,14 @@ def numpy_to_dcm_basic( patien_name: str = "Patient", scale: int = 1, ) -> None: - """Write a numpy array as a .dcm image for visualization purposes. Borrowed from: - - R. Fedrigo, et al., “Development of the Lymphatic System in the 4D XCAT Phantom for - Improved Multimodality Imaging Research”, J. Nuc. Med., vol. 62, publication 113, 2021. - - Parameters - ---------- - array: - - voxel_spacing: - - output_dir: - - scale: - - Returns: - None + """Write a NumPy array as a basic DICOM series for visualization/testing. + Notes + ----- + Adapted from: R. Fedrigo et al., "Development of the Lymphatic System in the + 4D XCAT Phantom for Improved Multimodality Imaging Research," J. Nucl. Med., + 62, 113 (2021). """ - # Create SimpleITK image from array array = array * scale sitk_image = SimpleITK.GetImageFromArray(array.astype(np.int16)) @@ -371,17 +333,7 @@ def numpy_to_dcm_basic( def sitk_load_dcm_series(dcm_dir: Path) -> SimpleITK.Image: - """Load Series from DICOM folder, and return SITK image - - Parameters - ---------- - dcm_dir: - - Returns - -------- - dicom_dataset: - """ - + """Load a DICOM series using SimpleITK and return it as an image volume.""" reader = SimpleITK.ImageSeriesReader() dcm_file_names = reader.GetGDCMSeriesFileNames(str(dcm_dir)) reader.SetFileNames(dcm_file_names) diff --git a/pytheranostics/dosimetry/dosiomicsclass.py b/pytheranostics/dosimetry/dosiomicsclass.py index 840931e..8b6c5d9 100644 --- a/pytheranostics/dosimetry/dosiomicsclass.py +++ b/pytheranostics/dosimetry/dosiomicsclass.py @@ -1,3 +1,5 @@ +"""Radiomics feature extraction utilities.""" + from __future__ import print_function import os @@ -9,7 +11,10 @@ class Radiomics: + """Generate radiomics features for longitudinal studies.""" + def __init__(self, imagemodality, patient_id, cycle, image, mask, organslist): + """Store study metadata, image arrays, and ROI masks.""" self.imagemodality = imagemodality self.patient_id = patient_id self.cycle = cycle @@ -18,6 +23,7 @@ def __init__(self, imagemodality, patient_id, cycle, image, mask, organslist): self.organslist = organslist def prepareimages(self): + """Export the image and ROI masks to NRRD files for PyRadiomics.""" img = sitk.GetImageFromArray(self.image) sitk.WriteImage( @@ -33,6 +39,7 @@ def prepareimages(self): ) def featureextractor(self): + """Run PyRadiomics using the configured parameter set.""" paramPath = os.path.join("..", "data", "Params.yaml") extractor = featureextractor.RadiomicsFeatureExtractor(paramPath) diff --git a/pytheranostics/dosimetry/image_analysis.py b/pytheranostics/dosimetry/image_analysis.py index 80a40e8..5a938fb 100644 --- a/pytheranostics/dosimetry/image_analysis.py +++ b/pytheranostics/dosimetry/image_analysis.py @@ -1,3 +1,5 @@ +"""Visualization and summary utilities for volumetric dosimetry images.""" + import gatetools as gt import itk import matplotlib.pyplot as plt @@ -5,7 +7,10 @@ class Image: + """Convenience wrapper to compute statistics on organ masks.""" + def __init__(self, df, patient_id, cycle, image, roi_masks_resampled): + """Store metadata, image array, and resampled ROI masks.""" self.df = df self.patient_id = patient_id self.cycle = int(cycle) @@ -14,6 +19,7 @@ def __init__(self, df, patient_id, cycle, image, roi_masks_resampled): self.organlist = self.roi_masks_resampled.keys() def SPECT_image_array(self, SPECT, scalefactor, xspacing, yspacing, zspacing): + """Convert a DICOM SPECT series into an MBq volume and cache it.""" SPECT_image = SPECT.pixel_array SPECT_image = np.transpose( SPECT_image, (1, 2, 0) @@ -35,6 +41,7 @@ def SPECT_image_array(self, SPECT, scalefactor, xspacing, yspacing, zspacing): return SPECTMBq def image_visualisation(self, image): + """Display three orthogonal slices for quick sanity checks.""" fig, axs = plt.subplots(1, 3, figsize=(15, 5)) axs[0].imshow(image[:, :, 50]) @@ -48,7 +55,7 @@ def image_visualisation(self, image): plt.show() def show_mean_statistics(self, output): - + """Compute mean activity per organ and store in the dataframe.""" self.ad_mean = {} for organ in self.organlist: mask = self.roi_masks_resampled[organ] @@ -58,12 +65,14 @@ def show_mean_statistics(self, output): self.df[output] = self.df["Contour"].map(self.ad_mean) def show_max_statistics(self): + """Print the maximum activity observed within each organ mask.""" for organ in self.organlist: mask = self.roi_masks_resampled[organ] x = self.image[mask].max() print(f"{organ}", x) def add(self, output): + """Compute total activity per organ and map it back into the dataframe.""" self.sum = {} for organ in self.organlist: mask = self.roi_masks_resampled[organ] @@ -73,6 +82,7 @@ def add(self, output): self.df[output] = self.df["Contour"].map(self.sum) def voxels_and_volume(self, output1, output2, voxel_volume): + """Record nonzero voxel counts and physical volumes per organ.""" self.no_voxels = {} self.volume = {} for organ in self.organlist: @@ -84,6 +94,7 @@ def voxels_and_volume(self, output1, output2, voxel_volume): self.df[output2] = self.df["Contour"].map(self.volume) def dose_volume_histogram(self): + """Generate and log dose-volume histograms for each organ.""" doseimage = self.image.astype(float) doseimage = itk.image_from_array(doseimage) diff --git a/pytheranostics/dosimetry/mc.py b/pytheranostics/dosimetry/mc.py index 24b0b1e..82a5f66 100644 --- a/pytheranostics/dosimetry/mc.py +++ b/pytheranostics/dosimetry/mc.py @@ -1,13 +1,19 @@ +"""Helpers to orchestrate Monte Carlo batch jobs.""" + import os class MonteCarlo: + """Split and execute Monte Carlo runs across multiple CPUs.""" + def __init__(self, n_cpu, n_primaries, output_dir): + """Store execution parameters for the simulation batch.""" self.n_cpu = n_cpu self.n_primaries = n_primaries self.output_dir = output_dir def split_simulations(self): + """Split total primaries across CPUs and write per-core macro files.""" n_primaries_per_mac = int(self.n_primaries / self.n_cpu) with open("./main_template.mac", "r") as mac_file: @@ -26,6 +32,7 @@ def split_simulations(self): output_mac.write(new_mac) def run_MC(self): + """Invoke the shell script that runs the Monte Carlo jobs.""" os.system( f"bash {self.output_dir}/runsimulation1.sh {self.output_dir} {self.n_cpu}" ) diff --git a/pytheranostics/fits/__init__.py b/pytheranostics/fits/__init__.py index e69de29..3731650 100644 --- a/pytheranostics/fits/__init__.py +++ b/pytheranostics/fits/__init__.py @@ -0,0 +1 @@ +"""PyTheranostics package.""" diff --git a/pytheranostics/fits/fits.py b/pytheranostics/fits/fits.py index e99b27c..a5cc94c 100644 --- a/pytheranostics/fits/fits.py +++ b/pytheranostics/fits/fits.py @@ -1,3 +1,5 @@ +"""Curve-fitting utilities built on top of lmfit.""" + from typing import Any, Callable, Dict, Optional, Tuple import lmfit @@ -69,7 +71,6 @@ def exponential_fit_lmfit( - For bi-exponential: B1 = -A1 - For tri-exponential: C1 = -(A1 + B1) """ - if num_exponentials not in [1, 2, 3]: raise ValueError( f"num_exponentials must be 1, 2, or 3., found {num_exponentials}" @@ -172,8 +173,7 @@ def fitted_model(x): def calculate_r_squared( time: numpy.ndarray, activity: numpy.ndarray, popt: numpy.ndarray, func: Callable ) -> Tuple[float, numpy.ndarray]: - """Calculate r_squared and residuals between fit and data-points.""" - + """Calculate r-squared and residuals between the fit and data points.""" residuals = activity - func(time, *popt) ss_res = numpy.sum(residuals**2) @@ -186,9 +186,7 @@ def calculate_r_squared( def get_exponential( order: int, param_init: Optional[Tuple[float, ...]], decayconst: float ) -> Tuple[Callable, Tuple[float, ...], Optional[Tuple[Any, ...]]]: - """Retrieve an exponential function given an input order 'order', initial parameters and a decay-constant - value (for defatult constrains)""" - + """Retrieve an exponential model, default parameters, and bounds.""" # Default initial parameters: default_initial = { 1: (1, 1), diff --git a/pytheranostics/fits/functions.py b/pytheranostics/fits/functions.py index 04af59f..513b849 100644 --- a/pytheranostics/fits/functions.py +++ b/pytheranostics/fits/functions.py @@ -1,3 +1,5 @@ +"""Elementary exponential functions used by the fitting module.""" + import math import numpy @@ -6,31 +8,37 @@ # Function definitions def monoexp_fun(x: numpy.ndarray, a: float, b: float) -> numpy.ndarray: + """Return a single exponential evaluated at ``x``.""" return a * exp(-b * x) def biexp_fun( x: numpy.ndarray, a: float, b: float, c: float, d: float ) -> numpy.ndarray: + """Return the sum of two exponential decay terms.""" return a * exp(-b * x) + c * exp(-d * x) def biexp_fun_uptake(x: numpy.ndarray, a: float, b: float, c: float) -> numpy.ndarray: + """Return the uptake-style biexponential curve.""" return a * exp(-b * x) - a * exp(-c * x) def triexp_fun( x: numpy.ndarray, a: float, b: float, c: float, d: float, f: float ) -> numpy.ndarray: + """Return the tri-exponential washout model.""" return a * exp(-b * x) + c * exp(-d * x) - (a + c) * exp(-f * x) def find_a_initial( f: numpy.ndarray, b: numpy.ndarray, t: numpy.ndarray ) -> numpy.ndarray: + """Estimate the initial amplitude given decay constants and time samples.""" return f * exp(b * t) # TODO: Review Hanscheid inputs/outputs. def Hanscheid(a, t): + """Compute the Hanscheid approximation for cumulated activity.""" return a * ((2 * t) / (math.log(2))) diff --git a/pytheranostics/plots/__init__.py b/pytheranostics/plots/__init__.py index e69de29..3731650 100644 --- a/pytheranostics/plots/__init__.py +++ b/pytheranostics/plots/__init__.py @@ -0,0 +1 @@ +"""PyTheranostics package.""" diff --git a/pytheranostics/plots/plots.py b/pytheranostics/plots/plots.py index bee8c8d..1d9f1ec 100644 --- a/pytheranostics/plots/plots.py +++ b/pytheranostics/plots/plots.py @@ -1,3 +1,5 @@ +"""Plotting utilities for PyTheranostics workflows.""" + from pathlib import Path from typing import Optional @@ -30,7 +32,6 @@ def ewin_montage(img: numpy.ndarray, ewin: dict) -> None: - Colorbars are added to each subplot. - The layout is automatically adjusted using tight_layout(). """ - plt.figure(figsize=(22, 6)) for ind, i in enumerate(range(0, int(img.shape[0]), 2)): keys = list(ewin.keys()) @@ -58,22 +59,8 @@ def plot_tac_residuals( y_label: str = "Activity [MBq]", output_dir: Optional[Path] = None, ) -> None: - """Plot Time activity curve and residuals. - - Parameters - ---------- - result : lmfit.model.ModelResult - The fitted lmfit model results. - region : str - The region (e.g., organ, tumor) where fit happened. - x_label: str - The label in X Axis. Defaults to "Time [hr]" - y_label: str - The label in Y Axis. Defaults to "Activity [MBq]" - output_dir: Optional[str] - A path to a directory where figure will be saved. - """ - + """Plot time-activity curve and residuals.""" + # Create a figure with 3 subplots # Create a figure with 3 subplots _, axs = plt.subplots(1, 3, figsize=(12, 4), constrained_layout=True) @@ -109,15 +96,15 @@ def plot_tac_residuals( # Plot fitted model ax1.plot(x_fit, y_fit, color="red") ax1.set_xlim(left=0) # Start x-axis from zero - ax1.set_xlim(right = x_data[-1] * 2) # Start y-axis from zero + ax1.set_xlim(right=x_data[-1] * 2) # Start y-axis from zero ax1.set_ylim(bottom=0) # Start y-axis from zero ax1.set_title(region) ax1.set_xlabel(x_label) ax1.set_ylabel(y_label) # Add R-squared and AIC as text try: - ax1.text(0.7, 0.9, f'$R^2={result.rsquared:.3f}$', transform=ax1.transAxes) - ax1.text(0.7, 0.85, f'AIC={result.aic:.3f}', transform=ax1.transAxes) + ax1.text(0.7, 0.9, f"$R^2={result.rsquared:.3f}$", transform=ax1.transAxes) + ax1.text(0.7, 0.85, f"AIC={result.aic:.3f}", transform=ax1.transAxes) except AttributeError: pass # Remove legend if present @@ -150,7 +137,12 @@ def plot_tac_residuals( ax3.set_ylabel("Residuals") if output_dir is not None: - plt.savefig(output_dir / f"{region}_fit_Cycle_0{cycle}.png", format="png", bbox_inches="tight", dpi=300) + plt.savefig( + output_dir / f"{region}_fit_Cycle_0{cycle}.png", + format="png", + bbox_inches="tight", + dpi=300, + ) plt.show() diff --git a/pytheranostics/qc/__init__.py b/pytheranostics/qc/__init__.py index e69de29..3731650 100644 --- a/pytheranostics/qc/__init__.py +++ b/pytheranostics/qc/__init__.py @@ -0,0 +1 @@ +"""PyTheranostics package.""" diff --git a/pytheranostics/qc/dosecal_qc.py b/pytheranostics/qc/dosecal_qc.py index 3ac002e..35ceab6 100644 --- a/pytheranostics/qc/dosecal_qc.py +++ b/pytheranostics/qc/dosecal_qc.py @@ -1,3 +1,5 @@ +"""Quality-control routines for dose calibrator submissions.""" + import numpy as np from pytheranostics.qc.qc import QC @@ -6,12 +8,14 @@ class DosecalQC(QC): + """Perform QC checks for dose calibrator calibration data.""" def __init__(self, isotope, db_dic, cal_type="dc"): + """Initialize the QC helper with isotope metadata and DB extracts.""" super().__init__(isotope, db_dic=db_dic, cal_type=cal_type) def check_calibration(self, accepted_percent=1.5, accepted_recovery=(97, 103)): - + """Run the full calibration workflow and append findings to the summary.""" # keep a flag to accept or reject depending on the different tests. Default is to accept (1): # If something fails it will be changed to 2 if needs to verify and 3 if it completely fails @@ -131,6 +135,7 @@ def check_calibration(self, accepted_percent=1.5, accepted_recovery=(97, 103)): self.print_summary() def check_source_decay(self, accepted_percent): + """Validate decay corrections for each shipped source.""" # find the shipped sources sources = self.db_df["shipped_data"].source_id.unique() @@ -194,7 +199,7 @@ def check_source_decay(self, accepted_percent): ) def check_syringe_recovery(self, syringe_name="syringe_20_mL"): - + """Verify the syringe recovery curve stays within allowed tolerances.""" self.db_df["cal_data"].loc[ self.db_df["cal_data"].source_id == syringe_name, "syringe_activity_calculated", diff --git a/pytheranostics/qc/planar_qc.py b/pytheranostics/qc/planar_qc.py index 6f33acb..176abb9 100644 --- a/pytheranostics/qc/planar_qc.py +++ b/pytheranostics/qc/planar_qc.py @@ -1,16 +1,20 @@ +"""QC checks for planar acquisitions.""" + import pydicom from pytheranostics.qc.qc import QC class PlanarQC(QC): + """QC checks specific to planar acquisitions.""" def __init__(self, isotope, dicomfile, db_dic, cal_type="planar"): + """Load the planar DICOM file and associated calibration forms.""" super().__init__(isotope, db_dic=db_dic, cal_type=cal_type) self.ds = pydicom.dcmread(dicomfile) def check_windows_energy(self): - + """Run the planar QC workflow and populate the summary.""" self.append_to_summary(f"QC for planar scan of {self.isotope}:\n\n") self.check_camera_parameters() @@ -18,6 +22,7 @@ def check_windows_energy(self): self.print_summary() def check_camera_parameters(self): + """Verify DICOM acquisition parameters match the expected protocol.""" camera_manufacturer = self.ds.Manufacturer camera_model = self.ds.ManufacturerModelName acquisition_date = self.ds.AcquisitionDate diff --git a/pytheranostics/qc/qc.py b/pytheranostics/qc/qc.py index b383b47..2e76b6f 100644 --- a/pytheranostics/qc/qc.py +++ b/pytheranostics/qc/qc.py @@ -1,3 +1,5 @@ +"""Shared utilities for QC workflows.""" + import json from io import StringIO from pathlib import Path @@ -13,17 +15,10 @@ class QC: - def __init__(self, isotope, **kwargs): - """ - - **kwargs: - db_dic: containing three keys - db_file - sheet_names (list) - header - site_id - """ + """Base class for QC workflows (planar, SPECT, dose calibrator).""" + def __init__(self, isotope, **kwargs): + """Load isotope metadata and qualifying site data required for QC.""" self.db_df = {} with open(ISOTOPE_DATA_FILE) as f: @@ -82,7 +77,7 @@ def __init__(self, isotope, **kwargs): self.db_df["shipped_data"] = ref_shipped def window_check(self, win_perdiff_max=2, type="planar"): - + """Compare configured energy windows against protocol tolerances.""" if type == "planar": ds = self.ds elif type == "spect": @@ -205,9 +200,11 @@ def window_check(self, win_perdiff_max=2, type="planar"): return window_check_df def append_to_summary(self, text): + """Append formatted text to the QC summary buffer.""" self.summary = self.summary + text def print_summary(self): + """Convert the text summary into a styled pandas DataFrame.""" # print(self.summary) summary = StringIO(self.summary) self.summary_df = pd.read_csv(summary, sep="\t") @@ -221,6 +218,7 @@ def print_summary(self): # print(self.summary) def update_db(self, syringe_name="syringe_20_mL"): + """Refresh calibration tables with decay-corrected references and recoveries.""" sources = self.db_df["shipped_data"].source_id.unique() centres = self.db_df["shipped_data"].site_id.unique() diff --git a/pytheranostics/qc/spect_qc.py b/pytheranostics/qc/spect_qc.py index a6b6ecc..9f774d4 100644 --- a/pytheranostics/qc/spect_qc.py +++ b/pytheranostics/qc/spect_qc.py @@ -1,3 +1,5 @@ +"""QC checks for SPECT projections and reconstructions.""" + import numpy as np import pydicom @@ -5,14 +7,16 @@ class SPECTQC(QC): + """QC checks for raw SPECT projections and reconstructed images.""" def __init__(self, isotope, projections_file, recon_file, db_dic, cal_type="spect"): + """Load projection and reconstruction DICOM datasets for QC.""" super().__init__(isotope, db_dic=db_dic, cal_type=cal_type) self.proj_ds = pydicom.dcmread(projections_file) self.recon_ds = pydicom.dcmread(recon_file) def check_projs(self): - + """Run the full QC pipeline for projections and reconstructed images.""" self.window_check_df = {} self.append_to_summary(f"QC for SPECT RAW DATA of {self.isotope}:\n\n") @@ -26,6 +30,7 @@ def check_projs(self): self.print_summary() def check_camera_parameters(self, ds, projs=True): + """Append camera parameter checks for either projection or reconstruction.""" camera_manufacturer = ds.Manufacturer camera_model = ds.ManufacturerModelName acquisition_date = ds.AcquisitionDate diff --git a/pytheranostics/registration/demons.py b/pytheranostics/registration/demons.py index 4e59294..f7295c1 100644 --- a/pytheranostics/registration/demons.py +++ b/pytheranostics/registration/demons.py @@ -1,15 +1,10 @@ +"""Multiscale Demons registration helpers.""" + import SimpleITK def smooth_and_resample(image, shrink_factor, smoothing_sigma): - """ - Args: - image: The image we want to resample. - shrink_factor: A number greater than one, such that the new image's size is original_size/shrink_factor. - smoothing_sigma: Sigma for Gaussian smoothing, this is in physical (image spacing) units, not pixels. - Return: - Image which is a result of smoothing the input and then resampling it using the given sigma and shrink factor. - """ + """Gaussian smooth an image and resample it by the given shrink factor.""" smoothed_image = SimpleITK.SmoothingRecursiveGaussian(image, smoothing_sigma) original_spacing = image.GetSpacing() @@ -42,21 +37,7 @@ def multiscale_demons( shrink_factors=None, smoothing_sigmas=None, ): - """ - Run the given registration algorithm in a multiscale fashion. The original scale should not be given as input as the - original images are implicitly incorporated as the base of the pyramid. - Args: - registration_algorithm: Any registration algorithm that has an Execute(fixed_image, moving_image, displacement_field_image) - method. - fixed_image: Resulting transformation maps points from this image's spatial domain to the moving image spatial domain. - moving_image: Resulting transformation maps points from the fixed_image's spatial domain to this image's spatial domain. - initial_transform: Any SimpleITK transform, used to initialize the displacement field. - shrink_factors: Shrink factors relative to the original image's size. - smoothing_sigmas: Amount of smoothing which is done prior to resmapling the image using the given shrink factor. These - are in physical (image spacing) units. - Returns: - SimpleITK.DisplacementFieldTransform - """ + """Run a multiscale Demons registration on the provided fixed/moving pair.""" # Create image pyramid. fixed_images = [fixed_image] moving_images = [moving_image] diff --git a/pytheranostics/segmentation/__init__.py b/pytheranostics/segmentation/__init__.py index e69de29..3731650 100644 --- a/pytheranostics/segmentation/__init__.py +++ b/pytheranostics/segmentation/__init__.py @@ -0,0 +1 @@ +"""PyTheranostics package.""" diff --git a/pytheranostics/segmentation/tools.py b/pytheranostics/segmentation/tools.py index e11d7c3..f18b29b 100644 --- a/pytheranostics/segmentation/tools.py +++ b/pytheranostics/segmentation/tools.py @@ -1,7 +1,10 @@ +"""Helpers for working with RT structure sets.""" + from rt_utils import RTStructBuilder def rtst_to_mask(dicom_series_path, rt_struct_path): + """Load an RTSTRUCT and return a dict of ROI masks keyed by ROI name.""" # Load existing RT Struct. Requires the series path and existing RT Struct path rtstruct = RTStructBuilder.create_from( dicom_series_path=dicom_series_path, rt_struct_path=rt_struct_path diff --git a/pytheranostics/shared/__init__.py b/pytheranostics/shared/__init__.py index e69de29..3731650 100644 --- a/pytheranostics/shared/__init__.py +++ b/pytheranostics/shared/__init__.py @@ -0,0 +1 @@ +"""PyTheranostics package.""" diff --git a/pytheranostics/shared/corrections.py b/pytheranostics/shared/corrections.py index adf6dd2..28b98f7 100644 --- a/pytheranostics/shared/corrections.py +++ b/pytheranostics/shared/corrections.py @@ -1,5 +1,8 @@ -def tew_scatt(window_dic): +"""Scatter-correction helpers.""" + +def tew_scatt(window_dic): + """Apply triple-energy-window scatter correction to window counts.""" Cp = {} ls_width = window_dic["low_scatter"]["width"] diff --git a/pytheranostics/shared/evaluation_metrics.py b/pytheranostics/shared/evaluation_metrics.py index c3f34ee..4fd7b58 100644 --- a/pytheranostics/shared/evaluation_metrics.py +++ b/pytheranostics/shared/evaluation_metrics.py @@ -1,6 +1,8 @@ +"""Simple evaluation metrics used in QC reports.""" + import numpy as np def perc_diff(measured_value, expected_value, decimals=2): - - return np.round((measured_value - expected_value) / expected_value * 100, 2) + """Return the percent difference between measured and expected values.""" + return np.round((measured_value - expected_value) / expected_value * 100, decimals) diff --git a/pytheranostics/shared/radioactive_decay.py b/pytheranostics/shared/radioactive_decay.py index 7c2223c..d259ec8 100644 --- a/pytheranostics/shared/radioactive_decay.py +++ b/pytheranostics/shared/radioactive_decay.py @@ -1,9 +1,12 @@ +"""Radioactive decay helpers shared across modules.""" + from datetime import datetime import numpy as np def decay_act(a_initial, delta_t, half_life): + """Return decayed activity after `delta_t` given the half-life.""" if np.any(np.asarray(a_initial) < 0): raise ValueError("a_initial must be positive") if np.any(np.asarray(delta_t) < 0): @@ -23,7 +26,7 @@ def get_activity_at_injection( injection_time, half_life, ): - + """Compute injection datetime and activity from pre/post syringe readings.""" # Pass half-life in seconds # Set the times and the time deltas to injection time diff --git a/setup_dev.py b/setup_dev.py index 5bb8844..d9333b2 100644 --- a/setup_dev.py +++ b/setup_dev.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """ Development environment setup script. + Activate a virtual environment or conda environment before running. """ diff --git a/tests/conftest.py b/tests/conftest.py index fac2793..2406a63 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,6 @@ def pytest_collection_modifyitems(config, items): """Modify test collection to run smoke tests first.""" - # Separate smoke tests from other tests smoke_tests = [] other_tests = [] From 9b42cc80f6a2b30b1577af03155f1c9c7ec3ba91 Mon Sep 17 00:00:00 2001 From: Carlos Uribe Date: Sat, 8 Nov 2025 20:55:15 -0800 Subject: [PATCH 07/11] remove trailing whitespace in output.json --- pytheranostics/data/output.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytheranostics/data/output.json b/pytheranostics/data/output.json index 7ebdbdd..db61af9 100644 --- a/pytheranostics/data/output.json +++ b/pytheranostics/data/output.json @@ -4,8 +4,8 @@ "ClinicalTrial": "MyClinicalTrial", "Radionuclide": "NA", "PatientID": "NA", - "Gender": "NA", - + "Gender": "NA", + "No_of_completed_cycles": "NA", "Cycle_01": [ { From 38481a3dae88d085a4ebae28008a6f98216d3fe8 Mon Sep 17 00:00:00 2001 From: Carlos Uribe Date: Sat, 8 Nov 2025 21:02:29 -0800 Subject: [PATCH 08/11] Trigger GitHub action for checks only on PRs --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14fbd8d..69ff892 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,6 @@ name: CI on: - push: - branches: - - main - - dev pull_request: jobs: From 3bf1c84c3abed920b2cae0c06bd02331ad26e118 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 05:04:27 +0000 Subject: [PATCH 09/11] Initial plan From 2092c8a652eb7612bf02f5d4a46afb2eb6cf6fb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 05:12:07 +0000 Subject: [PATCH 10/11] Improve exception handling for RadiopharmaceuticalInformationSequence Co-authored-by: carluri <3673343+carluri@users.noreply.github.com> --- pytheranostics/imaging_tools/tools.py | 45 ++++++++++++++++----------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/pytheranostics/imaging_tools/tools.py b/pytheranostics/imaging_tools/tools.py index b57dd86..0be1243 100644 --- a/pytheranostics/imaging_tools/tools.py +++ b/pytheranostics/imaging_tools/tools.py @@ -78,24 +78,33 @@ def load_metadata(dir: str, modality: str) -> ImagingMetadata: radionuclide = modality.split("_")[0] # This only applies to Q-SPECT TODO: replace for something more generic. - try: - injected_activity = ( - dicom_slices[0] - .RadiopharmaceuticalInformationSequence[0] - .RadionuclideTotalDose - ) - - # Currently we don't have a way to know the units ... so we use common sense. - if injected_activity > 20000: # Activity likely in Bq instead of MBq - injected_activity /= 1e6 - print( - f"Injected activity found in DICOM Header: {injected_activity:2.1f} MBq. Please verify." - ) - - except (AttributeError, IndexError): - print( - "Injected activity not found in DICOM header. Using default: 7400 MBq" - ) + injected_activity = None + + if hasattr(dicom_slices[0], "RadiopharmaceuticalInformationSequence"): + rp_seq = dicom_slices[0].RadiopharmaceuticalInformationSequence + if len(rp_seq) > 0: + try: + injected_activity = rp_seq[0].RadionuclideTotalDose + + # Currently we don't have a way to know the units ... so we use common sense. + if injected_activity > 20000: # Activity likely in Bq instead of MBq + injected_activity /= 1e6 + print( + f"Injected activity found in DICOM Header: {injected_activity:2.1f} MBq. Please verify." + ) + except AttributeError: + # Sequence exists but RadionuclideTotalDose attribute is missing + print( + "RadiopharmaceuticalInformationSequence found but RadionuclideTotalDose is missing." + ) + else: + # Sequence exists but is empty - this may indicate a data quality issue + print( + "Warning: RadiopharmaceuticalInformationSequence is empty. This may indicate a data quality issue." + ) + + if injected_activity is None: + print("Using default injected activity: 7400 MBq") injected_activity = 7400.0 # Global attributes. Should be the same in all slices! From 8384299ad2bc312bdd3fda46727945a7bafce03e Mon Sep 17 00:00:00 2001 From: Carlos Uribe Date: Sun, 9 Nov 2025 06:11:49 -0800 Subject: [PATCH 11/11] reformatted as per black --- pytheranostics/imaging_tools/tools.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pytheranostics/imaging_tools/tools.py b/pytheranostics/imaging_tools/tools.py index 0be1243..0cb3538 100644 --- a/pytheranostics/imaging_tools/tools.py +++ b/pytheranostics/imaging_tools/tools.py @@ -79,15 +79,17 @@ def load_metadata(dir: str, modality: str) -> ImagingMetadata: # This only applies to Q-SPECT TODO: replace for something more generic. injected_activity = None - + if hasattr(dicom_slices[0], "RadiopharmaceuticalInformationSequence"): rp_seq = dicom_slices[0].RadiopharmaceuticalInformationSequence if len(rp_seq) > 0: try: injected_activity = rp_seq[0].RadionuclideTotalDose - + # Currently we don't have a way to know the units ... so we use common sense. - if injected_activity > 20000: # Activity likely in Bq instead of MBq + if ( + injected_activity > 20000 + ): # Activity likely in Bq instead of MBq injected_activity /= 1e6 print( f"Injected activity found in DICOM Header: {injected_activity:2.1f} MBq. Please verify." @@ -102,7 +104,7 @@ def load_metadata(dir: str, modality: str) -> ImagingMetadata: print( "Warning: RadiopharmaceuticalInformationSequence is empty. This may indicate a data quality issue." ) - + if injected_activity is None: print("Using default injected activity: 7400 MBq") injected_activity = 7400.0