LSST Applications 26.0.0,g0265f82a02+6660c170cc,g07994bdeae+30b05a742e,g0a0026dc87+17526d298f,g0a60f58ba1+17526d298f,g0e4bf8285c+96dd2c2ea9,g0ecae5effc+c266a536c8,g1e7d6db67d+6f7cb1f4bb,g26482f50c6+6346c0633c,g2bbee38e9b+6660c170cc,g2cc88a2952+0a4e78cd49,g3273194fdb+f6908454ef,g337abbeb29+6660c170cc,g337c41fc51+9a8f8f0815,g37c6e7c3d5+7bbafe9d37,g44018dc512+6660c170cc,g4a941329ef+4f7594a38e,g4c90b7bd52+5145c320d2,g58be5f913a+bea990ba40,g635b316a6c+8d6b3a3e56,g67924a670a+bfead8c487,g6ae5381d9b+81bc2a20b4,g93c4d6e787+26b17396bd,g98cecbdb62+ed2cb6d659,g98ffbb4407+81bc2a20b4,g9ddcbc5298+7f7571301f,ga1e77700b3+99e9273977,gae46bcf261+6660c170cc,gb2715bf1a1+17526d298f,gc86a011abf+17526d298f,gcf0d15dbbd+96dd2c2ea9,gdaeeff99f8+0d8dbea60f,gdb4ec4c597+6660c170cc,ge23793e450+96dd2c2ea9,gf041782ebf+171108ac67
LSST Data Management Base Package
Loading...
Searching...
No Matches
healSparseMappingProperties.py
Go to the documentation of this file.
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/>.
21
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"]
29
30import numpy as np
31import healsparse as hsp
32
33import lsst.pex.config as pexConfig
34import lsst.geom
35from lsst.afw.math import ChebyshevBoundedField, ChebyshevBoundedFieldControl
36
37
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.")
49
50
51class PropertyMapRegistry(pexConfig.Registry):
52 """Class for property map registry.
53
54 Notes
55 -----
56 This code is based on `lsst.meas.base.PluginRegistry`.
57 """
59 """Class used as the element in the property map registry.
60
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
70
71 @property
72 def ConfigClass(self):
73 return self.PropertyMapClass.ConfigClass
74
75 def __call__(self, config):
76 return (self.name, config, self.PropertyMapClass)
77
78 def register(self, name, PropertyMapClass):
79 """Register a property map class with the given name.
80
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))
88
89
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
96
97
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.
100
101 This routine fits how the psf size and shape varies over a field by approximating
102 with a Chebyshev bounded field.
103
104 Parameters
105 ----------
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.
120
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)
129
131 ctrl.orderX = orderx
132 ctrl.orderY = ordery
133 ctrl.triangular = False
134
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)
140
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)
145
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()
153
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.)
156
157 im = psf.computeKernelImage(lsst.geom.Point2D(x[i], y[i]))
158 psf_area[i] = np.sum(im.array)/np.sum(im.array**2.)
159
160 pixel_x = np.array([pix.getX() for pix in pixels])
161 pixel_y = np.array([pix.getY() for pix in pixels])
162
163 psf_array = np.zeros(pixel_x.size, dtype=[("psf_size", "f8"),
164 ("psf_e1", "f8"),
165 ("psf_e2", "f8"),
166 ("psf_area", "f8")])
167
168 # Protect against nans which can come in at the edges and masked regions.
169 good = np.isfinite(psf_size)
170 x = x[good]
171 y = y[good]
172
173 cheb_size = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_size[good], ctrl)
174 psf_array["psf_size"] = cheb_size.evaluate(pixel_x, pixel_y)
175 cheb_e1 = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_e1[good], ctrl)
176 psf_array["psf_e1"] = cheb_e1.evaluate(pixel_x, pixel_y)
177 cheb_e2 = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_e2[good], ctrl)
178 psf_array["psf_e2"] = cheb_e2.evaluate(pixel_x, pixel_y)
179 cheb_area = ChebyshevBoundedField.fit(lsst.geom.Box2I(bbox), x, y, psf_area[good], ctrl)
180 psf_array["psf_area"] = cheb_area.evaluate(pixel_x, pixel_y)
181
182 return psf_array
183
184
185class PropertyMapMap(dict):
186 """Map of property maps to be run for a given task.
187
188 Notes
189 -----
190 Property maps are classes derived from `BasePropertyMap`
191 """
192 def __iter__(self):
193 for property_map in self.values():
194 if (property_map.config.do_min or property_map.config.do_max or property_map.config.do_mean
195 or property_map.config.do_weighted_mean or property_map.config.do_sum):
196 yield property_map
197
198
200 """Base class for property maps.
201
202 Parameters
203 ----------
204 config : `BasePropertyMapConfig`
205 Property map configuration.
206 name : `str`
207 Property map name.
208 """
209 dtype = np.float64
210 requires_psf = False
211
212 ConfigClass = BasePropertyMapConfig
213
214 registry = PropertyMapRegistry(BasePropertyMapConfig)
215
216 def __init__(self, config, name):
217 object.__init__(self)
218 self.config = config
219 self.name = name
220 self.zeropoint = 0.0
221
222 def initialize_tract_maps(self, nside_coverage, nside):
223 """Initialize the tract maps.
224
225 Parameters
226 ----------
227 nside_coverage : `int`
228 Healpix nside of the healsparse coverage map.
229 nside : `int`
230 Healpix nside of the property map.
231 """
232 if self.config.do_min:
233 self.min_map = hsp.HealSparseMap.make_empty(nside_coverage,
234 nside,
235 self.dtype)
236 if self.config.do_max:
237 self.max_map = hsp.HealSparseMap.make_empty(nside_coverage,
238 nside,
239 self.dtype)
240 if self.config.do_mean:
241 self.mean_map = hsp.HealSparseMap.make_empty(nside_coverage,
242 nside,
243 self.dtype)
244 if self.config.do_weighted_mean:
245 self.weighted_mean_map = hsp.HealSparseMap.make_empty(nside_coverage,
246 nside,
247 self.dtype)
248 if self.config.do_sum:
249 self.sum_map = hsp.HealSparseMap.make_empty(nside_coverage,
250 nside,
251 self.dtype)
252
253 def initialize_values(self, n_pixels):
254 """Initialize the value arrays for accumulation.
255
256 Parameters
257 ----------
258 n_pixels : `int`
259 Number of pixels in the map.
260 """
261 if self.config.do_min:
262 self.min_values = np.zeros(n_pixels, dtype=self.dtype)
263 # This works for float types, need check for integers...
264 self.min_values[:] = np.nan
265 if self.config.do_max:
266 self.max_values = np.zeros(n_pixels, dtype=self.dtype)
267 self.max_values[:] = np.nan
268 if self.config.do_mean:
269 self.mean_values = np.zeros(n_pixels, dtype=self.dtype)
270 if self.config.do_weighted_mean:
271 self.weighted_mean_values = np.zeros(n_pixels, dtype=self.dtype)
272 if self.config.do_sum:
273 self.sum_values = np.zeros(n_pixels, dtype=self.dtype)
274
275 def accumulate_values(self, indices, ra, dec, weights, scalings, row,
276 psf_array=None):
277 """Accumulate values from a row of a visitSummary table.
278
279 Parameters
280 ----------
281 indices : `np.ndarray`
282 Indices of values that should be accumulated.
283 ra : `np.ndarray`
284 Array of right ascension for indices
285 dec : `np.ndarray`
286 Array of declination for indices
287 weights : `float` or `np.ndarray`
288 Weight(s) for indices to be accumulated.
289 scalings : `float` or `np.ndarray`
290 Scaling values to coadd zeropoint.
292 Row of a visitSummary ExposureCatalog.
293 psf_array : `np.ndarray`, optional
294 Array of approximate psf values matched to ra/dec.
295
296 Raises
297 ------
298 ValueError : Raised if requires_psf is True and psf_array is None.
299 """
300 if self.requires_psf and psf_array is None:
301 name = self.__class__.__name__
302 raise ValueError(f"Cannot compute {name} without psf_array.")
303
304 values = self._compute(row, ra, dec, scalings, psf_array=psf_array)
305 if self.config.do_min:
306 self.min_values[indices] = np.fmin(self.min_values[indices], values)
307 if self.config.do_max:
308 self.max_values[indices] = np.fmax(self.max_values[indices], values)
309 if self.config.do_mean:
310 self.mean_values[indices] += values
311 if self.config.do_weighted_mean:
312 self.weighted_mean_values[indices] += weights*values
313 if self.config.do_sum:
314 self.sum_values[indices] += values
315
316 def finalize_mean_values(self, total_weights, total_inputs):
317 """Finalize the accumulation of the mean and weighted mean.
318
319 Parameters
320 ----------
321 total_weights : `np.ndarray`
322 Total accumulated weights, for each value index.
323 total_inputs : `np.ndarray`
324 Total number of inputs, for each value index.
325 """
326 if self.config.do_mean:
327 use, = np.where(total_inputs > 0)
328 self.mean_values[use] /= total_inputs[use]
329 if self.config.do_weighted_mean:
330 use, = np.where(total_weights > 0.0)
331 self.weighted_mean_values[use] /= total_weights[use]
332
333 # And perform any necessary post-processing
334 self._post_process(total_weights, total_inputs)
335
336 def set_map_values(self, pixels):
337 """Assign accumulated values to the maps.
338
339 Parameters
340 ----------
341 pixels : `np.ndarray`
342 Array of healpix pixels (nest scheme) to set in the map.
343 """
344 if self.config.do_min:
345 self.min_map[pixels] = self.min_values
346 if self.config.do_max:
347 self.max_map[pixels] = self.max_values
348 if self.config.do_mean:
349 self.mean_map[pixels] = self.mean_values
350 if self.config.do_weighted_mean:
351 self.weighted_mean_map[pixels] = self.weighted_mean_values
352 if self.config.do_sum:
353 self.sum_map[pixels] = self.sum_values
354
355 def _compute(self, row, ra, dec, scalings, psf_array=None):
356 """Compute map value from a row in the visitSummary catalog.
357
358 Parameters
359 ----------
361 Row of a visitSummary ExposureCatalog.
362 ra : `np.ndarray`
363 Array of right ascensions
364 dec : `np.ndarray`
365 Array of declinations
366 scalings : `float` or `np.ndarray`
367 Scaling values to coadd zeropoint.
368 psf_array : `np.ndarray`, optional
369 Array of approximate psf values matched to ra/dec.
370 """
371 raise NotImplementedError("All property maps must implement _compute()")
372
373 def _post_process(self, total_weights, total_inputs):
374 """Perform post-processing on values.
375
376 Parameters
377 ----------
378 total_weights : `np.ndarray`
379 Total accumulated weights, for each value index.
380 total_inputs : `np.ndarray`
381 Total number of inputs, for each value index.
382 """
383 # Override of this method is not required.
384 pass
385
386
387@register_property_map("exposure_time")
389 """Exposure time property map."""
390
391 def _compute(self, row, ra, dec, scalings, psf_array=None):
392 return row.getVisitInfo().getExposureTime()
393
394
395@register_property_map("psf_size")
397 """PSF size property map."""
398 requires_psf = True
399
400 def _compute(self, row, ra, dec, scalings, psf_array=None):
401 return psf_array["psf_size"]
402
403
404@register_property_map("psf_e1")
406 """PSF shape e1 property map."""
407 requires_psf = True
408
409 def _compute(self, row, ra, dec, scalings, psf_array=None):
410 return psf_array["psf_e1"]
411
412
413@register_property_map("psf_e2")
415 """PSF shape e2 property map."""
416 requires_psf = True
417
418 def _compute(self, row, ra, dec, scalings, psf_array=None):
419 return psf_array["psf_e2"]
420
421
422@register_property_map("n_exposure")
424 """Number of exposures property map."""
425 dtype = np.int32
426
427 def _compute(self, row, ra, dec, scalings, psf_array=None):
428 return 1
429
430
432 """Configuration for the PsfMaglim property map."""
433 maglim_nsigma = pexConfig.Field(dtype=float, default=5.0,
434 doc="Number of sigma to compute magnitude limit.")
435
436 def validate(self):
437 super().validate()
438 if self.do_min or self.do_max or self.do_mean or self.do_sum:
439 raise ValueError("Can only use do_weighted_mean with PsfMaglimPropertyMap")
440
441
442@register_property_map("psf_maglim")
444 """PSF magnitude limit property map."""
445 requires_psf = True
446
447 ConfigClass = PsfMaglimPropertyMapConfig
448
449 def _compute(self, row, ra, dec, scalings, psf_array=None):
450 # Our values are the weighted mean of the psf area
451 return psf_array["psf_area"]
452
453 def _post_process(self, total_weights, total_inputs):
454 psf_area = self.weighted_mean_values.copy()
455 maglim = (self.zeropoint
456 - 2.5*np.log10(self.config.maglim_nsigma*np.sqrt(psf_area/total_weights)))
457 self.weighted_mean_values[:] = maglim
458
459
460@register_property_map("sky_background")
462 """Sky background property map."""
463 def _compute(self, row, ra, dec, scalings, psf_array=None):
464 return scalings*row["skyBg"]
465
466
467@register_property_map("sky_noise")
469 """Sky noise property map."""
470 def _compute(self, row, ra, dec, scalings, psf_array=None):
471 return scalings*row["skyNoise"]
472
473
474@register_property_map("dcr_dra")
476 """Effect of DCR on delta-RA property map."""
477 def _compute(self, row, ra, dec, scalings, psf_array=None):
478 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
479 return np.tan(np.deg2rad(row["zenithDistance"]))*np.sin(par_angle)
480
481
482@register_property_map("dcr_ddec")
484 """Effect of DCR on delta-Dec property map."""
485 def _compute(self, row, ra, dec, scalings, psf_array=None):
486 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
487 return np.tan(np.deg2rad(row["zenithDistance"]))*np.cos(par_angle)
488
489
490@register_property_map("dcr_e1")
492 """Effect of DCR on psf shape e1 property map."""
493 def _compute(self, row, ra, dec, scalings, psf_array=None):
494 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
495 return (np.tan(np.deg2rad(row["zenithDistance"]))**2.)*np.cos(2.*par_angle)
496
497
498@register_property_map("dcr_e2")
500 """Effect of DCR on psf shape e2 property map."""
501 def _compute(self, row, ra, dec, scalings, psf_array=None):
502 par_angle = row.getVisitInfo().getBoresightParAngle().asRadians()
503 return (np.tan(np.deg2rad(row["zenithDistance"]))**2.)*np.sin(2.*par_angle)
504
505
506@register_property_map("epoch")
508 """Observation epoch (mjd) property map."""
509 def _compute(self, row, ra, dec, scalings, psf_array=None):
510 date = row.getVisitInfo().getDate()
511 return date.get(date.MJD)
A control object used when fitting ChebyshevBoundedField to data (see ChebyshevBoundedField::fit)
Record class used to store exposure metadata.
Definition Exposure.h:79
An integer coordinate rectangle.
Definition Box.h:55
Point in an unspecified spherical coordinate system.
Definition SpherePoint.h:57
accumulate_values(self, indices, ra, dec, weights, scalings, row, psf_array=None)