Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions portpy/photon/optimization.py
Copy link
Collaborator

@gourav3017 gourav3017 Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding it. It looks good. I think it would be more cleaner to have constraint_def[i]['type'] == 'dose_volume_D' or constraint_def[i]['type'] == 'dose_volume_V'. But we can add dvh constraint method as CVAR. Something like below.
{
"type": "dose_volume_V",
"parameters": {
"structure_name": "RECTUM",
"dose_gy": 5,
"dvh_method": "cvar" // or use default available one in portpy echo-vmat extension
},
"constraints": {
"limit_volume_perc": 84,
"constraint_type": "upper"
}
}
In the code, you should check first if it is dose volume constraint. If yes, check if it is using cvar or not. If yes, then implement using cvar or else skip it/use portpy default. You can refer dvh constraint implementation in github.com/PortPy-Project/ECHO-VMAT/blob/main/examples/echo_vmat_portpy.ipynb

You have to modify clinical_criteria.py --> get_dvh_table method to add column for 'dvh_method' or other parameters you might need for cvar (I assume your "alpha" is same as 1-volume_perc/100. In this case you don't need to add alpha in dvh_table). Then loop through dvh table in optimization.py code to add the constraint. Let me know if you need any help. We can discuss.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed suggestion. I’ll implement it.

Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,89 @@ def create_cvxpy_problem(self):
(cp.sum((cp.multiply(st.get_opt_voxels_volume_cc(org),
A[st.get_opt_voxels_idx(org), :] @ x))))
<= limit / num_fractions]

elif constraint_def[i]['type'] == 'CVaR_Upper':

'''
Json example:
{
"type": "CVaR_Upper",
"parameters": {
"structure_name": "HEART",
"alpha": 0.90
},
"constraints": {
"limit": 5
}
}
'''

alpha = float(constraint_def[i]['parameters']['alpha'])
if not (0.0 < alpha < 1.0):
raise ValueError(f"CVaR_Upper: alpha must be in (0,1), got {alpha}")
struct = constraint_def[i]['parameters']['structure_name']
limit = constraint_def[i]['constraints']['limit']
if struct in self.my_plan.structures.get_structures():
voxel_idx = st.get_opt_voxels_idx(struct)
if len(voxel_idx) > 0:
dose_1d_list = A[voxel_idx, :] @ x * num_fractions # dose per voxel

label = f"constraint_{struct}_a{alpha:.4f}"

zeta = cp.Variable(name=f"zeta_{label}")
w = cp.Variable(len(voxel_idx), name=f"w_{label}")

# Store variables in self.vars using label
self.vars[f"zeta_{label}"] = zeta
self.vars[f"w_{label}"] = w

# Add CVaR constraint
constraints += [
zeta + (1 / ((1 - alpha) * len(voxel_idx))) * cp.sum(w) <= limit,
w >= dose_1d_list - zeta,
w >= 0
]

mask = np.isfinite(d_max)
# Create index mask arrays
indices = np.arange(len(mask)) # assumes mask is 1D and corresponds to voxel indices
all_d_max_vox_ind = indices[mask]
constraints += [A[all_d_max_vox_ind, :] @ x <= d_max[all_d_max_vox_ind]] # Add constraint for all d_max voxels at once
print('Problem created')

def add_CVaR_Upper(self, alpha: float, limit: float, struct: str):
"""
add CVaR+ to the problem as a constraint

"""
constraints = self.constraints
x = self.vars["x"]
st = self.inf_matrix
A = self.inf_matrix.A
num_fractions = self.clinical_criteria.get_num_of_fractions()

if struct in self.my_plan.structures.get_structures():
voxel_idx = st.get_opt_voxels_idx(struct)

if len(voxel_idx) > 0:
dose_1d_list = A[voxel_idx, :] @ x * num_fractions # dose per voxel

label = f"constraint_{struct}_a{alpha:.4f}"

zeta = cp.Variable(name=f"zeta_{label}")
w = cp.Variable(len(voxel_idx), name=f"w_{label}")

# Store variables in self.vars using label
self.vars[f"zeta_{label}"] = zeta
self.vars[f"w_{label}"] = w

# Add CVaR constraint
constraints += [
zeta + (1 / ((1 - alpha) * len(voxel_idx))) * cp.sum(w) <= limit,
w >= dose_1d_list - zeta,
w >= 0
]

def add_max(self, struct: str, dose_gy: float):
"""
Add max constraints to the problem
Expand Down