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