qin1·intermediate

Are crop-residue fires upwind turning my city's air toxic?

atmosphereair-qualityagriculturepublic-health Datasets: 5 15–40 min

After the rice harvest, farmers in north-west India often clear their fields by burning the leftover straw and stubble. That's thousands of small fires lit over a few autumn weeks. The smoke doesn't stay put. When the wind blows the right way, it drifts hundreds of kilometres and settles over downwind cities as choking haze. This question asks whether those upwind fires line up with the bad air over your city, using satellites that can see both the flames and the pollution from orbit.

Are crop-residue fires upwind turning my city’s air toxic?

After the rice harvest, farmers in north-west India often clear their fields by burning the leftover straw and stubble. That’s thousands of small fires lit over a few autumn weeks. The smoke doesn’t stay put. When the wind blows the right way, it drifts hundreds of kilometres and settles over downwind cities as choking haze. This question asks whether those upwind fires line up with the bad air over your city, using satellites that can see both the flames and the pollution from orbit.

Which data to use, and how

Use this dataset: VIIRS active fire (VNP14IMG) (short name VNP14IMG). A satellite instrument scans the ground for hot spots and flags any pixel that’s much hotter than its surroundings: a thermal anomaly, almost always a fire. Each detection comes with its Fire Radiative Power (FRP), basically how much heat the fire is throwing off in megawatts, a stand-in for how intensely it’s burning. VIIRS sees fires as small as ~375 m across, so it’s the sharpest and easiest place to start.

Then, to connect the fires to your air, you add two more pieces: the smoke’s chemical signature over your city, and the wind that carries it.

Then do four steps (the runnable code further down does all of this; this is just the idea):

  1. Draw two boxes: a source box around the farming belt that’s burning, and a receptor box around your city downwind. Pick the burning season (Oct–Nov in NW India).
  2. Count the fires: for each day, count how many VIIRS fire pixels fall in the source box and add up their FRP. This is your daily “how much is burning upwind” number.
  3. Measure the city’s air: for the same days, average the pollution over your receptor box. Smoke shows up as extra TROPOMI NO₂ or CO (gases from burning) and as haze in MODIS aerosol optical depth (AOD, a measure of how much airborne dust and smoke is dimming the view from space).
  4. Only trust the days the wind agrees: use MERRA-2 winds to keep only the days the wind actually blows from the fields toward your city, then check whether more burning on those days lines up with worse air.

That’s the whole method. Below, “How a scientist answers this” names the exact tools, and the Run it cell does it live on synthetic data so you can see each step work.

What you can find out

  • How much is burning upwind each day: the daily count of active fires and total FRP over the crop belt through the Oct–Nov window.
  • How dirty your city’s air was on the same days: TROPOMI NO₂ and CO, plus MODIS AOD, over your receptor city.
  • Whether the two move together when the wind connects them: on days the wind blows from the fields toward you, does more burning line up with worse air?
  • Burning season vs a calm month: how fire activity and downwind pollution compare against a baseline period with no burning.
  • A map of the smoke: where the fire clusters are, and which way the plume points relative to the prevailing wind.

What it can’t tell you

  • What you actually breathe at street level. The satellite measures pollution in the whole vertical column of air (and AOD measures haze in that column), not the concentration at nose height. For “is it safe to breathe right now” you need ground monitors (free: CPCB, OpenAQ) plus a model that converts a column into a surface number.
  • Exactly what share is the stubble’s fault. A correlation can’t tell you “40% stubble, 30% traffic, 30% industry.” Splitting the blame (formal source apportionment) needs chemical transport models or receptor models, not a scatter plot.
  • Every fire, including at night. The satellite only sees your region when it flies overhead, and clouds (or thick smoke itself) hide fires. FRP is a snapshot at overpass time, not a full-day total, so the fire count is always an undercount.
  • Health effects. Linking the smoke to actual illness needs medical and epidemiological data that lives entirely outside these satellite products.

Gotchas to watch for

  • Fire heat is not the same as breathable smog. FRP tells you how hard a fire is burning (combustion energy), not how many micrograms of particles end up in someone’s lungs. And a column amount isn’t a surface amount until you correct for how deep the air near the ground is mixed (the boundary-layer height), which itself changes day to day and can swing your city’s pollution up or down with no change in fires at all. A low, stagnant air layer traps smoke and makes a quiet fire day look terrible, so treat the link as suggestive and check ground monitors before claiming anything about health.
  • Overpass timing and clouds hide things. The satellite passes over only at certain hours, and clouds or heavy haze block both the fire detection and the pollution retrieval. That biases your fire count low, and worst of all on the haziest days you most care about. Lean on each product’s quality (QA) flags to drop bad retrievals, and read the counts as a floor rather than a total.
  • Correlation is not proof. The burning season also brings festival firecrackers (Diwali), winter traffic, industry and regional dust, all happening at once, and any of them could be the real culprit. Wind-direction gating (keeping only source-to-city transport days) trims the overlap but won’t erase it, so say “likely link,” not “proven cause.”
  • The two fire satellites don’t count the same. VIIRS (375 m) spots smaller, cooler fires that MODIS (1 km) misses entirely. Merge their raw counts and you’ll read the sharper sensor as a fake jump in fires. Stick to one sensor, or harmonize their detection limits before combining.
  • Very intense fires can fool the FRP. Extreme heat can saturate the detector or smear the location (geolocation error), inflating the reported power, and a handful of giant values is enough to distort your totals. Treat very high FRP cautiously, capping or flagging the outliers.

The real-data code

The Run it cell above runs this method on synthetic data with no login. Below is the whole recipe against the real archive: search the fires, NO₂ and winds, open each granule, count fires and sum FRP upwind, average NO₂ over the city, keep only wind-aligned transport days, compare against a calm baseline, and plot it. It needs a free Earthdata Login.

import earthaccess, numpy as np, xarray as xr, h5py
import matplotlib.pyplot as plt

earthaccess.login(strategy="netrc")

source_aoi   = (74, 28, 78, 32)            # Punjab–Haryana crop belt (upwind, W,S,E,N)
receptor_aoi = (76.8, 28.4, 77.4, 28.9)    # Delhi NCR (downwind)
season   = ("2024-10-15", "2024-11-15")    # post-monsoon burning window
baseline = ("2024-08-15", "2024-09-15")    # calm, pre-burning month

# 1. VIIRS fires + FRP upwind. VNP14IMG is beam-group HDF5 -> open with h5py.
def daily_fires(t0, t1):
    grans = earthaccess.search_data(short_name="VNP14IMG", bounding_box=source_aoi, temporal=(t0, t1))
    cnt, frp = [], []
    for fh in earthaccess.open(grans):
        with h5py.File(fh, "r") as f:
            lat, lon = f["FP_latitude"][:], f["FP_longitude"][:]
            power, conf = f["FP_power"][:], f["FP_confidence"][:]   # MW, 0/7/8/9 nom+
        box = ((lon >= source_aoi[0]) & (lon <= source_aoi[2]) &
               (lat >= source_aoi[1]) & (lat <= source_aoi[3]) & (conf >= 7))
        cnt.append(int(box.sum())); frp.append(float(power[box].sum()))
    return np.array(cnt), np.array(frp), np.array([[lat[box], lon[box]]], dtype=object)

n_fire, frp_burn, fire_pts = daily_fires(*season)

# 2. Downwind NO2 over the city: TROPOMI granules, QA-screened area mean per granule.
def city_no2(t0, t1):
    grans = earthaccess.search_data(short_name="S5P_L2__NO2___", bounding_box=receptor_aoi, temporal=(t0, t1))
    out = []
    for fh in earthaccess.open(grans):
        ds = xr.open_dataset(fh, group="PRODUCT")
        keep = ((ds["qa_value"] > 0.75)
                & (ds.longitude >= receptor_aoi[0]) & (ds.longitude <= receptor_aoi[2])
                & (ds.latitude  >= receptor_aoi[1]) & (ds.latitude  <= receptor_aoi[3]))
        out.append(float(ds["nitrogendioxide_tropospheric_column"].where(keep).mean()))
    return np.array(out)

no2_burn, no2_base = city_no2(*season), city_no2(*baseline)

# 3. Wind gating: MERRA-2 near-surface U,V over the belt. Punjab->Delhi is SE-ward
#    transport, i.e. wind blowing toward the city => U>0 (eastward) & V<0 (southward).
wg = earthaccess.search_data(short_name="M2I3NVASM", bounding_box=source_aoi, temporal=season)
transport_days = 0
for fh in earthaccess.open(wg):
    w = xr.open_dataset(fh)
    u = float(w["U"].isel(lev=-1).mean()); v = float(w["V"].isel(lev=-1).mean())
    if u > 0 and v < 0:                      # vector points from belt toward Delhi
        transport_days += 1

# 4. Verdict: does burning-season city NO2 exceed the calm baseline, and is the wind aligned?
ratio = np.nanmean(no2_burn) / np.nanmean(no2_base)
print(f"Fires upwind: {n_fire.sum()} detections, {frp_burn.sum():.0f} MW total FRP | "
      f"transport-aligned days: {transport_days} | "
      f"city NO2 burning vs baseline: {ratio:.2f}x  -> "
      f"{'likely smoke link' if ratio > 1.3 and transport_days > 0 else 'no clear link'}")

# Plot: daily upwind FRP next to the burning vs baseline city-NO2 distributions.
fig, (a, b) = plt.subplots(1, 2, figsize=(9, 3))
a.bar(range(len(frp_burn)), frp_burn); a.set_title("upwind FRP/day (MW)"); a.set_xlabel("granule")
b.boxplot([no2_base[~np.isnan(no2_base)], no2_burn[~np.isnan(no2_burn)]], labels=["baseline", "burning"])
b.set_title("Delhi NO2 column"); plt.tight_layout(); plt.show()

# Before you trust it: this is correlational, not source apportionment — cross-check the city
# columns against CPCB / OpenAQ surface PM2.5, since column != what you breathe (see the gotchas).

Where the data comes from

This question stitches together several archives. The fires and the haze (VIIRS, MODIS, MODIS AOD) come from NASA’s LAADS archive, plus FIRMS for near-real-time fire alerts. The winds (MERRA-2) come from NASA’s GES DISC archive. The TROPOMI smoke gases (Sentinel-5P) are a European Space Agency product. You can pull them from a NASA mirror at GES DISC, or from Copernicus directly, which may need a separate login. The street-level reality check (CPCB and OpenAQ ground monitors) is free and separate.

Sources

How a scientist answers this
Parameters
Active-fire detections and Fire Radiative Power (FRP) from VIIRS (VNP14IMG, 375 m) and MODIS (MOD14/MYD14, 1 km) thermal anomalies over the Punjab–Haryana crop belt during the Oct–Nov post-monsoon burning window; downwind air quality from TROPOMI Sentinel-5P tropospheric NO2 and CO columns plus MODIS aerosol optical depth (AOD) over the receptor city; MERRA-2 (or ERA5) low-level winds to link source to receptor. FRP is a proxy for combustion intensity, not directly for surface PM2.5.
Method
Count and aggregate fire pixels and sum FRP over the upwind source box per day through the burning season, then relate them to downwind NO2/CO/AOD columns on days when winds blow from source toward the city. Compare the burning-season signal against a non-burning baseline period. Wind direction gating (source-to-receptor transport) is what separates plausible smoke transport from coincidence; treat the link as correlational, not a formal source attribution.
Validation
Cross-check satellite columns against ground monitors (CPCB / OpenAQ surface PM2.5 and NO2), since column amounts are not the same as what people breathe at the surface. Separate fire-driven pollution from local traffic, industry and dust, and screen for clouds and low-quality retrievals using each product's QA flags. Day-to-day boundary-layer height changes strongly modulate surface concentrations independent of fires.
In plain EnglishSatellites spot the fires burning in the fields upwind and, separately, measure the haze and pollutant gases over your city. When the wind is blowing from the fields toward you, more fires usually line up with worse air — but local traffic, weather and dust matter too, so this shows a likely link, not absolute proof.

Make it yours → Set your source box (the farming belt) and your receptor city, the season window, and which pollutant to track (NO2, CO or AOD). Switch between VIIRS (sharper, 375 m) and MODIS (longer record) for fires, and choose MERRA-2 or ERA5 for the winds that connect the two.

🧪 Virtual Mock Lab stand-in data · runs in your browser · no login

A safe place to practise the method on VNP14IMG. The code is the real earthaccess workflow, but we feed it stand-in data shaped like the real product — so you can run it, change the numbers, and break it without an account. When you want the real data, use the recipe under “The real-data code” above, or download the notebook below.

editable · runs in your browser

Datasets used

Recipe ready · not yet run The recipe and the Virtual Mock Lab above are ready to use. We haven't yet run this question end-to-end on real data, so it has no verified answer yet — only the questions we've actually computed carry one. That's the honest line between a method and a result.