Coverage for python / lsst / cp / pipe / cpGainCorrection.py: 30%
92 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 09:35 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 09:35 +0000
1# This file is part of cp_pipe.
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 <http://www.gnu.org/licenses/>.
21import numpy as np
22from scipy.stats import median_abs_deviation
24import lsst.pipe.base
25from lsst.ip.isr import GainCorrection
26import lsst.pex.config
27from lsst.utils.plotting import make_figure
29from .ptc.cpPtcAdjustGainRatios import _choose_reference_amplifier, _compute_gain_ratios
30from .utils import bin_flat
32__all__ = ["CpMeasureGainCorrectionTask", "CpMeasureGainCorrectionConfig"]
35class CpMeasureGainCorrectionConnections(
36 lsst.pipe.base.PipelineTaskConnections,
37 dimensions=("instrument", "detector"),
38):
39 input_reference_flat = lsst.pipe.base.connectionTypes.PrerequisiteInput(
40 name="flat",
41 doc="Input reference flat (certified).",
42 storageClass="Exposure",
43 dimensions=("instrument", "detector", "physical_filter"),
44 isCalibration=True,
45 )
46 input_reference_ptc = lsst.pipe.base.connectionTypes.PrerequisiteInput(
47 name="ptc",
48 doc="Input reference ptc (certified).",
49 storageClass="PhotonTransferCurveDataset",
50 dimensions=("instrument", "detector"),
51 isCalibration=True,
52 )
53 input_flat = lsst.pipe.base.connectionTypes.Input(
54 name="flat_gain_correction",
55 doc="Input flat to derive gain correction.",
56 storageClass="Exposure",
57 dimensions=("instrument", "detector", "physical_filter"),
58 isCalibration=True,
59 )
60 output_gain_correction = lsst.pipe.base.connectionTypes.Output(
61 name="gain_correction",
62 doc="Output gain correction calibration.",
63 storageClass="IsrCalib",
64 dimensions=("instrument", "detector"),
65 isCalibration=True,
66 )
67 output_flat_ratio_plot = lsst.pipe.base.connectionTypes.Output(
68 name="gain_correction_flat_ratio_plot",
69 doc="Gain correction flat ratio plot.",
70 storageClass="Plot",
71 dimensions=("instrument", "detector"),
72 )
74 def adjustQuantum(self, inputs, outputs, label, dataId):
75 # We need to remove reference flats that do not match
76 # the physical filter of the input flat.
77 input_flat_ref = inputs["input_flat"][1][0]
78 physical_filter = input_flat_ref.dataId["physical_filter"]
80 input_reference_flat_refs = inputs["input_reference_flat"][1]
81 input_reference_flat_ref = None
82 for ref in input_reference_flat_refs:
83 if ref.dataId["physical_filter"] == physical_filter:
84 input_reference_flat_ref = ref
85 break
87 if input_reference_flat_ref is None:
88 raise lsst.pipe.base.NoWorkFound(
89 f"No matched input flat with physical filter {physical_filter}.",
90 )
92 inputs["input_reference_flat"] = (inputs["input_reference_flat"][0], (input_reference_flat_ref,))
94 return inputs, outputs
97class CpMeasureGainCorrectionConfig(
98 lsst.pipe.base.PipelineTaskConfig,
99 pipelineConnections=CpMeasureGainCorrectionConnections,
100):
101 max_noise_reference = lsst.pex.config.Field(
102 dtype=float,
103 doc="Maximum read noise (e-) in the PTC for an amp to be considered as a reference",
104 default=12.0,
105 )
106 turnoff_percentile_reference = lsst.pex.config.Field(
107 dtype=float,
108 doc="Percentile threshold for sorting PTC turnoff for an amp to be considered as a reference",
109 default=25.0,
110 )
111 do_remove_radial_gradient = lsst.pex.config.Field(
112 dtype=bool,
113 doc="Remove radial gradient before measureting amp gain ratios?",
114 default=True,
115 )
116 radial_gradient_n_spline_nodes = lsst.pex.config.Field(
117 dtype=int,
118 doc="Number of radial spline nodes for radial gradient.",
119 default=20,
120 )
121 chebyshev_gradient_order = lsst.pex.config.Field(
122 dtype=int,
123 doc="Order of chebyshev x/y polynomials to remove additional gradients.",
124 default=1,
125 )
126 chebyshev_gradient_nsig_clip = lsst.pex.config.Field(
127 dtype=float,
128 doc="Number of sigma to clip gain ratios when fitting the Chebyshev gradient.",
129 default=5.0,
130 )
131 bin_factor = lsst.pex.config.Field(
132 dtype=int,
133 doc="Binning factor to compute gradients/gain ratios (pixels).",
134 default=8,
135 )
136 amp_boundary = lsst.pex.config.Field(
137 dtype=int,
138 doc="Amplifier boundary to ignore when computing gradients/gain ratios (pixels).",
139 default=20,
140 )
141 max_fractional_gain_ratio = lsst.pex.config.Field(
142 dtype=float,
143 doc="Maximum fractional gain ratio to consider. Any amps with larger "
144 "offset will be excluded from the measurement and will have no corrections "
145 "computed.",
146 default=0.05,
147 )
148 ratio_outlier_nsig_clip = lsst.pex.config.Field(
149 dtype=float,
150 doc="Number of sigma to clip in ratio image.",
151 default=5.0,
152 )
155class CpMeasureGainCorrectionTask(lsst.pipe.base.PipelineTask):
156 """Task to measure gain corrections."""
158 ConfigClass = CpMeasureGainCorrectionConfig
159 _DefaultName = "cpMeasureGainCorrection"
161 def run(self, *, input_reference_flat, input_reference_ptc, input_flat):
162 """
164 Parameters
165 ----------
166 input_reference_flat : `lsst.afw.image.Exposure`
167 Input reference flat (typically a certified calibration).
168 input_reference_ptc : `lsst.ip.isr.PhotonTransferCurveDataset`
169 Input reference PTC (typically a certified calibration).
170 input_flat : `lsst.afw.image.Exposure`
171 Input flat to derive gain correction (relative to reference flat).
173 Returns
174 -------
175 result : `lsst.pipe.base.Struct`
176 Result struct containing:
177 ``output_gain_correction`` : `lsst.ip.isr.GainCorrection`
178 ``output_flat_ratio_plot`` : `matplotlib.Figure`
179 """
180 gain_correction = GainCorrection(
181 ampNames=input_reference_ptc.ampNames,
182 gainAdjustments=np.ones(len(input_reference_ptc.ampNames)),
183 )
185 fixed_amp_index = _choose_reference_amplifier(
186 input_reference_ptc,
187 self.config.max_noise_reference,
188 self.config.turnoff_percentile_reference,
189 )
191 if fixed_amp_index < 0:
192 return lsst.pipe.base.Struct(output_gain_correction=gain_correction)
194 ratio = input_flat.clone()
195 ratio.image /= input_reference_flat.image
197 binned_ref = bin_flat(input_reference_ptc, input_reference_flat, apply_gains=False)
198 binned_flat = bin_flat(input_reference_ptc, input_flat, apply_gains=False)
199 binned_ratio = bin_flat(input_reference_ptc, ratio, apply_gains=False)
201 # Clip out non-finite and extreme values from both the input flat
202 # and the reference flat.
203 ok = (
204 np.isfinite(binned_ref["value"])
205 & np.isfinite(binned_flat["value"])
206 & np.isfinite(binned_ratio["value"])
207 )
208 lo_ref, hi_ref = np.percentile(binned_ref["value"][ok], [5.0, 95.0])
209 lo_ref *= 0.8
210 hi_ref *= 1.2
211 lo_flat, hi_flat = np.percentile(binned_flat["value"][ok], [5.0, 95.0])
212 lo_flat *= 0.8
213 hi_flat *= 1.2
214 med_ratio = np.median(binned_ratio["value"][ok])
215 # Make sure this is non-zero for noise-free tests in particular.
216 sig_ratio = np.clip(
217 median_abs_deviation(binned_ratio["value"][ok], scale="normal"),
218 0.01,
219 None,
220 )
221 lo_ratio = med_ratio - self.config.ratio_outlier_nsig_clip * sig_ratio
222 hi_ratio = med_ratio + self.config.ratio_outlier_nsig_clip * sig_ratio
223 use = (
224 np.isfinite(binned_ratio["value"])
225 & (binned_ref["value"] >= lo_ref)
226 & (binned_ref["value"] <= hi_ref)
227 & (binned_flat["value"] >= lo_flat)
228 & (binned_flat["value"] <= hi_flat)
229 & (binned_ratio["value"] >= lo_ratio)
230 & (binned_ratio["value"] <= hi_ratio)
231 )
232 binned = binned_ratio[use]
234 self.log.info(
235 "Using amplifier %d (%s) as fixed reference amplifier in fit.",
236 fixed_amp_index,
237 input_reference_ptc.ampNames[fixed_amp_index],
238 )
240 gain_ratios = _compute_gain_ratios(
241 input_flat.getDetector(),
242 binned,
243 fixed_amp_index,
244 do_remove_radial_gradient=self.config.do_remove_radial_gradient,
245 radial_gradient_n_spline_nodes=self.config.radial_gradient_n_spline_nodes,
246 chebyshev_gradient_order=self.config.chebyshev_gradient_order,
247 max_fractional_gain_ratio=self.config.max_fractional_gain_ratio,
248 nsig_clip=self.config.chebyshev_gradient_nsig_clip,
249 log=self.log,
250 )
252 # Sort ratios to find the approximate midpoint. The goal is
253 # to have one amplifier exactly 1.0 correction, and if there
254 # are an even number of amps simply taking the median will
255 # interpolate between two ratios. In addition, this allows
256 # us to explicitly log the new reference amplifier.
257 st = np.argsort(gain_ratios)
258 fixed_amp_index2 = st[int(0.5 * len(st))]
260 gain_ratios /= gain_ratios[fixed_amp_index2]
262 self.log.info(
263 "Adjusting to amplifier %d (%s) as fixed reference amplifier after fit.",
264 fixed_amp_index2,
265 input_reference_ptc.ampNames[fixed_amp_index2],
266 )
268 gain_correction.gainAdjustments[:] = gain_ratios
270 value2 = binned["value"].copy()
271 for ind in range(len(input_reference_ptc.ampNames)):
272 value2[binned["amp_index"] == ind] /= gain_ratios[ind]
274 vmin, vmax = np.nanpercentile(binned["value"], [10, 90])
276 fig = make_figure(figsize=(16, 6))
278 ax1 = fig.add_subplot(121)
280 im1 = ax1.hexbin(binned["xd"], binned["yd"], C=binned["value"], vmin=vmin, vmax=vmax)
282 ax1.set_xlabel("Detector x (pix)")
283 ax1.set_ylabel("Detector y (pix)")
284 ax1.set_aspect("equal")
285 ax1.set_title("Flat Ratio (uncorrected)")
287 fig.colorbar(im1, ax=ax1)
289 ax2 = fig.add_subplot(122)
291 im2 = ax2.hexbin(binned["xd"], binned["yd"], C=value2, vmin=vmin, vmax=vmax)
293 ax2.set_xlabel("Detector x (pix)")
294 ax2.set_ylabel("Detector y (pix)")
295 ax2.set_aspect("equal")
296 ax2.set_title("Flat Ratio (corrected)")
298 fig.colorbar(im2, ax=ax2)
300 return lsst.pipe.base.Struct(
301 output_gain_correction=gain_correction,
302 output_flat_ratio_plot=fig,
303 )