Coverage for python / lsst / images / fields / _sum.py: 33%
75 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 08:47 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-21 08:47 +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.
12from __future__ import annotations
14__all__ = ("SumField", "SumFieldSerializationModel")
16from collections.abc import Iterable
17from typing import TYPE_CHECKING, Any, Literal, final
19import astropy.units
20import numpy as np
21import pydantic
23from .._geom import Bounds, Box
24from .._image import Image
25from ..serialization import ArchiveTree, InputArchive, InvalidParameterError, OutputArchive
26from ._base import BaseField
28if TYPE_CHECKING:
29 try:
30 from lsst.afw.math import BackgroundList as LegacyBackgroundList
31 except ImportError:
32 type LegacyBackgroundList = Any # type: ignore[no-redef]
34 from ._concrete import Field, FieldSerializationModel
37@final
38class SumField(BaseField):
39 """A field that sums other fields lazily.
41 Parameters
42 ----------
43 operands : `~collections.abc.Iterable` [ `BaseField` ]
44 The fields to sum together.
45 """
47 def __init__(self, operands: Iterable[Field]):
48 self._operands = tuple(operands)
49 if not self._operands:
50 raise ValueError("At least one operand must be provided.")
51 iterator = iter(self._operands)
52 first = next(iterator)
53 self._bounds = first.bounds
54 self._unit = first.unit
55 for operand in iterator:
56 self._bounds = self._bounds.intersection(operand.bounds)
57 if operand.unit is None:
58 if self._unit is not None:
59 raise astropy.units.UnitConversionError(
60 "Cannot add a field with no units to a field with units."
61 )
62 elif self._unit is None:
63 raise astropy.units.UnitConversionError(
64 "Cannot add a field with units to a field with no units."
65 )
66 else:
67 # Raise if these units are not sum-compatible.
68 self._unit.to(operand.unit)
70 @property
71 def bounds(self) -> Bounds:
72 return self._bounds
74 @property
75 def unit(self) -> astropy.units.UnitBase | None:
76 return self._unit
78 @property
79 def operands(self) -> tuple[Field, ...]:
80 """The fields that are summed together (`tuple` [`BaseField`, ...])."""
81 return self._operands
83 def evaluate(
84 self, *, x: np.ndarray, y: np.ndarray, quantity: bool = False
85 ) -> np.ndarray | astropy.units.Quantity:
86 iterator = iter(self._operands)
87 first = next(iterator)
88 # We have to add quantities if this is a unit-aware field, as the
89 # terms in the sum might have different-but-compatible units.
90 result = first(x=x, y=y, quantity=(self.unit is not None))
91 for operand in iterator:
92 result += operand(x=x, y=y, quantity=(self.unit is not None))
93 if self.unit is not None and not quantity:
94 # Caller doesn't want a Quantity back.
95 assert isinstance(result, astropy.units.Quantity)
96 return result.to_value(self.unit)
97 if self.unit is None and quantity:
98 # Caller wants a Quantity back even though there's no units.
99 return astropy.units.Quantity(result)
100 return result
102 def render(self, bbox: Box | None = None, *, dtype: np.typing.DTypeLike | None = None) -> Image:
103 if bbox is None:
104 bbox = self.bounds.bbox
105 result = Image(0.0, bbox=bbox, dtype=dtype, unit=self.unit)
106 for operand in self._operands:
107 result.quantity += operand.render(bbox, dtype=dtype).quantity
108 return result
110 def multiply_constant(self, factor: float | astropy.units.Quantity | astropy.units.UnitBase) -> SumField:
111 return SumField([operand * factor for operand in self._operands])
113 def serialize(self, archive: OutputArchive[Any]) -> SumFieldSerializationModel:
114 """Serialize the field to an output archive."""
115 return SumFieldSerializationModel(operands=[operand.serialize(archive) for operand in self._operands])
117 @staticmethod
118 def _get_archive_tree_type(
119 pointer_type: type[Any],
120 ) -> type[SumFieldSerializationModel]:
121 """Return the serialization model type for this object for an archive
122 type that uses the given pointer type.
123 """
124 return SumFieldSerializationModel
126 @staticmethod
127 def from_legacy_background(
128 legacy_background: LegacyBackgroundList,
129 bounds: Bounds | None = None,
130 unit: astropy.units.UnitBase | None = None,
131 ) -> SumField:
132 """Convert from a legacy `lsst.afw.math.BackgroundList` instance.
134 Parameters
135 ----------
136 legacy
137 Legacy background object to convert.
138 bounds
139 The bounds of the returned field, if they should be different from
140 the bounding box of ``legacy_background``.
141 unit
142 The units of the returned field (`lsst.afw.math.BackgroundList`
143 objects do not know their units).
144 """
145 from ._concrete import field_from_legacy_background
147 return SumField(
148 [field_from_legacy_background(b, bounds=bounds, unit=unit) for b, *_ in legacy_background]
149 )
152class SumFieldSerializationModel(ArchiveTree):
153 """Serialization model for `SumField`."""
155 operands: list[FieldSerializationModel] = pydantic.Field(default_factory=list)
157 field_type: Literal["SUM"] = "SUM"
159 def deserialize(self, archive: InputArchive, **kwargs: Any) -> SumField:
160 """Deserialize the field from an input archive."""
161 if kwargs:
162 raise InvalidParameterError(f"Unrecognized parameters for SumField: {set(kwargs.keys())}.")
163 return SumField([operand.deserialize(archive) for operand in self.operands])