import os
# import earthaccess
import warnings
import requests
import pandas as pd
'USE_PYGEOS'] = '0'
os.environ[import geopandas
import folium
import folium.plugins
import numpy as np
import matplotlib.pyplot as plt
import branca.colormap as cm
import seaborn as sns
from folium import Map, TileLayer
from branca.element import Figure
from pystac_client import Client
from pyproj import Geod
from shapely import wkt
Interactive Session: Identifying and quantifying emissions from large point source methane emission events leveraging aircraft and satellite data
About the Data
The National Aeronautics and Space Administration (NASA) has a long history of developing advanced imaging spectrometers for new science and applications, and the Earth Surface Mineral Dust Source Investigation (EMIT) builds on this foundation. EMIT launched to the International Space Station (ISS) on July 14, 2022. The data shows high-confidence research grade methane plumes from point source emitters - updated as they are identified - in keeping with Jet Propulsion Laboratory (JPL) Open Science and Open Data policy.
Methane is a strong greenhouse gas that is invisible to the human eye. Large methane emissions, typically referred to as point source emissions, represent a significant proportion of total methane emissions from the production, transport, and processing of oil and natural gas, landfills, and other sources. By measuring the spectral fingerprint of methane, EMIT can map areas of high methane concentration over background levels in the atmosphere, identifying plume complexes, and estimating the methane enhancements.
Requirements
- Set up Python Environment - See setup_instructions.md in the
/setup/
folder
Learning Objectives
- How to use
U.S. GHG Center STAC Catalog
to accessEMIT Methane Point Source Plume Complexes
data. - How to visualize ISS Spatial coverage and detect plumes along the path of the ISS.
- How to visualize datasets using
folium
.
Approach
- Visualize the spatial coverage of the ISS and the observed plumes along its path
- Identify available dates and temporal frequency of observations for a given collection using the GHGC API
/stac
endpoint. The collection processed in this notebook is the Earth Surface Mineral Dust Source Investigation (EMIT) methane emission plumes data product - Pass the STAC item into the raster API
/stac/tilejson.json
endpoint - Display the plumes using
folium.Map
Setup
Import the required Python libraries by running the next cell.
NASA Earth Data Login Credentials
To download or stream NASA data you will need an Earthdata account, you can create one here https://urs.earthdata.nasa.gov/home. We will use the login
function from the earthaccess
library for authentication before downloading at the end of the notebook. This function can also be used to create a local .netrc
file if it doesn’t exist or add your login info to an existing .netrc
file. If no Earthdata Login credentials are found in the .netrc
you’ll be prompted for them. This step is not necessary to conduct searches but is needed to download or stream data.
Define the Spatial Region of Interest
For this example, our spatial region of interest (ROI) will be the Permian Basin, USA. The Permian Basin is a regional depression expanding from the eastern side of New Mexico to West Texas, covering roughly 75 thousand square miles of land. This region is known to be “the largest hydrocarbon-producing basins in the United States and the world” U.S. Energy Information Administration. Consequently, the Permian Basin is prone to excessive air pollution and a suitable site for monitoring methane variability and concentrations using the EMIT data.
# Loading Permian Basin shape file as a region of interest
# User can pass any json or shape file here
# The "geopandas" library makes working with geospatial data easier by facilitating spatial operations on geometric types
= f"{os.getenv('HOME')}/shared/data/use-case-3"
file_path = geopandas.read_file(f"{file_path}/Permian.zip")
permian_basin = permian_basin.set_crs(4326,allow_override=True) permian_basin
:
# The ISS spatial coverage data is called on here
= geopandas.read_file(f'{file_path}/coverage.json')
coverage = coverage.set_crs(4326,allow_override=True)
coverage
# The ISS Date Time is converted to strings
'new_start_time'] = coverage.start_time.astype('str')
coverage[
# locations of where plumes are detected for the August 1, 2022 - present time period
= geopandas.read_file(f'{file_path}/methane_metadata.json')
metadata_json = metadata_json.set_crs(4326,allow_override=True)
metadata_json 'new_start_time'] = metadata_json['UTC Time Observed'].astype('str')
metadata_json['Scene FIDs'] = list(map(lambda x : x[0] ,metadata_json['Scene FIDs']))
metadata_json[= geopandas.read_file(f'{file_path}/target_mask.json')
target_mask = target_mask.set_crs(4326,allow_override=True)
target_mask
# The ISS spatial coverage data is clipped to the ROI (Permian Basin)
= geopandas.clip(coverage, permian_basin)
result_coverage # result_metadata = geopandas.clip(metadata_json, permian_basin)
= result_coverage.reset_index(drop=True)
new_coverage # result_coverage['date'] = result_coverage[(result_coverage['new_start_time'][:10]<='2023-12-15') & (result_coverage['new_start_time'][:10]>='2023-07-01')]
'date'] = new_coverage.start_time.dt.strftime('%Y-%m-%d')
new_coverage[= new_coverage[(new_coverage['date']<='2023-12-15') & (new_coverage['date']>='2023-07-01')]
new_coverage =True, inplace=True)
new_coverage.reset_index(drop new_coverage
Opening and Exploring the Spatial Coverage of the ISS and where plumes were detected
In this cell, we will visualize the EMIT-derived methane plumes over the Permian Basin using the folium
library.
# Setting a colormap using the “branca.colormap” library
= cm.linear.YlGn_09.scale(
colormap min(), coverage.start_time.max()
coverage.start_time.
)
# Initializing a map with a center at [43, -100] and a zoom level of 4. The use of tiles = None has been employed to remove the default basemap.
= folium.Map(location=[43, -100], zoom_start=4, tiles=None)
m_
# The TileLayer library helps in manipulating and displaying layers on a map
# For more information on other available tile layers, please visit https://leaflet-extras.github.io/leaflet-providers/preview/
='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png', name='ESRI World Imagery', attr='Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',overlay='True').add_to(m_)
folium.TileLayer(tiles
# From covergae.json fid, geometry has been selected
# map_layer_coverage_target = folium.GeoJson(target_mask, name = ' Target Mask', marker=folium.Marker(icon=folium.Icon(color='blue', icon='globe')))
= folium.GeoJson(target_mask, name = ' Target Mask', marker=folium.Circle(radius=3, fill_color='blue', color='blue'))
map_layer_coverage_target = folium.GeoJson(metadata_json[['Scene FIDs', 'geometry', 'new_start_time']], name = ' Metadata', marker=folium.Circle(radius=3, fill_color='red', color='red'))
map_layer_coverage_metadata
map_layer_coverage_target.add_to(m_)# map_layer_coverage_target.marker(icon=folium.Icon(color='blue', icon='star'))
map_layer_coverage_metadata.add_to(m_)
# We select “geometry” from the “fid” field in “coverage.json”
'fid', 'geometry', 'new_start_time']].explore(
coverage[["fid",
=False,
categorical=[
tooltip"fid",
"new_start_time",
],=True,
popup=dict(fillOpacity=0.1, width=2),
style_kwds="Coverage",
name=m_,
m=False,
legend=False
show
)
# The “LayerControl” creates the toggle button for all the layers
=False).add_to(m_)
folium.LayerControl(collapsed# Display the map
m_
Opening and Exploring the EMIT Methane Point Source Plume Complexes Data
Search for the EMIT Methane Point Source Plume Complexes Data using the Raster API and its STAC collection name
# Provide STAC and RASTER API endpoints
= "http://ghg.center/api/stac"
STAC_API_URL = "https://ghg.center/api/raster"
RASTER_API_URL
# Please use the collection name similar to the one used in STAC collection
# Name of the collection for methane emission plumes
= "emit-ch4plume-v1" collection_name
# Fetching the collection from STAC collections using appropriate endpoint
# the 'requests' library allows a HTTP request possible
= requests.get(f"{STAC_API_URL}/collections/{collection_name}").json()
collection = {key: value for key, value in collection.items() if key != 'summaries'}
collection collection
Here we are examining the contents of our collection
under the temporal
variable. Please keep in mind that data is available from August 2022 to May 2023. By looking at the dashboard: time density
, we can see that observations are collected on a daily-basis with plume emissions emerging from multiple places on the same date.
# Create a function that would search for the number of items in above data collection in the STAC API
def get_item_count(collection_id):
= 0
count = f"{STAC_API_URL}/collections/{collection_id}/items"
items_url
while True:
= requests.get(items_url)
response
if not response.ok:
print("error getting items")
exit()
= response.json()
stac += int(stac["context"].get("returned", 0))
count next = [link for link in stac["links"] if link["rel"] == "next"]
if not next:
break
= next[0]["href"]
items_url
return count
# Apply the above function and check the total number of items available within the collection
= get_item_count(collection_name)
number_of_items = requests.get(f"{STAC_API_URL}/collections/{collection_name}/items?limit={number_of_items}").json()["features"]
plume_complexes = plume_complexes
parse_plume_complexes print(f"Found {len(plume_complexes)} items")
# Exploring the items in the collection
= list(map(lambda d: d.get('id', f"id not found in dictionary"), plume_complexes))
plume_complex_ids 10] plume_complex_ids[:
# To access the year value from each item more easily, this will let us query more explicitly by year and month (e.g., 2020-02)
= {plume_complex["id"]: plume_complex for plume_complex in plume_complexes}
plume_complexes
# Set the asset value to the appropriate parameter
= "ch4-plume-emissions" asset_name
Below, we are entering the minimum and maximum values to provide our upper and lower bounds in rescale_values
.
# Fetching the min and max values for a specific item
= {"max":plume_complexes[list(plume_complexes.keys())[0]]["assets"][asset_name]["raster:bands"][0]["histogram"]["max"], "min":plume_complexes[list(plume_complexes.keys())[0]]["assets"][asset_name]["raster:bands"][0]["histogram"]["min"]} rescale_values
Now we will pass the item id, collection name, and rescaling_factor
to the Raster API
endpoint. We will do this for only one item so that we can visualize the event.
# Set a colormap for the granule
# Please refer to matplotlib library if you'd prefer choosing a different color ramp (https://matplotlib.org/stable/users/explain/colors/colormaps.html)
= "magma"
color_map
# Select the item ID which you want to visualize. Item ID is in the format yyyymmdd followed by the timestamp. This ID can be extracted from the Cloud Optimized GeoTIFF (OG) name as well.
= "EMIT_L2B_CH4PLM_001_20230418T200118_000829"
plume_complex_id_1
# Extract the tile using the appropriate ID and colormap
= requests.get(
methane_plume_tile_1 f"{RASTER_API_URL}/stac/tilejson.json?collection={plume_complexes[plume_complex_id_1]['collection']}&item={plume_complexes[plume_complex_id_1]['id']}"
f"&assets={asset_name}"
f"&color_formula=gamma+r+1.05&colormap_name={color_map}"
f"&rescale={rescale_values['min']},{rescale_values['max']}",
).json() methane_plume_tile_1
# Set a colormap for the granule
# Please refer to matplotlib library if you'd prefer choosing a different color ramp (https://matplotlib.org/stable/users/explain/colors/colormaps.html)
= "magma"
color_map
# Select the item ID which you want to visualize. Item ID is in the format yyyymmdd followed by the timestamp. This ID can be extracted from the COG name as well.
= "EMIT_L2B_CH4PLM_001_20230614T193706_000038"
plume_complex_id_2
# Extract the tile using the appropriate ID and colormap
= requests.get(
methane_plume_tile_2 f"{RASTER_API_URL}/stac/tilejson.json?collection={plume_complexes[plume_complex_id_2]['collection']}&item={plume_complexes[plume_complex_id_2]['id']}"
f"&assets={asset_name}"
f"&color_formula=gamma+r+1.05&colormap_name={color_map}"
f"&rescale={rescale_values['min']},{rescale_values['max']}",
).json() methane_plume_tile_2
Visualizing CH₄ Emission Plume
# Set a colormap for the granule
# Please refer to matplotlib library if you'd prefer choosing a different color ramp (https://matplotlib.org/stable/users/explain/colors/colormaps.html)
= "magma"
colormap
#Defining the breaks in the colormap
= cm.LinearColormap(colors = ['#310597', '#4C02A1', '#6600A7', '#7E03A8', '#9511A1', '#AA2395', '#BC3587', '#CC4778', '#DA5A6A', '#E66C5C', '#F0804E', '#F89540','#FDAC33', '#FDC527', '#F8DF25'], vmin = 0, vmax = 1500 )
color_map
# Add an appropriate caption, in this case it would be Parts per million meter
= 'ppm-m'
color_map.caption
# Select the item ID which you want to visualize. Item ID is in the format yyyymmdd followed by the timestamp. This ID can be extracted from the COG name as well.
# Set initial zoom and center of map for plume Layer
= folium.Map(location=(methane_plume_tile_1["center"][1], methane_plume_tile_1["center"][0]), zoom_start=13, tiles=None, tooltip = 'test tool tip')
map_ ='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png', name='ESRI World Imagery', attr='Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',overlay='True').add_to(map_)
folium.TileLayer(tiles
# Use the 'TileLayer' library to display the raster layer, add an appropriate caption, and adjust the transparency of the layer on the map
= TileLayer(
map_layer =methane_plume_tile_1["tiles"][0],
tiles='Plume Complex Landfill',
name='True',
overlay="GHG",
attr=1,
opacity
)
map_layer.add_to(map_)
# Adjust map elements
=False, position='bottomleft').add_to(map_)
folium.LayerControl(collapsed
map_.add_child(color_map)= '<style>svg#legend {font-size: 14px; background-color: white;}</style>'
svg_style
map_.get_root().header.add_child(folium.Element(svg_style))
# Visualizing the map
map_
= "magma" # please refer to matplotlib library if you'd prefer choosing a different color ramp.
colormap # For more information on Colormaps in Matplotlib, please visit https://matplotlib.org/stable/users/explain/colors/colormaps.html
#Defining the breaks in the colormap
= cm.LinearColormap(colors = ['#310597', '#4C02A1', '#6600A7', '#7E03A8', '#9511A1', '#AA2395', '#BC3587', '#CC4778', '#DA5A6A', '#E66C5C', '#F0804E', '#F89540','#FDAC33', '#FDC527', '#F8DF25'], vmin = 0, vmax = 1500 )
color_map = 'ppm-m'
color_map.caption
# Set initial zoom and center of map for plume Layer
= folium.Map(location=(methane_plume_tile_2["center"][1], methane_plume_tile_2["center"][0]), zoom_start=13, tiles=None, tooltip = 'test tool tip')
map_ ='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png', name='ESRI World Imagery', attr='Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',overlay='True').add_to(map_)
folium.TileLayer(tiles
= TileLayer(
map_layer =methane_plume_tile_2["tiles"][0],
tiles='Plume Complex Agriculture',
name='True',
overlay="GHG",
attr=1,
opacity
)
map_layer.add_to(map_)
# Adjust map elements
=False, position='bottomleft').add_to(map_)
folium.LayerControl(collapsed
map_.add_child(color_map)= '<style>svg#legend {font-size: 14px; background-color: white;}</style>'
svg_style
map_.get_root().header.add_child(folium.Element(svg_style))
# Visualizing the map
map_
# Creating a dataframe for each plume in the STAC and its corresponding area
= pd.DataFrame()
plume_df for plume in parse_plume_complexes:
= geopandas.GeoDataFrame.from_features([plume])['geometry'].values[0]
temp_polygon = Geod(ellps='WGS84')
geod = wkt.loads(str(temp_polygon))
ply
= {'id':plume['id'], 'geometry':temp_polygon, 'area(km2)':(abs(geod.geometry_area_perimeter(ply)[0])/1e+6), 'date':plume['properties']['datetime'][:10]}
temp_dict = plume_df._append(temp_dict, ignore_index = True) plume_df
# Print the generated dataframe
plume_df# plume['properties']['datetime'][:10]
Visualizing CH4 Emission Plumes in the Permian Basin
# Converting plume_df into a geo dataframe for further geospatial analysis
= geopandas.GeoDataFrame(plume_df, geometry=plume_df.geometry, crs = 'EPSG:4326') geo_temp_df
# Selecting all the targeted plume areas that are within the Permian Basin region
= geopandas.clip(geo_temp_df, permian_basin)
geo_temp_df # filtered_geo_temp_df = geo_temp_df[(geo_temp_df['date']<='2023-12-15') & (geo_temp_df['date']>='2023-07-01') ]
# filtered_geo_temp_df
= "magma" # please refer to matplotlib library if you'd prefer choosing a different color ramp.
colormap # For more information on Colormaps in Matplotlib, please visit https://matplotlib.org/stable/users/explain/colors/colormaps.html
# Defining the breaks in the colormap
= cm.LinearColormap(colors = ['#310597', '#4C02A1', '#6600A7', '#7E03A8', '#9511A1', '#AA2395', '#BC3587', '#CC4778', '#DA5A6A', '#E66C5C', '#F0804E', '#F89540','#FDAC33', '#FDC527', '#F8DF25'], vmin = 0, vmax = 1500 )
color_map = 'ppm-m'
color_map.caption
# Set the initial zoom value and adjust the plume layer to be displayed at the 31.8 degree North and -101.7 degree West
= folium.Map(location=[31.8,-101.7], zoom_start=11, tiles=None)
map_ ='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png', name='ESRI World Imagery', attr='Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',overlay='True').add_to(map_)
folium.TileLayer(tiles'fid', 'geometry', 'new_start_time']].explore(
new_coverage[["fid",
=True,
categorical=[
tooltip"fid",
"new_start_time",
],=True,
popup=dict(fillOpacity=0.1, width=2),
style_kwds="Coverage",
name=map_,
m=False,
legend=True
show
)
= folium.GeoJson(permian_basin, name= 'Permian Shape')
map_layer_permian
map_layer_permian.add_to(map_)
# Extract and display the tile using the appropriate ID, colormap, asset name (parameter), and rescale values
for tile_id in geo_temp_df['id']:
= requests.get(
methane_plume_tile f"{RASTER_API_URL}/stac/tilejson.json?collection={plume_complexes[tile_id]['collection']}&item={plume_complexes[tile_id]['id']}"
f"&assets={asset_name}"
f"&color_formula=gamma+r+1.05&colormap_name={colormap}"
f"&rescale={rescale_values['min']},{rescale_values['max']}",
).json()
methane_plume_tile
= TileLayer(
map_layer =methane_plume_tile["tiles"][0],
tiles=tile_id,
name=True,
show='True',
overlay="GHG",
attr=1,
opacity
)
map_layer.add_to(map_)
# Adjust map elements
=True, position='bottomleft').add_to(map_)
folium.LayerControl(collapsed
map_.add_child(color_map)= '<style>svg#legend {font-size: 14px; background-color: white;}</style>'
svg_style
map_.get_root().header.add_child(folium.Element(svg_style))
# Visualizing the map
map_