Coverage for python / lsst / images / fields / _base.py: 56%

49 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-28 09:01 +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__ = ("BaseField",) 

15 

16from abc import ABC, abstractmethod 

17from typing import Literal, Self, overload 

18 

19import astropy.units 

20import numpy as np 

21 

22from .._geom import Bounds, Box 

23from .._image import Image 

24 

25 

26class BaseField(ABC): 

27 """An abstract base class for parametric or interpolated 2-d functions, 

28 generally representing some sort of calculated image. 

29 

30 Notes 

31 ----- 

32 The field hierarchy is closed to the types in this package, so we can 

33 enumerate all of the serializations and avoid any kind of extension system. 

34 All field types are immutable. 

35 

36 Field types implement the function call operator and both multiplication 

37 and division by a constant via operator overloading. See the named 

38 `evaluate` and `multiply_constant` methods (respectively) for more 

39 information about those operations. 

40 

41 This interface will probably change in the future to incorporate options 

42 for dealing with out-of-bounds positions. At present the behavior for 

43 such positions is implementation-specific and should not be relied upon. 

44 """ 

45 

46 @property 

47 @abstractmethod 

48 def bounds(self) -> Bounds: 

49 """The region over which this field can be evaluated (`.Bounds`).""" 

50 raise NotImplementedError() 

51 

52 @property 

53 @abstractmethod 

54 def unit(self) -> astropy.units.UnitBase | None: 

55 """The units of the field (`astropy.units.UnitBase` or `None`).""" 

56 raise NotImplementedError() 

57 

58 @overload 

59 def __call__(self, *, x: np.ndarray, y: np.ndarray, quantity: Literal[False] = False) -> np.ndarray: ... 59 ↛ exitline 59 didn't return from function '__call__' because

60 

61 @overload 

62 def __call__( 62 ↛ exitline 62 didn't return from function '__call__' because

63 self, *, x: np.ndarray, y: np.ndarray, quantity: Literal[True] 

64 ) -> astropy.units.Quantity: ... 

65 

66 @overload 

67 def __call__( 67 ↛ exitline 67 didn't return from function '__call__' because

68 self, *, x: np.ndarray, y: np.ndarray, quantity: bool 

69 ) -> np.ndarray | astropy.units.Quantity: ... 

70 

71 def __call__( 

72 self, *, x: np.ndarray, y: np.ndarray, quantity: bool = False 

73 ) -> np.ndarray | astropy.units.Quantity: 

74 return self.evaluate(x=x, y=y, quantity=quantity) 

75 

76 @abstractmethod 

77 def render( 

78 self, 

79 bbox: Box | None = None, 

80 *, 

81 dtype: np.typing.DTypeLike | None = None, 

82 ) -> Image: 

83 """Create an image realization of the field. 

84 

85 Parameters 

86 ---------- 

87 bbox 

88 Bounding box of the image. If not provided, ``self.bounds.bbox`` 

89 will be used. 

90 dtype 

91 Pixel data type for the returned image. 

92 """ 

93 raise NotImplementedError() 

94 

95 def __mul__(self, factor: float | astropy.units.Quantity | astropy.units.UnitBase) -> Self: 

96 return self.multiply_constant(factor) 

97 

98 def __rmul__(self, factor: float | astropy.units.Quantity | astropy.units.UnitBase) -> Self: 

99 return self.multiply_constant(factor) 

100 

101 def __truediv__(self, factor: float | astropy.units.Quantity | astropy.units.UnitBase) -> Self: 

102 return self.multiply_constant(1.0 / factor) 

103 

104 @abstractmethod 

105 def evaluate( 

106 self, *, x: np.ndarray, y: np.ndarray, quantity: bool 

107 ) -> np.ndarray | astropy.units.Quantity: 

108 """Evaluate at non-gridded points. 

109 

110 Parameters 

111 ---------- 

112 x 

113 X coordinates to evaluate at. 

114 y 

115 Y coordinates to evaluate at; must be broadcast-compatible with 

116 ``x``. 

117 quantity 

118 If `True`, return an `astropy.units.Quantity` instead of a 

119 `numpy.ndarray`. If `unit` is `None`, the returned object will 

120 be a dimensionless `~astropy.units.Quantity`. 

121 """ 

122 raise NotImplementedError() 

123 

124 @abstractmethod 

125 def multiply_constant(self, factor: float | astropy.units.Quantity | astropy.units.UnitBase) -> Self: 

126 """Multiply by a constant, returning a new field of the same type. 

127 

128 Parameters 

129 ---------- 

130 factor 

131 Factor to multiply by. When this has units, those should multiply 

132 ``self.unit`` or set the units of the returned field if 

133 ``self.unit is None``. 

134 """ 

135 raise NotImplementedError() 

136 

137 def _handle_factor_units( 

138 self, factor: float | astropy.units.Quantity | astropy.units.UnitBase 

139 ) -> tuple[float, astropy.units.UnitBase | None]: 

140 """Interpret the ``factor`` argument to `multiply_constant` and apply 

141 any units it carries to this field's units. 

142 

143 This is a convenience function for subclass implementations of 

144 `multiply_constant`. 

145 

146 Parameters 

147 ---------- 

148 factor 

149 Factor passed by the caller. 

150 

151 Returns 

152 ------- 

153 `float` 

154 The factor to multiply by as a pure `float` 

155 `astropy.units.UnitBase` | `None` 

156 The units for the new field returned by `multiply_constant`. 

157 """ 

158 unit = self.unit 

159 factor_unit = None 

160 if isinstance(factor, astropy.units.Quantity): 

161 factor_unit = factor.unit 

162 factor = factor.to_value() 

163 elif isinstance(factor, astropy.units.UnitBase): 

164 factor_unit = factor 

165 factor = 1.0 

166 if factor_unit is not None: 

167 if unit is None: 

168 unit = factor_unit 

169 else: 

170 unit *= factor_unit 

171 return factor, unit