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 create_dose_function(net_od, dose): net_od = np.array(net_od, copy=False) dose = np.array(dose, copy=False) to_minimise = create_to_minimise(net_od, dose) result = basinhopping(to_minimise, [np.max(dose) / np.max(net_od), 1, 1]) return create_cal_fit(*result.x)
def _calc_time_steps(positions, grid_resolution, min_step_per_pixel): maximum_travel = [] for _, value in positions.items(): for _, (start, end) in value.items(): maximum_travel.append(np.max(np.abs(end - start))) maximum_travel = np.max(maximum_travel) number_of_pixels = np.ceil(maximum_travel / grid_resolution) time_steps = number_of_pixels * min_step_per_pixel if time_steps < 10: time_steps = 10 return time_steps
def folder_analyze(volume): for item in range(0, volume.shape[2]): stack1 = np.sum(volume[:, :, item], axis=0) maxstack1 = np.max(stack1) stack2 = np.sum(volume[:, :, item], axis=1) maxstack2 = np.max(stack2) if maxstack2 / maxstack1 > 1.5: # It is a Y field folder field = 2 elif maxstack2 / maxstack1 < 0.5: # It is a X field folder field = 1 else: field = 3 # It is a field rotation folder return field
def normalise_pdd(relative_dose, depth=None, normalisation_depth=None, smoothed_normalisation=False): """Normalise a pdd at a given depth. If normalisation_depth is left undefined then the depth of dose maximum is used for the normalisation depth. """ if smoothed_normalisation: filtered = scipy.signal.savgol_filter(relative_dose, 21, 2) else: filtered = relative_dose # normalisation_depth will be None if the user does not define it, if that # is the case simply define normalisation by 100 / the maximum value if normalisation_depth is None: normalisation = 100 / np.max(filtered) # However if the user did define a normalisation depth then need to # interpolate using the provided depth variable to find the relative dose # value to normalise to else: if depth is None: raise ValueError( "distance variable needs to be defined to normalise to a " "depth") interpolation = scipy.interpolate.interp1d(depth, filtered) normalisation = 100 / interpolation(normalisation_depth) return relative_dose * normalisation
def _matches_fraction(self, dicom_dataset, fraction_number, gantry_tol=3, meterset_tol=0.5): filtered = self._filter_cps() dicom_metersets = get_fraction_group_beam_sequence_and_meterset( dicom_dataset, fraction_number)[1] dicom_fraction = convert_to_one_fraction_group(dicom_dataset, fraction_number) gantry_angles = get_gantry_angles_from_dicom(dicom_fraction) delivery_metersets = filtered._metersets( # pylint: disable = protected-access gantry_angles, gantry_tol) try: maximmum_diff = np.max( np.abs( np.array(dicom_metersets) - np.array(delivery_metersets))) except ValueError: maximmum_diff = np.inf return maximmum_diff <= meterset_tol
def set_defaults(self): if self.maximum_test_distance == -1: object.__setattr__(self, "maximum_test_distance", np.inf) if self.global_normalisation is None: object.__setattr__(self, "global_normalisation", np.max(self.flat_dose_reference))
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 plot_gamma_hist(gamma, percent, dist): valid_gamma = gamma[~np.isnan(gamma)] plt.hist(valid_gamma, 50, density=True) pass_ratio = np.sum(valid_gamma <= 1) / len(valid_gamma) plt.title( "Local Gamma ({0}%/{1}mm) | Percent Pass: {2:.2f} % | Mean Gamma: {3:.2f} | Max Gamma: {4:.2f}" .format(percent, dist, pass_ratio * 100, np.mean(valid_gamma), np.max(valid_gamma)))
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 display_mu_density_diff( grid, mudensity_eval, mudensity_ref, grid_resolution=None, colour_range=None ): cmap = "bwr" diff = mudensity_eval - mudensity_ref if colour_range is None: colour_range = np.max(np.abs(diff)) # pylint: disable=invalid-unary-operand-type display_mu_density( grid, diff, grid_resolution=grid_resolution, cmap=cmap, vmin=-colour_range, vmax=colour_range, )
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 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 normalise_profile( distance, relative_dose, pdd_distance=None, pdd_relative_dose=None, scan_depth=None, normalisation_position="cra", scale_to_pdd=False, smoothed_normalisation=False, ): """Normalise a profile given a defined normalisation position and normalisation scaling """ # If scaling is to PDD interpolate along the PDD to find the scaling, # otherwise set scaling to 100. if scale_to_pdd: # If insufficient information has been supplies raise a meaningful # error if pdd_distance is None or pdd_relative_dose is None or scan_depth is None: raise ValueError( "Scaling to PDD requires pdd_distance, pdd_relative_dose, " "and scan_depth to be defined.") pdd_interpolation = scipy.interpolate.interp1d(pdd_distance, pdd_relative_dose) scaling = pdd_interpolation(scan_depth) else: scaling = 100 # Linear interpolation function if smoothed_normalisation: filtered = scipy.signal.savgol_filter(relative_dose, 21, 2) interpolation = scipy.interpolate.interp1d(distance, filtered) else: interpolation = scipy.interpolate.interp1d(distance, relative_dose) try: # Check if user wrote a number for normalisation position float_position = float(normalisation_position) except ValueError: # If text was written the conversion to float will fail float_position = None # If position was given by the user as a number then define the # normalisation to that position if float_position is not None: normalisation = scaling / interpolation(float_position) # Otherwise if the user gave 'cra' (case independent) normalise at 0 elif normalisation_position.lower() == "cra": normalisation = scaling / interpolation(0) # Otherwise if the user gave 'cm' (case independent) normalise to the # centre of mass elif normalisation_position.lower() == "cm": threshold = 0.5 * np.max(relative_dose) weights = relative_dose.copy() weights[weights < threshold] = 0 centre_of_mass = np.average(distance, weights=weights) normalisation = scaling / interpolation(centre_of_mass) # Otherwise if the user gave 'max' (case independent) normalise to the # point of dose maximum elif normalisation_position.lower() == "max": normalisation = scaling / np.max(relative_dose) else: raise TypeError("Expected either a float for `normalisation_position` " "or one of 'cra', 'cm', or 'max'") return relative_dose * normalisation
def plot_and_save_results( reference_mudensity, evaluation_mudensity, gamma, gamma_options, png_record_directory, header_text="", footer_text="", ): reference_filepath = png_record_directory.joinpath("reference.png") evaluation_filepath = png_record_directory.joinpath("evaluation.png") diff_filepath = png_record_directory.joinpath("diff.png") gamma_filepath = png_record_directory.joinpath("gamma.png") diff = evaluation_mudensity - reference_mudensity imageio.imwrite(reference_filepath, reference_mudensity) imageio.imwrite(evaluation_filepath, evaluation_mudensity) imageio.imwrite(diff_filepath, diff) imageio.imwrite(gamma_filepath, gamma) largest_mu_density = np.max( [np.max(evaluation_mudensity), np.max(reference_mudensity)]) largest_diff = np.max(np.abs(diff)) widths = [1, 1] heights = [0.5, 1, 1, 1, 0.4] gs_kw = dict(width_ratios=widths, height_ratios=heights) fig, axs = plt.subplots(5, 2, figsize=(10, 16), gridspec_kw=gs_kw) gs = axs[0, 0].get_gridspec() for ax in axs[0, 0:]: ax.remove() for ax in axs[1, 0:]: ax.remove() for ax in axs[4, 0:]: ax.remove() ax_header = fig.add_subplot(gs[0, :]) ax_hist = fig.add_subplot(gs[1, :]) ax_footer = fig.add_subplot(gs[4, :]) ax_header.axis("off") ax_footer.axis("off") ax_header.text(0, 0, header_text, ha="left", wrap=True, fontsize=21) ax_footer.text(0, 1, footer_text, ha="left", va="top", wrap=True, fontsize=6) plt.sca(axs[2, 0]) pymedphys.mudensity.display(GRID, reference_mudensity, vmin=0, vmax=largest_mu_density) axs[2, 0].set_title("Reference MU Density") plt.sca(axs[2, 1]) pymedphys.mudensity.display(GRID, evaluation_mudensity, vmin=0, vmax=largest_mu_density) axs[2, 1].set_title("Evaluation MU Density") plt.sca(axs[3, 0]) pymedphys.mudensity.display(GRID, diff, cmap="seismic", vmin=-largest_diff, vmax=largest_diff) plt.title("Evaluation - Reference") plt.sca(axs[3, 1]) pymedphys.mudensity.display(GRID, gamma, cmap="coolwarm", vmin=0, vmax=2) plt.title("Local Gamma | " f"{gamma_options['dose_percent_threshold']}%/" f"{gamma_options['distance_mm_threshold']}mm") plt.sca(ax_hist) plot_gamma_hist( gamma, gamma_options["dose_percent_threshold"], gamma_options["distance_mm_threshold"], ) return fig
def _single_calculate_deformability(x_test, y_test, x_data, y_data, z_data): """Return the result of the deformability test for a single test point. The deformability test applies a shift to the spline to determine whether or not sufficient information for modelling is available. For further details on the deformability test see the *Methods: Defining valid prediction regions of the spline* section within <http://dx.doi.org/10.1016/j.ejmp.2015.11.002>. Parameters ---------- x_test : float The x coordinate of the point to test y_test : float The y coordinate of the point to test x_data : np.ndarray The x coordinates of the model data to test y_data : np.ndarray The y coordinates of the model data to test z_data : np.ndarray The z coordinates of the model data to test Returns ------- deformability : float The resulting deformability between 0 and 1 representing the ratio of deviation the spline model underwent at the point in question by introducing an outlier at the point in question. """ deviation = 0.02 adjusted_x_data = np.append(x_data, x_test) adjusted_y_data = np.append(y_data, y_test) bbox = [ min(adjusted_x_data), max(adjusted_x_data), min(adjusted_y_data), max(adjusted_y_data), ] initial_model = scipy.interpolate.SmoothBivariateSpline(x_data, y_data, z_data, bbox=bbox, kx=2, ky=1).ev( x_test, y_test) pos_adjusted_z_data = np.append(z_data, initial_model + deviation) neg_adjusted_z_data = np.append(z_data, initial_model - deviation) pos_adjusted_model = scipy.interpolate.SmoothBivariateSpline( adjusted_x_data, adjusted_y_data, pos_adjusted_z_data, kx=2, ky=1).ev(x_test, y_test) neg_adjusted_model = scipy.interpolate.SmoothBivariateSpline( adjusted_x_data, adjusted_y_data, neg_adjusted_z_data, kx=2, ky=1).ev(x_test, y_test) deformability_from_pos_adjustment = (pos_adjusted_model - initial_model) / deviation deformability_from_neg_adjustment = (initial_model - neg_adjusted_model) / deviation deformability = np.max( [deformability_from_pos_adjustment, deformability_from_neg_adjustment]) return deformability
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 calc_single_control_point( mlc, jaw, delivered_mu=1, leaf_pair_widths=__DEFAULT_LEAF_PAIR_WIDTHS, grid_resolution=__DEFAULT_GRID_RESOLUTION, min_step_per_pixel=__DEFAULT_MIN_STEP_PER_PIXEL, ): """Calculate the MU Density for a single control point. Examples -------- >>> from pymedphys._imports import numpy as np >>> from pymedphys._mudensity.mudensity import ( ... calc_single_control_point, display_mu_density) >>> >>> leaf_pair_widths = (2, 2) >>> mlc = np.array([ ... [ ... [1, 1], ... [2, 2], ... ], ... [ ... [2, 2], ... [3, 3], ... ] ... ]) >>> jaw = np.array([ ... [1.5, 1.2], ... [1.5, 1.2] ... ]) >>> grid, mu_density = calc_single_control_point( ... mlc, jaw, leaf_pair_widths=leaf_pair_widths) >>> display_mu_density(grid, mu_density) >>> >>> grid['mlc'] array([-3., -2., -1., 0., 1., 2., 3.]) >>> >>> grid['jaw'] array([-1.5, -0.5, 0.5, 1.5]) >>> >>> np.round(mu_density, 2) array([[0. , 0.07, 0.43, 0.5 , 0.43, 0.07, 0. ], [0. , 0.14, 0.86, 1. , 0.86, 0.14, 0. ], [0.14, 0.86, 1. , 1. , 1. , 0.86, 0.14], [0.03, 0.17, 0.2 , 0.2 , 0.2 , 0.17, 0.03]]) """ leaf_pair_widths = np.array(leaf_pair_widths) leaf_division = leaf_pair_widths / grid_resolution if not np.all(leaf_division.astype(int) == leaf_division): raise ValueError( "The grid resolution needs to exactly divide every leaf pair width." ) if ( not np.max(np.abs(jaw)) # pylint: disable = unneeded-not <= np.sum(leaf_pair_widths) / 2 ): raise ValueError( "The jaw should not travel further out than the maximum leaf limits. " f"Max travel was {np.max(np.abs(jaw))}" ) (grid, grid_leaf_map, mlc) = _determine_calc_grid_and_adjustments( mlc, jaw, leaf_pair_widths, grid_resolution ) positions = { "mlc": { 1: (-mlc[0, :, 0], -mlc[1, :, 0]), # left -1: (mlc[0, :, 1], mlc[1, :, 1]), # right }, "jaw": { 1: (-jaw[0::-1, 0], -jaw[1::, 0]), # bot -1: (jaw[0::-1, 1], jaw[1::, 1]), # top }, } time_steps = _calc_time_steps(positions, grid_resolution, min_step_per_pixel) blocked_by_device = _calc_blocked_by_device( grid, positions, grid_resolution, time_steps ) device_open = _calc_device_open(blocked_by_device) mlc_open, jaw_open = _remap_mlc_and_jaw(device_open, grid_leaf_map) open_fraction = _calc_open_fraction(mlc_open, jaw_open) mu_density = open_fraction * delivered_mu return grid, mu_density
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 from_user_inputs( cls, axes_reference, dose_reference, axes_evaluation, dose_evaluation, dose_percent_threshold, distance_mm_threshold, lower_percent_dose_cutoff=20, interp_fraction=10, max_gamma=None, local_gamma=False, global_normalisation=None, skip_once_passed=False, random_subset=None, ram_available=None, quiet=False, ): if max_gamma is None: max_gamma = np.inf axes_reference, axes_evaluation = run_input_checks( axes_reference, dose_reference, axes_evaluation, dose_evaluation) dose_percent_threshold = expand_dims_to_1d(dose_percent_threshold) distance_mm_threshold = expand_dims_to_1d(distance_mm_threshold) if global_normalisation is None: global_normalisation = np.max(dose_reference) lower_dose_cutoff = lower_percent_dose_cutoff / 100 * global_normalisation maximum_test_distance = np.max(distance_mm_threshold) * max_gamma evaluation_interpolation = scipy.interpolate.RegularGridInterpolator( axes_evaluation, np.array(dose_evaluation), bounds_error=False, fill_value=np.inf, ) dose_reference = np.array(dose_reference) reference_dose_above_threshold = dose_reference >= lower_dose_cutoff mesh_axes_reference = np.meshgrid(*axes_reference, indexing="ij") flat_mesh_axes_reference = np.array( [np.ravel(item) for item in mesh_axes_reference]) reference_points_to_calc = reference_dose_above_threshold reference_points_to_calc = np.ravel(reference_points_to_calc) if random_subset is not None: to_calc_index = np.where(reference_points_to_calc)[0] np.random.shuffle(to_calc_index) random_subset_to_calc = np.full_like(reference_points_to_calc, False, dtype=bool) random_subset_to_calc[ # pylint: disable=unsupported-assignment-operation to_calc_index[0:random_subset]] = True reference_points_to_calc = random_subset_to_calc flat_dose_reference = np.ravel(dose_reference) return cls( flat_mesh_axes_reference, flat_dose_reference, reference_points_to_calc, dose_percent_threshold, distance_mm_threshold, evaluation_interpolation, interp_fraction, max_gamma, lower_dose_cutoff, maximum_test_distance, global_normalisation, local_gamma, skip_once_passed, ram_available, quiet, )
def calc_mu_density( mu, mlc, jaw, grid_resolution=None, max_leaf_gap=None, leaf_pair_widths=None, min_step_per_pixel=None, ): """Determine the MU Density. Both jaw and mlc positions are defined in bipolar format for each control point. A negative value indicates travel over the isocentre. All positional arguments are defined at the isocentre projection with the units of mm. Parameters ---------- mu : numpy.ndarray 1-D array containing an MU value for each control point. mlc : numpy.ndarray 3-D array containing the MLC positions | axis 0: control point | axis 1: mlc pair | axis 2: leaf bank jaw : numpy.ndarray 2-D array containing the jaw positions. | axis 0: control point | axis 1: diaphragm grid_resolution : float, optional The calc grid resolution. Defaults to 1 mm. max_leaf_gap : float, optional The maximum possible distance between opposing leaves. Defaults to 400 mm. leaf_pair_widths : tuple, optional The widths of each leaf pair in the MLC limiting device. The number of entries in the tuples defines the number of leaf pairs. Each entry itself defines that particular leaf pair width. Defaults to 80 leaf pairs each 5 mm wide. min_step_per_pixel : int, optional The minimum number of time steps used per pixel for each control point. Defaults to 10. Returns ------- mu_density : numpy.ndarray 2-D array containing the calculated mu density. | axis 0: jaw direction | axis 1: mlc direction Examples -------- >>> import numpy as np >>> import pymedphys >>> >>> leaf_pair_widths = (5, 5, 5) >>> max_leaf_gap = 10 >>> mu = np.array([0, 2, 5, 10]) >>> mlc = np.array([ ... [ ... [1, 1], ... [2, 2], ... [3, 3] ... ], ... [ ... [2, 2], ... [3, 3], ... [4, 4] ... ], ... [ ... [-2, 3], ... [-2, 4], ... [-2, 5] ... ], ... [ ... [0, 0], ... [0, 0], ... [0, 0] ... ] ... ]) >>> jaw = np.array([ ... [7.5, 7.5], ... [7.5, 7.5], ... [-2, 7.5], ... [0, 0] ... ]) >>> >>> grid = pymedphys.mudensity.grid( ... max_leaf_gap=max_leaf_gap, leaf_pair_widths=leaf_pair_widths) >>> grid['mlc'] array([-5., -4., -3., -2., -1., 0., 1., 2., 3., 4., 5.]) >>> >>> grid['jaw'] array([-8., -7., -6., -5., -4., -3., -2., -1., 0., 1., 2., 3., 4., 5., 6., 7., 8.]) >>> >>> mu_density = pymedphys.mudensity.calculate( ... mu, mlc, jaw, max_leaf_gap=max_leaf_gap, ... leaf_pair_widths=leaf_pair_widths) >>> pymedphys.mudensity.display(grid, mu_density) >>> >>> np.round(mu_density, 1) array([[0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [0. , 0. , 0. , 0.3, 1.9, 2.2, 1.9, 0.4, 0. , 0. , 0. ], [0. , 0. , 0. , 0.4, 2.2, 2.5, 2.2, 0.6, 0. , 0. , 0. ], [0. , 0. , 0. , 0.4, 2.4, 2.8, 2.5, 0.8, 0. , 0. , 0. ], [0. , 0. , 0. , 0.4, 2.5, 3.1, 2.8, 1. , 0. , 0. , 0. ], [0. , 0. , 0. , 0.4, 2.5, 3.4, 3.1, 1.3, 0. , 0. , 0. ], [0. , 0. , 0.4, 2.3, 3.2, 3.7, 3.7, 3.5, 1.6, 0. , 0. ], [0. , 0. , 0.4, 2.3, 3.2, 3.8, 4. , 3.8, 1.9, 0.1, 0. ], [0. , 0. , 0.4, 2.3, 3.2, 3.8, 4.3, 4.1, 2.3, 0.1, 0. ], [0. , 0. , 0.4, 2.3, 3.2, 3.9, 5.2, 4.7, 2.6, 0.2, 0. ], [0. , 0. , 0.4, 2.3, 3.2, 3.8, 5.4, 6.6, 3.8, 0.5, 0. ], [0. , 0.3, 2.2, 3. , 3.5, 4. , 5.1, 7.5, 6.7, 3.9, 0.5], [0. , 0.3, 2.2, 3. , 3.5, 4. , 4.7, 6.9, 6.7, 3.9, 0.5], [0. , 0.3, 2.2, 3. , 3.5, 4. , 4.5, 6.3, 6.4, 3.9, 0.5], [0. , 0.3, 2.2, 3. , 3.5, 4. , 4.5, 5.6, 5.7, 3.8, 0.5], [0. , 0.3, 2.2, 3. , 3.5, 4. , 4.5, 5.1, 5.1, 3.3, 0.5], [0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ]]) MU Density from a Mosaiq record >>> import pymedphys >>> >>> def mu_density_from_mosaiq(msq_server_name, field_id): ... with pymedphys.mosaiq.connect(msq_server_name) as cursor: ... delivery = pymedphys.Delivery.from_mosaiq(cursor, field_id) ... ... grid = pymedphys.mudensity.grid() ... mu_density = delivery.mudensity() ... pymedphys.mudensity.display(grid, mu_density) >>> >>> mu_density_from_mosaiq('a_server_name', 11111) # doctest: +SKIP MU Density from a logfile at a given filepath >>> import pymedphys >>> >>> def mu_density_from_logfile(filepath): ... delivery_data = Delivery.from_logfile(filepath) ... mu_density = Delivery.mudensity() ... ... grid = pymedphys.mudensity.grid() ... pymedphys.mudensity.display(grid, mu_density) >>> >>> mu_density_from_logfile(r"a/path/goes/here") # doctest: +SKIP """ if grid_resolution is None: grid_resolution = __DEFAULT_GRID_RESOLUTION if max_leaf_gap is None: max_leaf_gap = __DEFAULT_MAX_LEAF_GAP if leaf_pair_widths is None: leaf_pair_widths = __DEFAULT_LEAF_PAIR_WIDTHS if min_step_per_pixel is None: min_step_per_pixel = __DEFAULT_MIN_STEP_PER_PIXEL divisibility_of_max_leaf_gap = np.array(max_leaf_gap / 2 / grid_resolution) max_leaf_gap_is_divisible = ( divisibility_of_max_leaf_gap.astype(int) == divisibility_of_max_leaf_gap ) if not max_leaf_gap_is_divisible: raise ValueError( "The grid resolution needs to be able to divide the max leaf gap exactly by" " four" ) leaf_pair_widths = np.array(leaf_pair_widths) if not np.max(np.abs(mlc)) <= max_leaf_gap / 2: # pylint: disable = unneeded-not first_failing_control_point = np.where(np.abs(mlc) > max_leaf_gap / 2)[0][0] raise ValueError( "The mlc should not travel further out than half the maximum leaf gap.\n" "The first failing control point has the following positions:\n" f"{np.array(mlc)[first_failing_control_point, :, :]}" ) mu, mlc, jaw = remove_irrelevant_control_points(mu, mlc, jaw) full_grid = get_grid(max_leaf_gap, grid_resolution, leaf_pair_widths) mu_density = np.zeros((len(full_grid["jaw"]), len(full_grid["mlc"]))) for i in range(len(mu) - 1): control_point_slice = slice(i, i + 2, 1) current_mlc = mlc[control_point_slice, :, :] current_jaw = jaw[control_point_slice, :] delivered_mu = np.diff(mu[control_point_slice]) grid, mu_density_of_slice = calc_single_control_point( current_mlc, current_jaw, delivered_mu, leaf_pair_widths=leaf_pair_widths, grid_resolution=grid_resolution, min_step_per_pixel=min_step_per_pixel, ) full_grid_mu_density_of_slice = _convert_to_full_grid( grid, full_grid, mu_density_of_slice ) mu_density += full_grid_mu_density_of_slice return mu_density