Coverage for python/lsst/summit/utils/astrometry/utils.py: 12%
132 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-10 03:53 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-10 03:53 -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 patchHeader(header):
151 """This is a TEMPORARY function to patch some info into the headers.
152 """
153 if header.get('SECPIX') == '3.11':
154 # the narrow camera currently is wrong about its place scale by of ~2.2
155 header['SECPIX'] = '1.44'
156 # update the boresight locations until this goes into the header
157 # service
158 header['CRPIX1'] = 1898.10
159 header['CRPIX2'] = 998.47
160 if header.get('SECPIX') == '8.64':
161 # update the boresight locations until this goes into the header
162 # service
163 header['CRPIX1'] = 1560.85
164 header['CRPIX2'] = 1257.15
165 return header
168def genericCameraHeaderToWcs(exp):
169 header = exp.getMetadata().toDict()
170 header = patchHeader(header)
172 header['CTYPE1'] = 'RA---TAN-SIP'
173 header['CTYPE2'] = 'DEC--TAN-SIP'
175 header['CRVAL1'] = getAverageRaFromHeader(header)
176 header['CRVAL2'] = getAverageDecFromHeader(header)
178 plateScale = header.get('SECPIX')
179 if not plateScale:
180 raise RuntimeError('Failed to find platescale in header')
181 plateScale = float(plateScale)
183 header['CD1_1'] = plateScale / 3600
184 header['CD1_2'] = 0
185 header['CD2_1'] = 0
186 header['CD2_2'] = plateScale / 3600
188 wcsPropSet = PropertySet.from_mapping(header)
189 wcs = SkyWcs(wcsPropSet)
190 return wcs
193def getIcrsAtZenith(lon, lat, height, utc):
194 """Get the icrs at zenith given a lat/long/height/time in UTC.
196 Parameters
197 ----------
198 lon : `float`
199 The longitude, in degrees.
200 lat : `float`
201 The latitude, in degrees.
202 height : `float`
203 The height above sea level in meters.
204 utc : `str`
205 The time in UTC as an ISO string, e.g. '2022-05-27 20:41:02'
207 Returns
208 -------
209 skyCoordAtZenith : `astropy.coordinates.SkyCoord`
210 The skyCoord at zenith.
211 """
212 location = EarthLocation.from_geodetic(lon=lon*u.deg,
213 lat=lat*u.deg,
214 height=height)
215 obsTime = Time(utc, format='iso', scale='utc')
216 skyCoord = SkyCoord(AltAz(obstime=obsTime,
217 alt=90.0*u.deg,
218 az=180.0*u.deg,
219 location=location))
220 return skyCoord.transform_to('icrs')
223def headerToWcs(header):
224 """Convert an astrometry.net wcs header dict to a DM wcs object.
226 Parameters
227 ----------
228 header : `dict`
229 The wcs header, as returned from from the astrometry_net fit.
231 Returns
232 -------
233 wcs : `lsst.afw.geom.SkyWcs`
234 The wcs.
235 """
236 wcsPropSet = PropertySet.from_mapping(header)
237 return SkyWcs(wcsPropSet)
240def runCharactierizeImage(exp, snr, minPix):
241 """Run the image characterization task, finding only bright sources.
243 Parameters
244 ----------
245 exp : `lsst.afw.image.Exposure`
246 The exposure to characterize.
247 snr : `float`
248 The SNR threshold for detection.
249 minPix : `int`
250 The minimum number of pixels to count as a source.
252 Returns
253 -------
254 result : `lsst.pipe.base.Struct`
255 The result from the image characterization task.
256 """
257 charConfig = CharacterizeImageConfig()
258 charConfig.doMeasurePsf = False
259 charConfig.doApCorr = False
260 charConfig.doDeblend = False
261 charConfig.repair.doCosmicRay = False
263 charConfig.detection.minPixels = minPix
264 charConfig.detection.thresholdValue = snr
265 charConfig.detection.includeThresholdMultiplier = 1
267 # fit background with the most simple thing possible as we don't need
268 # much sophistication here. weighting=False is *required* for very
269 # large binSizes.
270 charConfig.background.algorithm = 'CONSTANT'
271 charConfig.background.approxOrderX = 0
272 charConfig.background.approxOrderY = -1
273 charConfig.background.binSize = max(exp.getWidth(), exp.getHeight())
274 charConfig.background.weighting = False
276 # set this to use all the same minimal settings as those above
277 charConfig.detection.background = charConfig.background
279 charTask = CharacterizeImageTask(config=charConfig)
281 charResult = charTask.run(exp)
282 return charResult
285def filterSourceCatOnBrightest(catalog, brightFraction, minSources=15, maxSources=200,
286 flux_field="base_CircularApertureFlux_3_0_instFlux"):
287 """Filter a sourceCat on the brightness, leaving only the top fraction.
289 Return a catalog containing the brightest sources in the input. Makes an
290 initial coarse cut, keeping those above 0.1% of the maximum finite flux,
291 and then returning the specified fraction of the remaining sources,
292 or minSources, whichever is greater.
294 Parameters
295 ----------
296 catalog : `lsst.afw.table.SourceCatalog`
297 Catalog to be filtered.
298 brightFraction : `float`
299 Return this fraction of the brightest sources.
300 minSources : `int`, optional
301 Always return at least this many sources.
302 maxSources : `int`, optional
303 Never return more than this many sources.
304 flux_field : `str`, optional
305 Name of flux field to filter on.
307 Returns
308 -------
309 result : `lsst.afw.table.SourceCatalog`
310 Brightest sources in the input image, in ascending order of brightness.
311 """
312 assert minSources > 0
313 assert brightFraction > 0 and brightFraction <= 1
314 if not maxSources >= minSources:
315 raise ValueError('maxSources must be greater than or equal to minSources, got '
316 f'{maxSources=}, {minSources=}')
318 maxFlux = np.nanmax(catalog[flux_field])
319 result = catalog.subset(catalog[flux_field] > maxFlux * 0.001)
321 print(f"Catalog had {len(catalog)} sources, of which {len(result)} were above 0.1% of max")
323 item = catalog.schema.find(flux_field)
324 result = catalog.copy(deep=True) # sort() is in place; copy so we don't modify the original
325 result.sort(item.key)
326 result = result.copy(deep=True) # make it memory contiguous
327 end = int(np.ceil(len(result)*brightFraction))
328 return result[-min(maxSources, max(end, minSources)):]