def _padshape(array, Q): y, x = array.shape out_x = int(m.ceil(x * Q)) out_y = int(m.ceil(y * Q)) factor_x = (out_x - x) / 2 factor_y = (out_y - y) / 2 return ( (int(m.floor(factor_y)), int(m.ceil(factor_y))), (int(m.floor(factor_x)), int(m.ceil(factor_x)))), out_x, out_y
def prop_pupil_plane_to_psf_plane_units(wavefunction, input_sample_spacing, prop_dist, wavelength, Q): """Compute the ordinate axes for a pupil plane to PSF plane propagation. Parameters ---------- wavefunction : `numpy.ndarray` the pupil wavefunction input_sample_spacing : `float` spacing between samples in the pupil plane prop_dist : `float` propagation distance along the z distance wavelength : `float` wavelength of light Q : `float` oversampling / padding factor Returns ------- unit_x : `numpy.ndarray` x axis unit, 1D ndarray unit_y : `numpy.ndarray` y axis unit, 1D ndarray """ s = wavefunction.shape samples_x, samples_y = s[1] * Q, s[0] * Q sample_spacing_x = pupil_sample_to_psf_sample( pupil_sample=input_sample_spacing, # factor of samples=samples_x, # 1e3 corrects wavelength=wavelength, # for unit efl=prop_dist) / 1e3 # translation sample_spacing_y = pupil_sample_to_psf_sample( pupil_sample=input_sample_spacing, # factor of samples=samples_y, # 1e3 corrects wavelength=wavelength, # for unit efl=prop_dist) / 1e3 # translation unit_x = m.arange(-1 * int(m.ceil(samples_x / 2)), int(m.floor(samples_x / 2))) * sample_spacing_x unit_y = m.arange(-1 * int(m.ceil(samples_y / 2)), int(m.floor(samples_y / 2))) * sample_spacing_y return unit_x, unit_y
def matrix_dft(f, alpha, npix, shift=None, unitary=False): ''' A technique shamelessly stolen from Andy Kee @ NASA JPL Is it magic or math? ''' if np.isscalar(alpha): ax = ay = alpha else: ax = ay = np.asarray(alpha) f = np.asarray(f) m, n = f.shape if np.isscalar(npix): M = N = npix else: M = N = np.asarray(npix) if shift is None: sx = sy = 0 else: sx = sy = np.asarray(shift) # Y and X are (r,c) coordinates in the (m x n) input plane, f # V and U are (r,c) coordinates in the (M x N) output plane, F X = np.arange(n) - floor(n / 2) - sx Y = np.arange(m) - floor(m / 2) - sy U = np.arange(N) - floor(N / 2) - sx V = np.arange(M) - floor(M / 2) - sy E1 = exp(1j * -2 * np.pi * (ay / m) * np.outer(Y, V).T) E2 = exp(1j * -2 * np.pi * (ax / m) * np.outer(X, U)) F = E1.dot(f).dot(E2) if unitary is True: norm_coef = sqrt((ay * ax) / (m * n * M * N)) return F * norm_coef else: return F
def pad2d(array, Q=2, value=0): """Symmetrically pads a 2D array with a value. Parameters ---------- array : `numpy.ndarray` source array Q : `float` or `int` oversampling factor; ratio of input to output array widths value : `float` or `int` value with which to pad the array Returns ------- `numpy.ndarray` padded array Notes ----- padding will be symmetric. """ if Q is 1: return array else: y, x = array.shape out_x = int(x * Q) out_y = int(y * Q) factor_x = (out_x - x) / 2 factor_y = (out_y - y) / 2 pad_shape = ((int(m.floor(factor_y)), int(m.ceil(factor_y))), (int(m.floor(factor_x)), int(m.ceil(factor_x)))) if value is 0: out = m.zeros((out_y, out_x), dtype=array.dtype) else: out = m.zeros((out_y, out_x), dtype=array.dtype) + value yy, xx = pad_shape out[yy[0]:yy[0] + y, xx[0]:xx[0] + x] = array return out
def mask(self, shape_or_mask, diameter=None): """Mask the signal. The mask will be inscribed in the axis with fewer pixels. I.e., for a interferogram with 1280x1000 pixels, the mask will be 1000x1000 at largest. Parameters ---------- shape_or_mask : `str` or `numpy.ndarray` valid shape from prysm.geometry diameter : `float` diameter of the mask, in self.spatial_units mask : `numpy.ndarray` user-provided mask Returns ------- self modified Interferogram instance. """ if isinstance(shape_or_mask, str): if diameter is None: diameter = self.diameter mask = mcache(shape_or_mask, min(self.shape), radius=diameter / min(self.diameter_x, self.diameter_y)) base = m.zeros(self.shape, dtype=config.precision) difference = abs(self.shape[0] - self.shape[1]) l, u = int(m.floor(difference / 2)), int(m.ceil(difference / 2)) if u is 0: # guard against nocrop scenario _slice = slice(None) else: _slice = slice(l, -u) if self.shape[0] < self.shape[1]: base[:, _slice] = mask else: base[_slice, :] = mask mask = base else: mask = shape_or_mask hitpts = mask == 0 self.phase[hitpts] = m.nan return self
def regular_polygon(sides, samples, radius=1): """Generate a regular polygon mask with the given number of sides and samples in the mask array. Parameters ---------- sides : `int` number of sides to the polygon samples : `int` number of samples in the output polygon radius : `float`, optional radius of the regular polygon. For R=1, will fill the x and y extent Returns ------- `numpy.ndarray` mask for regular polygon with radius equal to the array radius """ verts = generate_vertices(sides, int(m.floor((samples // 2) * radius))) verts[:, 0] += samples // 2 # shift y to center verts[:, 1] += samples // 2 # shift x to center return generate_mask(verts, samples).astype(config.precision)
def plot2d(self, axlim=25, power=1, clim=(None, None), interp_method='lanczos', pix_grid=None, cmap=config.image_colormap, fig=None, ax=None, show_axlabels=True, show_colorbar=True, circle_ee=None, circle_ee_lw=None): """Create a 2D plot of the PSF. Parameters ---------- axlim : `float` limits of axis, symmetric. xlim=(-axlim,axlim), ylim=(-axlim, axlim) power : `float` power to stretch the data by for plotting clim : iterable limits to use for log color scaling. If power != 1 and clim != (None, None), clim (log axes) takes precedence interp_method : `string` method used to interpolate the image between samples of the PSF pix_grid : `float` if not None, overlays gridlines with spacing equal to pix_grid. Intended to show the collection into camera pixels while still in the oversampled domain cmap : `str`, optional colormap, passed directly to matplotlib fig : `matplotlib.figure.Figure`, optional: Figure containing the plot ax : `matplotlib.axes.Axis`, optional: Axis containing the plot show_axlabels : `bool` whether or not to show the axis labels show_colorbar : `bool` whether or not to show the colorbar circle_ee : `float`, optional relative encircled energy to draw a circle at, in addition to diffraction limited airy radius (1.22*λ*F#). First airy zero occurs at circle_ee=0.8377850436212378 circle_ee_lw : `float`, optional linewidth passed to matplotlib for the encircled energy circles Returns ------- fig : `matplotlib.figure.Figure`, optional Figure containing the plot ax : `matplotlib.axes.Axis`, optional Axis containing the plot """ from matplotlib import colors, patches label_str = 'Normalized Intensity [a.u.]' left, right = self.unit_x[0], self.unit_x[-1] bottom, top = self.unit_y[0], self.unit_y[-1] fig, ax = share_fig_ax(fig, ax) plt_opts = { 'extent': [left, right, bottom, top], 'origin': 'lower', 'cmap': cmap, 'interpolation': interp_method, } cb_opts = {} if power is not 1: plt_opts['norm'] = colors.PowerNorm(1 / power) plt_opts['clim'] = (0, 1) elif clim[1] is not None: plt_opts['norm'] = colors.LogNorm(*clim) cb_opts = {'extend': 'both'} im = ax.imshow(self.data, **plt_opts) if show_colorbar: cb = fig.colorbar(im, label=label_str, ax=ax, fraction=0.046, **cb_opts) cb.outline.set_edgecolor('k') cb.outline.set_linewidth(0.5) if show_axlabels: ax.set(xlabel='Image Plane x [μm]', ylabel='Image Plane y [μm]') ax.set(xlim=(-axlim, axlim), ylim=(-axlim, axlim)) if pix_grid is not None: # if pixel grid is desired, add it mult = m.floor(axlim / pix_grid) gmin, gmax = -mult * pix_grid, mult * pix_grid pts = m.arange(gmin, gmax, pix_grid) ax.set_yticks(pts, minor=True) ax.set_xticks(pts, minor=True) ax.yaxis.grid(True, which='minor', color='white', alpha=0.25) ax.xaxis.grid(True, which='minor', color='white', alpha=0.25) if circle_ee is not None: if self.fno is None: raise ValueError( 'F/# must be known to compute EE, set self.fno') elif self.wavelength is None: raise ValueError( 'wavelength must be known to compute EE, set self.wavelength' ) radius = self.ee_radius(circle_ee) analytic = _inverse_analytic_encircled_energy( self.fno, self.wavelength, circle_ee) c_diff = patches.Circle((0, 0), analytic, fill=False, color='r', ls='--', lw=circle_ee_lw) c_true = patches.Circle((0, 0), radius, fill=False, color='r', lw=circle_ee_lw) ax.add_artist(c_diff) ax.add_artist(c_true) ax.legend([c_diff, c_true], ['Diff. Lim.', 'Actual'], ncol=2) return fig, ax
def bindown(array, nsamples_x, nsamples_y=None, mode='avg'): """Bin (resample) an array. Parameters ---------- array : `numpy.ndarray` array of values nsamples_x : `int` number of samples in x axis to bin by nsamples_y : `int` number of samples in y axis to bin by. If None, duplicates value from nsamples_x mode : `str`, {'avg', 'sum'} sum or avg, how to adjust the output signal Returns ------- `numpy.ndarray` ndarray binned by given number of samples Notes ----- Array should be 2D. TODO: patch to allow 3D data. If the size of `array` is not evenly divisible by the number of samples, the algorithm will trim around the border of the array. If the trim length is odd, one extra sample will be lost on the left side as opposed to the right side. Raises ------ ValueError invalid mode """ if nsamples_y is None: nsamples_y = nsamples_x if nsamples_x == 1 and nsamples_y == 1: return array # determine amount we need to trim the array samples_x, samples_y = array.shape total_samples_x = samples_x // nsamples_x total_samples_y = samples_y // nsamples_y final_idx_x = total_samples_x * nsamples_x final_idx_y = total_samples_y * nsamples_y residual_x = int(samples_x - final_idx_x) residual_y = int(samples_y - final_idx_y) # if the amount to trim is symmetric, trim symmetrically. if not is_odd(residual_x) and not is_odd(residual_y): samples_to_trim_x = residual_x // 2 samples_to_trim_y = residual_y // 2 trimmed_data = array[samples_to_trim_x:final_idx_x + samples_to_trim_x, samples_to_trim_y:final_idx_y + samples_to_trim_y] # if not, trim more on the left. else: samples_tmp_x = (samples_x - final_idx_x) // 2 samples_tmp_y = (samples_y - final_idx_y) // 2 samples_top = int(m.floor(samples_tmp_y)) samples_bottom = int(m.ceil(samples_tmp_y)) samples_left = int(m.ceil(samples_tmp_x)) samples_right = int(m.floor(samples_tmp_x)) trimmed_data = array[samples_left:final_idx_x + samples_right, samples_bottom:final_idx_y + samples_top] intermediate_view = trimmed_data.reshape(total_samples_x, nsamples_x, total_samples_y, nsamples_y) if mode.lower() in ('avg', 'average', 'mean'): output_data = intermediate_view.mean(axis=(1, 3)) elif mode.lower() == 'sum': output_data = intermediate_view.sum(axis=(1, 3)) else: raise ValueError('mode must be average of sum.') # trim as needed to make even number of samples. # TODO: allow work with images that are of odd dimensions px_x, px_y = output_data.shape trim_x, trim_y = 0, 0 if is_odd(px_x): trim_x = 1 if is_odd(px_y): trim_y = 1 return output_data[:px_y - trim_y, :px_x - trim_x]
def plot2d(self, axlim=25, interp_method='lanczos', pix_grid=None, fig=None, ax=None): '''Create a 2D color plot of the PSF. Parameters ---------- axlim : `float` limits of axis, symmetric. xlim=(-axlim,axlim), ylim=(-axlim, axlim) interp_method : `str` method used to interpolate the image between samples of the PSF pix_grid : `float` if not None, overlays gridlines with spacing equal to pix_grid. Intended to show the collection into camera pixels while still in the oversampled domain fig : `matplotlib.figure.Figure`, optional Figure containing the plot ax : `matplotlib.axes.Axis`, optional: Axis containing the plot Returns ------- fig : `matplotlib.figure.Figure`, optional Figure containing the plot ax : `matplotlib.axes.Axis`, optional: Axis containing the plot ''' dat = m.empty((self.samples_x, self.samples_y, 3)) dat[:, :, 0] = self.R dat[:, :, 1] = self.G dat[:, :, 2] = self.B left, right = self.unit_y[0], self.unit_y[-1] bottom, top = self.unit_x[0], self.unit_x[-1] fig, ax = share_fig_ax(fig, ax) ax.imshow(dat, extent=[left, right, bottom, top], interpolation=interp_method, origin='lower') ax.set(xlabel=r'Image Plane X [$\mu m$]', ylabel=r'Image Plane Y [$\mu m$]', xlim=(-axlim, axlim), ylim=(-axlim, axlim)) if pix_grid is not None: # if pixel grid is desired, add it mult = m.floor(axlim / pix_grid) gmin, gmax = -mult * pix_grid, mult * pix_grid pts = m.arange(gmin, gmax, pix_grid) ax.set_yticks(pts, minor=True) ax.set_xticks(pts, minor=True) ax.yaxis.grid(True, which='minor') ax.xaxis.grid(True, which='minor') return fig, ax
def plot2d(self, axlim=25, power=1, interp_method='lanczos', pix_grid=None, fig=None, ax=None, show_axlabels=True, show_colorbar=True, circle_ee=None): """Create a 2D plot of the PSF. Parameters ---------- axlim : `float` limits of axis, symmetric. xlim=(-axlim,axlim), ylim=(-axlim, axlim) power : `float` power to stretch the data by for plotting interp_method : `string` method used to interpolate the image between samples of the PSF pix_grid : `float` if not None, overlays gridlines with spacing equal to pix_grid. Intended to show the collection into camera pixels while still in the oversampled domain fig : `matplotlib.figure.Figure`, optional: Figure containing the plot ax : `matplotlib.axes.Axis`, optional: Axis containing the plot show_axlabels : `bool` whether or not to show the axis labels show_colorbar : `bool` whether or not to show the colorbar circle_ee : `float`, optional relative encircled energy to draw a circle at, in addition to diffraction limited airy radius (1.22*λ*F#). First airy zero occurs at circle_ee=0.8377850436212378 Returns ------- fig : `matplotlib.figure.Figure`, optional Figure containing the plot ax : `matplotlib.axes.Axis`, optional Axis containing the plot """ label_str = 'Normalized Intensity [a.u.]' lims = (0, 1) left, right = self.unit_x[0], self.unit_x[-1] bottom, top = self.unit_y[0], self.unit_y[-1] fig, ax = share_fig_ax(fig, ax) im = ax.imshow(self.data, extent=[left, right, bottom, top], origin='lower', cmap='Greys_r', norm=colors.PowerNorm(1 / power), interpolation=interp_method, clim=lims) if show_colorbar: cb = fig.colorbar(im, label=label_str, ax=ax, fraction=0.046) cb.outline.set_edgecolor('k') cb.outline.set_linewidth(0.5) if show_axlabels: ax.set(xlabel=r'Image Plane $x$ [$\mu m$]', ylabel=r'Image Plane $y$ [$\mu m$]') ax.set(xlim=(-axlim, axlim), ylim=(-axlim, axlim)) if pix_grid is not None: # if pixel grid is desired, add it mult = m.floor(axlim / pix_grid) gmin, gmax = -mult * pix_grid, mult * pix_grid pts = m.arange(gmin, gmax, pix_grid) ax.set_yticks(pts, minor=True) ax.set_xticks(pts, minor=True) ax.yaxis.grid(True, which='minor', color='white', alpha=0.25) ax.xaxis.grid(True, which='minor', color='white', alpha=0.25) if circle_ee is not None: if self.fno is None: raise ValueError( 'F/# must be known to compute EE, set self.fno') elif self.wavelength is None: raise ValueError( 'wavelength must be known to compute EE, set self.wavelength' ) radius = self.ee_radius(circle_ee) analytic = _inverse_analytic_encircled_energy( self.fno, self.wavelength, circle_ee) c_diff = patches.Circle((0, 0), analytic, fill=False, color='r', ls='--') c_true = patches.Circle((0, 0), radius, fill=False, color='r') ax.add_artist(c_diff) ax.add_artist(c_true) return fig, ax
def trace_focus(self, algorithm='avg'): ''' finds the focus position in each field. This is, in effect, the "field curvature" for this azimuth. Args: algorithm (`str): algorithm to use to trace focus, currently only supports '0.5', see notes for a description of this technique. Returns: `numpy.ndarray`: focal surface sag, in microns, vs field. Notes: Algorithm '0.5' uses the frequency that has its peak closest to 0.5 on-axis to estimate the focus coresponding to the minimum RMS WFE condition. This is based on the following assumptions: * Any combination of third, fifth, and seventh order spherical aberration will produce a focus shift that depends on frequency, and this dependence can be well fit by an equation of the form y(x) = ax^2 + bx + c. If this is true, then the frequency which peaks at 0.5 will be near the vertex of the quadratic, which converges to the min RMS WFE condition. * Coma, while it enhances depth of field, does not shift the focus peak. * Astigmatism and field curvature are the dominant cause of any shift in best focus with field. * Chromatic aberrations do not influence the thru-focus MTF peak in a way that varies with field. ''' if algorithm == '0.5': # locate the frequency index on axis idx_axis = np.searchsorted(self.field, 0) idx_freq = abs(self.data[:, idx_axis, :].max(axis=0) - 0.5).argmin(axis=1) focus_idx = self.data[:, np.arange(self.data.shape[1]), idx_freq].argmax(axis=0) return self.focus[focus_idx], self.field elif algorithm.lower() in ('avg', 'average'): if self.freq[0] == 0: # if the zero frequency is included, exclude it from our calculations avg_idxs = self.data.argmax(axis=0)[:, 1:].mean(axis=1) else: avg_idxs = self.data.argmax(axis=0).mean(axis=1) # account for fractional indexes focus_out = avg_idxs.copy() for i, idx in enumerate(avg_idxs): li, ri = floor(idx), ceil(idx) lf, rf = self.focus[li], self.focus[ri] diff = rf - lf part = idx % 1 focus_out[i] = lf + diff * part return focus_out, self.field else: raise ValueError('0.5 is only algorithm supported')
def plot2d(self, log=False, axlim=25, interp_method='lanczos', pix_grid=None, fig=None, ax=None): ''' Creates a 2D color plot of the PSF. Args: log (`bool`): if true, plot in log scale. If false, plot in linear scale. axlim (`float`): limits of axis, symmetric. xlim=(-axlim,axlim), ylim=(-axlim, axlim). interp_method (`string`): method used to interpolate the image between samples of the PSF. pix_grid (`float`): if not None, overlays gridlines with spacing equal to pix_grid. Intended to show the collection into camera pixels while still in the oversampled domain. fig (pyplot.figure): figure to plot in. ax (pyplot.axis): axis to plot in. Returns: pyplot.fig, pyplot.axis. Figure and axis containing the plot. Notes: Largely a copy-paste of plot2d() from the PSF class. Some refactoring could be done to make the code more succinct and unified. ''' dat = np.empty((self.samples_x, self.samples_y, 3)) dat[:, :, 0] = self.R dat[:, :, 1] = self.G dat[:, :, 2] = self.B if log: fcn = 20 * np.log10(1e-100 + dat) lims = (-100, 0) # show first 100dB -- range from (1e-6, 1) in linear scale else: fcn = correct_gamma(dat) lims = (0, 1) left, right = self.unit_x[0], self.unit_x[-1] bottom, top = self.unit_y[0], self.unit_y[-1] fig, ax = share_fig_ax(fig, ax) ax.imshow(fcn, extent=[left, right, bottom, top], interpolation=interp_method, origin='lower') ax.set(xlabel=r'Image Plane X [$\mu m$]', ylabel=r'Image Plane Y [$\mu m$]', xlim=(-axlim, axlim), ylim=(-axlim, axlim)) if pix_grid is not None: # if pixel grid is desired, add it mult = floor(axlim / pix_grid) gmin, gmax = -mult * pix_grid, mult * pix_grid pts = np.arange(gmin, gmax, pix_grid) ax.set_yticks(pts, minor=True) ax.set_xticks(pts, minor=True) ax.yaxis.grid(True, which='minor') ax.xaxis.grid(True, which='minor') return fig, ax
def plot2d(self, axlim=25, power=1, interp_method='lanczos', pix_grid=None, fig=None, ax=None, show_axlabels=True, show_colorbar=True): ''' Creates a 2D plot of the PSF. Args: axlim (`float`): limits of axis, symmetric. xlim=(-axlim,axlim), ylim=(-axlim, axlim). power (`float`): power to stretch the data by for plotting. interp_method (`string`): method used to interpolate the image between samples of the PSF. pix_grid (`float`): if not None, overlays gridlines with spacing equal to pix_grid. Intended to show the collection into camera pixels while still in the oversampled domain. fig (pyplot.figure): figure to plot in. ax (pyplot.axis): axis to plot in. show_axlabels (`bool`): whether or not to show the axis labels. show_colorbar (`bool`): whether or not to show the colorbar. Returns: pyplot.fig, pyplot.axis. Figure and axis containing the plot. ''' fcn = correct_gamma(self.data ** power) label_str = 'Normalized Intensity [a.u.]' lims = (0, 1) left, right = self.unit_x[0], self.unit_x[-1] bottom, top = self.unit_y[0], self.unit_y[-1] fig, ax = share_fig_ax(fig, ax) im = ax.imshow(fcn, extent=[left, right, bottom, top], origin='lower', cmap='Greys_r', interpolation=interp_method, clim=lims) if show_colorbar: cb = fig.colorbar(im, label=label_str, ax=ax, fraction=0.046) cb.outline.set_edgecolor('k') cb.outline.set_linewidth(0.5) if show_axlabels: ax.set(xlabel=r'Image Plane $x$ [$\mu m$]', ylabel=r'Image Plane $y$ [$\mu m$]') ax.set(xlim=(-axlim, axlim), ylim=(-axlim, axlim)) if pix_grid is not None: # if pixel grid is desired, add it mult = floor(axlim / pix_grid) gmin, gmax = -mult * pix_grid, mult * pix_grid pts = np.arange(gmin, gmax, pix_grid) ax.set_yticks(pts, minor=True) ax.set_xticks(pts, minor=True) ax.yaxis.grid(True, which='minor', color='white', alpha=0.25) ax.xaxis.grid(True, which='minor', color='white', alpha=0.25) return fig, ax
make_focus_range_realistic_number_of_microns, prepare_document_local, prepare_document_global, ) from iris.recipes import opt_routine_lbfgsb, opt_routine_basinhopping from iris.core import config_codex_params_to_pupil from iris.rings import W1 efl, fno, lambda_ = 50, 2, 0.55 extinction = 1000 / (fno * lambda_) DEFAULT_CONFIG = SimulationConfig( efl=efl, fno=fno, wvl=lambda_, samples=128, freqs=tuple(range(10, floor(extinction), 10)), focus_range_waves=1 / 2 * sqrt(3), # waves / Zernike/Hopkins / norm(Z4) focus_zernike=True, focus_normed=True, focus_planes=21) DEFAULT_CONFIG = make_focus_range_realistic_number_of_microns( DEFAULT_CONFIG, 5) def run_simulation(truth=(0, 0.125, 0, 0), guess=(0, 0.0, 0, 0), cfg=None, solver='global', decoder_ring=None, solver_opts=None, core_opts=None):