Coverage for python/lsst/meas/algorithms/testUtils.py: 29%

100 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-04-12 02:22 -0700

1# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2017 AURA/LSST. 

5# 

6# This product includes software developed by the 

7# LSST Project (http://www.lsst.org/). 

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 LSST License Statement and 

20# the GNU General Public License along with this program. If not, 

21# see <https://www.lsstcorp.org/LegalNotices/>. 

22# 

23 

24__all__ = ["plantSources", "makeRandomTransmissionCurve", "makeDefectList", 

25 "MockReferenceObjectLoaderFromFiles"] 

26 

27import numpy as np 

28import esutil 

29 

30import lsst.geom 

31import lsst.afw.image as afwImage 

32from lsst import sphgeom 

33from . import SingleGaussianPsf 

34from . import Defect 

35 

36from . import ReferenceObjectLoader 

37import lsst.afw.table as afwTable 

38 

39 

40def plantSources(bbox, kwid, sky, coordList, addPoissonNoise=True): 

41 """Make an exposure with stars (modelled as Gaussians) 

42 

43 Parameters 

44 ---------- 

45 bbox : `lsst.geom.Box2I` 

46 Parent bbox of exposure 

47 kwid : `int` 

48 Kernal width (and height; kernal is square) 

49 sky : `float` 

50 Amount of sky background (counts) 

51 coordList : `list [tuple]` 

52 A list of [x, y, counts, sigma] where: 

53 * x,y are relative to exposure origin 

54 * counts is the integrated counts for the star 

55 * sigma is the Gaussian sigma in pixels 

56 addPoissonNoise : `bool` 

57 If True: add Poisson noise to the exposure 

58 """ 

59 # make an image with sources 

60 img = afwImage.ImageD(bbox) 

61 meanSigma = 0.0 

62 for coord in coordList: 

63 x, y, counts, sigma = coord 

64 meanSigma += sigma 

65 

66 # make a single gaussian psf 

67 psf = SingleGaussianPsf(kwid, kwid, sigma) 

68 

69 # make an image of it and scale to the desired number of counts 

70 thisPsfImg = psf.computeImage(lsst.geom.PointD(x, y)) 

71 thisPsfImg *= counts 

72 

73 # bbox a window in our image and add the fake star image 

74 psfBox = thisPsfImg.getBBox() 

75 psfBox.clip(bbox) 

76 if psfBox != thisPsfImg.getBBox(): 

77 thisPsfImg = thisPsfImg[psfBox, afwImage.PARENT] 

78 imgSeg = img[psfBox, afwImage.PARENT] 

79 imgSeg += thisPsfImg 

80 meanSigma /= len(coordList) 

81 

82 img += sky 

83 

84 # add Poisson noise 

85 if (addPoissonNoise): 

86 np.random.seed(seed=1) # make results reproducible 

87 imgArr = img.getArray() 

88 imgArr[:] = np.random.poisson(imgArr) 

89 

90 # bundle into a maskedimage and an exposure 

91 mask = afwImage.Mask(bbox) 

92 var = img.convertFloat() 

93 img -= sky 

94 mimg = afwImage.MaskedImageF(img.convertFloat(), mask, var) 

95 exposure = afwImage.makeExposure(mimg) 

96 

97 # insert an approximate psf 

98 psf = SingleGaussianPsf(kwid, kwid, meanSigma) 

99 exposure.setPsf(psf) 

100 

101 return exposure 

102 

103 

104def makeRandomTransmissionCurve(rng, minWavelength=4000.0, maxWavelength=7000.0, nWavelengths=200, 

105 maxRadius=80.0, nRadii=30, perturb=0.05): 

106 """Create a random TransmissionCurve with nontrivial spatial and 

107 wavelength variation. 

108 

109 Parameters 

110 ---------- 

111 rng : numpy.random.RandomState 

112 Random number generator. 

113 minWavelength : float 

114 Average minimum wavelength for generated TransmissionCurves (will be 

115 randomly perturbed). 

116 maxWavelength : float 

117 Average maximum wavelength for generated TransmissionCurves (will be 

118 randomly perturbed). 

119 nWavelengths : int 

120 Number of samples in the wavelength dimension. 

121 maxRadius : float 

122 Average maximum radius for spatial variation (will be perturbed). 

123 nRadii : int 

124 Number of samples in the radial dimension. 

125 perturb: float 

126 Fraction by which wavelength and radius bounds should be randomly 

127 perturbed. 

128 """ 

129 dWavelength = maxWavelength - minWavelength 

130 

131 def perturbed(x, s=perturb*dWavelength): 

132 return x + 2.0*s*(rng.rand() - 0.5) 

133 

134 wavelengths = np.linspace(perturbed(minWavelength), perturbed(maxWavelength), nWavelengths) 

135 radii = np.linspace(0.0, perturbed(maxRadius, perturb*maxRadius), nRadii) 

136 throughput = np.zeros(wavelengths.shape + radii.shape, dtype=float) 

137 # throughput will be a rectangle in wavelength, shifting to higher wavelengths and shrinking 

138 # in height with radius, going to zero at all bounds. 

139 peak0 = perturbed(0.9, 0.05) 

140 start0 = perturbed(minWavelength + 0.25*dWavelength) 

141 stop0 = perturbed(minWavelength + 0.75*dWavelength) 

142 for i, r in enumerate(radii): 

143 mask = np.logical_and(wavelengths >= start0 + r, wavelengths <= stop0 + r) 

144 throughput[mask, i] = peak0*(1.0 - r/1000.0) 

145 return afwImage.TransmissionCurve.makeRadial(throughput, wavelengths, radii) 

146 

147 

148def makeDefectList(): 

149 """Create a list of defects that can be used for testing. 

150 

151 Returns 

152 ------- 

153 defectList = `list` [`lsst.meas.algorithms.Defect`] 

154 The list of defects. 

155 """ 

156 defectList = [Defect(lsst.geom.Box2I(lsst.geom.Point2I(962, 0), 

157 lsst.geom.Extent2I(2, 4611))), 

158 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1316, 0), 

159 lsst.geom.Extent2I(2, 4611))), 

160 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1576, 0), 

161 lsst.geom.Extent2I(4, 4611))), 

162 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1626, 0), 

163 lsst.geom.Extent2I(2, 4611))), 

164 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1994, 252), 

165 lsst.geom.Extent2I(2, 4359))), 

166 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1426, 702), 

167 lsst.geom.Extent2I(2, 3909))), 

168 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1526, 1140), 

169 lsst.geom.Extent2I(2, 3471))), 

170 Defect(lsst.geom.Box2I(lsst.geom.Point2I(856, 2300), 

171 lsst.geom.Extent2I(2, 2311))), 

172 Defect(lsst.geom.Box2I(lsst.geom.Point2I(858, 2328), 

173 lsst.geom.Extent2I(2, 65))), 

174 Defect(lsst.geom.Box2I(lsst.geom.Point2I(859, 2328), 

175 lsst.geom.Extent2I(1, 56))), 

176 Defect(lsst.geom.Box2I(lsst.geom.Point2I(844, 2796), 

177 lsst.geom.Extent2I(4, 1814))), 

178 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1366, 2804), 

179 lsst.geom.Extent2I(2, 1806))), 

180 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1766, 3844), 

181 lsst.geom.Extent2I(2, 766))), 

182 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1872, 4228), 

183 lsst.geom.Extent2I(2, 382))), 

184 ] 

185 

186 return defectList 

187 

188 

189class MockRefcatDataId: 

190 """Mock reference catalog dataId. 

191 

192 The reference catalog dataId is only used to retrieve a region property. 

193 

194 Parameters 

195 ---------- 

196 pixelization : `lsst.sphgeom.Pixelization` 

197 Pixelization object. 

198 index : `int` 

199 Pixel index number. 

200 """ 

201 def __init__(self, pixelization, index): 

202 self._region = pixelization.pixel(index) 

203 

204 @property 

205 def region(self): 

206 return self._region 

207 

208 

209class MockRefcatDeferredDatasetHandle: 

210 """Mock reference catalog dataset handle. 

211 

212 The mock handle needs a get() and a name for logging. 

213 

214 Parameters 

215 ---------- 

216 catalog : `lsst.afw.table.SourceCatalog` 

217 Reference catalog. 

218 name : `str` 

219 Name of reference catalog. 

220 """ 

221 def __init__(self, catalog, name): 

222 self._catalog = catalog 

223 self._name = name 

224 

225 def get(self): 

226 return self._catalog 

227 

228 class MockRef: 

229 def __init__(self, name): 

230 self._name = name 

231 

232 class MockDatasetType: 

233 def __init__(self, name): 

234 self._name = name 

235 

236 @property 

237 def name(self): 

238 return self._name 

239 

240 @property 

241 def datasetType(self): 

242 return self.MockDatasetType(self._name) 

243 

244 @property 

245 def ref(self): 

246 return self.MockRef(self._name) 

247 

248 

249class MockReferenceObjectLoaderFromFiles(ReferenceObjectLoader): 

250 """A simple mock of ReferenceObjectLoader. 

251 

252 This mock ReferenceObjectLoader uses a set of files on disk to create 

253 mock dataIds and data reference handles that can be accessed 

254 without a butler. The files must be afw catalog files in the reference 

255 catalog format, sharded with HTM pixelization. 

256 

257 Parameters 

258 ---------- 

259 filenames : `list` [`str`] 

260 Names of files to use. 

261 config : `lsst.meas.astrom.LoadReferenceObjectsConfig`, optional 

262 Configuration object if necessary to override defaults. 

263 htmLevel : `int`, optional 

264 HTM level to use for the loader. 

265 """ 

266 def __init__(self, filenames, name='cal_ref_cat', config=None, htmLevel=4): 

267 dataIds, refCats = self._createDataIdsAndRefcats(filenames, htmLevel, name) 

268 

269 super().__init__(dataIds, refCats, name=name, config=config) 

270 

271 def _createDataIdsAndRefcats(self, filenames, htmLevel, name): 

272 """Create mock dataIds and refcat handles. 

273 

274 Parameters 

275 ---------- 

276 filenames : `list` [`str`] 

277 Names of files to use. 

278 htmLevel : `int` 

279 HTM level to use for the loader. 

280 name : `str` 

281 Name of reference catalog (for logging). 

282 

283 Returns 

284 ------- 

285 dataIds : `list` [`MockRefcatDataId`] 

286 List of mock dataIds. 

287 refCats : `list` [`MockRefcatDeferredDatasetHandle`] 

288 List of mock deferred dataset handles. 

289 

290 Raises 

291 ------ 

292 RuntimeError if any file contains sources that cover more than one HTM 

293 pixel at level ``htmLevel``. 

294 """ 

295 pixelization = sphgeom.HtmPixelization(htmLevel) 

296 htm = esutil.htm.HTM(htmLevel) 

297 

298 dataIds = [] 

299 refCats = [] 

300 

301 for filename in filenames: 

302 cat = afwTable.BaseCatalog.readFits(filename) 

303 

304 ids = htm.lookup_id(np.rad2deg(cat['coord_ra']), np.rad2deg(cat['coord_dec'])) 

305 

306 if len(np.unique(ids)) != 1: 

307 raise RuntimeError(f"File {filename} contains more than one pixel at level {htmLevel}") 

308 

309 dataIds.append(MockRefcatDataId(pixelization, ids[0])) 

310 refCats.append(MockRefcatDeferredDatasetHandle(cat, name)) 

311 

312 return dataIds, refCats