Coverage for python/lsst/images/_backgrounds.py: 50%
60 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 08:08 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 08:08 +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 .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.
48 """
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.")
56 @property
57 def subtracted(self) -> Background | None:
58 """The background subtracted from this image (`Background` | `None`).
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]
72 def __iter__(self) -> Iterator[str]:
73 return iter(self._backgrounds.keys())
75 def __getitem__(self, key: str) -> Background:
76 return self._backgrounds[key]
78 def __len__(self) -> int:
79 return len(self._backgrounds)
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)
90 def copy(self) -> BackgroundMap:
91 """Return a copy of the background map."""
92 return BackgroundMap(self.values(), self._subtracted)
94 def add(self, name: str, field: Field, description: str = "", *, is_subtracted: bool = False) -> None:
95 """Add a new background to the map.
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.
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
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
136@final
137class BackgroundMapSerializationModel(ArchiveTree):
138 """Serialization model for background maps."""
140 SCHEMA_NAME: ClassVar[str] = "background_map"
141 SCHEMA_VERSION: ClassVar[str] = "1.0.0"
142 MIN_READ_VERSION: ClassVar[int] = 1
144 fields: dict[str, FieldSerializationModel] = pydantic.Field(
145 default_factory=dict,
146 description="Mapping from background model name to the model field itself.",
147 )
149 descriptions: dict[str, str] = pydantic.Field(
150 default_factory=dict,
151 description="Mapping from background model name to its description.",
152 )
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 )
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 )