def _mtf_cost_core_euclidian(difference_t, difference_s): """Adjust the raw difference of MTF to the Euclidian distance. Parameters ---------- difference_t : `numpy.ndarray` raw difference of measured and modeled tangential MTF data difference_s : `numpy.ndarray` raw difference of measured and modeled tangential MTF data Returns ------- difference_t : `numpy.ndarray` adjusted difference of measured and modeled tangential MTF data difference_s : `numpy.ndarray` adjusted difference of measured and modeled sagittal MTF data Notes ----- see NIST - https://xlinux.nist.gov/dads/HTML/euclidndstnc.html """ t = (sqrt(difference_t**2)).sum() s = (sqrt(difference_s**2)).sum() return t, s
def __init__(self, width, sample_spacing=None, samples=0): """Create a Pinhole instance. 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, None, None super().__init__(data=arr, unit_x=x, unit_y=y, has_analytic_ft=True)
def __init__(self, width, sample_spacing=0.025, samples=384): ''' Produces a Pinhole. Args: 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. ''' self.width = width # produce coordinate arrays ext = samples / 2 * sample_spacing x, y = np.linspace(-ext, ext, samples), np.linspace(-ext, ext, samples) xv, yv = np.meshgrid(x, y) w = width / 2 # paint a circle on a black background arr = np.zeros((samples, samples)) arr[sqrt(xv**2 + yv**2) < w] = 1 super().__init__(data=arr, sample_spacing=sample_spacing, synthetic=True)
def difflim_mtf_core(normalized_frequency): ''' Computes the MTF at a given normalized spatial frequency. ''' if normalized_frequency >= 1.0: return 0 else: return (2 / pi) * \ (arccos(normalized_frequency) - normalized_frequency * sqrt(1 - normalized_frequency ** 2))
def CCT_Duv_to_uvprime(CCT, Duv, delta_t=0.01): ''' Converts (CCT,Duv) coordinates to upvp coordinates. Args: CCT (`float` or `iterable`): CCT coordinate. Duv (`float` or `iterable`): Duv coordinate. delta_t (`float`): temperature differential used to compute the tangent line to the plankian locust. Default to 0.01, Ohno suggested (2011). Returns: `tuple` containing: `float` u' `float` v' ''' CCT, Duv = np.asarray(CCT), np.asarray(Duv) wvl = np.arange(360, 835, 5) bb_spec_0 = blackbody_spectrum(CCT, wvl) bb_spec_1 = blackbody_spectrum(CCT + delta_t, wvl) bb_spec_0 = { 'wvl': wvl, 'values': bb_spec_0, } bb_spec_1 = { 'wvl': wvl, 'values': bb_spec_1, } xyz_0 = spectrum_to_XYZ_emissive(bb_spec_0) xyz_1 = spectrum_to_XYZ_emissive(bb_spec_1) upvp_0 = XYZ_to_uvprime(xyz_0) upvp_1 = XYZ_to_uvprime(xyz_1) u0, v0 = upvp_0[..., 0], upvp_0[..., 1] u1, v1 = upvp_1[..., 0], upvp_1[..., 1] du, dv = u1 - u0, v1 - v0 u = u0 + Duv * dv / sqrt(du**2 + dv**2) v = u0 + Duv * du / sqrt(du**2 + dv**2) return u, v * 1.5**2 # factor of 1.5 converts v -> v'
def rms(array): ''' Returns the RMS value of the valid elements of an array. Args: array (`numpy.ndarray`) array of values. Returns: `float`. RMS of the array. ''' non_nan = np.isfinite(array) return sqrt((array[non_nan]**2).mean())
def encircled_energy(self, radius): """Compute the encircled energy of the PSF. Parameters ---------- radius : `float` or iterable radius or radii to evaluate encircled energy at Returns ------- encircled energy if radius is a float, returns a float, else returns a list. Notes ----- implementation of "Simplified Method for Calculating Encircled Energy," Baliga, J. V. and Cohn, B. D., doi: 10.1117/12.944334 """ from .otf import MTF if hasattr(radius, '__iter__'): # user wants multiple points # um to mm, cy/mm assumed in Fourier plane radius_is_array = True else: radius_is_array = False # compute MTF from the PSF if self._mtf is None: self._mtf = MTF.from_psf(self) nx, ny = m.meshgrid(self._mtf.unit_x, self._mtf.unit_y) self._nu_p = m.sqrt(nx**2 + ny**2) # this is meaninglessly small and will avoid division by 0 self._nu_p[self._nu_p == 0] = 1e-99 self._dnx, self._dny = ny[1, 0] - ny[0, 0], nx[0, 1] - nx[0, 0] if radius_is_array: out = [] for r in radius: if r not in self._ee: self._ee[r] = _encircled_energy_core( self._mtf.data, r / 1e3, self._nu_p, self._dnx, self._dny) out.append(self._ee[r]) return m.asarray(out) else: if radius not in self._ee: self._ee[radius] = _encircled_energy_core( self._mtf.data, radius / 1e3, self._nu_p, self._dnx, self._dny) return self._ee[radius]
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 Luv_to_chroma_hue(luv): ''' Converts L*u*v* coordiantes to a chroma and hue. Args: luv (`numpy.ndarray`): array with last dimension L*, u*, v*. Returns: `numpy.ndarray` with last dimension corresponding to C* and h. ''' luv = np.asarray(luv) u, v = luv[..., 1], luv[..., 2] C = sqrt(u**2 + v**2) h = atan2(v, u) shape = luv.shape return np.stack((C, h), axis=len(shape))
def _difflim_mtf_core(normalized_frequency): """Compute the MTF at a given normalized spatial frequency. Parameters ---------- normalized_frequency : `numpy.ndarray` normalized frequency; function is defined over [0, and takes a value of 0 for [1, Returns ------- `numpy.ndarray` The diffraction MTF function at a given normalized spatial frequency """ return (2 / m.pi) * \ (m.arccos(normalized_frequency) - normalized_frequency * m.sqrt(1 - normalized_frequency ** 2))
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 cart_to_polar(x, y): ''' Returns the (rho,phi) coordinates of the (x,y) input points. Args: x (float): x coordinate. y (float): y coordinate. Returns: `tuple` containing: `float` or `numpy.ndarray`: radial coordinate. `float` or `numpy.ndarray`: azimuthal coordinate. ''' rho = sqrt(x**2 + y**2) phi = atan2(y, x) return rho, phi
def cart_to_polar(x, y): '''Return the (rho,phi) coordinates of the (x,y) input points. Parameters ---------- x : `numpy.ndarray` or number x coordinate y : `numpy.ndarray` or number y coordinate Returns ------- rho : `numpy.ndarray` or number radial coordinate phi : `numpy.ndarray` or number azimuthal coordinate ''' rho = m.sqrt(x**2 + y**2) phi = m.arctan2(y, x) return rho, phi
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(zernfcns): raise ValueError(f'number of terms must be less than {len(zernfcns)}') # precompute the valid indexes in the original data pts = np.isfinite(data) # set up an x/y rho/phi grid to evaluate zernikes on x, y = np.linspace(-1, 1, data.shape[1]), np.linspace(-1, 1, data.shape[0]) xv, yv = np.meshgrid(x, y) rho = sqrt(xv**2 + yv**2)[pts].flatten() phi = atan2(xv, yv)[pts].flatten() # compute each zernike term zernikes = [] for i in range(num_terms): zernikes.append(zernwrapper(i, rms_norm, rho, phi)) zerns = np.asarray(zernikes).T # use least squares to compute the coefficients coefs = np.linalg.lstsq(zerns, data[pts].flatten())[0] return coefs.round(round_at)
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 analytic_ft(self, unit_x, unit_y): """Analytic fourier transform of a slit. Parameters ---------- unit_x : `numpy.ndarray` sample points in x frequency axis unit_y : `numpy.ndarray` sample points in y frequency axis Returns ------- `numpy.ndarray` 2D numpy array containing the analytic fourier transform """ xq, yq = m.meshgrid(unit_x, unit_y) # factor of pi corrects for jinc being modulo pi # factor of 2 converts radius to diameter rho = m.sqrt(xq**2 + yq**2) * self.width * 2 * m.pi return m.jinc(rho).astype(config.precision)
def uvprime_to_Duv(uvprime): ''' Computes Duv from u'v' coordiantes. Args: uv (`numpy.ndarray`): array with last dimensions corresponding to u, v Returns: `float`: CCT. Notes: see "Calculation of CCT and Duv and Practical Conversion Formulae", Yoshi Ohno http://www.cormusa.org/uploads/CORM_2011_Calculation_of_CCT_and_Duv_and_Practical_Conversion_Formulae.PDF ''' k0, k1, k2, k3 = CIE_DUV_k0, CIE_DUV_k1, CIE_DUV_k2, CIE_DUV_k3 k4, k5, k6 = CIE_DUV_k4, CIE_DUV_k5, CIE_DUV_k6 uv = np.asarray(uvprime) u, v = uv[..., 0], uv[..., 1] / 1.5 # inline convert v' to v L_FP = sqrt((u - 0.292) ** 2 + (v - 0.24) ** 2) a = arccos((u - 0.292) / L_FP) L_BB = k6 * a ** 6 + k5 * a ** 5 + k4 * a ** 4 + k3 * a ** 3 + k2 * a ** 2 + k1 * a + k0 return L_FP - L_BB
def window_2d_welch(x, y, alpha=8): """Return a 2D welch window for a given alpha. Parameters ---------- x : `numpy.ndarray` x values, 1D array y : `numpy.ndarray` y values, 1D array alpha : `float` alpha (edge roll) parameter Returns ------- `numpy.ndarray` window """ xx, yy = m.meshgrid(x, y) r, _ = cart_to_polar(xx, yy) rmax = m.sqrt(x.max()**2 + y.max()**2) window = 1 - abs(r/rmax)**alpha return window
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 zernikes_to_magnitude_angle(coefs, namer): """Convert Fringe Zernike polynomial set to a magnitude and phase representation.""" def mkary(): # default for defaultdict return m.zeros(2) # make a list of names to go with the coefficients names = [namer(i, base=0) for i in range(len(coefs))] combinations = defaultdict(mkary) # for each name and coefficient, make a len 2 array. Put the Y or 0 degree values in the first slot for coef, name in zip(coefs, names): if name.endswith(('X', 'Y', '°')): newname = ' '.join(name.split(' ')[:-1]) if name.endswith('Y'): combinations[newname][0] = coef elif name.endswith('X'): combinations[newname][1] = coef elif name[-2] == '5': # 45 degree case combinations[newname][1] = coef else: combinations[newname][0] = coef else: combinations[name][0] = coef # now go over the combinations and compute the L2 norms and angles for name in combinations: ovals = combinations[name] magnitude = m.sqrt((ovals**2).sum()) if 'Spheric' in name or 'focus' in name or 'iston' in name: phase = 0 else: phase = m.degrees(m.arctan2(*ovals)) values = (magnitude, phase) combinations[name] = values return dict(combinations) # cast to regular dict for return
def zernwrapper(term, rms_norm, rho, phi): ''' Wraps the Z0..Z48 functions. ''' if rms_norm is True: return _normalizations[term] * zernfcns[term](rho, phi) else: return zernfcns[term](rho, phi) # See JCW - http://wp.optics.arizona.edu/jcwyant/wp-content/uploads/sites/13/2016/08/ZernikePolynomialsForTheWeb.pdf _normalizations = ( 1, # Z 0 2, # Z 1 2, # Z 2 sqrt(3), # Z 3 sqrt(6), # Z 4 sqrt(6), # Z 5 2 * sqrt(2), # Z 6 2 * sqrt(2), # Z 7 sqrt(5), # Z 8 2 * sqrt(2), # Z 9 2 * sqrt(2), # Z10 sqrt(10), # Z11 sqrt(10), # Z12 2 * sqrt(3), # Z13 2 * sqrt(3), # Z14 sqrt(7), # Z15 sqrt(10), # Z16 sqrt(10), # Z17 2 * sqrt(3), # Z18
('freqs', 'focus_range_waves', 'focus_zernike', 'focus_normed', 'focus_planes', ) + SystemConfig._fields) SystemConfig.__new__.__defaults__ = ('circle', 'fcn') SimulationConfig.__new__.__defaults__ = ('circle', 'fcn') DEFAULT_SIM_PARAMS = SimulationConfig( efl=50, fno=2, wvl=0.55, samples=128, freqs=tuple(range(10, 850, 10)), focus_range_waves=0.5 / m.sqrt(3), focus_zernike=True, focus_normed=True, focus_planes=21) def thrufocus_mtf_from_wavefront(focused_wavefront, sim_params): """Create a thru-focus T/S MTF curve at each frequency requested from a focused wavefront. Parameters ---------- focused_wavefront : `Pupil` a pupil object sim_params : `SimulationConfig` a SimulationConfig namedtuple
def bandlimited_rms(ux, uy, psd, wllow=None, wlhigh=None, flow=None, fhigh=None): """Calculate the bandlimited RMS of a signal from its PSD. Parameters ---------- ux : `numpy.ndarray` x spatial frequencies uy : `numpy.ndarray` y spatial frequencies psd : `numpy.ndarray` power spectral density wllow : `float` short spatial scale wlhigh : `float` long spatial scale flow : `float` low frequency fhigh : `float` high frequency Returns ------- `float` band-limited RMS value. """ if wllow is not None or wlhigh is not None: # spatial period given if wllow is None: flow = 0 else: fhigh = 1 / wllow if wlhigh is None: fhigh = max(ux[-1], uy[-1]) else: flow = 1 / wlhigh elif flow is not None or fhigh is not None: # spatial frequency given if flow is None: flow = 0 if fhigh is None: fhigh = max(ux[-1], uy[-1]) else: raise ValueError('must specify either period (wavelength) or frequency') ux2, uy2 = m.meshgrid(ux, uy) r, p = cart_to_polar(ux2, uy2) if flow is None: warnings.warn('no lower limit given, using 0 for low frequency') flow = 0 if fhigh is None: warnings.warn('no upper limit given, using limit imposed by data.') fhigh = r.max() work = psd.copy() work[r < flow] = 0 work[r > fhigh] = 0 first = m.trapz(work, uy, axis=0) second = m.trapz(first, ux, axis=0) return m.sqrt(second)
def test_cart_to_polar(x, y): rho, phi = coordinates.cart_to_polar(x, y) assert np.allclose(rho, sqrt(x**2 + y**2)) assert np.allclose(phi, arctan2(y, x))
def primary_septafoil_y(rho, phi): """Zernike primary septafoil.""" return 4 * rho**7 * m.cos(7 * phi) def primary_septafoil_x(rho, phi): """Zernike primary septafoil.""" return 4 * rho**7 * m.sin(7 * phi) # norms piston.norm = 1 tip.norm = 2 tilt.norm = 2 defocus.norm = m.sqrt(3) primary_astigmatism_00.norm = m.sqrt(6) primary_astigmatism_45.norm = m.sqrt(6) primary_coma_y.norm = 2 * m.sqrt(2) primary_coma_x.norm = 2 * m.sqrt(2) primary_spherical.norm = m.sqrt(5) primary_trefoil_y.norm = 2 * m.sqrt(2) primary_trefoil_x.norm = 2 * m.sqrt(2) secondary_astigmatism_00.norm = m.sqrt(10) secondary_astigmatism_45.norm = m.sqrt(10) secondary_coma_y.norm = 2 * m.sqrt(3) secondary_coma_x.norm = 2 * m.sqrt(3) secondary_spherical.norm = m.sqrt(7) primary_tetrafoil_y.norm = m.sqrt(10) primary_tetrafoil_x.norm = m.sqrt(10) secondary_trefoil_y.norm = 2 * m.sqrt(3)
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): """Run a complete simulation generating and retrieving azimuthal order zero terms.
def window_2d_welch(x, y, alpha=8): xx, yy = m.meshgrid(x, y) r, _ = cart_to_polar(xx, yy) rmax = m.sqrt(x.max()**2 + y.max()**2) window = 1 - abs(r / rmax)**alpha return window