def __init__(self, width, sample_spacing=0.025, samples=0): '''Produce a Pinhole. Parameters ---------- width : `float` the width of the pinhole sample_spacing : `float` spacing of samples in the synthetic image samples : `int` number of samples per dimension in the synthetic image Notes ----- Default of 0 samples allows quick creation for convolutions without generating the image; use samples > 0 for an actual image. ''' self.width = width # produce coordinate arrays if samples > 0: ext = samples / 2 * sample_spacing x, y = m.linspace(-ext, ext, samples), m.linspace(-ext, ext, samples) xv, yv = m.meshgrid(x, y) w = width / 2 # paint a circle on a black background arr = m.zeros((samples, samples)) arr[m.sqrt(xv**2 + yv**2) < w] = 1 else: arr, x, y = None, m.zeros(2), m.zeros(2) super().__init__(data=arr, unit_x=x, unit_y=y, has_analytic_ft=True)
def __init__(self, width, orientation='Vertical', sample_spacing=0.075, samples=0): '''Create a new Slit instance. Parameters ---------- width : `float` the width of the slit in microns orientation : `string`, {'Horizontal', 'Vertical', 'Crossed', 'Both'} the orientation of the slit; Crossed and Both produce the same results sample_spacing : `float` spacing of samples in the synthetic image samples : `int` number of samples per dimension in the synthetic image Notes ----- Default of 0 samples allows quick creation for convolutions without generating the image; use samples > 0 for an actual image. ''' w = width / 2 if samples > 0: ext = samples / 2 * sample_spacing x, y = m.linspace(-ext, ext, samples), m.linspace(-ext, ext, samples) arr = m.zeros((samples, samples)) else: arr, x, y = None, m.zeros(2), m.zeros(2) # paint in the slit if orientation.lower() in ('v', 'vert', 'vertical'): if samples > 0: arr[:, abs(x) < w] = 1 self.orientation = 'Vertical' self.width_x = width self.width_y = 0 elif orientation.lower() in ('h', 'horiz', 'horizontal'): if samples > 0: arr[abs(y) < w, :] = 1 self.width_x = 0 self.width_y = width self.orientation = 'Horizontal' elif orientation.lower() in ('b', 'both', 'c', 'crossed'): if samples > 0: arr[abs(y) < w, :] = 1 arr[:, abs(x) < w] = 1 self.orientation = 'Crossed' self.width_x, self.width_y = width, width super().__init__(arr, x, y, has_analytic_ft=True)
def __init__(self, width_x, width_y=None, sample_spacing=0.1, samples_x=384, samples_y=None): """Create a new OLPF object. Parameters ---------- width_x : `float` blur width in the x direction, microns width_y : `float` blur width in the y direction, microns sample_spacing : `float`, optional center to center spacing of samples samples_x : `int`, optional number of samples along x axis samples_y : `int`, optional number of samples along y axis; duplicates x if None """ # compute relevant spacings if width_y is None: width_y = width_x if samples_y is None: samples_y = samples_x self.width_x = width_x self.width_y = width_y if samples_x is None: # do no math data, ux, uy = None, m.zeros(2), m.zeros(2) else: space_x = width_x / 2 space_y = width_y / 2 shift_x = int(space_x // sample_spacing) shift_y = int(space_y // sample_spacing) center_x = samples_x // 2 center_y = samples_y // 2 data = m.zeros((samples_x, samples_y)) data[center_y - shift_y, center_x - shift_x] = 1 data[center_y - shift_y, center_x + shift_x] = 1 data[center_y + shift_y, center_x - shift_x] = 1 data[center_y + shift_y, center_x + shift_x] = 1 ux = m.linspace(-space_x, space_x, samples_x) uy = m.linspace(-space_y, space_y, samples_y) super().__init__(data=data, unit_x=ux, unit_y=uy, has_analytic_ft=True)
def truecircle(samples=128, radius=1): """Create a "true" circular mask with anti-aliasing. Parameters ---------- samples : `int`, optional number of samples in the square output array radius : `float`, optional radius of the shape in the square output array. radius=1 will fill the x Returns ------- `numpy.ndarray` nonbinary ndarray representation of the mask Notes ----- Based on a more general algorithm by Jim Fienup """ if radius is 0: return m.zeros((samples, samples), dtype=config.precision) else: rho, phi = make_rho_phi_grid(samples, samples) one_pixel = 2 / samples radius_plus = radius + (one_pixel / 2) intermediate = (radius_plus - rho) * (samples / 2) return m.minimum(m.maximum(intermediate, 0), 1)
def __init__(self, width_x, width_y=None, sample_spacing=0, samples_x=None, samples_y=None): """Create a new `PixelAperture` object. Parameters ---------- width_x : `float` width of the aperture in the x dimension, in microns. width_y : `float`, optional siez of the aperture in the y dimension, in microns sample_spacing : `float`, optional spacing of samples, in microns samples_x : `int`, optional number of samples in the x dimension samples_y : `int`, optional number of samples in the y dimension """ if width_y is None: width_y = width_x if samples_y is None: samples_y = samples_x self.width_x = width_x self.width_y = width_y if samples_x is None: # do no math data, ux, uy = None, m.zeros(2), m.zeros(2) else: # build PixelAperture model center_x = samples_x // 2 center_y = samples_y // 2 half_width = width_x / 2 half_height = width_y / 2 steps_x = int(half_width // sample_spacing) steps_y = int(half_height // sample_spacing) data = m.zeros((samples_x, samples_y)) data[center_y - steps_y:center_y + steps_y, center_x - steps_x:center_x + steps_x] = 1 extx, exty = samples_x // 2 * sample_spacing, samples_y // 2 * sample_spacing ux, uy = m.linspace(-extx, extx, samples_x), m.linspace(-exty, exty, samples_y) super().__init__(data=data, unit_x=ux, unit_y=uy, has_analytic_ft=True)
def pad2d(array, Q=2, value=0, mode='constant'): """Symmetrically pads a 2D array with a value. Parameters ---------- array : `numpy.ndarray` source array Q : `float`, optional oversampling factor; ratio of input to output array widths value : `float`, optioanl value with which to pad the array mode : `str`, optional mode, passed directly to np.pad Returns ------- `numpy.ndarray` padded array Notes ----- padding will be symmetric. """ if Q is 1: return array else: if mode == 'constant': pad_shape, out_x, out_y = _padshape(array, Q) y, x = array.shape 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 else: pad_shape, *_ = _padshape(array, Q) if mode == 'constant': kwargs = {'constant_values': value, 'mode': mode} else: kwargs = {'mode': mode} return m.pad(array, pad_shape, **kwargs)
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 build(self): # construct an equation for the phase of the pupil # build a coordinate system over which to evaluate this function self._gengrid() self.phase = m.zeros((self.samples, self.samples), dtype=config.precision) for term, coef in enumerate(self.coefs): # short circuit for speed if coef == 0: continue self.phase += coef * zernwrapper( term=term, rho=self.rho, phi=self.phi) self._correct_phase_units() self._phase_to_wavefunction() return self.phase, self.fcn
def polychromatic(psfs, spectral_weights=None, interp_method='linear'): """Create a new PSF instance from an ensemble of monochromatic PSFs given spectral weights. The new PSF is the polychromatic PSF, assuming the wavelengths are sufficiently different that they do not interfere and the mode of imaging is incoherent. """ if spectral_weights is None: spectral_weights = [1] * len(psfs) # find the most densely sampled PSF min_spacing = 1e99 ref_idx = None ref_unit_x = None ref_unit_y = None ref_samples_x = None ref_samples_y = None for idx, psf in enumerate(psfs): if psf.sample_spacing < min_spacing: min_spacing = psf.sample_spacing ref_idx = idx ref_unit_x = psf.unit_x ref_unit_y = psf.unit_y ref_samples_x = psf.samples_x ref_samples_y = psf.samples_y merge_data = m.zeros((ref_samples_x, ref_samples_y, len(psfs))) for idx, psf in enumerate(psfs): # don't do anything to the reference PSF besides spectral scaling if idx is ref_idx: merge_data[:, :, idx] = psf.data * spectral_weights[idx] else: xv, yv = m.meshgrid(ref_unit_x, ref_unit_y) interpf = interpolate.RegularGridInterpolator( (psf.unit_y, psf.unit_x), psf.data) merge_data[:, :, idx] = interpf( (yv, xv), method=interp_method) * spectral_weights[idx] psf = PSF(data=merge_data.sum(axis=2), unit_x=ref_unit_x, unit_y=ref_unit_y) psf.spectral_weights = spectral_weights psf._renorm() return psf
def __init__(self, psfs, weights=None): '''Create a new `MultispectralPSF` instance. Parameters ---------- psfs : iterable iterable of PSFs weights : iterable iterable of weights associated with each PSF ''' if weights is None: weights = [1] * len(psfs) # find the most densely sampled PSF min_spacing = 1e99 ref_idx = None ref_unit_x = None ref_unit_y = None ref_samples_x = None ref_samples_y = None for idx, psf in enumerate(psfs): if psf.sample_spacing < min_spacing: min_spacing = psf.sample_spacing ref_idx = idx ref_unit_x = psf.unit_x ref_unit_y = psf.unit_y ref_samples_x = psf.samples_x ref_samples_y = psf.samples_y merge_data = m.zeros((ref_samples_x, ref_samples_y, len(psfs))) for idx, psf in enumerate(psfs): # don't do anything to our reference PSF if idx is ref_idx: merge_data[:, :, idx] = psf.data * weights[idx] else: xv, yv = m.meshgrid(ref_unit_x, ref_unit_y) interpf = interpolate.RegularGridInterpolator( (psf.unit_x, psf.unit_y), psf.data) merge_data[:, :, idx] = interpf( (xv, yv), method='linear') * weights[idx] self.weights = weights super().__init__(merge_data.sum(axis=2), min_spacing) self._renorm()
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 __init__(self, angle=4, background='white', sample_spacing=2, samples=256): """Create a new TitledSquare instance. Parameters ---------- angle : `float` angle in degrees to tilt w.r.t. the x axis background : `string` white or black; the square will be the opposite color of the background sample_spacing : `float` spacing of samples samples : `int` number of samples """ radius = 0.3 if background.lower() == 'white': arr = m.ones((samples, samples)) fill_with = 0 else: arr = m.zeros((samples, samples)) fill_with = 1 # TODO: optimize by working with index numbers directly and avoid # creation of X,Y arrays for performance. x = m.linspace(-0.5, 0.5, samples) y = m.linspace(-0.5, 0.5, samples) xx, yy = m.meshgrid(x, y) sf = samples * sample_spacing # TODO: convert inline operation to use of rotation matrix angle = m.radians(angle) xp = xx * m.cos(angle) - yy * m.sin(angle) yp = xx * m.sin(angle) + yy * m.cos(angle) mask = (abs(xp) < radius) * (abs(yp) < radius) arr[mask] = fill_with super().__init__(data=arr, unit_x=x * sf, unit_y=y * sf, has_analytic_ft=False)
def build(self): """Construct a numerical model of a `Pupil`. The method should be overloaded by all subclasses to impart their unique mathematical models to the simulation. Returns ------- `Pupil` this pupil instance """ # build up the pupil self._gengrid() # fill in the phase of the pupil self.phase = m.zeros((self.samples, self.samples), dtype=config.precision) return self
def build(self): '''Uses the wavefront coefficients stored in this class instance to build a wavefront model. Args: none Returns: (numpy.ndarray, numpy.ndarray) arrays containing the phase, and the wavefunction for the pupil. ''' # build a coordinate system over which to evaluate this function self.phase = m.zeros((self.samples, self.samples), dtype=config.precision) for term, coef in enumerate(self.coefs): # short circuit for speed if coef == 0: continue self.phase += coef * zcache.get_zernike(term, self.normalize, self.samples) self._phase_to_wavefunction() return self.phase, self.fcn
def build(self): """Use the wavefront coefficients stored in this class instance to build a wavefront model. Returns ------- self.phase : `numpy.ndarray` arrays containing the phase associated with the pupil self.fcn : `numpy.ndarray` array containing the wavefunction of the pupil plane """ # build a coordinate system over which to evaluate this function self.phase = m.zeros((self.samples, self.samples), dtype=config.precision) for term, coef in enumerate(self.coefs): # short circuit for speed if coef == 0: continue else: idx = self._map[term] self.phase += coef * self._cache(idx, self.normalize, self.samples) # NOQA return self
def circle(samples=128, radius=1): """Create a circular mask. Parameters ---------- samples : `int`, optional number of samples in the square output array Returns ------- `numpy.ndarray` binary ndarray representation of the mask """ if radius is 0: return m.zeros((samples, samples)) else: x = m.linspace(-1, 1, samples) y = x xx, yy = m.meshgrid(x, y) rho, phi = cart_to_polar(xx, yy) mask = m.ones(rho.shape) mask[rho > radius] = 0 return mask
def inverted_circle(samples=128, radius=1): """Create an inverted circular mask (obscuration). Parameters ---------- samples : `int`, optional number of samples in the square output array radius : `float`, optional radius of the shape in the square output array. radius=1 will fill the x Returns ------- `numpy.ndarray` binary ndarray representation of the mask """ if radius is 0: return m.zeros((samples, samples), dtype=config.precision) else: rho, phi = make_rho_phi_grid(samples, samples) mask = m.ones(rho.shape, dtype=config.precision) mask[rho < radius] = 0 return mask
def __init__(self, *args, **kwargs): """Initialize a new Zernike instance.""" if args is not None: if len(args) is 0: self.coefs = m.zeros(len(self._map), dtype=config.precision) else: self.coefs = m.asarray([*args[0]], dtype=config.precision) self.normalize = False pass_args = {} self.base = config.zernike_base try: bb = kwargs['base'] if bb > 1: raise ValueError('It violates convention to use a base greater than 1.') elif bb < 0: raise ValueError('It is nonsensical to use a negative base.') self.base = bb except KeyError: # user did not specify base pass if kwargs is not None: for key, value in kwargs.items(): if key[0].lower() == 'z': idx = int(key[1:]) # strip 'Z' from index self.coefs[idx - self.base] = value elif key in ('norm'): self.normalize = value elif key.lower() == 'base': self.base = value else: pass_args[key] = value super().__init__(**pass_args)
def truncate_topn(self, n): """Truncate the pupil to only the top n terms. Parameters ---------- n : `int` number of parameters to keep Returns ------- `self` modified FringeZernike instance. """ topn = self.top_n(n) new_coefs = m.zeros(len(self.coefs), dtype=config.precision) for coef in topn: mag, index, *_ = coef new_coefs[index-self.base] = mag self.coefs = new_coefs self.build() self.mask(self._mask, self.mask_target) return self
def mkary(): # default for defaultdict return m.zeros(2)
def plot2d_rgbgrid(self, axlim=25, interp_method='lanczos', pix_grid=None, fig=None, ax=None): """Create a 2D color plot of the PSF and R,G,B components. 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 fig : `matplotlib.figure.Figure`, optional Figure containing the plot ax : `matplotlib.axes.Axis`, optional: Axis containing the plot Notes ----- Need to refine internal workings at some point. """ # make the arrays for the RGB images dat = m.empty((self.samples_y, self.samples_x, 3)) datr = m.zeros((self.samples_y, self.samples_x, 3)) datg = m.zeros((self.samples_y, self.samples_x, 3)) datb = m.zeros((self.samples_y, self.samples_x, 3)) dat[:, :, 0] = self.R dat[:, :, 1] = self.G dat[:, :, 2] = self.B datr[:, :, 0] = self.R datg[:, :, 1] = self.G datb[:, :, 2] = self.B left, right = self.unit[0], self.unit[-1] ax_width = 2 * axlim # generate a figure and axes to plot in fig, ax = share_fig_ax(fig, ax) axr, axg, axb = make_rgb_axes(ax) ax.imshow(dat, extent=[left, right, left, right], interpolation=interp_method, origin='lower') axr.imshow(datr, extent=[left, right, left, right], interpolation=interp_method, origin='lower') axg.imshow(datg, extent=[left, right, left, right], interpolation=interp_method, origin='lower') axb.imshow(datb, extent=[left, right, left, right], interpolation=interp_method, origin='lower') for axs in (ax, axr, axg, axb): ax.set(xlim=(-axlim, axlim), ylim=(-axlim, axlim)) if pix_grid is not None: # if pixel grid is desired, add it mult = m.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') ax.set(xlabel=r'Image Plane X [$\mu m$]', ylabel=r'Image Plane Y [$\mu m$]') axr.text(-axlim + 0.1 * ax_width, axlim - 0.2 * ax_width, 'R', color='white') axg.text(-axlim + 0.1 * ax_width, axlim - 0.2 * ax_width, 'G', color='white') axb.text(-axlim + 0.1 * ax_width, axlim - 0.2 * ax_width, 'B', color='white') return fig, ax