Coverage for python/lsst/pipe/tasks/healSparseMappingProperties.py: 39%
218 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-09 03:48 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-09 03:48 -0800
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22__all__ = ["BasePropertyMapConfig", "PropertyMapRegistry", "register_property_map",
23 "PropertyMapMap", "BasePropertyMap", "ExposureTimePropertyMap",
24 "PsfSizePropertyMap", "PsfE1PropertyMap", "PsfE2PropertyMap",
25 "NExposurePropertyMap", "PsfMaglimPropertyMapConfig",
26 "PsfMaglimPropertyMap", "SkyBackgroundPropertyMap", "SkyNoisePropertyMap",
27 "DcrDraPropertyMap", "DcrDdecPropertyMap", "DcrE1PropertyMap",
28 "DcrE2PropertyMap", "EpochPropertyMap", "compute_approx_psf_size_and_shape"]
30import numpy as np
31import healsparse as hsp
33import lsst.pex.config as pexConfig
34import lsst.geom
35from lsst.afw.math import ChebyshevBoundedField, ChebyshevBoundedFieldControl
38class BasePropertyMapConfig(pexConfig.Config):
39 do_min = pexConfig.Field(dtype=bool, default=False,
40 doc="Compute map of property minima.")
41 do_max = pexConfig.Field(dtype=bool, default=False,
42 doc="Compute map of property maxima.")
43 do_mean = pexConfig.Field(dtype=bool, default=False,
44 doc="Compute map of property means.")
45 do_weighted_mean = pexConfig.Field(dtype=bool, default=False,
46 doc="Compute map of weighted property means.")
47 do_sum = pexConfig.Field(dtype=bool, default=False,
48 doc="Compute map of property sums.")
51class PropertyMapRegistry(pexConfig.Registry):
52 """Class for property map registry.
54 Notes
55 -----
56 This code is based on `lsst.meas.base.PluginRegistry`.
57 """
58 class Configurable:
59 """Class used as the element in the property map registry.
61 Parameters
62 ----------
63 name : `str`
64 Name under which the property map is registered.
65 PropertyMapClass : subclass of `BasePropertyMap`
66 """
67 def __init__(self, name, PropertyMapClass):
68 self.name = name
69 self.PropertyMapClass = PropertyMapClass
71 @property
72 def ConfigClass(self):
73 return self.PropertyMapClass.ConfigClass
75 def __call__(self, config):
76 return (self.name, config, self.PropertyMapClass)
78 def register(self, name, PropertyMapClass):
79 """Register a property map class with the given name.
81 Parameters
82 ----------
83 name : `str`
84 The name of the property map.
85 PropertyMapClass : subclass of `BasePropertyMap`
86 """
87 pexConfig.Registry.register(self, name, self.Configurable(name, PropertyMapClass))
90def register_property_map(name):
91 """A decorator to register a property map class in its base class's registry."""
92 def decorate(PropertyMapClass):
93 PropertyMapClass.registry.register(name, PropertyMapClass)
94 return PropertyMapClass
95 return decorate
98def compute_approx_psf_size_and_shape(ccd_row, ra, dec, nx=20, ny=20, orderx=2, ordery=2):
99 """Compute the approximate psf size and shape.
101 This routine fits how the psf size and shape varies over a field by approximating
102 with a Chebyshev bounded field.
104 Parameters
105 ----------
106 ccd_row : `lsst.afw.table.ExposureRecord`
107 Exposure metadata for a given detector exposure.
108 ra : `np.ndarray`
109 Right ascension of points to compute size and shape (degrees).
110 dec : `np.ndarray`
111 Declination of points to compute size and shape (degrees).
112 nx : `int`, optional
113 Number of sampling points in the x direction.
114 ny : `int`, optional
115 Number of sampling points in the y direction.
116 orderx : `int`, optional
117 Chebyshev polynomial order for fit in x direction.
118 ordery : `int`, optional
119 Chebyshev polynomial order for fit in y direction.
121 Returns
122 -------
123 psf_array : `np.ndarray`
124 Record array with "psf_size", "psf_e1", "psf_e2".
125 """
126 pts = [lsst.geom.SpherePoint(r*lsst.geom.degrees, d*lsst.geom.degrees) for
127 r, d in zip(ra, dec)]
128 pixels = ccd_row.getWcs().skyToPixel(pts)
130 ctrl = ChebyshevBoundedFieldControl()
131 ctrl.orderX = orderx
132 ctrl.orderY = ordery
133 ctrl.triangular = False
135 bbox = ccd_row.getBBox()
136 xSteps = np.linspace(bbox.getMinX(), bbox.getMaxX(), nx)
137 ySteps = np.linspace(bbox.getMinY(), bbox.getMaxY(), ny)
138 x = np.tile(xSteps, nx)
139 y = np.repeat(ySteps, ny)
141 psf_size = np.zeros(x.size)
142 psf_e1 = np.zeros(x.size)
143 psf_e2 = np.zeros(x.size)
144 psf_area = np.zeros(x.size)
146 psf = ccd_row.getPsf()
147 for i in range(x.size):
148 shape = psf.computeShape(lsst.geom.Point2D(x[i], y[i]))
149 psf_size[i] = shape.getDeterminantRadius()
150 ixx = shape.getIxx()
151 iyy = shape.getIyy()
152 ixy = shape.getIxy()
154 psf_e1[i] = (ixx - iyy)/(ixx + iyy + 2.*psf_size[i]**2.)
155 psf_e2[i] = (2.*ixy)/(ixx + iyy + 2.*psf_size[i]**2.)
157 im = psf.computeKernelImage(lsst.geom.Point2D(x[i], y[i]))
158 psf_area[i] = np.sum(im.array)/np.sum(im.array**2.)
160 pixel_x = np.array([pix.getX() for pix in pixels])
161 pixel_y = np.array([pix.getY() for pix in pixels])
163 psf_array = np.zeros(pixel_x.size, dtype=[("psf_size", "f8"),
164 ("psf_e1", "f8"),
165 ("psf_e2", "f8"),
166 ("psf_area", "f8")])
168 cheb_size = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_size, ctrl)
169 psf_array["psf_size"] = cheb_size.evaluate(pixel_x, pixel_y)
170 cheb_e1 = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_e1, ctrl)
171 psf_array["psf_e1"] = cheb_e1.evaluate(pixel_x, pixel_y)
172 cheb_e2 = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_e2, ctrl)
173 psf_array["psf_e2"] = cheb_e2.evaluate(pixel_x, pixel_y)
174 cheb_area = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_area, ctrl)
175 psf_array["psf_area"] = cheb_area.evaluate(pixel_x, pixel_y)
177 return psf_array
180class PropertyMapMap(dict):
181 """Map of property maps to be run for a given task.
183 Notes
184 -----
185 Property maps are classes derived from `BasePropertyMap`
186 """
187 def __iter__(self):
188 for property_map in self.values():
189 if (property_map.config.do_min or property_map.config.do_max or property_map.config.do_mean
190 or property_map.config.do_weighted_mean or property_map.config.do_sum):
191 yield property_map
194class BasePropertyMap:
195 """Base class for property maps.
197 Parameters
198 ----------
199 config : `BasePropertyMapConfig`
200 Property map configuration.
201 name : `str`
202 Property map name.
203 """
204 dtype = np.float64
205 requires_psf = False
207 ConfigClass = BasePropertyMapConfig
209 registry = PropertyMapRegistry(BasePropertyMapConfig)
211 def __init__(self, config, name):
212 object.__init__(self)
213 self.config = config
214 self.name = name
215 self.zeropoint = 0.0
217 def initialize_tract_maps(self, nside_coverage, nside):
218 """Initialize the tract maps.
220 Parameters
221 ----------
222 nside_coverage : `int`
223 Healpix nside of the healsparse coverage map.
224 nside : `int`
225 Healpix nside of the property map.
226 """
227 if self.config.do_min:
228 self.min_map = hsp.HealSparseMap.make_empty(nside_coverage,
229 nside,
230 self.dtype)
231 if self.config.do_max:
232 self.max_map = hsp.HealSparseMap.make_empty(nside_coverage,
233 nside,
234 self.dtype)
235 if self.config.do_mean:
236 self.mean_map = hsp.HealSparseMap.make_empty(nside_coverage,
237 nside,
238 self.dtype)
239 if self.config.do_weighted_mean:
240 self.weighted_mean_map = hsp.HealSparseMap.make_empty(nside_coverage,
241 nside,
242 self.dtype)
243 if self.config.do_sum:
244 self.sum_map = hsp.HealSparseMap.make_empty(nside_coverage,
245 nside,
246 self.dtype)
248 def initialize_values(self, n_pixels):
249 """Initialize the value arrays for accumulation.
251 Parameters
252 ----------
253 n_pixels : `int`
254 Number of pixels in the map.
255 """
256 if self.config.do_min:
257 self.min_values = np.zeros(n_pixels, dtype=self.dtype)
258 # This works for float types, need check for integers...
259 self.min_values[:] = np.nan
260 if self.config.do_max:
261 self.max_values = np.zeros(n_pixels, dtype=self.dtype)
262 self.max_values[:] = np.nan
263 if self.config.do_mean:
264 self.mean_values = np.zeros(n_pixels, dtype=self.dtype)
265 if self.config.do_weighted_mean:
266 self.weighted_mean_values = np.zeros(n_pixels, dtype=self.dtype)
267 if self.config.do_sum:
268 self.sum_values = np.zeros(n_pixels, dtype=self.dtype)
270 def accumulate_values(self, indices, ra, dec, weights, scalings, row,
271 psf_array=None):
272 """Accumulate values from a row of a visitSummary table.
274 Parameters
275 ----------
276 indices : `np.ndarray`
277 Indices of values that should be accumulated.
278 ra : `np.ndarray`
279 Array of right ascension for indices
280 dec : `np.ndarray`
281 Array of declination for indices
282 weights : `float` or `np.ndarray`
283 Weight(s) for indices to be accumulated.
284 scalings : `float` or `np.ndarray`
285 Scaling values to coadd zeropoint.
286 row : `lsst.afw.table.ExposureRecord`
287 Row of a visitSummary ExposureCatalog.
288 psf_array : `np.ndarray`, optional
289 Array of approximate psf values matched to ra/dec.
291 Raises
292 ------
293 ValueError : Raised if requires_psf is True and psf_array is None.
294 """
295 if self.requires_psf and psf_array is None:
296 name = self.__class__.__name__
297 raise ValueError(f"Cannot compute {name} without psf_array.")
299 values = self._compute(row, ra, dec, scalings, psf_array=psf_array)
300 if self.config.do_min:
301 self.min_values[indices] = np.fmin(self.min_values[indices], values)
302 if self.config.do_max:
303 self.max_values[indices] = np.fmax(self.max_values[indices], values)
304 if self.config.do_mean:
305 self.mean_values[indices] += values
306 if self.config.do_weighted_mean:
307 self.weighted_mean_values[indices] += weights*values
308 if self.config.do_sum:
309 self.sum_values[indices] += values
311 def finalize_mean_values(self, total_weights, total_inputs):
312 """Finalize the accumulation of the mean and weighted mean.
314 Parameters
315 ----------
316 total_weights : `np.ndarray`
317 Total accumulated weights, for each value index.
318 total_inputs : `np.ndarray`
319 Total number of inputs, for each value index.
320 """
321 if self.config.do_mean:
322 use, = np.where(total_inputs > 0)
323 self.mean_values[use] /= total_inputs[use]
324 if self.config.do_weighted_mean:
325 use, = np.where(total_weights > 0.0)
326 self.weighted_mean_values[use] /= total_weights[use]
328 # And perform any necessary post-processing
329 self._post_process(total_weights, total_inputs)
331 def set_map_values(self, pixels):
332 """Assign accumulated values to the maps.
334 Parameters
335 ----------
336 pixels : `np.ndarray`
337 Array of healpix pixels (nest scheme) to set in the map.
338 """
339 if self.config.do_min:
340 self.min_map[pixels] = self.min_values
341 if self.config.do_max:
342 self.max_map[pixels] = self.max_values
343 if self.config.do_mean:
344 self.mean_map[pixels] = self.mean_values
345 if self.config.do_weighted_mean:
346 self.weighted_mean_map[pixels] = self.weighted_mean_values
347 if self.config.do_sum:
348 self.sum_map[pixels] = self.sum_values
350 def _compute(self, row, ra, dec, scalings, psf_array=None):
351 """Compute map value from a row in the visitSummary catalog.
353 Parameters
354 ----------
355 row : `lsst.afw.table.ExposureRecord`
356 Row of a visitSummary ExposureCatalog.
357 ra : `np.ndarray`
358 Array of right ascensions
359 dec : `np.ndarray`
360 Array of declinations
361 scalings : `float` or `np.ndarray`
362 Scaling values to coadd zeropoint.
363 psf_array : `np.ndarray`, optional
364 Array of approximate psf values matched to ra/dec.
365 """
366 raise NotImplementedError("All property maps must implement _compute()")
368 def _post_process(self, total_weights, total_inputs):
369 """Perform post-processing on values.
371 Parameters
372 ----------
373 total_weights : `np.ndarray`
374 Total accumulated weights, for each value index.
375 total_inputs : `np.ndarray`
376 Total number of inputs, for each value index.
377 """
378 # Override of this method is not required.
379 pass
382@register_property_map("exposure_time")
383class ExposureTimePropertyMap(BasePropertyMap):
384 """Exposure time property map."""
386 def _compute(self, row, ra, dec, scalings, psf_array=None):
387 return row.getVisitInfo().getExposureTime()
390@register_property_map("psf_size")
391class PsfSizePropertyMap(BasePropertyMap):
392 """PSF size property map."""
393 requires_psf = True
395 def _compute(self, row, ra, dec, scalings, psf_array=None):
396 return psf_array["psf_size"]
399@register_property_map("psf_e1")
400class PsfE1PropertyMap(BasePropertyMap):
401 """PSF shape e1 property map."""
402 requires_psf = True
404 def _compute(self, row, ra, dec, scalings, psf_array=None):
405 return psf_array["psf_e1"]
408@register_property_map("psf_e2")
409class PsfE2PropertyMap(BasePropertyMap):
410 """PSF shape e2 property map."""
411 requires_psf = True
413 def _compute(self, row, ra, dec, scalings, psf_array=None):
414 return psf_array["psf_e2"]
417@register_property_map("n_exposure")
418class NExposurePropertyMap(BasePropertyMap):
419 """Number of exposures property map."""
420 dtype = np.int32
422 def _compute(self, row, ra, dec, scalings, psf_array=None):
423 return 1
426class PsfMaglimPropertyMapConfig(BasePropertyMapConfig):
427 """Configuration for the PsfMaglim property map."""
428 maglim_nsigma = pexConfig.Field(dtype=float, default=5.0,
429 doc="Number of sigma to compute magnitude limit.")
431 def validate(self):
432 super().validate()
433 if self.do_min or self.do_max or self.do_mean or self.do_sum:
434 raise ValueError("Can only use do_weighted_mean with PsfMaglimPropertyMap")
437@register_property_map("psf_maglim")
438class PsfMaglimPropertyMap(BasePropertyMap):
439 """PSF magnitude limit property map."""
440 requires_psf = True
442 ConfigClass = PsfMaglimPropertyMapConfig
444 def _compute(self, row, ra, dec, scalings, psf_array=None):
445 # Our values are the weighted mean of the psf area
446 return psf_array["psf_area"]
448 def _post_process(self, total_weights, total_inputs):
449 psf_area = self.weighted_mean_values.copy()
450 maglim = (self.zeropoint
451 - 2.5*np.log10(self.config.maglim_nsigma*np.sqrt(psf_area/total_weights)))
452 self.weighted_mean_values[:] = maglim
455@register_property_map("sky_background")
456class SkyBackgroundPropertyMap(BasePropertyMap):
457 """Sky background property map."""
458 def _compute(self, row, ra, dec, scalings, psf_array=None):
459 return scalings*row["skyBg"]
462@register_property_map("sky_noise")
463class SkyNoisePropertyMap(BasePropertyMap):
464 """Sky noise property map."""
465 def _compute(self, row, ra, dec, scalings, psf_array=None):
466 return scalings*row["skyNoise"]
469@register_property_map("dcr_dra")
470class DcrDraPropertyMap(BasePropertyMap):
471 """Effect of DCR on delta-RA property map."""
472 def _compute(self, row, ra, dec, scalings, psf_array=None):
473 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
474 return np.tan(np.deg2rad(row["zenithDistance"]))*np.sin(par_angle)
477@register_property_map("dcr_ddec")
478class DcrDdecPropertyMap(BasePropertyMap):
479 """Effect of DCR on delta-Dec property map."""
480 def _compute(self, row, ra, dec, scalings, psf_array=None):
481 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
482 return np.tan(np.deg2rad(row["zenithDistance"]))*np.cos(par_angle)
485@register_property_map("dcr_e1")
486class DcrE1PropertyMap(BasePropertyMap):
487 """Effect of DCR on psf shape e1 property map."""
488 def _compute(self, row, ra, dec, scalings, psf_array=None):
489 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
490 return (np.tan(np.deg2rad(row["zenithDistance"]))**2.)*np.cos(2.*par_angle)
493@register_property_map("dcr_e2")
494class DcrE2PropertyMap(BasePropertyMap):
495 """Effect of DCR on psf shape e2 property map."""
496 def _compute(self, row, ra, dec, scalings, psf_array=None):
497 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
498 return (np.tan(np.deg2rad(row["zenithDistance"]))**2.)*np.sin(2.*par_angle)
501@register_property_map("epoch")
502class EpochPropertyMap(BasePropertyMap):
503 """Observation epoch (mjd) property map."""
504 def _compute(self, row, ra, dec, scalings, psf_array=None):
505 date = row.getVisitInfo().getDate()
506 return date.get(date.MJD)