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

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 warnings
32import numpy as np
34import lsst.afw.image.utils
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 # surpress Filter warnings; we already know this is deprecated
87 warnings.simplefilter('ignore', category=FutureWarning)
88 self.reset()
89 for filter in self._filters: 89 ↛ 90line 89 didn't jump to line 90, because the loop on line 89 never started
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 # surpress 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 name == filter.physical_filter or name == filter.band or name == filter.afw_name \
131 or name in filter.alias:
132 matches.add(filter)
133 return matches
136@dataclasses.dataclass(frozen=True)
137class FilterDefinition:
138 """The definition of an instrument's filter bandpass.
140 This class is used to interface between the `~lsst.afw.image.Filter` class
141 and the Gen2 `~lsst.daf.persistence.CameraMapper` and Gen3
142 `~lsst.obs.base.Instruments` and ``physical_filter``/``band``
143 `~lsst.daf.butler.Dimension`.
145 This class is likely temporary, until we have a better versioned filter
146 definition system that includes complete transmission information.
147 """
149 physical_filter: str
150 """The name of a filter associated with a particular instrument: unique for
151 each piece of glass. This should match the exact filter name used in the
152 observatory's metadata.
154 This name is used to define the ``physical_filter`` gen3 Butler Dimension.
156 If neither ``band`` or ``afw_name`` is defined, this is used
157 as the `~lsst.afw.image.Filter` ``name``, otherwise it is added to the
158 list of `~lsst.afw.image.Filter` aliases.
159 """
161 lambdaEff: float
162 """The effective wavelength of this filter (nm)."""
164 band: str = None
165 """The generic name of a filter not associated with a particular instrument
166 (e.g. `r` for the SDSS Gunn r-band, which could be on SDSS, LSST, or HSC).
168 Not all filters have an abstract filter: engineering or test filters may
169 not have a genericly-termed filter name.
171 If specified and if `afw_name` is None, this is used as the
172 `~lsst.afw.image.Filter` ``name`` field, otherwise it is added to the list
173 of `~lsst.afw.image.Filter` aliases.
174 """
176 doc: str = None
177 """A short description of this filter, possibly with a link to more
178 information.
179 """
181 afw_name: str = None
182 """If not None, the name of the `~lsst.afw.image.Filter` object.
184 This is distinct from physical_filter and band to maintain
185 backwards compatibility in some obs packages.
186 For example, for HSC there are two distinct ``r`` and ``i`` filters, named
187 ``r/r2`` and ``i/i2``.
188 """
190 lambdaMin: float = np.nan
191 """The minimum wavelength of this filter (nm; defined as 1% throughput)"""
192 lambdaMax: float = np.nan
193 """The maximum wavelength of this filter (nm; defined as 1% throughput)"""
195 alias: set = frozenset()
196 """Alternate names for this filter. These are added to the
197 `~lsst.afw.image.Filter` alias list.
198 """
200 def __post_init__(self):
201 # force alias to be immutable, so that hashing works
202 if not isinstance(self.alias, frozenset):
203 object.__setattr__(self, 'alias', frozenset(self.alias))
205 def __str__(self):
206 txt = f"FilterDefinition(physical_filter='{self.physical_filter}', lambdaEff='{self.lambdaEff}'"
207 if self.band is not None:
208 txt += f", band='{self.band}'"
209 if self.afw_name is not None:
210 txt += f", afw_name='{self.afw_name}'"
211 if not np.isnan(self.lambdaMin):
212 txt += f", lambdaMin='{self.lambdaMin}'"
213 if not np.isnan(self.lambdaMax):
214 txt += f", lambdaMax='{self.lambdaMax}'"
215 if len(self.alias) != 0:
216 txt += f", alias='{self.alias}'"
217 return txt + ")"
219 def defineFilter(self):
220 """Declare the filters via afw.image.Filter.
221 """
222 aliases = set(self.alias)
223 name = self.physical_filter
224 if self.band is not None:
225 name = self.band
226 aliases.add(self.physical_filter)
227 if self.afw_name is not None:
228 name = self.afw_name
229 aliases.add(self.physical_filter)
230 # Only add `physical_filter/band` as an alias if afw_name is defined.ee
231 if self.afw_name is not None:
232 if self.band is not None:
233 aliases.add(self.band)
234 with warnings.catch_warnings():
235 # surpress Filter warnings; we already know this is deprecated
236 warnings.simplefilter('ignore', category=FutureWarning)
237 lsst.afw.image.utils.defineFilter(name,
238 lambdaEff=self.lambdaEff,
239 lambdaMin=self.lambdaMin,
240 lambdaMax=self.lambdaMax,
241 alias=sorted(aliases))
243 def makeFilterLabel(self):
244 """Create a complete FilterLabel for this filter.
245 """
246 return lsst.afw.image.FilterLabel(band=self.band, physical=self.physical_filter)