def convert_IEC_angle_to_bipolar(angle): angle = np.copy(angle) if np.all(angle == 180): return angle angle[angle > 180] = angle[angle > 180] - 360 is_180 = np.where(angle == 180)[0] not_180 = np.where(np.invert(angle == 180))[0] where_closest_left_leaning = np.argmin(np.abs(is_180[:, None] - not_180[None, :]), axis=1) where_closest_right_leaning = (len(not_180) - 1 - np.argmin( np.abs(is_180[::-1, None] - not_180[None, ::-1]), axis=1)[::-1]) closest_left_leaning = not_180[where_closest_left_leaning] closest_right_leaning = not_180[where_closest_right_leaning] assert np.all( np.sign(angle[closest_left_leaning]) == np.sign( angle[closest_right_leaning]) ), "Unable to automatically determine whether angle is 180 or -180" angle[is_180] = np.sign(angle[closest_left_leaning]) * angle[is_180] return angle
def penumbra_width( self, side: str = "left", lower: int = 20, upper: int = 80, interpolate: bool = False, ): """Return the penumbra width of the profile. This is the standard "penumbra width" calculation that medical physics talks about in radiation profiles. Standard is the 80/20 width, although 90/10 is sometimes used. Parameters ---------- side : {'left', 'right', 'both'} Which side of the profile to determined penumbra. If 'both', the left and right sides are averaged. lower : int The "lower" penumbra value used to calculate penumbra. Must be lower than upper. upper : int The "upper" penumbra value used to calculate penumbra. interpolate : bool Whether to interpolate the profile to get more accurate values. Raises ------ ValueError If lower penumbra is larger than upper penumbra """ if lower > upper: raise ValueError( "Upper penumbra value must be larger than the lower penumbra value" ) if side in (LEFT, RIGHT): li = self._penumbra_point(side, lower, interpolate) ui = self._penumbra_point(side, upper, interpolate) pen = np.abs(ui - li) elif side == BOTH: li = self._penumbra_point(LEFT, lower, interpolate) ui = self._penumbra_point(LEFT, upper, interpolate) lpen = np.abs(ui - li) li = self._penumbra_point(RIGHT, lower, interpolate) ui = self._penumbra_point(RIGHT, upper, interpolate) rpen = np.abs(ui - li) pen = np.mean([lpen, rpen]) return pen
def _convert_to_full_grid(grid, full_grid, mu_density): grid_xx, grid_yy = np.meshgrid(grid["mlc"], grid["jaw"]) full_grid_xx, full_grid_yy = np.meshgrid(full_grid["mlc"], full_grid["jaw"]) xx_from, xx_to = np.where( np.abs(full_grid_xx[None, 0, :] - grid_xx[0, :, None]) < 0.0001) yy_from, yy_to = np.where( np.abs(full_grid_yy[None, :, 0] - grid_yy[:, 0, None]) < 0.0001) full_grid_mu_density = np.zeros_like(full_grid_xx) full_grid_mu_density[ # pylint: disable=unsupported-assignment-operation np.ix_(yy_to, xx_to)] = mu_density[np.ix_(yy_from, xx_from)] return full_grid_mu_density
def compare_mosaiq_fields(servers, field_ids): unique_servers = list(set(servers)) with pymedphys.mosaiq.connect(unique_servers) as cursors: deliveries = [ pymedphys.Delivery.from_mosaiq(cursors[server], field_id) for server, field_id in zip(servers, field_ids) ] mu_density_results = [ delivery_data.mudensity() for delivery_data in deliveries ] mu_densities = [results[2] for results in mu_density_results] labels = [ "Server: `{}` | Field ID: `{}`".format(server, field_id) for server, field_id in zip(servers, field_ids) ] plot_gantry_collimator(labels, deliveries) plot_mu_densities(labels, mu_density_results) mu_densities_match = np.all([ np.all(np.abs(mu_density_a - mu_density_b) < 0.1) for mu_density_a, mu_density_b in itertools.combinations( mu_densities, 2) ]) plt.show() print("MU Densities match: {}".format(mu_densities_match)) return deliveries, mu_densities
def calc_comparison(logfile_mu_density, mosaiq_mu_density, normalisation=None): if normalisation is None: normalisation = np.sum(mosaiq_mu_density) comparison = np.sum(np.abs(logfile_mu_density - mosaiq_mu_density)) / normalisation return comparison
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 pcolormesh_grid(x, y, grid_resolution=None): if grid_resolution is None: diffs = np.hstack([np.diff(x), np.diff(y)]) assert np.all(np.abs(diffs - diffs[0]) < 10 ** -12) grid_resolution = diffs[0] new_x = np.concatenate([x - grid_resolution / 2, [x[-1] + grid_resolution / 2]]) new_y = np.concatenate([y - grid_resolution / 2, [y[-1] + grid_resolution / 2]]) return new_x, new_y
def soft_surface_dice(reference, evaluation): """Non-TensorFlow implementation of a soft surface dice """ edge_reference = skimage.filters.scharr(reference) edge_evaluation = skimage.filters.scharr(evaluation) score = np.sum(np.abs(edge_evaluation - edge_reference)) / np.sum( edge_evaluation + edge_reference ) return 1 - score
def define_rotation_field_points_at_origin(edge_lengths, penumbra): x_half_range = edge_lengths[0] / 2 + penumbra / 2 y_half_range = edge_lengths[1] / 2 + penumbra / 2 num_x = np.ceil(x_half_range * 2 * 8) + 1 num_y = np.ceil(y_half_range * 2 * 8) + 1 x = np.linspace(-x_half_range, x_half_range, int(num_x)) y = np.linspace(-y_half_range, y_half_range, int(num_y)) xx, yy = np.meshgrid(x, y) xx_flat = np.ravel(xx) yy_flat = np.ravel(yy) inside = np.logical_and((np.abs(xx_flat) < x_half_range), (np.abs(yy_flat) < y_half_range)) xx_flat = xx_flat[np.invert(inside)] yy_flat = yy_flat[np.invert(inside)] return xx_flat, yy_flat
def get_symmetry(self): """ max point diff relative to mean Calculated as the maximum difference between corresponding points on opposite sides of the profile center, relative to mean dose. Returns ------- float """ dose = self.slice_umbra().y return max(np.abs(np.subtract(dose, dose[::-1]) / np.average(dose)))
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 find_consecutive_logfiles(field_id_key_map, field_id, filehash, index): keys = np.array(field_id_key_map[field_id]) times = np.array([index[key]["local_time"] for key in keys]).astype(np.datetime64) sort_reference = np.argsort(times) keys = keys[sort_reference] times = times[sort_reference] hours_4 = np.array(60 * 60 * 4).astype(np.timedelta64) delivery_time = np.array(index[filehash]["local_time"]).astype(np.datetime64) within_4_hours_reference = np.abs(delivery_time - times) < hours_4 within_4_hours = keys[within_4_hours_reference].tolist() return within_4_hours
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 get_logfile_delivery_data_bygantry(index, config, logfile_groups, mosaiq_gantry_angles): logfile_delivery_data_bygantry = dict() for logfile_group in logfile_groups: logfile_delivery_data_bygantry[logfile_group] = dict() for file_hash in logfile_group: filepath = get_filepath(index, config, file_hash) logfile_delivery_data = pymedphys.Delivery.from_logfile(filepath) mu = np.array(logfile_delivery_data.monitor_units) filtered = ( logfile_delivery_data._filter_cps( ) # pylint: disable = protected-access ) mu = filtered.monitor_units mlc = filtered.mlc jaw = filtered.jaw logfile_gantry_angles = filtered.gantry gantry_tolerance = get_gantry_tolerance(index, file_hash, config) unique_logfile_gantry_angles = np.unique(logfile_gantry_angles) assert_array_agreement(unique_logfile_gantry_angles, mosaiq_gantry_angles, gantry_tolerance) logfile_delivery_data_bygantry[logfile_group][file_hash] = dict() for mosaiq_gantry_angle in mosaiq_gantry_angles: logfile_delivery_data_bygantry[logfile_group][file_hash][ mosaiq_gantry_angle] = dict() agrees_within_tolerance = ( np.abs(logfile_gantry_angles - mosaiq_gantry_angle) <= gantry_tolerance) logfile_delivery_data_bygantry[logfile_group][file_hash][ mosaiq_gantry_angle]["mu"] = mu[agrees_within_tolerance] logfile_delivery_data_bygantry[logfile_group][file_hash][ mosaiq_gantry_angle]["mlc"] = mlc[agrees_within_tolerance] logfile_delivery_data_bygantry[logfile_group][file_hash][ mosaiq_gantry_angle]["jaw"] = jaw[agrees_within_tolerance] return logfile_delivery_data_bygantry
def get_comparison_results(mosaiq_mu_density_bygantry, logfile_mu_density_bygantry, normalisation): comparison_results = dict() mosaiq_gantry_angles = mosaiq_mu_density_bygantry.keys() logfile_groups = list(logfile_mu_density_bygantry.keys()) for mosaiq_gantry_angle in mosaiq_gantry_angles: comparison_results[mosaiq_gantry_angle] = dict() comparison_results[mosaiq_gantry_angle]["comparisons"] = {} grid_xx = mosaiq_mu_density_bygantry[mosaiq_gantry_angle][0] grid_yy = mosaiq_mu_density_bygantry[mosaiq_gantry_angle][1] mosaiq_mu_density = mosaiq_mu_density_bygantry[mosaiq_gantry_angle][2] for logfile_group in logfile_groups: assert np.all(grid_xx == logfile_mu_density_bygantry[logfile_group] [mosaiq_gantry_angle][0]) assert np.all(grid_yy == logfile_mu_density_bygantry[logfile_group] [mosaiq_gantry_angle][1]) logfile_mu_density = logfile_mu_density_bygantry[logfile_group][ mosaiq_gantry_angle][2] comparison = calc_comparison(logfile_mu_density, mosaiq_mu_density, normalisation) comparison_results[mosaiq_gantry_angle]["comparisons"][ logfile_group] = comparison comparisons = np.array([ comparison_results[mosaiq_gantry_angle]["comparisons"] [logfile_group] for logfile_group in logfile_groups ]) comparison_results[mosaiq_gantry_angle]["median"] = np.median( comparisons) ref = np.argmin( np.abs(comparisons - comparison_results[mosaiq_gantry_angle]["median"])) comparison_results[mosaiq_gantry_angle][ "median_filehash_group"] = logfile_groups[ref] comparison_results[mosaiq_gantry_angle][ "filehash_groups"] = logfile_groups return comparison_results
def assert_array_agreement(unique_logfile_gantry_angles, mosaiq_gantry_angles, allowed_deviation): difference_matrix = np.abs(unique_logfile_gantry_angles[:, None] - mosaiq_gantry_angles[None, :]) agreement_matrix = difference_matrix <= allowed_deviation row_agreement = np.any(agreement_matrix, axis=1) at_least_one_agreement = np.all(row_agreement) assert at_least_one_agreement, ( "There is a logfile gantry angle that deviates by more than {} degrees" " from the Mosaiq control points. Unsure how to handle this.\n\n" "Logfile: {}\nMosaiq: {}\nDifference Matrix:\n{}\n" "Agreement Matrix:\n{}".format( allowed_deviation, unique_logfile_gantry_angles, mosaiq_gantry_angles, difference_matrix, agreement_matrix, ))
def fwxm(self, x: int = 50, interpolate: bool = False): """Return the width at X-Max, where X is the percentage height. Parameters ---------- x : int The percent height of the profile. E.g. x = 50 is 50% height, i.e. FWHM. interpolate : bool If True, interpolates the values to give a more accurate FWXM. Returns ------- int, float The width in number of elements of the FWXM. """ li = self._penumbra_point(LEFT, x, interpolate) ri = self._penumbra_point(RIGHT, x, interpolate) fwxm = np.abs(ri - li) return fwxm
def fwxm_center(self, x: int = 50, interpolate: bool = False, kind: str = "index"): """Return the center index of the FWXM. See Also -------- fwxm() : Further parameter info """ fwxm = self.fwxm(x, interpolate=interpolate) li = self._penumbra_point(LEFT, x, interpolate) fwxmcen = np.abs(li + fwxm / 2) if not interpolate: fwxmcen = int(round(fwxmcen)) if kind == VALUE: return ( self.values[fwxmcen] if not interpolate else self._values_interp[int(fwxmcen * self.interpolation_factor)] ) else: return fwxmcen
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 _gantry_angle_masks(self, gantry_angles, gantry_tol, allow_missing_angles=False): masks = [ self._gantry_angle_mask(gantry_angle, gantry_tol) for gantry_angle in gantry_angles ] for mask in masks: if np.all(mask == 0): continue # TODO: Apply mask by more than just gantry angle to appropriately # extract beam index even when multiple beams have the same gantry # angle is_duplicate_gantry_angles = (np.sum( np.abs(np.diff(np.concatenate([[0], mask, [0]])))) != 2) if is_duplicate_gantry_angles: raise ValueError("Duplicate gantry angles not yet supported") try: assert np.all(np.sum(masks, axis=0) == 1), ( "Not all beams were captured by the gantry tolerance of " " {}".format(gantry_tol)) except AssertionError: if not allow_missing_angles: print("Allowable gantry angles = {}".format(gantry_angles)) gantry = np.array(self.gantry, copy=False) out_of_tolerance = np.unique( gantry[np.sum(masks, axis=0) == 0]).tolist() print("The gantry angles out of tolerance were {}".format( out_of_tolerance)) raise return masks
def resample_y(self, step): """ resampled y-values at a given increment Resulting profile has nonuniform step-size, but each step represents and approximately equal step in dose. Parameters ---------- step : float sampling increment Returns ------- Profile """ temp_x = np.arange(min(self.x), max(self.x), 0.01 * self.get_increment()) temp_y = self.interp(temp_x) resamp_x = [temp_x[0]] resamp_y = [temp_y[0]] last_y = temp_y[0] for i, _ in enumerate(temp_x): if np.abs(temp_y[i] - last_y) >= step: resamp_x.append(temp_x[i]) resamp_y.append(temp_y[i]) last_y = temp_y[i] if temp_x[-1] not in resamp_x: resamp_x.append(temp_x[-1]) resamp_y.append(temp_y[-1]) return Profile().from_lists(resamp_x, resamp_y, meta=self.meta)
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 minimize_junction_X(amplitude, peaks, peak_type, dx): print("Analyzing X jaws...") amp_prev = 0 amp_filt_prev = 0 fig = plt.figure(figsize=(10, 6)) # create the plot kk = 0 # counter for figure generation for j in range(0, amplitude.shape[1] - 1): for k in range(j + 1, amplitude.shape[1]): # looping through remaining images amp_base_res = signal.convolve(amplitude[:, j], amplitude[:, j], mode="full") amp_base_res = signal.resample( amp_base_res / np.amax(amp_base_res), int(np.ceil(len(amp_base_res) / 2)), ) amp_overlay_res = signal.convolve(amplitude[:, k], amplitude[:, k], mode="full") amp_overlay_res = signal.resample( amp_overlay_res / np.amax(amp_overlay_res), int(np.ceil(len(amp_overlay_res) / 2)), ) peak1, _ = find_peaks(amp_base_res, prominence=0.5) peak2, _ = find_peaks(amp_overlay_res, prominence=0.5) if (abs(peak2 - peak1) < 2500 ): # if the two peaks are close together proceeed to analysis kk = kk + 1 # incrementing the figure generator cumsum_prev = 1e7 if peak2 < peak1: # this guarantee that we always slide the overlay amp_base_res = amplitude[:, k] amp_overlay_res = amplitude[:, j] else: amp_base_res = amplitude[:, j] amp_overlay_res = amplitude[:, k] if peak_type[j] == 0: inc = -1 else: inc = 1 for i in range(0, inc * 80, inc * 1): # x = np.linspace(0, 0 + (len(amp_base_res) * dx), len(amplitude), # endpoint=False) # definition of the distance axis amp_overlay_res_roll = np.roll(amp_overlay_res, i) # amplitude is the vector to analyze +-500 samples from the center amp_tot = ( amp_base_res[peaks[j] - 1000:peaks[j] + 1000] + amp_overlay_res_roll[peaks[j] - 1000:peaks[j] + 1000] ) # divided by 2 to normalize # xsel = x[peaks[j] - 1000:peaks[j] + 1000] amp_filt = rm.running_mean(amp_tot, 281) cumsum = np.sum(np.abs(amp_tot - amp_filt)) if ( # pylint: disable = no-else-break cumsum > cumsum_prev): # then we went too far break else: amp_prev = amp_tot amp_filt_prev = amp_filt cumsum_prev = cumsum ax = fig.add_subplot(amplitude.shape[1] - 1, 1, kk) ax.plot(amp_prev) ax.plot(amp_filt_prev) if kk == 1: ax.set_title("Minimization result", fontsize=16) if (kk == amplitude.shape[1] - 1 ): # if we reach the final plot the add the x axis label ax.set_xlabel("distance [mm]") ax.set_ylabel("amplitude") # ax.annotate('delta=' + str(abs(i - inc * 1) * dx) + ' mm', xy=(2, 1), xycoords='axes fraction', # xytext=(.35, .10)) if peaks[kk - 1] != 0: ax.annotate( "delta=" + str(abs(i - inc * 1) * dx) + " mm", xy=(2, 1), xycoords="axes fraction", xytext=(0.35, 0.10), ) else: ax.annotate( "delta= 0 mm (NO PEAK FOUND)", xy=(2, 1), xycoords="axes fraction", xytext=(0.35, 0.10), ) return fig
def xyz_axes_from_dataset(ds, coord_system="DICOM"): r"""Returns the x, y and z axes of a DICOM dataset's pixel array in the specified coordinate system. For DICOM RT Dose datasets, these are the x, y, z axes of the dose grid. Parameters ---------- ds : pydicom.dataset.Dataset A DICOM dataset that contains pixel data. Supported modalities include 'CT' and 'RTDOSE'. coord_system : str, optional The coordinate system in which to return the `x`, `y` and `z` axes of the DICOM dataset. The accepted, case-insensitive values of `coord_system` are: 'DICOM' or 'd': Return axes in the DICOM coordinate system. 'patient', 'IEC patient' or 'p': Return axes in the IEC patient coordinate system. 'fixed', 'IEC fixed' or 'f': Return axes in the IEC fixed coordinate system. Returns ------- (x, y, z) A tuple containing three `numpy.ndarray`s corresponding to the `x`, `y` and `z` axes of the DICOM dataset's pixel array in the specified coordinate system. Notes ----- Supported scan orientations [1]_: =========================== ========================== Orientation ds.ImageOrientationPatient =========================== ========================== Feet First Decubitus Left [0, 1, 0, 1, 0, 0] Feet First Decubitus Right [0, -1, 0, -1, 0, 0] Feet First Prone [1, 0, 0, 0, -1, 0] Feet First Supine [-1, 0, 0, 0, 1, 0] Head First Decubitus Left [0, -1, 0, 1, 0, 0] Head First Decubitus Right [0, 1, 0, -1, 0, 0] Head First Prone [-1, 0, 0, 0, -1, 0] Head First Supine [1, 0, 0, 0, 1, 0] =========================== ========================== References ---------- .. [1] O. McNoleg, "Generalized coordinate transformations for Monte Carlo (DOSXYZnrc and VMC++) verifications of DICOM compatible radiotherapy treatment plans", arXiv:1406.0014, Table 1, https://arxiv.org/ftp/arxiv/papers/1406/1406.0014.pdf """ position = np.array(ds.ImagePositionPatient) orientation = np.array(ds.ImageOrientationPatient) if not ( np.array_equal(np.abs(orientation), np.array([1, 0, 0, 0, 1, 0])) or np.array_equal(np.abs(orientation), np.array([0, 1, 0, 1, 0, 0])) ): raise ValueError( "Dose grid orientation is not supported. Dose " "grid slices must be aligned along the " "superoinferior axis of patient." ) is_decubitus = orientation[0] == 0 is_head_first = _orientation_is_head_first(orientation, is_decubitus) di = float(ds.PixelSpacing[0]) dj = float(ds.PixelSpacing[1]) col_range = np.arange(0, ds.Columns * di, di) row_range = np.arange(0, ds.Rows * dj, dj) if is_decubitus: x_dicom_fixed = orientation[1] * position[1] + col_range y_dicom_fixed = orientation[3] * position[0] + row_range else: x_dicom_fixed = orientation[0] * position[0] + col_range y_dicom_fixed = orientation[4] * position[1] + row_range if is_head_first: z_dicom_fixed = position[2] + np.array(ds.GridFrameOffsetVector) else: z_dicom_fixed = -position[2] + np.array(ds.GridFrameOffsetVector) if coord_system.upper() in ("FIXED", "IEC FIXED", "F"): x = x_dicom_fixed y = z_dicom_fixed z = -np.flip(y_dicom_fixed) elif coord_system.upper() in ("DICOM", "D", "PATIENT", "IEC PATIENT", "P"): if orientation[0] == 1: x = x_dicom_fixed elif orientation[0] == -1: x = np.flip(x_dicom_fixed) elif orientation[1] == 1: y_d = x_dicom_fixed elif orientation[1] == -1: y_d = np.flip(x_dicom_fixed) if orientation[4] == 1: y_d = y_dicom_fixed elif orientation[4] == -1: y_d = np.flip(y_dicom_fixed) elif orientation[3] == 1: x = y_dicom_fixed elif orientation[3] == -1: x = np.flip(y_dicom_fixed) if not is_head_first: z_d = np.flip(z_dicom_fixed) else: z_d = z_dicom_fixed if coord_system.upper() in ("DICOM", "D"): y = y_d z = z_d elif coord_system.upper() in ("PATIENT", "IEC PATIENT", "P"): y = z_d z = -np.flip(y_d) return (x, y, z)
def _orientation_is_head_first(orientation_vector, is_decubitus): if is_decubitus: return np.abs(np.sum(orientation_vector)) != 2 return np.abs(np.sum(orientation_vector)) == 2
def minimize_junction_fieldrot( amplitude, peaks, peak_type, dx, profilename ): # minimize junction for field rotations is done differently given the shape of the fields print("Field rotation jaw analysis...") # print('number of peaks=', peaks) amp_prev = 0 amp_filt_prev = 0 fig = plt.figure(figsize=(10, 6)) # create the plot kk = 1 # counter for figure generation for j in range(0, amplitude.shape[1] - 1): for k in range(j + 1, amplitude.shape[1]): # looping through remaining images amp_base_res = signal.convolve(amplitude[:, j], amplitude[:, j], mode="full") amp_base_res = signal.resample( amp_base_res / np.amax(amp_base_res), int(np.ceil(len(amp_base_res) / 2)), ) amp_overlay_res = signal.convolve(amplitude[:, k], amplitude[:, k], mode="full") amp_overlay_res = signal.resample( amp_overlay_res / np.amax(amp_overlay_res), int(np.ceil(len(amp_overlay_res) / 2)), ) # amp_base_res = signal.savgol_filter(amplitude[:, j], 1001, 3) # amp_overlay_res = signal.savgol_filter(amplitude[:, k], 1001, 3) # peak1, _ = find_peaks(amp_base_res, prominence=0.5) # peak2, _ = find_peaks(amp_overlay_res, prominence=0.5) cumsum_prev = 1e7 amp_base_res = amplitude[:, j] amp_overlay_res = amplitude[:, k] if peak_type[j] == 0: inc = -1 else: inc = 1 for i in range(0, inc * 80, inc * 1): # x = np.linspace(0, 0 + (len(amp_base_res) * dx), len(amplitude), # endpoint=False) # definition of the distance axis amp_overlay_res_roll = np.roll(amp_overlay_res, i) # amplitude is the vector to analyze +-500 samples from the center amp_tot = ( amp_base_res[peaks[j] - 1000:peaks[j] + 1000] + amp_overlay_res_roll[peaks[j] - 1000:peaks[j] + 1000] ) # divided by 2 to normalize # xsel = x[peaks[j] - 1000:peaks[j] + 1000] amp_filt = rm.running_mean(amp_tot, 281) cumsum = np.sum(np.abs(amp_tot - amp_filt)) if ( # pylint: disable = no-else-break cumsum > cumsum_prev): # then we went too far ax = fig.add_subplot(amplitude.shape[1] - 1, 1, kk) ax.plot(amp_prev) ax.plot(amp_filt_prev) if kk == 1: ax.set_title("Minimization result - " + profilename, fontsize=16) if ( kk == amplitude.shape[1] - 1 ): # if we reach the final plot the add the x axis label ax.set_xlabel("distance [mm]") ax.set_ylabel("amplitude") ax.annotate( "delta=" + str(abs(i - inc * 1) * dx) + " mm", xy=(2, 1), xycoords="axes fraction", xytext=(0.35, 0.10), ) # plt.show() kk = kk + 1 break else: amp_prev = amp_tot amp_filt_prev = amp_filt cumsum_prev = cumsum return fig
def mudensity_comparisons(config, plot=True, new_logfiles=False): (comparison_storage_filepath, comparison_storage_scratch) = get_cache_filepaths( config ) grid_resolution, _ = get_mu_density_parameters(config) index = get_index(config) field_id_key_map = get_field_id_key_map(index) (file_hashes, comparisons, _) = load_comparisons_from_cache(config) if new_logfiles: file_hashes, _ = random_uncompared_logfiles(index, config, file_hashes) sql_servers_list = get_sql_servers_list(config) with pymedphys.mosaiq.connect(sql_servers_list) as cursors: for file_hash in file_hashes: try: logfile_filepath = get_filepath(index, config, file_hash) print("\n{}".format(logfile_filepath)) if (new_logfiles) and (file_hash in comparisons): raise AssertionError( "A new logfile shouldn't have already been compared" ) if index[file_hash]["delivery_details"]["qa_mode"]: print("Skipping QA field") else: if file_hash in comparisons: print( "Cached comparison value = {}".format( comparisons[file_hash] ) ) results = get_logfile_mosaiq_results( index, config, field_id_key_map, file_hash, cursors, grid_resolution=grid_resolution, ) new_comparison = calc_comparison(results[2], results[3]) if file_hash not in comparisons: update_comparison_file( file_hash, new_comparison, comparison_storage_filepath, comparison_storage_scratch, ) print( "Newly calculated comparison value = {}".format( new_comparison ) ) elif np.abs(comparisons[file_hash] - new_comparison) > 0.00001: print( "Calculated comparison value does not agree with the " "cached value." ) print( "Newly calculated comparison value = {}".format( new_comparison ) ) update_comparison_file( file_hash, new_comparison, comparison_storage_filepath, comparison_storage_scratch, ) print("Overwrote the cache with the new result.") else: print( "Calculated comparison value agrees with the cached value" ) if plot: plot_results(*results) except KeyboardInterrupt: raise except AssertionError: raise except Exception: # pylint: disable = broad-except print(traceback.format_exc())
def find_nearest( array, value ): # find the nearest element of the array to a certain value and return the index of that element array = np.asarray(array) idx = (np.abs(array - value)).argmin() return array[idx], idx
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
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