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 # ForcedPhotImage options 

195 references = lsst.pex.config.ConfigurableField( 

196 target=MultiBandReferencesTask, 

197 doc="subtask to retrieve reference source catalog" 

198 ) 

199 measurement = lsst.pex.config.ConfigurableField( 

200 target=ForcedMeasurementTask, 

201 doc="subtask to do forced measurement" 

202 ) 

203 coaddName = lsst.pex.config.Field( 

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

205 dtype=str, 

206 default="deep", 

207 ) 

208 doApCorr = lsst.pex.config.Field( 

209 dtype=bool, 

210 default=True, 

211 doc="Run subtask to apply aperture corrections" 

212 ) 

213 applyApCorr = lsst.pex.config.ConfigurableField( 

214 target=ApplyApCorrTask, 

215 doc="Subtask to apply aperture corrections" 

216 ) 

217 catalogCalculation = lsst.pex.config.ConfigurableField( 

218 target=CatalogCalculationTask, 

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

220 ) 

221 doApplyUberCal = lsst.pex.config.Field( 

222 dtype=bool, 

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

224 default=False, 

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

226 ) 

227 doApplyExternalPhotoCalib = lsst.pex.config.Field( 

228 dtype=bool, 

229 default=False, 

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

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

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

233 "to load."), 

234 ) 

235 doApplyExternalSkyWcs = lsst.pex.config.Field( 

236 dtype=bool, 

237 default=False, 

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

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

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

241 ) 

242 doApplySkyCorr = lsst.pex.config.Field( 

243 dtype=bool, 

244 default=False, 

245 doc="Apply sky correction?", 

246 ) 

247 includePhotoCalibVar = lsst.pex.config.Field( 

248 dtype=bool, 

249 default=False, 

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

251 ) 

252 externalPhotoCalibName = lsst.pex.config.ChoiceField( 

253 dtype=str, 

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

255 "Unused for Gen3 middleware."), 

256 default="jointcal", 

257 allowed={ 

258 "jointcal": "Use jointcal_photoCalib", 

259 "fgcm": "Use fgcm_photoCalib", 

260 "fgcm_tract": "Use fgcm_tract_photoCalib" 

261 }, 

262 ) 

263 externalSkyWcsName = lsst.pex.config.ChoiceField( 

264 dtype=str, 

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

266 default="jointcal", 

267 allowed={ 

268 "jointcal": "Use jointcal_wcs" 

269 }, 

270 ) 

271 

272 def setDefaults(self): 

273 # Docstring inherited. 

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

275 # ForcedMeasurementTask 

276 super().setDefaults() 

277 

278 self.catalogCalculation.plugins.names = [] 

279 

280 

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

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

283 

284 Parameters 

285 ---------- 

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

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

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

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

290 precedence. 

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

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

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

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

295 **kwds 

296 Keyword arguments are passed to the supertask constructor. 

297 

298 Notes 

299 ----- 

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

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

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

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

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

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

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

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

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

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

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

311 the ``filter`` option in the configuration of 

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

313 """ 

314 

315 ConfigClass = ForcedPhotCcdConfig 

316 RunnerClass = pipeBase.ButlerInitializedTaskRunner 

317 _DefaultName = "forcedPhotCcd" 

318 dataPrefix = "" 

319 

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

321 super().__init__(**kwds) 

322 

323 if initInputs is not None: 

324 refSchema = initInputs['inputSchema'].schema 

325 

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

327 if refSchema is None: 

328 refSchema = self.references.schema 

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

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

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

332 if self.config.doApCorr: 

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

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

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

336 

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

338 inputs = butlerQC.get(inputRefs) 

339 

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

341 skyMap = inputs.pop("skyMap") 

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

343 

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

345 inputs['refWcs']) 

346 

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

348 inputs['exposure'], 

349 inputs['refCat'], inputs['refWcs'], 

350 "visit_detector") 

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

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

353 outputs = self.run(**inputs) 

354 butlerQC.put(outputs, outputRefs) 

355 

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

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

358 boundaries of the exposure. 

359 

360 Parameters 

361 ---------- 

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

363 Exposure to generate the catalog for. 

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

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

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

367 Reference world coordinate system. 

368 

369 Returns 

370 ------- 

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

372 Filtered catalog of forced sources to measure. 

373 

374 Notes 

375 ----- 

376 Filtering the reference catalog is currently handled by Gen2 

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

378 code segments to do the filtering and transformation. The 

379 majority of this code is based on the methods of 

380 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader 

381 

382 """ 

383 

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

385 # be performed on. 

386 expWcs = exposure.getWcs() 

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

388 expBBox = lsst.geom.Box2D(expRegion) 

389 expBoxCorners = expBBox.getCorners() 

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

391 corner in expBoxCorners] 

392 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners) 

393 

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

395 # not contained within the exposure boundaries, or whose 

396 # parents are not within the exposure boundaries. Note 

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

398 # appear before the children. 

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

400 for refCat in refCats: 

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

402 for record in refCat: 

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

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

405 mergedRefCat.append(record) 

406 containedIds.add(record.getId()) 

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

408 return mergedRefCat 

409 

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

411 """Generate a measurement catalog for Gen3. 

412 

413 Parameters 

414 ---------- 

415 exposureDataId : `DataId` 

416 Butler dataId for this exposure. 

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

418 Exposure to generate the catalog for. 

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

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

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

422 Reference world coordinate system. 

423 idPackerName : `str` 

424 Type of ID packer to construct from the registry. 

425 

426 Returns 

427 ------- 

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

429 Catalog of forced sources to measure. 

430 expId : `int` 

431 Unique binary id associated with the input exposure 

432 """ 

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

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

435 

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

437 idFactory=idFactory) 

438 return measCat, expId 

439 

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

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

442 

443 Parameters 

444 ---------- 

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

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

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

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

449 get the exposure and load the reference catalog (see 

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

451 Refer to derived class documentation for details of the datasets 

452 and data ID keys which are used. 

453 psfCache : `int`, optional 

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

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

456 

457 Notes 

458 ----- 

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

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

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

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

463 derived classes) which writes the outputs. 

464 """ 

465 refWcs = self.references.getWcs(dataRef) 

466 exposure = self.getExposure(dataRef) 

467 if psfCache is not None: 

468 exposure.getPsf().setCacheSize(psfCache) 

469 refCat = self.fetchReferences(dataRef, exposure) 

470 

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

472 idFactory=self.makeIdFactory(dataRef)) 

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

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

475 

476 exposureId = self.getExposureId(dataRef) 

477 

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

479 

480 self.writeOutput(dataRef, forcedPhotResult.measCat) 

481 

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

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

484 

485 Parameters 

486 ---------- 

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

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

489 reference catalog. 

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

491 The measurement image upon which to perform forced detection. 

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

493 The reference catalog of sources to measure. 

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

495 The WCS for the references. 

496 exposureId : `int` 

497 Optional unique exposureId used for random seed in measurement 

498 task. 

499 

500 Returns 

501 ------- 

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

503 Structure with fields: 

504 

505 ``measCat`` 

506 Catalog of forced measurement results 

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

508 """ 

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

510 if self.config.doApCorr: 

511 self.applyApCorr.run( 

512 catalog=measCat, 

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

514 ) 

515 self.catalogCalculation.run(measCat) 

516 

517 return pipeBase.Struct(measCat=measCat) 

518 

519 def makeIdFactory(self, dataRef): 

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

521 

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

523 itself. 

524 

525 Parameters 

526 ---------- 

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

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

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

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

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

532 ``sensor`` for LSST data). 

533 """ 

534 expBits = dataRef.get("ccdExposureId_bits") 

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

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

537 

538 def getExposureId(self, dataRef): 

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

540 

541 def fetchReferences(self, dataRef, exposure): 

542 """Get sources that overlap the exposure. 

543 

544 Parameters 

545 ---------- 

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

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

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

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

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

551 box). 

552 

553 Returns 

554 ------- 

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

556 Catalog of sources that overlap the exposure 

557 

558 Notes 

559 ----- 

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

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

562 

563 All work is delegated to the references subtask; see 

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

565 for information about the default behavior. 

566 """ 

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

568 badParents = set() 

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

570 for record in unfiltered: 

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

572 if record.getParent() != 0: 

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

574 record.getId(), record.getParent()) 

575 else: 

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

577 badParents.add(record.getId()) 

578 elif record.getParent() not in badParents: 

579 references.append(record) 

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

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

582 return references 

583 

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

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

586 

587 Notes 

588 ----- 

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

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

591 detections may start out in a different coordinate system. 

592 

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

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

595 

596 This default implementation transforms the 

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

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

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

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

601 """ 

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

603 

604 def getExposure(self, dataRef): 

605 """Read input exposure for measurement. 

606 

607 Parameters 

608 ---------- 

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

610 Butler data reference. 

611 """ 

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

613 

614 if self.config.doApplyExternalPhotoCalib: 

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

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

617 photoCalib = dataRef.get(source) 

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

619 

620 if self.config.doApplyExternalSkyWcs: 

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

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

623 skyWcs = dataRef.get(source) 

624 exposure.setWcs(skyWcs) 

625 

626 if self.config.doApplySkyCorr: 

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

628 skyCorr = dataRef.get("skyCorr") 

629 exposure.maskedImage -= skyCorr.getImage() 

630 

631 return exposure 

632 

633 def writeOutput(self, dataRef, sources): 

634 """Write forced source table 

635 

636 Parameters 

637 ---------- 

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

639 Butler data reference. The forced_src dataset (with 

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

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

642 Catalog of sources to save. 

643 """ 

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

645 

646 def getSchemaCatalogs(self): 

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

648 

649 Returns 

650 ------- 

651 schemaCatalogs : `dict` 

652 Dictionary mapping dataset type to schema catalog. 

653 

654 Notes 

655 ----- 

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

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

658 """ 

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

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

661 datasetType = self.dataPrefix + "forced_src" 

662 return {datasetType: catalog} 

663 

664 def _getConfigName(self): 

665 # Documented in superclass. 

666 return self.dataPrefix + "forcedPhotCcd_config" 

667 

668 def _getMetadataName(self): 

669 # Documented in superclass 

670 return self.dataPrefix + "forcedPhotCcd_metadata" 

671 

672 @classmethod 

673 def _makeArgumentParser(cls): 

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

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

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

677 ContainerClass=PerTractCcdDataIdContainer) 

678 return parser