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

96 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-22 02:38 -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", "MockRefcatDataId", 

26 "MockReferenceObjectLoaderFromMemory"] 

27 

28import numpy as np 

29import esutil 

30 

31import lsst.geom 

32import lsst.afw.image as afwImage 

33from lsst.pipe.base import InMemoryDatasetHandle 

34from lsst import sphgeom 

35from . import SingleGaussianPsf 

36from . import Defect 

37 

38from . import ReferenceObjectLoader 

39import lsst.afw.table as afwTable 

40 

41 

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

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

44 

45 Parameters 

46 ---------- 

47 bbox : `lsst.geom.Box2I` 

48 Parent bbox of exposure 

49 kwid : `int` 

50 Kernal width (and height; kernal is square) 

51 sky : `float` 

52 Amount of sky background (counts) 

53 coordList : `list [tuple]` 

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

55 * x,y are relative to exposure origin 

56 * counts is the integrated counts for the star 

57 * sigma is the Gaussian sigma in pixels 

58 addPoissonNoise : `bool` 

59 If True: add Poisson noise to the exposure 

60 """ 

61 # make an image with sources 

62 img = afwImage.ImageD(bbox) 

63 meanSigma = 0.0 

64 for coord in coordList: 

65 x, y, counts, sigma = coord 

66 meanSigma += sigma 

67 

68 # make a single gaussian psf 

69 psf = SingleGaussianPsf(kwid, kwid, sigma) 

70 

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

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

73 thisPsfImg *= counts 

74 

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

76 psfBox = thisPsfImg.getBBox() 

77 psfBox.clip(bbox) 

78 if psfBox != thisPsfImg.getBBox(): 

79 thisPsfImg = thisPsfImg[psfBox, afwImage.PARENT] 

80 imgSeg = img[psfBox, afwImage.PARENT] 

81 imgSeg += thisPsfImg 

82 meanSigma /= len(coordList) 

83 

84 img += sky 

85 

86 # add Poisson noise 

87 if (addPoissonNoise): 

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

89 imgArr = img.getArray() 

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

91 

92 # bundle into a maskedimage and an exposure 

93 mask = afwImage.Mask(bbox) 

94 var = img.convertFloat() 

95 img -= sky 

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

97 exposure = afwImage.makeExposure(mimg) 

98 

99 # insert an approximate psf 

100 psf = SingleGaussianPsf(kwid, kwid, meanSigma) 

101 exposure.setPsf(psf) 

102 

103 return exposure 

104 

105 

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

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

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

109 wavelength variation. 

110 

111 Parameters 

112 ---------- 

113 rng : numpy.random.RandomState 

114 Random number generator. 

115 minWavelength : float 

116 Average minimum wavelength for generated TransmissionCurves (will be 

117 randomly perturbed). 

118 maxWavelength : float 

119 Average maximum wavelength for generated TransmissionCurves (will be 

120 randomly perturbed). 

121 nWavelengths : int 

122 Number of samples in the wavelength dimension. 

123 maxRadius : float 

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

125 nRadii : int 

126 Number of samples in the radial dimension. 

127 perturb: float 

128 Fraction by which wavelength and radius bounds should be randomly 

129 perturbed. 

130 """ 

131 dWavelength = maxWavelength - minWavelength 

132 

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

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

135 

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

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

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

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

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

141 peak0 = perturbed(0.9, 0.05) 

142 start0 = perturbed(minWavelength + 0.25*dWavelength) 

143 stop0 = perturbed(minWavelength + 0.75*dWavelength) 

144 for i, r in enumerate(radii): 

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

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

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

148 

149 

150def makeDefectList(): 

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

152 

153 Returns 

154 ------- 

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

156 The list of defects. 

157 """ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

186 ] 

187 

188 return defectList 

189 

190 

191class MockRefcatDataId: 

192 """Mock reference catalog dataId. 

193 

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

195 

196 Parameters 

197 ---------- 

198 region : `lsst.sphgeom.Region` 

199 The region associated with this mock dataId. 

200 """ 

201 def __init__(self, region): 

202 self._region = region 

203 

204 @property 

205 def region(self): 

206 return self._region 

207 

208 

209class MockReferenceObjectLoaderFromFiles(ReferenceObjectLoader): 

210 """A mock of ReferenceObjectLoader using files on disk. 

211 

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

213 mock dataIds and data reference handles that can be accessed 

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

215 catalog format, sharded with HTM pixelization. 

216 

217 Parameters 

218 ---------- 

219 filenames : `list` [`str`] 

220 Names of files to use. 

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

222 Configuration object if necessary to override defaults. 

223 htmLevel : `int`, optional 

224 HTM level to use for the loader. 

225 """ 

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

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

228 

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

230 

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

232 """Create mock dataIds and refcat handles. 

233 

234 Parameters 

235 ---------- 

236 filenames : `list` [`str`] 

237 Names of files to use. 

238 htmLevel : `int` 

239 HTM level to use for the loader. 

240 name : `str` 

241 Name of reference catalog (for logging). 

242 

243 Returns 

244 ------- 

245 dataIds : `list` [`MockRefcatDataId`] 

246 List of mock dataIds. 

247 refCats : `list` [`lsst.pipe.base.InMemoryDatasetHandle`] 

248 List of mock deferred dataset handles. 

249 

250 Raises 

251 ------ 

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

253 pixel at level ``htmLevel``. 

254 """ 

255 pixelization = sphgeom.HtmPixelization(htmLevel) 

256 htm = esutil.htm.HTM(htmLevel) 

257 

258 dataIds = [] 

259 refCats = [] 

260 

261 for filename in filenames: 

262 cat = afwTable.BaseCatalog.readFits(filename) 

263 

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

265 

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

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

268 

269 dataIds.append(MockRefcatDataId(pixelization.pixel(ids[0]))) 

270 refCats.append(InMemoryDatasetHandle(cat, name=name)) 

271 

272 return dataIds, refCats 

273 

274 

275class MockReferenceObjectLoaderFromMemory(ReferenceObjectLoader): 

276 """A mock of ReferenceObjectLoader using catalogs in memory. 

277 

278 Parameters 

279 ---------- 

280 catalogs : `list` [`lsst.afw.table.SimpleCatalog`] 

281 In-memory catalogs to use to mock dataIds. 

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

283 Configuration object if necessary to override defaults. 

284 htmLevel : `int`, optional 

285 HTM level to use for the loader. 

286 """ 

287 def __init__(self, catalogs, name='mock_ref_cat', config=None, htmLevel=4): 

288 dataIds, refCats = self._createDataIdsAndRefcats(catalogs, htmLevel, name) 

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

290 

291 def _createDataIdsAndRefcats(self, catalogs, htmLevel, name): 

292 pixelization = sphgeom.HtmPixelization(htmLevel) 

293 htm = esutil.htm.HTM(htmLevel) 

294 

295 dataIds = [] 

296 refCats = [] 

297 

298 for i, catalog in enumerate(catalogs): 

299 ids = htm.lookup_id(np.rad2deg(catalog['coord_ra']), np.rad2deg(catalog['coord_dec'])) 

300 

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

302 raise RuntimeError(f"Catalog number {i} contains more than one pixel at level {htmLevel}") 

303 

304 dataIds.append(MockRefcatDataId(pixelization.pixel(ids[0]))) 

305 refCats.append(InMemoryDatasetHandle(catalog, name=name)) 

306 

307 return dataIds, refCats