Skip to content

Commit efc8e94

Browse files
committed
Version 1.0.1:
1. Added CI and HI for plan evaluation 2. Fixed bug for creating beam map in original resolution for down sampled beamlets
1 parent aeb37ab commit efc8e94

File tree

2 files changed

+88
-12
lines changed

2 files changed

+88
-12
lines changed

portpy/photon/evaluation.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,58 @@ def get_mean_dose(sol: dict, struct: str, dose_1d=None) -> float:
333333
mean_dose = (1 / sum(inf_matrix.get_opt_voxels_volume_cc(struct))) * (np.sum((np.multiply(inf_matrix.get_opt_voxels_volume_cc(struct), dose_1d[vox]))))
334334
return mean_dose * frac_vol
335335

336+
@staticmethod
337+
def get_conformity_index(my_plan: Plan, sol: dict = None, dose_3d: np.ndarray = None, target_structure='PTV') -> float:
338+
"""
339+
Calculate conformity index for the dose
340+
Closer to 1 is more better
341+
342+
:param my_plan: object of class Plan
343+
:param sol: optimal solution dictionary
344+
:param dose_3d: dose in 3d array
345+
:param target_structure: target structure name
346+
347+
:return: paddick conformity index
348+
349+
"""
350+
# calulating paddick conformity index
351+
percentile = 0.95 # reference isodose
352+
pres = my_plan.get_prescription()
353+
if dose_3d is None:
354+
dose_1d = sol['inf_matrix'].A @ (sol['optimal_intensity'] * my_plan.get_num_of_fractions())
355+
dose_3d = sol['inf_matrix'].dose_1d_to_3d(dose_1d=dose_1d)
356+
pres_iso_dose_mask = (dose_3d >= pres * percentile).astype(int)
357+
V_iso_pres = np.count_nonzero(pres_iso_dose_mask)
358+
ptv_mask = my_plan.structures.get_structure_mask_3d(target_structure)
359+
V_ptv = np.count_nonzero(ptv_mask)
360+
V_pres_iso_ptv = np.count_nonzero(pres_iso_dose_mask * ptv_mask)
361+
conformity_index = V_pres_iso_ptv * V_pres_iso_ptv / (V_ptv * V_iso_pres)
362+
return conformity_index
363+
364+
@staticmethod
365+
def get_homogeneity_index(my_plan: Plan, sol: dict = None, dose_3d: np.ndarray = None, target_structure='PTV') -> float:
366+
"""
367+
Calculate homogeneity index for the dose
368+
Closer to 0 is more better
369+
370+
:param my_plan: object of class Plan
371+
:param sol: optimal solution dictionary
372+
:param dose_3d: dose in 3d array
373+
:param target_structure: target structure name
374+
375+
:return: homogeneity index
376+
377+
"""
378+
if dose_3d is None:
379+
dose_1d = sol['inf_matrix'].A @ (sol['optimal_intensity'] * my_plan.get_num_of_fractions())
380+
dose_3d = sol['inf_matrix'].dose_1d_to_3d(dose_1d=dose_1d)
381+
ptv = my_plan.structures.get_structure_mask_3d(target_structure)
382+
ptv_dose = dose_3d[np.where(ptv == 1)]
383+
PTV_D2 = np.percentile(ptv_dose, 98)
384+
PTV_D50 = np.percentile(ptv_dose, 50)
385+
PTV_D98 = np.percentile(ptv_dose, 2)
386+
return (PTV_D2 - PTV_D98) / PTV_D50
387+
336388
@staticmethod
337389
def get_BED(my_plan: Plan, sol: dict = None, dose_per_fraction_1d: np.ndarray = None, alpha=1, beta=1) -> np.ndarray:
338390
"""

portpy/photon/influence_matrix.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -694,26 +694,50 @@ def get_bev_2d_grid(self, beam_id: Union[int, List[int]] = None, ind: int = None
694694
# add this part in case remove corner beamlets create issue for getting original resolution
695695
remove_row = []
696696
remove_col = []
697-
for row in range(beam_map.shape[0]):
698-
if row < beam_map.shape[0] - 1:
699-
prev_elem = beam_map[row, :][beam_map[row, :] > -1]
700-
next_elem = beam_map[row + 1, :][beam_map[row + 1, :] > -1]
697+
for row in range(1, beam_map.shape[0]):
698+
prev_elem = beam_map[row-1, :][beam_map[row-1, :] > -1]
699+
next_elem = beam_map[row, :][beam_map[row, :] > -1]
701700
matching_elements = np.intersect1d(prev_elem, next_elem)
702701
if len(matching_elements) >= 1:
703702
if len(prev_elem) <= len(next_elem):
704-
remove_row.append(row)
703+
if (row-1) not in remove_row:
704+
remove_row.append(row-1)
705+
else:
706+
remove_row.append(row)
705707
else:
706-
remove_row.append(row + 1)
707-
for col in range(beam_map.shape[1]):
708-
if col < beam_map.shape[1] - 1:
709-
prev_elem = beam_map[:, col][beam_map[:, col] > -1]
710-
next_elem = beam_map[:, col + 1][beam_map[:, col + 1] > -1]
708+
remove_row.append(row)
709+
for col in range(1, beam_map.shape[1]):
710+
prev_elem = beam_map[:, col-1][beam_map[:, col-1] > -1]
711+
next_elem = beam_map[:, col][beam_map[:, col] > -1]
711712
matching_elements = np.intersect1d(prev_elem, next_elem)
712713
if len(matching_elements) >= 1:
713714
if len(prev_elem) <= len(next_elem):
714-
remove_col.append(col)
715+
if (col - 1) not in remove_col:
716+
remove_col.append(col-1)
717+
else:
718+
remove_col.append(col)
715719
else:
716-
remove_col.append(col + 1)
720+
remove_col.append(col)
721+
# for row in range(beam_map.shape[0]):
722+
# if row < beam_map.shape[0] - 1:
723+
# prev_elem = beam_map[row, :][beam_map[row, :] > -1]
724+
# next_elem = beam_map[row + 1, :][beam_map[row + 1, :] > -1]
725+
# matching_elements = np.intersect1d(prev_elem, next_elem)
726+
# if len(matching_elements) >= 1:
727+
# if len(prev_elem) <= len(next_elem):
728+
# remove_row.append(row)
729+
# else:
730+
# remove_row.append(row + 1)
731+
# for col in range(beam_map.shape[1]):
732+
# if col < beam_map.shape[1] - 1:
733+
# prev_elem = beam_map[:, col][beam_map[:, col] > -1]
734+
# next_elem = beam_map[:, col + 1][beam_map[:, col + 1] > -1]
735+
# matching_elements = np.intersect1d(prev_elem, next_elem)
736+
# if len(matching_elements) >= 1:
737+
# if len(prev_elem) <= len(next_elem):
738+
# remove_col.append(col)
739+
# else:
740+
# remove_col.append(col + 1)
717741
mask_rows = np.ones(beam_map.shape[0], dtype=bool)
718742
mask_columns = np.ones(beam_map.shape[1], dtype=bool)
719743
if len(remove_row) > 0:

0 commit comments

Comments
 (0)