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 

24 

25import lsst.pex.config as pexConfig 

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

27import lsst.pipe.base.connectionTypes as connTypes 

28from lsst.pipe.tasks.insertFakes import InsertFakesConfig 

29 

30__all__ = ["CreateRandomApFakesTask", 

31 "CreateRandomApFakesConfig", 

32 "CreateRandomApFakesConnections"] 

33 

34 

35class CreateRandomApFakesConnections(PipelineTaskConnections, 

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

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

38 skyMap = connTypes.Input( 

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

40 "template exposures", 

41 name="{CoaddName}Coadd_skyMap", 

42 dimensions=("skymap",), 

43 storageClass="SkyMap", 

44 ) 

45 fakeCat = connTypes.Output( 

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

47 name="{CoaddName}Coadd_fakeSourceCat", 

48 storageClass="DataFrame", 

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

50 ) 

51 

52 

53class CreateRandomApFakesConfig( 

54 InsertFakesConfig, 

55 pipelineConnections=CreateRandomApFakesConnections): 

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

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

58 the InsertFakes and related tasks. 

59 """ 

60 fakeDensity = pexConfig.RangeField( 

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

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

63 "visit.", 

64 dtype=float, 

65 default=1000, 

66 min=0, 

67 ) 

68 filterSet = pexConfig.ListField( 

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

70 dtype=str, 

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

72 ) 

73 fraction = pexConfig.RangeField( 

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

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

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

77 dtype=float, 

78 default=1/3, 

79 min=0, 

80 max=1, 

81 ) 

82 magMin = pexConfig.RangeField( 

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

84 "are set to the same value.", 

85 dtype=float, 

86 default=20, 

87 min=1, 

88 max=40, 

89 ) 

90 magMax = pexConfig.RangeField( 

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

92 "are set to the same value.", 

93 dtype=float, 

94 default=30, 

95 min=1, 

96 max=40, 

97 ) 

98 randomSeed = pexConfig.Field( 

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

100 dtype=int, 

101 default=1234, 

102 ) 

103 visitSourceFlagCol = pexConfig.Field( 

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

105 "image.", 

106 dtype=str, 

107 default="isVisitSource" 

108 ) 

109 templateSourceFlagCol = pexConfig.Field( 

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

111 "template image.", 

112 dtype=str, 

113 default="isTemplateSource" 

114 ) 

115 

116 

117class CreateRandomApFakesTask(PipelineTask): 

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

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

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

121 template exposure. 

122 """ 

123 

124 _DefaultName = "createApFakes" 

125 ConfigClass = CreateRandomApFakesConfig 

126 

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

128 inputs = butlerQC.get(inputRefs) 

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

130 

131 outputs = self.run(**inputs) 

132 butlerQC.put(outputs, outputRefs) 

133 

134 def run(self, tractId, skyMap): 

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

136 

137 Parameters 

138 ---------- 

139 tractId : `int` 

140 Tract id to produce randoms over. 

141 skyMap : `lsst.skymap.SkyMap` 

142 Skymap to produce randoms over. 

143 

144 Returns 

145 ------- 

146 randoms : `pandas.DataFrame` 

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

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

149 """ 

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

151 tractBoundingCircle = \ 

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

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

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

155 

156 self.log.info( 

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

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

159 

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

161 # Set all data to PSF like objects. 

162 randData = { 

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

164 **self.createVisitCoaddSubdivision(nFakes), 

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

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

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

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

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

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

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

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

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

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

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

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

177 

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

179 

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

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

182 circle on the sphere. 

183 

184 Parameters 

185 ---------- 

186 nFakes : `int` 

187 Number of fakes to create. 

188 boundingCicle : `lsst.sphgeom.BoundingCircle` 

189 Circle bound covering the tract. 

190 rng : `numpy.random.Generator` 

191 Initialized random number generator. 

192 

193 Returns 

194 ------- 

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

196 Dictionary of RA and Dec locations over the tract. 

197 """ 

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

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

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

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

202 1, 

203 nFakes) 

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

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

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

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

208 

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

210 # correct location. 

211 rotMatrix = self._createRotMatrix(boundingCircle) 

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

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

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

215 

216 return {self.config.decColName: decs, 

217 self.config.raColName: ras} 

218 

219 def _createRotMatrix(self, boundingCircle): 

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

221 center of the circle bound. 

222 

223 Parameters 

224 ---------- 

225 boundingCircle : `lsst.sphgeom.BoundingCircle` 

226 Circle bound covering the tract. 

227 

228 Returns 

229 ------- 

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

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

232 the circle bound. 

233 

234 Notes 

235 ----- 

236 Rotation matrix follows 

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

238 """ 

239 # Get the center point of our tract 

240 center = boundingCircle.getCenter() 

241 

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

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

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

245 center.x(), 

246 0]) 

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

248 

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

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

251 # around the pole to the tract location. 

252 cosTheta = center.z() 

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

254 

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

256 # the cross product. 

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

258 [0, 1, 0], 

259 [0, 0, 1]]) 

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

261 [cross[2], 0, -cross[0]], 

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

263 rotMatrix += ( 

264 (1 - cosTheta) 

265 * np.array( 

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

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

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

269 ) 

270 return rotMatrix 

271 

272 def createVisitCoaddSubdivision(self, nFakes): 

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

274 the ``faction`` config value. 

275 

276 Parameters 

277 ---------- 

278 nFakes : `int` 

279 Number of fakes to create. 

280 

281 Returns 

282 ------- 

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

284 Dictionary of boolean arrays specifying which image to put a 

285 given fake into. 

286 """ 

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

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

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

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

291 if nBoth > 0: 

292 isVisitSource[:nBoth] = True 

293 isTemplateSource[:nBoth] = True 

294 if nOnly > 0: 

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

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

297 

298 return {self.config.visitSourceFlagCol: isVisitSource, 

299 self.config.templateSourceFlagCol: isTemplateSource} 

300 

301 def createRandomMagnitudes(self, nFakes, rng): 

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

303 

304 Parameters 

305 ---------- 

306 nFakes : `int` 

307 Number of fakes to create. 

308 rng : `numpy.random.Generator` 

309 Initialized random number generator. 

310 

311 Returns 

312 ------- 

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

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

315 config option. 

316 """ 

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

318 self.config.magMax, 

319 size=nFakes) 

320 randMags = {} 

321 for fil in self.config.filterSet: 

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

323 

324 return randMags