Coverage for python / lsst / meas / base / simple_forced_measurement.py: 0%

71 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-17 09:05 +0000

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__all__ = ("SimpleForcedMeasurementConfig", "SimpleForcedMeasurementTask") 

23 

24import numpy as np 

25 

26import lsst.afw.geom 

27import lsst.afw.image 

28import lsst.afw.table 

29import lsst.daf.base 

30import lsst.geom 

31import lsst.pex.config 

32import lsst.pipe.base 

33from lsst.utils.logging import PeriodicLogger 

34 

35from .baseMeasurement import SimpleBaseMeasurementConfig, SimpleBaseMeasurementTask 

36from .forcedMeasurement import ForcedPlugin 

37 

38 

39class SimpleForcedMeasurementConfig(SimpleBaseMeasurementConfig): 

40 """Config class for SimpleForcedMeasurementTask.""" 

41 plugins = ForcedPlugin.registry.makeField( 

42 multi=True, 

43 default=["base_PixelFlags", 

44 "base_TransformedCentroidFromCoord", 

45 "base_PsfFlux", 

46 ], 

47 doc="Plugins to be run and their configuration" 

48 ) 

49 psfFootprintScaling = lsst.pex.config.Field( 

50 dtype=float, 

51 doc="Scaling factor to apply to the PSF shape when footprintSource='psf' (ignored otherwise).", 

52 default=3.0, 

53 ) 

54 copyColumns = lsst.pex.config.DictField( 

55 keytype=str, itemtype=str, doc="Mapping of reference columns to source columns", 

56 default={"id": "objectId", "parent": "parentObjectId", 

57 "coord_ra": "coord_ra", "coord_dec": "coord_dec"} 

58 ) 

59 checkUnitsParseStrict = lsst.pex.config.Field( 

60 doc="Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'", 

61 dtype=str, 

62 default="raise", 

63 ) 

64 

65 def setDefaults(self): 

66 self.slots.centroid = "base_TransformedCentroidFromCoord" 

67 self.slots.shape = None 

68 self.slots.apFlux = None 

69 self.slots.modelFlux = None 

70 self.slots.psfFlux = "base_PsfFlux" 

71 self.slots.gaussianFlux = None 

72 self.slots.calibFlux = None 

73 

74 

75class SimpleForcedMeasurementTask(SimpleBaseMeasurementTask): 

76 """Measure sources on an image using a simple forced measurement algorithm. 

77 

78 This differes from ForcedMeasurmentTask in that it uses a PSF-based 

79 footprint for every source so it does not need to transform footprints. 

80 

81 Parameters 

82 ---------- 

83 algMetadata : `lsst.daf.base.PropertyList` or `None` 

84 Will be updated in place to to record information about each 

85 algorithm. An empty `~lsst.daf.base.PropertyList` will be created if 

86 `None`. 

87 **kwds 

88 Keyword arguments are passed to the supertask constructor. 

89 """ 

90 ConfigClass = SimpleForcedMeasurementConfig 

91 

92 def __init__(self, refSchema, algMetadata: lsst.daf.base.PropertyList = None, **kwds): 

93 super().__init__(algMetadata=algMetadata, **kwds) 

94 self.mapper = lsst.afw.table.SchemaMapper(refSchema) 

95 self.mapper.addMinimalSchema(lsst.afw.table.SourceTable.makeMinimalSchema(), False) 

96 self.config.slots.setupSchema(self.mapper.editOutputSchema()) 

97 for refName, targetName in self.config.copyColumns.items(): 

98 refItem = refSchema.find(refName) 

99 self.mapper.addMapping(refItem.key, targetName) 

100 self.config.slots.setupSchema(self.mapper.editOutputSchema()) 

101 self.initializePlugins(schemaMapper=self.mapper) 

102 self.addInvalidPsfFlag(self.mapper.editOutputSchema()) 

103 self.schema = self.mapper.getOutputSchema() 

104 self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict) 

105 

106 def run( 

107 self, 

108 refCat: lsst.afw.table.SourceCatalog, 

109 measCat: lsst.afw.table.SourceCatalog, 

110 exposure: lsst.afw.image.Exposure, 

111 refWcs: lsst.afw.geom.SkyWcs, 

112 beginOrder: int | None = None, 

113 endOrder: int | None = None, 

114 ) -> None: 

115 """Perform forced measurement. 

116 

117 Parameters 

118 ---------- 

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

120 Catalog with locations and ids of sources to measure. 

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

122 Catalog that measurements are made on. 

123 exposure : `lsst.afw.image.exposureF` 

124 Image to be measured. Must have at least a `lsst.afw.geom.SkyWcs` 

125 attached. 

126 refWcs : `lsst.afw.geom.SkyWcs` 

127 Defines the X,Y coordinate system of ``refCat``. 

128 beginOrder : `int`, optional 

129 Beginning execution order (inclusive). Algorithms with 

130 ``executionOrder`` < ``beginOrder`` are not executed. `None` for no limit. 

131 endOrder : `int`, optional 

132 Ending execution order (exclusive). Algorithms with 

133 ``executionOrder`` >= ``endOrder`` are not executed. `None` for no limit. 

134 idFactory : `lsst.afw.table.IdFactory`, optional 

135 Factory for creating IDs for sources. 

136 """ 

137 self._attachPsfShapeFootprints(measCat, exposure, scaling=self.config.psfFootprintScaling) 

138 self.log.info("Performing forced measurement on %d source%s", len(refCat), 

139 "" if len(refCat) == 1 else "s") 

140 # Wrap the task logger into a periodic logger. 

141 periodicLog = PeriodicLogger(self.log) 

142 

143 for index in range(len(refCat)): 

144 measRecord = measCat[index] 

145 refRecord = refCat[index] 

146 if measRecord.getFootprint() is None: 

147 self.log.warning("Skipping object with ID %s that is off the image.", measRecord.getId()) 

148 self.callMeasure(measRecord, exposure, refRecord, refWcs, 

149 beginOrder=beginOrder, endOrder=endOrder) 

150 # Log a message if it has been a while since the last log. 

151 periodicLog.log("Forced measurement complete for %d parents (and their children) out of %d", 

152 index + 1, len(refCat)) 

153 

154 def _attachPsfShapeFootprints(self, sources, exposure, scaling=3): 

155 """Attach Footprints to blank sources prior to measurement, by 

156 creating elliptical Footprints from the PSF moments. 

157 

158 Parameters 

159 ---------- 

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

161 Blank catalog (with all rows and columns, but values other than 

162 ``coord_ra``, ``coord_dec`` unpopulated). 

163 to which footprints should be attached. 

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

165 Image object from which peak values and the PSF are obtained. 

166 scaling : `int`, optional 

167 Scaling factor to apply to the PSF second-moments ellipse in order 

168 to determine the footprint boundary. 

169 """ 

170 psf = exposure.getPsf() 

171 if psf is None: 

172 raise RuntimeError("Cannot construct Footprints from PSF shape without a PSF.") 

173 bbox = exposure.getBBox() 

174 wcs = exposure.getWcs() 

175 # This will always be coord_ra, coord_dec since we converted the 

176 # astropy table into a schema and schema is always coord_ra, coord_dec. 

177 x, y = wcs.skyToPixelArray(sources["coord_ra"], sources["coord_dec"], degrees=False) 

178 inBBox = np.atleast_1d(lsst.geom.Box2D(bbox).contains(x, y)) 

179 for idx, record in enumerate(sources): 

180 localPoint = lsst.geom.Point2D(x[idx], y[idx]) 

181 localIntPoint = lsst.geom.Point2I(localPoint) 

182 if not inBBox[idx]: 

183 record.setFootprint(None) 

184 continue 

185 ellipse = lsst.afw.geom.ellipses.Ellipse(psf.computeShape(localPoint), localPoint) 

186 ellipse.getCore().scale(scaling) 

187 spans = lsst.afw.geom.SpanSet.fromShape(ellipse) 

188 footprint = lsst.afw.detection.Footprint(spans.clippedTo(bbox), bbox) 

189 footprint.addPeak(localIntPoint.getX(), localIntPoint.getY(), 

190 exposure.image._get(localIntPoint, lsst.afw.image.PARENT)) 

191 record.setFootprint(footprint)