Coverage for python/lsst/obs/lsst/filters.py: 98%
85 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-30 09:12 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-05-30 09:12 +0000
1# This file is part of obs_lsst.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://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 <http://www.gnu.org/licenses/>.
22__all__ = (
23 "LSSTCAM_FILTER_DEFINITIONS",
24 "LATISS_FILTER_DEFINITIONS",
25 "LSSTCAM_IMSIM_FILTER_DEFINITIONS",
26 "TS3_FILTER_DEFINITIONS",
27 "TS8_FILTER_DEFINITIONS",
28 "COMCAM_FILTER_DEFINITIONS",
29 "GENERIC_FILTER_DEFINITIONS",
30 "UCD_FILTER_DEFINITIONS",
31)
33from lsst.obs.base import FilterDefinition, FilterDefinitionCollection
34from .translators.lsst import FILTER_DELIMITER
37def addFilter(filter_dict, band, physical_filter):
38 """Define a filter in filter_dict, to be converted to a Filter later"""
40 # index by band but keep distinct physical filters by band
41 # since there can be many physical filters for a single band
42 if band not in filter_dict:
43 filter_dict[band] = {}
45 filter_dict[band][physical_filter] = dict(physical_filter=physical_filter,
46 band=band, alias=[],
47 )
50# Collection to handle the distinct cases where no filter is being used.
51NoFilterCollection = FilterDefinitionCollection(
52 # For this case, no filter is being used and the optical path to
53 # the focal plane is unoccluded.
54 FilterDefinition(physical_filter="empty", band="white",
55 alias={"no_filter", "open"}),
56 # For this case, all filters are returned to the carousel and the
57 # auto-changer partially occludes the focal plane. See Tony
58 # Johnson's comment at https://jira.lsstcorp.org/browse/DM-41675.
59 FilterDefinition(physical_filter="none", band="white",
60 alias={"no_filter", "open"}),
61)
63# Generic filters used by PhoSim and UCDCam
64LsstCamFiltersGeneric = FilterDefinitionCollection(
65 FilterDefinition(physical_filter="u", band="u"),
66 FilterDefinition(physical_filter="g", band="g"),
67 FilterDefinition(physical_filter="r", band="r"),
68 FilterDefinition(physical_filter="i", band="i"),
69 FilterDefinition(physical_filter="z", band="z"),
70 FilterDefinition(physical_filter="y", band="y"),
71)
73# The LSST Filters from Tony Johnson, 05/09/2023, in DM-38882.
74LsstCamFiltersBaseline = FilterDefinitionCollection(
75 FilterDefinition(physical_filter="ph_5", band="white"), # pinhole filter
76 FilterDefinition(physical_filter="ef_43", band="white"), # "empty" filter
77 FilterDefinition(physical_filter="u_24", band="u"),
78 FilterDefinition(physical_filter="g_6", band="g"),
79 FilterDefinition(physical_filter="r_57", band="r"),
80 FilterDefinition(physical_filter="i_39", band="i"),
81 FilterDefinition(physical_filter="z_20", band="z"),
82 FilterDefinition(physical_filter="y_10", band="y"),
83)
85#
86# Define the filters present in the BOT.
87# According to Tony Johnson the possible physical filters are
88# ColorFWheel [SDSSu,SDSSg,SDSSr,SDSSi,SDSSz,SDSSY,
89# 480nm,650nm,750nm,870nm,950nm,970nm]
90# SpotProjFWheel [grid,spot,empty3,empty4,empty5,empty6]
91# NeutralFWheel [ND_OD1.0,ND_OD0.5,ND_OD0.3,empty,ND_OD2.0,ND_OD0.7]
92# where:
93# ColorFWheel and SpotProjFWheel are mutually exclusive and
94# both appear in FILTER,
95# NeutralFWheel appears in FILTER2
96#
97# Experimentally we also see FILTER2 values of:
98# ['ND_OD0.01', 'ND_OD0.05', 'ND_OD0.4', 'ND_OD3.0', 'ND_OD4.0']
99#
100# The band names are not yet defined, so I'm going to invent them
102# Update from 2023-11-18:
103# in DM-41675 Yousuke notes the following update to the BOT filters:
104# SpotProjFWheel: [grid, spot, sparsegrid, streak, ellipses, empty6]
106# Map the BOT filters to corresponding band explicitly
107BOT_filter_map = {
108 "empty": "white",
109 "SDSSu": "u",
110 "SDSSg": "g",
111 "SDSSr": "r",
112 "SDSSi": "i",
113 "SDSSz": "z",
114 "SDSSY": "y",
115 "480nm": "g",
116 "650nm": "r",
117 "750nm": "i",
118 "870nm": "z",
119 "950nm": "y",
120 "970nm": "y",
121 "grid": "grid",
122 "spot": "spot",
123 "sparsegrid": "sparsegrid",
124 "streak": "streak",
125 "ellipses": "ellipses"
126}
128BOTFilters_dict = {}
129for physical_filter, band in BOT_filter_map.items():
130 if physical_filter == "empty":
131 pass # Already defined above
132 else:
133 addFilter(BOTFilters_dict, band, physical_filter)
135 # Empty ND is removed by metadata translator so is not needed here
136 ndFilters = ["ND_OD0.1", "ND_OD0.3", "ND_OD0.5", "ND_OD0.7", "ND_OD1.0", "ND_OD2.0"]
137 # We found these additional filters in BOT data files:
138 ndFilters += ['ND_OD0.01', 'ND_OD0.05', 'ND_OD0.4', 'ND_OD3.0', 'ND_OD4.0']
140 for nd in ndFilters:
141 # fully qualified physical filter
142 phys_plus_nd = f"{physical_filter}{FILTER_DELIMITER}{nd}"
144 # When one of the filters is empty we can just use the real filter
145 # (e.g. "u" not "u~empty"); but we always need at least one "empty"
146 if band == "white":
147 # Use the ND on its own
148 phys_plus_nd = nd
150 # Use a generic ND modifier for the band
151 ndband = f"{band}{FILTER_DELIMITER}nd"
153 addFilter(BOTFilters_dict, band=ndband, physical_filter=phys_plus_nd)
155BOTFilters = [
156 FilterDefinition(band="unknown", physical_filter="unknown"),
157]
158for band, physical_filters in BOTFilters_dict.items():
159 for physical_filter, filter_defn in physical_filters.items():
160 BOTFilters.append(FilterDefinition(**filter_defn))
162#
163# These are the filters used by the CCOB for both TS8 and LSSTCam.
164# For the testing of LSSTCam at SLAC, they will be combined with the
165# real LSSTCam filters, so we include those combinations in the CCOB
166# filter definitions.
167#
168CCOB_filter_map = {
169 "": "white",
170 "HIGH": "white",
171 "LOW": "white",
172 "uv": "u",
173 "blue": "g",
174 "red": "r",
175 "nm750": "i",
176 "nm850": "z",
177 "nm960": "y",
178}
180CCOBFilters = []
181for lsst_filter_def in (*NoFilterCollection, *LsstCamFiltersBaseline):
182 lsstcam_filter = lsst_filter_def.physical_filter
183 if lsstcam_filter == "empty":
184 lsstcam_filter = ""
185 lsstcam_band = lsst_filter_def.band
186 for ccob_filter, ccob_band in CCOB_filter_map.items():
187 if lsstcam_band != "white" and ccob_band != "white" and band != ccob_band:
188 # Skip disallowed filter combinations based on band values.
189 continue
190 if ccob_filter == "":
191 # This would correspond to an entry already in
192 # LSSTCamFilterBaseline
193 continue
194 filters = lsstcam_filter, ccob_filter
195 physical_filter = FILTER_DELIMITER.join(filters).strip(FILTER_DELIMITER)
196 if lsstcam_band == "white":
197 band = ccob_band
198 else:
199 band = lsstcam_band
200 if physical_filter == "": 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true
201 physical_filter = "empty"
202 CCOBFilters.append(FilterDefinition(band=band, physical_filter=physical_filter))
204#
205# The filters that we might see in the real LSSTCam (including in SLAC)
206#
207# Note that the filters we'll use on the sky, LsstCamFiltersBaseline, must
208# come first as we're not allocating enough bits in _computeCoaddExposureId
209# for all the BOT composite filters (i.e. "u~ND_OD1.0")
210#
211LSSTCAM_FILTER_DEFINITIONS = FilterDefinitionCollection(
212 *NoFilterCollection,
213 *LsstCamFiltersBaseline,
214 *BOTFilters,
215 *CCOBFilters,
216)
218GENERIC_FILTER_DEFINITIONS = FilterDefinitionCollection(
219 *NoFilterCollection,
220 *LsstCamFiltersGeneric,
221)
223#
224# Filters in SLAC's Test Stand 3
225#
226TS3Filters = [
227 FilterDefinition(band="unknown", physical_filter="unknown"),
228 FilterDefinition(physical_filter="275CutOn"),
229 FilterDefinition(physical_filter="550CutOn")]
231TS3_FILTER_DEFINITIONS = FilterDefinitionCollection(
232 *NoFilterCollection,
233 *LsstCamFiltersGeneric,
234 *TS3Filters,
235)
236#
237# Filters in SLAC's Test Stand 8
238#
239TS8Filters = [
240 FilterDefinition(band="unknown", physical_filter="unknown"),
241 FilterDefinition(physical_filter="275CutOn"),
242 FilterDefinition(physical_filter="550CutOn")]
244TS8_FILTER_DEFINITIONS = FilterDefinitionCollection(
245 *NoFilterCollection,
246 *LsstCamFiltersGeneric,
247 *LsstCamFiltersBaseline,
248 *TS8Filters,
249 *BOTFilters,
250 *CCOBFilters,
251)
252#
253# Filters in UC Davis Beam Simulator
254#
255UCDFilters = [
256 FilterDefinition(band="unknown", physical_filter="unknown")]
258UCD_FILTER_DEFINITIONS = FilterDefinitionCollection(
259 *NoFilterCollection,
260 *LsstCamFiltersGeneric,
261 *UCDFilters,
262)
263# LATISS filters include a grating in the name so we need to construct
264# filters for each combination of filter+grating.
266# This set of filters can be installed in either the grating or filter wheel,
267# so we define them here to avoid duplication.
268sdss65mm_filters = [FilterDefinition(physical_filter="SDSSu_65mm",
269 band="u"),
270 FilterDefinition(physical_filter="SDSSg_65mm",
271 band="g"),
272 FilterDefinition(physical_filter="SDSSr_65mm",
273 band="r"),
274 FilterDefinition(physical_filter="SDSSi_65mm",
275 band="i"),
276 FilterDefinition(physical_filter="SDSSz_65mm",
277 band="z"),
278 FilterDefinition(physical_filter="SDSSy_65mm",
279 band="y"),
280 ]
282_latiss_filters = (
283 FilterDefinition(physical_filter="empty",
284 band="white",
285 alias={"no_filter", "open"}),
286 FilterDefinition(physical_filter="blank_bk7_wg05",
287 band="white"),
288 FilterDefinition(physical_filter="KPNO_1111_436nm",
289 band="g"),
290 FilterDefinition(physical_filter="KPNO_373A_677nm",
291 band="r"),
292 FilterDefinition(physical_filter="KPNO_406_828nm",
293 band="z"),
294 FilterDefinition(physical_filter="diffuser",
295 band="diffuser"),
296 FilterDefinition(physical_filter="unknown",
297 band="unknown"),
298 FilterDefinition(physical_filter="BG40",
299 band="g",
300 afw_name="bg"),
301 FilterDefinition(physical_filter="BG40_65mm_1",
302 band="g",
303 afw_name="bg"),
304 FilterDefinition(physical_filter="BG40_65mm_2",
305 band="g",
306 afw_name="bg"),
307 FilterDefinition(physical_filter="quadnotch1",
308 band="notch"),
309 FilterDefinition(physical_filter="nonanotch1",
310 band="notch9"),
311 FilterDefinition(physical_filter="RG610",
312 band="r",
313 afw_name="rg"),
314 FilterDefinition(physical_filter="OG550_65mm_1",
315 band="g",
316 afw_name="bg"),
317 FilterDefinition(physical_filter="OG550_65mm_2",
318 band="g",
319 afw_name="bg"),
320 FilterDefinition(physical_filter="FELH0600",
321 band="r",
322 afw_name="rg"),
323 FilterDefinition(physical_filter="SDSSg",
324 band="g"),
325 FilterDefinition(physical_filter="SDSSr",
326 band="r"),
327 FilterDefinition(physical_filter="SDSSi",
328 band="i"),
329 FilterDefinition(physical_filter="collimator",
330 band="white"),
331 FilterDefinition(physical_filter="cyl_lens",
332 band="white"),
333 FilterDefinition(physical_filter="prism",
334 band="white"),
335 *sdss65mm_filters,
336)
338# Form a new set of filter definitions from all the explicit gratings, and add
339# sdss65mm_filter set.
340_latiss_gratings = ("ronchi90lpmm",
341 "ronchi170lpmm",
342 "empty",
343 "unknown",
344 "holo4_003",
345 "holo4_003_prism",
346 "blue300lpmm_qn1",
347 "300lpmm",
348 "holo4_001",
349 "holo4_001_prism",
350 "pinhole_1_1000",
351 "pinhole_1_0500",
352 "pinhole_1_0200",
353 "pinhole_1_0100",
354 "pinhole_2_1_1000",
355 "pinhole_2_1_0500",
356 "pinhole_2_1_0200",
357 "pinhole_2_1_0100",
358 "pinhole_2_2_1000",
359 "pinhole_2_2_0500",
360 "pinhole_2_2_0200",
361 "pinhole_2_2_0100",
362 "pinhole_2_3_1000",
363 "pinhole_2_3_0500",
364 "pinhole_2_3_0200",
365 "pinhole_2_3_0100",
366 "pinhole_2_4_1000",
367 "pinhole_2_4_0500",
368 "pinhole_2_4_0200",
369 "pinhole_2_4_0100",
370 *sdss65mm_filters,
371 )
373# Include the filters without the grating in case someone wants
374# to retrieve a filter by an actual filter name
375_latiss_filter_and_grating = [f for f in _latiss_filters]
377for filter in _latiss_filters:
378 for grating in _latiss_gratings:
379 # The diffuser "filter" was never used with gratings
380 # so skip it
381 if filter.physical_filter == "diffuser":
382 continue
384 # If the grating is a FilterDefinition, use the band and alias
385 # attributes defined in the grating for the new, combined
386 # FilterDefinition. In addition, filter out any combinations that do
387 # not use the "empty" filter in combination with a grating that is a
388 # FitlerDefintion as we should never combine multiple filters in real
389 # observations.
390 if isinstance(grating, FilterDefinition):
391 if filter.physical_filter != "empty":
392 continue
394 new_name = FILTER_DELIMITER.join([filter.physical_filter, grating.physical_filter])
395 new_aliases = {FILTER_DELIMITER.join([filter.physical_filter, a]) for a in grating.alias}
397 combo = FilterDefinition(physical_filter=new_name,
398 band=grating.band,
399 afw_name=grating.afw_name,
400 alias=new_aliases)
401 else:
402 new_name = FILTER_DELIMITER.join([filter.physical_filter, grating])
403 new_aliases = {FILTER_DELIMITER.join([a, grating]) for a in filter.alias}
405 combo = FilterDefinition(physical_filter=new_name,
406 band=filter.band,
407 afw_name=filter.afw_name,
408 alias=new_aliases)
410 _latiss_filter_and_grating.append(combo)
413LATISS_FILTER_DEFINITIONS = FilterDefinitionCollection(*_latiss_filter_and_grating)
416LSSTCAM_IMSIM_FILTER_DEFINITIONS = FilterDefinitionCollection(
417 # These were computed using throughputs 1.4 and
418 # lsst.sims.photUtils.BandpassSet.
419 FilterDefinition(physical_filter="u_sim_1.4",
420 band="u"),
421 FilterDefinition(physical_filter="g_sim_1.4",
422 band="g"),
423 FilterDefinition(physical_filter="r_sim_1.4",
424 band="r"),
425 FilterDefinition(physical_filter="i_sim_1.4",
426 band="i"),
427 FilterDefinition(physical_filter="z_sim_1.4",
428 band="z"),
429 FilterDefinition(physical_filter="y_sim_1.4",
430 band="y"),
431 *LsstCamFiltersGeneric,
432)
434# ###########################################################################
435#
436# ComCam
437#
438# See https://jira.lsstcorp.org/browse/DM-21706
440ComCamFilters_dict = {}
441for band, sn in [("u", "SN-05"), # incorrect sub thickness
442 ("u", "SN-02"), # not yet coated
443 ("u", "SN-06"), # not yet coated
444 ("g", "SN-07"), # bad cosmetics
445 ("g", "SN-01"),
446 ("r", "SN-03"),
447 ("i", "SN-06"),
448 ("z", "SN-03"),
449 ("z", "SN-02"), # failed specs
450 ("y", "SN-04"),
451 ]:
452 physical_filter = f"{band}_{sn[3:]}"
453 addFilter(ComCamFilters_dict, band, physical_filter)
456ComCamFilters = [
457 FilterDefinition(band="unknown", physical_filter="unknown"),
458 FilterDefinition(band="white", physical_filter="pinhole"),
459 FilterDefinition(band="OG590", physical_filter="OG590_2_5mm"),
460]
461for band, physical_filters in ComCamFilters_dict.items():
462 for physical_filter, filter_defn in physical_filters.items():
463 ComCamFilters.append(FilterDefinition(**filter_defn))
465COMCAM_FILTER_DEFINITIONS = FilterDefinitionCollection(
466 *NoFilterCollection,
467 *ComCamFilters,
468)