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.",
83 alternate_background = cT.Input(
85 storageClass=
"Background",
86 dimensions={
"visit",
"detector"},
87 doc=
"A different background model that was not subtracted from the image.",
90 future_visit_image = cT.Output(
91 "{future_prefix}visit_image",
92 storageClass=
"VisitImage",
93 dimensions={
"visit",
"detector"},
94 doc=
"The output VisitImage.",
97 config: RewriteVisitImageConfig
99 def __init__(self, config: RewriteVisitImageConfig |
None):
100 super().__init__(config=config)
101 match self.config.photo_calib_source:
103 del self.visit_summary
105 case
"visit_summary":
108 del self.visit_summary
109 if self.config.alternate_background_type
is None:
110 del self.alternate_background
113class RewriteVisitImageConfig(
115 pipelineConnections=RewriteVisitImageConnections,
117 photo_calib_source = ChoiceField[str](
118 "Which kind of PhotoCalib to load.",
121 "attached":
"The input image has uncalibrated pixels and a nontrivial PhotoCalib attached to it.",
122 "visit_summary":
"Load the visit_summary connection to find the PhotoCalib already applied.",
123 "standalone":
"Load the photo_calib connection to find the PhotoCalib already applied.",
125 default=
"visit_summary",
127 subtracted_background_description = Field(
128 "Description of the subtracted background, to be stored with the image.",
130 default=
"Background subtracted from the image when generating the Source catalog.",
132 alternate_background_name = Field(
133 "Name for the alternate background attached to the image. "
134 "ignored if alternate_background_type is None.",
138 alternate_background_description = Field(
139 "Description for the alternate background attached to the image. "
140 "Ignored if alternate_background_type is None.",
143 "An alternate large-scale visit-level background subtracted from the input images that go into "
144 "pretty_coadd and RGB color images. May preserve more low surface-brightness features, but may "
145 "also be more affected by scattered light and other artifacts."
148 alternate_background_type = ChoiceField(
149 "How the alternate background relates to the subtracted background.",
153 "The alternate background should be subtracted after restoring the subtracted background."
155 "differential_composed": (
156 "The alternate background starts with terms that invert the subtracted background."
158 "differential_fit": (
159 "The alternate background was fit to the already-subtracted image, and "
160 "can only be subtracted from the already-subtracted image."
163 default=
"differential_composed",
166 instrumental_unit = Field(
167 "Unit for instrumental flux pixels (i.e. uncalibrated side of a PhotoCalib, "
168 "and the units of all background inputs).",
174class RewriteVisitImageTask(PipelineTask):
175 ConfigClass: ClassVar[type[RewriteVisitImageConfig]] = RewriteVisitImageConfig
176 config: RewriteVisitImageConfig
177 _DefaultName =
"rewriteVisitImage"
181 butlerQC: QuantumContext,
182 inputRefs: InputQuantizedConnection,
183 outputRefs: OutputQuantizedConnection,
185 inputs = butlerQC.get(inputRefs)
186 visit_image = inputs.pop(
"legacy_exposure").get(parameters={
"preserve_quantization":
True})
187 photo_calib: PhotoCalib |
None
188 match self.config.photo_calib_source:
191 case
"visit_summary":
192 visit_summary = inputs.pop(
"visit_summary")
193 photo_calib = visit_summary.find(butlerQC.quantum.dataId[
"detector"]).getPhotoCalib()
195 photo_calib = inputs.pop(
"photo_calib")
196 outputs = self.run(visit_image, photo_calib=photo_calib, **inputs)
197 butlerQC.put(outputs, outputRefs)
201 visit_image: VisitImage,
203 photo_calib: PhotoCalib |
None =
None,
204 subtracted_background: BackgroundList |
None =
None,
205 alternate_background: BackgroundList |
None =
None,
207 instrumental_unit = astropy.units.Unit(self.config.instrumental_unit)
208 if photo_calib
is not None:
209 visit_image.photometric_scaling = field_from_legacy_photo_calib(
210 photo_calib, bounds=visit_image.bbox, instrumental_unit=instrumental_unit
212 if subtracted_background
is not None:
216 visit_image.backgrounds.add(
218 field_from_legacy_background(
219 subtracted_background, bounds=visit_image.bbox, unit=instrumental_unit
221 self.config.subtracted_background_description,
224 if alternate_background
is not None:
225 assert self.config.alternate_background_name
is not None, (
226 "Configuration and arguments are inconsistent."
228 match self.config.alternate_background_type:
231 case
"differential_composed":
232 if subtracted_background
is None:
235 return Struct(future_visit_image=visit_image)
236 for (subtracted_term, *_), (alternate_term, *_)
in zip(
237 subtracted_background, alternate_background
239 if not self._do_backgrounds_cancel(subtracted_term, alternate_term):
241 "alternate_background_type='differential_composed', but the alternate "
242 "background does not start with the inverse of the subtracted background."
244 alternate_background = BackgroundList(
246 alternate_background[n]
247 for n
in range(len(subtracted_background), len(alternate_background))
250 case
"differential_fit":
251 if subtracted_background
is None:
254 return Struct(future_visit_image=visit_image)
255 alternate_background = BackgroundList(*subtracted_background, *alternate_background)
256 visit_image.backgrounds.add(
257 self.config.alternate_background_name,
258 field_from_legacy_background(
259 alternate_background, bounds=visit_image.bbox, unit=instrumental_unit
261 self.config.alternate_background_description,
264 return Struct(future_visit_image=visit_image)
266 def _do_backgrounds_cancel(self, bg1: BackgroundMI, bg2: BackgroundMI) -> bool:
267 ctrl1 = bg1.getBackgroundControl()
268 ctrl2 = bg2.getBackgroundControl()
269 if ctrl1.getInterpStyle() != ctrl2.getInterpStyle():
271 if ctrl1.getApproximateControl().getStyle() != ctrl2.getApproximateControl().getStyle():
273 bins1 = bg1.getStatsImage()
274 bins2 = bg2.getStatsImage()
275 if bins1.getBBox() != bins2.getBBox():
277 return np.array_equal(bins1.image.array, -bins2.image.array, equal_nan=
True)