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