23import healsparse
as hsp
27from lsst.afw.math import ChebyshevBoundedField, ChebyshevBoundedFieldControl
30__all__ = [
"BasePropertyMapConfig",
"PropertyMapRegistry",
"register_property_map",
31 "PropertyMapMap",
"BasePropertyMap",
"ExposureTimePropertyMap",
32 "PsfSizePropertyMap",
"PsfE1PropertyMap",
"PsfE2PropertyMap",
33 "NExposurePropertyMap",
"PsfMaglimPropertyMapConfig",
34 "PsfMaglimPropertyMap",
"SkyBackgroundPropertyMap",
"SkyNoisePropertyMap",
35 "DcrDraPropertyMap",
"DcrDdecPropertyMap",
"DcrE1PropertyMap",
36 "DcrE2PropertyMap",
"compute_approx_psf_size_and_shape"]
40 do_min = pexConfig.Field(dtype=bool, default=
False,
41 doc=
"Compute map of property minima.")
42 do_max = pexConfig.Field(dtype=bool, default=
False,
43 doc=
"Compute map of property maxima.")
44 do_mean = pexConfig.Field(dtype=bool, default=
False,
45 doc=
"Compute map of property means.")
46 do_weighted_mean = pexConfig.Field(dtype=bool, default=
False,
47 doc=
"Compute map of weighted property means.")
48 do_sum = pexConfig.Field(dtype=bool, default=
False,
49 doc=
"Compute map of property sums.")
53 """Class for property map registry.
57 This code is based on `lsst.meas.base.PluginRegistry`.
60 """Class used as the element in the property map registry.
65 Name under which the property map is registered.
66 PropertyMapClass : subclass of `BasePropertyMap`
73 def ConfigClass(self):
79 def register(self, name, PropertyMapClass):
80 """Register a property map class with the given name.
85 The name of the property map.
86 PropertyMapClass : subclass of `BasePropertyMap`
88 pexConfig.Registry.register(self, name, self.Configurable(name, PropertyMapClass))
92 """A decorator to register a property map class in its base class's registry."""
93 def decorate(PropertyMapClass):
94 PropertyMapClass.registry.register(name, PropertyMapClass)
95 return PropertyMapClass
99def compute_approx_psf_size_and_shape(ccd_row, ra, dec, nx=20, ny=20, orderx=2, ordery=2):
100 """Compute the approximate psf size and shape.
102 This routine fits how the psf size and shape varies over a field by approximating
103 with a Chebyshev bounded field.
108 Exposure metadata
for a given detector exposure.
110 Right ascension of points to compute size
and shape (degrees).
112 Declination of points to compute size
and shape (degrees).
114 Number of sampling points
in the x direction.
116 Number of sampling points
in the y direction.
117 orderx : `int`, optional
118 Chebyshev polynomial order
for fit
in x direction.
119 ordery : `int`, optional
120 Chebyshev polynomial order
for fit
in y direction.
124 psf_array : `np.ndarray`
125 Record array
with "psf_size",
"psf_e1",
"psf_e2".
128 r, d
in zip(ra, dec)]
129 pixels = ccd_row.getWcs().skyToPixel(pts)
134 ctrl.triangular =
False
136 bbox = ccd_row.getBBox()
137 xSteps = np.linspace(bbox.getMinX(), bbox.getMaxX(), nx)
138 ySteps = np.linspace(bbox.getMinY(), bbox.getMaxY(), ny)
139 x = np.tile(xSteps, nx)
140 y = np.repeat(ySteps, ny)
142 psf_size = np.zeros(x.size)
143 psf_e1 = np.zeros(x.size)
144 psf_e2 = np.zeros(x.size)
145 psf_area = np.zeros(x.size)
147 psf = ccd_row.getPsf()
148 for i
in range(x.size):
150 psf_size[i] = shape.getDeterminantRadius()
155 psf_e1[i] = (ixx - iyy)/(ixx + iyy + 2.*psf_size[i]**2.)
156 psf_e2[i] = (2.*ixy)/(ixx + iyy + 2.*psf_size[i]**2.)
159 psf_area[i] = np.sum(im.array)/np.sum(im.array**2.)
161 pixel_x = np.array([pix.getX()
for pix
in pixels])
162 pixel_y = np.array([pix.getY()
for pix
in pixels])
164 psf_array = np.zeros(pixel_x.size, dtype=[(
"psf_size",
"f8"),
170 good = np.isfinite(psf_size)
174 cheb_size = ChebyshevBoundedField.fit(
lsst.geom.Box2I(bbox), x, y, psf_size[good], ctrl)
175 psf_array[
"psf_size"] = cheb_size.evaluate(pixel_x, pixel_y)
176 cheb_e1 = ChebyshevBoundedField.fit(
lsst.geom.Box2I(bbox), x, y, psf_e1[good], ctrl)
177 psf_array[
"psf_e1"] = cheb_e1.evaluate(pixel_x, pixel_y)
178 cheb_e2 = ChebyshevBoundedField.fit(
lsst.geom.Box2I(bbox), x, y, psf_e2[good], ctrl)
179 psf_array[
"psf_e2"] = cheb_e2.evaluate(pixel_x, pixel_y)
180 cheb_area = ChebyshevBoundedField.fit(
lsst.geom.Box2I(bbox), x, y, psf_area[good], ctrl)
181 psf_array[
"psf_area"] = cheb_area.evaluate(pixel_x, pixel_y)
187 """Map of property maps to be run for a given task.
191 Property maps are classes derived from `BasePropertyMap`
194 for property_map
in self.values():
195 if (property_map.config.do_min
or property_map.config.do_max
or property_map.config.do_mean
196 or property_map.config.do_weighted_mean
or property_map.config.do_sum):
201 """Base class for property maps.
205 config : `BasePropertyMapConfig`
206 Property map configuration.
213 ConfigClass = BasePropertyMapConfig
218 object.__init__(self)
224 """Initialize the tract maps.
228 nside_coverage : `int`
229 Healpix nside of the healsparse coverage map.
231 Healpix nside of the property map.
234 self.
min_map = hsp.HealSparseMap.make_empty(nside_coverage,
238 self.
max_map = hsp.HealSparseMap.make_empty(nside_coverage,
242 self.
mean_map = hsp.HealSparseMap.make_empty(nside_coverage,
245 if self.
config.do_weighted_mean:
250 self.
sum_map = hsp.HealSparseMap.make_empty(nside_coverage,
255 """Initialize the value arrays for accumulation.
260 Number of pixels in the map.
271 if self.
config.do_weighted_mean:
278 """Accumulate values from a row of a visitSummary table.
282 indices : `np.ndarray`
283 Indices of values that should be accumulated.
285 Array of right ascension for indices
287 Array of declination
for indices
288 weights : `float`
or `np.ndarray`
289 Weight(s)
for indices to be accumulated.
290 scalings : `float`
or `np.ndarray`
291 Scaling values to coadd zeropoint.
293 Row of a visitSummary ExposureCatalog.
294 psf_array : `np.ndarray`, optional
295 Array of approximate psf values matched to ra/dec.
299 ValueError : Raised
if requires_psf
is True and psf_array
is None.
302 name = self.__class__.__name__
303 raise ValueError(f
"Cannot compute {name} without psf_array.")
305 values = self.
_compute(row, ra, dec, scalings, psf_array=psf_array)
312 if self.
config.do_weighted_mean:
318 """Finalize the accumulation of the mean and weighted mean.
322 total_weights : `np.ndarray`
323 Total accumulated weights, for each value index.
324 total_inputs : `np.ndarray`
325 Total number of inputs,
for each value index.
328 use, = np.where(total_inputs > 0)
330 if self.
config.do_weighted_mean:
331 use, = np.where(total_weights > 0.0)
338 """Assign accumulated values to the maps.
342 pixels : `np.ndarray`
343 Array of healpix pixels (nest scheme) to set in the map.
351 if self.
config.do_weighted_mean:
356 def _compute(self, row, ra, dec, scalings, psf_array=None):
357 """Compute map value from a row in the visitSummary catalog.
362 Row of a visitSummary ExposureCatalog.
364 Array of right ascensions
366 Array of declinations
367 scalings : `float` or `np.ndarray`
368 Scaling values to coadd zeropoint.
369 psf_array : `np.ndarray`, optional
370 Array of approximate psf values matched to ra/dec.
372 raise NotImplementedError(
"All property maps must implement _compute()")
374 def _post_process(self, total_weights, total_inputs):
375 """Perform post-processing on values.
379 total_weights : `np.ndarray`
380 Total accumulated weights, for each value index.
381 total_inputs : `np.ndarray`
382 Total number of inputs,
for each value index.
388@register_property_map("exposure_time")
390 """Exposure time property map."""
392 def _compute(self, row, ra, dec, scalings, psf_array=None):
393 return row.getVisitInfo().getExposureTime()
396@register_property_map("psf_size")
398 """PSF size property map."""
401 def _compute(self, row, ra, dec, scalings, psf_array=None):
402 return psf_array[
"psf_size"]
405@register_property_map("psf_e1")
407 """PSF shape e1 property map."""
410 def _compute(self, row, ra, dec, scalings, psf_array=None):
411 return psf_array[
"psf_e1"]
414@register_property_map("psf_e2")
416 """PSF shape e2 property map."""
419 def _compute(self, row, ra, dec, scalings, psf_array=None):
420 return psf_array[
"psf_e2"]
423@register_property_map("n_exposure")
425 """Number of exposures property map."""
428 def _compute(self, row, ra, dec, scalings, psf_array=None):
433 """Configuration for the PsfMaglim property map."""
434 maglim_nsigma = pexConfig.Field(dtype=float, default=5.0,
435 doc=
"Number of sigma to compute magnitude limit.")
440 raise ValueError(
"Can only use do_weighted_mean with PsfMaglimPropertyMap")
443@register_property_map("psf_maglim")
445 """PSF magnitude limit property map."""
448 ConfigClass = PsfMaglimPropertyMapConfig
450 def _compute(self, row, ra, dec, scalings, psf_array=None):
452 return psf_array[
"psf_area"]
454 def _post_process(self, total_weights, total_inputs):
457 - 2.5*np.log10(self.
config.maglim_nsigma*np.sqrt(psf_area/total_weights)))
461@register_property_map("sky_background")
463 """Sky background property map."""
464 def _compute(self, row, ra, dec, scalings, psf_array=None):
465 return scalings*row[
"skyBg"]
468@register_property_map("sky_noise")
470 """Sky noise property map."""
471 def _compute(self, row, ra, dec, scalings, psf_array=None):
472 return scalings*row[
"skyNoise"]
475@register_property_map("dcr_dra")
477 """Effect of DCR on delta-RA property map."""
478 def _compute(self, row, ra, dec, scalings, psf_array=None):
479 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
480 return np.tan(np.deg2rad(row[
"zenithDistance"]))*np.sin(par_angle)
483@register_property_map("dcr_ddec")
485 """Effect of DCR on delta-Dec property map."""
486 def _compute(self, row, ra, dec, scalings, psf_array=None):
487 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
488 return np.tan(np.deg2rad(row[
"zenithDistance"]))*np.cos(par_angle)
491@register_property_map("dcr_e1")
493 """Effect of DCR on psf shape e1 property map."""
494 def _compute(self, row, ra, dec, scalings, psf_array=None):
495 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
496 return (np.tan(np.deg2rad(row[
"zenithDistance"]))**2.)*np.cos(2.*par_angle)
499@register_property_map("dcr_e2")
501 """Effect of DCR on psf shape e2 property map."""
502 def _compute(self, row, ra, dec, scalings, psf_array=None):
503 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
504 return (np.tan(np.deg2rad(row[
"zenithDistance"]))**2.)*np.sin(2.*par_angle)
def _compute(self, row, ra, dec, scalings, psf_array=None)
def accumulate_values(self, indices, ra, dec, weights, scalings, row, psf_array=None)
def set_map_values(self, pixels)
def __init__(self, config, name)
def initialize_values(self, n_pixels)
def initialize_tract_maps(self, nside_coverage, nside)
def finalize_mean_values(self, total_weights, total_inputs)
def _post_process(self, total_weights, total_inputs)
def __call__(self, config)
def __init__(self, name, PropertyMapClass)
def register_property_map(name)