LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
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 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)
176
177 return psf_array
178
179
180class PropertyMapMap(dict):
181 """Map of property maps to be run for a given task.
182
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
192
193
195 """Base class for property maps.
196
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
206
207 ConfigClass = BasePropertyMapConfig
208
209 registry = PropertyMapRegistry(BasePropertyMapConfig)
210
211 def __init__(self, config, name):
212 object.__init__(self)
213 self.config = config
214 self.name = name
215 self.zeropoint = 0.0
216
217 def initialize_tract_maps(self, nside_coverage, nside):
218 """Initialize the tract maps.
219
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)
247
248 def initialize_values(self, n_pixels):
249 """Initialize the value arrays for accumulation.
250
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)
269
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.
273
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.
287 Row of a visitSummary ExposureCatalog.
288 psf_array : `np.ndarray`, optional
289 Array of approximate psf values matched to ra/dec.
290
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.")
298
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
310
311 def finalize_mean_values(self, total_weights, total_inputs):
312 """Finalize the accumulation of the mean and weighted mean.
313
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]
327
328 # And perform any necessary post-processing
329 self._post_process(total_weights, total_inputs)
330
331 def set_map_values(self, pixels):
332 """Assign accumulated values to the maps.
333
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
349
350 def _compute(self, row, ra, dec, scalings, psf_array=None):
351 """Compute map value from a row in the visitSummary catalog.
352
353 Parameters
354 ----------
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()")
367
368 def _post_process(self, total_weights, total_inputs):
369 """Perform post-processing on values.
370
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
380
381
382@register_property_map("exposure_time")
384 """Exposure time property map."""
385
386 def _compute(self, row, ra, dec, scalings, psf_array=None):
387 return row.getVisitInfo().getExposureTime()
388
389
390@register_property_map("psf_size")
392 """PSF size property map."""
393 requires_psf = True
394
395 def _compute(self, row, ra, dec, scalings, psf_array=None):
396 return psf_array["psf_size"]
397
398
399@register_property_map("psf_e1")
401 """PSF shape e1 property map."""
402 requires_psf = True
403
404 def _compute(self, row, ra, dec, scalings, psf_array=None):
405 return psf_array["psf_e1"]
406
407
408@register_property_map("psf_e2")
410 """PSF shape e2 property map."""
411 requires_psf = True
412
413 def _compute(self, row, ra, dec, scalings, psf_array=None):
414 return psf_array["psf_e2"]
415
416
417@register_property_map("n_exposure")
419 """Number of exposures property map."""
420 dtype = np.int32
421
422 def _compute(self, row, ra, dec, scalings, psf_array=None):
423 return 1
424
425
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.")
430
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")
435
436
437@register_property_map("psf_maglim")
439 """PSF magnitude limit property map."""
440 requires_psf = True
441
442 ConfigClass = PsfMaglimPropertyMapConfig
443
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"]
447
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
453
454
455@register_property_map("sky_background")
457 """Sky background property map."""
458 def _compute(self, row, ra, dec, scalings, psf_array=None):
459 return scalings*row["skyBg"]
460
461
462@register_property_map("sky_noise")
464 """Sky noise property map."""
465 def _compute(self, row, ra, dec, scalings, psf_array=None):
466 return scalings*row["skyNoise"]
467
468
469@register_property_map("dcr_dra")
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)
475
476
477@register_property_map("dcr_ddec")
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)
483
484
485@register_property_map("dcr_e1")
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)
491
492
493@register_property_map("dcr_e2")
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)
499
500
501@register_property_map("epoch")
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)
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
def _compute(self, row, ra, dec, scalings, psf_array=None)
def accumulate_values(self, indices, ra, dec, weights, scalings, row, psf_array=None)