@@ -140,34 +140,38 @@ def get_criteria(self, type: str = None) -> List[dict]:
140140 return all_criteria
141141
142142 def check_criterion_exists (self , criterion , return_ind :bool = False ):
143- criterion_exist = False
144- criterion_ind = None
145- for ind , crit in enumerate (self .clinical_criteria_dict ['criteria' ]):
146- if (crit ['type' ] == criterion ['type' ]) and crit ['parameters' ] == criterion ['parameters' ]:
147- for constraint in crit ['constraints' ]:
148- if constraint == criterion ['constraints' ]:
149- criterion_exist = True
150- criterion_ind = ind
151- if return_ind :
152- return criterion_exist ,criterion_ind
153- else :
154- return criterion_exist
143+ criterion_exist = False
144+ criterion_ind = None
145+ for ind , crit in enumerate (self .clinical_criteria_dict ['criteria' ]):
146+ if (crit ['type' ] == criterion ['type' ]) and crit ['parameters' ] == criterion ['parameters' ]:
147+ for constraint in crit ['constraints' ]:
148+ for key , _ in criterion ['constraints' ].items ():
149+ if constraint == key :
150+ criterion_exist = True
151+ criterion_ind = ind
152+ if return_ind :
153+ return criterion_exist ,criterion_ind
154+ else :
155+ return criterion_exist
155156
156157 def modify_criterion (self , criterion ):
157158 """
158- Modify the criterion the clinical criteria
159-
160-
159+ Update constraints for an existing criterion.
160+ Only updates keys that already exist in the original constraint dict.
161161 """
162- criterion_found = False
163162 for ind , crit in enumerate (self .clinical_criteria_dict ['criteria' ]):
164- if (crit ['type' ] == criterion ['type' ]) and crit ['parameters' ] == criterion ['parameters' ]:
165- for constraint in crit ['constraints' ]:
166- if constraint == criterion ['constraints' ]:
167- self .clinical_criteria_dict ['criteria' ][ind ]['constraints' ][constraint ] = criterion ['constraints' ]
168- criterion_found = True
169- if not criterion_found :
170- raise Warning ('No criteria for {}' .format (criterion ))
163+
164+ if crit ['type' ] == criterion ['type' ] and crit ['parameters' ] == criterion ['parameters' ]:
165+ existing_keys = crit ['constraints' ]
166+ updated = False
167+ for key , value in criterion .get ('constraints' , {}).items ():
168+ if key in existing_keys :
169+ existing_keys [key ] = value
170+ updated = True
171+ if updated :
172+ return
173+
174+ raise Warning (f"No criteria found for { criterion } " )
171175
172176
173177 def get_num (self , string : Union [str , float ]):
@@ -279,6 +283,7 @@ def get_dvh_table(self, my_plan: Plan, constraint_list: list = None, opt_params:
279283 df .at [count , 'volume_perc' ] = dvh_updated_list [i ]['constraints' ][limit_key ]
280284 df .at [count , 'dvh_type' ] = 'constraint'
281285 df .at [count , 'dvh_method' ] = dvh_method
286+ df .at [count , 'bound_type' ] = dvh_updated_list [i ]['constraints' ].get ('bound_type' , 'upper' )
282287 count = count + 1
283288 goal_key = self .matching_keys (dvh_updated_list [i ]['constraints' ], 'goal' )
284289 if goal_key in dvh_updated_list [i ]['constraints' ]:
@@ -288,6 +293,7 @@ def get_dvh_table(self, my_plan: Plan, constraint_list: list = None, opt_params:
288293 df .at [count , 'dvh_type' ] = 'goal'
289294 df .at [count , 'dvh_method' ] = dvh_method
290295 df .at [count , 'weight' ] = dvh_updated_list [i ]['parameters' ]['weight' ]
296+ df .at [count , 'bound_type' ] = dvh_updated_list [i ]['constraints' ].get ('bound_type' , 'upper' )
291297 count = count + 1
292298 if 'dose_volume_D' in dvh_updated_list [i ]['type' ]:
293299 limit_key = self .matching_keys (dvh_updated_list [i ]['constraints' ], 'limit' )
@@ -297,6 +303,7 @@ def get_dvh_table(self, my_plan: Plan, constraint_list: list = None, opt_params:
297303 df .at [count , 'dose_gy' ] = self .dose_to_gy (limit_key , dvh_updated_list [i ]['constraints' ][limit_key ])
298304 df .at [count , 'dvh_method' ] = dvh_method
299305 df .at [count , 'dvh_type' ] = 'constraint'
306+ df .at [count , 'bound_type' ] = dvh_updated_list [i ]['constraints' ].get ('bound_type' , 'upper' )
300307 count = count + 1
301308 goal_key = self .matching_keys (dvh_updated_list [i ]['constraints' ], 'goal' )
302309 if goal_key in dvh_updated_list [i ]['constraints' ]:
@@ -306,14 +313,15 @@ def get_dvh_table(self, my_plan: Plan, constraint_list: list = None, opt_params:
306313 df .at [count , 'dvh_method' ] = dvh_method
307314 df .at [count , 'dvh_type' ] = 'goal'
308315 df .at [count , 'weight' ] = dvh_updated_list [i ]['parameters' ]['weight' ]
316+ df .at [count , 'bound_type' ] = dvh_updated_list [i ]['constraints' ].get ('bound_type' , 'upper' )
309317 count = count + 1
310318 self .dvh_table = df
311319 self .get_max_tol (constraints_list = constraint_list )
312320 self .filter_dvh (my_plan = my_plan )
313321 return self .dvh_table
314322
315323
316- def get_low_dose_vox_ind (self , my_plan : Plan , dose : np .ndarray , inf_matrix : InfluenceMatrix ):
324+ def get_low_dose_vox_ind (self , my_plan : Plan , dose : np .ndarray , inf_matrix : InfluenceMatrix = None ):
317325 """
318326 Identifies and stores the indices of low-dose voxels for each DVH constraint or goal.
319327
@@ -342,15 +350,25 @@ def get_low_dose_vox_ind(self, my_plan: Plan, dose: np.ndarray, inf_matrix: Infl
342350 volume percentage.
343351 """
344352 dvh_table = self .dvh_table
345- inf_matrix = my_plan .inf_matrix
353+ if inf_matrix is None :
354+ inf_matrix = my_plan .inf_matrix
346355 for ind in dvh_table .index :
347356 structure_name , dose_gy , vol_perc = dvh_table ['structure_name' ][ind ], dvh_table ['dose_gy' ][ind ], \
348357 dvh_table ['volume_perc' ][ind ]
349358 dvh_type = dvh_table ['dvh_type' ][ind ]
359+ bound_type = dvh_table .at [ind , 'bound_type' ] if 'bound_type' in dvh_table .columns else 'upper'
350360 vol_perc = vol_perc / inf_matrix .get_fraction_of_vol_in_calc_box (structure_name )
351361 struct_vox = inf_matrix .get_opt_voxels_idx (structure_name )
352362 n_struct_vox = len (struct_vox )
353- sort_ind = np .argsort (dose [struct_vox ])
363+ # sort_ind = np.argsort(dose[struct_vox])
364+ if bound_type == 'lower' :
365+ # For lower bound (e.g., PTV coverage), pick HIGHEST-dose voxels up to p%
366+ sort_ind = np .argsort (- dose [struct_vox ]) # descending
367+ target_perc = vol_perc # accumulate to p%
368+ else :
369+ # Original behavior (upper bound): pick LOWEST-dose voxels up to (100 - p)%
370+ sort_ind = np .argsort (dose [struct_vox ]) # ascending
371+ target_perc = 100 - vol_perc # accumulate to (100 - p)%
354372 voxel_sort = struct_vox [sort_ind ]
355373 weights = inf_matrix .get_opt_voxels_volume_cc (structure_name )
356374 weights_sort = weights [sort_ind ]
@@ -360,7 +378,7 @@ def get_low_dose_vox_ind(self, my_plan: Plan, dose: np.ndarray, inf_matrix: Infl
360378 for w_ind in range (n_struct_vox ):
361379 w_sum = w_sum + weights_sort [w_ind ]
362380 w_ratio = w_sum / weight_all_sum
363- if w_ratio * 100 >= ( 100 - vol_perc ) :
381+ if w_ratio * 100 >= target_perc :
364382 break
365383 low_dose_voxels = voxel_sort [:w_ind + 1 ]
366384 if ind == 0 :
0 commit comments