Did a wildfire impact downwind air quality, and how far did the smoke reach?
Draw a rectangle, then check which collections cover it. Most NASA data is global, so a big box matches a lot.
-125, 35 → -115, 42 (California + western Nevada)Smoke doesn't stay put. Wind carries it hundreds, sometimes thousands of kilometres downwind, dirtying the air in cities far from the flames. Satellites can spot the fires themselves (each hot pixel is a flame burning right then) and track the chemical haze the smoke leaves behind as it drifts. Put the two together and you can watch a plume travel from where it started to where people are breathing it.
Did a wildfire impact downwind air quality?
Smoke doesn’t stay put. Wind carries it hundreds, sometimes thousands of kilometres downwind, dirtying the air in cities far from the flames. Satellites can spot the fires themselves (each hot pixel is a flame burning right then) and track the chemical haze the smoke leaves behind as it drifts. Put the two together and you can watch a plume travel from where it started to where people are breathing it.
Which data to use, and how
Use this dataset: TEMPO HCHO (short name TEMPO_HCHO_L2).
HCHO is formaldehyde, a gas that smoke pumps into the air, so a spike in HCHO downwind of a fire
is a fingerprint that the smoke arrived. TEMPO watches North America every daylight hour. That’s what
you want for something that moves as fast as a smoke plume. (Outside North America, swap in the global
daily OMI products instead. See the gotchas below.)
Then do four steps (the runnable code further down does all of this; this is just the idea):
- Find the fire. Pull the hot-pixel detections so you know where and when the fire was burning, and pick a study window (a few days during and after the event).
- Pick a downwind box. Choose a latitude/longitude box somewhere downwind of the fire. That’s where you’ll check whether the smoke arrived.
- Watch the chemistry over time. Average TEMPO’s HCHO over your downwind box, hour by hour, and plot it before, during, and after the fire. A clear bump means the smoke reached you.
- Add wind to trace the path. Overlay wind direction (which way the air was moving) on top of the fire location and the HCHO haze, so you can see the plume’s actual route from source to city.
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 watch each step work.
What you can find out
- Where the fire was burning, within hours of it being detected (the near-real-time fire pixels).
- Which way the smoke went and how far it travelled, by following the haze (aerosol index, true-color imagery, HCHO) across the map.
- A downwind chemical spike: a jump in HCHO or NO₂ over a city as the plume arrives (TEMPO for North America, OMI/TROPOMI for the rest of the world).
- The burn scar afterward: how much land actually burned, and how badly, once the fire is out (MCD64A1 monthly burned area, or HLS burn-severity imagery).
- A rough headcount of who was exposed, if you overlay a gridded population map on the plume’s path.
What it can’t tell you
- What the air at street level actually measured. The satellite sees gases and haze in the whole column of air above a place, not the fine soot (PM₂.₅) you’d read off a ground monitor. For real surface numbers you need ground stations (e.g. EPA AirNow) or an air-chemistry model.
- The health impact. “The plume reached the city” is not “X people got sick.” Turning exposure into health outcomes needs epidemiology and exposure modelling, well beyond what the imagery alone shows.
- Exactly what the smoke is made of. Satellites tell you something is in the air, not the precise mix of particles. Composition needs samples taken on the ground or from aircraft.
Gotchas to watch for
- The fast fire data is a heads-up, not a final answer. The near-real-time fire pixels (FIRMS) are produced within hours so responders can act, but they’re not the cleaned-up record. For the authoritative “how much burned,” use the monthly burned-area product (MCD64A1) once it’s available.
- HCHO is a stand-in for smoke, not smoke itself. TEMPO HCHO tracks a gas the fire emits. It’s a handy proxy, but it isn’t the smoke particles. For the actual haze, pair it with a particle measure (MODIS aerosol optical depth, or the OMI Aerosol Index).
- How high the air mixes changes everything. On days when the lower atmosphere stays shallow (a low “boundary layer”), smoke piles up near the ground; when it’s deep, the plume thins out fast. Check the boundary-layer height from MERRA-2 for your dates so you don’t misread a thinning plume as a vanishing one.
- Smoke can fool the satellite into seeing “cloud.” Thick smoke looks a lot like cloud in some retrievals and may get thrown out, leaving a hole right where you wanted data. Cross-check the cloud flags (MOD06) to tell smoke from real cloud.
- TEMPO only watches North America. For fires elsewhere (Amazon, Siberia, Indonesia) TEMPO sees nothing. Use the global OMI/TROPOMI products instead.
The real-data code
The Run it cell above runs the method on synthetic data (no login). Below is the whole recipe against the real archive: find the fire, open TEMPO HCHO over a downwind box, build the hourly before/during/after series, add wind, and plot it. It needs a free Earthdata Login (the fast fire feed, FIRMS, uses its own free API key instead).
import earthaccess, requests, io
import numpy as np, pandas as pd, xarray as xr
import matplotlib.pyplot as plt
earthaccess.login(strategy="netrc")
# Example: California wildfire complex, July 2024
fire_window = ("2024-07-15", "2024-08-15")
aoi = (-125, 35, -115, 42) # CA + western NV (W, S, E, N)
downwind = (-121.0, 38.0, -119.5, 39.0) # box NE / downwind of the source
# 1. Find the fire: FIRMS near-real-time VIIRS hot pixels (free key, REST, no EDL).
firms_key = "YOUR_KEY"
firms_url = (f"https://firms.modaps.eosdis.nasa.gov/api/area/csv/{firms_key}/"
f"VIIRS_SNPP_NRT/{aoi[0]},{aoi[1]},{aoi[2]},{aoi[3]}/10/2024-07-15")
fires = pd.read_csv(io.StringIO(requests.get(firms_url, timeout=60).text))
fire_lon, fire_lat = fires["longitude"].mean(), fires["latitude"].mean()
print(f"{len(fires)} fire detections; centroid {fire_lat:.2f}N {fire_lon:.2f}W")
# 2. Watch the chemistry: open each TEMPO HCHO granule, keep good pixels in the
# downwind box, area-average the HCHO column, and tag it with the scan time.
grans = earthaccess.search_data(short_name="TEMPO_HCHO_L2",
bounding_box=aoi, temporal=fire_window)
times, hcho = [], []
for fh in earthaccess.open(grans):
geo = xr.open_dataset(fh, group="geolocation")
prod = xr.open_dataset(fh, group="product")
lon, lat = geo["longitude"], geo["latitude"]
col = prod["vertical_column"] # HCHO slant->vertical column
keep = ((prod["main_data_quality_flag"] == 0)
& (lon >= downwind[0]) & (lon <= downwind[2])
& (lat >= downwind[1]) & (lat <= downwind[3]))
v = float(col.where(keep).mean())
if np.isfinite(v):
times.append(pd.to_datetime(str(geo["time"].values.ravel()[0])))
hcho.append(v)
ts = pd.Series(hcho, index=pd.DatetimeIndex(times)).sort_index().resample("1D").mean()
# 3. Verdict: compare downwind HCHO during the fire vs the quiet days before it.
during = ts["2024-07-18":"2024-08-05"].mean()
before = ts[:"2024-07-17"].mean()
ratio = during / before
print(f"Downwind HCHO during fire = {during:.2e}, before = {before:.2e} "
f"-> {ratio:.1f}x ->", "smoke reached the box" if ratio > 1.3 else "no clear plume")
# 4. Add wind: MERRA-2 3-hourly U/V over the fire confirms the plume blew downwind.
wgran = earthaccess.search_data(short_name="M2I3NPASM", bounding_box=aoi,
temporal=fire_window)
wds = xr.open_mfdataset(earthaccess.open(wgran[:8]))
u = wds["U"].sel(lev=850, lon=fire_lon, lat=fire_lat, method="nearest").mean().item()
v = wds["V"].sel(lev=850, lon=fire_lon, lat=fire_lat, method="nearest").mean().item()
print(f"Mean 850-hPa wind at the fire: U={u:+.1f} V={v:+.1f} m/s "
f"(blows toward {'E' if u>0 else 'W'}/{'N' if v>0 else 'S'})")
# 5. Plot the downwind HCHO time series and mark the fire onset.
ts.plot(marker="o")
plt.axvline(pd.Timestamp("2024-07-17"), ls="--", c="r", label="fire onset")
plt.ylabel("HCHO column (downwind box)"); plt.xlabel("date")
plt.legend(); plt.tight_layout(); plt.show()
# Before you trust it: HCHO is a gas proxy, not the smoke particles — cross-check with
# an aerosol product (OMI Aerosol Index) and a ground PM2.5 monitor (EPA AirNow).
Where the data comes from
Everything but the fire feed comes through one free Earthdata Login, even though the pieces live in different NASA archives: TEMPO from the ASDC archive, OMI and MERRA-2 from GES DISC, and the burned-area product (MCD64A1) from the LP DAAC. The one exception is the fast near-real-time fire feed (FIRMS), which uses its own separate (also free) API key.
Sources
- FIRMS API: https://firms.modaps.eosdis.nasa.gov/api/
- TEMPO HCHO: https://asdc.larc.nasa.gov/project/TEMPO
- MCD64A1: https://lpdaac.usgs.gov/products/mcd64a1v061/
Make it yours → Set the fire bounding box, the event date window, and the baseline period in the notebook, and swap TEMPO for OMI/TROPOMI outside North America.
A safe place to practise the method on TEMPO_HCHO_L2. 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.