def create_transformed_mesh(width_data, length_data, factor_data): """Return factor data meshgrid.""" x = np.arange( np.floor(np.min(width_data)) - 1, np.ceil(np.max(width_data)) + 1, 0.1) y = np.arange( np.floor(np.min(length_data)) - 1, np.ceil(np.max(length_data)) + 1, 0.1) xx, yy = np.meshgrid(x, y) zz = spline_model_with_deformability( xx, convert2_ratio_perim_area(xx, yy), width_data, convert2_ratio_perim_area(width_data, length_data), factor_data, ) zz[xx > yy] = np.nan no_data_x = np.all(np.isnan(zz), axis=0) no_data_y = np.all(np.isnan(zz), axis=1) x = x[np.invert(no_data_x)] y = y[np.invert(no_data_y)] zz = zz[np.invert(no_data_y), :] zz = zz[:, np.invert(no_data_x)] return x, y, zz
def get_initial_centre(x, y, img): wl_image = pymedphys._vendor.pylinac.winstonlutz.WLImageOld( # pylint: disable = protected-access img) min_x = np.min(x) dx = x[1] - x[0] min_y = np.min(y) dy = y[1] - y[0] field_centre = [ wl_image.field_cax.x * dx + min_x, wl_image.field_cax.y * dy + min_y, ] return field_centre
def calc_min_distance(cube_definition, contours): vertices = cube_vertices(cube_definition) vectors = cube_vectors(cube_definition) unit_vectors = [vector / np.linalg.norm(vector) for vector in vectors] plane_norms = np.array( [ unit_vectors[1], -unit_vectors[0], -unit_vectors[1], unit_vectors[0], unit_vectors[2], -unit_vectors[2], ] ) plane_points = np.array( [vertices[0], vertices[1], vertices[2], vertices[0], vertices[0], vertices[3]] ) plane_origin_dist = -np.sum(plane_points * plane_norms, axis=1) distance_to_planes = np.dot(plane_norms, contours) + plane_origin_dist[:, None] min_dist_squared = np.min(distance_to_planes ** 2, axis=0) return min_dist_squared
def multi_thresholds_gamma_calc( options: GammaInternalFixedOptions, current_gamma, min_relative_dose_difference, distance, to_be_checked, ): gamma_at_distance = np.sqrt( (min_relative_dose_difference[:, None, None] / (options.dose_percent_threshold[None, :, None] / 100))**2 + (distance / options.distance_mm_threshold[None, None, :])**2) current_gamma[to_be_checked, :, :] = np.min( np.concatenate( [ gamma_at_distance[None, :, :, :], current_gamma[None, to_be_checked, :, :], ], axis=0, ), axis=0, ) still_searching_for_gamma = current_gamma > ( distance / options.distance_mm_threshold[None, None, :]) if options.skip_once_passed: still_searching_for_gamma = still_searching_for_gamma & (current_gamma >= 1) return current_gamma, still_searching_for_gamma
def check_aspect_ratio(edge_lengths): if not np.allclose(*edge_lengths): if np.min(edge_lengths) > 0.95 * np.max(edge_lengths): raise ValueError( "For non-square rectangular fields, " "to accurately determine the rotation, " "need to have the small edge be less than 95% of the long edge." )
def get_bounding_box(points): x_min = np.min(points[:, 1]) x_max = np.max(points[:, 1]) y_min = np.min(points[:, 0]) y_max = np.max(points[:, 0]) z_min = np.min(points[:, 2]) z_max = np.max(points[:, 2]) max_range = np.array([x_max - x_min, y_max - y_min, z_max - z_min]).max() / 2.0 mid_x = (x_max + x_min) * 0.5 mid_y = (y_max + y_min) * 0.5 mid_z = (z_max + z_min) * 0.5 return [ [mid_y - max_range, mid_y + max_range], [mid_x - max_range, mid_x + max_range], [mid_z - max_range, mid_z + max_range], ]
def _determine_calc_grid_and_adjustments(mlc, jaw, leaf_pair_widths, grid_resolution): min_y = np.min(-jaw[:, 0]) max_y = np.max(jaw[:, 1]) leaf_centres, top_of_reference_leaf = _determine_leaf_centres(leaf_pair_widths) grid_reference_position = _determine_reference_grid_position( top_of_reference_leaf, grid_resolution ) top_grid_pos = ( np.round((max_y - grid_reference_position) / grid_resolution) ) * grid_resolution + grid_reference_position bot_grid_pos = ( grid_reference_position - (np.round((-min_y + grid_reference_position) / grid_resolution)) * grid_resolution ) grid = dict() grid["jaw"] = np.arange( bot_grid_pos, top_grid_pos + grid_resolution, grid_resolution ).astype("float") grid_leaf_map = np.argmin( np.abs(grid["jaw"][:, None] - leaf_centres[None, :]), axis=1 ) adjusted_grid_leaf_map = grid_leaf_map - np.min(grid_leaf_map) leaves_to_be_calced = np.unique(grid_leaf_map) adjusted_mlc = mlc[:, leaves_to_be_calced, :] min_x = np.round(np.min(-adjusted_mlc[:, :, 0]) / grid_resolution) * grid_resolution max_x = np.round(np.max(adjusted_mlc[:, :, 1]) / grid_resolution) * grid_resolution grid["mlc"] = np.arange(min_x, max_x + grid_resolution, grid_resolution).astype( "float" ) return grid, adjusted_grid_leaf_map, adjusted_mlc
def gamma_filter_brute_force(axes_reference, dose_reference, axes_evaluation, dose_evaluation, distance_mm_threshold, dose_threshold, lower_dose_cutoff=0, **_): xx_ref, yy_ref, zz_ref = np.meshgrid(*axes_reference, indexing="ij") gamma_array = np.ones_like(dose_evaluation).astype(np.float) * np.nan mesh_index = np.meshgrid( *[np.arange(len(coord_eval)) for coord_eval in axes_evaluation]) eval_index = np.reshape(np.array(mesh_index), (3, -1)) run_index = np.arange(np.shape(eval_index)[1]) np.random.shuffle(run_index) sys.stdout.write(" ") for counter, point_index in enumerate(run_index): i, j, k = eval_index[:, point_index] eval_x = axes_evaluation[0][i] eval_y = axes_evaluation[1][j] eval_z = axes_evaluation[2][k] if dose_evaluation[i, j, k] < lower_dose_cutoff: continue distance = np.sqrt((xx_ref - eval_x)**2 + (yy_ref - eval_y)**2 + (zz_ref - eval_z)**2) dose_diff = dose_evaluation[i, j, k] - dose_reference gamma = np.min( np.sqrt((dose_diff / dose_threshold)**2 + (distance / distance_mm_threshold)**2)) gamma_array[i, j, k] = gamma if counter // 30 == counter / 30: percent_pass = str( np.round(calculate_pass_rate(gamma_array), decimals=1)) sys.stdout.write( "\rPercent Pass: {0}% | Percent Complete: {1:.2f}%".format( percent_pass, counter / np.shape(eval_index)[1] * 100)) sys.stdout.flush() return calculate_pass_rate(gamma_array)
def spline_model(width_test, ratio_perim_area_test, width_data, ratio_perim_area_data, factor_data): """Return the result of the spline model. The bounding box is chosen so as to allow extrapolation. The spline orders are two in the width direction and one in the perimeter/area direction. For justification on using this method for modelling electron insert factors see the *Methods: Bivariate spline model* section within <http://dx.doi.org/10.1016/j.ejmp.2015.11.002>. Parameters ---------- width_test : np.ndarray The width point(s) which are to have the electron insert factor interpolated. ratio_perim_area_test : np.ndarray The perimeter/area which are to have the electron insert factor interpolated. width_data : np.ndarray The width data points for the relevant applicator, energy and ssd. ratio_perim_area_data : np.ndarray The perimeter/area data points for the relevant applicator, energy and ssd. factor_data : np.ndarray The insert factor data points for the relevant applicator, energy and ssd. Returns ------- result : np.ndarray The interpolated electron insert factors for width_test and ratio_perim_area_test. """ bbox = [ np.min([np.min(width_data), np.min(width_test)]), np.max([np.max(width_data), np.max(width_test)]), np.min([np.min(ratio_perim_area_data), np.min(ratio_perim_area_test)]), np.max([np.max(ratio_perim_area_data), np.max(ratio_perim_area_test)]), ] spline = scipy.interpolate.SmoothBivariateSpline(width_data, ratio_perim_area_data, factor_data, kx=2, ky=1, bbox=bbox) return spline.ev(width_test, ratio_perim_area_test)
def align_cube_to_structure( structure_name: str, dcm_struct: pydicom.dataset.FileDataset, quiet=False, niter=10, x0=None, ): """Align a cube to a dicom structure set. Designed to allow arbitrary references frames within a dicom file to be extracted via contouring a cube. Parameters ---------- structure_name The DICOM label of the cube structure dcm_struct The pydicom reference to the DICOM structure file. quiet : ``bool`` Tell the function to not print anything. Defaults to False. x0 : ``np.ndarray``, optional A 3x3 array with each row defining a 3-D point in space. These three points are used as initial conditions to search for a cube that fits the contours. Choosing initial values close to the structure set, and in the desired orientation will allow consistent results. See examples within `pymedphys.experimental.cubify`_ on what the effects of each of the three points are on the resulting cube. By default, this parameter is defined using the min/max values of the contour structure. Returns ------- cube_definition_array Four 3-D points the define the vertices of the cube. vectors The vectors between the points that can be used to traverse the cube. Examples -------- >>> import numpy as np >>> import pydicom >>> import pymedphys >>> from pymedphys.experimental import align_cube_to_structure >>> >>> struct_path = str(pymedphys.data_path('example_structures.dcm')) >>> dcm_struct = pydicom.dcmread(struct_path, force=True) >>> structure_name = 'ANT Box' >>> cube_definition_array, vectors = align_cube_to_structure( ... structure_name, dcm_struct, quiet=True, niter=1) >>> np.round(cube_definition_array) array([[-266., -31., 43.], [-266., 29., 42.], [-207., -31., 33.], [-276., -31., -16.]]) >>> >>> np.round(vectors, 1) array([[ 0.7, 59.9, -0.5], [ 59.2, -0.7, -9.7], [ -9.7, -0.4, -59.2]]) """ contours = pull_structure(structure_name, dcm_struct) contour_points = contour_to_points(contours) def to_minimise(cube): cube_definition = cubify([tuple(cube[0:3]), tuple(cube[3:6]), tuple(cube[6::])]) min_dist_squared = calc_min_distance(cube_definition, contour_points) return np.sum(min_dist_squared) if x0 is None: concatenated_contours = [ np.concatenate(contour_coord) for contour_coord in contours ] bounds = [ (np.min(concatenated_contour), np.max(concatenated_contour)) for concatenated_contour in concatenated_contours ] x0 = np.array( [ (bounds[1][0], bounds[0][0], bounds[2][1]), (bounds[1][0], bounds[0][1], bounds[2][1]), (bounds[1][1], bounds[0][0], bounds[2][1]), ] ) if quiet: def print_fun(x, f, accepted): # pylint: disable = unused-argument pass else: def print_fun(x, f, accepted): # pylint: disable = unused-argument print("at minimum %.4f accepted %d" % (f, int(accepted))) result = basinhopping(to_minimise, x0, callback=print_fun, niter=niter, stepsize=5) cube = result.x cube_definition = cubify([tuple(cube[0:3]), tuple(cube[3:6]), tuple(cube[6::])]) cube_definition_array = np.array([np.array(list(item)) for item in cube_definition]) vectors = [ cube_definition_array[1] - cube_definition_array[0], cube_definition_array[2] - cube_definition_array[0], cube_definition_array[3] - cube_definition_array[0], ] return cube_definition_array, vectors
def plot_results( grid_xx, grid_yy, logfile_mu_density, mosaiq_mu_density, diff_colour_scale=0.1 ): min_val = np.min([logfile_mu_density, mosaiq_mu_density]) max_val = np.max([logfile_mu_density, mosaiq_mu_density]) plt.figure() plt.pcolormesh(grid_xx, grid_yy, logfile_mu_density, vmin=min_val, vmax=max_val) plt.colorbar() plt.title("Logfile MU density") plt.xlabel("MLC direction (mm)") plt.ylabel("Jaw direction (mm)") plt.gca().invert_yaxis() plt.figure() plt.pcolormesh(grid_xx, grid_yy, mosaiq_mu_density, vmin=min_val, vmax=max_val) plt.colorbar() plt.title("Mosaiq MU density") plt.xlabel("MLC direction (mm)") plt.ylabel("Jaw direction (mm)") plt.gca().invert_yaxis() scaled_diff = (logfile_mu_density - mosaiq_mu_density) / max_val plt.figure() plt.pcolormesh( grid_xx, grid_yy, scaled_diff, vmin=-diff_colour_scale / 2, vmax=diff_colour_scale / 2, ) plt.colorbar(label="Limited colour range = {}".format(diff_colour_scale / 2)) plt.title("(Logfile - Mosaiq MU density) / Maximum MU Density") plt.xlabel("MLC direction (mm)") plt.ylabel("Jaw direction (mm)") plt.gca().invert_yaxis() plt.show() plt.figure() plt.pcolormesh( grid_xx, grid_yy, scaled_diff, vmin=-diff_colour_scale, vmax=diff_colour_scale ) plt.colorbar(label="Limited colour range = {}".format(diff_colour_scale)) plt.title("(Logfile - Mosaiq MU density) / Maximum MU Density") plt.xlabel("MLC direction (mm)") plt.ylabel("Jaw direction (mm)") plt.gca().invert_yaxis() plt.show() absolute_range = np.max([-np.min(scaled_diff), np.max(scaled_diff)]) plt.figure() plt.pcolormesh( grid_xx, grid_yy, scaled_diff, vmin=-absolute_range, vmax=absolute_range ) plt.colorbar(label="No limited colour range") plt.title("(Logfile - Mosaiq MU density) / Maximum MU Density") plt.xlabel("MLC direction (mm)") plt.ylabel("Jaw direction (mm)") plt.gca().invert_yaxis() plt.show()
def calculate_min_dose_difference(options, distance, to_be_checked, distance_step_size): """Determine the minimum dose difference. Calculated for a given distance from each reference point. """ min_relative_dose_difference = np.nan * np.ones_like( options.flat_dose_reference[to_be_checked]) num_dimensions = np.shape(options.flat_mesh_axes_reference)[0] coordinates_at_distance_shell = pymedphys._utilities.createshells.calculate_coordinates_shell( # pylint: disable = protected-access distance, num_dimensions, distance_step_size) num_points_in_shell = np.shape(coordinates_at_distance_shell)[1] estimated_ram_needed = (np.uint64(num_points_in_shell) * np.uint64(np.count_nonzero(to_be_checked)) * np.uint64(32) * np.uint64(num_dimensions) * np.uint64(2)) num_slices = np.floor( estimated_ram_needed / options.ram_available).astype(int) + 1 if not options.quiet: sys.stdout.write( " | Points tested per reference point: {} | RAM split count: {}". format(num_points_in_shell, num_slices)) sys.stdout.flush() all_checks = np.where(np.ravel(to_be_checked))[0] index = np.arange(len(all_checks)) sliced = np.array_split(index, num_slices) sorted_sliced = [np.sort(current_slice) for current_slice in sliced] for current_slice in sorted_sliced: to_be_checked_sliced = np.full_like(to_be_checked, False, dtype=bool) to_be_checked_sliced[ # pylint: disable=unsupported-assignment-operation all_checks[current_slice]] = True assert np.all(to_be_checked[to_be_checked_sliced]) axes_reference_to_be_checked = options.flat_mesh_axes_reference[:, to_be_checked_sliced] evaluation_dose = interpolate_evaluation_dose_at_distance( options.evaluation_interpolation, axes_reference_to_be_checked, coordinates_at_distance_shell, ) if options.local_gamma: with np.errstate(divide="ignore"): relative_dose_difference = ( evaluation_dose - options.flat_dose_reference[to_be_checked_sliced][None, :] ) / ( options.flat_dose_reference[to_be_checked_sliced][None, :]) else: relative_dose_difference = ( evaluation_dose - options.flat_dose_reference[to_be_checked_sliced][None, :] ) / options.global_normalisation min_relative_dose_difference[current_slice] = np.min( np.abs(relative_dose_difference), axis=0) return min_relative_dose_difference
def gamma_loop(options: GammaInternalFixedOptions): still_searching_for_gamma = np.full_like(options.flat_dose_reference, True, dtype=bool) current_gamma = np.inf * np.ones(( len(options.flat_dose_reference), len(options.dose_percent_threshold), len(options.distance_mm_threshold), )) distance_step_size = np.min( options.distance_mm_threshold) / options.interp_fraction to_be_checked = options.reference_points_to_calc & still_searching_for_gamma distance = 0.0 force_search_distances = np.sort(options.distance_mm_threshold) while distance <= options.maximum_test_distance: if not options.quiet: sys.stdout.write( "\rCurrent distance: {0:.2f} mm | " "Number of reference points remaining: {1}".format( distance, np.sum(to_be_checked))) min_relative_dose_difference = calculate_min_dose_difference( options, distance, to_be_checked, distance_step_size) current_gamma, still_searching_for_gamma_all = multi_thresholds_gamma_calc( options, current_gamma, min_relative_dose_difference, distance, to_be_checked, ) still_searching_for_gamma = np.any(np.any( still_searching_for_gamma_all, axis=-1), axis=-1) to_be_checked = options.reference_points_to_calc & still_searching_for_gamma if np.sum(to_be_checked) == 0: break relevant_distances = options.distance_mm_threshold[np.any( np.any( options.reference_points_to_calc[:, None, None] & still_searching_for_gamma_all, axis=0, ), axis=0, )] distance_step_size = np.min( relevant_distances) / options.interp_fraction distance_step_size = np.max([ distance / options.interp_fraction / options.max_gamma, distance_step_size ]) distance += distance_step_size if len(force_search_distances) != 0: if distance >= force_search_distances[0]: distance = force_search_distances[0] force_search_distances = np.delete(force_search_distances, 0) return current_gamma
def gantry_tol_from_gantry_angles(gantry_angles): min_diff = np.min(np.diff(sorted(gantry_angles))) gantry_tol = np.min([min_diff / 2 - 0.1, 3]) return gantry_tol