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

80 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-23 08:56 +0000

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 

27import numpy as np 

28import esutil 

29 

30import lsst.geom 

31import lsst.afw.image as afwImage 

32from lsst.pipe.base import InMemoryDatasetHandle 

33from lsst import sphgeom 

34from . import SingleGaussianPsf 

35from . import Defect 

36 

37from . import ReferenceObjectLoader 

38import lsst.afw.table as afwTable 

39 

40 

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

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

43 

44 Parameters 

45 ---------- 

46 bbox : `lsst.geom.Box2I` 

47 Parent bbox of exposure 

48 kwid : `int` 

49 Kernal width (and height; kernal is square) 

50 sky : `float` 

51 Amount of sky background (counts) 

52 coordList : `list [tuple]` 

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

54 * x,y are relative to exposure origin 

55 * counts is the integrated counts for the star 

56 * sigma is the Gaussian sigma in pixels 

57 addPoissonNoise : `bool` 

58 If True: add Poisson noise to the exposure 

59 """ 

60 # make an image with sources 

61 img = afwImage.ImageD(bbox) 

62 meanSigma = 0.0 

63 for coord in coordList: 

64 x, y, counts, sigma = coord 

65 meanSigma += sigma 

66 

67 # make a single gaussian psf 

68 psf = SingleGaussianPsf(kwid, kwid, sigma) 

69 

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

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

72 thisPsfImg *= counts 

73 

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

75 psfBox = thisPsfImg.getBBox() 

76 psfBox.clip(bbox) 

77 if psfBox != thisPsfImg.getBBox(): 

78 thisPsfImg = thisPsfImg[psfBox, afwImage.PARENT] 

79 imgSeg = img[psfBox, afwImage.PARENT] 

80 imgSeg += thisPsfImg 

81 meanSigma /= len(coordList) 

82 

83 img += sky 

84 

85 # add Poisson noise 

86 if (addPoissonNoise): 

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

88 imgArr = img.getArray() 

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

90 

91 # bundle into a maskedimage and an exposure 

92 mask = afwImage.Mask(bbox) 

93 var = img.convertFloat() 

94 img -= sky 

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

96 exposure = afwImage.makeExposure(mimg) 

97 

98 # insert an approximate psf 

99 psf = SingleGaussianPsf(kwid, kwid, meanSigma) 

100 exposure.setPsf(psf) 

101 

102 return exposure 

103 

104 

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

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

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

108 wavelength variation. 

109 

110 Parameters 

111 ---------- 

112 rng : numpy.random.RandomState 

113 Random number generator. 

114 minWavelength : float 

115 Average minimum wavelength for generated TransmissionCurves (will be 

116 randomly perturbed). 

117 maxWavelength : float 

118 Average maximum wavelength for generated TransmissionCurves (will be 

119 randomly perturbed). 

120 nWavelengths : int 

121 Number of samples in the wavelength dimension. 

122 maxRadius : float 

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

124 nRadii : int 

125 Number of samples in the radial dimension. 

126 perturb: float 

127 Fraction by which wavelength and radius bounds should be randomly 

128 perturbed. 

129 """ 

130 dWavelength = maxWavelength - minWavelength 

131 

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

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

134 

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

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

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

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

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

140 peak0 = perturbed(0.9, 0.05) 

141 start0 = perturbed(minWavelength + 0.25*dWavelength) 

142 stop0 = perturbed(minWavelength + 0.75*dWavelength) 

143 for i, r in enumerate(radii): 

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

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

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

147 

148 

149def makeDefectList(): 

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

151 

152 Returns 

153 ------- 

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

155 The list of defects. 

156 """ 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

185 ] 

186 

187 return defectList 

188 

189 

190class MockRefcatDataId: 

191 """Mock reference catalog dataId. 

192 

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

194 

195 Parameters 

196 ---------- 

197 region : `lsst.sphgeom.Region` 

198 The region associated with this mock dataId. 

199 """ 

200 def __init__(self, region): 

201 self._region = region 

202 

203 @property 

204 def region(self): 

205 return self._region 

206 

207 

208class MockReferenceObjectLoaderFromFiles(ReferenceObjectLoader): 

209 """A simple mock of ReferenceObjectLoader. 

210 

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

212 mock dataIds and data reference handles that can be accessed 

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

214 catalog format, sharded with HTM pixelization. 

215 

216 Parameters 

217 ---------- 

218 filenames : `list` [`str`] 

219 Names of files to use. 

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

221 Configuration object if necessary to override defaults. 

222 htmLevel : `int`, optional 

223 HTM level to use for the loader. 

224 """ 

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

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

227 

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

229 

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

231 """Create mock dataIds and refcat handles. 

232 

233 Parameters 

234 ---------- 

235 filenames : `list` [`str`] 

236 Names of files to use. 

237 htmLevel : `int` 

238 HTM level to use for the loader. 

239 name : `str` 

240 Name of reference catalog (for logging). 

241 

242 Returns 

243 ------- 

244 dataIds : `list` [`MockRefcatDataId`] 

245 List of mock dataIds. 

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

247 List of mock deferred dataset handles. 

248 

249 Raises 

250 ------ 

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

252 pixel at level ``htmLevel``. 

253 """ 

254 pixelization = sphgeom.HtmPixelization(htmLevel) 

255 htm = esutil.htm.HTM(htmLevel) 

256 

257 dataIds = [] 

258 refCats = [] 

259 

260 for filename in filenames: 

261 cat = afwTable.BaseCatalog.readFits(filename) 

262 

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

264 

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

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

267 

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

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

270 

271 return dataIds, refCats