This tutorial demonstrates how to access and compare coincident snow data across in-situ, airborne, and satellite platforms from NASA's SnowEx, ASO, and MODIS data sets, respectively. All data are available from the NASA National Snow and Ice Data Center Distributed Active Archive Center, or NSIDC DAAC.
The National Snow and Ice Data Center provides over 1100 data sets covering the Earth's cryosphere and more, all of which are available to the public free of charge. Beyond providing these data, NSIDC creates tools for data access, supports data users, performs scientific research, and educates the public about the cryosphere.
Snow Today, a collaboration with the University of Colorado's Institute of Alpine and Arctic Research (INSTAAR), provides near-real-time snow analysis for the western United States and regular reports on conditions during the winter season. Snow Today is funded by NASA Hydrological Sciences Program and utilizes data from the Moderate Resolution Imaging Spectroradiometer (MODIS) instrument and snow station data from the Snow Telemetry (SNOTEL) network by the Natural Resources Conservation Service (NRCS), United States Department of Agriculture (USDA) and the California Department of Water Resources: www.wcc.nrcs.usda.gov/snow.
Near-Real-Time SSM/I-SSMIS EASE-Grid Daily Global Ice Concentration and Snow Extent (NISE), Version 5: https://doi.org/10.5067/3KB2JPLFPK3R
Get started by importing packages needed to run the following code blocks, including the tutorial_helper_functions
module provided within this repository.
import os
import geopandas as gpd
from shapely.geometry import Polygon, mapping
from shapely.geometry.polygon import orient
import pandas as pd
import matplotlib.pyplot as plt
import rasterio
from rasterio.plot import show
import numpy as np
import pyresample as prs
import requests
import json
import pprint
from rasterio.mask import mask
from mpl_toolkits.axes_grid1 import make_axes_locatable
# This is our functions module. We created several helper functions to discover, access, and harmonize the data below.
import tutorial_helper_functions as fn
Start by identifying your study area and exploring coincident data over the same time and area.
NASA Earthdata Search can be used to visualize file coverage over mulitple data sets and to access the same data you will be working with below: https://search.earthdata.nasa.gov/projects?projectId=5366449248
Since our focus is on the Grand Mesa study site of the NASA SnowEx campaign, we'll use that area to search for coincident data across other data products. From the SnowEx17 Ground Penetrating Radar Version 2 landing page, you can find the rectangular spatial coverage under the Overview tab, or you can draw a polygon over your area of interest in the map under the Download Data tab and export the shape as a geojson file using the Export Polygon icon shown below. An example polygon geojson file is provided in the /Data folder of this repository.
Read in the geojson file as a GeoDataFrame object and simplify and reorder using the shapely package. This will be converted back to a dictionary to be applied as our polygon search parameter.
polygon_filepath = str(os.getcwd() + '/Data/nsidc-polygon.json') # Note: A shapefile or other vector-based spatial data format could be substituted here.
gdf = gpd.read_file(polygon_filepath) #Return a GeoDataFrame object
# Simplify polygon for complex shapes in order to pass a reasonable request length to CMR. The larger the tolerance value, the more simplified the polygon.
# Orient counter-clockwise: CMR polygon points need to be provided in counter-clockwise order. The last point should match the first point to close the polygon.
poly = orient(gdf.simplify(0.05, preserve_topology=False).loc[0],sign=1.0)
#Format dictionary to polygon coordinate pairs for CMR polygon filtering
polygon = ','.join([str(c) for xy in zip(*poly.exterior.coords.xy) for c in xy])
print('Polygon coordinates to be used in search:', polygon)
poly
Polygon coordinates to be used in search: -108.2352445938561,38.98556907427165,-107.85284607930835,38.978765032966244,-107.85494925720668,39.10596902171742,-108.22772795408136,39.11294532581687,-108.2352445938561,38.98556907427165
We are interested in accessing files within each data set over the same time range, so we'll start by searching all of 2017.
temporal = '2017-01-01T00:00:00Z,2017-12-31T23:59:59Z' # Set temporal range
Create a nested dictionary with each data set shortname and version, as well as shared temporal range and polygonal area of interest. Data set shortnames, or IDs, as well as version numbers, are located at the top of every NSIDC landing page.
data_dict = { 'snowex': {'short_name': 'SNEX17_GPR','version': '2','polygon': polygon,'temporal':temporal},
'aso': {'short_name': 'ASO_3M_SD','version': '1','polygon': polygon,'temporal':temporal},
'modis': {'short_name': 'MOD10A1','version': '6','polygon': polygon,'temporal':temporal}
}
We will use the granule_info
function to query metadata about each data set and associated files using the Common Metadata Repository (CMR), which is a high-performance, high-quality, continuously evolving metadata system that catalogs Earth Science data and associated service metadata records. Note that not all NSIDC data can be searched at the file level using CMR, particularly those outside of the NASA DAAC program.
for k, v in data_dict.items(): fn.granule_info(data_dict[k])
There are 3 files of SNEX17_GPR version 2 over my area and time of interest. The average size of each file is 69.73 MB and the total size of all 3 granules is 209.20 MB There are 5 files of ASO_3M_SD version 1 over my area and time of interest. The average size of each file is 1689.92 MB and the total size of all 5 granules is 8449.60 MB There are 364 files of MOD10A1 version 6 over my area and time of interest. The average size of each file is 8.23 MB and the total size of all 364 granules is 2995.34 MB
The function above tells us the size of data available for each data set over our time and area of interest, but we want to go a step further and determine what time ranges are coincident based on our bounding box. This time_overlap
helper function returns a dataframe with file names, dataset_id, start date, and end date for all files that overlap in temporal range across all data sets of interest.
search_df = fn.time_overlap(data_dict)
print(len(search_df), ' total files returned')
search_df
19 total files returned
dataset_id | short_name | version | producer_granule_id | start_date | end_date | |
---|---|---|---|---|---|---|
0 | SnowEx17 Ground Penetrating Radar V002 | SNEX17_GPR | 002 | SnowEx17_GPR_Version2_Week1.csv | 2017-02-08T00:00:00.000Z | 2017-02-10T23:59:59.000Z |
1 | SnowEx17 Ground Penetrating Radar V002 | SNEX17_GPR | 002 | SnowEx17_GPR_Version2_Week2.csv | 2017-02-14T00:00:00.000Z | 2017-02-17T23:59:59.000Z |
2 | SnowEx17 Ground Penetrating Radar V002 | SNEX17_GPR | 002 | SnowEx17_GPR_Version2_Week3.csv | 2017-02-21T00:00:00.000Z | 2017-02-25T23:59:59.000Z |
3 | ASO L4 Lidar Snow Depth 3m UTM Grid V001 | ASO_3M_SD | 001 | ASO_3M_SD_USCOGM_20170208 | 2017-02-08T00:00:00.000Z | 2017-02-08T23:59:59.000Z |
4 | ASO L4 Lidar Snow Depth 3m UTM Grid V001 | ASO_3M_SD | 001 | ASO_3M_SD_USCOGM_20170216 | 2017-02-16T00:00:00.000Z | 2017-02-16T23:59:59.000Z |
6 | ASO L4 Lidar Snow Depth 3m UTM Grid V001 | ASO_3M_SD | 001 | ASO_3M_SD_USCOGM_20170221 | 2017-02-21T00:00:00.000Z | 2017-02-21T23:59:59.000Z |
7 | ASO L4 Lidar Snow Depth 3m UTM Grid V001 | ASO_3M_SD | 001 | ASO_3M_SD_USCOGM_20170225 | 2017-02-25T00:00:00.000Z | 2017-02-25T23:59:59.000Z |
46 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017039.h09v05.006.2017041102600.hdf | 2017-02-08T16:20:00.000Z | 2017-02-08T19:40:00.000Z |
47 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017040.h09v05.006.2017042102640.hdf | 2017-02-09T17:05:00.000Z | 2017-02-09T18:50:00.000Z |
48 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017041.h09v05.006.2017043095629.hdf | 2017-02-10T16:10:00.000Z | 2017-02-10T19:30:00.000Z |
52 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017045.h09v05.006.2017047103323.hdf | 2017-02-14T17:20:00.000Z | 2017-02-14T19:05:00.000Z |
53 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017046.h09v05.006.2017052213130.hdf | 2017-02-15T16:30:00.000Z | 2017-02-15T18:10:00.000Z |
54 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017047.h09v05.006.2017053103120.hdf | 2017-02-16T17:10:00.000Z | 2017-02-16T18:55:00.000Z |
55 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017048.h09v05.006.2017050103600.hdf | 2017-02-17T16:15:00.000Z | 2017-02-17T19:35:00.000Z |
59 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017052.h09v05.006.2017054100801.hdf | 2017-02-21T17:30:00.000Z | 2017-02-21T19:10:00.000Z |
60 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017053.h09v05.006.2017055094801.hdf | 2017-02-22T16:35:00.000Z | 2017-02-22T18:20:00.000Z |
61 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017054.h09v05.006.2017059063600.hdf | 2017-02-23T17:15:00.000Z | 2017-02-23T19:00:00.000Z |
62 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017055.h09v05.006.2017057092149.hdf | 2017-02-24T16:20:00.000Z | 2017-02-24T19:40:00.000Z |
63 | MODIS/Terra Snow Cover Daily L3 Global 500m SI... | MOD10A1 | 006 | MOD10A1.A2017056.h09v05.006.2017058092815.hdf | 2017-02-25T17:05:00.000Z | 2017-02-25T18:50:00.000Z |
The number of files has been greatly reduced to only those needed to compare data across these data sets. This CMR query will collect the data file URLs, including the associated quality and metadata files if available.
# Create new dictionary with fields needed for CMR url search
url_df = search_df.drop(columns=['start_date', 'end_date','version','dataset_id'])
url_dict = url_df.to_dict('records')
# CMR search variables
granule_search_url = 'https://cmr.earthdata.nasa.gov/search/granules'
headers= {'Accept': 'application/json'}
# Create URL list from each df row
urls = []
for i in range(len(url_dict)):
response = requests.get(granule_search_url, params=url_dict[i], headers=headers)
results = json.loads(response.content)
urls.append(fn.cmr_filter_urls(results))
# flatten url list
urls = list(np.concatenate(urls))
urls
['https://n5eil01u.ecs.nsidc.org/DP1/SNOWEX/SNEX17_GPR.002/2017.02.08/SnowEx17_GPR_Version2_Week1.csv', 'https://n5eil01u.ecs.nsidc.org/DP1/SNOWEX/SNEX17_GPR.002/2017.02.08/SnowEx17_GPR_Version2_Week1.csv.xml', 'https://n5eil01u.ecs.nsidc.org/DP1/SNOWEX/SNEX17_GPR.002/2017.02.14/SnowEx17_GPR_Version2_Week2.csv', 'https://n5eil01u.ecs.nsidc.org/DP1/SNOWEX/SNEX17_GPR.002/2017.02.14/SnowEx17_GPR_Version2_Week2.csv.xml', 'https://n5eil01u.ecs.nsidc.org/DP1/SNOWEX/SNEX17_GPR.002/2017.02.21/SnowEx17_GPR_Version2_Week3.csv', 'https://n5eil01u.ecs.nsidc.org/DP1/SNOWEX/SNEX17_GPR.002/2017.02.21/SnowEx17_GPR_Version2_Week3.csv.xml', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.08/ASO_3M_QF_USCOGM_20170208.tif', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.08/ASO_3M_SD_USCOGM_20170208.tif', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.08/ASO_3M_SD_USCOGM_20170208.tif.xml', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.16/ASO_3M_QF_USCOGM_20170216.tif', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.16/ASO_3M_SD_USCOGM_20170216.tif', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.16/ASO_3M_SD_USCOGM_20170216.tif.xml', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.21/ASO_3M_QF_USCOGM_20170221.tif', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.21/ASO_3M_SD_USCOGM_20170221.tif', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.21/ASO_3M_SD_USCOGM_20170221.tif.xml', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.25/ASO_3M_QF_USCOGM_20170225.tif', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.25/ASO_3M_SD_USCOGM_20170225.tif', 'https://n5eil01u.ecs.nsidc.org/DP1/ASO/ASO_3M_SD.001/2017.02.25/ASO_3M_SD_USCOGM_20170225.tif.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.08/MOD10A1.A2017039.h09v05.006.2017041102600.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.08/MOD10A1.A2017039.h09v05.006.2017041102600.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.09/MOD10A1.A2017040.h09v05.006.2017042102640.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.09/MOD10A1.A2017040.h09v05.006.2017042102640.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.10/MOD10A1.A2017041.h09v05.006.2017043095629.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.10/MOD10A1.A2017041.h09v05.006.2017043095629.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.14/MOD10A1.A2017045.h09v05.006.2017047103323.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.14/MOD10A1.A2017045.h09v05.006.2017047103323.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.15/MOD10A1.A2017046.h09v05.006.2017052213130.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.15/MOD10A1.A2017046.h09v05.006.2017052213130.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.16/MOD10A1.A2017047.h09v05.006.2017053103120.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.16/MOD10A1.A2017047.h09v05.006.2017053103120.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.17/MOD10A1.A2017048.h09v05.006.2017050103600.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.17/MOD10A1.A2017048.h09v05.006.2017050103600.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.21/MOD10A1.A2017052.h09v05.006.2017054100801.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.21/MOD10A1.A2017052.h09v05.006.2017054100801.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.22/MOD10A1.A2017053.h09v05.006.2017055094801.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.22/MOD10A1.A2017053.h09v05.006.2017055094801.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.23/MOD10A1.A2017054.h09v05.006.2017059063600.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.23/MOD10A1.A2017054.h09v05.006.2017059063600.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.24/MOD10A1.A2017055.h09v05.006.2017057092149.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.24/MOD10A1.A2017055.h09v05.006.2017057092149.hdf.xml', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.25/MOD10A1.A2017056.h09v05.006.2017058092815.hdf', 'https://n5eil01u.ecs.nsidc.org/DP4/MOST/MOD10A1.006/2017.02.25/MOD10A1.A2017056.h09v05.006.2017058092815.hdf.xml']
Data can be accessed directly from our HTTPS file system through the URLs collected above, or through our Application Programming Interface (API). Our API offers you the ability to order data using specific temporal and spatial filters, as well as subset, reformat, and reproject select data sets. The same subsetting, reformatting, and reprojection services available on select data sets through NASA Earthdata Search can also be applied using this API. These options can be requested in a single access command without the need to script against our data directory structure. See our programmatic access guide for more information on API options.
According to https://nsidc.org/support/faq/what-data-subsetting-reformatting-and-reprojection-services-are-available-for-MODIS-data, we can see that spatial subsetting and GeoTIFF reformatting are available for MOD10A1 so those options are requested below. The area subset must be described as a bounding box, which can be created based on the polygon bounds above. We will also add GeoTIFF reformatting to the MOD10A1 data dictionary and the temporal range will be set based on the range of MOD10A1 files in the dataframe above. These new parameters will be added to the API request below.
bounds = poly.bounds # Get polygon bounds to be used as bounding box input
data_dict['modis']['bbox'] = ','.join(map(str, list(bounds))) # Add bounding box subsetting to MODIS dictionary
data_dict['modis']['format'] = 'GeoTIFF' # Add geotiff reformatting to MODIS dictionary
# Set new temporal range based on dataframe above. Note that this will request all MOD10A1 data falling within this time range.
modis_start = min(search_df.loc[search_df['short_name'] == 'MOD10A1', 'start_date'])
modis_end = max(search_df.loc[search_df['short_name'] == 'MOD10A1', 'end_date'])
data_dict['modis']['temporal'] = ','.join([modis_start,modis_end])
print(data_dict['modis'])
{'short_name': 'MOD10A1', 'version': '6', 'polygon': '-108.2352445938561,38.98556907427165,-107.85284607930835,38.978765032966244,-107.85494925720668,39.10596902171742,-108.22772795408136,39.11294532581687,-108.2352445938561,38.98556907427165', 'temporal': '2017-02-08T16:20:00.000Z,2017-02-25T18:50:00.000Z', 'page_size': 2000, 'page_num': 1, 'bbox': '-108.2352445938561,38.978765032966244,-107.85284607930835,39.11294532581687', 'format': 'GeoTIFF'}
Programmatic API requests are formatted as HTTPS URLs that contain key-value-pairs specifying the service operations that we specified above. We will first create a string of key-value-pairs from our data dictionary and we'll feed those into our API endpoint. This API endpoint can be executed via command line, a web browser, or in Python below.
base_url = 'https://n5eil02u.ecs.nsidc.org/egi/request' # Set NSIDC data access base URL
#data_dict['modis']['request_mode'] = 'stream' # Set the request mode to asynchronous
param_string = '&'.join("{!s}={!r}".format(k,v) for (k,v) in data_dict['modis'].items()) # Convert param_dict to string
param_string = param_string.replace("'","") # Remove quotes
api_request = [f'{base_url}?{param_string}']
print(api_request[0]) # Print API base URL + request parameters
https://n5eil02u.ecs.nsidc.org/egi/request?short_name=MOD10A1&version=6&polygon=-108.2352445938561,38.98556907427165,-107.85284607930835,38.978765032966244,-107.85494925720668,39.10596902171742,-108.22772795408136,39.11294532581687,-108.2352445938561,38.98556907427165&temporal=2017-02-08T16:20:00.000Z,2017-02-25T18:50:00.000Z&page_size=2000&page_num=1&bbox=-108.2352445938561,38.978765032966244,-107.85284607930835,39.11294532581687&format=GeoTIFF
The following functions will return the file URLs and the MOD10A1 API request. For demonstration purposes, these functions have been commented out, and instead the data utilized in the following steps will be accessed from a staged directory. *Note that if you are running this notebook in Binder, the memory may not be sufficient to download these files. Please use the Docker or local Conda options provided in the README if you are interested in downloading all files.*
path = str(os.getcwd() + '/Data')
if not os.path.exists(path):
os.mkdir(path)
os.chdir(path)
#fn.cmr_download(urls)
#fn.cmr_download(api_request)
# pull data from staged bucket for demonstration
!awscliv2 --no-sign-request s3 cp s3://snowex-aso-modis-tutorial-data/ ./ --recursive #access data in staged directory
zsh:1: command not found: aws
This SnowEx data set is provided in CSV. A Pandas DataFrame is used to easily read in data. For these next steps, just one day's worth of data will be selected from this file and the coincident ASO and MODIS data will be selected.
snowex_path = './SnowEx17_GPR_Version2_Week1.csv' # Define local filepath
df = pd.read_csv(snowex_path, sep='\t')
df.head()
collection | trace | long | lat | elev | twtt | Thickness | SWE | x | y | UTM_Zone | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | GPR_0042_020817 | 2581 | -108.066856 | 39.043146 | 3240.2 | 5.89 | 0.692 | 225 | 753854.880092 | 4.325659e+06 | 12 S |
1 | GPR_0042_020817 | 2582 | -108.066856 | 39.043146 | 3240.2 | 5.89 | 0.692 | 225 | 753854.899385 | 4.325660e+06 | 12 S |
2 | GPR_0042_020817 | 2583 | -108.066856 | 39.043146 | 3240.2 | 5.87 | 0.690 | 224 | 753854.918686 | 4.325660e+06 | 12 S |
3 | GPR_0042_020817 | 2584 | -108.066855 | 39.043146 | 3240.2 | 5.86 | 0.689 | 224 | 753854.937987 | 4.325660e+06 | 12 S |
4 | GPR_0042_020817 | 2585 | -108.066855 | 39.043147 | 3240.2 | 5.84 | 0.686 | 223 | 753854.957280 | 4.325660e+06 | 12 S |
The collection date needs to be extracted from the collection
value and a new dataframe will be generated as a subset of the original based on a single day:
df['date'] = df.collection.str.rsplit('_').str[-1].astype(str)
df.date = pd.to_datetime(df.date, format="%m%d%y")
df = df.sort_values(['date'])
df_subset = df[df['date'] == '2017-02-08'] # subset original dataframe and only select this date
df.head()
collection | trace | long | lat | elev | twtt | Thickness | SWE | x | y | UTM_Zone | date | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | GPR_0042_020817 | 2581 | -108.066856 | 39.043146 | 3240.20 | 5.89 | 0.692 | 225 | 753854.880092 | 4.325659e+06 | 12 S | 2017-02-08 |
109172 | GPR_0043_020817 | 6360 | -108.063209 | 39.049202 | 3248.49 | 11.49 | 1.350 | 439 | 754148.853700 | 4.326342e+06 | 12 S | 2017-02-08 |
109173 | GPR_0043_020817 | 6361 | -108.063209 | 39.049202 | 3248.50 | 11.56 | 1.358 | 441 | 754148.882549 | 4.326342e+06 | 12 S | 2017-02-08 |
109174 | GPR_0043_020817 | 6362 | -108.063208 | 39.049202 | 3248.50 | 11.62 | 1.365 | 444 | 754148.911407 | 4.326342e+06 | 12 S | 2017-02-08 |
109175 | GPR_0043_020817 | 6363 | -108.063208 | 39.049202 | 3248.50 | 11.64 | 1.368 | 445 | 754148.947466 | 4.326342e+06 | 12 S | 2017-02-08 |
According to the SnowEx documentation, the data are available in UTM Zone 12N so we'll set to this projection so that we can buffer in meters in the next step:
gdf_utm= gpd.GeoDataFrame(df_subset, geometry=gpd.points_from_xy(df_subset.x, df_subset.y), crs='EPSG:32612')
gdf_utm.head()
collection | trace | long | lat | elev | twtt | Thickness | SWE | x | y | UTM_Zone | date | geometry | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | GPR_0042_020817 | 2581 | -108.066856 | 39.043146 | 3240.20 | 5.89 | 0.692 | 225 | 753854.880092 | 4.325659e+06 | 12 S | 2017-02-08 | POINT (753854.880 4325659.484) |
109172 | GPR_0043_020817 | 6360 | -108.063209 | 39.049202 | 3248.49 | 11.49 | 1.350 | 439 | 754148.853700 | 4.326342e+06 | 12 S | 2017-02-08 | POINT (754148.854 4326341.915) |
109173 | GPR_0043_020817 | 6361 | -108.063209 | 39.049202 | 3248.50 | 11.56 | 1.358 | 441 | 754148.882549 | 4.326342e+06 | 12 S | 2017-02-08 | POINT (754148.883 4326341.916) |
109174 | GPR_0043_020817 | 6362 | -108.063208 | 39.049202 | 3248.50 | 11.62 | 1.365 | 444 | 754148.911407 | 4.326342e+06 | 12 S | 2017-02-08 | POINT (754148.911 4326341.917) |
109175 | GPR_0043_020817 | 6363 | -108.063208 | 39.049202 | 3248.50 | 11.64 | 1.368 | 445 | 754148.947466 | 4.326342e+06 | 12 S | 2017-02-08 | POINT (754148.947 4326341.918) |
We can further subset the SnowEx snow depth data to get within a 500 m radius of the SNOTEL Mesa Lakes site.
First we'll create a new geodataframe with the SNOTEL site location, set to our SnowEx UTM coordinate reference system, and create a 500 meter buffer around this point. Then we'll subset the SnowEx points to the buffer and convert back to the WGS84 CRS:
# Create another geodataframe (gdfsel) with the center point for the selection
df_snotel = pd.DataFrame(
{'SNOTEL Site': ['Mesa Lakes'],
'Latitude': [39.05],
'Longitude': [-108.067]})
gdf_snotel = gpd.GeoDataFrame(df_snotel, geometry=gpd.points_from_xy(df_snotel.Longitude, df_snotel.Latitude), crs='EPSG:4326')
gdf_snotel.to_crs('EPSG:32612', inplace=True) # set CRS to UTM 12 N
buffer = gdf_snotel.buffer(500) #create 500 m buffer
gdf_buffer = gdf_utm.loc[gdf_utm.geometry.within(buffer.unary_union)] # subset dataframe to buffer region
gdf_buffer = gdf_buffer.to_crs('EPSG:4326')
Snow depth data from the ASO L4 Lidar Snow Depth 3m UTM Grid data set were calculated from surface elevation measured by the Riegl LMS-Q1560 airborne laser scanner (ALS). The data are provided in GeoTIFF format, so we'll use the Rasterio library to read in the data.
aso_path = './ASO_3M_SD_USCOGM_20170208.tif' # Define local filepath
aso = rasterio.open(aso_path)
In order to reduce the data volume to the buffered region of interest, we can subset this GeoTIFF to the same SNOTEL buffer:
buffer = buffer.to_crs(crs=aso.crs) # convert buffer to CRS of ASO rasterio object
out_img, out_transform = mask(aso, buffer, crop=True)
out_meta = aso.meta.copy()
epsg_code = int(aso.crs.data['init'][5:])
out_meta.update({"driver": "GTiff", "height": out_img.shape[1], "width": out_img.shape[2], "transform": out_transform, "crs": '+proj=utm +zone=13 +datum=WGS84 +units=m +no_defs'})
out_tif = 'clipped_ASO_3M_SD_USCOGM_20170208.tif'
with rasterio.open(out_tif, 'w', **out_meta) as dest:
dest.write(out_img)
clipped_aso = rasterio.open(out_tif)
aso_array = clipped_aso.read(1, masked=True)
We are interested in the Normalized Difference Snow Index (NDSI) snow cover value from the MOD10A1 data set, which is an index that is related to the presence of snow in a pixel. According to the MOD10A1 FAQ, snow cover is detected using the NDSI ratio of the difference in visible reflectance (VIS) and shortwave infrared reflectance (SWIR), where NDSI = ((band 4-band 6) / (band 4 + band 6)).
Note that you may need to change this filename output below if you download the data outside of the staged bucket, as the output names may vary per request.
modis_path = './MOD10A1_A2017039_h09v05_006_2017041102600_MOD_Grid_Snow_500m_NDSI_Snow_Cover_99f6ee91_subsetted.tif' # Define local filepath
modis = rasterio.open(modis_path)
modis_array = modis.read(1, masked=True)
In order to add data from these ASO and MODIS gridded data sets, we need to define the geometry parameters for theses, as well as the SnowEx data. The SnowEx geometry is set using the longitude and latitude values of the geodataframe:
snowex_geometry = prs.geometry.SwathDefinition(lons=gdf_buffer['long'], lats=gdf_buffer['lat'])
print('snowex geometry: ', snowex_geometry)
snowex geometry: Shape: (26516,) Lons: [-108.063209 -108.06320867 -108.06320833 ... -108.06127167 -108.061267 -108.06214585] Lats: [39.04920167 39.04920167 39.04920167 ... 39.04973833 39.04973658 39.05015724]
With ASO and MODIS data on regular grids, we can create area definitions for these using projection and extent metadata:
pprint.pprint(clipped_aso.profile)
print('')
print(clipped_aso.bounds)
pprint.pprint(modis.profile)
print('')
print(modis.bounds)
{'count': 1, 'crs': CRS.from_epsg(32613), 'driver': 'GTiff', 'dtype': 'float32', 'height': 334, 'interleave': 'band', 'nodata': -9999.0, 'tiled': False, 'transform': Affine(3.0, 0.0, 234081.0, 0.0, -3.0, 4327305.0), 'width': 335} BoundingBox(left=234081.0, bottom=4326303.0, right=235086.0, top=4327305.0) {'compress': 'deflate', 'count': 1, 'crs': CRS.from_wkt('PROJCS["Sinusoidal Modis Spheroid",GEOGCS["Unknown datum based upon the Authalic Sphere",DATUM["Not_specified_based_on_Authalic_Sphere",SPHEROID["Sphere",6371007.181,0,AUTHORITY["EPSG","7035"]],AUTHORITY["EPSG","6035"]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4035"]],PROJECTION["Sinusoidal"],PARAMETER["longitude_of_center",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]]]'), 'driver': 'GTiff', 'dtype': 'uint8', 'height': 34, 'interleave': 'band', 'nodata': None, 'tiled': False, 'transform': Affine(463.3127165279165, 0.0, -9356136.99756175, 0.0, -463.3127165279165, 4349579.782763082), 'width': 110} BoundingBox(left=-9356136.99756175, bottom=4333827.150401132, right=-9305172.598743679, top=4349579.782763082)
# Create area definition for ASO
area_id = 'UTM_13N' # area_id: ID of area
description = 'WGS 84 / UTM zone 13N' # description: Description
proj_id = 'UTM_13N' # proj_id: ID of projection (being deprecated)
projection = 'EPSG:32613' # projection: Proj4 parameters as a dict or string
width = clipped_aso.width # width: Number of grid columns
height = clipped_aso.height # height: Number of grid rows
area_extent = (234081.0, 4326303.0, 235086.0, 4327305.0)
aso_geometry = prs.geometry.AreaDefinition(area_id, description, proj_id, projection, width, height, area_extent)
# Create area definition for MODIS
area_id = 'Sinusoidal' # area_id: ID of area
description = 'Sinusoidal Modis Spheroid' # description: Description
proj_id = 'Sinusoidal' # proj_id: ID of projection (being deprecated)
projection = 'PROJCS["Sinusoidal Modis Spheroid",GEOGCS["Unknown datum based upon the Authalic Sphere",DATUM["Not_specified_based_on_Authalic_Sphere",SPHEROID["Sphere",6371007.181,887203.3395236016,AUTHORITY["EPSG","7035"]],AUTHORITY["EPSG","6035"]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4035"]],PROJECTION["Sinusoidal"],PARAMETER["longitude_of_center",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]]]' # projection: Proj4 parameters as a dict or string
width = modis.width # width: Number of grid columns
height = modis.height # height: Number of grid rows
area_extent = (-9332971.361735353, 4341240.1538655795, -9331118.110869242, 4343093.404731691)
modis_geometry = prs.geometry.AreaDefinition(area_id, description, proj_id, projection, width, height, area_extent)
To interpolate ASO snow depth and MODIS snow cover data to SnowEx snow depth points, we can use the pyresample
library. The radius_of_influence
parameter determines maximum radius to look for nearest neighbor interpolation.
# add ASO values to geodataframe
import warnings
warnings.filterwarnings('ignore') # ignore warning when resampling to a different projection
gdf_buffer['aso_snow_depth'] = prs.kd_tree.resample_nearest(aso_geometry, aso_array, snowex_geometry, radius_of_influence=3)
# add MODIS values to geodataframe
gdf_buffer['modis_ndsi'] = prs.kd_tree.resample_nearest(modis_geometry, modis_array, snowex_geometry, radius_of_influence=500)
gdf_buffer.head()
collection | trace | long | lat | elev | twtt | Thickness | SWE | x | y | UTM_Zone | date | geometry | aso_snow_depth | modis_ndsi | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
109172 | GPR_0043_020817 | 6360 | -108.063209 | 39.049202 | 3248.49 | 11.49 | 1.350 | 439 | 754148.853700 | 4.326342e+06 | 12 S | 2017-02-08 | POINT (-108.06321 39.04920) | 1.302658 | 71 |
109173 | GPR_0043_020817 | 6361 | -108.063209 | 39.049202 | 3248.50 | 11.56 | 1.358 | 441 | 754148.882549 | 4.326342e+06 | 12 S | 2017-02-08 | POINT (-108.06321 39.04920) | 1.302658 | 71 |
109174 | GPR_0043_020817 | 6362 | -108.063208 | 39.049202 | 3248.50 | 11.62 | 1.365 | 444 | 754148.911407 | 4.326342e+06 | 12 S | 2017-02-08 | POINT (-108.06321 39.04920) | 1.302658 | 71 |
109175 | GPR_0043_020817 | 6363 | -108.063208 | 39.049202 | 3248.50 | 11.64 | 1.368 | 445 | 754148.947466 | 4.326342e+06 | 12 S | 2017-02-08 | POINT (-108.06321 39.04920) | 1.302658 | 71 |
109176 | GPR_0043_020817 | 6364 | -108.063207 | 39.049202 | 3248.50 | 11.68 | 1.372 | 446 | 754148.983533 | 4.326342e+06 | 12 S | 2017-02-08 | POINT (-108.06321 39.04920) | 1.302658 | 71 |
The rasterio plot module allows you to directly plot GeoTIFFs objects. The SnowEx Thickness
values are plotted against the clipped ASO snow depth raster.
gdf_buffer_aso_crs = gdf_buffer.to_crs('EPSG:32613')
from mpl_toolkits.axes_grid1 import make_axes_locatable
fig, ax = plt.subplots(figsize=(10, 10))
show(clipped_aso, ax=ax)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.1) # fit legend to height of plot
gdf_buffer_aso_crs.plot(column='Thickness', ax=ax, cmap='OrRd', legend=True, cax=cax, legend_kwds=
{'label': "Snow Depth (m)",});
We can do the same for MOD10A1: This was subsetted to the entire Grand Mesa region defined by the SnowEx data set coverage.
# Set dataframe to MOD10A1 Sinusoidal projection
gdf_buffer_modis_crs = gdf_buffer.to_crs('PROJCS["Sinusoidal Modis Spheroid",GEOGCS["Unknown datum based upon the Authalic Sphere",DATUM["Not_specified_based_on_Authalic_Sphere",SPHEROID["Sphere",6371007.181,887203.3395236016,AUTHORITY["EPSG","7035"]],AUTHORITY["EPSG","6035"]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433],AUTHORITY["EPSG","4035"]],PROJECTION["Sinusoidal"],PARAMETER["longitude_of_center",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]]]')
fig, ax = plt.subplots(figsize=(10, 10))
show(modis, ax=ax)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.1) # fit legend to height of plot
gdf_buffer_modis_crs.plot(column='Thickness', ax=ax, cmap='OrRd', legend=True, cax=cax, legend_kwds=
{'label': "Snow Depth (m)",});
NASA’s EOSDIS Worldview mapping application provides the capability to interactively browse over 900 global, full-resolution satellite imagery layers and then download the underlying data. Many of the available imagery layers are updated within three hours of observation, essentially showing the entire Earth as it looks “right now."
According to the MOD10A1 landing page, snow cover imagery layers from this data set are available through NASA Worldview. This layer can be downloaded as various image files including GeoTIFF using the snapshot feature at the top right of the page. This link presents the MOD10A1 NDSI layer over our time and area of interest: https://go.nasa.gov/35CgYMd.
Additionally, the NASA Global Browse Imagery Service provides up to date, full resolution imagery for select NSIDC DAAC data sets as web services including WMTS, WMS, KML, and more. These layers can be accessed in GIS applications following guidance on the GIBS documentation pages.
Finally, the dataframe can be exported as an Esri shapefile for further analysis in GIS:
gdf_buffer = gdf_buffer.drop(columns=['date'])
gdf_buffer.to_file('snow-data-20170208.shp')