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

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

39 

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] 

49 

50 

51class CellCoadd(MaskedImage): 

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

53 

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

100 

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

148 

149 @property 

150 def skymap(self) -> str: 

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

152 return self.sky_projection.pixel_frame.skymap 

153 

154 @property 

155 def tract(self) -> int: 

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

157 return self.sky_projection.pixel_frame.tract 

158 

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 

167 

168 @property 

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

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

171 return self._band 

172 

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 

180 

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 

187 

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) 

192 

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) 

199 

200 @property 

201 def psf(self) -> CellPointSpreadFunction: 

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

203 (`CellPointSpreadFunction`). 

204 """ 

205 return self._psf 

206 

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 

213 

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 

220 

221 @property 

222 def grid(self) -> CellGrid: 

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

224 return self._psf.bounds.grid 

225 

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 

234 

235 @property 

236 def backgrounds(self) -> BackgroundMap: 

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

238 (`.BackgroundMap`). 

239 """ 

240 return self._backgrounds 

241 

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 ) 

266 

267 def _describe(self, options: DescribeOptions = DescribeOptions(), /) -> Report: 

268 """Return a `Report` describing this cell coadd. 

269 

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 ) 

336 

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 ) 

356 

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

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

359 in place. 

360 

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

362 

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 

380 

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

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

383 

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 ) 

453 

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 

462 

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. 

472 

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 

487 

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 ) 

546 

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. 

551 

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 

564 

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 

573 

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 ) 

639 

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. 

644 

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. 

655 

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. 

666 

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 

674 

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 

692 

693 

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

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

696 

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 

701 

702 # Inherited attributes are duplicated because that improves the docs 

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

704 # important docs. 

705 

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 ) 

741 

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. 

751 

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) 

797 

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)