Commit e9a7cf48 authored by Gabriel Couture's avatar Gabriel Couture

Adding get_analytic_dvhs function to dao

parent bda61233
*
!.gitignore
!requirements.txt
!README.md
!setup.py
......
# coding: utf-8
# author: Gabriel Couture
"""
This module is a Data Access Object (DAO) for the DICOM
file and corresponding DVHs.
"""
import os
from os.path import join
from typing import List
import pandas as pd
import pydicom
from dicomgenerator.exceptions import WrongGeometryError, InvalidDoseGradientError
from dicomgenerator.exceptions import InvalidGeometryError, InvalidDoseGradientError, InvalidRoiError, \
InvalidSpacingError
def _valid_dose_grading(dose_gradient: str):
if dose_gradient not in {'AP', 'SI'}:
raise InvalidDoseGradientError("Dose gradient should be in {'AP', 'SI'}")
def _valid_spacing(spacing: float):
spacing_str = str(spacing).replace('.0', '').replace('.', '')
if spacing_str not in {'02', '1', '2', '3'}:
raise InvalidSpacingError("Spacing should be in {0.2, 1, 2, 3}")
def _valid_geometry(geometry: str):
if geometry not in {'cone', 'sphere', 'cylinder'}:
raise InvalidGeometryError("Geometry should be in {'cone', 'sphere', 'cylinder'}")
def _valid_roi(roi: str) -> None:
if roi not in {'axial', 'right'}:
raise InvalidRoiError("ROI should be in {'axial', 'right'}")
def _format_spacing(spacing: float) -> str:
......@@ -44,15 +73,22 @@ def get_rtstruct(geometry: str = 'all', spacing='all') -> List[pydicom.FileDatas
-------
List of DICOM file data set.
"""
if spacing != 'all':
_valid_spacing(spacing)
if geometry != 'all':
_valid_geometry(geometry)
directory, _ = os.path.split(__file__)
if geometry == 'all':
geometry_path = os.path.join(directory, 'data', 'RTSTRUCT')
geometry_paths = [os.path.join(directory, 'data', 'RTSTRUCT', i) for i in os.listdir(geometry_path)]
geometry_path = join(directory, 'data', 'RTSTRUCT')
geometry_paths = [join(directory, 'data', 'RTSTRUCT', i) for i in os.listdir(geometry_path)]
else:
geometry_paths = [os.path.join(directory, 'data', 'RTSTRUCT', geometry)]
geometry_paths = [join(directory, 'data', 'RTSTRUCT', geometry)]
if False in [os.path.exists(i) for i in geometry_paths]:
raise WrongGeometryError("Geometry should be in {'cone', 'sphere', 'cylinder', 'all'}")
raise InvalidGeometryError("Geometry should be in {'cone', 'sphere', 'cylinder', 'all'}")
dicom_paths: List[str] = []
for geometry_path in geometry_paths:
......@@ -60,7 +96,7 @@ def get_rtstruct(geometry: str = 'all', spacing='all') -> List[pydicom.FileDatas
lambda i: f'{_format_spacing(spacing)}_' in i or spacing == 'all',
os.listdir(geometry_path)
)
dicom_paths += [os.path.join(geometry_path, i) for i in dicom_files]
dicom_paths += [join(geometry_path, i) for i in dicom_files]
return [pydicom.dcmread(i) for i in dicom_paths]
......@@ -80,8 +116,14 @@ def get_rtdose(dose_gradient: str = 'all', spacing='all') -> List[pydicom.FileDa
-------
List of DICOM file data set.
"""
if spacing != 'all':
_valid_spacing(spacing)
if spacing == 0.2:
raise InvalidSpacingError("Should be {1, 2, 3, 'all'}")
directory, _ = os.path.split(__file__)
rtdose_path = os.path.join(directory, 'data', 'RTDOSE')
rtdose_path = join(directory, 'data', 'RTDOSE')
dicom_files = filter(
lambda i: _format_dose_gradient(dose_gradient) in i or dose_gradient == 'all',
......@@ -90,7 +132,7 @@ def get_rtdose(dose_gradient: str = 'all', spacing='all') -> List[pydicom.FileDa
os.listdir(rtdose_path)
)
)
dicom_paths = [os.path.join(rtdose_path, i) for i in dicom_files]
dicom_paths = [join(rtdose_path, i) for i in dicom_files]
return [pydicom.dcmread(i) for i in dicom_paths]
......@@ -103,10 +145,40 @@ def get_rtplan() -> pydicom.FileDataset:
RTPLAN DICOM file data set.
"""
directory, _ = os.path.split(__file__)
rtplan_path = os.path.join(directory, 'data', 'RTPLAN', 'RTPLAN.dcm')
rtplan_path = join(directory, 'data', 'RTPLAN', 'RTPLAN.dcm')
return pydicom.dcmread(rtplan_path)
def get_analytic_results():
raise NotImplementedError
def get_analytic_dvhs(geometry: str, spacing: float, dose_gradient: str, roi: str = '') -> pd.DataFrame:
"""Get Analytic DVH results.
Parameters
----------
geometry
Geometry: {'cone', 'sphere', 'cylinder'}.
spacing
Spacing in millimeters: {0.2, 1, 2, 3}.
dose_gradient
Dose gradient: {'AP', 'SI', 'all'}.
AP := Anterior Posterior, SI := Superior Inferior.
roi: (Optional)
Roi: {'right', 'axial'}.
Returns
-------
pandas.DataFrame
Analytic DVHs
"""
_valid_geometry(geometry), _valid_spacing(spacing), _valid_dose_grading(dose_gradient)
if geometry != 'sphere':
_valid_roi(roi)
directory, _ = os.path.split(__file__)
dvhs_path = join(directory, 'data', 'ANALYTICAL_RESULTS', 'DVHs')
filename = join(dvhs_path, f'{geometry}.csv' if geometry == 'sphere' else f'{geometry}_{roi}Roi.csv')
spacing_str = str(spacing).replace('.0', '').replace('.', '')
return pd.read_csv(filename)[['Dose (cGy)', f'{spacing_str}mm{dose_gradient}']]
......@@ -2,9 +2,17 @@
# author: Gabriel Couture
class WrongGeometryError(ValueError):
class InvalidGeometryError(ValueError):
pass
class InvalidDoseGradientError(Exception):
class InvalidDoseGradientError(ValueError):
pass
class InvalidRoiError(ValueError):
pass
class InvalidSpacingError(ValueError):
pass
entrypoints==0.3
flake8==3.7.8
mccabe==0.6.1
mypy==0.740
mypy-extensions==0.4.3
pycodestyle==2.5.0
pydicom==1.3.0
pyflakes==2.1.1
typed-ast==1.4.0
typing-extensions==3.7.4
......@@ -57,7 +57,7 @@ setup(
package_data={
'dicomgenerator': ['*.dcm']
},
install_requires=['pydicom'],
install_requires=['pydicom', 'pandas'],
cmdclass={
'lint': LintTests,
'test': Tests
......
......@@ -2,15 +2,18 @@
# author: Gabriel Couture
import unittest
import pandas as pd
import pydicom
from dicomgenerator import dao
from dicomgenerator.exceptions import WrongGeometryError, InvalidDoseGradientError
from dicomgenerator.exceptions import InvalidGeometryError, InvalidDoseGradientError, InvalidSpacingError
A_GEOMETRY = 'cone'
A_SPACING = 1.0
A_DOSE_GRADIENTS = 'AP'
A_DOSE_GRADIENT = 'AP'
A_ROI = 'axial'
A_BAD_GEOMETRY = 'A_BAD_GEOMETRY'
A_BAD_SPACING = 0.1
A_BAD_DOSE_GRADIENT = 'A_BAD_DOSE_GRADIENT'
......@@ -30,23 +33,35 @@ class TestDao(unittest.TestCase):
for i in result:
self.assert_is_rtstruct(i)
def test_givenAGeometryAndASpacing_whenGettingRtstruct_thenRaiseWrongGeometryError(self):
def test_givenABadGeometryAndASpacing_whenGettingRtstruct_thenRaiseWrongGeometryError(self):
self.assertRaises(
WrongGeometryError,
InvalidGeometryError,
lambda: dao.get_rtstruct(A_BAD_GEOMETRY, A_SPACING)
)
def test_givenAGeometryAndABadSpacing_whenGettingRtstruct_thenRaiseWrongGeometryError(self):
self.assertRaises(
InvalidSpacingError,
lambda: dao.get_rtstruct(A_GEOMETRY, A_BAD_SPACING)
)
def test_givenADoseGradientAndASpacing_whenGettingRtdose_thenResultIsOneRtdoseFileDataSet(self):
result = dao.get_rtdose(A_DOSE_GRADIENTS, A_SPACING)
result = dao.get_rtdose(A_DOSE_GRADIENT, A_SPACING)
self.assertEqual(len(result), 1)
for i in result:
self.assert_is_rtdose(i)
def test_givenADoseGradientAndASpacing_whenGettingRtstruct_thenRaiseWrongGeometryError(self):
def test_givenABadDoseGradientAndASpacing_whenGettingRtdose_thenRaiseWrongGeometryError(self):
self.assertRaises(
InvalidDoseGradientError,
lambda: dao.get_rtdose(A_BAD_GEOMETRY, A_SPACING)
lambda: dao.get_rtdose(A_BAD_DOSE_GRADIENT, A_SPACING)
)
def test_givenADoseGradientAndABadSpacing_whenGettingRtdose_thenRaiseWrongGeometryError(self):
self.assertRaises(
InvalidSpacingError,
lambda: dao.get_rtdose(A_DOSE_GRADIENT, A_BAD_SPACING)
)
def test_whenGettingRtdose_thenResultIsARtplanFileDataSet(self):
......@@ -55,6 +70,14 @@ class TestDao(unittest.TestCase):
self.assertIsInstance(result, pydicom.FileDataset)
self.assertEqual(result['Modality'].value, 'RTPLAN')
def test_whenGettingAnalyticDVHs_thenResultIsDVHsDataFrame(self):
expected_columns = ['Dose (cGy)', '1mmAP']
result = dao.get_analytic_dvhs(A_GEOMETRY, A_SPACING, A_DOSE_GRADIENT, A_ROI)
self.assertIsInstance(result, pd.DataFrame)
self.assertEqual(list(result.columns), expected_columns)
def assert_is_rtstruct(self, data_set: pydicom.FileDataset):
self.assertIsInstance(data_set, pydicom.FileDataset)
self.assertEqual(data_set['Modality'].value, 'RTSTRUCT')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment