Coverage for python/lsst/images/psfs/_gaussian.py: 50%
85 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 08:38 +0000
« 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.
12from __future__ import annotations
14__all__ = (
15 "GaussianPSFSerializationModel",
16 "GaussianPointSpreadFunction",
17)
19from functools import cached_property
20from typing import Any, ClassVar
22import numpy as np
23import pydantic
25from lsst.images._image import Image
27from .. import serialization
28from .._concrete_bounds import BoundsSerializationModel
29from .._geom import Bounds, Box
30from ..describe import DescribeOptions, Report, ReportField
31from ..utils import round_half_up
32from ._base import PointSpreadFunction
35class GaussianPointSpreadFunction(PointSpreadFunction):
36 """A PSF with a spatially-invariant circular Gaussian profile.
38 Parameters
39 ----------
40 sigma
41 Standard deviation of the Gaussian profile in pixels.
42 bounds
43 The pixel-coordinate region where the model can safely be
44 evaluated.
45 stamp_size
46 Side length in pixels of the PSF image stamps; must be a positive
47 odd number.
48 """
50 def __init__(self, sigma: float, bounds: Bounds, stamp_size: int) -> None:
51 if sigma <= 0:
52 raise ValueError(f"sigma must be positive; got {sigma}.")
53 if stamp_size <= 0:
54 raise ValueError(f"stamp_size must be positive; got {stamp_size}.")
55 if stamp_size % 2 != 1:
56 raise ValueError(f"stamp_size must be odd number, got {stamp_size}")
57 self.sigma = float(sigma)
58 self._stamp_size = stamp_size
59 self._bounds = bounds
60 self._sigma2 = self.sigma * self.sigma
62 def __eq__(self, other: Any) -> bool:
63 if not isinstance(other, GaussianPointSpreadFunction): 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true
64 return NotImplemented
65 if self.sigma != other.sigma: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true
66 return False
67 if self._stamp_size != other._stamp_size: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true
68 return False
69 if self._bounds != other._bounds: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true
70 return False
71 return True
73 def _describe(self, options: DescribeOptions = DescribeOptions(), /) -> Report:
74 """Return a `Report` describing this Gaussian PSF.
76 Parameters
77 ----------
78 options : `DescribeOptions`, optional
79 Unused; accepted for interface compatibility.
80 """
81 return Report(
82 type_name="GaussianPointSpreadFunction",
83 summary=f"Gaussian PSF sigma={self.sigma}",
84 fields=[
85 ReportField(label="sigma", value=self.sigma, positional=True),
86 ReportField(label="stamp_size", value=self._stamp_size),
87 ReportField(label="bounds", value=self._bounds, repr_value=repr(self._bounds)),
88 ],
89 )
91 @property
92 def bounds(self) -> Bounds:
93 return self._bounds
95 @cached_property
96 def kernel_bbox(self) -> Box:
97 r = self._stamp_size // 2
98 return Box.factory[-r : r + 1, -r : r + 1]
100 @cached_property
101 def _centered_coordinates(self) -> np.ndarray:
102 r = self._stamp_size // 2
103 return np.arange(-r, r + 1, dtype=np.float64)
105 @cached_property
106 def _kernel_array(self) -> np.ndarray:
107 profile = np.exp(-0.5 * np.square(self._centered_coordinates) / self._sigma2)
108 kernel = np.multiply.outer(profile, profile)
109 kernel /= kernel.sum()
110 return kernel
112 def compute_kernel_image(self, *, x: float, y: float) -> Image:
113 return Image(self._kernel_array.copy(), bbox=self.kernel_bbox)
115 def compute_stellar_image(self, *, x: float, y: float) -> Image:
116 bbox = self.compute_stellar_bbox(x=x, y=y)
117 x_profile = np.exp(-0.5 * np.square(bbox.x.arange - x) / self._sigma2)
118 y_profile = np.exp(-0.5 * np.square(bbox.y.arange - y) / self._sigma2)
119 image = np.multiply.outer(y_profile, x_profile)
120 image /= image.sum()
121 return Image(image, bbox=bbox)
123 def compute_stellar_bbox(self, *, x: float, y: float) -> Box:
124 r = self._stamp_size // 2
125 xi = round_half_up(x)
126 yi = round_half_up(y)
127 return Box.factory[yi - r : yi + r + 1, xi - r : xi + r + 1]
129 def serialize(self, archive: serialization.OutputArchive[Any]) -> GaussianPSFSerializationModel:
130 return GaussianPSFSerializationModel(
131 sigma=self.sigma, stamp_size=self._stamp_size, bounds=self._bounds.serialize()
132 )
134 @staticmethod
135 def _get_archive_tree_type(
136 pointer_type: type[pydantic.BaseModel],
137 ) -> type[GaussianPSFSerializationModel]:
138 """Return the serialization model type for this object for an archive
139 type that uses the given pointer type.
140 """
141 return GaussianPSFSerializationModel
144class GaussianPSFSerializationModel(serialization.ArchiveTree):
145 SCHEMA_NAME: ClassVar[str] = "gaussian_psf"
146 SCHEMA_VERSION: ClassVar[str] = "1.0.0.dev0"
147 MIN_READ_VERSION: ClassVar[int] = 1
148 PUBLIC_TYPE: ClassVar[type] = GaussianPointSpreadFunction
150 sigma: float = pydantic.Field(
151 description="Gaussian sigma for the PSF.",
152 )
153 stamp_size: int = pydantic.Field(
154 description="Width of the (square) images returned by this PSF's methods."
155 )
156 bounds: BoundsSerializationModel = pydantic.Field(
157 description="The bounds object that represents the PSF's validity region."
158 )
160 def deserialize(
161 self, archive: serialization.InputArchive[Any], **kwargs: Any
162 ) -> GaussianPointSpreadFunction:
163 if kwargs: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true
164 raise serialization.InvalidParameterError(
165 f"Unrecognized parameters for GaussianPointSpreadFunction: {set(kwargs.keys())}."
166 )
167 return GaussianPointSpreadFunction(
168 sigma=self.sigma, bounds=self.bounds.deserialize(), stamp_size=self.stamp_size
169 )