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

67 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-04 17:44 +0000

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 

32from lsst.source.injection import generate_injection_catalog 

33 

34from deprecated.sphinx import deprecated 

35 

36__all__ = ["CreateRandomApFakesTask", 

37 "CreateRandomApFakesConfig", 

38 "CreateRandomApFakesConnections"] 

39 

40 

41class CreateRandomApFakesConnections(PipelineTaskConnections, 

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

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

44 skyMap = connTypes.Input( 

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

46 "template exposures", 

47 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

48 dimensions=("skymap",), 

49 storageClass="SkyMap", 

50 ) 

51 fakeCat = connTypes.Output( 

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

53 name="{fakesType}fakeSourceCat", 

54 storageClass="DataFrame", 

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

56 ) 

57 

58 

59@deprecated( 

60 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.", 

61 version="v28.0", 

62 category=FutureWarning, 

63) 

64class CreateRandomApFakesConfig( 

65 InsertFakesConfig, 

66 pipelineConnections=CreateRandomApFakesConnections): 

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

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

69 the InsertFakes and related tasks. 

70 """ 

71 fakeDensity = pexConfig.RangeField( 

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

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

74 "visit.", 

75 dtype=float, 

76 default=1000, 

77 min=0, 

78 ) 

79 filterSet = pexConfig.ListField( 

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

81 dtype=str, 

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

83 ) 

84 fraction = pexConfig.RangeField( 

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

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

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

88 dtype=float, 

89 default=1/3, 

90 min=0, 

91 max=1, 

92 ) 

93 magMin = pexConfig.RangeField( 

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

95 "are set to the same value.", 

96 dtype=float, 

97 default=20, 

98 min=1, 

99 max=40, 

100 ) 

101 magMax = pexConfig.RangeField( 

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

103 "are set to the same value.", 

104 dtype=float, 

105 default=30, 

106 min=1, 

107 max=40, 

108 ) 

109 visitSourceFlagCol = pexConfig.Field( 

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

111 "image.", 

112 dtype=str, 

113 default="isVisitSource" 

114 ) 

115 templateSourceFlagCol = pexConfig.Field( 

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

117 "template image.", 

118 dtype=str, 

119 default="isTemplateSource" 

120 ) 

121 

122 

123@deprecated( 

124 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.", 

125 version="v28.0", 

126 category=FutureWarning, 

127) 

128class CreateRandomApFakesTask(PipelineTask): 

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

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

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

132 template exposure. 

133 """ 

134 

135 _DefaultName = "createApFakes" 

136 ConfigClass = CreateRandomApFakesConfig 

137 

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

139 inputs = butlerQC.get(inputRefs) 

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

141 

142 outputs = self.run(**inputs) 

143 butlerQC.put(outputs, outputRefs) 

144 

145 def run(self, tractId, skyMap): 

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

147 

148 Parameters 

149 ---------- 

150 tractId : `int` 

151 Tract id to produce randoms over. 

152 skyMap : `lsst.skymap.SkyMap` 

153 Skymap to produce randoms over. 

154 

155 Returns 

156 ------- 

157 randoms : `pandas.DataFrame` 

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

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

160 """ 

161 # Use the tractId as the random seed. 

162 rng = np.random.default_rng(tractId) 

163 

164 tract = skyMap.generateTract(tractId) 

165 tractArea = tract.getOuterSkyPolygon().getBoundingBox().getArea() 

166 tractArea *= (180 / np.pi) ** 2 

167 tractWcs = tract.getWcs() 

168 vertexList = tract.getVertexList() 

169 vertexRas = [vertex.getRa().asDegrees() for vertex in vertexList] 

170 vertexDecs = [vertex.getDec().asDegrees() for vertex in vertexList] 

171 

172 catalog = generate_injection_catalog( 

173 ra_lim=sorted([np.min(vertexRas), np.max(vertexRas)]), 

174 dec_lim=sorted([np.min(vertexDecs), np.max(vertexDecs)]), 

175 density=self.config.fakeDensity, 

176 source_type="Star", 

177 seed=str(tractId), 

178 wcs=tractWcs 

179 ) 

180 

181 nFakes = len(catalog) 

182 

183 self.log.info( 

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

185 f" RA in ({sorted([np.min(vertexRas), np.max(vertexRas)])} " 

186 f" Dec in ({sorted([np.min(vertexDecs), np.max(vertexDecs)])}), " 

187 f"area={tractArea:.4f} deg^2 and " 

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

189 

190 onesColumn = np.ones(nFakes, dtype="float") 

191 zerosColumn = np.zeros(nFakes, dtype="float") 

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

193 # Set all data to PSF like objects. 

194 randData = { 

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

196 self.config.ra_col: catalog['ra'].value, 

197 self.config.dec_col: catalog['dec'].value, 

198 **self.createVisitCoaddSubdivision(nFakes), 

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

200 self.config.disk_semimajor_col: onesColumn, 

201 self.config.bulge_semimajor_col: onesColumn, 

202 self.config.disk_n_col: onesColumn, 

203 self.config.bulge_n_col: onesColumn, 

204 self.config.disk_axis_ratio_col: onesColumn, 

205 self.config.bulge_axis_ratio_col: onesColumn, 

206 self.config.disk_pa_col: zerosColumn, 

207 self.config.bulge_pa_col: onesColumn, 

208 self.config.sourceType: catalog['source_type'].value, 

209 "source_type": catalog['source_type'].value, 

210 "injection_id": catalog['injection_id'].value 

211 } 

212 

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

214 

215 def createVisitCoaddSubdivision(self, nFakes): 

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

217 the ``faction`` config value. 

218 

219 Parameters 

220 ---------- 

221 nFakes : `int` 

222 Number of fakes to create. 

223 

224 Returns 

225 ------- 

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

227 Dictionary of boolean arrays specifying which image to put a 

228 given fake into. 

229 """ 

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

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

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

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

234 if nBoth > 0: 

235 isVisitSource[:nBoth] = True 

236 isTemplateSource[:nBoth] = True 

237 if nOnly > 0: 

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

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

240 

241 return {self.config.visitSourceFlagCol: isVisitSource, 

242 self.config.templateSourceFlagCol: isTemplateSource} 

243 

244 def createRandomMagnitudes(self, nFakes, rng): 

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

246 

247 Parameters 

248 ---------- 

249 nFakes : `int` 

250 Number of fakes to create. 

251 rng : `numpy.random.Generator` 

252 Initialized random number generator. 

253 

254 Returns 

255 ------- 

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

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

258 config option. 

259 """ 

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

261 self.config.magMax, 

262 size=nFakes) 

263 randMags = {} 

264 for fil in self.config.filterSet: 

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

266 # adding a non-filter column for magnitudes 

267 randMags["mag"] = mags 

268 return randMags