• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

desihub / desiutil / 3988894829

pending completion
3988894829

Pull #190

github-actions

GitHub
Merge 89db93e8c into da1060a80
Pull Request #190: Replace setup.py plugins with stand-alone scripts

199 of 199 new or added lines in 7 files covered. (100.0%)

2010 of 2644 relevant lines covered (76.02%)

0.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.25
/py/desiutil/dust.py
1
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2
# -*- coding: utf-8 -*-
3
"""
1✔
4
=============
5
desiutil.dust
6
=============
7

8
Get :math:`E(B-V)` values from the `Schlegel, Finkbeiner & Davis (1998; SFD98)`_ dust map.
9

10
.. _`Schlegel, Finkbeiner & Davis (1998; SFD98)`: http://adsabs.harvard.edu/abs/1998ApJ...500..525S.
11
"""
12
import os
1✔
13
import numpy as np
1✔
14
import itertools
1✔
15
from astropy.io.fits import getdata
1✔
16
from astropy.coordinates import SkyCoord
1✔
17
from astropy import units as u
1✔
18
from desiutil.log import get_logger
1✔
19
log = get_logger()
1✔
20

21

22
def extinction_total_to_selective_ratio(band, photsys, match_legacy_surveys=False):
1✔
23
    """Return the linear coefficient R_X = A(X)/E(B-V) where
24
    A(X) = -2.5*log10(transmission in X band),
25
    for band X in 'G','R' or 'Z' when
26
    photsys = 'N' or 'S' specifies the survey (BASS+MZLS or DECALS),
27
    or for band X in 'G', 'BP', 'RP' when photsys = 'G' (when gaia dr2)
28
    or for band X in 'W1', 'W2', 'W3', 'W4' when photsys is either 'N' or 'S'
29
    E(B-V) is interpreted as SFD.
30

31
    Args:
32
        band : 'G', 'R', 'Z', 'BP', 'RP', 'W1', 'W2', 'W3', or 'W4'
33
        photsys : 'N' or 'S'
34

35
    Returns:
36
        scalar, total extinction A(band) = -2.5*log10(transmission(band))
37
    """
38
    if match_legacy_surveys:
1✔
39
        # Based on the fit from the columns MW_TRANSMISSION_X and EBV
40
        # for the DR8 target catalogs and propagated in fibermaps
41
        # R_X = -2.5*log10(MW_TRANSMISSION_X) / EBV
42
        # It is the same value for the N and S surveys in DR8 and DR9 catalogs.
43
        R = {"G_N": 3.2140,
×
44
             "R_N": 2.1650,
45
             "Z_N": 1.2110,
46
             "G_S": 3.2140,
47
             "R_S": 2.1650,
48
             "Z_S": 1.2110,
49
             "G_G": 2.512,
50
             "BP_G": 3.143,
51
             "RP_G": 1.663}
52
    else:
53
        # From https://desi.lbl.gov/trac/wiki/ImagingStandardBandpass
54
        # DECam u  3881.6   3.994
55
        # DECam g  4830.8   3.212
56
        # DECam r  6409.0   2.164
57
        # DECam i  7787.5   1.591
58
        # DECam z  9142.7   1.211
59
        # DECam Y  9854.5   1.063
60
        # BASS g  4772.1   3.258
61
        # BASS r  6383.6   2.176
62
        # MzLS z  9185.1   1.199
63
        # Consistent with the synthetic magnitudes and function dust_transmission
64

65
        R = {"G_N": 3.258,
1✔
66
             "R_N": 2.176,
67
             "Z_N": 1.199,
68
             "G_S": 3.212,
69
             "R_S": 2.164,
70
             "Z_S": 1.211,
71
             "G_G": 2.197,
72
             "BP_G": 2.844,
73
             "RP_G": 1.622}
74

75
    # Add WISE from
76
    # https://github.com/dstndstn/tractor/blob/main/tractor/sfd.py#L23-L35
77
    R.update({'W1_N': 0.184,
1✔
78
              'W2_N': 0.113,
79
              'W3_N': 0.0241,
80
              'W4_N': 0.00910,
81
              'W1_S': 0.184,
82
              'W2_S': 0.113,
83
              'W3_S': 0.0241,
84
              'W4_S': 0.00910})
85

86
    assert band.upper() in ["G", "R", "Z", "BP", "RP", 'W1', 'W2', 'W3', 'W4']
1✔
87
    assert photsys.upper() in ["N", "S", "G"]
1✔
88
    return R["{}_{}".format(band.upper(), photsys.upper())]
1✔
89

90

91
def mwdust_transmission(ebv, band, photsys, match_legacy_surveys=False):
1✔
92
    """Convert SFD E(B-V) value to dust transmission 0-1 for band and photsys
93

94
    Args:
95
        ebv (float or array-like): SFD E(B-V) value(s)
96
        band (str): 'G', 'R', 'Z', 'W1', 'W2', 'W3', or 'W4'
97
        photsys (str or array of str): 'N' or 'S' imaging surveys photo system
98

99
    Returns:
100
        scalar or array (same as ebv input), Milky Way dust transmission 0-1
101

102
    If `photsys` is an array, `ebv` must also be array of same length.
103
    However, `ebv` can be an array with a str `photsys`.
104

105
    Also see `dust_transmission` which returns transmission vs input wavelength
106
    """
107
    if isinstance(photsys, str):
1✔
108
        r_band = extinction_total_to_selective_ratio(band, photsys, match_legacy_surveys=match_legacy_surveys)
1✔
109
        a_band = r_band * ebv
1✔
110
        transmission = 10**(-a_band / 2.5)
1✔
111
        return transmission
1✔
112
    else:
113
        photsys = np.asarray(photsys)
1✔
114
        if np.isscalar(ebv):
1✔
115
            raise ValueError('array photsys requires array ebv')
1✔
116
        if len(ebv) != len(photsys):
1✔
117
            raise ValueError('len(ebv) {} != len(photsys) {}'.format(
1✔
118
                len(ebv), len(photsys)))
119

120
        transmission = np.zeros(len(ebv))
1✔
121
        for p in np.unique(photsys):
1✔
122
            ii = (photsys == p)
1✔
123
            r_band = extinction_total_to_selective_ratio(band, p, match_legacy_surveys=match_legacy_surveys)
1✔
124
            a_band = r_band * ebv[ii]
1✔
125
            transmission[ii] = 10**(-a_band / 2.5)
1✔
126

127
        return transmission
1✔
128

129

130
def ext_odonnell(wave, Rv=3.1):
1✔
131
    """Return extinction curve from Odonnell (1994), defined in the wavelength
132
    range [3030,9091] Angstroms.  Outside this range, use CCM (1989).
133

134
    Args:
135
        wave : 1D array of vacuum wavelength [Angstroms]
136
        Rv   : Value of R_V (scalar); default is 3.1
137

138
    Returns:
139
        1D array of A(lambda)/A(V)
140
    """
141

142
    # direct python translation of idlutils/pro/dust/ext_odonnell.pro
143

144
    A = np.zeros(wave.shape)
1✔
145
    xx = 10000. / wave
1✔
146

147
    optical_waves = (xx >= 1.1) & (xx <= 3.3)
1✔
148
    other_waves = (xx < 1.1) | (xx > 3.3)
1✔
149

150
    if np.sum(optical_waves) > 0:
1✔
151
        yy = xx[optical_waves] - 1.82
1✔
152
        afac = (1.0 + 0.104*yy - 0.609*yy**2 + 0.701*yy**3 + 1.137*yy**4 -
1✔
153
                1.718*yy**5 - 0.827*yy**6 + 1.647*yy**7 - 0.505*yy**8)
154
        bfac = (1.952*yy + 2.908*yy**2 - 3.989*yy**3 - 7.985*yy**4 +
1✔
155
                11.102*yy**5 + 5.491*yy**6 - 10.805*yy**7 + 3.347*yy**8)
156
        A[optical_waves] = afac + bfac / Rv
1✔
157
    if np.sum(other_waves) > 0:
1✔
158
        A[other_waves] = ext_ccm(wave[other_waves], Rv=Rv)
1✔
159

160
    return A
1✔
161

162

163
def ext_ccm(wave, Rv=3.1):
1✔
164
    """Return extinction curve from CCM (1989), defined in the wavelength
165
    range [1250,33333] Angstroms.
166

167
    Args:
168
        wave : 1D array of vacuum wavelength [Angstroms]
169
        Rv   : Value of R_V (scalar); default is 3.1
170

171
    Returns:
172
        1D array of A(lambda)/A(V)
173
    """
174

175
    # direct python translation of idlutils/pro/dust/ext_ccm.pro
176
    # numeric values checked with other implementation
177

178
    A = np.zeros(wave.shape)
1✔
179
    xx = 10000. / wave
1✔
180

181
    # Limits for CCM fitting function
182
    qLO = (xx > 8.0)                   # No data, lambda < 1250 Ang
1✔
183
    qUV = (xx > 3.3) & (xx <= 8.0)     # UV + FUV
1✔
184
    qOPT = (xx > 1.1) & (xx <= 3.3)    # Optical/NIR
1✔
185
    qIR = (xx > 0.3) & (xx <= 1.1)     # IR
1✔
186
    qHI = (xx <= 0.3)                  # No data, lambda > 33,333 Ang
1✔
187

188
    # For lambda < 1250 Ang, arbitrarily return Alam=5
189
    if np.sum(qLO) > 0:
1✔
190
        A[qLO] = 5.0
×
191

192
    if np.sum(qUV) > 0:
1✔
193
        xt = xx[qUV]
1✔
194
        afac = 1.752 - 0.316*xt - 0.104 / ((xt - 4.67)**2 + 0.341)
1✔
195
        bfac = -3.090 + 1.825*xt + 1.206 / ((xt - 4.62)**2 + 0.263)
1✔
196

197
        qq = (xt >= 5.9) & (xt <= 8.0)
1✔
198
        if np.sum(qq) > 0:
1✔
199
            Fa = -0.04473*(xt[qq]-5.9)**2 - 0.009779*(xt[qq]-5.9)**3
×
200
            Fb = 0.2130*(xt[qq]-5.9)**2 + 0.1207*(xt[qq]-5.9)**3
×
201
            afac[qq] += Fa
×
202
            bfac[qq] += Fb
×
203

204
        A[qUV] = afac + bfac / Rv
1✔
205

206
    if np.sum(qOPT) > 0:
1✔
207
        yy = xx[qOPT] - 1.82
1✔
208
        afac = (1.0 + 0.17699*yy - 0.50447*yy**2 - 0.02427*yy**3 +
1✔
209
                0.72085*yy**4 + 0.01979*yy**5 - 0.77530*yy**6 + 0.32999*yy**7)
210
        bfac = (1.41338*yy + 2.28305*yy**2 + 1.07233*yy**3 -
1✔
211
                5.38434*yy**4 - 0.62251*yy**5 + 5.30260*yy**6 - 2.09002*yy**7)
212
        A[qOPT] = afac + bfac / Rv
1✔
213

214
    if np.sum(qIR) > 0:
1✔
215
        yy = xx[qIR]**1.61
1✔
216
        afac = 0.574*yy
1✔
217
        bfac = -0.527*yy
1✔
218
        A[qIR] = afac + bfac / Rv
1✔
219

220
    # For lambda > 33,333 Ang, arbitrarily extrapolate the IR curve
221
    if np.sum(qHI) > 0:
1✔
222
        yy = xx[qHI]**1.61
×
223
        afac = 0.574*yy
×
224
        bfac = -0.527*yy
×
225
        A[qHI] = afac + bfac / Rv
×
226

227
    return A
1✔
228

229

230
def ext_fitzpatrick(wave, R_V=3.1, avglmc=False, lmc2=False,
1✔
231
                    x0=None, gamma=None,
232
                    c1=None, c2=None, c3=None, c4=None):
233
    """
234
    Return extinction curve from Fitzpatrick (1999).
235

236
    Args:
237
        wave : 1D array of vacuum wavelength [Angstroms]
238

239
    Returns:
240
        1D array of A(lambda)/A(V)
241

242
    OPTIONAL INPUT KEYWORDS
243
      R_V - scalar specifying the ratio of total to selective extinction
244
               R(V) = A(V) / E(B - V).  If not specified, then R = 3.1
245
               Extreme values of R(V) range from 2.3 to 5.3
246

247
      avglmc - if set, then the default fit parameters c1,c2,c3,c4,gamma,x0
248
             are set to the average values determined for reddening in the
249
             general Large Magellanic Cloud (LMC) field by Misselt et al.
250
             (1999, ApJ, 515, 128)
251
      lmc2 - if set, then the fit parameters are set to the values determined
252
             for the LMC2 field (including 30 Dor) by Misselt et al.
253
             Note that neither /AVGLMC or /LMC2 will alter the default value
254
             of R_V which is poorly known for the LMC.
255

256
    The following five input keyword parameters allow the user to customize
257
    the adopted extinction curve.  For example, see Clayton et al. (2003,
258
    ApJ, 588, 871) for examples of these parameters in different interstellar
259
    environments.
260

261
      x0 - Centroid of 2200 A bump in microns
262
           (default = 4.596)
263
      gamma - Width of 2200 A bump in microns
264
           (default = 0.99)
265
      c3 - Strength of the 2200 A bump
266
           (default = 3.23)
267
      c4 - FUV curvature
268
           (default = 0.41)
269
      c2 - Slope of the linear UV extinction component
270
           (default = -0.824 + 4.717 / R)
271
      c1 - Intercept of the linear UV extinction component
272
           (default = 2.030 - 3.007 * c2)
273

274
    NOTES:
275
       (1) The following comparisons between the FM curve and that of Cardelli,
276
           Clayton, & Mathis (1989), (see ccm_unred.pro):
277

278
           (a) - In the UV, the FM and CCM curves are similar for R < 4.0, but
279
                 diverge for larger R
280
           (b) - In the optical region, the FM more closely matches the
281
                 monochromatic extinction, especially near the R band.
282
       (2)  Many sightlines with peculiar ultraviolet interstellar extinction
283
               can be represented with the FM curve, if the proper value of
284
               R(V) is supplied.
285
    REQUIRED MODULES:
286
       scipy, numpy
287
    REVISION HISTORY:
288
       Written   W. Landsman        Raytheon  STX   October, 1998
289
       Based on FMRCurve by E. Fitzpatrick (Villanova)
290
       Added /LMC2 and /AVGLMC keywords,  W. Landsman   August 2000
291
       Added ExtCurve keyword, J. Wm. Parker   August 2000
292
       Assume since V5.4 use COMPLEMENT to WHERE  W. Landsman April 2006
293
       Ported to Python, C. Theissen August 2012
294
    """
295

296
    # copied by J. Guy from E. Schlafly fm_unred function
297

298
    from scipy.interpolate import CubicSpline
1✔
299

300
    x = 10000. / np.array(wave)  # Convert to inverse microns
1✔
301
    curve = np.zeros(x.shape)
1✔
302

303
    if lmc2:
1✔
304
        if x0 is None:
×
305
            x0 = 4.626
×
306
        if gamma is None:
×
307
            gamma = 1.05
×
308
        if c4 is None:
×
309
            c4 = 0.42
×
310
        if c3 is None:
×
311
            c3 = 1.92
×
312
        if c2 is None:
×
313
            c2 = 1.31
×
314
        if c1 is None:
×
315
            c1 = -2.16
×
316
    elif avglmc:
1✔
317
        if x0 is None:
×
318
            x0 = 4.596
×
319
        if gamma is None:
×
320
            gamma = 0.91
×
321
        if c4 is None:
×
322
            c4 = 0.64
×
323
        if c3 is None:
×
324
            c3 = 2.73
×
325
        if c2 is None:
×
326
            c2 = 1.11
×
327
        if c1 is None:
×
328
            c1 = -1.28
×
329
    else:
330
        if x0 is None:
1✔
331
            x0 = 4.596
1✔
332
        if gamma is None:
1✔
333
            gamma = 0.99
1✔
334
        if c4 is None:
1✔
335
            c4 = 0.41
1✔
336
        if c3 is None:
1✔
337
            c3 = 3.23
1✔
338
        if c2 is None:
1✔
339
            c2 = -0.824 + 4.717 / R_V
1✔
340
        if c1 is None:
1✔
341
            c1 = 2.030 - 3.007 * c2
1✔
342

343
    # Compute UV portion of A(lambda)/E(B-V) curve using FM fitting function and
344
    # R-dependent coefficients
345

346
    xcutuv = 10000.0 / 2700.0
1✔
347
    xspluv = 10000.0 / np.array([2700.0, 2600.0])
1✔
348

349
    iuv = x >= xcutuv
1✔
350
    iuv_comp = ~iuv
1✔
351

352
    if len(x[iuv]) > 0:
1✔
353
        xuv = np.concatenate((xspluv, x[iuv]))
×
354
    else:
355
        xuv = xspluv.copy()
1✔
356

357
    yuv = c1 + c2 * xuv
1✔
358
    yuv = yuv + c3 * xuv**2 / ((xuv**2 - x0**2)**2 + (xuv * gamma)**2)
1✔
359

360
    filter1 = xuv.copy()
1✔
361
    filter1[xuv <= 5.9] = 5.9
1✔
362

363
    yuv = yuv + c4 * (0.5392 * (filter1 - 5.9)**2 + 0.05644 * (filter1 - 5.9)**3)
1✔
364
    yuv = yuv + R_V
1✔
365
    yspluv = yuv[0:2].copy()  # save spline points
1✔
366

367
    if len(x[iuv]) > 0:
1✔
368
        curve[iuv] = yuv[2:len(yuv)]  # remove spline points
×
369

370
    # Compute optical portion of A(lambda)/E(B-V) curve
371
    # using cubic spline anchored in UV, optical, and IR
372

373
    xsplopir = np.concatenate(([0], 10000.0 / np.array([26500.0, 12200.0, 6000.0, 5470.0, 4670.0, 4110.0])))
1✔
374
    ysplir = np.array([0.0, 0.26469, 0.82925]) * R_V / 3.1
1✔
375
    ysplop = [np.polyval(np.array([2.13572e-04, 1.00270, -4.22809e-01]), R_V),
1✔
376
              np.polyval(np.array([-7.35778e-05, 1.00216, -5.13540e-02]), R_V),
377
              np.polyval(np.array([-3.32598e-05, 1.00184, 7.00127e-01]), R_V),
378
              np.polyval(np.array([-4.45636e-05, 7.97809e-04, -5.46959e-03, 1.01707, 1.19456]), R_V)]
379

380
    ysplopir = np.concatenate((ysplir, ysplop))
1✔
381

382
    if len(iuv_comp) > 0:
1✔
383
        cubic = CubicSpline(np.concatenate((xsplopir, xspluv)),
1✔
384
                            np.concatenate((ysplopir, yspluv)), bc_type='natural')
385
        curve[iuv_comp] = cubic(x[iuv_comp])
1✔
386

387
    return curve/R_V
1✔
388

389

390
#
391
# based on the work from https://ui.adsabs.harvard.edu/abs/2011ApJ...737..103S
392
# note from Eddie: I recommend applying the SF11 calibration in the following way:
393
# A(lambda, F99) = A(lambda, F99, rv)/A(1 micron, F99, rv) * SFD_EBV * 1.029.
394
# That's a definition that only uses monochromatic extinctions,
395
# so a lot of ambiguity in what extinction means goes away.
396
# That doesn't look like the 0.86 rescaling that we quote in abstract,
397
# but it's convenient because it uses only monochromatic extinctions.
398
#
399
def dust_transmission(wave, ebv_sfd, Rv=3.1):
1✔
400
    """
401
    Return the dust transmission [0-1] vs. wavelength.
402

403
    Args:
404
        wave : 1D array of vacuum wavelength [Angstroms]
405
        ebv_sfd : E(B-V) as derived from the map of Schlegel, Finkbeiner and Davis (1998)
406
        Rv : total-to-selective extinction ratio, defaults to 3.1
407

408
    Returns:
409
        1D array of dust transmission (between 0 and 1)
410

411
    The routine does internally multiply ebv_sfd by dust.ebv_sfd_scaling.
412
    The Fitzpatrick dust extinction law is used, given R_V (default 3.1).
413

414
    Also see `mwdust_transmission` which return transmission within a filter
415
    """
416
    extinction = ext_fitzpatrick(np.atleast_1d(wave), R_V=Rv) / ext_fitzpatrick(np.array([10000.]), R_V=Rv) * ebv_sfd * 1.029
1✔
417
    if np.isscalar(wave):
1✔
418
        extinction = float(extinction[0])
×
419
    return 10**(-extinction/2.5)
1✔
420

421
# The SFDMap and _Hemisphere classes and the _bilinear_interpolate and ebv
422
# functions below were copied on Nov/20/2016 from
423
# https://github.com/kbarbary/sfdmap/ commit: bacdbbd
424
# which was originally Licensed under an MIT "Expat" license:
425
#
426
# Copyright (c) 2016 Kyle Barbary
427
#
428
# Permission is hereby granted, free of charge, to any person obtaining a copy
429
# of this software and associated documentation files (the "Software"), to deal
430
# in the Software without restriction, including without limitation the rights
431
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
432
# copies of the Software, and to permit persons to whom the Software is
433
# furnished to do so, subject to the following conditions:
434
#
435
# The above copyright notice and this permission notice shall be included in all
436
# copies or substantial portions of the Software.
437
#
438
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
439
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
440
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
441
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
442
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
443
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
444
# SOFTWARE.
445
#
446

447

448
def _bilinear_interpolate(data, y, x):
1✔
449
    """Map a two-dimensional integer pixel-array at float coordinates.
450

451
    Parameters
452
    ----------
453
    data : :class:`~numpy.ndarray`
454
        Pixelized array of values.
455
    y : :class:`float` or :class:`~numpy.ndarray`
456
        y coordinates (each integer y is a row) of
457
        location in pixel-space at which to interpolate.
458
    x : :class:`float` or :class:`~numpy.ndarray`
459
        x coordinates (each integer x is a column) of
460
        location in pixel-space at which to interpolate.
461

462
    Returns
463
    -------
464
    :class:`float` or :class:`~numpy.ndarray`
465
        Interpolated data values at the passed locations.
466

467
    Notes
468
    -----
469
    Taken in full from https://github.com/kbarbary/sfdmap/
470
    """
471
    yfloor = np.floor(y)
1✔
472
    xfloor = np.floor(x)
1✔
473
    yw = y - yfloor
1✔
474
    xw = x - xfloor
1✔
475

476
    # pixel locations
477
    y0 = yfloor.astype(np.int32)
1✔
478
    y1 = y0 + 1
1✔
479
    x0 = xfloor.astype(np.int32)
1✔
480
    x1 = x0 + 1
1✔
481

482
    # clip locations out of range
483
    ny, nx = data.shape
1✔
484
    y0 = np.maximum(y0, 0)
1✔
485
    y1 = np.minimum(y1, ny-1)
1✔
486
    x0 = np.maximum(x0, 0)
1✔
487
    x1 = np.minimum(x1, nx-1)
1✔
488

489
    return ((1.0 - xw) * (1.0 - yw) * data[y0, x0] +
1✔
490
            xw * (1.0-yw) * data[y0, x1] +
491
            (1.0 - xw) * yw * data[y1, x0] +
492
            xw * yw * data[y1, x1])
493

494

495
class _Hemisphere(object):
1✔
496
    """Represents one of the hemispheres (in a single file).
497

498
    Parameters
499
    ----------
500
    fname : :class:`str`
501
        File name containing one hemisphere of the dust map.
502
    scaling : :class:`float`
503
        Multiplicative factor by which to scale the dust map.
504

505
    Attributes
506
    ----------
507
    data : :class:`~numpy.ndarray`
508
        Pixelated array of dust map values.
509
    crpix1, crpix2 : :class:`float`
510
        World Coordinate System: Represent the 1-indexed
511
        X and Y pixel numbers of the poles.
512
    lam_scal : :class:`int`
513
        Number of pixels from b=0 to b=90 deg.
514
    lam_nsgp : :class:`int`
515
        +1 for the northern hemisphere, -1 for the south.
516

517
    Notes
518
    -----
519
    Taken in full from https://github.com/kbarbary/sfdmap/
520
    """
521
    def __init__(self, fname, scaling):
1✔
522
        self.data, header = getdata(fname, header=True)
1✔
523
        self.data *= scaling
1✔
524
        self.crpix1 = header['CRPIX1']
1✔
525
        self.crpix2 = header['CRPIX2']
1✔
526
        self.lam_scal = header['LAM_SCAL']
1✔
527
        self.sign = header['LAM_NSGP']  # north = 1, south = -1
1✔
528

529
    def ebv(self, l, b, interpolate):
1✔
530
        """Project Galactic longitude/latitude to lambert pixels (See SFD98).
531

532
        Parameters
533
        ----------
534
        l, b : :class:`numpy.ndarray`
535
            Galactic longitude and latitude.
536
        interpolate : :class:`bool`
537
            If ``True`` use bilinear interpolation to obtain values.
538

539
        Returns
540
        -------
541
        :class:`~numpy.ndarray`
542
            Reddening values.
543
        """
544
        x = (self.crpix1 - 1.0 +
1✔
545
             self.lam_scal * np.cos(l) *
546
             np.sqrt(1.0 - self.sign * np.sin(b)))
547
        y = (self.crpix2 - 1.0 -
1✔
548
             self.sign * self.lam_scal * np.sin(l) *
549
             np.sqrt(1.0 - self.sign * np.sin(b)))
550

551
        # Get map values at these pixel coordinates.
552
        if interpolate:
1✔
553
            return _bilinear_interpolate(self.data, y, x)
1✔
554
        else:
555
            x = np.round(x).astype(np.int32)
1✔
556
            y = np.round(y).astype(np.int32)
1✔
557

558
            # some valid coordinates are right on the border (e.g., x/y = 4096)
559
            x = np.clip(x, 0, self.data.shape[1]-1)
1✔
560
            y = np.clip(y, 0, self.data.shape[0]-1)
1✔
561
            return self.data[y, x]
1✔
562

563

564
class SFDMap(object):
1✔
565
    """Map of E(B-V) from Schlegel, Finkbeiner and Davis (1998).
566

567
    Use this class for repeated retrieval of E(B-V) values when
568
    there is no way to retrieve all the values at the same time: It keeps
569
    a reference to the FITS data from the maps so that each FITS image
570
    is read only once.
571

572
    Parameters
573
    ----------
574
    mapdir : :class:`str`, optional, defaults to :envvar:`DUST_DIR`+``/maps``.
575
        Directory in which to find dust map FITS images, named
576
        ``SFD_dust_4096_ngp.fits`` and ``SFD_dust_4096_sgp.fits``.
577
        If not specified, the map directory is derived from the value of
578
        the :envvar:`DUST_DIR` environment variable, otherwise an empty
579
        string is used.
580
    north, south : :class:`str`, optional
581
        Names of north and south galactic pole FITS files. Defaults are
582
        ``SFD_dust_4096_ngp.fits`` and ``SFD_dust_4096_sgp.fits``
583
        respectively.
584
    scaling : :class:`float`, optional, defaults to 1
585
        Scale all E(B-V) map values by this multiplicative factor.
586
        Pass scaling=0.86 for the recalibration from
587
        `Schlafly & Finkbeiner (2011) <http://adsabs.harvard.edu/abs/2011ApJ...737..103S)>`_.
588

589
    Notes
590
    -----
591
    Modified from https://github.com/kbarbary/sfdmap/
592
    """
593
    def __init__(self, mapdir=None, north="SFD_dust_4096_ngp.fits",
1✔
594
                 south="SFD_dust_4096_sgp.fits", scaling=1.):
595

596
        if mapdir is None:
1✔
597
            dustdir = os.environ.get('DUST_DIR')
1✔
598
            if dustdir is None:
1✔
599
                log.critical('Pass mapdir or set $DUST_DIR')
1✔
600
                raise ValueError('Pass mapdir or set $DUST_DIR')
1✔
601
            else:
602
                mapdir = os.path.join(dustdir, 'maps')
×
603

604
        if not os.path.exists(mapdir):
1✔
605
            log.critical('Dust maps not found in directory {}'.format(mapdir))
1✔
606
            raise ValueError('Dust maps not found in directory {}'.format(mapdir))
1✔
607

608
        self.mapdir = mapdir
1✔
609

610
        # don't load maps initially
611
        self.fnames = {'north': north, 'south': south}
1✔
612
        self.hemispheres = {'north': None, 'south': None}
1✔
613

614
        self.scaling = scaling
1✔
615

616
    def ebv(self, *args, **kwargs):
1✔
617
        """Get E(B-V) value(s) at given coordinate(s).
618

619
        Parameters
620
        ----------
621
        coordinates : :class:`~astropy.coordinates.SkyCoord` or :class:`~numpy.ndarray`
622
            If one argument is passed, assumed to be an :class:`~astropy.coordinates.SkyCoord`
623
            instance, in which case the ``frame`` and ``unit`` keyword arguments are
624
            ignored. If two arguments are passed, they are treated as
625
            ``latitute, longitude`` (can be scalars or arrays or a tuple), in which
626
            case the frame and unit are taken from the passed keywords.
627
        frame : :class:`str`, optional, defaults to ``'icrs'``
628
            Coordinate frame, if two arguments are passed. Allowed values are any
629
            :class:`~astropy.coordinates.SkyCoord` frame, and ``'fk5j2000'`` and ``'j2000'``.
630
        unit : :class:`str`, optional, defaults to ``'degree'``
631
            Any :class:`~astropy.coordinates.SkyCoord` unit.
632
        interpolate : :class:`bool`, optional, defaults to ``True``
633
            Interpolate between the map values using bilinear interpolation.
634

635
        Returns
636
        -------
637
        :class:`~numpy.ndarray`
638
            Specific extinction E(B-V) at the given locations.
639

640
        Notes
641
        -----
642
        Modified from https://github.com/kbarbary/sfdmap/
643
        """
644
        # collect kwargs
645
        frame = kwargs.get('frame', 'icrs')
1✔
646
        unit = kwargs.get('unit', 'degree')
1✔
647
        interpolate = kwargs.get('interpolate', True)
1✔
648

649
        # ADM convert to a frame understood by SkyCoords
650
        # ADM (for backwards-compatibility)
651
        if frame in ('fk5j2000', 'j2000'):
1✔
652
            frame = 'fk5'
1✔
653

654
        # compatibility: treat single argument 2-tuple as (RA, Dec)
655
        if (
1✔
656
                (len(args) == 1) and (type(args[0]) is tuple)
657
                and (len(args[0]) == 2)
658
        ):
659
            args = args[0]
1✔
660

661
        if len(args) == 1:
1✔
662
            # treat object as already an astropy.coordinates.SkyCoords
663
            try:
1✔
664
                c = args[0]
1✔
665
            except AttributeError:
×
666
                raise ValueError("single argument must be "
×
667
                                 "astropy.coordinates.SkyCoord")
668

669
        elif len(args) == 2:
1✔
670
            lat, lon = args
1✔
671
            c = SkyCoord(lat, lon, unit=unit, frame=frame)
1✔
672

673
        else:
674
            raise ValueError("too many arguments")
×
675

676
        # ADM extract Galactic coordinates from astropy
677
        l, b = c.galactic.l.radian, c.galactic.b.radian
1✔
678

679
        # Check if l, b are scalar. If so, convert to 1-d arrays.
680
        # ADM use numpy.atleast_1d. Store whether the
681
        # ADM passed values were scalars or not
682
        return_scalar = not np.atleast_1d(l) is l
1✔
683
        l, b = np.atleast_1d(l), np.atleast_1d(b)
1✔
684

685
        # Initialize return array
686
        values = np.empty_like(l)
1✔
687

688
        # Treat north (b>0) separately from south (b<0).
689
        for pole, mask in (('north', b >= 0), ('south', b < 0)):
1✔
690
            if not np.any(mask):
1✔
691
                continue
1✔
692

693
            # Initialize hemisphere if it hasn't already been done.
694
            if self.hemispheres[pole] is None:
1✔
695
                fname = os.path.join(self.mapdir, self.fnames[pole])
1✔
696
                self.hemispheres[pole] = _Hemisphere(fname, self.scaling)
1✔
697

698
            values[mask] = self.hemispheres[pole].ebv(l[mask], b[mask],
1✔
699
                                                      interpolate)
700

701
        if return_scalar:
1✔
702
            return values[0]
1✔
703
        else:
704
            return values
1✔
705

706
    def __repr__(self):
1✔
707
        return ("SFDMap(mapdir={!r}, north={!r}, south={!r}, scaling={!r})"
×
708
                .format(self.mapdir, self.fnames['north'],
709
                        self.fnames['south'], self.scaling))
710

711

712
def ebv(*args, **kwargs):
1✔
713
    """Convenience function, equivalent to ``SFDMap().ebv(*args)``.
714
    """
715

716
    m = SFDMap(mapdir=kwargs.get('mapdir', None),
1✔
717
               north=kwargs.get('north', "SFD_dust_4096_ngp.fits"),
718
               south=kwargs.get('south', "SFD_dust_4096_sgp.fits"),
719
               scaling=kwargs.get('scaling', 1.))
720
    return m.ebv(*args, **kwargs)
1✔
721

722

723
def gaia_extinction(g, bp, rp, ebv_sfd):
1✔
724
    # return extinction of gaia magnitudes based on Babusiaux2018 (eqn1/tab1)
725
    # we assume the EBV the original SFD scale
726
    # we return A_G, A_BP, A_RP
727
    gaia_poly_coeff = {'G': [0.9761, -0.1704, 0.0086, 0.0011, -0.0438, 0.0013, 0.0099],
1✔
728
                       'BP': [1.1517, -0.0871, -0.0333, 0.0173, -0.0230, 0.0006, 0.0043],
729
                       'RP': [0.6104, -0.0170, -0.0026, -0.0017, -0.0078, 0.00005, 0.0006]}
730
    ebv = 0.86 * ebv_sfd  # Apply Schlafly+11 correction
1✔
731

732
    gaia_a0 = 3.1 * ebv
1✔
733
    # here I apply a second-order correction for extinction
734
    # i.e. I use corrected colors after 1 iteration to determine
735
    # the best final correction
736
    inmag = {'G': g, 'RP': rp, 'BP': bp}
1✔
737
    retmag = {'BP': bp * 1, 'RP': rp * 1, 'G': g * 1}
1✔
738
    for i in range(10):
1✔
739
        bprp = retmag['BP'] - retmag['RP']
1✔
740
        for band in ['G', 'BP', 'RP']:
1✔
741
            curp = gaia_poly_coeff[band]
1✔
742
            dmag = (np.poly1d(gaia_poly_coeff[band][:4][::-1])(bprp) +
1✔
743
                    curp[4] * gaia_a0 + curp[5]*gaia_a0**2 + curp[6]*bprp*gaia_a0)*gaia_a0
744
            retmag[band] = inmag[band] - dmag
1✔
745
    return {'G': inmag['G'] - retmag['G'],
1✔
746
            'BP': inmag['BP'] - retmag['BP'],
747
            'RP': inmag['RP'] - retmag["RP"]}
748

749

750
#
751
# Used for dust development debugging, but not a core part of the
752
# desiutil.dust module for end-users
753
#
754
def _get_ext_coeff(temp, photsys, band, ebv_sfd, rv=3.1):
1✔
755
    """
756
    Obtain extinction coeffiecient A_X/E(B-V)_SFD for black body spectrum
757
    with a given temperature observed with photsys and band and
758
    extinction ebv_sfd
759

760
    Args:
761
        temp (float): temperature in Kelvin
762
        photsys (str): N, S, or G (gaia)
763
        band (str): g,r,z (if photsys=N or S); G, BP, or RP if photsys=G
764
        ebv_sfd (float): E(B-V) from SFD dust map
765
        Rv (float) : Value of extinction law R_V; default is 3.1
766

767
    Returns extinction coefficient A_X / E(B-V)_SFD
768

769
    Note: this is currently a _hidden function due to its speclite dependency,
770
    but it could be promoted if needed by external libraries.
771
    """
772
    wave = np.linspace(2900, 11000, 4000)
×
773
    sed = 1. / (wave/6000)**5 / (np.exp(143877687. / wave / temp) - 1)
×
774
    # code to use Munari library
775
    # http://cdsarc.u-strasbg.fr/ftp/J/A+A/442/1127/disp10A/fluxed_spectra/T_07000/
776
    # wave = np.loadtxt('LAMBDA_D10.DAT.txt')
777
    # wave = np.r_[wave, 10999]
778
    # sed = np.loadtxt('T07000G40M10V000K2SNWNVD10F.ASC')
779
    # sed = np.r_[sed, sed[-1]]
780

781
    trans = dust_transmission(wave, ebv_sfd)
×
782

783
    # import speclite only if needed
784
    import speclite.filters
×
785

786
    if photsys == "N":
×
787
        if band.upper() in ["G", "R"]:
×
788
            filtername = "BASS-{}".format(band.lower())
×
789
        else:
790
            filtername = "MzLS-z"
×
791
    elif photsys == "S":
×
792
        filtername = "decam2014-{}".format(band.lower())
×
793
    elif photsys == "G":
×
794
        filtername = "gaiadr2-{}".format(band)
×
795
    else:
796
        raise Exception('unrecognized photsys')
×
797

798
    filter_response = speclite.filters.load_filter(filtername)
×
799

800
    fluxunits = 1e-17 * u.erg / u.s / u.cm**2 / u.Angstrom
×
801
    mag_no_extinction = filter_response.get_ab_magnitude(sed * fluxunits,
×
802
                                                         wave)
803
    mag_with_extinction = filter_response.get_ab_magnitude(sed * trans * fluxunits, wave)
×
804

805
    # f = np.interp(wave, filter_response.wavelength,filter_response.response)
806
    # fwave = np.sum(sed * f * wave)/np.sum(sed * f)
807
    Rbis = (mag_with_extinction-mag_no_extinction)/ebv_sfd
×
808
    return Rbis
×
809

810

811
def _main():
1✔
812
    # Wrapper for development debugging of extinction coeffs
813
    import argparse
×
814
    import matplotlib.pyplot as plt
×
815

816
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter,
×
817
                                     description="Runs and displays a comparison between tabulated values of total to selective extinction ratios and values obtained with synthetic mags.")
818
    args = parser.parse_args()
×
819

820
    plt.figure("reddening")
×
821

822
    wave = np.linspace(3000-1, 11000, 1000)
×
823
    rv = 3.1
×
824
    ebv_sfd = 1.
×
825
    trans = dust_transmission(wave, ebv_sfd)
×
826
    ext = -2.5*np.log10(trans)
×
827
    plt.plot(wave, ext, label="-2.5*log10(dust_transmission)")
×
828

829
    # load filters and compute extinctions here
830
    try:
×
831
        import speclite.filters
×
832

833
        temp = 7000
×
834
        sed = 1./(wave/6000)**5/(np.exp(143877687./wave/temp)-1)  # dummy star with a spectrum close to a 7000K star (for wavelength>4000A)
×
835

836
        photsys_survey = {"N": "BASS+MZLS", "S": "DECALS", "G": "Gaia"}
×
837
        count = 0
×
838

839
        photsys_bands = list(itertools.product('NS', 'GRZ')) + list(itertools.product('G', ['G', 'BP', 'RP']))
×
840
        for photsys, band in photsys_bands:
×
841
            count += 1
×
842
            color = "C{}".format(count % 10)
×
843

844
            if photsys == "N":
×
845
                if band.upper() in ["G", "R"]:
×
846
                    filtername = "BASS-{}".format(band.lower())
×
847
                else:
848
                    filtername = "MzLS-z"
×
849
            elif photsys == "S":
×
850
                filtername = "decam2014-{}".format(band.lower())
×
851
            elif photsys == "G":
×
852
                filtername = "gaiadr2-{}".format(band)
×
853

854
            R = extinction_total_to_selective_ratio(band, photsys)
×
855

856
            Rbis = _get_ext_coeff(temp, photsys, band, ebv_sfd, rv=3.1)
×
857

858
            filter_response = speclite.filters.load_filter(filtername)
×
859

860
            f = np.interp(wave, filter_response.wavelength, filter_response.response)
×
861
            fwave = np.sum(sed*f*wave)/np.sum(sed*f)
×
862

863
            if count == 1:
×
864
                label1 = "with extinction_total_to_selective_ratio"
×
865
            else:
866
                label1 = None
×
867

868
            plt.plot(fwave, R, "x", color=color, label=label1)
×
869

870
            f = np.interp(wave, filter_response.wavelength, filter_response.response)
×
871
            ii = (f > 0.01)
×
872
            plt.plot(wave[ii], f[ii], "--", color=color, label=filtername)
×
873
            print("R_{}({}:{})= {:4.3f} =? {:4.3f}, delta = {:5.4f}".format(band, photsys, photsys_survey[photsys], R, Rbis, R-Rbis))
×
874

875
    except ImportError as e:
876
        print(e)
877
        print("Cannot import speclite, so no broadband comparison")
878

879
    plt.legend(title="For Rv={}".format(rv))
×
880
    plt.xlabel("wavelength (A)")
×
881
    plt.ylabel("A(wavelength)/E(B-V)")
×
882
    plt.grid()
×
883
    plt.show()
×
884

885

886
if __name__ == '__main__':
1✔
887
    _main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc