def test_compound_osys_errors(): """ Test that it rejects incompatible inputs""" import poppy inputs_and_errors = (( None, "Missing required optsyslist argument" ), ([], "The provided optsyslist argument is an empty list"), ([ poppy.CircularAperture() ], "All items in the optical system list must be OpticalSystem instances" )) for test_input, expected_error in inputs_and_errors: with pytest.raises(ValueError) as excinfo: poppy.CompoundOpticalSystem(test_input) assert _exception_message_starts_with(excinfo, expected_error) osys = poppy.OpticalSystem() osys.add_pupil(poppy.CircularAperture()) cosys = poppy.CompoundOpticalSystem([osys]) with pytest.raises(RuntimeError) as excinfo: cosys.add_pupil(poppy.SquareAperture()) assert _exception_message_starts_with( excinfo, "Adding individual optical elements is disallowed")
def test_CompoundOpticalSystem(): """ Verify basic functionality of concatenating optical systems """ opt1 = poppy.SquareAperture() opt2 = poppy.CircularAperture(radius=0.55) # a single optical system osys = poppy.OpticalSystem() osys.add_pupil(opt1) osys.add_pupil(opt2) osys.add_detector(pixelscale=0.1, fov_pixels=128) # two systems, joined into a CompoundOpticalSystem osys1 = poppy.OpticalSystem() osys1.add_pupil(opt1) osys2 = poppy.OpticalSystem() osys2.add_pupil(opt2) osys2.add_detector(pixelscale=0.1, fov_pixels=128) cosys = poppy.CompoundOpticalSystem([osys1, osys2]) # PSF calculations psf_simple = osys.calc_psf() psf_compound = cosys.calc_psf() np.testing.assert_allclose( psf_simple[0].data, psf_compound[0].data, err_msg= "PSFs do not match between equivalent simple and compound optical systems" ) # check the planes assert len(cosys.planes) == len(osys1.planes) + len(osys2.planes)
def test_CompoundOpticalSystem_fresnel(npix=128, display=False): """ Test that the CompoundOpticalSystem container works for Fresnel systems Parameters ---------- npix : int Number of pixels for the pupil sampling. Kept small by default to reduce test run time. """ import poppy opt1 = poppy.SquareAperture() opt2 = poppy.CircularAperture(radius=0.55) # a single optical system osys = poppy.FresnelOpticalSystem(beam_ratio=0.25, npix=npix) osys.add_optic(opt1) osys.add_optic(opt2, distance=10 * u.cm) osys.add_optic(poppy.QuadraticLens(1.0 * u.m)) osys.add_optic(poppy.Detector(pixelscale=0.25 * u.micron / u.pixel, fov_pixels=512), distance=1 * u.m) psf = osys.calc_psf(display_intermediates=display) if display: plt.figure() # a Compound Fresnel optical system osys1 = poppy.FresnelOpticalSystem(beam_ratio=0.25, npix=npix) osys1.add_optic(opt1) osys2 = poppy.FresnelOpticalSystem(beam_ratio=0.25) osys2.add_optic(opt2, distance=10 * u.cm) osys2.add_optic(poppy.QuadraticLens(1.0 * u.m)) osys2.add_optic(poppy.Detector(pixelscale=0.25 * u.micron / u.pixel, fov_pixels=512), distance=1 * u.m) cosys = poppy.CompoundOpticalSystem([osys1, osys2]) psf2 = cosys.calc_psf(display_intermediates=display) assert np.allclose( psf[0].data, psf2[0].data ), "Results from simple and compound Fresnel systems differ unexpectedly." return psf, psf2
def __init__(self, lenslet_pitch=300 * u.um, lenslet_fl=14.2 * u.mm, pixel_pitch=2.2 * u.um, n_lenslets=12, circular=False, detector=None, **kwargs): self.lenslet_pitch = lenslet_pitch self.lenslet_fl = lenslet_fl self.pixel_pitch = pixel_pitch self.r_lenslet = self.lenslet_pitch / 2. self.n_lenslets = n_lenslets if circular: aperture = poppy.CircularAperture(radius=self.lenslet_pitch / 2, planetype=PlaneType.pupil) else: ap_keywords = { "size": self.lenslet_pitch, "planetype": PlaneType.pupil } aperture = poppy.SquareAperture(size=self.lenslet_pitch, planetype=PlaneType.pupil) optic_array = np.array([[aperture, aperture], [aperture, aperture]]) if detector is None: pixelscale = 1.0 * u.rad / (lenslet_fl * u.pix / pixel_pitch) pix_per_lenslet = int(lenslet_pitch / pixel_pitch) detector = Detector(pixelscale, fov_pixels=pix_per_lenslet) # expand the array to make big_optic_array if n_lenslets % 2 != 0: raise ValueError( "aperture replication only works for even numbers of apertures" ) big_optic_array = optic_array.repeat(n_lenslets / 2., axis=0).repeat(n_lenslets / 2., axis=1) Subapertures.__init__(self, optic_array=big_optic_array, detector=detector, **kwargs) return
def test_radial_profile(plot=False): """ Test radial profile calculation, including circular and square apertures, and including with the pa_range option. """ ### Tests on a circular aperture o = poppy_core.OpticalSystem() o.add_pupil(poppy.CircularAperture(radius=1.0)) o.add_detector(0.010, fov_pixels=512) psf = o.calc_psf() rad, prof = poppy.radial_profile(psf) rad2, prof2 = poppy.radial_profile(psf, pa_range=[-20, 20]) rad3, prof3 = poppy.radial_profile(psf, pa_range=[-20 + 90, 20 + 90]) # Compute analytical Airy function, on exact same radial sampling as that profile. v = np.pi * rad * poppy.misc._ARCSECtoRAD * 2.0 / 1e-06 airy = ((2 * scipy.special.jn(1, v)) / v)**2 r0 = 33 # 0.33 arcsec ~ first airy ring in this case. airy_peak_envelope = airy[r0] * prof.max() / (rad / rad[r0])**3 absdiff = np.abs(prof - airy * prof.max()) if plot: import matplotlib.pyplot as plt plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) poppy.display_psf(psf, colorbar_orientation='horizontal', title='Circular Aperture, d=2 m') plt.subplot(1, 2, 2) plt.semilogy(rad, prof) plt.semilogy(rad2, prof2, ls='--', color='red') plt.semilogy(rad3, prof3, ls=':', color='cyan') plt.semilogy(rad, airy_peak_envelope, color='gray') plt.semilogy(rad, airy_peak_envelope / 50, color='gray', alpha=0.5) plt.semilogy(rad, absdiff, color='purple') # Test the radial profile is close to the analytical Airy function. # It's hard to define relative fractional closeness for comparisons to # a function with many zero crossings; we can't just take (f1-f2)/(f1+f2) # This is a bit of a hack but let's test that the difference between # numerical and analytical is always less than 1/50th of the peaks of the # Airy function (fit based on the 1/r^3 power law fall off) assert np.all(absdiff[0:300] < airy_peak_envelope[0:300] / 50) # Test that the partial radial profiles agree with the full one. This test is # a little tricky since the sampling in r may not agree exactly. # TODO write test comparison here # Let's also test that the partial radial profiles on 90 degrees agree with each other. # These should match to machine precision. assert np.allclose(prof2, prof3) ### Next test is on a square aperture o = poppy.OpticalSystem() o.add_pupil(poppy.SquareAperture()) o.add_detector(0.010, fov_pixels=512) psf = o.calc_psf() rad, prof = poppy.radial_profile(psf) rad2, prof2 = poppy.radial_profile(psf, pa_range=[-20, 20]) rad3, prof3 = poppy.radial_profile(psf, pa_range=[-20 + 90, 20 + 90]) if plot: plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) poppy.display_psf(psf, colorbar_orientation='horizontal', title='Square Aperture, size=1 m') plt.subplot(1, 2, 2) plt.semilogy(rad, prof) plt.semilogy(rad2, prof2, ls='--', color='red') plt.semilogy(rad3, prof3, ls=':', color='cyan') assert np.allclose(prof2, prof3)
def test_CompoundOpticalSystem_hybrid(npix=128): """ Test that the CompoundOpticalSystem container works for hybrid Fresnel+Fraunhofer systems Defining "works correctly" here is a bit arbitrary given the different physical assumptions. For the purpose of this test we consider a VERY simple case, mostly a Fresnel system. We split out the first optic and put that in a Fraunhofer system. We then test that a compound hybrid system yields the same results as the original fully-Fresnel system. Parameters ---------- npix : int Number of pixels for the pupil sampling. Kept small by default to reduce test run time. """ import poppy opt1 = poppy.SquareAperture() opt2 = poppy.CircularAperture(radius=0.55) ###### Simple test case to exercise the conversion functions, with only trivial propagation osys1 = poppy.OpticalSystem() osys1.add_pupil(opt1) osys2 = poppy.FresnelOpticalSystem() osys2.add_optic(poppy.ScalarTransmission()) osys3 = poppy.OpticalSystem() osys3.add_pupil(poppy.ScalarTransmission()) osys3.add_detector(fov_pixels=64, pixelscale=0.01) cosys = poppy.CompoundOpticalSystem([osys1, osys2, osys3]) psf, ints = cosys.calc_psf(return_intermediates=True) assert len(ints) == 4, "Unexpected number of intermediate wavefronts" assert isinstance(ints[0], poppy.Wavefront), "Unexpected output type" assert isinstance(ints[1], poppy.FresnelWavefront), "Unexpected output type" assert isinstance(ints[2], poppy.Wavefront), "Unexpected output type" ###### Harder case involving more complex actual propagations #===== a single Fresnel optical system ===== osys = poppy.FresnelOpticalSystem(beam_ratio=0.25, npix=128, pupil_diameter=2 * u.m) osys.add_optic(opt1) osys.add_optic(opt2, distance=10 * u.cm) osys.add_optic(poppy.QuadraticLens(1.0 * u.m)) osys.add_optic(poppy.Detector(pixelscale=0.125 * u.micron / u.pixel, fov_pixels=512), distance=1 * u.m) #===== two systems, joined into a CompoundOpticalSystem ===== # First part is Fraunhofer then second is Fresnel osys1 = poppy.OpticalSystem(npix=128, oversample=4, name="FIRST PART, FRAUNHOFER") # Note for strict consistency we need to apply a half pixel shift to optics in the Fraunhofer part; # this accomodates the differences between different types of image centering. pixscl = osys.input_wavefront().pixelscale halfpixshift = (pixscl * 0.5 * u.pixel).to(u.m).value opt1shifted = poppy.SquareAperture(shift_x=halfpixshift, shift_y=halfpixshift) osys1.add_pupil(opt1shifted) osys2 = poppy.FresnelOpticalSystem(name='SECOND PART, FRESNEL') osys2.add_optic(opt2, distance=10 * u.cm) osys2.add_optic(poppy.QuadraticLens(1.0 * u.m)) osys2.add_optic(poppy.Detector(pixelscale=0.125 * u.micron / u.pixel, fov_pixels=512), distance=1 * u.m) cosys = poppy.CompoundOpticalSystem([osys1, osys2]) #===== PSF calculations ===== psf_simple = osys.calc_psf(return_intermediates=False) poppy.poppy_core._log.info( "******=========calculation divider============******") psf_compound = cosys.calc_psf(return_intermediates=False) np.testing.assert_allclose( psf_simple[0].data, psf_compound[0].data, err_msg= "PSFs do not match between equivalent simple and compound/hybrid optical systems" )
def make_coronagraph(wfe_coeffs,npix_pupil=512,npix_detector=128,wavelength=1e-6*u.m,oversample=4,pixelscale=0.01,sensor_defocus=0.5,vortex_charge=2,llowfs=False,mask_type='fqpm',obscuration=False,lyot_factor=0.9): #sensor_defocus: defocus of llowfs detector in waves peak-to-valley #these values are picked rather arbitrarily, but seem to work aperture_radius = 3*u.m #lyot_radius=2.6*u.m lyot_radius=lyot_factor*aperture_radius pupil_radius = 3*aperture_radius #create the optical system osys = poppy.OpticalSystem("LLOWFS", oversample=oversample, npix=npix_pupil, pupil_diameter=2*pupil_radius) ap = poppy.CircularAperture(radius=aperture_radius) if obscuration: obsc = poppy.SecondaryObscuration(secondary_radius=0.5*u.m, n_supports=0) osys.add_pupil(poppy.CompoundAnalyticOptic(opticslist=[ap,obsc])) #osys.add_pupil(poppy.AsymmetricSecondaryObscuration(secondary_radius=0.5,support_width=0.2*u.meter,support_angle=(0,120,240))) else: osys.add_pupil(ap) error = poppy.ZernikeWFE(radius=aperture_radius, coefficients=wfe_coeffs) osys.add_pupil(error) #inject wavefrotn error at the pupil #correct for fqpm (and vvc) alignment osys.add_pupil(poppy.FQPM_FFT_aligner()) osys.add_image() #helper for plotting intermediates #select mask type if mask_type is 'fqpm': cgph_mask = poppy.IdealFQPM(wavelength=wavelength,name='FQPM Mask') elif mask_type is 'vortex': cgph_mask = VortexMask(charge=vortex_charge,wavelength=wavelength,name='Vortex Mask') else: raise ValueError("mask_type must be 'fqpm' or 'vortex'") cgph_mask._wavefront_display_hint='phase' osys.add_image(cgph_mask) #correct alignment back the other way osys.add_pupil(poppy.FQPM_FFT_aligner(direction='backward')) #osys.add_pupil() lyot = poppy.CircularAperture(radius=lyot_radius,name='Lyot Stop') lyot._wavefront_display_hint='intensity' if obscuration: lyot_obsc = poppy.InverseTransmission(poppy.SquareAperture(size=1.4*u.m)) lyot = poppy.CompoundAnalyticOptic(opticslist=[lyot,lyot_obsc]) if llowfs: #take the rejected light for the LLOWFS lyot_reflection = poppy.InverseTransmission(lyot) lyot_extent = poppy.CircularAperture(radius=pupil_radius) lyot = poppy.CompoundAnalyticOptic(opticslist = [lyot_reflection,lyot_extent]) lyot._wavefront_display_hint='intensity' osys.add_pupil(lyot) #if obscuration: # obsc = poppy.InverseTransmission(obsc) # osys.add_pupil(obsc) #Add a defocus term to the sensor #Calc of peak-to-valley WFE: https://poppy-optics.readthedocs.io/en/stable/wfe.html defocus_coeff = sensor_defocus*wavelength.to(u.m).value sensor_defocus_wfe = poppy.ZernikeWFE(radius=pupil_radius,coefficients=[0,0,0,defocus_coeff]) osys.add_pupil(sensor_defocus_wfe) #osys.add_pupil() osys.add_detector(pixelscale=pixelscale, fov_pixels=npix_detector, oversample=1) else: #add lyot stop osys.add_pupil(lyot) osys.add_detector(pixelscale=pixelscale, fov_arcsec=1) return osys