def rotated_ellipse(width_major, width_minor, major_axis_angle=0, samples=128): """Generate a binary mask for an ellipse, centered at the origin. The major axis will notionally extend to the limits of the array, but this will not be the case for rotated cases. Parameters ---------- width_major : `float` width of the ellipse in its major axis width_minor : `float` width of the ellipse in its minor axis major_axis_angle : `float` angle of the major axis w.r.t. the x axis, degrees samples : `int` number of samples Returns ------- `numpy.ndarray` An ndarray of shape (samples,samples) of value 0 outside the ellipse, and value 1 inside the ellipse Notes ----- The formula applied is: ((x-h)cos(A)+(y-k)sin(A))^2 ((x-h)sin(A)+(y-k)cos(A))^2 ______________________________ + ______________________________ 1 a^2 b^2 where x and y are the x and y dimensions, A is the rotation angle of the major axis, h and k are the centers of the the ellipse, and a and b are the major and minor axis widths. In this implementation, h=k=0 and the formula simplifies to: (x*cos(A)+y*sin(A))^2 (x*sin(A)+y*cos(A))^2 ______________________________ + ______________________________ 1 a^2 b^2 see SO: https://math.stackexchange.com/questions/426150/what-is-the-general-equation-of-the-ellipse-that-is-not-in-the-origin-and-rotate Raises ------ ValueError Description """ if width_minor > width_major: raise ValueError( 'By definition, major axis must be larger than minor.') arr = m.ones((samples, samples)) lim = width_major x, y = m.linspace(-lim, lim, samples), m.linspace(-lim, lim, samples) xv, yv = m.meshgrid(x, y) A = m.radians(-major_axis_angle) a, b = width_major, width_minor major_axis_term = ((xv * m.cos(A) + yv * m.sin(A))**2) / a**2 minor_axis_term = ((xv * m.sin(A) - yv * m.cos(A))**2) / b**2 arr[major_axis_term + minor_axis_term > 1] = 0 return arr
def fit_plane(x, y, z): xx, yy = m.meshgrid(x, y) pts = m.isfinite(z) xx_, yy_ = xx[pts].flatten(), yy[pts].flatten() flat = m.ones(xx_.shape) coefs = m.lstsq(m.stack([xx_, yy_, flat]).T, z[pts].flatten(), rcond=None)[0] plane_fit = coefs[0] * xx + coefs[1] * yy + coefs[2] return plane_fit
def fit_sphere(z): x, y = m.linspace(-1, 1, z.shape[1]), m.linspace(-1, 1, z.shape[0]) xx, yy = m.meshgrid(x, y) pts = m.isfinite(z) xx_, yy_ = xx[pts].flatten(), yy[pts].flatten() rho, phi = cart_to_polar(xx_, yy_) focus = defocus(rho, phi) coefs = m.lstsq(m.stack([focus, m.ones(focus.shape)]).T, z[pts].flatten(), rcond=None)[0] rho, phi = cart_to_polar(xx, yy) sphere = defocus(rho, phi) * coefs[0] return sphere
def fit_plane(x, y, z): pts = m.isfinite(z) if len(z.shape) > 1: x, y = m.meshgrid(x, y) xx, yy = x[pts].flatten(), y[pts].flatten() else: xx, yy = x, y flat = m.ones(xx.shape) coefs = m.lstsq(m.stack([xx, yy, flat]).T, z[pts].flatten(), rcond=None)[0] plane_fit = coefs[0] * x + coefs[1] * y + coefs[2] return plane_fit
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 Returns ------ `numpy.ndarray` binary ndarray representation of the mask """ if radius is 0: return m.ones((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 square(samples=128): """Create a square mask. Parameters ---------- samples : `int`, optional number of samples in the square output array Returns ------- `numpy.ndarray` binary ndarray representation of the mask """ return m.ones((samples, samples), dtype=bool)
def square(samples=128, radius=1): """Create a square mask. 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 """ return m.ones((samples, samples), dtype=bool)
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 psd(height, sample_spacing): """Compute the power spectral density of a signal. Parameters ---------- height : `numpy.ndarray` height or phase data sample_spacing : `float` spacing of samples in the input data Returns ------- unit_x : `numpy.ndarray` ordinate x frequency axis unit_y : `numpy.ndarray` ordinate y frequency axis psd : `numpy.ndarray` power spectral density """ s = height.shape window = window_2d_welch( m.arange(s[1]) * sample_spacing, m.arange(s[0]) * sample_spacing) window = m.ones(height.shape) psd = prop_pupil_plane_to_psf_plane(height * window, norm='ortho') ux = forward_ft_unit(sample_spacing, int(round(height.shape[1], 0))) uy = forward_ft_unit(sample_spacing, int(round(height.shape[0], 0))) psd /= height.size # adjustment for 2D welch window bias, there is room for improvement to this # approximate value from: # Power Spectral Density Specification and Analysis of Large Optical Surfaces # E. Sidick, JPL psd /= 0.925 return ux, uy, psd
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, samples=128, dia=1.0, wavelength=0.55, opd_unit='waves', mask='circle', mask_target='both', ux=None, uy=None, phase=None): """Create a new `Pupil` instance. Parameters ---------- samples : `int`, optional number of samples across the pupil interior dia : `float`, optional diameter of the pupil, mm wavelength : `float`, optional wavelength of light, um opd_unit : `str`, optional, {'waves', 'um', 'nm'} unit used to m.express the OPD. Equivalent strings may be used to the valid options, e.g. 'microns', or 'nanometers' mask : `str` or `numpy.ndarray` mask used to define the amplitude and boundary of the pupil; any regular polygon from `prysm.geometry` as a string, e.g. 'circle' is valid. A user-provided ndarray can also be used. mask_target : `str`, {'phase', 'fcn', 'both', None} which array to mask during pupil creation; only masking fcn is faster for numerical propagations but will make plot2d() and the phase array not be truncated properly. Note that if the mask is not binary and `phase` or `both` is used, phase plots will also not be correct, as they will be attenuated by the mask. ux : `np.ndarray` x axis units uy : `np.ndarray` y axis units phase : `np.ndarray` phase data Notes ----- If ux give, assume uy and phase also given; skip much of the pupil building process and simply copy values. Raises ------ ValueError if the OPD unit given is invalid """ if ux is None: # must build a pupil self.dia = dia ux = m.linspace(-dia / 2, dia / 2, samples) uy = m.linspace(-dia / 2, dia / 2, samples) self.samples = samples need_to_build = True else: # data already known need_to_build = False super().__init__(unit_x=ux, unit_y=uy, phase=phase, wavelength=wavelength, phase_unit=opd_unit, spatial_unit='mm') self.xaxis_label = 'Pupil ξ' self.yaxis_label = 'Pupil η' self.zaxis_label = 'OPD' self.rho = self.phi = None if need_to_build: if type(mask) is not m.ndarray: mask = mcache(mask, self.samples) self._mask = mask self.mask_target = mask_target self.build() self.mask(self._mask, self.mask_target) else: protomask = m.isnan(phase) mask = m.ones(protomask.shape) mask[protomask] = 0 self._mask = mask self.mask_target = 'fcn'
def Z0(rho, phi): return m.ones(rho.shape)
def piston(rho, phi): """Zernike Piston.""" return m.ones(rho.shape)
def piston(rho, phi): return m.ones(rho.shape)