Coverage for python / lsst / obs / base / filters.py: 54%

56 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-24 08:20 +0000

1# This file is part of obs_base. 

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# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22"""Classes to allow obs packages to define the filters used by an Instrument 

23and for use by `lsst.afw.image.Filter`, gen2 dataIds, and gen3 Dimensions. 

24""" 

25 

26from __future__ import annotations 

27 

28__all__ = ("FilterDefinition", "FilterDefinitionCollection") 

29 

30import dataclasses 

31from collections.abc import Sequence, Set 

32from typing import Any, overload 

33 

34import lsst.afw.image.utils 

35 

36 

37@dataclasses.dataclass(frozen=True) 

38class FilterDefinition: 

39 """The definition of an instrument's filter bandpass. 

40 

41 This class is used to declare ``physical_filter`` and ``band`` 

42 information for an instrument. 

43 

44 This class is likely temporary, until we have a better versioned filter 

45 definition system that includes complete transmission information. 

46 """ 

47 

48 physical_filter: str 

49 """The name of a filter associated with a particular instrument. 

50 

51 Unique for each piece of glass. This should match the exact filter name 

52 used in the observatory's metadata. 

53 This name is used to define the ``physical_filter`` Butler Dimension. 

54 

55 If neither `band` nor `afw_name` is defined, this is used 

56 as the `~lsst.afw.image.FilterLabel` ``name``, otherwise it is added to the 

57 list of `~lsst.afw.image.FilterLabel` aliases. 

58 """ 

59 

60 band: str | None = None 

61 """The generic name of a filter not associated with a particular instrument 

62 (e.g. ``r`` for the SDSS Gunn r-band, which could be on SDSS, LSST, or 

63 HSC). 

64 

65 Not all filters have an abstract filter: engineering or test filters may 

66 not have a generically-termed filter name. 

67 

68 If specified and if `afw_name` is None, this is used as the 

69 `~lsst.afw.image.FilterLabel` ``name`` field, otherwise it is added to the 

70 list of `~lsst.afw.image.FilterLabel` aliases. 

71 """ 

72 

73 doc: str | None = None 

74 """A short description of this filter, possibly with a link to more 

75 information. 

76 """ 

77 

78 afw_name: str | None = None 

79 """If not None, the name of the `~lsst.afw.image.Filter` object. 

80 

81 This is distinct from physical_filter and band to maintain 

82 backwards compatibility in some obs packages. 

83 For example, for HSC there are two distinct ``r`` and ``i`` filters, named 

84 ``r/r2`` and ``i/i2``. 

85 """ 

86 

87 alias: Set[str] = frozenset() 

88 """Alternate names for this filter. These are added to the 

89 `~lsst.afw.image.Filter` alias list. 

90 """ 

91 

92 def __post_init__(self) -> None: 

93 # force alias to be immutable, so that hashing works 

94 if not isinstance(self.alias, frozenset): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true

95 object.__setattr__(self, "alias", frozenset(self.alias)) 

96 

97 def __str__(self) -> str: 

98 txt = f"FilterDefinition(physical_filter='{self.physical_filter}'" 

99 if self.band is not None: 

100 txt += f", band='{self.band}'" 

101 if self.afw_name is not None: 

102 txt += f", afw_name='{self.afw_name}'" 

103 if len(self.alias) != 0: 

104 txt += f", alias='{self.alias}'" 

105 return txt + ")" 

106 

107 def makeFilterLabel(self) -> lsst.afw.image.FilterLabel: 

108 """Create a complete FilterLabel for this filter.""" 

109 return lsst.afw.image.FilterLabel(band=self.band, physical=self.physical_filter) 

110 

111 

112class FilterDefinitionCollection(Sequence[FilterDefinition]): 

113 """An order-preserving collection of multiple `FilterDefinition`. 

114 

115 Parameters 

116 ---------- 

117 *filters : `FilterDefinition` 

118 The filters in this collection. 

119 """ 

120 

121 physical_to_band: dict[str, str | None] 

122 """A mapping from physical filter name to band name. 

123 This is a convenience feature to allow file readers to create a FilterLabel 

124 when reading a raw file that only has a physical filter name, without 

125 iterating over the entire collection. 

126 """ 

127 

128 def __init__(self, *filters: FilterDefinition): 

129 self._filters = list(filters) 

130 self.physical_to_band = {filter.physical_filter: filter.band for filter in self._filters} 

131 

132 @overload 

133 def __getitem__(self, i: int) -> FilterDefinition: 

134 pass 

135 

136 @overload 

137 def __getitem__(self, s: slice) -> Sequence[FilterDefinition]: 

138 pass 

139 

140 def __getitem__(self, index: Any) -> Any: 

141 return self._filters[index] 

142 

143 def __len__(self) -> int: 

144 return len(self._filters) 

145 

146 def __str__(self) -> str: 

147 return "FilterDefinitions(" + ", ".join(str(f) for f in self._filters) + ")" 

148 

149 def findAll(self, name: str) -> set[FilterDefinition]: 

150 """Return the FilterDefinitions that match a particular name. 

151 

152 This method makes no attempt to prioritize, e.g., band names over 

153 physical filter names; any definition that makes *any* reference 

154 to the name is returned. 

155 

156 Parameters 

157 ---------- 

158 name : `str` 

159 The name to search for. May be any band, physical, or alias name. 

160 

161 Returns 

162 ------- 

163 matches : `set` [`FilterDefinition`] 

164 All FilterDefinitions containing ``name`` as one of their 

165 filter names. 

166 """ 

167 matches = set() 

168 for filter in self._filters: 

169 if ( 

170 name == filter.physical_filter 

171 or name == filter.band 

172 or name == filter.afw_name 

173 or name in filter.alias 

174 ): 

175 matches.add(filter) 

176 return matches