Coverage for python/lsst/images/cells/_coadd.py: 65%
264 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 09:39 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 09:39 +0000
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:
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:
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 and name == current_bg.name: 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true
372 return
374 adjustment: astropy.units.Quantity | None = None
375 if current_bg is not None:
376 adjustment = current_bg.field.render(self.bbox, dtype=self.image.array.dtype).quantity
377 if name is not None:
378 new_bg = self.backgrounds[name]
379 new_values = new_bg.field.render(self.bbox, dtype=self.image.array.dtype).quantity
380 adjustment = -new_values if adjustment is None else adjustment - new_values
382 # Resolve and render every model before changing either pixels or
383 # state. Computing one combined adjustment also ensures a failed unit
384 # conversion cannot leave a previously subtracted background restored
385 # while the map still says it is subtracted.
386 if adjustment is not None: 386 ↛ 388line 386 didn't jump to line 388 because the condition on line 386 was always true
387 self.image.quantity += adjustment
388 self._backgrounds._subtracted = name
390 def serialize(self, archive: OutputArchive[Any]) -> CellCoaddSerializationModel:
391 """Serialize the image to an output archive.
393 Parameters
394 ----------
395 archive
396 Archive to write to.
397 """
398 serialized_image = archive.serialize_direct(
399 "image",
400 functools.partial(self.image.serialize, save_projection=False, tile_shape=self.grid.cell_shape),
401 )
402 serialized_mask = archive.serialize_direct(
403 "mask",
404 functools.partial(self.mask.serialize, save_projection=False, tile_shape=self.grid.cell_shape),
405 )
406 serialized_variance = archive.serialize_direct(
407 "variance",
408 functools.partial(
409 self.variance.serialize, save_projection=False, tile_shape=self.grid.cell_shape
410 ),
411 )
412 serialized_projection = archive.serialize_direct("sky_projection", self.sky_projection.serialize)
413 serialized_mask_fractions = {
414 k: archive.serialize_direct(
415 f"mask_fractions/{k}",
416 functools.partial(
417 v.serialize,
418 save_projection=False,
419 tile_shape=self.grid.cell_shape,
420 options_name="mask_fractions",
421 ),
422 )
423 for k, v in self.mask_fractions.items()
424 }
425 serialized_noise_realizations = [
426 archive.serialize_direct(
427 f"noise_realizations/{n}",
428 functools.partial(
429 v.serialize, save_projection=False, tile_shape=self.grid.cell_shape, options_name="image"
430 ),
431 )
432 for n, v in enumerate(self.noise_realizations)
433 ]
434 serialized_psf = archive.serialize_direct("psf", self.psf.serialize)
435 serialized_aperture_corrections = archive.serialize_direct(
436 "aperture_corrections",
437 functools.partial(
438 CellApertureCorrectionMapSerializationModel.serialize, self.aperture_corrections
439 ),
440 )
441 serialized_provenance = (
442 archive.serialize_direct("provenance", self._provenance.serialize)
443 if self._provenance is not None
444 else None
445 )
446 serialized_backgrounds = archive.serialize_direct("background", self._backgrounds.serialize)
447 return CellCoaddSerializationModel(
448 image=serialized_image,
449 mask=serialized_mask,
450 variance=serialized_variance,
451 sky_projection=serialized_projection,
452 mask_fractions=serialized_mask_fractions,
453 noise_realizations=serialized_noise_realizations,
454 band=self._band,
455 psf=serialized_psf,
456 aperture_corrections=serialized_aperture_corrections,
457 patch=self._patch,
458 provenance=serialized_provenance,
459 backgrounds=serialized_backgrounds,
460 metadata=self.metadata,
461 )
463 @staticmethod
464 def _get_archive_tree_type[P: pydantic.BaseModel](
465 pointer_type: type[P],
466 ) -> type[CellCoaddSerializationModel[P]]:
467 """Return the serialization model type for this object for an archive
468 type that uses the given pointer type.
469 """
470 return CellCoaddSerializationModel[pointer_type] # type: ignore
472 @staticmethod
473 def from_legacy_cell_coadd(
474 legacy: LegacyMultipleCellCoadd,
475 *,
476 plane_map: Mapping[str, MaskPlane] | None = None,
477 tract_info: TractInfo,
478 bbox: Box | None = None,
479 ) -> CellCoadd:
480 """Convert from a `lsst.cell_coadds.MultipleCellCoadd` instance.
482 Parameters
483 ----------
484 legacy
485 A `lsst.cell_coadds.MultipleCellCoadd` instance to convert.
486 plane_map
487 A mapping from legacy mask plane name to the new plane name and
488 description.
489 tract_info
490 Information about the full tract.
491 bbox
492 Bounding box of the image. The default is to include just the
493 bounding box of the valid cells, which may not cover a full patch.
494 """
495 from lsst.geom import Box2I
497 if plane_map is None:
498 plane_map = get_legacy_deep_coadd_mask_planes()
499 if bbox is None:
500 legacy_bbox = Box2I()
501 for single_cell in legacy.cells.values():
502 legacy_bbox.include(single_cell.inner.bbox)
503 else:
504 legacy_bbox = bbox.to_legacy()
505 legacy_stitched = legacy.stitch(legacy_bbox)
506 unit = astropy.units.Unit(legacy.units.value)
507 tract_bbox = Box.from_legacy(tract_info.getBBox())
508 sky_projection = SkyProjection.from_legacy(
509 legacy.wcs,
510 TractFrame(
511 skymap=legacy.identifiers.skymap,
512 tract=legacy.identifiers.tract,
513 bbox=tract_bbox,
514 ),
515 pixel_bounds=tract_bbox,
516 )
517 band = legacy.identifiers.band
518 image = Image.from_legacy(legacy_stitched.image, unit=unit)
519 mask = Mask.from_legacy(legacy_stitched.mask, plane_map=plane_map)
520 variance = Image.from_legacy(legacy_stitched.variance, unit=unit**2)
521 noise_realizations = [
522 Image.from_legacy(noise_image) for noise_image in legacy_stitched.noise_realizations
523 ]
524 mask_fractions = (
525 {"rejected": Image.from_legacy(legacy_stitched.mask_fractions)}
526 if legacy_stitched.mask_fractions is not None
527 else {}
528 )
529 psf = CellPointSpreadFunction.from_legacy(legacy_stitched.psf, image.bbox)
530 aperture_corrections = {
531 ap_corr_name: CellField.from_legacy_aperture_correction(legacy_ap_corr, psf.bounds)
532 for ap_corr_name, legacy_ap_corr in legacy_stitched.ap_corr_map.items()
533 }
534 patch_info = tract_info[legacy.identifiers.patch]
535 patch = PatchDefinition(
536 id=patch_info.getSequentialIndex(),
537 index=YX(y=legacy.identifiers.patch.y, x=legacy.identifiers.patch.x),
538 inner_bbox=Box.from_legacy(patch_info.getInnerBBox()),
539 cells=CellGrid.from_legacy(legacy.grid),
540 )
541 provenance = CoaddProvenance.from_legacy(legacy)
542 return CellCoadd(
543 image=image,
544 mask=mask,
545 variance=variance,
546 mask_fractions=mask_fractions,
547 noise_realizations=noise_realizations,
548 sky_projection=sky_projection,
549 band=band,
550 psf=psf,
551 aperture_corrections=aperture_corrections,
552 patch=patch,
553 provenance=provenance,
554 )
556 def to_legacy_cell_coadd(
557 self, copy: bool | None = None, plane_map: Mapping[str, MaskPlane] | None = None
558 ) -> LegacyMultipleCellCoadd:
559 """Convert to a `lsst.cell_coadds.MultipleCellCoadd` instance.
561 Parameters
562 ----------
563 copy
564 If `True`, always copy the image and variance pixel data.
565 If `False`, return a view, and raise `TypeError` if the pixel data
566 is read-only (this is not supported by afw). If `None`, only copy
567 if the pixel data is read-only. Mask pixel data is always copied.
568 plane_map
569 A mapping from legacy mask plane name to the new plane name and
570 description.
571 """
572 from frozendict import frozendict
574 from lsst.cell_coadds import CellIdentifiers as LegacyCellIdentifiers
575 from lsst.cell_coadds import CoaddUnits as LegacyCoaddUnits
576 from lsst.cell_coadds import CommonComponents as LegacyCommonComponents
577 from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd
578 from lsst.cell_coadds import OwnedImagePlanes as LegacyOwnedImagePlanes
579 from lsst.cell_coadds import PatchIdentifiers as LegacyPatchIdentifiers
580 from lsst.cell_coadds import SingleCellCoadd as LegacySingleCellCoadd
581 from lsst.skymap import Index2D as LegacyIndex2D
583 if plane_map is None:
584 plane_map = get_legacy_deep_coadd_mask_planes()
585 if self.unit != astropy.units.nJy:
586 raise ValueError("CellCoadd.to_legacy requires nJy pixel units.")
587 if self.bbox != self.bounds.bbox:
588 raise ValueError("MultipleCellCoadd requires its bounding box to lie on the cell grid.")
589 legacy_grid = self.grid.to_legacy()
590 visit_polygons = self.provenance.to_legacy_polygon_map()
591 legacy_common = LegacyCommonComponents(
592 units=LegacyCoaddUnits.nJy,
593 wcs=self.sky_projection.to_legacy(),
594 band=self.band,
595 identifiers=LegacyPatchIdentifiers(
596 self.skymap,
597 self.tract,
598 LegacyIndex2D(x=self.patch.index.x, y=self.patch.index.y),
599 band=self.band,
600 ),
601 visit_polygons=visit_polygons,
602 )
603 legacy_inputs = self.provenance.to_legacy_cell_coadd_inputs(visit_polygons.keys())
604 cells: list[LegacySingleCellCoadd] = []
605 for cell_index in self.bounds.cell_indices():
606 cell_bbox = self.grid.bbox_of(cell_index)
607 # Legacy type only has room for one mask_fractions plane.
608 legacy_mask_fractions = (
609 next(iter(self.mask_fractions.values()))[cell_bbox].to_legacy(copy=copy)
610 if self.mask_fractions
611 else None
612 )
613 legacy_planes = LegacyOwnedImagePlanes(
614 image=self.image[cell_bbox].to_legacy(copy=copy),
615 mask=self.mask[cell_bbox].to_legacy(plane_map),
616 variance=self.variance[cell_bbox].to_legacy(copy=copy),
617 mask_fractions=legacy_mask_fractions,
618 noise_realizations=[n[cell_bbox].to_legacy(copy=copy) for n in self.noise_realizations],
619 )
620 legacy_aperture_correction_map = frozendict(
621 {
622 name: field.value_in_cell(cell_index)
623 if cell_index not in field.bounds.missing
624 else np.nan
625 for name, field in self.aperture_corrections.items()
626 }
627 )
628 cells.append(
629 LegacySingleCellCoadd(
630 legacy_planes,
631 psf=self.psf[cell_index].to_legacy(copy=copy),
632 inner_bbox=cell_bbox.to_legacy(),
633 common=legacy_common,
634 inputs=legacy_inputs[cell_index.to_legacy()],
635 identifiers=LegacyCellIdentifiers(
636 self.skymap,
637 self.tract,
638 legacy_common.identifiers.patch,
639 band=self.band,
640 cell=cell_index.to_legacy(),
641 ),
642 aperture_correction_map=legacy_aperture_correction_map,
643 )
644 )
645 return LegacyMultipleCellCoadd(
646 cells,
647 legacy_grid,
648 outer_cell_size=self.grid.cell_shape.to_legacy_int_extent(),
649 psf_image_size=self.psf.kernel_bbox.shape.to_legacy_int_extent(),
650 common=legacy_common,
651 inner_bbox=self.bbox.to_legacy(),
652 )
654 def to_legacy(
655 self, copy: bool | None = None, plane_map: Mapping[str, MaskPlane] | None = None
656 ) -> LegacyExposure:
657 """Convert to a `lsst.afw.image.Exposure` instance.
659 Parameters
660 ----------
661 copy
662 If `True`, always copy the image and variance pixel data.
663 If `False`, return a view, and raise `TypeError` if the pixel data
664 is read-only (this is not supported by afw). If `None`, only copy
665 if the pixel data is read-only. Mask pixel data is always copied.
666 plane_map
667 A mapping from legacy mask plane name to the new plane name and
668 description.
670 Returns
671 -------
672 `lsst.afw.image.Exposure`
673 A legacy representation of the coadd. This will have its ``wcs``,
674 ``psf``, ``filter``, ``photoCalib``, and ``metadata`` components
675 set. The ``apCorrMap`` component is not set, because there is no
676 true `lsst.afw.math.BoundedField` representation for cell-coadd
677 aperture corrections, and the ``coaddInputs`` component is not set
678 because that data structure cannot fully capture cell-coadd
679 provenance.
681 Notes
682 -----
683 This method requires the `provenance` attribute to have been populated
684 at construction.
685 """
686 from lsst.afw.image import Exposure as LegacyExposure
687 from lsst.afw.image import FilterLabel as LegacyFilterLabel
689 if plane_map is None:
690 plane_map = get_legacy_deep_coadd_mask_planes()
691 legacy_masked_image = super().to_legacy(copy=copy, plane_map=plane_map)
692 result = LegacyExposure(legacy_masked_image, dtype=self.image.array.dtype)
693 result_info = result.info
694 result_info.setWcs(self.sky_projection.to_legacy())
695 result_info.setPsf(self.psf.to_legacy())
696 result_info.setFilter(LegacyFilterLabel.fromBand(self.band))
697 result_info.setPhotoCalib(BaseField.make_legacy_photo_calib(self.unit))
698 # We don't do setCoaddInputs because that data structure can't really
699 # represent cell-coadd provenance accurately, and it's not clear
700 # anything would use it.
701 self._fill_legacy_metadata(result_info.getMetadata())
702 # We can't do setApCorrMap because the legacy
703 # StitchedApertureCorrection is not a real C++ BoundedField, just a
704 # Python duck-alike.
705 return result
708class CellCoaddSerializationModel[P: pydantic.BaseModel](MaskedImageSerializationModel[P]):
709 """A Pydantic model used to represent a serialized `CellCoadd`."""
711 SCHEMA_NAME: ClassVar[str] = "cell_coadd"
712 SCHEMA_VERSION: ClassVar[str] = "1.0.0"
713 MIN_READ_VERSION: ClassVar[int] = 1
714 PUBLIC_TYPE: ClassVar[type] = CellCoadd
716 # Inherited attributes are duplicated because that improves the docs
717 # (some limitation in the sphinx/pydantic integration), and these are
718 # important docs.
720 image: ImageSerializationModel[P] = pydantic.Field(description="The main data image.")
721 mask: MaskSerializationModel[P] = pydantic.Field(
722 description="Bitmask that annotates the main image's pixels."
723 )
724 variance: ImageSerializationModel[P] = pydantic.Field(
725 description="Per-pixel variance estimates for the main image."
726 )
727 sky_projection: SkyProjectionSerializationModel[P] = pydantic.Field(
728 description="Projection that maps the pixel grid to the sky.",
729 )
730 mask_fractions: dict[str, ImageSerializationModel[P]] = pydantic.Field(
731 description=(
732 "A mapping from an input-image mask plane name to an image of the weights sums of that plane."
733 )
734 )
735 noise_realizations: list[ImageSerializationModel[P]] = pydantic.Field(
736 description=(
737 "A mapping from an input-image mask plane name to an image of the weights sums of that plane."
738 )
739 )
740 band: str | None = pydantic.Field(description="Name of the band.")
741 psf: CellPointSpreadFunctionSerializationModel = pydantic.Field(
742 description="Effective point-spread function model for the coadd."
743 )
744 aperture_corrections: CellApertureCorrectionMapSerializationModel | None = pydantic.Field(
745 None, description="Coadded aperture corrections for different photometry algorithms."
746 )
747 patch: PatchDefinition | None = pydantic.Field(description="Identifiers and geometry for the patch.")
748 provenance: CoaddProvenanceSerializationModel | None = pydantic.Field(
749 description="Information about the images that went into the coadd."
750 )
751 backgrounds: BackgroundMapSerializationModel = pydantic.Field(
752 default_factory=BackgroundMapSerializationModel,
753 description="Background models associated with this image.",
754 )
756 def deserialize(
757 self,
758 archive: InputArchive[Any],
759 *,
760 bbox: Box | None = None,
761 provenance: bool = True,
762 **kwargs: Any,
763 ) -> CellCoadd:
764 """Deserialize an image from an input archive.
766 Parameters
767 ----------
768 archive
769 Archive to read from.
770 bbox
771 Bounding box of a subimage to read instead.
772 provenance
773 Whether to read and attach provenance information.
774 **kwargs
775 Unsupported keyword arguments are accepted only to provide better
776 error messages (raising `.serialization.InvalidParameterError`).
777 """
778 if kwargs: 778 ↛ 779line 778 didn't jump to line 779 because the condition on line 778 was never true
779 raise InvalidParameterError(f"Unrecognized parameters for CellCoadd: {set(kwargs.keys())}.")
780 masked_image = super().deserialize(archive, bbox=bbox)
781 mask_fractions = {
782 k.removeprefix("mask_fractions/"): v.deserialize(archive, bbox=bbox)
783 for k, v in self.mask_fractions.items()
784 }
785 noise_realizations = [v.deserialize(archive, bbox=bbox) for v in self.noise_realizations]
786 sky_projection = self.sky_projection.deserialize(archive)
787 psf = self.psf.deserialize(archive, bbox=bbox)
788 aperture_corrections = (
789 self.aperture_corrections.deserialize(archive) if self.aperture_corrections is not None else {}
790 )
791 coadd_provenance: CoaddProvenance | None = None
792 if self.provenance is not None and provenance: 792 ↛ 796line 792 didn't jump to line 796 because the condition on line 792 was always true
793 coadd_provenance = self.provenance.deserialize(archive)
794 if bbox is not None:
795 coadd_provenance = coadd_provenance.subset(psf.bounds.cell_indices())
796 backgrounds = self.backgrounds.deserialize(archive)
797 return CellCoadd(
798 masked_image.image,
799 mask=masked_image.mask,
800 variance=masked_image.variance,
801 mask_fractions=mask_fractions,
802 noise_realizations=noise_realizations,
803 sky_projection=sky_projection,
804 band=self.band,
805 psf=psf,
806 aperture_corrections=aperture_corrections,
807 patch=self.patch,
808 provenance=coadd_provenance,
809 backgrounds=backgrounds,
810 )._finish_deserialize(self)
812 def deserialize_component(self, component: str, archive: InputArchive[Any], **kwargs: Any) -> Any:
813 match component:
814 case "mask_fractions":
815 return {
816 name: image_model.deserialize(archive, **kwargs)
817 for name, image_model in self.mask_fractions.items()
818 }
819 case "noise_realizations":
820 return [image_model.deserialize(archive, **kwargs) for image_model in self.noise_realizations]
821 case "aperture_corrections" if self.aperture_corrections is None:
822 # super() delegation handles the not-None case.
823 return {}
824 case "masked_image":
825 return super().deserialize(archive, **kwargs)
826 return super().deserialize_component(component, archive, **kwargs)