11"""Base class for pencil beam dose calculation algorithms."""
22
33from abc import abstractmethod
4- from typing import cast , Any , Literal
4+ from typing import Any , Literal
55import warnings
66import logging
77import time
@@ -97,6 +97,7 @@ class PencilBeamEngineAbstract(DoseEngineBase):
9797 ssd_density_threshold : float
9898 use_given_eq_density_cube : bool
9999 ignore_outside_densities : bool
100+ trace_on_dose_grid : bool
100101 cube_wed : sitk .Image
101102 hlut : np .ndarray
102103
@@ -107,13 +108,15 @@ def __init__(self, pln=None):
107108 self .ssd_density_threshold : float = 0.0500
108109 self .use_given_eq_density_cube : bool = False
109110 self .ignore_outside_densities : bool = True
111+ self .trace_on_dose_grid : bool = True
110112 self .cube_wed = None
111113 self .hlut = None
112114
113115 self ._computed_quantities = []
114116 self ._effective_lateral_cutoff = None
115117 self ._num_of_bixels_container = None
116118 self ._rad_depth_cubes = []
119+ self ._raytracer = None
117120
118121 super ().__init__ (pln )
119122
@@ -275,6 +278,18 @@ def _init_dose_calc(self, ct: CT, cst: StructureSet, stf: SteeringInformation):
275278 # Allocate memory for quantity containers
276279 dij = self ._allocate_quantity_matrices (dij , ["physical_dose" ])
277280
281+ # Initialize ray-tracer
282+ if self .trace_on_dose_grid :
283+ wed_cube_trace = resample_image (
284+ input_image = self .cube_wed ,
285+ interpolator = sitk .sitkLinear ,
286+ target_grid = self .dose_grid ,
287+ )
288+ else :
289+ wed_cube_trace = self .cube_wed
290+
291+ self ._raytracer = RayTracerSiddon ([wed_cube_trace ])
292+
278293 return dij
279294
280295 def _allocate_quantity_matrices (self , dij : dict [str , Any ], names : list [str ]):
@@ -293,14 +308,18 @@ def _allocate_quantity_matrices(self, dij: dict[str, Any], names: list[str]):
293308 (self .dose_grid .num_voxels , dij ["num_of_beams" ]), dtype = np .float32
294309 )
295310 else :
296- # This could probably be optimized by using direct access to the
297- # lil_matrix's data structures
298- # TODO: we could store a single sparse pattern matrix and then only store
299- # the values for all quantities for better memory management
300- dij [q_name ].flat [i ] = sparse .lil_matrix (
301- (self ._num_of_columns_dij , self .dose_grid .num_voxels ),
302- dtype = np .float32 ,
303- )
311+ # We allocate raw csc sparse matrix structures using
312+ # a rough estimat ebased on number of voxels and
313+ # beamlets
314+ est_nnz = np .fix (
315+ 5e-4 * self .dose_grid .num_voxels * self ._num_of_columns_dij
316+ ).astype (np .int64 )
317+ dij [q_name ].flat [i ] = {
318+ "data" : np .empty ((est_nnz ,), dtype = np .float32 ),
319+ "indices" : np .empty ((est_nnz ,), dtype = np .int64 ),
320+ "indptr" : np .zeros ((self ._num_of_columns_dij + 1 ,), dtype = np .int64 ),
321+ "nnz" : 0 ,
322+ }
304323
305324 self ._computed_quantities .append (q_name )
306325
@@ -360,12 +379,11 @@ def _init_beam(
360379 # Calculate radiological depth cube
361380 logger .info ("Calculating radiological depth cube..." )
362381
363- ct_cube = self .cube_wed
364- raytracer = RayTracerSiddon ([ct_cube ])
365-
366382 start_time = time .time ()
367383
368- rad_depth_cubes = raytracer .trace_cubes (beam_info ["beam" ])
384+ self ._raytracer .debug_core_performance = True
385+ rad_depth_cubes = self ._raytracer .trace_cubes (beam_info ["beam" ])
386+ self ._raytracer .debug_core_performance = False
369387
370388 # TODO: add universal time-debugging method
371389 end_time = time .time ()
@@ -376,15 +394,18 @@ def _init_beam(
376394 beam_info ["rad_depths" ] = [None ] * len (rad_depth_cubes )
377395
378396 for c , rad_depth_cube in enumerate (rad_depth_cubes ):
379- resampled_rad_depth_cube = resample_image (
380- input_image = rad_depth_cube ,
381- interpolator = sitk .sitkLinear ,
382- target_grid = self .dose_grid ,
383- )
397+ if self .trace_on_dose_grid :
398+ rad_depth_cube_dose_grid = rad_depth_cube
399+ else :
400+ rad_depth_cube_dose_grid = resample_image (
401+ input_image = rad_depth_cube ,
402+ interpolator = sitk .sitkLinear ,
403+ target_grid = self .dose_grid ,
404+ )
384405 if self .keep_rad_depth_cubes :
385- self ._rad_depth_cubes .append (resampled_rad_depth_cube )
406+ self ._rad_depth_cubes .append (rad_depth_cube_dose_grid )
386407
387- rad_depth_cube_dosegrid = sitk .GetArrayViewFromImage (resampled_rad_depth_cube )
408+ rad_depth_cube_dosegrid = sitk .GetArrayViewFromImage (rad_depth_cube_dose_grid )
388409 rad_depth_vdose_grid = rad_depth_cube_dosegrid .ravel ()[self ._vdose_grid ]
389410
390411 # Find valid coordinates
@@ -484,8 +505,7 @@ def _compute_ssd(
484505 ray_pos_bev = np .array ([ray ["ray_pos_bev" ] for ray in beam ["rays" ]])
485506 target_points = np .array ([ray ["target_point" ] for ray in beam ["rays" ]])
486507
487- raytracer = RayTracerSiddon ([self .cube_wed ])
488- alpha , _ , rho , d12 , _ = raytracer .trace_rays (
508+ alpha , _ , rho , d12 , _ = self ._raytracer .trace_rays (
489509 beam ["iso_center" ],
490510 beam ["source_point" ].reshape ((1 , 3 )),
491511 target_points ,
@@ -708,10 +728,44 @@ def _fill_dij(
708728 bixel ["weight" ] * bixel [q_name ]
709729 )
710730 else :
711- # We insert into the lil sparse matrix
712- # This could probably be optimized by using direct access to the lil_matrix's
713- # data structures
714- dij [q_name ][sub_scen_idx ][bixel_counter , bixel ["ix" ]] = bixel [q_name ]
731+ # first we fill the index pointer array
732+ dij [q_name ][sub_scen_idx ]["indptr" ][bixel_counter + 1 ] = (
733+ dij [q_name ][sub_scen_idx ]["indptr" ][bixel_counter ] + bixel ["ix" ].size
734+ )
735+
736+ # If we haven't preallocated enough, we need to expand the arrays
737+ if (
738+ dij [q_name ][sub_scen_idx ]["data" ].size
739+ < dij [q_name ][sub_scen_idx ]["nnz" ] + bixel ["ix" ].size
740+ ):
741+ logger .debug ("Resizing data and indices arrays for %s..." , q_name )
742+
743+ # We estimate the required size by the remaining
744+ # number of beamlets / columns in the sparse matrix
745+ # 1 additional beamlet is added
746+ shape_resize = (self ._num_of_columns_dij - bixel_counter ) * (
747+ bixel ["ix" ].size + 1
748+ )
749+ shape_resize += dij [q_name ][sub_scen_idx ]["data" ].size
750+ dij [q_name ][sub_scen_idx ]["data" ].resize ((shape_resize ,), refcheck = False )
751+ dij [q_name ][sub_scen_idx ]["indices" ].resize (
752+ (shape_resize ,), refcheck = False
753+ )
754+
755+ # Fill the corresponding values and indices using indptr
756+ dij [q_name ][sub_scen_idx ]["data" ][
757+ dij [q_name ][sub_scen_idx ]["indptr" ][bixel_counter ] : dij [q_name ][
758+ sub_scen_idx
759+ ]["indptr" ][bixel_counter + 1 ]
760+ ] = bixel [q_name ]
761+ dij [q_name ][sub_scen_idx ]["indices" ][
762+ dij [q_name ][sub_scen_idx ]["indptr" ][bixel_counter ] : dij [q_name ][
763+ sub_scen_idx
764+ ]["indptr" ][bixel_counter + 1 ]
765+ ] = bixel ["ix" ]
766+
767+ # Store how many nnzs we actually have in the matrix
768+ dij [q_name ][sub_scen_idx ]["nnz" ] += bixel ["ix" ].size
715769
716770 # Bookkeeping of bixel numbers
717771 # remember beam and bixel number
@@ -749,9 +803,34 @@ def _finalize_dose(self, dij: dict):
749803 # Loop over all used quantities
750804 for q_name in self ._computed_quantities :
751805 if not self ._calc_dose_direct :
752- tmp_matrix = cast (sparse .lil_matrix , dij [q_name ].flat [i ])
753- tmp_matrix = tmp_matrix .tocsr ().T
806+ # tmp_matrix = cast(sparse.lil_matrix, dij[q_name].flat[i])
807+ # tmp_matrix = tmp_matrix.tocsr().T
808+ data_dict = dij [q_name ].flat [i ]
809+
810+ # Resize to the actual number of non-zero elements
811+ data_dict ["data" ].resize ((data_dict ["nnz" ],), refcheck = False )
812+ data_dict ["indices" ].resize ((data_dict ["nnz" ],), refcheck = False )
813+
814+ # Create the matrix, avoid copies
815+ tmp_matrix = sparse .csc_array (
816+ (data_dict ["data" ], data_dict ["indices" ], data_dict ["indptr" ]),
817+ shape = (self .dose_grid .num_voxels , self ._num_of_columns_dij ),
818+ dtype = np .float32 ,
819+ copy = False ,
820+ )
821+
822+ # Do we need this?
754823 tmp_matrix .eliminate_zeros ()
824+
825+ # make sure indices are sorted and matrix is canonical
826+ if not tmp_matrix .has_sorted_indices :
827+ logger .debug ("Sorting indices for %s..." , q_name )
828+ tmp_matrix .sort_indices ()
829+
830+ if not tmp_matrix .has_canonical_format :
831+ logger .debug ("Matrix is not in canonical format for %s..." , q_name )
832+ tmp_matrix .sum_duplicates ()
833+
755834 dij [q_name ].flat [i ] = tmp_matrix
756835
757836 if self .keep_rad_depth_cubes and self ._rad_depth_cubes :
0 commit comments