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." )
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()])
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
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)
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)
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)])
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)
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)
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")