Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

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

27 

28import dataclasses 

29import collections.abc 

30import re 

31import warnings 

32 

33import numpy as np 

34 

35import lsst.afw.image.utils 

36 

37 

38class FilterDefinitionCollection(collections.abc.Sequence): 

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

40 

41 Parameters 

42 ---------- 

43 filters : `~collections.abc.Sequence` 

44 The filters in this collection. 

45 """ 

46 

47 _defined = None 

48 """Whether these filters have been defined via 

49 `~lsst.afw.image.utils.defineFilter`. If so, set to ``self`` to identify 

50 the filter collection that defined them. 

51 """ 

52 

53 physical_to_band = {} 

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

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

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

57 iterating over the entire collection. 

58 """ 

59 

60 def __init__(self, *filters): 

61 self._filters = list(filters) 

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

63 

64 def __getitem__(self, key): 

65 return self._filters[key] 

66 

67 def __len__(self): 

68 return len(self._filters) 

69 

70 def __str__(self): 

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

72 

73 def defineFilters(self): 

74 """Define all the filters to `lsst.afw.image.Filter`. 

75 

76 `~lsst.afw.image.Filter` objects are singletons, so we protect against 

77 filters being defined multiple times. 

78 

79 Raises 

80 ------ 

81 RuntimeError 

82 Raised if any other `FilterDefinitionCollection` has already called 

83 ``defineFilters``. 

84 """ 

85 if self._defined is None: 85 ↛ 93line 85 didn't jump to line 93, because the condition on line 85 was never false

86 with warnings.catch_warnings(): 

87 # surpress Filter warnings; we already know this is deprecated 

88 warnings.simplefilter('ignore', category=FutureWarning) 

89 self.reset() 

90 for filter in self._filters: 90 ↛ 91line 90 didn't jump to line 91, because the loop on line 90 never started

91 filter.defineFilter() 

92 FilterDefinitionCollection._defined = self 

93 elif self._defined is self: 

94 # noop: we've already defined these filters, so do nothing 

95 pass 

96 else: 

97 msg = f"afw Filters were already defined on: {self._defined}" 

98 raise RuntimeError(msg) 

99 

100 @classmethod 

101 def reset(cls): 

102 """Reset the afw Filter definitions and clear the `defined` singleton. 

103 Use this in unittests that define different filters. 

104 """ 

105 with warnings.catch_warnings(): 

106 # surpress Filter warnings; we already know this is deprecated 

107 warnings.simplefilter('ignore', category=FutureWarning) 

108 lsst.afw.image.utils.resetFilters() 

109 cls._defined = None 

110 

111 def findAll(self, name): 

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

113 

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

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

116 to the name is returned. 

117 

118 Parameters 

119 ---------- 

120 name : `str` 

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

122 

123 Returns 

124 ------- 

125 matches : `set` [`FilterDefinition`] 

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

127 filter names. 

128 """ 

129 matches = set() 

130 for filter in self._filters: 

131 if name == filter.physical_filter or name == filter.band or name == filter.afw_name \ 

132 or name in filter.alias: 

133 matches.add(filter) 

134 return matches 

135 

136 

137@dataclasses.dataclass(frozen=True) 

138class FilterDefinition: 

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

140 

141 This class is used to interface between the `~lsst.afw.image.Filter` class 

142 and the Gen2 `~lsst.daf.persistence.CameraMapper` and Gen3 

143 `~lsst.obs.base.Instruments` and ``physical_filter``/``band`` 

144 `~lsst.daf.butler.Dimension`. 

145 

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

147 definition system that includes complete transmission information. 

148 """ 

149 

150 physical_filter: str 

151 """The name of a filter associated with a particular instrument: unique for 

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

153 observatory's metadata. 

154 

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

156 

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

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

159 list of `~lsst.afw.image.Filter` aliases. 

160 """ 

161 

162 lambdaEff: float 

163 """The effective wavelength of this filter (nm).""" 

164 

165 band: str = None 

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

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

168 

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

170 not have a genericly-termed filter name. 

171 

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

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

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

175 """ 

176 

177 doc: str = None 

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

179 information. 

180 """ 

181 

182 afw_name: str = None 

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

184 

185 This is distinct from physical_filter and band to maintain 

186 backwards compatibility in some obs packages. 

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

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

189 """ 

190 

191 lambdaMin: float = np.nan 

192 """The minimum wavelength of this filter (nm; defined as 1% throughput)""" 

193 lambdaMax: float = np.nan 

194 """The maximum wavelength of this filter (nm; defined as 1% throughput)""" 

195 

196 alias: set = frozenset() 

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

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

199 """ 

200 

201 def __post_init__(self): 

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

203 if not isinstance(self.alias, frozenset): 

204 object.__setattr__(self, 'alias', frozenset(self.alias)) 

205 

206 def __str__(self): 

207 txt = f"FilterDefinition(physical_filter='{self.physical_filter}', lambdaEff='{self.lambdaEff}'" 

208 if self.band is not None: 

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

210 if self.afw_name is not None: 

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

212 if not np.isnan(self.lambdaMin): 

213 txt += f", lambdaMin='{self.lambdaMin}'" 

214 if not np.isnan(self.lambdaMax): 

215 txt += f", lambdaMax='{self.lambdaMax}'" 

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

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

218 return txt + ")" 

219 

220 def defineFilter(self): 

221 """Declare the filters via afw.image.Filter. 

222 """ 

223 aliases = set(self.alias) 

224 name = self.physical_filter 

225 

226 current_names = set(lsst.afw.image.Filter.getNames()) 

227 # band can be defined multiple times -- only use the first 

228 # occurrence 

229 band = self.band 

230 if band is not None: 

231 # Special case code that uses afw_name to override the band. 

232 # This was generally used as a workaround. 

233 if self.afw_name is not None and (mat := re.match(fr"{band}(\d)+$", self.afw_name)): 

234 i = int(mat.group(1)) 

235 else: 

236 i = 0 

237 while i < 50: # in some instruments gratings or ND filters are combined 

238 if i == 0: 

239 nband = band 

240 else: 

241 nband = f"{band}{i}" 

242 if nband not in current_names: 

243 band = nband 

244 name = band 

245 aliases.add(self.physical_filter) 

246 break 

247 i += 1 

248 else: 

249 warnings.warn(f"Too many band aliases found for physical_filter {self.physical_filter}" 

250 f" with band {band}") 

251 

252 if self.physical_filter == self.band and self.physical_filter in current_names: 

253 # We have already defined a filter like this 

254 return 

255 

256 # Do not add an alias for band if the given afw_name matches 

257 # the dynamically calculated band. 

258 if self.afw_name is not None and self.afw_name != band and self.afw_name not in current_names: 

259 # This will override the band setting above but it 

260 # is still used as an alias below 

261 name = self.afw_name 

262 aliases.add(self.physical_filter) 

263 

264 # Only add physical_filter/band as an alias if afw_name is defined. 

265 if band is not None: 

266 aliases.add(band) 

267 

268 # Aliases are a serious issue so as a last attempt to clean up 

269 # remove any registered names from the new aliases 

270 # This usually means some variant filter name is being used 

271 aliases.difference_update(current_names) 

272 

273 with warnings.catch_warnings(): 

274 # surpress Filter warnings; we already know this is deprecated 

275 warnings.simplefilter('ignore', category=FutureWarning) 

276 lsst.afw.image.utils.defineFilter(name, 

277 lambdaEff=self.lambdaEff, 

278 lambdaMin=self.lambdaMin, 

279 lambdaMax=self.lambdaMax, 

280 alias=sorted(aliases)) 

281 

282 def makeFilterLabel(self): 

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

284 """ 

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