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

368 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:53 +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 logging 

30import numpy as np 

31 

32import lsst.pex.exceptions 

33import lsst.geom 

34import lsst.afw.detection 

35import lsst.afw.geom 

36 

37from deprecated.sphinx import deprecated 

38from ._measBaseLib import (ApertureFluxControl, ApertureFluxTransform, 

39 BaseTransform, BlendednessAlgorithm, 

40 BlendednessControl, CircularApertureFluxAlgorithm, 

41 GaussianFluxAlgorithm, GaussianFluxControl, 

42 GaussianFluxTransform, LocalBackgroundAlgorithm, 

43 LocalBackgroundControl, LocalBackgroundTransform, 

44 MeasurementError, 

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", # TODO: Remove in DM-47494. 

72 "SingleFrameClassificationSizeExtendednessConfig", 

73 "SingleFrameClassificationSizeExtendednessPlugin", 

74 "ForcedPeakCentroidConfig", "ForcedPeakCentroidPlugin", 

75 "ForcedTransformedCentroidConfig", "ForcedTransformedCentroidPlugin", 

76 "ForcedTransformedCentroidFromCoordConfig", 

77 "ForcedTransformedCentroidFromCoordPlugin", 

78 "ForcedTransformedShapeConfig", "ForcedTransformedShapePlugin", 

79 "EvaluateLocalPhotoCalibPlugin", "EvaluateLocalPhotoCalibPluginConfig", 

80 "EvaluateLocalWcsPlugin", "EvaluateLocalWcsPluginConfig", 

81) 

82 

83 

84wrapSimpleAlgorithm(PsfFluxAlgorithm, Control=PsfFluxControl, 

85 TransformClass=PsfFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

86 shouldApCorr=True, hasLogName=True) 

87wrapSimpleAlgorithm(PeakLikelihoodFluxAlgorithm, Control=PeakLikelihoodFluxControl, 

88 TransformClass=PeakLikelihoodFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

89wrapSimpleAlgorithm(GaussianFluxAlgorithm, Control=GaussianFluxControl, 

90 TransformClass=GaussianFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

91 shouldApCorr=True) 

92wrapSimpleAlgorithm(SdssCentroidAlgorithm, Control=SdssCentroidControl, 

93 TransformClass=SdssCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

94wrapSimpleAlgorithm(PixelFlagsAlgorithm, Control=PixelFlagsControl, 

95 executionOrder=BasePlugin.FLUX_ORDER) 

96wrapSimpleAlgorithm(SdssShapeAlgorithm, Control=SdssShapeControl, 

97 TransformClass=SdssShapeTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

98wrapSimpleAlgorithm(ScaledApertureFluxAlgorithm, Control=ScaledApertureFluxControl, 

99 TransformClass=ScaledApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

100 

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

102 TransformClass=ApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

103wrapSimpleAlgorithm(BlendednessAlgorithm, Control=BlendednessControl, 

104 TransformClass=BaseTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

105 

106wrapSimpleAlgorithm(LocalBackgroundAlgorithm, Control=LocalBackgroundControl, 

107 TransformClass=LocalBackgroundTransform, executionOrder=BasePlugin.FLUX_ORDER) 

108 

109wrapTransform(PsfFluxTransform) 

110wrapTransform(PeakLikelihoodFluxTransform) 

111wrapTransform(GaussianFluxTransform) 

112wrapTransform(SdssCentroidTransform) 

113wrapTransform(SdssShapeTransform) 

114wrapTransform(ScaledApertureFluxTransform) 

115wrapTransform(ApertureFluxTransform) 

116wrapTransform(LocalBackgroundTransform) 

117 

118log = logging.getLogger(__name__) 

119 

120 

121class SingleFrameFPPositionConfig(SingleFramePluginConfig): 

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

123 """ 

124 

125 

126@register("base_FPPosition") 

127class SingleFrameFPPositionPlugin(SingleFramePlugin): 

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

129 

130 Parameters 

131 ---------- 

132 config : `SingleFrameFPPositionConfig` 

133 Plugin configuraion. 

134 name : `str` 

135 Plugin name. 

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

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

138 added to hold measurements produced by this plugin. 

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

140 Plugin metadata that will be attached to the output catalog 

141 """ 

142 

143 ConfigClass = SingleFrameFPPositionConfig 

144 

145 @classmethod 

146 def getExecutionOrder(cls): 

147 return cls.SHAPE_ORDER 

148 

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

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

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

152 "mm") 

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

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

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

156 

157 def measure(self, measRecord, exposure): 

158 det = exposure.getDetector() 

159 if not det: 

160 measRecord.set(self.detectorFlag, True) 

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

162 else: 

163 center = measRecord.getCentroid() 

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

165 measRecord.set(self.focalValue, fp) 

166 

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

168 measRecord.set(self.focalFlag, True) 

169 

170 

171class SingleFrameJacobianConfig(SingleFramePluginConfig): 

172 """Configuration for the Jacobian calculation plugin. 

173 """ 

174 

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

176 

177 

178@register("base_Jacobian") 

179class SingleFrameJacobianPlugin(SingleFramePlugin): 

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

181 

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

183 

184 Parameters 

185 ---------- 

186 config : `SingleFrameJacobianConfig` 

187 Plugin configuraion. 

188 name : `str` 

189 Plugin name. 

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

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

192 added to hold measurements produced by this plugin. 

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

194 Plugin metadata that will be attached to the output catalog 

195 """ 

196 

197 ConfigClass = SingleFrameJacobianConfig 

198 

199 @classmethod 

200 def getExecutionOrder(cls): 

201 return cls.SHAPE_ORDER 

202 

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

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

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

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

207 # Calculate one over the area of a nominal reference pixel, where area 

208 # is in arcsec^2. 

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

210 

211 def measure(self, measRecord, exposure): 

212 center = measRecord.getCentroid() 

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

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

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

216 center, 

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

218 measRecord.set(self.jacValue, result) 

219 

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

221 measRecord.set(self.jacFlag, True) 

222 

223 

224class VarianceConfig(BaseMeasurementPluginConfig): 

225 """Configuration for the variance calculation plugin. 

226 """ 

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

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

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

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

231 

232 

233class VariancePlugin(GenericPlugin): 

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

235 

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

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

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

239 

240 Parameters 

241 ---------- 

242 config : `VarianceConfig` 

243 Plugin configuraion. 

244 name : `str` 

245 Plugin name. 

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

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

248 added to hold measurements produced by this plugin. 

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

250 Plugin metadata that will be attached to the output catalog 

251 """ 

252 

253 ConfigClass = VarianceConfig 

254 

255 FAILURE_BAD_CENTROID = 1 

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

257 """ 

258 

259 FAILURE_EMPTY_FOOTPRINT = 2 

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

261 """ 

262 

263 @classmethod 

264 def getExecutionOrder(cls): 

265 return BasePlugin.FLUX_ORDER 

266 

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

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

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

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

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

272 

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

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

275 # that could be changed post-measurement. 

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

277 

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

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

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

281 # statistics 

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

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

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

285 aperture.scale(self.config.scale) 

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

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

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

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

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

291 maskedImage = exposure.getMaskedImage() 

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

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

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

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

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

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

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

299 if np.any(logicalMask): 

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

301 measRecord.set(self.varValue, medVar) 

302 else: 

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

304 self.FAILURE_EMPTY_FOOTPRINT) 

305 

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

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

308 # MeasurementError 

309 if isinstance(error, MeasurementError): 

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

311 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

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

313 measRecord.set(self.emptyFootprintFlag, True) 

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

315 GenericPlugin.fail(self, measRecord, error) 

316 

317 

318SingleFrameVariancePlugin = VariancePlugin.makeSingleFramePlugin("base_Variance") 

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

320""" 

321 

322ForcedVariancePlugin = VariancePlugin.makeForcedPlugin("base_Variance") 

323"""Forced version of `VariancePlugin`. 

324""" 

325 

326 

327class InputCountConfig(BaseMeasurementPluginConfig): 

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

329 """ 

330 

331 

332class InputCountPlugin(GenericPlugin): 

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

334 

335 Parameters 

336 ---------- 

337 config : `InputCountConfig` 

338 Plugin configuration. 

339 name : `str` 

340 Plugin name. 

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

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

343 added to hold measurements produced by this plugin. 

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

345 Plugin metadata that will be attached to the output catalog 

346 

347 Notes 

348 ----- 

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

350 Note these limitation: 

351 

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

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

354 source. 

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

356 """ 

357 

358 ConfigClass = InputCountConfig 

359 

360 FAILURE_BAD_CENTROID = 1 

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

362 """ 

363 

364 FAILURE_NO_INPUTS = 2 

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

366 """ 

367 

368 @classmethod 

369 def getExecutionOrder(cls): 

370 return BasePlugin.SHAPE_ORDER 

371 

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

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

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

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

376 "clipping") 

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

378 doc="No coadd inputs available") 

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

380 # the centroid slot. We do not simply rely on the alias because that 

381 # 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 (coaddInputs := 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 count = len(coaddInputs.subset_containing_ccds(center, exposure.wcs)) 

391 measRecord.set(self.numberKey, count) 

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 photoCalib = exposure.getPhotoCalib() 

443 if photoCalib is None: 

444 log.debug( 

445 "%s: photoCalib is None. Setting localPhotoCalib to NaN for record %d", 

446 self.name, 

447 measRecord.getId(), 

448 ) 

449 calib = np.nan 

450 calibErr = np.nan 

451 measRecord.set(self._failKey, True) 

452 else: 

453 calib = photoCalib.getLocalCalibration(center) 

454 calibErr = photoCalib.getCalibrationErr() 

455 measRecord.set(self.photoKey, calib) 

456 measRecord.set(self.photoErrKey, calibErr) 

457 

458 

459SingleFrameEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeSingleFramePlugin( 

460 "base_LocalPhotoCalib") 

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

462""" 

463 

464ForcedEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeForcedPlugin( 

465 "base_LocalPhotoCalib") 

466"""Forced version of `EvaluatePhotoCalibPlugin`. 

467""" 

468 

469 

470class EvaluateLocalWcsPluginConfig(BaseMeasurementPluginConfig): 

471 """Configuration for the variance calculation plugin. 

472 """ 

473 

474 

475class EvaluateLocalWcsPlugin(GenericPlugin): 

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

477 

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

479 use in the Science Data Model functors. 

480 """ 

481 ConfigClass = EvaluateLocalWcsPluginConfig 

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

483 

484 @classmethod 

485 def getExecutionOrder(cls): 

486 return BasePlugin.FLUX_ORDER 

487 

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

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

490 self.cdMatrix11Key = schema.addField( 

491 f"{name}_CDMatrix_1_1", 

492 type="D", 

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

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

495 self.cdMatrix12Key = schema.addField( 

496 f"{name}_CDMatrix_1_2", 

497 type="D", 

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

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

500 self.cdMatrix21Key = schema.addField( 

501 f"{name}_CDMatrix_2_1", 

502 type="D", 

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

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

505 self.cdMatrix22Key = schema.addField( 

506 f"{name}_CDMatrix_2_2", 

507 type="D", 

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

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

510 

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

512 wcs = exposure.getWcs() 

513 if wcs is None: 

514 log.debug( 

515 "%s: WCS is None. Setting localWcs matrix values to NaN for record %d", 

516 self.name, 

517 measRecord.getId(), 

518 ) 

519 localMatrix = np.array([[np.nan, np.nan], [np.nan, np.nan]]) 

520 measRecord.set(self._failKey, True) 

521 else: 

522 localMatrix = self.makeLocalTransformMatrix(wcs, center) 

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

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

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

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

527 

528 def makeLocalTransformMatrix(self, wcs, center): 

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

530 matrix. 

531 

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

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

534 is initially calculated with units arcseconds and then converted to 

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

536 

537 Parameters 

538 ---------- 

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

540 Wcs to approximate 

541 center : `lsst.geom.Point2D` 

542 Point at which to evaluate the LocalWcs. 

543 

544 Returns 

545 ------- 

546 localMatrix : `numpy.ndarray` 

547 Matrix representation the local wcs approximation with units 

548 radians. 

549 """ 

550 skyCenter = wcs.pixelToSky(center) 

551 localGnomonicWcs = lsst.afw.geom.makeSkyWcs( 

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

553 measurementToLocalGnomonic = wcs.getTransform().then( 

554 localGnomonicWcs.getTransform().inverted() 

555 ) 

556 localMatrix = measurementToLocalGnomonic.getJacobian(center) 

557 return np.radians(localMatrix / 3600) 

558 

559 

560SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs") 

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

562""" 

563 

564ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs") 

565"""Forced version of `EvaluateLocalWcsPlugin`. 

566""" 

567 

568 

569class SingleFramePeakCentroidConfig(SingleFramePluginConfig): 

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

571 """ 

572 

573 

574@register("base_PeakCentroid") 

575class SingleFramePeakCentroidPlugin(SingleFramePlugin): 

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

577 

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

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

580 

581 Parameters 

582 ---------- 

583 config : `SingleFramePeakCentroidConfig` 

584 Plugin configuraion. 

585 name : `str` 

586 Plugin name. 

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

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

589 added to hold measurements produced by this plugin. 

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

591 Plugin metadata that will be attached to the output catalog 

592 """ 

593 

594 ConfigClass = SingleFramePeakCentroidConfig 

595 

596 @classmethod 

597 def getExecutionOrder(cls): 

598 return cls.CENTROID_ORDER 

599 

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

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

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

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

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

605 

606 def measure(self, measRecord, exposure): 

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

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

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

610 

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

612 measRecord.set(self.flag, True) 

613 

614 @staticmethod 

615 def getTransformClass(): 

616 return SimpleCentroidTransform 

617 

618 

619class SingleFrameSkyCoordConfig(SingleFramePluginConfig): 

620 """Configuration for the sky coordinates algorithm. 

621 """ 

622 

623 

624@register("base_SkyCoord") 

625class SingleFrameSkyCoordPlugin(SingleFramePlugin): 

626 """Record the sky position and uncertainties of an object based on its 

627 centroid slot and WCS. 

628 

629 The position is recorded in the ``coord`` field, which is part of the 

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

631 

632 Parameters 

633 ---------- 

634 config : `SingleFrameSkyCoordConfig` 

635 Plugin configuraion. 

636 name : `str` 

637 Plugin name. 

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

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

640 added to hold measurements produced by this plugin. 

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

642 Plugin metadata that will be attached to the output catalog 

643 """ 

644 

645 ConfigClass = SingleFrameSkyCoordConfig 

646 

647 @classmethod 

648 def getExecutionOrder(cls): 

649 return cls.SHAPE_ORDER 

650 

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

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

653 if "coord_raErr" not in schema: 

654 lsst.afw.table.CoordKey.addErrorFields(schema) 

655 

656 def measure(self, measRecord, exposure): 

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

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

659 # the appropriate type for this error 

660 if not exposure.hasWcs(): 

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

662 measRecord.updateCoord(exposure.getWcs()) 

663 

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

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

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

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

668 # DM-1011 

669 pass 

670 

671 

672class SingleFrameClassificationSizeExtendednessConfig(SingleFramePluginConfig): 

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

674 

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

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

677 "in the likelihood normalization", 

678 default=0.5, 

679 ) 

680 

681 

682@register("base_ClassificationSizeExtendedness") 

683class SingleFrameClassificationSizeExtendednessPlugin(SingleFramePlugin): 

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

685 

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

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

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

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

690 

691 Parameters 

692 ---------- 

693 config : `SingleFrameClassificationSizeExtendednessConfig` 

694 Plugin configuration. 

695 name : `str` 

696 Plugin name. 

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

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

699 added to hold measurements produced by this plugin. 

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

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

702 

703 Notes 

704 ----- 

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

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

707 """ 

708 

709 ConfigClass = SingleFrameClassificationSizeExtendednessConfig 

710 

711 FAILURE_BAD_SHAPE = 1 

712 """Denotes failures due to bad shape (`int`). 

713 """ 

714 

715 @classmethod 

716 def getExecutionOrder(cls): 

717 return cls.FLUX_ORDER 

718 

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

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

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

722 type="D", 

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

724 ) 

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

726 

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

728 # Docstring inherited. 

729 

730 if measRecord.getShapeFlag(): 

731 raise MeasurementError( 

732 "Shape flag is set. Required for " + self.name + " algorithm", 

733 self.FAILURE_BAD_SHAPE, 

734 ) 

735 

736 shape = measRecord.getShape() 

737 psf_shape = measRecord.getPsfShape() 

738 

739 ixx = shape.getIxx() 

740 iyy = shape.getIyy() 

741 ixx_psf = psf_shape.getIxx() 

742 iyy_psf = psf_shape.getIyy() 

743 

744 object_t = ixx + iyy 

745 psf_t = ixx_psf + iyy_psf 

746 

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

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

749 measRecord.set(self.key, likelihood) 

750 

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

752 # Docstring inherited. 

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

754 measRecord.set(self.flag, True) 

755 

756 

757@deprecated(reason="Use SingleFrameClassificationSizeExtendednessConfig instead", version="v29.0.0", 

758 category=FutureWarning) 

759class SingleFrameMomentsClassifierConfig(SingleFrameClassificationSizeExtendednessConfig): 

760 pass 

761 

762 

763@deprecated(reason="Use SingleFrameClassificationSizeExtendednessPlugin instead", version="v29.0.0", 

764 category=FutureWarning) 

765class SingleFrameMomentsClassifierPlugin(SingleFrameClassificationSizeExtendednessPlugin): 

766 ConfigClass = SingleFrameMomentsClassifierConfig 

767 

768 

769class ForcedPeakCentroidConfig(ForcedPluginConfig): 

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

771 """ 

772 

773 

774@register("base_PeakCentroid") 

775class ForcedPeakCentroidPlugin(ForcedPlugin): 

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

777 

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

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

780 

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

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

783 coordinate system of the exposure being measured. 

784 

785 Parameters 

786 ---------- 

787 config : `ForcedPeakCentroidConfig` 

788 Plugin configuraion. 

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 

798 ConfigClass = ForcedPeakCentroidConfig 

799 

800 @classmethod 

801 def getExecutionOrder(cls): 

802 return cls.CENTROID_ORDER 

803 

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

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

806 schema = schemaMapper.editOutputSchema() 

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

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

809 

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

811 targetWcs = exposure.getWcs() 

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

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

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

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

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

817 

818 @staticmethod 

819 def getTransformClass(): 

820 return SimpleCentroidTransform 

821 

822 

823class ForcedTransformedCentroidConfig(ForcedPluginConfig): 

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

825 """ 

826 

827 

828@register("base_TransformedCentroid") 

829class ForcedTransformedCentroidPlugin(ForcedPlugin): 

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

831 

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

833 measurement coordinate system and stored. 

834 

835 Parameters 

836 ---------- 

837 config : `ForcedTransformedCentroidConfig` 

838 Plugin configuration 

839 name : `str` 

840 Plugin name 

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

842 A mapping from reference catalog fields to output 

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

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

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

846 

847 Notes 

848 ----- 

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

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

851 they would in single-frame measurement. 

852 """ 

853 

854 ConfigClass = ForcedTransformedCentroidConfig 

855 

856 @classmethod 

857 def getExecutionOrder(cls): 

858 return cls.CENTROID_ORDER 

859 

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

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

862 schema = schemaMapper.editOutputSchema() 

863 # Allocate x and y fields, join these into a single FunctorKey for 

864 # ease-of-use. 

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

866 units="pixel") 

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

868 units="pixel") 

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

870 # Because we're taking the reference position as given, we don't bother 

871 # transforming its uncertainty and reporting that here, so there are no 

872 # sigma or cov fields. We do propagate the flag field, if it exists. 

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

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

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

876 else: 

877 self.flagKey = None 

878 

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

880 targetWcs = exposure.getWcs() 

881 if not refWcs == targetWcs: 

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

883 measRecord.set(self.centroidKey, targetPos) 

884 else: 

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

886 if self.flagKey is not None: 

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

888 

889 

890class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig): 

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

892 """ 

893 

894 

895@register("base_TransformedCentroidFromCoord") 

896class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin): 

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

898 

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

900 measurement coordinate system and stored. 

901 

902 Parameters 

903 ---------- 

904 config : `ForcedTransformedCentroidFromCoordConfig` 

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 can be used as the slot centroid in forced measurement when only a 

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

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

919 """ 

920 

921 ConfigClass = ForcedTransformedCentroidFromCoordConfig 

922 

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

924 targetWcs = exposure.getWcs() 

925 

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

927 measRecord.set(self.centroidKey, targetPos) 

928 

929 if self.flagKey is not None: 

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

931 

932 

933class ForcedTransformedShapeConfig(ForcedPluginConfig): 

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

935 """ 

936 

937 

938@register("base_TransformedShape") 

939class ForcedTransformedShapePlugin(ForcedPlugin): 

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

941 

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

943 measurement coordinate system and stored. 

944 

945 Parameters 

946 ---------- 

947 config : `ForcedTransformedShapeConfig` 

948 Plugin configuration 

949 name : `str` 

950 Plugin name 

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

952 A mapping from reference catalog fields to output 

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

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

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

956 

957 Notes 

958 ----- 

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

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

961 would in single-frame measurement. 

962 """ 

963 

964 ConfigClass = ForcedTransformedShapeConfig 

965 

966 @classmethod 

967 def getExecutionOrder(cls): 

968 return cls.SHAPE_ORDER 

969 

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

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

972 schema = schemaMapper.editOutputSchema() 

973 # Allocate xx, yy, xy fields, join these into a single FunctorKey for 

974 # ease-of-use. 

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

976 units="pixel^2") 

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

978 units="pixel^2") 

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

980 units="pixel^2") 

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

982 # Because we're taking the reference position as given, we don't bother 

983 # transforming its uncertainty and reporting that here, so there are no 

984 # sigma or cov fields. We do propagate the flag field, if it exists. 

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

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

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

988 else: 

989 self.flagKey = None 

990 

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

992 targetWcs = exposure.getWcs() 

993 if not refWcs == targetWcs: 

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

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

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

997 else: 

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

999 if self.flagKey is not None: 

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