Coverage for python/lsst/summit/utils/astrometry/utils.py: 12%
138 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-10 05:48 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-10 05:48 -0700
1# This file is part of summit_utils.
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 astropy.units as u
23import numpy as np
24from astropy.coordinates import AltAz, EarthLocation, SkyCoord
25from astropy.time import Time
27from lsst.afw.geom import SkyWcs
28from lsst.daf.base import PropertySet
29from lsst.pipe.tasks.characterizeImage import CharacterizeImageConfig, CharacterizeImageTask
31__all__ = [
32 "claverHeaderToWcs",
33 "getAverageRaFromHeader",
34 "getAverageDecFromHeader",
35 "getAverageAzFromHeader",
36 "getAverageElFromHeader",
37 "genericCameraHeaderToWcs",
38 "getIcrsAtZenith",
39 "headerToWcs",
40 "runCharactierizeImage",
41 "filterSourceCatOnBrightest",
42]
45def claverHeaderToWcs(exp, nominalRa=None, nominalDec=None):
46 """Given an exposure taken by Chuck Claver at his house, construct a wcs
47 with the ra/dec set to zenith unless a better guess is supplied.
49 Automatically sets the platescale depending on the lens.
51 Parameters
52 ----------
53 exp : `lsst.afw.image.Exposure`
54 The exposure to construct the wcs for.
55 nominalRa : `float`, optional
56 The guess for the ra.
57 nominalDec : `float`, optional
58 The guess for the Dec.
60 Returns
61 -------
62 wcs : `lsst.afw.geom.SkyWcs`
63 The constructed wcs.
64 """
65 header = exp.getMetadata().toDict()
67 # set the plate scale depending on the lens and put into CD matrix
68 # plate scale info from:
69 # https://confluence.lsstcorp.org/pages/viewpage.action?pageId=191987725
70 lens = header["INSTLEN"]
71 if "135mm" in lens:
72 arcSecPerPix = 8.64
73 elif "375mm" in lens:
74 arcSecPerPix = 3.11
75 elif "750mm" in lens:
76 arcSecPerPix = 1.56
77 else:
78 raise ValueError(f"Unrecognised lens: {lens}")
80 header["CD1_1"] = arcSecPerPix / 3600
81 header["CD1_2"] = 0
82 header["CD2_1"] = 0
83 header["CD2_2"] = arcSecPerPix / 3600
85 # calculate the ra/dec at zenith and assume Chuck pointed it vertically
86 icrs = getIcrsAtZenith(
87 float(header["OBSLON"]), float(header["OBSLAT"]), float(header["OBSHGT"]), header["UTC"]
88 )
89 header["CRVAL1"] = nominalRa if nominalRa else icrs.ra.degree
90 header["CRVAL2"] = nominalDec if nominalDec else icrs.dec.degree
92 # just use the nomimal chip centre, not that it matters
93 # given radec = zenith
94 width, height = exp.image.array.shape
95 header["CRPIX1"] = width / 2
96 header["CRPIX2"] = height / 2
98 header["CTYPE1"] = "RA---TAN-SIP"
99 header["CTYPE2"] = "DEC--TAN-SIP"
101 wcsPropSet = PropertySet.from_mapping(header)
102 wcs = SkyWcs(wcsPropSet)
103 return wcs
106# don't be tempted to get cute and try to combine these 4 functions. It would
107# be easy to do but it's not unlikley they will diverge in the future.
108def getAverageRaFromHeader(header):
109 raStart = header.get("RASTART")
110 raEnd = header.get("RAEND")
111 if not raStart or not raEnd:
112 raise RuntimeError(f"Failed to get RA from header due to missing RASTART/END {raStart} {raEnd}")
113 raStart = float(raStart)
114 raEnd = float(raEnd)
115 return (raStart + raEnd) / 2
118def getAverageDecFromHeader(header):
119 decStart = header.get("DECSTART")
120 decEnd = header.get("DECEND")
121 if not decStart or not decEnd:
122 raise RuntimeError(f"Failed to get DEC from header due to missing DECSTART/END {decStart} {decEnd}")
123 decStart = float(decStart)
124 decEnd = float(decEnd)
125 return (decStart + decEnd) / 2
128def getAverageAzFromHeader(header):
129 azStart = header.get("AZSTART")
130 azEnd = header.get("AZEND")
131 if not azStart or not azEnd:
132 raise RuntimeError(f"Failed to get az from header due to missing AZSTART/END {azStart} {azEnd}")
133 azStart = float(azStart)
134 azEnd = float(azEnd)
135 return (azStart + azEnd) / 2
138def getAverageElFromHeader(header):
139 elStart = header.get("ELSTART")
140 elEnd = header.get("ELEND")
141 if not elStart or not elEnd:
142 raise RuntimeError(f"Failed to get el from header due to missing ELSTART/END {elStart} {elEnd}")
143 elStart = float(elStart)
144 elEnd = float(elEnd)
145 return (elStart + elEnd) / 2
148def patchHeader(header):
149 """This is a TEMPORARY function to patch some info into the headers."""
150 if header.get("CAMCODE") == "GC102": # regular aka narrow camera
151 # the narrow camera currently is wrong about its place scale by of ~2.2
152 header["SECPIX"] = "1.44"
153 # update the boresight locations until this goes into the header
154 # service
155 header["CRPIX1"] = 1898.10
156 header["CRPIX2"] = 998.47
157 if header.get("CAMCODE") == "GC101": # wide camera
158 # update the boresight locations until this goes into the header
159 # service
160 header["CRPIX1"] = 1560.85
161 header["CRPIX2"] = 1257.15
162 if header.get("CAMCODE") == "GC103": # fast camera
163 # use the fast camera chip centre until we know better
164 header["SECPIX"] = "0.6213" # measured from a fit
165 header["CRPIX1"] = 329.5
166 header["CRPIX2"] = 246.5
167 return header
170def genericCameraHeaderToWcs(exp):
171 header = exp.getMetadata().toDict()
172 header = patchHeader(header)
174 header["CTYPE1"] = "RA---TAN-SIP"
175 header["CTYPE2"] = "DEC--TAN-SIP"
177 header["CRVAL1"] = getAverageRaFromHeader(header)
178 header["CRVAL2"] = getAverageDecFromHeader(header)
180 plateScale = header.get("SECPIX")
181 if not plateScale:
182 raise RuntimeError("Failed to find platescale in header")
183 plateScale = float(plateScale)
185 header["CD1_1"] = plateScale / 3600
186 header["CD1_2"] = 0
187 header["CD2_1"] = 0
188 header["CD2_2"] = plateScale / 3600
190 wcsPropSet = PropertySet.from_mapping(header)
191 wcs = SkyWcs(wcsPropSet)
192 return wcs
195def getIcrsAtZenith(lon, lat, height, utc):
196 """Get the icrs at zenith given a lat/long/height/time in UTC.
198 Parameters
199 ----------
200 lon : `float`
201 The longitude, in degrees.
202 lat : `float`
203 The latitude, in degrees.
204 height : `float`
205 The height above sea level in meters.
206 utc : `str`
207 The time in UTC as an ISO string, e.g. '2022-05-27 20:41:02'
209 Returns
210 -------
211 skyCoordAtZenith : `astropy.coordinates.SkyCoord`
212 The skyCoord at zenith.
213 """
214 location = EarthLocation.from_geodetic(lon=lon * u.deg, lat=lat * u.deg, height=height)
215 obsTime = Time(utc, format="iso", scale="utc")
216 skyCoord = SkyCoord(AltAz(obstime=obsTime, alt=90.0 * u.deg, az=180.0 * u.deg, location=location))
217 return skyCoord.transform_to("icrs")
220def headerToWcs(header):
221 """Convert an astrometry.net wcs header dict to a DM wcs object.
223 Parameters
224 ----------
225 header : `dict`
226 The wcs header, as returned from from the astrometry_net fit.
228 Returns
229 -------
230 wcs : `lsst.afw.geom.SkyWcs`
231 The wcs.
232 """
233 wcsPropSet = PropertySet.from_mapping(header)
234 return SkyWcs(wcsPropSet)
237def runCharactierizeImage(exp, snr, minPix):
238 """Run the image characterization task, finding only bright sources.
240 Parameters
241 ----------
242 exp : `lsst.afw.image.Exposure`
243 The exposure to characterize.
244 snr : `float`
245 The SNR threshold for detection.
246 minPix : `int`
247 The minimum number of pixels to count as a source.
249 Returns
250 -------
251 result : `lsst.pipe.base.Struct`
252 The result from the image characterization task.
253 """
254 charConfig = CharacterizeImageConfig()
255 charConfig.doMeasurePsf = False
256 charConfig.doApCorr = False
257 charConfig.doDeblend = False
258 charConfig.doMaskStreaks = False
259 charConfig.repair.doCosmicRay = False
261 charConfig.detection.minPixels = minPix
262 charConfig.detection.thresholdValue = snr
263 charConfig.detection.includeThresholdMultiplier = 1
264 charConfig.detection.thresholdType = "stdev"
266 # fit background with the most simple thing possible as we don't need
267 # much sophistication here. weighting=False is *required* for very
268 # large binSizes.
269 charConfig.background.algorithm = "CONSTANT"
270 charConfig.background.approxOrderX = 0
271 charConfig.background.approxOrderY = -1
272 charConfig.background.binSize = max(exp.getWidth(), exp.getHeight())
273 charConfig.background.weighting = False
275 # set this to use all the same minimal settings as those above
276 charConfig.detection.background = charConfig.background
278 charTask = CharacterizeImageTask(config=charConfig)
280 charResult = charTask.run(exp)
281 return charResult
284def filterSourceCatOnBrightest(
285 catalog,
286 brightFraction,
287 minSources=15,
288 maxSources=200,
289 flux_field="base_CircularApertureFlux_3_0_instFlux",
290):
291 """Filter a sourceCat on the brightness, leaving only the top fraction.
293 Return a catalog containing the brightest sources in the input. Makes an
294 initial coarse cut, keeping those above 0.1% of the maximum finite flux,
295 and then returning the specified fraction of the remaining sources,
296 or minSources, whichever is greater.
298 Parameters
299 ----------
300 catalog : `lsst.afw.table.SourceCatalog`
301 Catalog to be filtered.
302 brightFraction : `float`
303 Return this fraction of the brightest sources.
304 minSources : `int`, optional
305 Always return at least this many sources.
306 maxSources : `int`, optional
307 Never return more than this many sources.
308 flux_field : `str`, optional
309 Name of flux field to filter on.
311 Returns
312 -------
313 result : `lsst.afw.table.SourceCatalog`
314 Brightest sources in the input image, in ascending order of brightness.
315 """
316 assert minSources > 0
317 assert brightFraction > 0 and brightFraction <= 1
318 if not maxSources >= minSources:
319 raise ValueError(
320 "maxSources must be greater than or equal to minSources, got " f"{maxSources=}, {minSources=}"
321 )
323 maxFlux = np.nanmax(catalog[flux_field])
324 result = catalog.subset(catalog[flux_field] > maxFlux * 0.001)
326 print(f"Catalog had {len(catalog)} sources, of which {len(result)} were above 0.1% of max")
328 item = catalog.schema.find(flux_field)
329 result = catalog.copy(deep=True) # sort() is in place; copy so we don't modify the original
330 result.sort(item.key)
331 result = result.copy(deep=True) # make it memory contiguous
332 end = int(np.ceil(len(result) * brightFraction))
333 return result[-min(maxSources, max(end, minSources)) :]