Coverage for python/lsst/images/_backgrounds.py: 56%
80 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 08:38 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 08:38 +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
13__all__ = ("Background", "BackgroundMap", "BackgroundMapSerializationModel")
15import dataclasses
16import sys
17from collections.abc import Iterable, Iterator, Mapping
18from typing import Any, ClassVar, cast, final
20import pydantic
22from .describe import DescribableMixin, DescribeOptions, FieldRole, Report, ReportField
23from .fields import Field, FieldSerializationModel
24from .serialization import ArchiveTree, InputArchive, InvalidParameterError, OutputArchive
27@dataclasses.dataclass(frozen=True)
28class Background:
29 """A named background model and optional description."""
31 name: str
32 """A unique name for this background."""
34 field: Field
35 """The actual background model itself."""
37 description: str = ""
38 """A description of how the background model was produced and/or how it
39 should be used.
40 """
43class BackgroundMap(DescribableMixin, Mapping[str, Background]):
44 """A mapping of background models associated with an image.
46 Unlike most image characterization objects, the best background model
47 often depends on the science case, and hence we may want to associate more
48 than one with an image.
50 Parameters
51 ----------
52 backgrounds
53 Background models to include in the map, keyed by their name.
54 subtracted
55 Name of the background that has been subtracted from the image, or
56 `None` if no background has been subtracted.
57 """
59 def __init__(self, backgrounds: Iterable[Background] = (), subtracted: str | None = None) -> None:
60 self._backgrounds = {b.name: b for b in backgrounds}
61 self._subtracted = subtracted
62 if isinstance(self._subtracted, str) and self._subtracted not in self._backgrounds: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 raise KeyError(f"Subtracted background {self._subtracted!r} not present in map.")
65 @property
66 def subtracted(self) -> Background | None:
67 """The background subtracted from this image (`Background` | `None`).
69 Notes
70 -----
71 If `None`, none of the backgrounds in this map were subtracted from
72 the image. This does not necessarily mean no background at all was
73 subtracted (e.g. in a coadd, backgrounds are generally subtracted from
74 the input images before they are combined, and the sum of those
75 backgrounds may not be available in a coadd background map.)
76 """
77 if self._subtracted is None:
78 return None
79 return self._backgrounds[self._subtracted]
81 def __iter__(self) -> Iterator[str]:
82 return iter(self._backgrounds.keys())
84 def __getitem__(self, key: str) -> Background:
85 return self._backgrounds[key]
87 def __len__(self) -> int:
88 return len(self._backgrounds)
90 if "sphinx" in sys.modules:
91 # The Python standard library docstring is not valid reStructuredText,
92 # but the true signature (with involves overloads) is complicated.
93 def get[V](self, key: str, default: V | None = None) -> Background | V | None: # type: ignore
94 """Return the background with the given key or the given default
95 value.
96 """
97 return super().get(key, default)
99 def copy(self) -> BackgroundMap:
100 """Return a copy of the background map."""
101 return BackgroundMap(self.values(), self._subtracted)
103 def add(self, name: str, field: Field, description: str = "", *, is_subtracted: bool = False) -> None:
104 """Add a new background to the map.
106 Parameters
107 ----------
108 name
109 Unique name for this background model.
110 field
111 The background field itself.
112 description
113 A description of how this background model was produced and/or how
114 it should be used.
115 is_subtracted
116 Whether this background is the one that was subtracted from the
117 image this background map is attached to.
119 Notes
120 -----
121 There are no guards against ``is_subtracted=True`` being passed for
122 multiple different backgrounds; correctness is up to the caller. Note
123 that we only allow one background to be subtracted at once
124 (incremental backgrounds should be modeled via `.fields.SumField`, not
125 multiple named entries in this map).
126 """
127 if name in self._backgrounds: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 raise KeyError(f"A background with name {name!r} already exists.")
129 self._backgrounds[name] = Background(name, field, description)
130 if is_subtracted:
131 self._subtracted = name
133 def _describe(self, options: DescribeOptions = DescribeOptions(), /) -> Report:
134 """Return a `Report` describing this background map.
136 Parameters
137 ----------
138 options : `DescribeOptions`, optional
139 Rendering options; forwarded to the background models.
141 Notes
142 -----
143 The report is ``inline``, so a composite that holds a map shows only
144 the summary line. The children below are what a map describes on its
145 own, where the background models themselves are the point.
146 """
147 subtracted_name = self._subtracted
148 if not self._backgrounds:
149 summary = "no backgrounds"
150 else:
151 summary = ", ".join(
152 f"{name} (subtracted)" if name == subtracted_name else name for name in self._backgrounds
153 )
154 children: dict[str, Report] = {}
155 if not options.brief:
156 child = options.for_child()
157 for name, background in self._backgrounds.items():
158 # The model builds its own report; putting the background's
159 # attributes at the top of it keeps them beside the model they
160 # describe, rather than behind another level of nesting.
161 report = background.field._describe(child)
162 attributes = []
163 if name == subtracted_name:
164 attributes.append(ReportField(label="subtracted", value="yes", role=FieldRole.DERIVED))
165 if background.description:
166 attributes.append(
167 ReportField(
168 label="description",
169 value=background.description,
170 role=FieldRole.DERIVED,
171 )
172 )
173 report.fields[:0] = attributes
174 children[name] = report
175 return Report(
176 type_name="BackgroundMap",
177 summary=summary,
178 children=children,
179 inline=True,
180 )
182 def serialize(self, archive: OutputArchive[Any]) -> BackgroundMapSerializationModel:
183 """Write a background map to an archive.
185 Parameters
186 ----------
187 archive
188 Archive to write to.
189 """
190 result = BackgroundMapSerializationModel(subtracted=self._subtracted)
191 for name, background in self.items():
192 result.fields[name] = cast(
193 FieldSerializationModel,
194 archive.serialize_direct(f"fields/{name}", background.field.serialize),
195 )
196 result.descriptions[name] = background.description
197 return result
200@final
201class BackgroundMapSerializationModel(ArchiveTree):
202 """Serialization model for background maps."""
204 SCHEMA_NAME: ClassVar[str] = "background_map"
205 SCHEMA_VERSION: ClassVar[str] = "1.0.0"
206 MIN_READ_VERSION: ClassVar[int] = 1
207 PUBLIC_TYPE: ClassVar[type] = BackgroundMap
209 fields: dict[str, FieldSerializationModel] = pydantic.Field(
210 default_factory=dict,
211 description="Mapping from background model name to the model field itself.",
212 )
214 descriptions: dict[str, str] = pydantic.Field(
215 default_factory=dict,
216 description="Mapping from background model name to its description.",
217 )
219 subtracted: str | None = pydantic.Field(
220 default=None,
221 description="Name of the background that was subtracted, or None if no background was subtracted.",
222 )
224 def deserialize(self, archive: InputArchive[Any], **kwargs: Any) -> BackgroundMap:
225 if kwargs: 225 ↛ 226line 225 didn't jump to line 226 because the condition on line 225 was never true
226 raise InvalidParameterError(f"Unrecognized parameters for BackgroundMap: {set(kwargs.keys())}.")
227 return BackgroundMap(
228 [
229 Background(
230 name=name, field=field.deserialize(archive), description=self.descriptions.get(name, "")
231 )
232 for name, field in self.fields.items()
233 ],
234 subtracted=self.subtracted,
235 )