Coverage for python/lsst/atmospec/centroiding.py: 30%

80 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-26 11:45 +0000

1# This file is part of atmospec. 

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 lsst.afw.image as afwImage 

23import lsst.pipe.base as pipeBase 

24 

25from lsst.meas.algorithms import LoadReferenceObjectsConfig, MagnitudeLimit, ReferenceObjectLoader 

26from lsst.meas.astrom import AstrometryTask, FitAffineWcsTask 

27from lsst.pipe.tasks.quickFrameMeasurement import (QuickFrameMeasurementTask) 

28from lsst.pipe.base.task import TaskError 

29import lsst.pipe.base.connectionTypes as cT 

30import lsst.pex.config as pexConfig 

31 

32from .utils import getTargetCentroidFromWcs 

33 

34__all__ = ['SingleStarCentroidTaskConfig', 'SingleStarCentroidTask'] 

35 

36 

37class SingleStarCentroidTaskConnections(pipeBase.PipelineTaskConnections, 

38 dimensions=("instrument", "visit", "detector")): 

39 inputExp = cT.Input( 

40 name="icExp", 

41 doc="Image-characterize output exposure", 

42 storageClass="ExposureF", 

43 dimensions=("instrument", "visit", "detector"), 

44 multiple=False, 

45 ) 

46 inputSources = cT.Input( 

47 name="icSrc", 

48 doc="Image-characterize output sources.", 

49 storageClass="SourceCatalog", 

50 dimensions=("instrument", "visit", "detector"), 

51 multiple=False, 

52 ) 

53 astromRefCat = cT.PrerequisiteInput( 

54 doc="Reference catalog to use for astrometry", 

55 name="gaia_dr2_20200414", 

56 storageClass="SimpleCatalog", 

57 dimensions=("skypix",), 

58 deferLoad=True, 

59 multiple=True, 

60 ) 

61 atmospecCentroid = cT.Output( 

62 name="atmospecCentroid", 

63 doc="The main star centroid in yaml format.", 

64 storageClass="StructuredDataDict", 

65 dimensions=("instrument", "visit", "detector"), 

66 ) 

67 

68 

69class SingleStarCentroidTaskConfig(pipeBase.PipelineTaskConfig, 

70 pipelineConnections=SingleStarCentroidTaskConnections): 

71 """Configuration parameters for ProcessStarTask.""" 

72 astromRefObjLoader = pexConfig.ConfigField( 

73 dtype=LoadReferenceObjectsConfig, 

74 doc="Reference object loader config for astrometric calibration.", 

75 ) 

76 astrometry = pexConfig.ConfigurableField( 

77 target=AstrometryTask, 

78 doc="Task to perform astrometric calibration to refine the WCS", 

79 ) 

80 qfmTask = pexConfig.ConfigurableField( 

81 target=QuickFrameMeasurementTask, 

82 doc="XXX", 

83 ) 

84 referenceFilterOverride = pexConfig.Field( 

85 dtype=str, 

86 doc="Which filter in the reference catalog to match to?", 

87 default="phot_g_mean" 

88 ) 

89 

90 def setDefaults(self): 

91 super().setDefaults() 

92 self.astromRefObjLoader.pixelMargin = 1000 

93 

94 self.astrometry.wcsFitter.retarget(FitAffineWcsTask) 

95 self.astrometry.referenceSelector.doMagLimit = True 

96 magLimit = MagnitudeLimit() 

97 magLimit.minimum = 1 

98 magLimit.maximum = 15 

99 self.astrometry.referenceSelector.magLimit = magLimit 

100 self.astrometry.referenceSelector.magLimit.fluxField = "phot_g_mean_flux" 

101 self.astrometry.matcher.maxRotationDeg = 5.99 

102 self.astrometry.matcher.maxOffsetPix = 3000 

103 self.astrometry.sourceSelector['matcher'].minSnr = 10 

104 self.astrometry.sourceSelector["science"].doRequirePrimary = False 

105 self.astrometry.sourceSelector["science"].doIsolated = False 

106 

107 

108class SingleStarCentroidTask(pipeBase.PipelineTask): 

109 """XXX Docs here 

110 """ 

111 

112 ConfigClass = SingleStarCentroidTaskConfig 

113 _DefaultName = 'singleStarCentroid' 

114 

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

116 super().__init__(**kwargs) 

117 

118 self.makeSubtask("astrometry", refObjLoader=None) 

119 self.makeSubtask('qfmTask') 

120 

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

122 inputs = butlerQC.get(inputRefs) 

123 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId 

124 for ref in inputRefs.astromRefCat], 

125 refCats=inputs.pop('astromRefCat'), 

126 name=self.config.connections.astromRefCat, 

127 config=self.config.astromRefObjLoader, log=self.log) 

128 

129 refObjLoader.pixelMargin = 1000 

130 self.astrometry.setRefObjLoader(refObjLoader) 

131 

132 # See L603 (def runQuantum(self, butlerQC, inputRefs, outputRefs):) 

133 # in calibrate.py to put photocal back in 

134 

135 outputs = self.run(**inputs) 

136 butlerQC.put(outputs, outputRefs) 

137 

138 def run(self, inputExp, inputSources): 

139 """XXX Docs 

140 """ 

141 

142 # TODO: Change this to doing this the proper way 

143 referenceFilterName = self.config.referenceFilterOverride 

144 referenceFilterLabel = afwImage.FilterLabel(physical=referenceFilterName, band=referenceFilterName) 

145 # there's a better way of doing this with the task I think 

146 originalFilterLabel = inputExp.getFilter() 

147 inputExp.setFilter(referenceFilterLabel) 

148 originalWcs = inputExp.getWcs() 

149 

150 successfulFit = False 

151 try: 

152 astromResult = self.astrometry.run(sourceCat=inputSources, exposure=inputExp) 

153 scatter = astromResult.scatterOnSky.asArcseconds() 

154 inputExp.setFilter(originalFilterLabel) 

155 if scatter < 1: 

156 successfulFit = True 

157 except (RuntimeError, TaskError, IndexError, ValueError, AttributeError) as e: 

158 # IndexError raised for low source counts: 

159 # index 0 is out of bounds for axis 0 with size 0 

160 

161 # ValueError: negative dimensions are not allowed 

162 # seen when refcat source count is low (but non-zero) 

163 

164 # AttributeError: 'NoneType' object has no attribute 'asArcseconds' 

165 # when the result is a failure as the wcs is set to None on failure 

166 self.log.warn(f"Solving failed: {e}") 

167 inputExp.setWcs(originalWcs) # this is set to None when the fit fails, so restore it 

168 finally: 

169 inputExp.setFilter(originalFilterLabel) # always restore this 

170 

171 centroid = None 

172 if successfulFit: 

173 target = inputExp.visitInfo.object 

174 centroid = getTargetCentroidFromWcs(inputExp, target, logger=self.log) 

175 if not centroid: 

176 successfulFit = False 

177 self.log.warning(f'Failed to find target centroid for {target} via WCS') 

178 if not centroid: 

179 result = self.qfmTask.run(inputExp) 

180 centroid = result.brightestObjCentroid 

181 

182 centroidTuple = (centroid[0], centroid[1]) # unify Point2D or tuple to tuple 

183 self.log.info(f"Centroid of main star found at {centroidTuple} found" 

184 f" via {'astrometry' if successfulFit else 'QuickFrameMeasurement'}") 

185 result = pipeBase.Struct(atmospecCentroid={'centroid': centroidTuple, 

186 'astrometricMatch': successfulFit}) 

187 return result