LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
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.linalg import lstsq
28
29from lsst.afw.geom import makeSkyWcs, SkyWcs
30import lsst.afw.math
31from lsst.geom import Point2D, radians
32import lsst.pex.config as pexConfig
33import lsst.pipe.base as pipeBase
34from lsst.utils.timer import timeMethod
35
36from ._measAstromLib import makeMatchStatisticsInRadians
37from .setMatchDistance import setMatchDistance
38
39
40# Keeping this around for now in case any of the fit parameters need to be
41# configurable. Likely the maximum allowed shift magnitude (parameter 2 in the
42# fit.)
43class FitAffineWcsConfig(pexConfig.Config):
44 """Config for FitTanSipWcsTask."""
45 pass
46
47
48class FitAffineWcsTask(pipeBase.Task):
49 """Fit a TAN-SIP WCS given a list of reference object/source matches.
50
51 This WCS fitter should be used on top of a cameraGeom distortion model as
52 the model assumes that only a shift the WCS center position and a small
53 affine transform are required.
54 """
55 ConfigClass = FitAffineWcsConfig
56 _DefaultName = "fitAffineWcs"
57
58 @timeMethod
59 def fitWcs(self,
60 matches,
61 initWcs,
62 bbox=None,
63 refCat=None,
64 sourceCat=None,
65 exposure=None):
66 """Fit a simple Affine transform with a shift to the matches and update
67 the WCS.
68
69 This method assumes that the distortion model of the telescope is
70 applied correctly and is accurate with only a slight rotation,
71 rotation, and "squish" required to fit to the reference locations.
72
73 Parameters
74 ----------
75 matches : `list` of `lsst.afw.table.ReferenceMatch`
76 The following fields are read:
77
78 - match.first (reference object) coord
79 - match.second (source) centroid
80
81 The following fields are written:
82
83 - match.first (reference object) centroid,
84 - match.second (source) centroid
85 - match.distance (on sky separation, in radians)
86
87 initWcs : `lsst.afw.geom.SkyWcs`
88 initial WCS
89 bbox : `lsst.geom.Box2I`
90 Ignored; present for consistency with FitSipDistortionTask.
91 refCat : `lsst.afw.table.SimpleCatalog`
92 reference object catalog, or None.
93 If provided then all centroids are updated with the new WCS,
94 otherwise only the centroids for ref objects in matches are
95 updated. Required fields are "centroid_x", "centroid_y",
96 "coord_ra", and "coord_dec".
97 sourceCat : `lsst.afw.table.SourceCatalog`
98 source catalog, or None.
99 If provided then coords are updated with the new WCS;
100 otherwise only the coords for sources in matches are updated.
101 Required fields are "slot_Centroid_x", "slot_Centroid_y", and
102 "coord_ra", and "coord_dec".
103 exposure : `lsst.afw.image.Exposure`
104 Ignored; present for consistency with FitSipDistortionTask.
105
106 Returns
107 -------
108 result : `lsst.pipe.base.Struct`
109 with the following fields:
110
111 - ``wcs`` : the fit WCS (`lsst.afw.geom.SkyWcs`)
112 - ``scatterOnSky`` : median on-sky separation between reference
113 objects and sources in "matches" (`lsst.afw.geom.Angle`)
114 """
115 # Create a data-structure that decomposes the input Wcs frames and
116 # appends the new transform.
117 wcsMaker = TransformedSkyWcsMaker(initWcs)
118
119 # Grab the initial transformations going back from sky coordinates
120 # and forward from pixel coordinates.
121 back = wcsMaker.frameDict.getMapping(wcsMaker.frameMax, wcsMaker.frameMax-1)
122 forward = wcsMaker.lastMapBeforeSky
123
124 # Create containers for the data going into the Affine fit. This will
125 # be done by approximating the solution to Ax=b where x will be the
126 # affine parameters and a linear shift. The approximate solution is
127 # calculated using a least squares minimization of the Ax=b equation.
128 #
129 # This is looking to find the affine transform of the following form:
130 # [x', y'] = [[a, b], [c, d]] [x, y] + [s, t]
131 #
132 # where a,b,c,d are the parameters of the affine transform, and s,t
133 # are linear shift parameters.
134 #
135 # To solve for these unknown parameters the unknown matrix x in the
136 # equation Ax=b will be of the form:
137 # [a, b, c, d, s, t].
138 #
139 # This implies that each constraining point will correspond to two rows
140 # in the A matrix in the following form:
141 # [x_i, y_i, 0, 0, 1, 0]
142 # [0, 0, x_i, y_i, 0, 1].
143 #
144 # The corresponding output points in the b vector will have the form:
145 # [x'_i, y'_i, x'_(i+i), y'_(i+1)....]
146 A = np.zeros((len(matches)*2, 6), dtype=float)
147 b = np.empty(len(matches)*2, dtype=float)
148
149 # Constant terms related to the shift in x and and y parameters.
150 A[::2, 4] = 1
151 A[1::2, 5] = 1
152
153 # loop over each of the matches and populate the matrices.
154 for i, match in enumerate(matches):
155 refCoord = match.first.getCoord()
156 b[i*2:i*2+2] = back.applyForward(refCoord)
157
158 srcCentroid = match.second.getCentroid()
159 val = forward.applyForward(srcCentroid)
160 A[i*2, :2] = val
161 A[i*2+1, 2:4] = val
162
163 # solve for the affine and shift parameters
164 # The lapack_driver parameter is set to the quickest routine tested for
165 # this application at the time of writing.
166 fit = lstsq(A, b, lapack_driver='gelsy')[0]
167
168 self.log.debug("Linear shift in x: %.3f, y: %.3f, "
169 "Affine matrix: [[%.6f, %.6f], [%.6f, %.6f]]...",
170 fit[4], fit[5],
171 fit[0], fit[1], fit[2], fit[3])
172
173 # create the final wcs
174 wcs = wcsMaker.makeWcs(fit[4:], fit[:4].reshape((2, 2)))
175
176 # Copied from other fit*WcsTasks.
177 if refCat is not None:
178 self.log.debug("Updating centroids in refCat")
179 lsst.afw.table.updateRefCentroids(wcs, refList=refCat)
180 else:
181 self.log.warning("Updating reference object centroids in match list; refCat is None")
183 wcs,
184 refList=[match.first for match in matches])
185
186 if sourceCat is not None:
187 self.log.debug("Updating coords in sourceCat")
188 lsst.afw.table.updateSourceCoords(wcs, sourceList=sourceCat)
189 else:
190 self.log.warning("Updating source coords in match list; sourceCat is None")
192 wcs,
193 sourceList=[match.second for match in matches])
194 setMatchDistance(matches)
195
196 stats = makeMatchStatisticsInRadians(wcs,
197 matches,
198 lsst.afw.math.MEDIAN)
199 scatterOnSky = stats.getValue() * radians
200
201 self.log.debug("In fitter scatter %.4f", scatterOnSky.asArcseconds())
202
203 return lsst.pipe.base.Struct(
204 wcs=wcs,
205 scatterOnSky=scatterOnSky,
206 )
207
208
210 """Convenience class for appending a shifting an input SkyWcs on sky and
211 appending an affine transform.
212
213 The class assumes that all frames are sequential and are mapped one to the
214 next.
215
216 Parameters
217 ----------
218 input_sky_wcs : `lsst.afw.geom.SkyWcs`
219 WCS to decompose and append affine matrix and shift in on sky
220 location to.
221 """
222
223 def __init__(self, inputSkyWcs):
224 self.frameDict = inputSkyWcs.getFrameDict()
225
226 # Grab the order of the frames by index.
227 # TODO: DM-20825
228 # Change the frame the transform is appended to to be explicitly
229 # the FIELD_ANGLE->IWC transform. Requires related tickets to be
230 # completed.
231 domains = self.frameDict.getAllDomains()
232 self.frameIdxs = np.sort([self.frameDict.getIndex(domain)
233 for domain in domains])
234 self.frameMin = np.min(self.frameIdxs)
235 self.frameMax = np.max(self.frameIdxs)
236
237 # Find frame just before the final mapping to sky and store those
238 # indices and mappings for later.
239 self.mapFrom = self.frameMax - 2
240 if self.mapFrom < self.frameMin:
241 self.mapFrom = self.frameMin
242 self.mapTo = self.frameMax - 1
243 if self.mapTo <= self.mapFrom:
244 self.mapTo = self.frameMax
245 self.lastMapBeforeSky = self.frameDict.getMapping(
246 self.mapFrom, self.mapTo)
247
248 # Get the original WCS sky location.
249
250 self.origin = inputSkyWcs.getSkyOrigin()
251
252 def makeWcs(self, linearShift, affMatrix):
253 """Apply a shift and affine transform to the WCS internal to this
254 class.
255
256 A new SkyWcs with these transforms applied is returns.
257
258 Parameters
259 ----------
260 linearShift : `numpy.ndarray`, (2,)
261 A linear shift to apply at the same time as applying the affine
262 matrix transform.
263 aff_matrix : 'numpy.ndarray', (3, 3)
264 Affine matrix to apply to the mapping/transform to add to the
265 WCS.
266
267 Returns
268 -------
269 outputWcs : `lsst.afw.geom.SkyWcs`
270 Wcs with a final shift and affine transform applied.
271 """
272 # Create a WCS that only maps from IWC to Sky with the shifted
273 # Sky origin position. This is simply the final undistorted tangent
274 # plane to sky. The PIXELS to SKY map will be become our IWC to SKY
275 # map and gives us our final shift position.
276 iwcsToSkyWcs = makeSkyWcs(
277 Point2D(0., 0.),
278 self.origin,
279 np.array([[1., 0.], [0., 1.]]))
280 iwcToSkyMap = iwcsToSkyWcs.getFrameDict().getMapping("PIXELS", "SKY")
281
282 # Append a simple affine Matrix transform to the current to the
283 # second to last frame mapping. e.g. the one just before IWC to SKY.
284 newMapping = self.lastMapBeforeSky.then(astshim.MatrixMap(affMatrix))
285 newMapping = newMapping.then(astshim.ShiftMap(linearShift))
286
287 # Create a new frame dict starting from the input_sky_wcs's first
288 # frame. Append the correct mapping created above and our new on
289 # sky location.
290 outputFrameDict = astshim.FrameDict(
291 self.frameDict.getFrame(self.frameMin))
292 for frameIdx in self.frameIdxs:
293 if frameIdx == self.mapFrom:
294 outputFrameDict.addFrame(
295 self.mapFrom,
296 newMapping,
297 self.frameDict.getFrame(self.mapTo))
298 elif frameIdx >= self.mapTo:
299 continue
300 else:
301 outputFrameDict.addFrame(
302 frameIdx,
303 self.frameDict.getMapping(frameIdx, frameIdx + 1),
304 self.frameDict.getFrame(frameIdx + 1))
305 # Append the final sky frame to the frame dict.
306 outputFrameDict.addFrame(
307 self.frameMax - 1,
308 iwcToSkyMap,
309 iwcsToSkyWcs.getFrameDict().getFrame("SKY"))
310
311 return SkyWcs(outputFrameDict)
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Definition SkyWcs.h:117
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:73
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList, bool include_covariance=true)
Update sky coordinates in a collection of source objects.
Definition wcsUtils.cc:125