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.",
96 exposure_record_proxy = cT.Input(
98 storageClass=
"Exposure",
99 dimensions={
"exposure",
"detector"},
100 doc=
"An arbitrary input used to provide an 'exposure' dimension record."
103 config: RewriteVisitImageConfig
105 def __init__(self, config: RewriteVisitImageConfig |
None):
106 super().__init__(config=config)
107 match self.config.photo_calib_source:
109 del self.visit_summary
111 case
"visit_summary":
114 del self.visit_summary
115 if self.config.alternate_background_type
is None:
116 del self.alternate_background
119class RewriteVisitImageConfig(
121 pipelineConnections=RewriteVisitImageConnections,
123 photo_calib_source = ChoiceField[str](
124 "Which kind of PhotoCalib to load.",
127 "attached":
"The input image has uncalibrated pixels and a nontrivial PhotoCalib attached to it.",
128 "visit_summary":
"Load the visit_summary connection to find the PhotoCalib already applied.",
129 "standalone":
"Load the photo_calib connection to find the PhotoCalib already applied.",
131 default=
"visit_summary",
133 subtracted_background_description = Field(
134 "Description of the subtracted background, to be stored with the image.",
136 default=
"Background subtracted from the image when generating the Source catalog.",
138 alternate_background_name = Field(
139 "Name for the alternate background attached to the image. "
140 "ignored if alternate_background_type is None.",
144 alternate_background_description = Field(
145 "Description for the alternate background attached to the image. "
146 "Ignored if alternate_background_type is None.",
149 "An alternate large-scale visit-level background subtracted from the input images that go into "
150 "pretty_coadd and RGB color images. May preserve more low surface-brightness features, but may "
151 "also be more affected by scattered light and other artifacts."
154 alternate_background_type = ChoiceField(
155 "How the alternate background relates to the subtracted background.",
159 "The alternate background should be subtracted after restoring the subtracted background."
161 "differential_composed": (
162 "The alternate background starts with terms that invert the subtracted background."
164 "differential_fit": (
165 "The alternate background was fit to the already-subtracted image, and "
166 "can only be subtracted from the already-subtracted image."
169 default=
"differential_composed",
172 instrumental_unit = Field(
173 "Unit for instrumental flux pixels (i.e. uncalibrated side of a PhotoCalib, "
174 "and the units of all background inputs).",
180class RewriteVisitImageTask(PipelineTask):
181 ConfigClass: ClassVar[type[RewriteVisitImageConfig]] = RewriteVisitImageConfig
182 config: RewriteVisitImageConfig
183 _DefaultName =
"rewriteVisitImage"
187 butlerQC: QuantumContext,
188 inputRefs: InputQuantizedConnection,
189 outputRefs: OutputQuantizedConnection,
191 exposure_record = inputRefs.exposure_record_proxy.dataId.records[
"exposure"]
192 del inputRefs.exposure_record_proxy
193 inputs = butlerQC.get(inputRefs)
194 visit_image = inputs.pop(
"legacy_exposure").get(
195 parameters={
"preserve_quantization":
True,
"exposure_record": exposure_record}
197 photo_calib: PhotoCalib |
None
198 match self.config.photo_calib_source:
201 case
"visit_summary":
202 visit_summary = inputs.pop(
"visit_summary")
203 photo_calib = visit_summary.find(butlerQC.quantum.dataId[
"detector"]).getPhotoCalib()
205 photo_calib = inputs.pop(
"photo_calib")
206 outputs = self.run(visit_image, photo_calib=photo_calib, **inputs)
207 butlerQC.put(outputs, outputRefs)
211 visit_image: VisitImage,
213 photo_calib: PhotoCalib |
None =
None,
214 subtracted_background: BackgroundList |
None =
None,
215 alternate_background: BackgroundList |
None =
None,
217 instrumental_unit = astropy.units.Unit(self.config.instrumental_unit)
218 if photo_calib
is not None:
219 visit_image.photometric_scaling = field_from_legacy_photo_calib(
220 photo_calib, bounds=visit_image.bbox, instrumental_unit=instrumental_unit
222 if subtracted_background
is not None:
226 visit_image.backgrounds.add(
228 field_from_legacy_background(
229 subtracted_background, bounds=visit_image.bbox, unit=instrumental_unit
231 self.config.subtracted_background_description,
234 if alternate_background
is not None:
235 assert self.config.alternate_background_name
is not None, (
236 "Configuration and arguments are inconsistent."
238 match self.config.alternate_background_type:
241 case
"differential_composed":
242 if subtracted_background
is None:
245 return Struct(future_visit_image=visit_image)
246 for (subtracted_term, *_), (alternate_term, *_)
in zip(
247 subtracted_background, alternate_background
249 if not self._do_backgrounds_cancel(subtracted_term, alternate_term):
251 "alternate_background_type='differential_composed', but the alternate "
252 "background does not start with the inverse of the subtracted background."
254 alternate_background = BackgroundList(
256 alternate_background[n]
257 for n
in range(len(subtracted_background), len(alternate_background))
260 case
"differential_fit":
261 if subtracted_background
is None:
264 return Struct(future_visit_image=visit_image)
265 alternate_background = BackgroundList(*subtracted_background, *alternate_background)
266 visit_image.backgrounds.add(
267 self.config.alternate_background_name,
268 field_from_legacy_background(
269 alternate_background, bounds=visit_image.bbox, unit=instrumental_unit
271 self.config.alternate_background_description,
274 return Struct(future_visit_image=visit_image)
276 def _do_backgrounds_cancel(self, bg1: BackgroundMI, bg2: BackgroundMI) -> bool:
277 ctrl1 = bg1.getBackgroundControl()
278 ctrl2 = bg2.getBackgroundControl()
279 if ctrl1.getInterpStyle() != ctrl2.getInterpStyle():
281 if ctrl1.getApproximateControl().getStyle() != ctrl2.getApproximateControl().getStyle():
283 bins1 = bg1.getStatsImage()
284 bins2 = bg2.getStatsImage()
285 if bins1.getBBox() != bins2.getBBox():
287 return np.array_equal(bins1.image.array, -bins2.image.array, equal_nan=
True)