Coverage for python/lsst/meas/base/plugins.py: 50%

328 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-19 04:56 -0700

1# This file is part of meas_base. 

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"""Definition of measurement plugins. 

23 

24This module defines and registers a series of pure-Python measurement plugins 

25which have trivial implementations. It also wraps measurement algorithms 

26defined in C++ to expose them to the measurement framework. 

27""" 

28 

29import numpy as np 

30 

31import lsst.pex.exceptions 

32import lsst.geom 

33import lsst.afw.detection 

34import lsst.afw.geom 

35 

36from ._measBaseLib import (ApertureFluxControl, ApertureFluxTransform, 

37 BaseTransform, BlendednessAlgorithm, 

38 BlendednessControl, CircularApertureFluxAlgorithm, 

39 GaussianFluxAlgorithm, GaussianFluxControl, 

40 GaussianFluxTransform, LocalBackgroundAlgorithm, 

41 LocalBackgroundControl, LocalBackgroundTransform, 

42 MeasurementError, NaiveCentroidAlgorithm, 

43 NaiveCentroidControl, NaiveCentroidTransform, 

44 PeakLikelihoodFluxAlgorithm, 

45 PeakLikelihoodFluxControl, 

46 PeakLikelihoodFluxTransform, PixelFlagsAlgorithm, 

47 PixelFlagsControl, PsfFluxAlgorithm, PsfFluxControl, 

48 PsfFluxTransform, ScaledApertureFluxAlgorithm, 

49 ScaledApertureFluxControl, 

50 ScaledApertureFluxTransform, SdssCentroidAlgorithm, 

51 SdssCentroidControl, SdssCentroidTransform, 

52 SdssShapeAlgorithm, SdssShapeControl, 

53 SdssShapeTransform) 

54from .baseMeasurement import BaseMeasurementPluginConfig 

55from .forcedMeasurement import ForcedPlugin, ForcedPluginConfig 

56from .pluginRegistry import register 

57from .pluginsBase import BasePlugin 

58from .sfm import SingleFramePlugin, SingleFramePluginConfig 

59from .transforms import SimpleCentroidTransform 

60from .wrappers import GenericPlugin, wrapSimpleAlgorithm, wrapTransform 

61 

62__all__ = ( 

63 "SingleFrameFPPositionConfig", "SingleFrameFPPositionPlugin", 

64 "SingleFrameJacobianConfig", "SingleFrameJacobianPlugin", 

65 "VarianceConfig", "SingleFrameVariancePlugin", "ForcedVariancePlugin", 

66 "InputCountConfig", "SingleFrameInputCountPlugin", "ForcedInputCountPlugin", 

67 "SingleFramePeakCentroidConfig", "SingleFramePeakCentroidPlugin", 

68 "SingleFrameSkyCoordConfig", "SingleFrameSkyCoordPlugin", 

69 "ForcedPeakCentroidConfig", "ForcedPeakCentroidPlugin", 

70 "ForcedTransformedCentroidConfig", "ForcedTransformedCentroidPlugin", 

71 "ForcedTransformedCentroidFromCoordConfig", 

72 "ForcedTransformedCentroidFromCoordPlugin", 

73 "ForcedTransformedShapeConfig", "ForcedTransformedShapePlugin", 

74 "EvaluateLocalPhotoCalibPlugin", "EvaluateLocalPhotoCalibPluginConfig", 

75 "EvaluateLocalWcsPlugin", "EvaluateLocalWcsPluginConfig", 

76) 

77 

78 

79wrapSimpleAlgorithm(PsfFluxAlgorithm, Control=PsfFluxControl, 

80 TransformClass=PsfFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

81 shouldApCorr=True, hasLogName=True) 

82wrapSimpleAlgorithm(PeakLikelihoodFluxAlgorithm, Control=PeakLikelihoodFluxControl, 

83 TransformClass=PeakLikelihoodFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

84wrapSimpleAlgorithm(GaussianFluxAlgorithm, Control=GaussianFluxControl, 

85 TransformClass=GaussianFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

86 shouldApCorr=True) 

87wrapSimpleAlgorithm(NaiveCentroidAlgorithm, Control=NaiveCentroidControl, 

88 TransformClass=NaiveCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

89wrapSimpleAlgorithm(SdssCentroidAlgorithm, Control=SdssCentroidControl, 

90 TransformClass=SdssCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

91wrapSimpleAlgorithm(PixelFlagsAlgorithm, Control=PixelFlagsControl, 

92 executionOrder=BasePlugin.FLUX_ORDER) 

93wrapSimpleAlgorithm(SdssShapeAlgorithm, Control=SdssShapeControl, 

94 TransformClass=SdssShapeTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

95wrapSimpleAlgorithm(ScaledApertureFluxAlgorithm, Control=ScaledApertureFluxControl, 

96 TransformClass=ScaledApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

97 

98wrapSimpleAlgorithm(CircularApertureFluxAlgorithm, needsMetadata=True, Control=ApertureFluxControl, 

99 TransformClass=ApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

100wrapSimpleAlgorithm(BlendednessAlgorithm, Control=BlendednessControl, 

101 TransformClass=BaseTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

102 

103wrapSimpleAlgorithm(LocalBackgroundAlgorithm, Control=LocalBackgroundControl, 

104 TransformClass=LocalBackgroundTransform, executionOrder=BasePlugin.FLUX_ORDER) 

105 

106wrapTransform(PsfFluxTransform) 

107wrapTransform(PeakLikelihoodFluxTransform) 

108wrapTransform(GaussianFluxTransform) 

109wrapTransform(NaiveCentroidTransform) 

110wrapTransform(SdssCentroidTransform) 

111wrapTransform(SdssShapeTransform) 

112wrapTransform(ScaledApertureFluxTransform) 

113wrapTransform(ApertureFluxTransform) 

114wrapTransform(LocalBackgroundTransform) 

115 

116 

117class SingleFrameFPPositionConfig(SingleFramePluginConfig): 

118 """Configuration for the focal plane position measurment algorithm. 

119 """ 

120 

121 pass 

122 

123 

124@register("base_FPPosition") 

125class SingleFrameFPPositionPlugin(SingleFramePlugin): 

126 """Algorithm to calculate the position of a centroid on the focal plane. 

127 

128 Parameters 

129 ---------- 

130 config : `SingleFrameFPPositionConfig` 

131 Plugin configuraion. 

132 name : `str` 

133 Plugin name. 

134 schema : `lsst.afw.table.Schema` 

135 The schema for the measurement output catalog. New fields will be 

136 added to hold measurements produced by this plugin. 

137 metadata : `lsst.daf.base.PropertySet` 

138 Plugin metadata that will be attached to the output catalog 

139 """ 

140 

141 ConfigClass = SingleFrameFPPositionConfig 

142 

143 @classmethod 

144 def getExecutionOrder(cls): 

145 return cls.SHAPE_ORDER 

146 

147 def __init__(self, config, name, schema, metadata): 

148 SingleFramePlugin.__init__(self, config, name, schema, metadata) 

149 self.focalValue = lsst.afw.table.Point2DKey.addFields(schema, name, "Position on the focal plane", 

150 "mm") 

151 self.focalFlag = schema.addField(name + "_flag", type="Flag", doc="Set to True for any fatal failure") 

152 self.detectorFlag = schema.addField(name + "_missingDetector_flag", type="Flag", 

153 doc="Set to True if detector object is missing") 

154 

155 def measure(self, measRecord, exposure): 

156 det = exposure.getDetector() 

157 if not det: 

158 measRecord.set(self.detectorFlag, True) 

159 fp = lsst.geom.Point2D(np.nan, np.nan) 

160 else: 

161 center = measRecord.getCentroid() 

162 fp = det.transform(center, lsst.afw.cameraGeom.PIXELS, lsst.afw.cameraGeom.FOCAL_PLANE) 

163 measRecord.set(self.focalValue, fp) 

164 

165 def fail(self, measRecord, error=None): 

166 measRecord.set(self.focalFlag, True) 

167 

168 

169class SingleFrameJacobianConfig(SingleFramePluginConfig): 

170 """Configuration for the Jacobian calculation plugin. 

171 """ 

172 

173 pixelScale = lsst.pex.config.Field(dtype=float, default=0.5, doc="Nominal pixel size (arcsec)") 

174 

175 

176@register("base_Jacobian") 

177class SingleFrameJacobianPlugin(SingleFramePlugin): 

178 """Compute the Jacobian and its ratio with a nominal pixel area. 

179 

180 This enables one to compare relative, rather than absolute, pixel areas. 

181 

182 Parameters 

183 ---------- 

184 config : `SingleFrameJacobianConfig` 

185 Plugin configuraion. 

186 name : `str` 

187 Plugin name. 

188 schema : `lsst.afw.table.Schema` 

189 The schema for the measurement output catalog. New fields will be 

190 added to hold measurements produced by this plugin. 

191 metadata : `lsst.daf.base.PropertySet` 

192 Plugin metadata that will be attached to the output catalog 

193 """ 

194 

195 ConfigClass = SingleFrameJacobianConfig 

196 

197 @classmethod 

198 def getExecutionOrder(cls): 

199 return cls.SHAPE_ORDER 

200 

201 def __init__(self, config, name, schema, metadata): 

202 SingleFramePlugin.__init__(self, config, name, schema, metadata) 

203 self.jacValue = schema.addField(name + '_value', type="D", doc="Jacobian correction") 

204 self.jacFlag = schema.addField(name + '_flag', type="Flag", doc="Set to 1 for any fatal failure") 

205 # Calculate one over the area of a nominal reference pixel, where area is in arcsec^2 

206 self.scale = pow(self.config.pixelScale, -2) 

207 

208 def measure(self, measRecord, exposure): 

209 center = measRecord.getCentroid() 

210 # Compute the area of a pixel at a source record's centroid, and take 

211 # the ratio of that with the defined reference pixel area. 

212 result = np.abs(self.scale*exposure.getWcs().linearizePixelToSky( 

213 center, 

214 lsst.geom.arcseconds).getLinear().computeDeterminant()) 

215 measRecord.set(self.jacValue, result) 

216 

217 def fail(self, measRecord, error=None): 

218 measRecord.set(self.jacFlag, True) 

219 

220 

221class VarianceConfig(BaseMeasurementPluginConfig): 

222 """Configuration for the variance calculation plugin. 

223 """ 

224 scale = lsst.pex.config.Field(dtype=float, default=5.0, optional=True, 

225 doc="Scale factor to apply to shape for aperture") 

226 mask = lsst.pex.config.ListField(doc="Mask planes to ignore", dtype=str, 

227 default=["DETECTED", "DETECTED_NEGATIVE", "BAD", "SAT"]) 

228 

229 

230class VariancePlugin(GenericPlugin): 

231 """Compute the median variance corresponding to a footprint. 

232 

233 The aim here is to measure the background variance, rather than that of 

234 the object itself. In order to achieve this, the variance is calculated 

235 over an area scaled up from the shape of the input footprint. 

236 

237 Parameters 

238 ---------- 

239 config : `VarianceConfig` 

240 Plugin configuraion. 

241 name : `str` 

242 Plugin name. 

243 schema : `lsst.afw.table.Schema` 

244 The schema for the measurement output catalog. New fields will be 

245 added to hold measurements produced by this plugin. 

246 metadata : `lsst.daf.base.PropertySet` 

247 Plugin metadata that will be attached to the output catalog 

248 """ 

249 

250 ConfigClass = VarianceConfig 

251 

252 FAILURE_BAD_CENTROID = 1 

253 """Denotes failures due to bad centroiding (`int`). 

254 """ 

255 

256 FAILURE_EMPTY_FOOTPRINT = 2 

257 """Denotes failures due to a lack of usable pixels (`int`). 

258 """ 

259 

260 @classmethod 

261 def getExecutionOrder(cls): 

262 return BasePlugin.FLUX_ORDER 

263 

264 def __init__(self, config, name, schema, metadata): 

265 GenericPlugin.__init__(self, config, name, schema, metadata) 

266 self.varValue = schema.addField(name + '_value', type="D", doc="Variance at object position") 

267 self.emptyFootprintFlag = schema.addField(name + '_flag_emptyFootprint', type="Flag", 

268 doc="Set to True when the footprint has no usable pixels") 

269 

270 # Alias the badCentroid flag to that which is defined for the target 

271 # of the centroid slot. We do not simply rely on the alias because 

272 # that could be changed post-measurement. 

273 schema.getAliasMap().set(name + '_flag_badCentroid', schema.getAliasMap().apply("slot_Centroid_flag")) 

274 

275 def measure(self, measRecord, exposure, center): 

276 # Create an aperture and grow it by scale value defined in config to 

277 # ensure there are enough pixels around the object to get decent 

278 # statistics 

279 if not np.all(np.isfinite(measRecord.getCentroid())): 

280 raise MeasurementError("Bad centroid and/or shape", self.FAILURE_BAD_CENTROID) 

281 aperture = lsst.afw.geom.Ellipse(measRecord.getShape(), measRecord.getCentroid()) 

282 aperture.scale(self.config.scale) 

283 ellipse = lsst.afw.geom.SpanSet.fromShape(aperture) 

284 foot = lsst.afw.detection.Footprint(ellipse) 

285 foot.clipTo(exposure.getBBox(lsst.afw.image.PARENT)) 

286 # Filter out any pixels which have mask bits set corresponding to the 

287 # planes to be excluded (defined in config.mask) 

288 maskedImage = exposure.getMaskedImage() 

289 pixels = lsst.afw.detection.makeHeavyFootprint(foot, maskedImage) 

290 maskBits = maskedImage.getMask().getPlaneBitMask(self.config.mask) 

291 logicalMask = np.logical_not(pixels.getMaskArray() & maskBits) 

292 # Compute the median variance value for each pixel not excluded by the 

293 # mask and write the record. Numpy median is used here instead of 

294 # afw.math makeStatistics because of an issue with data types being 

295 # passed into the C++ layer (DM-2379). 

296 if np.any(logicalMask): 

297 medVar = np.median(pixels.getVarianceArray()[logicalMask]) 

298 measRecord.set(self.varValue, medVar) 

299 else: 

300 raise MeasurementError("Footprint empty, or all pixels are masked, can't compute median", 

301 self.FAILURE_EMPTY_FOOTPRINT) 

302 

303 def fail(self, measRecord, error=None): 

304 # Check that we have an error object and that it is of type 

305 # MeasurementError 

306 if isinstance(error, MeasurementError): 

307 assert error.getFlagBit() in (self.FAILURE_BAD_CENTROID, self.FAILURE_EMPTY_FOOTPRINT) 

308 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

309 if error.getFlagBit() == self.FAILURE_EMPTY_FOOTPRINT: 

310 measRecord.set(self.emptyFootprintFlag, True) 

311 measRecord.set(self.varValue, np.nan) 

312 GenericPlugin.fail(self, measRecord, error) 

313 

314 

315SingleFrameVariancePlugin = VariancePlugin.makeSingleFramePlugin("base_Variance") 

316"""Single-frame version of `VariancePlugin`. 

317""" 

318 

319ForcedVariancePlugin = VariancePlugin.makeForcedPlugin("base_Variance") 

320"""Forced version of `VariancePlugin`. 

321""" 

322 

323 

324class InputCountConfig(BaseMeasurementPluginConfig): 

325 """Configuration for the input image counting plugin. 

326 """ 

327 pass 

328 

329 

330class InputCountPlugin(GenericPlugin): 

331 """Count the number of input images which contributed to a a source. 

332 

333 Parameters 

334 ---------- 

335 config : `InputCountConfig` 

336 Plugin configuraion. 

337 name : `str` 

338 Plugin name. 

339 schema : `lsst.afw.table.Schema` 

340 The schema for the measurement output catalog. New fields will be 

341 added to hold measurements produced by this plugin. 

342 metadata : `lsst.daf.base.PropertySet` 

343 Plugin metadata that will be attached to the output catalog 

344 

345 Notes 

346 ----- 

347 Information is derived from the image's `~lsst.afw.image.CoaddInputs`. 

348 Note these limitation: 

349 

350 - This records the number of images which contributed to the pixel in the 

351 center of the source footprint, rather than to any or all pixels in the 

352 source. 

353 - Clipping in the coadd is not taken into account. 

354 """ 

355 

356 ConfigClass = InputCountConfig 

357 

358 FAILURE_BAD_CENTROID = 1 

359 """Denotes failures due to bad centroiding (`int`). 

360 """ 

361 

362 FAILURE_NO_INPUTS = 2 

363 """Denotes failures due to the image not having coadd inputs. (`int`) 

364 """ 

365 

366 @classmethod 

367 def getExecutionOrder(cls): 

368 return BasePlugin.SHAPE_ORDER 

369 

370 def __init__(self, config, name, schema, metadata): 

371 GenericPlugin.__init__(self, config, name, schema, metadata) 

372 self.numberKey = schema.addField(name + '_value', type="I", 

373 doc="Number of images contributing at center, not including any" 

374 "clipping") 

375 self.noInputsFlag = schema.addField(name + '_flag_noInputs', type="Flag", 

376 doc="No coadd inputs available") 

377 # Alias the badCentroid flag to that which is defined for the target of the centroid slot. 

378 # We do not simply rely on the alias because that could be changed post-measurement. 

379 schema.getAliasMap().set(name + '_flag_badCentroid', schema.getAliasMap().apply("slot_Centroid_flag")) 

380 

381 def measure(self, measRecord, exposure, center): 

382 if not exposure.getInfo().getCoaddInputs(): 

383 raise MeasurementError("No coadd inputs defined.", self.FAILURE_NO_INPUTS) 

384 if not np.all(np.isfinite(center)): 

385 raise MeasurementError("Source has a bad centroid.", self.FAILURE_BAD_CENTROID) 

386 

387 ccds = exposure.getInfo().getCoaddInputs().ccds 

388 measRecord.set(self.numberKey, len(ccds.subsetContaining(center, exposure.getWcs()))) 

389 

390 def fail(self, measRecord, error=None): 

391 if error is not None: 

392 assert error.getFlagBit() in (self.FAILURE_BAD_CENTROID, self.FAILURE_NO_INPUTS) 

393 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

394 if error.getFlagBit() == self.FAILURE_NO_INPUTS: 

395 measRecord.set(self.noInputsFlag, True) 

396 GenericPlugin.fail(self, measRecord, error) 

397 

398 

399SingleFrameInputCountPlugin = InputCountPlugin.makeSingleFramePlugin("base_InputCount") 

400"""Single-frame version of `InputCoutPlugin`. 

401""" 

402 

403ForcedInputCountPlugin = InputCountPlugin.makeForcedPlugin("base_InputCount") 

404"""Forced version of `InputCoutPlugin`. 

405""" 

406 

407 

408class EvaluateLocalPhotoCalibPluginConfig(BaseMeasurementPluginConfig): 

409 """Configuration for the variance calculation plugin. 

410 """ 

411 pass 

412 

413 

414class EvaluateLocalPhotoCalibPlugin(GenericPlugin): 

415 """Evaluate the local value of the Photometric Calibration in the exposure. 

416 

417 The aim is to store the local calib value within the catalog for later 

418 use in the Science Data Model functors. 

419 """ 

420 ConfigClass = EvaluateLocalPhotoCalibPluginConfig 

421 

422 @classmethod 

423 def getExecutionOrder(cls): 

424 return BasePlugin.FLUX_ORDER 

425 

426 def __init__(self, config, name, schema, metadata): 

427 GenericPlugin.__init__(self, config, name, schema, metadata) 

428 self.photoKey = schema.addField( 

429 name, 

430 type="D", 

431 doc="Local approximation of the PhotoCalib calibration factor at " 

432 "the location of the src.") 

433 self.photoErrKey = schema.addField( 

434 "%sErr" % name, 

435 type="D", 

436 doc="Error on the local approximation of the PhotoCalib " 

437 "calibration factor at the location of the src.") 

438 

439 def measure(self, measRecord, exposure, center): 

440 

441 photoCalib = exposure.getPhotoCalib() 

442 calib = photoCalib.getLocalCalibration(center) 

443 measRecord.set(self.photoKey, calib) 

444 

445 calibErr = photoCalib.getCalibrationErr() 

446 measRecord.set(self.photoErrKey, calibErr) 

447 

448 

449SingleFrameEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeSingleFramePlugin( 

450 "base_LocalPhotoCalib") 

451"""Single-frame version of `EvaluatePhotoCalibPlugin`. 

452""" 

453 

454ForcedEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeForcedPlugin( 

455 "base_LocalPhotoCalib") 

456"""Forced version of `EvaluatePhotoCalibPlugin`. 

457""" 

458 

459 

460class EvaluateLocalWcsPluginConfig(BaseMeasurementPluginConfig): 

461 """Configuration for the variance calculation plugin. 

462 """ 

463 pass 

464 

465 

466class EvaluateLocalWcsPlugin(GenericPlugin): 

467 """Evaluate the local, linear approximation of the Wcs. 

468 

469 The aim is to store the local calib value within the catalog for later 

470 use in the Science Data Model functors. 

471 """ 

472 ConfigClass = EvaluateLocalWcsPluginConfig 

473 _scale = (1.0 * lsst.geom.arcseconds).asDegrees() 

474 

475 @classmethod 

476 def getExecutionOrder(cls): 

477 return BasePlugin.FLUX_ORDER 

478 

479 def __init__(self, config, name, schema, metadata): 

480 GenericPlugin.__init__(self, config, name, schema, metadata) 

481 self.cdMatrix11Key = schema.addField( 

482 f"{name}_CDMatrix_1_1", 

483 type="D", 

484 doc="(1, 1) element of the CDMatrix for the linear approximation " 

485 "of the WCS at the src location. Gives units in radians.") 

486 self.cdMatrix12Key = schema.addField( 

487 f"{name}_CDMatrix_1_2", 

488 type="D", 

489 doc="(1, 2) element of the CDMatrix for the linear approximation " 

490 "of the WCS at the src location. Gives units in radians.") 

491 self.cdMatrix21Key = schema.addField( 

492 f"{name}_CDMatrix_2_1", 

493 type="D", 

494 doc="(2, 1) element of the CDMatrix for the linear approximation " 

495 "of the WCS at the src location. Gives units in radians.") 

496 self.cdMatrix22Key = schema.addField( 

497 f"{name}_CDMatrix_2_2", 

498 type="D", 

499 doc="(2, 2) element of the CDMatrix for the linear approximation " 

500 "of the WCS at the src location. Gives units in radians.") 

501 

502 def measure(self, measRecord, exposure, center): 

503 wcs = exposure.getWcs() 

504 localMatrix = self.makeLocalTransformMatrix(wcs, center) 

505 measRecord.set(self.cdMatrix11Key, localMatrix[0, 0]) 

506 measRecord.set(self.cdMatrix12Key, localMatrix[0, 1]) 

507 measRecord.set(self.cdMatrix21Key, localMatrix[1, 0]) 

508 measRecord.set(self.cdMatrix22Key, localMatrix[1, 1]) 

509 

510 def makeLocalTransformMatrix(self, wcs, center): 

511 """Create a local, linear approximation of the wcs transformation 

512 matrix. 

513 

514 The approximation is created as if the center is at RA=0, DEC=0. All 

515 comparing x,y coordinate are relative to the position of center. Matrix 

516 is initially calculated with units arcseconds and then converted to 

517 radians. This yields higher precision results due to quirks in AST. 

518 

519 Parameters 

520 ---------- 

521 wcs : `lsst.afw.geom.SkyWcs` 

522 Wcs to approximate 

523 center : `lsst.geom.Point2D` 

524 Point at which to evaluate the LocalWcs. 

525 

526 Returns 

527 ------- 

528 localMatrix : `numpy.ndarray` 

529 Matrix representation the local wcs approximation with units 

530 radians. 

531 """ 

532 skyCenter = wcs.pixelToSky(center) 

533 localGnomonicWcs = lsst.afw.geom.makeSkyWcs( 

534 center, skyCenter, np.diag((self._scale, self._scale))) 

535 measurementToLocalGnomonic = wcs.getTransform().then( 

536 localGnomonicWcs.getTransform().inverted() 

537 ) 

538 localMatrix = measurementToLocalGnomonic.getJacobian(center) 

539 return np.radians(localMatrix / 3600) 

540 

541 

542SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs") 

543"""Single-frame version of `EvaluateLocalWcsPlugin`. 

544""" 

545 

546ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs") 

547"""Forced version of `EvaluateLocalWcsPlugin`. 

548""" 

549 

550 

551class SingleFramePeakCentroidConfig(SingleFramePluginConfig): 

552 """Configuration for the single frame peak centroiding algorithm. 

553 """ 

554 pass 

555 

556 

557@register("base_PeakCentroid") 

558class SingleFramePeakCentroidPlugin(SingleFramePlugin): 

559 """Record the highest peak in a source footprint as its centroid. 

560 

561 This is of course a relatively poor measure of the true centroid of the 

562 object; this algorithm is provided mostly for testing and debugging. 

563 

564 Parameters 

565 ---------- 

566 config : `SingleFramePeakCentroidConfig` 

567 Plugin configuraion. 

568 name : `str` 

569 Plugin name. 

570 schema : `lsst.afw.table.Schema` 

571 The schema for the measurement output catalog. New fields will be 

572 added to hold measurements produced by this plugin. 

573 metadata : `lsst.daf.base.PropertySet` 

574 Plugin metadata that will be attached to the output catalog 

575 """ 

576 

577 ConfigClass = SingleFramePeakCentroidConfig 

578 

579 @classmethod 

580 def getExecutionOrder(cls): 

581 return cls.CENTROID_ORDER 

582 

583 def __init__(self, config, name, schema, metadata): 

584 SingleFramePlugin.__init__(self, config, name, schema, metadata) 

585 self.keyX = schema.addField(name + "_x", type="D", doc="peak centroid", units="pixel") 

586 self.keyY = schema.addField(name + "_y", type="D", doc="peak centroid", units="pixel") 

587 self.flag = schema.addField(name + "_flag", type="Flag", doc="Centroiding failed") 

588 

589 def measure(self, measRecord, exposure): 

590 peak = measRecord.getFootprint().getPeaks()[0] 

591 measRecord.set(self.keyX, peak.getFx()) 

592 measRecord.set(self.keyY, peak.getFy()) 

593 

594 def fail(self, measRecord, error=None): 

595 measRecord.set(self.flag, True) 

596 

597 @staticmethod 

598 def getTransformClass(): 

599 return SimpleCentroidTransform 

600 

601 

602class SingleFrameSkyCoordConfig(SingleFramePluginConfig): 

603 """Configuration for the sky coordinates algorithm. 

604 """ 

605 pass 

606 

607 

608@register("base_SkyCoord") 

609class SingleFrameSkyCoordPlugin(SingleFramePlugin): 

610 """Record the sky position of an object based on its centroid slot and WCS. 

611 

612 The position is record in the ``coord`` field, which is part of the 

613 `~lsst.afw.table.SourceCatalog` minimal schema. 

614 

615 Parameters 

616 ---------- 

617 config : `SingleFrameSkyCoordConfig` 

618 Plugin configuraion. 

619 name : `str` 

620 Plugin name. 

621 schema : `lsst.afw.table.Schema` 

622 The schema for the measurement output catalog. New fields will be 

623 added to hold measurements produced by this plugin. 

624 metadata : `lsst.daf.base.PropertySet` 

625 Plugin metadata that will be attached to the output catalog 

626 """ 

627 

628 ConfigClass = SingleFrameSkyCoordConfig 

629 

630 @classmethod 

631 def getExecutionOrder(cls): 

632 return cls.SHAPE_ORDER 

633 

634 def measure(self, measRecord, exposure): 

635 # There should be a base class method for handling this exception. Put 

636 # this on a later ticket. Also, there should be a python Exception of 

637 # the appropriate type for this error 

638 if not exposure.hasWcs(): 

639 raise RuntimeError("Wcs not attached to exposure. Required for " + self.name + " algorithm") 

640 measRecord.updateCoord(exposure.getWcs()) 

641 

642 def fail(self, measRecord, error=None): 

643 # Override fail() to do nothing in the case of an exception: this is 

644 # not ideal, but we don't have a place to put failures because we 

645 # don't allocate any fields. Should consider fixing as part of 

646 # DM-1011 

647 pass 

648 

649 

650class ForcedPeakCentroidConfig(ForcedPluginConfig): 

651 """Configuration for the forced peak centroid algorithm. 

652 """ 

653 pass 

654 

655 

656@register("base_PeakCentroid") 

657class ForcedPeakCentroidPlugin(ForcedPlugin): 

658 """Record the highest peak in a source footprint as its centroid. 

659 

660 This is of course a relatively poor measure of the true centroid of the 

661 object; this algorithm is provided mostly for testing and debugging. 

662 

663 This is similar to `SingleFramePeakCentroidPlugin`, except that transforms 

664 the peak coordinate from the original (reference) coordinate system to the 

665 coordinate system of the exposure being measured. 

666 

667 Parameters 

668 ---------- 

669 config : `ForcedPeakCentroidConfig` 

670 Plugin configuraion. 

671 name : `str` 

672 Plugin name. 

673 schemaMapper : `lsst.afw.table.SchemaMapper` 

674 A mapping from reference catalog fields to output 

675 catalog fields. Output fields are added to the output schema. 

676 metadata : `lsst.daf.base.PropertySet` 

677 Plugin metadata that will be attached to the output catalog. 

678 """ 

679 

680 ConfigClass = ForcedPeakCentroidConfig 

681 

682 @classmethod 

683 def getExecutionOrder(cls): 

684 return cls.CENTROID_ORDER 

685 

686 def __init__(self, config, name, schemaMapper, metadata): 

687 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata) 

688 schema = schemaMapper.editOutputSchema() 

689 self.keyX = schema.addField(name + "_x", type="D", doc="peak centroid", units="pixel") 

690 self.keyY = schema.addField(name + "_y", type="D", doc="peak centroid", units="pixel") 

691 

692 def measure(self, measRecord, exposure, refRecord, refWcs): 

693 targetWcs = exposure.getWcs() 

694 peak = refRecord.getFootprint().getPeaks()[0] 

695 result = lsst.geom.Point2D(peak.getFx(), peak.getFy()) 

696 result = targetWcs.skyToPixel(refWcs.pixelToSky(result)) 

697 measRecord.set(self.keyX, result.getX()) 

698 measRecord.set(self.keyY, result.getY()) 

699 

700 @staticmethod 

701 def getTransformClass(): 

702 return SimpleCentroidTransform 

703 

704 

705class ForcedTransformedCentroidConfig(ForcedPluginConfig): 

706 """Configuration for the forced transformed centroid algorithm. 

707 """ 

708 pass 

709 

710 

711@register("base_TransformedCentroid") 

712class ForcedTransformedCentroidPlugin(ForcedPlugin): 

713 """Record the transformation of the reference catalog centroid. 

714 

715 The centroid recorded in the reference catalog is tranformed to the 

716 measurement coordinate system and stored. 

717 

718 Parameters 

719 ---------- 

720 config : `ForcedTransformedCentroidConfig` 

721 Plugin configuration 

722 name : `str` 

723 Plugin name 

724 schemaMapper : `lsst.afw.table.SchemaMapper` 

725 A mapping from reference catalog fields to output 

726 catalog fields. Output fields are added to the output schema. 

727 metadata : `lsst.daf.base.PropertySet` 

728 Plugin metadata that will be attached to the output catalog. 

729 

730 Notes 

731 ----- 

732 This is used as the slot centroid by default in forced measurement, 

733 allowing subsequent measurements to simply refer to the slot value just as 

734 they would in single-frame measurement. 

735 """ 

736 

737 ConfigClass = ForcedTransformedCentroidConfig 

738 

739 @classmethod 

740 def getExecutionOrder(cls): 

741 return cls.CENTROID_ORDER 

742 

743 def __init__(self, config, name, schemaMapper, metadata): 

744 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata) 

745 schema = schemaMapper.editOutputSchema() 

746 # Allocate x and y fields, join these into a single FunctorKey for ease-of-use. 

747 xKey = schema.addField(name + "_x", type="D", doc="transformed reference centroid column", 

748 units="pixel") 

749 yKey = schema.addField(name + "_y", type="D", doc="transformed reference centroid row", 

750 units="pixel") 

751 self.centroidKey = lsst.afw.table.Point2DKey(xKey, yKey) 

752 # Because we're taking the reference position as given, we don't bother transforming its 

753 # uncertainty and reporting that here, so there are no sigma or cov fields. We do propagate 

754 # the flag field, if it exists. 

755 if "slot_Centroid_flag" in schemaMapper.getInputSchema(): 

756 self.flagKey = schema.addField(name + "_flag", type="Flag", 

757 doc="whether the reference centroid is marked as bad") 

758 else: 

759 self.flagKey = None 

760 

761 def measure(self, measRecord, exposure, refRecord, refWcs): 

762 targetWcs = exposure.getWcs() 

763 if not refWcs == targetWcs: 

764 targetPos = targetWcs.skyToPixel(refWcs.pixelToSky(refRecord.getCentroid())) 

765 measRecord.set(self.centroidKey, targetPos) 

766 else: 

767 measRecord.set(self.centroidKey, refRecord.getCentroid()) 

768 if self.flagKey is not None: 

769 measRecord.set(self.flagKey, refRecord.getCentroidFlag()) 

770 

771 

772class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig): 

773 """Configuration for the forced transformed coord algorithm. 

774 """ 

775 pass 

776 

777 

778@register("base_TransformedCentroidFromCoord") 

779class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin): 

780 """Record the transformation of the reference catalog coord. 

781 

782 The coord recorded in the reference catalog is tranformed to the 

783 measurement coordinate system and stored. 

784 

785 Parameters 

786 ---------- 

787 config : `ForcedTransformedCentroidFromCoordConfig` 

788 Plugin configuration 

789 name : `str` 

790 Plugin name 

791 schemaMapper : `lsst.afw.table.SchemaMapper` 

792 A mapping from reference catalog fields to output 

793 catalog fields. Output fields are added to the output schema. 

794 metadata : `lsst.daf.base.PropertySet` 

795 Plugin metadata that will be attached to the output catalog. 

796 

797 Notes 

798 ----- 

799 This can be used as the slot centroid in forced measurement when only a 

800 reference coord exist, allowing subsequent measurements to simply refer to 

801 the slot value just as they would in single-frame measurement. 

802 """ 

803 

804 ConfigClass = ForcedTransformedCentroidFromCoordConfig 

805 

806 def measure(self, measRecord, exposure, refRecord, refWcs): 

807 targetWcs = exposure.getWcs() 

808 

809 targetPos = targetWcs.skyToPixel(refRecord.getCoord()) 

810 measRecord.set(self.centroidKey, targetPos) 

811 

812 if self.flagKey is not None: 

813 measRecord.set(self.flagKey, refRecord.getCentroidFlag()) 

814 

815 

816class ForcedTransformedShapeConfig(ForcedPluginConfig): 

817 """Configuration for the forced transformed shape algorithm. 

818 """ 

819 pass 

820 

821 

822@register("base_TransformedShape") 

823class ForcedTransformedShapePlugin(ForcedPlugin): 

824 """Record the transformation of the reference catalog shape. 

825 

826 The shape recorded in the reference catalog is tranformed to the 

827 measurement coordinate system and stored. 

828 

829 Parameters 

830 ---------- 

831 config : `ForcedTransformedShapeConfig` 

832 Plugin configuration 

833 name : `str` 

834 Plugin name 

835 schemaMapper : `lsst.afw.table.SchemaMapper` 

836 A mapping from reference catalog fields to output 

837 catalog fields. Output fields are added to the output schema. 

838 metadata : `lsst.daf.base.PropertySet` 

839 Plugin metadata that will be attached to the output catalog. 

840 

841 Notes 

842 ----- 

843 This is used as the slot shape by default in forced measurement, allowing 

844 subsequent measurements to simply refer to the slot value just as they 

845 would in single-frame measurement. 

846 """ 

847 

848 ConfigClass = ForcedTransformedShapeConfig 

849 

850 @classmethod 

851 def getExecutionOrder(cls): 

852 return cls.SHAPE_ORDER 

853 

854 def __init__(self, config, name, schemaMapper, metadata): 

855 ForcedPlugin.__init__(self, config, name, schemaMapper, metadata) 

856 schema = schemaMapper.editOutputSchema() 

857 # Allocate xx, yy, xy fields, join these into a single FunctorKey for ease-of-use. 

858 xxKey = schema.addField(name + "_xx", type="D", doc="transformed reference shape x^2 moment", 

859 units="pixel^2") 

860 yyKey = schema.addField(name + "_yy", type="D", doc="transformed reference shape y^2 moment", 

861 units="pixel^2") 

862 xyKey = schema.addField(name + "_xy", type="D", doc="transformed reference shape xy moment", 

863 units="pixel^2") 

864 self.shapeKey = lsst.afw.table.QuadrupoleKey(xxKey, yyKey, xyKey) 

865 # Because we're taking the reference position as given, we don't bother transforming its 

866 # uncertainty and reporting that here, so there are no sigma or cov fields. We do propagate 

867 # the flag field, if it exists. 

868 if "slot_Shape_flag" in schemaMapper.getInputSchema(): 

869 self.flagKey = schema.addField(name + "_flag", type="Flag", 

870 doc="whether the reference shape is marked as bad") 

871 else: 

872 self.flagKey = None 

873 

874 def measure(self, measRecord, exposure, refRecord, refWcs): 

875 targetWcs = exposure.getWcs() 

876 if not refWcs == targetWcs: 

877 fullTransform = lsst.afw.geom.makeWcsPairTransform(refWcs, targetWcs) 

878 localTransform = lsst.afw.geom.linearizeTransform(fullTransform, refRecord.getCentroid()) 

879 measRecord.set(self.shapeKey, refRecord.getShape().transform(localTransform.getLinear())) 

880 else: 

881 measRecord.set(self.shapeKey, refRecord.getShape()) 

882 if self.flagKey is not None: 

883 measRecord.set(self.flagKey, refRecord.getShapeFlag())