Ejemplo n.º 1
0
def test_opt_indiv_aberrations():
    """Test that aberrations specified by name match those specified in `aberrations` list."""
    screen1 = galsim.OpticalScreen(diam=4.0,
                                   tip=0.2,
                                   tilt=0.3,
                                   defocus=0.4,
                                   astig1=0.5,
                                   astig2=0.6,
                                   coma1=0.7,
                                   coma2=0.8,
                                   trefoil1=0.9,
                                   trefoil2=1.0,
                                   spher=1.1)
    screen2 = galsim.OpticalScreen(diam=4.0,
                                   aberrations=[
                                       0.0, 0.0, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7,
                                       0.8, 0.9, 1.0, 1.1
                                   ])

    psf1 = galsim.PhaseScreenList(screen1).makePSF(diam=4.0, lam=500.0)
    psf2 = galsim.PhaseScreenList(screen2).makePSF(diam=4.0, lam=500.0)

    np.testing.assert_array_equal(
        psf1.img, psf2.img,
        "Individually specified aberrations differs from aberrations specified as list."
    )
Ejemplo n.º 2
0
def test_gc():
    """Make sure that pending psfs don't leak memory.
    """
    import gc
    atm = galsim.Atmosphere(screen_size=10.0, altitude=0, r0_500=0.15, suppress_warning=True)

    # First check that no PhaseScreenPSFs are known to the garbage collector
    assert not any([isinstance(it, galsim.phase_psf.PhaseScreenPSF) for it in gc.get_objects()])

    # Make a PhaseScreenPSF and check that it's known to the garbage collector
    psf = atm.makePSF(exptime=0.02, time_step=0.01, diam=1.1, lam=1000.0)
    assert any([isinstance(it, galsim.phase_psf.PhaseScreenPSF) for it in gc.get_objects()])

    # If we delete it, it disappears everywhere
    del psf
    gc.collect()
    assert not any([isinstance(it, galsim.phase_psf.PhaseScreenPSF) for it in gc.get_objects()])

    # If we draw one using photon-shooting, it still exists in _pending
    psf = atm.makePSF(exptime=0.02, time_step=0.01, diam=1.1, lam=1000.0)
    psf.drawImage(nx=10, ny=10, scale=0.2, method='phot', n_photons=100)
    assert psf in [p[1]() for p in atm._pending]

    # If we draw even one of many using fft, _pending gets completely emptied
    psf2 = atm.makePSF(exptime=0.02, time_step=0.01, diam=1.1, lam=1000.0)
    psf.drawImage(nx=10, ny=10, scale=0.2)
    assert atm._pending == []

    # And if then deleted, they again don't exist anywhere
    del psf, psf2
    gc.collect()
    assert not any([isinstance(it, galsim.phase_psf.PhaseScreenPSF) for it in gc.get_objects()])

    # A corner case revealed in coverage tests:
    # Make sure that everything still works if some, but not all static pending PSFs are deleted.
    screen = galsim.OpticalScreen(diam=1.1)
    phaseScreenList = galsim.PhaseScreenList(screen)
    psf1 = phaseScreenList.makePSF(lam=1000.0, diam=1.1)
    psf2 = phaseScreenList.makePSF(lam=1000.0, diam=1.1)
    psf3 = phaseScreenList.makePSF(lam=1000.0, diam=1.1)
    del psf2
    psf1.drawImage(nx=10, ny=10, scale=0.2)
    del psf1, psf3
    assert phaseScreenList._pending == []
    gc.collect()
    assert not any([isinstance(it, galsim.phase_psf.PhaseScreenPSF) for it in gc.get_objects()])
Ejemplo n.º 3
0
    def _build_optics(self):
        # from galsim examples/great3/cgc.yaml
        rms_aberration = 0.26
        names = [
            "defocus", "astig1", "astig2", "coma1", "coma2", "trefoil1",
            "trefoil2", "spher"
        ]
        weights = np.array([0.13, 0.13, 0.14, 0.06, 0.06, 0.05, 0.06, 0.03])
        weights /= np.sqrt(np.sum(weights**2))
        weights *= rms_aberration
        kwargs = {k: a * self.rng.normal() for k, a in zip(names, weights)}

        opt = galsim.OpticalScreen(lam_0=self.lam,
                                   diam=self.diam,
                                   obscuration=self.obscuration,
                                   **kwargs)

        # order them so I know where things are for later...
        _screens = galsim.PhaseScreenList()
        _screens.append(opt)
        for i in range(len(self._screens)):
            _screens.append(self._screens[i])
        self._screens = _screens
Ejemplo n.º 4
0
def test_ne():
    """Test Apertures, PhaseScreens, PhaseScreenLists, and PhaseScreenPSFs for not-equals."""
    pupil_plane_im = galsim.fits.read(os.path.join(imgdir, pp_file))

    # Test galsim.Aperture __ne__
    objs = [galsim.Aperture(diam=1.0),
            galsim.Aperture(diam=1.1),
            galsim.Aperture(diam=1.0, oversampling=1.5),
            galsim.Aperture(diam=1.0, pad_factor=1.5),
            galsim.Aperture(diam=1.0, circular_pupil=False),
            galsim.Aperture(diam=1.0, obscuration=0.3),
            galsim.Aperture(diam=1.0, nstruts=3),
            galsim.Aperture(diam=1.0, nstruts=3, strut_thick=0.2),
            galsim.Aperture(diam=1.0, nstruts=3, strut_angle=15*galsim.degrees),
            galsim.Aperture(diam=1.0, pupil_plane_im=pupil_plane_im),
            galsim.Aperture(diam=1.0, pupil_plane_im=pupil_plane_im,
                            pupil_angle=10.0*galsim.degrees)]
    all_obj_diff(objs)

    # Test AtmosphericScreen __ne__
    rng = galsim.BaseDeviate(1)
    objs = [galsim.AtmosphericScreen(10.0, rng=rng),
            galsim.AtmosphericScreen(1.0, rng=rng),
            galsim.AtmosphericScreen(10.0, rng=rng, vx=1.0),
            galsim.AtmosphericScreen(10.0, rng=rng, vy=1.0),
            galsim.AtmosphericScreen(10.0, rng=rng, alpha=0.999, time_step=0.01),
            galsim.AtmosphericScreen(10.0, rng=rng, altitude=1.0),
            galsim.AtmosphericScreen(10.0, rng=rng, alpha=0.999, time_step=0.02),
            galsim.AtmosphericScreen(10.0, rng=rng, alpha=0.998, time_step=0.02),
            galsim.AtmosphericScreen(10.0, rng=rng, r0_500=0.1),
            galsim.AtmosphericScreen(10.0, rng=rng, L0=10.0),
            galsim.AtmosphericScreen(10.0, rng=rng, vx=10.0),
            ]
    all_obj_diff(objs)
    objs.append(galsim.AtmosphericScreen(10.0, rng=rng))
    objs[-1].instantiate()
    # Should still all be __ne__, but first and last will have the same hash this time.
    assert hash(objs[0]) == hash(objs[-1])
    all_obj_diff(objs, check_hash=False)

    # Test OpticalScreen __ne__
    objs = [galsim.OpticalScreen(diam=1.0),
            galsim.OpticalScreen(diam=1.0, tip=1.0),
            galsim.OpticalScreen(diam=1.0, tilt=1.0),
            galsim.OpticalScreen(diam=1.0, defocus=1.0),
            galsim.OpticalScreen(diam=1.0, astig1=1.0),
            galsim.OpticalScreen(diam=1.0, astig2=1.0),
            galsim.OpticalScreen(diam=1.0, coma1=1.0),
            galsim.OpticalScreen(diam=1.0, coma2=1.0),
            galsim.OpticalScreen(diam=1.0, trefoil1=1.0),
            galsim.OpticalScreen(diam=1.0, trefoil2=1.0),
            galsim.OpticalScreen(diam=1.0, spher=1.0),
            galsim.OpticalScreen(diam=1.0, spher=1.0, lam_0=100.0),
            galsim.OpticalScreen(diam=1.0, aberrations=[0,0,1.1]), # tip=1.1
            ]
    all_obj_diff(objs)

    # Test PhaseScreenList __ne__
    atm = galsim.Atmosphere(10.0, vx=1.0)
    objs = [galsim.PhaseScreenList(atm),
            galsim.PhaseScreenList(objs),  # Reuse list of OpticalScreens above
            galsim.PhaseScreenList(objs[0:2])]
    all_obj_diff(objs)

    # Test PhaseScreenPSF __ne__
    psl = galsim.PhaseScreenList(atm)
    objs = [galsim.PhaseScreenPSF(psl, 500.0, exptime=0.03, diam=1.0)]
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0)]
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.1)]
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0, flux=1.1)]
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0, interpolant='linear')]
    stepk = objs[0].stepk
    maxk = objs[0].maxk
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0, _force_stepk=stepk/1.5)]
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0, _force_maxk=maxk*2.0)]
    all_obj_diff(objs)
Ejemplo n.º 5
0
def test_phase_screen_list():
    """Test list-like behaviors of PhaseScreenList."""
    rng = galsim.BaseDeviate(1234)
    rng2 = galsim.BaseDeviate(123)

    aper = galsim.Aperture(diam=1.0)

    ar1 = galsim.AtmosphericScreen(10, 1, alpha=0.997, L0=None, time_step=0.01, rng=rng)
    assert ar1._time == 0.0, "AtmosphericScreen initialized with non-zero time."
    do_pickle(ar1)
    do_pickle(ar1, func=lambda x: x.wavefront(aper.u, aper.v, 0.0).sum())
    do_pickle(ar1, func=lambda x: np.sum(x.wavefront_gradient(aper.u, aper.v, 0.0)))
    t = np.empty_like(aper.u)
    ud = galsim.UniformDeviate(rng.duplicate())
    ud.generate(t.ravel())
    t *= 0.1  # Only do a few boiling steps
    do_pickle(ar1, func=lambda x: x.wavefront(aper.u, aper.v, t).sum())
    do_pickle(ar1, func=lambda x: np.sum(x.wavefront_gradient(aper.u, aper.v, t)))

    # Try seeking backwards
    assert ar1._time > 0.0
    ar1._seek(0.0)
    # But not before t=0.0
    with assert_raises(ValueError):
        ar1._seek(-1.0)

    # Check that L0=np.inf and L0=None yield the same thing here too.
    ar2 = galsim.AtmosphericScreen(10, 1, alpha=0.997, L0=np.inf, time_step=0.01, rng=rng)
    # Before ar2 is instantiated, it's unequal to ar1, even though they were initialized with the
    # same arguments (the hashes are the same though).  After both have been instantiated with the
    # same range of k (ar1 through use of .wavefront() and ar2 explicitly), then they are equal (
    # and the hashes are still the same).
    assert hash(ar1) == hash(ar2)
    assert ar1 != ar2
    ar2.instantiate()
    assert ar1 == ar2
    assert hash(ar1) == hash(ar2)
    # Create a couple new screens with different types/parameters
    ar2 = galsim.AtmosphericScreen(10, 1, alpha=0.995, time_step=0.015, rng=rng2)
    ar2.instantiate()
    assert ar1 != ar2
    ar3 = galsim.OpticalScreen(diam=1.0, aberrations=[0, 0, 0, 0, 0, 0, 0, 0, 0.1],
                               obscuration=0.3, annular_zernike=True)
    do_pickle(ar3)
    do_pickle(ar3, func=lambda x:x.wavefront(aper.u, aper.v).sum())
    do_pickle(ar3, func=lambda x:np.sum(x.wavefront_gradient(aper.u, aper.v)))
    atm = galsim.Atmosphere(screen_size=30.0,
                            altitude=[0.0, 1.0],
                            speed=[1.0, 2.0],
                            direction=[0.0*galsim.degrees, 120*galsim.degrees],
                            r0_500=0.15,
                            rng=rng)
    atm.append(ar3)
    do_pickle(atm)
    do_pickle(atm, func=lambda x:x.wavefront(aper.u, aper.v, 0.0, theta0).sum())
    do_pickle(atm, func=lambda x:np.sum(x.wavefront_gradient(aper.u, aper.v, 0.0)))

    # testing append, extend, __getitem__, __setitem__, __delitem__, __eq__, __ne__
    atm2 = atm[:-1]  # Refers to first n-1 screens
    assert atm != atm2
    # Append a different screen to the end of atm2
    atm2.append(ar2)
    assert atm != atm2
    # Swap the last screen in atm2 for the one that should match atm.
    del atm2[-1]
    atm2.append(atm[-1])
    assert atm == atm2

    with assert_raises(TypeError):
        atm['invalid']
    with assert_raises(IndexError):
        atm[3]

    # Test building from empty PhaseScreenList
    atm3 = galsim.PhaseScreenList()
    atm3.extend(atm2)
    assert atm == atm3

    # Test constructing from existing PhaseScreenList
    atm4 = galsim.PhaseScreenList(atm3)
    del atm4[-1]
    assert atm != atm4
    atm4.append(atm[-1])
    assert atm == atm4

    # Test swap
    atm4[0], atm4[1] = atm4[1], atm4[0]
    assert atm != atm4
    atm4[0], atm4[1] = atm4[1], atm4[0]
    assert atm == atm4

    wf = atm.wavefront(aper.u, aper.v, None, theta0)
    wf2 = atm2.wavefront(aper.u, aper.v, None, theta0)
    wf3 = atm3.wavefront(aper.u, aper.v, None, theta0)
    wf4 = atm4.wavefront(aper.u, aper.v, None, theta0)

    np.testing.assert_array_equal(wf, wf2, "PhaseScreenLists are inconsistent")
    np.testing.assert_array_equal(wf, wf3, "PhaseScreenLists are inconsistent")
    np.testing.assert_array_equal(wf, wf4, "PhaseScreenLists are inconsistent")

    # Check copy
    import copy
    # Shallow copy copies by reference.
    atm5 = copy.copy(atm)
    assert atm[0] == atm5[0]
    assert atm[0] is atm5[0]
    atm._seek(1.0)
    assert atm[0]._time == 1.0, "Wrong time for AtmosphericScreen"
    assert atm[0] == atm5[0]
    assert atm[0] is atm5[0]
    # Deepcopy actually makes an indepedent object in memory.
    atm5 = copy.deepcopy(atm)
    assert atm[0] == atm5[0]
    assert atm[0] is not atm5[0]
    atm._seek(2.0)
    assert atm[0]._time == 2.0, "Wrong time for AtmosphericScreen"
    # But we still get equality, since this doesn't depend on mutable internal state:
    assert atm[0] == atm5[0]

    # Constructor should accept both list and indiv layers as arguments.
    atm6 = galsim.PhaseScreenList(atm[0])
    atm7 = galsim.PhaseScreenList([atm[0]])
    assert atm6 == atm7
    do_pickle(atm6, func=lambda x:x.wavefront(aper.u, aper.v, None, theta0).sum())
    do_pickle(atm6, func=lambda x:np.sum(x.wavefront_gradient(aper.u, aper.v, 0.0)))

    atm6 = galsim.PhaseScreenList(atm[0], atm[1])
    atm7 = galsim.PhaseScreenList([atm[0], atm[1]])
    atm8 = galsim.PhaseScreenList(atm[0:2])  # Slice returns PhaseScreenList, so this works too.
    assert atm6 == atm7
    assert atm6 == atm8

    # Check some actual derived PSFs too, not just phase screens.  Use a small pupil_plane_size and
    # relatively large pupil_plane_scale to speed up the unit test.
    atm._reset()
    assert atm[0]._time == 0.0, "Wrong time for AtmosphericScreen"
    kwargs = dict(exptime=0.05, time_step=0.01, diam=1.1, lam=1000.0)
    psf = atm.makePSF(**kwargs)
    do_pickle(psf)
    do_pickle(psf, func=lambda x:x.drawImage(nx=20, ny=20, scale=0.1))

    psf2 = atm2.makePSF(**kwargs)
    psf3 = atm3.makePSF(**kwargs)
    psf4 = atm4.makePSF(**kwargs)

    np.testing.assert_array_equal(psf, psf2, "PhaseScreenPSFs are inconsistent")
    np.testing.assert_array_equal(psf, psf3, "PhaseScreenPSFs are inconsistent")
    np.testing.assert_array_equal(psf, psf4, "PhaseScreenPSFs are inconsistent")

    # Check errors in u,v,t shapes.
    assert_raises(ValueError, ar1.wavefront, aper.u, aper.v[:-1,:-1])
    assert_raises(ValueError, ar1.wavefront, aper.u[:-1,:-1], aper.v)
    assert_raises(ValueError, ar1.wavefront, aper.u, aper.v, 0.1 * aper.u[:-1,:-1])
    assert_raises(ValueError, ar1.wavefront_gradient, aper.u, aper.v[:-1,:-1])
    assert_raises(ValueError, ar1.wavefront_gradient, aper.u[:-1,:-1], aper.v)
    assert_raises(ValueError, ar1.wavefront_gradient, aper.u, aper.v, 0.1 * aper.u[:-1,:-1])

    assert_raises(ValueError, ar3.wavefront, aper.u, aper.v[:-1,:-1])
    assert_raises(ValueError, ar3.wavefront, aper.u[:-1,:-1], aper.v)
    assert_raises(ValueError, ar3.wavefront_gradient, aper.u, aper.v[:-1,:-1])
    assert_raises(ValueError, ar3.wavefront_gradient, aper.u[:-1,:-1], aper.v)
Ejemplo n.º 6
0
def Atmosphere(screen_size, rng=None, **kwargs):
    """Create an atmosphere as a list of turbulent phase screens at different altitudes.  The
    atmosphere model can then be used to simulate atmospheric PSFs.

    Simulating an atmospheric PSF is typically accomplished by first representing the 3-dimensional
    turbulence in the atmosphere as a series of discrete 2-dimensional phase screens.  These screens
    may blow around in the wind, and may or may not also evolve in time.  This function allows one
    to quickly assemble a list of atmospheric phase screens into a galsim.PhaseScreenList object,
    which can then be used to evaluate PSFs through various columns of atmosphere at different field
    angles.

    The atmospheric screens currently available represent turbulence following a von Karman power
    spectrum.  Specifically, the phase power spectrum in each screen can be written

    psi(nu) = 0.023 r0^(-5/3) (nu^2 + 1/L0^2)^(11/6)

    where psi(nu) is the power spectral density at spatial frequency nu, r0 is the Fried parameter
    (which has dimensions of length) and sets the amplitude of the turbulence, and L0 is the outer
    scale (also dimensions of length) beyond which the power asymptotically flattens.  Typical
    values for r0 are ~0.1 to 0.2 meters, which corresponds roughly to PSF FWHMs of ~0.5 to 1.0
    arcsec for optical wavelengths.  Note that r0 is a function of wavelength, scaling like
    r0 ~ wavelength^(6/5).  To reduce confusion, the input parameter here is named r0_500 and refers
    explicitly to the Fried parameter at a wavelength of 500 nm.  The outer scale is typically in
    the 10s of meters and does not vary with wavelength.

    To create multiple layers, simply specify keyword arguments as length-N lists instead of scalars
    (works for all arguments except `rng`).  If, for any of these keyword arguments, you want to use
    the same value for each layer, then you can just specify the argument as a scalar and the
    function will automatically broadcast it into a list with length equal to the longest found
    keyword argument list.  Note that it is an error to specify keywords with lists of different
    lengths (unless only one of them has length > 1).

    The one exception to the above is the keyword `r0_500`.  The effective Fried parameter for a set
    of atmospheric layers is r0_500_effective = (sum(r**(-5./3) for r in r0_500s))**(-3./5).
    Providing `r0_500` as a scalar or single-element list will result in broadcasting such that the
    effective Fried parameter for the whole set of layers equals the input argument.  You can weight
    the contribution of each layer with the `r0_weights` keyword.

    As an example, the following code approximately creates the atmosphere used by Jee+Tyson(2011)
    for their study of atmospheric PSFs for LSST.  Note this code takes about ~2 minutes to run on
    a fast laptop, and will consume about (8192**2 pixels) * (8 bytes) * (6 screens) ~ 3 GB of
    RAM in its final state, and more at intermediate states.

        >>> altitude = [0, 2.58, 5.16, 7.73, 12.89, 15.46]  # km
        >>> r0_500 = 0.16  # m
        >>> weights = [0.652, 0.172, 0.055, 0.025, 0.074, 0.022]
        >>> speed = np.random.uniform(0, 20, size=6)  # m/s
        >>> direction = [np.random.uniform(0, 360)*galsim.degrees for i in range(6)]
        >>> npix = 8192
        >>> screen_scale = r0_500
        >>> atm = galsim.Atmosphere(r0_500=r0_500, r0_weights=weights,
                                    screen_size=screen_scale*npix,
                                    altitude=altitude, L0=25.0, speed=speed,
                                    direction=direction, screen_scale=screen_scale)

    Once the atmosphere is constructed, a 15-sec exposure length, 5ms time step, monochromatic PSF
    at 700nm (using an 8.4 meter aperture, 0.6 fractional obscuration and otherwise default
    settings) takes about 7 minutes to draw on a fast laptop.

        >>> psf = atm.makePSF(lam=700.0, exptime=15.0, time_step=0.005, diam=8.4, obscuration=0.6)
        >>> img1 = psf.drawImage()  # ~7 min

    The same psf, if drawn using photon-shooting on the same laptop, will generate photons at a rate
    of about 1 million per second.

        >>> img2 = psf.drawImage(nx=32, ny=32, scale=0.2, method='phot', n_photons=1e6)  # ~1 sec.

    Note that the Fourier-based calculation compute time will scale linearly with exposure time,
    while the photon-shooting calculation compute time will scale linearly with the number of
    photons being shot.

    Many factors will affect the timing of results, of course, including aperture diameter, gsparams
    settings, pad_factor and oversampling options to makePSF, time_step and exposure time, frozen
    vs. non-frozen atmospheric layers, and so on.  We recommend that users try varying these
    settings to find a balance of speed and fidelity.

    @param r0_500        Fried parameter setting the amplitude of turbulence; contributes to "size"
                         of the resulting atmospheric PSF.  Specified at wavelength 500 nm, in units
                         of meters.  [default: 0.2]
    @param r0_weights    Weights for splitting up the contribution of r0_500 between different
                         layers.  Note that this keyword is only allowed if r0_500 is either a
                         scalar or a single-element list.  [default: None]
    @param screen_size   Physical extent of square phase screen in meters.  This should be large
                         enough to accommodate the desired field-of-view of the telescope as well as
                         the meta-pupil defined by the wind speed and exposure time.  Note that
                         the screen will have periodic boundary conditions, so the code will run
                         with a smaller sized screen, though this may introduce artifacts into PSFs
                         or PSF correlation functions. Note that screen_size may be tweaked by the
                         initializer to ensure screen_size is a multiple of screen_scale.
    @param screen_scale  Physical pixel scale of phase screen in meters.  A fraction of the Fried
                         parameter is usually sufficiently small, but users should test the effects
                         of this parameter to ensure robust results.
                         [default: same as each screen's r0_500]
    @param altitude      Altitude of phase screen in km.  This is with respect to the telescope, not
                         sea-level.  [default: 0.0]
    @param L0            Outer scale in meters.  The turbulence power spectrum will smoothly
                         approach a constant at scales larger than L0.  Set to `None` or `np.inf`
                         for a power spectrum without an outer scale.  [default: 25.0]
    @param speed         Wind speed in meters/second.  [default: 0.0]
    @param direction     Wind direction as galsim.Angle [default: 0.0 * galsim.degrees]
    @param alpha         Square root of fraction of phase that is "remembered" between time_steps
                         (i.e., alpha**2 is the fraction remembered). The fraction sqrt(1-alpha**2)
                         is then the amount of turbulence freshly generated in each step.  Setting
                         alpha=1.0 results in a frozen-flow atmosphere.  Note that computing PSFs
                         from frozen-flow atmospheres may be significantly faster than computing
                         PSFs with non-frozen-flow atmospheres.  [default: 1.0]
    @param time_step     Time interval between phase boiling updates.  Note that this is distinct
                         from the time interval used when integrating the PSF over time, which is
                         set by the `time_step` keyword argument to `PhaseScreenPSF` or
                         `PhaseScreenList.makePSF`.  If `time_step` is not None, then it is required
                         that `alpha` is set to something other than 1.0.  [default: None]
    @param rng           Random number generator as a galsim.BaseDeviate().  If None, then use the
                         clock time or system entropy to seed a new generator.  [default: None]
    """
    # Fill in screen_size here, since there isn't a default in AtmosphericScreen
    kwargs['screen_size'] = galsim.utilities.listify(screen_size)

    # Set default r0_500 here; it will get broadcasted below such that the _total_ r0_500 from _all_
    # screens is 0.2 m.
    if 'r0_500' not in kwargs:
        kwargs['r0_500'] = [0.2]
    kwargs['r0_500'] = galsim.utilities.listify(kwargs['r0_500'])

    # Turn speed, direction into vx, vy
    if 'speed' in kwargs:
        kwargs['speed'] = galsim.utilities.listify(kwargs['speed'])
        if 'direction' not in kwargs:
            kwargs['direction'] = [0*galsim.degrees]*len(kwargs['speed'])
        kwargs['vx'], kwargs['vy'] = zip(*[v*d.sincos()
                                         for v, d in zip(kwargs['speed'],
                                                         kwargs['direction'])])
        del kwargs['speed']
        del kwargs['direction']

    # Determine broadcast size
    nmax = max(len(v) for v in kwargs.values() if hasattr(v, '__len__'))

    # Broadcast r0_500 here, since logical combination of indiv layers' r0s is complex:
    if len(kwargs['r0_500']) == 1:
        r0_weights = np.array(kwargs.pop('r0_weights', [1.]*nmax), dtype=float)
        r0_weights /= np.sum(r0_weights)
        r0_500 = kwargs['r0_500'][0]
        kwargs['r0_500'] = [r0_500 * w**(-3./5) for w in r0_weights]
        # kwargs['r0_500'] = [nmax**(3./5) * kwargs['r0_500'][0]] * nmax
    elif 'r0_weights' in kwargs:
        raise ValueError("Cannot use r0_weights if r0_500 is specified as a list.")

    if rng is None:
        rng = galsim.BaseDeviate()
    kwargs['rng'] = [galsim.BaseDeviate(rng.raw()) for i in range(nmax)]
    return galsim.PhaseScreenList([AtmosphericScreen(**kw)
                                   for kw in galsim.utilities.dol_to_lod(kwargs, nmax)])
Ejemplo n.º 7
0
def make_movie(args):
    rng = galsim.BaseDeviate(args.seed)
    u = galsim.UniformDeviate(rng)
    # Generate 1D Gaussian random fields for each aberration.
    t = np.arange(-args.n/2, args.n/2)
    corr = np.exp(-0.5*t**2/args.ell**2)
    pk = np.fft.fft(np.fft.fftshift(corr))
    ak = np.sqrt(2*pk)
    phi = np.random.uniform(size=(args.n, args.jmax))
    zk = ak[:, None]*np.exp(2j*np.pi*phi)
    aberrations = args.n/2*np.fft.ifft(zk, axis=0).real
    measured_std = np.mean(np.std(aberrations, axis=0))
    aberrations *= args.sigma/measured_std
    aberrations -= np.mean(aberrations, axis=0)

    # For the atmosphere screens, we first estimates weights, so that the turbulence is dominated by
    # the lower layers consistent with direct measurements.  The specific values we use are from
    # SCIDAR measurements on Cerro Pachon as part of the 1998 Gemini site selection process
    # (Ellerbroek 2002, JOSA Vol 19 No 9).
    Ellerbroek_alts = [0.0, 2.58, 5.16, 7.73, 12.89, 15.46]  # km
    Ellerbroek_weights = [0.652, 0.172, 0.055, 0.025, 0.074, 0.022]
    Ellerbroek_interp = galsim.LookupTable(Ellerbroek_alts, Ellerbroek_weights,
                                           interpolant='linear')
    alts = np.max(Ellerbroek_alts)*np.arange(args.nlayers)/(args.nlayers-1)
    weights = Ellerbroek_interp(alts)  # interpolate the weights
    weights /= sum(weights)  # and renormalize
    spd = []  # Wind speed in m/s
    dirn = [] # Wind direction in radians
    r0_500 = [] # Fried parameter in m at a wavelength of 500 nm.
    for i in range(args.nlayers):
        spd.append(u()*args.max_speed)  # Use a random speed between 0 and args.max_speed
        dirn.append(u()*360*galsim.degrees)  # And an isotropically distributed wind direction.
        r0_500.append(args.r0_500*weights[i]**(-3./5))
        print("Adding layer at altitude {:5.2f} km with velocity ({:5.2f}, {:5.2f}) m/s, "
              "and r0_500 {:5.3f} m."
              .format(alts[i], spd[i]*dirn[i].cos(), spd[i]*dirn[i].sin(), r0_500[i]))
    if args.nlayers > 0:
        # Make two identical Atmospheres.  They will diverge when one gets drawn using Fourier
        # optics and the other gets drawn with geometric optics.
        fft_atm = galsim.Atmosphere(r0_500=r0_500, speed=spd, direction=dirn, altitude=alts,
                                    rng=rng.duplicate(),
                                    screen_size=args.screen_size, screen_scale=args.screen_scale)
        geom_atm = galsim.Atmosphere(r0_500=r0_500, speed=spd, direction=dirn, altitude=alts,
                                     rng=rng.duplicate(), screen_size=args.screen_size,
                                     screen_scale=args.screen_scale)
    else:
        fft_atm = galsim.PhaseScreenList()
        geom_atm = galsim.PhaseScreenList()

    # Before either of this has been instantiated, they are identical
    assert fft_atm == geom_atm

    # If any AtmosphericScreens are included, we manually instantiate here so we can have a
    # uniformly updating ProgressBar both here and below when actually drawing PSFs.  Normally, it's
    # okay to let the atms automatically instantiate, which happens when the first PSF is drawn, or
    # the first wavefront is queried.
    if args.nlayers > 0:
        print("Instantiating screens")
        with ProgressBar(2*args.nlayers) as bar:
            fft_atm.instantiate(_bar=bar)
            r0 = args.r0_500*(args.lam/500)**1.2
            geom_atm.instantiate(kmax=0.2/r0, _bar=bar)
            # After instantiation, they're only equal if there's no atmosphere.
            assert fft_atm != geom_atm

    # Setup Fourier and geometric apertures
    fft_aper = galsim.Aperture(args.diam, args.lam, obscuration=args.obscuration,
                               pad_factor=args.pad_factor, oversampling=args.oversampling,
                               nstruts=args.nstruts, strut_thick=args.strut_thick,
                               strut_angle=args.strut_angle*galsim.degrees)
    geom_aper = galsim.Aperture(args.diam, args.lam, obscuration=args.obscuration,
                                pad_factor=args.geom_oversampling, oversampling=0.5,
                                nstruts=args.nstruts, strut_thick=args.strut_thick,
                                strut_angle=args.strut_angle*galsim.degrees)

    scale = args.size/args.nx
    extent = np.r_[-1,1,-1,1]*args.size/2

    fft_img_sum = galsim.ImageD(args.nx, args.nx, scale=scale)
    geom_img_sum = galsim.ImageD(args.nx, args.nx, scale=scale)

    # Code to setup the Matplotlib animation.
    metadata = dict(title="FFT vs geom movie", artist='Matplotlib')
    writer = anim.FFMpegWriter(fps=15, bitrate=10000, metadata=metadata)

    fig = Figure(facecolor='k', figsize=(16, 9))
    FigureCanvasAgg(fig)

    fft_ax = fig.add_axes([0.07, 0.08, 0.36, 0.9])
    fft_ax.set_xlabel("Arcsec")
    fft_ax.set_ylabel("Arcsec")
    fft_ax.set_title("Fourier Optics")
    fft_im = fft_ax.imshow(np.ones((args.nx, args.nx), dtype=float), animated=True, extent=extent,
                           vmin=0.0, vmax=args.vmax)

    # Axis for the wavefront image on the right.
    geom_ax = fig.add_axes([0.50, 0.08, 0.36, 0.9])
    geom_ax.set_xlabel("Arcsec")
    geom_ax.set_ylabel("Arcsec")
    geom_ax.set_title("Geometric Optics")
    geom_im = geom_ax.imshow(np.ones((args.nx, args.nx), dtype=float), animated=True, extent=extent,
                             vmin=0.0, vmax=args.vmax)

    # Color items white to show up on black background
    for ax in [fft_ax, geom_ax]:
        for _, spine in ax.spines.items():
            spine.set_color('w')
        ax.title.set_color('w')
        ax.xaxis.label.set_color('w')
        ax.yaxis.label.set_color('w')
        ax.tick_params(axis='both', colors='w')

    ztext = []
    for i in range(2, args.jmax+1):
        x = 0.88
        y = 0.1 + (args.jmax-i)/args.jmax*0.8
        ztext.append(fig.text(x, y, "Z{:d} = {:5.3f}".format(i, 0.0)))
        ztext[-1].set_color('w')

    M_fft = fft_ax.text(0.02, 0.955, '', transform=fft_ax.transAxes)
    M_fft.set_color('w')
    M_geom = geom_ax.text(0.02, 0.955, '', transform=geom_ax.transAxes)
    M_geom.set_color('w')

    etext_fft = fft_ax.text(0.02, 0.91, '', transform=fft_ax.transAxes)
    etext_fft.set_color('w')
    etext_geom = geom_ax.text(0.02, 0.91, '', transform=geom_ax.transAxes)
    etext_geom.set_color('w')

    fft_mom = np.empty((args.n, 8), dtype=float)
    geom_mom = np.empty((args.n, 8), dtype=float)

    fullpath = args.out+"movie.mp4"
    subdir, filename = os.path.split(fullpath)
    if subdir and not os.path.isdir(subdir):
        os.makedirs(subdir)

    print("Drawing PSFs")
    with ProgressBar(args.n) as bar:
        with writer.saving(fig, fullpath, 100):
            t0 = 0.0
            for i, aberration in enumerate(aberrations):
                optics = galsim.OpticalScreen(args.diam, obscuration=args.obscuration,
                                              aberrations=[0]+aberration.tolist())
                fft_psl = galsim.PhaseScreenList(fft_atm._layers+[optics])
                geom_psl = galsim.PhaseScreenList(geom_atm._layers+[optics])
                fft_psf = fft_psl.makePSF(
                        lam=args.lam, aper=fft_aper, t0=t0, exptime=args.time_step)
                geom_psf = geom_psl.makePSF(
                        lam=args.lam, aper=geom_aper, t0=t0, exptime=args.time_step)

                fft_img0 = fft_psf.drawImage(nx=args.nx, ny=args.nx, scale=scale)

                geom_img0 = geom_psf.drawImage(nx=args.nx, ny=args.nx, scale=scale,
                                               method='phot', n_photons=100000)

                t0 += args.time_step

                if args.accumulate:
                    fft_img_sum += fft_img0
                    geom_img_sum += geom_img0
                    fft_img = fft_img_sum/(i+1)
                    geom_img = geom_img_sum/(i+1)
                else:
                    fft_img = fft_img0
                    geom_img = geom_img0


                fft_im.set_array(fft_img.array)
                geom_im.set_array(geom_img.array)

                for j, ab in enumerate(aberration):
                    if j == 0:
                        continue
                    ztext[j-1].set_text("Z{:d} = {:5.3f}".format(j+1, ab))

                # Calculate simple estimate of ellipticity
                mom_fft = galsim.utilities.unweighted_moments(fft_img, origin=fft_img.true_center)
                mom_geom = galsim.utilities.unweighted_moments(geom_img,
                                                               origin=geom_img.true_center)
                e_fft = galsim.utilities.unweighted_shape(mom_fft)
                e_geom = galsim.utilities.unweighted_shape(mom_geom)

                Is = ("$M_x$={: 6.4f}, $M_y$={: 6.4f}, $M_{{xx}}$={:6.4f},"
                      " $M_{{yy}}$={:6.4f}, $M_{{xy}}$={: 6.4f}")
                M_fft.set_text(Is.format(mom_fft['Mx']*fft_img.scale,
                                         mom_fft['My']*fft_img.scale,
                                         mom_fft['Mxx']*fft_img.scale**2,
                                         mom_fft['Myy']*fft_img.scale**2,
                                         mom_fft['Mxy']*fft_img.scale**2))
                M_geom.set_text(Is.format(mom_geom['Mx']*geom_img.scale,
                                          mom_geom['My']*geom_img.scale,
                                          mom_geom['Mxx']*geom_img.scale**2,
                                          mom_geom['Myy']*geom_img.scale**2,
                                          mom_geom['Mxy']*geom_img.scale**2))
                etext_fft.set_text("$e_1$={: 6.4f}, $e_2$={: 6.4f}, $r^2$={:6.4f}".format(
                                   e_fft['e1'], e_fft['e2'], e_fft['rsqr']*fft_img.scale**2))
                etext_geom.set_text("$e_1$={: 6.4f}, $e_2$={: 6.4f}, $r^2$={:6.4f}".format(
                                    e_geom['e1'], e_geom['e2'], e_geom['rsqr']*geom_img.scale**2))


                fft_mom[i] = (mom_fft['Mx']*fft_img.scale, mom_fft['My']*fft_img.scale,
                              mom_fft['Mxx']*fft_img.scale**2, mom_fft['Myy']*fft_img.scale**2,
                              mom_fft['Mxy']*fft_img.scale**2,
                              e_fft['e1'], e_fft['e2'], e_fft['rsqr']*fft_img.scale**2)

                geom_mom[i] = (mom_geom['Mx']*geom_img.scale, mom_geom['My']*geom_img.scale,
                              mom_geom['Mxx']*geom_img.scale**2, mom_geom['Myy']*geom_img.scale**2,
                              mom_geom['Mxy']*geom_img.scale**2,
                              e_geom['e1'], e_geom['e2'], e_geom['rsqr']*geom_img.scale**2)

                writer.grab_frame(facecolor=fig.get_facecolor())

                bar.update()

    def symmetrize_axis(ax):
        xlim = ax.get_xlim()
        ylim = ax.get_ylim()
        lim = min(xlim[0], ylim[0]), max(xlim[1], ylim[1])
        ax.set_xlim(lim)
        ax.set_ylim(lim)
        ax.plot(lim, lim)

    # Centroid plot
    fig = Figure(figsize=(10, 6))
    FigureCanvasAgg(fig)
    axes = []
    axes.append(fig.add_subplot(1, 2, 1))
    axes.append(fig.add_subplot(1, 2, 2))
    axes[0].scatter(fft_mom[:, 0], geom_mom[:, 0])
    axes[1].scatter(fft_mom[:, 1], geom_mom[:, 1])
    axes[0].set_title("Mx")
    axes[1].set_title("My")
    for ax in axes:
        ax.set_xlabel("Fourier Optics")
        ax.set_ylabel("Geometric Optics")
        symmetrize_axis(ax)
    fig.tight_layout()
    fig.savefig(args.out+"centroid.png", dpi=300)

    # Second moment plot
    fig = Figure(figsize=(16, 6))
    FigureCanvasAgg(fig)
    axes = []
    axes.append(fig.add_subplot(1, 3, 1))
    axes.append(fig.add_subplot(1, 3, 2))
    axes.append(fig.add_subplot(1, 3, 3))
    axes[0].scatter(fft_mom[:, 2], geom_mom[:, 2])
    axes[1].scatter(fft_mom[:, 3], geom_mom[:, 3])
    axes[2].scatter(fft_mom[:, 4], geom_mom[:, 4])
    axes[0].set_title("Mxx")
    axes[1].set_title("Myy")
    axes[2].set_title("Mxy")
    for ax in axes:
        ax.set_xlabel("Fourier Optics")
        ax.set_ylabel("Geometric Optics")
        symmetrize_axis(ax)
    fig.tight_layout()
    fig.savefig(args.out+"2ndMoment.png", dpi=300)

    # Ellipticity plot
    fig = Figure(figsize=(16, 6))
    FigureCanvasAgg(fig)
    axes = []
    axes.append(fig.add_subplot(1, 3, 1))
    axes.append(fig.add_subplot(1, 3, 2))
    axes.append(fig.add_subplot(1, 3, 3))
    axes[0].scatter(fft_mom[:, 5], geom_mom[:, 5])
    axes[1].scatter(fft_mom[:, 6], geom_mom[:, 6])
    axes[2].scatter(fft_mom[:, 7], geom_mom[:, 7])
    axes[0].set_title("e1")
    axes[1].set_title("e2")
    axes[2].set_title("rsqr")
    for ax in axes:
        ax.set_xlabel("Fourier Optics")
        ax.set_ylabel("Geometric Optics")
        symmetrize_axis(ax)
    fig.tight_layout()
    fig.savefig(args.out+"ellipticity.png", dpi=300)
Ejemplo n.º 8
0
def test_ne():
    """Test Apertures, PhaseScreens, PhaseScreenLists, and PhaseScreenPSFs for not-equals."""
    import copy
    pupil_plane_im = galsim.fits.read(os.path.join(imgdir, pp_file))

    # Test galsim.Aperture __ne__
    objs = [galsim.Aperture(diam=1.0),
            galsim.Aperture(diam=1.1),
            galsim.Aperture(diam=1.0, oversampling=1.5),
            galsim.Aperture(diam=1.0, pad_factor=1.5),
            galsim.Aperture(diam=1.0, circular_pupil=False),
            galsim.Aperture(diam=1.0, obscuration=0.3),
            galsim.Aperture(diam=1.0, nstruts=3),
            galsim.Aperture(diam=1.0, nstruts=3, strut_thick=0.2),
            galsim.Aperture(diam=1.0, nstruts=3, strut_angle=15*galsim.degrees),
            galsim.Aperture(diam=1.0, pupil_plane_im=pupil_plane_im),
            galsim.Aperture(diam=1.0, pupil_plane_im=pupil_plane_im,
                            pupil_angle=10.0*galsim.degrees)]
    all_obj_diff(objs)

    # Test AtmosphericScreen __ne__
    rng = galsim.BaseDeviate(1)
    objs = [galsim.AtmosphericScreen(10.0, rng=rng),
            galsim.AtmosphericScreen(10.0, rng=rng, vx=1.0),
            galsim.AtmosphericScreen(10.0, rng=rng, vx=1.0),  # advance this one below
            galsim.AtmosphericScreen(10.0, rng=rng, vy=1.0),
            galsim.AtmosphericScreen(10.0, rng=rng, alpha=0.999),
            galsim.AtmosphericScreen(10.0, rng=rng, altitude=1.0),
            galsim.AtmosphericScreen(10.0, rng=rng, time_step=0.1),
            galsim.AtmosphericScreen(10.0, rng=rng, r0_500=0.1),
            galsim.AtmosphericScreen(10.0, rng=rng, L0=10.0),
            galsim.AtmosphericScreen(10.0, rng=rng, vx=10.0),
            ]
    objs[2].advance()
    all_obj_diff(objs)

    # Test OpticalScreen __ne__
    objs = [galsim.OpticalScreen(),
            galsim.OpticalScreen(tip=1.0),
            galsim.OpticalScreen(tilt=1.0),
            galsim.OpticalScreen(defocus=1.0),
            galsim.OpticalScreen(astig1=1.0),
            galsim.OpticalScreen(astig2=1.0),
            galsim.OpticalScreen(coma1=1.0),
            galsim.OpticalScreen(coma2=1.0),
            galsim.OpticalScreen(trefoil1=1.0),
            galsim.OpticalScreen(trefoil2=1.0),
            galsim.OpticalScreen(spher=1.0),
            galsim.OpticalScreen(spher=1.0, lam_0=100.0),
            galsim.OpticalScreen(aberrations=[0,0,1.1]), # tip=1.1
            ]
    all_obj_diff(objs)

    # Test PhaseScreenList __ne__
    atm = galsim.Atmosphere(10.0, vx=1.0)
    objs = [galsim.PhaseScreenList(atm),
            galsim.PhaseScreenList(copy.deepcopy(atm)),  # advance down below
            galsim.PhaseScreenList(objs),  # Reuse list of OpticalScreens above
            galsim.PhaseScreenList(objs[0:2])]
    objs[1].advance()
    all_obj_diff(objs)

    # Test PhaseScreenPSF __ne__
    objs[0].reset()
    psl = galsim.PhaseScreenList(atm)
    objs = [galsim.PhaseScreenPSF(psl, 500.0, exptime=0.03, diam=1.0),
            galsim.PhaseScreenPSF(psl, 500.0, exptime=0.03, diam=1.0)] # advanced so differs
    psl.reset()
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0)]
    psl.reset()
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.1)]
    psl.reset()
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0, flux=1.1)]
    psl.reset()
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0, interpolant='linear')]
    stepk = objs[0].stepK()
    maxk = objs[0].maxK()
    psl.reset()
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0, _force_stepk=stepk/1.5)]
    psl.reset()
    objs += [galsim.PhaseScreenPSF(psl, 700.0, exptime=0.03, diam=1.0, _force_maxk=maxk*2.0)]
    all_obj_diff(objs)
Ejemplo n.º 9
0
def test_phase_screen_list():
    """Test list-like behaviors of PhaseScreenList."""
    rng = galsim.BaseDeviate(1234)
    rng2 = galsim.BaseDeviate(123)

    aper = galsim.Aperture(diam=1.0)

    ar1 = galsim.AtmosphericScreen(10, 1, alpha=0.997, L0=None, rng=rng)
    do_pickle(ar1)
    do_pickle(ar1, func=lambda x: x.tab2d(12.3, 45.6))
    do_pickle(ar1, func=lambda x: x.wavefront(aper).sum())

    # Check that L0=np.inf and L0=None yield the same thing here too.
    ar2 = galsim.AtmosphericScreen(10, 1, alpha=0.997, L0=np.inf, rng=rng)
    assert ar1 == ar2
    # Create a couple new screens with different types/parameters
    ar2 = galsim.AtmosphericScreen(10, 1, alpha=0.995, rng=rng2)
    assert ar1 != ar2
    ar3 = galsim.OpticalScreen(aberrations=[0, 0, 0, 0, 0, 0, 0, 0, 0.1])
    do_pickle(ar3)
    do_pickle(ar3, func=lambda x:x.wavefront(aper).sum())
    atm = galsim.Atmosphere(screen_size=30.0,
                            altitude=[0.0, 1.0],
                            speed=[1.0, 2.0],
                            direction=[0.0*galsim.degrees, 120*galsim.degrees],
                            r0_500=0.15,
                            rng=rng)
    atm.append(ar3)
    do_pickle(atm)
    do_pickle(atm, func=lambda x:x.wavefront(aper).sum())

    # testing append, extend, __getitem__, __setitem__, __delitem__, __eq__, __ne__
    atm2 = galsim.PhaseScreenList(atm[:-1])  # Refers to first n-1 screens
    assert atm != atm2
    # Append a different screen to the end of atm2
    atm2.append(ar2)
    assert atm != atm2
    # Swap the last screen in atm2 for the one that should match atm.
    del atm2[-1]
    atm2.append(atm[-1])
    assert atm == atm2

    # Test building from empty PhaseScreenList
    atm3 = galsim.PhaseScreenList()
    atm3.extend(atm2)
    assert atm == atm2

    # Test constructing from existing PhaseScreenList
    atm4 = galsim.PhaseScreenList(atm3)
    del atm4[-1]
    assert atm != atm4
    atm4.append(atm[-1])
    assert atm == atm4

    # Test swap
    atm4[0], atm4[1] = atm4[1], atm4[0]
    assert atm != atm4
    atm4[0], atm4[1] = atm4[1], atm4[0]
    assert atm == atm4

    wf = atm.wavefront(aper)
    wf2 = atm2.wavefront(aper)
    wf3 = atm3.wavefront(aper)
    wf4 = atm4.wavefront(aper)

    np.testing.assert_array_equal(wf, wf2, "PhaseScreenLists are inconsistent")
    np.testing.assert_array_equal(wf, wf3, "PhaseScreenLists are inconsistent")
    np.testing.assert_array_equal(wf, wf4, "PhaseScreenLists are inconsistent")

    # Check copy
    import copy
    # Shallow copy copies by reference.
    atm5 = copy.copy(atm)
    assert atm[0] == atm5[0]
    assert atm[0] is atm5[0]
    atm.advance()
    assert atm[0] == atm5[0]
    assert atm[0] is atm5[0]
    # Deepcopy actually makes an indepedent object in memory.
    atm5 = copy.deepcopy(atm)
    assert atm[0] == atm5[0]
    assert atm[0] is not atm5[0]
    atm.advance()
    assert atm[0] != atm5[0]

    # Constructor should accept both list and indiv layers as arguments.
    atm6 = galsim.PhaseScreenList(atm[0])
    atm7 = galsim.PhaseScreenList([atm[0]])
    assert atm6 == atm7
    atm6 = galsim.PhaseScreenList(atm[0], atm[1])
    atm7 = galsim.PhaseScreenList([atm[0], atm[1]])
    atm8 = galsim.PhaseScreenList(atm[0:2])  # Slice returns PhaseScreenList, so this works too.
    assert atm6 == atm7
    assert atm6 == atm8

    # Check some actual derived PSFs too, not just phase screens.  Use a small pupil_plane_size and
    # relatively large pupil_plane_scale to speed up the unit test.
    atm.advance_by(1.0)
    do_pickle(atm)
    atm.reset()
    kwargs = dict(exptime=0.06, diam=1.0, lam=1000.0)
    psf = atm.makePSF(**kwargs)
    do_pickle(psf)
    do_pickle(psf, func=lambda x:x.drawImage(nx=20, ny=20, scale=0.1))

    # Need to reset atm2 since both atm and atm2 reference the same layer objects (not copies).
    # Not sure if this is a feature or a bug, but it's also how regular python lists work.
    atm2.reset()
    psf2 = atm2.makePSF(**kwargs)

    atm3.reset()
    psf3 = atm3.makePSF(**kwargs)

    atm4.reset()
    psf4 = atm4.makePSF(**kwargs)

    np.testing.assert_array_equal(psf, psf2, "PhaseScreenPSFs are inconsistent")
    np.testing.assert_array_equal(psf, psf3, "PhaseScreenPSFs are inconsistent")
    np.testing.assert_array_equal(psf, psf4, "PhaseScreenPSFs are inconsistent")