Coverage for python/lsst/ap/pipe/createApFakes.py: 35%
76 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 21:33 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 21:33 +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/>.
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 visitSourceFlagCol = pexConfig.Field(
101 doc="Name of the column flagging objects for insertion into the visit "
102 "image.",
103 dtype=str,
104 default="isVisitSource"
105 )
106 templateSourceFlagCol = pexConfig.Field(
107 doc="Name of the column flagging objects for insertion into the "
108 "template image.",
109 dtype=str,
110 default="isTemplateSource"
111 )
114class CreateRandomApFakesTask(PipelineTask):
115 """Create and store a set of spatially uniform star fakes over the sphere
116 for use in AP processing. Additionally assign random magnitudes to said
117 fakes and assign them to be inserted into either a visit exposure or
118 template exposure.
119 """
121 _DefaultName = "createApFakes"
122 ConfigClass = CreateRandomApFakesConfig
124 def runQuantum(self, butlerQC, inputRefs, outputRefs):
125 inputs = butlerQC.get(inputRefs)
126 inputs["tractId"] = butlerQC.quantum.dataId["tract"]
128 outputs = self.run(**inputs)
129 butlerQC.put(outputs, outputRefs)
131 def run(self, tractId, skyMap):
132 """Create a set of uniform random points that covers a tract.
134 Parameters
135 ----------
136 tractId : `int`
137 Tract id to produce randoms over.
138 skyMap : `lsst.skymap.SkyMap`
139 Skymap to produce randoms over.
141 Returns
142 -------
143 randoms : `pandas.DataFrame`
144 Catalog of random points covering the given tract. Follows the
145 columns and format expected in `lsst.pipe.tasks.InsertFakes`.
146 """
147 # Use the tractId as the ranomd seed.
148 rng = np.random.default_rng(tractId)
149 tractBoundingCircle = \
150 skyMap.generateTract(tractId).getInnerSkyPolygon().getBoundingCircle()
151 tractArea = tractBoundingCircle.getArea() * (180 / np.pi) ** 2
152 nFakes = int(self.config.fakeDensity * tractArea)
154 self.log.info(
155 f"Creating {nFakes} star fakes over tractId={tractId} with "
156 f"bounding circle area: {tractArea} deg^2")
158 # Concatenate the data and add dummy values for the unused variables.
159 # Set all data to PSF like objects.
160 randData = {
161 "fakeId": [uuid.uuid4().int & (1 << 64) - 1 for n in range(nFakes)],
162 **self.createRandomPositions(nFakes, tractBoundingCircle, rng),
163 **self.createVisitCoaddSubdivision(nFakes),
164 **self.createRandomMagnitudes(nFakes, rng),
165 self.config.diskHLR: np.ones(nFakes, dtype="float"),
166 self.config.bulgeHLR: np.ones(nFakes, dtype="float"),
167 self.config.nDisk: np.ones(nFakes, dtype="float"),
168 self.config.nBulge: np.ones(nFakes, dtype="float"),
169 self.config.aDisk: np.ones(nFakes, dtype="float"),
170 self.config.aBulge: np.ones(nFakes, dtype="float"),
171 self.config.bDisk: np.ones(nFakes, dtype="float"),
172 self.config.bBulge: np.ones(nFakes, dtype="float"),
173 self.config.paDisk: np.ones(nFakes, dtype="float"),
174 self.config.paBulge: np.ones(nFakes, dtype="float"),
175 self.config.sourceType: nFakes * ["star"]}
177 return Struct(fakeCat=pd.DataFrame(data=randData))
179 def createRandomPositions(self, nFakes, boundingCircle, rng):
180 """Create a set of spatially uniform randoms over the tract bounding
181 circle on the sphere.
183 Parameters
184 ----------
185 nFakes : `int`
186 Number of fakes to create.
187 boundingCicle : `lsst.sphgeom.BoundingCircle`
188 Circle bound covering the tract.
189 rng : `numpy.random.Generator`
190 Initialized random number generator.
192 Returns
193 -------
194 data : `dict`[`str`, `numpy.ndarray`]
195 Dictionary of RA and Dec locations over the tract.
196 """
197 # Create uniform random vectors on the sky around the north pole.
198 randVect = np.empty((nFakes, 3))
199 randVect[:, 2] = rng.uniform(
200 np.cos(boundingCircle.getOpeningAngle().asRadians()),
201 1,
202 nFakes)
203 sinRawTheta = np.sin(np.arccos(randVect[:, 2]))
204 rawPhi = rng.uniform(0, 2 * np.pi, nFakes)
205 randVect[:, 0] = sinRawTheta * np.cos(rawPhi)
206 randVect[:, 1] = sinRawTheta * np.sin(rawPhi)
208 # Compute the rotation matrix to move our random points to the
209 # correct location.
210 rotMatrix = self._createRotMatrix(boundingCircle)
211 randVect = np.dot(rotMatrix, randVect.transpose()).transpose()
212 decs = np.arcsin(randVect[:, 2])
213 ras = np.arctan2(randVect[:, 1], randVect[:, 0])
215 return {self.config.decColName: decs,
216 self.config.raColName: ras}
218 def _createRotMatrix(self, boundingCircle):
219 """Compute the 3d rotation matrix to rotate the dec=90 pole to the
220 center of the circle bound.
222 Parameters
223 ----------
224 boundingCircle : `lsst.sphgeom.BoundingCircle`
225 Circle bound covering the tract.
227 Returns
228 -------
229 rotMatrix : `numpy.ndarray`, (3, 3)
230 3x3 rotation matrix to rotate the dec=90 pole to the location of
231 the circle bound.
233 Notes
234 -----
235 Rotation matrix follows
236 https://mathworld.wolfram.com/RodriguesRotationFormula.html
237 """
238 # Get the center point of our tract
239 center = boundingCircle.getCenter()
241 # Compute the axis to rotate around. This is done by taking the cross
242 # product of dec=90 pole into the tract center.
243 cross = np.array([-center.y(),
244 center.x(),
245 0])
246 cross /= np.sqrt(cross[0] ** 2 + cross[1] ** 2 + cross[2] ** 2)
248 # Get the cosine and sine of the dec angle of the tract center. This
249 # is the amount of rotation needed to move the points we created from
250 # around the pole to the tract location.
251 cosTheta = center.z()
252 sinTheta = np.sin(np.arccos(center.z()))
254 # Compose the rotation matrix for rotation around the axis created from
255 # the cross product.
256 rotMatrix = cosTheta * np.array([[1, 0, 0],
257 [0, 1, 0],
258 [0, 0, 1]])
259 rotMatrix += sinTheta * np.array([[0, -cross[2], cross[1]],
260 [cross[2], 0, -cross[0]],
261 [-cross[1], cross[0], 0]])
262 rotMatrix += (
263 (1 - cosTheta)
264 * np.array(
265 [[cross[0] ** 2, cross[0] * cross[1], cross[0] * cross[2]],
266 [cross[0] * cross[1], cross[1] ** 2, cross[1] * cross[2]],
267 [cross[0] * cross[2], cross[1] * cross[2], cross[2] ** 2]])
268 )
269 return rotMatrix
271 def createVisitCoaddSubdivision(self, nFakes):
272 """Assign a given fake either a visit image or coadd or both based on
273 the ``faction`` config value.
275 Parameters
276 ----------
277 nFakes : `int`
278 Number of fakes to create.
280 Returns
281 -------
282 output : `dict`[`str`, `numpy.ndarray`]
283 Dictionary of boolean arrays specifying which image to put a
284 given fake into.
285 """
286 nBoth = int(self.config.fraction * nFakes)
287 nOnly = int((1 - self.config.fraction) / 2 * nFakes)
288 isVisitSource = np.zeros(nFakes, dtype=bool)
289 isTemplateSource = np.zeros(nFakes, dtype=bool)
290 if nBoth > 0:
291 isVisitSource[:nBoth] = True
292 isTemplateSource[:nBoth] = True
293 if nOnly > 0:
294 isVisitSource[nBoth:(nBoth + nOnly)] = True
295 isTemplateSource[(nBoth + nOnly):] = True
297 return {self.config.visitSourceFlagCol: isVisitSource,
298 self.config.templateSourceFlagCol: isTemplateSource}
300 def createRandomMagnitudes(self, nFakes, rng):
301 """Create a random distribution of magnitudes for out fakes.
303 Parameters
304 ----------
305 nFakes : `int`
306 Number of fakes to create.
307 rng : `numpy.random.Generator`
308 Initialized random number generator.
310 Returns
311 -------
312 randMags : `dict`[`str`, `numpy.ndarray`]
313 Dictionary of magnitudes in the bands set by the ``filterSet``
314 config option.
315 """
316 mags = rng.uniform(self.config.magMin,
317 self.config.magMax,
318 size=nFakes)
319 randMags = {}
320 for fil in self.config.filterSet:
321 randMags[self.config.mag_col % fil] = mags
323 return randMags