94 | **Date last modified:** 02-03-2025
95 |
96 | 1LP DAAC Work performed under NASA contract NNG14HH33I.
97 |
--------------------------------------------------------------------------------
/python/tutorials/ECOSTRESS_Tutorial.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Working with ECOSTRESS Evapotranspiration Data\n",
8 | "### This tutorial demonstrates how to work with the ECOSTRESS Evapotranspiration PT-JPL Daily L3 Global 70m Version 1 ([ECO3ETPTJPL.001](https://doi.org/10.5067/ECOSTRESS/ECO3ETPTJPL.001)) data product in Python.\n",
9 | "The Land Processes Distributed Active Archive Center (LP DAAC) distributes the Ecosystem Spaceborne Thermal Radiometer Experiment on Space Station ([ECOSTRESS](https://ecostress.jpl.nasa.gov/)) data products. The ECOSTRESS mission is tasked with measuring the temperature of plants to better understand how much water plants need and how they respond to stress. ECOSTRESS products are archived and distributed in the HDF5 file format as swath-based products.\n",
10 | "\n",
11 | "In this tutorial, you will use Python to perform a swath to grid conversion to project the swath data on to a grid with a defined coordinate reference system (CRS), compare ECOSTRESS data with ground-based AmeriFlux flux tower observations, and export science dataset (SDS) layers as GeoTIFF files that can be loaded into a GIS and/or Remote Sensing software program. \n",
12 | "***\n",
13 | "### Example: Converting a swath ECO3ETPTJPL.001 HDF5 file into a GeoTIFF with a defined CRS and comparing ECOSTRESS Evapotranspiration (ET) with ground-based ET observations from an AmeriFlux flux tower location in California. \n",
14 | "#### Data Used in the Example: \n",
15 | "- Data Product: ECOSTRESS Evapotranspiration PT-JPL Daily L3 Global 70m Version 1 ([ECO3ETPTJPL.001](https://doi.org/10.5067/ECOSTRESS/ECO3ETPTJPL.001)) \n",
16 | " - Science Dataset (SDS) layers: \n",
17 | " - ETinst \n",
18 | " - ETinstUncertainty \n",
19 | "- Data Product: ECOSTRESS Geolocation Daily L1B Global 70m Version 1 ([ECO1BGEO.001](https://doi.org/10.5067/ECOSTRESS/ECO1BGEO.001)) \n",
20 | " - Science Dataset (SDS) layers: \n",
21 | " - Latitude \n",
22 | " - Longitude \n",
23 | "- Data Product: AmeriFlux Ground Observations for [Flux Tower US-CZ3](https://ameriflux.lbl.gov/sites/siteinfo/US-CZ3): Sierra Critical Zone, Sierra Transect, Sierran Mixed Conifer, P301\n",
24 | " - Variables: \n",
25 | " - Latent Heat (W/m$^{2}$) \n",
26 | "*** \n",
27 | "# Topics Covered:\n",
28 | "1. **Getting Started** \n",
29 | " 1a. Import Packages \n",
30 | " 1b. Set Up the Working Environment \n",
31 | " 1c. Retrieve Files \n",
32 | "2. **Importing and Interpreting Data** \n",
33 | " 2a. Open an ECOSTRESS HDF5 File and Read File Metadata \n",
34 | " 2b. Subset SDS Layers \n",
35 | "3. **Performing Swath2grid Conversion** \n",
36 | " 3a. Import Geolocation File \n",
37 | " 3b. Define Projection and Output Grid \n",
38 | " 3c. Read SDS Metadata \n",
39 | " 3d. Perform K-D Tree Resampling \n",
40 | " 3e. Basic Image Processing \n",
41 | "4. **Exporting Results** \n",
42 | " 4a. Set Up a Dictionary \n",
43 | " 4b. Define CRS and Export as GeoTIFFs \n",
44 | "5. **Combining ECOSTRESS and AmeriFlux Tower Data** \n",
45 | " 5a. Loading Tables with Pandas \n",
46 | " 5b. Locate ECOSTRESS Pixel from Lat/Lon Coordinates \n",
47 | "6. **Visualizing Data** \n",
48 | " 6a. Create Colormap \n",
49 | " 6b. Plot ET Data \n",
50 | " 6c. Exporting an Image \n",
51 | "7. **Comparing Observations** \n",
52 | " 7a. Calculate Distribution of ECOSTRESS Data \n",
53 | " 7b. Visualize Ground Observations \n",
54 | " 7c. Combine ECOSTRESS and Ground Observations \n",
55 | "\n",
56 | "***\n",
57 | "# Before Starting this Tutorial: \n",
58 | "#### If you are simply looking to batch process/perform the swath2grid conversion for ECOSTRESS files, be sure to check out the [ECOSTRESS Swath to Grid Conversion Script](https://git.earthdata.nasa.gov/projects/LPDUR/repos/ecostress_swath2grid/browse).\n",
59 | "\n",
60 | "NOTE: This tutorial was developed specifically for the ECOSTRESS Evapotranspiration PT-JPL Level 3, Version 1 HDF5 files and will need to be adapted to work with other ECOSTRESS products. \n",
61 | "## Dependencies: \n",
62 | "*Disclaimer: This tutorial has been tested on Windows and MacOS using the specifications identified below.* \n",
63 | "+ #### Python Version 3.8 \n",
64 | " + `h5py` \n",
65 | " + `pyproj` \n",
66 | " + `matplotlib` \n",
67 | " + `pandas` \n",
68 | " + `pyresample` \n",
69 | " + `pandas` \n",
70 | " + `scipy` \n",
71 | " + `gdal` \n",
72 | " + `jupyter notebook`\n",
73 | "\n",
74 | "- A [NASA Earthdata Login](https://urs.earthdata.nasa.gov/) account is required to download the data used in this tutorial. You can create an account at the link provided. \n",
75 | "- The [Geospatial Data Abstraction Library](http://www.gdal.org/) (GDAL) is required.\n",
76 | "\n",
77 | "---\n",
78 | "# Procedures: \n",
79 | "## Getting Started: \n",
80 | "#### 1. This tutorial uses data from ECOSTRESS Version 1, including an ECO3ETPTJPL.001 (and accompanying ECO1BGEO.001) observation from August 05, 2018. You can download the files directly from the [LP DAAC Data Pool](https://e4ftl01.cr.usgs.gov/ECOSTRESS/) at:\n",
81 | " - [ECOSTRESS_L1B_GEO_00468_007_20180805T220314_0601_03.h5](https://e4ftl01.cr.usgs.gov/ECOSTRESS/ECO1BGEO.001/2018.08.05/ECOSTRESS_L1B_GEO_00468_007_20180805T220314_0601_03.h5) \n",
82 | " - [ECOSTRESS_L3_ET_PT-JPL_00468_007_20180805T220314_0601_04.h5](https://e4ftl01.cr.usgs.gov/ECOSTRESS/ECO3ETPTJPL.001/2018.08.05/ECOSTRESS_L3_ET_PT-JPL_00468_007_20180805T220314_0601_04.h5) \n",
83 | "**A [NASA Earthdata Login](https://urs.earthdata.nasa.gov/) account is required to download the data used in this tutorial. You can create an account at the link provided.** \n",
84 | "\n",
85 | " - Ancillary Files Needed: \n",
86 | " - The AmeriFlux Latent Heat data used in Section 4 can be downloaded via a [csv file](https://git.earthdata.nasa.gov/projects/LPDUR/repos/tutorial-ecostress/raw/tower_data.csv?at=refs%2Fheads%2Fmain). \n",
87 | "\n",
88 | "The `tower_data.csv` file will need to be downloaded into the same directory as the tutorial in order to execute the tutorial. \n",
89 | "#### 2.\tCopy/clone/download the [ECOSTRESS Tutorial repo](https://git.earthdata.nasa.gov/rest/api/latest/projects/LPDUR/repos/tutorial-ecostress/archive?format=zip), or the desired tutorial from the LP DAAC Data User Resources Repository: \n",
90 | " - [Working with ECOSTRESS Evapotranspiration Data in Python Jupyter Notebook](https://git.earthdata.nasa.gov/projects/LPDUR/repos/tutorial-ecostress/browse/ECOSTRESS_Tutorial.ipynb) \n",
91 | "\n",
92 | "NOTE: This tutorial was developed specifically for the ECOSTRESS Evapotranspiration PT-JPL Level 3, Version 1 HDF5 files and will need to be adapted to work with other ECOSTRESS products.
"
93 | ]
94 | },
95 | {
96 | "cell_type": "markdown",
97 | "metadata": {},
98 | "source": [
99 | "***\n",
100 | "1. Getting Started
\n",
101 | "\n",
102 | "***\n",
103 | "## 1a. Import Packages \n",
104 | "#### Import the python packages required to complete this tutorial."
105 | ]
106 | },
107 | {
108 | "cell_type": "code",
109 | "execution_count": null,
110 | "metadata": {},
111 | "outputs": [],
112 | "source": [
113 | "# Import packages\n",
114 | "import warnings\n",
115 | "warnings.filterwarnings('ignore')\n",
116 | "\n",
117 | "import h5py\n",
118 | "import os\n",
119 | "from os.path import join\n",
120 | "import pyproj\n",
121 | "import numpy as np\n",
122 | "import pandas as pd\n",
123 | "import datetime\n",
124 | "from dateutil import parser\n",
125 | "import matplotlib.pyplot as plt\n",
126 | "from matplotlib.colors import LinearSegmentedColormap\n",
127 | "from pyresample import geometry as geom\n",
128 | "from pyresample import kd_tree as kdt\n",
129 | "from osgeo import gdal, gdal_array, gdalconst, osr\n",
130 | "\n",
131 | "# Set plots inside of the Jupyter Notebook\n",
132 | "%matplotlib inline"
133 | ]
134 | },
135 | {
136 | "cell_type": "markdown",
137 | "metadata": {},
138 | "source": [
139 | "***\n",
140 | "## 1b. Set Up the Working Environment\n",
141 | "#### The input directory is defined as the current working directory. Note that you will need to have the jupyter notebook and example data (.h5 and .csv) stored in this directory in order to execute the tutorial successfully."
142 | ]
143 | },
144 | {
145 | "cell_type": "code",
146 | "execution_count": null,
147 | "metadata": {},
148 | "outputs": [],
149 | "source": [
150 | "# Current working directory will be set as the input directory\n",
151 | "inDir = \"../data\" #os.getcwd() + os.sep \n",
152 | "print(\"input directory:\\n{}\".format(inDir))\n",
153 | "\n",
154 | "# Set output directory\n",
155 | "outDir = os.path.normpath(os.path.split(inDir)[0] + os.sep + 'output') + os.sep \n",
156 | "print(\"output directory:\\n{}\".format(outDir))\n",
157 | "\n",
158 | "# Create output directory\n",
159 | "if not os.path.exists(outDir): \n",
160 | " os.makedirs(outDir)"
161 | ]
162 | },
163 | {
164 | "cell_type": "markdown",
165 | "metadata": {},
166 | "source": [
167 | "#### Make sure that the ECOSTRESS .h5 data files, and Ameriflux ET data file (.csv) are located in the input directory printed above."
168 | ]
169 | },
170 | {
171 | "cell_type": "markdown",
172 | "metadata": {},
173 | "source": [
174 | "***\n",
175 | "## 1c. Retrieve Files\n",
176 | "#### Make sure that the ECO1BGEO and ECO3ETPTJPL .h5 files listed in the directions have been downloaded to the `inDir` defined above to follow along in the tutorial."
177 | ]
178 | },
179 | {
180 | "cell_type": "code",
181 | "execution_count": null,
182 | "metadata": {},
183 | "outputs": [],
184 | "source": [
185 | "os.chdir(inDir)"
186 | ]
187 | },
188 | {
189 | "cell_type": "code",
190 | "execution_count": null,
191 | "metadata": {},
192 | "outputs": [],
193 | "source": [
194 | "# List directory contents and create lists of ECOSTRESS HDF5 files (GEO, ET)\n",
195 | "geoList = [file for file in os.listdir() if file.endswith('.h5') and 'GEO' in file]\n",
196 | "print(\"geolocation:\\n{}\".format(\"\\n\".join(geoList)))\n",
197 | "ecoList = [file for file in os.listdir() if file.endswith('.h5') and 'GEO' not in file]\n",
198 | "print(\"products:\\n{}\".format(\"\\n\".join(ecoList)))"
199 | ]
200 | },
201 | {
202 | "cell_type": "markdown",
203 | "metadata": {},
204 | "source": [
205 | "#### The standard format for ECOSTRESS filenames is as follows:\n",
206 | "> **ECOSTRESS_L3_ET_PT-JPL**: Product Type \n",
207 | "**00468**: Orbit number; starting at start of mission, ascending equatorial crossing \n",
208 | "**007**: Scene ID; starting at first scene of first orbit \n",
209 | "**20180805T220314**: Date and time of data start: YYYYMMDDThhmmss \n",
210 | "**0601**: Build ID of software that generated product, Major+Minor (2+2 digits) \n",
211 | "**04**: Product version number (2 digits) "
212 | ]
213 | },
214 | {
215 | "cell_type": "markdown",
216 | "metadata": {},
217 | "source": [
218 | "***\n",
219 | "2. Importing and Interpreting Data
\n",
220 | "\n",
221 | "***\n",
222 | "## 2a. Open an ECOSTRESS HDF5 File\n",
223 | "#### Read in an ECOSTRESS HDF5 file using the `h5py` package."
224 | ]
225 | },
226 | {
227 | "cell_type": "code",
228 | "execution_count": null,
229 | "metadata": {
230 | "scrolled": true
231 | },
232 | "outputs": [],
233 | "source": [
234 | "f = h5py.File(ecoList[0]) # Read in ECOSTRESS HDF5 file\n",
235 | "ecoName = ecoList[0].split('.h5')[0] # Keep original filename\n",
236 | "print(ecoName)"
237 | ]
238 | },
239 | {
240 | "cell_type": "markdown",
241 | "metadata": {},
242 | "source": [
243 | "***\n",
244 | "## 2b. Subset SDS Layers and read SDS Metadata\n",
245 | "#### Identify and generate a list of all the SDS layers in the HDF5 file."
246 | ]
247 | },
248 | {
249 | "cell_type": "code",
250 | "execution_count": null,
251 | "metadata": {},
252 | "outputs": [],
253 | "source": [
254 | "# Create a list of all SDS inside of the .h5 file\n",
255 | "eco_objs = []\n",
256 | "f.visit(eco_objs.append)\n",
257 | "ecoSDS = [str(obj) for obj in eco_objs if isinstance(f[obj], h5py.Dataset)] \n",
258 | "for dataset in ecoSDS[0:10]: \n",
259 | " print(dataset)"
260 | ]
261 | },
262 | {
263 | "cell_type": "markdown",
264 | "metadata": {},
265 | "source": [
266 | "#### Below, subset the SDS list to the two layers needed for comparison with the ground-based AmeriFlux data, `ETinst` and `ETinstUncertainty`."
267 | ]
268 | },
269 | {
270 | "cell_type": "code",
271 | "execution_count": null,
272 | "metadata": {},
273 | "outputs": [],
274 | "source": [
275 | "# Subset list to ETinst and ETinstUncertainty\n",
276 | "sds = ['ETinst', 'ETinstUncertainty']\n",
277 | "ecoSDS = [dataset for dataset in ecoSDS if dataset.endswith(tuple(sds))]\n",
278 | "for dataset in ecoSDS:\n",
279 | " print(dataset.split('/')[-1])"
280 | ]
281 | },
282 | {
283 | "cell_type": "markdown",
284 | "metadata": {},
285 | "source": [
286 | "***\n",
287 | "3. Performing Swath2grid Conversion
\n",
288 | "\n",
289 | "#### Resample the native ECOSTRESS swath data to a grid with defined coordinate reference system (CRS). \n",
290 | "***\n",
291 | "## 3a. Import Geolocation File\n",
292 | "#### The `latitude` and `longitude` arrays from the ECO1BGEO product for the same ECOSTRESS orbit/scene ID are needed to perform the swath2grid conversion on the ECO3ETPT-JPL file. "
293 | ]
294 | },
295 | {
296 | "cell_type": "code",
297 | "execution_count": null,
298 | "metadata": {},
299 | "outputs": [],
300 | "source": [
301 | "# Find the matching ECO1BGEO file from the file list\n",
302 | "geo = [geoFile for geoFile in geoList if ecoList[0].split('ECOSTRESS_L3_ET_PT-JPL_')[-1].split('T')[0] in geoFile]\n",
303 | "print(geo[0])\n",
304 | "print(ecoList[0])"
305 | ]
306 | },
307 | {
308 | "cell_type": "markdown",
309 | "metadata": {},
310 | "source": [
311 | "#### Read in the ECO1BGEO file, search for the `latitude` and `longitude` SDS, and import into Python as arrays."
312 | ]
313 | },
314 | {
315 | "cell_type": "code",
316 | "execution_count": null,
317 | "metadata": {},
318 | "outputs": [],
319 | "source": [
320 | "# Open Geo File\n",
321 | "g = h5py.File(geo[0])\n",
322 | "geo_objs = []\n",
323 | "g.visit(geo_objs.append)\n",
324 | "\n",
325 | "# Search for lat/lon SDS inside data file\n",
326 | "latSD = [str(obj) for obj in geo_objs if isinstance(g[obj], h5py.Dataset) and '/latitude' in obj]\n",
327 | "lonSD = [str(obj) for obj in geo_objs if isinstance(g[obj], h5py.Dataset) and '/longitude' in obj]\n",
328 | "\n",
329 | "# Open SDS as arrays\n",
330 | "lat = g[latSD[0]][()].astype(float)\n",
331 | "lon = g[lonSD[0]][()].astype(float)\n",
332 | "\n",
333 | "# Read the array dimensions\n",
334 | "dims = lat.shape\n",
335 | "print(dims)"
336 | ]
337 | },
338 | {
339 | "cell_type": "markdown",
340 | "metadata": {},
341 | "source": [
342 | "***\n",
343 | "## 3b. Define Projection and Output Grid\n",
344 | "#### The `latitude` and `longitude` arrays from the ECO1BGEO product for the same ECOSTRESS orbit/scene ID are needed to perform the swath2grid conversion on the ECO3ETPT-JPL file. \n",
345 | "#### The following sections use the [pyresample package](https://pyresample.readthedocs.io/en/stable/) to resample the ECOSTRESS swath dataset to a grid using nearest neighbor method. This process begins by defining the swath dimensions using the lat/lon arrays below."
346 | ]
347 | },
348 | {
349 | "cell_type": "code",
350 | "execution_count": null,
351 | "metadata": {},
352 | "outputs": [],
353 | "source": [
354 | "# Set swath definition from lat/lon arrays\n",
355 | "swathDef = geom.SwathDefinition(lons=lon, lats=lat)\n",
356 | "swathDef.corners"
357 | ]
358 | },
359 | {
360 | "cell_type": "markdown",
361 | "metadata": {},
362 | "source": [
363 | "#### Define the coordinates in the middle of the swath, which are used to calculate an estimate of the output rows/columns for the gridded output. "
364 | ]
365 | },
366 | {
367 | "cell_type": "code",
368 | "execution_count": null,
369 | "metadata": {},
370 | "outputs": [],
371 | "source": [
372 | "# Define the lat/lon for the middle of the swath\n",
373 | "mid = [int(lat.shape[1] / 2) - 1, int(lat.shape[0] / 2) - 1]\n",
374 | "midLat, midLon = lat[mid[0]][mid[1]], lon[mid[0]][mid[1]]\n",
375 | "midLat, midLon"
376 | ]
377 | },
378 | {
379 | "cell_type": "markdown",
380 | "metadata": {},
381 | "source": [
382 | "#### Below, `pyproj.Proj` is used to perform a cartographic transformation by defining an Azimuthal Equidistant projection centered on the midpoint of the swath. Once the projection is defined, convert the lower left and upper right corners of the lat/lon arrays to a location (in meters) in the new projection. Lastly, measure the distance between the corners and divide by 70 (meters), the nominal pixel size that we are aiming for. Azimuthal Equidistant projection was chosen here based on the following characteristics of this projection:\n",
383 | "- Units in meters (necessary for defining 70 m pixels) \n",
384 | "- Distances between all points are proportionally correct from center point\n",
385 | "- Azimuth (direction) are correct from the center point"
386 | ]
387 | },
388 | {
389 | "cell_type": "code",
390 | "execution_count": null,
391 | "metadata": {},
392 | "outputs": [],
393 | "source": [
394 | "# Define AEQD projection centered at swath center\n",
395 | "epsgConvert = pyproj.Proj(\"+proj=aeqd +lat_0={} +lon_0={}\".format(midLat, midLon))\n",
396 | "\n",
397 | "# Use info from AEQD projection bbox to calculate output cols/rows/pixel size\n",
398 | "llLon, llLat = epsgConvert(np.min(lon), np.min(lat), inverse=False)\n",
399 | "urLon, urLat = epsgConvert(np.max(lon), np.max(lat), inverse=False)\n",
400 | "areaExtent = (llLon, llLat, urLon, urLat)\n",
401 | "cols = int(round((areaExtent[2] - areaExtent[0]) / 70)) # 70 m pixel size\n",
402 | "rows = int(round((areaExtent[3] - areaExtent[1]) / 70))"
403 | ]
404 | },
405 | {
406 | "cell_type": "markdown",
407 | "metadata": {},
408 | "source": [
409 | "#### Use number of rows and columns generated above from the AEQD projection to set a representative number of rows and columns in the Geographic area definition, which will then be translated to degrees below, then take the smaller of the two pixel dims to determine output size and ensure square pixels."
410 | ]
411 | },
412 | {
413 | "cell_type": "code",
414 | "execution_count": null,
415 | "metadata": {},
416 | "outputs": [],
417 | "source": [
418 | "# Define Geographic projection\n",
419 | "epsg, proj, pName = '4326', 'longlat', 'Geographic'\n",
420 | "\n",
421 | "# Define bounding box of swath\n",
422 | "llLon, llLat, urLon, urLat = np.min(lon), np.min(lat), np.max(lon), np.max(lat)\n",
423 | "areaExtent = (llLon, llLat, urLon, urLat)\n",
424 | "\n",
425 | "# Create area definition with estimated number of columns and rows\n",
426 | "projDict = pyproj.CRS(\"epsg:4326\")\n",
427 | "areaDef = geom.AreaDefinition(epsg, pName, proj, projDict, cols, rows, areaExtent)"
428 | ]
429 | },
430 | {
431 | "cell_type": "markdown",
432 | "metadata": {},
433 | "source": [
434 | "#### Below, square the pixels by setting the pixel size to the smaller of the x any y values output by the `AreaDefinition`, then use the pixel size to recalculate the number of output cols/rows."
435 | ]
436 | },
437 | {
438 | "cell_type": "code",
439 | "execution_count": null,
440 | "metadata": {},
441 | "outputs": [],
442 | "source": [
443 | "# Square pixels and calculate output cols/rows\n",
444 | "ps = np.min([areaDef.pixel_size_x, areaDef.pixel_size_y])\n",
445 | "cols = int(round((areaExtent[2] - areaExtent[0]) / ps))\n",
446 | "rows = int(round((areaExtent[3] - areaExtent[1]) / ps))\n",
447 | "\n",
448 | "# Set up a new Geographic area definition with the refined cols/rows\n",
449 | "areaDef = geom.AreaDefinition(epsg, pName, proj, projDict, cols, rows, areaExtent)"
450 | ]
451 | },
452 | {
453 | "cell_type": "markdown",
454 | "metadata": {},
455 | "source": [
456 | "#### Below, use `pyresample kd_tree`'s [get_neighbour_info](https://pyresample.readthedocs.io/en/latest/swath.html#resampling-from-neighbour-info) to create arrays with information on the nearest neighbor to each grid point. \n",
457 | "#### This is the most computationally heavy task in the swath2grid conversion and using `get_neighbour_info` speeds up the process if you plan to resample multiple SDS within an ECOSTRESS product (compute once instead of for every SDS). "
458 | ]
459 | },
460 | {
461 | "cell_type": "code",
462 | "execution_count": null,
463 | "metadata": {},
464 | "outputs": [],
465 | "source": [
466 | "# Get arrays with information about the nearest neighbor to each grid point \n",
467 | "index, outdex, indexArr, distArr = kdt.get_neighbour_info(swathDef, areaDef, 210, neighbours=1)"
468 | ]
469 | },
470 | {
471 | "cell_type": "markdown",
472 | "metadata": {},
473 | "source": [
474 | "#### Above, the function is comparing the swath and area definitions to locate the nearest neighbor (neighbours=1). 210 is the `radius_of_influence`, or the radius used to search for the nearest neighboring pixel in the swath (in meters). "
475 | ]
476 | },
477 | {
478 | "cell_type": "markdown",
479 | "metadata": {},
480 | "source": [
481 | "***\n",
482 | "## 3c. Read SDS Metadata\n",
483 | "#### List the attributes for the `ETinst` layer, which can then be used to define the fill value and scale factor. "
484 | ]
485 | },
486 | {
487 | "cell_type": "code",
488 | "execution_count": null,
489 | "metadata": {},
490 | "outputs": [],
491 | "source": [
492 | "# Read in ETinst and print out SDS attributes\n",
493 | "s = ecoSDS[0]\n",
494 | "ecoSD = f[s][()]\n",
495 | "for attr in f[s].attrs:\n",
496 | " if type(f[s].attrs[attr]) == np.ndarray:\n",
497 | " print(f'{attr} = {f[s].attrs[attr][0]}')\n",
498 | " else:\n",
499 | " print(f'{attr} = {f[s].attrs[attr].decode(\"utf-8\")}')"
500 | ]
501 | },
502 | {
503 | "cell_type": "markdown",
504 | "metadata": {},
505 | "source": [
506 | "#### Extract the scale factor, add offset and fill value from the SDS metadata."
507 | ]
508 | },
509 | {
510 | "cell_type": "code",
511 | "execution_count": null,
512 | "metadata": {},
513 | "outputs": [],
514 | "source": [
515 | "f[s].attrs['_FillValue'][0]"
516 | ]
517 | },
518 | {
519 | "cell_type": "code",
520 | "execution_count": null,
521 | "metadata": {},
522 | "outputs": [],
523 | "source": [
524 | "# Read SDS attributes and define fill value, add offset, and scale factor if available\n",
525 | "try:\n",
526 | " fv = int(f[s].attrs['_FillValue'])\n",
527 | "except KeyError:\n",
528 | " fv = None\n",
529 | "except ValueError:\n",
530 | " fv = f[s].attrs['_FillValue'][0]\n",
531 | "try:\n",
532 | " sf = f[s].attrs['_Scale'][0]\n",
533 | "except:\n",
534 | " sf = 1\n",
535 | "try:\n",
536 | " add_off = f[s].attrs['_Offset'][0]\n",
537 | "except:\n",
538 | " add_off = 0\n",
539 | "try:\n",
540 | " units = f[s].attrs['units'].decode(\"utf-8\")\n",
541 | "except:\n",
542 | " units = 'none'"
543 | ]
544 | },
545 | {
546 | "cell_type": "markdown",
547 | "metadata": {},
548 | "source": [
549 | "***\n",
550 | "## 3d. Perform K-D Tree Resampling\n",
551 | "#### Remember that the resampling has been split into two steps. In section 3b. arrays containing the nearest neighbor to each grid point were created. The second step is to use those arrays to retrieve a resampled result. "
552 | ]
553 | },
554 | {
555 | "cell_type": "code",
556 | "execution_count": null,
557 | "metadata": {},
558 | "outputs": [],
559 | "source": [
560 | "# Perform K-D Tree nearest neighbor resampling (swath 2 grid conversion)\n",
561 | "ETgeo = kdt.get_sample_from_neighbour_info('nn', areaDef.shape, ecoSD, index, outdex, indexArr, fill_value=None)"
562 | ]
563 | },
564 | {
565 | "cell_type": "markdown",
566 | "metadata": {},
567 | "source": [
568 | "#### Above, resample the swath `ecoSD` array using nearest neighbor (already calculated in section 3b. and defined above as the `index`, `outdex`, and `indexArr`), and also set the fill value that was defined in section 3c. \n",
569 | "#### Below, define the geotransform for the output (upper left x, horizontal pixel size, rotation, upper left y, rotation, vertical pixel size)."
570 | ]
571 | },
572 | {
573 | "cell_type": "code",
574 | "execution_count": null,
575 | "metadata": {},
576 | "outputs": [],
577 | "source": [
578 | "# Define the geotransform \n",
579 | "gt = [areaDef.area_extent[0], ps, 0, areaDef.area_extent[3], 0, -ps]\n",
580 | "gt"
581 | ]
582 | },
583 | {
584 | "cell_type": "markdown",
585 | "metadata": {},
586 | "source": [
587 | "***\n",
588 | "## 3e. Basic Image Processing\n",
589 | "#### Apply the scale factor and add offset and set the fill value defined in the previous section on the resampled data."
590 | ]
591 | },
592 | {
593 | "cell_type": "code",
594 | "execution_count": null,
595 | "metadata": {},
596 | "outputs": [],
597 | "source": [
598 | "ETgeo = ETgeo * sf + add_off # Apply Scale Factor and Add Offset\n",
599 | "ETgeo[ETgeo == fv * sf + add_off] = fv # Set Fill Value"
600 | ]
601 | },
602 | {
603 | "cell_type": "markdown",
604 | "metadata": {},
605 | "source": [
606 | "#### Rerun steps 3c - 3e for `ETinstUncertainty`."
607 | ]
608 | },
609 | {
610 | "cell_type": "code",
611 | "execution_count": null,
612 | "metadata": {},
613 | "outputs": [],
614 | "source": [
615 | "s = ecoSDS[1]\n",
616 | "ecoSD = f[s][()]\n",
617 | "try:\n",
618 | " fv = int(f[s].attrs['_FillValue'])\n",
619 | "except KeyError:\n",
620 | " fv = None\n",
621 | "except ValueError:\n",
622 | " fv = f[s].attrs['_FillValue'][0]\n",
623 | "try:\n",
624 | " sf = f[s].attrs['_Scale'][0]\n",
625 | "except:\n",
626 | " sf = 1\n",
627 | "try:\n",
628 | " add_off = f[s].attrs['_Offset'][0]\n",
629 | "except:\n",
630 | " add_off = 0\n",
631 | "UNgeo = kdt.get_sample_from_neighbour_info('nn', areaDef.shape, ecoSD, index, outdex, indexArr, fill_value=None)\n",
632 | "UNgeo = UNgeo * sf + add_off\n",
633 | "UNgeo[UNgeo == fv * sf + add_off] = fv"
634 | ]
635 | },
636 | {
637 | "cell_type": "markdown",
638 | "metadata": {},
639 | "source": [
640 | "***\n",
641 | "4. Exporting Results
\n",
642 | "\n",
643 | "***\n",
644 | "## 4a. Set Up a Dictionary\n",
645 | "#### In this section, create a dictionary containing each of the arrays that will be exported as GeoTIFFs."
646 | ]
647 | },
648 | {
649 | "cell_type": "code",
650 | "execution_count": null,
651 | "metadata": {},
652 | "outputs": [],
653 | "source": [
654 | "# Set up dictionary of arrays to export\n",
655 | "outFiles = {'ETinst': ETgeo, 'ETinstUncertainty': UNgeo}"
656 | ]
657 | },
658 | {
659 | "cell_type": "markdown",
660 | "metadata": {},
661 | "source": [
662 | "***\n",
663 | "## 4b. Define CRS and Export as GeoTIFFs\n",
664 | "#### Now that the data have been imported and resampled into a gridded raster array, export the results as GeoTIFFs using a `for` loop in this section."
665 | ]
666 | },
667 | {
668 | "cell_type": "code",
669 | "execution_count": null,
670 | "metadata": {},
671 | "outputs": [],
672 | "source": [
673 | "fv = np.nan"
674 | ]
675 | },
676 | {
677 | "cell_type": "code",
678 | "execution_count": null,
679 | "metadata": {
680 | "scrolled": true
681 | },
682 | "outputs": [],
683 | "source": [
684 | "# Loop through each item in dictionary created above\n",
685 | "for file in outFiles:\n",
686 | " \n",
687 | " # Set up output name\n",
688 | " outName = join(outDir, '{}_{}.tif'.format(ecoName, file))\n",
689 | " print(\"output file:\\n{}\\n\".format(outName))\n",
690 | " \n",
691 | " # Get driver, specify dimensions, define and set output geotransform\n",
692 | " height, width = outFiles[file].shape\n",
693 | " driv = gdal.GetDriverByName('GTiff')\n",
694 | " dataType = gdal_array.NumericTypeCodeToGDALTypeCode(outFiles[file].dtype)\n",
695 | " d = driv.Create(outName, width, height, 1, dataType)\n",
696 | " d.SetGeoTransform(gt)\n",
697 | " \n",
698 | " # Create and set output projection, write output array data\n",
699 | " # Define target SRS\n",
700 | " srs = osr.SpatialReference()\n",
701 | " srs.ImportFromEPSG(int(epsg))\n",
702 | " d.SetProjection(srs.ExportToWkt())\n",
703 | " srs.ExportToWkt()\n",
704 | " \n",
705 | " # Write array to band\n",
706 | " band = d.GetRasterBand(1)\n",
707 | " band.WriteArray(outFiles[file])\n",
708 | " \n",
709 | " # Define fill value if it exists, if not, set to mask fill value\n",
710 | " if fv is not None and fv != 'NaN':\n",
711 | " band.SetNoDataValue(fv)\n",
712 | " else:\n",
713 | " try:\n",
714 | " band.SetNoDataValue(outFiles[file].fill_value)\n",
715 | " except:\n",
716 | " pass\n",
717 | " band.FlushCache()\n",
718 | " d, band = None, None "
719 | ]
720 | },
721 | {
722 | "cell_type": "markdown",
723 | "metadata": {},
724 | "source": [
725 | "***\n",
726 | "5. Combining ECOSTRESS and AmeriFlux Tower Data
\n",
727 | "\n",
728 | "***\n",
729 | "## 5a. Loading Tables with Pandas\n",
730 | "#### In this section, begin by highlighting how to open a csv file using the `pandas` package. \n",
731 | "> The AmeriFlux tower data was provided by Mike Goulden for the AmeriFlux US-CZ3 tower. The csv includes half-hourly observations of Latent Heat (W/m$^{2}$) for the same day as the ECOSTRESS observation. "
732 | ]
733 | },
734 | {
735 | "cell_type": "code",
736 | "execution_count": null,
737 | "metadata": {},
738 | "outputs": [],
739 | "source": [
740 | "# Import csv file with AmeriFlux data and drop NaNs\n",
741 | "towerData = pd.read_csv('tower_data.csv')\n",
742 | "towerData = towerData.dropna()"
743 | ]
744 | },
745 | {
746 | "cell_type": "markdown",
747 | "metadata": {},
748 | "source": [
749 | "#### Next, use the `parser` package and a lambda function to go through each time stamp and reformat to date and time objects."
750 | ]
751 | },
752 | {
753 | "cell_type": "code",
754 | "execution_count": null,
755 | "metadata": {},
756 | "outputs": [],
757 | "source": [
758 | "# Define a lambda function to use the parser packgage to convert each time stamp to a datetime object\n",
759 | "towerData[\"Date/Time\"] = towerData[\"Time\"].apply(lambda x: parser.parse(x))\n",
760 | "towerData[\"Time\"] = towerData[\"Date/Time\"].apply(lambda x: datetime.time(x.hour, x.minute))\n",
761 | "towerData = towerData[[\"Date/Time\", \"Time\", \"LE\"]]\n",
762 | "towerData"
763 | ]
764 | },
765 | {
766 | "cell_type": "markdown",
767 | "metadata": {},
768 | "source": [
769 | "***\n",
770 | "## 5b. Locate ECOSTRESS Pixel from Lat/Lon Coordinates\n",
771 | "\n",
772 | "#### Calculate the gridded pixel nearest to the tower location."
773 | ]
774 | },
775 | {
776 | "cell_type": "code",
777 | "execution_count": null,
778 | "metadata": {},
779 | "outputs": [],
780 | "source": [
781 | "towerLat, towerLon = 37.0674, -119.1951 # AmeriFlux US-CZ3 tower location\n",
782 | "\n",
783 | "# Calculate tower lat/lon distance from upper left corner, then divide by pixel size to find x,y pixel location\n",
784 | "Tcol = int(round((towerLon - gt[0]) / gt[1]))\n",
785 | "Trow = int(round((towerLat - gt[3]) / gt[5]))\n",
786 | "\n",
787 | "# Print ET at the tower location\n",
788 | "ETgeo[Trow, Tcol] "
789 | ]
790 | },
791 | {
792 | "cell_type": "markdown",
793 | "metadata": {},
794 | "source": [
795 | "***\n",
796 | "6. Visualizing Data
\n",
797 | "\n",
798 | "***\n",
799 | "## 6a. Create a Colormap\n",
800 | "#### Before plotting the ET data, set up an Evapotranspiration color map using `LinearSegmentedColormap` from the `matplotlib` package."
801 | ]
802 | },
803 | {
804 | "cell_type": "code",
805 | "execution_count": null,
806 | "metadata": {},
807 | "outputs": [],
808 | "source": [
809 | "# Create a colormap for the ET data\n",
810 | "ETcolors = [\"#f6e8c3\", \"#d8b365\", \"#99974a\", \"#53792d\", \"#6bdfd2\", \"#1839c5\"]\n",
811 | "ETcmap = LinearSegmentedColormap.from_list(\"ET\", ETcolors)"
812 | ]
813 | },
814 | {
815 | "cell_type": "markdown",
816 | "metadata": {},
817 | "source": [
818 | "***\n",
819 | "## 6b. Calculate Local Overpass Time\n",
820 | "#### ECOSTRESS observation times are reported in Universal Time Coordinated (UTC). Below, grab the observation time from the filename and convert to local time using the longitude location of the tower."
821 | ]
822 | },
823 | {
824 | "cell_type": "code",
825 | "execution_count": null,
826 | "metadata": {},
827 | "outputs": [],
828 | "source": [
829 | "# Grab UTC time of observation from file name\n",
830 | "ecoTime = ecoName.split('_')[-3]\n",
831 | "ecoTime"
832 | ]
833 | },
834 | {
835 | "cell_type": "markdown",
836 | "metadata": {},
837 | "source": [
838 | "#### Next, convert UTC observation time to local overpass time. "
839 | ]
840 | },
841 | {
842 | "cell_type": "code",
843 | "execution_count": null,
844 | "metadata": {},
845 | "outputs": [],
846 | "source": [
847 | "observationTime = parser.parse(ecoTime)\n",
848 | "solarOverpass = observationTime + datetime.timedelta(hours=(np.radians(towerLon) / np.pi * 12))\n",
849 | "overpass = datetime.time(solarOverpass.hour, solarOverpass.minute)\n",
850 | "date = observationTime.strftime('%Y-%m-%d')"
851 | ]
852 | },
853 | {
854 | "cell_type": "markdown",
855 | "metadata": {},
856 | "source": [
857 | "***\n",
858 | "## 6c. Plot ET Data\n",
859 | "#### In this section, begin by highlighting the functionality of the `matplotlib` plotting package. First, make a plot of the entire gridded ET output. Next, zoom in on the tower location and add some additional parameters to the plot. Finally, export the completed plot to an image file."
860 | ]
861 | },
862 | {
863 | "cell_type": "code",
864 | "execution_count": null,
865 | "metadata": {},
866 | "outputs": [],
867 | "source": [
868 | "title = 'ECO3ETPTJPL Evapotranspiration'\n",
869 | "SDSname = ecoSDS[0].split(\"/\")[-1]\n",
870 | "fig = plt.figure(figsize=(9.7,7.6)) # Set the figure size (x,y)\n",
871 | "fig.suptitle(f'{title} ({ecoSDS[0].split(\"/\")[-1]})\\n{date} at {overpass}', fontsize=22) # Add title for the plots\n",
872 | "plt.axis('off') # Remove axes from plot\n",
873 | "im = plt.imshow(ETgeo, cmap=ETcmap); # Plot array using colormap\n",
874 | "plt.scatter(Tcol, Trow, color=\"black\", marker='x') # Plot tower location\n",
875 | "\n",
876 | "# Add a colormap legend\n",
877 | "plt.colorbar(im, orientation='horizontal', fraction=0.05, pad=0.004, label=f'ET ({units})', shrink=0.6).outline.set_visible(True)"
878 | ]
879 | },
880 | {
881 | "cell_type": "markdown",
882 | "metadata": {},
883 | "source": [
884 | "***\n",
885 | "## 6d. Exporting an Image\n",
886 | "#### Zoom in to get a closer look at the region surrounding the AmeriFlux tower by creating a subset."
887 | ]
888 | },
889 | {
890 | "cell_type": "code",
891 | "execution_count": null,
892 | "metadata": {},
893 | "outputs": [],
894 | "source": [
895 | "# Set a Radius and calculate subset region from flux tower location (row, col)\n",
896 | "radius = 1700\n",
897 | "ETsubset = ETgeo[(Trow - radius):(Trow + radius + 1), (Tcol - radius):(Tcol + radius + 1)]"
898 | ]
899 | },
900 | {
901 | "cell_type": "markdown",
902 | "metadata": {},
903 | "source": [
904 | "#### Make another plot, this time zoomed in to the tower location. Export the plot as a .png file. "
905 | ]
906 | },
907 | {
908 | "cell_type": "code",
909 | "execution_count": null,
910 | "metadata": {},
911 | "outputs": [],
912 | "source": [
913 | "fig = plt.figure(figsize=(14,12)) # Set the figure size (x,y)\n",
914 | "fig.suptitle(f'{title} ({SDSname})\\n{date} at {overpass}', fontsize=26) # Add title for the plots\n",
915 | "plt.axis('off') # Remove axes from plot\n",
916 | "im = plt.imshow(ETsubset, cmap=ETcmap); # Plot array using colormap\n",
917 | "plt.scatter(ETsubset.shape[0]/2, ETsubset.shape[1]/2, color=\"black\", marker='x') # Tower is in middle of subset\n",
918 | "\n",
919 | "# Add a colormap legend\n",
920 | "plt.colorbar(im, orientation='horizontal', fraction=0.05, pad=0.004, label=f'ET ({units})', shrink=0.6).outline.set_visible(True)\n",
921 | "\n",
922 | "# Set up file name and export to png file\n",
923 | "figure_filename = join(outDir, \"{}_{}.png\".format(ecoName, SDSname))\n",
924 | "print(\"figure filename: {}\".format(figure_filename))\n",
925 | "fig.savefig(figure_filename, dpi=300)\n",
926 | "plt.show()"
927 | ]
928 | },
929 | {
930 | "cell_type": "markdown",
931 | "metadata": {},
932 | "source": [
933 | "***\n",
934 | "7. Comparing Observations
\n",
935 | "\n",
936 | "***\n",
937 | "## 7a. Calculate Distribution of ECOSTRESS Data\n",
938 | "#### First, collect a 3x3 grid centered on the flux tower pixel as a subset to calculate statistics on. "
939 | ]
940 | },
941 | {
942 | "cell_type": "code",
943 | "execution_count": null,
944 | "metadata": {},
945 | "outputs": [],
946 | "source": [
947 | "# Subset data to 3x3 grid surrounding flux tower for both layers\n",
948 | "ETfootprint = ETgeo[(Trow - 1):(Trow + 2), (Tcol - 1):(Tcol + 2)] \n",
949 | "UNfootprint = UNgeo[(Trow - 1):(Trow + 2), (Tcol - 1):(Tcol + 2)] \n",
950 | "print(ETfootprint)"
951 | ]
952 | },
953 | {
954 | "cell_type": "markdown",
955 | "metadata": {},
956 | "source": [
957 | "#### In case the 3x3 grid contains missing values, use `np.nanmedian` to ignoring missing values and calculate the measure of central tendency."
958 | ]
959 | },
960 | {
961 | "cell_type": "code",
962 | "execution_count": null,
963 | "metadata": {},
964 | "outputs": [],
965 | "source": [
966 | "ETmedian = np.nanmedian(ETfootprint)\n",
967 | "UNmedian = np.nanmedian(UNfootprint)\n",
968 | "print(f\"Median ET: {ETmedian:0.3f} \\nUncertainty: {UNmedian:0.3f}\")"
969 | ]
970 | },
971 | {
972 | "cell_type": "markdown",
973 | "metadata": {},
974 | "source": [
975 | "#### Next, generate a probability density function for the 3x3 grid of ET values."
976 | ]
977 | },
978 | {
979 | "cell_type": "code",
980 | "execution_count": null,
981 | "metadata": {},
982 | "outputs": [],
983 | "source": [
984 | "pd.DataFrame({title: ETfootprint.flatten()}).plot.kde() # Pandas Kernel Density Estimate\n",
985 | "plt.title(f'Probability Density of {SDSname} Surrounding US-CZ3\\n{date} at {overpass}'); # Title\n",
986 | "plt.xlabel(f'{SDSname} ({units})'); # X-axis label"
987 | ]
988 | },
989 | {
990 | "cell_type": "markdown",
991 | "metadata": {},
992 | "source": [
993 | "***\n",
994 | "## 7b. Visualize Ground Observations \n",
995 | "\n",
996 | "#### Next, examine the series of eddy covariance observations from the AmeriFlux US-CZ3 dataset."
997 | ]
998 | },
999 | {
1000 | "cell_type": "code",
1001 | "execution_count": null,
1002 | "metadata": {},
1003 | "outputs": [],
1004 | "source": [
1005 | "fig = plt.figure(figsize=(12,7)) # Set fig size (x,y)\n",
1006 | "ax = fig.add_subplot(111) # Create a subplot\n",
1007 | "ax.plot(towerData['Date/Time'], towerData.LE, 'k', lw=2.5, color='black') # Plot as a black line\n",
1008 | "ax.set_title(f'Time Series of Eddy Covariance Data at Site US-CZ3', fontsize=20, fontweight='bold'); # Set Title\n",
1009 | "ax.set_ylabel('Latent Heat (W/m^2)', fontsize=18); # Y-axis label\n",
1010 | "ax.set_xlabel(f'Time of Day ({date})', fontsize=18); # X-axis label\n",
1011 | "ax.set_xticks(towerData['Date/Time']); # Set the x ticks\n",
1012 | "ax.set_xticklabels(towerData['Time'], rotation=45,fontsize=12); # Set x tick labels"
1013 | ]
1014 | },
1015 | {
1016 | "cell_type": "markdown",
1017 | "metadata": {},
1018 | "source": [
1019 | "#### Above, we can see the daily range in Latent Heat as captured by the eddy covariance observations on the flux tower."
1020 | ]
1021 | },
1022 | {
1023 | "cell_type": "markdown",
1024 | "metadata": {},
1025 | "source": [
1026 | "***\n",
1027 | "## 7c. Combine ECOSTRESS and Ground Observations \n",
1028 | "\n",
1029 | "#### Finally, compare the ECOSTRESS Evapotranspiration and uncertainty with the time series of observations from the flux tower."
1030 | ]
1031 | },
1032 | {
1033 | "cell_type": "code",
1034 | "execution_count": null,
1035 | "metadata": {},
1036 | "outputs": [],
1037 | "source": [
1038 | "# Set the figure size, create a subplot\n",
1039 | "fig = plt.figure(1, figsize=(12, 7))\n",
1040 | "ax = fig.add_subplot(111)\n",
1041 | "\n",
1042 | "# Plot the flux tower observatisons followed by the ecostress median ET and median uncertainty\n",
1043 | "ax.plot(towerData['Date/Time'], towerData.LE, 'k', lw=2.5, color='black')\n",
1044 | "ax.plot(solarOverpass, ETmedian, 'bo', ms=10, color='darkgreen')\n",
1045 | "ax.errorbar(solarOverpass, ETmedian, yerr=UNmedian, lw=2.0, c='lightgreen', fmt='o', capsize=3, capthick=2)\n",
1046 | "\n",
1047 | "# Set x/y axes and labels\n",
1048 | "ax.set_xlabel(f'Time of Day ({date})', fontsize=18);\n",
1049 | "ax.set_xticks(towerData['Date/Time']);\n",
1050 | "ax.set_xticklabels(towerData.Time, rotation=45,fontsize=12);\n",
1051 | "ax.set_ylabel(\"Latent Heat Flux (Wm2)\", fontsize=16, fontweight='bold')\n",
1052 | "\n",
1053 | "# Add a title and export figure as png file\n",
1054 | "ax.set_title(f\"Time Series of Eddy Covariance Data at Site US-CZ3\\n vs. {title} ({SDSname})\", fontsize=22)\n",
1055 | "figure_filename = join(outDir, \"{}_{}_vs_fluxtower.png\".format(ecoName, SDSname))\n",
1056 | "print(\"figure filename: {}\".format(figure_filename))\n",
1057 | "fig.savefig(figure_filename, bbox_inches='tight')"
1058 | ]
1059 | },
1060 | {
1061 | "cell_type": "markdown",
1062 | "metadata": {},
1063 | "source": [
1064 | "***\n",
1065 | "## Citations\n",
1066 | "- Hook, S., Fisher, J. (2019). ECOSTRESS Evapotranspiration PT-JPL Daily L3 Global 70 m V001 [Data set]. NASA EOSDIS Land Processes DAAC. Accessed 2021-03-11 from https://doi.org/10.5067/ECOSTRESS/ECO3ETPTJPL.001. \n",
1067 | "\n",
1068 | "\n",
1069 | "- Goulden, M., (2018). AmeriFlux US-CZ3 Sierra Critical Zone, Sierra Transect, Sierran Mixed Conifer, P301, [doi:10.17190/AMF/1419512](http://dx.doi.org/10.17190/AMF/1419512). \n",
1070 | "\n",
1071 | "\n",
1072 | "- Krehbiel, C., and Halverson, G.H., (2019). Working with ECOSTRESS Evapotranspiration Data [Jupyter Notebook]. Retrieved from https://git.earthdata.nasa.gov/projects/LPDUR/repos/tutorial-ecostress/browse"
1073 | ]
1074 | },
1075 | {
1076 | "cell_type": "markdown",
1077 | "metadata": {},
1078 | "source": [
1079 | "## Contact Info: \n",
1080 | "Material written by Cole Krehbiel1 and Gregory Halverson2 \n",
1081 | "Email: LPDAAC@usgs.gov \n",
1082 | "Voice: +1-866-573-3222 \n",
1083 | "Organization: Land Processes Distributed Active Archive Center (LP DAAC)<\n",
1084 | "Website: \n",
1085 | "\n",
1086 | "\n",
1087 | "1Innovate! Inc., contractor to the U.S. Geological Survey, Earth Resources Observation and Science (EROS) Center, Sioux Falls, South Dakota, 57198-001, USA. Work performed under USGS contract G15PD00467 for LP DAAC3. \n",
1088 | "2Jet Propulsion Laboratory, California Institute of Technology, Pasadena, CA, USA. \n",
1089 | "\n",
1090 | "3LP DAAC Work performed under NASA contract NNG14HH33I. \n",
1091 | "\n"
1092 | ]
1093 | }
1094 | ],
1095 | "metadata": {
1096 | "kernelspec": {
1097 | "display_name": "Python 3 (ipykernel)",
1098 | "language": "python",
1099 | "name": "python3"
1100 | },
1101 | "language_info": {
1102 | "codemirror_mode": {
1103 | "name": "ipython",
1104 | "version": 3
1105 | },
1106 | "file_extension": ".py",
1107 | "mimetype": "text/x-python",
1108 | "name": "python",
1109 | "nbconvert_exporter": "python",
1110 | "pygments_lexer": "ipython3",
1111 | "version": "3.11.13"
1112 | }
1113 | },
1114 | "nbformat": 4,
1115 | "nbformat_minor": 4
1116 | }
1117 |
--------------------------------------------------------------------------------
/python/tutorials/Work_with_ECOSTRESS_L2T_LSTE_data.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {
6 | "tags": []
7 | },
8 | "source": [
9 | "# Working with ECOSTRESS L2T LSTE Data \n",
10 | "\n",
11 | "**Summary** \n",
12 | "\n",
13 | "This notebook will show how to access [ECOsystem Spaceborne Thermal Radiometer Experiment on Space Station (ECOSTRESS)](https://ecostress.jpl.nasa.gov/) data programmatically using the [`earthaccess`](https://github.com/nsidc/earthaccess) python library to authenticate, search, download, and stream (directly access) data. It shows how to work with ECOSTRESS Tiled Land Surface Temperature and Emissivity ([`ECOSTRESS_L2T_LSTE`](https://doi.org/10.5067/ECOSTRESS/ECO_L2T_LSTE.002)) product hosted in the Earthdata Cloud and managed by the Land Processes Distributed Active Archive Center ([LP DAAC](https://lpdaac.usgs.gov/)). \n",
14 | "\n",
15 | "**Background** \n",
16 | "\n",
17 | "The [ECOSTRESS mission](https://ecostress.jpl.nasa.gov/) is answering these questions by accurately measuring the temperature of plants. Plants regulate their temperature by releasing water through tiny pores on their leaves called stomata. If they have sufficient water they can maintain their temperature, but if there is insufficient water, their temperatures rise and this temperature rise can be measured with ECOSTRESS. The images acquired by ECOSTRESS are the most detailed temperature images of the surface ever acquired from space and can be used to measure the temperature of an individual farm field. These temperature images, along with auxiliary inputs, are used to produce one of the primary science outputs of ECOSTRESS: evapotranspiration, an indicator of plant health via the measure of evaporation and transpiration of water through a plant.\n",
18 | "\n",
19 | "More details about ECOSTRESS and its associated products can be found on the [ECOSTRESS website](https://ecostress.jpl.nasa.gov/) and [ECOSTRESS product pages](https://lpdaac.usgs.gov/product_search/?collections=ECOSTRESS&status=Operational&view=cards&sort=title) hosted by the Land Processes Distributed Active Archive Center (LP DAAC).\n",
20 | "\n",
21 | "**Learning Objectives** \n",
22 | "\n",
23 | "- How to search ECOSTRESS data using `earthaccess`\n",
24 | "- How to stream or download ECOSTRESS data\n",
25 | "- How to clip ECOSTRESS data to a Region of Interest (ROI)\n",
26 | "- How to quality filter ECOSTRESS data\n",
27 | "- How to export the processed ECOSTRESS data\n",
28 | "\n",
29 | "**Requirements** \n",
30 | "\n",
31 | "- NASA [Earthdata Login](https://urs.earthdata.nasa.gov/) account. If you do not have an Earthdata Account, you can create one [here](https://urs.earthdata.nasa.gov/users/new). \n",
32 | "- A compatible Python Environment - See **setup_instructions.md** in the `/setup/` folder \n",
33 | "\n",
34 | "**Tutorial Outline**\n",
35 | "\n",
36 | "1. Setup\n",
37 | "2. Searching for ECOSTRESS L2T LSTE Data\n",
38 | "3. Accessing ECOSTRESS L2T LSTE Data\n",
39 | "4. Clipping ECOSTRESS L2T LSTE Data to a Region of Interest (ROI)\n",
40 | "5. Quality Filtering ECOSTRESS L2T LSTE Data\n",
41 | "6. Exporting Processed ECOSTRESS L2T LSTE Data"
42 | ]
43 | },
44 | {
45 | "cell_type": "markdown",
46 | "metadata": {},
47 | "source": [
48 | "## 1. Setup \n",
49 | "\n",
50 | "Import the required libraries."
51 | ]
52 | },
53 | {
54 | "cell_type": "code",
55 | "execution_count": null,
56 | "metadata": {
57 | "tags": []
58 | },
59 | "outputs": [],
60 | "source": [
61 | "# Import Packages\n",
62 | "import warnings\n",
63 | "# Some cells may generate warnings that we can ignore. Comment below lines to see.\n",
64 | "warnings.filterwarnings('ignore')\n",
65 | "\n",
66 | "import os, sys, shutil\n",
67 | "import earthaccess\n",
68 | "import numpy as np\n",
69 | "from osgeo import gdal\n",
70 | "import rasterio as rio\n",
71 | "import rioxarray as rxr\n",
72 | "import xarray as xr\n",
73 | "import hvplot.xarray\n",
74 | "import hvplot.pandas\n",
75 | "import geopandas as gp\n",
76 | "from zipfile import ZipFile "
77 | ]
78 | },
79 | {
80 | "cell_type": "markdown",
81 | "metadata": {},
82 | "source": [
83 | "### Authentication\n",
84 | "\n",
85 | "Log into Earthdata using the `Auth` and `login` functions from the `earthaccess` library. The `persist=True` argument will 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.\n"
86 | ]
87 | },
88 | {
89 | "cell_type": "code",
90 | "execution_count": null,
91 | "metadata": {},
92 | "outputs": [],
93 | "source": [
94 | "auth = earthaccess.login(persist = True)\n",
95 | "# are we authenticated?\n",
96 | "print(auth.authenticated)"
97 | ]
98 | },
99 | {
100 | "cell_type": "markdown",
101 | "metadata": {},
102 | "source": [
103 | "## 2. Searching for ECOSTRESS L2T LSTE Data\n",
104 | "\n",
105 | "In this example, we will use the cloud-hosted `ECOSTRESS_L2T_LSTE` product but the searching process can be used with other EMIT or ECOSTRESS products, other collections, or different data providers, as well as across multiple catalogs with some modification. The Land Surface Temperature and Emissivity values from ECOSTRESS Level 2 Tiled Land Surface Temperature (ECO_L2T_LSTE) are derived from five thermal infrared (TIR) bands using a physics-based Temperature and Emissivity Separation (TES) algorithm. This tiled data product uses a modified version of the Military Grid Reference System (MGRS) which divides Universal Transverse Mercator (UTM) zones into square tiles that are 109.8 km by 109.8 km with a 70 meter (m) spatial resolution. \n",
106 | "\n",
107 | "### Define Your Query Parameters\n",
108 | "\n",
109 | "We can search for granules using attributes such as collection short name, collection ID, acquisition time, and spatial footprint.\n",
110 | "\n",
111 | "##### Spatial region of interest (ROI)\n",
112 | "\n",
113 | "For this example, our spatial region of interest (ROI) will be Boulder city boundary. when we are working with multi-feature ROI, we can use a bounding box with larger spatial extent including all the features. To do this, we will first open a geojson file containing our region of interest (ROI) then simplify it to a bounding box by getting the bounds and putting them into a tuple. We will use the `total_bounds` property to get the bounding box of our ROI, and add that to a python tuple, which is the expected data type for the bounding_box parameter `earthaccess` `search_data`.\n",
114 | "\n",
115 | "\n",
116 | "**Note:** If our features are spread out spatially in the ROI, we can search for data available for each feature separately to avoid accessing a large volume of data we do not need."
117 | ]
118 | },
119 | {
120 | "cell_type": "markdown",
121 | "metadata": {},
122 | "source": [
123 | "Our ROI is stored as a .zip file. First we need to extract all the members of the zip into a specific location. "
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": null,
129 | "metadata": {},
130 | "outputs": [],
131 | "source": [
132 | "with ZipFile('../../data/City_of_Boulder_City_Limits.zip', 'r') as zObject: \n",
133 | " zObject.extractall( path=\"../../data\")"
134 | ]
135 | },
136 | {
137 | "cell_type": "markdown",
138 | "metadata": {},
139 | "source": [
140 | "Below, the polygon is opened using `geopandas` library and Coordinate Reference System (crs) is printed. Our polygon is in geographic coordinate reference system (`EPSG:3857`)."
141 | ]
142 | },
143 | {
144 | "cell_type": "code",
145 | "execution_count": null,
146 | "metadata": {},
147 | "outputs": [],
148 | "source": [
149 | "polygon = gp.read_file('../../data/City_of_Boulder_City_Limits.shp')\n",
150 | "polygon.crs"
151 | ]
152 | },
153 | {
154 | "cell_type": "markdown",
155 | "metadata": {},
156 | "source": [
157 | "Since this data is in **EPSG:3857**, we need to reproject to **EPSG:4326** to get latitude and longitude values for our search. We can use the `to_crs` method from `geopandas` to reproject our polygon."
158 | ]
159 | },
160 | {
161 | "cell_type": "code",
162 | "execution_count": null,
163 | "metadata": {},
164 | "outputs": [],
165 | "source": [
166 | "polygon_4326 = polygon.to_crs(\"EPSG:4326\")"
167 | ]
168 | },
169 | {
170 | "cell_type": "code",
171 | "execution_count": null,
172 | "metadata": {},
173 | "outputs": [],
174 | "source": [
175 | "polygon_4326.hvplot(tiles='ESRI', color='#d95f02',alpha=0.6, crs='EPSG:4326', frame_height=405, frame_width=720, fontscale=2) "
176 | ]
177 | },
178 | {
179 | "cell_type": "code",
180 | "execution_count": null,
181 | "metadata": {},
182 | "outputs": [],
183 | "source": [
184 | "bbox = tuple(list(polygon_4326.total_bounds))\n",
185 | "bbox"
186 | ]
187 | },
188 | {
189 | "cell_type": "markdown",
190 | "metadata": {},
191 | "source": [
192 | "Below, the parameters including `provider`, `short_name`, `version`, `bounding_box`, `temporal`, and `count` are used for our query. "
193 | ]
194 | },
195 | {
196 | "cell_type": "code",
197 | "execution_count": null,
198 | "metadata": {},
199 | "outputs": [],
200 | "source": [
201 | "results = earthaccess.search_data(\n",
202 | " provider='LPCLOUD',\n",
203 | " short_name='ECO_L2T_LSTE',\n",
204 | " version='002',\n",
205 | " bounding_box=bbox,\n",
206 | " temporal=('2023-07-01','2023-08-01'),\n",
207 | " count=100\n",
208 | ")"
209 | ]
210 | },
211 | {
212 | "cell_type": "markdown",
213 | "metadata": {},
214 | "source": [
215 | "Next, get the downloadable links for LSTE and quality layers using `data_links()` method from `earthaccess`. "
216 | ]
217 | },
218 | {
219 | "cell_type": "code",
220 | "execution_count": null,
221 | "metadata": {},
222 | "outputs": [],
223 | "source": [
224 | "lst_links = [l for dl in results for l in dl.data_links() if 'LST.tif' in l]\n",
225 | "lst_links"
226 | ]
227 | },
228 | {
229 | "cell_type": "code",
230 | "execution_count": null,
231 | "metadata": {},
232 | "outputs": [],
233 | "source": [
234 | "qc_links = [l for dl in results for l in dl.data_links() if 'QC.tif' in l]\n",
235 | "qc_links"
236 | ]
237 | },
238 | {
239 | "cell_type": "markdown",
240 | "metadata": {},
241 | "source": [
242 | "Let's take a look at the ECOSTRESS tiled data file name: \n",
243 | "\n",
244 | " Filename: **ECOv002_L2T_LSTE_28527_009_13TDE_20230718T081442_0710_01_LST.tif** \n",
245 | "\n",
246 | " ECO : Sensor \n",
247 | " v002 : Product Version \n",
248 | " L2T : Processing Level and Type (T = Tile) \n",
249 | " LSTE : Geophysical Parameter \n",
250 | " 28527 : Orbit Number \n",
251 | " 009 : Scene ID \n",
252 | " 13TDE : Military Grid Reference System (MGRS) Tile ID \n",
253 | " 20230718 : Date of Acquisition (YYYYMMDD) \n",
254 | " T081442 : Time of Acquisition (HHMMSS) (in UTC) \n",
255 | " 0710 : Build ID of software that generated product, Major+Minor (2+2 digits) \n",
256 | " 01 : Product Iteration Number \n",
257 | " LST : Layer/band Name (each layer is a separate file) \n",
258 | " .tif : Data Format for Tile \n",
259 | "\n",
260 | "\n",
261 | "Looking at Military Grid Reference System (MGRS) Tile ID of the outputs, they all are all in UTM Zone 13. "
262 | ]
263 | },
264 | {
265 | "cell_type": "markdown",
266 | "metadata": {},
267 | "source": [
268 | "## 3. Accessing ECOSTRESS L2T Land Surface Temperature and Emissivity Data\n",
269 | "\n",
270 | "ECOSTRESS data is stored in NASA's Earthdata Cloud and can be accessed in different ways.\n",
271 | "\n",
272 | "- *Downloaded* - This has been available since the existance of the NASA DAAC. Users can use the data link(s) to download the data files to their local working environment. This can be done whether the user is working from a non-cloud or cloud environment.\n",
273 | "- *Streamed* - Streaming is on-the-fly random reading of remote files, i.e. files not saved locally. The data accessed, however, must be able to be held in the workspaces' memory. This can be done whether the user is working from a non-cloud or cloud environment.\n",
274 | "- *Accessed in-place (i.e., direct s3 access)* - This is only available for working environment deployed in AWS us-west-2.\n",
275 | "\n",
276 | "In this example, we will show how to stream the data. For that, the gdal configuration is set and the one of our LSTE files is read into the workspace using `open_rasterio` from the `rioxarray` library. Since the file consists of only 1 layer, we can `squeeze` it, removing the `band` dimension."
277 | ]
278 | },
279 | {
280 | "cell_type": "code",
281 | "execution_count": null,
282 | "metadata": {},
283 | "outputs": [],
284 | "source": [
285 | "rio_env = rio.Env(GDAL_DISABLE_READDIR_ON_OPEN='EMPTY_DIR',\n",
286 | " GDAL_HTTP_COOKIEFILE=os.path.expanduser('~/cookies.txt'),\n",
287 | " GDAL_HTTP_COOKIEJAR=os.path.expanduser('~/cookies.txt'),\n",
288 | " GDAL_HTTP_MAX_RETRY=10,\n",
289 | " GDAL_HTTP_RETRY_DELAY=0.5)\n",
290 | "rio_env.__enter__()"
291 | ]
292 | },
293 | {
294 | "cell_type": "code",
295 | "execution_count": null,
296 | "metadata": {
297 | "tags": []
298 | },
299 | "outputs": [],
300 | "source": [
301 | "eco_lst_ds = rxr.open_rasterio(lst_links[14]).squeeze('band', drop=True)\n",
302 | "eco_lst_ds"
303 | ]
304 | },
305 | {
306 | "cell_type": "markdown",
307 | "metadata": {},
308 | "source": [
309 | "As mentioned, the ECOSTRESS product we are using here is tiled and the CRS is dependent on UTM zone. For this tile, we can look at the `spatial_ref` variable through the interactive object above to see details such as the well-known-text (WKT) representation of the CRS and other attributes. \n",
310 | "We are using `hvplot` for visualization here. For detailed information on available open-source Python tools and libraries for data visualization see .\n",
311 | "\n",
312 | "Now let's plot the data using `hvplot`. `reproject` function is applied only for the visualization. Make sure to specify the CRS argument within the `hvplot.image` function so the ESRI imagery RBG background tile aligns with our scene. \n",
313 | "\n"
314 | ]
315 | },
316 | {
317 | "cell_type": "code",
318 | "execution_count": null,
319 | "metadata": {},
320 | "outputs": [],
321 | "source": [
322 | "size_opts = dict(frame_height=405, frame_width=720, fontscale=2)\n",
323 | "\n",
324 | "eco_lst_ds.rio.reproject('EPSG:4326').hvplot.image(x='x', y='y', **size_opts, \n",
325 | " cmap='inferno', tiles='ESRI', xlabel='Longitude', \n",
326 | " ylabel='Latitude', title='ECOSTRESS LST (K)', \n",
327 | " crs='EPSG:4326')*polygon_4326.hvplot(color='Green',alpha=0.5, \n",
328 | " crs='EPSG:4326', rasterize=True)"
329 | ]
330 | },
331 | {
332 | "cell_type": "markdown",
333 | "metadata": {},
334 | "source": [
335 | "## 4. Cropping ECOSTRESS Data \n",
336 | "\n",
337 | "`clip` function from `rasterio` is used to mask data outside of our region of interest. Before clipping, we need to reproject our ROI to the projection of our dataset which is UTM zone 13N. "
338 | ]
339 | },
340 | {
341 | "cell_type": "code",
342 | "execution_count": null,
343 | "metadata": {},
344 | "outputs": [],
345 | "source": [
346 | "polygon"
347 | ]
348 | },
349 | {
350 | "cell_type": "code",
351 | "execution_count": null,
352 | "metadata": {},
353 | "outputs": [],
354 | "source": [
355 | "eco_lst_ds.rio.crs"
356 | ]
357 | },
358 | {
359 | "cell_type": "code",
360 | "execution_count": null,
361 | "metadata": {},
362 | "outputs": [],
363 | "source": [
364 | "polygon_reproj = polygon.to_crs(eco_lst_ds.rio.crs)"
365 | ]
366 | },
367 | {
368 | "cell_type": "code",
369 | "execution_count": null,
370 | "metadata": {},
371 | "outputs": [],
372 | "source": [
373 | "polygon_reproj.crs"
374 | ]
375 | },
376 | {
377 | "cell_type": "code",
378 | "execution_count": null,
379 | "metadata": {
380 | "tags": []
381 | },
382 | "outputs": [],
383 | "source": [
384 | "eco_lst_roi = eco_lst_ds.rio.clip(polygon_reproj.geometry.values, polygon_reproj.crs, all_touched=True)"
385 | ]
386 | },
387 | {
388 | "cell_type": "code",
389 | "execution_count": null,
390 | "metadata": {
391 | "tags": []
392 | },
393 | "outputs": [],
394 | "source": [
395 | "eco_lst_roi.hvplot.image(\n",
396 | " geo=True,cmap='inferno',**size_opts, tiles='ESRI',alpha=0.8, \n",
397 | " title='Cropped ECOSTRESS LST (K)', xlabel='Longitude',ylabel='Latitude', \n",
398 | " crs='EPSG:32613', rasterize=True)"
399 | ]
400 | },
401 | {
402 | "cell_type": "markdown",
403 | "metadata": {},
404 | "source": [
405 | "Next, we will repeate the same process for the associated quality layer."
406 | ]
407 | },
408 | {
409 | "cell_type": "code",
410 | "execution_count": null,
411 | "metadata": {},
412 | "outputs": [],
413 | "source": [
414 | "eco_qc_ds = rxr.open_rasterio(qc_links[14]).squeeze('band', drop=True)\n",
415 | "\n",
416 | "eco_qc_roi = eco_qc_ds.rio.clip(polygon_reproj.geometry.values, polygon_reproj.crs, all_touched=True) # assign a different value to fill value \n",
417 | "\n",
418 | "eco_qc_roi.hvplot.image(\n",
419 | " geo=True, cmap='inferno',**size_opts, tiles='ESRI',alpha=0.8, \n",
420 | " title='Cropped ECOSTRESS LST (K)', xlabel='Longitude',ylabel='Latitude', \n",
421 | " crs='EPSG:32613', rasterize=True)"
422 | ]
423 | },
424 | {
425 | "cell_type": "markdown",
426 | "metadata": {},
427 | "source": [
428 | "## 5. Quality Filtering\n",
429 | "\n",
430 | "The quality values are 16 digits bit values with bits 0 and 1 being the mandatory QA flag that will be interpreted as:\n",
431 | "\n",
432 | " 00 = Pixel produced, best quality\n",
433 | " 01 = Pixel produced, nominal quality. Either one or more of the following conditions are met: \n",
434 | "\n",
435 | " 1. emissivity in both bands 4 and 5 < 0.95, i.e. possible cloud contamination \n",
436 | " 2. low transmissivity due to high water vapor loading (<0.4), check PWV values and error estimates \n",
437 | " 3. Pixel falls on missing scan line in bands 1&5, and filled using spatial neural net. Check error estimates \n",
438 | " Recommend more detailed analysis of other QC information \n",
439 | " 10 = Pixel produced, but cloud detected \n",
440 | " 11 = Pixel not produced due to missing/bad data, user should check Data quality flag bits \n",
441 | "\n",
442 | "The detailed quality information is provided in Table 3-5 in ECOSTRESS [Product Specification Document](https://lpdaac.usgs.gov/documents/380/ECO2_PSD_V1.pdf). \n"
443 | ]
444 | },
445 | {
446 | "cell_type": "markdown",
447 | "metadata": {},
448 | "source": [
449 | "Below, the unique quality values are extracted from the clipped data, and only the values showing good quality are kept. "
450 | ]
451 | },
452 | {
453 | "cell_type": "code",
454 | "execution_count": null,
455 | "metadata": {},
456 | "outputs": [],
457 | "source": [
458 | "quality_vals = np.unique(eco_qc_roi.values).tolist()\n",
459 | "good_q = [q for q in quality_vals if np.binary_repr(q, width=16)[-2:] == '00']\n",
460 | "good_q\n"
461 | ]
462 | },
463 | {
464 | "cell_type": "markdown",
465 | "metadata": {},
466 | "source": [
467 | "`.where` method is used to filter the quality and keep only the LSTE values generated with the best quality. "
468 | ]
469 | },
470 | {
471 | "cell_type": "code",
472 | "execution_count": null,
473 | "metadata": {},
474 | "outputs": [],
475 | "source": [
476 | "eco_lst_roi_mask = eco_lst_roi.where(eco_qc_roi.isin(good_q))\n",
477 | "eco_lst_roi_mask"
478 | ]
479 | },
480 | {
481 | "cell_type": "code",
482 | "execution_count": null,
483 | "metadata": {},
484 | "outputs": [],
485 | "source": [
486 | "eco_lst_roi_mask.hvplot.image(\n",
487 | " geo=True,cmap='inferno',**size_opts, tiles='ESRI',alpha=0.9, \n",
488 | " title='Quality Masked ECOSTRESS LST (K)', xlabel='Longitude',ylabel='Latitude', \n",
489 | " crs='EPSG:32613', rasterize=True)"
490 | ]
491 | },
492 | {
493 | "cell_type": "markdown",
494 | "metadata": {},
495 | "source": [
496 | "## 6. Writing Outputs \n",
497 | "\n",
498 | "We now have a ECOSTRESS scene that is clipped to our ROI with only good quality values. Finally, we can save this file locally. "
499 | ]
500 | },
501 | {
502 | "cell_type": "code",
503 | "execution_count": null,
504 | "metadata": {},
505 | "outputs": [],
506 | "source": [
507 | "out_name = f\"../../data/{lst_links[14].split('/')[-1].split('.tif')[0]}_clipped.tif\"\n",
508 | "\n",
509 | "eco_lst_roi_mask.rio.to_raster(raster_path=out_name, driver='COG')"
510 | ]
511 | },
512 | {
513 | "cell_type": "markdown",
514 | "metadata": {},
515 | "source": [
516 | "## Contact Info: \n",
517 | "\n",
518 | "Email: LPDAAC@usgs.gov \n",
519 | "Voice: +1-866-573-3222 \n",
520 | "Organization: Land Processes Distributed Active Archive Center (LP DAAC)¹ \n",
521 | "Website: \n",
522 | "\n",
523 | "¹Work performed under USGS contract G15PD00467 for NASA contract NNG14HH33I. "
524 | ]
525 | }
526 | ],
527 | "metadata": {
528 | "kernelspec": {
529 | "display_name": "Python 3 (ipykernel)",
530 | "language": "python",
531 | "name": "python3"
532 | },
533 | "language_info": {
534 | "codemirror_mode": {
535 | "name": "ipython",
536 | "version": 3
537 | },
538 | "file_extension": ".py",
539 | "mimetype": "text/x-python",
540 | "name": "python",
541 | "nbconvert_exporter": "python",
542 | "pygments_lexer": "ipython3",
543 | "version": "3.11.13"
544 | }
545 | },
546 | "nbformat": 4,
547 | "nbformat_minor": 4
548 | }
549 |
--------------------------------------------------------------------------------
/setup.md:
--------------------------------------------------------------------------------
1 | ## Set Up Instruction
2 |
3 | **If you are attending the workshop:**
4 | - Go to the [Openscapes 2i2c JupyterHub](https://workshop.openscapes.2i2c.cloud/) and Log in using your email as the username and the shared password. Select **Python** and choose a session size of **14.8 GB RAM / up to 3.75 CPUs**. Most notebooks will run with this option unless otherwise noted.
5 |
6 | - Review the [prerequisites and workshop set up instructions](https://nasa.github.io/VITALS/setup/workshop_setup.html) for details.
7 |
8 | **If you are running the notebooks on your local machine:**
9 | - Follow the [instructions for setting up Python environmnet](https://github.com/nasa/LPDAAC-Data-Resources/blob/main/setup/setup_instructions_python.md) locally.
10 |
11 |
12 | ## Cloning the Repository
13 |
14 | To work with the notebooks or modules, clone the repository to your desired directory:
15 |
16 | ```cmd
17 | git clone https://github.com/nasa/ECOSTRESS-Data-Resources.git
18 | ```
19 |
20 | If you plan to edit or contribute to the `ECOSTRESS-Data-Resources` repository, we recommend following a fork and pull workflow: first fork the repository, then clone your fork to your local machine, make changes, push changes to your fork, then make a pull request back to the main repository. An example can be found in the [CONTRIBUTING.md](../CONTRIBUTING.md) file.
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | /* css styles */
2 |
--------------------------------------------------------------------------------