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

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/>.
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"""
26__all__ = ("FilterDefinition", "FilterDefinitionCollection")
28import dataclasses
29import collections.abc
30import re
31import warnings
33import numpy as np
35import lsst.afw.image.utils
38class FilterDefinitionCollection(collections.abc.Sequence):
39 """An order-preserving collection of multiple `FilterDefinition`.
41 Parameters
42 ----------
43 filters : `~collections.abc.Sequence`
44 The filters in this collection.
45 """
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 """
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 """
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}
64 def __getitem__(self, key):
65 return self._filters[key]
67 def __len__(self):
68 return len(self._filters)
70 def __str__(self):
71 return "FilterDefinitions(" + ', '.join(str(f) for f in self._filters) + ')'
73 def defineFilters(self):
74 """Define all the filters to `lsst.afw.image.Filter`.
76 `~lsst.afw.image.Filter` objects are singletons, so we protect against
77 filters being defined multiple times.
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)
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
111 def findAll(self, name):
112 """Return the FilterDefinitions that match a particular name.
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.
118 Parameters
119 ----------
120 name : `str`
121 The name to search for. May be any band, physical, or alias name.
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
137@dataclasses.dataclass(frozen=True)
138class FilterDefinition:
139 """The definition of an instrument's filter bandpass.
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`.
146 This class is likely temporary, until we have a better versioned filter
147 definition system that includes complete transmission information.
148 """
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.
155 This name is used to define the ``physical_filter`` gen3 Butler Dimension.
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 """
162 lambdaEff: float
163 """The effective wavelength of this filter (nm)."""
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).
169 Not all filters have an abstract filter: engineering or test filters may
170 not have a genericly-termed filter name.
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 """
177 doc: str = None
178 """A short description of this filter, possibly with a link to more
179 information.
180 """
182 afw_name: str = None
183 """If not None, the name of the `~lsst.afw.image.Filter` object.
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 """
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)"""
196 alias: set = frozenset()
197 """Alternate names for this filter. These are added to the
198 `~lsst.afw.image.Filter` alias list.
199 """
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))
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 + ")"
220 def defineFilter(self):
221 """Declare the filters via afw.image.Filter.
222 """
223 aliases = set(self.alias)
224 name = self.physical_filter
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}")
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
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)
264 # Only add physical_filter/band as an alias if afw_name is defined.
265 if band is not None:
266 aliases.add(band)
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)
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))
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)