Skip to content

Commit a759d08

Browse files
authored
Merge pull request #18 from sijiaww/feature/colgen-dose-mimic
Add prediction-based dose mimicking APIs to VMAT column generation
2 parents da63803 + 45ab36e commit a759d08

File tree

2 files changed

+57
-4
lines changed

2 files changed

+57
-4
lines changed

portpy/photon/vmat_scp/vmat_optimization_col_gen.py

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class VmatOptimizationColGen(Optimization):
4444
def __init__(self, my_plan: Plan, arcs=None, inf_matrix: InfluenceMatrix = None,
4545
clinical_criteria: ClinicalCriteria = None,
4646
opt_params: dict = None, vars: dict = None, sol=None,
47-
is_corr: bool = False, delta: np.ndarray = None):
47+
is_corr: bool = False, delta: np.ndarray = None, predicted_dose = None):
4848

4949
# combine steps objective functions into one
5050
if 'col_gen' in opt_params:
@@ -79,6 +79,7 @@ def __init__(self, my_plan: Plan, arcs=None, inf_matrix: InfluenceMatrix = None,
7979
self.constraints_ind_map = {}
8080
if delta is None:
8181
self.delta = np.zeros(self.inf_matrix.A.shape[0])
82+
self.predicted_dose = predicted_dose
8283

8384
def calculate_scores_grad_new(self, A, A_indices, sol):
8485
"""
@@ -107,6 +108,8 @@ def calculate_scores_grad_new(self, A, A_indices, sol):
107108

108109
# Initialize gradients
109110
num_beamlets = A.shape[1]
111+
ptv_dm_grad = np.zeros(num_beamlets)
112+
oar_dm_grad = np.zeros(num_beamlets)
110113
overdose_grad = np.zeros(num_beamlets)
111114
underdose_grad = np.zeros(num_beamlets)
112115
quadratic_grad = np.zeros(num_beamlets)
@@ -118,8 +121,33 @@ def calculate_scores_grad_new(self, A, A_indices, sol):
118121
inf_matrix = self.my_plan.inf_matrix
119122
num_fractions = self.my_plan.get_num_of_fractions()
120123

124+
if self.predicted_dose is not None:
125+
pred_dose_per_frac = self.predicted_dose / num_fractions
126+
121127
for obj in obj_funcs:
122-
if obj['type'] == 'quadratic-overdose':
128+
if obj['type'] == "PTV-dose-mimicking":
129+
struct = obj['structure_name']
130+
if struct in self.my_plan.structures.get_structures():
131+
vox = inf_matrix.get_opt_voxels_idx(struct)
132+
if len(vox) == 0:
133+
continue
134+
weight = float(obj['weight'])
135+
influence = A[vox, :]
136+
ptv_dm_grad += (2 * weight / len(vox)) * (influence.T @ (current_dose[vox] - pred_dose_per_frac[vox]))
137+
elif obj['type'] == "OAR-dose-mimicking":
138+
target_voxels = self.inf_matrix.get_opt_voxels_idx('PTV')
139+
all_vox = self.my_plan.inf_matrix.get_opt_voxels_idx('BODY')
140+
oar_voxels = np.setdiff1d(all_vox, target_voxels)
141+
if oar_voxels.size == 0:
142+
continue
143+
weight = obj['weight']
144+
influence = A[oar_voxels, :]
145+
d_now = current_dose[oar_voxels]
146+
d_hat = pred_dose_per_frac[oar_voxels]
147+
over = d_now - d_hat
148+
over[over < 0.0] = 0.0
149+
oar_dm_grad += (2.0 * weight / len(oar_voxels)) * (influence.T @ over)
150+
elif obj['type'] == 'quadratic-overdose':
123151
struct = obj['structure_name']
124152
if struct not in structures.get_structures():
125153
continue
@@ -567,10 +595,34 @@ def solve_rmp(self, *args, **kwargs):
567595
obj = self.obj
568596
constraints = self.constraints
569597

598+
if self.predicted_dose is not None:
599+
pred_dose_per_fraction = self.predicted_dose / num_fractions
600+
570601

571602
# obj += [(1 / B.shape[0]) * cp.sum_squares(cp.multiply(np.sqrt(self.weights), B @ y - p))]
572603
for i in range(len(obj_funcs)):
573-
if obj_funcs[i]['type'] == 'quadratic-overdose':
604+
if obj_funcs[i]['type'] == "PTV-dose-mimicking":
605+
struct = obj_funcs[i]['structure_name']
606+
if struct in self.my_plan.structures.get_structures():
607+
vox = st.get_opt_voxels_idx(struct)
608+
if len(vox) == 0:
609+
continue
610+
weight = float(obj_funcs[i]['weight'])
611+
obj += [
612+
(weight / len(vox)) * cp.sum_squares(B[vox, :] @ y - pred_dose_per_fraction[vox])
613+
]
614+
elif obj_funcs[i]['type'] == "OAR-dose-mimicking":
615+
target_voxels = self.inf_matrix.get_opt_voxels_idx('PTV')
616+
all_vox = self.my_plan.inf_matrix.get_opt_voxels_idx('BODY')
617+
oar_voxels = np.setdiff1d(all_vox, target_voxels)
618+
weight = float(obj_funcs[i]['weight'])
619+
dO = B[oar_voxels, :] @ y
620+
s_var = cp.Variable(len(oar_voxels), nonneg=True)
621+
constraints += [ dO <= pred_dose_per_fraction[oar_voxels] + s_var ]
622+
obj += [
623+
(weight / len(oar_voxels)) * cp.sum_squares(s_var)
624+
]
625+
elif obj_funcs[i]['type'] == 'quadratic-overdose':
574626
if obj_funcs[i]['structure_name'] in my_plan.structures.get_structures():
575627
struct = obj_funcs[i]['structure_name']
576628
if len(st.get_opt_voxels_idx(struct)) == 0: # check if there are any opt voxels for the structure

portpy/photon/vmat_scp/vmat_scp_optimization.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,8 @@ def run_sequential_cvx_algo_prediction(self, pred_dose_1d, *args, **kwargs):
12201220
inner_iteration = int(0)
12211221
best_obj_value = 0
12221222
vmat_params = self.vmat_params
1223-
self.arcs.get_initial_leaf_pos(initial_leaf_pos=vmat_params['initial_leaf_pos'])
1223+
if not vmat_params['initial_leaf_pos'].lower() == 'cg':
1224+
self.arcs.get_initial_leaf_pos(initial_leaf_pos=vmat_params['initial_leaf_pos'])
12241225
self.create_cvx_params()
12251226
sol_convergence = []
12261227
while True:

0 commit comments

Comments
 (0)