Coverage for python/lsst/obs/base/filters.py: 57%
Shortcuts 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
Shortcuts 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 collections.abc
29import dataclasses
30import re
31import warnings
33import lsst.afw.image.utils
34import numpy as np
37class FilterDefinitionCollection(collections.abc.Sequence):
38 """An order-preserving collection of multiple `FilterDefinition`.
40 Parameters
41 ----------
42 filters : `~collections.abc.Sequence`
43 The filters in this collection.
44 """
46 _defined = None
47 """Whether these filters have been defined via
48 `~lsst.afw.image.utils.defineFilter`. If so, set to ``self`` to identify
49 the filter collection that defined them.
50 """
52 physical_to_band = {}
53 """A mapping from physical filter name to band name.
54 This is a convenience feature to allow file readers to create a FilterLabel
55 when reading a raw file that only has a physical filter name, without
56 iterating over the entire collection.
57 """
59 def __init__(self, *filters):
60 self._filters = list(filters)
61 self.physical_to_band = {filter.physical_filter: filter.band for filter in self._filters}
63 def __getitem__(self, key):
64 return self._filters[key]
66 def __len__(self):
67 return len(self._filters)
69 def __str__(self):
70 return "FilterDefinitions(" + ", ".join(str(f) for f in self._filters) + ")"
72 def defineFilters(self):
73 """Define all the filters to `lsst.afw.image.Filter`.
75 `~lsst.afw.image.Filter` objects are singletons, so we protect against
76 filters being defined multiple times.
78 Raises
79 ------
80 RuntimeError
81 Raised if any other `FilterDefinitionCollection` has already called
82 ``defineFilters``.
83 """
84 if self._defined is None: 84 ↛ 92line 84 didn't jump to line 92, because the condition on line 84 was never false
85 with warnings.catch_warnings():
86 # suppress Filter warnings; we already know this is deprecated
87 warnings.simplefilter("ignore", category=FutureWarning)
88 self.reset()
89 for filter in self._filters:
90 filter.defineFilter()
91 FilterDefinitionCollection._defined = self
92 elif self._defined is self:
93 # noop: we've already defined these filters, so do nothing
94 pass
95 else:
96 msg = f"afw Filters were already defined on: {self._defined}"
97 raise RuntimeError(msg)
99 @classmethod
100 def reset(cls):
101 """Reset the afw Filter definitions and clear the `defined` singleton.
102 Use this in unittests that define different filters.
103 """
104 with warnings.catch_warnings():
105 # suppress Filter warnings; we already know this is deprecated
106 warnings.simplefilter("ignore", category=FutureWarning)
107 lsst.afw.image.utils.resetFilters()
108 cls._defined = None
110 def findAll(self, name):
111 """Return the FilterDefinitions that match a particular name.
113 This method makes no attempt to prioritize, e.g., band names over
114 physical filter names; any definition that makes *any* reference
115 to the name is returned.
117 Parameters
118 ----------
119 name : `str`
120 The name to search for. May be any band, physical, or alias name.
122 Returns
123 -------
124 matches : `set` [`FilterDefinition`]
125 All FilterDefinitions containing ``name`` as one of their
126 filter names.
127 """
128 matches = set()
129 for filter in self._filters:
130 if (
131 name == filter.physical_filter
132 or name == filter.band
133 or name == filter.afw_name
134 or name in filter.alias
135 ):
136 matches.add(filter)
137 return matches
140@dataclasses.dataclass(frozen=True)
141class FilterDefinition:
142 """The definition of an instrument's filter bandpass.
144 This class is used to interface between the `~lsst.afw.image.Filter` class
145 and the Gen2 `~lsst.daf.persistence.CameraMapper` and Gen3
146 `~lsst.obs.base.Instruments` and ``physical_filter``/``band``
147 `~lsst.daf.butler.Dimension`.
149 This class is likely temporary, until we have a better versioned filter
150 definition system that includes complete transmission information.
151 """
153 physical_filter: str
154 """The name of a filter associated with a particular instrument: unique for
155 each piece of glass. This should match the exact filter name used in the
156 observatory's metadata.
158 This name is used to define the ``physical_filter`` gen3 Butler Dimension.
160 If neither ``band`` or ``afw_name`` is defined, this is used
161 as the `~lsst.afw.image.Filter` ``name``, otherwise it is added to the
162 list of `~lsst.afw.image.Filter` aliases.
163 """
165 lambdaEff: float
166 """The effective wavelength of this filter (nm)."""
168 band: str = None
169 """The generic name of a filter not associated with a particular instrument
170 (e.g. `r` for the SDSS Gunn r-band, which could be on SDSS, LSST, or HSC).
172 Not all filters have an abstract filter: engineering or test filters may
173 not have a genericly-termed filter name.
175 If specified and if `afw_name` is None, this is used as the
176 `~lsst.afw.image.Filter` ``name`` field, otherwise it is added to the list
177 of `~lsst.afw.image.Filter` aliases.
178 """
180 doc: str = None
181 """A short description of this filter, possibly with a link to more
182 information.
183 """
185 afw_name: str = None
186 """If not None, the name of the `~lsst.afw.image.Filter` object.
188 This is distinct from physical_filter and band to maintain
189 backwards compatibility in some obs packages.
190 For example, for HSC there are two distinct ``r`` and ``i`` filters, named
191 ``r/r2`` and ``i/i2``.
192 """
194 lambdaMin: float = np.nan
195 """The minimum wavelength of this filter (nm; defined as 1% throughput)"""
196 lambdaMax: float = np.nan
197 """The maximum wavelength of this filter (nm; defined as 1% throughput)"""
199 alias: set = frozenset()
200 """Alternate names for this filter. These are added to the
201 `~lsst.afw.image.Filter` alias list.
202 """
204 def __post_init__(self):
205 # force alias to be immutable, so that hashing works
206 if not isinstance(self.alias, frozenset): 206 ↛ 207line 206 didn't jump to line 207, because the condition on line 206 was never true
207 object.__setattr__(self, "alias", frozenset(self.alias))
209 def __str__(self):
210 txt = f"FilterDefinition(physical_filter='{self.physical_filter}', lambdaEff='{self.lambdaEff}'"
211 if self.band is not None:
212 txt += f", band='{self.band}'"
213 if self.afw_name is not None:
214 txt += f", afw_name='{self.afw_name}'"
215 if not np.isnan(self.lambdaMin):
216 txt += f", lambdaMin='{self.lambdaMin}'"
217 if not np.isnan(self.lambdaMax):
218 txt += f", lambdaMax='{self.lambdaMax}'"
219 if len(self.alias) != 0:
220 txt += f", alias='{self.alias}'"
221 return txt + ")"
223 def defineFilter(self):
224 """Declare the filters via afw.image.Filter."""
225 aliases = set(self.alias)
226 name = self.physical_filter
228 current_names = set(lsst.afw.image.Filter.getNames())
229 # band can be defined multiple times -- only use the first
230 # occurrence
231 band = self.band
232 if band is not None: 232 ↛ 256line 232 didn't jump to line 256, because the condition on line 232 was never false
233 # Special case code that uses afw_name to override the band.
234 # This was generally used as a workaround.
235 if self.afw_name is not None and (mat := re.match(fr"{band}(\d)+$", self.afw_name)): 235 ↛ 236line 235 didn't jump to line 236, because the condition on line 235 was never true
236 i = int(mat.group(1))
237 else:
238 i = 0
239 while i < 50: # in some instruments gratings or ND filters are combined 239 ↛ 251line 239 didn't jump to line 251, because the condition on line 239 was never false
240 if i == 0: 240 ↛ 243line 240 didn't jump to line 243, because the condition on line 240 was never false
241 nband = band
242 else:
243 nband = f"{band}{i}"
244 if nband not in current_names: 244 ↛ 249line 244 didn't jump to line 249, because the condition on line 244 was never false
245 band = nband
246 name = band
247 aliases.add(self.physical_filter)
248 break
249 i += 1
250 else:
251 warnings.warn(
252 f"Too many band aliases found for physical_filter {self.physical_filter}"
253 f" with band {band}"
254 )
256 if self.physical_filter == self.band and self.physical_filter in current_names: 256 ↛ 258line 256 didn't jump to line 258, because the condition on line 256 was never true
257 # We have already defined a filter like this
258 return
260 # Do not add an alias for band if the given afw_name matches
261 # the dynamically calculated band.
262 if self.afw_name is not None and self.afw_name != band and self.afw_name not in current_names: 262 ↛ 265line 262 didn't jump to line 265, because the condition on line 262 was never true
263 # This will override the band setting above but it
264 # is still used as an alias below
265 name = self.afw_name
266 aliases.add(self.physical_filter)
268 # Only add physical_filter/band as an alias if afw_name is defined.
269 if band is not None:
270 aliases.add(band)
272 # Aliases are a serious issue so as a last attempt to clean up
273 # remove any registered names from the new aliases
274 # This usually means some variant filter name is being used
275 aliases.difference_update(current_names)
277 with warnings.catch_warnings():
278 # suppress Filter warnings; we already know this is deprecated
279 warnings.simplefilter("ignore", category=FutureWarning)
280 lsst.afw.image.utils.defineFilter(
281 name,
282 lambdaEff=self.lambdaEff,
283 lambdaMin=self.lambdaMin,
284 lambdaMax=self.lambdaMax,
285 alias=sorted(aliases),
286 )
288 def makeFilterLabel(self):
289 """Create a complete FilterLabel for this filter."""
290 return lsst.afw.image.FilterLabel(band=self.band, physical=self.physical_filter)