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 

22import collections 

23 

24import lsst.pex.config 

25import lsst.pex.exceptions 

26from lsst.log import Log 

27import lsst.pipe.base 

28import lsst.geom 

29import lsst.afw.geom 

30import lsst.afw.image 

31import lsst.afw.table 

32import lsst.sphgeom 

33 

34from lsst.pipe.base import PipelineTaskConnections 

35import lsst.pipe.base.connectionTypes as cT 

36 

37import lsst.pipe.base as pipeBase 

38from lsst.skymap import BaseSkyMap 

39 

40from .references import MultiBandReferencesTask 

41from .forcedMeasurement import ForcedMeasurementTask 

42from .applyApCorr import ApplyApCorrTask 

43from .catalogCalculation import CatalogCalculationTask 

44 

45try: 

46 from lsst.meas.mosaic import applyMosaicResults 

47except ImportError: 

48 applyMosaicResults = None 

49 

50__all__ = ("PerTractCcdDataIdContainer", "ForcedPhotCcdConfig", "ForcedPhotCcdTask", "imageOverlapsTract") 

51 

52 

53class PerTractCcdDataIdContainer(pipeBase.DataIdContainer): 

54 """A data ID container which combines raw data IDs with a tract. 

55 

56 Notes 

57 ----- 

58 Required because we need to add "tract" to the raw data ID keys (defined as 

59 whatever we use for ``src``) when no tract is provided (so that the user is 

60 not required to know which tracts are spanned by the raw data ID). 

61 

62 This subclass of `~lsst.pipe.base.DataIdContainer` assumes that a calexp is 

63 being measured using the detection information, a set of reference 

64 catalogs, from the set of coadds which intersect with the calexp. It needs 

65 the calexp id (e.g. visit, raft, sensor), but is also uses the tract to 

66 decide what set of coadds to use. The references from the tract whose 

67 patches intersect with the calexp are used. 

68 """ 

69 

70 def makeDataRefList(self, namespace): 

71 """Make self.refList from self.idList 

72 """ 

73 if self.datasetType is None: 

74 raise RuntimeError("Must call setDatasetType first") 

75 log = Log.getLogger("meas.base.forcedPhotCcd.PerTractCcdDataIdContainer") 

76 skymap = None 

77 visitTract = collections.defaultdict(set) # Set of tracts for each visit 

78 visitRefs = collections.defaultdict(list) # List of data references for each visit 

79 for dataId in self.idList: 

80 if "tract" not in dataId: 

81 # Discover which tracts the data overlaps 

82 log.info("Reading WCS for components of dataId=%s to determine tracts", dict(dataId)) 

83 if skymap is None: 

84 skymap = namespace.butler.get(namespace.config.coaddName + "Coadd_skyMap") 

85 

86 for ref in namespace.butler.subset("calexp", dataId=dataId): 

87 if not ref.datasetExists("calexp"): 

88 continue 

89 

90 visit = ref.dataId["visit"] 

91 visitRefs[visit].append(ref) 

92 

93 md = ref.get("calexp_md", immediate=True) 

94 wcs = lsst.afw.geom.makeSkyWcs(md) 

95 box = lsst.geom.Box2D(lsst.afw.image.bboxFromMetadata(md)) 

96 # Going with just the nearest tract. Since we're throwing all tracts for the visit 

97 # together, this shouldn't be a problem unless the tracts are much smaller than a CCD. 

98 tract = skymap.findTract(wcs.pixelToSky(box.getCenter())) 

99 if imageOverlapsTract(tract, wcs, box): 

100 visitTract[visit].add(tract.getId()) 

101 else: 

102 self.refList.extend(ref for ref in namespace.butler.subset(self.datasetType, dataId=dataId)) 

103 

104 # Ensure all components of a visit are kept together by putting them all in the same set of tracts 

105 for visit, tractSet in visitTract.items(): 

106 for ref in visitRefs[visit]: 

107 for tract in tractSet: 

108 self.refList.append(namespace.butler.dataRef(datasetType=self.datasetType, 

109 dataId=ref.dataId, tract=tract)) 

110 if visitTract: 

111 tractCounter = collections.Counter() 

112 for tractSet in visitTract.values(): 

113 tractCounter.update(tractSet) 

114 log.info("Number of visits for each tract: %s", dict(tractCounter)) 

115 

116 

117def imageOverlapsTract(tract, imageWcs, imageBox): 

118 """Return whether the given bounding box overlaps the tract given a WCS. 

119 

120 Parameters 

121 ---------- 

122 tract : `lsst.skymap.TractInfo` 

123 TractInfo specifying a tract. 

124 imageWcs : `lsst.afw.geom.SkyWcs` 

125 World coordinate system for the image. 

126 imageBox : `lsst.geom.Box2I` 

127 Bounding box for the image. 

128 

129 Returns 

130 ------- 

131 overlap : `bool` 

132 `True` if the bounding box overlaps the tract; `False` otherwise. 

133 """ 

134 tractPoly = tract.getOuterSkyPolygon() 

135 

136 imagePixelCorners = lsst.geom.Box2D(imageBox).getCorners() 

137 try: 

138 imageSkyCorners = imageWcs.pixelToSky(imagePixelCorners) 

139 except lsst.pex.exceptions.LsstCppException as e: 

140 # Protecting ourselves from awful Wcs solutions in input images 

141 if (not isinstance(e.message, lsst.pex.exceptions.DomainErrorException) 

142 and not isinstance(e.message, lsst.pex.exceptions.RuntimeErrorException)): 

143 raise 

144 return False 

145 

146 imagePoly = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in imageSkyCorners]) 

147 return tractPoly.intersects(imagePoly) # "intersects" also covers "contains" or "is contained by" 

148 

149 

150class ForcedPhotCcdConnections(PipelineTaskConnections, 

151 dimensions=("instrument", "visit", "detector", "skymap", "tract"), 

152 defaultTemplates={"inputCoaddName": "deep", 

153 "inputName": "calexp"}): 

154 inputSchema = cT.InitInput( 

155 doc="Schema for the input measurement catalogs.", 

156 name="{inputCoaddName}Coadd_ref_schema", 

157 storageClass="SourceCatalog", 

158 ) 

159 outputSchema = cT.InitOutput( 

160 doc="Schema for the output forced measurement catalogs.", 

161 name="forced_src_schema", 

162 storageClass="SourceCatalog", 

163 ) 

164 exposure = cT.Input( 

165 doc="Input exposure to perform photometry on.", 

166 name="{inputName}", 

167 storageClass="ExposureF", 

168 dimensions=["instrument", "visit", "detector"], 

169 ) 

170 refCat = cT.Input( 

171 doc="Catalog of shapes and positions at which to force photometry.", 

172 name="{inputCoaddName}Coadd_ref", 

173 storageClass="SourceCatalog", 

174 dimensions=["skymap", "tract", "patch"], 

175 multiple=True 

176 ) 

177 skyMap = cT.Input( 

178 doc="SkyMap dataset that defines the coordinate system of the reference catalog.", 

179 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

180 storageClass="SkyMap", 

181 dimensions=["skymap"], 

182 ) 

183 measCat = cT.Output( 

184 doc="Output forced photometry catalog.", 

185 name="forced_src", 

186 storageClass="SourceCatalog", 

187 dimensions=["instrument", "visit", "detector", "skymap", "tract"], 

188 ) 

189 

190 

191class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig, 

192 pipelineConnections=ForcedPhotCcdConnections): 

193 """Config class for forced measurement driver task.""" 

194 references = lsst.pex.config.ConfigurableField( 

195 target=MultiBandReferencesTask, 

196 doc="subtask to retrieve reference source catalog" 

197 ) 

198 measurement = lsst.pex.config.ConfigurableField( 

199 target=ForcedMeasurementTask, 

200 doc="subtask to do forced measurement" 

201 ) 

202 coaddName = lsst.pex.config.Field( 

203 doc="coadd name: typically one of deep or goodSeeing", 

204 dtype=str, 

205 default="deep", 

206 ) 

207 doApCorr = lsst.pex.config.Field( 

208 dtype=bool, 

209 default=True, 

210 doc="Run subtask to apply aperture corrections" 

211 ) 

212 applyApCorr = lsst.pex.config.ConfigurableField( 

213 target=ApplyApCorrTask, 

214 doc="Subtask to apply aperture corrections" 

215 ) 

216 catalogCalculation = lsst.pex.config.ConfigurableField( 

217 target=CatalogCalculationTask, 

218 doc="Subtask to run catalogCalculation plugins on catalog" 

219 ) 

220 doApplyUberCal = lsst.pex.config.Field( 

221 dtype=bool, 

222 doc="Apply meas_mosaic ubercal results to input calexps?", 

223 default=False, 

224 deprecated="Deprecated by DM-23352; use doApplyExternalPhotoCalib and doApplyExternalSkyWcs instead", 

225 ) 

226 doApplyExternalPhotoCalib = lsst.pex.config.Field( 

227 dtype=bool, 

228 default=False, 

229 doc=("Whether to apply external photometric calibration via an " 

230 "`lsst.afw.image.PhotoCalib` object. Uses the " 

231 "``externalPhotoCalibName`` field to determine which calibration " 

232 "to load."), 

233 ) 

234 doApplyExternalSkyWcs = lsst.pex.config.Field( 

235 dtype=bool, 

236 default=False, 

237 doc=("Whether to apply external astrometric calibration via an " 

238 "`lsst.afw.geom.SkyWcs` object. Uses ``externalSkyWcsName`` " 

239 "field to determine which calibration to load."), 

240 ) 

241 doApplySkyCorr = lsst.pex.config.Field( 

242 dtype=bool, 

243 default=False, 

244 doc="Apply sky correction?", 

245 ) 

246 includePhotoCalibVar = lsst.pex.config.Field( 

247 dtype=bool, 

248 default=False, 

249 doc="Add photometric calibration variance to warp variance plane?", 

250 ) 

251 externalPhotoCalibName = lsst.pex.config.ChoiceField( 

252 dtype=str, 

253 doc=("Type of external PhotoCalib if ``doApplyExternalPhotoCalib`` is True. " 

254 "Unused for Gen3 middleware."), 

255 default="jointcal", 

256 allowed={ 

257 "jointcal": "Use jointcal_photoCalib", 

258 "fgcm": "Use fgcm_photoCalib", 

259 "fgcm_tract": "Use fgcm_tract_photoCalib" 

260 }, 

261 ) 

262 externalSkyWcsName = lsst.pex.config.ChoiceField( 

263 dtype=str, 

264 doc="Type of external SkyWcs if ``doApplyExternalSkyWcs`` is True. Unused for Gen3 middleware.", 

265 default="jointcal", 

266 allowed={ 

267 "jointcal": "Use jointcal_wcs" 

268 }, 

269 ) 

270 

271 def setDefaults(self): 

272 # Docstring inherited. 

273 # Make catalogCalculation a no-op by default as no modelFlux is setup by default in 

274 # ForcedMeasurementTask 

275 super().setDefaults() 

276 

277 self.catalogCalculation.plugins.names = [] 

278 

279 

280class ForcedPhotCcdTask(pipeBase.PipelineTask, pipeBase.CmdLineTask): 

281 """A command-line driver for performing forced measurement on CCD images. 

282 

283 Parameters 

284 ---------- 

285 butler : `lsst.daf.persistence.butler.Butler`, optional 

286 A Butler which will be passed to the references subtask to allow it to 

287 load its schema from disk. Optional, but must be specified if 

288 ``refSchema`` is not; if both are specified, ``refSchema`` takes 

289 precedence. 

290 refSchema : `lsst.afw.table.Schema`, optional 

291 The schema of the reference catalog, passed to the constructor of the 

292 references subtask. Optional, but must be specified if ``butler`` is 

293 not; if both are specified, ``refSchema`` takes precedence. 

294 **kwds 

295 Keyword arguments are passed to the supertask constructor. 

296 

297 Notes 

298 ----- 

299 The `runDataRef` method takes a `~lsst.daf.persistence.ButlerDataRef` argument 

300 that corresponds to a single CCD. This should contain the data ID keys that 

301 correspond to the ``forced_src`` dataset (the output dataset for this 

302 task), which are typically all those used to specify the ``calexp`` dataset 

303 (``visit``, ``raft``, ``sensor`` for LSST data) as well as a coadd tract. 

304 The tract is used to look up the appropriate coadd measurement catalogs to 

305 use as references (e.g. ``deepCoadd_src``; see 

306 :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` for more 

307 information). While the tract must be given as part of the dataRef, the 

308 patches are determined automatically from the bounding box and WCS of the 

309 calexp to be measured, and the filter used to fetch references is set via 

310 the ``filter`` option in the configuration of 

311 :lsst-task:`lsst.meas.base.references.BaseReferencesTask`). 

312 """ 

313 

314 ConfigClass = ForcedPhotCcdConfig 

315 RunnerClass = pipeBase.ButlerInitializedTaskRunner 

316 _DefaultName = "forcedPhotCcd" 

317 dataPrefix = "" 

318 

319 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds): 

320 super().__init__(**kwds) 

321 

322 if initInputs is not None: 

323 refSchema = initInputs['inputSchema'].schema 

324 

325 self.makeSubtask("references", butler=butler, schema=refSchema) 

326 if refSchema is None: 

327 refSchema = self.references.schema 

328 self.makeSubtask("measurement", refSchema=refSchema) 

329 # It is necessary to get the schema internal to the forced measurement task until such a time 

330 # that the schema is not owned by the measurement task, but is passed in by an external caller 

331 if self.config.doApCorr: 

332 self.makeSubtask("applyApCorr", schema=self.measurement.schema) 

333 self.makeSubtask('catalogCalculation', schema=self.measurement.schema) 

334 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema) 

335 

336 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

337 inputs = butlerQC.get(inputRefs) 

338 

339 tract = butlerQC.quantum.dataId['tract'] 

340 skyMap = inputs.pop("skyMap") 

341 inputs['refWcs'] = skyMap[tract].getWcs() 

342 

343 inputs['refCat'] = self.mergeAndFilterReferences(inputs['exposure'], inputs['refCat'], 

344 inputs['refWcs']) 

345 

346 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId, 

347 inputs['exposure'], 

348 inputs['refCat'], inputs['refWcs'], 

349 "visit_detector") 

350 self.attachFootprints(inputs['measCat'], inputs['refCat'], inputs['exposure'], inputs['refWcs']) 

351 # TODO: apply external calibrations (DM-17062) 

352 outputs = self.run(**inputs) 

353 butlerQC.put(outputs, outputRefs) 

354 

355 def mergeAndFilterReferences(self, exposure, refCats, refWcs): 

356 """Filter reference catalog so that all sources are within the 

357 boundaries of the exposure. 

358 

359 Parameters 

360 ---------- 

361 exposure : `lsst.afw.image.exposure.Exposure` 

362 Exposure to generate the catalog for. 

363 refCats : sequence of `lsst.afw.table.SourceCatalog` 

364 Catalogs of shapes and positions at which to force photometry. 

365 refWcs : `lsst.afw.image.SkyWcs` 

366 Reference world coordinate system. 

367 

368 Returns 

369 ------- 

370 refSources : `lsst.afw.table.SourceCatalog` 

371 Filtered catalog of forced sources to measure. 

372 

373 Notes 

374 ----- 

375 Filtering the reference catalog is currently handled by Gen2 

376 specific methods. To function for Gen3, this method copies 

377 code segments to do the filtering and transformation. The 

378 majority of this code is based on the methods of 

379 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader 

380 

381 """ 

382 

383 # Step 1: Determine bounds of the exposure photometry will 

384 # be performed on. 

385 expWcs = exposure.getWcs() 

386 expRegion = exposure.getBBox(lsst.afw.image.PARENT) 

387 expBBox = lsst.geom.Box2D(expRegion) 

388 expBoxCorners = expBBox.getCorners() 

389 expSkyCorners = [expWcs.pixelToSky(corner).getVector() for 

390 corner in expBoxCorners] 

391 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners) 

392 

393 # Step 2: Filter out reference catalog sources that are 

394 # not contained within the exposure boundaries, or whose 

395 # parents are not within the exposure boundaries. Note 

396 # that within a single input refCat, the parents always 

397 # appear before the children. 

398 mergedRefCat = lsst.afw.table.SourceCatalog(refCats[0].table) 

399 for refCat in refCats: 

400 containedIds = {0} # zero as a parent ID means "this is a parent" 

401 for record in refCat: 

402 if expPolygon.contains(record.getCoord().getVector()) and record.getParent() in containedIds: 

403 record.setFootprint(record.getFootprint().transform(refWcs, expWcs, expRegion)) 

404 mergedRefCat.append(record) 

405 containedIds.add(record.getId()) 

406 mergedRefCat.sort(lsst.afw.table.SourceTable.getParentKey()) 

407 return mergedRefCat 

408 

409 def generateMeasCat(self, exposureDataId, exposure, refCat, refWcs, idPackerName): 

410 """Generate a measurement catalog for Gen3. 

411 

412 Parameters 

413 ---------- 

414 exposureDataId : `DataId` 

415 Butler dataId for this exposure. 

416 exposure : `lsst.afw.image.exposure.Exposure` 

417 Exposure to generate the catalog for. 

418 refCat : `lsst.afw.table.SourceCatalog` 

419 Catalog of shapes and positions at which to force photometry. 

420 refWcs : `lsst.afw.image.SkyWcs` 

421 Reference world coordinate system. 

422 idPackerName : `str` 

423 Type of ID packer to construct from the registry. 

424 

425 Returns 

426 ------- 

427 measCat : `lsst.afw.table.SourceCatalog` 

428 Catalog of forced sources to measure. 

429 expId : `int` 

430 Unique binary id associated with the input exposure 

431 """ 

432 expId, expBits = exposureDataId.pack(idPackerName, returnMaxBits=True) 

433 idFactory = lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits) 

434 

435 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs, 

436 idFactory=idFactory) 

437 return measCat, expId 

438 

439 def runDataRef(self, dataRef, psfCache=None): 

440 """Perform forced measurement on a single exposure. 

441 

442 Parameters 

443 ---------- 

444 dataRef : `lsst.daf.persistence.ButlerDataRef` 

445 Passed to the ``references`` subtask to obtain the reference WCS, 

446 the ``getExposure`` method (implemented by derived classes) to 

447 read the measurment image, and the ``fetchReferences`` method to 

448 get the exposure and load the reference catalog (see 

449 :lsst-task`lsst.meas.base.references.CoaddSrcReferencesTask`). 

450 Refer to derived class documentation for details of the datasets 

451 and data ID keys which are used. 

452 psfCache : `int`, optional 

453 Size of PSF cache, or `None`. The size of the PSF cache can have 

454 a significant effect upon the runtime for complicated PSF models. 

455 

456 Notes 

457 ----- 

458 Sources are generated with ``generateMeasCat`` in the ``measurement`` 

459 subtask. These are passed to ``measurement``'s ``run`` method, which 

460 fills the source catalog with the forced measurement results. The 

461 sources are then passed to the ``writeOutputs`` method (implemented by 

462 derived classes) which writes the outputs. 

463 """ 

464 refWcs = self.references.getWcs(dataRef) 

465 exposure = self.getExposure(dataRef) 

466 if psfCache is not None: 

467 exposure.getPsf().setCacheSize(psfCache) 

468 refCat = self.fetchReferences(dataRef, exposure) 

469 

470 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs, 

471 idFactory=self.makeIdFactory(dataRef)) 

472 self.log.info("Performing forced measurement on %s" % (dataRef.dataId,)) 

473 self.attachFootprints(measCat, refCat, exposure, refWcs) 

474 

475 exposureId = self.getExposureId(dataRef) 

476 

477 forcedPhotResult = self.run(measCat, exposure, refCat, refWcs, exposureId=exposureId) 

478 

479 self.writeOutput(dataRef, forcedPhotResult.measCat) 

480 

481 def run(self, measCat, exposure, refCat, refWcs, exposureId=None): 

482 """Perform forced measurement on a single exposure. 

483 

484 Parameters 

485 ---------- 

486 measCat : `lsst.afw.table.SourceCatalog` 

487 The measurement catalog, based on the sources listed in the 

488 reference catalog. 

489 exposure : `lsst.afw.image.Exposure` 

490 The measurement image upon which to perform forced detection. 

491 refCat : `lsst.afw.table.SourceCatalog` 

492 The reference catalog of sources to measure. 

493 refWcs : `lsst.afw.image.SkyWcs` 

494 The WCS for the references. 

495 exposureId : `int` 

496 Optional unique exposureId used for random seed in measurement 

497 task. 

498 

499 Returns 

500 ------- 

501 result : `lsst.pipe.base.Struct` 

502 Structure with fields: 

503 

504 ``measCat`` 

505 Catalog of forced measurement results 

506 (`lsst.afw.table.SourceCatalog`). 

507 """ 

508 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId) 

509 if self.config.doApCorr: 

510 self.applyApCorr.run( 

511 catalog=measCat, 

512 apCorrMap=exposure.getInfo().getApCorrMap() 

513 ) 

514 self.catalogCalculation.run(measCat) 

515 

516 return pipeBase.Struct(measCat=measCat) 

517 

518 def makeIdFactory(self, dataRef): 

519 """Create an object that generates globally unique source IDs. 

520 

521 Source IDs are created based on a per-CCD ID and the ID of the CCD 

522 itself. 

523 

524 Parameters 

525 ---------- 

526 dataRef : `lsst.daf.persistence.ButlerDataRef` 

527 Butler data reference. The ``ccdExposureId_bits`` and 

528 ``ccdExposureId`` datasets are accessed. The data ID must have the 

529 keys that correspond to ``ccdExposureId``, which are generally the 

530 same as those that correspond to ``calexp`` (``visit``, ``raft``, 

531 ``sensor`` for LSST data). 

532 """ 

533 expBits = dataRef.get("ccdExposureId_bits") 

534 expId = int(dataRef.get("ccdExposureId")) 

535 return lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits) 

536 

537 def getExposureId(self, dataRef): 

538 return int(dataRef.get("ccdExposureId", immediate=True)) 

539 

540 def fetchReferences(self, dataRef, exposure): 

541 """Get sources that overlap the exposure. 

542 

543 Parameters 

544 ---------- 

545 dataRef : `lsst.daf.persistence.ButlerDataRef` 

546 Butler data reference corresponding to the image to be measured; 

547 should have ``tract``, ``patch``, and ``filter`` keys. 

548 exposure : `lsst.afw.image.Exposure` 

549 The image to be measured (used only to obtain a WCS and bounding 

550 box). 

551 

552 Returns 

553 ------- 

554 referencs : `lsst.afw.table.SourceCatalog` 

555 Catalog of sources that overlap the exposure 

556 

557 Notes 

558 ----- 

559 The returned catalog is sorted by ID and guarantees that all included 

560 children have their parent included and that all Footprints are valid. 

561 

562 All work is delegated to the references subtask; see 

563 :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` 

564 for information about the default behavior. 

565 """ 

566 references = lsst.afw.table.SourceCatalog(self.references.schema) 

567 badParents = set() 

568 unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs()) 

569 for record in unfiltered: 

570 if record.getFootprint() is None or record.getFootprint().getArea() == 0: 

571 if record.getParent() != 0: 

572 self.log.warn("Skipping reference %s (child of %s) with bad Footprint", 

573 record.getId(), record.getParent()) 

574 else: 

575 self.log.warn("Skipping reference parent %s with bad Footprint", record.getId()) 

576 badParents.add(record.getId()) 

577 elif record.getParent() not in badParents: 

578 references.append(record) 

579 # catalog must be sorted by parent ID for lsst.afw.table.getChildren to work 

580 references.sort(lsst.afw.table.SourceTable.getParentKey()) 

581 return references 

582 

583 def attachFootprints(self, sources, refCat, exposure, refWcs): 

584 r"""Attach footprints to blank sources prior to measurements. 

585 

586 Notes 

587 ----- 

588 `~lsst.afw.detection.Footprint`\ s for forced photometry must be in the 

589 pixel coordinate system of the image being measured, while the actual 

590 detections may start out in a different coordinate system. 

591 

592 Subclasses of this class must implement this method to define how 

593 those `~lsst.afw.detection.Footprint`\ s should be generated. 

594 

595 This default implementation transforms the 

596 `~lsst.afw.detection.Footprint`\ s from the reference catalog from the 

597 reference WCS to the exposure's WcS, which downgrades 

598 `lsst.afw.detection.heavyFootprint.HeavyFootprint`\ s into regular 

599 `~lsst.afw.detection.Footprint`\ s, destroying deblend information. 

600 """ 

601 return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs) 

602 

603 def getExposure(self, dataRef): 

604 """Read input exposure for measurement. 

605 

606 Parameters 

607 ---------- 

608 dataRef : `lsst.daf.persistence.ButlerDataRef` 

609 Butler data reference. 

610 """ 

611 exposure = dataRef.get(self.dataPrefix + "calexp", immediate=True) 

612 

613 if self.config.doApplyExternalPhotoCalib: 

614 source = f"{self.config.externalPhotoCalibName}_photoCalib" 

615 self.log.info("Applying external photoCalib from %s", source) 

616 photoCalib = dataRef.get(source) 

617 exposure.setPhotoCalib(photoCalib) # No need for calibrateImage; having the photoCalib suffices 

618 

619 if self.config.doApplyExternalSkyWcs: 

620 source = f"{self.config.externalSkyWcsName}_wcs" 

621 self.log.info("Applying external skyWcs from %s", source) 

622 skyWcs = dataRef.get(source) 

623 exposure.setWcs(skyWcs) 

624 

625 if self.config.doApplySkyCorr: 

626 self.log.info("Apply sky correction") 

627 skyCorr = dataRef.get("skyCorr") 

628 exposure.maskedImage -= skyCorr.getImage() 

629 

630 return exposure 

631 

632 def writeOutput(self, dataRef, sources): 

633 """Write forced source table 

634 

635 Parameters 

636 ---------- 

637 dataRef : `lsst.daf.persistence.ButlerDataRef` 

638 Butler data reference. The forced_src dataset (with 

639 self.dataPrefix prepended) is all that will be modified. 

640 sources : `lsst.afw.table.SourceCatalog` 

641 Catalog of sources to save. 

642 """ 

643 dataRef.put(sources, self.dataPrefix + "forced_src", flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS) 

644 

645 def getSchemaCatalogs(self): 

646 """The schema catalogs that will be used by this task. 

647 

648 Returns 

649 ------- 

650 schemaCatalogs : `dict` 

651 Dictionary mapping dataset type to schema catalog. 

652 

653 Notes 

654 ----- 

655 There is only one schema for each type of forced measurement. The 

656 dataset type for this measurement is defined in the mapper. 

657 """ 

658 catalog = lsst.afw.table.SourceCatalog(self.measurement.schema) 

659 catalog.getTable().setMetadata(self.measurement.algMetadata) 

660 datasetType = self.dataPrefix + "forced_src" 

661 return {datasetType: catalog} 

662 

663 def _getConfigName(self): 

664 # Documented in superclass. 

665 return self.dataPrefix + "forcedPhotCcd_config" 

666 

667 def _getMetadataName(self): 

668 # Documented in superclass 

669 return self.dataPrefix + "forcedPhotCcd_metadata" 

670 

671 @classmethod 

672 def _makeArgumentParser(cls): 

673 parser = pipeBase.ArgumentParser(name=cls._DefaultName) 

674 parser.add_id_argument("--id", "forced_src", help="data ID with raw CCD keys [+ tract optionally], " 

675 "e.g. --id visit=12345 ccd=1,2 [tract=0]", 

676 ContainerClass=PerTractCcdDataIdContainer) 

677 return parser