FIRMS — MODIS vs VIIRS¶
Compare MODIS (1 km) and VIIRS (375 m) detections over the same bbox and window. Shows the resolution difference and how the backend normalises the two confidence schemas (MODIS numeric 0–100 vs VIIRS categorical l/n/h) into one confidence_pct.
Setup¶
Consolidate the imports and create the output directory. earthlens provides the unified EarthLens entry point; the download workflow writes a GeoJSON to OUT_DIR and returns the merged detections as a GeoDataFrame.
import datetime as dt
import os
from pathlib import Path
from earthlens import EarthLens
OUT_DIR = Path('firms_output')
OUT_DIR.mkdir(exist_ok=True)
Credentials¶
FIRMS needs a free MAP_KEY (https://firms.modaps.eosdis.nasa.gov/api/map_key/). Set FIRMS_MAP_KEY in your environment; the live cells below skip cleanly when it is absent, so the notebook never fails offline.
HAS_KEY = bool(os.environ.get('FIRMS_MAP_KEY'))
print('FIRMS_MAP_KEY set:', HAS_KEY)
Query both sensors¶
Pass both sensor codes in variables; the backend issues one request per (sensor, chunk) and merges them. The sensor column distinguishes the source.
Build the request¶
Define the seven-day window and the southern-California bbox, then construct the EarthLens request for both sensors. The construct step is kept separate from the download so each is easy to read and re-run.
TODAY = dt.date.today()
START = (TODAY - dt.timedelta(days=7)).strftime('%Y-%m-%d')
END = TODAY.strftime('%Y-%m-%d')
fires = None
if HAS_KEY:
request = EarthLens(
data_source='firms',
variables=['MODIS_NRT', 'VIIRS_SNPP_NRT'],
start=START,
end=END,
aoi=[-119.0, 33.0, -117.0, 35.0],
path=str(OUT_DIR),
)
Download the detections¶
Run the merged query. The live call is wrapped in try/except so the notebook degrades gracefully (an expired key, a network outage, or an offline run prints the reason and leaves fires empty instead of raising).
if HAS_KEY:
try:
fires = request.download(progress_bar=False)
print('detections:', len(fires))
except Exception as exc:
print('skipped live query:', exc)
Detections per sensor and confidence handling¶
Count the rows per sensor and peek at the confidence columns. Both families share a normalised confidence_pct even though the raw confidence differs (MODIS numeric vs VIIRS categorical).
if fires is not None and len(fires):
print(fires.groupby('sensor').size())
display(fires.groupby('sensor')[['confidence', 'confidence_pct', 'frp']].head(3))
Overlay the two sensors¶
Plot both sensors on one map: MODIS in blue at a larger marker size, VIIRS in red at a smaller one, so the finer VIIRS sampling shows through the coarser MODIS footprints.
if fires is not None and len(fires):
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(7, 6))
for sensor, colour, size in [
('MODIS_NRT', 'tab:blue', 60),
('VIIRS_SNPP_NRT', 'tab:red', 18),
]:
subset = fires[fires['sensor'] == sensor]
if len(subset):
subset.plot(ax=ax, color=colour, markersize=size, alpha=0.5, label=sensor)
ax.legend()
ax.set_title(f'MODIS 1 km vs VIIRS 375 m, {START} to {END}')
ax.set_xlabel('longitude')
ax.set_ylabel('latitude')
plt.show()