Coverage for python/lsst/summit/utils/astrometry/utils.py: 14%
117 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-06 03:24 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-06 03:24 -0800
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 numpy as np
24import astropy.units as u
25from astropy.time import Time
26from astropy.coordinates import AltAz, EarthLocation, SkyCoord
28from lsst.afw.geom import SkyWcs
29from lsst.daf.base import PropertySet
30from lsst.pipe.tasks.characterizeImage import CharacterizeImageTask, CharacterizeImageConfig
32__all__ = [
33 'claverHeaderToWcs',
34 'getAverageRaFromHeader',
35 'getAverageDecFromHeader',
36 'getAverageAzFromHeader',
37 'getAverageElFromHeader',
38 'genericCameraHeaderToWcs',
39 'getIcrsAtZenith',
40 'headerToWcs',
41 'runCharactierizeImage',
42 'filterSourceCatOnBrightest',
43]
46def claverHeaderToWcs(exp, nominalRa=None, nominalDec=None):
47 """Given an exposure taken by Chuck Claver at his house, construct a wcs
48 with the ra/dec set to zenith unless a better guess is supplied.
50 Automatically sets the platescale depending on the lens.
52 Parameters
53 ----------
54 exp : `lsst.afw.image.Exposure`
55 The exposure to construct the wcs for.
56 nominalRa : `float`, optional
57 The guess for the ra.
58 nominalDec : `float`, optional
59 The guess for the Dec.
61 Returns
62 -------
63 wcs : `lsst.afw.geom.SkyWcs`
64 The constructed wcs.
65 """
66 header = exp.getMetadata().toDict()
68 # set the plate scale depending on the lens and put into CD matrix
69 # plate scale info from:
70 # https://confluence.lsstcorp.org/pages/viewpage.action?pageId=191987725
71 lens = header['INSTLEN']
72 if '135mm' in lens:
73 arcSecPerPix = 8.64
74 elif '375mm' in lens:
75 arcSecPerPix = 3.11
76 elif '750mm' in lens:
77 arcSecPerPix = 1.56
78 else:
79 raise ValueError(f'Unrecognised lens: {lens}')
81 header['CD1_1'] = arcSecPerPix / 3600
82 header['CD1_2'] = 0
83 header['CD2_1'] = 0
84 header['CD2_2'] = arcSecPerPix / 3600
86 # calculate the ra/dec at zenith and assume Chuck pointed it vertically
87 icrs = getIcrsAtZenith(float(header['OBSLON']),
88 float(header['OBSLAT']),
89 float(header['OBSHGT']),
90 header['UTC'])
91 header['CRVAL1'] = nominalRa if nominalRa else icrs.ra.degree
92 header['CRVAL2'] = nominalDec if nominalDec else icrs.dec.degree
94 # just use the nomimal chip centre, not that it matters
95 # given radec = zenith
96 width, height = exp.image.array.shape
97 header['CRPIX1'] = width/2
98 header['CRPIX2'] = height/2
100 header['CTYPE1'] = 'RA---TAN-SIP'
101 header['CTYPE2'] = 'DEC--TAN-SIP'
103 wcsPropSet = PropertySet.from_mapping(header)
104 wcs = SkyWcs(wcsPropSet)
105 return wcs
108# don't be tempted to get cute and try to combine these 4 functions. It would
109# be easy to do but it's not unlikley they will diverge in the future.
110def getAverageRaFromHeader(header):
111 raStart = header.get('RASTART')
112 raEnd = header.get('RAEND')
113 if not raStart or not raEnd:
114 raise RuntimeError(f'Failed to get RA from header due to missing RASTART/END {raStart} {raEnd}')
115 raStart = float(raStart)
116 raEnd = float(raEnd)
117 return (raStart + raEnd) / 2
120def getAverageDecFromHeader(header):
121 decStart = header.get('DECSTART')
122 decEnd = header.get('DECEND')
123 if not decStart or not decEnd:
124 raise RuntimeError(f'Failed to get DEC from header due to missing DECSTART/END {decStart} {decEnd}')
125 decStart = float(decStart)
126 decEnd = float(decEnd)
127 return (decStart + decEnd) / 2
130def getAverageAzFromHeader(header):
131 azStart = header.get('AZSTART')
132 azEnd = header.get('AZEND')
133 if not azStart or not azEnd:
134 raise RuntimeError(f'Failed to get az from header due to missing AZSTART/END {azStart} {azEnd}')
135 azStart = float(azStart)
136 azEnd = float(azEnd)
137 return (azStart + azEnd) / 2
140def getAverageElFromHeader(header):
141 elStart = header.get('ELSTART')
142 elEnd = header.get('ELEND')
143 if not elStart or not elEnd:
144 raise RuntimeError(f'Failed to get el from header due to missing ELSTART/END {elStart} {elEnd}')
145 elStart = float(elStart)
146 elEnd = float(elEnd)
147 return (elStart + elEnd) / 2
150def genericCameraHeaderToWcs(exp):
151 header = exp.getMetadata().toDict()
152 width, height = exp.image.array.shape
153 header['CRPIX1'] = width/2
154 header['CRPIX2'] = height/2
156 header['CTYPE1'] = 'RA---TAN-SIP'
157 header['CTYPE2'] = 'DEC--TAN-SIP'
159 header['CRVAL1'] = getAverageRaFromHeader(header)
160 header['CRVAL2'] = getAverageDecFromHeader(header)
162 plateScale = header.get('SECPIX')
163 if not plateScale:
164 raise RuntimeError('Failed to find platescale in header')
165 plateScale = float(plateScale)
167 header['CD1_1'] = plateScale / 3600
168 header['CD1_2'] = 0
169 header['CD2_1'] = 0
170 header['CD2_2'] = plateScale / 3600
172 wcsPropSet = PropertySet.from_mapping(header)
173 wcs = SkyWcs(wcsPropSet)
174 return wcs
177def getIcrsAtZenith(lon, lat, height, utc):
178 """Get the icrs at zenith given a lat/long/height/time in UTC.
180 Parameters
181 ----------
182 lon : `float`
183 The longitude, in degrees.
184 lat : `float`
185 The latitude, in degrees.
186 height : `float`
187 The height above sea level in meters.
188 utc : `str`
189 The time in UTC as an ISO string, e.g. '2022-05-27 20:41:02'
191 Returns
192 -------
193 skyCoordAtZenith : `astropy.coordinates.SkyCoord`
194 The skyCoord at zenith.
195 """
196 location = EarthLocation.from_geodetic(lon=lon*u.deg,
197 lat=lat*u.deg,
198 height=height)
199 obsTime = Time(utc, format='iso', scale='utc')
200 skyCoord = SkyCoord(AltAz(obstime=obsTime,
201 alt=90.0*u.deg,
202 az=180.0*u.deg,
203 location=location))
204 return skyCoord.transform_to('icrs')
207def headerToWcs(header):
208 """Convert an astrometry.net wcs header dict to a DM wcs object.
210 Parameters
211 ----------
212 header : `dict`
213 The wcs header, as returned from from the astrometry_net fit.
215 Returns
216 -------
217 wcs : `lsst.afw.geom.SkyWcs`
218 The wcs.
219 """
220 wcsPropSet = PropertySet.from_mapping(header)
221 return SkyWcs(wcsPropSet)
224def runCharactierizeImage(exp, snr, minPix):
225 """Run the image characterization task, finding only bright sources.
227 Parameters
228 ----------
229 exp : `lsst.afw.image.Exposure`
230 The exposure to characterize.
231 snr : `float`
232 The SNR threshold for detection.
233 minPix : `int`
234 The minimum number of pixels to count as a source.
236 Returns
237 -------
238 result : `lsst.pipe.base.Struct`
239 The result from the image characterization task.
240 """
241 charConfig = CharacterizeImageConfig()
242 charConfig.doMeasurePsf = False
243 charConfig.doApCorr = False
244 charConfig.doDeblend = False
245 charConfig.repair.doCosmicRay = False
246 charConfig.repair.doInterpolate = True
247 charConfig.detection.minPixels = minPix
248 charConfig.detection.thresholdValue = snr
250 charTask = CharacterizeImageTask(config=charConfig)
252 charResult = charTask.run(exp)
253 return charResult
256def filterSourceCatOnBrightest(catalog, brightFraction, minSources=15,
257 flux_field="base_CircularApertureFlux_3_0_instFlux"):
258 """Filter a sourceCat on the brightness, leaving only the top fraction.
260 Return a catalog containing the brightest sources in the input. Makes an
261 initial coarse cut, keeping those above 0.1% of the maximum finite flux,
262 and then returning the specified fraction of the remaining sources,
263 or minSources, whichever is greater.
265 Parameters
266 ----------
267 catalog : `lsst.afw.table.SourceCatalog`
268 Catalog to be filtered.
269 brightFraction : `float`
270 Return this fraction of the brightest sources.
271 minSources : `int`, optional
272 Always return at least this many sources.
273 flux_field : `str`, optional
274 Name of flux field to filter on.
276 Returns
277 -------
278 result : `lsst.afw.table.SourceCatalog`
279 Brightest sources in the input image, in ascending order of brightness.
280 """
281 assert minSources > 0
282 assert brightFraction > 0 and brightFraction <= 1
283 maxFlux = np.nanmax(catalog[flux_field])
284 result = catalog.subset(catalog[flux_field] > maxFlux * 0.001)
286 print(f"Catalog had {len(catalog)} sources, of which {len(result)} were above 0.1% of max")
288 item = catalog.schema.find(flux_field)
289 result = catalog.copy(deep=True) # sort() is in place; copy so we don't modify the original
290 result.sort(item.key)
291 result = result.copy(deep=True) # make it memory contiguous
292 end = int(np.ceil(len(result)*brightFraction))
293 return result[-max(end, minSources):]