diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74f08a6..d96f8c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v2 diff --git a/.readthedocs.yml b/.readthedocs.yml index fe015c5..e0ee73c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -8,7 +8,7 @@ formats: - epub python: - version: 3.8 + version: 3.12 install: - requirements: docs/requirements.txt - method: pip diff --git a/docs/requirements.txt b/docs/requirements.txt index 3dd64d0..05a06ce 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,5 @@ sphinx sphinx_rtd_theme myst_parser +nbsphinx +ipykernel diff --git a/docs/source/conf.py b/docs/source/conf.py index a568571..d3687dd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -40,6 +40,7 @@ "sphinx_rtd_theme", "sphinx.ext.autodoc", "sphinx.ext.napoleon", + "nbsphinx", ] default_dark_mode = True diff --git a/docs/source/getting_started/getting_started.ipynb b/docs/source/getting_started/getting_started.ipynb new file mode 100644 index 0000000..bc130a3 --- /dev/null +++ b/docs/source/getting_started/getting_started.ipynb @@ -0,0 +1,498 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c9a2425d-49a8-4060-8af9-a078a7ca7475", + "metadata": {}, + "source": [ + "# Getting started with PySkinDose\n", + "Welcome! This is a Jupyter Notebook based getting-started tutorial for PySkinDose, which contains tools and scripts for calculating estimates of peak skin dose and to create dose maps for fluoroscopic exams from RDSR data\n", + "\n", + "The tutorial will be updated when more functionality is added or modified.\n", + "\n", + "The latest version of the tutorial is available on GitHub at [https://github.com/rvbCMTS/PySkinDose/blob/master/docs/source/getting_started/getting_started.ipynb](https://github.com/rvbCMTS/PySkinDose/blob/master/docs/source/getting_started/getting_started.ipynb).\n", + "\n", + "PyPI: [https://pypi.org/project/pyskindose/](https://pypi.org/project/pyskindose/)\n", + "\n", + "Code repository: [https://github.com/rvbCMTS/PySkinDose](https://github.com/rvbCMTS/PySkinDose)\n", + "\n", + "Documentation: [https://pyskindose.readthedocs.io/en/latest/](https://pyskindose.readthedocs.io/en/latest/)\n" + ] + }, + { + "cell_type": "markdown", + "id": "65c80f08-d40f-41fe-8c8d-ebe6070dd3ec", + "metadata": {}, + "source": [ + "## PART I: Settings " + ] + }, + { + "cell_type": "markdown", + "id": "70d7cd1d", + "metadata": {}, + "source": [ + "This tutorial generates several interactive plots directly within the notebook. If you prefer to view them in a separate browser tab, simply uncomment the following two lines and set `settings.plot.notebook_mode` to False when running PySkinDose.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c4f3f56", + "metadata": {}, + "outputs": [], + "source": [ + "# import plotly.io as pio\n", + "# pio.renderers.default = \"browser\"" + ] + }, + { + "cell_type": "markdown", + "id": "85e68b59-b761-4ed1-bc2c-9ff2463d03f3", + "metadata": {}, + "source": [ + "We need to start our session with adding the class PyskindoseSettings for parsing user defined settings:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b61afc58-ec9f-48e2-9231-9a5a908e6a5c", + "metadata": {}, + "outputs": [], + "source": [ + "from pyskindose import (\n", + " PyskindoseSettings,\n", + " load_settings_example_json,\n", + " print_available_human_phantoms,\n", + " get_path_to_example_rdsr_files,\n", + " print_example_rdsr_files,\n", + ")\n", + "from pyskindose.main import main" + ] + }, + { + "cell_type": "markdown", + "id": "2de6a01c-a52e-4558-9bcd-58b3bb05126d", + "metadata": {}, + "source": [ + "Once completed, we need to initialize all the user defined settings. Lets load a template of pre-loaded settings (located in `settings_example.json`), then we can change each of the individual settings prior to calculating skin dose estimate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83490313-f4be-4ec0-8f8f-06d03a669226", + "metadata": {}, + "outputs": [], + "source": [ + "# Parse the settings to a setting class:\n", + "settings_json = load_settings_example_json()\n", + "settings = PyskindoseSettings(settings=settings_json)" + ] + }, + { + "cell_type": "markdown", + "id": "8547c150-7c47-4173-90ea-ea2ad9964270", + "metadata": {}, + "source": [ + "Now, all the settings are populated in `settings`. We can print all user defined setting by typing `settings.print_parameters()`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c96c974c-cb21-464c-8ce1-ec6790024939", + "metadata": {}, + "outputs": [], + "source": [ + "settings.print_parameters()" + ] + }, + { + "cell_type": "markdown", + "id": "07a55d3e-f007-4bff-82e1-165ecacfdb2d", + "metadata": {}, + "source": [ + "As seen in the above output, `settings` includes the sections __general__, __phantom__, __plot__ and __normalisation__. \n", + "\n", + "You can access each of the settings in __general__ by simply typing `settings.mode`, `settings.k_tab_val` etc. For the other sections, include the section name, e.g. `settings.phantom.patient_orientation`.\n", + "\n", + "You can read more about each setting by adressing the corresponding docstring with the `__doc__` attribute. E.g: `settings.__doc__` returns detailed descriptions of the setting in __general__, i.e., \"mode\", \"k_tab_val\", \"estimate_k_tab\", and \"rdsr_filename\". \n", + "\n", + "Uncomment any of the following lines for more information regarding each of the subsections in `settings`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f44c480-0a73-4921-96c6-b0a4c8f3c465", + "metadata": {}, + "outputs": [], + "source": [ + "# print(settings.__doc__) # uncomment this line to read about settings.general\n", + "# print(settings.phantom.__doc__) # uncomment this line to read about settings.phantom\n", + "# print(settings.phantom.patient_offset.__doc__) # uncomment this line to read about settings.phantom.patient_offset\n", + "# print(settings.phantom.dimension.__doc__) # uncomment this line to read about settings.phantom.dimension\n", + "# print(settings.plot.__doc__) # uncomment this line to read about settings.plot\n", + "# print(settings.normalization_settings.__doc__) # uncomment this line to read about settings.normalisation_settings" + ] + }, + { + "cell_type": "markdown", + "id": "fec17d93-48b5-4d54-9d14-09a415ca024f", + "metadata": {}, + "source": [ + "## PART II: Setup of the skin dose calculation geometry" + ] + }, + { + "cell_type": "markdown", + "id": "4665e721-9935-4b3a-a4df-f1c0b9c6de23", + "metadata": {}, + "source": [ + "Now, lets have a look on the geometry in which the skin dose is calculated. PySkinDose has a built in mode for plotting the setup (i.e. the phantom and its position upon the patient support table). This is displayed by running the cell below with `settings.mode` = \"plot_setup\".\n", + "\n", + "The position of the phantom can be modified by changing the translation parameters in `settings.phantom.patient_offset`. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2290d2af-b61b-4827-832b-aab10465aa35", + "metadata": {}, + "outputs": [], + "source": [ + "settings = PyskindoseSettings(settings=load_settings_example_json())\n", + "settings.mode = \"plot_setup\"\n", + "settings.phantom.model = \"cylinder\"\n", + "\n", + "# Uncomment any of the following lines to translate the position of the phantom:\n", + "# settings.phantom.patient_offset.d_lat = -20\n", + "# settings.phantom.patient_offset.d_lon = +10\n", + "# settings.phantom.patient_offset.d_ver = -10\n", + "\n", + "main(settings=settings)" + ] + }, + { + "cell_type": "markdown", + "id": "ee930e3c-5ef4-4a85-8a49-11d54afcd2d1", + "metadata": {}, + "source": [ + "In the above output, you will see the phantom and its position upon the support table. The X-ray source, beam, and image receptor is also added in standard position (AP1=AP2=0)\n", + "\n", + "\n", + "Select your phantom by setting `settings.phantom.model` to either \"plane\", \"cylinder\", or \"human\". Cylindrical phantoms are great for general purposes but for skin dose map visualisation based patient data a human phantom is the best option. If you select \"human\", you need to specify what human phantom to use. Our current default phantom is named \"hudfrid\" which is an adult male phantom slightly optimized for skin dose estimations. \n", + "\n", + "Several more advanced phantoms (e.g. for neurointerventions) are in development. If you select any of the mathematical phantoms (plane or cylinder), then you can tweak settings such as length and radius in `settings.phantom.dimension`. You can also change the dimension of the patient support table and pad." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ed64f1d-4d57-4c05-8491-35b43754eaec", + "metadata": {}, + "outputs": [], + "source": [ + "settings = PyskindoseSettings(settings=load_settings_example_json())\n", + "settings.mode = \"plot_setup\"\n", + "\n", + "# Select an elliptical cylinder with length 150 cm and a \n", + "# radius of[20, 10] cm\n", + "# settings.phantom.model = \"cylinder\"\n", + "# settings.phantom.dimension.cylinder_length = 150\n", + "# settings.phantom.dimension.cylinder_radii_a = 20\n", + "# settings.phantom.dimension.cylinder_radii_b = 10\n", + "\n", + "# Select a planar phantom with length 120 cm and width 40 cm\n", + "# settings.phantom.model = \"plane\"\n", + "# settings.phantom.dimension.plane_length = 120\n", + "# settings.phantom.dimension.plane_width = 40\n", + "\n", + "# Select a human phantom\n", + "settings.phantom.model = \"human\"\n", + "settings.phantom.human_mesh = \"hudfrid\"\n", + "\n", + "main(settings=settings)" + ] + }, + { + "cell_type": "markdown", + "id": "d80ce60b", + "metadata": {}, + "source": [ + "Use `print_available_human_phantoms()` to show currently available human phantoms. To change human phantom, rerun the previous cell after replacing `hudfrid` with any of the options presented in `print_available_human_phantoms()`\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e851494b", + "metadata": {}, + "outputs": [], + "source": [ + "print_available_human_phantoms()" + ] + }, + { + "cell_type": "markdown", + "id": "833ffbd5", + "metadata": {}, + "source": [ + "Once you are satisfied with the selection of patient phantom, and the patient position on the support table, we can move on to examine the contents of an RDSR file prior to calculating the skin dose estimate." + ] + }, + { + "cell_type": "markdown", + "id": "4fc40fd0", + "metadata": {}, + "source": [ + "## PART III: Load and examine an RDSR file" + ] + }, + { + "cell_type": "markdown", + "id": "a670f6df", + "metadata": {}, + "source": [ + "PySkinDose contains the following example RDSR files: " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34bb18cd", + "metadata": {}, + "outputs": [], + "source": [ + "print_example_rdsr_files()" + ] + }, + { + "cell_type": "markdown", + "id": "0b02b619", + "metadata": {}, + "source": [ + "A useful tool during analysis of individual procedures is to visualize the orientation and size of the x-ray beam in every irradiation event one by one. In the example below the `settings.mode` is changed to `plot_procedure`. With this, you can use the slider in the plot to scroll through each irradiation event in the study. \n", + "\n", + "Please be aware that the mode `plot_procedure` can be quite computationally intensive when the patient phantom is included in the interactive plot. To reduce this load, you can disable the patient phantom by adjusting the `settings.plot.max_events_for_patient_inclusion` to a value smaller than the number of irradiation events in the RDSR file.\n", + "\n", + "Now, lets load the siemens axiom artis example procedure and visualize the procedure with `plot_procedure`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85920ff8-0729-40ce-b066-5ea63930a423", + "metadata": {}, + "outputs": [], + "source": [ + "settings = PyskindoseSettings(settings=load_settings_example_json())\n", + "settings.mode = \"plot_procedure\"\n", + "settings.phantom.model = \"cylinder\"\n", + "\n", + "rdsr_data_dir = get_path_to_example_rdsr_files()\n", + "\n", + "# You can set the maximum number of irradiation events for including the\n", + "# phantom in the plot. Here, we set it to 0 to reduce memory use.\n", + "settings.plot.max_events_for_patient_inclusion = 0\n", + "\n", + "# Change this path to use your own RDSR file\n", + "# N.B: If your are using a windows OS, you need to set the path as r raw string,\n", + "# for example: \n", + "# selected_rdsr_filepath = Path(r'c:\\rdsr_files\\file_name.dcm')\n", + "selected_rdsr_filepath = rdsr_data_dir / \"siemens_axiom_example_procedure.dcm\"\n", + "\n", + "main(settings=settings, file_path=selected_rdsr_filepath)" + ] + }, + { + "cell_type": "markdown", + "id": "d2fb2e78", + "metadata": {}, + "source": [ + "To use your own RDSR file, just change the `file_path` to the location of your RDSR file." + ] + }, + { + "cell_type": "markdown", + "id": "d8580806db62171b", + "metadata": { + "collapsed": false + }, + "source": [ + "## PART IV: Calculate and plot a dosemap" + ] + }, + { + "cell_type": "markdown", + "id": "5ec95719", + "metadata": {}, + "source": [ + "At this point, you should be satisfied with your choice of patient phantom, the phantom position on the support table and also understand the contents in the selected RDSR-file (from the `settings.plot.plot_dosemap` function).\n", + "\n", + "Now we can finally address the purpose of PySkinDose, which is to calculate the skin dose estimate and visualize the result in a dosemap plot. \n", + "\n", + "A plot will illustrate the skin dose estimation map based on information in the RDSR-file together with available correction factors for the specific X-ray device. The plot is interactive so the phantom can rotate and if you hover the mouse over the phantom an estimate of skin dose will appear." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5528235c", + "metadata": {}, + "outputs": [], + "source": [ + "settings = PyskindoseSettings(settings=load_settings_example_json())\n", + "settings.mode = \"calculate_dose\"\n", + "settings.output_format = 'html' # set output format to html\n", + "settings.plot.plot_dosemap = True # enable dosemap plot\n", + "settings.phantom.model = \"human\"\n", + "settings.phantom.human_mesh = \"hudfrid\"\n", + "\n", + "settings.phantom.patient_offset.d_lat = -35\n", + " \n", + "main(settings=settings, file_path=rdsr_data_dir / \"siemens_axiom_example_procedure.dcm\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "43a5b3f9", + "metadata": {}, + "source": [ + "If you want to examine, analyze or save the output of the calculated skin dose estimation, you need to change output mode to either __dict__ or __json__:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc103caa", + "metadata": {}, + "outputs": [], + "source": [ + "settings = PyskindoseSettings(settings=load_settings_example_json())\n", + "settings.mode = \"calculate_dose\"\n", + "settings.output_format = 'dict'\n", + "\n", + "rdsr_data_dir = get_path_to_example_rdsr_files()\n", + "output = main(settings=settings, file_path=rdsr_data_dir / \"siemens_axiom_example_procedure.dcm\")\n", + "\n", + "print(f'estimated psd {output[\"psd\"].round(1)} mGy')" + ] + }, + { + "cell_type": "markdown", + "id": "594b2616", + "metadata": {}, + "source": [ + "The output (in this case a dictionary) contains the following parameters from the\n", + "conducted skin dose calculation.\n", + "- **table**\n", + " - parameters regarding the patient support table\n", + "- **pad**\n", + " - parameters regarding the patient support pad\n", + "- **patient**\n", + " - parameters regarding the phantom\n", + "- **dose_map**\n", + " - skin patch index and estimated skin dose value for each patch on the phantom\n", + "- **psd**\n", + " - Estimated peak skin dose, i.e., max(dose_map)\n", + "- **air_kerma**\n", + " - Air kerma, i.e., psd but without the correction factors\n", + "- **events**\n", + " - parameters on translation, rotation for each irradiation event\n", + "- **corrections**\n", + " - The correction factor used to convert air kerma to skin dose. This includes\n", + " - backscatter correction\n", + " - medium correction\n", + " - table correction\n", + " - inverse square law correction\n" + ] + }, + { + "cell_type": "markdown", + "id": "a1647009cb57c086", + "metadata": { + "collapsed": false + }, + "source": [ + "## Adding/Altering correction factors\n", + "\n", + "The fine-tuning and optimization of correction factors are, and will always be, a work in progress. Currently, our priority is to implement compliance across additional X-ray devices. One key correction that can be easily adjusted is the estimated attenuation for the patient support table and pad. This can be configured by setting `k_tab_val` to a value between 0 and 1, with 1 indicating that the X-ray beam is not attenuated at all by the table and pad.\n", + "\n", + "Lets try to lower the table and pad correction factor (that is, X-ray beam transmission) and observe the difference in the skin dose map. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eef10eea", + "metadata": {}, + "outputs": [], + "source": [ + "settings = PyskindoseSettings(settings=load_settings_example_json())\n", + "settings.mode = \"calculate_dose\"\n", + "settings.output_format = 'html' # set output format to html\n", + "settings.plot.plot_dosemap = True # enable dosemap plot\n", + "settings.phantom.model = \"human\"\n", + "settings.phantom.human_mesh = \"hudfrid\"\n", + "\n", + "settings.phantom.patient_offset.d_lat = -35\n", + "settings.k_tab_val = 0.5\n", + "main(settings=settings, file_path=rdsr_data_dir / \"siemens_axiom_example_procedure.dcm\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "381a15f6", + "metadata": {}, + "source": [ + "In the above output, we note that the skin dose estimates on the back of the phantom is decreased due to increase attenuation in the patient support table and pad. However, the skin dose estimates on the side of the phantom remain the same as before since the X-rays never pass the table and pad.\n" + ] + }, + { + "cell_type": "markdown", + "id": "d304981aa175afa", + "metadata": { + "collapsed": false + }, + "source": [ + "## Further development and contributions" + ] + }, + { + "cell_type": "markdown", + "id": "31b6b893", + "metadata": {}, + "source": [ + "Contributions and collaborations for further development of PySkinDose is greatly appreciated. Please visit the link below for suggested topics.\n", + "[https://pyskindose.readthedocs.io/en/latest/user/contribute/](https://pyskindose.readthedocs.io/en/latest/user/contribute.html)\n", + "\n", + "If you encounter issues using PySkinDose please describe them as detailed as possible at the project GIT linked below\n", + "[https://github.com/rvbCMTS/PySkinDose/issues/](https://github.com/rvbCMTS/PySkinDose/issues/)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "pyskindose", + "language": "python", + "name": "python3" + }, + "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.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/index.rst b/docs/source/index.rst index 0d73dc9..c160d36 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,7 +8,7 @@ Welcome to PySkinDose's documentation! user/background.md user/description.md user/install.md - user/get_started.rst + getting_started/getting_started user/contribute.md modules diff --git a/docs/source/user/get_started.rst b/docs/source/user/get_started.rst deleted file mode 100644 index ad02948..0000000 --- a/docs/source/user/get_started.rst +++ /dev/null @@ -1,2 +0,0 @@ -Getting Started -====================================== diff --git a/pyproject.toml b/pyproject.toml index 952ed10..1894adf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "pyskindose" -version = "24.10.0" +version = "25.1.0" description = "Tools and script for calculating peak skin dose and create dose maps for fluoroscopic exams from RDSR data" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [ { name="Max Hellström", email="max.hellstrom@gmail.com" }, { name="Josef Lundman", email="josef@lundman.eu" }, @@ -14,12 +14,12 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "pandas", - "numpy >= 1.26.0", + "pandas >= 2.2.3", + "numpy >= 2.2.4", "pydicom >= 2.0", - "numpy-stl", + "numpy-stl >= 3.2.0", "plotly >= 4.13.3", - "scipy", + "scipy >= 1.15.2", "tqdm", "psutil", "pillow >= 10.0.1", diff --git a/src/pyskindose/__init__.py b/src/pyskindose/__init__.py index bbdf4f0..0c6083d 100644 --- a/src/pyskindose/__init__.py +++ b/src/pyskindose/__init__.py @@ -5,3 +5,40 @@ from .phantom_class import Phantom from pyskindose.plotting import plot_geometry from .analyze_data import analyze_data +from .settings import PyskindoseSettings + + +def load_settings_example_json() -> dict: + import json + from pathlib import Path + + return json.loads((Path(__file__).parent / "settings_example.json").read_text()) + + +def print_available_human_phantoms(): + from pathlib import Path + + phantom_data_dir = Path(__file__).parent / "phantom_data" + phantoms = [ + phantom.stem for phantom in phantom_data_dir.glob("*.stl") if not phantom.stem.endswith("reduced_1000t") + ] + + for phantom in phantoms: + print(phantom) + + +def print_example_rdsr_files(): + rdsr_data_dir = get_path_to_example_rdsr_files() + files = [file.name for file in rdsr_data_dir.glob("*.dcm")] + + print("Available RDSR files:\n") + for filename in files: + print(f"\t{filename}") + + print(f"\nFiles located in {rdsr_data_dir.absolute()}") + + +def get_path_to_example_rdsr_files(): + from pathlib import Path + + return Path(__file__).parent / "example_data" / "RDSR" diff --git a/src/pyskindose/constants.py b/src/pyskindose/constants.py index 755548c..d149798 100644 --- a/src/pyskindose/constants.py +++ b/src/pyskindose/constants.py @@ -33,6 +33,7 @@ KEY_PARAM_PHANTOM_MODEL = "model" KEY_PARAM_HUMAN_MESH = "human_mesh" KEY_PARAM_INHERENT_FILTRATION = "inherent_filtration" +KEY_PARAM_REMOVE_INVALID_ROWS = "remove_invalid_rows" KEY_PARAM_SILENCE_PYDICOM_WARNINGS = "silence_pydicom_warnings" DIMENSION_PLANE_LENGTH = "plane_length" @@ -195,8 +196,11 @@ PLOT_ZERO_LINE_WIDTH = 5 -PLOT_HEIGHT_NOTEBOOK = 800 -PLOT_WIDTH_NOTEBOOK = None +# PLOT_HEIGHT_NOTEBOOK = 800 +# PLOT_WIDTH_NOTEBOOK = None +PLOT_HEIGHT_NOTEBOOK = 900 +PLOT_WIDTH_NOTEBOOK = 600 + PLOT_HEIGHT = None PLOT_WIDTH = None diff --git a/src/pyskindose/helpers/read_and_normalize_rdsr_data.py b/src/pyskindose/helpers/read_and_normalize_rdsr_data.py index 0d25275..090ce8a 100644 --- a/src/pyskindose/helpers/read_and_normalize_rdsr_data.py +++ b/src/pyskindose/helpers/read_and_normalize_rdsr_data.py @@ -34,4 +34,9 @@ def read_and_normalise_rdsr_data(rdsr_filepath: str, settings: PyskindoseSetting # normalized rdsr for compliance with PySkinDose normalized_data = rdsr_normalizer(data_parsed, settings=settings) + if settings.remove_invalid_rows: + if invalid_kvp_rows := len(normalized_data[normalized_data.kVp == 0]): + print(f"Removing {invalid_kvp_rows} rows with kVp value = 0") + normalized_data = normalized_data[normalized_data.kVp != 0].reset_index(drop=True) + return normalized_data diff --git a/src/pyskindose/main.py b/src/pyskindose/main.py index 82edfce..22317cc 100644 --- a/src/pyskindose/main.py +++ b/src/pyskindose/main.py @@ -50,8 +50,10 @@ def main(file_path: Optional[str] = None, settings: Union[str, dict, PyskindoseS data_norm = read_and_normalise_rdsr_data(rdsr_filepath=file_path, settings=settings) - _ = analyze_data(normalized_data=data_norm, settings=settings) + output = analyze_data(normalized_data=data_norm, settings=settings) + if settings.output_format in ("dict", "json"): + return output def analyze_normalized_data_with_custom_settings_object( data_norm: pd.DataFrame, diff --git a/src/pyskindose/settings/pyskindose_settings.py b/src/pyskindose/settings/pyskindose_settings.py index c64fbe7..874b2c8 100644 --- a/src/pyskindose/settings/pyskindose_settings.py +++ b/src/pyskindose/settings/pyskindose_settings.py @@ -10,6 +10,7 @@ KEY_PARAM_K_TAB_VAL, KEY_PARAM_MODE, KEY_PARAM_RDSR_FILENAME, + KEY_PARAM_REMOVE_INVALID_ROWS, KEY_PARAM_SILENCE_PYDICOM_WARNINGS, RUN_ARGUMENTS_OUTPUT_DICT, RUN_ARGUMENTS_OUTPUT_HTML, @@ -49,7 +50,7 @@ class PyskindoseSettings: rdsr_filename : str filename of the RDSR file, without the .dcm file ending. estimate_k_tab : bool - Whether k_tab should be approximated or not. You should set this if you + Whether k_tab should be approximated or not. You should set this to true if you have not conducted table attenuation measurements. k_tab_val : float Value of k_tab, in range 0.0 -> 1.0. @@ -106,6 +107,8 @@ def __init__( self.normalization_settings = self._initialize_normalization_settings(normalization_settings) + self.remove_invalid_rows: bool = True if tmp.get(KEY_PARAM_REMOVE_INVALID_ROWS) else None + @staticmethod def _initialize_output_path(output_path: Optional[Union[str, Path]], output_format: str) -> Path: if output_path is None: @@ -161,11 +164,11 @@ def print_parameters(self, return_as_string: bool = False): to the terminal. The default is False. """ - phantom_settings_string = self.phantom.to_printable_string(color="bright_magenta on black") - plot_settings_string = self.plot.to_printable_string(color="steel_blue1 on black") - normalization_settings_string = self.normalization_settings.to_printable_string(color="bright_green on black") + phantom_settings_string = self.phantom.to_printable_string(color="bright_magenta") + plot_settings_string = self.plot.to_printable_string(color="steel_blue1") + normalization_settings_string = self.normalization_settings.to_printable_string(color="bright_green") - color = "bright_cyan on black" + color = "bright_cyan" output_str = ( f"[b u {color}]General settings[/b u {color}]\n" diff --git a/src/pyskindose/settings_example.json b/src/pyskindose/settings_example.json index b9ccffc..75df83b 100644 --- a/src/pyskindose/settings_example.json +++ b/src/pyskindose/settings_example.json @@ -2,13 +2,15 @@ "mode": "plot_event", "rdsr_filename": "siemens_axiom_example_procedure.dcm", "plot_event_index": 12, - "estimate_k_tab": false, + "estimate_k_tab": true, "k_tab_val": 0.8, "inherent_filtration": 3.1, + "remove_invalid_rows": false, "silence_pydicom_warnings": true, "plot": { + "interactivity": true, "dark_mode": true, - "notebook_mode": false, + "notebook_mode": true, "plot_dosemap": false, "colorscale": "jet", "max_events_for_patient_inclusion": 0, @@ -20,8 +22,7 @@ "patient_offset": { "d_lon": 0, "d_ver": 0, - "d_lat": -35, - "unit": "cm" + "d_lat": 0 }, "patient_orientation": "head_first_supine", "dimension": { @@ -37,8 +38,7 @@ "table_thickness": 5, "pad_length": 281.5, "pad_width": 45, - "pad_thickness": 4, - "unit": "cm" + "pad_thickness": 4 } }, "corrections_db_path": "corrections.db" diff --git a/src/pyskindose/user_defined_parameters.py b/src/pyskindose/user_defined_parameters.py new file mode 100644 index 0000000..ba957e8 --- /dev/null +++ b/src/pyskindose/user_defined_parameters.py @@ -0,0 +1,73 @@ +from pyskindose import constants as c + +PARAMETERS = dict( + # modes: 'calculate_dose', 'plot_setup', 'plot_event', 'plot_procedure' + mode=c.MODE_PLOT_PROCEDURE, + # RDSR filename + rdsr_filename="S1.dcm", + # Irrading event index for mode='plot_event' + plot_event_index=12, + # Set True to estimate table correction, or False to use measured k_tab + estimate_k_tab=False, + # Numeric value of estimated table correction + k_tab_val=0.8, + # plot settings + plot={ + # dark mode for plots + c.MODE_DARK_MODE: True, + # notebook mode + c.MODE_NOTEBOOK_MODE: False, + # choose if dosemap should be plotted after dose calculations. + c.MODE_PLOT_DOSEMAP: False, + # colorscale for dosemaps + c.DOSEMAP_COLORSCALE_KEY: c.DOSEMAP_COLORSCALE, + # max number of event for inclusion of patient phantom in plot + # procedure + c.MAX_EVENT_FOR_PATIENT_INCLUSION_IN_PROCEDURE_KEY: 0, + # Irrading event index for mode='plot_event' + c.PLOT_EVENT_INDEX_KEY: 12, + }, + # Phantom settings: + phantom=dict( + # Phantom model, valid selections: 'plane', 'cylinder', or 'human' + model=c.PHANTOM_MODEL_HUMAN, + # Human phantom .stl filename, without .stl ending. + human_mesh=c.PHANTOM_MESH_ADULT_MALE, + # Patient offset from table isocenter (centered at head end side). + patient_offset={ + c.OFFSET_LONGITUDINAL_KEY: 0, + c.OFFSET_VERTICAL_KEY: 0, + c.OFFSET_LATERAL_KEY: -35, + }, + # Dimensions of matematical phantoms (except model='human') + patient_orientation=c.PATIENT_ORIENTATION_HEAD_FIRST_SUPINE, + dimension={ + # Length of plane phantom + c.DIMENSION_PLANE_LENGTH: 120, + # Width of plane phantom + c.DIMENSION_PLANE_WIDTH: 40, + # Resolution of plane phantom + c.DIMENSION_PLANE_RESOLUTION: c.RESOLUTION_SPARSE, + # Length of cylinder phantom + c.DIMENSION_CYLINDER_LENGTH: 150, + # First radii of cylinder phantom + c.DIMENSION_CYLINDER_RADII_A: 20, + # Second radii of cylinder phantom + c.DIMENSION_CYLINDER_RADII_B: 10, + # Resolution of cylinder. + c.DIMENSION_CYLINDER_RESOLUTION: c.RESOLUTION_DENSE, + # Support table length + c.DIMENSION_TABLE_LENGTH: 281.5, + # Support table width + c.DIMENSION_TABLE_WIDTH: 45, + # Support table thickness + c.DIMENSION_TABLE_THICKNESS: 5, + # Support pad length + c.DIMENSION_PAD_LENGTH: 281.5, + # Support pad width + c.DIMENSION_PAD_WIDTH: 45, + # Support pad thickness + c.DIMENSION_PAD_THICKNESS: 4, + }, + ), +)