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

64 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-18 02:22 -0700

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(flux_str, type="D", doc="Compensated Gaussian flux measurement.") 

103 

104 # flux error 

105 err_str = f"{base_key}_instFluxErr" 

106 err_key = schema.addField(err_str, type="D", doc="Compensated Gaussian flux error.") 

107 

108 # mask bits 

109 mask_str = f"{base_key}_mask_bits" 

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

111 

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

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

114 

115 self._max_rad = max(self._rads) 

116 

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

118 if isinstance(error, OutOfBoundsError): 

119 measRecord.set(self.ooBoundsKey, True) 

120 measRecord.set(self.fatalFailKey, True) 

121 

122 def measure(self, measRecord, exposure): 

123 center = measRecord.getCentroid() 

124 bbox = exposure.getBBox() 

125 

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

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

128 

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

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

131 

132 y_floor = math.floor(y) 

133 x_floor = math.floor(x) 

134 

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

136 rad = self._rads[width] 

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

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

139 y_mean = y - y_floor + rad 

140 x_mean = x - x_floor + rad 

141 

142 flux, var = _compensatedGaussianFiltInnerProduct( 

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

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

145 x_mean, 

146 y_mean, 

147 width, 

148 self._t, 

149 ) 

150 measRecord.set(flux_key, flux) 

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

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