Coverage for python/lsst/summit/utils/astrometry/utils.py: 12%
136 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-14 11:16 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-14 11:16 +0000
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 if header.get('SECPIX') == '0.67':
166 # use the fast camera chip centre until we know better
167 header['SECPIX'] = '0.6213' # measured from a fit
168 header['CRPIX1'] = 329.5
169 header['CRPIX2'] = 246.5
170 return header
173def genericCameraHeaderToWcs(exp):
174 header = exp.getMetadata().toDict()
175 header = patchHeader(header)
177 header['CTYPE1'] = 'RA---TAN-SIP'
178 header['CTYPE2'] = 'DEC--TAN-SIP'
180 header['CRVAL1'] = getAverageRaFromHeader(header)
181 header['CRVAL2'] = getAverageDecFromHeader(header)
183 plateScale = header.get('SECPIX')
184 if not plateScale:
185 raise RuntimeError('Failed to find platescale in header')
186 plateScale = float(plateScale)
188 header['CD1_1'] = plateScale / 3600
189 header['CD1_2'] = 0
190 header['CD2_1'] = 0
191 header['CD2_2'] = plateScale / 3600
193 wcsPropSet = PropertySet.from_mapping(header)
194 wcs = SkyWcs(wcsPropSet)
195 return wcs
198def getIcrsAtZenith(lon, lat, height, utc):
199 """Get the icrs at zenith given a lat/long/height/time in UTC.
201 Parameters
202 ----------
203 lon : `float`
204 The longitude, in degrees.
205 lat : `float`
206 The latitude, in degrees.
207 height : `float`
208 The height above sea level in meters.
209 utc : `str`
210 The time in UTC as an ISO string, e.g. '2022-05-27 20:41:02'
212 Returns
213 -------
214 skyCoordAtZenith : `astropy.coordinates.SkyCoord`
215 The skyCoord at zenith.
216 """
217 location = EarthLocation.from_geodetic(lon=lon*u.deg,
218 lat=lat*u.deg,
219 height=height)
220 obsTime = Time(utc, format='iso', scale='utc')
221 skyCoord = SkyCoord(AltAz(obstime=obsTime,
222 alt=90.0*u.deg,
223 az=180.0*u.deg,
224 location=location))
225 return skyCoord.transform_to('icrs')
228def headerToWcs(header):
229 """Convert an astrometry.net wcs header dict to a DM wcs object.
231 Parameters
232 ----------
233 header : `dict`
234 The wcs header, as returned from from the astrometry_net fit.
236 Returns
237 -------
238 wcs : `lsst.afw.geom.SkyWcs`
239 The wcs.
240 """
241 wcsPropSet = PropertySet.from_mapping(header)
242 return SkyWcs(wcsPropSet)
245def runCharactierizeImage(exp, snr, minPix):
246 """Run the image characterization task, finding only bright sources.
248 Parameters
249 ----------
250 exp : `lsst.afw.image.Exposure`
251 The exposure to characterize.
252 snr : `float`
253 The SNR threshold for detection.
254 minPix : `int`
255 The minimum number of pixels to count as a source.
257 Returns
258 -------
259 result : `lsst.pipe.base.Struct`
260 The result from the image characterization task.
261 """
262 charConfig = CharacterizeImageConfig()
263 charConfig.doMeasurePsf = False
264 charConfig.doApCorr = False
265 charConfig.doDeblend = False
266 charConfig.repair.doCosmicRay = False
268 charConfig.detection.minPixels = minPix
269 charConfig.detection.thresholdValue = snr
270 charConfig.detection.includeThresholdMultiplier = 1
272 # fit background with the most simple thing possible as we don't need
273 # much sophistication here. weighting=False is *required* for very
274 # large binSizes.
275 charConfig.background.algorithm = 'CONSTANT'
276 charConfig.background.approxOrderX = 0
277 charConfig.background.approxOrderY = -1
278 charConfig.background.binSize = max(exp.getWidth(), exp.getHeight())
279 charConfig.background.weighting = False
281 # set this to use all the same minimal settings as those above
282 charConfig.detection.background = charConfig.background
284 charTask = CharacterizeImageTask(config=charConfig)
286 charResult = charTask.run(exp)
287 return charResult
290def filterSourceCatOnBrightest(catalog, brightFraction, minSources=15, maxSources=200,
291 flux_field="base_CircularApertureFlux_3_0_instFlux"):
292 """Filter a sourceCat on the brightness, leaving only the top fraction.
294 Return a catalog containing the brightest sources in the input. Makes an
295 initial coarse cut, keeping those above 0.1% of the maximum finite flux,
296 and then returning the specified fraction of the remaining sources,
297 or minSources, whichever is greater.
299 Parameters
300 ----------
301 catalog : `lsst.afw.table.SourceCatalog`
302 Catalog to be filtered.
303 brightFraction : `float`
304 Return this fraction of the brightest sources.
305 minSources : `int`, optional
306 Always return at least this many sources.
307 maxSources : `int`, optional
308 Never return more than this many sources.
309 flux_field : `str`, optional
310 Name of flux field to filter on.
312 Returns
313 -------
314 result : `lsst.afw.table.SourceCatalog`
315 Brightest sources in the input image, in ascending order of brightness.
316 """
317 assert minSources > 0
318 assert brightFraction > 0 and brightFraction <= 1
319 if not maxSources >= minSources:
320 raise ValueError('maxSources must be greater than or equal to minSources, got '
321 f'{maxSources=}, {minSources=}')
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)):]