def test_lsst_y_focus(): # Check that applying reasonable focus depth (from O'Connor++06) indeed leads to smaller spot # size for LSST y-band. rng = galsim.BaseDeviate(9876543210) bandpass = galsim.Bandpass("LSST_y.dat", wave_type='nm') sed = galsim.SED("1", wave_type='nm', flux_type='flambda') obj = galsim.Gaussian(fwhm=1e-5) oversampling = 32 photon_ops0 = [ galsim.WavelengthSampler(sed, bandpass, rng=rng), galsim.FRatioAngles(1.234, 0.606, rng=rng), galsim.FocusDepth(0.0), galsim.Refraction(3.9) ] img0 = obj.drawImage( sensor=galsim.SiliconSensor(), method='phot', n_photons=100000, photon_ops=photon_ops0, scale=0.2/oversampling, nx=32*oversampling, ny=32*oversampling, rng=rng ) T0 = img0.calculateMomentRadius() T0 *= 10*oversampling/0.2 # arcsec => microns # O'Connor finds minimum spot size when the focus depth is ~ -12 microns. Our sensor isn't # necessarily the same as the one there though; our minimum seems to be around -6 microns. # That could be due to differences in the design of the sensor though. We just use -6 microns # here, which is still useful to test the sign of the `depth` parameter and the interaction of # the 4 different surface operators required to produce this effect, and is roughly consistent # with O'Connor. depth1 = -6. # microns, negative means surface is intrafocal depth1 /= 10 # microns => pixels photon_ops1 = [ galsim.WavelengthSampler(sed, bandpass, rng=rng), galsim.FRatioAngles(1.234, 0.606, rng=rng), galsim.FocusDepth(depth1), galsim.Refraction(3.9) ] img1 = obj.drawImage( sensor=galsim.SiliconSensor(), method='phot', n_photons=100000, photon_ops=photon_ops1, scale=0.2/oversampling, nx=32*oversampling, ny=32*oversampling, rng=rng ) T1 = img1.calculateMomentRadius() T1 *= 10*oversampling/0.2 # arcsec => microns np.testing.assert_array_less(T1, T0)
def time_silicon_accumulate(): nx = 1000 ny = 1000 nobj = 1000 photons_per_obj = 10000 flux_per_photon = 1 rng = galsim.UniformDeviate(314159) sensor = galsim.SiliconSensor(rng=rng.duplicate(), diffusion_factor=0.0) im = galsim.ImageF(nx, ny) num_photons = nobj * photons_per_obj photons = galsim.PhotonArray(num_photons) rng.generate(photons.x) photons.x *= nx photons.x += 0.5 rng.generate(photons.y) photons.y *= ny photons.y += 0.5 photons.flux = flux_per_photon t1 = time.time() sensor.accumulate(photons, im) t2 = time.time() print('Time = ', t2 - t1)
def __init__(self, obs_metadata=None, detectors=None, bandpassDict=None, noiseWrapper=None, epoch=None, seed=None, bf_strength=1): super(GalSimSiliconInterpeter, self)\ .__init__(obs_metadata=obs_metadata, detectors=detectors, bandpassDict=bandpassDict, noiseWrapper=noiseWrapper, epoch=epoch, seed=seed) self.gs_bandpass_dict = {} for bandpassName in bandpassDict: bandpass = bandpassDict[bandpassName] index = np.where(bandpass.sb != 0) bp_lut = galsim.LookupTable(x=bandpass.wavelen[index], f=bandpass.sb[index]) self.gs_bandpass_dict[bandpassName] \ = galsim.Bandpass(bp_lut, wave_type='nm') self.sky_bg_per_pixel = None # Create a PSF that's fast to evaluate for the postage stamp # size calculation for extended objects in .getStampBounds. FWHMgeom = obs_metadata.OpsimMetaData['FWHMgeom'] self._double_gaussian_psf = SNRdocumentPSF(FWHMgeom) # Save the parameters needed to create a Kolmogorov PSF for a # custom value of gsparams.folding_threshold. That PSF will # to be used in the .getStampBounds function for bright stars. altRad = np.radians(obs_metadata.OpsimMetaData['altitude']) self._airmass = 1.0/np.sqrt(1.0-0.96*(np.sin(0.5*np.pi-altRad))**2) self._rawSeeing = obs_metadata.OpsimMetaData['rawSeeing'] self._band = obs_metadata.bandpass # Save the default folding threshold for determining when to recompute # the PSF for bright point sources. self._ft_default = galsim.GSParams().folding_threshold # Create SiliconSensor objects for each detector. self.sensor = dict() for det in detectors: self.sensor[det.name] \ = galsim.SiliconSensor(strength=bf_strength, treering_center=det.tree_rings.center, treering_func=det.tree_rings.func, transpose=True)
def test_resume(): """Test that the resume option for accumulate works properly. """ # Note: This test is based on a script devel/lsst/treering_skybg_check.py rng = galsim.UniformDeviate(314159) if __name__ == "__main__": flux_per_pixel = 40 nx = 200 ny = 200 block_size = int(1.2e5) nrecalc = 1.e6 else: flux_per_pixel = 40 nx = 20 ny = 20 block_size = int(1.3e3) nrecalc = 1.e4 expected_num_photons = nx * ny * flux_per_pixel pd = galsim.PoissonDeviate(rng, mean=expected_num_photons) num_photons = int(pd()) # Poisson realization of the given expected number of photons. #nrecalc = num_photons / 2 # Only recalc once. flux_per_photon = 1 print('num_photons = ',num_photons,' .. expected = ',expected_num_photons) # Use treerings to make sure that aspect of the setup is preserved properly on resume treering_func = galsim.SiliconSensor.simple_treerings(0.5, 250.) treering_center = galsim.PositionD(-1000,0) sensor1 = galsim.SiliconSensor(rng=rng.duplicate(), nrecalc=nrecalc, treering_func=treering_func, treering_center=treering_center) sensor2 = galsim.SiliconSensor(rng=rng.duplicate(), nrecalc=nrecalc, treering_func=treering_func, treering_center=treering_center) sensor3 = galsim.SiliconSensor(rng=rng.duplicate(), nrecalc=nrecalc, treering_func=treering_func, treering_center=treering_center) waves = galsim.WavelengthSampler(sed = galsim.SED('1', 'nm', 'fphotons'), bandpass = galsim.Bandpass('LSST_r.dat', 'nm'), rng=rng) angles = galsim.FRatioAngles(1.2, 0.4, rng) im1 = galsim.ImageF(nx,ny) # Will not use resume im2 = galsim.ImageF(nx,ny) # Will use resume im3 = galsim.ImageF(nx,ny) # Will run all photons in one pass t_resume = 0 t_no_resume = 0 all_photons = galsim.PhotonArray(num_photons) n_added = 0 first = True while num_photons > 0: print(num_photons,'photons left. image min/max =',im1.array.min(),im1.array.max()) nphot = min(block_size, num_photons) num_photons -= nphot t0 = time.time() photons = galsim.PhotonArray(int(nphot)) rng.generate(photons.x) # 0..1 so far photons.x *= nx photons.x += 0.5 # Now from xmin-0.5 .. xmax+0.5 rng.generate(photons.y) photons.y *= ny photons.y += 0.5 photons.flux = flux_per_photon waves.applyTo(photons) angles.applyTo(photons) all_photons.x[n_added:n_added+nphot] = photons.x all_photons.y[n_added:n_added+nphot] = photons.y all_photons.flux[n_added:n_added+nphot] = photons.flux all_photons.dxdz[n_added:n_added+nphot] = photons.dxdz all_photons.dydz[n_added:n_added+nphot] = photons.dydz all_photons.wavelength[n_added:n_added+nphot] = photons.wavelength n_added += nphot t1 = time.time() sensor1.accumulate(photons, im1) t2 = time.time() sensor2.accumulate(photons, im2, resume = not first) first = False t3 = time.time() print('Times = ',t1-t0,t2-t1,t3-t2) t_resume += t3-t2 t_no_resume += t2-t1 print('max diff = ',np.max(np.abs(im1.array - im2.array))) print('max rel diff = ',np.max(np.abs(im1.array - im2.array)/np.abs(im2.array))) np.testing.assert_almost_equal(im2.array/expected_num_photons, im1.array/expected_num_photons, decimal=5) print('Time with resume = ',t_resume) print('Time without resume = ',t_no_resume) assert t_resume < t_no_resume # The resume path should be exactly the same as doing all the photons at once. sensor3.accumulate(all_photons, im3) np.testing.assert_array_equal(im2.array, im3.array) # If resume is used either with the wrong image or on the first call to accumulate, then # this should raise an exception. assert_raises(RuntimeError, sensor3.accumulate, all_photons, im1, resume=True) sensor4 = galsim.SiliconSensor(rng=rng.duplicate(), nrecalc=nrecalc, treering_func=treering_func, treering_center=treering_center) assert_raises(RuntimeError, sensor4.accumulate, all_photons, im1, resume=True)
def test_treerings(): """Test the addition of tree rings. compare image positions with the simple sensor to a SiliconSensor with no tree rings and six different additions of tree rings. """ # Set up the different sensors. treering_amplitude = 0.5 rng1 = galsim.BaseDeviate(5678) rng2 = galsim.BaseDeviate(5678) rng3 = galsim.BaseDeviate(5678) rng4 = galsim.BaseDeviate(5678) rng5 = galsim.BaseDeviate(5678) rng6 = galsim.BaseDeviate(5678) rng7 = galsim.BaseDeviate(5678) sensor0 = galsim.Sensor() sensor1 = galsim.SiliconSensor(rng=rng1) tr2 = galsim.SiliconSensor.simple_treerings(treering_amplitude, 250.) sensor2 = galsim.SiliconSensor(rng=rng2, treering_func=tr2, treering_center=galsim.PositionD(-1000.0,0.0)) sensor3 = galsim.SiliconSensor(rng=rng3, treering_func=tr2, treering_center=galsim.PositionD(1000.0,0.0)) sensor4 = galsim.SiliconSensor(rng=rng4, treering_func=tr2, treering_center=galsim.PositionD(0.0,-1000.0)) tr5 = galsim.SiliconSensor.simple_treerings(treering_amplitude, 250., r_max=2000, dr=1.) sensor5 = galsim.SiliconSensor(rng=rng5, treering_func=tr5, treering_center=galsim.PositionD(0.0,1000.0)) # Now test the ability to read in a user-specified function tr6 = galsim.LookupTable.from_func(treering_function, x_min=0.0, x_max=2000) sensor6 = galsim.SiliconSensor(rng=rng6, treering_func=tr6, treering_center=galsim.PositionD(-1000.0,0.0)) # Now test the ability to read in a lookup table tr7 = galsim.LookupTable.from_file('tree_ring_lookup.dat', amplitude=treering_amplitude) sensor7 = galsim.SiliconSensor(rng=rng7, treering_func=tr7, treering_center=galsim.PositionD(-1000.0,0.0)) sensors = [sensor0, sensor1, sensor2, sensor3, sensor4, sensor5, sensor6, sensor7] names = ['Sensor()', 'SiliconSensor, no TreeRings', 'SiliconSensor, TreeRingCenter= (-1000,0)', 'SiliconSensor, TreeRingCenter= (1000,0)', 'SiliconSensor, TreeRingCenter= (0,-1000)', 'SiliconSensor, TreeRingCenter= (0,1000)', 'SiliconSensor with specified function, TreeRingCenter= (-1000,0)', 'SiliconSensor with lookup table, TreeRingCenter= (-1000,0)'] centers = [None, None, (-1000,0), (1000,0), (0,-1000), (0,1000), (-1000,0), (-1000,0)] init_flux = 200000 obj = galsim.Gaussian(flux=init_flux, sigma=0.3) im = galsim.ImageD(10,10, scale=0.3) obj.drawImage(im) ref_mom = galsim.utilities.unweighted_moments(im) print('Reference Moments Mx, My = (%f, %f):'%(ref_mom['Mx'], ref_mom['My'])) for sensor, name, center in zip(sensors, names, centers): im = galsim.ImageD(10,10, scale=0.3) obj.drawImage(im, method='phot', sensor=sensor) mom = galsim.utilities.unweighted_moments(im) print('%s, Moments Mx, My = (%f, %f):'%(name, mom['Mx'], mom['My'])) if center is None: np.testing.assert_almost_equal(mom['Mx'], ref_mom['Mx'], decimal = 1) np.testing.assert_almost_equal(mom['My'], ref_mom['My'], decimal = 1) else: np.testing.assert_almost_equal(ref_mom['Mx'] + treering_amplitude * center[0] / 1000, mom['Mx'], decimal=1) np.testing.assert_almost_equal(ref_mom['My'] + treering_amplitude * center[1] / 1000, mom['My'], decimal=1) assert_raises(TypeError, galsim.SiliconSensor, treering_func=lambda x:np.cos(x)) assert_raises(TypeError, galsim.SiliconSensor, treering_func=tr7, treering_center=(3,4))
def test_bf_slopes(): """Test the brighter-fatter slopes with both the B-F effect and diffusion turned on and off. """ from scipy import stats simple = galsim.Sensor() init_flux = 200000 obj = galsim.Gaussian(flux=init_flux, sigma=0.3) num_fluxes = 5 x_moments = np.zeros([num_fluxes, 3]) y_moments = np.zeros([num_fluxes, 3]) fluxes = np.zeros([num_fluxes]) for fluxmult in range(num_fluxes): rng1 = galsim.BaseDeviate(5678) rng2 = galsim.BaseDeviate(5678) rng3 = galsim.BaseDeviate(5678) # silicon1 has diffusion turned off, silicon2 has it turned on. silicon1 = galsim.SiliconSensor(rng=rng1, diffusion_factor=0.0) silicon2 = galsim.SiliconSensor(rng=rng2) # We'll draw the same object using SiliconSensor, Sensor, and the default (sensor=None) im1 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=silicon1 (diffusion off) im2 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=silicon2 (diffusion on) im3 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=simple obj1 = obj * (fluxmult + 1) obj1.drawImage(im1, method='phot', poisson_flux=False, sensor=silicon1, rng=rng1) obj1.drawImage(im2, method='phot', poisson_flux=False, sensor=silicon2, rng=rng2) obj1.drawImage(im3, method='phot', poisson_flux=False, sensor=simple, rng=rng3) print('Moments Mx, My, Mxx, Myy, Mxy for im1, flux = %f:'%obj1.flux) mom = galsim.utilities.unweighted_moments(im1) x_moments[fluxmult,0] = mom['Mxx'] y_moments[fluxmult,0] = mom['Myy'] print('Moments Mx, My, Mxx, Myy, Mxy for im2, flux = %f:'%obj1.flux) mom = galsim.utilities.unweighted_moments(im2) x_moments[fluxmult,1] = mom['Mxx'] y_moments[fluxmult,1] = mom['Myy'] print('Moments Mx, My, Mxx, Myy, Mxy for im3, flux = %f:'%obj1.flux) mom = galsim.utilities.unweighted_moments(im3) x_moments[fluxmult,2] = mom['Mxx'] y_moments[fluxmult,2] = mom['Myy'] fluxes[fluxmult] = im1.array.max() print('fluxes = ',fluxes) print('x_moments = ',x_moments[:,0]) print('y_moments = ',y_moments[:,0]) x_slope, intercept, r_value, p_value, std_err = stats.linregress(fluxes,x_moments[:,0]) y_slope, intercept, r_value, p_value, std_err = stats.linregress(fluxes,y_moments[:,0]) x_slope *= 50000.0 * 100.0 y_slope *= 50000.0 * 100.0 print('With BF turned on, diffusion off, x_slope = %.3f, y_slope = %.3f %% per 50K e-'%( x_slope, y_slope )) assert x_slope > 0.5 assert y_slope > 0.5 x_slope, intercept, r_value, p_value, std_err = stats.linregress(fluxes,x_moments[:,1]) y_slope, intercept, r_value, p_value, std_err = stats.linregress(fluxes,y_moments[:,1]) x_slope *= 50000.0 * 100.0 y_slope *= 50000.0 * 100.0 print('With BF turned on, diffusion on, x_slope = %.3f, y_slope = %.3f %% per 50K e-'%( x_slope, y_slope )) assert x_slope > 0.5 assert y_slope > 0.5 x_slope, intercept, r_value, p_value, std_err = stats.linregress(fluxes,x_moments[:,2]) y_slope, intercept, r_value, p_value, std_err = stats.linregress(fluxes,y_moments[:,2]) x_slope *= 50000.0 * 100.0 y_slope *= 50000.0 * 100.0 print('With BF turned off, x_slope = %.3f, y_slope = %.3f %% per 50K e-'%(x_slope, y_slope )) assert abs(x_slope) < 0.5 assert abs(y_slope) < 0.5
def test_silicon_fft(): """Test that drawing with method='fft' also works for SiliconSensor. """ # Lower this somewhat so we get more accurate fluxes from the FFT. # (Still only accurate to 3 d.p. though.) gsparams = galsim.GSParams(maxk_threshold=1.e-5) obj = galsim.Gaussian(flux=3539, sigma=0.3, gsparams=gsparams) # We'll draw the same object using SiliconSensor, Sensor, and the default (sensor=None) im1 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=silicon im2 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=simple im3 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=None rng = galsim.BaseDeviate(5678) silicon = galsim.SiliconSensor(rng=rng, diffusion_factor=0.0) simple = galsim.Sensor() obj.drawImage(im1, method='fft', sensor=silicon, rng=rng) obj.drawImage(im2, method='fft', sensor=simple, rng=rng) obj.drawImage(im3, method='fft') printval(im1,im2) r1 = im1.calculateMomentRadius(flux=obj.flux) r2 = im2.calculateMomentRadius(flux=obj.flux) r3 = im3.calculateMomentRadius(flux=obj.flux) print('Flux = %.0f: sum peak radius'%obj.flux) print('im1: %.1f %.2f %f'%(im1.array.sum(),im1.array.max(), r1)) print('im2: %.1f %.2f %f'%(im2.array.sum(),im2.array.max(), r2)) print('im3: %.1f %.2f %f'%(im3.array.sum(),im3.array.max(), r3)) # First, im2 and im3 should be almost exactly equal. Not precisely, since im2 was made with # subsampling and then binning, so the FFT ringing is different (im3 is worse in this regard, # since it used convolution with a larger pixel). So 3 digits is all we manage to get here. np.testing.assert_almost_equal(im2.array, im3.array, decimal=3) # im1 should be similar, but not equal np.testing.assert_almost_equal(im1.array/obj.flux, im2.array/obj.flux, decimal=2) # Fluxes should all equal obj.flux np.testing.assert_almost_equal(im1.array.sum(), obj.flux, decimal=3) np.testing.assert_almost_equal(im2.array.sum(), obj.flux, decimal=3) np.testing.assert_almost_equal(im3.array.sum(), obj.flux, decimal=3) np.testing.assert_almost_equal(im1.added_flux, obj.flux, decimal=3) np.testing.assert_almost_equal(im2.added_flux, obj.flux, decimal=3) np.testing.assert_almost_equal(im3.added_flux, obj.flux, decimal=3) # Sizes are all about equal since flux is not large enough for B/F to be significant sigma_r = 1./np.sqrt(obj.flux) * im1.scale np.testing.assert_allclose(r1, r2, atol=2.*sigma_r) np.testing.assert_allclose(r2, r3, atol=2.*sigma_r) # Repeat with 20X more photons where the brighter-fatter effect should kick in more. obj *= 200 obj.drawImage(im1, method='fft', sensor=silicon, rng=rng) obj.drawImage(im2, method='fft', sensor=simple, rng=rng) obj.drawImage(im3, method='fft') r1 = im1.calculateMomentRadius(flux=obj.flux) r2 = im2.calculateMomentRadius(flux=obj.flux) r3 = im3.calculateMomentRadius(flux=obj.flux) print('Flux = %.0f: sum peak radius'%obj.flux) print('im1: %.1f %.2f %f'%(im1.array.sum(),im1.array.max(), r1)) print('im2: %.1f %.2f %f'%(im2.array.sum(),im2.array.max(), r2)) print('im3: %.1f %.2f %f'%(im3.array.sum(),im3.array.max(), r3)) # Fluxes should still be fine. np.testing.assert_almost_equal(im1.array.sum(), obj.flux, decimal=1) np.testing.assert_almost_equal(im2.array.sum(), obj.flux, decimal=1) np.testing.assert_almost_equal(im3.array.sum(), obj.flux, decimal=1) np.testing.assert_almost_equal(im1.added_flux, obj.flux, decimal=1) np.testing.assert_almost_equal(im2.added_flux, obj.flux, decimal=1) np.testing.assert_almost_equal(im3.added_flux, obj.flux, decimal=1) # Sizes for 2,3 should be about equal, but 1 should be larger. sigma_r = 1./np.sqrt(obj.flux) * im1.scale print('check |r2-r3| = %f <? %f'%(np.abs(r2-r3), 2.*sigma_r)) np.testing.assert_allclose(r2, r3, atol=2.*sigma_r) print('check |r1-r3| = %f >? %f'%(np.abs(r1-r3), 2.*sigma_r)) assert r1-r3 > 2.*sigma_r
fig, ax = plt.subplots() oversampling = 16 for filter in ['g', 'z', 'y']: bandpass = galsim.Bandpass("LSST_{}.dat".format(filter), wave_type='nm') Ts = [] for depth in depths: depth_pix = depth / 10 surface_ops = [ galsim.WavelengthSampler(sed, bandpass, rng=bd), galsim.FRatioAngles(1.234, 0.606, rng=bd), galsim.FocusDepth(depth_pix), galsim.Refraction(3.9) # approx number for Silicon ] img = obj.drawImage( sensor=galsim.SiliconSensor(), method='phot', n_photons=1_000_000, surface_ops=surface_ops, scale=0.2 / oversampling, # oversample pixels to better resolve PSF size nx=32 * oversampling, # 6.4 arcsec stamp ny=32 * oversampling, ) Ts.append(img.calculateMomentRadius()) Ts = np.array(Ts) / 0.2 * 10 * oversampling # convert arcsec -> micron ax.scatter(depths, Ts, label=filter) ax.set_ylim(2, 7) ax.axvline(0, c='k') ax.legend() ax.set_xlabel("<- intrafocal focus(micron) extrafocal ->")
def main(argv): """ Make images to be used for characterizing the brighter-fatter effect - Each fits file is 10 x 10 postage stamps. - Each postage stamp is 40 x 40 pixels. - There are 2 fits files, one with tree rings and one without - Each image is in output/tr_nfile.fits, where nfile ranges from 1-2. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("tr_plots") # Define some parameters we'll use below. # Normally these would be read in from some parameter file. nx_tiles = 10 # ny_tiles = 10 # stamp_xsize = 40 # stamp_ysize = 40 # random_seed = 6424512 # pixel_scale = 0.2 # arcsec / pixel sky_level = 0.01 # ADU / arcsec^2 # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') gal_sigma = 1.0 # pixels psf_sigma = 0.01 # pixels pixel_scale = 1.0 noise = 0.01 # standard deviation of the counts in each pixel logger.info('Starting tr_plots using:') logger.info(' - image with %d x %d postage stamps',nx_tiles,ny_tiles) logger.info(' - postage stamps of size %d x %d pixels',stamp_xsize,stamp_ysize) rng = galsim.BaseDeviate(5678) sensor1 = galsim.SiliconSensor(rng=rng, diffusion_factor=0.0) # The following makes a sensor with hugely magnified tree rings so you can see the effect sensor2 = galsim.SiliconSensor(rng=rng, diffusion_factor = 0.0, treeringamplitude = 1.00) for nfile in range(1,3): starttime = time.time() exec("sensor = sensor%d"%nfile) gal_file_name = os.path.join('output','tr_%d.fits'%nfile) sex_file_name = os.path.join('output','tr_%d_SEX.fits.cat.reg'%nfile) sexfile = open(sex_file_name, 'w') gal_flux = 2.0e5 # total counts on the image # Define the galaxy profile gal = galsim.Gaussian(flux=gal_flux, sigma=gal_sigma) logger.debug('Made galaxy profile') # Define the PSF profile psf = galsim.Gaussian(flux=1., sigma=psf_sigma) # PSF flux should always = 1 logger.debug('Made PSF profile') # This profile is placed with different orientations and noise realizations # at each postage stamp in the gal image. gal_image = galsim.ImageF(stamp_xsize * nx_tiles-1 , stamp_ysize * ny_tiles-1, scale=pixel_scale) psf_image = galsim.ImageF(stamp_xsize * nx_tiles-1 , stamp_ysize * ny_tiles-1, scale=pixel_scale) k = 0 for iy in range(ny_tiles): for ix in range(nx_tiles): # The normal procedure for setting random numbers in GalSim is to start a new # random number generator for each object using sequential seed values. # This sounds weird at first (especially if you were indoctrinated by Numerical # Recipes), but for the boost random number generator we use, the "random" # number sequences produced from sequential initial seeds are highly uncorrelated. # # The reason for this procedure is that when we use multiple processes to build # our images, we want to make sure that the results are deterministic regardless # of the way the objects get parcelled out to the different processes. # # Of course, this script isn't using multiple processes, so it isn't required here. # However, we do it nonetheless in order to get the same results as the config # version of this demo script (demo5.yaml). ud = galsim.UniformDeviate(random_seed+k) # Any kind of random number generator can take another RNG as its first # argument rather than a seed value. This makes both objects use the same # underlying generator for their pseudo-random values. #gd = galsim.GaussianDeviate(ud, sigma=gal_ellip_rms) # The -1's in the next line are to provide a border of # 1 pixel between postage stamps b = galsim.BoundsI(ix*stamp_xsize+1 , (ix+1)*stamp_xsize-1, iy*stamp_ysize+1 , (iy+1)*stamp_ysize-1) sub_gal_image = gal_image[b] sub_psf_image = psf_image[b] #print "ix = %d, iy = %d, cenx = %d, ceny = %d"%(ix,iy,sub_gal_image.center().x,sub_gal_image.center().y) # Make the final image, convolving with the (unshifted) psf final_gal = galsim.Convolve([psf,gal]) # Draw the image final_gal.drawImage(sub_gal_image, method = 'phot', sensor=sensor, rng = rng) x = b.center().x y = b.center().y k = k+1 sexline = 'circle %f %f %f\n'%(x,y,gal_sigma/pixel_scale) sexfile.write(sexline) sexfile.close() logger.info('Done making images of postage stamps') # Now write the images to disk. #psf_image.write(psf_file_name) #logger.info('Wrote PSF file %s',psf_file_name) gal_image.write(gal_file_name) logger.info('Wrote image to %r',gal_file_name) # using %r adds quotes around filename for us finishtime = time.time() print("Time to complete file %d = %.2f seconds\n"%(nfile, finishtime-starttime))
def process_photons(self, image, skyCounts, detector, chunk_size=int(5e6)): tree_rings = detector.tree_rings # Add photons by amplifier since a full 4k x 4k sensor uses too much # memory to represent all of the pixel vertices. # imSim images use the Camera Coordinate System where the # parallel transfer direction is along the x-axis: nx, ny = 2, 8 nrow, ncol = image.array.shape dx = ncol // nx # number of pixels in x for an amp dy = nrow // ny # number of pixels in y config = get_config() # Disable the updating of the pixel boundaries by # setting nrecalc to 1e300 nrecalc = 1e300 sensor = galsim.SiliconSensor(rng=self.randomNumbers, nrecalc=nrecalc, strength=config['ccd']['bf_strength'], treering_center=tree_rings.center, treering_func=tree_rings.func, transpose=True) for i in range(nx): # galsim boundaries start at 1 and include pixels at both ends. xmin = i * dx + 1 xmax = (i + 1) * dx for j in range(ny): self.logger.info("ESOSiliconSkyModel: processing amp %d" % (i * ny + j + 1)) ymin = j * dy + 1 ymax = (j + 1) * dy # Actual bounds of the amplifier region. amp_bounds = galsim.BoundsI(xmin, xmax, ymin, ymax) # Create a temporary image to contain the single amp data # with a 1-pixel buffer around the perimeter. temp_image = galsim.ImageF(ncol, nrow) bounds = ( galsim.BoundsI(xmin - 1, xmax + 1, ymin - 1, ymax + 1) & temp_image.bounds) temp_amp = temp_image[bounds] nphotons = self.get_nphotons(temp_amp, skyCounts) chunks = [chunk_size] * (nphotons // chunk_size) if nphotons % chunk_size > 0: chunks.append(nphotons % chunk_size) for ichunk, nphot in enumerate(chunks): photon_array = self.get_photon_array(temp_amp, nphot) self.logger.info("chunk %d of %d", ichunk + 1, len(chunks)) self.waves.applyTo(photon_array) self.angles.applyTo(photon_array) # Accumulate the photons on the temporary amp image. sensor.accumulate(photon_array, temp_amp, resume=(ichunk > 0)) # Add the temp_amp image to the final image, excluding # the 1-pixel buffer. image[amp_bounds] += temp_image[amp_bounds] return image
# @param N The number of photons to store in this PhotonArray. This value cannot be # changed. # @param x Optionally, the initial x values. [default: None] # @param y Optionally, the initial y values. [default: None] # @param flux Optionally, the initial flux values. [default: None] # @param dxdz Optionally, the initial dxdz values. [default: None] # @param dydz Optionally, the initial dydz values. [default: None] # @param wavelength Optionally, the initial wavelength values. [default: None] # """ photons.x = 0.5 photons.y = 0.5 photons.flux = 1. # Accumulate these photons sensor = galsim.SiliconSensor(strength=1, diffusion_factor=1, qdist=4, name='lsst_itl_32') # """-------------------------------------------------- # A model of a silicon-based CCD sensor that converts photons to electrons at a wavelength- # dependent depth (probabilistically) and drifts them down to the wells, properly taking # into account the repulsion of previously accumulated electrons (known as the brighter-fatter # effect). # There are currently three sensors shipped with GalSim, which you can specify as the `name` # parameter mentioned below. # lsst_itl_8 The ITL sensor being used for LSST, using 8 points along each side of the # pixel boundaries. # lsst_itl_32 The ITL sensor being used for LSST, using 32 points along each side of the # pixel boundaries. (This is more accurate than the lsst_itl_8, but slower.) # lsst_etv_32 The ETV sensor being used for LSST, using 32 points along each side of the
def addNoiseAndBackground(self, image, bandpass=None, m5=None, FWHMeff=None, photParams=None, detector=None): """ Add the sky level counts to the image, rescale by the distorted pixel areas to account for tree rings, etc., then add Poisson noise. This implementation is based on GalSim/devel/lsst/treering_skybg2.py. """ if detector is None: raise RuntimeError("A GalSimDetector object must be provided.") # Create a SiliconSensor object to handle the calculations # of the quantities related to the pixel boundary distortions # from electrostatic effects such as tree rings, etc.. # Use the transpose=True option since "eimages" of LSST sensors # follow the Camera Coordinate System convention where the # parallel transfer direction is along the x-axis. config = get_config() nrecalc = 1e300 # disable pixel boundary updating. sensor = galsim.SiliconSensor( rng=self.randomNumbers, nrecalc=nrecalc, strength=config['ccd']['bf_strength'], treering_func=detector.tree_rings.func, treering_center=detector.tree_rings.center, transpose=True) # Loop over 1/2 amplifiers to save memory when storing the 36 # pixel vertices per pixel. The 36 vertices arise from the 8 # vertices per side + 4 corners (8*(4 sides) + 4 = 36). This # corresponds to 72 floats per pixel (x and y coordinates) for # representing the pixel distortions in memory. nrow, ncol = image.array.shape nx, ny = 4, 8 dx = ncol // nx dy = nrow // ny for i in range(nx): xmin = i * dx + 1 xmax = (i + 1) * dx for j in range(ny): self.logger.debug( "FastSiliconSkyModel: processing amp region %d" % (i * ny + j + 1)) ymin = j * dy + 1 ymax = (j + 1) * dy # Create a temporary image with the detector wcs to # contain the single amp data. temp_image = galsim.ImageF(ncol, nrow, wcs=detector.wcs) # Include a 2-pixel buffer around the perimeter of the # amp region to account for charge redistribution # across pixel boundaries into and out of neighboring # segments. buf = 2 bounds = (galsim.BoundsI(xmin - buf, xmax + buf, ymin - buf, ymax + buf) & temp_image.bounds) temp_amp = temp_image[bounds] # Compute the pixel areas as distorted by tree rings, etc.. sensor_area = sensor.calculate_pixel_areas(temp_amp) # Apply distortion from wcs. temp_amp.wcs.makeSkyImage(temp_amp, sky_level=1.) # Since sky_level was 1, the temp_amp array at this # point contains the pixel areas. mean_pixel_area = temp_amp.array.mean() # Scale by the sky counts. temp_amp *= self.sky_counts(detector.name) / mean_pixel_area # Include pixel area scaling from electrostatic distortions. temp_amp *= sensor_area # Add Poisson noise. noise = galsim.PoissonNoise(self.randomNumbers) temp_amp.addNoise(noise) # Actual bounds of the amplifier segment to be filled. amp_bounds = galsim.BoundsI(xmin, xmax, ymin, ymax) # Add the single amp image to the final full CCD image. image[amp_bounds] += temp_image[amp_bounds] return image
def make_flat(gs_det, counts_per_iter, niter, rng, buf=2, logger=None, wcs=None): """ Create a flat by successively adding lower level flat sky images. This is based on https://github.com/GalSim-developers/GalSim/blob/releases/2.0/devel/lsst/treering_flat.py Full LSST CCDs are assembled one amp at a time to limit the memory used by the galsim.SiliconSensor model. Parameters ---------- gs_det: GalSimDetector The detector in the LSST focalplane to use. This object contains the CCD pixel geometry, WCS, and treering info. counts_per_iter: int The mean number of electrons/pixel to add at each iteration. niter: int Number of iterations. Final flat will have niter*counts_per_iter electrons/pixel. rng: galsim.BaseDeviate Random number generator. buf: int [2] Pixel buffer around each to account for charge redistribution across pixel boundaries. logger: logging.Logger [None] Logging object. If None, then a default logger will be used. wcs: galsim.WCS [None] Alternative WCS object. If None, then gs_det.wcs will be used. Returns ------- galsim.ImageF """ config = desc.imsim.get_config() if logger is None: logger = desc.imsim.get_logger('DEBUG', name='make_flat') if wcs is None: wcs = gs_det.wcs ncol = gs_det.xMaxPix - gs_det.xMinPix + 1 nrow = gs_det.yMaxPix - gs_det.yMinPix + 1 flat = galsim.ImageF(ncol, nrow, wcs=wcs) sensor = galsim.SiliconSensor(rng=rng, strength=config['ccd']['bf_strength'], treering_center=gs_det.tree_rings.center, treering_func=gs_det.tree_rings.func, transpose=True) # Create a noise-free base image to add at each iteration. base_image = galsim.ImageF(ncol, nrow, wcs=wcs) base_image.wcs.makeSkyImage(base_image, sky_level=1.) mean_pixel_area = base_image.array.mean() sky_level_per_iter = counts_per_iter / mean_pixel_area base_image *= sky_level_per_iter noise = galsim.PoissonNoise(rng) # Build up the full CCD by amplifier segment to limit the memory # used by the silicon model. nx, ny = 2, 8 dx = ncol // nx dy = nrow // ny for i in range(nx): xmin = i * dx + 1 xmax = (i + 1) * dx for j in range(ny): logger.info("amp: %d, %d", i, j) ymin = j * dy + 1 ymax = (j + 1) * dy temp_image = galsim.ImageF(ncol, nrow, wcs=wcs) bounds = ( galsim.BoundsI(xmin - buf, xmax + buf, ymin - buf, ymax + buf) & temp_image.bounds) temp_amp = temp_image[bounds] for it in range(niter): logger.debug("iter %d", it) temp = sensor.calculate_pixel_areas(temp_amp) temp /= temp.array.mean() temp *= base_image[bounds] temp.addNoise(noise) temp_amp += temp amp_bounds = galsim.BoundsI(xmin, xmax, ymin, ymax) flat[amp_bounds] += temp_image[amp_bounds] return flat
def main(argv): """ About as simple as it gets: - Use a circular Gaussian profile for the galaxy. - Convolve it by a circular Gaussian PSF. - Add Gaussian noise to the image. """ # In non-script code, use getLogger(__name__) at module scope instead. logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("demo1") # MOD crank up the flux data_index = 1 flux_factor = 1 gal_flux = 2.e6 * flux_factor # total counts on the image gal_sigma = 2. # arcsec psf_sigma = 1. # arcsec # MOD decrease the resolution pixel_scale = 2.0 # arcsec / pixel noise = 30. # standard deviation of the counts in each pixel logger.info('Starting demo script 1 using:') logger.info(' - circular Gaussian galaxy (flux = %.1e, sigma = %.1f),',gal_flux,gal_sigma) logger.info(' - circular Gaussian PSF (sigma = %.1f),',psf_sigma) logger.info(' - pixel scale = %.2f,',pixel_scale) logger.info(' - Gaussian noise (sigma = %.2f).',noise) # Define the galaxy profile gal = galsim.Gaussian(flux=gal_flux, sigma=gal_sigma) logger.debug('Made galaxy profile') # Define the PSF profile psf = galsim.Gaussian(flux=1., sigma=psf_sigma) # PSF flux should always = 1 logger.debug('Made PSF profile') # Final profile is the convolution of these # Can include any number of things in the list, all of which are convolved # together to make the final flux profile. final = galsim.Convolve([gal, psf]) logger.debug('Convolved components into final profile') # Draw the image with a particular pixel scale, given in arcsec/pixel. # The returned image has a member, added_flux, which is gives the total flux actually added to # the image. One could use this value to check if the image is large enough for some desired # accuracy level. Here, we just ignore it. # MOD use the 'phot' method with an ideal sensor image = final.drawImage(scale=pixel_scale, method='phot', sensor=galsim.Sensor()) logger.debug('Made image of the profile: flux = %f, added_flux = %f', gal_flux, image.added_flux) file_name = os.path.join('output', 'spot_nobf_' + str(data_index) + '.fits') # Write the image to a file if not os.path.isdir('output'): os.mkdir('output') # Note: if the file already exists, this will overwrite it. image.write(file_name) logger.info('Wrote Ideal image to %r' % file_name) # using %r adds quotes around filename for us results = image.FindAdaptiveMom() logger.info('HSM reports that the image has observed shape and size:') logger.info(' e1 = %.3f, e2 = %.3f, sigma = %.3f (pixels)', results.observed_shape.e1, results.observed_shape.e2, results.moments_sigma) logger.info('Expected values in the limit that pixel response and noise are negligible:') logger.info(' e1 = %.3f, e2 = %.3f, sigma = %.3f', 0.0, 0.0, math.sqrt(gal_sigma ** 2 + psf_sigma ** 2) / pixel_scale) sensor_name = 'lsst_e2v_50_32' # sensor_name = 'lsst_itl_50_32' # MOD use the 'phot' method, with a e2v sensor rng = galsim.BaseDeviate(5678) image = final.drawImage(scale=pixel_scale, method='phot', sensor=galsim.SiliconSensor(name=sensor_name, transpose=False, rng=rng, diffusion_factor=0.0)) logger.debug('Made image of the profile: flux = %f, added_flux = %f', gal_flux, image.added_flux) file_name = os.path.join('output', 'spot_' + sensor_name + '_bf_' + str(data_index) + '.fits') # Add Gaussian noise to the image with specified sigma # MOD actually, don't add any noise # image.addNoise(galsim.GaussianNoise(sigma=noise)) # logger.debug('Added Gaussian noise') logger.debug('No noise added') # Write the image to a file if not os.path.isdir('output'): os.mkdir('output') # Note: if the file already exists, this will overwrite it. image.write(file_name) logger.info('Wrote bfed image to %r' % file_name) # using %r adds quotes around filename for us results = image.FindAdaptiveMom() logger.info('HSM reports that the image has observed shape and size:') logger.info(' e1 = %.3f, e2 = %.3f, sigma = %.3f (pixels)', results.observed_shape.e1, results.observed_shape.e2, results.moments_sigma) logger.info('Expected values in the limit that pixel response and noise are negligible:') logger.info(' e1 = %.3f, e2 = %.3f, sigma = %.3f', 0.0, 0.0, math.sqrt(gal_sigma**2 + psf_sigma**2)/pixel_scale)
def test_flat(): """Test building a flat field image using the Silicon class. """ # Note: This test is based on a script devel/lsst/treering_flat.py if __name__ == '__main__': nx = 200 ny = 200 nflats = 20 niter = 50 toler = 0.01 else: nx = 50 ny = 50 nflats = 3 niter = 20 # Seem to really need 20 or more iterations to get covariances close. toler = 0.05 counts_total = 80.e3 counts_per_iter = counts_total / niter # Silicon sensor with tree rings seed = 31415 rng = galsim.UniformDeviate(seed) treering_func = galsim.SiliconSensor.simple_treerings(0.26, 47) treering_center = galsim.PositionD(0,0) sensor = galsim.SiliconSensor(rng=rng, treering_func=treering_func, treering_center=treering_center) # Use a non-trivial WCS to make sure that works properly. wcs = galsim.FitsWCS('fits_files/tnx.fits') # We add on a border of 2 pixels, since the outer row/col get a little messed up by photons # falling off the edge, but not coming on from the other direction. # We do 2 rows/cols rather than just 1 to be safe, since I think diffusion can probably go # 2 pixels, even though the deficit is only really evident on the outer pixel. nborder = 2 base_image = galsim.ImageF(nx+2*nborder, ny+2*nborder, wcs=wcs) base_image.wcs.makeSkyImage(base_image, sky_level=1.) # Rescale so that the mean sky level per pixel is skyCounts mean_pixel_area = base_image.array.mean() sky_level_per_iter = counts_per_iter / mean_pixel_area # in ADU/arcsec^2 now. base_image *= sky_level_per_iter # The base_image now has the right level to account for the WCS distortion, but not any sensor # effects. # This is the noise-free level that we want to add each iteration modulated by the sensor. noise = galsim.PoissonNoise(rng) flats = [] for n in range(nflats): print('n = ',n) # image is the image that we will build up in steps. image = galsim.ImageF(nx+2*nborder, ny+2*nborder, wcs=wcs) for i in range(niter): # temp is the additional flux we will add to the image in this iteration. # Start with the right area due to the sensor effects. temp = sensor.calculate_pixel_areas(image) temp /= temp.array.mean() # Multiply by the base image to get the right mean level and wcs effects temp *= base_image # Finally, add noise. What we have here so far is the expectation value in each pixel. # We need to realize this according to Poisson statistics with these means. temp.addNoise(noise) # Add this to the image we are building up. image += temp # Cut off the outer border where things don't work quite right. image = image.subImage(galsim.BoundsI(1+nborder,nx+nborder,1+nborder,ny+nborder)) flats.append(image.array) # These are somewhat noisy, so compute for all pairs and average them. mean = var = cov01 = cov10 = cov11a = cov11b = cov02 = cov20 = 0 n = len(flats) npairs = 0 for i in range(n): flati = flats[i] print('mean ',i,' = ',flati.mean()) mean += flati.mean() for j in range(i+1,n): flatj = flats[j] diff = flati - flatj var += diff.var()/2 cov01 += np.mean(diff[1:,:] * diff[:-1,:]) cov10 += np.mean(diff[:,1:] * diff[:,:-1]) cov11a += np.mean(diff[1:,1:] * diff[:-1,:-1]) cov11b += np.mean(diff[1:,:-1] * diff[:-1,1:]) cov02 += np.mean(diff[2:,:] * diff[:-2,:]) cov20 += np.mean(diff[:,2:] * diff[:,:-2]) npairs += 1 mean /= n var /= npairs cov01 /= npairs cov10 /= npairs cov11a /= npairs cov11b /= npairs cov02 /= npairs cov20 /= npairs print('var(diff)/2 = ',var, 0.93*counts_total) print('cov01 = ',cov01, 0.03*counts_total) # Note: I don't actually know if these are print('cov10 = ',cov10, 0.015*counts_total) # the right covariances... print('cov11a = ',cov11a, cov11a/counts_total) print('cov11b = ',cov11b, cov11b/counts_total) print('cov02 = ',cov02, cov02/counts_total) print('cov20 = ',cov20, cov20/counts_total) # Mean should be close to target counts np.testing.assert_allclose(mean, counts_total, rtol=toler) # Variance is a bit less than the mean due to B/F. np.testing.assert_allclose(var, 0.93 * counts_total, rtol=toler) # 01 and 10 covariances are significant. np.testing.assert_allclose(cov01, 0.03 * counts_total, rtol=30*toler) np.testing.assert_allclose(cov10, 0.015 * counts_total, rtol=60*toler) # The rest are small np.testing.assert_allclose(cov11a / counts_total, 0., atol=2*toler) np.testing.assert_allclose(cov11b / counts_total, 0., atol=2*toler) np.testing.assert_allclose(cov20 / counts_total, 0., atol=2*toler) np.testing.assert_allclose(cov02 / counts_total, 0., atol=2*toler)
def test_poisson(): """Test the Poisson noise builder """ scale = 0.3 sky = 200 config = { 'image' : { 'type' : 'Single', 'random_seed' : 1234, 'pixel_scale' : scale, 'size' : 32, 'noise' : { 'type' : 'Poisson', 'sky_level' : sky, } }, 'gal' : { 'type' : 'Gaussian', 'sigma' : 1.1, 'flux' : 100, }, } # First build by hand rng = galsim.BaseDeviate(1234 + 1) gal = galsim.Gaussian(sigma=1.1, flux=100) im1a = gal.drawImage(nx=32, ny=32, scale=scale) sky_pixel = sky * scale**2 im1a.addNoise(galsim.PoissonNoise(rng, sky_level=sky_pixel)) # Compare to what config builds im1b = galsim.config.BuildImage(config) np.testing.assert_equal(im1b.array, im1a.array) # Check noise variance var1 = galsim.config.CalculateNoiseVariance(config) np.testing.assert_equal(var1, sky_pixel) var2 = galsim.Image(3,3) galsim.config.AddNoiseVariance(config, var2) np.testing.assert_almost_equal(var2.array, sky_pixel) # Check include_obj_var=True var3 = galsim.Image(32,32) galsim.config.AddNoiseVariance(config, var3, include_obj_var=True) np.testing.assert_almost_equal(var3.array, sky_pixel + im1a.array) # Repeat using photon shooting, which needs to do something slightly different, since the # signal photons already have shot noise. rng.seed(1234 + 1) im2a = gal.drawImage(nx=32, ny=32, scale=scale, method='phot', rng=rng) # Need to add Poisson noise for the sky, but not the signal (which already has shot noise) im2a.addNoise(galsim.DeviateNoise(galsim.PoissonDeviate(rng, mean=sky_pixel))) im2a -= sky_pixel # Compare to what config builds galsim.config.RemoveCurrent(config) config['image']['draw_method'] = 'phot' # Make sure it gets copied over to stamp properly. del config['stamp']['draw_method'] del config['stamp']['_done'] im2b = galsim.config.BuildImage(config) np.testing.assert_equal(im2b.array, im2a.array) # Check non-trivial sky image galsim.config.RemoveCurrent(config) config['image']['sky_level'] = sky config['image']['wcs'] = { 'type' : 'UVFunction', 'ufunc' : '0.05*x + 0.001*x**2', 'vfunc' : '0.05*y + 0.001*y**2', } del config['image']['pixel_scale'] del config['wcs'] rng.seed(1234+1) wcs = galsim.UVFunction(ufunc='0.05*x + 0.001*x**2', vfunc='0.05*y + 0.001*y**2') im3a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng) sky_im = galsim.Image(im3a.bounds, wcs=wcs) wcs.makeSkyImage(sky_im, sky) im3a += sky_im # Add 1 copy of the raw sky image for image[sky] noise_im = sky_im.copy() noise_im *= 2. # Now 2x because the noise includes both in image[sky] and noise[sky] noise_im.addNoise(galsim.PoissonNoise(rng)) noise_im -= 2.*sky_im im3a += noise_im im3b = galsim.config.BuildImage(config) np.testing.assert_almost_equal(im3b.array, im3a.array, decimal=6) # With tree rings, the sky includes them as well. config['image']['sensor'] = { 'type' : 'Silicon', 'treering_func' : { 'type' : 'File', 'file_name' : 'tree_ring_lookup.dat', 'amplitude' : 0.5 }, 'treering_center' : { 'type' : 'XY', 'x' : 0, 'y' : -500 } } galsim.config.RemoveCurrent(config) config = galsim.config.CleanConfig(config) rng.seed(1234+1) trfunc = galsim.LookupTable.from_file('tree_ring_lookup.dat', amplitude=0.5) sensor = galsim.SiliconSensor(treering_func=trfunc, treering_center=galsim.PositionD(0,-500), rng=rng) im4a = gal.drawImage(nx=32, ny=32, wcs=wcs, method='phot', rng=rng, sensor=sensor) sky_im = galsim.Image(im3a.bounds, wcs=wcs) wcs.makeSkyImage(sky_im, sky) areas = sensor.calculate_pixel_areas(sky_im, use_flux=False) sky_im *= areas im4a += sky_im noise_im = sky_im.copy() noise_im *= 2. noise_im.addNoise(galsim.PoissonNoise(rng)) noise_im -= 2.*sky_im im4a += noise_im im4b = galsim.config.BuildImage(config) np.testing.assert_almost_equal(im4b.array, im4a.array, decimal=6) # Can't have both sky_level and sky_level_pixel config['image']['noise']['sky_level_pixel'] = 2000. with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # Must have a valid noise type del config['image']['noise']['sky_level_pixel'] config['image']['noise']['type'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # noise must be a dict config['image']['noise'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # Can't have signal_to_noise and flux config['image']['noise'] = { 'type' : 'Poisson', 'sky_level' : sky } config['gal']['signal_to_noise'] = 100 with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # This should work del config['gal']['flux'] galsim.config.BuildImage(config) # These now hit the errors in CalculateNoiseVariance rather than AddNoise config['image']['noise']['type'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) config['image']['noise'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) del config['image']['noise'] with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) # If rather than signal_to_noise, we have an extra_weight output, then it hits # a different error. config['gal']['flux'] = 100 del config['gal']['signal_to_noise'] config['output'] = { 'weight' : {} } config['image']['noise'] = { 'type' : 'Poisson', 'sky_level' : sky } galsim.config.SetupExtraOutput(config) galsim.config.SetupConfigFileNum(config, 0, 0, 0) # This should work again. galsim.config.BuildImage(config) config['image']['noise']['type'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config) config['image']['noise'] = 'Invalid' with assert_raises(galsim.GalSimConfigError): galsim.config.BuildImage(config)
desc.imsim.read_config() instcat_file = os.path.join(os.environ['IMSIM_DIR'], 'tests', 'tiny_instcat.txt') instcat = desc.imsim.parsePhoSimInstanceFile(instcat_file) pixel_scale = 0.2 skymodel = desc.imsim.ESOSiliconSkyModel(instcat.obs_metadata, instcat.phot_params, seed=args.seed, addNoise=False, addBackground=True, bundles_per_pix=args.bundles_per_pix) waves = skymodel.waves angles = skymodel.angles for nxy in np.logspace(np.log10(args.nxy_min), np.log10(args.nxy_max), args.npts): image = galsim.Image(int(nxy), int(nxy), scale=pixel_scale) nrecalc = int(args.nrecalc_factor * np.prod(image.array.shape)) sensor = galsim.SiliconSensor(rng=skymodel.randomNumbers, nrecalc=nrecalc) photon_array = skymodel.get_photon_array(image, args.nphot) waves.applyTo(photon_array) angles.applyTo(photon_array) t0 = time.clock() sensor.accumulate(photon_array, image) print(int(nxy)**2, time.clock() - t0) sys.stdout.flush()
# # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions, and the disclaimer given in the accompanying LICENSE # file. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the disclaimer given in the documentation # and/or other materials provided with the distribution. # from __future__ import print_function import galsim obj = galsim.Gaussian(flux=3.539e6, sigma=0.1) rng = galsim.BaseDeviate(5678) silicon = galsim.SiliconSensor(rng=rng) im = obj.drawImage(nx=17, ny=17, scale=0.3, dtype=float, method='phot', sensor=silicon) im.setCenter(0,0) im.write('test.fits') print('im min = ',im.array.min()) print('im max = ',im.array.max()) print('im(0,0) = ',im(0,0)) print('+- 1 along column: ',im(0,1),im(0,-1)) print('+- 1 along row: ',im(1,0),im(-1,0)) area_image = silicon.calculate_pixel_areas(im) area_image.write('area.fits') print('area min = ',area_image.array.min()) print('area max = ',area_image.array.max())
niter = int(counts_total / counts_per_iter + 0.5) counts_per_iter = counts_total / niter # Recalculate in case not even multiple. print('Total counts = {} = {} * {}'.format(counts_total, niter, counts_per_iter)) # Not an LSST wcs, but just make sure this works properly with a non-trivial wcs. wcs = galsim.FitsWCS('../../tests/fits_files/tnx.fits') base_image = galsim.ImageF(nx + 2 * nborder, ny + 2 * nborder, wcs=wcs) print('image bounds = ', base_image.bounds) # nrecalc is actually irrelevant here, since a recalculation will be forced on each iteration. # Which is really the point. We need to set coundsPerIter appropriately so that the B/F effect # doesn't change *too* much between iterations. sensor = galsim.SiliconSensor(rng=rng, treering_func=treering_func, treering_center=treering_center) # We also need to account for the distortion of the wcs across the image. # This expects sky_level in ADU/arcsec^2, not ADU/pixel. base_image.wcs.makeSkyImage(base_image, sky_level=1.) base_image.write('wcs_area.fits') # Rescale so that the mean sky level per pixel is skyCounts mean_pixel_area = base_image.array.mean() sky_level_per_iter = counts_per_iter / mean_pixel_area # in ADU/arcsec^2 now. base_image *= sky_level_per_iter # The base_image has the right level to account for the WCS distortion, but not any sensor effects. # This is the noise-free level that we want to add each iteration modulated by the sensor.
spectrum_image.write(nobf_filename) if display: galsim_sensor_image = spectrum_image.array.copy() # now do it again, but with the BF effect # spectrum_image = galsim.Image(smeared_spectrum2d, scale=.25) # scale is angstroms/pixel # interpolate the image so GalSim can manipulate it # spectrum_interpolated = galsim.InterpolatedImage(spectrum_image) spectrum_interpolated.drawImage( image=spectrum_image, method='phot', # center=(15,57), offset=(0, 0), # this needs 4 digits sensor=galsim.SiliconSensor(name='lsst_e2v_50_32', transpose=False, rng=rng, diffusion_factor=0.0)) print('image center:', spectrum_image.center) print('image true center:', spectrum_image.true_center) spectrum_image.write(yesbf_filename) if display: galsim_bf_image = spectrum_image.array.copy() ''' block comment out the center correction apparently the correction is flux dependant, and therefore part of the data """Measure the offset between the two data sets:""" with fits.open(nobf_filename) as hdul: galsim_sensor_image = hdul[0].data with fits.open(yesbf_filename) as hdul: galsim_bf_image = hdul[0].data
def test_silicon(): """Test the basic construction and use of the SiliconSensor class. """ # Note: Use something quite small in terms of npixels so the B/F effect kicks in without # requiring a ridiculous number of photons obj = galsim.Gaussian(flux=10000, sigma=0.3) # We'll draw the same object using SiliconSensor, Sensor, and the default (sensor=None) im1 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=silicon im2 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=simple im3 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=None rng1 = galsim.BaseDeviate(5678) rng2 = galsim.BaseDeviate(5678) rng3 = galsim.BaseDeviate(5678) silicon = galsim.SiliconSensor(rng=rng1, diffusion_factor=0.0) simple = galsim.Sensor() # Start with photon shooting, since that's more straightforward. obj.drawImage(im1, method='phot', poisson_flux=False, sensor=silicon, rng=rng1) obj.drawImage(im2, method='phot', poisson_flux=False, sensor=simple, rng=rng2) obj.drawImage(im3, method='phot', poisson_flux=False, rng=rng3) # First, im2 and im3 should be exactly equal. np.testing.assert_array_equal(im2.array, im3.array) # im1 should be similar, but not equal np.testing.assert_almost_equal(im1.array/obj.flux, im2.array/obj.flux, decimal=2) # Now use a different seed for 3 to see how much of the variation is just from randomness. rng3.seed(234241) obj.drawImage(im3, method='phot', poisson_flux=False, rng=rng3) r1 = im1.calculateMomentRadius(flux=obj.flux) r2 = im2.calculateMomentRadius(flux=obj.flux) r3 = im3.calculateMomentRadius(flux=obj.flux) print('Flux = %.0f: sum peak radius'%obj.flux) print('im1: %.1f %.2f %f'%(im1.array.sum(),im1.array.max(), r1)) print('im2: %.1f %.2f %f'%(im2.array.sum(),im2.array.max(), r2)) print('im3: %.1f %.2f %f'%(im3.array.sum(),im3.array.max(), r3)) # Fluxes should all equal obj.flux np.testing.assert_almost_equal(im1.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im2.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im3.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im1.added_flux, obj.flux, decimal=6) np.testing.assert_almost_equal(im2.added_flux, obj.flux, decimal=6) np.testing.assert_almost_equal(im3.added_flux, obj.flux, decimal=6) # Sizes are all about equal since flux is not large enough for B/F to be significant # Variance of Irr for Gaussian with Poisson noise is # Var(Irr) = Sum(I r^4) = 4Irr [using Gaussian kurtosis = 8sigma^2, Irr = 2sigma^2] # r = sqrt(Irr/flux), so sigma(r) = 1/2 r sqrt(Var(Irr))/Irr = 1/sqrt(flux) # Use 2sigma for below checks. sigma_r = 1. / np.sqrt(obj.flux) * im1.scale np.testing.assert_allclose(r1, r2, atol=2.*sigma_r) np.testing.assert_allclose(r2, r3, atol=2.*sigma_r) # Repeat with 100X more photons where the brighter-fatter effect should kick in more. obj *= 100 rng1 = galsim.BaseDeviate(5678) rng2 = galsim.BaseDeviate(5678) rng3 = galsim.BaseDeviate(5678) obj.drawImage(im1, method='phot', poisson_flux=False, sensor=silicon, rng=rng1) obj.drawImage(im2, method='phot', poisson_flux=False, sensor=simple, rng=rng2) obj.drawImage(im3, method='phot', poisson_flux=False, rng=rng3) r1 = im1.calculateMomentRadius(flux=obj.flux) r2 = im2.calculateMomentRadius(flux=obj.flux) r3 = im3.calculateMomentRadius(flux=obj.flux) print('Flux = %.0f: sum peak radius'%obj.flux) print('im1: %.1f %.2f %f'%(im1.array.sum(),im1.array.max(), r1)) print('im2: %.1f %.2f %f'%(im2.array.sum(),im2.array.max(), r2)) print('im3: %.1f %.2f %f'%(im3.array.sum(),im3.array.max(), r3)) # Fluxes should still be fine. np.testing.assert_almost_equal(im1.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im2.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im3.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im1.added_flux, obj.flux, decimal=6) np.testing.assert_almost_equal(im2.added_flux, obj.flux, decimal=6) np.testing.assert_almost_equal(im3.added_flux, obj.flux, decimal=6) # Sizes for 2,3 should be about equal, but 1 should be larger. sigma_r = 1. / np.sqrt(obj.flux) * im1.scale print('check |r2-r3| = %f <? %f'%(np.abs(r2-r3), 2.*sigma_r)) np.testing.assert_allclose(r2, r3, atol=2.*sigma_r) print('check r1 - r3 = %f > %f due to brighter-fatter'%(r1-r2,sigma_r)) assert r1 - r3 > 2*sigma_r # Check that it is really responding to flux, not number of photons. # Using fewer shot photons will mean each one encapsulates several electrons at once. obj.drawImage(im1, method='phot', n_photons=1000, poisson_flux=False, sensor=silicon, rng=rng1) obj.drawImage(im2, method='phot', n_photons=1000, poisson_flux=False, sensor=simple, rng=rng2) obj.drawImage(im3, method='phot', n_photons=1000, poisson_flux=False, rng=rng3) r1 = im1.calculateMomentRadius(flux=obj.flux) r2 = im2.calculateMomentRadius(flux=obj.flux) r3 = im3.calculateMomentRadius(flux=obj.flux) print('Flux = %.0f: sum peak radius'%obj.flux) print('im1: %.1f %.2f %f'%(im1.array.sum(),im1.array.max(), r1)) print('im2: %.1f %.2f %f'%(im2.array.sum(),im2.array.max(), r2)) print('im3: %.1f %.2f %f'%(im3.array.sum(),im3.array.max(), r3)) np.testing.assert_almost_equal(im1.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im2.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im3.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im1.added_flux, obj.flux, decimal=6) np.testing.assert_almost_equal(im2.added_flux, obj.flux, decimal=6) np.testing.assert_almost_equal(im3.added_flux, obj.flux, decimal=6) print('check r1 - r3 = %f > %f due to brighter-fatter'%(r1-r2,sigma_r)) assert r1 - r3 > 2*sigma_r # Can also get the stronger BF effect with the strength parameter. obj /= 100 # Back to what it originally was. rng1 = galsim.BaseDeviate(5678) rng2 = galsim.BaseDeviate(5678) rng3 = galsim.BaseDeviate(5678) silicon = galsim.SiliconSensor(dir='lsst_itl', strength=100., rng=rng1, diffusion_factor=0.0) obj.drawImage(im1, method='phot', poisson_flux=False, sensor=silicon, rng=rng1) obj.drawImage(im2, method='phot', poisson_flux=False, sensor=simple, rng=rng2) obj.drawImage(im3, method='phot', poisson_flux=False, rng=rng3) r1 = im1.calculateMomentRadius(flux=obj.flux) r2 = im2.calculateMomentRadius(flux=obj.flux) r3 = im3.calculateMomentRadius(flux=obj.flux) print('Flux = %.0f: sum peak radius'%obj.flux) print('im1: %.1f %.2f %f'%(im1.array.sum(),im1.array.max(), r1)) print('im2: %.1f %.2f %f'%(im2.array.sum(),im2.array.max(), r2)) print('im3: %.1f %.2f %f'%(im3.array.sum(),im3.array.max(), r3)) # Fluxes should still be fine. np.testing.assert_almost_equal(im1.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im2.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im3.array.sum(), obj.flux, decimal=6) np.testing.assert_almost_equal(im1.added_flux, obj.flux, decimal=6) np.testing.assert_almost_equal(im2.added_flux, obj.flux, decimal=6) np.testing.assert_almost_equal(im3.added_flux, obj.flux, decimal=6) # Sizes for 2,3 should be about equal, but 1 should be larger. sigma_r = 1. / np.sqrt(obj.flux) * im1.scale print('check |r2-r3| = %f <? %f'%(np.abs(r2-r3), 2.*sigma_r)) np.testing.assert_allclose(r2, r3, atol=2.*sigma_r) print('check r1 - r3 = %f > %f due to brighter-fatter'%(r1-r2,sigma_r)) assert r1 - r3 > 2*sigma_r / 100 # Check the construction with an explicit directory. s0 = galsim.SiliconSensor(rng=rng1) dir = os.path.join(galsim.meta_data.share_dir, 'sensors', 'lsst_itl') s1 = galsim.SiliconSensor(dir=dir, strength=1.0, rng=rng1, diffusion_factor=1.0, qdist=3, nrecalc=10000) assert s0 == s1 s1 = galsim.SiliconSensor(dir, 1.0, rng1, 1.0, 3, 10000) assert s0 == s1 s2 = galsim.SiliconSensor(rng=rng1, dir='lsst_itl') assert s0 == s2 s3 = galsim.SiliconSensor(rng=rng1, strength=10.) s4 = galsim.SiliconSensor(rng=rng1, diffusion_factor=2.0) s5 = galsim.SiliconSensor(rng=rng1, qdist=4) s6 = galsim.SiliconSensor(rng=rng1, nrecalc=12345) s7 = galsim.SiliconSensor(dir=dir, strength=1.5, rng=rng1, diffusion_factor=1.3, qdist=4, nrecalc=12345) for s in [ s3, s4, s5, s6, s7 ]: assert silicon != s assert s != s0 do_pickle(s0) do_pickle(s1) do_pickle(s7) try: np.testing.assert_raises(IOError, galsim.SiliconSensor, dir='junk') np.testing.assert_raises(IOError, galsim.SiliconSensor, dir='output') np.testing.assert_raises(RuntimeError, galsim.SiliconSensor, rng=3.4) np.testing.assert_raises(TypeError, galsim.SiliconSensor, 'lsst_itl', 'hello') except ImportError: print('The assert_raises tests require nose')
def main(): pr = cProfile.Profile() pr.enable() rng = galsim.UniformDeviate(8675309) wcs = galsim.FitsWCS('../tests/des_data/DECam_00154912_12_header.fits') image = galsim.Image(xsize, ysize, wcs=wcs) bandpass = galsim.Bandpass('LSST_r.dat', wave_type='nm').thin(0.1) base_wavelength = bandpass.effective_wavelength angles = galsim.FRatioAngles(fratio, obscuration, rng) sensor = galsim.SiliconSensor(rng=rng, nrecalc=nrecalc) # Figure out the local_sidereal time from the observation location and time. lsst_lat = '-30:14:23.76' lsst_long = '-70:44:34.67' obs_time = '2012-11-24 03:37:25.023964' # From the header of the wcs file obs = astropy.time.Time(obs_time, scale='utc', location=(lsst_long + 'd', lsst_lat + 'd')) local_sidereal_time = obs.sidereal_time('apparent').value # Convert the ones we need below to galsim Angles. local_sidereal_time *= galsim.hours lsst_lat = galsim.Angle.from_dms(lsst_lat) times = [] mem = [] phot = [] t0 = time.clock() for iobj in range(nobjects): sys.stderr.write('.') psf = make_psf(rng) gal = make_gal(rng) obj = galsim.Convolve(psf, gal) sed = get_sed(rng) waves = galsim.WavelengthSampler(sed=sed, bandpass=bandpass, rng=rng) image_pos = get_pos(rng) sky_coord = wcs.toWorld(image_pos) bounds, offset = calculate_bounds(obj, image_pos, image) ha = local_sidereal_time - sky_coord.ra dcr = galsim.PhotonDCR(base_wavelength=base_wavelength, obj_coord=sky_coord, HA=ha, latitude=lsst_lat) surface_ops = (waves, angles, dcr) obj.drawImage(method='phot', image=image[bounds], offset=offset, rng=rng, sensor=sensor, surface_ops=surface_ops) times.append(time.clock() - t0) mem.append(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) phot.append(obj.flux) image.write('phot.fits') phot = np.cumsum(phot) make_plots(times, mem, phot) pr.disable() ps = pstats.Stats(pr).sort_stats('time') ps.print_stats(20)
def test_sensor_wavelengths_and_angles(): print('Starting test_wavelengths_and_angles') sys.stdout.flush() bppath = os.path.join(galsim.meta_data.share_dir, "bandpasses") sedpath = os.path.join(galsim.meta_data.share_dir, "SEDs") sed = galsim.SED(os.path.join(sedpath, 'CWW_E_ext.sed'), 'nm', 'flambda').thin() # Add the directions (not currently working?? seems to work - CL) fratio = 1.2 obscuration = 0.2 seed = 12345 assigner = galsim.FRatioAngles(fratio, obscuration, seed) obj = galsim.Gaussian(flux=3539, sigma=0.3) if __name__ == "__main__": bands = ['r', 'i', 'z', 'y'] else: bands = ['i'] # Only test the i band for nosetests for band in bands: bandpass = galsim.Bandpass(os.path.join(bppath, 'LSST_%s.dat'%band), 'nm').thin() rng3 = galsim.BaseDeviate(1234) sampler = galsim.WavelengthSampler(sed, bandpass, rng3) rng4 = galsim.BaseDeviate(5678) silicon = galsim.SiliconSensor(rng=rng4, diffusion_factor=0.0) # We'll draw the same object using SiliconSensor im1 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=silicon, no wavelengths im2 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=silicon, with wavelengths im3 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=silicon, with angles im4 = galsim.ImageD(64, 64, scale=0.3) # Will use sensor=silicon, with wavelengths, angles rng1 = galsim.BaseDeviate(5678) rng2 = galsim.BaseDeviate(5678) rng3 = galsim.BaseDeviate(5678) rng4 = galsim.BaseDeviate(5678) # Use photon shooting first obj.drawImage(im1, method='phot', sensor=silicon, rng=rng1) obj.drawImage(im2, method='phot', sensor=silicon, surface_ops=[sampler], rng=rng2) obj.drawImage(im3, method='phot', sensor=silicon, surface_ops=[assigner], rng=rng3) obj.drawImage(im4, method='phot', sensor=silicon, surface_ops=[sampler, assigner], rng=rng4) r1 = im1.calculateMomentRadius(flux=obj.flux) r2 = im2.calculateMomentRadius(flux=obj.flux) r3 = im3.calculateMomentRadius(flux=obj.flux) r4 = im4.calculateMomentRadius(flux=obj.flux) print('Testing Wavelength and Angle sampling - %s band'%band) print('Flux = %.0f: sum peak radius'%obj.flux) print('No lamb, no angles: %.1f %.2f %f'%( im1.array.sum(),im1.array.max(), r1)) print('W/ lamb, no angles: %.1f %.2f %f'%( im2.array.sum(),im2.array.max(), r2)) print('No lamb, w/ angles: %.1f %.2f %f'%( im3.array.sum(),im3.array.max(), r3)) print('W/ lamb, w/ angles: %.1f %.2f %f'%( im4.array.sum(),im4.array.max(), r4)) # r4 should be greater than r1 with wavelengths and angles turned on. sigma_r = 1. / np.sqrt(obj.flux) * im1.scale print('check r4 > r1 due to added wavelengths and angles') print('r1 = %f, r4 = %f, 2*sigma_r = %f'%(r1,r4,2.*sigma_r)) assert r4 > r1 # Now check fft obj.drawImage(im1, method='fft', sensor=silicon, rng=rng1) obj.drawImage(im2, method='fft', sensor=silicon, surface_ops=[sampler], rng=rng2) obj.drawImage(im3, method='fft', sensor=silicon, surface_ops=[assigner], rng=rng3) obj.drawImage(im4, method='fft', sensor=silicon, surface_ops=[sampler, assigner], rng=rng4) r1 = im1.calculateMomentRadius(flux=obj.flux) r2 = im2.calculateMomentRadius(flux=obj.flux) r3 = im3.calculateMomentRadius(flux=obj.flux) r4 = im4.calculateMomentRadius(flux=obj.flux) print('Testing Wavelength and Angle sampling - %s band'%band) print('Flux = %.0f: sum peak radius'%obj.flux) print('No lamb, no angles: %.1f %.2f %f'%( im1.array.sum(),im1.array.max(), r1)) print('W/ lamb, no angles: %.1f %.2f %f'%( im2.array.sum(),im2.array.max(), r2)) print('No lamb, w/ angles: %.1f %.2f %f'%( im3.array.sum(),im3.array.max(), r3)) print('W/ lamb, w/ angles: %.1f %.2f %f'%( im4.array.sum(),im4.array.max(), r4)) # r4 should be greater than r1 with wavelengths and angles turned on. sigma_r = 1. / np.sqrt(obj.flux) * im1.scale print('check r4 > r1 due to added wavelengths and angles') print('r1 = %f, r4 = %f, 2*sigma_r = %f'%(r1,r4,2.*sigma_r)) assert r4 > r1
def test_silicon_area(): """Test the Silicon class calculate_pixel_areas() function. """ # Adding this test to compare to Poisson simulation # Craig Lage - 27Apr18 # Draw a very small spot with 80K electrons # This puts all of the charge in the central pixel to # match how the Poisson simulations are done obj = galsim.Gaussian(flux=8.0E4, sigma=0.02) im = obj.drawImage(nx=9, ny=9, scale=0.3, dtype=float) im.setCenter(0,0) rng = galsim.BaseDeviate(5678) silicon = galsim.SiliconSensor(rng=rng) area_image = silicon.calculate_pixel_areas(im) # Get the area data from the Poisson simulation area_filename = silicon.vertex_file.split('/')[-1].strip('.dat')+'_areas.dat' area_dir = os.path.join(os.getcwd(),'sensor_validation/') area_data = np.loadtxt(area_dir+area_filename, skiprows = 1) # Now test that they are almost equal for line in area_data: nx = int(line[0]-4) ny = int(line[1]-4) poisson_area = line[2] galsim_area = area_image[nx,ny] #print(nx,ny,poisson_area,galsim_area) np.testing.assert_almost_equal(poisson_area/100.0, galsim_area, decimal=3) # Draw a smallish but very bright Gaussian image obj = galsim.Gaussian(flux=5.e5, sigma=0.2) im = obj.drawImage(nx=17, ny=17, scale=0.3, dtype=float) im.setCenter(0,0) print('im min = ',im.array.min()) print('im max = ',im.array.max()) print('im(0,0) = ',im(0,0)) assert im(0,0) == im.array.max() np.testing.assert_almost_equal(im(0,0), 149462.06966413918) rng = galsim.BaseDeviate(5678) silicon = galsim.SiliconSensor(rng=rng, diffusion_factor=0.0) area_image = silicon.calculate_pixel_areas(im) print('area min = ',area_image.array.min()) print('area max = ',area_image.array.max()) print('area(0,0) = ',area_image(0,0)) assert area_image(0,0) == area_image.array.min() # Regression test based on values current for GalSim version 1.6. np.testing.assert_almost_equal(area_image(0,0), 0.8962292034379046) np.testing.assert_almost_equal(area_image.array.max(), 1.0112006254350212) # The Silicon code is asymmetric. Charge flows more easily along columns than rows. # It's not completely intuitive, since there are competing effects in play, but the net # result on the areas for this image is that the pixels above and below the central pixel # are slightly larger than the ones to the left and right. print('+- 1 along column: ',area_image(0,1),area_image(0,-1)) print('+- 1 along row: ',area_image(1,0),area_image(-1,0)) np.testing.assert_almost_equal((area_image(0,1) + area_image(0,-1))/2., 0.9789158236482416) np.testing.assert_almost_equal((area_image(1,0) + area_image(-1,0))/2., 0.9686605382489861) # Just to confirm that the bigger effect really is along the column directions, draw the # object with the silicon sensor in play. im2 = obj.drawImage(nx=17, ny=17, scale=0.3, method='phot', sensor=silicon, rng=rng) im2.setCenter(0,0) print('im min = ',im2.array.min()) print('im max = ',im2.array.max()) print('im(0,0) = ',im2(0,0)) print('+- 1 along column: ',im2(0,1),im2(0,-1)) print('+- 1 along row: ',im2(1,0),im2(-1,0)) assert im2(0,0) == im2.array.max() assert im2(0,1) + im2(0,-1) > im2(1,0) + im2(-1,0) np.testing.assert_almost_equal(im2(0,0), 143340) np.testing.assert_almost_equal((im2(0,1) + im2(0,-1))/2., 59307.0) np.testing.assert_almost_equal((im2(1,0) + im2(-1,0))/2., 59031.0) # Repeat with transpose=True to check that things are transposed properly. siliconT = galsim.SiliconSensor(rng=rng, transpose=True, diffusion_factor=0.0) area_imageT = siliconT.calculate_pixel_areas(im) print('with transpose=True:') print('area min = ',area_imageT.array.min()) print('area max = ',area_imageT.array.max()) print('area(0,0) = ',area_imageT(0,0)) print('+- 1 along column: ',area_imageT(0,1),area_imageT(0,-1)) print('+- 1 along row: ',area_imageT(1,0),area_imageT(-1,0)) np.testing.assert_almost_equal(area_imageT(0,0), 0.8962292034379046) np.testing.assert_almost_equal((area_imageT(0,1) + area_imageT(0,-1))/2., 0.9686605382489861) np.testing.assert_almost_equal((area_imageT(1,0) + area_imageT(-1,0))/2., 0.9789158236482416) np.testing.assert_almost_equal(area_imageT.array, area_image.array.T, decimal=14) im2T = obj.drawImage(nx=17, ny=17, scale=0.3, method='phot', sensor=siliconT, rng=rng) im2T.setCenter(0,0) print('im min = ',im2T.array.min()) print('im max = ',im2T.array.max()) print('im(0,0) = ',im2T(0,0)) print('+- 1 along column: ',im2T(0,1),im2T(0,-1)) print('+- 1 along row: ',im2T(1,0),im2T(-1,0)) assert im2T(0,0) == im2T.array.max() assert im2T(0,1) + im2T(0,-1) < im2T(1,0) + im2T(-1,0) # Actual values are different, since rng is in different state. But qualitatively transposed. np.testing.assert_almost_equal(im2T(0,0), 142604) np.testing.assert_almost_equal((im2T(0,1) + im2T(0,-1))/2., 59185.5) np.testing.assert_almost_equal((im2T(1,0) + im2T(-1,0))/2., 59375.0) do_pickle(siliconT)
def main(argv): """ Make images to be used for characterizing the brighter-fatter effect - Each fits file is 5 x 5 postage stamps. - Each postage stamp is 40 x 40 pixels. - There are 3 sets of 5 images each. The 5 images are at 5 different flux levels - The three sets are (bf_1) B-F off, (bf_2) B-F on, diffusion off, (bf_3) B-F and diffusion on - Each image is in output/bf_set/bf_nfile.fits, where set ranges from 1-3 and nfile ranges from 1-5. """ logging.basicConfig(format="%(message)s", level=logging.INFO, stream=sys.stdout) logger = logging.getLogger("bf_plots") # Add the wavelength info bppath = "../../share/bandpasses/" sedpath = "../../share/" sed = galsim.SED(os.path.join(sedpath, 'CWW_E_ext.sed'), 'nm', 'flambda').thin() # Add the directions (seems to work - CL) fratio = 1.2 obscuration = 0.2 seed = 12345 assigner = galsim.FRatioAngles(fratio, obscuration, seed) bandpass = galsim.Bandpass(os.path.join(bppath, 'LSST_r.dat'), 'nm').thin() rng3 = galsim.BaseDeviate(1234) sampler = galsim.WavelengthSampler(sed, bandpass, rng3) # Define some parameters we'll use below. # Normally these would be read in from some parameter file. nx_tiles = 10 # ny_tiles = 10 # stamp_xsize = 40 # stamp_ysize = 40 # random_seed = 6424512 # pixel_scale = 0.2 # arcsec / pixel sky_level = 0.01 # ADU / arcsec^2 # Make output directory if not already present. if not os.path.isdir('output'): os.mkdir('output') gal_sigma = 0.2 # arcsec psf_sigma = 0.01 # arcsec pixel_scale = 0.2 # arcsec / pixel noise = 0.01 # standard deviation of the counts in each pixel shift_radius = 0.2 # arcsec (=pixels) logger.info('Starting bf_plots using:') logger.info(' - image with %d x %d postage stamps',nx_tiles,ny_tiles) logger.info(' - postage stamps of size %d x %d pixels',stamp_xsize,stamp_ysize) logger.info(' - Centroid shifts up to = %.2f pixels',shift_radius) rng = galsim.BaseDeviate(5678) sensor1 = galsim.Sensor() sensor2 = galsim.SiliconSensor(rng=rng, diffusion_factor=0.0) sensor3 = galsim.SiliconSensor(rng=rng) for set in range(1,4): starttime = time.time() exec("sensor = sensor%d"%set) for nfile in range(1,6): # Make bf_x directory if not already present. if not os.path.isdir('output/bf_%d'%set): os.mkdir('output/bf_%d'%set) gal_file_name = os.path.join('output','bf_%d/bf_%d.fits'%(set,nfile)) sex_file_name = os.path.join('output','bf_%d/bf_%d_SEX.fits.cat.reg'%(set,nfile)) sexfile = open(sex_file_name, 'w') gal_flux = 2.0e5 * nfile # total counts on the image # Define the galaxy profile gal = galsim.Gaussian(flux=gal_flux, sigma=gal_sigma) logger.debug('Made galaxy profile') # Define the PSF profile psf = galsim.Gaussian(flux=1., sigma=psf_sigma) # PSF flux should always = 1 logger.debug('Made PSF profile') # This profile is placed with different orientations and noise realizations # at each postage stamp in the gal image. gal_image = galsim.ImageF(stamp_xsize * nx_tiles-1 , stamp_ysize * ny_tiles-1, scale=pixel_scale) psf_image = galsim.ImageF(stamp_xsize * nx_tiles-1 , stamp_ysize * ny_tiles-1, scale=pixel_scale) shift_radius_sq = shift_radius**2 first_in_pair = True # Make pairs that are rotated by 90 degrees k = 0 for iy in range(ny_tiles): for ix in range(nx_tiles): # The normal procedure for setting random numbers in GalSim is to start a new # random number generator for each object using sequential seed values. # This sounds weird at first (especially if you were indoctrinated by Numerical # Recipes), but for the boost random number generator we use, the "random" # number sequences produced from sequential initial seeds are highly uncorrelated. # # The reason for this procedure is that when we use multiple processes to build # our images, we want to make sure that the results are deterministic regardless # of the way the objects get parcelled out to the different processes. # # Of course, this script isn't using multiple processes, so it isn't required here. # However, we do it nonetheless in order to get the same results as the config # version of this demo script (demo5.yaml). ud = galsim.UniformDeviate(random_seed+k) # Any kind of random number generator can take another RNG as its first # argument rather than a seed value. This makes both objects use the same # underlying generator for their pseudo-random values. #gd = galsim.GaussianDeviate(ud, sigma=gal_ellip_rms) # The -1's in the next line are to provide a border of # 1 pixel between postage stamps b = galsim.BoundsI(ix*stamp_xsize+1 , (ix+1)*stamp_xsize-1, iy*stamp_ysize+1 , (iy+1)*stamp_ysize-1) sub_gal_image = gal_image[b] sub_psf_image = psf_image[b] # Great08 randomized the locations of the two galaxies in each pair, # but for simplicity, we just do them in sequential postage stamps. if first_in_pair: # Use a random orientation: beta = ud() * 2. * math.pi * galsim.radians # Determine the ellipticity to use for this galaxy. ellip = 0.0 first_in_pair = False else: # Use the previous ellip and beta + 90 degrees beta += 90 * galsim.degrees first_in_pair = True # Make a new copy of the galaxy with an applied e1/e2-type distortion # by specifying the ellipticity and a real-space position angle this_gal = gal#gal.shear(e=ellip, beta=beta) # Apply a random shift_radius: rsq = 2 * shift_radius_sq while (rsq > shift_radius_sq): dx = (2*ud()-1) * shift_radius dy = (2*ud()-1) * shift_radius rsq = dx**2 + dy**2 this_gal = this_gal.shift(dx,dy) # Note that the shifted psf that we create here is purely for the purpose of being able # to draw a separate, shifted psf image. We do not use it when convolving the galaxy # with the psf. this_psf = psf.shift(dx,dy) # Make the final image, convolving with the (unshifted) psf final_gal = galsim.Convolve([psf,this_gal]) # Draw the image if ix == 0 and iy == 0: final_gal.drawImage(sub_gal_image, method = 'phot', sensor=sensor, surface_ops=[sampler, assigner], rng = rng, save_photons = True) photon_file = os.path.join('output','bf_%d/bf_%d_nx_%d_ny_%d_photon_file.fits'%(set,nfile,ix,iy)) sub_gal_image.photons.write(photon_file) else: final_gal.drawImage(sub_gal_image, method = 'phot', sensor=sensor, surface_ops=[sampler, assigner], rng = rng) # Now add an appropriate amount of noise to get our desired S/N # There are lots of definitions of S/N, but here is the one used by Great08 # We use a weighted integral of the flux: # S = sum W(x,y) I(x,y) / sum W(x,y) # N^2 = Var(S) = sum W(x,y)^2 Var(I(x,y)) / (sum W(x,y))^2 # Now we assume that Var(I(x,y)) is constant so # Var(I(x,y)) = noise_var # We also assume that we are using a matched filter for W, so W(x,y) = I(x,y). # Then a few things cancel and we find that # S/N = sqrt( sum I(x,y)^2 / noise_var ) # # The above procedure is encapsulated in the function image.addNoiseSNR which # sets the flux appropriately given the variance of the noise model. # In our case, noise_var = sky_level_pixel sky_level_pixel = sky_level * pixel_scale**2 noise = galsim.PoissonNoise(ud, sky_level=sky_level_pixel) #sub_gal_image.addNoiseSNR(noise, gal_signal_to_noise) # Draw the PSF image # No noise on PSF images. Just draw it as is. this_psf.drawImage(sub_psf_image) # For first instance, measure moments """ if ix==0 and iy==0: psf_shape = sub_psf_image.FindAdaptiveMom() temp_e = psf_shape.observed_shape.e if temp_e > 0.0: g_to_e = psf_shape.observed_shape.g / temp_e else: g_to_e = 0.0 logger.info('Measured best-fit elliptical Gaussian for first PSF image: ') logger.info(' g1, g2, sigma = %7.4f, %7.4f, %7.4f (pixels)', g_to_e*psf_shape.observed_shape.e1, g_to_e*psf_shape.observed_shape.e2, psf_shape.moments_sigma) """ x = b.center().x y = b.center().y logger.info('Galaxy (%d,%d): center = (%.0f,%0.f) (e,beta) = (%.4f,%.3f)', ix,iy,x,y,ellip,beta/galsim.radians) k = k+1 sexline = 'circle %f %f %f\n'%(x+dx/pixel_scale,y+dy/pixel_scale,gal_sigma/pixel_scale) sexfile.write(sexline) sexfile.close() logger.info('Done making images of postage stamps') # Now write the images to disk. #psf_image.write(psf_file_name) #logger.info('Wrote PSF file %s',psf_file_name) gal_image.write(gal_file_name) logger.info('Wrote image to %r',gal_file_name) # using %r adds quotes around filename for us finishtime = time.time() print("Time to complete set %d = %.2f seconds\n"%(set, finishtime-starttime))
def get_sensor(self, nrecalc): return galsim.SiliconSensor(rng=self._rng, nrecalc=nrecalc)