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

221 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-27 12:31 +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 # 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 

199class BasePropertyMap: 

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. 

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

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

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

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

388class ExposureTimePropertyMap(BasePropertyMap): 

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

396class PsfSizePropertyMap(BasePropertyMap): 

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

405class PsfE1PropertyMap(BasePropertyMap): 

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

414class PsfE2PropertyMap(BasePropertyMap): 

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

423class NExposurePropertyMap(BasePropertyMap): 

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 

431class PsfMaglimPropertyMapConfig(BasePropertyMapConfig): 

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

443class PsfMaglimPropertyMap(BasePropertyMap): 

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

461class SkyBackgroundPropertyMap(BasePropertyMap): 

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

468class SkyNoisePropertyMap(BasePropertyMap): 

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

475class DcrDraPropertyMap(BasePropertyMap): 

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

483class DcrDdecPropertyMap(BasePropertyMap): 

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

491class DcrE1PropertyMap(BasePropertyMap): 

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

499class DcrE2PropertyMap(BasePropertyMap): 

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

507class EpochPropertyMap(BasePropertyMap): 

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)