Coverage for python/lsst/ap/pipe/createApFakes.py: 35%

76 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-03 03:14 -0700

1# This file is part of ap_pipe. 

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 

22import numpy as np 

23import pandas as pd 

24import uuid 

25 

26import lsst.pex.config as pexConfig 

27from lsst.pipe.base import PipelineTask, PipelineTaskConnections, Struct 

28import lsst.pipe.base.connectionTypes as connTypes 

29from lsst.pipe.tasks.insertFakes import InsertFakesConfig 

30from lsst.skymap import BaseSkyMap 

31 

32__all__ = ["CreateRandomApFakesTask", 

33 "CreateRandomApFakesConfig", 

34 "CreateRandomApFakesConnections"] 

35 

36 

37class CreateRandomApFakesConnections(PipelineTaskConnections, 

38 defaultTemplates={"fakesType": "fakes_"}, 

39 dimensions=("tract", "skymap")): 

40 skyMap = connTypes.Input( 

41 doc="Input definition of geometry/bbox and projection/wcs for " 

42 "template exposures", 

43 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

44 dimensions=("skymap",), 

45 storageClass="SkyMap", 

46 ) 

47 fakeCat = connTypes.Output( 

48 doc="Catalog of fake sources to draw inputs from.", 

49 name="{fakesType}fakeSourceCat", 

50 storageClass="DataFrame", 

51 dimensions=("tract", "skymap") 

52 ) 

53 

54 

55class CreateRandomApFakesConfig( 

56 InsertFakesConfig, 

57 pipelineConnections=CreateRandomApFakesConnections): 

58 """Config for CreateRandomApFakesTask. Copy from the InsertFakesConfig to 

59 assert that columns created with in this task match that those expected in 

60 the InsertFakes and related tasks. 

61 """ 

62 fakeDensity = pexConfig.RangeField( 

63 doc="Goal density of random fake sources per square degree. Default " 

64 "value is roughly the density per square degree for ~10k sources " 

65 "visit.", 

66 dtype=float, 

67 default=1000, 

68 min=0, 

69 ) 

70 filterSet = pexConfig.ListField( 

71 doc="Set of Abstract filter names to produce magnitude columns for.", 

72 dtype=str, 

73 default=["u", "g", "r", "i", "z", "y"], 

74 ) 

75 fraction = pexConfig.RangeField( 

76 doc="Fraction of the created source that should be inserted into both " 

77 "the visit and template images. Values less than 1 will result in " 

78 "(1 - fraction) / 2 inserted into only visit or the template.", 

79 dtype=float, 

80 default=1/3, 

81 min=0, 

82 max=1, 

83 ) 

84 magMin = pexConfig.RangeField( 

85 doc="Minimum magnitude the mag distribution. All magnitudes requested " 

86 "are set to the same value.", 

87 dtype=float, 

88 default=20, 

89 min=1, 

90 max=40, 

91 ) 

92 magMax = pexConfig.RangeField( 

93 doc="Maximum magnitude the mag distribution. All magnitudes requested " 

94 "are set to the same value.", 

95 dtype=float, 

96 default=30, 

97 min=1, 

98 max=40, 

99 ) 

100 visitSourceFlagCol = pexConfig.Field( 

101 doc="Name of the column flagging objects for insertion into the visit " 

102 "image.", 

103 dtype=str, 

104 default="isVisitSource" 

105 ) 

106 templateSourceFlagCol = pexConfig.Field( 

107 doc="Name of the column flagging objects for insertion into the " 

108 "template image.", 

109 dtype=str, 

110 default="isTemplateSource" 

111 ) 

112 

113 

114class CreateRandomApFakesTask(PipelineTask): 

115 """Create and store a set of spatially uniform star fakes over the sphere 

116 for use in AP processing. Additionally assign random magnitudes to said 

117 fakes and assign them to be inserted into either a visit exposure or 

118 template exposure. 

119 """ 

120 

121 _DefaultName = "createApFakes" 

122 ConfigClass = CreateRandomApFakesConfig 

123 

124 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

125 inputs = butlerQC.get(inputRefs) 

126 inputs["tractId"] = butlerQC.quantum.dataId["tract"] 

127 

128 outputs = self.run(**inputs) 

129 butlerQC.put(outputs, outputRefs) 

130 

131 def run(self, tractId, skyMap): 

132 """Create a set of uniform random points that covers a tract. 

133 

134 Parameters 

135 ---------- 

136 tractId : `int` 

137 Tract id to produce randoms over. 

138 skyMap : `lsst.skymap.SkyMap` 

139 Skymap to produce randoms over. 

140 

141 Returns 

142 ------- 

143 randoms : `pandas.DataFrame` 

144 Catalog of random points covering the given tract. Follows the 

145 columns and format expected in `lsst.pipe.tasks.InsertFakes`. 

146 """ 

147 # Use the tractId as the ranomd seed. 

148 rng = np.random.default_rng(tractId) 

149 tractBoundingCircle = \ 

150 skyMap.generateTract(tractId).getInnerSkyPolygon().getBoundingCircle() 

151 tractArea = tractBoundingCircle.getArea() * (180 / np.pi) ** 2 

152 nFakes = int(self.config.fakeDensity * tractArea) 

153 

154 self.log.info( 

155 f"Creating {nFakes} star fakes over tractId={tractId} with " 

156 f"bounding circle area: {tractArea:.4f} deg^2 and " 

157 f"magnitude range: [{self.config.magMin, self.config.magMax}]") 

158 

159 # Concatenate the data and add dummy values for the unused variables. 

160 # Set all data to PSF like objects. 

161 randData = { 

162 "fakeId": [uuid.uuid4().int & (1 << 64) - 1 for n in range(nFakes)], 

163 **self.createRandomPositions(nFakes, tractBoundingCircle, rng), 

164 **self.createVisitCoaddSubdivision(nFakes), 

165 **self.createRandomMagnitudes(nFakes, rng), 

166 self.config.disk_semimajor_col: np.ones(nFakes, dtype="float"), 

167 self.config.bulge_semimajor_col: np.ones(nFakes, dtype="float"), 

168 self.config.disk_n_col: np.ones(nFakes, dtype="float"), 

169 self.config.bulge_n_col: np.ones(nFakes, dtype="float"), 

170 self.config.disk_axis_ratio_col: np.ones(nFakes, dtype="float"), 

171 self.config.bulge_axis_ratio_col: np.ones(nFakes, dtype="float"), 

172 self.config.disk_pa_col: np.zeros(nFakes, dtype="float"), 

173 self.config.bulge_pa_col: np.ones(nFakes, dtype="float"), 

174 self.config.sourceType: nFakes * ["star"]} 

175 

176 return Struct(fakeCat=pd.DataFrame(data=randData)) 

177 

178 def createRandomPositions(self, nFakes, boundingCircle, rng): 

179 """Create a set of spatially uniform randoms over the tract bounding 

180 circle on the sphere. 

181 

182 Parameters 

183 ---------- 

184 nFakes : `int` 

185 Number of fakes to create. 

186 boundingCicle : `lsst.sphgeom.BoundingCircle` 

187 Circle bound covering the tract. 

188 rng : `numpy.random.Generator` 

189 Initialized random number generator. 

190 

191 Returns 

192 ------- 

193 data : `dict`[`str`, `numpy.ndarray`] 

194 Dictionary of RA and Dec locations over the tract. 

195 """ 

196 # Create uniform random vectors on the sky around the north pole. 

197 randVect = np.empty((nFakes, 3)) 

198 randVect[:, 2] = rng.uniform( 

199 np.cos(boundingCircle.getOpeningAngle().asRadians()), 

200 1, 

201 nFakes) 

202 sinRawTheta = np.sin(np.arccos(randVect[:, 2])) 

203 rawPhi = rng.uniform(0, 2 * np.pi, nFakes) 

204 randVect[:, 0] = sinRawTheta * np.cos(rawPhi) 

205 randVect[:, 1] = sinRawTheta * np.sin(rawPhi) 

206 

207 # Compute the rotation matrix to move our random points to the 

208 # correct location. 

209 rotMatrix = self._createRotMatrix(boundingCircle) 

210 randVect = np.dot(rotMatrix, randVect.transpose()).transpose() 

211 decs = np.arcsin(randVect[:, 2]) 

212 ras = np.arctan2(randVect[:, 1], randVect[:, 0]) 

213 

214 return {self.config.dec_col: decs, 

215 self.config.ra_col: ras} 

216 

217 def _createRotMatrix(self, boundingCircle): 

218 """Compute the 3d rotation matrix to rotate the dec=90 pole to the 

219 center of the circle bound. 

220 

221 Parameters 

222 ---------- 

223 boundingCircle : `lsst.sphgeom.BoundingCircle` 

224 Circle bound covering the tract. 

225 

226 Returns 

227 ------- 

228 rotMatrix : `numpy.ndarray`, (3, 3) 

229 3x3 rotation matrix to rotate the dec=90 pole to the location of 

230 the circle bound. 

231 

232 Notes 

233 ----- 

234 Rotation matrix follows 

235 https://mathworld.wolfram.com/RodriguesRotationFormula.html 

236 """ 

237 # Get the center point of our tract 

238 center = boundingCircle.getCenter() 

239 

240 # Compute the axis to rotate around. This is done by taking the cross 

241 # product of dec=90 pole into the tract center. 

242 cross = np.array([-center.y(), 

243 center.x(), 

244 0]) 

245 cross /= np.sqrt(cross[0] ** 2 + cross[1] ** 2 + cross[2] ** 2) 

246 

247 # Get the cosine and sine of the dec angle of the tract center. This 

248 # is the amount of rotation needed to move the points we created from 

249 # around the pole to the tract location. 

250 cosTheta = center.z() 

251 sinTheta = np.sin(np.arccos(center.z())) 

252 

253 # Compose the rotation matrix for rotation around the axis created from 

254 # the cross product. 

255 rotMatrix = cosTheta * np.array([[1, 0, 0], 

256 [0, 1, 0], 

257 [0, 0, 1]]) 

258 rotMatrix += sinTheta * np.array([[0, -cross[2], cross[1]], 

259 [cross[2], 0, -cross[0]], 

260 [-cross[1], cross[0], 0]]) 

261 rotMatrix += ( 

262 (1 - cosTheta) 

263 * np.array( 

264 [[cross[0] ** 2, cross[0] * cross[1], cross[0] * cross[2]], 

265 [cross[0] * cross[1], cross[1] ** 2, cross[1] * cross[2]], 

266 [cross[0] * cross[2], cross[1] * cross[2], cross[2] ** 2]]) 

267 ) 

268 return rotMatrix 

269 

270 def createVisitCoaddSubdivision(self, nFakes): 

271 """Assign a given fake either a visit image or coadd or both based on 

272 the ``faction`` config value. 

273 

274 Parameters 

275 ---------- 

276 nFakes : `int` 

277 Number of fakes to create. 

278 

279 Returns 

280 ------- 

281 output : `dict`[`str`, `numpy.ndarray`] 

282 Dictionary of boolean arrays specifying which image to put a 

283 given fake into. 

284 """ 

285 nBoth = int(self.config.fraction * nFakes) 

286 nOnly = int((1 - self.config.fraction) / 2 * nFakes) 

287 isVisitSource = np.zeros(nFakes, dtype=bool) 

288 isTemplateSource = np.zeros(nFakes, dtype=bool) 

289 if nBoth > 0: 

290 isVisitSource[:nBoth] = True 

291 isTemplateSource[:nBoth] = True 

292 if nOnly > 0: 

293 isVisitSource[nBoth:(nBoth + nOnly)] = True 

294 isTemplateSource[(nBoth + nOnly):] = True 

295 

296 return {self.config.visitSourceFlagCol: isVisitSource, 

297 self.config.templateSourceFlagCol: isTemplateSource} 

298 

299 def createRandomMagnitudes(self, nFakes, rng): 

300 """Create a random distribution of magnitudes for out fakes. 

301 

302 Parameters 

303 ---------- 

304 nFakes : `int` 

305 Number of fakes to create. 

306 rng : `numpy.random.Generator` 

307 Initialized random number generator. 

308 

309 Returns 

310 ------- 

311 randMags : `dict`[`str`, `numpy.ndarray`] 

312 Dictionary of magnitudes in the bands set by the ``filterSet`` 

313 config option. 

314 """ 

315 mags = rng.uniform(self.config.magMin, 

316 self.config.magMax, 

317 size=nFakes) 

318 randMags = {} 

319 for fil in self.config.filterSet: 

320 randMags[self.config.mag_col % fil] = mags 

321 

322 return randMags