23"""Make an illumination correction from the fgcmcal outputs.
25from datetime
import datetime, UTC
27from astropy.time
import Time, TimeDelta
31from lsst.daf.butler
import Timespan
32from lsst.pipe.base import PipelineTaskConnections, PipelineTaskConfig, connectionTypes, PipelineTask, Struct
35from .fgcmOutputProducts
import FgcmOutputProductsTask
36from .utilities
import computePixelAreaFieldDetector, computeReferencePixelScale
38__all__ = [
"FgcmOutputIlluminationCorrectionConfig",
"FgcmOutputIlluminationCorrectionTask"]
42 PipelineTaskConnections,
43 dimensions=(
"instrument",
"detector"),
44 defaultTemplates={
"cycleNumber":
"0"},
46 camera = connectionTypes.PrerequisiteInput(
47 doc=
"Camera instrument",
49 storageClass=
"Camera",
50 dimensions=(
"instrument",),
53 fgcm_visit_catalog = connectionTypes.Input(
54 doc=
"Catalog of visit information for fgcm.",
55 name=
"fgcmVisitCatalog",
56 storageClass=
"Catalog",
57 dimensions=(
"instrument",),
59 fgcm_fit_parameters_catalog = connectionTypes.Input(
60 doc=
"Catalog of fgcm fit parameters.",
61 name=
"fgcm_Cycle{cycleNumber}_FitParameters",
62 storageClass=
"Catalog",
63 dimensions=(
"instrument",),
65 flat_metadata = connectionTypes.PrerequisiteInput(
66 doc=
"Flat field metadata associated with illumination correction epoch.",
68 storageClass=
"PropertyList",
69 dimensions=[
"instrument",
"detector",
"physical_filter"],
73 illumination_corrections = connectionTypes.Output(
74 doc=
"Illumination corrections from fgcm fit.",
75 name=
"illuminationCorrection",
76 storageClass=
"ExposureF",
77 dimensions=[
"instrument",
"detector",
"physical_filter"],
82 def __init__(self, *, config=None):
83 super().__init__(config=config)
85 if str(int(config.connections.cycleNumber)) != config.connections.cycleNumber:
86 raise ValueError(
"cycleNumber must be of integer format")
88 if not config.use_flat_metadata:
89 del self.flat_metadata
91 def lookup_flat_metadata(dataset_type, registry, data_id, collections):
93 time = Time(config.epoch_time, format=config.epoch_format)
97 dataset_type.makeCompositeDatasetType(),
99 physical_filter=physical_filter,
101 time - TimeDelta(1, format=
"sec"),
102 time + TimeDelta(1, format=
"sec"),
104 collections=collections,
105 ).makeComponentRef(
"metadata")
106 for physical_filter
in config.physical_filters
109 self.flat_metadata = dataclasses.replace(self.flat_metadata, lookupFunction=lookup_flat_metadata)
112class FgcmOutputIlluminationCorrectionConfig(
114 pipelineConnections=FgcmOutputIlluminationCorrectionConnections,
116 """Configuration for FgcmOutputIlluminationCorrectionTask."""
119 doc=
"Use flat-field metadata for illumination correction metadata?",
124 doc=
"Time string (UTC) that corresponds to a date in the desired epoch.",
130 doc=
"Format for time string (e.g. iso, mjd, etc.), used by astropy.time.Time()",
135 doc=
"List of physical filters to produce illumination corrections.",
140 doc=
"Include WCS jacobian in illumination correction?",
145 doc=
"Use a Chebyshev approximation of the WCS jacobian in illumination correction?",
152 _ = Time(self.epoch_time, format=self.epoch_format)
153 except Exception
as e:
155 "Could not parse epoch_time/epoch_format ", e)
158class FgcmOutputIlluminationCorrectionTask(PipelineTask):
159 """Output illumination corrections from fgcm."""
160 ConfigClass = FgcmOutputIlluminationCorrectionConfig
161 _DefaultName =
"fgcmOutputIlluminationCorrection"
163 def runQuantum(self, butlerQC, inputRefs, outputRefs):
165 inputs = butlerQC.get(inputRefs)
167 detector_id = butlerQC.quantum.dataId[
"detector"]
169 filter_label_dict = {ref.dataId[
"physical_filter"]:
170 FilterLabel(physical=ref.dataId[
"physical_filter"], band=ref.dataId[
"band"])
171 for ref
in outputRefs.illumination_corrections}
173 flat_metadata_dict = {}
174 if self.config.use_flat_metadata:
175 for i, flat_metadata
in enumerate(inputs[
"flat_metadata"]):
176 ref = inputRefs.flat_metadata[i]
177 flat_metadata_dict[ref.dataId[
"physical_filter"]] = (ref.id, flat_metadata)
180 camera=inputs[
"camera"],
181 detector_id=detector_id,
182 fgcm_fit_parameters_catalog=inputs[
"fgcm_fit_parameters_catalog"],
183 filter_label_dict=filter_label_dict,
184 flat_metadata_dict=flat_metadata_dict,
188 illum_corr_ref_dict = {ref.dataId[
"physical_filter"]:
189 ref
for ref
in outputRefs.illumination_corrections}
190 for physical_filter
in retval.illum_corr_dict:
191 if physical_filter
in illum_corr_ref_dict:
193 "Serializing illumination correction for detector %d, physical_filter %s",
197 butlerQC.put(retval.illum_corr_dict[physical_filter], illum_corr_ref_dict[physical_filter])
204 fgcm_fit_parameters_catalog,
206 flat_metadata_dict={},
208 """Run the illumination correction output task.
212 camera : `lsst.afw.cameraGeom.camera`
213 The camera with camera geometry
215 The id of the detector.
216 fgcm_fit_parameters_catalog : `lsst.afw.SimpleCatalog`
217 Catalog of fgcm fit parameters.
218 filter_label_dict : `dict` [`str`: `lsst.afw.image.FilterLabel`]
219 Dictionary of filter labels, keyed by physical_filter.
220 flat_metadata_dict : `dict` [`str`: (`uuid.UUID`, `lsst.pipe.base.PropertyList`]
221 Dictionary of UUIDs and flat metadata, keyed by physical_filter.
225 struct : `lsst.pipe.base.Struct`
226 Output structure with keys:
228 ``illum_corr_dict``: dictionary keyed by physical_filter,
229 with illumination correction ExposureF.
231 epoch_time = Time(self.config.epoch_time, format=self.config.epoch_format)
232 epoch_mjd = epoch_time.mjd
234 detector_index = detector_id - camera[0].getId()
237 fgcm_star_flat = np.zeros(fgcm_fit_parameters_catalog[
"superstarSize"][0, :], dtype=
"f8")
238 fgcm_star_flat[:, :, :, :] = fgcm_fit_parameters_catalog[
"superstar"][0, :].reshape(
239 fgcm_star_flat.shape,
244 fgcm_filter_names = np.asarray(fgcm_fit_parameters_catalog[0][
"lutFilterNames"].split(
","))
246 epoch_mjd_start = fgcm_fit_parameters_catalog[0][
"epochMjdStart"]
247 epoch_mjd_end = fgcm_fit_parameters_catalog[0][
"epochMjdEnd"]
249 epoch_index, = np.where((epoch_mjd > epoch_mjd_start) & (epoch_mjd < epoch_mjd_end))
251 if len(epoch_index) == 0:
252 raise RuntimeError(f
"Could not find epoch at {epoch_time} in fgcm epochs.")
254 detector = camera[detector_id]
255 xymax = np.array([detector.getBBox().getMaxX(), detector.getBBox().getMaxY()])
256 area_scaling = 1. / computeReferencePixelScale(camera)**2.
261 for physical_filter
in self.config.physical_filters:
262 if physical_filter
not in filter_label_dict:
267 if physical_filter
not in fgcm_filter_names:
269 "FgcmOutputIlluminationCorrectionTask configured to generate correction for "
270 f
"physical filter {physical_filter} but that filter was not calibrated.",
274 filter_index, = np.where(fgcm_filter_names == physical_filter)
276 star_flat_pars = fgcm_star_flat[epoch_index, filter_index, detector_index, :].ravel()
278 illum_corr = ExposureF(detector.getBBox())
279 illum_corr.image.array[:, :] = 1.0
280 illum_corr.setDetector(detector)
283 if physical_filter
in flat_metadata_dict:
284 flat_uuid = str(flat_metadata_dict[physical_filter][0])
285 flat_md = flat_metadata_dict[physical_filter][1]
286 units = flat_md[
"LSST ISR UNITS"]
289 flat_uuid =
"Unknown"
293 illum_corr.info.setFilter(filter_label_dict[physical_filter])
294 illum_corr.metadata[
"LSST CALIB UUID FLAT"] = flat_uuid
295 illum_corr.metadata[
"LSST ISR UNITS"] = units
300 now = datetime.now(tz=UTC)
301 illum_corr.metadata.set(
302 "CALIB_CREATION_DATETIME",
303 now.strftime(
"%Y-%m-%dT%T"),
304 comment=
"UTC of processing",
306 local_time = now.astimezone()
307 calib_date = local_time.strftime(
"%Y-%m-%d")
308 calib_time = local_time.strftime(
"%X %Z")
309 illum_corr.metadata.set(
310 "CALIB_CREATION_DATE",
312 comment=
"Local time day of creation",
314 illum_corr.metadata.set(
315 "CALIB_CREATION_TIME",
317 comment=
"Local time in day of creation",
322 if star_flat_pars[0] < 100.0:
323 star_flat_field = FgcmOutputProductsTask._getChebyshevBoundedField(
328 star_flat_field.divideImage(illum_corr.image)
330 illum_corr.image.array[:, :] = np.clip(illum_corr.image.array[:, :],
None, 10.0)
334 f
"Invalid star flat found for detector {physical_filter} {detector_id}; "
335 "setting to all 1.0s.",
338 if self.config.include_wcs_jacobian:
341 pixel_area_field = computePixelAreaFieldDetector(
343 areaScaling=area_scaling,
344 approximate=self.config.approximate_wcs_jacobian,
347 pixel_area_field.divideImage(illum_corr.image)
350 illum_corr_dict[physical_filter] = illum_corr
352 self.log.info(
"Successfully created %d illumination corrections for detector %d", count, detector_id)
354 return Struct(illum_corr_dict=illum_corr_dict)
A group of labels for a filter in an exposure or coadd.