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

161 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__ = ("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 .describe import DescribeOptions, Report 

41from .fields import Field 

42from .psfs import ( 

43 PointSpreadFunction, 

44) 

45from .serialization import ( 

46 ArchiveReadError, 

47 InputArchive, 

48 InvalidParameterError, 

49 MetadataValue, 

50 OutputArchive, 

51) 

52 

53if TYPE_CHECKING: 

54 from lsst.daf.butler import DataId 

55 

56 try: 

57 from lsst.afw.geom import SkyWcs as LegacySkyWcs 

58 from lsst.afw.image import Exposure as LegacyExposure 

59 from lsst.geom import Box2I as LegacyBox2I 

60 from lsst.meas.algorithms import CoaddPsf as LegacyCoaddPsf 

61 except ImportError: 

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

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

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

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

66 

67 

68class DifferenceImage(VisitImage): 

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

70 

71 Parameters 

72 ---------- 

73 image 

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

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

76 mask 

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

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

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

80 variance 

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

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

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

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

85 (possibly by `None`). 

86 mask_schema 

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

88 not provided. 

89 sky_projection 

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

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

92 bounds 

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

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

95 components (``psf``, ``sky_projection``, ``aperture_corrections``, 

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

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

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

99 bounding box is used instead. 

100 obs_info 

101 General information about this visit in standardized form. 

102 summary_stats 

103 Summary statistics associated with this visit. Initialized to default 

104 values if not provided. 

105 photometric_scaling 

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

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

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

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

110 psf 

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

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

113 detector 

114 Geometry and electronic information for the detector attached to this 

115 image. 

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

117 Mapping from photometry algorithm name to the aperture correction for 

118 that algorithm. 

119 backgrounds 

120 Background models associated with this image. 

121 band 

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

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

124 kernel 

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

126 science image. 

127 templates 

128 Information about the template coadds that went into this difference 

129 image. 

130 metadata 

131 Arbitrary flexible metadata to associate with the image. 

132 

133 Notes 

134 ----- 

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

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

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

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

139 resampled coadd images stitched together. 

140 

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

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

143 the right state to play this role. 

144 """ 

145 

146 def __init__( 

147 self, 

148 image: Image, 

149 *, 

150 mask: Mask | None = None, 

151 variance: Image | None = None, 

152 mask_schema: MaskSchema | None = None, 

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

154 bounds: Bounds | None = None, 

155 obs_info: ObservationInfo | None = None, 

156 summary_stats: ObservationSummaryStats | None = None, 

157 photometric_scaling: Field | None = None, 

158 psf: PointSpreadFunction | ArchiveReadError, 

159 detector: Detector, 

160 aperture_corrections: ApertureCorrectionMap | None = None, 

161 backgrounds: BackgroundMap | None = None, 

162 band: str, 

163 kernel: ConvolutionKernel | None = None, 

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

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

166 ) -> None: 

167 super().__init__( 

168 image, 

169 mask=mask, 

170 variance=variance, 

171 mask_schema=mask_schema, 

172 sky_projection=sky_projection, 

173 bounds=bounds, 

174 obs_info=obs_info, 

175 summary_stats=summary_stats, 

176 photometric_scaling=photometric_scaling, 

177 psf=psf, 

178 detector=detector, 

179 aperture_corrections=aperture_corrections, 

180 backgrounds=backgrounds, 

181 band=band, 

182 metadata=metadata, 

183 ) 

184 self._kernel = kernel 

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

186 

187 @staticmethod 

188 def _from_visit_image( 

189 visit_image: VisitImage, 

190 kernel: ConvolutionKernel | None, 

191 templates: Iterable[DifferenceImageTemplateInfo] | None, 

192 ) -> DifferenceImage: 

193 return visit_image._transfer_metadata( 

194 DifferenceImage( 

195 visit_image.image, 

196 mask=visit_image.mask, 

197 variance=visit_image.variance, 

198 sky_projection=visit_image.sky_projection, 

199 bounds=visit_image.bounds, 

200 obs_info=visit_image.obs_info, 

201 summary_stats=visit_image.summary_stats, 

202 photometric_scaling=visit_image.photometric_scaling, 

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

204 detector=visit_image.detector, 

205 aperture_corrections=visit_image.aperture_corrections, 

206 backgrounds=visit_image.backgrounds, 

207 kernel=kernel, 

208 templates=templates, 

209 band=visit_image.band, 

210 ), 

211 ) 

212 

213 @property 

214 def kernel(self) -> ConvolutionKernel: 

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

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

217 """ 

218 if self._kernel is None: 

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

220 return self._kernel 

221 

222 @kernel.setter 

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

224 self._kernel = kernel 

225 

226 @kernel.deleter 

227 def kernel(self) -> None: 

228 self._kernel = None 

229 

230 @property 

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

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

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

234 """ 

235 if self._templates is None: 

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

237 return self._templates 

238 

239 @templates.setter 

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

241 self._templates = list(templates) 

242 

243 @templates.deleter 

244 def templates(self) -> None: 

245 self._templates = None 

246 

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

248 if bbox is ...: 

249 return self 

250 return self._from_visit_image( 

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

252 ) 

253 

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

255 """Return a `Report` describing this difference image. 

256 

257 Parameters 

258 ---------- 

259 options : `DescribeOptions`, optional 

260 Rendering options; forwarded to the base-class report. 

261 """ 

262 report = super()._describe(options) 

263 report.type_name = "DifferenceImage" 

264 report.summary = f"DifferenceImage({self.image!s}, {list(self.mask.schema.names)})" 

265 # ConvolutionKernel does not implement _describe; omit it from the 

266 # report until a describe method is added to that class. 

267 return report 

268 

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

270 """Deep-copy the difference image. 

271 

272 Parameters 

273 ---------- 

274 copy_detector 

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

276 """ 

277 return self._from_visit_image( 

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

279 ) 

280 

281 def convert_unit( 

282 self, 

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

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

285 copy_detector: bool = False, 

286 ) -> DifferenceImage: 

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

288 

289 Parameters 

290 ---------- 

291 unit 

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

293 

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

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

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

297 instrumental units but we know how to calibrate them) 

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

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

300 calibrated units and we want to revert back to instrumental 

301 units). 

302 copy 

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

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

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

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

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

308 already in the right units. 

309 copy_detector 

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

311 

312 Returns 

313 ------- 

314 `DifferenceImage` 

315 An image with the given units. 

316 """ 

317 return self._from_visit_image( 

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

319 kernel=self._kernel, 

320 templates=self._templates, 

321 ) 

322 

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

324 result = self._serialize_impl(DifferenceImageSerializationModel, archive) 

325 if self._kernel is not None: 

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

327 else: 

328 result.kernel = None 

329 result.templates = self._templates 

330 return result 

331 

332 @staticmethod 

333 def _get_archive_tree_type[P: pydantic.BaseModel]( 

334 pointer_type: type[P], 

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

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

337 type that uses the given pointer type. 

338 """ 

339 return DifferenceImageSerializationModel[pointer_type] # type: ignore 

340 

341 @staticmethod 

342 def from_legacy( # type: ignore[override] 

343 legacy: LegacyExposure, 

344 *, 

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

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

347 instrument: str | None = None, 

348 visit: int | None = None, 

349 ) -> DifferenceImage: 

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

351 

352 Parameters 

353 ---------- 

354 legacy 

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

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

357 unit 

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

359 key will be used, if available. 

360 plane_map 

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

362 description. If `None` (default) 

363 `get_legacy_visit_image_mask_planes` is used. 

364 instrument 

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

366 provided. 

367 visit 

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

369 """ 

370 if plane_map is None: 

371 plane_map = get_legacy_difference_image_mask_planes() 

372 return DifferenceImage._from_visit_image( 

373 VisitImage.from_legacy( 

374 legacy, unit=unit, plane_map=plane_map, instrument=instrument, visit=visit 

375 ), 

376 kernel=None, 

377 templates=None, 

378 ) 

379 

380 def to_legacy( 

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

382 ) -> LegacyExposure: 

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

384 

385 Parameters 

386 ---------- 

387 copy 

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

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

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

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

392 plane_map 

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

394 description. If `None` (default), 

395 `get_legacy_visit_image_mask_planes` is used. 

396 """ 

397 if plane_map is None: 

398 plane_map = get_legacy_difference_image_mask_planes() 

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

400 

401 @staticmethod 

402 def read_legacy( # type: ignore[override] 

403 filename: str, 

404 *, 

405 preserve_quantization: bool = False, 

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

407 instrument: str | None = None, 

408 visit: int | None = None, 

409 component: Literal[ 

410 "bbox", 

411 "image", 

412 "mask", 

413 "variance", 

414 "sky_projection", 

415 "psf", 

416 "detector", 

417 "photometric_scaling", 

418 "obs_info", 

419 "summary_stats", 

420 "aperture_corrections", 

421 ] 

422 | None = None, 

423 ) -> Any: 

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

425 

426 Parameters 

427 ---------- 

428 filename 

429 Full name of the file. 

430 preserve_quantization 

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

432 exactly preserve quantization-compressed pixel values. This causes 

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

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

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

436 not transferred to the copy. 

437 plane_map 

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

439 description. If `None` (default) 

440 `get_legacy_visit_image_mask_planes` is used. 

441 instrument 

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

443 provided. 

444 visit 

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

446 provided. 

447 component 

448 A component to read instead of the full image. 

449 """ 

450 if plane_map is None: 

451 plane_map = get_legacy_difference_image_mask_planes() 

452 result = VisitImage.read_legacy( 

453 filename, 

454 preserve_quantization=preserve_quantization, 

455 plane_map=plane_map, 

456 instrument=instrument, 

457 visit=visit, 

458 component=component, 

459 ) 

460 if component is None: 

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

462 return result 

463 

464 

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

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

467 image. 

468 """ 

469 

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

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

472 patch: int = pydantic.Field( 

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

474 ) 

475 dataset_id: uuid.UUID = pydantic.Field( 

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

477 ) 

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

479 bounds: Polygon = pydantic.Field( 

480 description=( 

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

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

483 ) 

484 ) 

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

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

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

488 psf_shape_flag: bool = pydantic.Field( 

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

490 ) 

491 

492 @staticmethod 

493 def from_legacy( 

494 detector_frame: DetectorFrame, 

495 legacy_template_psf: LegacyCoaddPsf, 

496 legacy_template_metadata: Mapping[str, Any], 

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

498 coadd_dataset_type: str = "template_coadd", 

499 log: logging.Logger | None = None, 

500 ) -> list[DifferenceImageTemplateInfo]: 

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

502 stored in a legacy stitched template image. 

503 

504 Parameters 

505 ---------- 

506 detector_frame 

507 Coordinate system and bounding box of the science image. 

508 legacy_template_psf 

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

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

511 their PSF models. 

512 legacy_template_metadata 

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

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

515 coadds. 

516 coadd_data_ids_by_uuid 

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

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

519 be a much larger superset of the needed datasets. 

520 coadd_dataset_type 

521 The name of the coadd template dataset type. 

522 log 

523 Logger to use for diagnostic messages. 

524 """ 

525 from lsst.afw.geom import makeWcsPairTransform 

526 

527 n_inputs = legacy_template_metadata["LSST BUTLER N_INPUTS"] 

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

529 skymap: str | None = None 

530 for n in range(n_inputs): 

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

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

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

534 input_data_id = coadd_data_ids_by_uuid[input_id] 

535 if skymap is None: 

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

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

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

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

540 input_id, 

541 input_run, 

542 ) 

543 result: list[DifferenceImageTemplateInfo] = [] 

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

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

546 tract = legacy_template_psf.getTract(n) 

547 patch = legacy_template_psf.getPatch(n) 

548 dataset_id, dataset_run = butler_info[tract, patch] 

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

550 coadd_frame = TractFrame( 

551 skymap=skymap, 

552 tract=tract, 

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

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

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

556 # argument). 

557 bbox=patch_bbox, 

558 ) 

559 detector_to_coadd = Transform.from_legacy( 

560 makeWcsPairTransform( 

561 # CoaddPsf method names did not anticipate being used for 

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

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

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

565 ), 

566 detector_frame, 

567 coadd_frame, 

568 ) 

569 coadd_to_detector = detector_to_coadd.inverted() 

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

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

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

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

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

575 tmp_bounds = ( 

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

577 ).transform(coadd_to_detector) 

578 # Unfortunately doing the intersection in the coadd coordinate 

579 # system means the transformed intersection might not quite be 

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

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

582 bounds = tmp_bounds.intersection(detector_frame.bbox) 

583 assert isinstance(bounds, Polygon), ( 

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

585 ) 

586 try: 

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

588 except Exception: 

589 if log is not None: 

590 log.exception( 

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

592 ) 

593 else: 

594 raise 

595 psf_shape = None 

596 result.append( 

597 DifferenceImageTemplateInfo( 

598 skymap=skymap, 

599 tract=tract, 

600 patch=patch, 

601 dataset_id=dataset_id, 

602 dataset_run=dataset_run, 

603 bounds=bounds, 

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

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

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

607 psf_shape_flag=psf_shape is None, 

608 ) 

609 ) 

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

611 return result 

612 

613 

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

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

616 

617 SCHEMA_NAME: ClassVar[str] = "difference_image" 

618 SCHEMA_VERSION: ClassVar[str] = "1.0.0.dev0" 

619 MIN_READ_VERSION: ClassVar[int] = 1 

620 PUBLIC_TYPE: ClassVar[type] = DifferenceImage 

621 

622 kernel: ConvolutionKernelSerializationModel | None = pydantic.Field( 

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

624 ) 

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

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

627 ) 

628 

629 def deserialize( 

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

631 ) -> DifferenceImage: 

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

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

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

635 return DifferenceImage._from_visit_image( 

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

637 ) 

638 

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

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

641 raise InvalidParameterError( 

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

643 ) 

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