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

218 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-13 04:14 +0000

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

58 class Configurable: 

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 

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 

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

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. 

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 

130 ctrl = ChebyshevBoundedFieldControl() 

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 

194class BasePropertyMap: 

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. 

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. 

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

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

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

383class ExposureTimePropertyMap(BasePropertyMap): 

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

391class PsfSizePropertyMap(BasePropertyMap): 

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

400class PsfE1PropertyMap(BasePropertyMap): 

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

409class PsfE2PropertyMap(BasePropertyMap): 

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

418class NExposurePropertyMap(BasePropertyMap): 

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 

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

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

438class PsfMaglimPropertyMap(BasePropertyMap): 

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

456class SkyBackgroundPropertyMap(BasePropertyMap): 

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

463class SkyNoisePropertyMap(BasePropertyMap): 

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

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) 

475 

476 

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) 

483 

484 

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) 

491 

492 

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) 

499 

500 

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)