Skip to content

Commit

Permalink
V1.0.3.2 Patch: Fix Evaluation.py for displaying clinical criteria
Browse files Browse the repository at this point in the history
  • Loading branch information
gourav3017 committed May 23, 2024
1 parent 8e05ae0 commit f0f7759
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 45 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="./images/PortPy_logo.png" width="40%" height="40%">
</p>

![Version](https://img.shields.io/static/v1?label=latest&message=v1.0.3&color=darkgreen)
![Version](https://img.shields.io/static/v1?label=latest&message=v1.0.3.2&color=darkgreen)
[![Total Downloads](https://static.pepy.tech/personalized-badge/portpy?period=total&units=international_system&left_color=grey&right_color=blue&left_text=total%20downloads)](https://pepy.tech/project/portpy?&left_text=totalusers)

# What is PortPy?
Expand Down
2 changes: 1 addition & 1 deletion portpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = "1.0.3.1"
__version__ = "1.0.3.2"

from portpy import photon
188 changes: 145 additions & 43 deletions portpy/photon/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ class Evaluation:
"""

@staticmethod
def display_clinical_criteria(my_plan: Plan, sol: Union[dict, List[dict]], html_file_name='temp.html',
def display_clinical_criteria(my_plan: Plan, sol: Union[dict, List[dict]] = None, dose_1d: Union[np.ndarray, List[np.ndarray]]=None, html_file_name='temp.html',
sol_names: List[str] = None, clinical_criteria: ClinicalCriteria = None,
return_df: bool = False, in_browser: bool = False):
return_df: bool = False, in_browser: bool = False, path: str = None, open_browser: bool = True):
"""
Visualization the plan metrics for clinical criteria in browser.
It evaluate the plan by comparing the metrics against required criteria.
Expand All @@ -44,11 +44,14 @@ def display_clinical_criteria(my_plan: Plan, sol: Union[dict, List[dict]], html_
:param my_plan: object of class Plan
:param sol: optimal solution dictionary
:param dose_1d: vectorized dose 1d array
:param html_file_name: name of the html file to be launched in browser
:param sol_names: Default to Plan Value. column names for the plan evaluation
:param clinical_criteria: clinical criteria to be evaluated
:param return_df: return df instead of visualization
:param in_browser: display table in browser
:param path: path for saving the html file which opens up in browser
:param open_browser: if true, html will be launched in browser
:return: plan metrics in browser
"""
import re
Expand All @@ -57,67 +60,138 @@ def display_clinical_criteria(my_plan: Plan, sol: Union[dict, List[dict]], html_
clinical_criteria = my_plan.clinical_criteria
# df = pd.DataFrame.from_dict(clinical_criteria.clinical_criteria_dict['criteria'])
df = pd.json_normalize(clinical_criteria.clinical_criteria_dict['criteria'])
dose_volume_ind = df.index[df['type'] == 'dose_volume_V'].tolist()
constraint_limit_perc_ind = df.index[~df['constraints.limit_volume_perc'].isnull()].tolist()
constraint_goal_perc_ind = df.index[~df['constraints.goal_volume_perc'].isnull()].tolist()
constraint_limit_gy_ind = df.index[~df['constraints.limit_dose_gy'].isnull()].tolist()
constraint_goal_gy_ind = df.index[~df['constraints.goal_dose_gy'].isnull()].tolist()
for ind in dose_volume_ind:
df.loc[ind, 'type'] = 'V(' + str(round(df['parameters.dose_gy'][ind])) + 'Gy)'
for ind in constraint_limit_gy_ind:
df.loc[ind, 'Limit'] = str(round(df['constraints.limit_dose_gy'][ind])) + 'Gy'
for ind in constraint_limit_perc_ind:
df.loc[ind, 'Limit'] = str(round(df['constraints.limit_volume_perc'][ind])) + '%'
for ind in constraint_goal_gy_ind:
df.loc[ind, 'Goal'] = str(round(df['constraints.goal_dose_gy'][ind])) + 'Gy'
for ind in constraint_goal_perc_ind:
df.loc[ind, 'Goal'] = str(round(df['constraints.goal_volume_perc'][ind])) + '%'
dose_volume_V_ind = df.index[df['type'] == 'dose_volume_V'].tolist()
if dose_volume_V_ind:
volumn_cols = [col for col in df.columns if 'volume' in col]
if volumn_cols:
perc_col = [col_name for col_name in volumn_cols if 'perc' in col_name]
cc_col = [col_name for col_name in volumn_cols if 'cc' in col_name]
for col in perc_col:
if 'limit' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'Limit', col, '%')
if 'goal' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'Goal', col, '%')
for col in cc_col:
if 'limit' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'Limit', col, 'cc')
if 'goal' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'Goal', col, 'cc')
dose_cols = [col for col in df.columns if 'parameters.dose' in col]
if dose_cols:
for col in dose_cols:
if 'perc' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'type', col, '%', 'dose_volume_V')
if 'gy' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'type', col, 'Gy', 'dose_volume_V')

dose_cols = [col for col in df.columns if 'dose' in col]
if dose_cols:
perc_col = [col_name for col_name in dose_cols if 'perc' in col_name]
gy_col = [col_name for col_name in dose_cols if 'gy' in col_name]
for col in perc_col:
if 'limit' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'Limit', col, '%')
if 'goal' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'Goal', col, '%')
for col in gy_col:
if 'limit' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'Limit', col, 'Gy')
if 'goal' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'Goal', col, 'Gy')

dose_volume_D_ind = df.index[df['type'] == 'dose_volume_D'].tolist()
if dose_volume_D_ind:
vol_cols = [col for col in df.columns if 'parameters.volume' in col]
if vol_cols:
for col in vol_cols:
if 'perc' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'type', col, '%', 'dose_volume_D')
if 'cc' in col:
df = Evaluation.add_dvh_to_frame(my_plan, df, 'type', col, 'cc', 'dose_volume_D')

# refine df
df = df.rename(columns={'parameters.structure_name': 'structure_name', 'type': 'constraint'})
df = df.drop(
['parameters.dose_gy', 'constraints.limit_dose_gy', 'constraints.limit_volume_perc',
'constraints.goal_dose_gy', 'constraints.goal_volume_perc','parameters.structure_def'], axis=1, errors='ignore')

# df = df.drop(
# ['parameters.dose_gy', 'constraints.limit_dose_gy', 'constraints.limit_volume_perc',
# 'constraints.goal_dose_gy', 'constraints.goal_volume_perc','parameters.structure_def'], axis=1, errors='ignore')
if 'Goal' not in df:
df['Goal'] = ''
df = df[['constraint', 'structure_name', 'Limit', 'Goal']]

dose_1d_list = []
dummy_sol = {}
if isinstance(sol, dict):
sol = [sol]
if dose_1d is None:
for p, s in enumerate(sol):
dose_1d_list.append(s['inf_matrix'].A @ (s['optimal_intensity'] * my_plan.get_num_of_fractions()))
else:
if isinstance(dose_1d, np.ndarray):
dose_1d_list = [dose_1d]
else:
dose_1d_list = dose_1d
if sol_names is None:
if len(sol) > 1:
sol_names = ['Plan Value ' + str(i) for i in range(len(sol))]
if len(dose_1d_list) > 1:
sol_names = ['Plan Value ' + str(i) for i in range(len(dose_1d_list))]
else:
sol_names = ['Plan Value']
for p, s in enumerate(sol):
dose_1d = s['inf_matrix'].A @ (s['optimal_intensity'] * my_plan.get_num_of_fractions())
for p, dose_1d in enumerate(dose_1d_list):
dummy_sol['inf_matrix'] = my_plan.inf_matrix
dummy_sol['dose_1d'] = dose_1d
for ind in range(len(df)): # Loop through the clinical criteria
if df.constraint[ind] == 'max_dose':
struct = df.structure_name[ind]
if struct in my_plan.structures.get_structures():
max_dose = Evaluation.get_max_dose(s, dose_1d=dose_1d, struct=struct) # get max dose_1d

max_dose = Evaluation.get_max_dose(dummy_sol, dose_1d=dose_1d, struct=struct) # get max dose_1d
if 'Gy' in str(df.Limit[ind]) or 'Gy' in str(df.Goal[ind]):
df.at[ind, sol_names[p]] = round(max_dose,2)
elif '%' in str(df.Limit[ind]) or '%' in str(df.Limit[ind]):
df.at[ind, sol_names[p]] = round(max_dose / my_plan.get_prescription() * 100, 2)
if df.constraint[ind] == 'mean_dose':
df.at[ind, sol_names[p]] = np.round(max_dose,2)
elif '%' in str(df.Limit[ind]) or '%' in str(df.Goal[ind]):
df.at[ind, sol_names[p]] = np.round(max_dose / my_plan.get_prescription() * 100, 2)
elif df.constraint[ind] == 'mean_dose':
struct = df.structure_name[ind]
if struct in my_plan.structures.get_structures():
mean_dose = Evaluation.get_mean_dose(s, dose_1d=dose_1d, struct=struct)
df.at[ind, sol_names[p]] = round(mean_dose, 2)
if "V(" in df.constraint[ind]:
mean_dose = Evaluation.get_mean_dose(dummy_sol, dose_1d=dose_1d, struct=struct)
if 'Gy' in str(df.Limit[ind]) or 'Gy' in str(df.Goal[ind]):
df.at[ind, sol_names[p]] = np.round(mean_dose, 2)
elif '%' in str(df.Limit[ind]) or '%' in str(df.Goal[ind]):
df.at[ind, sol_names[p]] = np.round(mean_dose / my_plan.get_prescription() * 100, 2)
elif "V(" in df.constraint[ind]:
struct = df.structure_name[ind]
if struct in my_plan.structures.get_structures():
if '%' in str(df.Limit[ind]) or '%' in str(df.Goal[ind]): # we are writing str since nan values throws error
dose = re.findall(r"[-+]?(?:\d*\.*\d+)", df.constraint[ind])[0]
dose = re.findall(r"[-+]?(?:\d*\.*\d+)", df.constraint[ind])[0]
# convert dose to Gy
if '%' in df.constraint[ind]:
dose = float(dose)
dose = dose * my_plan.get_prescription() / 100
elif 'Gy' in df.constraint[ind]:
dose = float(dose)
volume = Evaluation.get_volume(s, dose_1d=dose_1d, struct=struct, dose_value_gy=dose)
# get volume in perc
volume = Evaluation.get_volume(dummy_sol, dose_1d=dose_1d, struct=struct, dose_value_gy=dose)
if '%' in str(df.Limit[ind]) or '%' in str(df.Goal[ind]): # we are writing str since nan values throws error
df.at[ind, sol_names[p]] = np.round(volume, 2)
elif 'cc' in str(df.Limit[ind]) or 'cc' in str(df.Goal[ind]):
dose = re.findall(r"[-+]?(?:\d*\.*\d+)", df.constraint[ind])[0]
dose = float(dose)
volume = Evaluation.get_volume(s, dose_1d=dose_1d, struct=struct, dose_value_gy=dose)
vol_cc = my_plan.structures.get_volume_cc(structure_name=struct) * volume / 100
df.at[ind, sol_names[p]] = np.round(vol_cc, 2)
elif "D(" in df.constraint[ind]:
struct = df.structure_name[ind]
if struct in my_plan.structures.get_structures():
volume = re.findall(r"[-+]?(?:\d*\.*\d+)", df.constraint[ind])[0]
# convert volume to perc
if '%' in df.constraint[ind]:
volume = float(volume)
elif 'cc' in df.constraint[ind]:
volume = float(volume)
volume = volume / my_plan.structures.get_volume_cc(structure_name=struct) * 100

# get dose
dose = Evaluation.get_dose(dummy_sol, dose_1d=dose_1d, struct=struct, volume_per=volume)
if '%' in str(df.Limit[ind]) or '%' in str(df.Goal[ind]): # we are writing str since nan values throws error
df.at[ind, sol_names[p]] = np.round(dose/my_plan.get_prescription()*100, 2)
elif 'Gy' in str(df.Limit[ind]) or 'Gy' in str(df.Goal[ind]):
df.at[ind, sol_names[p]] = np.round(dose, 2)
df.round(2)
df = df[df['Plan Value'].notna()] # remove rows for which plan value is Nan
df = df[df[sol_names].notna().all(axis=1)] # remove rows for which plan value is Nan
df = df.fillna('')
# df.dropna(axis=0, inplace=True) # remove structures which are not present
# df.reset_index(drop=True, inplace=True) # reset the index
Expand Down Expand Up @@ -175,9 +249,13 @@ def color_plan_value(row):
</body>
</html>.
'''
with open(html_file_name, 'w') as f:
if path is None:
path = os.getcwd()
html_file_path = os.path.join(path, html_file_name)
with open(html_file_path, 'w') as f:
f.write(html_string.format(table=html))
webbrowser.open('file://' + os.path.realpath(html_file_name))
if open_browser:
webbrowser.open('file://' + os.path.realpath(html_file_path))

else:
if Evaluation.is_notebook():
Expand Down Expand Up @@ -213,7 +291,9 @@ def get_dose(sol: dict, struct: str, volume_per: float, dose_1d: np.ndarray = No
if np.array_equal(x, np.array([0])) and np.array_equal(y, np.array([0])):
return 0
f = interpolate.interp1d(100 * y, x)

if volume_per > 100.1:
print('Warning: Volume Percentage: {} for structure {} is invalid'.format(volume_per, struct))
return 0
return f(volume_per)

@staticmethod
Expand Down Expand Up @@ -405,6 +485,28 @@ def get_BED(my_plan: Plan, sol: dict = None, dose_per_fraction_1d: np.ndarray =
return bed_d

@staticmethod
def add_dvh_to_frame(my_plan: Plan, df: pd.DataFrame, new_column_name: str, old_column_name: str, unit: str, dvh_type=None):
req_ind = df.index[~df[old_column_name].isnull()].tolist()
for ind in req_ind:
if dvh_type is None:
df.loc[ind, new_column_name] = str(round(Evaluation.get_num(my_plan, df[old_column_name][ind]), 2)) + unit
elif dvh_type is not None:
if dvh_type == 'dose_volume_V':
df.loc[ind, new_column_name] = 'V(' + str(round(Evaluation.get_num(my_plan, df[old_column_name][ind]), 2)) + unit + ')'
elif dvh_type == 'dose_volume_D':
df.loc[ind, new_column_name] = 'D(' + str(round(Evaluation.get_num(my_plan, df[old_column_name][ind]), 2)) + unit + ')'
return df

@staticmethod
def get_num(my_plan, string: Union[str, float]):
if "prescription_gy" in str(string):
prescription_gy = my_plan.get_prescription()
return eval(string)
elif isinstance(string, float) or isinstance(string, int):
return string
else:
raise Exception('Invalid constraint')
@staticmethod
def is_notebook() -> bool:
try:
from IPython import get_ipython
Expand Down

0 comments on commit f0f7759

Please sign in to comment.