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

57 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-30 12:18 +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: unique for 

50 each piece of glass. This should match the exact filter name used in the 

51 observatory's metadata. 

52 

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

54 

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

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

57 list of `~lsst.afw.image.Filter` 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 HSC). 

63 

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

65 not have a generically-termed filter name. 

66 

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

68 `~lsst.afw.image.Filter` ``name`` field, otherwise it is added to the list 

69 of `~lsst.afw.image.Filter` aliases. 

70 """ 

71 

72 doc: str | None = None 

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

74 information. 

75 """ 

76 

77 afw_name: str | None = None 

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

79 

80 This is distinct from physical_filter and band to maintain 

81 backwards compatibility in some obs packages. 

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

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

84 """ 

85 

86 alias: Set[str] = frozenset() 

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

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

89 """ 

90 

91 def __post_init__(self) -> None: 

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

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

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

95 

96 def __str__(self) -> str: 

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

98 if self.band is not None: 

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

100 if self.afw_name is not None: 

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

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

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

104 return txt + ")" 

105 

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

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

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

109 

110 

111class FilterDefinitionCollection(Sequence[FilterDefinition]): 

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

113 

114 Parameters 

115 ---------- 

116 filters : `Sequence` 

117 The filters in this collection. 

118 """ 

119 

120 physical_to_band: dict[str, str | None] 

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

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

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

124 iterating over the entire collection. 

125 """ 

126 

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

128 self._filters = list(filters) 

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

130 

131 @overload 

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

133 pass 

134 

135 @overload 

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

137 pass 

138 

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

140 return self._filters[index] 

141 

142 def __len__(self) -> int: 

143 return len(self._filters) 

144 

145 def __str__(self) -> str: 

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

147 

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

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

150 

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

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

153 to the name is returned. 

154 

155 Parameters 

156 ---------- 

157 name : `str` 

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

159 

160 Returns 

161 ------- 

162 matches : `set` [`FilterDefinition`] 

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

164 filter names. 

165 """ 

166 matches = set() 

167 for filter in self._filters: 

168 if ( 

169 name == filter.physical_filter 

170 or name == filter.band 

171 or name == filter.afw_name 

172 or name in filter.alias 

173 ): 

174 matches.add(filter) 

175 return matches