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

264 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-08-29 09:37 +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 numpy as np 

25import pydantic 

26 

27from .._backgrounds import BackgroundMap, BackgroundMapSerializationModel 

28from .._cell_grid import CellGrid, CellGridBounds, PatchDefinition 

29from .._geom import YX, Box 

30from .._image import Image, ImageSerializationModel 

31from .._mask import Mask, MaskPlane, MaskSchema, MaskSerializationModel, get_legacy_deep_coadd_mask_planes 

32from .._masked_image import MaskedImage, MaskedImageSerializationModel 

33from .._transforms import SkyProjection, SkyProjectionSerializationModel, TractFrame 

34from ..describe import DescribeOptions, FieldRole, Report, ReportField 

35from ..fields import BaseField 

36from ..serialization import InputArchive, InvalidParameterError, OutputArchive 

37from ._aperture_corrections import CellApertureCorrectionMapSerializationModel, CellField 

38from ._provenance import CoaddProvenance, CoaddProvenanceSerializationModel 

39from ._psf import CellPointSpreadFunction, CellPointSpreadFunctionSerializationModel 

40 

41if TYPE_CHECKING: 

42 try: 

43 from lsst.afw.image import Exposure as LegacyExposure 

44 from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd 

45 from lsst.skymap import TractInfo 

46 except ImportError: 

47 type LegacyExposure = Any # type: ignore[no-redef] 

48 type LegacyMultipleCellCoadd = Any # type: ignore[no-redef] 

49 type TractInfo = Any # type: ignore[no-redef] 

50 

51 

52class CellCoadd(MaskedImage): 

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

54 

55 Parameters 

56 ---------- 

57 image 

58 The main image plane. If this has a `.SkyProjection`, it will be used 

59 for all planes unless a ``sky_projection`` is passed separately. 

60 mask 

61 A bitmask image that annotates the main image plane. Must have the 

62 same bounding box as ``image`` if provided. Any attached 

63 ``sky_projection`` is replaced (possibly by `None`). 

64 variance 

65 The per-pixel uncertainty of the main image as an image of variance 

66 values. Must have the same bounding box as ``image`` if provided, and 

67 its units must be the square of ``image.unit`` or `None`. 

68 Values default to ``1.0``. Any attached ``sky_projection`` is replaced 

69 (possibly by `None`). 

70 mask_fractions 

71 A mapping from an input-image mask plane name to an image of the 

72 weights sums of that plane. 

73 noise_realizations 

74 A sequence of images with Monte Carlo realizations of the noise in 

75 the coadd. 

76 mask_schema 

77 Schema for the mask plane. Must be provided if and only if ``mask`` is 

78 not provided. 

79 sky_projection 

80 Projection that maps the pixel grid to the sky. Can only be `None` if 

81 a projection is already attached to ``image``. 

82 band 

83 Name of the band. 

84 psf 

85 Effective point-spread function for the coadd. The missing cells 

86 reported by ``psf.bounds`` are assumed to apply to all image data for 

87 that cell as well (i.e. there is a PSF for a cell if and only if 

88 there is image data for that cell). 

89 aperture_corrections 

90 Aperture corrections for different photometry algorithms. 

91 patch 

92 Identifiers and geometry of the full patch, if the image is confined 

93 to a single patch. When present, the cell grid of the PSF and 

94 provenance (if provideD) must be the full patch grid, even if its 

95 bounds select a subset of that area. 

96 provenance 

97 Information about the images that went into the coadd. 

98 backgrounds 

99 Background models associated with this image. 

100 """ 

101 

102 def __init__( 

103 self, 

104 image: Image, 

105 *, 

106 mask: Mask | None = None, 

107 variance: Image | None = None, 

108 mask_fractions: Mapping[str, Image] | None = None, 

109 noise_realizations: Sequence[Image] = (), 

110 mask_schema: MaskSchema | None = None, 

111 sky_projection: SkyProjection[TractFrame] | None = None, 

112 band: str | None = None, 

113 psf: CellPointSpreadFunction, 

114 aperture_corrections: Mapping[str, CellField] | None = None, 

115 patch: PatchDefinition | None = None, 

116 provenance: CoaddProvenance | None = None, 

117 backgrounds: BackgroundMap | None = None, 

118 ) -> None: 

119 super().__init__( 

120 image, 

121 mask=mask, 

122 variance=variance, 

123 mask_schema=mask_schema, 

124 sky_projection=sky_projection, 

125 ) 

126 if self.image.unit is None: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true

127 raise TypeError("The image component of a CellCoadd must have units.") 

128 if self.image.sky_projection is None: 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true

129 raise TypeError("The sky_projection component of a CellCoadd cannot be None.") 

130 if not isinstance(self.image.sky_projection.pixel_frame, TractFrame): 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true

131 raise TypeError("The sky_projection's pixel frame must be a TractFrame for CellCoadd.") 

132 self._mask_fractions = dict(mask_fractions) if mask_fractions is not None else {} 

133 self._noise_realizations = list(noise_realizations) 

134 self._band = band 

135 if psf.bounds.bbox != self.bbox: 

136 psf = psf[self.bbox.intersection(psf.bounds.bbox)] 

137 self._psf = psf 

138 self._aperture_corrections = dict(aperture_corrections) if aperture_corrections is not None else {} 

139 for ap_corr_name, ap_corr_field in self._aperture_corrections.items(): 

140 if ap_corr_field.bounds.grid != self.grid: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true

141 raise ValueError( 

142 f"Grids for cell PSF and {ap_corr_name} aperture corrections are not consistent." 

143 ) 

144 self._patch = patch 

145 self._provenance = provenance 

146 if self._provenance and not self._patch: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true

147 raise TypeError("A CellCoadd cannot carry provenance without a patch definition.") 

148 self._backgrounds = backgrounds if backgrounds is not None else BackgroundMap() 

149 

150 @property 

151 def skymap(self) -> str: 

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

153 return self.sky_projection.pixel_frame.skymap 

154 

155 @property 

156 def tract(self) -> int: 

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

158 return self.sky_projection.pixel_frame.tract 

159 

160 @property 

161 def patch(self) -> PatchDefinition: 

162 """Identifiers and geometry of the full patch, if the image is confined 

163 to a single patch (`PatchDefinition`). 

164 """ 

165 if self._patch is None: 

166 raise AttributeError("Coadd has no patch information.") 

167 return self._patch 

168 

169 @property 

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

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

172 return self._band 

173 

174 @property 

175 def mask_fractions(self) -> Mapping[str, Image]: 

176 """A mapping from an input-image mask plane name to an image of the 

177 weights sums of that plane 

178 (`~collections.abc.Mapping` [`str`, `.Image`]). 

179 """ 

180 return self._mask_fractions 

181 

182 @property 

183 def noise_realizations(self) -> Sequence[Image]: 

184 """A sequence of images with Monte Carlo realizations of the noise in 

185 the coadd (`~collections.abc.Sequence` [`.Image`]). 

186 """ 

187 return self._noise_realizations 

188 

189 @property 

190 def unit(self) -> astropy.units.UnitBase: 

191 """The units of the image plane (`astropy.units.Unit`).""" 

192 return cast(astropy.units.UnitBase, super().unit) 

193 

194 @property 

195 def sky_projection(self) -> SkyProjection[TractFrame]: 

196 """The projection that maps the pixel grid to the sky 

197 (`.SkyProjection` [`.TractFrame`]). 

198 """ 

199 return cast(SkyProjection[TractFrame], super().sky_projection) 

200 

201 @property 

202 def psf(self) -> CellPointSpreadFunction: 

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

204 (`CellPointSpreadFunction`). 

205 """ 

206 return self._psf 

207 

208 @property 

209 def aperture_corrections(self) -> Mapping[str, CellField]: 

210 """Aperture corrections for different photometry algorithms 

211 (`dict` [`str`, `CellField`]). 

212 """ 

213 return self._aperture_corrections 

214 

215 @property 

216 def bounds(self) -> CellGridBounds: 

217 """The grid of cells that overlap this coadd and a set of missing 

218 cells (`CellGridBounds`). 

219 """ 

220 return self._psf.bounds 

221 

222 @property 

223 def grid(self) -> CellGrid: 

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

225 return self._psf.bounds.grid 

226 

227 @property 

228 def provenance(self) -> CoaddProvenance: 

229 """Information about the images that went into the coadd 

230 (`CoaddProvenance` or `None`). 

231 """ 

232 if self._provenance is None: 

233 raise AttributeError("Coadd has no provenance information.") 

234 return self._provenance 

235 

236 @property 

237 def backgrounds(self) -> BackgroundMap: 

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

239 (`.BackgroundMap`). 

240 """ 

241 return self._backgrounds 

242 

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

244 bbox, _ = self._handle_getitem_args(bbox) 

245 psf = self.psf[bbox] 

246 return self._transfer_metadata( 

247 CellCoadd( 

248 self.image[bbox], 

249 mask=self.mask[bbox], 

250 variance=self.variance[bbox], 

251 sky_projection=self.sky_projection, 

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

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

254 band=self.band, 

255 psf=psf, 

256 patch=self._patch, 

257 provenance=( 

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

259 if self._provenance is not None 

260 else None 

261 ), 

262 backgrounds=self._backgrounds, 

263 aperture_corrections=self._aperture_corrections.copy(), 

264 ), 

265 bbox=bbox, 

266 ) 

267 

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

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

270 

271 Parameters 

272 ---------- 

273 options : `DescribeOptions`, optional 

274 Rendering options; forwarded to all children. 

275 """ 

276 fields = [ 

277 ReportField(label="skymap", value=self.skymap, role=FieldRole.DERIVED), 

278 ReportField(label="tract", value=self.tract, role=FieldRole.DERIVED), 

279 ReportField(label="patch", value=self._patch, role=FieldRole.DERIVED), 

280 ReportField(label="band", value=self.band, role=FieldRole.DERIVED), 

281 ReportField(label="bbox", value=self.bbox, repr_value=repr(self.bbox), role=FieldRole.DERIVED), 

282 ] 

283 summary = f"CellCoadd({self.bbox!s}, tract={self.tract})" 

284 if options.brief: 

285 return Report(type_name="CellCoadd", summary=summary, fields=fields) 

286 # Components with no report of their own get a line each, so that 

287 # nothing the coadd carries is absent from its description. 

288 if self._mask_fractions: 

289 fields.append( 

290 ReportField( 

291 label="mask_fractions", value=", ".join(self._mask_fractions), role=FieldRole.DERIVED 

292 ) 

293 ) 

294 if self._noise_realizations: 

295 n_noise = len(self._noise_realizations) 

296 fields.append( 

297 ReportField( 

298 label="noise_realizations", 

299 value=( 

300 f"{n_noise} image{'s' if n_noise != 1 else ''} " 

301 f"(dtype {self._noise_realizations[0].array.dtype})" 

302 ), 

303 role=FieldRole.DERIVED, 

304 ) 

305 ) 

306 if self._aperture_corrections: 

307 n_ap_corr = len(self._aperture_corrections) 

308 # Never print all the keys even in detailed mode since there 

309 # are usually many of them and they swamp the report. 

310 ap_corr = f"{n_ap_corr} field{'s' if n_ap_corr != 1 else ''}" 

311 fields.append(ReportField(label="aperture_corrections", value=ap_corr, role=FieldRole.DERIVED)) 

312 if self._provenance is None: 

313 # A coadd without provenance is a meaningful state: it is what 

314 # reading with provenance=False gives, and it is what makes 

315 # to_legacy_cell_coadd fail. 

316 fields.append(ReportField(label="provenance", value="none", role=FieldRole.DERIVED)) 

317 child = options.for_child() 

318 plane = options.for_child("sky_projection", "bbox") 

319 children: dict[str, Report] = { 

320 "image": self.image._describe(plane), 

321 "mask": self.mask._describe(plane), 

322 "variance": self.variance._describe(plane), 

323 "sky_projection": self.sky_projection._describe(child, bbox=self.bbox), 

324 "psf": self.psf._describe(child), 

325 } 

326 if self._provenance is not None: 

327 # The provenance property raises when there is none. Its cells 

328 # come from this coadd, so pass them down for the coverage ratio. 

329 children["provenance"] = self._provenance._describe(child, bounds=self.bounds) 

330 children["backgrounds"] = self.backgrounds._describe(child) 

331 return Report( 

332 type_name="CellCoadd", 

333 summary=summary, 

334 fields=fields, 

335 children=children, 

336 ) 

337 

338 def copy(self) -> CellCoadd: 

339 """Deep-copy the coadd.""" 

340 return self._transfer_metadata( 

341 CellCoadd( 

342 image=self._image.copy(), 

343 mask=self._mask.copy(), 

344 variance=self._variance.copy(), 

345 sky_projection=self.sky_projection, 

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

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

348 band=self.band, 

349 psf=self.psf, 

350 patch=self._patch, 

351 provenance=self._provenance, 

352 backgrounds=self._backgrounds.copy(), 

353 aperture_corrections=self._aperture_corrections.copy(), 

354 ), 

355 copy=True, 

356 ) 

357 

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

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

360 in place. 

361 

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

363 

364 Parameters 

365 ---------- 

366 name 

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

368 original background. 

369 """ 

370 current_bg = self.backgrounds.subtracted 

371 if current_bg is not None and name == current_bg.name: 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true

372 return 

373 

374 adjustment: astropy.units.Quantity | None = None 

375 if current_bg is not None: 

376 adjustment = current_bg.field.render(self.bbox, dtype=self.image.array.dtype).quantity 

377 if name is not None: 

378 new_bg = self.backgrounds[name] 

379 new_values = new_bg.field.render(self.bbox, dtype=self.image.array.dtype).quantity 

380 adjustment = -new_values if adjustment is None else adjustment - new_values 

381 

382 # Resolve and render every model before changing either pixels or 

383 # state. Computing one combined adjustment also ensures a failed unit 

384 # conversion cannot leave a previously subtracted background restored 

385 # while the map still says it is subtracted. 

386 if adjustment is not None: 386 ↛ 388line 386 didn't jump to line 388 because the condition on line 386 was always true

387 self.image.quantity += adjustment 

388 self._backgrounds._subtracted = name 

389 

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

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

392 

393 Parameters 

394 ---------- 

395 archive 

396 Archive to write to. 

397 """ 

398 serialized_image = archive.serialize_direct( 

399 "image", 

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

401 ) 

402 serialized_mask = archive.serialize_direct( 

403 "mask", 

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

405 ) 

406 serialized_variance = archive.serialize_direct( 

407 "variance", 

408 functools.partial( 

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

410 ), 

411 ) 

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

413 serialized_mask_fractions = { 

414 k: archive.serialize_direct( 

415 f"mask_fractions/{k}", 

416 functools.partial( 

417 v.serialize, 

418 save_projection=False, 

419 tile_shape=self.grid.cell_shape, 

420 options_name="mask_fractions", 

421 ), 

422 ) 

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

424 } 

425 serialized_noise_realizations = [ 

426 archive.serialize_direct( 

427 f"noise_realizations/{n}", 

428 functools.partial( 

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

430 ), 

431 ) 

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

433 ] 

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

435 serialized_aperture_corrections = archive.serialize_direct( 

436 "aperture_corrections", 

437 functools.partial( 

438 CellApertureCorrectionMapSerializationModel.serialize, self.aperture_corrections 

439 ), 

440 ) 

441 serialized_provenance = ( 

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

443 if self._provenance is not None 

444 else None 

445 ) 

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

447 return CellCoaddSerializationModel( 

448 image=serialized_image, 

449 mask=serialized_mask, 

450 variance=serialized_variance, 

451 sky_projection=serialized_projection, 

452 mask_fractions=serialized_mask_fractions, 

453 noise_realizations=serialized_noise_realizations, 

454 band=self._band, 

455 psf=serialized_psf, 

456 aperture_corrections=serialized_aperture_corrections, 

457 patch=self._patch, 

458 provenance=serialized_provenance, 

459 backgrounds=serialized_backgrounds, 

460 metadata=self.metadata, 

461 ) 

462 

463 @staticmethod 

464 def _get_archive_tree_type[P: pydantic.BaseModel]( 

465 pointer_type: type[P], 

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

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

468 type that uses the given pointer type. 

469 """ 

470 return CellCoaddSerializationModel[pointer_type] # type: ignore 

471 

472 @staticmethod 

473 def from_legacy_cell_coadd( 

474 legacy: LegacyMultipleCellCoadd, 

475 *, 

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

477 tract_info: TractInfo, 

478 bbox: Box | None = None, 

479 ) -> CellCoadd: 

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

481 

482 Parameters 

483 ---------- 

484 legacy 

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

486 plane_map 

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

488 description. 

489 tract_info 

490 Information about the full tract. 

491 bbox 

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

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

494 """ 

495 from lsst.geom import Box2I 

496 

497 if plane_map is None: 

498 plane_map = get_legacy_deep_coadd_mask_planes() 

499 if bbox is None: 

500 legacy_bbox = Box2I() 

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

502 legacy_bbox.include(single_cell.inner.bbox) 

503 else: 

504 legacy_bbox = bbox.to_legacy() 

505 legacy_stitched = legacy.stitch(legacy_bbox) 

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

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

508 sky_projection = SkyProjection.from_legacy( 

509 legacy.wcs, 

510 TractFrame( 

511 skymap=legacy.identifiers.skymap, 

512 tract=legacy.identifiers.tract, 

513 bbox=tract_bbox, 

514 ), 

515 pixel_bounds=tract_bbox, 

516 ) 

517 band = legacy.identifiers.band 

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

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

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

521 noise_realizations = [ 

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

523 ] 

524 mask_fractions = ( 

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

526 if legacy_stitched.mask_fractions is not None 

527 else {} 

528 ) 

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

530 aperture_corrections = { 

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

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

533 } 

534 patch_info = tract_info[legacy.identifiers.patch] 

535 patch = PatchDefinition( 

536 id=patch_info.getSequentialIndex(), 

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

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

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

540 ) 

541 provenance = CoaddProvenance.from_legacy(legacy) 

542 return CellCoadd( 

543 image=image, 

544 mask=mask, 

545 variance=variance, 

546 mask_fractions=mask_fractions, 

547 noise_realizations=noise_realizations, 

548 sky_projection=sky_projection, 

549 band=band, 

550 psf=psf, 

551 aperture_corrections=aperture_corrections, 

552 patch=patch, 

553 provenance=provenance, 

554 ) 

555 

556 def to_legacy_cell_coadd( 

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

558 ) -> LegacyMultipleCellCoadd: 

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

560 

561 Parameters 

562 ---------- 

563 copy 

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

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

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

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

568 plane_map 

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

570 description. 

571 """ 

572 from frozendict import frozendict 

573 

574 from lsst.cell_coadds import CellIdentifiers as LegacyCellIdentifiers 

575 from lsst.cell_coadds import CoaddUnits as LegacyCoaddUnits 

576 from lsst.cell_coadds import CommonComponents as LegacyCommonComponents 

577 from lsst.cell_coadds import MultipleCellCoadd as LegacyMultipleCellCoadd 

578 from lsst.cell_coadds import OwnedImagePlanes as LegacyOwnedImagePlanes 

579 from lsst.cell_coadds import PatchIdentifiers as LegacyPatchIdentifiers 

580 from lsst.cell_coadds import SingleCellCoadd as LegacySingleCellCoadd 

581 from lsst.skymap import Index2D as LegacyIndex2D 

582 

583 if plane_map is None: 

584 plane_map = get_legacy_deep_coadd_mask_planes() 

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

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

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

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

589 legacy_grid = self.grid.to_legacy() 

590 visit_polygons = self.provenance.to_legacy_polygon_map() 

591 legacy_common = LegacyCommonComponents( 

592 units=LegacyCoaddUnits.nJy, 

593 wcs=self.sky_projection.to_legacy(), 

594 band=self.band, 

595 identifiers=LegacyPatchIdentifiers( 

596 self.skymap, 

597 self.tract, 

598 LegacyIndex2D(x=self.patch.index.x, y=self.patch.index.y), 

599 band=self.band, 

600 ), 

601 visit_polygons=visit_polygons, 

602 ) 

603 legacy_inputs = self.provenance.to_legacy_cell_coadd_inputs(visit_polygons.keys()) 

604 cells: list[LegacySingleCellCoadd] = [] 

605 for cell_index in self.bounds.cell_indices(): 

606 cell_bbox = self.grid.bbox_of(cell_index) 

607 # Legacy type only has room for one mask_fractions plane. 

608 legacy_mask_fractions = ( 

609 next(iter(self.mask_fractions.values()))[cell_bbox].to_legacy(copy=copy) 

610 if self.mask_fractions 

611 else None 

612 ) 

613 legacy_planes = LegacyOwnedImagePlanes( 

614 image=self.image[cell_bbox].to_legacy(copy=copy), 

615 mask=self.mask[cell_bbox].to_legacy(plane_map), 

616 variance=self.variance[cell_bbox].to_legacy(copy=copy), 

617 mask_fractions=legacy_mask_fractions, 

618 noise_realizations=[n[cell_bbox].to_legacy(copy=copy) for n in self.noise_realizations], 

619 ) 

620 legacy_aperture_correction_map = frozendict( 

621 { 

622 name: field.value_in_cell(cell_index) 

623 if cell_index not in field.bounds.missing 

624 else np.nan 

625 for name, field in self.aperture_corrections.items() 

626 } 

627 ) 

628 cells.append( 

629 LegacySingleCellCoadd( 

630 legacy_planes, 

631 psf=self.psf[cell_index].to_legacy(copy=copy), 

632 inner_bbox=cell_bbox.to_legacy(), 

633 common=legacy_common, 

634 inputs=legacy_inputs[cell_index.to_legacy()], 

635 identifiers=LegacyCellIdentifiers( 

636 self.skymap, 

637 self.tract, 

638 legacy_common.identifiers.patch, 

639 band=self.band, 

640 cell=cell_index.to_legacy(), 

641 ), 

642 aperture_correction_map=legacy_aperture_correction_map, 

643 ) 

644 ) 

645 return LegacyMultipleCellCoadd( 

646 cells, 

647 legacy_grid, 

648 outer_cell_size=self.grid.cell_shape.to_legacy_int_extent(), 

649 psf_image_size=self.psf.kernel_bbox.shape.to_legacy_int_extent(), 

650 common=legacy_common, 

651 inner_bbox=self.bbox.to_legacy(), 

652 ) 

653 

654 def to_legacy( 

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

656 ) -> LegacyExposure: 

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

658 

659 Parameters 

660 ---------- 

661 copy 

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

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

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

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

666 plane_map 

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

668 description. 

669 

670 Returns 

671 ------- 

672 `lsst.afw.image.Exposure` 

673 A legacy representation of the coadd. This will have its ``wcs``, 

674 ``psf``, ``filter``, ``photoCalib``, and ``metadata`` components 

675 set. The ``apCorrMap`` component is not set, because there is no 

676 true `lsst.afw.math.BoundedField` representation for cell-coadd 

677 aperture corrections, and the ``coaddInputs`` component is not set 

678 because that data structure cannot fully capture cell-coadd 

679 provenance. 

680 

681 Notes 

682 ----- 

683 This method requires the `provenance` attribute to have been populated 

684 at construction. 

685 """ 

686 from lsst.afw.image import Exposure as LegacyExposure 

687 from lsst.afw.image import FilterLabel as LegacyFilterLabel 

688 

689 if plane_map is None: 

690 plane_map = get_legacy_deep_coadd_mask_planes() 

691 legacy_masked_image = super().to_legacy(copy=copy, plane_map=plane_map) 

692 result = LegacyExposure(legacy_masked_image, dtype=self.image.array.dtype) 

693 result_info = result.info 

694 result_info.setWcs(self.sky_projection.to_legacy()) 

695 result_info.setPsf(self.psf.to_legacy()) 

696 result_info.setFilter(LegacyFilterLabel.fromBand(self.band)) 

697 result_info.setPhotoCalib(BaseField.make_legacy_photo_calib(self.unit)) 

698 # We don't do setCoaddInputs because that data structure can't really 

699 # represent cell-coadd provenance accurately, and it's not clear 

700 # anything would use it. 

701 self._fill_legacy_metadata(result_info.getMetadata()) 

702 # We can't do setApCorrMap because the legacy 

703 # StitchedApertureCorrection is not a real C++ BoundedField, just a 

704 # Python duck-alike. 

705 return result 

706 

707 

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

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

710 

711 SCHEMA_NAME: ClassVar[str] = "cell_coadd" 

712 SCHEMA_VERSION: ClassVar[str] = "1.0.0" 

713 MIN_READ_VERSION: ClassVar[int] = 1 

714 PUBLIC_TYPE: ClassVar[type] = CellCoadd 

715 

716 # Inherited attributes are duplicated because that improves the docs 

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

718 # important docs. 

719 

720 image: ImageSerializationModel[P] = pydantic.Field(description="The main data image.") 

721 mask: MaskSerializationModel[P] = pydantic.Field( 

722 description="Bitmask that annotates the main image's pixels." 

723 ) 

724 variance: ImageSerializationModel[P] = pydantic.Field( 

725 description="Per-pixel variance estimates for the main image." 

726 ) 

727 sky_projection: SkyProjectionSerializationModel[P] = pydantic.Field( 

728 description="Projection that maps the pixel grid to the sky.", 

729 ) 

730 mask_fractions: dict[str, ImageSerializationModel[P]] = pydantic.Field( 

731 description=( 

732 "A mapping from an input-image mask plane name to an image of the weights sums of that plane." 

733 ) 

734 ) 

735 noise_realizations: list[ImageSerializationModel[P]] = pydantic.Field( 

736 description=( 

737 "A mapping from an input-image mask plane name to an image of the weights sums of that plane." 

738 ) 

739 ) 

740 band: str | None = pydantic.Field(description="Name of the band.") 

741 psf: CellPointSpreadFunctionSerializationModel = pydantic.Field( 

742 description="Effective point-spread function model for the coadd." 

743 ) 

744 aperture_corrections: CellApertureCorrectionMapSerializationModel | None = pydantic.Field( 

745 None, description="Coadded aperture corrections for different photometry algorithms." 

746 ) 

747 patch: PatchDefinition | None = pydantic.Field(description="Identifiers and geometry for the patch.") 

748 provenance: CoaddProvenanceSerializationModel | None = pydantic.Field( 

749 description="Information about the images that went into the coadd." 

750 ) 

751 backgrounds: BackgroundMapSerializationModel = pydantic.Field( 

752 default_factory=BackgroundMapSerializationModel, 

753 description="Background models associated with this image.", 

754 ) 

755 

756 def deserialize( 

757 self, 

758 archive: InputArchive[Any], 

759 *, 

760 bbox: Box | None = None, 

761 provenance: bool = True, 

762 **kwargs: Any, 

763 ) -> CellCoadd: 

764 """Deserialize an image from an input archive. 

765 

766 Parameters 

767 ---------- 

768 archive 

769 Archive to read from. 

770 bbox 

771 Bounding box of a subimage to read instead. 

772 provenance 

773 Whether to read and attach provenance information. 

774 **kwargs 

775 Unsupported keyword arguments are accepted only to provide better 

776 error messages (raising `.serialization.InvalidParameterError`). 

777 """ 

778 if kwargs: 778 ↛ 779line 778 didn't jump to line 779 because the condition on line 778 was never true

779 raise InvalidParameterError(f"Unrecognized parameters for CellCoadd: {set(kwargs.keys())}.") 

780 masked_image = super().deserialize(archive, bbox=bbox) 

781 mask_fractions = { 

782 k.removeprefix("mask_fractions/"): v.deserialize(archive, bbox=bbox) 

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

784 } 

785 noise_realizations = [v.deserialize(archive, bbox=bbox) for v in self.noise_realizations] 

786 sky_projection = self.sky_projection.deserialize(archive) 

787 psf = self.psf.deserialize(archive, bbox=bbox) 

788 aperture_corrections = ( 

789 self.aperture_corrections.deserialize(archive) if self.aperture_corrections is not None else {} 

790 ) 

791 coadd_provenance: CoaddProvenance | None = None 

792 if self.provenance is not None and provenance: 792 ↛ 796line 792 didn't jump to line 796 because the condition on line 792 was always true

793 coadd_provenance = self.provenance.deserialize(archive) 

794 if bbox is not None: 

795 coadd_provenance = coadd_provenance.subset(psf.bounds.cell_indices()) 

796 backgrounds = self.backgrounds.deserialize(archive) 

797 return CellCoadd( 

798 masked_image.image, 

799 mask=masked_image.mask, 

800 variance=masked_image.variance, 

801 mask_fractions=mask_fractions, 

802 noise_realizations=noise_realizations, 

803 sky_projection=sky_projection, 

804 band=self.band, 

805 psf=psf, 

806 aperture_corrections=aperture_corrections, 

807 patch=self.patch, 

808 provenance=coadd_provenance, 

809 backgrounds=backgrounds, 

810 )._finish_deserialize(self) 

811 

812 def deserialize_component(self, component: str, archive: InputArchive[Any], **kwargs: Any) -> Any: 

813 match component: 

814 case "mask_fractions": 

815 return { 

816 name: image_model.deserialize(archive, **kwargs) 

817 for name, image_model in self.mask_fractions.items() 

818 } 

819 case "noise_realizations": 

820 return [image_model.deserialize(archive, **kwargs) for image_model in self.noise_realizations] 

821 case "aperture_corrections" if self.aperture_corrections is None: 

822 # super() delegation handles the not-None case. 

823 return {} 

824 case "masked_image": 

825 return super().deserialize(archive, **kwargs) 

826 return super().deserialize_component(component, archive, **kwargs)