lsst.meas.astrom
g91f025aaea+e2f0a7808d
Toggle main menu visibility
Loading...
Searching...
No Matches
python
lsst
meas
astrom
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
25
import
astshim
26
import
numpy
as
np
27
from
scipy.linalg
import
lstsq
28
29
from
lsst.afw.geom
import
makeSkyWcs, SkyWcs
30
import
lsst.afw.math
31
from
lsst.geom
import
Point2D, radians
32
import
lsst.pex.config
as
pexConfig
33
import
lsst.pipe.base
as
pipeBase
34
from
lsst.utils.timer
import
timeMethod
35
36
from
._measAstromLib
import
makeMatchStatisticsInRadians
37
from
.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.)
43
class
FitAffineWcsConfig
(pexConfig.Config):
44
"""Config for FitTanSipWcsTask."""
45
pass
46
47
48
class
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"
)
182
lsst.afw.table.updateRefCentroids
(
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"
)
191
lsst.afw.table.updateSourceCoords
(
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
209
class
TransformedSkyWcsMaker
():
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)
lsst::afw::geom::SkyWcs
lsst::geom::Point< double, 2 >
lsst::meas::astrom.fitAffineWcs.FitAffineWcsConfig
Definition
fitAffineWcs.py:43
lsst::meas::astrom.fitAffineWcs.FitAffineWcsTask
Definition
fitAffineWcs.py:48
lsst::meas::astrom.fitAffineWcs.FitAffineWcsTask.fitWcs
fitWcs(self, matches, initWcs, bbox=None, refCat=None, sourceCat=None, exposure=None)
Definition
fitAffineWcs.py:65
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker
Definition
fitAffineWcs.py:209
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.frameMin
frameMin
Definition
fitAffineWcs.py:234
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.mapTo
int mapTo
Definition
fitAffineWcs.py:242
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.makeWcs
makeWcs(self, linearShift, affMatrix)
Definition
fitAffineWcs.py:252
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.frameDict
frameDict
Definition
fitAffineWcs.py:224
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.origin
origin
Definition
fitAffineWcs.py:250
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.frameMax
frameMax
Definition
fitAffineWcs.py:235
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.__init__
__init__(self, inputSkyWcs)
Definition
fitAffineWcs.py:223
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.lastMapBeforeSky
lastMapBeforeSky
Definition
fitAffineWcs.py:245
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.frameIdxs
frameIdxs
Definition
fitAffineWcs.py:232
lsst::meas::astrom.fitAffineWcs.TransformedSkyWcsMaker.mapFrom
int mapFrom
Definition
fitAffineWcs.py:239
lsst::afw::geom
lsst::afw::math
lsst::afw::table::updateRefCentroids
void updateRefCentroids(geom::SkyWcs const &wcs, ReferenceCollection &refList)
lsst::afw::table::updateSourceCoords
void updateSourceCoords(geom::SkyWcs const &wcs, SourceCollection &sourceList, bool include_covariance=true)
lsst::geom
lsst::meas::astrom::makeMatchStatisticsInRadians
afw::math::Statistics makeMatchStatisticsInRadians(afw::geom::SkyWcs const &wcs, std::vector< MatchT > const &matchList, int const flags, afw::math::StatisticsControl const &sctrl=afw::math::StatisticsControl())
Compute statistics of on-sky radial separation for a match list, in radians.
Definition
makeMatchStatistics.cc:71
lsst::pex::config
lsst.pipe.base
Generated on
for lsst.meas.astrom by
1.17.0