Skip to content

Dust extinction

Interstellar dust absorbs and scatters starlight, more strongly in the blue than in the red. The dimming at wavelength \(\lambda\) is the extinction \(A(\lambda)\), in magnitudes, so the observed flux is

\[ F_\mathrm{obs}(\lambda) = F_\mathrm{int}(\lambda)\, 10^{-0.4\,A(\lambda)} . \]

An extinction law gives the shape of \(A(\lambda)\). Its amount is set by the extinction in the V band, \(A_V\), or by the colour excess \(E(B-V) = A_B - A_V\), the reddening of the B−V colour. The two are linked by \(R_V = A_V / E(B-V)\), which describes the grain-size distribution: 3.1 for the average diffuse Milky Way, larger in dense clouds.

torchsky.extinction evaluates these laws on tensors and returns either \(A(\lambda)\) (a_lambda) or the multiplicative factor \(10^{-0.4A}\) (transmission). \(E(B-V)\), \(A_V\), and \(R_V\) may be tensors with gradients, one per object.

Use it to correct spectra and photometry for Milky Way foreground dust, and to model dust in the source itself (a host galaxy) as part of a fit.

How It Works

The Milky Way laws are parameterized by \(R_V\) in inverse wavelength \(x = 1/\lambda\) (µm⁻¹). For CCM89 and O'Donnell (1994),

\[ \frac{A(\lambda)}{A_V} = a(x) + \frac{b(x)}{R_V}, \]

with \(a\) and \(b\) piecewise polynomials in the infrared, optical, and ultraviolet. F99 uses the Fitzpatrick and Massa ultraviolet form plus a cubic spline through optical and infrared anchor points that also depend on \(R_V\). Calzetti (2000) is an empirical attenuation curve for star-forming galaxies, which mixes dust and stars rather than placing a screen in front of them.

law Reference Use for Range
"ccm89" Cardelli, Clayton & Mathis (1989) Milky Way, default 0.1–3.3 µm (\(x = 0.3\)–10); IR power law extrapolated below, far-UV frozen above
"odonnell94" O'Donnell (1994) Milky Way, improved optical Optical polynomial replaced; UV and IR as CCM89
"f99" Fitzpatrick (1999) Milky Way, the law behind SFD/SF11 recalibrations Calibrated over its spline knots; outside is extrapolation
"calzetti" Calzetti et al. (2000) Star-forming host galaxies, \(R_V = 4.05\) default 0.12–2.2 µm; clamped outside
"g23" Gordon, Clayton, Decleir & Fitzpatrick (2023, ApJ 950, 86) Milky Way, current average from the far-UV to the mid-IR, with silicate features 912 Å–32 µm, \(2.3 \le R_V \le 5.6\); raises outside
"f19" Fitzpatrick, Massa, Gordon et al. (2019, ApJ 886, 108) Milky Way, HST/STIS spectrophotometric optical and UV 1150 Å–3.3 µm, \(2 \le R(55) \le 6\); raises outside
"gcc09" Gordon, Cartledge & Clayton (2009, ApJ 705, 1320; erratum 2014) Milky Way ultraviolet down to the Lyman limit 909–3030 Å only, \(2 \le R_V \le 6\); raises outside

G23 combines a broken power law with silicate features (infrared), a polynomial with three intermediate-scale features (optical), and the Fitzpatrick–Massa form (ultraviolet), joined smoothly at 1 µm and 0.3 µm; like dust_extinction, it includes the 0.9854 renormalization to the HST/STIS scale. F19 is a cubic spline through its tabulated \(k(\lambda-55)\) and is normalized at 5500 Å, so a_v and r_v are \(A(55)\) and \(R(55)\) there. GCC09 was derived for the ultraviolet only. The coefficients of all three are those of the papers as implemented in dust_extinction (BSD-3-Clause). These newer laws fail closed: a wavelength or \(R_V\) outside the calibrated range raises ValueError instead of extrapolating.

Assumptions and Accuracy

  • One foreground screen per object: no scattering into the beam, no emission, no mixing of dust and stars (Calzetti aside, which is empirical).
  • Wavelengths are Angstrom unless wavelength_unit="nm"; for a host-galaxy screen they are rest-frame wavelengths.
  • \(A(\lambda)\) is clamped at zero in transmission, so a screen never brightens a source.
  • G23, F19, and GCC09 match dust_extinction to \(10^{-12}\) relative across their ranges and \(R_V\) limits. F99 agrees with it to 2 per cent from 1500 to 8000 Å, and CCM89 matches its V-band anchor, in the test suite.

A Real Spectrum: Galactic Dust Correction

The Schlafly & Finkbeiner (2011) map gives \(E(B-V) = 0.0698\) toward the SDSS quasar on plate 0751, fibre 160. Removing the dust is a multiplication by \(10^{+0.4A}\):

# fragment: needs spec-0751-52251-0160.fits, SDSS.g.dat and SDSS.r.dat (see photometry.md)
import torch
import torchfits

from torchsky.extinction import a_lambda
from torchsky.filter_profiles import bandpass_from_transmission_file
from torchsky.spectrophotometry import Passband, synthetic_photometry_bands

coadd = torchfits.read("spec-0751-52251-0160.fits", hdu=1)
wave = 10.0 ** coadd["loglam"].to(torch.float64)
flux = coadd["flux"].to(torch.float64) * 1e-17
good = coadd["ivar"] > 0

dereddened = flux * 10.0 ** (0.4 * a_lambda(wave, ebv=0.0698, law="f99"))

bands = []
for name in "gr":
    curve = bandpass_from_transmission_file(f"SDSS.{name}.dat", band_name=name)
    bands.append(Passband.from_samples(curve["transmission_wavelength"].double(),
                                       curve["transmission_throughput"].double(), name=name))
mags = synthetic_photometry_bands(wave, torch.stack([flux, dereddened]), bands,
                                  output="ab_mag", mask=good.expand(2, -1), min_coverage=0.9)
print(mags.value)   # [[17.470, 16.776], [17.213, 16.597]]: g-r is 0.077 mag bluer

The g band brightens by 0.26 mag and r by 0.18 mag. The complete analysis is examples/sdss_spectrum_photometry.py.

A Dust Map at Catalogue Positions

For a catalogue, E(B−V) comes from a dust map sampled at each star. The Taurus example samples the SFD map (HEALPix, nside 512, Galactic) at 4831 Gaia DR3 positions and applies G23 at the Gaia pivot wavelengths:

# fragment: sfd is the SFD map column read with torchfits; ra, dec, bp, rp are Gaia columns
from torchsky.coords import transform
from torchsky.extinction import a_lambda
from torchsky.sphere import sample_healpix_map

l, b = transform(ra, dec, from_frame="ICRS", to_frame="GALACTIC")
ebv = 0.86 * sample_healpix_map(sfd, 512, l, b, nest=True)      # Schlafly & Finkbeiner 2011
per_ebv = a_lambda(torch.tensor([6217.59, 5109.71, 7769.02]), ebv=1.0, law="g23")
bp_rp_0 = (bp - rp) - ebv * (per_ebv[1] - per_ebv[2])           # E(BP-RP) = 1.49 E(B-V)

Behind the clouds the main sequence narrows from 0.19 to 0.10 mag (NMAD). A dust-map scale fitted by gradient descent through the same operations narrows it to 0.08 mag at 0.60 times the rescaled map: SFD over-predicts reddening in Taurus, and pivot wavelengths overstate the band extinction of red stars.

SFD E(B-V) around the Taurus field with the Gaia stars, and the colour-magnitude diagram before and after G23 dereddening

Recipes

Extinction at Given Wavelengths

import torch

from torchsky.extinction import a_lambda

wave = torch.tensor([3500.0, 5510.0, 8000.0], dtype=torch.float64)
a = a_lambda(wave, ebv=0.5, r_v=3.1)                # CCM89 by default
assert abs(float(a[1]) - 1.55) < 0.1               # A_V = R_V E(B-V) at 5510 A
assert a[0] > a[1] > a[2]                          # blue is extinguished more

same = a_lambda(torch.tensor([551.0]), ebv=0.5, r_v=3.1, wavelength_unit="nm")
torch.testing.assert_close(same, a[1:2].float(), atol=1e-5, rtol=0.0)

Redden or Deredden a Batch

A per-object ebv of shape (B,) broadcasts against a shared (L,) grid to (B, L). apply_extinction is the one-call form of the multiplication by transmission.

import torch

from torchsky.extinction import a_lambda, transmission

wave = torch.linspace(4000.0, 8000.0, 5, dtype=torch.float64)
intrinsic = torch.ones(2, wave.numel(), dtype=torch.float64)
ebv = torch.tensor([0.0, 1.0], dtype=torch.float64)

reddened = intrinsic * transmission(wave, ebv, law="f99")
assert reddened.shape == (2, 5) and torch.all(reddened[1] < reddened[0])
restored = reddened * 10.0 ** (0.4 * a_lambda(wave, ebv=ebv, law="f99"))
torch.testing.assert_close(restored, intrinsic)

Fit E(B−V) by Gradient Descent

import torch

from torchsky.extinction import a_lambda, transmission

wave = torch.linspace(3500.0, 9000.0, 300, dtype=torch.float64)
intrinsic = (wave / 5500.0) ** -1.0
observed = intrinsic * transmission(wave, 0.3, law="f99")

ebv = torch.tensor(0.0, dtype=torch.float64, requires_grad=True)
optimizer = torch.optim.LBFGS([ebv], max_iter=50, line_search_fn="strong_wolfe")


def closure():
    optimizer.zero_grad()
    model = intrinsic * 10.0 ** (-0.4 * a_lambda(wave, ebv=ebv, law="f99"))
    loss = (torch.log(model) - torch.log(observed)).square().sum()
    loss.backward()
    return loss


optimizer.step(closure)
assert abs(float(ebv) - 0.3) < 1e-4

Host-Galaxy Dust Before Redshifting

Evaluate a host screen at rest-frame wavelength, then shift. The normalized curve k_lambda, \(A(\lambda)/A_V\), is convenient as a prior shape.

import torch

from torchsky.extinction import calzetti, k_lambda
from torchsky.redshift import shift_wavelength

rest = torch.linspace(1500.0, 9000.0, 200, dtype=torch.float64)
sed = (rest / 5500.0) ** -2.0
attenuated = sed * 10.0 ** (-0.4 * calzetti(rest, a_v=0.8))   # host dust, rest frame
observed_wave = shift_wavelength(rest, 1.0 + 1.5)               # then redshift to z = 1.5

k = k_lambda(torch.tensor([5510.0], dtype=torch.float64), law="ccm89")
assert abs(float(k) - 1.0) < 0.1 and observed_wave[0] == 3750.0

Compare Laws

The laws agree in the optical and diverge in the ultraviolet, where the choice matters most.

import torch

from torchsky.extinction import a_lambda

uv = torch.tensor([2000.0], dtype=torch.float64)
laws = ("ccm89", "odonnell94", "f99", "calzetti", "g23", "f19", "gcc09")
values = {law: float(a_lambda(uv, a_v=1.0, law=law)) for law in laws}
assert max(values.values()) - min(values.values()) > 0.1
print(values)

Pitfalls

  • Magnitudes, not factors. a_lambda returns magnitudes; the flux factor is \(10^{-0.4A}\).
  • Frame. Milky Way dust acts at observed wavelength; host dust at rest wavelength. Mixing them shifts features by \(1+z\).
  • \(E(B-V)\) maps are calibrated for one law. The Schlafly & Finkbeiner (2011) recalibration of the SFD map assumes F99 with \(R_V = 3.1\); use the same law when applying it.
  • Range. Check that your wavelengths lie within the calibrated interval of the law before relying on far-UV or infrared corrections.

See Also