Coverage for python/lsst/meas/base/compensatedGaussian/_compensatedGaussian.py: 36%
64 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-20 04:26 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-20 04:26 -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/>.
22from __future__ import annotations
24__all__ = (
25 "SingleFrameCompensatedGaussianFluxConfig",
26 "SingleFrameCompensatedGaussianFluxPlugin",
27)
29import math
30import numpy as np
31import scipy.stats as sps
33from lsst.pex.config import Field, ListField
34from lsst.geom import Point2I
36from ..sfm import SingleFramePlugin, SingleFramePluginConfig
37from ..pluginRegistry import register
39from .._measBaseLib import _compensatedGaussianFiltInnerProduct
42class OutOfBoundsError(Exception):
43 pass
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 )
54 t = Field(
55 doc="Scale parameter of outer Gaussian compared to inner Gaussian.",
56 dtype=float,
57 default=2.0,
58 )
61@register("base_CompensatedGaussianFlux")
62class SingleFrameCompensatedGaussianFluxPlugin(SingleFramePlugin):
63 ConfigClass = SingleFrameCompensatedGaussianFluxConfig
65 @classmethod
66 def getExecutionOrder(cls):
67 return cls.FLUX_ORDER
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)
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 )
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 )
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}"
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 )
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 )
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.")
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])
125 self._max_rad = max(self._rads)
127 def fail(self, measRecord, error=None):
128 if isinstance(error, OutOfBoundsError):
129 measRecord.set(self.ooBoundsKey, True)
130 measRecord.set(self.fatalFailKey, True)
132 def measure(self, measRecord, exposure):
133 center = measRecord.getCentroid()
134 bbox = exposure.getBBox()
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.")
139 y = center.getY() - bbox.beginY
140 x = center.getX() - bbox.beginX
142 y_floor = math.floor(y)
143 x_floor = math.floor(x)
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
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))