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