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 

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 randomSeed = pexConfig.Field( 

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

102 dtype=int, 

103 default=1234, 

104 ) 

105 visitSourceFlagCol = pexConfig.Field( 

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

107 "image.", 

108 dtype=str, 

109 default="isVisitSource" 

110 ) 

111 templateSourceFlagCol = pexConfig.Field( 

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

113 "template image.", 

114 dtype=str, 

115 default="isTemplateSource" 

116 ) 

117 

118 

119class CreateRandomApFakesTask(PipelineTask): 

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

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

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

123 template exposure. 

124 """ 

125 

126 _DefaultName = "createApFakes" 

127 ConfigClass = CreateRandomApFakesConfig 

128 

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

130 inputs = butlerQC.get(inputRefs) 

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

132 

133 outputs = self.run(**inputs) 

134 butlerQC.put(outputs, outputRefs) 

135 

136 def run(self, tractId, skyMap): 

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

138 

139 Parameters 

140 ---------- 

141 tractId : `int` 

142 Tract id to produce randoms over. 

143 skyMap : `lsst.skymap.SkyMap` 

144 Skymap to produce randoms over. 

145 

146 Returns 

147 ------- 

148 randoms : `pandas.DataFrame` 

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

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

151 """ 

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

153 tractBoundingCircle = \ 

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

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

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

157 

158 self.log.info( 

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

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

161 

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

163 # Set all data to PSF like objects. 

164 randData = { 

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

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

167 **self.createVisitCoaddSubdivision(nFakes), 

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

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

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

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

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

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

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

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

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

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

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

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

180 

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

182 

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

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

185 circle on the sphere. 

186 

187 Parameters 

188 ---------- 

189 nFakes : `int` 

190 Number of fakes to create. 

191 boundingCicle : `lsst.sphgeom.BoundingCircle` 

192 Circle bound covering the tract. 

193 rng : `numpy.random.Generator` 

194 Initialized random number generator. 

195 

196 Returns 

197 ------- 

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

199 Dictionary of RA and Dec locations over the tract. 

200 """ 

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

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

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

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

205 1, 

206 nFakes) 

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

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

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

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

211 

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

213 # correct location. 

214 rotMatrix = self._createRotMatrix(boundingCircle) 

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

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

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

218 

219 return {self.config.decColName: decs, 

220 self.config.raColName: ras} 

221 

222 def _createRotMatrix(self, boundingCircle): 

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

224 center of the circle bound. 

225 

226 Parameters 

227 ---------- 

228 boundingCircle : `lsst.sphgeom.BoundingCircle` 

229 Circle bound covering the tract. 

230 

231 Returns 

232 ------- 

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

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

235 the circle bound. 

236 

237 Notes 

238 ----- 

239 Rotation matrix follows 

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

241 """ 

242 # Get the center point of our tract 

243 center = boundingCircle.getCenter() 

244 

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

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

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

248 center.x(), 

249 0]) 

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

251 

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

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

254 # around the pole to the tract location. 

255 cosTheta = center.z() 

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

257 

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

259 # the cross product. 

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

261 [0, 1, 0], 

262 [0, 0, 1]]) 

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

264 [cross[2], 0, -cross[0]], 

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

266 rotMatrix += ( 

267 (1 - cosTheta) 

268 * np.array( 

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

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

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

272 ) 

273 return rotMatrix 

274 

275 def createVisitCoaddSubdivision(self, nFakes): 

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

277 the ``faction`` config value. 

278 

279 Parameters 

280 ---------- 

281 nFakes : `int` 

282 Number of fakes to create. 

283 

284 Returns 

285 ------- 

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

287 Dictionary of boolean arrays specifying which image to put a 

288 given fake into. 

289 """ 

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

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

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

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

294 if nBoth > 0: 

295 isVisitSource[:nBoth] = True 

296 isTemplateSource[:nBoth] = True 

297 if nOnly > 0: 

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

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

300 

301 return {self.config.visitSourceFlagCol: isVisitSource, 

302 self.config.templateSourceFlagCol: isTemplateSource} 

303 

304 def createRandomMagnitudes(self, nFakes, rng): 

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

306 

307 Parameters 

308 ---------- 

309 nFakes : `int` 

310 Number of fakes to create. 

311 rng : `numpy.random.Generator` 

312 Initialized random number generator. 

313 

314 Returns 

315 ------- 

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

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

318 config option. 

319 """ 

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

321 self.config.magMax, 

322 size=nFakes) 

323 randMags = {} 

324 for fil in self.config.filterSet: 

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

326 

327 return randMags