Skip to content

Commit 742ece6

Browse files
authored
Merge pull request #15 from sijiaww/cvar-upper-only-clean
Add CVaR upper constraint helper in Optimization. Merged to dev branch
2 parents 3196613 + 8ddcf25 commit 742ece6

File tree

2 files changed

+75
-1
lines changed

2 files changed

+75
-1
lines changed

portpy/photon/clinical_criteria.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,9 @@ def get_dvh_table(self, my_plan: Plan, constraint_list: list = None, opt_params:
271271
df = pd.DataFrame()
272272
count = 0
273273
for i in range(len(dvh_updated_list)):
274+
275+
dvh_method = dvh_updated_list[i]['parameters'].get('dvh_method', None)
276+
274277
if 'dose_volume_V' in dvh_updated_list[i]['type']:
275278
limit_key = self.matching_keys(dvh_updated_list[i]['constraints'], 'limit')
276279
dose_key = self.matching_keys(dvh_updated_list[i]['parameters'], 'dose_')
@@ -279,6 +282,7 @@ def get_dvh_table(self, my_plan: Plan, constraint_list: list = None, opt_params:
279282
df.at[count, 'dose_gy'] = self.dose_to_gy(dose_key, dvh_updated_list[i]['parameters'][dose_key])
280283
df.at[count, 'volume_perc'] = dvh_updated_list[i]['constraints'][limit_key]
281284
df.at[count, 'dvh_type'] = 'constraint'
285+
df.at[count, 'dvh_method'] = dvh_method
282286
df.at[count, 'bound_type'] = dvh_updated_list[i]['constraints'].get('bound_type', 'upper')
283287
count = count + 1
284288
goal_key = self.matching_keys(dvh_updated_list[i]['constraints'], 'goal')
@@ -287,6 +291,7 @@ def get_dvh_table(self, my_plan: Plan, constraint_list: list = None, opt_params:
287291
df.at[count, 'dose_gy'] = self.dose_to_gy(dose_key, dvh_updated_list[i]['parameters'][dose_key])
288292
df.at[count, 'volume_perc'] = dvh_updated_list[i]['constraints'][goal_key]
289293
df.at[count, 'dvh_type'] = 'goal'
294+
df.at[count, 'dvh_method'] = dvh_method
290295
df.at[count, 'weight'] = dvh_updated_list[i]['parameters']['weight']
291296
df.at[count, 'bound_type'] = dvh_updated_list[i]['constraints'].get('bound_type', 'upper')
292297
count = count + 1
@@ -296,6 +301,7 @@ def get_dvh_table(self, my_plan: Plan, constraint_list: list = None, opt_params:
296301
df.at[count, 'structure_name'] = dvh_updated_list[i]['parameters']['structure_name']
297302
df.at[count, 'volume_perc'] = dvh_updated_list[i]['parameters']['volume_perc']
298303
df.at[count, 'dose_gy'] = self.dose_to_gy(limit_key, dvh_updated_list[i]['constraints'][limit_key])
304+
df.at[count, 'dvh_method'] = dvh_method
299305
df.at[count, 'dvh_type'] = 'constraint'
300306
df.at[count, 'bound_type'] = dvh_updated_list[i]['constraints'].get('bound_type', 'upper')
301307
count = count + 1
@@ -304,6 +310,7 @@ def get_dvh_table(self, my_plan: Plan, constraint_list: list = None, opt_params:
304310
df.at[count, 'structure_name'] = dvh_updated_list[i]['parameters']['structure_name']
305311
df.at[count, 'volume_perc'] = dvh_updated_list[i]['parameters']['volume_perc']
306312
df.at[count, 'dose_gy'] = self.dose_to_gy(goal_key, dvh_updated_list[i]['constraints'][goal_key])
313+
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']
309316
df.at[count, 'bound_type'] = dvh_updated_list[i]['constraints'].get('bound_type', 'upper')

portpy/photon/optimization.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,73 @@ def create_cvxpy_problem(self):
159159
d_max = np.infty * np.ones(A.shape[0]) # create a vector to avoid putting redundant max constraint on
160160
# duplicate voxels among structure
161161

162-
# Adding max/mean constraints
162+
# Adding constraints
163163
for i in range(len(constraint_def)):
164+
165+
item = constraint_def[i]
166+
item_type = item.get('type')
167+
168+
if item_type in ('dose_volume_V', 'dose_volume_D'):
169+
170+
params = item.get('parameters', {})
171+
cons = item.get('constraints', {})
172+
173+
dvh_method = params.get('dvh_method', None)
174+
if dvh_method != 'cvar':
175+
continue
176+
177+
struct = params['structure_name']
178+
voxel_idx = st.get_opt_voxels_idx(struct)
179+
if len(voxel_idx) == 0:
180+
continue
181+
182+
limit = float(params['dose_gy'])
183+
184+
volume_perc = float(cons['limit_volume_perc'])
185+
if not (0.0 < volume_perc < 100.0):
186+
raise ValueError(
187+
f"limit_volume_perc must be in (0,100), got {volume_perc}"
188+
)
189+
190+
alpha = 1.0 - volume_perc / 100.0
191+
dose_1d_list = A[voxel_idx, :] @ x * num_fractions
192+
193+
if cons.get('constraint_type') == 'upper':
194+
195+
label = f"constraint_{struct}_{item_type}_upper_a{alpha:.4f}"
196+
197+
zeta = cp.Variable(name=f"zeta_{label}")
198+
w = cp.Variable(len(voxel_idx), name=f"w_{label}")
199+
200+
self.vars[f"zeta_{label}"] = zeta
201+
self.vars[f"w_{label}"] = w
202+
203+
constraints += [
204+
zeta + (1.0 / ((1.0 - alpha) * len(voxel_idx))) * cp.sum(w) <= limit,
205+
w >= dose_1d_list - zeta,
206+
w >= 0
207+
]
208+
209+
elif cons.get('constraint_type') == 'lower':
210+
211+
dose_neg = -dose_1d_list
212+
limit_neg = -limit
213+
214+
label = f"constraint_{struct}_{item_type}_lower_a{alpha:.4f}"
215+
216+
zeta = cp.Variable(name=f"zeta_{label}")
217+
w = cp.Variable(len(voxel_idx), name=f"w_{label}")
218+
219+
self.vars[f"zeta_{label}"] = zeta
220+
self.vars[f"w_{label}"] = w
221+
222+
constraints += [
223+
zeta + (1.0 / ((1.0 - alpha) * len(voxel_idx))) * cp.sum(w) <= limit_neg,
224+
w >= dose_neg - zeta,
225+
w >= 0
226+
]
227+
228+
164229
if constraint_def[i]['type'] == 'max_dose':
165230
limit_key = self.matching_keys(constraint_def[i]['constraints'], 'limit')
166231
if limit_key:
@@ -188,13 +253,15 @@ def create_cvxpy_problem(self):
188253
(cp.sum((cp.multiply(st.get_opt_voxels_volume_cc(org),
189254
A[st.get_opt_voxels_idx(org), :] @ x))))
190255
<= limit / num_fractions]
256+
191257
mask = np.isfinite(d_max)
192258
# Create index mask arrays
193259
indices = np.arange(len(mask)) # assumes mask is 1D and corresponds to voxel indices
194260
all_d_max_vox_ind = indices[mask]
195261
constraints += [A[all_d_max_vox_ind, :] @ x <= d_max[all_d_max_vox_ind]] # Add constraint for all d_max voxels at once
196262
print('Problem created')
197263

264+
198265
def add_max(self, struct: str, dose_gy: float):
199266
"""
200267
Add max constraints to the problem

0 commit comments

Comments
 (0)