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

60 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-03 08:10 +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. 

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 

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

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

52 self._subtracted = subtracted 

53 if isinstance(self._subtracted, str) and self._subtracted not in self._backgrounds: 

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

55 

56 @property 

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

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

59 

60 Notes 

61 ----- 

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

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

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

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

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

67 """ 

68 if self._subtracted is None: 

69 return None 

70 return self._backgrounds[self._subtracted] 

71 

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

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

74 

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

76 return self._backgrounds[key] 

77 

78 def __len__(self) -> int: 

79 return len(self._backgrounds) 

80 

81 if "sphinx" in sys.modules: 

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

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

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

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

86 value. 

87 """ 

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

89 

90 def copy(self) -> BackgroundMap: 

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

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

93 

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

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

96 

97 Parameters 

98 ---------- 

99 name 

100 Unique name for this background model. 

101 field 

102 The background field itself. 

103 description 

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

105 it should be used. 

106 is_subtracted 

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

108 image this background map is attached to. 

109 

110 Notes 

111 ----- 

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

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

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

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

116 multiple named entries in this map). 

117 """ 

118 if name in self._backgrounds: 

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

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

121 if is_subtracted: 

122 self._subtracted = name 

123 

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

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

126 result = BackgroundMapSerializationModel(subtracted=self._subtracted) 

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

128 result.fields[name] = cast( 

129 FieldSerializationModel, 

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

131 ) 

132 result.descriptions[name] = background.description 

133 return result 

134 

135 

136@final 

137class BackgroundMapSerializationModel(ArchiveTree): 

138 """Serialization model for background maps.""" 

139 

140 SCHEMA_NAME: ClassVar[str] = "background_map" 

141 SCHEMA_VERSION: ClassVar[str] = "1.0.0" 

142 MIN_READ_VERSION: ClassVar[int] = 1 

143 

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

145 default_factory=dict, 

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

147 ) 

148 

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

150 default_factory=dict, 

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

152 ) 

153 

154 subtracted: str | None = pydantic.Field( 

155 default=None, 

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

157 ) 

158 

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

160 if kwargs: 

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

162 return BackgroundMap( 

163 [ 

164 Background( 

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

166 ) 

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

168 ], 

169 subtracted=self.subtracted, 

170 )