-
Notifications
You must be signed in to change notification settings - Fork 10
/
sim.py
754 lines (648 loc) · 21.3 KB
/
sim.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
from copy import deepcopy
import esutil as eu
import galsim
import numpy as np
import lsst.afw.image as afw_image
from lsst.afw.cameraGeom.testUtils import DetectorWrapper
from .lsst_bits import get_flagval
from .saturation import saturate_image_and_mask, BAND_SAT_VALS
from .surveys import get_survey, rescale_wldeblend_exp
from .constants import SCALE, ZERO_POINT
from .artifacts import add_bleed, get_max_mag_with_bleed
from .masking import (
get_bmask_and_set_image,
calculate_bright_star_mask_radius,
)
from .objlists import get_objlist
from .psfs import make_dm_psf
from .wcs import make_wcs, make_dm_wcs, make_coadd_dm_wcs
DEFAULT_SIM_CONFIG = {
"gal_type": "fixed", # note "exp" also means "fixed" for back compat
"psf_type": "gauss",
"psf_dim": 51,
"psf_variation_factor": 1, # for power spectrum psf
# randomize the psf fwhm and shape for each trial. PSF is still same
# for all epochs/bands
"randomize_psf": False,
"coadd_dim": 250,
"se_dim": None,
"buff": 0,
"layout": "grid",
"sep": None, # sep in arcsec for layout=pair
"dither": False,
"rotate": False,
"bands": ["i"],
"epochs_per_band": 1,
"noise_factor": 1.0,
"stars": False,
"star_bleeds": False,
"draw_stars": True,
"cosmic_rays": False,
"bad_columns": False,
"sky_n_sigma": None,
}
def make_sim(
*,
rng,
galaxy_catalog,
coadd_dim,
g1,
g2,
psf,
se_dim=None,
star_catalog=None,
draw_stars=True,
psf_dim=51,
dither=False,
rotate=False,
bands=['i'],
epochs_per_band=1,
noise_factor=1.0,
cosmic_rays=False,
bad_columns=False,
star_bleeds=False,
sky_n_sigma=None,
draw_method='auto',
theta0=0.,
):
"""
Make simulation data
Parameters
----------
rng: numpy.random.RandomState
Numpy random state
galaxy_catalog: catalog
E.g. WLDeblendGalaxyCatalog or FixedGalaxyCatalog
coadd_dim: int
Dimensions for planned final coadd. This is used for generating
the final coadd WCS and deteremines some properties of
the single epoch images.
g1: float
Shear g1 for galaxies
g2: float
Shear g2 for galaxies
psf: GSObject or PowerSpectrumPSF
The psf object or power spectrum psf
se_dim: int, optional
Force the single epoch images to have this dimension. If not
sent it is calculated to be large enough to encompass the coadd
with rotations and small dithers.
star_catalog: catalog, optional
A catalog to generate star locations and fluxes. See the psfs module
psf_dim: int, optional
Dimensions of psf image. Default 51
dither: bool, optional
Whether to dither the images at the pixel level, default False
rotate: bool, optional
Whether to randomly rotate the image exposures randomly [not the
rotation of intrinsic galaxies], default False
bands: list, optional
Default ['i']
epochs_per_band: int, optional
Number of epochs per band, default 1
noise_factor: float, optional
Factor by which to multiply the noise, default 1
cosmic_rays: bool, optional
If set to True, add cosmic rays. Default False.
bad_columns: bool, optional
If set to True, add bad columns. Default False.
star_bleeds: bool, optional
If set to True, draw simulated bleed trails for saturated stars.
Default False
sky_n_sigma: float, optional
Number of sigma to set the sky value. Can be negative to
mock up a sky oversubtraction. Default None.
draw_method: string, optional
Draw method for galaxy galsim objects, default 'auto'. Set to
'phot' to get poisson noise. Note this is much slower.
theta0: float, optional
rotation angle of intrinsic galaxies and positions [for ring test],
default 0, in units of radians
Returns
-------
sim_data: dict
band_data: a dict keyed by band name, holding a list of exp
coadd_wcs: lsst.afw.geom.makeSkyWcs
psf_dims: (int, int)
coadd_dims: (int, int)
coadd_bbox: lsst.geom.Box2I
bright_info: structured array
fields are
ra, dec: sky position of bright stars
radius_pixels: radius of mask in pixels
has_bleed: bool, True if there is a bleed trail
se_wcs: list of WCS
"""
coadd_wcs, coadd_bbox = make_coadd_dm_wcs(coadd_dim)
coadd_bbox_cen_gs_skypos = get_coadd_center_gs_pos(
coadd_wcs=coadd_wcs, coadd_bbox=coadd_bbox,
)
if se_dim is None:
se_dim = get_se_dim(coadd_dim=coadd_dim, dither=dither, rotate=rotate)
band_data = {}
bright_info = []
se_wcs = []
for band in bands:
survey = get_survey(gal_type=galaxy_catalog.gal_type, band=band)
noise_for_gsparams = survey.noise*noise_factor
noise_per_epoch = survey.noise*np.sqrt(epochs_per_band)*noise_factor
# go down to coadd depth in this band, not dividing by sqrt(nbands)
# but we could if we want to be more conservative
mask_threshold = survey.noise*noise_factor
lists = get_objlist(
galaxy_catalog=galaxy_catalog,
survey=survey,
star_catalog=star_catalog,
noise=noise_for_gsparams,
)
# note the bright list is the same for all exps, so we only add
# bright_info once
bdata_list = []
for epoch in range(epochs_per_band):
exp, this_bright_info, this_se_wcs = make_exp(
rng=rng,
band=band,
noise=noise_per_epoch,
objlist=lists['objlist'],
shifts=lists['shifts'],
dim=se_dim,
psf=psf,
psf_dim=psf_dim,
g1=g1, g2=g2,
star_objlist=lists['star_objlist'],
star_shifts=lists['star_shifts'],
draw_stars=draw_stars,
bright_objlist=lists['bright_objlist'],
bright_shifts=lists['bright_shifts'],
bright_mags=lists['bright_mags'],
coadd_bbox_cen_gs_skypos=coadd_bbox_cen_gs_skypos,
dither=dither,
rotate=rotate,
mask_threshold=mask_threshold,
cosmic_rays=cosmic_rays,
bad_columns=bad_columns,
star_bleeds=star_bleeds,
sky_n_sigma=sky_n_sigma,
draw_method=draw_method,
theta0=theta0,
)
if epoch == 0:
bright_info += this_bright_info
se_wcs.append(this_se_wcs)
if galaxy_catalog.gal_type == 'wldeblend':
rescale_wldeblend_exp(
survey=survey.descwl_survey,
exp=exp,
)
# mark high pixels SAT and also set sat value in image for
# any pixels already marked SAT
saturate_image_and_mask(
image=exp.image.array,
bmask=exp.mask.array,
sat_val=BAND_SAT_VALS[band],
flagval=get_flagval('SAT'),
)
bdata_list.append(exp)
band_data[band] = bdata_list
bright_info = eu.numpy_util.combine_arrlist(bright_info)
return {
'band_data': band_data,
'coadd_wcs': coadd_wcs,
'psf_dims': (psf_dim, )*2,
'coadd_dims': (coadd_dim, )*2,
'coadd_bbox': coadd_bbox,
'bright_info': bright_info,
'se_wcs': se_wcs,
}
def make_exp(
*,
rng,
band,
noise,
objlist,
shifts,
dim,
psf,
psf_dim,
g1,
g2,
star_objlist=None,
star_shifts=None,
draw_stars=True,
bright_objlist=None,
bright_shifts=None,
bright_mags=None,
coadd_bbox_cen_gs_skypos,
dither=False,
rotate=False,
mask_threshold=None,
cosmic_rays=False,
bad_columns=False,
star_bleeds=False,
sky_n_sigma=None,
draw_method='auto',
theta0=0.
):
"""
Make an SEObs
Parameters
----------
rng: numpy.random.RandomState
The random number generator
band: str
Band as a string, e.g. 'i'
noise: float
Gaussian noise level
objlist: list
List of GSObj
shifts: array
List of PositionD representing offsets
dim: int
Dimension of image
psf: GSObject or PowerSpectrumPSF
the psf
psf_dim: int
Dimensions of psf image that will be drawn when psf func is called
coadd_bbox_cen_gs_skypos: galsim.CelestialCoord
The sky position of the center (origin) of the coadd we
will make, as a galsim object not stack object
dither: bool
If set to True, dither randomly by a pixel width
rotate: bool
If set to True, rotate the image exposure randomly, note, this is not
the rotation of intrinsic galaxies in ring test
star_objlist: list
List of GSObj for stars
star_shifts: array
List of PositionD for stars representing offsets
draw_stars: bool, optional
Draw the stars, don't just mask bright ones. Default True.
bright_objlist: list, optional
List of GSObj for bright objects
bright_shifts: array, optional
List of PositionD for stars representing offsets
bright_mags: array, optional
Mags for the bright objects
mask_threshold: float
Bright masks are created such that the profile goes out
to this threshold
cosmic_rays: bool
If True, put in cosmic rays
bad_columns: bool
If True, put in bad columns
star_bleeds: bool
If True, add bleed trails to stars
sky_n_sigma: float
Number of sigma to set the sky value. Can be negative to
mock up a sky oversubtraction. Default None.
draw_method: string
Draw method for galsim objects, default 'auto'. Set to
'phot' to get poisson noise. Note this is much slower.
theta0: float
rotation angle of intrinsic galaxies and positions [for ring test],
default 0, in units of radians
Returns
-------
exp: lsst.afw.image.ExposureF
Exposure data
bright_info: list of structured arrays
fields are
ra, dec: sky position of bright stars
radius_pixels: radius of mask in pixels
has_bleed: bool, True if there is a bleed trail
"""
shear = galsim.Shear(g1=g1, g2=g2)
dims = [dim]*2
# I think Galsim uses 1 offset. An array with length=dim=5
# The center is at 3=(5+1)/2
cen = (np.array(dims)+1)/2
se_origin = galsim.PositionD(x=cen[1], y=cen[0])
if dither:
dither_range = 0.5
off = rng.uniform(low=-dither_range, high=dither_range, size=2)
offset = galsim.PositionD(x=off[0], y=off[1])
se_origin = se_origin + offset
if rotate:
theta = rng.uniform(low=0, high=2*np.pi)
else:
theta = None
# galsim wcs
se_wcs = make_wcs(
scale=SCALE,
theta=theta,
image_origin=se_origin,
world_origin=coadd_bbox_cen_gs_skypos,
)
image = galsim.Image(dim, dim, wcs=se_wcs)
_draw_objects(
image,
objlist, shifts, psf, draw_method,
coadd_bbox_cen_gs_skypos,
rng,
shear=shear,
theta0=theta0,
)
if star_objlist is not None and draw_stars:
assert star_shifts is not None, 'send star_shifts with star_objlist'
_draw_objects(
image,
star_objlist, star_shifts, psf, draw_method,
coadd_bbox_cen_gs_skypos,
rng,
)
image.array[:, :] += rng.normal(scale=noise, size=dims)
if sky_n_sigma is not None:
image.array[:, :] += sky_n_sigma * noise
# pixels flagged as bad cols and cosmics will get
# set to np.nan
bmask = get_bmask_and_set_image(
image=image,
rng=rng,
cosmic_rays=cosmic_rays,
bad_columns=bad_columns,
)
if bright_objlist is not None:
bright_info = _draw_bright_objects(
image=image,
noise=noise,
origin=se_origin,
bmask=bmask,
band=band,
objlist=bright_objlist,
shifts=bright_shifts,
mags=bright_mags,
psf=psf,
coadd_bbox_cen_gs_skypos=coadd_bbox_cen_gs_skypos,
mask_threshold=mask_threshold,
rng=rng,
star_bleeds=star_bleeds,
draw_stars=draw_stars,
)
else:
bright_info = []
dm_wcs = make_dm_wcs(se_wcs)
dm_psf = make_dm_psf(psf=psf, psf_dim=psf_dim, wcs=se_wcs)
variance = image.copy()
variance.array[:, :] = noise**2
masked_image = afw_image.MaskedImageF(dim, dim)
masked_image.image.array[:, :] = image.array
masked_image.variance.array[:, :] = variance.array
masked_image.mask.array[:, :] = bmask.array
exp = afw_image.ExposureF(masked_image)
# Prepare the frc, and save it to the DM exposure
# It can be retrieved as follow
# zero_flux= exposure.getPhotoCalib().getInstFluxAtZeroMagnitude()
# magz = np.log10(zero_flux)*2.5 # magnitude zero point
zero_flux = 10.**(0.4*ZERO_POINT)
photoCalib = afw_image.makePhotoCalibFromCalibZeroPoint(zero_flux)
exp.setPhotoCalib(photoCalib)
filter_label = afw_image.FilterLabel(band=band, physical=band)
exp.setFilter(filter_label)
exp.setPsf(dm_psf)
exp.setWcs(dm_wcs)
detector = DetectorWrapper().detector
exp.setDetector(detector)
return exp, bright_info, se_wcs
def _draw_objects(
image, objlist, shifts, psf, draw_method,
coadd_bbox_cen_gs_skypos,
rng,
shear=None,
theta0=None,
):
wcs = image.wcs
kw = {}
if draw_method == 'phot':
kw['maxN'] = 1_000_000
kw['rng'] = galsim.BaseDeviate(seed=rng.randint(low=0, high=2**30))
for obj, shift in zip(objlist, shifts):
if theta0 is not None:
ang = theta0*galsim.radians
# rotation on intrinsic galaxies comes before shear distortion
obj = obj.rotate(ang)
shift = _roate_pos(shift, theta0)
if shear is not None:
obj = obj.shear(shear)
shift = shift.shear(shear)
# Deproject from u,v onto sphere. Then use wcs to get to image pos.
world_pos = coadd_bbox_cen_gs_skypos.deproject(
shift.x * galsim.arcsec,
shift.y * galsim.arcsec,
)
image_pos = wcs.toImage(world_pos)
local_wcs = wcs.local(image_pos=image_pos)
convolved_object = get_convolved_object(obj, psf, image_pos)
stamp = convolved_object.drawImage(
center=image_pos, wcs=local_wcs, method=draw_method, **kw
)
b = stamp.bounds & image.bounds
if b.isDefined():
image[b] += stamp[b]
def _draw_bright_objects(
image,
noise,
origin,
bmask,
band,
objlist,
shifts,
mags,
psf,
coadd_bbox_cen_gs_skypos,
mask_threshold,
rng,
star_bleeds,
draw_stars,
):
"""
draw bright objects.
Returns
-------
bright_info: list of structured arrays
fields are
ra, dec: sky position of bright stars
radius_pixels: radius of mask in pixels
has_bleed: bool, True if there is a bleed trail
"""
# extra array needed to determine star mask accurately
timage = image.copy()
timage.setZero()
grng = galsim.BaseDeviate(rng.randint(0, 2**30))
assert shifts is not None
assert mags is not None and len(mags) == len(shifts)
assert mask_threshold is not None
max_bleed_mag = get_max_mag_with_bleed(band=band)
wcs = image.wcs
bright_info = []
indices = np.arange(len(objlist))
for index, obj, shift, mag in zip(indices, objlist, shifts, mags):
# profiles can have detectably sharp edges if the
# profile is very high s/n and we have not set the
# thresholds right in the gs params.
#
# photon shooting reduces these sharp edges, reducing
# sensitivity to such an error
world_pos = coadd_bbox_cen_gs_skypos.deproject(
shift.x * galsim.arcsec,
shift.y * galsim.arcsec,
)
image_pos = wcs.toImage(world_pos)
local_wcs = wcs.local(image_pos=image_pos)
convolved_object = get_convolved_object(obj, psf, image_pos)
max_n_photons = 10_000_000
# 0 means use the flux for n_photons
n_photons = 0 if obj.flux < max_n_photons else max_n_photons
stamp = convolved_object.drawImage(
center=image_pos, wcs=local_wcs,
method='phot',
n_photons=n_photons,
poisson_flux=True,
maxN=1_000_000, # shoot in batches this size
rng=grng,
)
b = stamp.bounds & image.bounds
if b.isDefined():
if draw_stars:
image[b] += stamp[b]
# use smooth version for radius calculation
stamp_fft = convolved_object.drawImage(
center=image_pos, wcs=local_wcs,
)
timage[b] += stamp_fft[b]
radius_pixels = calculate_bright_star_mask_radius(
image=timage.array,
objrow=image_pos.y,
objcol=image_pos.x,
threshold=mask_threshold,
)
info = get_bright_info_struct()
info['ra'] = world_pos.ra / galsim.degrees
info['dec'] = world_pos.dec / galsim.degrees
info['radius_pixels'] = radius_pixels
if star_bleeds and mag < max_bleed_mag:
info['has_bleed'] = True
add_bleed(
image=image.array,
bmask=bmask.array,
pos=image_pos,
mag=mag,
band=band,
)
else:
info['has_bleed'] = False
bright_info.append(info)
# reset for next object
timage.setZero()
return bright_info
def get_sim_config(config=None):
"""
Get a simulation configuration, with defaults that can
be over-ridden by the input. The defaults are in
DEFAULT_SIM_CONFIG
Parameters
----------
config: dict, optional
Dict of options to over ride the defaults
Returns
-------
config dict
"""
out_config = deepcopy(DEFAULT_SIM_CONFIG)
sub_configs = ['gal_config', 'star_config']
if config is not None:
for key in config:
if key not in out_config and key not in sub_configs:
raise ValueError("bad key for sim: '%s'" % key)
out_config.update(deepcopy(config))
return out_config
def get_se_dim(*, coadd_dim, dither, rotate):
"""
get single epoch (se) dimensions given coadd dim.
Parameters
----------
coadd_dim: int
dimensions of coadd
dither: bool
Whether there is dithering or not
rotate: bool
Whether there are random rotations of image exposure or not
Returns
-------
integer dimensions of SE image
"""
if rotate:
# make sure to completely cover the coadd
se_dim = int(np.ceil(coadd_dim * np.sqrt(2))) + 20
else:
# make big enough to avoid boundary checks for downstream
# which are 3 pixels
se_dim = coadd_dim + 5 * 2
return se_dim
def get_coadd_center_gs_pos(coadd_wcs, coadd_bbox):
"""
get the sky position of the center of the coadd within the
bbox as a galsim CelestialCoord
Parameters
----------
coadd_wcs: DM wcs
The wcs for the coadd
coadd_bbox: geom.Box2I
The bounding box for the coadd within larger wcs system
Returns
-------
galsim CelestialCoord
"""
# world origin is at center of the coadd, which itself
# is in a bbox shifted from the overall WORLD_ORIGIN
bbox_cen_skypos = coadd_wcs.pixelToSky(
coadd_bbox.getCenter()
)
return galsim.CelestialCoord(
ra=float(bbox_cen_skypos.getRa()) * galsim.radians,
dec=float(bbox_cen_skypos.getDec()) * galsim.radians,
)
def get_convolved_object(obj, psf, image_pos):
"""
Get a convolved object for either constant or varying psf
Parameters
----------
obj: galsim object
The object to be convolved
psf: galsim object or PowerSpectrumPSF
Constant or varying psf
image_pos: galsim.PositionD
For varying psfs, will be used to get the psf
Returns
-------
galsim object
"""
if isinstance(psf, galsim.GSObject):
convolved_object = galsim.Convolve(obj, psf)
else:
psf_gsobj = psf.getPSF(image_pos)
convolved_object = galsim.Convolve(obj, psf_gsobj)
return convolved_object
def get_bright_info_struct():
dt = [
('ra', 'f8'),
('dec', 'f8'),
('radius_pixels', 'f4'),
('has_bleed', bool),
]
return np.zeros(1, dtype=dt)
def _roate_pos(pos, theta):
'''Rotates coordinates by an angle theta
Args:
pos (PositionD):a galsim position
theta (float): rotation angle [rads]
Returns:
x2 (ndarray): rotated coordiantes [x]
y2 (ndarray): rotated coordiantes [y]
'''
x = pos.x
y = pos.y
cost = np.cos(theta)
sint = np.sin(theta)
x2 = cost*x - sint*y
y2 = sint*x + cost*y
return galsim.PositionD(x=x2, y=y2)