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
24import uuid
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
32__all__ = ["CreateRandomApFakesTask",
33 "CreateRandomApFakesConfig",
34 "CreateRandomApFakesConnections"]
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 )
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 )
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 """
126 _DefaultName = "createApFakes"
127 ConfigClass = CreateRandomApFakesConfig
129 def runQuantum(self, butlerQC, inputRefs, outputRefs):
130 inputs = butlerQC.get(inputRefs)
131 inputs["tractId"] = butlerQC.quantum.dataId["tract"]
133 outputs = self.run(**inputs)
134 butlerQC.put(outputs, outputRefs)
136 def run(self, tractId, skyMap):
137 """Create a set of uniform random points that covers a tract.
139 Parameters
140 ----------
141 tractId : `int`
142 Tract id to produce randoms over.
143 skyMap : `lsst.skymap.SkyMap`
144 Skymap to produce randoms over.
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)
158 self.log.info(
159 f"Creating {nFakes} star fakes over tractId={tractId} with "
160 f"bounding circle area: {tractArea} deg^2")
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"]}
181 return Struct(fakeCat=pd.DataFrame(data=randData))
183 def createRandomPositions(self, nFakes, boundingCircle, rng):
184 """Create a set of spatially uniform randoms over the tract bounding
185 circle on the sphere.
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.
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)
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])
219 return {self.config.decColName: decs,
220 self.config.raColName: ras}
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.
226 Parameters
227 ----------
228 boundingCircle : `lsst.sphgeom.BoundingCircle`
229 Circle bound covering the tract.
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.
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()
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)
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()))
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
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.
279 Parameters
280 ----------
281 nFakes : `int`
282 Number of fakes to create.
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
301 return {self.config.visitSourceFlagCol: isVisitSource,
302 self.config.templateSourceFlagCol: isTemplateSource}
304 def createRandomMagnitudes(self, nFakes, rng):
305 """Create a random distribution of magnitudes for out fakes.
307 Parameters
308 ----------
309 nFakes : `int`
310 Number of fakes to create.
311 rng : `numpy.random.Generator`
312 Initialized random number generator.
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
327 return randMags