Coverage for python/lsst/pipe/tasks/finalizeCharacterization.py: 15%

246 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-23 10:48 +0000

1# This file is part of pipe_tasks. 

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"""Task to run a finalized image characterization, using additional data. 

23""" 

24 

25__all__ = ['FinalizeCharacterizationConnections', 

26 'FinalizeCharacterizationConfig', 

27 'FinalizeCharacterizationTask'] 

28 

29import numpy as np 

30import esutil 

31import pandas as pd 

32 

33import lsst.pex.config as pexConfig 

34import lsst.pipe.base as pipeBase 

35import lsst.daf.base as dafBase 

36import lsst.afw.table as afwTable 

37import lsst.meas.algorithms as measAlg 

38import lsst.meas.extensions.piff.piffPsfDeterminer # noqa: F401 

39from lsst.meas.algorithms import MeasureApCorrTask 

40from lsst.meas.base import SingleFrameMeasurementTask, ApplyApCorrTask 

41from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry 

42 

43from .reserveIsolatedStars import ReserveIsolatedStarsTask 

44 

45 

46class FinalizeCharacterizationConnections(pipeBase.PipelineTaskConnections, 

47 dimensions=('instrument', 'visit',), 

48 defaultTemplates={}): 

49 src_schema = pipeBase.connectionTypes.InitInput( 

50 doc='Input schema used for src catalogs.', 

51 name='src_schema', 

52 storageClass='SourceCatalog', 

53 ) 

54 srcs = pipeBase.connectionTypes.Input( 

55 doc='Source catalogs for the visit', 

56 name='src', 

57 storageClass='SourceCatalog', 

58 dimensions=('instrument', 'visit', 'detector'), 

59 deferLoad=True, 

60 multiple=True, 

61 ) 

62 calexps = pipeBase.connectionTypes.Input( 

63 doc='Calexps for the visit', 

64 name='calexp', 

65 storageClass='ExposureF', 

66 dimensions=('instrument', 'visit', 'detector'), 

67 deferLoad=True, 

68 multiple=True, 

69 ) 

70 isolated_star_cats = pipeBase.connectionTypes.Input( 

71 doc=('Catalog of isolated stars with average positions, number of associated ' 

72 'sources, and indexes to the isolated_star_sources catalogs.'), 

73 name='isolated_star_cat', 

74 storageClass='DataFrame', 

75 dimensions=('instrument', 'tract', 'skymap'), 

76 deferLoad=True, 

77 multiple=True, 

78 ) 

79 isolated_star_sources = pipeBase.connectionTypes.Input( 

80 doc=('Catalog of isolated star sources with sourceIds, and indexes to the ' 

81 'isolated_star_cats catalogs.'), 

82 name='isolated_star_sources', 

83 storageClass='DataFrame', 

84 dimensions=('instrument', 'tract', 'skymap'), 

85 deferLoad=True, 

86 multiple=True, 

87 ) 

88 finalized_psf_ap_corr_cat = pipeBase.connectionTypes.Output( 

89 doc=('Per-visit finalized psf models and aperture corrections. This ' 

90 'catalog uses detector id for the id and are sorted for fast ' 

91 'lookups of a detector.'), 

92 name='finalized_psf_ap_corr_catalog', 

93 storageClass='ExposureCatalog', 

94 dimensions=('instrument', 'visit'), 

95 ) 

96 finalized_src_table = pipeBase.connectionTypes.Output( 

97 doc=('Per-visit catalog of measurements for psf/flag/etc.'), 

98 name='finalized_src_table', 

99 storageClass='DataFrame', 

100 dimensions=('instrument', 'visit'), 

101 ) 

102 

103 

104class FinalizeCharacterizationConfig(pipeBase.PipelineTaskConfig, 

105 pipelineConnections=FinalizeCharacterizationConnections): 

106 """Configuration for FinalizeCharacterizationTask.""" 

107 source_selector = sourceSelectorRegistry.makeField( 

108 doc="How to select sources", 

109 default="science" 

110 ) 

111 id_column = pexConfig.Field( 

112 doc='Name of column in isolated_star_sources with source id.', 

113 dtype=str, 

114 default='sourceId', 

115 ) 

116 reserve_selection = pexConfig.ConfigurableField( 

117 target=ReserveIsolatedStarsTask, 

118 doc='Task to select reserved stars', 

119 ) 

120 make_psf_candidates = pexConfig.ConfigurableField( 

121 target=measAlg.MakePsfCandidatesTask, 

122 doc='Task to make psf candidates from selected stars.', 

123 ) 

124 psf_determiner = measAlg.psfDeterminerRegistry.makeField( 

125 'PSF Determination algorithm', 

126 default='piff' 

127 ) 

128 measurement = pexConfig.ConfigurableField( 

129 target=SingleFrameMeasurementTask, 

130 doc='Measure sources for aperture corrections' 

131 ) 

132 measure_ap_corr = pexConfig.ConfigurableField( 

133 target=MeasureApCorrTask, 

134 doc="Subtask to measure aperture corrections" 

135 ) 

136 apply_ap_corr = pexConfig.ConfigurableField( 

137 target=ApplyApCorrTask, 

138 doc="Subtask to apply aperture corrections" 

139 ) 

140 

141 def setDefaults(self): 

142 super().setDefaults() 

143 

144 source_selector = self.source_selector['science'] 

145 source_selector.setDefaults() 

146 

147 # We use the source selector only to select out flagged objects 

148 # and signal-to-noise. Isolated, unresolved sources are handled 

149 # by the isolated star catalog. 

150 

151 source_selector.doFlags = True 

152 source_selector.doSignalToNoise = True 

153 source_selector.doFluxLimit = False 

154 source_selector.doUnresolved = False 

155 source_selector.doIsolated = False 

156 

157 source_selector.signalToNoise.minimum = 20.0 

158 source_selector.signalToNoise.maximum = 1000.0 

159 

160 source_selector.signalToNoise.fluxField = 'base_GaussianFlux_instFlux' 

161 source_selector.signalToNoise.errField = 'base_GaussianFlux_instFluxErr' 

162 

163 source_selector.flags.bad = ['base_PixelFlags_flag_edge', 

164 'base_PixelFlags_flag_interpolatedCenter', 

165 'base_PixelFlags_flag_saturatedCenter', 

166 'base_PixelFlags_flag_crCenter', 

167 'base_PixelFlags_flag_bad', 

168 'base_PixelFlags_flag_interpolated', 

169 'base_PixelFlags_flag_saturated', 

170 'slot_Centroid_flag', 

171 'base_GaussianFlux_flag'] 

172 

173 # Configure aperture correction to select only high s/n sources (that 

174 # were used in the psf modeling) to avoid background problems when 

175 # computing the aperture correction map. 

176 self.measure_ap_corr.sourceSelector = 'science' 

177 

178 ap_selector = self.measure_ap_corr.sourceSelector['science'] 

179 # We do not need to filter flags or unresolved because we have used 

180 # the filtered isolated stars as an input 

181 ap_selector.doFlags = False 

182 ap_selector.doUnresolved = False 

183 

184 import lsst.meas.modelfit # noqa: F401 

185 import lsst.meas.extensions.photometryKron # noqa: F401 

186 import lsst.meas.extensions.convolved # noqa: F401 

187 import lsst.meas.extensions.gaap # noqa: F401 

188 import lsst.meas.extensions.shapeHSM # noqa: F401 

189 

190 # Set up measurement defaults 

191 self.measurement.plugins.names = [ 

192 'base_PsfFlux', 

193 'base_GaussianFlux', 

194 'modelfit_DoubleShapeletPsfApprox', 

195 'modelfit_CModel', 

196 'ext_photometryKron_KronFlux', 

197 'ext_convolved_ConvolvedFlux', 

198 'ext_gaap_GaapFlux', 

199 'ext_shapeHSM_HsmShapeRegauss', 

200 'ext_shapeHSM_HsmSourceMoments', 

201 'ext_shapeHSM_HsmPsfMoments', 

202 'ext_shapeHSM_HsmSourceMomentsRound', 

203 ] 

204 self.measurement.slots.modelFlux = 'modelfit_CModel' 

205 self.measurement.plugins['ext_convolved_ConvolvedFlux'].seeing.append(8.0) 

206 self.measurement.plugins['ext_gaap_GaapFlux'].sigmas = [ 

207 0.5, 

208 0.7, 

209 1.0, 

210 1.5, 

211 2.5, 

212 3.0 

213 ] 

214 self.measurement.plugins['ext_gaap_GaapFlux'].doPsfPhotometry = True 

215 self.measurement.slots.shape = 'ext_shapeHSM_HsmSourceMoments' 

216 self.measurement.slots.psfShape = 'ext_shapeHSM_HsmPsfMoments' 

217 self.measurement.plugins['ext_shapeHSM_HsmShapeRegauss'].deblendNChild = "" 

218 # Turn off slot setting for measurement for centroid and shape 

219 # (for which we use the input src catalog measurements) 

220 self.measurement.slots.centroid = None 

221 self.measurement.slots.apFlux = None 

222 self.measurement.slots.calibFlux = None 

223 

224 names = self.measurement.plugins['ext_convolved_ConvolvedFlux'].getAllResultNames() 

225 self.measure_ap_corr.allowFailure += names 

226 names = self.measurement.plugins["ext_gaap_GaapFlux"].getAllGaapResultNames() 

227 self.measure_ap_corr.allowFailure += names 

228 

229 

230class FinalizeCharacterizationTask(pipeBase.PipelineTask): 

231 """Run final characterization on exposures.""" 

232 ConfigClass = FinalizeCharacterizationConfig 

233 _DefaultName = 'finalize_characterization' 

234 

235 def __init__(self, initInputs=None, **kwargs): 

236 super().__init__(initInputs=initInputs, **kwargs) 

237 

238 self.schema_mapper, self.schema = self._make_output_schema_mapper( 

239 initInputs['src_schema'].schema 

240 ) 

241 

242 self.makeSubtask('reserve_selection') 

243 self.makeSubtask('source_selector') 

244 self.makeSubtask('make_psf_candidates') 

245 self.makeSubtask('psf_determiner') 

246 self.makeSubtask('measurement', schema=self.schema) 

247 self.makeSubtask('measure_ap_corr', schema=self.schema) 

248 self.makeSubtask('apply_ap_corr', schema=self.schema) 

249 

250 # Only log warning and fatal errors from the source_selector 

251 self.source_selector.log.setLevel(self.source_selector.log.WARN) 

252 

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

254 input_handle_dict = butlerQC.get(inputRefs) 

255 

256 band = butlerQC.quantum.dataId['band'] 

257 visit = butlerQC.quantum.dataId['visit'] 

258 

259 src_dict_temp = {handle.dataId['detector']: handle 

260 for handle in input_handle_dict['srcs']} 

261 calexp_dict_temp = {handle.dataId['detector']: handle 

262 for handle in input_handle_dict['calexps']} 

263 isolated_star_cat_dict_temp = {handle.dataId['tract']: handle 

264 for handle in input_handle_dict['isolated_star_cats']} 

265 isolated_star_source_dict_temp = {handle.dataId['tract']: handle 

266 for handle in input_handle_dict['isolated_star_sources']} 

267 # TODO: Sort until DM-31701 is done and we have deterministic 

268 # dataset ordering. 

269 src_dict = {detector: src_dict_temp[detector] for 

270 detector in sorted(src_dict_temp.keys())} 

271 calexp_dict = {detector: calexp_dict_temp[detector] for 

272 detector in sorted(calexp_dict_temp.keys())} 

273 isolated_star_cat_dict = {tract: isolated_star_cat_dict_temp[tract] for 

274 tract in sorted(isolated_star_cat_dict_temp.keys())} 

275 isolated_star_source_dict = {tract: isolated_star_source_dict_temp[tract] for 

276 tract in sorted(isolated_star_source_dict_temp.keys())} 

277 

278 struct = self.run(visit, 

279 band, 

280 isolated_star_cat_dict, 

281 isolated_star_source_dict, 

282 src_dict, 

283 calexp_dict) 

284 

285 butlerQC.put(struct.psf_ap_corr_cat, 

286 outputRefs.finalized_psf_ap_corr_cat) 

287 butlerQC.put(pd.DataFrame(struct.output_table), 

288 outputRefs.finalized_src_table) 

289 

290 def run(self, visit, band, isolated_star_cat_dict, isolated_star_source_dict, src_dict, calexp_dict): 

291 """ 

292 Run the FinalizeCharacterizationTask. 

293 

294 Parameters 

295 ---------- 

296 visit : `int` 

297 Visit number. Used in the output catalogs. 

298 band : `str` 

299 Band name. Used to select reserved stars. 

300 isolated_star_cat_dict : `dict` 

301 Per-tract dict of isolated star catalog handles. 

302 isolated_star_source_dict : `dict` 

303 Per-tract dict of isolated star source catalog handles. 

304 src_dict : `dict` 

305 Per-detector dict of src catalog handles. 

306 calexp_dict : `dict` 

307 Per-detector dict of calibrated exposure handles. 

308 

309 Returns 

310 ------- 

311 struct : `lsst.pipe.base.struct` 

312 Struct with outputs for persistence. 

313 """ 

314 # We do not need the isolated star table in this task. 

315 # However, it is used in tests to confirm consistency of indexes. 

316 _, isolated_source_table = self.concat_isolated_star_cats( 

317 band, 

318 isolated_star_cat_dict, 

319 isolated_star_source_dict 

320 ) 

321 

322 exposure_cat_schema = afwTable.ExposureTable.makeMinimalSchema() 

323 exposure_cat_schema.addField('visit', type='L', doc='Visit number') 

324 

325 metadata = dafBase.PropertyList() 

326 metadata.add("COMMENT", "Catalog id is detector id, sorted.") 

327 metadata.add("COMMENT", "Only detectors with data have entries.") 

328 

329 psf_ap_corr_cat = afwTable.ExposureCatalog(exposure_cat_schema) 

330 psf_ap_corr_cat.setMetadata(metadata) 

331 

332 measured_src_tables = [] 

333 

334 for detector in src_dict: 

335 src = src_dict[detector].get() 

336 exposure = calexp_dict[detector].get() 

337 

338 psf, ap_corr_map, measured_src = self.compute_psf_and_ap_corr_map( 

339 visit, 

340 detector, 

341 exposure, 

342 src, 

343 isolated_source_table 

344 ) 

345 

346 # And now we package it together... 

347 record = psf_ap_corr_cat.addNew() 

348 record['id'] = int(detector) 

349 record['visit'] = visit 

350 if psf is not None: 

351 record.setPsf(psf) 

352 if ap_corr_map is not None: 

353 record.setApCorrMap(ap_corr_map) 

354 

355 measured_src['visit'][:] = visit 

356 measured_src['detector'][:] = detector 

357 

358 measured_src_tables.append(measured_src.asAstropy().as_array()) 

359 

360 measured_src_table = np.concatenate(measured_src_tables) 

361 

362 return pipeBase.Struct(psf_ap_corr_cat=psf_ap_corr_cat, 

363 output_table=measured_src_table) 

364 

365 def _make_output_schema_mapper(self, input_schema): 

366 """Make the schema mapper from the input schema to the output schema. 

367 

368 Parameters 

369 ---------- 

370 input_schema : `lsst.afw.table.Schema` 

371 Input schema. 

372 

373 Returns 

374 ------- 

375 mapper : `lsst.afw.table.SchemaMapper` 

376 Schema mapper 

377 output_schema : `lsst.afw.table.Schema` 

378 Output schema (with alias map) 

379 """ 

380 mapper = afwTable.SchemaMapper(input_schema) 

381 mapper.addMinimalSchema(afwTable.SourceTable.makeMinimalSchema()) 

382 mapper.addMapping(input_schema['slot_Centroid_x'].asKey()) 

383 mapper.addMapping(input_schema['slot_Centroid_y'].asKey()) 

384 

385 # The aperture fields may be used by the psf determiner. 

386 aper_fields = input_schema.extract('base_CircularApertureFlux_*') 

387 for field, item in aper_fields.items(): 

388 mapper.addMapping(item.key) 

389 

390 # The following two may be redundant, but then the mapping is a no-op. 

391 apflux_fields = input_schema.extract('slot_ApFlux_*') 

392 for field, item in apflux_fields.items(): 

393 mapper.addMapping(item.key) 

394 

395 calibflux_fields = input_schema.extract('slot_CalibFlux_*') 

396 for field, item in calibflux_fields.items(): 

397 mapper.addMapping(item.key) 

398 

399 mapper.addMapping( 

400 input_schema[self.config.source_selector.active.signalToNoise.fluxField].asKey(), 

401 'calib_psf_selection_flux') 

402 mapper.addMapping( 

403 input_schema[self.config.source_selector.active.signalToNoise.errField].asKey(), 

404 'calib_psf_selection_flux_err') 

405 

406 output_schema = mapper.getOutputSchema() 

407 

408 output_schema.addField( 

409 'calib_psf_candidate', 

410 type='Flag', 

411 doc=('set if the source was a candidate for PSF determination, ' 

412 'as determined from FinalizeCharacterizationTask.'), 

413 ) 

414 output_schema.addField( 

415 'calib_psf_reserved', 

416 type='Flag', 

417 doc=('set if source was reserved from PSF determination by ' 

418 'FinalizeCharacterizationTask.'), 

419 ) 

420 output_schema.addField( 

421 'calib_psf_used', 

422 type='Flag', 

423 doc=('set if source was used in the PSF determination by ' 

424 'FinalizeCharacterizationTask.'), 

425 ) 

426 output_schema.addField( 

427 'visit', 

428 type=np.int64, 

429 doc='Visit number for the sources.', 

430 ) 

431 output_schema.addField( 

432 'detector', 

433 type=np.int32, 

434 doc='Detector number for the sources.', 

435 ) 

436 

437 alias_map = input_schema.getAliasMap() 

438 alias_map_output = afwTable.AliasMap() 

439 alias_map_output.set('slot_Centroid', alias_map.get('slot_Centroid')) 

440 alias_map_output.set('slot_ApFlux', alias_map.get('slot_ApFlux')) 

441 alias_map_output.set('slot_CalibFlux', alias_map.get('slot_CalibFlux')) 

442 

443 output_schema.setAliasMap(alias_map_output) 

444 

445 return mapper, output_schema 

446 

447 def _make_selection_schema_mapper(self, input_schema): 

448 """Make the schema mapper from the input schema to the selection schema. 

449 

450 Parameters 

451 ---------- 

452 input_schema : `lsst.afw.table.Schema` 

453 Input schema. 

454 

455 Returns 

456 ------- 

457 mapper : `lsst.afw.table.SchemaMapper` 

458 Schema mapper 

459 selection_schema : `lsst.afw.table.Schema` 

460 Selection schema (with alias map) 

461 """ 

462 mapper = afwTable.SchemaMapper(input_schema) 

463 mapper.addMinimalSchema(input_schema) 

464 

465 selection_schema = mapper.getOutputSchema() 

466 

467 selection_schema.setAliasMap(input_schema.getAliasMap()) 

468 

469 return mapper, selection_schema 

470 

471 def concat_isolated_star_cats(self, band, isolated_star_cat_dict, isolated_star_source_dict): 

472 """ 

473 Concatenate isolated star catalogs and make reserve selection. 

474 

475 Parameters 

476 ---------- 

477 band : `str` 

478 Band name. Used to select reserved stars. 

479 isolated_star_cat_dict : `dict` 

480 Per-tract dict of isolated star catalog handles. 

481 isolated_star_source_dict : `dict` 

482 Per-tract dict of isolated star source catalog handles. 

483 

484 Returns 

485 ------- 

486 isolated_table : `np.ndarray` (N,) 

487 Table of isolated stars, with indexes to isolated sources. 

488 isolated_source_table : `np.ndarray` (M,) 

489 Table of isolated sources, with indexes to isolated stars. 

490 """ 

491 isolated_tables = [] 

492 isolated_sources = [] 

493 merge_cat_counter = 0 

494 merge_source_counter = 0 

495 

496 for tract in isolated_star_cat_dict: 

497 df_cat = isolated_star_cat_dict[tract].get() 

498 table_cat = df_cat.to_records() 

499 

500 df_source = isolated_star_source_dict[tract].get( 

501 parameters={'columns': [self.config.id_column, 

502 'obj_index']} 

503 ) 

504 table_source = df_source.to_records() 

505 

506 # Cut isolated star table to those observed in this band, and adjust indexes 

507 (use_band,) = (table_cat[f'nsource_{band}'] > 0).nonzero() 

508 

509 if len(use_band) == 0: 

510 # There are no sources in this band in this tract. 

511 self.log.info("No sources found in %s band in tract %d.", band, tract) 

512 continue 

513 

514 # With the following matching: 

515 # table_source[b] <-> table_cat[use_band[a]] 

516 obj_index = table_source['obj_index'][:] 

517 a, b = esutil.numpy_util.match(use_band, obj_index) 

518 

519 # Update indexes and cut to band-selected stars/sources 

520 table_source['obj_index'][b] = a 

521 _, index_new = np.unique(a, return_index=True) 

522 table_cat[f'source_cat_index_{band}'][use_band] = index_new 

523 

524 # After the following cuts, the catalogs have the following properties: 

525 # - table_cat only contains isolated stars that have at least one source 

526 # in ``band``. 

527 # - table_source only contains ``band`` sources. 

528 # - The slice table_cat["source_cat_index_{band}"]: table_cat["source_cat_index_{band}"] 

529 # + table_cat["nsource_{band}] 

530 # applied to table_source will give all the sources associated with the star. 

531 # - For each source, table_source["obj_index"] points to the index of the associated 

532 # isolated star. 

533 table_source = table_source[b] 

534 table_cat = table_cat[use_band] 

535 

536 # Add reserved flag column to tables 

537 table_cat = np.lib.recfunctions.append_fields( 

538 table_cat, 

539 'reserved', 

540 np.zeros(table_cat.size, dtype=bool), 

541 usemask=False 

542 ) 

543 table_source = np.lib.recfunctions.append_fields( 

544 table_source, 

545 'reserved', 

546 np.zeros(table_source.size, dtype=bool), 

547 usemask=False 

548 ) 

549 

550 # Get reserve star flags 

551 table_cat['reserved'][:] = self.reserve_selection.run( 

552 len(table_cat), 

553 extra=f'{band}_{tract}', 

554 ) 

555 table_source['reserved'][:] = table_cat['reserved'][table_source['obj_index']] 

556 

557 # Offset indexes to account for tract merging 

558 table_cat[f'source_cat_index_{band}'] += merge_source_counter 

559 table_source['obj_index'] += merge_cat_counter 

560 

561 isolated_tables.append(table_cat) 

562 isolated_sources.append(table_source) 

563 

564 merge_cat_counter += len(table_cat) 

565 merge_source_counter += len(table_source) 

566 

567 isolated_table = np.concatenate(isolated_tables) 

568 isolated_source_table = np.concatenate(isolated_sources) 

569 

570 return isolated_table, isolated_source_table 

571 

572 def compute_psf_and_ap_corr_map(self, visit, detector, exposure, src, isolated_source_table): 

573 """Compute psf model and aperture correction map for a single exposure. 

574 

575 Parameters 

576 ---------- 

577 visit : `int` 

578 Visit number (for logging). 

579 detector : `int` 

580 Detector number (for logging). 

581 exposure : `lsst.afw.image.ExposureF` 

582 src : `lsst.afw.table.SourceCatalog` 

583 isolated_source_table : `np.ndarray` 

584 

585 Returns 

586 ------- 

587 psf : `lsst.meas.algorithms.ImagePsf` 

588 PSF Model 

589 ap_corr_map : `lsst.afw.image.ApCorrMap` 

590 Aperture correction map. 

591 measured_src : `lsst.afw.table.SourceCatalog` 

592 Updated source catalog with measurements, flags and aperture corrections. 

593 """ 

594 # Apply source selector (s/n, flags, etc.) 

595 good_src = self.source_selector.selectSources(src) 

596 

597 # Cut down input src to the selected sources 

598 # We use a separate schema/mapper here than for the output/measurement catalog because of 

599 # clashes between fields that were previously run and those that need to be rerun with 

600 # the new psf model. This may be slightly inefficient but keeps input 

601 # and output values cleanly separated. 

602 selection_mapper, selection_schema = self._make_selection_schema_mapper(src.schema) 

603 

604 selected_src = afwTable.SourceCatalog(selection_schema) 

605 selected_src.reserve(good_src.selected.sum()) 

606 selected_src.extend(src[good_src.selected], mapper=selection_mapper) 

607 

608 # The calib flags have been copied from the input table, 

609 # and we reset them here just to ensure they aren't propagated. 

610 selected_src['calib_psf_candidate'] = np.zeros(len(selected_src), dtype=bool) 

611 selected_src['calib_psf_used'] = np.zeros(len(selected_src), dtype=bool) 

612 selected_src['calib_psf_reserved'] = np.zeros(len(selected_src), dtype=bool) 

613 

614 # Find the isolated sources and set flags 

615 matched_src, matched_iso = esutil.numpy_util.match( 

616 selected_src['id'], 

617 isolated_source_table[self.config.id_column] 

618 ) 

619 

620 matched_arr = np.zeros(len(selected_src), dtype=bool) 

621 matched_arr[matched_src] = True 

622 selected_src['calib_psf_candidate'] = matched_arr 

623 

624 reserved_arr = np.zeros(len(selected_src), dtype=bool) 

625 reserved_arr[matched_src] = isolated_source_table['reserved'][matched_iso] 

626 selected_src['calib_psf_reserved'] = reserved_arr 

627 

628 selected_src = selected_src[selected_src['calib_psf_candidate']].copy(deep=True) 

629 

630 # Make the measured source catalog as well, based on the selected catalog. 

631 measured_src = afwTable.SourceCatalog(self.schema) 

632 measured_src.reserve(len(selected_src)) 

633 measured_src.extend(selected_src, mapper=self.schema_mapper) 

634 

635 # We need to copy over the calib_psf flags because they were not in the mapper 

636 measured_src['calib_psf_candidate'] = selected_src['calib_psf_candidate'] 

637 measured_src['calib_psf_reserved'] = selected_src['calib_psf_reserved'] 

638 

639 # Select the psf candidates from the selection catalog 

640 try: 

641 psf_selection_result = self.make_psf_candidates.run(selected_src, exposure=exposure) 

642 except Exception as e: 

643 self.log.warning('Failed to make psf candidates for visit %d, detector %d: %s', 

644 visit, detector, e) 

645 return None, None, measured_src 

646 

647 psf_cand_cat = psf_selection_result.goodStarCat 

648 

649 # Make list of psf candidates to send to the determiner 

650 # (omitting those marked as reserved) 

651 psf_determiner_list = [cand for cand, use 

652 in zip(psf_selection_result.psfCandidates, 

653 ~psf_cand_cat['calib_psf_reserved']) if use] 

654 flag_key = psf_cand_cat.schema['calib_psf_used'].asKey() 

655 try: 

656 psf, cell_set = self.psf_determiner.determinePsf(exposure, 

657 psf_determiner_list, 

658 self.metadata, 

659 flagKey=flag_key) 

660 except Exception as e: 

661 self.log.warning('Failed to determine psf for visit %d, detector %d: %s', 

662 visit, detector, e) 

663 return None, None, measured_src 

664 

665 # Set the psf in the exposure for measurement/aperture corrections. 

666 exposure.setPsf(psf) 

667 

668 # At this point, we need to transfer the psf used flag from the selection 

669 # catalog to the measurement catalog. 

670 matched_selected, matched_measured = esutil.numpy_util.match( 

671 selected_src['id'], 

672 measured_src['id'] 

673 ) 

674 measured_used = np.zeros(len(measured_src), dtype=bool) 

675 measured_used[matched_measured] = selected_src['calib_psf_used'][matched_selected] 

676 measured_src['calib_psf_used'] = measured_used 

677 

678 # Next, we do the measurement on all the psf candidate, used, and reserved stars. 

679 try: 

680 self.measurement.run(measCat=measured_src, exposure=exposure) 

681 except Exception as e: 

682 self.log.warning('Failed to make measurements for visit %d, detector %d: %s', 

683 visit, detector, e) 

684 return psf, None, measured_src 

685 

686 # And finally the ap corr map. 

687 try: 

688 ap_corr_map = self.measure_ap_corr.run(exposure=exposure, 

689 catalog=measured_src).apCorrMap 

690 except Exception as e: 

691 self.log.warning('Failed to compute aperture corrections for visit %d, detector %d: %s', 

692 visit, detector, e) 

693 return psf, None, measured_src 

694 

695 self.apply_ap_corr.run(catalog=measured_src, apCorrMap=ap_corr_map) 

696 

697 return psf, ap_corr_map, measured_src