LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
LSST Data Management Base Package
Loading...
Searching...
No Matches
fitAffineWcs.py
Go to the documentation of this file.
1# This file is part of meas_astrom.
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/>.
21
22__all__ = ["FitAffineWcsTask", "FitAffineWcsConfig", "TransformedSkyWcsMaker"]
23
24
25import astshim
26import numpy as np
27from scipy.optimize import least_squares
28
29from lsst.afw.geom import makeSkyWcs, SkyWcs
30import lsst.afw.math
31from lsst.geom import Point2D, degrees, arcseconds, radians
32import lsst.pex.config as pexConfig
33import lsst.pipe.base as pipeBase
34from lsst.utils.timer import timeMethod
35
36from .makeMatchStatistics import makeMatchStatisticsInRadians
37from .setMatchDistance import setMatchDistance
38
39
40def _chiFunc(x, refPoints, srcPixels, wcsMaker):
41 """Function to minimize to fit the shift and rotation in the WCS.
42
43 Parameters
44 ----------
45 x : `numpy.ndarray`
46 Current fit values to test. Float values in array are:
47
48 - ``bearingTo``: Direction to move the wcs coord in.
49 - ``separation``: Distance along sphere to move wcs coord in.
50 - ``affine0,0``: [0, 0] value of the 2x2 affine transform matrix.
51 - ``affine0,1``: [0, 1] value of the 2x2 affine transform matrix.
52 - ``affine1,0``: [1, 0] value of the 2x2 affine transform matrix.
53 - ``affine1,1``: [1, 1] value of the 2x2 affine transform matrix.
54 refPoints : `list` of `lsst.afw.geom.SpherePoint`
55 Reference object on Sky locations.
56 srcPixels : `list` of `lsst.geom.Point2D`
57 Source object positions on the pixels.
58 wcsMaker : `TransformedSkyWcsMaker`
59 Container class for producing the updated Wcs.
60
61 Returns
62 -------
63 outputSeparations : `list` of `float`
64 Separation between predicted source location and reference location in
65 radians.
66 """
67 wcs = wcsMaker.makeWcs(x[:2], x[2:].reshape((2, 2)))
68
69 outputSeparations = []
70 # Fit both sky to pixel and pixel to sky to avoid any non-invertible
71 # affine matrices.
72 for ref, src in zip(refPoints, srcPixels):
73 skySep = ref.getTangentPlaneOffset(wcs.pixelToSky(src))
74 outputSeparations.append(skySep[0].asArcseconds())
75 outputSeparations.append(skySep[1].asArcseconds())
76 xySep = src - wcs.skyToPixel(ref)
77 # Convert the pixel separations to units, arcseconds to match units
78 # of sky separation.
79 outputSeparations.append(
80 xySep[0] * wcs.getPixelScale(src).asArcseconds())
81 outputSeparations.append(
82 xySep[1] * wcs.getPixelScale(src).asArcseconds())
83
84 return outputSeparations
85
86
87# Keeping this around for now in case any of the fit parameters need to be
88# configurable. Likely the maximum allowed shift magnitude (parameter 2 in the
89# fit.)
90class FitAffineWcsConfig(pexConfig.Config):
91 """Config for FitTanSipWcsTask."""
92 pass
93
94
95class FitAffineWcsTask(pipeBase.Task):
96 """Fit a TAN-SIP WCS given a list of reference object/source matches.
97
98 This WCS fitter should be used on top of a cameraGeom distortion model as
99 the model assumes that only a shift the WCS center position and a small
100 affine transform are required.
101 """
102 ConfigClass = FitAffineWcsConfig
103 _DefaultName = "fitAffineWcs"
104
105 @timeMethod
106 def fitWcs(self,
107 matches,
108 initWcs,
109 bbox=None,
110 refCat=None,
111 sourceCat=None,
112 exposure=None):
113 """Fit a simple Affine transform with a shift to the matches and update
114 the WCS.
115
116 This method assumes that the distortion model of the telescope is
117 applied correctly and is accurate with only a slight rotation,
118 rotation, and "squish" required to fit to the reference locations.
119
120 Parameters
121 ----------
122 matches : `list` of `lsst.afw.table.ReferenceMatch`
123 The following fields are read:
124
125 - match.first (reference object) coord
126 - match.second (source) centroid
127
128 The following fields are written:
129
130 - match.first (reference object) centroid,
131 - match.second (source) centroid
132 - match.distance (on sky separation, in radians)
133
134 initWcs : `lsst.afw.geom.SkyWcs`
135 initial WCS
136 bbox : `lsst.geom.Box2I`
137 Ignored; present for consistency with FitSipDistortionTask.
139 reference object catalog, or None.
140 If provided then all centroids are updated with the new WCS,
141 otherwise only the centroids for ref objects in matches are
142 updated. Required fields are "centroid_x", "centroid_y",
143 "coord_ra", and "coord_dec".
144 sourceCat : `lsst.afw.table.SourceCatalog`
145 source catalog, or None.
146 If provided then coords are updated with the new WCS;
147 otherwise only the coords for sources in matches are updated.
148 Required fields are "slot_Centroid_x", "slot_Centroid_y", and
149 "coord_ra", and "coord_dec".
150 exposure : `lsst.afw.image.Exposure`
151 Ignored; present for consistency with FitSipDistortionTask.
152
153 Returns
154 -------
155 result : `lsst.pipe.base.Struct`
156 with the following fields:
157
158 - ``wcs`` : the fit WCS (`lsst.afw.geom.SkyWcs`)
159 - ``scatterOnSky`` : median on-sky separation between reference
160 objects and sources in "matches" (`lsst.afw.geom.Angle`)
161 """
162 # Create a data-structure that decomposes the input Wcs frames and
163 # appends the new transform.
164 wcsMaker = TransformedSkyWcsMaker(initWcs)
165
166 refPoints = []
167 srcPixels = []
168 offsetLong = 0
169 offsetLat = 0
170 # Grab reference coordinates and source centroids. Compute the average
171 # direction and separation between the reference and the sources.
172 for match in matches:
173 refCoord = match.first.getCoord()
174 refPoints.append(refCoord)
175 srcCentroid = match.second.getCentroid()
176 srcPixels.append(srcCentroid)
177 srcCoord = initWcs.pixelToSky(srcCentroid)
178 deltaLong, deltaLat = srcCoord.getTangentPlaneOffset(refCoord)
179 offsetLong += deltaLong.asArcseconds()
180 offsetLat += deltaLat.asArcseconds()
181 offsetLong /= len(srcPixels)
182 offsetLat /= len(srcPixels)
183 offsetDist = np.sqrt(offsetLong ** 2 + offsetLat ** 2)
184 if offsetDist > 0.:
185 offsetDir = np.degrees(np.arccos(offsetLong / offsetDist))
186 else:
187 offsetDir = 0.
188 offsetDir *= np.sign(offsetLat)
189 self.log.debug("Initial shift guess: Direction: %.3f, Dist %.3f...",
190 offsetDir, offsetDist)
191
192 # Best performing fitter in scipy tried so far (vs. default settings in
193 # minimize). Exits early because of the xTol value which cannot be
194 # disabled in scipy1.2.1. Matrix starting values are non-zero as this
195 # results in better fit off-diagonal terms.
196 fit = least_squares(
197 _chiFunc,
198 x0=[offsetDir, offsetDist, 1., 1e-8, 1e-8, 1.],
199 args=(refPoints, srcPixels, wcsMaker),
200 method='dogbox',
201 bounds=[[-360, -np.inf, -np.inf, -np.inf, -np.inf, -np.inf],
202 [360, np.inf, np.inf, np.inf, np.inf, np.inf]],
203 ftol=2.3e-16,
204 gtol=2.31e-16,
205 xtol=2.3e-16)
206 self.log.debug("Best fit: Direction: %.3f, Dist: %.3f, "
207 "Affine matrix: [[%.6f, %.6f], [%.6f, %.6f]]...",
208 fit.x[0], fit.x[1],
209 fit.x[2], fit.x[3], fit.x[4], fit.x[5])
210
211 wcs = wcsMaker.makeWcs(fit.x[:2], fit.x[2:].reshape((2, 2)))
212
213 # Copied from other fit*WcsTasks.
214 if refCat is not None:
215 self.log.debug("Updating centroids in refCat")
216 lsst.afw.table.updateRefCentroids(wcs, refList=refCat)
217 else:
218 self.log.warning("Updating reference object centroids in match list; refCat is None")
220 wcs,
221 refList=[match.first for match in matches])
222
223 if sourceCat is not None:
224 self.log.debug("Updating coords in sourceCat")
225 lsst.afw.table.updateSourceCoords(wcs, sourceList=sourceCat)
226 else:
227 self.log.warning("Updating source coords in match list; sourceCat is None")
229 wcs,
230 sourceList=[match.second for match in matches])
231 setMatchDistance(matches)
232
233 stats = makeMatchStatisticsInRadians(wcs,
234 matches,
235 lsst.afw.math.MEDIAN)
236 scatterOnSky = stats.getValue() * radians
237
238 self.log.debug("In fitter scatter %.4f", scatterOnSky.asArcseconds())
239
240 return lsst.pipe.base.Struct(
241 wcs=wcs,
242 scatterOnSky=scatterOnSky,
243 )
244
245
247 """Convenience class for appending a shifting an input SkyWcs on sky and
248 appending an affine transform.
249
250 The class assumes that all frames are sequential and are mapped one to the
251 next.
252
253 Parameters
254 ----------
255 input_sky_wcs : `lsst.afw.geom.SkyWcs`
256 WCS to decompose and append affine matrix and shift in on sky
257 location to.
258 """
259
260 def __init__(self, inputSkyWcs):
261 self.frameDict = inputSkyWcs.getFrameDict()
262
263 # Grab the order of the frames by index.
264 # TODO: DM-20825
265 # Change the frame the transform is appended to to be explicitly
266 # the FIELD_ANGLE->IWC transform. Requires related tickets to be
267 # completed.
268 domains = self.frameDict.getAllDomains()
269 self.frameIdxs = np.sort([self.frameDict.getIndex(domain)
270 for domain in domains])
271 self.frameMin = np.min(self.frameIdxs)
272 self.frameMax = np.max(self.frameIdxs)
273
274 # Find frame just before the final mapping to sky and store those
275 # indices and mappings for later.
276 self.mapFrom = self.frameMax - 2
277 if self.mapFrom < self.frameMin:
278 self.mapFrom = self.frameMin
279 self.mapTo = self.frameMax - 1
280 if self.mapTo <= self.mapFrom:
281 self.mapTo = self.frameMax
282 self.lastMapBeforeSky = self.frameDict.getMapping(
283 self.mapFrom, self.mapTo)
284
285 # Get the original WCS sky location.
286
287 self.origin = inputSkyWcs.getSkyOrigin()
288
289 def makeWcs(self, crvalOffset, affMatrix):
290 """Apply a shift and affine transform to the WCS internal to this
291 class.
292
293 A new SkyWcs with these transforms applied is returns.
294
295 Parameters
296 ----------
297 crval_shift : `numpy.ndarray`, (2,)
298 Shift in radians to apply to the Wcs origin/crvals.
299 aff_matrix : 'numpy.ndarray', (3, 3)
300 Affine matrix to apply to the mapping/transform to add to the
301 WCS.
302
303 Returns
304 -------
305 outputWcs : `lsst.afw.geom.SkyWcs`
306 Wcs with a final shift and affine transform applied.
307 """
308 # Create a WCS that only maps from IWC to Sky with the shifted
309 # Sky origin position. This is simply the final undistorted tangent
310 # plane to sky. The PIXELS to SKY map will be become our IWC to SKY
311 # map and gives us our final shift position.
312 iwcsToSkyWcs = makeSkyWcs(
313 Point2D(0., 0.),
314 self.origin.offset(crvalOffset[0] * degrees,
315 crvalOffset[1] * arcseconds),
316 np.array([[1., 0.], [0., 1.]]))
317 iwcToSkyMap = iwcsToSkyWcs.getFrameDict().getMapping("PIXELS", "SKY")
318
319 # Append a simple affine Matrix transform to the current to the
320 # second to last frame mapping. e.g. the one just before IWC to SKY.
321 newMapping = self.lastMapBeforeSky.then(astshim.MatrixMap(affMatrix))
322
323 # Create a new frame dict starting from the input_sky_wcs's first
324 # frame. Append the correct mapping created above and our new on
325 # sky location.
326 outputFrameDict = astshim.FrameDict(
327 self.frameDict.getFrame(self.frameMin))
328 for frameIdx in self.frameIdxs:
329 if frameIdx == self.mapFrom:
330 outputFrameDict.addFrame(
331 self.mapFrom,
332 newMapping,
333 self.frameDict.getFrame(self.mapTo))
334 elif frameIdx >= self.mapTo:
335 continue
336 else:
337 outputFrameDict.addFrame(
338 frameIdx,
339 self.frameDict.getMapping(frameIdx, frameIdx + 1),
340 self.frameDict.getFrame(frameIdx + 1))
341 # Append the final sky frame to the frame dict.
342 outputFrameDict.addFrame(
343 self.frameMax - 1,
344 iwcToSkyMap,
345 iwcsToSkyWcs.getFrameDict().getFrame("SKY"))
346
347 return SkyWcs(outputFrameDict)
table::Key< int > to
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Definition: SkyWcs.h:117
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
Custom catalog class for record/table subclasses that are guaranteed to have an ID,...
Definition: SortedCatalog.h:42
An integer coordinate rectangle.
Definition: Box.h:55
def fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None)
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
Update centroids in a collection of reference objects.
Definition: wcsUtils.cc:72
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList)
Update sky coordinates in a collection of source objects.
Definition: wcsUtils.cc:95
Lightweight representation of a geometric match between two records.
Definition: Match.h:67