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

341 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-28 02:19 -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 .pluginRegistry import register 

37from .pluginsBase import BasePlugin 

38from .baseMeasurement import BaseMeasurementPluginConfig 

39from .sfm import SingleFramePluginConfig, SingleFramePlugin 

40from .forcedMeasurement import ForcedPluginConfig, ForcedPlugin 

41from .wrappers import wrapSimpleAlgorithm, wrapTransform, GenericPlugin 

42from .transforms import SimpleCentroidTransform 

43 

44from .apertureFlux import ApertureFluxControl, ApertureFluxTransform 

45from .transform import BaseTransform 

46from .blendedness import BlendednessAlgorithm, BlendednessControl 

47from .circularApertureFlux import CircularApertureFluxAlgorithm 

48from .gaussianFlux import GaussianFluxAlgorithm, GaussianFluxControl, GaussianFluxTransform 

49from .exceptions import MeasurementError 

50from .localBackground import LocalBackgroundControl, LocalBackgroundAlgorithm, LocalBackgroundTransform 

51from .naiveCentroid import NaiveCentroidAlgorithm, NaiveCentroidControl, NaiveCentroidTransform 

52from .peakLikelihoodFlux import PeakLikelihoodFluxAlgorithm, PeakLikelihoodFluxControl, \ 

53 PeakLikelihoodFluxTransform 

54from .pixelFlags import PixelFlagsAlgorithm, PixelFlagsControl 

55from .psfFlux import PsfFluxAlgorithm, PsfFluxControl, PsfFluxTransform 

56from .scaledApertureFlux import ScaledApertureFluxAlgorithm, ScaledApertureFluxControl, \ 

57 ScaledApertureFluxTransform 

58from .sdssCentroid import SdssCentroidAlgorithm, SdssCentroidControl, SdssCentroidTransform 

59from .sdssShape import SdssShapeAlgorithm, SdssShapeControl, SdssShapeTransform 

60 

61__all__ = ( 

62 "SingleFrameFPPositionConfig", "SingleFrameFPPositionPlugin", 

63 "SingleFrameJacobianConfig", "SingleFrameJacobianPlugin", 

64 "VarianceConfig", "SingleFrameVariancePlugin", "ForcedVariancePlugin", 

65 "InputCountConfig", "SingleFrameInputCountPlugin", "ForcedInputCountPlugin", 

66 "SingleFramePeakCentroidConfig", "SingleFramePeakCentroidPlugin", 

67 "SingleFrameSkyCoordConfig", "SingleFrameSkyCoordPlugin", 

68 "ForcedPeakCentroidConfig", "ForcedPeakCentroidPlugin", 

69 "ForcedTransformedCentroidConfig", "ForcedTransformedCentroidPlugin", 

70 "ForcedTransformedCentroidFromCoordConfig", 

71 "ForcedTransformedCentroidFromCoordPlugin", 

72 "ForcedTransformedShapeConfig", "ForcedTransformedShapePlugin", 

73 "EvaluateLocalPhotoCalibPlugin", "EvaluateLocalPhotoCalibPluginConfig", 

74 "EvaluateLocalWcsPlugin", "EvaluateLocalWcsPluginConfig", 

75) 

76 

77 

78wrapSimpleAlgorithm(PsfFluxAlgorithm, Control=PsfFluxControl, 

79 TransformClass=PsfFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

80 shouldApCorr=True, hasLogName=True) 

81wrapSimpleAlgorithm(PeakLikelihoodFluxAlgorithm, Control=PeakLikelihoodFluxControl, 

82 TransformClass=PeakLikelihoodFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

83wrapSimpleAlgorithm(GaussianFluxAlgorithm, Control=GaussianFluxControl, 

84 TransformClass=GaussianFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

85 shouldApCorr=True) 

86wrapSimpleAlgorithm(NaiveCentroidAlgorithm, Control=NaiveCentroidControl, 

87 TransformClass=NaiveCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

88wrapSimpleAlgorithm(SdssCentroidAlgorithm, Control=SdssCentroidControl, 

89 TransformClass=SdssCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

90wrapSimpleAlgorithm(PixelFlagsAlgorithm, Control=PixelFlagsControl, 

91 executionOrder=BasePlugin.FLUX_ORDER) 

92wrapSimpleAlgorithm(SdssShapeAlgorithm, Control=SdssShapeControl, 

93 TransformClass=SdssShapeTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

94wrapSimpleAlgorithm(ScaledApertureFluxAlgorithm, Control=ScaledApertureFluxControl, 

95 TransformClass=ScaledApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

96 

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

98 TransformClass=ApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

99wrapSimpleAlgorithm(BlendednessAlgorithm, Control=BlendednessControl, 

100 TransformClass=BaseTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

101 

102wrapSimpleAlgorithm(LocalBackgroundAlgorithm, Control=LocalBackgroundControl, 

103 TransformClass=LocalBackgroundTransform, executionOrder=BasePlugin.FLUX_ORDER) 

104 

105wrapTransform(PsfFluxTransform) 

106wrapTransform(PeakLikelihoodFluxTransform) 

107wrapTransform(GaussianFluxTransform) 

108wrapTransform(NaiveCentroidTransform) 

109wrapTransform(SdssCentroidTransform) 

110wrapTransform(SdssShapeTransform) 

111wrapTransform(ScaledApertureFluxTransform) 

112wrapTransform(ApertureFluxTransform) 

113wrapTransform(LocalBackgroundTransform) 

114 

115 

116class SingleFrameFPPositionConfig(SingleFramePluginConfig): 

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

118 """ 

119 

120 pass 

121 

122 

123@register("base_FPPosition") 

124class SingleFrameFPPositionPlugin(SingleFramePlugin): 

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

126 

127 Parameters 

128 ---------- 

129 config : `SingleFrameFPPositionConfig` 

130 Plugin configuraion. 

131 name : `str` 

132 Plugin name. 

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

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

135 added to hold measurements produced by this plugin. 

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

137 Plugin metadata that will be attached to the output catalog 

138 """ 

139 

140 ConfigClass = SingleFrameFPPositionConfig 

141 

142 @classmethod 

143 def getExecutionOrder(cls): 

144 return cls.SHAPE_ORDER 

145 

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

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

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

149 "mm") 

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

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

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

153 

154 def measure(self, measRecord, exposure): 

155 det = exposure.getDetector() 

156 if not det: 

157 measRecord.set(self.detectorFlag, True) 

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

159 else: 

160 center = measRecord.getCentroid() 

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

162 measRecord.set(self.focalValue, fp) 

163 

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

165 measRecord.set(self.focalFlag, True) 

166 

167 

168class SingleFrameJacobianConfig(SingleFramePluginConfig): 

169 """Configuration for the Jacobian calculation plugin. 

170 """ 

171 

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

173 

174 

175@register("base_Jacobian") 

176class SingleFrameJacobianPlugin(SingleFramePlugin): 

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

178 

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

180 

181 Parameters 

182 ---------- 

183 config : `SingleFrameJacobianConfig` 

184 Plugin configuraion. 

185 name : `str` 

186 Plugin name. 

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

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

189 added to hold measurements produced by this plugin. 

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

191 Plugin metadata that will be attached to the output catalog 

192 """ 

193 

194 ConfigClass = SingleFrameJacobianConfig 

195 

196 @classmethod 

197 def getExecutionOrder(cls): 

198 return cls.SHAPE_ORDER 

199 

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

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

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

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

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

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

206 

207 def measure(self, measRecord, exposure): 

208 center = measRecord.getCentroid() 

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

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

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

212 center, 

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

214 measRecord.set(self.jacValue, result) 

215 

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

217 measRecord.set(self.jacFlag, True) 

218 

219 

220class VarianceConfig(BaseMeasurementPluginConfig): 

221 """Configuration for the variance calculation plugin. 

222 """ 

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

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

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

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

227 

228 

229class VariancePlugin(GenericPlugin): 

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

231 

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

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

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

235 

236 Parameters 

237 ---------- 

238 config : `VarianceConfig` 

239 Plugin configuraion. 

240 name : `str` 

241 Plugin name. 

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

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

244 added to hold measurements produced by this plugin. 

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

246 Plugin metadata that will be attached to the output catalog 

247 """ 

248 

249 ConfigClass = VarianceConfig 

250 

251 FAILURE_BAD_CENTROID = 1 

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

253 """ 

254 

255 FAILURE_EMPTY_FOOTPRINT = 2 

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

257 """ 

258 

259 @classmethod 

260 def getExecutionOrder(cls): 

261 return BasePlugin.FLUX_ORDER 

262 

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

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

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

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

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

268 

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

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

271 # that could be changed post-measurement. 

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

273 

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

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

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

277 # statistics 

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

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

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

281 aperture.scale(self.config.scale) 

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

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

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

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

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

287 maskedImage = exposure.getMaskedImage() 

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

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

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

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

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

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

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

295 if np.any(logicalMask): 

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

297 measRecord.set(self.varValue, medVar) 

298 else: 

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

300 self.FAILURE_EMPTY_FOOTPRINT) 

301 

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

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

304 # MeasurementError 

305 if isinstance(error, MeasurementError): 

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

307 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

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

309 measRecord.set(self.emptyFootprintFlag, True) 

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

311 GenericPlugin.fail(self, measRecord, error) 

312 

313 

314SingleFrameVariancePlugin = VariancePlugin.makeSingleFramePlugin("base_Variance") 

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

316""" 

317 

318ForcedVariancePlugin = VariancePlugin.makeForcedPlugin("base_Variance") 

319"""Forced version of `VariancePlugin`. 

320""" 

321 

322 

323class InputCountConfig(BaseMeasurementPluginConfig): 

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

325 """ 

326 pass 

327 

328 

329class InputCountPlugin(GenericPlugin): 

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

331 

332 Parameters 

333 ---------- 

334 config : `InputCountConfig` 

335 Plugin configuraion. 

336 name : `str` 

337 Plugin name. 

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

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

340 added to hold measurements produced by this plugin. 

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

342 Plugin metadata that will be attached to the output catalog 

343 

344 Notes 

345 ----- 

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

347 Note these limitation: 

348 

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

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

351 source. 

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

353 """ 

354 

355 ConfigClass = InputCountConfig 

356 

357 FAILURE_BAD_CENTROID = 1 

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

359 """ 

360 

361 FAILURE_NO_INPUTS = 2 

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

363 """ 

364 

365 @classmethod 

366 def getExecutionOrder(cls): 

367 return BasePlugin.SHAPE_ORDER 

368 

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

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

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

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

373 "clipping") 

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

375 doc="No coadd inputs available") 

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

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

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

379 

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

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

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

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

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

385 

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

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

388 

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

390 if error is not None: 

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

392 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

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

394 measRecord.set(self.noInputsFlag, True) 

395 GenericPlugin.fail(self, measRecord, error) 

396 

397 

398SingleFrameInputCountPlugin = InputCountPlugin.makeSingleFramePlugin("base_InputCount") 

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

400""" 

401 

402ForcedInputCountPlugin = InputCountPlugin.makeForcedPlugin("base_InputCount") 

403"""Forced version of `InputCoutPlugin`. 

404""" 

405 

406 

407class EvaluateLocalPhotoCalibPluginConfig(BaseMeasurementPluginConfig): 

408 """Configuration for the variance calculation plugin. 

409 """ 

410 pass 

411 

412 

413class EvaluateLocalPhotoCalibPlugin(GenericPlugin): 

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

415 

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

417 use in the Science Data Model functors. 

418 """ 

419 ConfigClass = EvaluateLocalPhotoCalibPluginConfig 

420 

421 @classmethod 

422 def getExecutionOrder(cls): 

423 return BasePlugin.FLUX_ORDER 

424 

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

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

427 self.photoKey = schema.addField( 

428 name, 

429 type="D", 

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

431 "the location of the src.") 

432 self.photoErrKey = schema.addField( 

433 "%sErr" % name, 

434 type="D", 

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

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

437 

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

439 

440 photoCalib = exposure.getPhotoCalib() 

441 calib = photoCalib.getLocalCalibration(center) 

442 measRecord.set(self.photoKey, calib) 

443 

444 calibErr = photoCalib.getCalibrationErr() 

445 measRecord.set(self.photoErrKey, calibErr) 

446 

447 

448SingleFrameEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeSingleFramePlugin( 

449 "base_LocalPhotoCalib") 

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

451""" 

452 

453ForcedEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeForcedPlugin( 

454 "base_LocalPhotoCalib") 

455"""Forced version of `EvaluatePhotoCalibPlugin`. 

456""" 

457 

458 

459class EvaluateLocalWcsPluginConfig(BaseMeasurementPluginConfig): 

460 """Configuration for the variance calculation plugin. 

461 """ 

462 pass 

463 

464 

465class EvaluateLocalWcsPlugin(GenericPlugin): 

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

467 

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

469 use in the Science Data Model functors. 

470 """ 

471 ConfigClass = EvaluateLocalWcsPluginConfig 

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

473 

474 @classmethod 

475 def getExecutionOrder(cls): 

476 return BasePlugin.FLUX_ORDER 

477 

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

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

480 self.cdMatrix11Key = schema.addField( 

481 f"{name}_CDMatrix_1_1", 

482 type="D", 

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

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

485 self.cdMatrix12Key = schema.addField( 

486 f"{name}_CDMatrix_1_2", 

487 type="D", 

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

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

490 self.cdMatrix21Key = schema.addField( 

491 f"{name}_CDMatrix_2_1", 

492 type="D", 

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

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

495 self.cdMatrix22Key = schema.addField( 

496 f"{name}_CDMatrix_2_2", 

497 type="D", 

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

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

500 

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

502 wcs = exposure.getWcs() 

503 localMatrix = self.makeLocalTransformMatrix(wcs, center) 

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

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

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

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

508 

509 def makeLocalTransformMatrix(self, wcs, center): 

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

511 matrix. 

512 

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

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

515 is initially calculated with units arcseconds and then converted to 

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

517 

518 Parameters 

519 ---------- 

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

521 Wcs to approximate 

522 center : `lsst.geom.Point2D` 

523 Point at which to evaluate the LocalWcs. 

524 

525 Returns 

526 ------- 

527 localMatrix : `numpy.ndarray` 

528 Matrix representation the local wcs approximation with units 

529 radians. 

530 """ 

531 skyCenter = wcs.pixelToSky(center) 

532 localGnomonicWcs = lsst.afw.geom.makeSkyWcs( 

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

534 measurementToLocalGnomonic = wcs.getTransform().then( 

535 localGnomonicWcs.getTransform().inverted() 

536 ) 

537 localMatrix = measurementToLocalGnomonic.getJacobian(center) 

538 return np.radians(localMatrix / 3600) 

539 

540 

541SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs") 

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

543""" 

544 

545ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs") 

546"""Forced version of `EvaluateLocalWcsPlugin`. 

547""" 

548 

549 

550class SingleFramePeakCentroidConfig(SingleFramePluginConfig): 

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

552 """ 

553 pass 

554 

555 

556@register("base_PeakCentroid") 

557class SingleFramePeakCentroidPlugin(SingleFramePlugin): 

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

559 

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

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

562 

563 Parameters 

564 ---------- 

565 config : `SingleFramePeakCentroidConfig` 

566 Plugin configuraion. 

567 name : `str` 

568 Plugin name. 

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

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

571 added to hold measurements produced by this plugin. 

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

573 Plugin metadata that will be attached to the output catalog 

574 """ 

575 

576 ConfigClass = SingleFramePeakCentroidConfig 

577 

578 @classmethod 

579 def getExecutionOrder(cls): 

580 return cls.CENTROID_ORDER 

581 

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

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

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

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

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

587 

588 def measure(self, measRecord, exposure): 

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

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

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

592 

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

594 measRecord.set(self.flag, True) 

595 

596 @staticmethod 

597 def getTransformClass(): 

598 return SimpleCentroidTransform 

599 

600 

601class SingleFrameSkyCoordConfig(SingleFramePluginConfig): 

602 """Configuration for the sky coordinates algorithm. 

603 """ 

604 pass 

605 

606 

607@register("base_SkyCoord") 

608class SingleFrameSkyCoordPlugin(SingleFramePlugin): 

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

610 

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

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

613 

614 Parameters 

615 ---------- 

616 config : `SingleFrameSkyCoordConfig` 

617 Plugin configuraion. 

618 name : `str` 

619 Plugin name. 

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

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

622 added to hold measurements produced by this plugin. 

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

624 Plugin metadata that will be attached to the output catalog 

625 """ 

626 

627 ConfigClass = SingleFrameSkyCoordConfig 

628 

629 @classmethod 

630 def getExecutionOrder(cls): 

631 return cls.SHAPE_ORDER 

632 

633 def measure(self, measRecord, exposure): 

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

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

636 # the appropriate type for this error 

637 if not exposure.hasWcs(): 

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

639 measRecord.updateCoord(exposure.getWcs()) 

640 

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

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

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

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

645 # DM-1011 

646 pass 

647 

648 

649class ForcedPeakCentroidConfig(ForcedPluginConfig): 

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

651 """ 

652 pass 

653 

654 

655@register("base_PeakCentroid") 

656class ForcedPeakCentroidPlugin(ForcedPlugin): 

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

658 

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

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

661 

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

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

664 coordinate system of the exposure being measured. 

665 

666 Parameters 

667 ---------- 

668 config : `ForcedPeakCentroidConfig` 

669 Plugin configuraion. 

670 name : `str` 

671 Plugin name. 

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

673 A mapping from reference catalog fields to output 

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

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

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

677 """ 

678 

679 ConfigClass = ForcedPeakCentroidConfig 

680 

681 @classmethod 

682 def getExecutionOrder(cls): 

683 return cls.CENTROID_ORDER 

684 

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

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

687 schema = schemaMapper.editOutputSchema() 

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

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

690 

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

692 targetWcs = exposure.getWcs() 

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

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

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

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

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

698 

699 @staticmethod 

700 def getTransformClass(): 

701 return SimpleCentroidTransform 

702 

703 

704class ForcedTransformedCentroidConfig(ForcedPluginConfig): 

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

706 """ 

707 pass 

708 

709 

710@register("base_TransformedCentroid") 

711class ForcedTransformedCentroidPlugin(ForcedPlugin): 

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

713 

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

715 measurement coordinate system and stored. 

716 

717 Parameters 

718 ---------- 

719 config : `ForcedTransformedCentroidConfig` 

720 Plugin configuration 

721 name : `str` 

722 Plugin name 

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

724 A mapping from reference catalog fields to output 

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

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

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

728 

729 Notes 

730 ----- 

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

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

733 they would in single-frame measurement. 

734 """ 

735 

736 ConfigClass = ForcedTransformedCentroidConfig 

737 

738 @classmethod 

739 def getExecutionOrder(cls): 

740 return cls.CENTROID_ORDER 

741 

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

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

744 schema = schemaMapper.editOutputSchema() 

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

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

747 units="pixel") 

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

749 units="pixel") 

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

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

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

753 # the flag field, if it exists. 

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

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

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

757 else: 

758 self.flagKey = None 

759 

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

761 targetWcs = exposure.getWcs() 

762 if not refWcs == targetWcs: 

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

764 measRecord.set(self.centroidKey, targetPos) 

765 else: 

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

767 if self.flagKey is not None: 

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

769 

770 

771class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig): 

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

773 """ 

774 pass 

775 

776 

777@register("base_TransformedCentroidFromCoord") 

778class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin): 

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

780 

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

782 measurement coordinate system and stored. 

783 

784 Parameters 

785 ---------- 

786 config : `ForcedTransformedCentroidFromCoordConfig` 

787 Plugin configuration 

788 name : `str` 

789 Plugin name 

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

791 A mapping from reference catalog fields to output 

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

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

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

795 

796 Notes 

797 ----- 

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

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

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

801 """ 

802 

803 ConfigClass = ForcedTransformedCentroidFromCoordConfig 

804 

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

806 targetWcs = exposure.getWcs() 

807 

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

809 measRecord.set(self.centroidKey, targetPos) 

810 

811 if self.flagKey is not None: 

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

813 

814 

815class ForcedTransformedShapeConfig(ForcedPluginConfig): 

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

817 """ 

818 pass 

819 

820 

821@register("base_TransformedShape") 

822class ForcedTransformedShapePlugin(ForcedPlugin): 

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

824 

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

826 measurement coordinate system and stored. 

827 

828 Parameters 

829 ---------- 

830 config : `ForcedTransformedShapeConfig` 

831 Plugin configuration 

832 name : `str` 

833 Plugin name 

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

835 A mapping from reference catalog fields to output 

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

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

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

839 

840 Notes 

841 ----- 

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

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

844 would in single-frame measurement. 

845 """ 

846 

847 ConfigClass = ForcedTransformedShapeConfig 

848 

849 @classmethod 

850 def getExecutionOrder(cls): 

851 return cls.SHAPE_ORDER 

852 

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

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

855 schema = schemaMapper.editOutputSchema() 

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

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

858 units="pixel^2") 

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

860 units="pixel^2") 

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

862 units="pixel^2") 

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

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

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

866 # the flag field, if it exists. 

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

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

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

870 else: 

871 self.flagKey = None 

872 

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

874 targetWcs = exposure.getWcs() 

875 if not refWcs == targetWcs: 

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

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

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

879 else: 

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

881 if self.flagKey is not None: 

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