def gaussian(sigma=0.5, samples=128): """Generate a gaussian mask with a given sigma. Parameters ---------- sigma : `float` width parameter of the gaussian, expressed in samples of the output array samples : `int` number of samples in square array Returns ------- `numpy.ndarray` mask with gaussian shape """ s = sigma x = m.arange(0, samples, 1, dtype=config.precision) y = x[:, m.newaxis] # // is floor division in python x0 = y0 = samples // 2 return m.exp(-4 * m.log(2) * ((x - x0)**2 + (y - y0)**2) / (s * samples)**2)
def resample_2d_complex(array, sample_pts, query_pts): '''Resamples a 2D complex array. Works by interpolating the magnitude and phase independently and merging the results into a complex value. Parameters ---------- array : `numpy.ndarray` complex 2D array sample_pts : `tuple` pair of `numpy.ndarray` objects that contain the x and y sample locations, each array should be 1D query_pts : `tuple` points to interpolate onto, also 1D for each array Returns ------- `numpy.ndarray` array resampled onto query_pts via bivariate spline ''' xq, yq = m.meshgrid(*query_pts) mag = abs(array) phase = m.angle(array) magfunc = interpolate.RegularGridInterpolator(sample_pts, mag) phasefunc = interpolate.RegularGridInterpolator(sample_pts, phase) interp_mag = magfunc((yq, xq)) interp_phase = phasefunc((yq, xq)) return interp_mag * m.exp(1j * interp_phase)
def longexposure_otf(nu, Cn, z, f, lambdabar, h_z_by_r=2.91): """Compute the long exposure OTF for given parameters. Parameters ---------- nu : `numpy.ndarray` spatial frequencies, cy/mm Cn: `float` atmospheric structure constant of refractive index, ranges ~ 10^-13 - 10^-17 z : `float` propagation distance through atmosphere, m f : `float` effective focal length of the optical system, mm lambdabar : `float` mean wavelength, microns h_z_by_r : `float`, optional constant for h[z/r] -- see Eq. 8.5-37 & 8.5-38 in Statistical Optics, J. Goodman, 2nd ed. Returns ------- `numpy.ndarray` the OTF """ # homogenize units nu = nu / 1e3 f = f / 1e3 lambdabar = lambdabar / 1e6 power = 5 / 3 const1 = -m.pi**2 * 2 * h_z_by_r * Cn**2 const2 = z * f**power / (lambdabar**3) nupow = nu**power const = const1 * const2 return m.exp(const * nupow)
def resample_2d_complex(array, sample_pts, query_pts): ''' Resamples a 2D complex array by interpolating the magnitude and phase independently and merging the results into a complex value. Args: array (numpy.ndarray): complex 2D array. sample_pts (tuple): pair of numpy.ndarray objects that contain the x and y sample locations, each array should be 1D. query_pts (tuple): points to interpolate onto, also 1D for each array. Returns: numpy.ndarray array resampled onto query_pts via bivariate spline ''' xq, yq = np.meshgrid(*query_pts) mag = abs(array) phase = np.angle(array) magfunc = interpolate.RegularGridInterpolator(sample_pts, mag) phasefunc = interpolate.RegularGridInterpolator(sample_pts, phase) interp_mag = magfunc((yq, xq)) interp_phase = phasefunc((yq, xq)) return interp_mag * exp(1j * interp_phase)
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 fcn(self): """Complex wavefunction associated with the pupil.""" phase = self.change_phase_unit(to='waves', inplace=False) fcn = m.exp(1j * 2 * m.pi * phase) # phase implicitly in units of waves, no 2pi/l # guard against nans in phase fcn[m.isnan(phase)] = 0 if self.mask_target in ('fcn', 'both'): fcn *= self._mask return fcn
def blackbody_spectrum(temperature, wavelengths): ''' Computes the spectral power distribution of a black body at a given temperature. Args: temperature (`float`): body temp, in Kelvin. wavelengths (`numpy.ndarray`): array of wavelengths, in nanometers. Returns: numpy.ndarray: spectral power distribution in units of W/m^2/nm ''' wavelengths = wavelengths.astype(config.precision) / 1e9 return (2 * h * c ** 2) / (wavelengths ** 5) * \ 1 / (exp((h * c) / (wavelengths * k * temperature) - 1))
def _phase_to_wavefunction(self): """Compute the wavefunction from the phase. Returns ------- `Pupil` this pupil instance """ phase = self.change_phase_unit(to='waves', inplace=False) self.fcn = m.exp(1j * 2 * m.pi * phase) # phase implicitly in units of waves, no 2pi/l # guard against nans in phase self.fcn[m.isnan(phase)] = 0 return self
def gaussian(sigma=0.5, samples=128): ''' Generates a gaussian mask with a given sigma Args: sigma (`float`): width parameter of the gaussian, expressed in radii of the output array. samples (`int`): number of samples in square array. Returns: `numpy.ndarray`: mask with gaussian shape. ''' s = sigma x = np.arange(0, samples, 1, float) y = x[:, np.newaxis] # // is floor division in python x0 = y0 = samples // 2 return exp(-4 * log(2) * ((x - x0**2) + (y - y0)**2) / (s * samples)**2)
def merge_pupils(pupil1, pupil2): '''Merges the phase from two pupils and returns a new :class:`Pupil` instance Args: pupil1 (:class:`Pupil`): first pupil pupil2 (:class:`Pupil`): second pupil Returns :class:`Pupil`: New pupil with merged phase ''' if pupil1.sample_spacing != pupil2.sample_spacing or pupil1.samples != pupil2.samples: raise ValueError('Pupils must be identically sampled') # create a new pupil and copy Pupil1's dictionary into it retpupil = pupil1.clone() retpupil.phase = pupil1.phase + pupil2.phase retpupil.fcn = exp(1j * 2 * pi / retpupil.wavelength * retpupil.phase) retpupil.clip() return retpupil
def total_integrated_scatter(self, wavelength, incident_angle=0): """Calculate the total integrated scatter (TIS) for an angle or angles. Parameters ---------- wavelength : `float` wavelength of light in microns. incident_angle : `float` or `numpy.ndarray` incident angle(s) of light. Returns ------- `float` or `numpy.ndarray` TIS value. """ if self.spatial_unit != 'μm': raise ValueError('Use microns for spatial unit when evaluating TIS.') upper_limit = 1 / wavelength kernel = 4 * m.pi * m.cos(m.radians(incident_angle)) kernel *= self.bandlimited_rms(upper_limit, None) / wavelength return 1 - m.exp(-kernel**2)
def synthesize_surface_from_psd(psd, nu_x, nu_y): """Synthesize a surface height map from PSD data. Parameters ---------- psd : `numpy.ndarray` PSD data, units nm²/(cy/mm)² nu_x : `numpy.ndarray` x spatial frequency, cy/mm nu_y : `numpy.ndarray` y spatial frequency, cy_mm """ # generate a random phase to be matched to the PSD randnums = m.rand(*psd.shape) randfft = m.fft2(randnums) phase = m.angle(randfft) # calculate the output window # the 0th element of nu_y has the greatest frequency in magnitude because of # the convention to put the nyquist sample at -fs instead of +fs for even-size arrays fs = -2 * nu_y[0] dx = dy = 1 / fs ny, nx = psd.shape x, y = m.arange(nx) * dx, m.arange(ny) * dy # calculate the area of the output window, "S2" in GH_FFT notation A = x[-1] * y[-1] # use ifft to compute the PSD signal = m.exp(1j * phase) * m.sqrt(A * psd) coef = 1 / dx / dy out = m.ifftshift(m.ifft2(m.fftshift(signal))) * coef out = out.real return x, y, out
def _phase_to_wavefunction(self): ''' Computes the wavefunction from the phase ''' self.fcn = exp(1j * 2 * pi / self.wavelength * self.phase) return self