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

347 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-06 04:20 -0700

1# This file is part of meas_base. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22"""Definition of measurement plugins. 

23 

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

25which have trivial implementations. It also wraps measurement algorithms 

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

27""" 

28 

29import numpy as np 

30 

31import lsst.pex.exceptions 

32import lsst.geom 

33import lsst.afw.detection 

34import lsst.afw.geom 

35 

36from ._measBaseLib import (ApertureFluxControl, ApertureFluxTransform, 

37 BaseTransform, BlendednessAlgorithm, 

38 BlendednessControl, CircularApertureFluxAlgorithm, 

39 GaussianFluxAlgorithm, GaussianFluxControl, 

40 GaussianFluxTransform, LocalBackgroundAlgorithm, 

41 LocalBackgroundControl, LocalBackgroundTransform, 

42 MeasurementError, NaiveCentroidAlgorithm, 

43 NaiveCentroidControl, NaiveCentroidTransform, 

44 PeakLikelihoodFluxAlgorithm, 

45 PeakLikelihoodFluxControl, 

46 PeakLikelihoodFluxTransform, PixelFlagsAlgorithm, 

47 PixelFlagsControl, PsfFluxAlgorithm, PsfFluxControl, 

48 PsfFluxTransform, ScaledApertureFluxAlgorithm, 

49 ScaledApertureFluxControl, 

50 ScaledApertureFluxTransform, SdssCentroidAlgorithm, 

51 SdssCentroidControl, SdssCentroidTransform, 

52 SdssShapeAlgorithm, SdssShapeControl, 

53 SdssShapeTransform) 

54from .baseMeasurement import BaseMeasurementPluginConfig 

55from .forcedMeasurement import ForcedPlugin, ForcedPluginConfig 

56from .pluginRegistry import register 

57from .pluginsBase import BasePlugin 

58from .sfm import SingleFramePlugin, SingleFramePluginConfig 

59from .transforms import SimpleCentroidTransform 

60from .wrappers import GenericPlugin, wrapSimpleAlgorithm, wrapTransform 

61 

62__all__ = ( 

63 "SingleFrameFPPositionConfig", "SingleFrameFPPositionPlugin", 

64 "SingleFrameJacobianConfig", "SingleFrameJacobianPlugin", 

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

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

67 "SingleFramePeakCentroidConfig", "SingleFramePeakCentroidPlugin", 

68 "SingleFrameSkyCoordConfig", "SingleFrameSkyCoordPlugin", 

69 "SingleFrameMomentsClassifierConfig", "SingleFrameMomentsClassifierPlugin", 

70 "ForcedPeakCentroidConfig", "ForcedPeakCentroidPlugin", 

71 "ForcedTransformedCentroidConfig", "ForcedTransformedCentroidPlugin", 

72 "ForcedTransformedCentroidFromCoordConfig", 

73 "ForcedTransformedCentroidFromCoordPlugin", 

74 "ForcedTransformedShapeConfig", "ForcedTransformedShapePlugin", 

75 "EvaluateLocalPhotoCalibPlugin", "EvaluateLocalPhotoCalibPluginConfig", 

76 "EvaluateLocalWcsPlugin", "EvaluateLocalWcsPluginConfig", 

77) 

78 

79 

80wrapSimpleAlgorithm(PsfFluxAlgorithm, Control=PsfFluxControl, 

81 TransformClass=PsfFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

82 shouldApCorr=True, hasLogName=True) 

83wrapSimpleAlgorithm(PeakLikelihoodFluxAlgorithm, Control=PeakLikelihoodFluxControl, 

84 TransformClass=PeakLikelihoodFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

85wrapSimpleAlgorithm(GaussianFluxAlgorithm, Control=GaussianFluxControl, 

86 TransformClass=GaussianFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

87 shouldApCorr=True) 

88wrapSimpleAlgorithm(NaiveCentroidAlgorithm, Control=NaiveCentroidControl, 

89 TransformClass=NaiveCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

90wrapSimpleAlgorithm(SdssCentroidAlgorithm, Control=SdssCentroidControl, 

91 TransformClass=SdssCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

92wrapSimpleAlgorithm(PixelFlagsAlgorithm, Control=PixelFlagsControl, 

93 executionOrder=BasePlugin.FLUX_ORDER) 

94wrapSimpleAlgorithm(SdssShapeAlgorithm, Control=SdssShapeControl, 

95 TransformClass=SdssShapeTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

96wrapSimpleAlgorithm(ScaledApertureFluxAlgorithm, Control=ScaledApertureFluxControl, 

97 TransformClass=ScaledApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

98 

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

100 TransformClass=ApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

101wrapSimpleAlgorithm(BlendednessAlgorithm, Control=BlendednessControl, 

102 TransformClass=BaseTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

103 

104wrapSimpleAlgorithm(LocalBackgroundAlgorithm, Control=LocalBackgroundControl, 

105 TransformClass=LocalBackgroundTransform, executionOrder=BasePlugin.FLUX_ORDER) 

106 

107wrapTransform(PsfFluxTransform) 

108wrapTransform(PeakLikelihoodFluxTransform) 

109wrapTransform(GaussianFluxTransform) 

110wrapTransform(NaiveCentroidTransform) 

111wrapTransform(SdssCentroidTransform) 

112wrapTransform(SdssShapeTransform) 

113wrapTransform(ScaledApertureFluxTransform) 

114wrapTransform(ApertureFluxTransform) 

115wrapTransform(LocalBackgroundTransform) 

116 

117 

118class SingleFrameFPPositionConfig(SingleFramePluginConfig): 

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

120 """ 

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 

327 

328class InputCountPlugin(GenericPlugin): 

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

330 

331 Parameters 

332 ---------- 

333 config : `InputCountConfig` 

334 Plugin configuration. 

335 name : `str` 

336 Plugin name. 

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

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

339 added to hold measurements produced by this plugin. 

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

341 Plugin metadata that will be attached to the output catalog 

342 

343 Notes 

344 ----- 

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

346 Note these limitation: 

347 

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

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

350 source. 

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

352 """ 

353 

354 ConfigClass = InputCountConfig 

355 

356 FAILURE_BAD_CENTROID = 1 

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

358 """ 

359 

360 FAILURE_NO_INPUTS = 2 

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

362 """ 

363 

364 @classmethod 

365 def getExecutionOrder(cls): 

366 return BasePlugin.SHAPE_ORDER 

367 

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

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

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

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

372 "clipping") 

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

374 doc="No coadd inputs available") 

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

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

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

378 

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

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

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

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

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

384 

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

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

387 

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

389 if error is not None: 

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

391 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

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

393 measRecord.set(self.noInputsFlag, True) 

394 GenericPlugin.fail(self, measRecord, error) 

395 

396 

397SingleFrameInputCountPlugin = InputCountPlugin.makeSingleFramePlugin("base_InputCount") 

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

399""" 

400 

401ForcedInputCountPlugin = InputCountPlugin.makeForcedPlugin("base_InputCount") 

402"""Forced version of `InputCoutPlugin`. 

403""" 

404 

405 

406class EvaluateLocalPhotoCalibPluginConfig(BaseMeasurementPluginConfig): 

407 """Configuration for the variance calculation plugin. 

408 """ 

409 

410 

411class EvaluateLocalPhotoCalibPlugin(GenericPlugin): 

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

413 

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

415 use in the Science Data Model functors. 

416 """ 

417 ConfigClass = EvaluateLocalPhotoCalibPluginConfig 

418 

419 @classmethod 

420 def getExecutionOrder(cls): 

421 return BasePlugin.FLUX_ORDER 

422 

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

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

425 self.photoKey = schema.addField( 

426 name, 

427 type="D", 

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

429 "the location of the src.") 

430 self.photoErrKey = schema.addField( 

431 "%sErr" % name, 

432 type="D", 

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

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

435 

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

437 

438 photoCalib = exposure.getPhotoCalib() 

439 calib = photoCalib.getLocalCalibration(center) 

440 measRecord.set(self.photoKey, calib) 

441 

442 calibErr = photoCalib.getCalibrationErr() 

443 measRecord.set(self.photoErrKey, calibErr) 

444 

445 

446SingleFrameEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeSingleFramePlugin( 

447 "base_LocalPhotoCalib") 

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

449""" 

450 

451ForcedEvaluateLocalPhotoCalibPlugin = EvaluateLocalPhotoCalibPlugin.makeForcedPlugin( 

452 "base_LocalPhotoCalib") 

453"""Forced version of `EvaluatePhotoCalibPlugin`. 

454""" 

455 

456 

457class EvaluateLocalWcsPluginConfig(BaseMeasurementPluginConfig): 

458 """Configuration for the variance calculation plugin. 

459 """ 

460 

461 

462class EvaluateLocalWcsPlugin(GenericPlugin): 

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

464 

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

466 use in the Science Data Model functors. 

467 """ 

468 ConfigClass = EvaluateLocalWcsPluginConfig 

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

470 

471 @classmethod 

472 def getExecutionOrder(cls): 

473 return BasePlugin.FLUX_ORDER 

474 

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

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

477 self.cdMatrix11Key = schema.addField( 

478 f"{name}_CDMatrix_1_1", 

479 type="D", 

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

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

482 self.cdMatrix12Key = schema.addField( 

483 f"{name}_CDMatrix_1_2", 

484 type="D", 

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

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

487 self.cdMatrix21Key = schema.addField( 

488 f"{name}_CDMatrix_2_1", 

489 type="D", 

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

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

492 self.cdMatrix22Key = schema.addField( 

493 f"{name}_CDMatrix_2_2", 

494 type="D", 

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

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

497 

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

499 wcs = exposure.getWcs() 

500 localMatrix = self.makeLocalTransformMatrix(wcs, center) 

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

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

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

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

505 

506 def makeLocalTransformMatrix(self, wcs, center): 

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

508 matrix. 

509 

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

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

512 is initially calculated with units arcseconds and then converted to 

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

514 

515 Parameters 

516 ---------- 

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

518 Wcs to approximate 

519 center : `lsst.geom.Point2D` 

520 Point at which to evaluate the LocalWcs. 

521 

522 Returns 

523 ------- 

524 localMatrix : `numpy.ndarray` 

525 Matrix representation the local wcs approximation with units 

526 radians. 

527 """ 

528 skyCenter = wcs.pixelToSky(center) 

529 localGnomonicWcs = lsst.afw.geom.makeSkyWcs( 

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

531 measurementToLocalGnomonic = wcs.getTransform().then( 

532 localGnomonicWcs.getTransform().inverted() 

533 ) 

534 localMatrix = measurementToLocalGnomonic.getJacobian(center) 

535 return np.radians(localMatrix / 3600) 

536 

537 

538SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs") 

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

540""" 

541 

542ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs") 

543"""Forced version of `EvaluateLocalWcsPlugin`. 

544""" 

545 

546 

547class SingleFramePeakCentroidConfig(SingleFramePluginConfig): 

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

549 """ 

550 

551 

552@register("base_PeakCentroid") 

553class SingleFramePeakCentroidPlugin(SingleFramePlugin): 

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

555 

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

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

558 

559 Parameters 

560 ---------- 

561 config : `SingleFramePeakCentroidConfig` 

562 Plugin configuraion. 

563 name : `str` 

564 Plugin name. 

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

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

567 added to hold measurements produced by this plugin. 

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

569 Plugin metadata that will be attached to the output catalog 

570 """ 

571 

572 ConfigClass = SingleFramePeakCentroidConfig 

573 

574 @classmethod 

575 def getExecutionOrder(cls): 

576 return cls.CENTROID_ORDER 

577 

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

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

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

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

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

583 

584 def measure(self, measRecord, exposure): 

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

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

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

588 

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

590 measRecord.set(self.flag, True) 

591 

592 @staticmethod 

593 def getTransformClass(): 

594 return SimpleCentroidTransform 

595 

596 

597class SingleFrameSkyCoordConfig(SingleFramePluginConfig): 

598 """Configuration for the sky coordinates algorithm. 

599 """ 

600 

601 

602@register("base_SkyCoord") 

603class SingleFrameSkyCoordPlugin(SingleFramePlugin): 

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

605 

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

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

608 

609 Parameters 

610 ---------- 

611 config : `SingleFrameSkyCoordConfig` 

612 Plugin configuraion. 

613 name : `str` 

614 Plugin name. 

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

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

617 added to hold measurements produced by this plugin. 

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

619 Plugin metadata that will be attached to the output catalog 

620 """ 

621 

622 ConfigClass = SingleFrameSkyCoordConfig 

623 

624 @classmethod 

625 def getExecutionOrder(cls): 

626 return cls.SHAPE_ORDER 

627 

628 def measure(self, measRecord, exposure): 

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

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

631 # the appropriate type for this error 

632 if not exposure.hasWcs(): 

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

634 measRecord.updateCoord(exposure.getWcs()) 

635 

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

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

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

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

640 # DM-1011 

641 pass 

642 

643 

644class SingleFrameMomentsClassifierConfig(SingleFramePluginConfig): 

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

646 

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

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

649 "in the likelihood normalization", 

650 default=0.5, 

651 ) 

652 

653 

654@register("base_ClassificationSizeExtendedness") 

655class SingleFrameMomentsClassifierPlugin(SingleFramePlugin): 

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

657 

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

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

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

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

662 

663 Parameters 

664 ---------- 

665 config : `MomentsClassifierConfig` 

666 Plugin configuration. 

667 name : `str` 

668 Plugin name. 

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

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

671 added to hold measurements produced by this plugin. 

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

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

674 

675 Notes 

676 ----- 

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

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

679 """ 

680 

681 ConfigClass = SingleFrameMomentsClassifierConfig 

682 

683 @classmethod 

684 def getExecutionOrder(cls): 

685 return cls.FLUX_ORDER 

686 

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

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

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

690 type="D", 

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

692 ) 

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

694 

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

696 # Docstring inherited. 

697 

698 if measRecord.getShapeFlag(): 

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

700 

701 shape = measRecord.getShape() 

702 psf_shape = measRecord.getPsfShape() 

703 

704 ixx = shape.getIxx() 

705 iyy = shape.getIyy() 

706 ixx_psf = psf_shape.getIxx() 

707 iyy_psf = psf_shape.getIyy() 

708 

709 object_t = ixx + iyy 

710 psf_t = ixx_psf + iyy_psf 

711 

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

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

714 measRecord.set(self.key, likelihood) 

715 

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

717 # Docstring inherited. 

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

719 measRecord.set(self.flag, True) 

720 

721 

722class ForcedPeakCentroidConfig(ForcedPluginConfig): 

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

724 """ 

725 

726 

727@register("base_PeakCentroid") 

728class ForcedPeakCentroidPlugin(ForcedPlugin): 

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

730 

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

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

733 

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

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

736 coordinate system of the exposure being measured. 

737 

738 Parameters 

739 ---------- 

740 config : `ForcedPeakCentroidConfig` 

741 Plugin configuraion. 

742 name : `str` 

743 Plugin name. 

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

745 A mapping from reference catalog fields to output 

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

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

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

749 """ 

750 

751 ConfigClass = ForcedPeakCentroidConfig 

752 

753 @classmethod 

754 def getExecutionOrder(cls): 

755 return cls.CENTROID_ORDER 

756 

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

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

759 schema = schemaMapper.editOutputSchema() 

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

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

762 

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

764 targetWcs = exposure.getWcs() 

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

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

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

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

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

770 

771 @staticmethod 

772 def getTransformClass(): 

773 return SimpleCentroidTransform 

774 

775 

776class ForcedTransformedCentroidConfig(ForcedPluginConfig): 

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

778 """ 

779 

780 

781@register("base_TransformedCentroid") 

782class ForcedTransformedCentroidPlugin(ForcedPlugin): 

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

784 

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

786 measurement coordinate system and stored. 

787 

788 Parameters 

789 ---------- 

790 config : `ForcedTransformedCentroidConfig` 

791 Plugin configuration 

792 name : `str` 

793 Plugin name 

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

795 A mapping from reference catalog fields to output 

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

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

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

799 

800 Notes 

801 ----- 

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

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

804 they would in single-frame measurement. 

805 """ 

806 

807 ConfigClass = ForcedTransformedCentroidConfig 

808 

809 @classmethod 

810 def getExecutionOrder(cls): 

811 return cls.CENTROID_ORDER 

812 

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

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

815 schema = schemaMapper.editOutputSchema() 

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

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

818 units="pixel") 

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

820 units="pixel") 

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

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

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

824 # the flag field, if it exists. 

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

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

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

828 else: 

829 self.flagKey = None 

830 

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

832 targetWcs = exposure.getWcs() 

833 if not refWcs == targetWcs: 

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

835 measRecord.set(self.centroidKey, targetPos) 

836 else: 

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

838 if self.flagKey is not None: 

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

840 

841 

842class ForcedTransformedCentroidFromCoordConfig(ForcedTransformedCentroidConfig): 

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

844 """ 

845 

846 

847@register("base_TransformedCentroidFromCoord") 

848class ForcedTransformedCentroidFromCoordPlugin(ForcedTransformedCentroidPlugin): 

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

850 

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

852 measurement coordinate system and stored. 

853 

854 Parameters 

855 ---------- 

856 config : `ForcedTransformedCentroidFromCoordConfig` 

857 Plugin configuration 

858 name : `str` 

859 Plugin name 

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

861 A mapping from reference catalog fields to output 

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

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

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

865 

866 Notes 

867 ----- 

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

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

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

871 """ 

872 

873 ConfigClass = ForcedTransformedCentroidFromCoordConfig 

874 

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

876 targetWcs = exposure.getWcs() 

877 

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

879 measRecord.set(self.centroidKey, targetPos) 

880 

881 if self.flagKey is not None: 

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

883 

884 

885class ForcedTransformedShapeConfig(ForcedPluginConfig): 

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

887 """ 

888 

889 

890@register("base_TransformedShape") 

891class ForcedTransformedShapePlugin(ForcedPlugin): 

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

893 

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

895 measurement coordinate system and stored. 

896 

897 Parameters 

898 ---------- 

899 config : `ForcedTransformedShapeConfig` 

900 Plugin configuration 

901 name : `str` 

902 Plugin name 

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

904 A mapping from reference catalog fields to output 

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

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

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

908 

909 Notes 

910 ----- 

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

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

913 would in single-frame measurement. 

914 """ 

915 

916 ConfigClass = ForcedTransformedShapeConfig 

917 

918 @classmethod 

919 def getExecutionOrder(cls): 

920 return cls.SHAPE_ORDER 

921 

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

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

924 schema = schemaMapper.editOutputSchema() 

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

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

927 units="pixel^2") 

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

929 units="pixel^2") 

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

931 units="pixel^2") 

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

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

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

935 # the flag field, if it exists. 

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

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

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

939 else: 

940 self.flagKey = None 

941 

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

943 targetWcs = exposure.getWcs() 

944 if not refWcs == targetWcs: 

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

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

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

948 else: 

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

950 if self.flagKey is not None: 

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