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