Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of meas_base. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

22"""Definition of measurement plugins. 

23 

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

25which have trivial implementations. It also wraps measurement algorithms 

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

27""" 

28 

29import numpy as np 

30 

31import lsst.pex.exceptions 

32import lsst.geom 

33import lsst.afw.detection 

34import lsst.afw.geom 

35 

36from .pluginRegistry import register 

37from .pluginsBase import BasePlugin 

38from .baseMeasurement import BaseMeasurementPluginConfig 

39from .sfm import SingleFramePluginConfig, SingleFramePlugin 

40from .forcedMeasurement import ForcedPluginConfig, ForcedPlugin 

41from .wrappers import wrapSimpleAlgorithm, wrapTransform, GenericPlugin 

42from .transforms import SimpleCentroidTransform 

43 

44from .apertureFlux import ApertureFluxControl, ApertureFluxTransform 

45from .transform import BaseTransform 

46from .blendedness import BlendednessAlgorithm, BlendednessControl 

47from .circularApertureFlux import CircularApertureFluxAlgorithm 

48from .gaussianFlux import GaussianFluxAlgorithm, GaussianFluxControl, GaussianFluxTransform 

49from .exceptions import MeasurementError 

50from .localBackground import LocalBackgroundControl, LocalBackgroundAlgorithm, LocalBackgroundTransform 

51from .naiveCentroid import NaiveCentroidAlgorithm, NaiveCentroidControl, NaiveCentroidTransform 

52from .peakLikelihoodFlux import PeakLikelihoodFluxAlgorithm, PeakLikelihoodFluxControl, \ 

53 PeakLikelihoodFluxTransform 

54from .pixelFlags import PixelFlagsAlgorithm, PixelFlagsControl 

55from .psfFlux import PsfFluxAlgorithm, PsfFluxControl, PsfFluxTransform 

56from .scaledApertureFlux import ScaledApertureFluxAlgorithm, ScaledApertureFluxControl, \ 

57 ScaledApertureFluxTransform 

58from .sdssCentroid import SdssCentroidAlgorithm, SdssCentroidControl, SdssCentroidTransform 

59from .sdssShape import SdssShapeAlgorithm, SdssShapeControl, SdssShapeTransform 

60 

61__all__ = ( 

62 "SingleFrameFPPositionConfig", "SingleFrameFPPositionPlugin", 

63 "SingleFrameJacobianConfig", "SingleFrameJacobianPlugin", 

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

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

66 "SingleFramePeakCentroidConfig", "SingleFramePeakCentroidPlugin", 

67 "SingleFrameSkyCoordConfig", "SingleFrameSkyCoordPlugin", 

68 "ForcedPeakCentroidConfig", "ForcedPeakCentroidPlugin", 

69 "ForcedTransformedCentroidConfig", "ForcedTransformedCentroidPlugin", 

70 "ForcedTransformedShapeConfig", "ForcedTransformedShapePlugin", 

71 "EvaluateLocalPhotoCalibPlugin", "EvaluateLocalPhotoCalibPluginConfig", 

72 "EvaluateLocalWcsPlugin", "EvaluateLocalWcsPluginConfig", 

73) 

74 

75 

76wrapSimpleAlgorithm(PsfFluxAlgorithm, Control=PsfFluxControl, 

77 TransformClass=PsfFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

78 shouldApCorr=True, hasLogName=True) 

79wrapSimpleAlgorithm(PeakLikelihoodFluxAlgorithm, Control=PeakLikelihoodFluxControl, 

80 TransformClass=PeakLikelihoodFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

81wrapSimpleAlgorithm(GaussianFluxAlgorithm, Control=GaussianFluxControl, 

82 TransformClass=GaussianFluxTransform, executionOrder=BasePlugin.FLUX_ORDER, 

83 shouldApCorr=True) 

84wrapSimpleAlgorithm(NaiveCentroidAlgorithm, Control=NaiveCentroidControl, 

85 TransformClass=NaiveCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

86wrapSimpleAlgorithm(SdssCentroidAlgorithm, Control=SdssCentroidControl, 

87 TransformClass=SdssCentroidTransform, executionOrder=BasePlugin.CENTROID_ORDER) 

88wrapSimpleAlgorithm(PixelFlagsAlgorithm, Control=PixelFlagsControl, 

89 executionOrder=BasePlugin.FLUX_ORDER) 

90wrapSimpleAlgorithm(SdssShapeAlgorithm, Control=SdssShapeControl, 

91 TransformClass=SdssShapeTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

92wrapSimpleAlgorithm(ScaledApertureFluxAlgorithm, Control=ScaledApertureFluxControl, 

93 TransformClass=ScaledApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

94 

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

96 TransformClass=ApertureFluxTransform, executionOrder=BasePlugin.FLUX_ORDER) 

97wrapSimpleAlgorithm(BlendednessAlgorithm, Control=BlendednessControl, 

98 TransformClass=BaseTransform, executionOrder=BasePlugin.SHAPE_ORDER) 

99 

100wrapSimpleAlgorithm(LocalBackgroundAlgorithm, Control=LocalBackgroundControl, 

101 TransformClass=LocalBackgroundTransform, executionOrder=BasePlugin.FLUX_ORDER) 

102 

103wrapTransform(PsfFluxTransform) 

104wrapTransform(PeakLikelihoodFluxTransform) 

105wrapTransform(GaussianFluxTransform) 

106wrapTransform(NaiveCentroidTransform) 

107wrapTransform(SdssCentroidTransform) 

108wrapTransform(SdssShapeTransform) 

109wrapTransform(ScaledApertureFluxTransform) 

110wrapTransform(ApertureFluxTransform) 

111wrapTransform(LocalBackgroundTransform) 

112 

113 

114class SingleFrameFPPositionConfig(SingleFramePluginConfig): 

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

116 """ 

117 

118 pass 

119 

120 

121@register("base_FPPosition") 

122class SingleFrameFPPositionPlugin(SingleFramePlugin): 

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

124 

125 Parameters 

126 ---------- 

127 config : `SingleFrameFPPositionConfig` 

128 Plugin configuraion. 

129 name : `str` 

130 Plugin name. 

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

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

133 added to hold measurements produced by this plugin. 

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

135 Plugin metadata that will be attached to the output catalog 

136 """ 

137 

138 ConfigClass = SingleFrameFPPositionConfig 

139 

140 @classmethod 

141 def getExecutionOrder(cls): 

142 return cls.SHAPE_ORDER 

143 

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

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

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

147 "mm") 

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

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

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

151 

152 def measure(self, measRecord, exposure): 

153 det = exposure.getDetector() 

154 if not det: 

155 measRecord.set(self.detectorFlag, True) 

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

157 else: 

158 center = measRecord.getCentroid() 

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

160 measRecord.set(self.focalValue, fp) 

161 

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

163 measRecord.set(self.focalFlag, True) 

164 

165 

166class SingleFrameJacobianConfig(SingleFramePluginConfig): 

167 """Configuration for the Jacobian calculation plugin. 

168 """ 

169 

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

171 

172 

173@register("base_Jacobian") 

174class SingleFrameJacobianPlugin(SingleFramePlugin): 

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

176 

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

178 

179 Parameters 

180 ---------- 

181 config : `SingleFrameJacobianConfig` 

182 Plugin configuraion. 

183 name : `str` 

184 Plugin name. 

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

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

187 added to hold measurements produced by this plugin. 

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

189 Plugin metadata that will be attached to the output catalog 

190 """ 

191 

192 ConfigClass = SingleFrameJacobianConfig 

193 

194 @classmethod 

195 def getExecutionOrder(cls): 

196 return cls.SHAPE_ORDER 

197 

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

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

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

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

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

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

204 

205 def measure(self, measRecord, exposure): 

206 center = measRecord.getCentroid() 

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

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

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

210 center, 

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

212 measRecord.set(self.jacValue, result) 

213 

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

215 measRecord.set(self.jacFlag, True) 

216 

217 

218class VarianceConfig(BaseMeasurementPluginConfig): 

219 """Configuration for the variance calculation plugin. 

220 """ 

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

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

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

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

225 

226 

227class VariancePlugin(GenericPlugin): 

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

229 

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

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

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

233 

234 Parameters 

235 ---------- 

236 config : `VarianceConfig` 

237 Plugin configuraion. 

238 name : `str` 

239 Plugin name. 

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

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

242 added to hold measurements produced by this plugin. 

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

244 Plugin metadata that will be attached to the output catalog 

245 """ 

246 

247 ConfigClass = VarianceConfig 

248 

249 FAILURE_BAD_CENTROID = 1 

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

251 """ 

252 

253 FAILURE_EMPTY_FOOTPRINT = 2 

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

255 """ 

256 

257 @classmethod 

258 def getExecutionOrder(cls): 

259 return BasePlugin.FLUX_ORDER 

260 

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

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

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

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

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

266 

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

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

269 # that could be changed post-measurement. 

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

271 

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

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

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

275 # statistics 

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

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

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

279 aperture.scale(self.config.scale) 

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

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

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

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

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

285 maskedImage = exposure.getMaskedImage() 

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

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

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

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

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

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

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

293 if np.any(logicalMask): 

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

295 measRecord.set(self.varValue, medVar) 

296 else: 

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

298 self.FAILURE_EMPTY_FOOTPRINT) 

299 

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

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

302 # MeasurementError 

303 if isinstance(error, MeasurementError): 

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

305 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

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

307 measRecord.set(self.emptyFootprintFlag, True) 

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

309 GenericPlugin.fail(self, measRecord, error) 

310 

311 

312SingleFrameVariancePlugin = VariancePlugin.makeSingleFramePlugin("base_Variance") 

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

314""" 

315 

316ForcedVariancePlugin = VariancePlugin.makeForcedPlugin("base_Variance") 

317"""Forced version of `VariancePlugin`. 

318""" 

319 

320 

321class InputCountConfig(BaseMeasurementPluginConfig): 

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

323 """ 

324 pass 

325 

326 

327class InputCountPlugin(GenericPlugin): 

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

329 

330 Parameters 

331 ---------- 

332 config : `InputCountConfig` 

333 Plugin configuraion. 

334 name : `str` 

335 Plugin name. 

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

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

338 added to hold measurements produced by this plugin. 

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

340 Plugin metadata that will be attached to the output catalog 

341 

342 Notes 

343 ----- 

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

345 Note these limitation: 

346 

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

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

349 source. 

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

351 """ 

352 

353 ConfigClass = InputCountConfig 

354 

355 FAILURE_BAD_CENTROID = 1 

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

357 """ 

358 

359 FAILURE_NO_INPUTS = 2 

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

361 """ 

362 

363 @classmethod 

364 def getExecutionOrder(cls): 

365 return BasePlugin.SHAPE_ORDER 

366 

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

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

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

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

371 "clipping") 

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

373 doc="No coadd inputs available") 

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

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

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

377 

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

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

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

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

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

383 

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

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

386 

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

388 if error is not None: 

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

390 # FAILURE_BAD_CENTROID handled by alias to centroid record. 

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

392 measRecord.set(self.noInputsFlag, True) 

393 GenericPlugin.fail(self, measRecord, error) 

394 

395 

396SingleFrameInputCountPlugin = InputCountPlugin.makeSingleFramePlugin("base_InputCount") 

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

398""" 

399 

400ForcedInputCountPlugin = InputCountPlugin.makeForcedPlugin("base_InputCount") 

401"""Forced version of `InputCoutPlugin`. 

402""" 

403 

404 

405class EvaluateLocalPhotoCalibPluginConfig(BaseMeasurementPluginConfig): 

406 """Configuration for the variance calculation plugin. 

407 """ 

408 pass 

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 pass 

461 

462 

463class EvaluateLocalWcsPlugin(GenericPlugin): 

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

465 

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

467 use in the Science Data Model functors. 

468 """ 

469 ConfigClass = EvaluateLocalWcsPluginConfig 

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

471 

472 @classmethod 

473 def getExecutionOrder(cls): 

474 return BasePlugin.FLUX_ORDER 

475 

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

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

478 self.cdMatrix11Key = schema.addField( 

479 f"{name}_CDMatrix_1_1", 

480 type="D", 

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

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

483 self.cdMatrix12Key = schema.addField( 

484 f"{name}_CDMatrix_1_2", 

485 type="D", 

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

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

488 self.cdMatrix21Key = schema.addField( 

489 f"{name}_CDMatrix_2_1", 

490 type="D", 

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

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

493 self.cdMatrix22Key = schema.addField( 

494 f"{name}_CDMatrix_2_2", 

495 type="D", 

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

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

498 

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

500 wcs = exposure.getWcs() 

501 localMatrix = self.makeLocalTransformMatrix(wcs, center) 

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

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

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

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

506 

507 def makeLocalTransformMatrix(self, wcs, center): 

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

509 matrix. 

510 

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

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

513 is initially calculated with units arcseconds and then converted to 

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

515 

516 Parameters 

517 ---------- 

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

519 Wcs to approximate 

520 center : `lsst.geom.Point2D` 

521 Point at which to evaluate the LocalWcs. 

522 

523 Returns 

524 ------- 

525 localMatrix : `numpy.ndarray` 

526 Matrix representation the local wcs approximation with units 

527 radians. 

528 """ 

529 skyCenter = wcs.pixelToSky(center) 

530 localGnomonicWcs = lsst.afw.geom.makeSkyWcs( 

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

532 measurementToLocalGnomonic = wcs.getTransform().then( 

533 localGnomonicWcs.getTransform().inverted() 

534 ) 

535 localMatrix = measurementToLocalGnomonic.getJacobian(center) 

536 return np.radians(localMatrix / 3600) 

537 

538 

539SingleFrameEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeSingleFramePlugin("base_LocalWcs") 

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

541""" 

542 

543ForcedEvaluateLocalWcsPlugin = EvaluateLocalWcsPlugin.makeForcedPlugin("base_LocalWcs") 

544"""Forced version of `EvaluateLocalWcsPlugin`. 

545""" 

546 

547 

548class SingleFramePeakCentroidConfig(SingleFramePluginConfig): 

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

550 """ 

551 pass 

552 

553 

554@register("base_PeakCentroid") 

555class SingleFramePeakCentroidPlugin(SingleFramePlugin): 

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

557 

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

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

560 

561 Parameters 

562 ---------- 

563 config : `SingleFramePeakCentroidConfig` 

564 Plugin configuraion. 

565 name : `str` 

566 Plugin name. 

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

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

569 added to hold measurements produced by this plugin. 

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

571 Plugin metadata that will be attached to the output catalog 

572 """ 

573 

574 ConfigClass = SingleFramePeakCentroidConfig 

575 

576 @classmethod 

577 def getExecutionOrder(cls): 

578 return cls.CENTROID_ORDER 

579 

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

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

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

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

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

585 

586 def measure(self, measRecord, exposure): 

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

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

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

590 

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

592 measRecord.set(self.flag, True) 

593 

594 @staticmethod 

595 def getTransformClass(): 

596 return SimpleCentroidTransform 

597 

598 

599class SingleFrameSkyCoordConfig(SingleFramePluginConfig): 

600 """Configuration for the sky coordinates algorithm. 

601 """ 

602 pass 

603 

604 

605@register("base_SkyCoord") 

606class SingleFrameSkyCoordPlugin(SingleFramePlugin): 

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

608 

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

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

611 

612 Parameters 

613 ---------- 

614 config : `SingleFrameSkyCoordConfig` 

615 Plugin configuraion. 

616 name : `str` 

617 Plugin name. 

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

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

620 added to hold measurements produced by this plugin. 

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

622 Plugin metadata that will be attached to the output catalog 

623 """ 

624 

625 ConfigClass = SingleFrameSkyCoordConfig 

626 

627 @classmethod 

628 def getExecutionOrder(cls): 

629 return cls.SHAPE_ORDER 

630 

631 def measure(self, measRecord, exposure): 

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

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

634 # the appropriate type for this error 

635 if not exposure.hasWcs(): 

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

637 measRecord.updateCoord(exposure.getWcs()) 

638 

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

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

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

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

643 # DM-1011 

644 pass 

645 

646 

647class ForcedPeakCentroidConfig(ForcedPluginConfig): 

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

649 """ 

650 pass 

651 

652 

653@register("base_PeakCentroid") 

654class ForcedPeakCentroidPlugin(ForcedPlugin): 

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

656 

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

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

659 

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

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

662 coordinate system of the exposure being measured. 

663 

664 Parameters 

665 ---------- 

666 config : `ForcedPeakCentroidConfig` 

667 Plugin configuraion. 

668 name : `str` 

669 Plugin name. 

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

671 A mapping from reference catalog fields to output 

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

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

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

675 """ 

676 

677 ConfigClass = ForcedPeakCentroidConfig 

678 

679 @classmethod 

680 def getExecutionOrder(cls): 

681 return cls.CENTROID_ORDER 

682 

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

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

685 schema = schemaMapper.editOutputSchema() 

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

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

688 

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

690 targetWcs = exposure.getWcs() 

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

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

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

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

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

696 

697 @staticmethod 

698 def getTransformClass(): 

699 return SimpleCentroidTransform 

700 

701 

702class ForcedTransformedCentroidConfig(ForcedPluginConfig): 

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

704 """ 

705 pass 

706 

707 

708@register("base_TransformedCentroid") 

709class ForcedTransformedCentroidPlugin(ForcedPlugin): 

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

711 

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

713 measurement coordinate system and stored. 

714 

715 Parameters 

716 ---------- 

717 config : `ForcedTransformedCentroidConfig` 

718 Plugin configuration 

719 name : `str` 

720 Plugin name 

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

722 A mapping from reference catalog fields to output 

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

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

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

726 

727 Notes 

728 ----- 

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

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

731 they would in single-frame measurement. 

732 """ 

733 

734 ConfigClass = ForcedTransformedCentroidConfig 

735 

736 @classmethod 

737 def getExecutionOrder(cls): 

738 return cls.CENTROID_ORDER 

739 

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

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

742 schema = schemaMapper.editOutputSchema() 

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

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

745 units="pixel") 

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

747 units="pixel") 

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

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

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

751 # the flag field, if it exists. 

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

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

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

755 else: 

756 self.flagKey = None 

757 

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

759 targetWcs = exposure.getWcs() 

760 if not refWcs == targetWcs: 

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

762 measRecord.set(self.centroidKey, targetPos) 

763 else: 

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

765 if self.flagKey is not None: 

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

767 

768 

769class ForcedTransformedShapeConfig(ForcedPluginConfig): 

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

771 """ 

772 pass 

773 

774 

775@register("base_TransformedShape") 

776class ForcedTransformedShapePlugin(ForcedPlugin): 

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

778 

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

780 measurement coordinate system and stored. 

781 

782 Parameters 

783 ---------- 

784 config : `ForcedTransformedShapeConfig` 

785 Plugin configuration 

786 name : `str` 

787 Plugin name 

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

789 A mapping from reference catalog fields to output 

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

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

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

793 

794 Notes 

795 ----- 

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

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

798 would in single-frame measurement. 

799 """ 

800 

801 ConfigClass = ForcedTransformedShapeConfig 

802 

803 @classmethod 

804 def getExecutionOrder(cls): 

805 return cls.SHAPE_ORDER 

806 

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

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

809 schema = schemaMapper.editOutputSchema() 

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

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

812 units="pixel^2") 

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

814 units="pixel^2") 

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

816 units="pixel^2") 

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

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

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

820 # the flag field, if it exists. 

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

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

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

824 else: 

825 self.flagKey = None 

826 

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

828 targetWcs = exposure.getWcs() 

829 if not refWcs == targetWcs: 

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

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

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

833 else: 

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

835 if self.flagKey is not None: 

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