lsst.pipe.tasks g3d3fc359a7+8335dcbd4d
Loading...
Searching...
No Matches
healSparseMappingProperties.py
Go to the documentation of this file.
2# LSST Data Management System
3# Copyright 2008-2021 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22import numpy as np
23import healsparse as hsp
24
25import lsst.pex.config as pexConfig
26import lsst.geom
27from lsst.afw.math import ChebyshevBoundedField, ChebyshevBoundedFieldControl
28
29
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"]
37
38
39class BasePropertyMapConfig(pexConfig.Config):
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.")
50
51
52class PropertyMapRegistry(pexConfig.Registry):
53 """Class for property map registry.
54
55 Notes
56 -----
57 This code is based on `lsst.meas.base.PluginRegistry`.
58 """
60 """Class used as the element in the property map registry.
61
62 Parameters
63 ----------
64 name : `str`
65 Name under which the property map is registered.
66 PropertyMapClass : subclass of `BasePropertyMap`
67 """
68 def __init__(self, name, PropertyMapClass):
69 self.name = name
70 self.PropertyMapClass = PropertyMapClass
71
72 @property
73 def ConfigClass(self):
74 return self.PropertyMapClass.ConfigClass
75
76 def __call__(self, config):
77 return (self.name, config, self.PropertyMapClass)
78
79 def register(self, name, PropertyMapClass):
80 """Register a property map class with the given name.
81
82 Parameters
83 ----------
84 name : `str`
85 The name of the property map.
86 PropertyMapClass : subclass of `BasePropertyMap`
87 """
88 pexConfig.Registry.register(self, name, self.Configurable(name, PropertyMapClass))
89
90
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
96 return decorate
97
98
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.
101
102 This routine fits how the psf size and shape varies over a field by approximating
103 with a Chebyshev bounded field.
104
105 Parameters
106 ----------
108 Exposure metadata for a given detector exposure.
109 ra : `np.ndarray`
110 Right ascension of points to compute size and shape (degrees).
111 dec : `np.ndarray`
112 Declination of points to compute size and shape (degrees).
113 nx : `int`, optional
114 Number of sampling points in the x direction.
115 ny : `int`, optional
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.
121
122 Returns
123 -------
124 psf_array : `np.ndarray`
125 Record array with "psf_size", "psf_e1", "psf_e2".
126 """
127 pts = [lsst.geom.SpherePoint(r*lsst.geom.degrees, d*lsst.geom.degrees) for
128 r, d in zip(ra, dec)]
129 pixels = ccd_row.getWcs().skyToPixel(pts)
130
132 ctrl.orderX = orderx
133 ctrl.orderY = ordery
134 ctrl.triangular = False
135
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)
141
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)
146
147 psf = ccd_row.getPsf()
148 for i in range(x.size):
149 shape = psf.computeShape(lsst.geom.Point2D(x[i], y[i]))
150 psf_size[i] = shape.getDeterminantRadius()
151 ixx = shape.getIxx()
152 iyy = shape.getIyy()
153 ixy = shape.getIxy()
154
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.)
157
158 im = psf.computeKernelImage(lsst.geom.Point2D(x[i], y[i]))
159 psf_area[i] = np.sum(im.array)/np.sum(im.array**2.)
160
161 pixel_x = np.array([pix.getX() for pix in pixels])
162 pixel_y = np.array([pix.getY() for pix in pixels])
163
164 psf_array = np.zeros(pixel_x.size, dtype=[("psf_size", "f8"),
165 ("psf_e1", "f8"),
166 ("psf_e2", "f8"),
167 ("psf_area", "f8")])
168
169 # Protect against nans which can come in at the edges and masked regions.
170 good = np.isfinite(psf_size)
171 x = x[good]
172 y = y[good]
173
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)
182
183 return psf_array
184
185
186class PropertyMapMap(dict):
187 """Map of property maps to be run for a given task.
188
189 Notes
190 -----
191 Property maps are classes derived from `BasePropertyMap`
192 """
193 def __iter__(self):
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):
197 yield property_map
198
199
201 """Base class for property maps.
202
203 Parameters
204 ----------
205 config : `BasePropertyMapConfig`
206 Property map configuration.
207 name : `str`
208 Property map name.
209 """
210 dtype = np.float64
211 requires_psf = False
212
213 ConfigClass = BasePropertyMapConfig
214
215 registry = PropertyMapRegistry(BasePropertyMapConfig)
216
217 def __init__(self, config, name):
218 object.__init__(self)
219 self.config = config
220 self.name = name
221 self.zeropoint = 0.0
222
223 def initialize_tract_maps(self, nside_coverage, nside):
224 """Initialize the tract maps.
225
226 Parameters
227 ----------
228 nside_coverage : `int`
229 Healpix nside of the healsparse coverage map.
230 nside : `int`
231 Healpix nside of the property map.
232 """
233 if self.config.do_min:
234 self.min_map = hsp.HealSparseMap.make_empty(nside_coverage,
235 nside,
236 self.dtype)
237 if self.config.do_max:
238 self.max_map = hsp.HealSparseMap.make_empty(nside_coverage,
239 nside,
240 self.dtype)
241 if self.config.do_mean:
242 self.mean_map = hsp.HealSparseMap.make_empty(nside_coverage,
243 nside,
244 self.dtype)
245 if self.config.do_weighted_mean:
246 self.weighted_mean_map = hsp.HealSparseMap.make_empty(nside_coverage,
247 nside,
248 self.dtype)
249 if self.config.do_sum:
250 self.sum_map = hsp.HealSparseMap.make_empty(nside_coverage,
251 nside,
252 self.dtype)
253
254 def initialize_values(self, n_pixels):
255 """Initialize the value arrays for accumulation.
256
257 Parameters
258 ----------
259 n_pixels : `int`
260 Number of pixels in the map.
261 """
262 if self.config.do_min:
263 self.min_values = np.zeros(n_pixels, dtype=self.dtype)
264 # This works for float types, need check for integers...
265 self.min_values[:] = np.nan
266 if self.config.do_max:
267 self.max_values = np.zeros(n_pixels, dtype=self.dtype)
268 self.max_values[:] = np.nan
269 if self.config.do_mean:
270 self.mean_values = np.zeros(n_pixels, dtype=self.dtype)
271 if self.config.do_weighted_mean:
272 self.weighted_mean_values = np.zeros(n_pixels, dtype=self.dtype)
273 if self.config.do_sum:
274 self.sum_values = np.zeros(n_pixels, dtype=self.dtype)
275
276 def accumulate_values(self, indices, ra, dec, weights, scalings, row,
277 psf_array=None):
278 """Accumulate values from a row of a visitSummary table.
279
280 Parameters
281 ----------
282 indices : `np.ndarray`
283 Indices of values that should be accumulated.
284 ra : `np.ndarray`
285 Array of right ascension for indices
286 dec : `np.ndarray`
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.
296
297 Raises
298 ------
299 ValueError : Raised if requires_psf is True and psf_array is None.
300 """
301 if self.requires_psf and psf_array is None:
302 name = self.__class__.__name__
303 raise ValueError(f"Cannot compute {name} without psf_array.")
304
305 values = self._compute(row, ra, dec, scalings, psf_array=psf_array)
306 if self.config.do_min:
307 self.min_values[indices] = np.fmin(self.min_values[indices], values)
308 if self.config.do_max:
309 self.max_values[indices] = np.fmax(self.max_values[indices], values)
310 if self.config.do_mean:
311 self.mean_values[indices] += values
312 if self.config.do_weighted_mean:
313 self.weighted_mean_values[indices] += weights*values
314 if self.config.do_sum:
315 self.sum_values[indices] += values
316
317 def finalize_mean_values(self, total_weights, total_inputs):
318 """Finalize the accumulation of the mean and weighted mean.
319
320 Parameters
321 ----------
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.
326 """
327 if self.config.do_mean:
328 use, = np.where(total_inputs > 0)
329 self.mean_values[use] /= total_inputs[use]
330 if self.config.do_weighted_mean:
331 use, = np.where(total_weights > 0.0)
332 self.weighted_mean_values[use] /= total_weights[use]
333
334 # And perform any necessary post-processing
335 self._post_process(total_weights, total_inputs)
336
337 def set_map_values(self, pixels):
338 """Assign accumulated values to the maps.
339
340 Parameters
341 ----------
342 pixels : `np.ndarray`
343 Array of healpix pixels (nest scheme) to set in the map.
344 """
345 if self.config.do_min:
346 self.min_map[pixels] = self.min_values
347 if self.config.do_max:
348 self.max_map[pixels] = self.max_values
349 if self.config.do_mean:
350 self.mean_map[pixels] = self.mean_values
351 if self.config.do_weighted_mean:
352 self.weighted_mean_map[pixels] = self.weighted_mean_values
353 if self.config.do_sum:
354 self.sum_map[pixels] = self.sum_values
355
356 def _compute(self, row, ra, dec, scalings, psf_array=None):
357 """Compute map value from a row in the visitSummary catalog.
358
359 Parameters
360 ----------
362 Row of a visitSummary ExposureCatalog.
363 ra : `np.ndarray`
364 Array of right ascensions
365 dec : `np.ndarray`
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.
371 """
372 raise NotImplementedError("All property maps must implement _compute()")
373
374 def _post_process(self, total_weights, total_inputs):
375 """Perform post-processing on values.
376
377 Parameters
378 ----------
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.
383 """
384 # Override of this method is not required.
385 pass
386
387
388@register_property_map("exposure_time")
390 """Exposure time property map."""
391
392 def _compute(self, row, ra, dec, scalings, psf_array=None):
393 return row.getVisitInfo().getExposureTime()
394
395
396@register_property_map("psf_size")
398 """PSF size property map."""
399 requires_psf = True
400
401 def _compute(self, row, ra, dec, scalings, psf_array=None):
402 return psf_array["psf_size"]
403
404
405@register_property_map("psf_e1")
407 """PSF shape e1 property map."""
408 requires_psf = True
409
410 def _compute(self, row, ra, dec, scalings, psf_array=None):
411 return psf_array["psf_e1"]
412
413
414@register_property_map("psf_e2")
416 """PSF shape e2 property map."""
417 requires_psf = True
418
419 def _compute(self, row, ra, dec, scalings, psf_array=None):
420 return psf_array["psf_e2"]
421
422
423@register_property_map("n_exposure")
425 """Number of exposures property map."""
426 dtype = np.int32
427
428 def _compute(self, row, ra, dec, scalings, psf_array=None):
429 return 1
430
431
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.")
436
437 def validate(self):
438 super().validate()
439 if self.do_min or self.do_max or self.do_mean or self.do_sum:
440 raise ValueError("Can only use do_weighted_mean with PsfMaglimPropertyMap")
441
442
443@register_property_map("psf_maglim")
445 """PSF magnitude limit property map."""
446 requires_psf = True
447
448 ConfigClass = PsfMaglimPropertyMapConfig
449
450 def _compute(self, row, ra, dec, scalings, psf_array=None):
451 # Our values are the weighted mean of the psf area
452 return psf_array["psf_area"]
453
454 def _post_process(self, total_weights, total_inputs):
455 psf_area = self.weighted_mean_values.copy()
456 maglim = (self.zeropoint
457 - 2.5*np.log10(self.config.maglim_nsigma*np.sqrt(psf_area/total_weights)))
458 self.weighted_mean_values[:] = maglim
459
460
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"]
466
467
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"]
473
474
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)
481
482
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)
489
490
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)
497
498
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)