Coverage for python/lsst/images/fields/_concrete.py: 35%

39 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-29 08:40 +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__ = ( 

15 "Field", 

16 "FieldSerializationModel", 

17 "field_from_legacy", 

18 "field_from_legacy_background", 

19 "field_from_legacy_photo_calib", 

20) 

21 

22from typing import TYPE_CHECKING, Annotated, Any 

23 

24import astropy.units 

25import numpy as np 

26import pydantic 

27 

28from .._geom import Bounds 

29from ._chebyshev import ChebyshevField, ChebyshevFieldSerializationModel 

30from ._product import ProductField, ProductFieldSerializationModel 

31from ._spline import SplineField, SplineFieldSerializationModel 

32from ._sum import SumField, SumFieldSerializationModel 

33 

34if TYPE_CHECKING: 

35 try: 

36 from lsst.afw.image import PhotoCalib as LegacyPhotoCalib 

37 from lsst.afw.math import BackgroundList as LegacyBackgroundList 

38 from lsst.afw.math import BackgroundMI as LegacyBackground 

39 from lsst.afw.math import BoundedField as LegacyBoundedField 

40 except ImportError: 

41 type LegacyBoundedField = Any # type: ignore[no-redef] 

42 type LegacyBackground = Any # type: ignore[no-redef] 

43 type LegacyBackgroundList = Any # type: ignore[no-redef] 

44 type LegacyPhotoCalib = Any # type: ignore[no-redef] 

45 

46 

47# Since Sphinx can't handle doc links to type aliases, whenever we annotate 

48# a type as `Field`, we override the docs to say `BaseField`, since 

49# `BaseField` is a base class that serves as a much more useful doc link, and 

50# because the hierarchy is closed they're equivalent. But we have to use 

51# `Field` in the type annotations because there's no way to declare to MyPy 

52# et all that the hierarchy is closed. 

53 

54type Field = ChebyshevField | ProductField | SplineField | SumField 

55type FieldSerializationModel = Annotated[ 

56 ChebyshevFieldSerializationModel 

57 | ProductFieldSerializationModel 

58 | SplineFieldSerializationModel 

59 | SumFieldSerializationModel, 

60 pydantic.Field(discriminator="field_type"), 

61] 

62 

63 

64ProductFieldSerializationModel.model_rebuild() 

65SumFieldSerializationModel.model_rebuild() 

66 

67 

68def field_from_legacy( 

69 legacy_bounded_field: LegacyBoundedField, 

70 bounds: Bounds | None = None, 

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

72) -> Field: 

73 """Convert a legacy `lsst.afw.math.BoundedField` subclass to a `BaseField` 

74 object. 

75 

76 Parameters 

77 ---------- 

78 legacy_bounded_field 

79 Legacy field to convert. 

80 bounds 

81 The bounds of the returned field, if they should be different from 

82 the bounding box of ``legacy``. 

83 unit 

84 The units of the returned field (`lsst.afw.math.BoundedField` 

85 objects do not know their units). 

86 """ 

87 from lsst.afw.math import ChebyshevBoundedField, ProductBoundedField 

88 

89 match legacy_bounded_field: 

90 case ChebyshevBoundedField(): 

91 return ChebyshevField.from_legacy(legacy_bounded_field, unit=unit, bounds=bounds) 

92 case ProductBoundedField(): 

93 return ProductField.from_legacy(legacy_bounded_field, unit=unit, bounds=bounds) 

94 case _: 

95 raise NotImplementedError( 

96 f"Conversion from {type(legacy_bounded_field).__name__} is not supported." 

97 ) 

98 

99 

100def field_from_legacy_background( 

101 legacy_background: LegacyBackground | LegacyBackgroundList, 

102 bounds: Bounds | None = None, 

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

104) -> Field: 

105 """Convert a legacy `lsst.afw.math.Background` or 

106 `lsst.afw.math.BackgroundList` instance to a `BaseField` object. 

107 

108 Parameters 

109 ---------- 

110 legacy_background 

111 Legacy background object to convert. 

112 bounds 

113 The bounds of the returned field, if they should be different from 

114 the bounding box of ``legacy_background``. 

115 unit 

116 The units of the returned field (`lsst.afw.math.Background` 

117 objects do not know their units). 

118 """ 

119 from lsst.afw.math import ApproximateControl, BackgroundList 

120 

121 if isinstance(legacy_background, BackgroundList): 

122 return SumField.from_legacy_background(legacy_background) 

123 

124 approx_control = legacy_background.getBackgroundControl().getApproximateControl() 

125 if approx_control.getStyle() == ApproximateControl.UNKNOWN: 

126 return SplineField.from_legacy_background(legacy_background, unit=unit) 

127 else: 

128 return ChebyshevField.from_legacy_background(legacy_background, unit=unit) 

129 

130 

131def field_from_legacy_photo_calib( 

132 legacy_photo_calib: LegacyPhotoCalib, 

133 bounds: Bounds, 

134 instrumental_unit: astropy.units.UnitBase = astropy.units.electron, 

135) -> Field | None: 

136 """Convert a legacy `lsst.afw.image.PhotoCalib` into a `BaseField` object. 

137 

138 Parameters 

139 ---------- 

140 legacy_photo_calib 

141 Calibration object to convert. 

142 bounds 

143 Bounds of the returned field. 

144 instrumental_unit 

145 The instrumental units the legacy calibration transforms from. These 

146 will be used as the denominator of the units of the returned field, 

147 with ``astropy.units.nJy`` as the numerator. 

148 

149 Returns 

150 ------- 

151 `BaseField` | `None` 

152 A field that transforms instrumental units to ``nJy``, or `None` if 

153 the given calibration object was an identity mapping for a legacy 

154 image that already had ``nJy`` pixels. 

155 """ 

156 calibration_mean = legacy_photo_calib.getCalibrationMean() 

157 if legacy_photo_calib._isConstant: 

158 if calibration_mean == 1.0: 

159 # This image's pixels have been calibrated to nJy 

160 # already, which means the calibration *from* post-ISR 

161 # electrons that we want is stored elsewhere. 

162 return None 

163 else: 

164 return ChebyshevField( 

165 bounds, 

166 np.array([[calibration_mean]], dtype=np.float64), 

167 unit=astropy.units.nJy / instrumental_unit, 

168 ) 

169 else: 

170 normalized_field = field_from_legacy( 

171 legacy_photo_calib.computeScaledCalibration(), 

172 unit=astropy.units.nJy / instrumental_unit, 

173 bounds=bounds, 

174 ) 

175 return normalized_field * calibration_mean