22from __future__
import annotations
24__all__ = (
"RewriteVisitImageConnections",
"RewriteVisitImageTask",
"RewriteVisitImageConfig")
26from typing
import ClassVar
31import lsst.pipe.base.connectionTypes
as cT
34from lsst.images
import VisitImage
35from lsst.images.fields
import field_from_legacy_background, field_from_legacy_photo_calib
37from lsst.pipe.base
import (
38 InputQuantizedConnection,
39 OutputQuantizedConnection,
42 PipelineTaskConnections,
49 PipelineTaskConnections,
50 dimensions={
"visit",
"detector"},
51 defaultTemplates={
"legacy_prefix":
"legacy_",
"future_prefix":
""},
53 legacy_exposure = cT.Input(
54 "{legacy_prefix}visit_image",
59 storageClass=
"VisitImage",
60 dimensions={
"visit",
"detector"},
62 doc=
"The input image to convert.",
64 visit_summary = cT.Input(
66 storageClass=
"ExposureCatalog",
68 doc=
"A visit summary catalog with the PhotoCalib that was already applied to the image's pixels.",
70 photo_calib = cT.Input(
71 "initial_photo_calib_detector",
72 storageClass=
"PhotoCalib",
73 dimensions={
"visit",
"detector"},
74 doc=
"The PhotoCalib that was already applied to the image's pixels.",
76 subtracted_background = cT.Input(
77 "visit_image_background",
78 storageClass=
"Background",
79 dimensions={
"visit",
"detector"},
80 doc=
"The background model that was subtracted from this image.",
82 alternate_background = cT.Input(
84 storageClass=
"Background",
85 dimensions={
"visit",
"detector"},
86 doc=
"A different background model that was not subtracted from the image.",
88 future_visit_image = cT.Output(
89 "{future_prefix}visit_image",
90 storageClass=
"VisitImage",
91 dimensions={
"visit",
"detector"},
92 doc=
"The output VisitImage.",
95 config: RewriteVisitImageConfig
97 def __init__(self, config: RewriteVisitImageConfig |
None):
98 super().__init__(config=config)
99 match self.config.photo_calib_source:
101 del self.visit_summary
103 case
"visit_summary":
106 del self.visit_summary
107 if self.config.alternate_background_type
is None:
108 del self.alternate_background
111class RewriteVisitImageConfig(
113 pipelineConnections=RewriteVisitImageConnections,
115 photo_calib_source = ChoiceField[str](
116 "Which kind of PhotoCalib to load.",
119 "attached":
"The input image has uncalibrated pixels and a nontrivial PhotoCalib attached to it.",
120 "visit_summary":
"Load the visit_summary connection to find the PhotoCalib already applied.",
121 "standalone":
"Load the photo_calib connection to find the PhotoCalib already applied.",
123 default=
"visit_summary",
125 subtracted_background_description = Field(
126 "Description of the subtracted background, to be stored with the image.",
128 default=
"Background subtracted from the image when generating the Source catalog.",
130 alternate_background_name = Field(
131 "Name for the alternate background attached to the image. "
132 "ignored if alternate_background_type is None.",
136 alternate_background_description = Field(
137 "Description for the alternate background attached to the image. "
138 "Ignored if alternate_background_type is None.",
141 "An alternate large-scale visit-level background subtracted from the input images that go into "
142 "pretty_coadd and RGB color images. May preserve more low surface-brightness features, but may "
143 "also be more affected by scattered light and other artifacts."
146 alternate_background_type = ChoiceField(
147 "How the alternate background relates to the subtracted background.",
151 "The alternate background should be subtracted after restoring the subtracted background."
153 "differential_composed": (
154 "The alternate background starts with terms that invert the subtracted background."
156 "differential_fit": (
157 "The alternate background was fit to the already-subtracted image, and "
158 "can only be subtracted from the already-subtracted image."
161 default=
"differential_composed",
164 instrumental_unit = Field(
165 "Unit for instrumental flux pixels (i.e. uncalibrated side of a PhotoCalib, "
166 "and the units of all background inputs).",
172class RewriteVisitImageTask(PipelineTask):
173 ConfigClass: ClassVar[type[RewriteVisitImageConfig]] = RewriteVisitImageConfig
174 config: RewriteVisitImageConfig
175 _DefaultName =
"rewriteVisitImage"
179 butlerQC: QuantumContext,
180 inputRefs: InputQuantizedConnection,
181 outputRefs: OutputQuantizedConnection,
183 inputs = butlerQC.get(inputRefs)
184 visit_image = inputs.pop(
"legacy_exposure").get(parameters={
"preserve_quantization":
True})
185 photo_calib: PhotoCalib |
None
186 match self.config.photo_calib_source:
189 case
"visit_summary":
190 visit_summary = inputs.pop(
"visit_summary")
191 photo_calib = visit_summary.find(butlerQC.quantum.dataId[
"detector"]).getPhotoCalib()
193 photo_calib = inputs.pop(
"photo_calib")
194 outputs = self.run(visit_image, photo_calib=photo_calib, **inputs)
195 butlerQC.put(outputs, outputRefs)
199 visit_image: VisitImage,
201 photo_calib: PhotoCalib |
None =
None,
202 subtracted_background: BackgroundList,
203 alternate_background: BackgroundList |
None =
None,
205 instrumental_unit = astropy.units.Unit(self.config.instrumental_unit)
206 if photo_calib
is not None:
207 visit_image.photometric_scaling = field_from_legacy_photo_calib(
208 photo_calib, bounds=visit_image.bbox, instrumental_unit=instrumental_unit
210 visit_image.backgrounds.add(
212 field_from_legacy_background(
213 subtracted_background, bounds=visit_image.bbox, unit=instrumental_unit
215 self.config.subtracted_background_description,
218 if alternate_background
is not None:
219 assert self.config.alternate_background_name
is not None, (
220 "Configuration and arguments are inconsistent."
222 match self.config.alternate_background_type:
225 case
"differential_composed":
226 for (subtracted_term, *_), (alternate_term, *_)
in zip(
227 subtracted_background, alternate_background
229 if not self._do_backgrounds_cancel(subtracted_term, alternate_term):
231 "alternate_background_type='differential_composed', but the alternate "
232 "background does not start with the inverse of the subtracted background."
234 alternate_background = BackgroundList(
236 alternate_background[n]
237 for n
in range(len(subtracted_background), len(alternate_background))
240 case
"differential_fit":
241 alternate_background = BackgroundList(*subtracted_background, *alternate_background)
242 visit_image.backgrounds.add(
243 self.config.alternate_background_name,
244 field_from_legacy_background(
245 alternate_background, bounds=visit_image.bbox, unit=instrumental_unit
247 self.config.alternate_background_description,
250 return Struct(future_visit_image=visit_image)
252 def _do_backgrounds_cancel(self, bg1: BackgroundMI, bg2: BackgroundMI) -> bool:
253 ctrl1 = bg1.getBackgroundControl()
254 ctrl2 = bg2.getBackgroundControl()
255 if ctrl1.getInterpStyle() != ctrl2.getInterpStyle():
257 if ctrl1.getApproximateControl().getStyle() != ctrl2.getApproximateControl().getStyle():
259 bins1 = bg1.getStatsImage()
260 bins2 = bg2.getStatsImage()
261 if bins1.getBBox() != bins2.getBBox():
263 return np.array_equal(bins1.image.array, -bins2.image.array, equal_nan=
True)