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

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
31import numpy as np
33import lsst.afw.image.utils
36class FilterDefinitionCollection(collections.abc.Sequence):
37 """An order-preserving collection of multiple `FilterDefinition`.
39 Parameters
40 ----------
41 filters : `~collections.abc.Sequence`
42 The filters in this collection.
43 """
45 _defined = None
46 """Whether these filters have been defined via
47 `~lsst.afw.image.utils.defineFilter`. If so, set to ``self`` to identify
48 the filter collection that defined them.
49 """
51 physical_to_band = {}
52 """A mapping from physical filter name to band name.
53 This is a convenience feature to allow file readers to create a FilterLabel
54 when reading a raw file that only has a physical filter name, without
55 iterating over the entire collection.
56 """
58 def __init__(self, *filters):
59 self._filters = list(filters)
60 self.physical_to_band = {filter.physical_filter: filter.band for filter in self._filters}
62 def __getitem__(self, key):
63 return self._filters[key]
65 def __len__(self):
66 return len(self._filters)
68 def __str__(self):
69 return "FilterDefinitions(" + ', '.join(str(f) for f in self._filters) + ')'
71 def defineFilters(self):
72 """Define all the filters to `lsst.afw.image.Filter`.
74 `~lsst.afw.image.Filter` objects are singletons, so we protect against
75 filters being defined multiple times.
77 Raises
78 ------
79 RuntimeError
80 Raised if any other `FilterDefinitionCollection` has already called
81 ``defineFilters``.
82 """
83 if self._defined is None: 83 ↛ 88line 83 didn't jump to line 88, because the condition on line 83 was never false
84 self.reset()
85 for filter in self._filters: 85 ↛ 86line 85 didn't jump to line 86, because the loop on line 85 never started
86 filter.defineFilter()
87 FilterDefinitionCollection._defined = self
88 elif self._defined is self:
89 # noop: we've already defined these filters, so do nothing
90 pass
91 else:
92 msg = f"afw Filters were already defined on: {self._defined}"
93 raise RuntimeError(msg)
95 @classmethod
96 def reset(cls):
97 """Reset the afw Filter definitions and clear the `defined` singleton.
98 Use this in unittests that define different filters.
99 """
100 lsst.afw.image.utils.resetFilters()
101 cls._defined = None
103 def findAll(self, name):
104 """Return the FilterDefinitions that match a particular name.
106 This method makes no attempt to prioritize, e.g., band names over
107 physical filter names; any definition that makes *any* reference
108 to the name is returned.
110 Parameters
111 ----------
112 name : `str`
113 The name to search for. May be any band, physical, or alias name.
115 Returns
116 -------
117 matches : `set` [`FilterDefinition`]
118 All FilterDefinitions containing ``name`` as one of their
119 filter names.
120 """
121 matches = set()
122 for filter in self._filters:
123 if name == filter.physical_filter or name == filter.band or name == filter.afw_name \
124 or name in filter.alias:
125 matches.add(filter)
126 return matches
129@dataclasses.dataclass(frozen=True)
130class FilterDefinition:
131 """The definition of an instrument's filter bandpass.
133 This class is used to interface between the `~lsst.afw.image.Filter` class
134 and the Gen2 `~lsst.daf.persistence.CameraMapper` and Gen3
135 `~lsst.obs.base.Instruments` and ``physical_filter``/``band``
136 `~lsst.daf.butler.Dimension`.
138 This class is likely temporary, until we have a better versioned filter
139 definition system that includes complete transmission information.
140 """
142 physical_filter: str
143 """The name of a filter associated with a particular instrument: unique for
144 each piece of glass. This should match the exact filter name used in the
145 observatory's metadata.
147 This name is used to define the ``physical_filter`` gen3 Butler Dimension.
149 If neither ``band`` or ``afw_name`` is defined, this is used
150 as the `~lsst.afw.image.Filter` ``name``, otherwise it is added to the
151 list of `~lsst.afw.image.Filter` aliases.
152 """
154 lambdaEff: float
155 """The effective wavelength of this filter (nm)."""
157 band: str = None
158 """The generic name of a filter not associated with a particular instrument
159 (e.g. `r` for the SDSS Gunn r-band, which could be on SDSS, LSST, or HSC).
161 Not all filters have an abstract filter: engineering or test filters may
162 not have a genericly-termed filter name.
164 If specified and if `afw_name` is None, this is used as the
165 `~lsst.afw.image.Filter` ``name`` field, otherwise it is added to the list
166 of `~lsst.afw.image.Filter` aliases.
167 """
169 afw_name: str = None
170 """If not None, the name of the `~lsst.afw.image.Filter` object.
172 This is distinct from physical_filter and band to maintain
173 backwards compatibility in some obs packages.
174 For example, for HSC there are two distinct ``r`` and ``i`` filters, named
175 ``r/r2`` and ``i/i2``.
176 """
178 lambdaMin: float = np.nan
179 """The minimum wavelength of this filter (nm; defined as 1% throughput)"""
180 lambdaMax: float = np.nan
181 """The maximum wavelength of this filter (nm; defined as 1% throughput)"""
183 alias: set = frozenset()
184 """Alternate names for this filter. These are added to the
185 `~lsst.afw.image.Filter` alias list.
186 """
188 def __post_init__(self):
189 # force alias to be immutable, so that hashing works
190 if not isinstance(self.alias, frozenset):
191 object.__setattr__(self, 'alias', frozenset(self.alias))
193 def __str__(self):
194 txt = f"FilterDefinition(physical_filter='{self.physical_filter}', lambdaEff='{self.lambdaEff}'"
195 if self.band is not None:
196 txt += f", band='{self.band}'"
197 if self.afw_name is not None:
198 txt += f", afw_name='{self.afw_name}'"
199 if not np.isnan(self.lambdaMin):
200 txt += f", lambdaMin='{self.lambdaMin}'"
201 if not np.isnan(self.lambdaMax):
202 txt += f", lambdaMax='{self.lambdaMax}'"
203 if len(self.alias) != 0:
204 txt += f", alias='{self.alias}'"
205 return txt + ")"
207 def defineFilter(self):
208 """Declare the filters via afw.image.Filter.
209 """
210 aliases = set(self.alias)
211 name = self.physical_filter
212 if self.band is not None:
213 name = self.band
214 aliases.add(self.physical_filter)
215 if self.afw_name is not None:
216 name = self.afw_name
217 aliases.add(self.physical_filter)
218 # Only add `physical_filter/band` as an alias if afw_name is defined.ee
219 if self.afw_name is not None:
220 if self.band is not None:
221 aliases.add(self.band)
222 lsst.afw.image.utils.defineFilter(name,
223 lambdaEff=self.lambdaEff,
224 lambdaMin=self.lambdaMin,
225 lambdaMax=self.lambdaMax,
226 alias=sorted(aliases))
228 def makeFilterLabel(self):
229 """Create a complete FilterLabel for this filter.
230 """
231 return lsst.afw.image.FilterLabel(band=self.band, physical=self.physical_filter)