@@ -171,6 +171,188 @@ def plot_dvh(my_plan: Plan, sol: dict = None, dose_1d: np.ndarray = None, struct
171171 plt .savefig (filename , bbox_inches = "tight" , dpi = 300 )
172172 return ax
173173
174+ @staticmethod
175+ def plot_robust_dvh (my_plan : Plan , sol : dict = None , dose_1d_list : list = None , struct_names : List [str ] = None ,
176+ dose_scale : dose_type = "Absolute(Gy)" ,
177+ volume_scale : volume_type = "Relative(%)" , plot_scenario = None , ** options ):
178+ """
179+ Create dvh plot for the selected structures
180+
181+ :param my_plan: object of class Plan
182+ :param sol: optimal sol dictionary
183+ :param dose_1d: dose_1d in 1d voxels
184+ :param struct_names: structures to be included in dvh plot
185+ :param volume_scale: volume scale on y-axis. Default= Absolute(cc). e.g. volume_scale = "Absolute(cc)" or volume_scale = "Relative(%)"
186+ :param dose_scale: dose_1d scale on x axis. Default= Absolute(Gy). e.g. dose_scale = "Absolute(Gy)" or dose_scale = "Relative(%)"
187+ :keyword style (str): line style for dvh curve. default "solid". can be "dotted", "dash-dotted".
188+ :keyword width (int): width of line. Default 2
189+ :keyword colors(list): list of colors
190+ :keyword legend_font_size: Set legend_font_size. default 10
191+ :keyword figsize: Set figure size for the plot. Default figure size (12,8)
192+ :keyword create_fig: Create a new figure. Default True. If False, append to the previous figure
193+ :keyword title: Title for the figure
194+ :keyword filename: Name of the file to save the figure in current directory
195+ :keyword show: Show the figure. Default is True. If false, next plot can be append to it
196+ :keyword norm_flag: Use to normalize the plan. Default is False.
197+ :keyword norm_volume: Use to set normalization volume. default is 90 percentile.
198+ :return: dvh plot for the selected structures
199+
200+ :Example:
201+ >>> Visualization.plot_dvh(my_plan, sol=sol, struct_names=['PTV', 'ESOPHAGUS'], dose_scale='Absolute(Gy)',volume_scale="Relative(%)", show=False, create_fig=True )
202+ """
203+
204+ if not isinstance (dose_1d_list , list ):
205+ dose_1d_list = [dose_1d_list ]
206+ if len (dose_1d_list ) == 0 :
207+ raise ValueError ("dose_list is empty" )
208+ if sol is None :
209+ sol = dict ()
210+ sol ['inf_matrix' ] = my_plan .inf_matrix # create temporary solution
211+
212+ if dose_1d_list is None :
213+ dose_1d_list = []
214+ if isinstance (sol , list ):
215+ for s in sol :
216+ if 'inf_matrix' not in s :
217+ s ['inf_matrix' ] = my_plan .inf_matrix
218+ dose_1d_list += [s ['inf_matrix' ].A @ (s ['optimal_intensity' ] * my_plan .get_num_of_fractions ())]
219+
220+ # getting options_fig:
221+ style = options ['style' ] if 'style' in options else 'solid'
222+ width = options ['width' ] if 'width' in options else None
223+ colors = options ['colors' ] if 'colors' in options else None
224+ legend_font_size = options ['legend_font_size' ] if 'legend_font_size' in options else 15
225+ figsize = options ['figsize' ] if 'figsize' in options else (12 , 8 )
226+ title = options ['title' ] if 'title' in options else None
227+ filename = options ['filename' ] if 'filename' in options else None
228+ show = options ['show' ] if 'show' in options else False
229+ # create_fig = options['create_fig'] if 'create_fig' in options else False
230+ show_criteria = options ['show_criteria' ] if 'show_criteria' in options else None
231+ ax = options ['ax' ] if 'ax' in options else None
232+ fontsize = options ['fontsize' ] if 'fontsize' in options else 12
233+ legend_loc = options ["legend_loc" ] if "legend_loc" in options else "upper right"
234+ # getting norm options
235+ norm_flag = options ['norm_flag' ] if 'norm_flag' in options else False
236+ norm_volume = options ['norm_volume' ] if 'norm_volume' in options else 90
237+ norm_struct = options ['norm_struct' ] if 'norm_struct' in options else 'PTV'
238+
239+ # plt.rcParams['font.size'] = font_size
240+ # plt.rc('font', family='serif')
241+ if width is None :
242+ width = 3
243+ if colors is None :
244+ colors = Visualization .get_colors ()
245+ if struct_names is None :
246+ # orgs = []
247+ struct_names = my_plan .structures .structures_dict ['name' ]
248+ max_dose = 0.0
249+ max_vol = 0.0
250+ all_orgs = my_plan .structures .structures_dict ['name' ]
251+ # orgs = [struct.upper for struct in orgs]
252+ pres = my_plan .get_prescription ()
253+ legend = []
254+
255+ if ax is None :
256+ fig , ax = plt .subplots (figsize = figsize )
257+ # if norm_flag:
258+ # norm_factor = Evaluation.get_dose(sol, dose_1d=dose_1d, struct=norm_struct, volume_per=norm_volume) / pres
259+ # dose_1d = dose_1d / norm_factor
260+ count = 0
261+ for i in range (np .size (all_orgs )):
262+ if all_orgs [i ] not in struct_names :
263+ continue
264+ if my_plan .structures .get_fraction_of_vol_in_calc_box (all_orgs [i ]) == 0 : # check if the structure is within calc box
265+ print ('Skipping Structure {} as it is not within calculation box.' .format (all_orgs [i ]))
266+ continue
267+ dose_sort_list = []
268+ for dose_1d in dose_1d_list :
269+ x , y = Evaluation .get_dvh (sol , struct = all_orgs [i ], dose_1d = dose_1d )
270+ dose_sort_list .append (x )
271+ d_sort_mat = np .column_stack (dose_sort_list )
272+ # Compute min/max DVH curve taken across scenarios.
273+ d_min_mat = np .min (d_sort_mat , axis = 1 )
274+ d_max_mat = np .max (d_sort_mat , axis = 1 )
275+
276+ if dose_scale == 'Absolute(Gy)' :
277+ max_dose = np .maximum (max_dose , d_max_mat [- 1 ])
278+ ax .set_xlabel ('Dose (Gy)' , fontsize = fontsize )
279+ elif dose_scale == 'Relative(%)' :
280+ max_dose = np .maximum (max_dose , d_max_mat [- 1 ])
281+ max_dose = max_dose / pres * 100
282+ ax .set_xlabel ('Dose ($\%$)' , fontsize = fontsize )
283+
284+ if volume_scale == 'Absolute(cc)' :
285+ y = y * my_plan .structures .get_volume_cc (all_orgs [i ]) / 100
286+ max_vol = np .maximum (max_vol , y [1 ] * 100 )
287+ ax .set_ylabel ('Volume (cc)' , fontsize = fontsize )
288+ elif volume_scale == 'Relative(%)' :
289+ max_vol = np .maximum (max_vol , y [0 ] * 100 )
290+ ax .set_ylabel ('Volume Fraction ($\%$)' , fontsize = fontsize )
291+ # ax.plot(x, 100 * y, linestyle=style, linewidth=width, color=colors[count])
292+
293+ # ax.plot(d_min_mat, 100 * y, linestyle='dotted', linewidth=width*0.5, color=colors[count])
294+ # ax.plot(d_max_mat, 100 * y, linestyle='dotted', linewidth=width*0.5, color=colors[count])
295+ ax .fill_betweenx (100 * y , d_min_mat , d_max_mat , alpha = 0.25 , color = colors [count ])
296+
297+ # Plot user-specified scenarios.
298+ if plot_scenario is not None :
299+ if plot_scenario == 'mean' :
300+ dose_mean = np .mean (d_sort_mat , axis = 1 )
301+ ax .plot (dose_mean , 100 * y , linestyle = style , color = colors [count ], linewidth = width , label = all_orgs [i ])
302+ elif not isinstance (plot_scenario , list ):
303+ plot_scenario = [plot_scenario ]
304+
305+ for n in range (len (plot_scenario )):
306+ scene_num = plot_scenario [n ]
307+ if norm_flag :
308+ norm_factor = Evaluation .get_dose (sol , dose_1d = dose_1d_list [scene_num ], struct = norm_struct , volume_per = norm_volume ) / pres
309+ dose_sort_list [scene_num ] = dose_sort_list [scene_num ] / norm_factor
310+ d_min_mat = d_min_mat / norm_factor
311+ d_max_mat = d_max_mat / norm_factor
312+ ax .plot (dose_sort_list [scene_num ], 100 * y , linestyle = style , color = colors [count ], linewidth = width )
313+ count = count + 1
314+ # legend.append(all_orgs[i])
315+
316+ if show_criteria is not None :
317+ for s in range (len (show_criteria )):
318+ if 'dose_volume' in show_criteria [s ]['type' ]:
319+ x = show_criteria [s ]['parameters' ]['dose_gy' ]
320+ y = show_criteria [s ]['constraints' ]['limit_volume_perc' ]
321+ ax .plot (x , y , marker = 'x' , color = 'red' , markersize = 20 )
322+ # plt.xlabel('Dose (Gy)')
323+ # plt.ylabel('Volume Fraction (%)')
324+ current_xlim = ax .get_xlim ()
325+ final_xmax = max (current_xlim [1 ], max_dose * 1.1 )
326+ ax .set_xlim (0 , final_xmax )
327+ ax .set_ylim (0 , max_vol )
328+ handles , labels = ax .get_legend_handles_labels ()
329+ unique = [(h , l ) for i , (h , l ) in enumerate (zip (handles , labels )) if l not in labels [:i ]]
330+ ax .legend (* zip (* unique ), prop = {'size' : legend_font_size }, loc = legend_loc )
331+ # ax.legend(legend, prop={'size': legend_font_size}, loc=legend_loc)
332+ ax .grid (visible = True , which = 'major' , color = '#666666' , linestyle = '-' )
333+
334+ # Show the minor grid lines with very faint and almost transparent grey lines
335+ # plt.minorticks_on()
336+ ax .minorticks_on ()
337+ plt .grid (visible = True , which = 'minor' , color = '#999999' , linestyle = '--' , alpha = 0.2 )
338+ y = np .arange (0 , 101 )
339+ # if norm_flag:
340+ # x = pres * np.ones_like(y)
341+ # else:
342+ if dose_scale == "Absolute(Gy)" :
343+ x = pres * np .ones_like (y )
344+ else :
345+ x = 100 * np .ones_like (y )
346+
347+ ax .plot (x , y , color = 'black' )
348+ if title :
349+ ax .set_title (title )
350+ if show :
351+ plt .show ()
352+ if filename is not None :
353+ plt .savefig (filename , bbox_inches = "tight" , dpi = 300 )
354+ return ax
355+
174356 @staticmethod
175357 def plot_fluence_2d (beam_id : int , sol : dict = None , optimal_fluence_2d : List [np .ndarray ] = None ,
176358 inf_matrix : InfluenceMatrix = None , ** options ):
@@ -263,16 +445,17 @@ def plot_fluence_3d(beam_id: int, sol: dict = None, optimal_fluence_2d: List[np.
263445 return ax
264446
265447 @staticmethod
266- def plot_2d_slice (my_plan : Plan = None , sol : dict = None , ct : CT = None , structs : Structures = None ,
448+ def plot_2d_slice (my_plan : Plan = None , sol : dict = None , dose_1d : np . ndarray = None , ct : CT = None , structs : Structures = None ,
267449 slice_num : int = 40 , struct_names : List [str ] = None , show_dose : bool = False ,
268450 show_struct : bool = True , show_isodose : bool = False ,
269451 ** options ) -> None :
270452 """
271453
272454 Plot 2d view of ct, dose_1d, isodose and struct_name contours
273455
274- :param sol: solution to optimization
275456 :param my_plan: object of class Plan
457+ :param sol: Optional solution to optimization
458+ :param dose_1d: Optional dose as 1d array
276459 :param ct: Optional. object of class CT
277460 :param structs: Optional. object of class structs
278461 :param slice_num: slice number
@@ -309,8 +492,11 @@ def plot_2d_slice(my_plan: Plan = None, sol: dict = None, ct: CT = None, structs
309492 if sol is not None :
310493 show_dose = True
311494 if show_dose :
312- dose_1d = sol ['inf_matrix' ].A @ (sol ['optimal_intensity' ] * my_plan .get_num_of_fractions ())
313- dose_3d = sol ['inf_matrix' ].dose_1d_to_3d (dose_1d = dose_1d )
495+ if sol is not None :
496+ dose_1d = sol ['inf_matrix' ].A @ (sol ['optimal_intensity' ] * my_plan .get_num_of_fractions ())
497+ dose_3d = sol ['inf_matrix' ].dose_1d_to_3d (dose_1d = dose_1d )
498+ else :
499+ dose_3d = my_plan .inf_matrix .dose_1d_to_3d (dose_1d = dose_1d )
314500 if hasattr (my_plan , 'structures' ):
315501 body_mask = my_plan .structures .get_structure_mask_3d ('BODY' )
316502 masked = np .ma .masked_where (~ body_mask [slice_num , :, :].astype (bool ), dose_3d [slice_num , :, :])
0 commit comments