---
title: "Methane plume detection in Python with NASA EMIT: mapping and quantifying landfill plumes"
description: "Detect methane plumes in Python with NASA EMIT hyperspectral data: map the enhancement, compute IME and estimate emission rates with ERA5 winds."
url: https://www.technicalgis.com/tutorials/methane-plume-detection-emit-python/
author: TechnicalGIS Team
publisher: TechnicalGIS (https://www.technicalgis.com/about/)
published: 2026-10-23
updated: 2026-10-23
language: en-GB
---

# Methane plume detection in Python with NASA EMIT: mapping and quantifying landfill plumes

> Detect methane plumes in Python with NASA EMIT hyperspectral data: map the enhancement, compute IME and estimate emission rates with ERA5 winds.

- Level: Advanced
- Tested with: Python 3.12
- Source code: [Download scripts](https://www.technicalgis.com/code/methane-plume-detection-emit-python/)

## Key takeaways

1.  EMIT, NASA's imaging spectrometer on the International Space Station, maps methane as an enhancement in ppm m at about 60 m resolution. The full L2B products need a NASA Earthdata login, but NASA's open VEDA platform serves an EMIT landfill subset under CC0 that you can read with pystac-client and rasterio without an account.
2.  Single EMIT pixels are noisy: the background spread over Dallas-Fort Worth on 25 August 2023 is 511 ppm m (1.4826 × MAD), so one pixel needs about 1,500 ppm m to stand out at 3σ. A 1.5-pixel Gaussian filter cuts the noise to 238 ppm m and makes the plumes segmentable.
3.  The integrated mass enhancement (IME) is the excess methane in the plume: 99–249 kg for the three landfill plumes with a 2σ mask, using 0.6 mg CH₄ m⁻² per ppm m at the ERA5 pressure and 38.7 °C air temperature.
4.  With Q = U₁₀ · IME / L and ERA5 10 m winds of only 0.5–1.2 m/s, the snapshots give 614–1,601 kg/h per landfill and 3,098 kg/h (range 2,222–4,976) for all three, against an EPA GHGRP 2023 annual average of 3,407 kg/h. Agreement in total hides disagreement site by site.
5.  The answer depends heavily on choices you make: the mask threshold, the plume length, the wind product and, above all, wind speed in calm conditions. Report a range, not a single number, and treat one overpass as a snapshot, not an annual rate.

## Before you start

-   Comfortable with NumPy arrays and reading rasters with rasterio
-   Some familiarity with STAC searches (see the Sentinel-2 STAC tutorial) and with ERA5 in xarray
-   Python 3.10+ with pystac-client, planetary-computer, rasterio, rioxarray, xarray, zarr, gcsfs, scipy, shapely, pyproj, pandas, matplotlib and requests
-   An internet connection: the script downloads about 10 MB from NASA, Microsoft Planetary Computer, Google Cloud and the US EPA. No accounts or keys are needed

**Short answer:** to detect a methane plume in Python, load a satellite methane enhancement raster (here NASA **EMIT** data in ppm m), subtract the background, smooth the noise and threshold it into a plume mask connected to the source. Sum enhancement × pixel area inside the mask to get the **integrated mass enhancement (IME)** in kilograms, then estimate the emission rate as **Q = U · IME / L**, with U a wind speed from **ERA5** and L the plume length. For three Dallas-Fort Worth landfills seen by EMIT on 25 August 2023, this gives **614 to 1,601 kg of methane per hour per site** in very light winds, with uncertainties of tens of per cent at best.

Methane is the second most important greenhouse gas from human activity, and much of it comes from a small number of large point sources: oil and gas facilities, coal mines and landfills. Since 2022, imaging spectrometers in orbit have been able to see individual plumes from space. This tutorial uses real EMIT data and works through the whole chain from raster to emission estimate, with every number produced by the script. It spends as much time on the uncertainties as on the code, because methane emission estimates are easy to over-read.

You will learn how to:

1.  find **open EMIT methane data** in NASA’s VEDA STAC catalogue and read it with rasterio, without a login;
2.  match plumes to JPL’s public **plume-complex** metadata and to EPA-reported landfills;
3.  measure the **noise** of the enhancement field and set a detection threshold;
4.  **segment** each plume with smoothing, thresholds and connected components;
5.  get the **wind, pressure and temperature** at the overpass from ERA5;
6.  compute the **IME** and an **emission rate**, with a range, and compare it with the **EPA inventory**.

All outputs below are real results from [`emit_methane_plumes.py`](https://www.technicalgis.com/code/methane-plume-detection-emit-python/), run on 23 October 2026 with Python 3.12.0, rasterio 1.5.2, xarray 2026.9.0, scipy 1.18.1, numpy 2.5.4 and pystac-client 0.9.1.

## How satellites see methane

Methane absorbs sunlight in narrow lines in the shortwave infrared, most strongly around 2,300 nm. An **imaging spectrometer** records a full spectrum for each pixel, so a retrieval can look for that absorption pattern. EMIT (Earth Surface Mineral Dust Source Investigation) was built to map minerals in dust source regions, but its 285 bands from 380 to 2,500 nm and roughly 60 m pixels turned out to be very good at methane. It has flown on the International Space Station since July 2022.

EMIT’s methane retrieval is a **matched filter**: each spectrum is compared with the expected methane absorption signature, scaled to the scene’s own spectral variability. The result, the L2B CH4ENH product, is an **enhancement** in **ppm m**: extra methane above the local background, integrated along the light path. A value of 1,000 ppm m means the same extra methane as 1 ppm more through a 1 km layer. JPL scientists then review the scenes and draw outlines round plumes, which become the L2B CH4PLM **plume complexes**.

## Step 1: Load EMIT methane data from NASA VEDA

VEDA’s STAC API at `openveda.cloud` has a collection called `landfill-emit`, built for the US Greenhouse Gas Center. It holds two scenes. The larger one, from **25 August 2023**, covers three landfills south and north of Dallas, Texas.

The Cloud-Optimised GeoTIFFs sit in a bucket that is not public, but VEDA’s TiTiler raster API can stream any item as a GeoTIFF. Asking for a `max_size` at least as large as the native image returns every pixel at full resolution, as float32 values with a second band for the valid-data mask.

`emit_methane_plumes.py`

```python
VEDA_STAC = "https://openveda.cloud/api/stac"
VEDA_RASTER = "https://openveda.cloud/api/raster"

veda = pystac_client.Client.open(VEDA_STAC)
col = veda.get_collection("landfill-emit")
items = sorted(veda.search(collections=[col.id]).items(), key=lambda i: i.datetime)
item = next(i for i in items if i.id == "EMIT_landfill_cog_2023-08-25")
h, w = item.assets["cog_default"].extra_fields["proj:shape"]

tif = get(f"{VEDA_RASTER}/collections/{item.collection_id}/items/{item.id}/preview.tif",
          DATA / f"{item.id}.tif", assets="cog_default", max_size=max(h, w))
with rasterio.open(tif) as src:
    ch4 = src.read(1).astype("float64")
    valid = (src.read(2) > 0) & (ch4 > -9000)
    transform, bounds = src.transform, src.bounds
ch4[~valid] = np.nan
```

We also need the area of each pixel to turn ppm m into a mass. The grid is in degrees, so the script computes the true area of a pixel on the WGS84 ellipsoid for every row with `pyproj.Geod`.

`Output — EMIT data from VEDA`

```text
collection: landfill-emit | EMIT Landfill Plumes | licence CC0-1.0
  EMIT_landfill_cog_2023-06-22: 2023-06-22 | shape [26, 61] | bbox [-121.191, 37.869, -121.158, 37.883] | 81.7% valid
  EMIT_landfill_cog_2023-08-25: 2023-08-25 | shape [956, 592] | bbox [-96.994, 32.521, -96.673, 33.04] | 7.2% valid
read EMIT_landfill_cog_2023-08-25.tif: 592 x 956 px, EPSG:4326, pixel 0.000542 deg | native shape matched: True
15% of horizontally adjacent valid pixels hold identical values (EMIT's orthorectification repeats ~60 m sensor pixels on the output grid)
valid pixels: 40,911 (7.2% of the window) | pixel area 3,055 m2 (~55 m)
enhancement over valid pixels: median 101, 1st pct -1101, 99th pct 1494, max 3096 ppm m
```

Two details matter later. Only 7.2% of the window has data, because the product is clipped to the plume outlines. And 15% of neighbouring pixels have identical values: EMIT’s orthorectification places the roughly 60 m sensor pixels on a slightly finer grid, so some are repeated. Neighbouring pixels are therefore not fully independent.

## Step 2: Match the plumes to JPL plume complexes and landfills

JPL publishes the metadata for every EMIT plume complex as a single public GeoJSON file used by its greenhouse gas portal. Each plume complex appears twice, as a point at the maximum enhancement and as an outline polygon, with the time, the maximum in ppm m and, for some, an emission estimate. The script keeps the polygons for the three plume complexes inside the scene and matches each to the nearest landfill that reports to the EPA’s Greenhouse Gas Reporting Program (GHGRP).

`emit_methane_plumes.py`

```python
meta = json.loads(get(JPL_PLUMES, DATA / "emit_combined_plume_metadata.json").read_text())
jpl = {}
for f in meta["features"]:
    p = f["properties"]
    if not p["UTC Time Observed"].startswith("2023-08-25"):
        continue
    pt_lon, pt_lat = p["Longitude of max concentration"], p["Latitude of max concentration"]
    if bounds.left <= pt_lon <= bounds.right and bounds.bottom <= pt_lat <= bounds.top:
        j = jpl.setdefault(p["Plume ID"], {"id": p["Plume ID"], "time": p["UTC Time Observed"], "lat": pt_lat,
                                           "lon": pt_lon, "max_ppmm": p["Max Plume Concentration (ppm m)"],
                                           "q_jpl": p["Emissions Rate Estimate (kg/hr)"], "geom": None})
        if f["geometry"]["type"] in ("Polygon", "MultiPolygon"):
            j["geom"] = shape(f["geometry"])
```

`Output — plume complexes`

```text
3,372 plume-complex features in the JPL metadata file
McCommas Bluff: CH4_PlumeComplex-1109 at 2023-08-25T17:06:09Z | max 3274 ppm m at (32.6792, -96.7289), 0.6 km from the GHGRP location | JPL emission estimate: NA
Skyline (Ferris): CH4_PlumeComplex-1111 at 2023-08-25T17:06:09Z | max 2298 ppm m at (32.5480, -96.6958), 2.8 km from the GHGRP location | JPL emission estimate: NA
Camelot (Lewisville): CH4_PlumeComplex-1108 at 2023-08-25T17:05:57Z | max 2981 ppm m at (33.0317, -96.9550), 1.6 km from the GHGRP location | JPL emission estimate: NA
```

The overpass was at about 12:06 local time. JPL has not published emission rates for these three plumes, so we will make our own. The match to landfills is by distance only. McCommas Bluff, the City of Dallas landfill, is a clear match; for Skyline the maximum is 2.8 km from the GHGRP reference point, which is plausible for a large site but should be checked against the site boundary before you attribute emissions to it.

![Three map panels of EMIT methane plumes, coloured from purple to yellow by enhancement, over Sentinel-2 true colour imagery of the Camelot, McCommas Bluff and Skyline landfills near Dallas](https://www.technicalgis.com/_astro/01-emit-plumes-landfills.BWTFokLW_Z1rvbn1.webp)

_The three EMIT plumes on 25 August 2023, shown with their 1.5σ masks over a cloud-free Sentinel-2 image from 19 August 2023. Triangles are the facility locations reported to the EPA. All three plumes drift west and south-west from the landfills._

## Step 3: Measure the noise

A methane retrieval is noisy because the surface spectrum varies from pixel to pixel. Before calling anything a plume we need the background level and spread. The usual approach is to sample plume-free pixels, but this product is clipped to the plume outlines. The outlines are generous, though, and most of the pixels inside them are background. Iterative **3σ clipping** with a robust spread estimate (1.4826 × the median absolute deviation) recovers the background without the plume tail.

`emit_methane_plumes.py`

```python
x = ch4[valid]
for it_ in range(10):
    med = np.median(x)
    sigma = 1.4826 * np.median(np.abs(x - med))
    keep = np.abs(ch4[valid] - med) < 3 * sigma
    if keep.sum() == x.size:
        break
    x = ch4[valid][keep]
```

`Output — background noise`

```text
CH4_PlumeComplex-1108: outline 5,509 px, of which 5,475 carry data
CH4_PlumeComplex-1109: outline 16,307 px, of which 16,250 carry data
CH4_PlumeComplex-1111: outline 18,953 px, of which 18,925 carry data
sigma clipping converged after 3 iterations: median 97 ppm m | robust sigma (1.4826 x MAD) 511 ppm m | 0.62% of pixels above +3 sigma, 0.18% below -3 sigma (Gaussian: 0.13% each)
one-pixel detection limit at 3 sigma: 1,533 ppm m above background
```

The outline pixel counts confirm the clipping: almost every pixel inside each outline has data, and nothing outside. The background is close to Gaussian with σ = 511 ppm m. There are five times as many pixels above +3σ as a Gaussian would give (0.62% against 0.13%): that excess is the methane. But it also shows that **a single pixel needs about 1,500 ppm m to stand out**, which is roughly half of the plume maxima.

![Left: histogram of EMIT methane enhancement on a log scale with a Gaussian fit; right: a Q-Q plot where plume pixels lie well above the Gaussian line](https://www.technicalgis.com/_astro/02-noise-histogram.BvOiFpvW_Z1d2uyz.webp)

_The enhancement field is mostly Gaussian noise with σ ≈ 511 ppm m (left). The Q-Q plot (right) shows pixels outside the plume masks following the Gaussian line apart from a slightly heavy upper tail, while plume pixels sit hundreds of ppm m higher._

## Step 4: Segment the plumes

Plumes are spatially coherent and noise is not, so smoothing helps. A Gaussian filter with σ = 1.5 pixels, normalised by the smoothed valid-data mask so that edges are not pulled towards zero, cuts the noise by more than half. We then threshold the smoothed field at k × its noise and keep the connected regions that reach within 300 m of the **plume origin**: the smoothed peak within 1 km of JPL’s maximum pixel. Running this at 1.5σ, 2σ and 3σ gives nested masks and a feel for how sensitive the result is.

`emit_methane_plumes.py`

```python
filled = np.where(valid, ch4 - med, 0.0)
weight = ndimage.gaussian_filter(valid.astype(float), 1.5)
smooth = np.where(valid, ndimage.gaussian_filter(filled, 1.5) / np.maximum(weight, 1e-6), np.nan)

def plume_mask(k_sigma, seed_lonlat, radius=300):
    hot = np.nan_to_num(smooth) > k_sigma * sm_sigma
    lab, n = ndimage.label(hot, structure=np.ones((3, 3)))
    sx, sy = to_utm.transform(*seed_lonlat)
    near = np.hypot(ex - sx, ny - sy) <= radius
    keep = np.setdiff1d(np.unique(lab[near & hot]), [0])
    mask = np.isin(lab, keep)
    return ndimage.binary_closing(mask, iterations=2) & valid
```

`Output — smoothing`

```text
Gaussian smoothing (sigma 1.5 px) cuts the noise from 511 to 238 ppm m
```

The masks are applied to the smoothed field, but the mass in Step 6 is summed from the **raw** enhancement inside them, so the smoothing does not change the total.

![Three stacked maps of the McCommas Bluff plume complex: raw enhancement, smoothed enhancement and grey smoothed field with nested 1.5, 2 and 3 sigma mask outlines and the dashed JPL outline](https://www.technicalgis.com/_astro/03-plume-mask-mccommas.YiWwVlV8_ny73u.webp)

_Segmenting the McCommas Bluff plume. The raw field (top) is dominated by noise; smoothing (middle) shows a coherent enhancement over and west of the landfill and a weaker, patchy one further west. The nested masks (bottom) only capture the core near the source: the long tail is real methane, but it is too faint to separate from the noise pixel by pixel. The arrow shows the ERA5 wind direction._

This is the central difficulty of plume quantification. A strict mask misses the dilute far part of the plume and underestimates the mass; a loose one adds noise. There is no single right answer, which is why we carry three thresholds through to the end.

## Step 5: Get the wind from ERA5

To turn a mass into a rate you need to know how fast the air was moving. The script reads hourly ERA5 reanalysis fields from Google’s public **ARCO-ERA5** Zarr store (no login), interpolates them to each plume and to the overpass time, and also records the range of 10 m wind speeds within ±2 hours.

`emit_methane_plumes.py`

```python
ARCO = "gs://gcp-public-data-arco-era5/ar/full_37-1h-0p25deg-chunk-1.zarr-v3"
era = xr.open_zarr(ARCO, chunks=None, storage_options={"token": "anon"})
sub = era[vars_].sel(time=slice(day + pd.Timedelta(hours=12), day + pd.Timedelta(hours=23)),
                     latitude=slice(33.5, 32.0), longitude=slice(262.5, 263.75)).load()
p = sub.interp(latitude=j["lat"], longitude=j["lon"] % 360)
ts = p.interp(time=overpass)
```

`Output — ERA5 at the overpass`

```text
ERA5 subset: {'time': 12, 'latitude': 7, 'longitude': 6} | overpass 2023-08-25 17:05 UTC (12:05 local CDT)
McCommas Bluff: U10 1.05 m/s from 43 deg | U100 1.23 m/s | U10 range over +-2 h 0.98-1.13 m/s | ps 996 hPa | T2m 38.7 C | BLH 1165 m
Skyline (Ferris): U10 1.17 m/s from 51 deg | U100 1.38 m/s | U10 range over +-2 h 1.02-1.19 m/s | ps 996 hPa | T2m 38.6 C | BLH 1083 m
Camelot (Lewisville): U10 0.46 m/s from 67 deg | U100 0.51 m/s | U10 range over +-2 h 0.28-1.27 m/s | ps 993 hPa | T2m 38.8 C | BLH 1625 m
```

It was a very hot, nearly calm day: 38.7 °C at noon during the late-August 2023 Texas heatwave, with north-easterly winds of only 0.5 to 1.2 m/s. That fits the plumes drifting west and south-west in the map, but light winds are the worst case for emission estimates. A 0.2 m/s error in a 1 m/s wind is a 20% error in the emission rate, and in near-calm air the plume shape is controlled by eddies rather than the mean wind.

![Two line charts of ERA5 10 m wind speed and direction from 12 to 23 UTC on 25 August 2023 at the three landfills, with the EMIT overpass marked at 17 UTC](https://www.technicalgis.com/_astro/04-era5-wind.DZcXLRbn_Z1PNlKC.webp)

_ERA5 10 m wind speed (left) and direction (right) at the three plume origins. The overpass came during the calmest part of the day; at Camelot the wind changed from 0.3 to 1.3 m/s within two hours._

## Step 6: Integrated mass enhancement and emission rate

The **IME** is the sum of enhancement × pixel area over the mask. To convert ppm m to kg m⁻², multiply by the number of moles of air per cubic metre (p / RT from the ideal gas law) and the molar mass of methane. At the ERA5 surface pressure and temperature, 1 ppm m is **0.6 mg of CH₄ per m²**.

`emit_methane_plumes.py`

```python
def kg_per_m2_per_ppmm(p_pa, t_k):
    return 1e-6 * p_pa / (R_GAS * t_k) * M_CH4          # M_CH4 = 0.01604 kg/mol

ime = np.nansum(np.where(m, ch4 - med, 0) * pix_area) * kfac       # kg of excess CH4
length = dist[m].max()                                               # furthest mask pixel from the origin
q_simple = wv["U10"] * ime / length * 3600                           # kg/h
ueff = 1.1 * np.log(wv["U10"]) + 0.6                                 # Varon et al. (2018)
q_varon = ueff * ime / np.sqrt(area) * 3600
```

The simplest emission model treats the plume as methane carried downwind at speed U: the mass in a plume of length L took L/U seconds to build up, so **Q = U · IME / L**. Varon et al. (2018) calibrated a version against large-eddy simulations, with an effective wind U\_eff = 1.1 ln(U₁₀) + 0.6 and L = √(plume area). That calibration was for GHGSat’s 50 m pixels and a particular masking method, so we use it only as a cross-check. Note that it gives a negative wind below U₁₀ = 0.58 m/s, which already tells you that it is not meant for calm conditions.

`Output — IME and emission rates`

```text
McCommas Bluff: plume origin (smoothed peak) at (32.6863, -96.7272), 1,165 ppm m above background, 796 m from the JPL maximum pixel
McCommas Bluff: 0.6 mg CH4/m2 per ppm m | 2 sigma mask 65 px = 0.20 km2, reaching 0.4 km from the origin
   IME 1.5 sigma: 160 kg, 2.0 sigma: 99 kg, 3.0 sigma: 56 kg
   Q = U10*IME/L 1.5 sigma: 755, 2.0 sigma: 883, 3.0 sigma: 1,097 kg/h
   Varon U_eff 0.66 m/s -> U_eff*IME/sqrt(A) 524 kg/h | range with wind +-2 h and thresholds: 703-1,179 kg/h
   EPA GHGRP 2023: 482,797 t CO2e of CH4 = 19,312 t CH4 = 2,205 kg/h on average
   fetch curve without a mask peaks at r = 1.20 km: IME 877 kg, Q 2,773 kg/h; at r = 5 km the IME is 154 kg
Skyline (Ferris): plume origin (smoothed peak) at (32.5469, -96.6958), 1,134 ppm m above background, 121 m from the JPL maximum pixel
Skyline (Ferris): 0.6 mg CH4/m2 per ppm m | 2 sigma mask 174 px = 0.53 km2, reaching 0.7 km from the origin
   IME 1.5 sigma: 716 kg, 2.0 sigma: 249 kg, 3.0 sigma: 121 kg
   Q = U10*IME/L 1.5 sigma: 1,478, 2.0 sigma: 1,601, 3.0 sigma: 1,431 kg/h
   Varon U_eff 0.78 m/s -> U_eff*IME/sqrt(A) 952 kg/h | range with wind +-2 h and thresholds: 1,244-1,624 kg/h
   EPA GHGRP 2023: 158,052 t CO2e of CH4 = 6,322 t CH4 = 722 kg/h on average
   fetch curve without a mask peaks at r = 0.60 km: IME 313 kg, Q 2,206 kg/h; at r = 5 km the IME is 224 kg
Camelot (Lewisville): plume origin (smoothed peak) at (33.0317, -96.9555), 1,529 ppm m above background, 50 m from the JPL maximum pixel
Camelot (Lewisville): 0.6 mg CH4/m2 per ppm m | 2 sigma mask 61 px = 0.19 km2, reaching 0.4 km from the origin
   IME 1.5 sigma: 157 kg, 2.0 sigma: 135 kg, 3.0 sigma: 112 kg
   Q = U10*IME/L 1.5 sigma: 449, 2.0 sigma: 614, 3.0 sigma: 786 kg/h
   Varon U_eff -0.26 m/s -> U_eff*IME/sqrt(A) undefined (U_eff <= 0 below U10 = 0.58 m/s) | range with wind +-2 h and thresholds: 275-2,173 kg/h
   EPA GHGRP 2023: 105,232 t CO2e of CH4 = 4,209 t CH4 = 481 kg/h on average
   fetch curve without a mask peaks at r = 0.15 km: IME 66 kg, Q 731 kg/h; at r = 5 km the IME is 1,530 kg

                      ime_kg  length_km  U10  q_simple  q_varon    q_lo    q_hi  q_ghgrp
site
McCommas Bluff          98.8        0.4  1.1     883.2    524.4   703.4  1179.1   2204.6
Skyline (Ferris)       248.9        0.7  1.2    1600.6    952.4  1243.9  1624.4    721.7
Camelot (Lewisville)   134.9        0.4  0.5     614.3      NaN   275.0  2172.7    480.5
three landfills together: 3,098 kg/h (range 2,222-4,976) vs GHGRP annual average 3,407 kg/h | ratio 0.91
```

Some things to notice:

-   **The IME depends strongly on the threshold**, from 56 to 160 kg at McCommas Bluff and from 121 to 716 kg at Skyline. Because L grows with the mask as well, the emission rate is more stable than the IME: 755–1,097 kg/h at McCommas Bluff. This partial cancellation is why the IME method works at all.
-   **The range is driven by the wind** where the wind is changing. At Camelot the 10 m wind went from 0.28 to 1.27 m/s within two hours of the overpass, so the range is 275–2,173 kg/h, almost a factor of eight.
-   **The two wind models disagree by up to 40%**, and the Varon et al. model is undefined at Camelot.

### The fetch curve: why you need a mask

A useful check is the **fetch curve**: the IME inside a circle of radius r round the origin, divided by r. For an ideal plume in a steady wind it rises and then levels off. Figure 5 compares the curve inside the 1.5σ mask with the same calculation using every pixel in the outline and no mask at all.

![Two line charts: IME inside a growing radius and the corresponding emission estimate, for the three landfills, solid lines inside the 1.5 sigma mask and dashed lines for all pixels](https://www.technicalgis.com/_astro/05-ime-fetch-curve.CF79jmwO_1RdpR7.webp)

_Fetch curves. Solid: inside the 1.5σ mask, where the IME levels off once the mask ends. Dashed: every pixel in the outline. Without a mask the McCommas Bluff IME reaches 877 kg at 1.2 km (Q ≈ 2,770 kg/h) and then falls, because a background level only 50–100 ppm m too high, summed over thousands of pixels, removes hundreds of kilograms._

The unmasked curves look tempting: at McCommas Bluff they suggest that the faint tail holds several times more methane than the core and that the emission could be as high as 2,770 kg/h. But they never level off. Beyond about 1.5 km the IME falls again, and at Camelot it climbs steadily to 1,530 kg at 5 km. That is what a small error in the background does when it is multiplied by thousands of pixels. A 50 ppm m bias over 5,000 pixels is about 470 kg of methane. The masked estimates are more defensible, but they are probably biased low because they miss the dilute tail.

## Step 7: Compare with the EPA inventory

US landfills above a size threshold report their methane emissions to the EPA’s GHGRP every year. The script reads the 2023 figures for the three facilities from the EPA’s public Envirofacts API and converts them to an average hourly rate. GHGRP reports carbon dioxide equivalents with a global warming potential of 25 for methane in reporting year 2023.

`emit_methane_plumes.py`

```python
ghg = requests.get(f"{GHGRP}/pub_facts_subp_ghg_emission/facility_id/{s['ghgrp']}/year/2023/JSON",
                   timeout=120).json()
co2e = sum(r["co2e_emission"] for r in ghg if r["gas_id"] == 2)      # gas_id 2 = methane
q_ghgrp = co2e / GWP_RY2023 * 1000 / 8760                             # t CO2e -> kg CH4 per hour
```

![Bar chart comparing EMIT snapshot emission rates with EPA GHGRP 2023 annual average rates for McCommas Bluff, Skyline and Camelot landfills](https://www.technicalgis.com/_astro/06-emission-comparison.jdt7nrXl_Z27O7on.webp)

_EMIT snapshot estimates (coloured, with ranges from the wind and the masks) against EPA GHGRP 2023 annual averages (grey). The totals agree within 10%, but the snapshot is less than half the inventory at McCommas Bluff and more than double it at Skyline._

The three snapshots add up to **3,098 kg/h**, against **3,407 kg/h** in the inventory: a ratio of 0.91. It would be easy to declare success, but the site-by-site picture does not support it. EMIT sees less than half the reported rate at McCommas Bluff, the largest site, and more than twice the reported rate at Skyline.

Neither number is “the truth”. Landfill emissions vary through the day and with weather, active cell work and gas collection system outages, so a single midday snapshot need not match an annual average. GHGRP landfill emissions are mostly modelled from waste-in-place and assumed gas collection efficiency rather than measured, and published airborne surveys of US landfills have found observed emission rates higher than the reported ones on average. To compare the two properly you need many overpasses at each site.

## Other ways to see methane from space

EMIT is one of several instruments, and the right one depends on the size of the source:

-   **Sentinel-5P TROPOMI** maps total-column methane every day at about 5.5 × 7 km. It sees regional hot spots such as oil and gas basins and large landfills under the right conditions, but cannot separate individual facilities. Our [Sentinel-5P NO₂ air pollution tutorial](https://www.technicalgis.com/tutorials/sentinel-5p-no2-air-pollution-python/) shows how to work with TROPOMI Level 2 data; the methane product uses the same file structure.
-   **Sentinel-2 and Landsat** were not designed for methane, but their two shortwave infrared bands (B11 and B12 on Sentinel-2) can detect very large plumes at 20–30 m by comparing the bands, or one date with another (Varon et al., 2021). The detection limit is typically a few tonnes per hour, above the landfill plumes here. You can try it on Sentinel-2 imagery from the [Sentinel-2 STAC tutorial](https://www.technicalgis.com/tutorials/sentinel-2-stac-lake-mead/).
-   **Targeted imaging spectrometers** such as GHGSat (commercial) and Carbon Mapper’s Tanager satellites have finer pixels and lower detection limits than EMIT, but their data come with commercial or non-commercial licences.

## Limitations and honest caveats

-   **This is a teaching estimate, not an official one.** JPL has not published emission rates for these plumes. Operational teams use more careful background estimation, plume masks reviewed by analysts, wind products at higher resolution and calibrated uncertainty models.
-   **The wind dominates the error budget.** ERA5 is a 0.25° (about 28 km) reanalysis; it cannot resolve local winds at a landfill, and the 10 m wind is not the wind that carries a plume through the boundary layer, which was 1–1.6 km deep. At 0.5–1.2 m/s, the IME method is at the edge of its validity.
-   **The masks miss the dilute tail.** The 2σ masks reach only 0.4–0.7 km from the source, while the smoothed field shows enhancement further downwind. That biases the IME low; the fetch curves show that including more pixels without a mask is worse, not better.
-   **The background is estimated from the same pixels.** Because the VEDA subset is clipped to the plume outlines, we cannot sample a truly plume-free background. Full L2B granules let you do this properly.
-   **The source attribution is by distance.** The matches to GHGRP facilities are plausible, but you should check each plume origin against the active landfill area and other possible sources, such as wastewater treatment or gas pipelines, before naming a facility.
-   **One overpass is one moment.** A midday snapshot does not give an annual emission. Inventory comparisons need many observations, ideally at different times of day.
-   **Surface artefacts.** Matched-filter retrievals can produce false positives over some bright or dark surfaces and materials with absorption near 2,300 nm. JPL’s review reduces this, but it is worth checking plumes against true-colour imagery as in Figure 1.

## Frequently asked questions

### How do satellites detect methane?

Methane absorbs sunlight at specific shortwave infrared wavelengths, especially around 1,650 and 2,300 nm. Sensors with many narrow bands, such as EMIT, measure how much light is missing at those wavelengths compared with a methane-free spectrum and convert it to an enhancement in ppm m. Broadband sensors such as Sentinel-2 can see the same absorption only for very large plumes.

### What does ppm m mean in methane data?

It is a column enhancement: the extra methane mixing ratio multiplied by the depth of the layer it occupies. 1,000 ppm m is the same as 1 ppm of extra methane through 1 km of air, or 10 ppm through 100 m. Near sea level and at 39 °C, 1 ppm m equals about 0.6 mg of methane per square metre of ground.

### What is the integrated mass enhancement (IME) method?

IME is the total mass of excess methane in a plume, the sum of enhancement × pixel area over the plume. Dividing it by the time the air took to cross the plume, L / U, gives an emission rate Q = U · IME / L. It is simple and robust to plume shape, but depends on the wind speed, the plume mask and the length scale you choose.

### Can Sentinel-2 detect methane?

Yes, for very large plumes. Methods that compare Sentinel-2 bands 11 and 12, or the same bands on two dates, have detected oil and gas super-emitters of several tonnes per hour. They cannot see the smaller landfill plumes in this tutorial, and false positives are common over complex surfaces.

### Is EMIT data free?

Yes. All EMIT products are free and open. The full L2B methane enhancement and plume products need a free NASA Earthdata account, and the plume metadata used here and VEDA’s landfill subset can be read without one.

### How accurate are satellite methane emission estimates?

For a single plume, uncertainties of 30–50% are typical in good conditions and can be a factor of two or more in light winds, as here. Most of the error comes from the wind. Averages over many overpasses are much more reliable than any one estimate.

## Next steps

-   Work with daily methane and NO₂ columns from TROPOMI in the [Sentinel-5P air pollution tutorial](https://www.technicalgis.com/tutorials/sentinel-5p-no2-air-pollution-python/).
-   Learn more about ERA5 and the ARCO Zarr stores in [ERA5 in Python with xarray](https://www.technicalgis.com/tutorials/era5-python-xarray/).
-   Search and stream Sentinel-2 imagery, the background of Figure 1, with the [Sentinel-2 STAC tutorial](https://www.technicalgis.com/tutorials/sentinel-2-stac-lake-mead/), and clean it with [Sentinel-2 cloud masking](https://www.technicalgis.com/tutorials/sentinel-2-cloud-masking-python/).
-   Summarise raster values inside polygons, such as landfill boundaries, with [zonal statistics in Python](https://www.technicalgis.com/tutorials/zonal-statistics-python/).

## Data and licences

-   **EMIT methane enhancement**: NASA EMIT, _EMIT Landfill Plumes_ (`landfill-emit`) from the NASA VEDA STAC and raster APIs at `openveda.cloud`, licensed [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/). Accessed on 23 October 2026.
-   **EMIT plume-complex metadata**: NASA JPL, `combined_plume_metadata.json` from the EMIT greenhouse gas portal at `earth.jpl.nasa.gov`. NASA data are not copyrighted in the United States. Accessed on 23 October 2026.
-   **ERA5**: Hersbach, H. et al. (2018), ERA5 hourly data on single levels, Copernicus Climate Change Service (C3S) Climate Data Store, via Google’s ARCO-ERA5 Zarr store. Contains modified Copernicus Climate Change Service information 2026, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
-   **Sentinel-2**: Contains modified Copernicus Sentinel data 2023, Sentinel-2 L2A from [Microsoft Planetary Computer](https://planetarycomputer.microsoft.com/dataset/sentinel-2-l2a).
-   **Inventory**: US EPA Greenhouse Gas Reporting Program, reporting year 2023, from the Envirofacts API at `data.epa.gov`. US government work, public domain.
-   Further reading: Thorpe, A. K. et al. (2023), “Attribution of individual methane and carbon dioxide emission sources using EMIT observations from space”, _Science Advances_ 9: eadh2391; Varon, D. J. et al. (2018), “Quantifying methane point sources from fine-scale satellite observations of atmospheric methane plumes”, _Atmospheric Measurement Techniques_ 11: 5673–5686; Varon, D. J. et al. (2021), “High-frequency monitoring of anomalous methane point sources with multispectral Sentinel-2 satellite observations”, _Atmospheric Measurement Techniques_ 14: 2771–2785.
-   Code: [`emit_methane_plumes.py`](https://www.technicalgis.com/code/methane-plume-detection-emit-python/), tested with Python 3.12.0, pystac-client 0.9.1, planetary-computer 1.0.0, rasterio 1.5.2, rioxarray 0.23.0, xarray 2026.9.0, zarr 3.4.1, gcsfs 2026.10.1, numpy 2.5.4, pandas 3.1.0, scipy 1.18.1, shapely 2.2.0, pyproj 3.8.0, matplotlib 3.11.2 and requests 2.34.2.

## Key terms

- **Imaging spectrometer**: A sensor that records a near-continuous spectrum for every pixel, with hundreds of narrow bands. EMIT has 285 bands from about 380 to 2,500 nm.
- **Methane enhancement (ppm m)**: The extra methane along the light path above the local background, expressed as a mixing ratio multiplied by a path length. 1,000 ppm m equals 1 ppm extra through a 1 km layer, or 10 ppm through 100 m.
- **Matched filter**: The retrieval EMIT uses: each pixel's spectrum is compared with the expected methane absorption signature in the shortwave infrared, around 2,300 nm, after allowing for the scene's own spectral variability.
- **Plume complex**: JPL's term for an outline drawn round one or more methane plumes that appear together in an EMIT scene. Each has an ID, a time and the position and value of the maximum enhancement.
- **Integrated mass enhancement (IME)**: The total mass of excess methane in a plume: the sum of enhancement × pixel area over the plume mask, converted from ppm m to kilograms.
- **Effective wind speed**: The wind speed that turns IME into an emission rate. It is related to, but not the same as, the 10 m wind: plumes are mixed through a layer and the relation depends on the sensor and the plume-masking method.
- **GHGRP**: The US EPA Greenhouse Gas Reporting Program. Large emitters, including municipal solid waste landfills, report annual emissions, mostly calculated from models rather than measured.

## Data for this tutorial

-   [NASA Earthdata](https://www.technicalgis.com/data-sources/#src-nasa-earthdata) · Full EMIT L2B methane enhancement and plume products (free Earthdata login)
-   [ERA5](https://www.technicalgis.com/data-sources/#src-era5) · Hourly reanalysis winds, pressure and temperature; read here from Google's ARCO-ERA5 Zarr store
-   [Microsoft Planetary Computer](https://www.technicalgis.com/data-sources/#src-microsoft-planetary-computer) · Sentinel-2 L2A imagery used as the map background

Licences and formats for each source are listed in the [GIS data sources catalogue](https://www.technicalgis.com/data-sources/).

---

Source: https://www.technicalgis.com/tutorials/methane-plume-detection-emit-python/
