Coverage for python/lsst/meas/algorithms/testUtils.py: 23%
80 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 03:32 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 03:32 -0700
1#
2# LSST Data Management System
3#
4# Copyright 2008-2017 AURA/LSST.
5#
6# This product includes software developed by the
7# LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20# the GNU General Public License along with this program. If not,
21# see <https://www.lsstcorp.org/LegalNotices/>.
22#
24__all__ = ["plantSources", "makeRandomTransmissionCurve", "makeDefectList",
25 "MockReferenceObjectLoaderFromFiles", "MockRefcatDataId"]
27import numpy as np
28import esutil
30import lsst.geom
31import lsst.afw.image as afwImage
32from lsst.pipe.base import InMemoryDatasetHandle
33from lsst import sphgeom
34from . import SingleGaussianPsf
35from . import Defect
37from . import ReferenceObjectLoader
38import lsst.afw.table as afwTable
41def plantSources(bbox, kwid, sky, coordList, addPoissonNoise=True):
42 """Make an exposure with stars (modelled as Gaussians)
44 Parameters
45 ----------
46 bbox : `lsst.geom.Box2I`
47 Parent bbox of exposure
48 kwid : `int`
49 Kernal width (and height; kernal is square)
50 sky : `float`
51 Amount of sky background (counts)
52 coordList : `list [tuple]`
53 A list of [x, y, counts, sigma] where:
54 * x,y are relative to exposure origin
55 * counts is the integrated counts for the star
56 * sigma is the Gaussian sigma in pixels
57 addPoissonNoise : `bool`
58 If True: add Poisson noise to the exposure
59 """
60 # make an image with sources
61 img = afwImage.ImageD(bbox)
62 meanSigma = 0.0
63 for coord in coordList:
64 x, y, counts, sigma = coord
65 meanSigma += sigma
67 # make a single gaussian psf
68 psf = SingleGaussianPsf(kwid, kwid, sigma)
70 # make an image of it and scale to the desired number of counts
71 thisPsfImg = psf.computeImage(lsst.geom.PointD(x, y))
72 thisPsfImg *= counts
74 # bbox a window in our image and add the fake star image
75 psfBox = thisPsfImg.getBBox()
76 psfBox.clip(bbox)
77 if psfBox != thisPsfImg.getBBox():
78 thisPsfImg = thisPsfImg[psfBox, afwImage.PARENT]
79 imgSeg = img[psfBox, afwImage.PARENT]
80 imgSeg += thisPsfImg
81 meanSigma /= len(coordList)
83 img += sky
85 # add Poisson noise
86 if (addPoissonNoise):
87 np.random.seed(seed=1) # make results reproducible
88 imgArr = img.getArray()
89 imgArr[:] = np.random.poisson(imgArr)
91 # bundle into a maskedimage and an exposure
92 mask = afwImage.Mask(bbox)
93 var = img.convertFloat()
94 img -= sky
95 mimg = afwImage.MaskedImageF(img.convertFloat(), mask, var)
96 exposure = afwImage.makeExposure(mimg)
98 # insert an approximate psf
99 psf = SingleGaussianPsf(kwid, kwid, meanSigma)
100 exposure.setPsf(psf)
102 return exposure
105def makeRandomTransmissionCurve(rng, minWavelength=4000.0, maxWavelength=7000.0, nWavelengths=200,
106 maxRadius=80.0, nRadii=30, perturb=0.05):
107 """Create a random TransmissionCurve with nontrivial spatial and
108 wavelength variation.
110 Parameters
111 ----------
112 rng : numpy.random.RandomState
113 Random number generator.
114 minWavelength : float
115 Average minimum wavelength for generated TransmissionCurves (will be
116 randomly perturbed).
117 maxWavelength : float
118 Average maximum wavelength for generated TransmissionCurves (will be
119 randomly perturbed).
120 nWavelengths : int
121 Number of samples in the wavelength dimension.
122 maxRadius : float
123 Average maximum radius for spatial variation (will be perturbed).
124 nRadii : int
125 Number of samples in the radial dimension.
126 perturb: float
127 Fraction by which wavelength and radius bounds should be randomly
128 perturbed.
129 """
130 dWavelength = maxWavelength - minWavelength
132 def perturbed(x, s=perturb*dWavelength):
133 return x + 2.0*s*(rng.rand() - 0.5)
135 wavelengths = np.linspace(perturbed(minWavelength), perturbed(maxWavelength), nWavelengths)
136 radii = np.linspace(0.0, perturbed(maxRadius, perturb*maxRadius), nRadii)
137 throughput = np.zeros(wavelengths.shape + radii.shape, dtype=float)
138 # throughput will be a rectangle in wavelength, shifting to higher wavelengths and shrinking
139 # in height with radius, going to zero at all bounds.
140 peak0 = perturbed(0.9, 0.05)
141 start0 = perturbed(minWavelength + 0.25*dWavelength)
142 stop0 = perturbed(minWavelength + 0.75*dWavelength)
143 for i, r in enumerate(radii):
144 mask = np.logical_and(wavelengths >= start0 + r, wavelengths <= stop0 + r)
145 throughput[mask, i] = peak0*(1.0 - r/1000.0)
146 return afwImage.TransmissionCurve.makeRadial(throughput, wavelengths, radii)
149def makeDefectList():
150 """Create a list of defects that can be used for testing.
152 Returns
153 -------
154 defectList = `list` [`lsst.meas.algorithms.Defect`]
155 The list of defects.
156 """
157 defectList = [Defect(lsst.geom.Box2I(lsst.geom.Point2I(962, 0),
158 lsst.geom.Extent2I(2, 4611))),
159 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1316, 0),
160 lsst.geom.Extent2I(2, 4611))),
161 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1576, 0),
162 lsst.geom.Extent2I(4, 4611))),
163 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1626, 0),
164 lsst.geom.Extent2I(2, 4611))),
165 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1994, 252),
166 lsst.geom.Extent2I(2, 4359))),
167 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1426, 702),
168 lsst.geom.Extent2I(2, 3909))),
169 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1526, 1140),
170 lsst.geom.Extent2I(2, 3471))),
171 Defect(lsst.geom.Box2I(lsst.geom.Point2I(856, 2300),
172 lsst.geom.Extent2I(2, 2311))),
173 Defect(lsst.geom.Box2I(lsst.geom.Point2I(858, 2328),
174 lsst.geom.Extent2I(2, 65))),
175 Defect(lsst.geom.Box2I(lsst.geom.Point2I(859, 2328),
176 lsst.geom.Extent2I(1, 56))),
177 Defect(lsst.geom.Box2I(lsst.geom.Point2I(844, 2796),
178 lsst.geom.Extent2I(4, 1814))),
179 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1366, 2804),
180 lsst.geom.Extent2I(2, 1806))),
181 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1766, 3844),
182 lsst.geom.Extent2I(2, 766))),
183 Defect(lsst.geom.Box2I(lsst.geom.Point2I(1872, 4228),
184 lsst.geom.Extent2I(2, 382))),
185 ]
187 return defectList
190class MockRefcatDataId:
191 """Mock reference catalog dataId.
193 The reference catalog dataId is only used to retrieve a region property.
195 Parameters
196 ----------
197 region : `lsst.sphgeom.Region`
198 The region associated with this mock dataId.
199 """
200 def __init__(self, region):
201 self._region = region
203 @property
204 def region(self):
205 return self._region
208class MockReferenceObjectLoaderFromFiles(ReferenceObjectLoader):
209 """A simple mock of ReferenceObjectLoader.
211 This mock ReferenceObjectLoader uses a set of files on disk to create
212 mock dataIds and data reference handles that can be accessed
213 without a butler. The files must be afw catalog files in the reference
214 catalog format, sharded with HTM pixelization.
216 Parameters
217 ----------
218 filenames : `list` [`str`]
219 Names of files to use.
220 config : `lsst.meas.astrom.LoadReferenceObjectsConfig`, optional
221 Configuration object if necessary to override defaults.
222 htmLevel : `int`, optional
223 HTM level to use for the loader.
224 """
225 def __init__(self, filenames, name='cal_ref_cat', config=None, htmLevel=4):
226 dataIds, refCats = self._createDataIdsAndRefcats(filenames, htmLevel, name)
228 super().__init__(dataIds, refCats, name=name, config=config)
230 def _createDataIdsAndRefcats(self, filenames, htmLevel, name):
231 """Create mock dataIds and refcat handles.
233 Parameters
234 ----------
235 filenames : `list` [`str`]
236 Names of files to use.
237 htmLevel : `int`
238 HTM level to use for the loader.
239 name : `str`
240 Name of reference catalog (for logging).
242 Returns
243 -------
244 dataIds : `list` [`MockRefcatDataId`]
245 List of mock dataIds.
246 refCats : `list` [`lsst.pipe.base.InMemoryDatasetHandle`]
247 List of mock deferred dataset handles.
249 Raises
250 ------
251 RuntimeError if any file contains sources that cover more than one HTM
252 pixel at level ``htmLevel``.
253 """
254 pixelization = sphgeom.HtmPixelization(htmLevel)
255 htm = esutil.htm.HTM(htmLevel)
257 dataIds = []
258 refCats = []
260 for filename in filenames:
261 cat = afwTable.BaseCatalog.readFits(filename)
263 ids = htm.lookup_id(np.rad2deg(cat['coord_ra']), np.rad2deg(cat['coord_dec']))
265 if len(np.unique(ids)) != 1:
266 raise RuntimeError(f"File {filename} contains more than one pixel at level {htmLevel}")
268 dataIds.append(MockRefcatDataId(pixelization.pixel(ids[0])))
269 refCats.append(InMemoryDatasetHandle(cat, name=name))
271 return dataIds, refCats