def crop(self): """Crop data to rectangle bounding non-NaN region.""" nans = m.isfinite(self.phase) nancols = m.any(nans, axis=0) nanrows = m.any(nans, axis=1) left, right = nanrows.argmax(), nanrows[::-1].argmax() top, bottom = nancols.argmax(), nancols[::-1].argmax() if left == right == top == bottom == 0: return self if left == 0 and bottom == 0: lr = slice(None) if left == 0: lr = slice(-right) elif right == 0: lr = slice(left, self.phase.shape[0]) else: lr = slice(left, -right) if top == 0 and bottom == 0: tb = slice(None) elif top == 0: tb = slice(-bottom) elif bottom == 0: tb = slice(top, self.phase.shape[1]) else: tb = slice(top, -bottom) self.phase = self.phase[lr, tb] self.unit_y, self.unit_x = self.unit_y[lr], self.unit_x[tb] self.unit_x -= self.unit_x[0] self.unit_y -= self.unit_y[0] return self
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 pv(array): """Return the PV value of the valid elements of an array. Parameters ---------- array : `numpy.ndarray` array of values Returns ------- `float` PV of the array """ non_nan = m.isfinite(array) return array[non_nan].max() - array[non_nan].min()
def mean(array): """Return the mean value of the valid elements of an array. Parameters ---------- array : `numpy.ndarray` array of values Returns ------- `float` mean value """ non_nan = m.isfinite(array) return array[non_nan].mean()
def rms(array): """Return the RMS value of the valid elements of an array. Parameters ---------- array : `numpy.ndarray` array of values Returns ------- `float` RMS of the array """ non_nan = m.isfinite(array) return m.sqrt((array[non_nan]**2).mean())
def Sa(array): """Return the Ra value for the valid elements of an array. Parameters ---------- array: `numpy.ndarray` array of values Returns ------- `float` Ra of the array """ non_nan = m.isfinite(array) ary = array[non_nan] mean = ary.mean() return abs(ary - mean).sum() / ary.size
def fill(self, _with=0): """Fill invalid (NaN) values. Parameters ---------- _with : `float`, optional value to fill with Returns ------- `Interferogram` self """ nans = ~m.isfinite(self.phase) self.phase[nans] = _with return self
def std(array): """Return the standard deviation of the valid elements of an array. Parameters ---------- array: `numpy.ndarray` array of values Returns ------- `float` std of the array """ non_nan = m.isfinite(array) ary = array[non_nan] return ary.std()
def crop(self): """Crop data to rectangle bounding non-NaN region.""" nans = m.isfinite(self.phase) nancols = m.any(nans, axis=0) nanrows = m.any(nans, axis=1) left, right = nanrows.argmax(), nanrows[::-1].argmax() top, bottom = nancols.argmax(), nancols[::-1].argmax() if left == right == top == bottom == 0: return self self.phase = self.phase[left:-right, top:-bottom] self.unit_y, self.unit_x = self.unit_y[left:-right], self.unit_x[ top:-bottom] self.unit_x -= self.unit_x[0] self.unit_y -= self.unit_y[0] return self
def fit(data, num_terms=16, rms_norm=False, round_at=6): ''' Fits a number of Zernike coefficients to provided data by minimizing the root sum square between each coefficient and the given data. The data should be uniformly sampled in an x,y grid. Args: data (`numpy.ndarray`): data to fit to. num_terms (`int`): number of terms to fit, fits terms 0~num_terms. rms_norm (`bool`): if true, normalize coefficients to unit RMS value. round_at (`int`): decimal place to round values at. Returns: numpy.ndarray: an array of coefficients matching the input data. ''' if num_terms > len(zernmap): raise ValueError(f'number of terms must be less than {len(zernmap)}') # precompute the valid indexes in the original data pts = m.isfinite(data) # set up an x/y rho/phi grid to evaluate Zernikes on x, y = m.linspace(-1, 1, data.shape[1]), m.linspace(-1, 1, data.shape[0]) xx, yy = m.meshgrid(x, y) rho = m.sqrt(xx**2 + yy**2)[pts].flatten() phi = m.arctan2(xx, yy)[pts].flatten() # compute each Zernike term zernikes = [] for i in range(num_terms): func = z.zernikes[zernmap[i]] base_zern = func(rho, phi) if rms_norm: base_zern *= func.norm zernikes.append(base_zern) zerns = m.asarray(zernikes).T # use least squares to compute the coefficients coefs = m.lstsq(zerns, data[pts].flatten(), rcond=None)[0] return coefs.round(round_at)
def bandreject(self, wllow, wlhigh): """Apply a band-rejection filter to the phase (height) data. Parameters ---------- wllow : `float` low wavelength (spatial period), units of self.scale wlhigh : `float` high wavelength (spatial period), units of self.scale Returns ------- `Interferogram` in-place modified instance of self """ new_phase = bandreject_filter(self.phase, self.sample_spacing, wllow, wlhigh) new_phase[~m.isfinite(self.phase)] = m.nan self.phase = new_phase return self
def bandreject_filter(array, sample_spacing, wllow, wlhigh): sy, sx = array.shape # compute the bandpass in sample coordinates ux, uy = forward_ft_unit(sample_spacing, sx), forward_ft_unit(sample_spacing, sy) fhigh, flow = 1 / wllow, 1 / wlhigh # make an ordinate array in frequency space and use it to make a mask uxx, uyy = m.meshgrid(ux, uy) highpass = ((uxx < -fhigh) | (uxx > fhigh)) | ((uyy < -fhigh) | (uyy > fhigh)) lowpass = ((uxx > -flow) & (uxx < flow)) & ((uyy > -flow) & (uyy < flow)) mask = highpass | lowpass # adjust NaNs and FFT work = array.copy() work[~m.isfinite(work)] = 0 fourier = m.fftshift(m.fft2(m.ifftshift(work))) fourier[mask] = 0 out = m.fftshift(m.ifft2(m.ifftshift(fourier))) return out.real
def zernikefit(data, x=None, y=None, rho=None, phi=None, terms=16, norm=False, residual=False, round_at=6, map_='fringe'): """Fits a number of Zernike coefficients to provided data. Works by minimizing the mean square error between each coefficient and the given data. The data should be uniformly sampled in an x,y grid. Parameters ---------- data : `numpy.ndarray` data to fit to. x : `numpy.ndarray`, optional x coordinates, same shape as data y : `numpy.ndarray`, optional y coordinates, same shape as data rho : `numpy.ndarray`, optional radial coordinates, same shape as data phi : `numpy.ndarray`, optional azimuthal coordinates, same shape as data terms : `int`, optional number of terms to fit, fits terms 0~terms norm : `bool`, optional if True, normalize coefficients to unit RMS value residual : `bool`, optional if True, return a tuple of (coefficients, residual) round_at : `int`, optional decimal place to round values at. map_ : `str`, optional, {'fringe', 'noll'} which ordering of Zernikes to use Returns ------- coefficients : `numpy.ndarray` an array of coefficients matching the input data. residual : `float` RMS error between the input data and the fit. Raises ------ ValueError too many terms requested. """ map_ = maps[map_] if terms > len(fringemap): raise ValueError(f'number of terms must be less than {len(fringemap)}') data = data.T # transpose to mimic transpose of zernikes # precompute the valid indexes in the original data pts = m.isfinite(data) if x is None and rho is None: # set up an x/y rho/phi grid to evaluate Zernikes on rho, phi = make_rho_phi_grid(*reversed(data.shape)) rho = rho[pts].flatten() phi = phi[pts].flatten() elif rho is None: rho, phi = cart_to_polar(x, y) rho, phi = rho[pts].flatten(), phi[pts].flatten() # compute each Zernike term zerns_raw = [] for i in range(terms): func = zernikes[map_[i]] base_zern = func(rho, phi) if norm: base_zern *= func.norm zerns_raw.append(base_zern) zerns = m.asarray(zerns_raw).T # use least squares to compute the coefficients meas_pts = data[pts].flatten() coefs = m.lstsq(zerns, meas_pts, rcond=None)[0] if round_at is not None: coefs = coefs.round(round_at) if residual is True: components = [] for zern, coef in zip(zerns_raw, coefs): components.append(coef * zern) _fit = m.asarray(components) _fit = _fit.sum(axis=0) rmserr = rms(data[pts].flatten() - _fit) return coefs, rmserr else: return coefs
def dropout_percentage(self): """Percentage of pixels in the data that are invalid (NaN).""" return m.count_nonzero(~m.isfinite(self.phase)) / self.phase.size * 100
def remove_piston(self): """Remove piston from the data by subtracting the mean value.""" self.phase -= self.phase[m.isfinite(self.phase)].mean() return self