Coverage for python/lsst/images/_backgrounds.py: 88%

61 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-02 02:02 -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. 

11from __future__ import annotations 

12 

13__all__ = ("Background", "BackgroundMap", "BackgroundMapSerializationModel") 

14 

15import dataclasses 

16import sys 

17from collections.abc import Iterable, Iterator, Mapping 

18from typing import Any, ClassVar, cast, final 

19 

20import pydantic 

21 

22from .fields import Field, FieldSerializationModel 

23from .serialization import ArchiveTree, InputArchive, InvalidParameterError, OutputArchive 

24 

25 

26@dataclasses.dataclass(frozen=True) 

27class Background: 

28 """A named background model and optional description.""" 

29 

30 name: str 

31 """A unique name for this background.""" 

32 

33 field: Field 

34 """The actual background model itself.""" 

35 

36 description: str = "" 

37 """A description of how the background model was produced and/or how it 

38 should be used. 

39 """ 

40 

41 

42class BackgroundMap(Mapping[str, Background]): 

43 """A mapping of background models associated with an image. 

44 

45 Unlike most image characterization objects, the best background model 

46 often depends on the science case, and hence we may want to associate more 

47 than one with an image. 

48 

49 Parameters 

50 ---------- 

51 backgrounds 

52 Background models to include in the map, keyed by their name. 

53 subtracted 

54 Name of the background that has been subtracted from the image, or 

55 `None` if no background has been subtracted. 

56 """ 

57 

58 def __init__(self, backgrounds: Iterable[Background] = (), subtracted: str | None = None) -> None: 

59 self._backgrounds = {b.name: b for b in backgrounds} 

60 self._subtracted = subtracted 

61 if isinstance(self._subtracted, str) and self._subtracted not in self._backgrounds: 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true

62 raise KeyError(f"Subtracted background {self._subtracted!r} not present in map.") 

63 

64 @property 

65 def subtracted(self) -> Background | None: 

66 """The background subtracted from this image (`Background` | `None`). 

67 

68 Notes 

69 ----- 

70 If `None`, none of the backgrounds in this map were subtracted from 

71 the image. This does not necessarily mean no background at all was 

72 subtracted (e.g. in a coadd, backgrounds are generally subtracted from 

73 the input images before they are combined, and the sum of those 

74 backgrounds may not be available in a coadd background map.) 

75 """ 

76 if self._subtracted is None: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true

77 return None 

78 return self._backgrounds[self._subtracted] 

79 

80 def __iter__(self) -> Iterator[str]: 

81 return iter(self._backgrounds.keys()) 

82 

83 def __getitem__(self, key: str) -> Background: 

84 return self._backgrounds[key] 

85 

86 def __len__(self) -> int: 

87 return len(self._backgrounds) 

88 

89 if "sphinx" in sys.modules: 

90 # The Python standard library docstring is not valid reStructuredText, 

91 # but the true signature (with involves overloads) is complicated. 

92 def get[V](self, key: str, default: V | None = None) -> Background | V | None: # type: ignore 

93 """Return the background with the given key or the given default 

94 value. 

95 """ 

96 return super().get(key, default) 

97 

98 def copy(self) -> BackgroundMap: 

99 """Return a copy of the background map.""" 

100 return BackgroundMap(self.values(), self._subtracted) 

101 

102 def add(self, name: str, field: Field, description: str = "", *, is_subtracted: bool = False) -> None: 

103 """Add a new background to the map. 

104 

105 Parameters 

106 ---------- 

107 name 

108 Unique name for this background model. 

109 field 

110 The background field itself. 

111 description 

112 A description of how this background model was produced and/or how 

113 it should be used. 

114 is_subtracted 

115 Whether this background is the one that was subtracted from the 

116 image this background map is attached to. 

117 

118 Notes 

119 ----- 

120 There are no guards against ``is_subtracted=True`` being passed for 

121 multiple different backgrounds; correctness is up to the caller. Note 

122 that we only allow one background to be subtracted at once 

123 (incremental backgrounds should be modeled via `.fields.SumField`, not 

124 multiple named entries in this map). 

125 """ 

126 if name in self._backgrounds: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true

127 raise KeyError(f"A background with name {name!r} already exists.") 

128 self._backgrounds[name] = Background(name, field, description) 

129 if is_subtracted: 129 ↛ exitline 129 didn't return from function 'add' because the condition on line 129 was always true

130 self._subtracted = name 

131 

132 def serialize(self, archive: OutputArchive[Any]) -> BackgroundMapSerializationModel: 

133 """Write a background map to an archive. 

134 

135 Parameters 

136 ---------- 

137 archive 

138 Archive to write to. 

139 """ 

140 result = BackgroundMapSerializationModel(subtracted=self._subtracted) 

141 for name, background in self.items(): 

142 result.fields[name] = cast( 

143 FieldSerializationModel, 

144 archive.serialize_direct(f"fields/{name}", background.field.serialize), 

145 ) 

146 result.descriptions[name] = background.description 

147 return result 

148 

149 

150@final 

151class BackgroundMapSerializationModel(ArchiveTree): 

152 """Serialization model for background maps.""" 

153 

154 SCHEMA_NAME: ClassVar[str] = "background_map" 

155 SCHEMA_VERSION: ClassVar[str] = "1.0.0" 

156 MIN_READ_VERSION: ClassVar[int] = 1 

157 PUBLIC_TYPE: ClassVar[type] = BackgroundMap 

158 

159 fields: dict[str, FieldSerializationModel] = pydantic.Field( 

160 default_factory=dict, 

161 description="Mapping from background model name to the model field itself.", 

162 ) 

163 

164 descriptions: dict[str, str] = pydantic.Field( 

165 default_factory=dict, 

166 description="Mapping from background model name to its description.", 

167 ) 

168 

169 subtracted: str | None = pydantic.Field( 

170 default=None, 

171 description="Name of the background that was subtracted, or `None` if no background was subtracted.", 

172 ) 

173 

174 def deserialize(self, archive: InputArchive[Any], **kwargs: Any) -> BackgroundMap: 

175 if kwargs: 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true

176 raise InvalidParameterError(f"Unrecognized parameters for BackgroundMap: {set(kwargs.keys())}.") 

177 return BackgroundMap( 

178 [ 

179 Background( 

180 name=name, field=field.deserialize(archive), description=self.descriptions.get(name, "") 

181 ) 

182 for name, field in self.fields.items() 

183 ], 

184 subtracted=self.subtracted, 

185 )