Coverage for python/lsst/meas/base/compensatedGaussian/_compensatedGaussian.py: 36%

64 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-01 13:06 +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 

22from __future__ import annotations 

23 

24__all__ = ( 

25 "SingleFrameCompensatedGaussianFluxConfig", 

26 "SingleFrameCompensatedGaussianFluxPlugin", 

27) 

28 

29import math 

30import numpy as np 

31import scipy.stats as sps 

32 

33from lsst.pex.config import Field, ListField 

34from lsst.geom import Point2I 

35 

36from ..sfm import SingleFramePlugin, SingleFramePluginConfig 

37from ..pluginRegistry import register 

38 

39from .._measBaseLib import _compensatedGaussianFiltInnerProduct 

40 

41 

42class OutOfBoundsError(Exception): 

43 pass 

44 

45 

46class SingleFrameCompensatedGaussianFluxConfig(SingleFramePluginConfig): 

47 kernel_widths = ListField( 

48 doc="The widths (in pixels) of the kernels for which to measure compensated apertures.", 

49 dtype=int, 

50 minLength=1, 

51 default=[3, 5] 

52 ) 

53 

54 t = Field( 

55 doc="Scale parameter of outer Gaussian compared to inner Gaussian.", 

56 dtype=float, 

57 default=2.0, 

58 ) 

59 

60 

61@register("base_CompensatedGaussianFlux") 

62class SingleFrameCompensatedGaussianFluxPlugin(SingleFramePlugin): 

63 ConfigClass = SingleFrameCompensatedGaussianFluxConfig 

64 

65 @classmethod 

66 def getExecutionOrder(cls): 

67 return cls.FLUX_ORDER 

68 

69 def __init__( 

70 self, 

71 config: SingleFrameCompensatedGaussianFluxConfig, 

72 name: str, 

73 schema, 

74 metadata, 

75 logName=None, 

76 **kwds, 

77 ): 

78 super().__init__(config, name, schema, metadata, logName, **kwds) 

79 

80 # create generic failure key 

81 self.fatalFailKey = schema.addField( 

82 f"{name}_flag", type="Flag", doc="Set to 1 for any fatal failure." 

83 ) 

84 

85 # Out of bounds failure key 

86 self.ooBoundsKey = schema.addField( 

87 f"{name}_bounds_flag", 

88 type="Flag", 

89 doc="Flag set to 1 if not all filters fit within exposure.", 

90 ) 

91 

92 self.width_keys = {} 

93 self._rads = {} 

94 self._flux_corrections = {} 

95 self._variance_corrections = {} 

96 self._t = config.t 

97 for width in config.kernel_widths: 

98 base_key = f"{name}_{width}" 

99 

100 # flux 

101 flux_str = f"{base_key}_instFlux" 

102 flux_key = schema.addField( 

103 flux_str, 

104 type="D", 

105 doc="Compensated Gaussian flux measurement.", 

106 units="count", 

107 ) 

108 

109 # flux error 

110 err_str = f"{base_key}_instFluxErr" 

111 err_key = schema.addField( 

112 err_str, 

113 type="D", 

114 doc="Compensated Gaussian flux error.", 

115 units="count", 

116 ) 

117 

118 # mask bits 

119 mask_str = f"{base_key}_mask_bits" 

120 mask_key = schema.addField(mask_str, type=np.int32, doc="Mask bits set within aperture.") 

121 

122 self.width_keys[width] = (flux_key, err_key, mask_key) 

123 self._rads[width] = math.ceil(sps.norm.ppf((0.995,), scale=width * config.t)[0]) 

124 

125 self._max_rad = max(self._rads) 

126 

127 def fail(self, measRecord, error=None): 

128 if isinstance(error, OutOfBoundsError): 

129 measRecord.set(self.ooBoundsKey, True) 

130 measRecord.set(self.fatalFailKey, True) 

131 

132 def measure(self, measRecord, exposure): 

133 center = measRecord.getCentroid() 

134 bbox = exposure.getBBox() 

135 

136 if Point2I(center) not in exposure.getBBox().erodedBy(self._max_rad): 

137 raise OutOfBoundsError("Not all the kernels for this source fit inside the exposure.") 

138 

139 y = center.getY() - bbox.beginY 

140 x = center.getX() - bbox.beginX 

141 

142 y_floor = math.floor(y) 

143 x_floor = math.floor(x) 

144 

145 for width, (flux_key, err_key, mask_key) in self.width_keys.items(): 

146 rad = self._rads[width] 

147 y_slice = slice(y_floor - rad, y_floor + rad + 1, 1) 

148 x_slice = slice(x_floor - rad, x_floor + rad + 1, 1) 

149 y_mean = y - y_floor + rad 

150 x_mean = x - x_floor + rad 

151 

152 flux, var = _compensatedGaussianFiltInnerProduct( 

153 exposure.image.array[y_slice, x_slice], 

154 exposure.variance.array[y_slice, x_slice], 

155 x_mean, 

156 y_mean, 

157 width, 

158 self._t, 

159 ) 

160 measRecord.set(flux_key, flux) 

161 measRecord.set(err_key, np.sqrt(var)) 

162 measRecord.set(mask_key, np.bitwise_or.reduce(exposure.mask.array[y_slice, x_slice], axis=None))