Coverage for python/lsst/pipe/tasks/healSparseMappingProperties.py: 38%

216 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2024-02-08 07:10 +0000

1# 

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 """ 

59 class Configurable: 

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 

91def register_property_map(name): 

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 ---------- 

107 ccd_row : `lsst.afw.table.ExposureRecord` 

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 

131 ctrl = ChebyshevBoundedFieldControl() 

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 

200class BasePropertyMap: 

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. 

292 row : `lsst.afw.table.ExposureRecord` 

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 ---------- 

361 row : `lsst.afw.table.ExposureRecord` 

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") 

389class ExposureTimePropertyMap(BasePropertyMap): 

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") 

397class PsfSizePropertyMap(BasePropertyMap): 

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") 

406class PsfE1PropertyMap(BasePropertyMap): 

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") 

415class PsfE2PropertyMap(BasePropertyMap): 

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") 

424class NExposurePropertyMap(BasePropertyMap): 

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 

432class PsfMaglimPropertyMapConfig(BasePropertyMapConfig): 

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") 

444class PsfMaglimPropertyMap(BasePropertyMap): 

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") 

462class SkyBackgroundPropertyMap(BasePropertyMap): 

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") 

469class SkyNoisePropertyMap(BasePropertyMap): 

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") 

476class DcrDraPropertyMap(BasePropertyMap): 

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") 

484class DcrDdecPropertyMap(BasePropertyMap): 

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") 

492class DcrE1PropertyMap(BasePropertyMap): 

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") 

500class DcrE2PropertyMap(BasePropertyMap): 

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)