Coverage for python/lsst/images/_backgrounds.py: 88%
61 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 02:03 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 02:03 -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
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 .fields import Field, FieldSerializationModel
23from .serialization import ArchiveTree, InputArchive, InvalidParameterError, OutputArchive
26@dataclasses.dataclass(frozen=True)
27class Background:
28 """A named background model and optional description."""
30 name: str
31 """A unique name for this background."""
33 field: Field
34 """The actual background model itself."""
36 description: str = ""
37 """A description of how the background model was produced and/or how it
38 should be used.
39 """
42class BackgroundMap(Mapping[str, Background]):
43 """A mapping of background models associated with an image.
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.
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 """
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.")
64 @property
65 def subtracted(self) -> Background | None:
66 """The background subtracted from this image (`Background` | `None`).
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]
80 def __iter__(self) -> Iterator[str]:
81 return iter(self._backgrounds.keys())
83 def __getitem__(self, key: str) -> Background:
84 return self._backgrounds[key]
86 def __len__(self) -> int:
87 return len(self._backgrounds)
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)
98 def copy(self) -> BackgroundMap:
99 """Return a copy of the background map."""
100 return BackgroundMap(self.values(), self._subtracted)
102 def add(self, name: str, field: Field, description: str = "", *, is_subtracted: bool = False) -> None:
103 """Add a new background to the map.
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.
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
132 def serialize(self, archive: OutputArchive[Any]) -> BackgroundMapSerializationModel:
133 """Write a background map to an archive.
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
150@final
151class BackgroundMapSerializationModel(ArchiveTree):
152 """Serialization model for background maps."""
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
159 fields: dict[str, FieldSerializationModel] = pydantic.Field(
160 default_factory=dict,
161 description="Mapping from background model name to the model field itself.",
162 )
164 descriptions: dict[str, str] = pydantic.Field(
165 default_factory=dict,
166 description="Mapping from background model name to its description.",
167 )
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 )
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 )