Coverage for python / lsst / images / psfs / _gaussian.py: 37%
77 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-01 01:11 -0700
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-01 01:11 -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.
12from __future__ import annotations
14__all__ = (
15 "GaussianPSFSerializationModel",
16 "GaussianPointSpreadFunction",
17)
19from functools import cached_property
20from typing import Any
22import numpy as np
23import pydantic
25from lsst.images._image import Image
27from .. import serialization
28from .._geom import Bounds, Box, SerializableBounds
29from ._base import PointSpreadFunction
32class GaussianPointSpreadFunction(PointSpreadFunction):
33 """A PSF with a spatially-invariant circular Gaussian profile."""
35 def __init__(self, sigma: float, bounds: Bounds, stamp_size: int) -> None:
36 if sigma <= 0:
37 raise ValueError(f"sigma must be positive; got {sigma}.")
38 if stamp_size <= 0:
39 raise ValueError(f"stamp_size must be positive; got {stamp_size}.")
40 if stamp_size % 2 != 1:
41 raise ValueError(f"stamp_size must be odd number, got {stamp_size}")
42 self.sigma = float(sigma)
43 self._stamp_size = stamp_size
44 self._bounds = bounds
45 self._sigma2 = self.sigma * self.sigma
47 def __eq__(self, other: Any) -> bool:
48 if not isinstance(other, GaussianPointSpreadFunction):
49 return NotImplemented
50 if self.sigma != other.sigma:
51 return False
52 if self._stamp_size != other._stamp_size:
53 return False
54 if self._bounds != other._bounds:
55 return False
56 return True
58 def __repr__(self) -> str:
59 return (
60 f"GaussianPointSpreadFunction({self.sigma}, "
61 f"stamp_size={self._stamp_size}, bounds={self._bounds!r})"
62 )
64 @property
65 def bounds(self) -> Bounds:
66 return self._bounds
68 @cached_property
69 def kernel_bbox(self) -> Box:
70 r = self._stamp_size // 2
71 return Box.factory[-r : r + 1, -r : r + 1]
73 @cached_property
74 def _centered_coordinates(self) -> np.ndarray:
75 r = self._stamp_size // 2
76 return np.arange(-r, r + 1, dtype=np.float64)
78 @cached_property
79 def _kernel_array(self) -> np.ndarray:
80 profile = np.exp(-0.5 * np.square(self._centered_coordinates) / self._sigma2)
81 kernel = np.multiply.outer(profile, profile)
82 kernel /= kernel.sum()
83 return kernel
85 def compute_kernel_image(self, *, x: float, y: float) -> Image:
86 return Image(self._kernel_array.copy(), bbox=self.kernel_bbox)
88 def compute_stellar_image(self, *, x: float, y: float) -> Image:
89 bbox = self.compute_stellar_bbox(x=x, y=y)
90 x_profile = np.exp(-0.5 * np.square(bbox.x.arange - x) / self._sigma2)
91 y_profile = np.exp(-0.5 * np.square(bbox.y.arange - y) / self._sigma2)
92 image = np.multiply.outer(y_profile, x_profile)
93 image /= image.sum()
94 return Image(image, bbox=bbox)
96 def compute_stellar_bbox(self, *, x: float, y: float) -> Box:
97 r = self._stamp_size // 2
98 xi = round(x)
99 yi = round(y)
100 return Box.factory[yi - r : yi + r + 1, xi - r : xi + r + 1]
102 def serialize(self, archive: serialization.OutputArchive[Any]) -> GaussianPSFSerializationModel:
103 return GaussianPSFSerializationModel(
104 sigma=self.sigma, stamp_size=self._stamp_size, bounds=self._bounds.serialize()
105 )
107 @classmethod
108 def deserialize(
109 cls, model: GaussianPSFSerializationModel, archive: serialization.InputArchive[Any]
110 ) -> GaussianPointSpreadFunction:
111 return cls(sigma=model.sigma, bounds=Bounds.deserialize(model.bounds), stamp_size=model.stamp_size)
113 @staticmethod
114 def _get_archive_tree_type(
115 pointer_type: type[pydantic.BaseModel],
116 ) -> type[GaussianPSFSerializationModel]:
117 """Return the serialization model type for this object for an archive
118 type that uses the given pointer type.
119 """
120 return GaussianPSFSerializationModel
123class GaussianPSFSerializationModel(serialization.ArchiveTree):
124 sigma: float = pydantic.Field(
125 description="Gaussian sigma for the PSF.",
126 )
127 stamp_size: int = pydantic.Field(
128 description="Width of the (square) images returned by this PSF's methods."
129 )
130 bounds: SerializableBounds = pydantic.Field(
131 description="The bounds object that represents the PSF's validity region."
132 )