Coverage for python/lsst/images/cells/_coadd.py: 64%
262 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-17 14:19 -0700
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-17 14:19 -0700
1# This file is part of lsst-images.
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# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = ("CellCoadd", "CellCoaddSerializationModel")
16import functools
17from collections.abc import Mapping, Sequence
18from types import EllipsisType
19from typing import TYPE_CHECKING, Any, ClassVar, cast
21import astropy.io.fits
22import astropy.units
23import astropy.wcs
24import numpy as np
25import pydantic
27from .._backgrounds import BackgroundMap, BackgroundMapSerializationModel
28from .._cell_grid import CellGrid, CellGridBounds, PatchDefinition
29from .._geom import YX, Box
30from .._image import Image, ImageSerializationModel
31from .._mask import Mask, MaskPlane, MaskSchema, MaskSerializationModel, get_legacy_deep_coadd_mask_planes
32from .._masked_image import MaskedImage, MaskedImageSerializationModel
33from .._transforms import SkyProjection, SkyProjectionSerializationModel, TractFrame
34from ..describe import DescribeOptions, FieldRole, Report, ReportField
35from ..fields import BaseField
36from ..serialization import InputArchive, InvalidParameterError, OutputArchive
37from ._aperture_corrections import CellApertureCorrectionMapSerializationModel, CellField
38from ._provenance import CoaddProvenance, CoaddProvenanceSerializationModel
39from ._psf import CellPointSpreadFunction, CellPointSpreadFunctionSerializationModel
41if TYPE_CHECKING:
42 try:
43 from lsst.afw.image import Exposure as LegacyExposure
44 from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd
45 from lsst.skymap import TractInfo
46 except ImportError:
47 type LegacyExposure = Any # type: ignore[no-redef]
48 type LegacyMultipleCellCoadd = Any # type: ignore[no-redef]
49 type TractInfo = Any # type: ignore[no-redef]
52class CellCoadd(MaskedImage):
53 """A coadd comprised of cells on a regular grid.
55 Parameters
56 ----------
57 image
58 The main image plane. If this has a `.SkyProjection`, it will be used
59 for all planes unless a ``sky_projection`` is passed separately.
60 mask
61 A bitmask image that annotates the main image plane. Must have the
62 same bounding box as ``image`` if provided. Any attached
63 ``sky_projection`` is replaced (possibly by `None`).
64 variance
65 The per-pixel uncertainty of the main image as an image of variance
66 values. Must have the same bounding box as ``image`` if provided, and
67 its units must be the square of ``image.unit`` or `None`.
68 Values default to ``1.0``. Any attached ``sky_projection`` is replaced
69 (possibly by `None`).
70 mask_fractions
71 A mapping from an input-image mask plane name to an image of the
72 weights sums of that plane.
73 noise_realizations
74 A sequence of images with Monte Carlo realizations of the noise in
75 the coadd.
76 mask_schema
77 Schema for the mask plane. Must be provided if and only if ``mask`` is
78 not provided.
79 sky_projection
80 Projection that maps the pixel grid to the sky. Can only be `None` if
81 a projection is already attached to ``image``.
82 band
83 Name of the band.
84 psf
85 Effective point-spread function for the coadd. The missing cells
86 reported by ``psf.bounds`` are assumed to apply to all image data for
87 that cell as well (i.e. there is a PSF for a cell if and only if
88 there is image data for that cell).
89 aperture_corrections
90 Aperture corrections for different photometry algorithms.
91 patch
92 Identifiers and geometry of the full patch, if the image is confined
93 to a single patch. When present, the cell grid of the PSF and
94 provenance (if provideD) must be the full patch grid, even if its
95 bounds select a subset of that area.
96 provenance
97 Information about the images that went into the coadd.
98 backgrounds
99 Background models associated with this image.
100 """
102 def __init__(
103 self,
104 image: Image,
105 *,
106 mask: Mask | None = None,
107 variance: Image | None = None,
108 mask_fractions: Mapping[str, Image] | None = None,
109 noise_realizations: Sequence[Image] = (),
110 mask_schema: MaskSchema | None = None,
111 sky_projection: SkyProjection[TractFrame] | None = None,
112 band: str | None = None,
113 psf: CellPointSpreadFunction,
114 aperture_corrections: Mapping[str, CellField] | None = None,
115 patch: PatchDefinition | None = None,
116 provenance: CoaddProvenance | None = None,
117 backgrounds: BackgroundMap | None = None,
118 ) -> None:
119 super().__init__(
120 image,
121 mask=mask,
122 variance=variance,
123 mask_schema=mask_schema,
124 sky_projection=sky_projection,
125 )
126 if self.image.unit is None: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true
127 raise TypeError("The image component of a CellCoadd must have units.")
128 if self.image.sky_projection is None: 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true
129 raise TypeError("The sky_projection component of a CellCoadd cannot be None.")
130 if not isinstance(self.image.sky_projection.pixel_frame, TractFrame): 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true
131 raise TypeError("The sky_projection's pixel frame must be a TractFrame for CellCoadd.")
132 self._mask_fractions = dict(mask_fractions) if mask_fractions is not None else {}
133 self._noise_realizations = list(noise_realizations)
134 self._band = band
135 if psf.bounds.bbox != self.bbox:
136 psf = psf[self.bbox.intersection(psf.bounds.bbox)]
137 self._psf = psf
138 self._aperture_corrections = dict(aperture_corrections) if aperture_corrections is not None else {}
139 for ap_corr_name, ap_corr_field in self._aperture_corrections.items():
140 if ap_corr_field.bounds.grid != self.grid: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 raise ValueError(
142 f"Grids for cell PSF and {ap_corr_name} aperture corrections are not consistent."
143 )
144 self._patch = patch
145 self._provenance = provenance
146 if self._provenance and not self._patch: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true
147 raise TypeError("A CellCoadd cannot carry provenance without a patch definition.")
148 self._backgrounds = backgrounds if backgrounds is not None else BackgroundMap()
150 @property
151 def skymap(self) -> str:
152 """Name of the skymap (`str`)."""
153 return self.sky_projection.pixel_frame.skymap
155 @property
156 def tract(self) -> int:
157 """ID of the tract (`int`)."""
158 return self.sky_projection.pixel_frame.tract
160 @property
161 def patch(self) -> PatchDefinition:
162 """Identifiers and geometry of the full patch, if the image is confined
163 to a single patch (`PatchDefinition`).
164 """
165 if self._patch is None: 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 raise AttributeError("Coadd has no patch information.")
167 return self._patch
169 @property
170 def band(self) -> str | None:
171 """Name of the band (`str` or `None`)."""
172 return self._band
174 @property
175 def mask_fractions(self) -> Mapping[str, Image]:
176 """A mapping from an input-image mask plane name to an image of the
177 weights sums of that plane
178 (`~collections.abc.Mapping` [`str`, `.Image`]).
179 """
180 return self._mask_fractions
182 @property
183 def noise_realizations(self) -> Sequence[Image]:
184 """A sequence of images with Monte Carlo realizations of the noise in
185 the coadd (`~collections.abc.Sequence` [`.Image`]).
186 """
187 return self._noise_realizations
189 @property
190 def unit(self) -> astropy.units.UnitBase:
191 """The units of the image plane (`astropy.units.Unit`)."""
192 return cast(astropy.units.UnitBase, super().unit)
194 @property
195 def sky_projection(self) -> SkyProjection[TractFrame]:
196 """The projection that maps the pixel grid to the sky
197 (`.SkyProjection` [`.TractFrame`]).
198 """
199 return cast(SkyProjection[TractFrame], super().sky_projection)
201 @property
202 def psf(self) -> CellPointSpreadFunction:
203 """Effective point-spread function for the coadd
204 (`CellPointSpreadFunction`).
205 """
206 return self._psf
208 @property
209 def aperture_corrections(self) -> Mapping[str, CellField]:
210 """Aperture corrections for different photometry algorithms
211 (`dict` [`str`, `CellField`]).
212 """
213 return self._aperture_corrections
215 @property
216 def bounds(self) -> CellGridBounds:
217 """The grid of cells that overlap this coadd and a set of missing
218 cells (`CellGridBounds`).
219 """
220 return self._psf.bounds
222 @property
223 def grid(self) -> CellGrid:
224 """The grid of cells that overlap this coadd (`CellGrid`)."""
225 return self._psf.bounds.grid
227 @property
228 def provenance(self) -> CoaddProvenance:
229 """Information about the images that went into the coadd
230 (`CoaddProvenance` or `None`).
231 """
232 if self._provenance is None: 232 ↛ 233line 232 didn't jump to line 233 because the condition on line 232 was never true
233 raise AttributeError("Coadd has no provenance information.")
234 return self._provenance
236 @property
237 def backgrounds(self) -> BackgroundMap:
238 """A mapping of backgrounds associated with this image
239 (`.BackgroundMap`).
240 """
241 return self._backgrounds
243 def __getitem__(self, bbox: Box | EllipsisType) -> CellCoadd:
244 bbox, _ = self._handle_getitem_args(bbox)
245 psf = self.psf[bbox]
246 return self._transfer_metadata(
247 CellCoadd(
248 self.image[bbox],
249 mask=self.mask[bbox],
250 variance=self.variance[bbox],
251 sky_projection=self.sky_projection,
252 mask_fractions={k: v[bbox] for k, v in self._mask_fractions.items()},
253 noise_realizations=[v[bbox] for v in self._noise_realizations],
254 band=self.band,
255 psf=psf,
256 patch=self._patch,
257 provenance=(
258 self._provenance.subset(psf.bounds.cell_indices())
259 if self._provenance is not None
260 else None
261 ),
262 backgrounds=self._backgrounds,
263 aperture_corrections=self._aperture_corrections.copy(),
264 ),
265 bbox=bbox,
266 )
268 def _describe(self, options: DescribeOptions = DescribeOptions(), /) -> Report:
269 """Return a `Report` describing this cell coadd.
271 Parameters
272 ----------
273 options : `DescribeOptions`, optional
274 Rendering options; forwarded to all children.
275 """
276 fields = [
277 ReportField(label="skymap", value=self.skymap, role=FieldRole.DERIVED),
278 ReportField(label="tract", value=self.tract, role=FieldRole.DERIVED),
279 ReportField(label="patch", value=self._patch, role=FieldRole.DERIVED),
280 ReportField(label="band", value=self.band, role=FieldRole.DERIVED),
281 ReportField(label="bbox", value=self.bbox, repr_value=repr(self.bbox), role=FieldRole.DERIVED),
282 ]
283 summary = f"CellCoadd({self.bbox!s}, tract={self.tract})"
284 if options.brief:
285 return Report(type_name="CellCoadd", summary=summary, fields=fields)
286 # Components with no report of their own get a line each, so that
287 # nothing the coadd carries is absent from its description.
288 if self._mask_fractions:
289 fields.append(
290 ReportField(
291 label="mask_fractions", value=", ".join(self._mask_fractions), role=FieldRole.DERIVED
292 )
293 )
294 if self._noise_realizations:
295 n_noise = len(self._noise_realizations)
296 fields.append(
297 ReportField(
298 label="noise_realizations",
299 value=(
300 f"{n_noise} image{'s' if n_noise != 1 else ''} "
301 f"(dtype {self._noise_realizations[0].array.dtype})"
302 ),
303 role=FieldRole.DERIVED,
304 )
305 )
306 if self._aperture_corrections:
307 n_ap_corr = len(self._aperture_corrections)
308 # Never print all the keys even in detailed mode since there
309 # are usually many of them and they swamp the report.
310 ap_corr = f"{n_ap_corr} field{'s' if n_ap_corr != 1 else ''}"
311 fields.append(ReportField(label="aperture_corrections", value=ap_corr, role=FieldRole.DERIVED))
312 if self._provenance is None:
313 # A coadd without provenance is a meaningful state: it is what
314 # reading with provenance=False gives, and it is what makes
315 # to_legacy_cell_coadd fail.
316 fields.append(ReportField(label="provenance", value="none", role=FieldRole.DERIVED))
317 child = options.for_child()
318 plane = options.for_child("sky_projection", "bbox")
319 children: dict[str, Report] = {
320 "image": self.image._describe(plane),
321 "mask": self.mask._describe(plane),
322 "variance": self.variance._describe(plane),
323 "sky_projection": self.sky_projection._describe(child, bbox=self.bbox),
324 "psf": self.psf._describe(child),
325 }
326 if self._provenance is not None:
327 # The provenance property raises when there is none. Its cells
328 # come from this coadd, so pass them down for the coverage ratio.
329 children["provenance"] = self._provenance._describe(child, bounds=self.bounds)
330 children["backgrounds"] = self.backgrounds._describe(child)
331 return Report(
332 type_name="CellCoadd",
333 summary=summary,
334 fields=fields,
335 children=children,
336 )
338 def copy(self) -> CellCoadd:
339 """Deep-copy the coadd."""
340 return self._transfer_metadata(
341 CellCoadd(
342 image=self._image.copy(),
343 mask=self._mask.copy(),
344 variance=self._variance.copy(),
345 sky_projection=self.sky_projection,
346 mask_fractions={k: v.copy() for k, v in self._mask_fractions.items()},
347 noise_realizations=[v.copy() for v in self._noise_realizations],
348 band=self.band,
349 psf=self.psf,
350 patch=self.patch,
351 provenance=self.provenance,
352 backgrounds=self._backgrounds.copy(),
353 aperture_corrections=self._aperture_corrections.copy(),
354 ),
355 copy=True,
356 )
358 def apply_background(self, name: str | None) -> None:
359 """Subtract the background with the given name, modifying the image
360 in place.
362 If ``name`` is `None`, restore the original background.
364 Parameters
365 ----------
366 name
367 Name of the background to subtract, or `None` to restore the
368 original background.
369 """
370 current_bg = self.backgrounds.subtracted
371 if current_bg is not None:
372 if name == current_bg.name: 372 ↛ 373line 372 didn't jump to line 373 because the condition on line 372 was never true
373 return
374 self.image.quantity += current_bg.field.render(self.bbox, dtype=self.image.array.dtype).quantity
375 if name is None:
376 self._backgrounds._subtracted = None
377 return
378 new_bg = self.backgrounds[name]
379 self.image.quantity -= new_bg.field.render(self.bbox, dtype=self.image.array.dtype).quantity
380 self._backgrounds._subtracted = name
382 def serialize(self, archive: OutputArchive[Any]) -> CellCoaddSerializationModel:
383 """Serialize the image to an output archive.
385 Parameters
386 ----------
387 archive
388 Archive to write to.
389 """
390 serialized_image = archive.serialize_direct(
391 "image",
392 functools.partial(self.image.serialize, save_projection=False, tile_shape=self.grid.cell_shape),
393 )
394 serialized_mask = archive.serialize_direct(
395 "mask",
396 functools.partial(self.mask.serialize, save_projection=False, tile_shape=self.grid.cell_shape),
397 )
398 serialized_variance = archive.serialize_direct(
399 "variance",
400 functools.partial(
401 self.variance.serialize, save_projection=False, tile_shape=self.grid.cell_shape
402 ),
403 )
404 serialized_projection = archive.serialize_direct("sky_projection", self.sky_projection.serialize)
405 serialized_mask_fractions = {
406 k: archive.serialize_direct(
407 f"mask_fractions/{k}",
408 functools.partial(
409 v.serialize,
410 save_projection=False,
411 tile_shape=self.grid.cell_shape,
412 options_name="mask_fractions",
413 ),
414 )
415 for k, v in self.mask_fractions.items()
416 }
417 serialized_noise_realizations = [
418 archive.serialize_direct(
419 f"noise_realizations/{n}",
420 functools.partial(
421 v.serialize, save_projection=False, tile_shape=self.grid.cell_shape, options_name="image"
422 ),
423 )
424 for n, v in enumerate(self.noise_realizations)
425 ]
426 serialized_psf = archive.serialize_direct("psf", self.psf.serialize)
427 serialized_aperture_corrections = archive.serialize_direct(
428 "aperture_corrections",
429 functools.partial(
430 CellApertureCorrectionMapSerializationModel.serialize, self.aperture_corrections
431 ),
432 )
433 serialized_provenance = (
434 archive.serialize_direct("provenance", self._provenance.serialize)
435 if self._provenance is not None
436 else None
437 )
438 serialized_backgrounds = archive.serialize_direct("background", self._backgrounds.serialize)
439 return CellCoaddSerializationModel(
440 image=serialized_image,
441 mask=serialized_mask,
442 variance=serialized_variance,
443 sky_projection=serialized_projection,
444 mask_fractions=serialized_mask_fractions,
445 noise_realizations=serialized_noise_realizations,
446 band=self._band,
447 psf=serialized_psf,
448 aperture_corrections=serialized_aperture_corrections,
449 patch=self._patch,
450 provenance=serialized_provenance,
451 backgrounds=serialized_backgrounds,
452 metadata=self.metadata,
453 )
455 @staticmethod
456 def _get_archive_tree_type[P: pydantic.BaseModel](
457 pointer_type: type[P],
458 ) -> type[CellCoaddSerializationModel[P]]:
459 """Return the serialization model type for this object for an archive
460 type that uses the given pointer type.
461 """
462 return CellCoaddSerializationModel[pointer_type] # type: ignore
464 @staticmethod
465 def from_legacy_cell_coadd(
466 legacy: LegacyMultipleCellCoadd,
467 *,
468 plane_map: Mapping[str, MaskPlane] | None = None,
469 tract_info: TractInfo,
470 bbox: Box | None = None,
471 ) -> CellCoadd:
472 """Convert from a `lsst.cell_coadds.MultipleCellCoadd` instance.
474 Parameters
475 ----------
476 legacy
477 A `lsst.cell_coadds.MultipleCellCoadd` instance to convert.
478 plane_map
479 A mapping from legacy mask plane name to the new plane name and
480 description.
481 tract_info
482 Information about the full tract.
483 bbox
484 Bounding box of the image. The default is to include just the
485 bounding box of the valid cells, which may not cover a full patch.
486 """
487 from lsst.geom import Box2I
489 if plane_map is None:
490 plane_map = get_legacy_deep_coadd_mask_planes()
491 if bbox is None:
492 legacy_bbox = Box2I()
493 for single_cell in legacy.cells.values():
494 legacy_bbox.include(single_cell.inner.bbox)
495 else:
496 legacy_bbox = bbox.to_legacy()
497 legacy_stitched = legacy.stitch(legacy_bbox)
498 unit = astropy.units.Unit(legacy.units.value)
499 tract_bbox = Box.from_legacy(tract_info.getBBox())
500 sky_projection = SkyProjection.from_legacy(
501 legacy.wcs,
502 TractFrame(
503 skymap=legacy.identifiers.skymap,
504 tract=legacy.identifiers.tract,
505 bbox=tract_bbox,
506 ),
507 pixel_bounds=tract_bbox,
508 )
509 band = legacy.identifiers.band
510 image = Image.from_legacy(legacy_stitched.image, unit=unit)
511 mask = Mask.from_legacy(legacy_stitched.mask, plane_map=plane_map)
512 variance = Image.from_legacy(legacy_stitched.variance, unit=unit**2)
513 noise_realizations = [
514 Image.from_legacy(noise_image) for noise_image in legacy_stitched.noise_realizations
515 ]
516 mask_fractions = (
517 {"rejected": Image.from_legacy(legacy_stitched.mask_fractions)}
518 if legacy_stitched.mask_fractions is not None
519 else {}
520 )
521 psf = CellPointSpreadFunction.from_legacy(legacy_stitched.psf, image.bbox)
522 aperture_corrections = {
523 ap_corr_name: CellField.from_legacy_aperture_correction(legacy_ap_corr, psf.bounds)
524 for ap_corr_name, legacy_ap_corr in legacy_stitched.ap_corr_map.items()
525 }
526 patch_info = tract_info[legacy.identifiers.patch]
527 patch = PatchDefinition(
528 id=patch_info.getSequentialIndex(),
529 index=YX(y=legacy.identifiers.patch.y, x=legacy.identifiers.patch.x),
530 inner_bbox=Box.from_legacy(patch_info.getInnerBBox()),
531 cells=CellGrid.from_legacy(legacy.grid),
532 )
533 provenance = CoaddProvenance.from_legacy(legacy)
534 return CellCoadd(
535 image=image,
536 mask=mask,
537 variance=variance,
538 mask_fractions=mask_fractions,
539 noise_realizations=noise_realizations,
540 sky_projection=sky_projection,
541 band=band,
542 psf=psf,
543 aperture_corrections=aperture_corrections,
544 patch=patch,
545 provenance=provenance,
546 )
548 def to_legacy_cell_coadd(
549 self, copy: bool | None = None, plane_map: Mapping[str, MaskPlane] | None = None
550 ) -> LegacyMultipleCellCoadd:
551 """Convert to a `lsst.cell_coadds.MultipleCellCoadd` instance.
553 Parameters
554 ----------
555 copy
556 If `True`, always copy the image and variance pixel data.
557 If `False`, return a view, and raise `TypeError` if the pixel data
558 is read-only (this is not supported by afw). If `None`, only copy
559 if the pixel data is read-only. Mask pixel data is always copied.
560 plane_map
561 A mapping from legacy mask plane name to the new plane name and
562 description.
563 """
564 from frozendict import frozendict
566 from lsst.cell_coadds import CellIdentifiers as LegacyCellIdentifiers
567 from lsst.cell_coadds import CoaddUnits as LegacyCoaddUnits
568 from lsst.cell_coadds import CommonComponents as LegacyCommonComponents
569 from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd
570 from lsst.cell_coadds import OwnedImagePlanes as LegacyOwnedImagePlanes
571 from lsst.cell_coadds import PatchIdentifiers as LegacyPatchIdentifiers
572 from lsst.cell_coadds import SingleCellCoadd as LegacySingleCellCoadd
573 from lsst.skymap import Index2D as LegacyIndex2D
575 if plane_map is None:
576 plane_map = get_legacy_deep_coadd_mask_planes()
577 if self.unit != astropy.units.nJy:
578 raise ValueError("CellCoadd.to_legacy requires nJy pixel units.")
579 if self.bbox != self.bounds.bbox:
580 raise ValueError("MultipleCellCoadd requires its bounding box to lie on the cell grid.")
581 legacy_grid = self.grid.to_legacy()
582 visit_polygons = self.provenance.to_legacy_polygon_map()
583 legacy_common = LegacyCommonComponents(
584 units=LegacyCoaddUnits.nJy,
585 wcs=self.sky_projection.to_legacy(),
586 band=self.band,
587 identifiers=LegacyPatchIdentifiers(
588 self.skymap,
589 self.tract,
590 LegacyIndex2D(x=self.patch.index.x, y=self.patch.index.y),
591 band=self.band,
592 ),
593 visit_polygons=visit_polygons,
594 )
595 legacy_inputs = self.provenance.to_legacy_cell_coadd_inputs(visit_polygons.keys())
596 cells: list[LegacySingleCellCoadd] = []
597 for cell_index in self.bounds.cell_indices():
598 cell_bbox = self.grid.bbox_of(cell_index)
599 # Legacy type only has room for one mask_fractions plane.
600 legacy_mask_fractions = (
601 next(iter(self.mask_fractions.values()))[cell_bbox].to_legacy(copy=copy)
602 if self.mask_fractions
603 else None
604 )
605 legacy_planes = LegacyOwnedImagePlanes(
606 image=self.image[cell_bbox].to_legacy(copy=copy),
607 mask=self.mask[cell_bbox].to_legacy(plane_map),
608 variance=self.variance[cell_bbox].to_legacy(copy=copy),
609 mask_fractions=legacy_mask_fractions,
610 noise_realizations=[n[cell_bbox].to_legacy(copy=copy) for n in self.noise_realizations],
611 )
612 legacy_aperture_correction_map = frozendict(
613 {
614 name: field.value_in_cell(cell_index)
615 if cell_index not in field.bounds.missing
616 else np.nan
617 for name, field in self.aperture_corrections.items()
618 }
619 )
620 cells.append(
621 LegacySingleCellCoadd(
622 legacy_planes,
623 psf=self.psf[cell_index].to_legacy(copy=copy),
624 inner_bbox=cell_bbox.to_legacy(),
625 common=legacy_common,
626 inputs=legacy_inputs[cell_index.to_legacy()],
627 identifiers=LegacyCellIdentifiers(
628 self.skymap,
629 self.tract,
630 legacy_common.identifiers.patch,
631 band=self.band,
632 cell=cell_index.to_legacy(),
633 ),
634 aperture_correction_map=legacy_aperture_correction_map,
635 )
636 )
637 return LegacyMultipleCellCoadd(
638 cells,
639 legacy_grid,
640 outer_cell_size=self.grid.cell_shape.to_legacy_int_extent(),
641 psf_image_size=self.psf.kernel_bbox.shape.to_legacy_int_extent(),
642 common=legacy_common,
643 inner_bbox=self.bbox.to_legacy(),
644 )
646 def to_legacy(
647 self, copy: bool | None = None, plane_map: Mapping[str, MaskPlane] | None = None
648 ) -> LegacyExposure:
649 """Convert to a `lsst.afw.image.Exposure` instance.
651 Parameters
652 ----------
653 copy
654 If `True`, always copy the image and variance pixel data.
655 If `False`, return a view, and raise `TypeError` if the pixel data
656 is read-only (this is not supported by afw). If `None`, only copy
657 if the pixel data is read-only. Mask pixel data is always copied.
658 plane_map
659 A mapping from legacy mask plane name to the new plane name and
660 description.
662 Returns
663 -------
664 `lsst.afw.image.Exposure`
665 A legacy representation of the coadd. This will have its ``wcs``,
666 ``psf``, ``filter``, ``photoCalib``, and ``metadata`` components
667 set. The ``apCorrMap`` component is not set, because there is no
668 true `lsst.afw.math.BoundedField` representation for cell-coadd
669 aperture corrections, and the ``coaddInputs`` component is not set
670 because that data structure cannot fully capture cell-coadd
671 provenance.
673 Notes
674 -----
675 This method requires the `provenance` attribute to have been populated
676 at construction.
677 """
678 from lsst.afw.image import Exposure as LegacyExposure
679 from lsst.afw.image import FilterLabel as LegacyFilterLabel
681 if plane_map is None:
682 plane_map = get_legacy_deep_coadd_mask_planes()
683 legacy_masked_image = super().to_legacy(copy=copy, plane_map=plane_map)
684 result = LegacyExposure(legacy_masked_image, dtype=self.image.array.dtype)
685 result_info = result.info
686 result_info.setWcs(self.sky_projection.to_legacy())
687 result_info.setPsf(self.psf.to_legacy())
688 result_info.setFilter(LegacyFilterLabel.fromBand(self.band))
689 result_info.setPhotoCalib(BaseField.make_legacy_photo_calib(self.unit))
690 # We don't do setCoaddInputs because that data structure can't really
691 # represent cell-coadd provenance accurately, and it's not clear
692 # anything would use it.
693 self._fill_legacy_metadata(result_info.getMetadata())
694 # We can't do setApCorrMap because the legacy
695 # StitchedApertureCorrection is not a real C++ BoundedField, just a
696 # Python duck-alike.
697 return result
700class CellCoaddSerializationModel[P: pydantic.BaseModel](MaskedImageSerializationModel[P]):
701 """A Pydantic model used to represent a serialized `CellCoadd`."""
703 SCHEMA_NAME: ClassVar[str] = "cell_coadd"
704 SCHEMA_VERSION: ClassVar[str] = "1.0.0"
705 MIN_READ_VERSION: ClassVar[int] = 1
706 PUBLIC_TYPE: ClassVar[type] = CellCoadd
708 # Inherited attributes are duplicated because that improves the docs
709 # (some limitation in the sphinx/pydantic integration), and these are
710 # important docs.
712 image: ImageSerializationModel[P] = pydantic.Field(description="The main data image.")
713 mask: MaskSerializationModel[P] = pydantic.Field(
714 description="Bitmask that annotates the main image's pixels."
715 )
716 variance: ImageSerializationModel[P] = pydantic.Field(
717 description="Per-pixel variance estimates for the main image."
718 )
719 sky_projection: SkyProjectionSerializationModel[P] = pydantic.Field(
720 description="Projection that maps the pixel grid to the sky.",
721 )
722 mask_fractions: dict[str, ImageSerializationModel[P]] = pydantic.Field(
723 description=(
724 "A mapping from an input-image mask plane name to an image of the weights sums of that plane."
725 )
726 )
727 noise_realizations: list[ImageSerializationModel[P]] = pydantic.Field(
728 description=(
729 "A mapping from an input-image mask plane name to an image of the weights sums of that plane."
730 )
731 )
732 band: str | None = pydantic.Field(description="Name of the band.")
733 psf: CellPointSpreadFunctionSerializationModel = pydantic.Field(
734 description="Effective point-spread function model for the coadd."
735 )
736 aperture_corrections: CellApertureCorrectionMapSerializationModel | None = pydantic.Field(
737 None, description="Coadded aperture corrections for different photometry algorithms."
738 )
739 patch: PatchDefinition | None = pydantic.Field(description="Identifiers and geometry for the patch.")
740 provenance: CoaddProvenanceSerializationModel | None = pydantic.Field(
741 description="Information about the images that went into the coadd."
742 )
743 backgrounds: BackgroundMapSerializationModel = pydantic.Field(
744 default_factory=BackgroundMapSerializationModel,
745 description="Background models associated with this image.",
746 )
748 def deserialize(
749 self,
750 archive: InputArchive[Any],
751 *,
752 bbox: Box | None = None,
753 provenance: bool = True,
754 **kwargs: Any,
755 ) -> CellCoadd:
756 """Deserialize an image from an input archive.
758 Parameters
759 ----------
760 archive
761 Archive to read from.
762 bbox
763 Bounding box of a subimage to read instead.
764 provenance
765 Whether to read and attach provenance information.
766 **kwargs
767 Unsupported keyword arguments are accepted only to provide better
768 error messages (raising `.serialization.InvalidParameterError`).
769 """
770 if kwargs: 770 ↛ 771line 770 didn't jump to line 771 because the condition on line 770 was never true
771 raise InvalidParameterError(f"Unrecognized parameters for CellCoadd: {set(kwargs.keys())}.")
772 masked_image = super().deserialize(archive, bbox=bbox)
773 mask_fractions = {
774 k.removeprefix("mask_fractions/"): v.deserialize(archive, bbox=bbox)
775 for k, v in self.mask_fractions.items()
776 }
777 noise_realizations = [v.deserialize(archive, bbox=bbox) for v in self.noise_realizations]
778 sky_projection = self.sky_projection.deserialize(archive)
779 psf = self.psf.deserialize(archive, bbox=bbox)
780 aperture_corrections = (
781 self.aperture_corrections.deserialize(archive) if self.aperture_corrections is not None else {}
782 )
783 coadd_provenance: CoaddProvenance | None = None
784 if self.provenance is not None and provenance: 784 ↛ 788line 784 didn't jump to line 788 because the condition on line 784 was always true
785 coadd_provenance = self.provenance.deserialize(archive)
786 if bbox is not None:
787 coadd_provenance = coadd_provenance.subset(psf.bounds.cell_indices())
788 backgrounds = self.backgrounds.deserialize(archive)
789 return CellCoadd(
790 masked_image.image,
791 mask=masked_image.mask,
792 variance=masked_image.variance,
793 mask_fractions=mask_fractions,
794 noise_realizations=noise_realizations,
795 sky_projection=sky_projection,
796 band=self.band,
797 psf=psf,
798 aperture_corrections=aperture_corrections,
799 patch=self.patch,
800 provenance=coadd_provenance,
801 backgrounds=backgrounds,
802 )._finish_deserialize(self)
804 def deserialize_component(self, component: str, archive: InputArchive[Any], **kwargs: Any) -> Any:
805 match component:
806 case "mask_fractions":
807 return {
808 name: image_model.deserialize(archive, **kwargs)
809 for name, image_model in self.mask_fractions.items()
810 }
811 case "noise_realizations":
812 return [image_model.deserialize(archive, **kwargs) for image_model in self.noise_realizations]
813 case "aperture_corrections" if self.aperture_corrections is None:
814 # super() delegation handles the not-None case.
815 return {}
816 case "masked_image":
817 return super().deserialize(archive, **kwargs)
818 return super().deserialize_component(component, archive, **kwargs)