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

347 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-16 10:43 +0000

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, 

43 # Remove these three on DM-41701 

44 NaiveCentroidAlgorithm, NaiveCentroidControl, NaiveCentroidTransform, 

45 PeakLikelihoodFluxAlgorithm, 

46 PeakLikelihoodFluxControl, 

47 PeakLikelihoodFluxTransform, PixelFlagsAlgorithm, 

48 PixelFlagsControl, PsfFluxAlgorithm, PsfFluxControl, 

49 PsfFluxTransform, ScaledApertureFluxAlgorithm, 

50 ScaledApertureFluxControl, 

51 ScaledApertureFluxTransform, SdssCentroidAlgorithm, 

52 SdssCentroidControl, SdssCentroidTransform, 

53 SdssShapeAlgorithm, SdssShapeControl, 

54 SdssShapeTransform) 

55 

56from .baseMeasurement import BaseMeasurementPluginConfig 

57from .forcedMeasurement import ForcedPlugin, ForcedPluginConfig 

58from .pluginRegistry import register 

59from .pluginsBase import BasePlugin 

60from .sfm import SingleFramePlugin, SingleFramePluginConfig 

61from .transforms import SimpleCentroidTransform 

62from .wrappers import GenericPlugin, wrapSimpleAlgorithm, wrapTransform 

63 

64__all__ = ( 

65 "SingleFrameFPPositionConfig", "SingleFrameFPPositionPlugin", 

66 "SingleFrameJacobianConfig", "SingleFrameJacobianPlugin", 

67 "VarianceConfig", "SingleFrameVariancePlugin", "ForcedVariancePlugin", 

68 "InputCountConfig", "SingleFrameInputCountPlugin", "ForcedInputCountPlugin", 

69 "SingleFramePeakCentroidConfig", "SingleFramePeakCentroidPlugin", 

70 "SingleFrameSkyCoordConfig", "SingleFrameSkyCoordPlugin", 

71 "SingleFrameMomentsClassifierConfig", "SingleFrameMomentsClassifierPlugin", 

72 "ForcedPeakCentroidConfig", "ForcedPeakCentroidPlugin", 

73 "ForcedTransformedCentroidConfig", "ForcedTransformedCentroidPlugin", 

74 "ForcedTransformedCentroidFromCoordConfig", 

75 "ForcedTransformedCentroidFromCoordPlugin", 

76 "ForcedTransformedShapeConfig", "ForcedTransformedShapePlugin", 

77 "EvaluateLocalPhotoCalibPlugin", "EvaluateLocalPhotoCalibPluginConfig", 

78 "EvaluateLocalWcsPlugin", "EvaluateLocalWcsPluginConfig", 

79) 

80 

81 

82wrapSimpleAlgorithm(PsfFluxAlgorithm, Control=PsfFluxControl, 

83 TransformClass=PsfFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

84 shouldApCorr=True, hasLogName=True) 

85wrapSimpleAlgorithm(PeakLikelihoodFluxAlgorithm, Control=PeakLikelihoodFluxControl, 

86 TransformClass=PeakLikelihoodFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

87wrapSimpleAlgorithm(GaussianFluxAlgorithm, Control=GaussianFluxControl, 

88 TransformClass=GaussianFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

89 shouldApCorr=True) 

90# Remove this line on DM-41701 

91wrapSimpleAlgorithm(NaiveCentroidAlgorithm, Control=NaiveCentroidControl, 

92 TransformClass=NaiveCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER, 

93 deprecated="Plugin 'NaiveCentroid' is deprecated and will be removed after v27.") 

94wrapSimpleAlgorithm(SdssCentroidAlgorithm, Control=SdssCentroidControl, 

95 TransformClass=SdssCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

96wrapSimpleAlgorithm(PixelFlagsAlgorithm, Control=PixelFlagsControl, 

97 executionOrder=BasePlugin.FLUX_ORDER) 

98wrapSimpleAlgorithm(SdssShapeAlgorithm, Control=SdssShapeControl, 

99 TransformClass=SdssShapeTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

100wrapSimpleAlgorithm(ScaledApertureFluxAlgorithm, Control=ScaledApertureFluxControl, 

101 TransformClass=ScaledApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

102 

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

104 TransformClass=ApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

105wrapSimpleAlgorithm(BlendednessAlgorithm, Control=BlendednessControl, 

106 TransformClass=BaseTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

107 

108wrapSimpleAlgorithm(LocalBackgroundAlgorithm, Control=LocalBackgroundControl, 

109 TransformClass=LocalBackgroundTransform, executionOrder=BasePlugin.FLUX_ORDER) 

110 

111wrapTransform(PsfFluxTransform) 

112wrapTransform(PeakLikelihoodFluxTransform) 

113wrapTransform(GaussianFluxTransform) 

114# Remove this on DM-41701 

115wrapTransform(NaiveCentroidTransform) 

116wrapTransform(SdssCentroidTransform) 

117wrapTransform(SdssShapeTransform) 

118wrapTransform(ScaledApertureFluxTransform) 

119wrapTransform(ApertureFluxTransform) 

120wrapTransform(LocalBackgroundTransform) 

121 

122 

123class SingleFrameFPPositionConfig(SingleFramePluginConfig): 

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

125 """ 

126 

127 

128@register("base_FPPosition") 

129class SingleFrameFPPositionPlugin(SingleFramePlugin): 

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

131 

132 Parameters 

133 ---------- 

134 config : `SingleFrameFPPositionConfig` 

135 Plugin configuraion. 

136 name : `str` 

137 Plugin name. 

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

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

140 added to hold measurements produced by this plugin. 

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

142 Plugin metadata that will be attached to the output catalog 

143 """ 

144 

145 ConfigClass = SingleFrameFPPositionConfig 

146 

147 @classmethod 

148 def getExecutionOrder(cls): 

149 return cls.SHAPE_ORDER 

150 

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

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

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

154 "mm") 

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

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

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

158 

159 def measure(self, measRecord, exposure): 

160 det = exposure.getDetector() 

161 if not det: 

162 measRecord.set(self.detectorFlag, True) 

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

164 else: 

165 center = measRecord.getCentroid() 

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

167 measRecord.set(self.focalValue, fp) 

168 

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

170 measRecord.set(self.focalFlag, True) 

171 

172 

173class SingleFrameJacobianConfig(SingleFramePluginConfig): 

174 """Configuration for the Jacobian calculation plugin. 

175 """ 

176 

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

178 

179 

180@register("base_Jacobian") 

181class SingleFrameJacobianPlugin(SingleFramePlugin): 

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

183 

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

185 

186 Parameters 

187 ---------- 

188 config : `SingleFrameJacobianConfig` 

189 Plugin configuraion. 

190 name : `str` 

191 Plugin name. 

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

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

194 added to hold measurements produced by this plugin. 

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

196 Plugin metadata that will be attached to the output catalog 

197 """ 

198 

199 ConfigClass = SingleFrameJacobianConfig 

200 

201 @classmethod 

202 def getExecutionOrder(cls): 

203 return cls.SHAPE_ORDER 

204 

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

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

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

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

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

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

211 

212 def measure(self, measRecord, exposure): 

213 center = measRecord.getCentroid() 

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

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

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

217 center, 

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

219 measRecord.set(self.jacValue, result) 

220 

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

222 measRecord.set(self.jacFlag, True) 

223 

224 

225class VarianceConfig(BaseMeasurementPluginConfig): 

226 """Configuration for the variance calculation plugin. 

227 """ 

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

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

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

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

232 

233 

234class VariancePlugin(GenericPlugin): 

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

236 

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

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

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

240 

241 Parameters 

242 ---------- 

243 config : `VarianceConfig` 

244 Plugin configuraion. 

245 name : `str` 

246 Plugin name. 

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

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

249 added to hold measurements produced by this plugin. 

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

251 Plugin metadata that will be attached to the output catalog 

252 """ 

253 

254 ConfigClass = VarianceConfig 

255 

256 FAILURE_BAD_CENTROID = 1 

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

258 """ 

259 

260 FAILURE_EMPTY_FOOTPRINT = 2 

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

262 """ 

263 

264 @classmethod 

265 def getExecutionOrder(cls): 

266 return BasePlugin.FLUX_ORDER 

267 

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

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

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

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

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

273 

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

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

276 # that could be changed post-measurement. 

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

278 

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

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

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

282 # statistics 

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

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

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

286 aperture.scale(self.config.scale) 

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

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

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

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

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

292 maskedImage = exposure.getMaskedImage() 

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

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

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

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

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

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

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

300 if np.any(logicalMask): 

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

302 measRecord.set(self.varValue, medVar) 

303 else: 

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

305 self.FAILURE_EMPTY_FOOTPRINT) 

306 

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

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

309 # MeasurementError 

310 if isinstance(error, MeasurementError): 

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

312 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

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

314 measRecord.set(self.emptyFootprintFlag, True) 

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

316 GenericPlugin.fail(self, measRecord, error) 

317 

318 

319SingleFrameVariancePlugin = VariancePlugin.makeSingleFramePlugin("base_Variance") 

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

321""" 

322 

323ForcedVariancePlugin = VariancePlugin.makeForcedPlugin("base_Variance") 

324"""Forced version of `VariancePlugin`. 

325""" 

326 

327 

328class InputCountConfig(BaseMeasurementPluginConfig): 

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

330 """ 

331 

332 

333class InputCountPlugin(GenericPlugin): 

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

335 

336 Parameters 

337 ---------- 

338 config : `InputCountConfig` 

339 Plugin configuration. 

340 name : `str` 

341 Plugin name. 

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

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

344 added to hold measurements produced by this plugin. 

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

346 Plugin metadata that will be attached to the output catalog 

347 

348 Notes 

349 ----- 

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

351 Note these limitation: 

352 

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

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

355 source. 

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

357 """ 

358 

359 ConfigClass = InputCountConfig 

360 

361 FAILURE_BAD_CENTROID = 1 

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

363 """ 

364 

365 FAILURE_NO_INPUTS = 2 

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

367 """ 

368 

369 @classmethod 

370 def getExecutionOrder(cls): 

371 return BasePlugin.SHAPE_ORDER 

372 

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

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

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

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

377 "clipping") 

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

379 doc="No coadd inputs available") 

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

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

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

383 

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

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

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

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

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

389 

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

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

392 

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

394 if error is not None: 

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

396 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

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

398 measRecord.set(self.noInputsFlag, True) 

399 GenericPlugin.fail(self, measRecord, error) 

400 

401 

402SingleFrameInputCountPlugin = InputCountPlugin.makeSingleFramePlugin("base_InputCount") 

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

404""" 

405 

406ForcedInputCountPlugin = InputCountPlugin.makeForcedPlugin("base_InputCount") 

407"""Forced version of `InputCoutPlugin`. 

408""" 

409 

410 

411class EvaluateLocalPhotoCalibPluginConfig(BaseMeasurementPluginConfig): 

412 """Configuration for the variance calculation plugin. 

413 """ 

414 

415 

416class EvaluateLocalPhotoCalibPlugin(GenericPlugin): 

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

418 

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

420 use in the Science Data Model functors. 

421 """ 

422 ConfigClass = EvaluateLocalPhotoCalibPluginConfig 

423 

424 @classmethod 

425 def getExecutionOrder(cls): 

426 return BasePlugin.FLUX_ORDER 

427 

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

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

430 self.photoKey = schema.addField( 

431 name, 

432 type="D", 

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

434 "the location of the src.") 

435 self.photoErrKey = schema.addField( 

436 "%sErr" % name, 

437 type="D", 

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

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

440 

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

442 

443 photoCalib = exposure.getPhotoCalib() 

444 calib = photoCalib.getLocalCalibration(center) 

445 measRecord.set(self.photoKey, calib) 

446 

447 calibErr = photoCalib.getCalibrationErr() 

448 measRecord.set(self.photoErrKey, calibErr) 

449 

450 

451SingleFrameEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeSingleFramePlugin( 

452 "base_LocalPhotoCalib") 

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

454""" 

455 

456ForcedEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeForcedPlugin( 

457 "base_LocalPhotoCalib") 

458"""Forced version of `EvaluatePhotoCalibPlugin`. 

459""" 

460 

461 

462class EvaluateLocalWcsPluginConfig(BaseMeasurementPluginConfig): 

463 """Configuration for the variance calculation plugin. 

464 """ 

465 

466 

467class EvaluateLocalWcsPlugin(GenericPlugin): 

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

469 

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

471 use in the Science Data Model functors. 

472 """ 

473 ConfigClass = EvaluateLocalWcsPluginConfig 

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

475 

476 @classmethod 

477 def getExecutionOrder(cls): 

478 return BasePlugin.FLUX_ORDER 

479 

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

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

482 self.cdMatrix11Key = schema.addField( 

483 f"{name}_CDMatrix_1_1", 

484 type="D", 

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

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

487 self.cdMatrix12Key = schema.addField( 

488 f"{name}_CDMatrix_1_2", 

489 type="D", 

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

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

492 self.cdMatrix21Key = schema.addField( 

493 f"{name}_CDMatrix_2_1", 

494 type="D", 

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

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

497 self.cdMatrix22Key = schema.addField( 

498 f"{name}_CDMatrix_2_2", 

499 type="D", 

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

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

502 

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

504 wcs = exposure.getWcs() 

505 localMatrix = self.makeLocalTransformMatrix(wcs, center) 

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

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

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

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

510 

511 def makeLocalTransformMatrix(self, wcs, center): 

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

513 matrix. 

514 

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

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

517 is initially calculated with units arcseconds and then converted to 

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

519 

520 Parameters 

521 ---------- 

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

523 Wcs to approximate 

524 center : `lsst.geom.Point2D` 

525 Point at which to evaluate the LocalWcs. 

526 

527 Returns 

528 ------- 

529 localMatrix : `numpy.ndarray` 

530 Matrix representation the local wcs approximation with units 

531 radians. 

532 """ 

533 skyCenter = wcs.pixelToSky(center) 

534 localGnomonicWcs = lsst.afw.geom.makeSkyWcs( 

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

536 measurementToLocalGnomonic = wcs.getTransform().then( 

537 localGnomonicWcs.getTransform().inverted() 

538 ) 

539 localMatrix = measurementToLocalGnomonic.getJacobian(center) 

540 return np.radians(localMatrix / 3600) 

541 

542 

543SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs") 

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

545""" 

546 

547ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs") 

548"""Forced version of `EvaluateLocalWcsPlugin`. 

549""" 

550 

551 

552class SingleFramePeakCentroidConfig(SingleFramePluginConfig): 

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

554 """ 

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 

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 SingleFrameMomentsClassifierConfig(SingleFramePluginConfig): 

650 """Configuration for moments-based star-galaxy classifier.""" 

651 

652 exponent = lsst.pex.config.Field[float]( 

653 doc="Exponent to raise the PSF size squared (Ixx + Iyy) to, " 

654 "in the likelihood normalization", 

655 default=0.5, 

656 ) 

657 

658 

659@register("base_ClassificationSizeExtendedness") 

660class SingleFrameMomentsClassifierPlugin(SingleFramePlugin): 

661 """Classify objects by comparing their moments-based trace radius to PSF's. 

662 

663 The plugin computes chi^2 as ((T_obj - T_psf)/T_psf^exponent)^2, where 

664 T_obj is the sum of Ixx and Iyy moments of the object, and T_psf is the 

665 sum of Ixx and Iyy moments of the PSF. The exponent is configurable. 

666 The measure of being a galaxy is then 1 - exp(-0.5*chi^2). 

667 

668 Parameters 

669 ---------- 

670 config : `MomentsClassifierConfig` 

671 Plugin configuration. 

672 name : `str` 

673 Plugin name. 

674 schema : `~lsst.afw.table.Schema` 

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

676 added to hold measurements produced by this plugin. 

677 metadata : `~lsst.daf.base.PropertySet` 

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

679 

680 Notes 

681 ----- 

682 The ``measure`` method of the plugin requires a value for the ``exposure`` 

683 argument to maintain consistent API, but it is not used in the measurement. 

684 """ 

685 

686 ConfigClass = SingleFrameMomentsClassifierConfig 

687 

688 @classmethod 

689 def getExecutionOrder(cls): 

690 return cls.FLUX_ORDER 

691 

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

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

694 self.key = schema.addField(name + "_value", 

695 type="D", 

696 doc="Measure of being a galaxy based on trace of second order moments", 

697 ) 

698 self.flag = schema.addField(name + "_flag", type="Flag", doc="Moments-based classification failed") 

699 

700 def measure(self, measRecord, exposure) -> None: 

701 # Docstring inherited. 

702 

703 if measRecord.getShapeFlag(): 

704 raise MeasurementError("Shape flag is set. Required for " + self.name + " algorithm") 

705 

706 shape = measRecord.getShape() 

707 psf_shape = measRecord.getPsfShape() 

708 

709 ixx = shape.getIxx() 

710 iyy = shape.getIyy() 

711 ixx_psf = psf_shape.getIxx() 

712 iyy_psf = psf_shape.getIyy() 

713 

714 object_t = ixx + iyy 

715 psf_t = ixx_psf + iyy_psf 

716 

717 chi_sq = ((object_t - psf_t)/(psf_t**self.config.exponent))**2. 

718 likelihood = 1. - np.exp(-0.5*chi_sq) 

719 measRecord.set(self.key, likelihood) 

720 

721 def fail(self, measRecord, error=None) -> None: 

722 # Docstring inherited. 

723 measRecord.set(self.key, np.nan) 

724 measRecord.set(self.flag, True) 

725 

726 

727class ForcedPeakCentroidConfig(ForcedPluginConfig): 

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

729 """ 

730 

731 

732@register("base_PeakCentroid") 

733class ForcedPeakCentroidPlugin(ForcedPlugin): 

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

735 

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

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

738 

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

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

741 coordinate system of the exposure being measured. 

742 

743 Parameters 

744 ---------- 

745 config : `ForcedPeakCentroidConfig` 

746 Plugin configuraion. 

747 name : `str` 

748 Plugin name. 

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

750 A mapping from reference catalog fields to output 

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

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

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

754 """ 

755 

756 ConfigClass = ForcedPeakCentroidConfig 

757 

758 @classmethod 

759 def getExecutionOrder(cls): 

760 return cls.CENTROID_ORDER 

761 

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

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

764 schema = schemaMapper.editOutputSchema() 

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

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

767 

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

769 targetWcs = exposure.getWcs() 

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

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

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

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

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

775 

776 @staticmethod 

777 def getTransformClass(): 

778 return SimpleCentroidTransform 

779 

780 

781class ForcedTransformedCentroidConfig(ForcedPluginConfig): 

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

783 """ 

784 

785 

786@register("base_TransformedCentroid") 

787class ForcedTransformedCentroidPlugin(ForcedPlugin): 

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

789 

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

791 measurement coordinate system and stored. 

792 

793 Parameters 

794 ---------- 

795 config : `ForcedTransformedCentroidConfig` 

796 Plugin configuration 

797 name : `str` 

798 Plugin name 

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

800 A mapping from reference catalog fields to output 

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

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

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

804 

805 Notes 

806 ----- 

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

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

809 they would in single-frame measurement. 

810 """ 

811 

812 ConfigClass = ForcedTransformedCentroidConfig 

813 

814 @classmethod 

815 def getExecutionOrder(cls): 

816 return cls.CENTROID_ORDER 

817 

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

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

820 schema = schemaMapper.editOutputSchema() 

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

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

823 units="pixel") 

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

825 units="pixel") 

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

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

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

829 # the flag field, if it exists. 

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

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

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

833 else: 

834 self.flagKey = None 

835 

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

837 targetWcs = exposure.getWcs() 

838 if not refWcs == targetWcs: 

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

840 measRecord.set(self.centroidKey, targetPos) 

841 else: 

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

843 if self.flagKey is not None: 

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

845 

846 

847class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig): 

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

849 """ 

850 

851 

852@register("base_TransformedCentroidFromCoord") 

853class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin): 

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

855 

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

857 measurement coordinate system and stored. 

858 

859 Parameters 

860 ---------- 

861 config : `ForcedTransformedCentroidFromCoordConfig` 

862 Plugin configuration 

863 name : `str` 

864 Plugin name 

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

866 A mapping from reference catalog fields to output 

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

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

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

870 

871 Notes 

872 ----- 

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

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

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

876 """ 

877 

878 ConfigClass = ForcedTransformedCentroidFromCoordConfig 

879 

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

881 targetWcs = exposure.getWcs() 

882 

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

884 measRecord.set(self.centroidKey, targetPos) 

885 

886 if self.flagKey is not None: 

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

888 

889 

890class ForcedTransformedShapeConfig(ForcedPluginConfig): 

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

892 """ 

893 

894 

895@register("base_TransformedShape") 

896class ForcedTransformedShapePlugin(ForcedPlugin): 

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

898 

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

900 measurement coordinate system and stored. 

901 

902 Parameters 

903 ---------- 

904 config : `ForcedTransformedShapeConfig` 

905 Plugin configuration 

906 name : `str` 

907 Plugin name 

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

909 A mapping from reference catalog fields to output 

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

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

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

913 

914 Notes 

915 ----- 

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

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

918 would in single-frame measurement. 

919 """ 

920 

921 ConfigClass = ForcedTransformedShapeConfig 

922 

923 @classmethod 

924 def getExecutionOrder(cls): 

925 return cls.SHAPE_ORDER 

926 

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

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

929 schema = schemaMapper.editOutputSchema() 

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

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

932 units="pixel^2") 

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

934 units="pixel^2") 

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

936 units="pixel^2") 

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

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

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

940 # the flag field, if it exists. 

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

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

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

944 else: 

945 self.flagKey = None 

946 

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

948 targetWcs = exposure.getWcs() 

949 if not refWcs == targetWcs: 

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

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

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

953 else: 

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

955 if self.flagKey is not None: 

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