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

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/>.
22import numpy as np
23import pandas as pd
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
30__all__ = ["CreateRandomApFakesTask",
31 "CreateRandomApFakesConfig",
32 "CreateRandomApFakesConnections"]
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 )
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 )
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 """
124 _DefaultName = "createApFakes"
125 ConfigClass = CreateRandomApFakesConfig
127 def runQuantum(self, butlerQC, inputRefs, outputRefs):
128 inputs = butlerQC.get(inputRefs)
129 inputs["tractId"] = butlerQC.quantum.dataId["tract"]
131 outputs = self.run(**inputs)
132 butlerQC.put(outputs, outputRefs)
134 def run(self, tractId, skyMap):
135 """Create a set of uniform random points that covers a tract.
137 Parameters
138 ----------
139 tractId : `int`
140 Tract id to produce randoms over.
141 skyMap : `lsst.skymap.SkyMap`
142 Skymap to produce randoms over.
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)
156 self.log.info(
157 f"Creating {nFakes} star fakes over tractId={tractId} with "
158 f"bounding circle area: {tractArea} deg^2")
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"]}
178 return Struct(fakeCat=pd.DataFrame(data=randData))
180 def createRandomPositions(self, nFakes, boundingCircle, rng):
181 """Create a set of spatially uniform randoms over the tract bounding
182 circle on the sphere.
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.
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)
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])
216 return {self.config.decColName: decs,
217 self.config.raColName: ras}
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.
223 Parameters
224 ----------
225 boundingCircle : `lsst.sphgeom.BoundingCircle`
226 Circle bound covering the tract.
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.
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()
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)
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()))
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
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.
276 Parameters
277 ----------
278 nFakes : `int`
279 Number of fakes to create.
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
298 return {self.config.visitSourceFlagCol: isVisitSource,
299 self.config.templateSourceFlagCol: isTemplateSource}
301 def createRandomMagnitudes(self, nFakes, rng):
302 """Create a random distribution of magnitudes for out fakes.
304 Parameters
305 ----------
306 nFakes : `int`
307 Number of fakes to create.
308 rng : `numpy.random.Generator`
309 Initialized random number generator.
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
324 return randMags