def test_circle_correct_area(): x, y = coordinates.make_xy_grid(256, diameter=2) r, _ = coordinates.cart_to_polar(x, y) mask = geometry.circle(1, r) expected_area_of_circle = x.size * 3.14 # sum is integer quantized, binary mask, allow one half quanta of error assert pytest.approx(mask.sum(), expected_area_of_circle, abs=0.5)
def test_rectangle_correct_area(): # really this test should be done for a rectangle that is less than the # entire array x, y = coordinates.make_xy_grid(256, diameter=2) mask = geometry.rectangle(1, x, y) expected = x.size assert mask.sum() == expected
def test_qcon_zzprime_q2d(): # decent number of points, so that finite diff isn't awful x, y = coordinates.make_xy_grid(512, diameter=2) r, t = coordinates.cart_to_polar(x, y) coefs_c = np.random.rand(5) coefs_a = np.random.rand(4, 4) coefs_b = np.random.rand(4, 4) z, zprimer, zprimet = polynomials.qpoly.compute_z_zprime_Q2d( coefs_c, coefs_a, coefs_b, r, t) delta = x[0, 1] - x[0, 0] ddy, ddx = np.gradient(z, delta) dx, dy = surface_normal_from_cylindrical_derivatives( zprimer, zprimet, r, t) dx = fix_zero_singularity(dx, x, y) dy = fix_zero_singularity(dy, x, y) # apply this mask, otherwise the very large gradients outside the unit disk # make things look terrible. # even at 512x512, the relative error is very large at the edge of the unit # circle, hence the enormous rtol that works out to about 25% mask = r < 1 dx *= mask dy *= mask ddx *= mask ddy *= mask assert np.allclose(dx, ddx, atol=1) assert np.allclose(dy, ddy, atol=1)
def test_thinlens_hopkins_agree(): # F/10 beam x, y = coordinates.make_xy_grid(128, diameter=10) dx = x[0, 1] - x[0, 0] r = np.hypot(x, y) amp = geometry.circle(5, r) phs = polynomials.hopkins(0, 2, 0, r / 5, 0, 1) * ( 1.975347661 * HeNe * 1000) # 1000, nm to um wf = propagation.Wavefront.from_amp_and_phase(amp, phs, HeNe, dx) # easy case is to choose thin lens efl = 10,000 # which will result in an overall focal length of 99.0 mm # solve defocus delta z relation, then 1000 = 8 * .6328 * 100 * x # x = 1000 / 8 / .6328 / 100 # = 1.975347661 psf = wf.focus(efl=100, Q=2).intensity no_phs_wf = propagation.Wavefront.from_amp_and_phase(amp, None, HeNe, dx) # bea tl = propagation.Wavefront.thin_lens(10_000, HeNe, x, y) wf = no_phs_wf * tl psf2 = wf.focus(efl=100, Q=2).intensity # lo and behold all ye who read this test, the lies of physical optics modeling # did the beam propagate 100, or 99 millimeters? # is the PSF we're looking at in the z=100 plane, or the z=99 plane? # the answer is simply a matter of interpretation, # if the phase screen for the thin lens is in your mind as a way of going # to z=99, then we are in the z=99 plane. # if the lens is really there, we are in the z=100 plane. assert np.allclose(psf.data, psf2.data, rtol=1e-5)
def test_rotated_ellipse(maj, min, majang): x, y = coordinates.make_xy_grid(32, diameter=2) assert type( geometry.rotated_ellipse(x=x, y=y, width_major=maj, width_minor=min, major_axis_angle=majang)) is np.ndarray
def test_truecircle_correct_area(): # this test is identical to the test for circle. The tested accuracy is # 10x finer since this mask shader is not integer quantized x, y = coordinates.make_xy_grid(256, diameter=2) r, _ = coordinates.cart_to_polar(x, y) mask = geometry.truecircle(1, r) expected_area_of_circle = x.size * 3.14 # sum is integer quantized, binary mask, allow one half quanta of error assert pytest.approx(mask.sum(), expected_area_of_circle, abs=0.05)
def test_segmented_hex_functions(): x, y = coordinates.make_xy_grid(256, diameter=2) csa = segmented.CompositeHexagonalAperture(x, y, 2, 0.2, .007, exclude=(0, )) nms = [polynomials.noll_to_nm(j) for j in [1, 2, 3]] csa.prepare_opd_bases(polynomials.zernike_nm_sequence, nms) csa.compose_opd(np.random.rand(len(csa.segment_ids), len(nms))) assert csa
def test_diffprop_matches_airydisk(efl, epd, wvl): fno = efl / epd x, y = make_xy_grid(128, diameter=epd) r, t = cart_to_polar(x, y) amp = circle(epd/2, r) wf = Wavefront.from_amp_and_phase(amp/amp.sum(), None, wvl, x[0, 1] - x[0, 0]) psf = wf.focus(efl, Q=3) s = psf.intensity.slices() u_, sx = s.x u_, sy = s.y analytic = airydisk(u_, fno, wvl) assert np.allclose(sx, analytic, atol=PRECISION) assert np.allclose(sy, analytic, atol=PRECISION)
def test_diffprop_matches_analyticmtf(efl, epd, wvl): fno = efl / epd x, y = make_xy_grid(128, diameter=epd) r, t = cart_to_polar(x, y) amp = circle(epd/2, r) wf = Wavefront.from_amp_and_phase(amp, None, wvl, x[0, 1] - x[0, 0]) psf = wf.focus(efl, Q=3).intensity mtf = mtf_from_psf(psf.data, psf.dx) s = mtf.slices() u_, sx = s.x u_, sy = s.y analytic_1 = diffraction_limited_mtf(fno, wvl, frequencies=u_) analytic_2 = diffraction_limited_mtf(fno, wvl, frequencies=u_) assert np.allclose(analytic_1, sx, atol=PRECISION) assert np.allclose(analytic_2, sy, atol=PRECISION)
def test_array_orientation_consistency_tilt(): """The pupil array should be shaped as arr[y,x], as should the psf and MTF. A linear phase error in the pupil along y should cause a motion of the PSF in y. Specifically, for a positive-signed phase, that should cause a shift in the +y direction. """ N = 128 wvl = .5 Q = 3 x, y = make_xy_grid(N, diameter=2.1) r, t = cart_to_polar(x, y) amp = circle(1, r) wf = Wavefront.from_amp_and_phase(amp, None, wvl, x[0, 1] - x[0, 0]) psf = wf.focus(1, Q=Q).intensity idx_y, idx_x = np.unravel_index(psf.data.argmax(), psf.data.shape) # row-major y, x assert idx_x == (N*Q) // 2 assert idx_y > N // 2
def test_sum_and_lstsq(): x, y = make_xy_grid(100, diameter=2) ns = [0, 1, 2, 3, 4, 5] ms = [1, 2, 3, 4, 5, 6, 7] weights_x = np.random.rand(len(ns)) weights_y = np.random.rand(len(ms)) # "fun" thing, mix first and second kind chebyshev polynomials mx, my = polynomials.separable_2d_sequence(ns, ms, x, y, polynomials.cheby1_sequence, polynomials.cheby2_sequence) data = polynomials.sum_of_xy_modes(mx, my, x, y, weights_x, weights_y) mx = [polynomials.mode_1d_to_2d(m, x, y, 'x') for m in mx] my = [polynomials.mode_1d_to_2d(m, x, y, 'y') for m in my] modes = mx + my # concat exp = list(weights_x) + list(weights_y) # concat coefs = polynomials.lstsq(modes, data) assert np.allclose(coefs, exp)
def test_generate_spider_doesnt_error(vanes): x, y = coordinates.make_xy_grid(32, diameter=2) mask = geometry.spider(vanes, 1, x, y) assert isinstance(mask, np.ndarray)
"""Tests for detector modeling capabilities.""" import pytest import numpy as np from prysm import detector, coordinates import matplotlib as mpl mpl.use('Agg') SAMPLES = 128 x, y = coordinates.make_xy_grid(SAMPLES, dx=1) r, t = coordinates.cart_to_polar(x, y) def test_pixel_shades_properly(): px = detector.pixel(x, y, 10, 10) # 121 samples should be white, 5 row/col on each side of zero, plus zero, # = 11x11 = 121 assert px.sum() == 121 def test_analytic_fts_function(): # these numbers have no meaning, and the sense of x and y is wrong. Just # testing for crashes. # TODO: more thorough tests olpf_ft = detector.olpf_ft(x, y, 1.234, 4.567) assert olpf_ft.any() pixel_ft = detector.pixel_ft(x, y, 9.876, 5.4321) assert pixel_ft.any()
def __init__(self, ifn, Nact=50, sep=10, shift=(0, 0), rot=(0, 0, 0), upsample=1, spline_order=3, mask=None): """Create a new DM model. This model is based on convolution of a 'poke lattice' with the influence function. It has the following idiosyncracies: 1. The poke lattice is always "FFT centered" on the array, i.e. centered on the sample which would contain the DC frequency bin after an FFT. 2. The rotation is applied in the same sampling as ifn 3. Shifts and resizing are applied using a Fourier method and not subject to quantization Parameters ---------- ifn : numpy.ndarray influence function; assumes the same for all actuators and must be the same shape as (x,y). Assumed centered on N//2th sample of x, y. Assumed to be well-conditioned for use in convolution, i.e. compact compared to the array holding it Nact : int or tuple of int, length 2 (X, Y) actuator counts sep : int or tuple of int, length 2 (X, Y) actuator separation, samples of influence function shift : tuple of float, length 2 (X, Y) shift of the actuator grid to (x, y), units of x influence function sampling. E.g., influence function on 0.1 mm grid, shift=1 = 0.1 mm shift. Positive numbers describe (rightward, downward) shifts in image coordinates (origin lower left). rot : tuple of int, length <= 3 (Z, Y, X) rotations; see coordinates.make_rotation_matrix upsample : float upsampling factor used in determining output resolution, if it is different to the resolution of ifn. mask : numpy.ndarray boolean ndarray of shape Nact used to suppress/delete/exclude actuators; 1=keep, 0=suppress """ if isinstance(Nact, int): Nact = (Nact, Nact) if isinstance(sep, int): sep = (sep, sep) x, y = make_xy_grid(ifn.shape, dx=1) # stash inputs and some computed values on self self.ifn = ifn self.Ifn = fft.fft2(ifn) self.Nact = Nact self.sep = sep self.shift = shift self.obliquity = truenp.cos(truenp.radians(truenp.linalg.norm(rot))) self.rot = rot self.upsample = upsample # prepare the poke array and supplimentary integer arrays needed to # copy it into the working array out = prepare_actuator_lattice(ifn.shape, Nact, sep, mask, dtype=x.dtype) self.mask = out['mask'] self.actuators = out['actuators'] self.actuators_work = np.zeros_like(self.actuators) self.poke_arr = out['poke_arr'] self.ixx = out['ixx'] self.iyy = out['iyy'] # rotation data self.rotmat = make_rotation_matrix(rot) XY = apply_rotation_matrix(self.rotmat, x, y) XY2 = xyXY_to_pixels((x, y), XY) self.XY = XY self.XY2 = XY2 self.needs_rot = True if np.allclose(rot, [0, 0, 0]): self.needs_rot = False # shift data if shift[0] != 0 or shift[1] != 0: # caps = Fourier variable (x -> X, y -> Y) # make 2pi/px phase ramps in 1D (much faster) # then broadcast them to 2D when they're used as transfer functions # in a Fourier convolution Y, X = [forward_ft_unit(1, s, shift=False) for s in x.shape] Xramp = np.exp(X * (-2j * np.pi * shift[0])) Yramp = np.exp(Y * (-2j * np.pi * shift[1])) shpx = x.shape shpy = tuple(reversed(x.shape)) Xramp = np.broadcast_to(Xramp, shpx) Yramp = np.broadcast_to(Yramp, shpy).T self.Xramp = Xramp self.Yramp = Yramp self.tf = [self.Ifn * self.Xramp * self.Yramp] else: self.tf = [self.Ifn]
def test_rectangle_doesnt_break_angle(): x, y = coordinates.make_xy_grid(16, diameter=2) mask = geometry.rectangle(1, x, y, angle=45) assert mask.any()
def test_offset_circle(): # [-16, 15] grid x, y = coordinates.make_xy_grid(32, dx=1) c = geometry.offset_circle(3, x, y, center=(2, 2)) s = c.sum() assert s == 29 # 29 = roundup of 3^2 * pi
def test_regular_polygon(sides, samples): x, y = coordinates.make_xy_grid(samples, diameter=2) mask = geometry.regular_polygon(sides, 1, x, y) assert isinstance(mask, np.ndarray) assert mask.shape == (samples, samples)
# Need Q_forward >= 2 for forward model, so Q_forward = Q*(oversampling) = 2 oversampling = ceil(2 / Q) Q_forward = round(Q * oversampling, 1) # Intermediate higher res gives psize = pixel_pitch/oversampling psize = pixel_pitch / oversampling # PSF_domain_res will be output_res*oversampling # Pupil domain samples will be (PSF_domain_res)/Q_forward samples = ceil(output_res * oversampling / Q_forward) # Find pupil dx from wanted psize pup_dx = psf_sample_to_pupil_sample(psize, samples, wlen, f) # Construct pupil grid, convert to polar, construct normalized r for phase xi, eta = make_xy_grid(samples, dx=pup_dx) r, theta = cart_to_polar(xi, eta) norm_r = r / lens_R # Construct amplitude function of pupil function amp = circle(lens_R, r) amp = amp / amp.sum() # Construct phase mode aber = zernike_nm(4, 0, norm_r, theta) # spherical aberration # Scale phase mode to desired opd phase = aber * wlen / 16 * 1e3 # Construct pupil function from amp and phase functions, propagate to PSF plane, take square modulus. P = Wavefront.from_amp_and_phase(amp, phase, wlen, pup_dx) coherent_PSF = P.focus(f, Q=Q_forward)
def test_gaussian(sigma, samples): x, y = coordinates.make_xy_grid(samples, diameter=2) assert type(geometry.gaussian(sigma, x, y)) is np.ndarray