Coverage for python/lsst/images/cells/_coadd.py: 45%

241 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-16 15:24 -0700

1# This file is part of lsst-images. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14__all__ = ("CellCoadd", "CellCoaddSerializationModel") 

15 

16import functools 

17from collections.abc import Mapping, Sequence 

18from types import EllipsisType 

19from typing import TYPE_CHECKING, Any, ClassVar, cast 

20 

21import astropy.io.fits 

22import astropy.units 

23import astropy.wcs 

24import pydantic 

25 

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 

38 

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] 

48 

49 

50class CellCoadd(MaskedImage): 

51 """A coadd comprised of cells on a regular grid. 

52 

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 """ 

99 

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() 

147 

148 @property 

149 def skymap(self) -> str: 

150 """Name of the skymap (`str`).""" 

151 return self.sky_projection.pixel_frame.skymap 

152 

153 @property 

154 def tract(self) -> int: 

155 """ID of the tract (`int`).""" 

156 return self.sky_projection.pixel_frame.tract 

157 

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 

166 

167 @property 

168 def band(self) -> str | None: 

169 """Name of the band (`str` or `None`).""" 

170 return self._band 

171 

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 

179 

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 

186 

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) 

191 

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) 

198 

199 @property 

200 def psf(self) -> CellPointSpreadFunction: 

201 """Effective point-spread function for the coadd 

202 (`CellPointSpreadFunction`). 

203 """ 

204 return self._psf 

205 

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 

212 

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 

219 

220 @property 

221 def grid(self) -> CellGrid: 

222 """The grid of cells that overlap this coadd (`CellGrid`).""" 

223 return self._psf.bounds.grid 

224 

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 

233 

234 @property 

235 def backgrounds(self) -> BackgroundMap: 

236 """A mapping of backgrounds associated with this image 

237 (`.BackgroundMap`). 

238 """ 

239 return self._backgrounds 

240 

241 def __getitem__(self, bbox: Box | EllipsisType) -> CellCoadd: 

242 bbox, _ = self._handle_getitem_args(bbox) 

243 psf = self.psf[bbox] 

244 return self._transfer_metadata( 

245 CellCoadd( 

246 self.image[bbox], 

247 mask=self.mask[bbox], 

248 variance=self.variance[bbox], 

249 sky_projection=self.sky_projection, 

250 mask_fractions={k: v[bbox] for k, v in self._mask_fractions.items()}, 

251 noise_realizations=[v[bbox] for v in self._noise_realizations], 

252 band=self.band, 

253 psf=psf, 

254 patch=self._patch, 

255 provenance=( 

256 self._provenance.subset(psf.bounds.cell_indices()) 

257 if self._provenance is not None 

258 else None 

259 ), 

260 backgrounds=self._backgrounds, 

261 aperture_corrections=self._aperture_corrections.copy(), 

262 ), 

263 bbox=bbox, 

264 ) 

265 

266 def __str__(self) -> str: 

267 return f"CellCoadd({self.bbox!s}, tract={self.tract})" 

268 

269 def __repr__(self) -> str: 

270 return str(self) 

271 

272 def copy(self) -> CellCoadd: 

273 """Deep-copy the coadd.""" 

274 return self._transfer_metadata( 

275 CellCoadd( 

276 image=self._image.copy(), 

277 mask=self._mask.copy(), 

278 variance=self._variance.copy(), 

279 sky_projection=self.sky_projection, 

280 mask_fractions={k: v.copy() for k, v in self._mask_fractions.items()}, 

281 noise_realizations=[v.copy() for v in self._noise_realizations], 

282 band=self.band, 

283 psf=self.psf, 

284 patch=self.patch, 

285 provenance=self.provenance, 

286 backgrounds=self._backgrounds.copy(), 

287 aperture_corrections=self._aperture_corrections.copy(), 

288 ), 

289 copy=True, 

290 ) 

291 

292 def apply_background(self, name: str | None) -> None: 

293 """Subtract the background with the given name, modifying the image 

294 in place. 

295 

296 If ``name`` is `None`, restore the original background. 

297 

298 Parameters 

299 ---------- 

300 name 

301 Name of the background to subtract, or `None` to restore the 

302 original background. 

303 """ 

304 current_bg = self.backgrounds.subtracted 

305 if current_bg is not None: 

306 if name == current_bg.name: 

307 return 

308 self.image.quantity += current_bg.field.render(dtype=self.image.array.dtype).quantity 

309 if name is None: 

310 self._backgrounds._subtracted = None 

311 return 

312 new_bg = self.backgrounds[name] 

313 self.image.quantity -= new_bg.field.render(dtype=self.image.array.dtype).quantity 

314 self._backgrounds._subtracted = name 

315 

316 def serialize(self, archive: OutputArchive[Any]) -> CellCoaddSerializationModel: 

317 """Serialize the image to an output archive. 

318 

319 Parameters 

320 ---------- 

321 archive 

322 Archive to write to. 

323 """ 

324 serialized_image = archive.serialize_direct( 

325 "image", 

326 functools.partial(self.image.serialize, save_projection=False, tile_shape=self.grid.cell_shape), 

327 ) 

328 serialized_mask = archive.serialize_direct( 

329 "mask", 

330 functools.partial(self.mask.serialize, save_projection=False, tile_shape=self.grid.cell_shape), 

331 ) 

332 serialized_variance = archive.serialize_direct( 

333 "variance", 

334 functools.partial( 

335 self.variance.serialize, save_projection=False, tile_shape=self.grid.cell_shape 

336 ), 

337 ) 

338 serialized_projection = archive.serialize_direct("sky_projection", self.sky_projection.serialize) 

339 serialized_mask_fractions = { 

340 k: archive.serialize_direct( 

341 f"mask_fractions/{k}", 

342 functools.partial( 

343 v.serialize, 

344 save_projection=False, 

345 tile_shape=self.grid.cell_shape, 

346 options_name="mask_fractions", 

347 ), 

348 ) 

349 for k, v in self.mask_fractions.items() 

350 } 

351 serialized_noise_realizations = [ 

352 archive.serialize_direct( 

353 f"noise_realizations/{n}", 

354 functools.partial( 

355 v.serialize, save_projection=False, tile_shape=self.grid.cell_shape, options_name="image" 

356 ), 

357 ) 

358 for n, v in enumerate(self.noise_realizations) 

359 ] 

360 serialized_psf = archive.serialize_direct("psf", self.psf.serialize) 

361 serialized_aperture_corrections = archive.serialize_direct( 

362 "aperture_corrections", 

363 functools.partial( 

364 CellApertureCorrectionMapSerializationModel.serialize, self.aperture_corrections 

365 ), 

366 ) 

367 serialized_provenance = ( 

368 archive.serialize_direct("provenance", self._provenance.serialize) 

369 if self._provenance is not None 

370 else None 

371 ) 

372 serialized_backgrounds = archive.serialize_direct("background", self._backgrounds.serialize) 

373 return CellCoaddSerializationModel( 

374 image=serialized_image, 

375 mask=serialized_mask, 

376 variance=serialized_variance, 

377 sky_projection=serialized_projection, 

378 mask_fractions=serialized_mask_fractions, 

379 noise_realizations=serialized_noise_realizations, 

380 band=self._band, 

381 psf=serialized_psf, 

382 aperture_corrections=serialized_aperture_corrections, 

383 patch=self._patch, 

384 provenance=serialized_provenance, 

385 backgrounds=serialized_backgrounds, 

386 metadata=self.metadata, 

387 ) 

388 

389 @staticmethod 

390 def _get_archive_tree_type[P: pydantic.BaseModel]( 

391 pointer_type: type[P], 

392 ) -> type[CellCoaddSerializationModel[P]]: 

393 """Return the serialization model type for this object for an archive 

394 type that uses the given pointer type. 

395 """ 

396 return CellCoaddSerializationModel[pointer_type] # type: ignore 

397 

398 @staticmethod 

399 def from_legacy( # type: ignore[override] 

400 legacy: LegacyMultipleCellCoadd, 

401 *, 

402 plane_map: Mapping[str, MaskPlane] | None = None, 

403 tract_info: TractInfo, 

404 bbox: Box | None = None, 

405 ) -> CellCoadd: 

406 """Convert from a `lsst.cell_coadds.MultipleCellCoadd` instance. 

407 

408 Parameters 

409 ---------- 

410 legacy 

411 A `lsst.cell_coadds.MultipleCellCoadd` instance to convert. 

412 plane_map 

413 A mapping from legacy mask plane name to the new plane name and 

414 description. 

415 tract_info 

416 Information about the full tract. 

417 bbox 

418 Bounding box of the image. The default is to include just the 

419 bounding box of the valid cells, which may not cover a full patch. 

420 """ 

421 from lsst.geom import Box2I 

422 

423 if plane_map is None: 

424 plane_map = get_legacy_deep_coadd_mask_planes() 

425 if bbox is None: 

426 legacy_bbox = Box2I() 

427 for single_cell in legacy.cells.values(): 

428 legacy_bbox.include(single_cell.inner.bbox) 

429 else: 

430 legacy_bbox = bbox.to_legacy() 

431 legacy_stitched = legacy.stitch(legacy_bbox) 

432 unit = astropy.units.Unit(legacy.units.value) 

433 tract_bbox = Box.from_legacy(tract_info.getBBox()) 

434 sky_projection = SkyProjection.from_legacy( 

435 legacy.wcs, 

436 TractFrame( 

437 skymap=legacy.identifiers.skymap, 

438 tract=legacy.identifiers.tract, 

439 bbox=tract_bbox, 

440 ), 

441 pixel_bounds=tract_bbox, 

442 ) 

443 band = legacy.identifiers.band 

444 image = Image.from_legacy(legacy_stitched.image, unit=unit) 

445 mask = Mask.from_legacy(legacy_stitched.mask, plane_map=plane_map) 

446 variance = Image.from_legacy(legacy_stitched.variance, unit=unit**2) 

447 noise_realizations = [ 

448 Image.from_legacy(noise_image) for noise_image in legacy_stitched.noise_realizations 

449 ] 

450 mask_fractions = ( 

451 {"rejected": Image.from_legacy(legacy_stitched.mask_fractions)} 

452 if legacy_stitched.mask_fractions is not None 

453 else {} 

454 ) 

455 psf = CellPointSpreadFunction.from_legacy(legacy_stitched.psf, image.bbox) 

456 aperture_corrections = { 

457 ap_corr_name: CellField.from_legacy_aperture_correction(legacy_ap_corr, psf.bounds) 

458 for ap_corr_name, legacy_ap_corr in legacy_stitched.ap_corr_map.items() 

459 } 

460 patch_info = tract_info[legacy.identifiers.patch] 

461 patch = PatchDefinition( 

462 id=patch_info.getSequentialIndex(), 

463 index=YX(y=legacy.identifiers.patch.y, x=legacy.identifiers.patch.x), 

464 inner_bbox=Box.from_legacy(patch_info.getInnerBBox()), 

465 cells=CellGrid.from_legacy(legacy.grid), 

466 ) 

467 provenance = CoaddProvenance.from_legacy(legacy) 

468 return CellCoadd( 

469 image=image, 

470 mask=mask, 

471 variance=variance, 

472 mask_fractions=mask_fractions, 

473 noise_realizations=noise_realizations, 

474 sky_projection=sky_projection, 

475 band=band, 

476 psf=psf, 

477 aperture_corrections=aperture_corrections, 

478 patch=patch, 

479 provenance=provenance, 

480 ) 

481 

482 def to_legacy_cell_coadd( 

483 self, copy: bool | None = None, plane_map: Mapping[str, MaskPlane] | None = None 

484 ) -> LegacyMultipleCellCoadd: 

485 """Convert to a `lsst.cell_coadds.MultipleCellCoadd` instance. 

486 

487 Parameters 

488 ---------- 

489 copy 

490 If `True`, always copy the image and variance pixel data. 

491 If `False`, return a view, and raise `TypeError` if the pixel data 

492 is read-only (this is not supported by afw). If `None`, only copy 

493 if the pixel data is read-only. Mask pixel data is always copied. 

494 plane_map 

495 A mapping from legacy mask plane name to the new plane name and 

496 description. 

497 """ 

498 from frozendict import frozendict 

499 

500 from lsst.cell_coadds import CellIdentifiers as LegacyCellIdentifiers 

501 from lsst.cell_coadds import CoaddUnits as LegacyCoaddUnits 

502 from lsst.cell_coadds import CommonComponents as LegacyCommonComponents 

503 from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd 

504 from lsst.cell_coadds import OwnedImagePlanes as LegacyOwnedImagePlanes 

505 from lsst.cell_coadds import PatchIdentifiers as LegacyPatchIdentifiers 

506 from lsst.cell_coadds import SingleCellCoadd as LegacySingleCellCoadd 

507 from lsst.skymap import Index2D as LegacyIndex2D 

508 

509 if plane_map is None: 

510 plane_map = get_legacy_deep_coadd_mask_planes() 

511 if self.unit != astropy.units.nJy: 

512 raise ValueError("CellCoadd.to_legacy requires nJy pixel units.") 

513 if self.bbox != self.bounds.bbox: 

514 raise ValueError("MultipleCellCoadd requires its bounding box to lie on the cell grid.") 

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 ) 

574 

575 def to_legacy( 

576 self, copy: bool | None = None, plane_map: Mapping[str, MaskPlane] | None = None 

577 ) -> LegacyExposure: 

578 """Convert to a `lsst.afw.image.Exposure` instance. 

579 

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. 

590 

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. 

601 

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 

609 

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 

627 

628 

629class CellCoaddSerializationModel[P: pydantic.BaseModel](MaskedImageSerializationModel[P]): 

630 """A Pydantic model used to represent a serialized `CellCoadd`.""" 

631 

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 

636 

637 # Inherited attributes are duplicated because that improves the docs 

638 # (some limitation in the sphinx/pydantic integration), and these are 

639 # important docs. 

640 

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 ) 

676 

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. 

686 

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) 

732 

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)