Coverage for python/lsst/pipe/tasks/fit_coadd_psf.py: 41%

82 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-09-30 11:39 +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__all__ = [ 

23 "CatalogExposurePsf", "CoaddPsfFitConfig", "CoaddPsfFitConnections", 

24 "CoaddPsfFitSubConfig", "CoaddPsfFitSubTask", "CoaddPsfFitTask", 

25] 

26 

27from .fit_multiband import CatalogExposure, CatalogExposureConfig 

28from lsst.geom import Point2D 

29from lsst.meas.base import SkyMapIdGeneratorConfig 

30import lsst.pex.config as pexConfig 

31import lsst.pipe.base as pipeBase 

32import lsst.pipe.base.connectionTypes as cT 

33 

34import dataclasses 

35from abc import ABC, abstractmethod 

36from pydantic.dataclasses import dataclass 

37 

38 

39@dataclass(frozen=True, kw_only=True, config=CatalogExposureConfig) 

40class CatalogExposurePsf(CatalogExposure): 

41 def get_catalog(self): 

42 return self.catalog 

43 

44 def get_psf_image(self, source): 

45 """Return the PSF image for this object.""" 

46 center = Point2D(round(source.getX()), round(source.getY())) 

47 return self.exposure.getPsf().computeKernelImage(center).array 

48 

49 

50CoaddPsfFitBaseTemplates = { 

51 "name_coadd": "deep", 

52 "name_output_method": "multiprofit", 

53} 

54 

55 

56class CoaddPsfFitConnections( 

57 pipeBase.PipelineTaskConnections, 

58 dimensions=("tract", "patch", "band", "skymap"), 

59 defaultTemplates=CoaddPsfFitBaseTemplates, 

60): 

61 coadd = cT.Input( 

62 doc="Coadd image to fit a PSF model to", 

63 name="{name_coadd}Coadd_calexp", 

64 storageClass="ExposureF", 

65 dimensions=("tract", "patch", "band", "skymap"), 

66 ) 

67 coadd_cell = cT.Input( 

68 doc="Cell-coadd image to fit a PSF model to", 

69 name="{name_coadd}CoaddCell", 

70 storageClass="MultipleCellCoadd", 

71 dimensions=("tract", "patch", "band", "skymap"), 

72 ) 

73 background = cT.Input( 

74 doc="Background model to subtract from the coadd_cell", 

75 name="{name_coadd}Coadd_calexp_background", 

76 storageClass="Background", 

77 dimensions=("tract", "patch", "band", "skymap"), 

78 ) 

79 cat_meas = cT.Input( 

80 doc="Deblended single-band source catalog", 

81 name="{name_coadd}Coadd_meas", 

82 storageClass="SourceCatalog", 

83 dimensions=("tract", "patch", "band", "skymap"), 

84 ) 

85 cat_output = cT.Output( 

86 doc="Output PSF fit parameter catalog", 

87 name="{name_coadd}Coadd_psfs_{name_output_method}", 

88 storageClass="ArrowTable", 

89 dimensions=("tract", "patch", "band", "skymap"), 

90 ) 

91 

92 def __init__(self, *, config=None): 

93 super().__init__(config=config) 

94 if config is None: 

95 return 

96 

97 if config.image_type == "future": 

98 self.coadd = dataclasses.replace(self.coadd, storageClass="CellCoadd") 

99 del self.coadd_cell 

100 del self.background 

101 elif config.use_cell_coadds: 

102 del self.coadd 

103 else: 

104 del self.coadd_cell 

105 del self.background 

106 

107 

108class CoaddPsfFitSubConfig(pexConfig.Config): 

109 """Base config class for the CoaddPsfFitTask. 

110 

111 Implementing classes may add any necessary attributes. 

112 """ 

113 

114 

115class CoaddPsfFitSubTask(pipeBase.Task, ABC): 

116 """Interface for CoaddPsfFitTask subtasks to fit PSFs. 

117 

118 Parameters 

119 ---------- 

120 **kwargs 

121 Additional arguments to be passed to the `lsst.pipe.base.Task` 

122 constructor. 

123 """ 

124 ConfigClass = CoaddPsfFitSubConfig 

125 

126 def __init__(self, **kwargs): 

127 super().__init__(**kwargs) 

128 

129 @abstractmethod 

130 def run( 

131 self, catexp: CatalogExposurePsf 

132 ) -> pipeBase.Struct: 

133 """Fit PSF images at locations of sources in a single exposure. 

134 

135 Parameters 

136 ---------- 

137 catexp : `CatalogExposurePsf` 

138 An exposure to fit a model PSF at the position of all 

139 sources in the corresponding catalog. 

140 

141 Returns 

142 ------- 

143 retStruct : `lsst.pipe.base.Struct` 

144 A struct with a cat_output attribute containing the output 

145 measurement catalog. 

146 

147 Notes 

148 ----- 

149 Subclasses may have further requirements on the input parameters, 

150 including: 

151 - Passing only one catexp per band; 

152 - Catalogs containing HeavyFootprints with deblended images; 

153 - Fitting only a subset of the sources. 

154 If any requirements are not met, the subtask should fail as soon as 

155 possible. 

156 """ 

157 raise NotImplementedError() 

158 

159 

160class CoaddPsfFitConfig( 

161 pipeBase.PipelineTaskConfig, 

162 pipelineConnections=CoaddPsfFitConnections, 

163): 

164 """Configure a CoaddPsfFitTask, including a configurable fitting subtask. 

165 """ 

166 use_cell_coadds = pexConfig.Field( 

167 dtype=bool, 

168 default=False, 

169 doc="Use cell coadd images for PSF fitting", 

170 ) 

171 fit_coadd_psf = pexConfig.ConfigurableField( 

172 target=CoaddPsfFitSubTask, 

173 doc="Task to fit PSF models for a single coadd", 

174 ) 

175 idGenerator = SkyMapIdGeneratorConfig.make_field() 

176 image_type = pexConfig.ChoiceField( 

177 "Which image type to expect for the input coadd. " 

178 "This option only directly affects connection storage classes and hence 'runQuantum'; the 'run' " 

179 "method behavior is determined by which type is actually passed in.", 

180 allowed={ 

181 "legacy": ( 

182 "Read a lsst.cell_coadds.MultipleCellCoadd via 'coadd_cell` and restore 'background' " 

183 "(if use_cell_coadd) or lsst.afw.image.Exposure via `coadd` (if not use_cell_coadd)." 

184 ), 

185 "future": ( 

186 "Read lsst.images.cells.CellCoadd via the 'coadd' connection. use_cell_coadd is ignored." 

187 ), 

188 }, 

189 dtype=str, 

190 optional=False, 

191 default="legacy", 

192 ) 

193 

194 

195class CoaddPsfFitTask(pipeBase.PipelineTask): 

196 """Fit a PSF model at the location of sources in a coadd. 

197 

198 This task is intended to fit only a single PSF model at the 

199 centroid of all of the sources in a single coadd exposure. 

200 Subtasks may choose to filter which sources they fit, 

201 and may output whatever columns they desire in addition to 

202 the minimum of 'id'. 

203 """ 

204 ConfigClass = CoaddPsfFitConfig 

205 _DefaultName = "CoaddPsfFit" 

206 

207 def __init__(self, initInputs, **kwargs): 

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

209 self.makeSubtask("fit_coadd_psf") 

210 

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

212 inputs = butlerQC.get(inputRefs) 

213 id_tp = self.config.idGenerator.apply(butlerQC.quantum.dataId).catalog_id 

214 dataId = inputRefs.cat_meas.dataId 

215 

216 if self.config.image_type == "future": 

217 coaddDataRef = inputRefs.coadd 

218 exposure = inputs.pop('coadd').to_legacy() 

219 elif self.config.use_cell_coadds: 

220 coaddDataRef = inputRefs.coadd_cell 

221 multiple_cell_coadd = inputs.pop('coadd_cell') 

222 background = inputs.pop('background') 

223 exposure = multiple_cell_coadd.stitch().asExposure() 

224 exposure.image -= background.getImage() 

225 else: 

226 coaddDataRef = inputRefs.coadd 

227 exposure = inputs.pop('coadd') 

228 

229 for dataRef in (coaddDataRef,): 

230 if dataRef.dataId != dataId: 

231 raise RuntimeError(f'{dataRef=}.dataId != {inputRefs.cat_meas.dataId=}') 

232 

233 catalog = inputs.pop('cat_meas') 

234 catexp = CatalogExposurePsf( 

235 catalog=catalog, exposure=exposure, dataId=dataId, id_tract_patch=id_tp, 

236 ) 

237 assert not inputs, "runQuantum got more inputs than expected" 

238 outputs = self.run(catexp=catexp) 

239 butlerQC.put(outputs, outputRefs) 

240 

241 def run(self, catexp: CatalogExposurePsf) -> pipeBase.Struct: 

242 """Fit a PSF model at the location of sources in a coadd. 

243 

244 Parameters 

245 ---------- 

246 catexp : `typing.List [CatalogExposurePsf]` 

247 A list of catalog-exposure pairs in a given band. 

248 

249 Returns 

250 ------- 

251 retStruct : `lsst.pipe.base.Struct` 

252 A struct with a cat_output attribute containing the output 

253 measurement catalog. 

254 

255 Notes 

256 ----- 

257 Subtasks may have further requirements; see `CoaddPsfFitSubTask.run`. 

258 """ 

259 cat_output = self.fit_coadd_psf.run(catexp).output 

260 retStruct = pipeBase.Struct(cat_output=cat_output) 

261 return retStruct