"""
Define :class:`Prescription` and methods for parsing prescription data
from python objects as well as JSON- or YAML-formatted files.
Parsing methods expect the following formats.
YAML::
- name : PTV
label : 1
is_target: Yes
dose : 35.
constraints:
- "D90 >= 32.3Gy"
- "D1 <= 1.1rx"
- name : OAR1
label : 2
is_target: No
dose :
constraints:
- "D95 <= 20Gy"
- "V30 Gy <= 20%"
Python :obj:`list` of :obj:`dict` (JSON approximately the same)::
[{
"name" : "PTV",
"label" : 1,
"is_target" : True,
"dose" : 35.,
"constraints" : ["D1 <= 1.1rx", "D90 >= 32.3Gy"]
}, {
"name" : "OAR1",
"label" : 2,
"is_target" : False,
"dose" : None,
"constraints" : ["D95 <= 20Gy"]
}]
JSON verus Python syntax differences:
- ``true``/``false`` instead of ``True``/``False``
- ``null`` instead of ``None``
"""
"""
Copyright 2016 Baris Ungun, Anqi Fu
This file is part of CONRAD.
CONRAD is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CONRAD is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CONRAD. If not, see <http://www.gnu.org/licenses/>.
"""
from conrad.compat import *
import os
import json, yaml
import traceback
from conrad.physics.units import Gy
from conrad.physics.string import dose_from_string
from conrad.medicine.structure import Structure
from conrad.medicine.anatomy import Anatomy
from conrad.medicine.dose import eval_constraint, ConstraintList
[docs]class Prescription(object):
"""
Class for specifying structures with dose targets and constraints.
Attributes:
constraint_dict (:obj:`dict`): Dictionary of
:class:`ConstraintList` objects, keyed by structure labels.
structure_dict (:obj:`dict`): Diciontionary of
:class:`Structure` objects, keyed by structure labels.
rx_list (:obj:`list`): List of dictionaries representation of
prescription.
"""
def __init__(self, prescription_data=None):
"""
Intialize empty or populated :class:`Prescription` instance.
Arguments:
prescription_data (optional): Data to parse as prescription.
If input is of type :class:`Prescription`, intializer
acts as a copy constructor.
"""
self.constraint_dict = {}
self.structure_dict = {}
self.rx_list = []
if isinstance(prescription_data, Prescription):
self.constraint_dict = prescription_data.constraint_dict
self.structure_dict = prescription_data.structure_dict
self.rx_list = prescription_data.rx_list
elif prescription_data:
self.digest(prescription_data)
[docs] def add_structure_to_dictionaries(self, structure):
"""
Add a new structure to internal representation of prescription.
Arguments:
structure (:class:`Structure`): Structure added to
:attr:`Prescription.structure_dict`. An corresponding,
empty constraint list is added to
:attr:`Prescription.constraint_dict`.
Returns:
None
Raises:
TypeError: If ``structure`` not a :class:`Structure`.
"""
if not isinstance(structure, Structure):
raise TypeError('argumet "Structure" must be of type {}'
''.format(Structure))
self.structure_dict[structure.label] = structure
self.constraint_dict[structure.label] = ConstraintList()
[docs] def digest(self, prescription_data):
"""
Populate :class:`Prescription`'s structures and dose constraints.
Specifically, for each entry in ``prescription_data``, construct
a :class:`Structure` to capture structure data (e.g., name,
label), as well as a corresponding but separate
:class:`ConstraintList` object to capture any dose constraints
specified for the structure.
Add each such structure to :attr:`Prescription.structure_dict`,
and each such constraint list to
:attr:`Prescription.constraint_dict`. Build or copy a "list of
dictionaries" representation of the prescription data, assign to
:attr:`Prescription.rx_list`.
Arguments:
prescription_data: Input to be parsed for structure and dose
constraint data. Accepted formats include :obj:`str`
specifying a valid path to a suitably-formatted JSON or
YAML file, or a suitably-formatted :obj:`list` of
:obj:`dict` objects.
Returns:
None
Raises:
TypeError: If input not of type :obj:`list` or a :obj:`str`
specfying a valid path to file that can be loaded with
the :meth:`json.load` or :meth:`yaml.safe_load` methods.
"""
err = None
data_valid = False
rx_list = []
# read prescription data from list
if isinstance(prescription_data, list):
rx_list = prescription_data
data_valid = True
# read presription data from file
if isinstance(prescription_data, str):
if os.path.exists(prescription_data):
try:
f = open(prescription_data)
if '.json' in prescription_data:
rx_list = json.load(f)
else:
rx_list = yaml.safe_load(f)
f.close
data_valid = True
except:
err = traceback.format_exc()
if not data_valid:
if err is not None:
print(err)
raise TypeError(
'input prescription_data expected to be a list or '
'the path to a valid JSON or YAML file.')
try:
for item in rx_list:
rx_dose = None
label = item['label']
name = item['name']
dose = 0 * Gy
is_target = bool(item['is_target'])
if is_target:
if isinstance(item['dose'], (float, int)):
rx_dose = dose = float(item['dose']) * Gy
else:
rx_dose = dose = dose_from_string(item['dose'])
s = Structure(label, name, is_target, dose=dose)
self.add_structure_to_dictionaries(s)
if 'constraints' in item:
if item['constraints'] is not None:
for string in item['constraints']:
self.constraint_dict[label] += eval_constraint(
string, rx_dose=rx_dose)
self.rx_list = rx_list
except:
print(str('Unknown error: prescription_data could not be '
'converted to conrad.Prescription() datatype.'))
raise
@property
def list(self):
""" List of structures in prescription """
return self.rx_list
@property
def dict(self):
""" Dictionary of structures in prescription, by label. """
return {structure.label: structure for structure in self.rx_list}
@property
def constraints_by_label(self):
"""
Dictionary of constraints in prescription, by structure label.
"""
return self.constraint_dict
def __str__(self):
"""
String of structures in prescription with attached constraints.
"""
return str(self.rx_list)
[docs] def report(self, anatomy):
"""
Reports whether ``anatomy`` fulfills all prescribed constraints.
Arguments:
anatomy (:class:`Antomy`): Container of structures to
compare against prescribed constraints.
Returns:
:obj:`dict`: Dictionary keyed by structure label, with data
on each dose constraint associated with that structure in
this :class:`Prescription`. Reported data includes the
constraint, whether it was satisfied, and the actual dose
achieved at the percentile/threshold specified by the
constraint.
Raises:
TypeError: If ``anatomy`` not an :class:`Anatomy`.
"""
if not isinstance(anatomy, Anatomy):
raise TypeError('argument "anatomy" must be of type{}'.format(
Anatomy))
rx_constraints = self.constraints_by_label
report = {}
for label, s in anatomy.structures.items():
sat = []
for constr in rx_constraints[label].itervalues():
status, dose_achieved = s.satisfies(constr)
sat.append({'constraint': constr, 'status': status,
'dose_achieved': dose_achieved})
report[label] = sat
return report
[docs] def report_string(self, anatomy):
"""
Reports whether ``anatomy`` fulfills all prescribed constraints.
Arguments:
anatomy (:class:`Anatomy`): Container of structures to
compare against prescribed constraints.
Returns:
:obj:`str`: Stringified version of output from
:attr:`Presription.report`.
"""
report = self.report(anatomy)
out = ''
for label, replist in report.items():
sname = structures[label].name
sname = '' if sname is None else ' ({})\n'.format(sname)
for item in replist:
out += str(
'{}\tachieved? {}\tdose at level: {}\n'.format(
str(item['constraint']), item['status'],
item['dose_achieved']))
return out