Coverage for python/lsst/images/_difference_image.py: 44%

159 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__ = ("DifferenceImage", "DifferenceImageSerializationModel", "DifferenceImageTemplateInfo") 

15 

16import logging 

17import math 

18import uuid 

19from collections.abc import Iterable, Mapping 

20from types import EllipsisType 

21from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast 

22 

23import astropy.units 

24import pydantic 

25from astro_metadata_translator import ObservationInfo 

26 

27from ._backgrounds import BackgroundMap 

28from ._geom import Bounds, Box 

29from ._image import Image 

30from ._mask import Mask, MaskPlane, MaskSchema, get_legacy_difference_image_mask_planes 

31from ._observation_summary_stats import ObservationSummaryStats 

32from ._polygon import Polygon 

33from ._transforms import DetectorFrame, SkyProjection, TractFrame, Transform 

34from ._visit_image import VisitImage, VisitImageSerializationModel 

35from .aperture_corrections import ( 

36 ApertureCorrectionMap, 

37) 

38from .cameras import Detector 

39from .convolution_kernels import ConvolutionKernel, ConvolutionKernelSerializationModel 

40from .fields import Field 

41from .psfs import ( 

42 PointSpreadFunction, 

43) 

44from .serialization import ( 

45 ArchiveReadError, 

46 InputArchive, 

47 InvalidParameterError, 

48 MetadataValue, 

49 OutputArchive, 

50) 

51 

52if TYPE_CHECKING: 

53 from lsst.daf.butler import DataId 

54 

55 try: 

56 from lsst.afw.geom import SkyWcs as LegacySkyWcs 

57 from lsst.afw.image import Exposure as LegacyExposure 

58 from lsst.geom import Box2I as LegacyBox2I 

59 from lsst.meas.algorithms import CoaddPsf as LegacyCoaddPsf 

60 except ImportError: 

61 type LegacyBox2I = Any # type: ignore[no-redef] 

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

63 type LegacyCoaddPsf = Any # type: ignore[no-redef] 

64 type LegacySkyWcs = Any # type: ignore[no-redef] 

65 

66 

67class DifferenceImage(VisitImage): 

68 """An image that is the PSF-matched difference of two other images. 

69 

70 Parameters 

71 ---------- 

72 image 

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

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

75 mask 

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

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

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

79 variance 

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

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

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

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

84 (possibly by `None`). 

85 mask_schema 

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

87 not provided. 

88 sky_projection 

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

90 a ``sky_projection`` is already attached to ``image``. 

91 bounds 

92 The region where this image's pixels and other properties are valid. 

93 If not provided, the bounding box of the image is used. Other 

94 components (``psf``, ``sky_projection``, ``aperture_corrections``, 

95 etc.) are assumed to have their own bounds which may or may not be the 

96 same as the image bounds. If ``bounds`` extends beyond the image 

97 bounding box, the intersection between ``bounds`` and the image 

98 bounding box is used instead. 

99 obs_info 

100 General information about this visit in standardized form. 

101 summary_stats 

102 Summary statistics associated with this visit. Initialized to default 

103 values if not provided. 

104 photometric_scaling 

105 Field that can be used to multiply a post-ISR image units to yield 

106 calibrated image units. This may be a scaling that was already 

107 applied (so dividing by it will recover the post-ISR units) or a 

108 scaling that has not been applied, depending on ``image.unit``. 

109 psf 

110 Point-spread function model for this image, or an exception explaining 

111 why it could not be read (to be raised if the PSF is requested later). 

112 detector 

113 Geometry and electronic information for the detector attached to this 

114 image. 

115 aperture_corrections : `dict` [`str`, `~fields.BaseField`] 

116 Mapping from photometry algorithm name to the aperture correction for 

117 that algorithm. 

118 backgrounds 

119 Background models associated with this image. 

120 band 

121 Name of the passband the image was observed with (this is a shorter, 

122 less specific version of ``obs_info.physical_filter``). 

123 kernel 

124 The convolution kernel used to match the (warped) template to the 

125 science image. 

126 templates 

127 Information about the template coadds that went into this difference 

128 image. 

129 metadata 

130 Arbitrary flexible metadata to associate with the image. 

131 

132 Notes 

133 ----- 

134 This class assumes that the difference has been performed on the pixel 

135 grid of the 'science image' (i.e. a single observation, like `VisitImage`), 

136 and most of the attributes of `DifferenceImage` correspond to the science 

137 image. The 'template image' is assumed to be comprised of one or more 

138 resampled coadd images stitched together. 

139 

140 The `DifferenceImage` class can also be used to represent the stitched 

141 template itself; while this makes the naming a bit confusing, the type has 

142 the right state to play this role. 

143 """ 

144 

145 def __init__( 

146 self, 

147 image: Image, 

148 *, 

149 mask: Mask | None = None, 

150 variance: Image | None = None, 

151 mask_schema: MaskSchema | None = None, 

152 sky_projection: SkyProjection[DetectorFrame] | None = None, 

153 bounds: Bounds | None = None, 

154 obs_info: ObservationInfo | None = None, 

155 summary_stats: ObservationSummaryStats | None = None, 

156 photometric_scaling: Field | None = None, 

157 psf: PointSpreadFunction | ArchiveReadError, 

158 detector: Detector, 

159 aperture_corrections: ApertureCorrectionMap | None = None, 

160 backgrounds: BackgroundMap | None = None, 

161 band: str, 

162 kernel: ConvolutionKernel | None = None, 

163 templates: Iterable[DifferenceImageTemplateInfo] | None = None, 

164 metadata: dict[str, MetadataValue] | None = None, 

165 ) -> None: 

166 super().__init__( 

167 image, 

168 mask=mask, 

169 variance=variance, 

170 mask_schema=mask_schema, 

171 sky_projection=sky_projection, 

172 bounds=bounds, 

173 obs_info=obs_info, 

174 summary_stats=summary_stats, 

175 photometric_scaling=photometric_scaling, 

176 psf=psf, 

177 detector=detector, 

178 aperture_corrections=aperture_corrections, 

179 backgrounds=backgrounds, 

180 band=band, 

181 metadata=metadata, 

182 ) 

183 self._kernel = kernel 

184 self._templates = list(templates) if templates is not None else None 

185 

186 @staticmethod 

187 def _from_visit_image( 

188 visit_image: VisitImage, 

189 kernel: ConvolutionKernel | None, 

190 templates: Iterable[DifferenceImageTemplateInfo] | None, 

191 ) -> DifferenceImage: 

192 return visit_image._transfer_metadata( 

193 DifferenceImage( 

194 visit_image.image, 

195 mask=visit_image.mask, 

196 variance=visit_image.variance, 

197 sky_projection=visit_image.sky_projection, 

198 bounds=visit_image.bounds, 

199 obs_info=visit_image.obs_info, 

200 summary_stats=visit_image.summary_stats, 

201 photometric_scaling=visit_image.photometric_scaling, 

202 psf=visit_image._psf, # get private attr to avoid triggering on ArchiveReadError early. 

203 detector=visit_image.detector, 

204 aperture_corrections=visit_image.aperture_corrections, 

205 backgrounds=visit_image.backgrounds, 

206 kernel=kernel, 

207 templates=templates, 

208 band=visit_image.band, 

209 ), 

210 ) 

211 

212 @property 

213 def kernel(self) -> ConvolutionKernel: 

214 """The convolution kernel used to match the (warped) template 

215 to the science image (`.convolution_kernels.ConvolutionKernel`). 

216 """ 

217 if self._kernel is None: 

218 raise AttributeError("This difference image does not have a kernel attached.") 

219 return self._kernel 

220 

221 @kernel.setter 

222 def kernel(self, kernel: ConvolutionKernel) -> None: 

223 self._kernel = kernel 

224 

225 @kernel.deleter 

226 def kernel(self) -> None: 

227 self._kernel = None 

228 

229 @property 

230 def templates(self) -> list[DifferenceImageTemplateInfo]: 

231 """Information about the template coadds that went into this 

232 difference image (`list` [`DifferenceImageTemplateInfo`]). 

233 """ 

234 if self._templates is None: 

235 raise AttributeError("This difference image does not have any template information attached.") 

236 return self._templates 

237 

238 @templates.setter 

239 def templates(self, templates: Iterable[DifferenceImageTemplateInfo]) -> None: 

240 self._templates = list(templates) 

241 

242 @templates.deleter 

243 def templates(self) -> None: 

244 self._templates = None 

245 

246 def __getitem__(self, bbox: Box | EllipsisType) -> DifferenceImage: 

247 if bbox is ...: 

248 return self 

249 return self._from_visit_image( 

250 super().__getitem__(bbox), kernel=self._kernel, templates=self._templates 

251 ) 

252 

253 def __str__(self) -> str: 

254 return f"DifferenceImage({self.image!s}, {list(self.mask.schema.names)})" 

255 

256 def __repr__(self) -> str: 

257 return f"DifferenceImage({self.image!r}, mask_schema={self.mask.schema!r})" 

258 

259 def copy(self, *, copy_detector: bool = False) -> DifferenceImage: 

260 """Deep-copy the difference image. 

261 

262 Parameters 

263 ---------- 

264 copy_detector 

265 Whether to deep-copy the `detector` attribute. 

266 """ 

267 return self._from_visit_image( 

268 super().copy(copy_detector=copy_detector), kernel=self._kernel, templates=self._templates 

269 ) 

270 

271 def convert_unit( 

272 self, 

273 unit: astropy.units.UnitBase = astropy.units.nJy, 

274 copy: Literal["as-needed"] | bool = True, 

275 copy_detector: bool = False, 

276 ) -> DifferenceImage: 

277 """Return an equivalent image with different pixel units. 

278 

279 Parameters 

280 ---------- 

281 unit 

282 The unit to transform to. This may be any of the following: 

283 

284 - any unit directly relatable to the current units via Astropy; 

285 - any unit relatable to the product of the current units with the 

286 `photometric_scaling` (i.e. if the current image is in 

287 instrumental units but we know how to calibrate them) 

288 - any unit relatable to the quotient of the current units with the 

289 `photometric_scaling` (i.e. if the current image is in 

290 calibrated units and we want to revert back to instrumental 

291 units). 

292 copy 

293 Whether to copy the images and other components. If `True`, all 

294 components that aren't controlled by some other argument will 

295 always be deep-copied. If `False`, the operation will fail if the 

296 image is not already in the right units. If ``as-needed``, only 

297 the image and variance will be copied, and only if they are not 

298 already in the right units. 

299 copy_detector 

300 Whether to deep-copy the `detector` attribute. 

301 

302 Returns 

303 ------- 

304 `DifferenceImage` 

305 An image with the given units. 

306 """ 

307 return self._from_visit_image( 

308 super().convert_unit(unit, copy=copy, copy_detector=copy_detector), 

309 kernel=self._kernel, 

310 templates=self._templates, 

311 ) 

312 

313 def serialize(self, archive: OutputArchive[Any]) -> DifferenceImageSerializationModel[Any]: 

314 result = self._serialize_impl(DifferenceImageSerializationModel, archive) 

315 if self._kernel is not None: 

316 result.kernel = archive.serialize_direct("kernel", self._kernel.serialize) 

317 else: 

318 result.kernel = None 

319 result.templates = self._templates 

320 return result 

321 

322 @staticmethod 

323 def _get_archive_tree_type[P: pydantic.BaseModel]( 

324 pointer_type: type[P], 

325 ) -> type[DifferenceImageSerializationModel[P]]: 

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

327 type that uses the given pointer type. 

328 """ 

329 return DifferenceImageSerializationModel[pointer_type] # type: ignore 

330 

331 @staticmethod 

332 def from_legacy( 

333 legacy: LegacyExposure, 

334 *, 

335 unit: astropy.units.UnitBase | None = None, 

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

337 instrument: str | None = None, 

338 visit: int | None = None, 

339 ) -> DifferenceImage: 

340 """Convert from an `lsst.afw.image.Exposure` instance. 

341 

342 Parameters 

343 ---------- 

344 legacy 

345 An `lsst.afw.image.Exposure` instance that will share image and 

346 variance (but not mask) pixel data with the returned object. 

347 unit 

348 Units of the image. If not provided, the ``BUNIT`` metadata 

349 key will be used, if available. 

350 plane_map 

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

352 description. If `None` (default) 

353 `get_legacy_visit_image_mask_planes` is used. 

354 instrument 

355 Name of the instrument. Extracted from the metadata if not 

356 provided. 

357 visit 

358 ID of the visit. Extracted from the metadata if not provided. 

359 """ 

360 if plane_map is None: 

361 plane_map = get_legacy_difference_image_mask_planes() 

362 return DifferenceImage._from_visit_image( 

363 VisitImage.from_legacy( 

364 legacy, unit=unit, plane_map=plane_map, instrument=instrument, visit=visit 

365 ), 

366 kernel=None, 

367 templates=None, 

368 ) 

369 

370 def to_legacy( 

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

372 ) -> LegacyExposure: 

373 """Convert to an `lsst.afw.image.Exposure` instance. 

374 

375 Parameters 

376 ---------- 

377 copy 

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

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

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

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

382 plane_map 

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

384 description. If `None` (default), 

385 `get_legacy_visit_image_mask_planes` is used. 

386 """ 

387 if plane_map is None: 

388 plane_map = get_legacy_difference_image_mask_planes() 

389 return super().to_legacy(copy=copy, plane_map=plane_map) 

390 

391 @staticmethod 

392 def read_legacy( # type: ignore[override] 

393 filename: str, 

394 *, 

395 preserve_quantization: bool = False, 

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

397 instrument: str | None = None, 

398 visit: int | None = None, 

399 component: Literal[ 

400 "bbox", 

401 "image", 

402 "mask", 

403 "variance", 

404 "sky_projection", 

405 "psf", 

406 "detector", 

407 "photometric_scaling", 

408 "obs_info", 

409 "summary_stats", 

410 "aperture_corrections", 

411 ] 

412 | None = None, 

413 ) -> Any: 

414 """Read a FITS file written by `lsst.afw.image.Exposure.writeFits`. 

415 

416 Parameters 

417 ---------- 

418 filename 

419 Full name of the file. 

420 preserve_quantization 

421 If `True`, ensure that writing the masked image back out again will 

422 exactly preserve quantization-compressed pixel values. This causes 

423 the image and variance plane arrays to be marked as read-only and 

424 stores the original binary table data for those planes in memory. 

425 If the `MaskedImage` is copied, the precompressed pixel values are 

426 not transferred to the copy. 

427 plane_map 

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

429 description. If `None` (default) 

430 `get_legacy_visit_image_mask_planes` is used. 

431 instrument 

432 Name of the instrument. Read from the primary header if not 

433 provided. 

434 visit 

435 ID of the visit. Read from the primary header if not 

436 provided. 

437 component 

438 A component to read instead of the full image. 

439 """ 

440 if plane_map is None: 

441 plane_map = get_legacy_difference_image_mask_planes() 

442 result = VisitImage.read_legacy( 

443 filename, 

444 preserve_quantization=preserve_quantization, 

445 plane_map=plane_map, 

446 instrument=instrument, 

447 visit=visit, 

448 component=component, 

449 ) 

450 if component is None: 

451 return DifferenceImage._from_visit_image(result, kernel=None, templates=None) 

452 return result 

453 

454 

455class DifferenceImageTemplateInfo(pydantic.BaseModel, ser_json_inf_nan="constants"): 

456 """Information about how a template image contributed to a difference 

457 image. 

458 """ 

459 

460 skymap: str = pydantic.Field(description="Name of the skymap that defines the tract/patch tiling.") 

461 tract: int = pydantic.Field(description="ID of the tract (each tract is a different projection).") 

462 patch: int = pydantic.Field( 

463 description="ID of the patch (all patches within a tract share a projection)." 

464 ) 

465 dataset_id: uuid.UUID = pydantic.Field( 

466 description="Universally unique butler identifier for this template.", 

467 ) 

468 dataset_run: str = pydantic.Field(description="Name of the butler RUN collection for this template.") 

469 bounds: Polygon = pydantic.Field( 

470 description=( 

471 "The approximate intersection of the template and the science image, " 

472 "in the science image's pixel coordinate system." 

473 ) 

474 ) 

475 psf_shape_xx: float = pydantic.Field(description="Second moment of the effective PSF of the template.") 

476 psf_shape_yy: float = pydantic.Field(description="Second moment of the effective PSF of the template.") 

477 psf_shape_xy: float = pydantic.Field(description="Second moment of the effective PSF of the template.") 

478 psf_shape_flag: bool = pydantic.Field( 

479 description="Flag set if the second moments of the effective template PSF could not be computed." 

480 ) 

481 

482 @staticmethod 

483 def from_legacy( 

484 detector_frame: DetectorFrame, 

485 legacy_template_psf: LegacyCoaddPsf, 

486 legacy_template_metadata: Mapping[str, Any], 

487 coadd_data_ids_by_uuid: Mapping[uuid.UUID, DataId], 

488 coadd_dataset_type: str = "template_coadd", 

489 log: logging.Logger | None = None, 

490 ) -> list[DifferenceImageTemplateInfo]: 

491 """Construct a list of template information structs from information 

492 stored in a legacy stitched template image. 

493 

494 Parameters 

495 ---------- 

496 detector_frame 

497 Coordinate system and bounding box of the science image. 

498 legacy_template_psf 

499 The lazy-evaluation PSF model for the stitched template; used to 

500 extract the tract and patch IDs of the coadds actually used and 

501 their PSF models. 

502 legacy_template_metadata 

503 The FITS-style metadata of the stitched template; used to extract 

504 butler UUIDs and RUN collection names for all *potential* input 

505 coadds. 

506 coadd_data_ids_by_uuid 

507 A mapping from butler dataset ID to ``{tract, patch, band}`` data 

508 ID for all coadds that may have contributed to the template. May 

509 be a much larger superset of the needed datasets. 

510 coadd_dataset_type 

511 The name of the coadd template dataset type. 

512 log 

513 Logger to use for diagnostic messages. 

514 """ 

515 from lsst.afw.geom import makeWcsPairTransform 

516 

517 n_inputs = legacy_template_metadata["LSST BUTLER N_INPUTS"] 

518 butler_info: dict[tuple[int, int], tuple[uuid.UUID, str]] = {} 

519 skymap: str | None = None 

520 for n in range(n_inputs): 

521 if legacy_template_metadata[f"LSST BUTLER INPUT {n} DATASETTYPE"] == coadd_dataset_type: 

522 input_id = uuid.UUID(legacy_template_metadata[f"LSST BUTLER INPUT {n} ID"]) 

523 input_run = legacy_template_metadata[f"LSST BUTLER INPUT {n} RUN"] 

524 input_data_id = coadd_data_ids_by_uuid[input_id] 

525 if skymap is None: 

526 skymap = cast(str, input_data_id["skymap"]) 

527 elif skymap != input_data_id["skymap"]: 

528 raise RuntimeError("Cannot handle multiple skymaps in the inputs to a single template.") 

529 butler_info[cast(int, input_data_id["tract"]), cast(int, input_data_id["patch"])] = ( 

530 input_id, 

531 input_run, 

532 ) 

533 result: list[DifferenceImageTemplateInfo] = [] 

534 # A "component" of this PSF is an input {tract, patch} coadd. 

535 for n in range(legacy_template_psf.getComponentCount()): 

536 tract = legacy_template_psf.getTract(n) 

537 patch = legacy_template_psf.getPatch(n) 

538 dataset_id, dataset_run = butler_info[tract, patch] 

539 patch_bbox = Box.from_legacy(legacy_template_psf.getBBox(n)) 

540 coadd_frame = TractFrame( 

541 skymap=skymap, 

542 tract=tract, 

543 # This bbox is supposed to be the full tract bbox, but this 

544 # frame is just a temporary and we don't have access to that. 

545 # (If this ever becomes not-a-temporary, we could add a skymap 

546 # argument). 

547 bbox=patch_bbox, 

548 ) 

549 detector_to_coadd = Transform.from_legacy( 

550 makeWcsPairTransform( 

551 # CoaddPsf method names did not anticipate being used for 

552 # detector-level templates, so this is confusing: 

553 legacy_template_psf.getCoaddWcs(), # this is the template_detector WCS! 

554 legacy_template_psf.getWcs(n), # this is the template_coadd WCS! 

555 ), 

556 detector_frame, 

557 coadd_frame, 

558 ) 

559 coadd_to_detector = detector_to_coadd.inverted() 

560 # We transform the detector bbox to each coadd frame, do the 

561 # intersection there, and then transform the intersection back to 

562 # the detector frame, because we do not trust detector WCSs beyond 

563 # the detector bounding box; they can be polynomials that 

564 # extrapolate badly. Coadd WCSs in contrast are simple projections. 

565 tmp_bounds = ( 

566 Polygon.from_box(detector_frame.bbox).transform(detector_to_coadd).intersection(patch_bbox) 

567 ).transform(coadd_to_detector) 

568 # Unfortunately doing the intersection in the coadd coordinate 

569 # system means the transformed intersection might not quite be 

570 # contained by the detector bounding box, due to floating-point 

571 # round-off error. Intersect one more time to tidy it up. 

572 bounds = tmp_bounds.intersection(detector_frame.bbox) 

573 assert isinstance(bounds, Polygon), ( 

574 "The operations above should not change the region's fundamental topology." 

575 ) 

576 try: 

577 psf_shape = legacy_template_psf.computeShape(bounds.centroid.to_legacy_float_point()) 

578 except Exception: 

579 if log is not None: 

580 log.exception( 

581 "Could not compute PSF shape for template coadd with tract=%s, patch=%s", tract, patch 

582 ) 

583 else: 

584 raise 

585 psf_shape = None 

586 result.append( 

587 DifferenceImageTemplateInfo( 

588 skymap=skymap, 

589 tract=tract, 

590 patch=patch, 

591 dataset_id=dataset_id, 

592 dataset_run=dataset_run, 

593 bounds=bounds, 

594 psf_shape_xx=psf_shape.getIxx() if psf_shape is not None else math.nan, 

595 psf_shape_yy=psf_shape.getIyy() if psf_shape is not None else math.nan, 

596 psf_shape_xy=psf_shape.getIxy() if psf_shape is not None else math.nan, 

597 psf_shape_flag=psf_shape is None, 

598 ) 

599 ) 

600 result.sort(key=lambda item: (item.tract, item.patch)) 

601 return result 

602 

603 

604class DifferenceImageSerializationModel[P: pydantic.BaseModel](VisitImageSerializationModel[P]): 

605 """A Pydantic model used to represent a serialized `DifferenceImage`.""" 

606 

607 SCHEMA_NAME: ClassVar[str] = "difference_image" 

608 SCHEMA_VERSION: ClassVar[str] = "1.0.0" 

609 MIN_READ_VERSION: ClassVar[int] = 1 

610 PUBLIC_TYPE: ClassVar[type] = DifferenceImage 

611 

612 kernel: ConvolutionKernelSerializationModel | None = pydantic.Field( 

613 description="The convolution kernel used to match the (warped) template to the science image." 

614 ) 

615 templates: list[DifferenceImageTemplateInfo] | None = pydantic.Field( 

616 description="Information about the template coadds that went into this difference image" 

617 ) 

618 

619 def deserialize( 

620 self, archive: InputArchive[Any], *, bbox: Box | None = None, **kwargs: Any 

621 ) -> DifferenceImage: 

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

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

624 kernel = self.kernel.deserialize(archive) if self.kernel is not None else None 

625 return DifferenceImage._from_visit_image( 

626 super().deserialize(archive, bbox=bbox), kernel=kernel, templates=self.templates 

627 ) 

628 

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

630 if kwargs and component not in ("image", "mask", "variance", "masked_image"): 

631 raise InvalidParameterError( 

632 f"Unsupported parameters for DifferenceImage component {component}: {set(kwargs.keys())}." 

633 ) 

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