Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

30 

31__all__ = ["CreateRandomApFakesTask", 

32 "CreateRandomApFakesConfig", 

33 "CreateRandomApFakesConnections"] 

34 

35 

36class CreateRandomApFakesConnections(PipelineTaskConnections, 

37 defaultTemplates={"CoaddName": "deep"}, 

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

39 skyMap = connTypes.Input( 

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

41 "template exposures", 

42 name="{CoaddName}Coadd_skyMap", 

43 dimensions=("skymap",), 

44 storageClass="SkyMap", 

45 ) 

46 fakeCat = connTypes.Output( 

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

48 name="{CoaddName}Coadd_fakeSourceCat", 

49 storageClass="DataFrame", 

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

51 ) 

52 

53 

54class CreateRandomApFakesConfig( 

55 InsertFakesConfig, 

56 pipelineConnections=CreateRandomApFakesConnections): 

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

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

59 the InsertFakes and related tasks. 

60 """ 

61 fakeDensity = pexConfig.RangeField( 

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

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

64 "visit.", 

65 dtype=float, 

66 default=1000, 

67 min=0, 

68 ) 

69 filterSet = pexConfig.ListField( 

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

71 dtype=str, 

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

73 ) 

74 fraction = pexConfig.RangeField( 

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

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

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

78 dtype=float, 

79 default=1/3, 

80 min=0, 

81 max=1, 

82 ) 

83 magMin = pexConfig.RangeField( 

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

85 "are set to the same value.", 

86 dtype=float, 

87 default=20, 

88 min=1, 

89 max=40, 

90 ) 

91 magMax = pexConfig.RangeField( 

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

93 "are set to the same value.", 

94 dtype=float, 

95 default=30, 

96 min=1, 

97 max=40, 

98 ) 

99 randomSeed = pexConfig.Field( 

100 doc="Random seed to set for reproducible datasets", 

101 dtype=int, 

102 default=1234, 

103 ) 

104 visitSourceFlagCol = pexConfig.Field( 

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

106 "image.", 

107 dtype=str, 

108 default="isVisitSource" 

109 ) 

110 templateSourceFlagCol = pexConfig.Field( 

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

112 "template image.", 

113 dtype=str, 

114 default="isTemplateSource" 

115 ) 

116 

117 

118class CreateRandomApFakesTask(PipelineTask): 

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

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

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

122 template exposure. 

123 """ 

124 

125 _DefaultName = "createApFakes" 

126 ConfigClass = CreateRandomApFakesConfig 

127 

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

129 inputs = butlerQC.get(inputRefs) 

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

131 

132 outputs = self.run(**inputs) 

133 butlerQC.put(outputs, outputRefs) 

134 

135 def run(self, tractId, skyMap): 

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

137 

138 Parameters 

139 ---------- 

140 tractId : `int` 

141 Tract id to produce randoms over. 

142 skyMap : `lsst.skymap.SkyMap` 

143 Skymap to produce randoms over. 

144 

145 Returns 

146 ------- 

147 randoms : `pandas.DataFrame` 

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

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

150 """ 

151 rng = np.random.default_rng(self.config.randomSeed) 

152 tractBoundingCircle = \ 

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

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

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

156 

157 self.log.info( 

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

159 f"bounding circle area: {tractArea} deg^2") 

160 

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

162 # Set all data to PSF like objects. 

163 randData = { 

164 "fakeId": [uuid.uuid4().int for n in range(nFakes)], 

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

166 **self.createVisitCoaddSubdivision(nFakes), 

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

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

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

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

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

172 self.config.aDisk: np.ones(nFakes, dtype="float"), 

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

174 self.config.bDisk: np.ones(nFakes, dtype="float"), 

175 self.config.bBulge: np.ones(nFakes, dtype="float"), 

176 self.config.paDisk: np.ones(nFakes, dtype="float"), 

177 self.config.paBulge: np.ones(nFakes, dtype="float"), 

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

179 

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

181 

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

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

184 circle on the sphere. 

185 

186 Parameters 

187 ---------- 

188 nFakes : `int` 

189 Number of fakes to create. 

190 boundingCicle : `lsst.sphgeom.BoundingCircle` 

191 Circle bound covering the tract. 

192 rng : `numpy.random.Generator` 

193 Initialized random number generator. 

194 

195 Returns 

196 ------- 

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

198 Dictionary of RA and Dec locations over the tract. 

199 """ 

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

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

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

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

204 1, 

205 nFakes) 

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

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

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

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

210 

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

212 # correct location. 

213 rotMatrix = self._createRotMatrix(boundingCircle) 

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

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

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

217 

218 return {self.config.decColName: decs, 

219 self.config.raColName: ras} 

220 

221 def _createRotMatrix(self, boundingCircle): 

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

223 center of the circle bound. 

224 

225 Parameters 

226 ---------- 

227 boundingCircle : `lsst.sphgeom.BoundingCircle` 

228 Circle bound covering the tract. 

229 

230 Returns 

231 ------- 

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

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

234 the circle bound. 

235 

236 Notes 

237 ----- 

238 Rotation matrix follows 

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

240 """ 

241 # Get the center point of our tract 

242 center = boundingCircle.getCenter() 

243 

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

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

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

247 center.x(), 

248 0]) 

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

250 

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

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

253 # around the pole to the tract location. 

254 cosTheta = center.z() 

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

256 

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

258 # the cross product. 

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

260 [0, 1, 0], 

261 [0, 0, 1]]) 

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

263 [cross[2], 0, -cross[0]], 

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

265 rotMatrix += ( 

266 (1 - cosTheta) 

267 * np.array( 

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

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

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

271 ) 

272 return rotMatrix 

273 

274 def createVisitCoaddSubdivision(self, nFakes): 

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

276 the ``faction`` config value. 

277 

278 Parameters 

279 ---------- 

280 nFakes : `int` 

281 Number of fakes to create. 

282 

283 Returns 

284 ------- 

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

286 Dictionary of boolean arrays specifying which image to put a 

287 given fake into. 

288 """ 

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

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

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

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

293 if nBoth > 0: 

294 isVisitSource[:nBoth] = True 

295 isTemplateSource[:nBoth] = True 

296 if nOnly > 0: 

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

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

299 

300 return {self.config.visitSourceFlagCol: isVisitSource, 

301 self.config.templateSourceFlagCol: isTemplateSource} 

302 

303 def createRandomMagnitudes(self, nFakes, rng): 

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

305 

306 Parameters 

307 ---------- 

308 nFakes : `int` 

309 Number of fakes to create. 

310 rng : `numpy.random.Generator` 

311 Initialized random number generator. 

312 

313 Returns 

314 ------- 

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

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

317 config option. 

318 """ 

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

320 self.config.magMax, 

321 size=nFakes) 

322 randMags = {} 

323 for fil in self.config.filterSet: 

324 randMags[self.config.magVar % fil] = mags 

325 

326 return randMags