diff --git a/README.rst b/README.rst index 6083f30..76ee287 100644 --- a/README.rst +++ b/README.rst @@ -43,9 +43,9 @@ end_date geometry ee.Geometry() that is passed to the collection .filterBounds() calls. All images with a footprint that intersects the geometry will be included. -etr_source - Reference ET source collection ID. -etr_band +et_reference_source + Reference ET source collection ID (see `Reference ET Sources`_). +et_reference_band Reference ET source band name. Optional Inputs @@ -96,18 +96,19 @@ Collection Examples start_date='2017-06-01', end_date='2017-09-01', geometry=ee.Geometry.Point(-121.5265, 38.7399), - etr_source='IDAHO_EPSCOR/GRIDMET', - etr_band='etr') \ - .overpass(variables=['et', 'etr', 'etf']) + et_reference_source='IDAHO_EPSCOR/GRIDMET', + et_reference_band='eto') \ + .overpass(variables=['et', 'et_reference', 'et_fraction']) monthly_coll = model.Collection( collections=['LANDSAT/LC08/C01/T1_SR'], start_date='2017-06-01', end_date='2017-09-01', geometry=ee.Geometry.Point(-121.5265, 38.7399), - etr_source='IDAHO_EPSCOR/GRIDMET', - etr_band='etr') \ - .interpolate(variables=['et', 'etr', 'etf'] t_interval='monthly') + et_reference_source='IDAHO_EPSCOR/GRIDMET', + et_reference_band='eto') \ + .interpolate(variables=['et', 'et_reference', 'et_fraction'] + t_interval='monthly') Image ===== @@ -152,9 +153,10 @@ Image Example .. code-block:: python import openet.sims as model - landsat_img = ee.Image('LANDSAT/LC08/C01/T1_SR/LC08_044033_20170716') et_img = model.Image.from_landsat_c1_sr( - landsat_img, etr_source='IDAHO_EPSCOR/GRIDMET', etr_band='etr).et + ee.Image('LANDSAT/LC08/C01/T1_SR/LC08_044033_20170716'), + et_reference_source='IDAHO_EPSCOR/GRIDMET', + et_reference_band='eto').et Variables ========= @@ -163,10 +165,10 @@ The SIMS model can compute the following variables: ndvi Normalized difference vegetation index [unitless] -etf +et_fraction Fraction of reference ET [unitless] -etr - Reference ET (alfalfa) [mm] +et_reference + Reference ET [mm] (type will depend on `Reference ET`_ parameters) et Actual ET [mm] @@ -175,7 +177,7 @@ There is also a more general "calculate" method that can be used to return a mul Reference ET ============ -The reference ET data source is controlled using the "etr_source" and "etr_band" parameters. +The reference ET data source is controlled using the "et_reference_source" and "et_reference_band" parameters. The model is expecting a grass reference ET (ETo) and will not return valid results if an alfalfa reference ET (ETr) is used. @@ -185,12 +187,10 @@ Reference ET Sources GRIDMET | Collection ID: IDAHO_EPSCOR/GRIDMET | http://www.climatologylab.org/gridmet.html - | Alfalfa reference ET band: etr | Grass reference ET band: eto Spatial CIMIS | Collection ID: projects/openet/cimis/daily | https://cimis.water.ca.gov/SpatialData.aspx - | Alfalfa reference ET band: ETr_ASCE | Grass reference ET band: ETo_ASCE Example Notebooks diff --git a/examples/collection_interpolate.ipynb b/examples/collection_interpolate.ipynb index 3602869..a5b3627 100644 --- a/examples/collection_interpolate.ipynb +++ b/examples/collection_interpolate.ipynb @@ -23,7 +23,7 @@ "from IPython.display import Image\n", "import openet.sims as model\n", "\n", - "ee.Initialize()" + "ee.Initialize(use_cloud_api=False)" ] }, { @@ -243,7 +243,7 @@ "source": [ "et_monthly_coll = model_obj.interpolate(\n", " t_interval='monthly', \n", - " variables=['et', 'etr', 'etf'], \n", + " variables=['et', 'et_reference', 'et_fraction'], \n", " interp_method=interp_method,\n", " interp_days=interp_days,\n", ")\n", @@ -338,7 +338,7 @@ } ], "source": [ - "Image(url=ee.Image(et_monthly_coll.select(['etr']).sum())\\\n", + "Image(url=ee.Image(et_monthly_coll.select(['et_reference']).sum())\\\n", " .reproject(crs=study_crs, scale=100)\\\n", " .getThumbURL({'min': 0.0, 'max': 300, 'palette': et_palette, 'region': study_region}),\n", " embed=True, format='png') " diff --git a/examples/collection_overpass.ipynb b/examples/collection_overpass.ipynb index 7b670bf..03a0008 100644 --- a/examples/collection_overpass.ipynb +++ b/examples/collection_overpass.ipynb @@ -23,7 +23,7 @@ "from IPython.display import Image\n", "import openet.sims as model\n", "\n", - "ee.Initialize()" + "ee.Initialize(use_cloud_api=False)" ] }, { @@ -153,7 +153,7 @@ "metadata": {}, "outputs": [], "source": [ - "overpass_coll = model_obj.overpass(variables=['ndvi', 'et', 'etr', 'etf'])" + "overpass_coll = model_obj.overpass(variables=['ndvi', 'et', 'et_reference', 'et_fraction'])" ] }, { @@ -182,7 +182,7 @@ "overpass_df = get_region_df(overpass_coll.getRegion(test_point, scale=30).getInfo())\n", "pprint.pprint(overpass_df)\n", "print('')\n", - "pprint.pprint(overpass_df[['et', 'etr']].sum())" + "pprint.pprint(overpass_df[['et', 'et_reference']].sum())" ] }, { @@ -241,7 +241,7 @@ } ], "source": [ - "Image(url=ee.Image(overpass_coll.select(['etf']).mean())\\\n", + "Image(url=ee.Image(overpass_coll.select(['et_fraction']).mean())\\\n", " .reproject(crs=study_crs, scale=30)\\\n", " .getThumbURL({'min': 0.0, 'max': 1.2, 'palette': et_palette, 'region': study_region}),\n", " embed=True, format='png')" @@ -272,7 +272,7 @@ } ], "source": [ - "Image(url=ee.Image(overpass_coll.select(['etr']).mean())\\\n", + "Image(url=ee.Image(overpass_coll.select(['et_reference']).mean())\\\n", " .reproject(crs=study_crs, scale=30)\\\n", " .getThumbURL({'min': 0.0, 'max': 12, 'palette': et_palette, 'region': study_region}),\n", " embed=True, format='png')" diff --git a/examples/image_mapping.ipynb b/examples/image_mapping.ipynb index 6104f96..a5fad30 100644 --- a/examples/image_mapping.ipynb +++ b/examples/image_mapping.ipynb @@ -22,7 +22,7 @@ "from IPython.display import Image\n", "import openet.sims as model\n", "\n", - "ee.Initialize()" + "ee.Initialize(use_cloud_api=False)" ] }, { @@ -52,10 +52,10 @@ "source": [ "collection_id = 'LANDSAT/LC08/C01/T1_SR'\n", "\n", - "etr_source = 'projects/climate-engine/cimis/daily'\n", - "etr_band = 'ETo'\n", - "# etr_source = 'IDAHO_EPSCOR/GRIDMET'\n", - "# etr_band = 'eto'\n", + "et_reference_source = 'projects/climate-engine/cimis/daily'\n", + "et_reference_band = 'ETo'\n", + "# et_reference_source = 'IDAHO_EPSCOR/GRIDMET'\n", + "# et_reference_band = 'eto'\n", "\n", "# Date range you want to aggregate ET over\n", "# End date is inclusive\n", @@ -154,7 +154,8 @@ "def compute_et(image):\n", " \"\"\"Return an ET image for each input Landsat 8 C1 SR image\"\"\"\n", " return model.Image.from_landsat_c1_sr(\n", - " image, etr_source=etr_source, etr_band=etr_band).et\n", + " image, et_reference_source=et_reference_source, \n", + " et_reference_band=et_reference_band).et\n", " \n", "# Build the SIMS model for each image then compute and return ET\n", "et_coll = ee.ImageCollection(landsat_coll.map(compute_et))\n", @@ -207,8 +208,8 @@ "def custom_et(image):\n", " image_obj = model.Image.from_landsat_c1_sr(\n", " image, \n", - " etr_source='IDAHO_EPSCOR/GRIDMET', \n", - " etr_band='eto',\n", + " et_reference_source='IDAHO_EPSCOR/GRIDMET', \n", + " et_reference_band='eto',\n", " # tdiff_threshold=1\n", " )\n", " return image_obj.et\n", @@ -257,7 +258,7 @@ "source": [ "def compute_vars(image):\n", " \"\"\"Return an ET image for each input Landsat 8 C1 SR image\"\"\"\n", - " return model.Image.from_landsat_c1_sr(image, etr_source=etr_source, etr_band=etr_band)\\\n", + " return model.Image.from_landsat_c1_sr(image, et_reference_source=et_reference_source, et_reference_band=et_reference_band)\\\n", " .calculate(['et', 'etr', 'etf'])\n", " \n", "vars_coll = ee.ImageCollection(landsat_coll.map(compute_vars))\n", diff --git a/examples/single_image.ipynb b/examples/single_image.ipynb index dc7a03e..0da4b69 100644 --- a/examples/single_image.ipynb +++ b/examples/single_image.ipynb @@ -22,7 +22,7 @@ "from IPython.display import Image\n", "import openet.sims as model\n", "\n", - "ee.Initialize()" + "ee.Initialize(use_cloud_api=False)" ] }, { @@ -89,7 +89,9 @@ "source": [ "# Build the SIMS object from the Landsat image\n", "model_obj = model.Image.from_landsat_c1_sr(\n", - " landsat_img, etr_source='projects/climate-engine/cimis/daily', etr_band='ETo',\n", + " landsat_img, \n", + " et_reference_source='projects/climate-engine/cimis/daily', \n", + " et_reference_band='ETo',\n", " # crop_type_source='USDA/NASS/CDL/2017',\n", " crop_type_source='projects/openet/crop_type',\n", ")" @@ -207,7 +209,7 @@ } ], "source": [ - "Image(url=model_obj.etf.getThumbURL({'min': 0.0, 'max': 1.2, 'palette': et_palette}),\n", + "Image(url=model_obj.et_fraction.getThumbURL({'min': 0.0, 'max': 1.2, 'palette': et_palette}),\n", " embed=True, format='png')" ] }, @@ -237,7 +239,7 @@ ], "source": [ "# Clip and project to the Landsat image footprint and coordinate system\n", - "Image(url=model_obj.etr.getThumbURL({'min': 0, 'max': 12, 'palette': et_palette, 'region': landsat_region}),\n", + "Image(url=model_obj.et_reference.getThumbURL({'min': 0, 'max': 12, 'palette': et_palette, 'region': landsat_region}),\n", " embed=True, format='png')" ] }, @@ -297,7 +299,7 @@ } ], "source": [ - "Image(url=model_obj.calculate(['et', 'etr', 'etf']).select(['et'])\\\n", + "Image(url=model_obj.calculate(['et', 'et_reference', 'et_fraction']).select(['et'])\\\n", " .getThumbURL({'min': 0, 'max': 12, 'palette': et_palette}),\n", " embed=True, format='png')" ] diff --git a/openet/sims/__init__.py b/openet/sims/__init__.py index ae9e4e3..775f678 100644 --- a/openet/sims/__init__.py +++ b/openet/sims/__init__.py @@ -1,6 +1,7 @@ from .image import Image from .collection import Collection +from . import interpolate -__version__ = "0.0.12" +__version__ = "0.0.16" MODEL_NAME = 'SIMS' diff --git a/openet/sims/collection.py b/openet/sims/collection.py index 4e77fc8..90b541c 100644 --- a/openet/sims/collection.py +++ b/openet/sims/collection.py @@ -21,7 +21,7 @@ from .image import Image # Importing to get version number, is there a better way? import openet.sims -import openet.core.interp as interp +import openet.core.interpolate # TODO: import utils from openet.core # import openet.core.utils as utils @@ -52,16 +52,16 @@ def __init__( geometry, variables=None, cloud_cover_max=70, - etr_source=None, - etr_band=None, - etr_factor=None, - etr_resample=None, + et_reference_source=None, + et_reference_band=None, + et_reference_factor=None, + et_reference_resample=None, filter_args=None, model_args=None, - # model_args={'etr_source': 'IDAHO_EPSCOR/GRIDMET', - # 'etr_band': 'etr', - # 'etr_factor': 0.85, - # 'etr_resample': 'bilinear}, + # model_args={'et_reference_source': 'IDAHO_EPSCOR/GRIDMET', + # 'et_reference_band': 'etr', + # 'et_reference_factor': 0.85, + # 'et_reference_resample': 'bilinear}, # **kwargs ): """Earth Engine based SIMS ETcb Image Collection object @@ -85,16 +85,16 @@ def __init__( Maximum cloud cover percentage (the default is 70%). - Landsat SR: CLOUD_COVER_LAND - Sentinel2: CLOUDY_PIXEL_PERCENTAGE - etr_source : str, float, optional + et_reference_source : str, float, optional Reference ET source (the default is None). ETr Parameters must be be set here or in model args to interpolate ET, ETf, or ETr. - etr_band : str, optional + et_reference_band : str, optional Reference ET band name (the default is None). ETr Parameters must be be set here or in model args to interpolate ET, ETf, or ETr. - etr_factor : float, None, optional + et_reference_factor : float, None, optional Reference ET scaling factor. The default is None which is equivalent to 1.0 (or no scaling). - etr_resample : {'nearest', 'bilinear', 'bicubic'}, None, optional + et_reference_resample : {'nearest', 'bilinear', 'bicubic'}, None, optional Reference ET resampling. The default is None which is equivalent to nearest neighbor resampling. filter_args : dict @@ -124,33 +124,34 @@ def __init__( self.filter_args = {} # Reference ET parameters - self.etr_source = etr_source - self.etr_band = etr_band - self.etr_factor = etr_factor - self.etr_resample = etr_resample + self.et_reference_source = et_reference_source + self.et_reference_band = et_reference_band + self.et_reference_factor = et_reference_factor + self.et_reference_resample = et_reference_resample # Check reference ET parameters - if etr_factor and not utils.is_number(etr_factor): - raise ValueError('etr_factor must be a number') - if etr_factor and self.etr_factor < 0: - raise ValueError('etr_factor must be greater than zero') - etr_resample_methods = ['nearest', 'bilinear', 'bicubic'] - if etr_resample and etr_resample.lower() not in etr_resample_methods: - raise ValueError('unsupported etr_resample method') + if et_reference_factor and not utils.is_number(et_reference_factor): + raise ValueError('et_reference_factor must be a number') + if et_reference_factor and self.et_reference_factor < 0: + raise ValueError('et_reference_factor must be greater than zero') + et_reference_resample_methods = ['nearest', 'bilinear', 'bicubic'] + if (et_reference_resample and \ + et_reference_resample.lower() not in et_reference_resample_methods): + raise ValueError('unsupported et_reference_resample method') # Set/update the ETr parameters in model_args if they were set in init() - if self.etr_source: - self.model_args['etr_source'] = self.etr_source - if self.etr_band: - self.model_args['etr_band'] = self.etr_band - if self.etr_factor: - self.model_args['etr_factor'] = self.etr_factor - if self.etr_resample: - self.model_args['etr_resample'] = self.etr_resample + if self.et_reference_source: + self.model_args['et_reference_source'] = self.et_reference_source + if self.et_reference_band: + self.model_args['et_reference_band'] = self.et_reference_band + if self.et_reference_factor: + self.model_args['et_reference_factor'] = self.et_reference_factor + if self.et_reference_resample: + self.model_args['et_reference_resample'] = self.et_reference_resample # Model specific variables that can be interpolated to a daily timestep # CGM - Should this be specified in the interpolation method instead? - self._interp_vars = ['ndvi', 'etf'] + self._interp_vars = ['ndvi', 'et_fraction'] self._landsat_c1_sr_collections = [ 'LANDSAT/LC08/C01/T1_SR', @@ -313,8 +314,7 @@ def overpass(self, variables=None): return self._build(variables=variables) def interpolate(self, variables=None, t_interval='custom', - interp_method='linear', interp_days=32, - output_type='float', **kwargs): + interp_method='linear', interp_days=32, **kwargs): """ Parameters @@ -331,10 +331,6 @@ def interpolate(self, variables=None, t_interval='custom', interp_days : int, str, optional Number of extra days before the start date and after the end date to include in the interpolation calculation. (the default is 32). - output_type : {'int8', 'uint8', 'int16', 'float', 'double'}, optional - Output data type for the ET and ETr bands (the default is 'float'). - NDVI and ETf bands will always be written as float type. - Count band will always be written as uint8 type. kwargs : dict, optional Returns @@ -350,8 +346,8 @@ def interpolate(self, variables=None, t_interval='custom', Notes ----- Not all variables can be interpolated to new time steps. - Variables like ETr are simply summed whereas ETf is computed from the - interpolated/aggregated values. + Variables like reference ET are simply summed whereas ET fraction is + computed from the interpolated/aggregated values. """ # Check that the input parameters are valid @@ -375,10 +371,6 @@ def interpolate(self, variables=None, t_interval='custom', else: raise ValueError('variables parameter must be set') - output_types = ['int8', 'uint8', 'int16', 'uint16', 'float', 'double'] - if output_type.lower() not in output_types: - raise ValueError('unsupported output_type: {}'.format(output_type)) - # Adjust start/end dates based on t_interval # Increase the date range to fully include the time interval start_dt = datetime.datetime.strptime(self.start_date, '%Y-%m-%d') @@ -405,52 +397,57 @@ def interpolate(self, variables=None, t_interval='custom', interp_start_date = interp_start_dt.date().isoformat() interp_end_date = interp_end_dt.date().isoformat() - # Update model_args if etr parameters were passed to interpolate - # Intentionally using model_args (instead of self.etr_source, etc.) in + # Update model_args if et_reference parameters were passed to interpolate + # Intentionally using model_args (instead of self.et_reference_source, etc.) in # this function since model_args is passed to Image class in _build() - # if 'et' in variables or 'etr' in variables: - if 'etr_source' in kwargs.keys() and kwargs['etr_source'] is not None: - self.model_args['etr_source'] = kwargs['etr_source'] - if 'etr_band' in kwargs.keys() and kwargs['etr_band'] is not None: - self.model_args['etr_band'] = kwargs['etr_band'] - if 'etr_factor' in kwargs.keys() and kwargs['etr_factor'] is not None: - self.model_args['etr_factor'] = kwargs['etr_factor'] - if 'etr_resample' in kwargs.keys() and kwargs['etr_resample'] is not None: - self.model_args['etr_resample'] = kwargs['etr_resample'] - - # Check that all etr parameters were set - for etr_param in ['etr_source', 'etr_band', 'etr_factor', 'etr_resample']: - if etr_param not in self.model_args.keys(): - raise ValueError('{} was not set'.format(etr_param)) - elif not self.model_args[etr_param]: - raise ValueError('{} was not set'.format(etr_param)) - - if type(self.model_args['etr_source']) is str: + # if 'et' in variables or 'et_reference' in variables: + if ('et_reference_source' in kwargs.keys() and \ + kwargs['et_reference_source'] is not None): + self.model_args['et_reference_source'] = kwargs['et_reference_source'] + if ('et_reference_band' in kwargs.keys() and \ + kwargs['et_reference_band'] is not None): + self.model_args['et_reference_band'] = kwargs['et_reference_band'] + if ('et_reference_factor' in kwargs.keys() and \ + kwargs['et_reference_factor'] is not None): + self.model_args['et_reference_factor'] = kwargs['et_reference_factor'] + if ('et_reference_resample' in kwargs.keys() and \ + kwargs['et_reference_resample'] is not None): + self.model_args['et_reference_resample'] = kwargs['et_reference_resample'] + + # Check that all et_reference parameters were set + for et_reference_param in ['et_reference_source', 'et_reference_band', + 'et_reference_factor', 'et_reference_resample']: + if et_reference_param not in self.model_args.keys(): + raise ValueError('{} was not set'.format(et_reference_param)) + elif not self.model_args[et_reference_param]: + raise ValueError('{} was not set'.format(et_reference_param)) + + if type(self.model_args['et_reference_source']) is str: # Assume a string source is an single image collection ID # not an list of collection IDs or ee.ImageCollection - daily_etr_coll = ee.ImageCollection(self.model_args['etr_source']) \ + daily_et_reference_coll = ee.ImageCollection(self.model_args['et_reference_source']) \ .filterDate(start_date, end_date) \ - .select([self.model_args['etr_band']], ['etr']) - # elif isinstance(self.model_args['etr_source'], computedobject.ComputedObject): + .select([self.model_args['et_reference_band']], ['et_reference']) + # elif isinstance(self.model_args['et_reference_source'], computedobject.ComputedObject): # # Interpret computed objects as image collections - # daily_etr_coll = ee.ImageCollection(self.model_args['etr_source'])\ - # .select([self.model_args['etr_band']])\ + # daily_et_reference_coll = ee.ImageCollection(self.model_args['et_reference_source'])\ + # .select([self.model_args['et_reference_band']])\ # .filterDate(self.start_date, self.end_date) else: - raise ValueError('unsupported etr_source: {}'.format( - self.model_args['etr_source'])) + raise ValueError('unsupported et_reference_source: {}'.format( + self.model_args['et_reference_source'])) # Initialize variable list to only variables that can be interpolated interp_vars = list(set(self._interp_vars) & set(variables)) - # To return ET, the ETf must be interpolated - if 'et' in variables and 'etf' not in interp_vars: - interp_vars.append('etf') + # To return ET, the ET fraction must be interpolated + if 'et' in variables and 'et_fraction' not in interp_vars: + interp_vars.append('et_fraction') - # With the current interp.daily() function, - # something has to be interpolated in order to return etr - if 'etr' in variables and 'etf' not in interp_vars: - interp_vars.append('etf') + # With the current interpolate.daily() function, + # something has to be interpolated in order to return et_reference + if 'et_reference' in variables and 'et_fraction' not in interp_vars: + interp_vars.append('et_fraction') # The time band is always needed for interpolation interp_vars.append('time') @@ -467,12 +464,19 @@ def interpolate(self, variables=None, t_interval='custom', # For count, compute the composite/mosaic image for the mask band only if 'count' in variables: - aggregate_coll = interp.aggregate_daily( + aggregate_coll = openet.core.interpolate.aggregate_daily( image_coll=scene_coll.select(['mask']), - start_date=start_date, end_date=end_date, - ) - - # Including count/mask causes problems in interp.daily() function. + start_date=start_date, end_date=end_date) + # The following is needed because the aggregate collection can be + # empty if there are no scenes in the target date range but there + # are scenes in the interpolation date range. + # Without this the count image will not be built but the other + # bands will be which causes a non-homogenous image collection. + aggregate_coll = aggregate_coll.merge( + ee.Image.constant(0).rename(['mask']) + .set({'system:time_start': ee.Date(start_date).millis()})) + + # Including count/mask causes problems in interpolate.daily() function. # Issues with mask being an int but the values need to be double. # Casting the mask band to a double would fix this problem also. if 'mask' in interp_vars: @@ -481,20 +485,21 @@ def interpolate(self, variables=None, t_interval='custom', # Interpolate to a daily time step # NOTE: the daily function is not computing ET (ETf x ETr) # but is returning the target (ETr) band - daily_coll = interp.daily( - target_coll=daily_etr_coll, + daily_coll = openet.core.interpolate.daily( + target_coll=daily_et_reference_coll, source_coll=scene_coll.select(interp_vars), interp_method=interp_method, interp_days=interp_days, ) - # Compute ET from ETf and ETr (if necessary) - # if 'et' in variables or 'etf' in variables: + # Compute ET from ET fraction and reference ET (if necessary) + # if 'et' in variables or 'et_fraction' in variables: def compute_et(img): - """This function assumes ETr and ETf are present""" + """This function assumes et_reference and et_fraction are present""" - # TODO: Should ETr be mapped to the etf band here? + # TODO: Should ETr be mapped to the et_fraction band here? - et_img = img.select(['etf']).multiply(img.select(['etr'])) + et_img = img.select(['et_fraction']) \ + .multiply(img.select(['et_reference'])) return img.addBands(et_img.rename('et')) daily_coll = daily_coll.map(compute_et) @@ -530,74 +535,48 @@ def aggregate_image(agg_start_date, agg_end_date, date_format): for each time interval by separate mappable functions """ - # if 'et' in variables or 'etf' in variables: - et_img = daily_coll.filterDate(agg_start_date, agg_end_date)\ + # if 'et' in variables or 'et_fraction' in variables: + et_img = daily_coll.filterDate(agg_start_date, agg_end_date) \ .select(['et']).sum() - # if 'etr' in variables or 'etf' in variables: - etr_img = daily_coll.filterDate(agg_start_date, agg_end_date)\ - .select(['etr']).sum() + # if 'et_reference' in variables or 'et_fraction' in variables: + et_reference_img = daily_coll.filterDate(agg_start_date, agg_end_date) \ + .select(['et_reference']).sum() - if self.model_args['etr_factor']: - et_img = et_img.multiply(self.model_args['etr_factor']) - etr_img = etr_img.multiply(self.model_args['etr_factor']) + if self.model_args['et_reference_factor']: + et_img = et_img.multiply(self.model_args['et_reference_factor']) + et_reference_img = et_reference_img.multiply( + self.model_args['et_reference_factor']) # DEADBEEF - This doesn't seem to be doing anything - if self.etr_resample in ['bilinear', 'bicubic']: + if self.et_reference_resample in ['bilinear', 'bicubic']: print('collection interpolate aggregate bilinear') - etr_img = etr_img.resample(self.model_args['etr_resample']) + et_reference_img = et_reference_img.resample(self.model_args['et_reference_resample']) # Will mapping ETr to the ET band trigger the resample? - # etr_img = et_img.multiply(0).add(etr_img) - - # Round and save ET and ETr as integer values to save space - # Ensure that ETr > 0 after rounding to avoid divide by zero - # Compute ETf from the rounded values - if output_type.lower() == 'int16': - etf_img = et_img.round().divide(etr_img.round().max(1)).float() - et_img = et_img.round().int16() - etr_img = etr_img.round().int16() - elif output_type.lower() == 'uint16': - etf_img = et_img.round().divide(etr_img.round().max(1)).float() - et_img = et_img.round().uint16() - etr_img = etr_img.round().uint16() - elif output_type.lower() == 'int8': - etf_img = et_img.round().divide(etr_img.round().max(1)).float() - et_img = et_img.round().int8() - etr_img = etr_img.round().int8() - elif output_type.lower() == 'uint8': - etf_img = et_img.round().divide(etr_img.round().max(1)).float() - et_img = et_img.round().uint8() - etr_img = etr_img.round().uint8() - elif output_type.lower() == 'float': - etf_img = et_img.divide(etr_img).float() - et_img = et_img.float() - etr_img = etr_img.float() - elif output_type.lower() == 'double': - # Casting to double may be redundant since these values should - # all be doubles be default - etf_img = et_img.divide(etr_img).double() - et_img = et_img.double() - etr_img = etr_img.double() + # et_reference_img = et_img.multiply(0).add(et_reference_img) image_list = [] if 'et' in variables: - image_list.append(et_img) - if 'etr' in variables: - image_list.append(etr_img) - if 'etf' in variables: - image_list.append(etf_img.rename(['etf'])) + image_list.append(et_img.float()) + if 'et_reference' in variables: + image_list.append(et_reference_img.float()) + if 'et_fraction' in variables: + # Compute average et fraction over the aggregation period + image_list.append( + et_img.divide(et_reference_img).rename(['et_fraction']).float()) if 'ndvi' in variables: - ndvi_img = daily_coll\ - .filterDate(agg_start_date, agg_end_date)\ + # Compute average ndvi over the aggregation period + ndvi_img = daily_coll \ + .filterDate(agg_start_date, agg_end_date) \ .mean().select(['ndvi']).float() image_list.append(ndvi_img) if 'count' in variables: - count_img = aggregate_coll\ - .filterDate(agg_start_date, agg_end_date)\ + count_img = aggregate_coll \ + .filterDate(agg_start_date, agg_end_date) \ .select(['mask']).count().rename('count').uint8() image_list.append(count_img) - return ee.Image(image_list)\ - .set(interp_properties)\ + return ee.Image(image_list) \ + .set(interp_properties) \ .set({ 'system:index': ee.Date(agg_start_date).format(date_format), 'system:time_start': ee.Date(agg_start_date).millis(), diff --git a/openet/sims/image.py b/openet/sims/image.py index 2768a14..f67ef9e 100644 --- a/openet/sims/image.py +++ b/openet/sims/image.py @@ -43,10 +43,10 @@ def __init__( crop_type_source='USDA/NASS/CDL', crop_type_remap='CDL', crop_type_kc_flag=False, # CGM - Not sure what to call this parameter yet - etr_source=None, - etr_band=None, - etr_factor=None, - etr_resample=None, + et_reference_source=None, + et_reference_band=None, + et_reference_factor=None, + et_reference_resample=None, mask_non_ag_flag=False, water_kc_flag=True, ): @@ -67,16 +67,16 @@ def __init__( If True, compute Kc using crop type specific coefficients. If False, use generic crop class coefficients. The default is False. - etr_source : str, float, optional + et_reference_source : str, float, optional Reference ET source (the default is None). - Parameter is required if computing 'et' or 'etr'. - etr_band : str, optional + Parameter is required if computing 'et' or 'et_reference'. + et_reference_band : str, optional Reference ET band name (the default is None). - Parameter is required if computing 'et' or 'etr'. - etr_factor : float, None, optional + Parameter is required if computing 'et' or 'et_reference'. + et_reference_factor : float, None, optional Reference ET scaling factor. The default is None which is equivalent to 1.0 (or no scaling). - etr_resample : {'nearest', 'bilinear', 'bicubic', None}, optional + et_reference_resample : {'nearest', 'bilinear', 'bicubic', None}, optional Reference ET resampling. The default is None which is equivalent to nearest neighbor resampling. mask_non_ag_flag : bool, optional @@ -118,19 +118,20 @@ def __init__( self._doy = self._date.getRelative('day', 'year').add(1).int() # Reference ET parameters - self.etr_source = etr_source - self.etr_band = etr_band - self.etr_factor = etr_factor - self.etr_resample = etr_resample + self.et_reference_source = et_reference_source + self.et_reference_band = et_reference_band + self.et_reference_factor = et_reference_factor + self.et_reference_resample = et_reference_resample # Check reference ET parameters - if etr_factor and not utils.is_number(etr_factor): - raise ValueError('etr_factor must be a number') - if etr_factor and self.etr_factor < 0: - raise ValueError('etr_factor must be greater than zero') - etr_resample_methods = ['nearest', 'bilinear', 'bicubic'] - if etr_resample and etr_resample.lower() not in etr_resample_methods: - raise ValueError('unsupported etr_resample method') + if et_reference_factor and not utils.is_number(et_reference_factor): + raise ValueError('et_reference_factor must be a number') + if et_reference_factor and self.et_reference_factor < 0: + raise ValueError('et_reference_factor must be greater than zero') + et_reference_resample_methods = ['nearest', 'bilinear', 'bicubic'] + if (et_reference_resample and \ + et_reference_resample.lower() not in et_reference_resample_methods): + raise ValueError('unsupported et_reference_resample method') # CGM - Model class could inherit these from Image instead of passing them # Could pass time_start instead of separate year and doy @@ -159,10 +160,10 @@ def calculate(self, variables=['et']): for v in variables: if v.lower() == 'et': output_images.append(self.et.float()) - elif v.lower() == 'etr': - output_images.append(self.etr.float()) - elif v.lower() == 'etf': - output_images.append(self.etf.float()) + elif v.lower() == 'et_reference': + output_images.append(self.et_reference.float()) + elif v.lower() == 'et_fraction': + output_images.append(self.et_fraction.float()) # elif v.lower() == 'crop_class': # output_images.append(self.crop_class) # elif v.lower() == 'crop_type': @@ -183,7 +184,7 @@ def calculate(self, variables=['et']): return ee.Image(output_images).set(self._properties) @lazy_property - def etf(self): + def et_fraction(self): """Fraction of reference ET (equivalent to the Kc) This method is basically identical to the "kc" method and is only @@ -195,13 +196,13 @@ def etf(self): ee.Image """ - return self.kc.rename(['etf']).set(self._properties) - # ETf could also be calculated from ET and ETr - # return self.et.divide(self.etr)\ - # .rename(['etf']).set(self._properties) + return self.kc.rename(['et_fraction']).set(self._properties) + # ET fraction could also be calculated from ET and ET reference + # return self.et.divide(self.et_reference)\ + # .rename(['et_fraction']).set(self._properties) @lazy_property - def etr(self): + def et_reference(self): """Reference ET for the image date Returns @@ -209,28 +210,28 @@ def etr(self): ee.Image """ - if utils.is_number(self.etr_source): + if utils.is_number(self.et_reference_source): # Interpret numbers as constant images # CGM - Should we use the ee_types here instead? - # i.e. ee.ee_types.isNumber(self.etr_source) - etr_img = ee.Image.constant(self.etr_source) - elif type(self.etr_source) is str: + # i.e. ee.ee_types.isNumber(self.et_reference_source) + et_reference_img = ee.Image.constant(self.et_reference_source) + elif type(self.et_reference_source) is str: # Assume a string source is an image collection ID (not an image ID) - etr_coll = ee.ImageCollection(self.etr_source)\ - .filterDate(self._start_date, self._end_date)\ - .select([self.etr_band]) - etr_img = ee.Image(etr_coll.first()) - if self.etr_resample in ['bilinear', 'bicubic']: - etr_img = etr_img.resample(self.etr_resample) + et_reference_coll = ee.ImageCollection(self.et_reference_source) \ + .filterDate(self._start_date, self._end_date) \ + .select([self.et_reference_band]) + et_reference_img = ee.Image(et_reference_coll.first()) + if self.et_reference_resample in ['bilinear', 'bicubic']: + et_reference_img = et_reference_img.resample(self.et_reference_resample) else: - raise ValueError('unsupported etr_source: {}'.format( - self.etr_source)) + raise ValueError('unsupported et_reference_source: {}'.format( + self.et_reference_source)) - if self.etr_factor: - etr_img = etr_img.multiply(self.etr_factor) + if self.et_reference_factor: + et_reference_img = et_reference_img.multiply(self.et_reference_factor) - return self.ndvi.multiply(0).add(etr_img)\ - .rename(['etr']).set(self._properties) + return self.ndvi.multiply(0).add(et_reference_img) \ + .rename(['et_reference']).set(self._properties) @lazy_property def et(self): @@ -241,7 +242,7 @@ def et(self): ee.Image """ - return self.kc.multiply(self.etr)\ + return self.kc.multiply(self.et_reference) \ .rename(['et']).set(self._properties) @lazy_property @@ -254,8 +255,8 @@ def crop_class(self): """ # Map the the crop class values to the NDVI image - return self.ndvi.multiply(0)\ - .add(self.model.crop_class)\ + return self.ndvi.multiply(0) \ + .add(self.model.crop_class) \ .rename('crop_class').set(self._properties) @lazy_property @@ -269,8 +270,8 @@ def crop_type(self): """ # Map the the crop class values to the NDVI image # Crop type image ID property is set in model function - return self.ndvi.multiply(0)\ - .add(self.model.crop_type)\ + return self.ndvi.multiply(0) \ + .add(self.model.crop_type) \ .rename(['crop_type']) @lazy_property @@ -282,7 +283,7 @@ def fc(self): ee.Image """ - return self.model.fc(self.ndvi)\ + return self.model.fc(self.ndvi) \ .rename(['fc']).set(self._properties) @lazy_property @@ -294,7 +295,7 @@ def kc(self): ee.Image """ - return self.model.kc(self.ndvi)\ + return self.model.kc(self.ndvi) \ .rename(['kc']).set(self._properties) @lazy_property @@ -308,7 +309,7 @@ def mask(self): ee.Image """ - return self.kc.multiply(0).add(1).updateMask(1)\ + return self.kc.multiply(0).add(1).updateMask(1) \ .rename(['mask']).set(self._properties).uint8() @lazy_property diff --git a/openet/sims/interpolate.py b/openet/sims/interpolate.py new file mode 100644 index 0000000..bcfdbf4 --- /dev/null +++ b/openet/sims/interpolate.py @@ -0,0 +1,321 @@ +import datetime +import logging + +import ee +from dateutil.relativedelta import * + +from . import utils + +import openet.core.interpolate +# TODO: import utils from openet.core +# import openet.core.utils as utils + + +def from_scene_et_fraction(scene_coll, start_date, end_date, variables, + model_args, t_interval='custom', + interp_method='linear', interp_days=32, + _interp_vars=['et_fraction', 'ndvi'], + use_joins=False): + """ + + Parameters + ---------- + scene_coll : ee.ImageCollection + + start_date : str + + end_date : str + + variables : list + List of variables that will be returned in the Image Collection. + model_args : dict + + t_interval : {'daily', 'monthly', 'annual', 'custom'}, optional + Time interval over which to interpolate and aggregate values + The default is 'custom' which means the aggregation time period + will be controlled by the start and end date parameters. + interp_method : {'linear}, optional + Interpolation method. The default is 'linear'. + interp_days : int, str, optional + Number of extra days before the start date and after the end date + to include in the interpolation calculation. The default is 32. + _interp_vars : list, optional + The variables that can be interpolated to daily timesteps. + The default is to interpolate the 'et_fraction' and 'ndvi' bands. + + Returns + ------- + ee.ImageCollection + + Raises + ------ + ValueError + + """ + + # Check that the input parameters are valid + if t_interval.lower() not in ['daily', 'monthly', 'annual', 'custom']: + raise ValueError('unsupported t_interval: {}'.format(t_interval)) + elif interp_method.lower() not in ['linear']: + raise ValueError('unsupported interp_method: {}'.format( + interp_method)) + + if type(interp_days) is str and utils.is_number(interp_days): + interp_days = int(interp_days) + elif not type(interp_days) is int: + raise TypeError('interp_days must be an integer') + elif interp_days <= 0: + raise ValueError('interp_days must be a positive integer') + + if not variables: + raise ValueError('variables parameter must be set') + + # Adjust start/end dates based on t_interval + # Increase the date range to fully include the time interval + start_dt = datetime.datetime.strptime(start_date, '%Y-%m-%d') + end_dt = datetime.datetime.strptime(end_date, '%Y-%m-%d') + if t_interval.lower() == 'annual': + start_dt = datetime.datetime(start_dt.year, 1, 1) + # Covert end date to inclusive, flatten to beginning of year, + # then add a year which will make it exclusive + end_dt -= relativedelta(days=+1) + end_dt = datetime.datetime(end_dt.year, 1, 1) + end_dt += relativedelta(years=+1) + elif t_interval.lower() == 'monthly': + start_dt = datetime.datetime(start_dt.year, start_dt.month, 1) + end_dt -= relativedelta(days=+1) + end_dt = datetime.datetime(end_dt.year, end_dt.month, 1) + end_dt += relativedelta(months=+1) + start_date = start_dt.strftime('%Y-%m-%d') + end_date = end_dt.strftime('%Y-%m-%d') + + # The start/end date for the interpolation include more days + # (+/- interp_days) than are included in the ETr collection + interp_start_dt = start_dt - datetime.timedelta(days=interp_days) + interp_end_dt = end_dt + datetime.timedelta(days=interp_days) + interp_start_date = interp_start_dt.date().isoformat() + interp_end_date = interp_end_dt.date().isoformat() + + # Get reference ET source + if 'et_reference_source' in model_args.keys(): + et_reference_source = model_args['et_reference_source'] + else: + raise ValueError('et_reference_source was not set') + + # Get reference ET band name + if 'et_reference_band' in model_args.keys(): + et_reference_band = model_args['et_reference_band'] + else: + raise ValueError('et_reference_band was not set') + + # Get reference ET factor + if 'et_reference_factor' in model_args.keys(): + et_reference_factor = model_args['et_reference_factor'] + else: + et_reference_factor = 1.0 + logging.debug('et_reference_factor was not set, default to 1.0') + # raise ValueError('et_reference_factor was not set') + + # Get reference ET resample + if 'et_reference_resample' in model_args.keys(): + et_reference_resample = model_args['et_reference_resample'] + else: + et_reference_resample = 'nearest' + logging.debug( + 'et_reference_resample was not set, default to nearest') + # raise ValueError('et_reference_resample was not set') + + if type(et_reference_source) is str: + # Assume a string source is an single image collection ID + # not an list of collection IDs or ee.ImageCollection + daily_et_reference_coll = ee.ImageCollection(et_reference_source) \ + .filterDate(start_date, end_date) \ + .select([et_reference_band], ['et_reference']) + # elif isinstance(et_reference_source, computedobject.ComputedObject): + # # Interpret computed objects as image collections + # daily_et_reference_coll = ee.ImageCollection(et_reference_source)\ + # .select([et_reference_band])\ + # .filterDate(self.start_date, self.end_date) + else: + raise ValueError('unsupported et_reference_source: {}'.format( + et_reference_source)) + + # TODO: Need to add time and mask to the scene collection + # The time band is always needed for interpolation + interp_vars = _interp_vars + ['time'] + + # DEADBEEF - I don't think this is needed since interp_vars is hardcoded + # # Initialize variable list to only variables that can be interpolated + # interp_vars = list(set(interp_vars) & set(variables)) + # + # # To return ET, the ETf must be interpolated + # if 'et' in variables and 'et_fraction' not in interp_vars: + # interp_vars.append('et_fraction') + # + # # With the current interpolate.daily() function, + # # something has to be interpolated in order to return et_reference + # if 'et_reference' in variables and 'et_fraction' not in interp_vars: + # interp_vars.append('et_fraction') + + # Filter scene collection to the interpolation range + # This probably isn't needed since scene_coll was built to this range + # scene_coll = scene_coll.filterDate(interp_start_date, interp_end_date) + + # For count, compute the composite/mosaic image for the mask band only + if 'count' in variables: + aggregate_coll = openet.core.interpolate.aggregate_daily( + image_coll = scene_coll.select(['mask']), + start_date=start_date, end_date=end_date) + # The following is needed because the aggregate collection can be + # empty if there are no scenes in the target date range but there + # are scenes in the interpolation date range. + # Without this the count image will not be built but the other + # bands will be which causes a non-homogeneous image collection. + aggregate_coll = aggregate_coll.merge( + ee.Image.constant(0).rename(['mask']) + .set({'system:time_start': ee.Date(start_date).millis()})) + + # Interpolate to a daily time step + # NOTE: the daily function is not computing ET (ETf x ETr) + # but is returning the target (ETr) band + daily_coll = openet.core.interpolate.daily( + target_coll=daily_et_reference_coll, + source_coll=scene_coll.select(interp_vars), + interp_method=interp_method, interp_days=interp_days, + use_joins=use_joins, + ) + + # Compute ET from ETf and ETr (if necessary) + # The check for et_fraction is needed since it is back computed from ET and ETr + # if 'et' in variables or 'et_fraction' in variables: + def compute_et(img): + """This function assumes ETr and ETf are present""" + et_img = img.select(['et_fraction']).multiply( + img.select(['et_reference'])) + return img.addBands(et_img.double().rename('et')) + + daily_coll = daily_coll.map(compute_et) + + def aggregate_image(agg_start_date, agg_end_date, date_format): + """Aggregate the daily images within the target date range + + Parameters + ---------- + agg_start_date: str + Start date (inclusive). + agg_end_date : str + End date (exclusive). + date_format : str + Date format for system:index (uses EE JODA format). + + Returns + ------- + ee.Image + + Notes + ----- + Since this function takes multiple inputs it is being called + for each time interval by separate mappable functions + + """ + # if 'et' in variables or 'et_fraction' in variables: + et_img = daily_coll.filterDate(agg_start_date, agg_end_date) \ + .select(['et']).sum() + # if 'et_reference' in variables or 'et_fraction' in variables: + et_reference_img = daily_coll.filterDate(agg_start_date, + agg_end_date) \ + .select(['et_reference']).sum() + + if et_reference_factor: + et_img = et_img.multiply(et_reference_factor) + et_reference_img = et_reference_img.multiply( + et_reference_factor) + + # DEADBEEF - This doesn't seem to be doing anything + if et_reference_resample in ['bilinear', 'bicubic']: + et_reference_img = et_reference_img.resample( + et_reference_resample) + + image_list = [] + if 'et' in variables: + image_list.append(et_img.float()) + if 'et_reference' in variables: + image_list.append(et_reference_img.float()) + if 'et_fraction' in variables: + # Compute average et fraction over the aggregation period + image_list.append( + et_img.divide(et_reference_img).rename( + ['et_fraction']).float()) + if 'ndvi' in variables: + # Compute average ndvi over the aggregation period + ndvi_img = daily_coll \ + .filterDate(agg_start_date, agg_end_date) \ + .mean().select(['ndvi']).float() + image_list.append(ndvi_img) + if 'count' in variables: + count_img = aggregate_coll \ + .filterDate(agg_start_date, agg_end_date) \ + .select(['mask']).sum().rename('count').uint8() + image_list.append(count_img) + + return ee.Image(image_list) \ + .set({ + 'system:index': ee.Date(agg_start_date).format(date_format), + 'system:time_start': ee.Date(agg_start_date).millis()}) + # .set(interp_properties)\ + + # Combine input, interpolated, and derived values + if t_interval.lower() == 'daily': + def agg_daily(daily_img): + # CGM - Double check that this time_start is a 0 UTC time. + # It should be since it is coming from the interpolate source + # collection, but what if source is GRIDMET (+6 UTC)? + agg_start_date = ee.Date(daily_img.get('system:time_start')) + # CGM - This calls .sum() on collections with only one image + return aggregate_image( + agg_start_date=agg_start_date, + agg_end_date=ee.Date(agg_start_date).advance(1, 'day'), + date_format='YYYYMMdd') + + return ee.ImageCollection(daily_coll.map(agg_daily)) + + elif t_interval.lower() == 'monthly': + def month_gen(iter_start_dt, iter_end_dt): + iter_dt = iter_start_dt + # Conditional is "less than" because end date is exclusive + while iter_dt < iter_end_dt: + yield iter_dt.strftime('%Y-%m-%d') + iter_dt += relativedelta(months=+1) + + month_list = ee.List(list(month_gen(start_dt, end_dt))) + + def agg_monthly(agg_start_date): + return aggregate_image( + agg_start_date=agg_start_date, + agg_end_date=ee.Date(agg_start_date).advance(1, 'month'), + date_format='YYYYMM') + + return ee.ImageCollection(month_list.map(agg_monthly)) + + elif t_interval.lower() == 'annual': + def year_gen(iter_start_dt, iter_end_dt): + iter_dt = iter_start_dt + while iter_dt < iter_end_dt: + yield iter_dt.strftime('%Y-%m-%d') + iter_dt += relativedelta(years=+1) + + year_list = ee.List(list(year_gen(start_dt, end_dt))) + + def agg_annual(agg_start_date): + return aggregate_image( + agg_start_date=agg_start_date, + agg_end_date=ee.Date(agg_start_date).advance(1, 'year'), + date_format='YYYY') + + return ee.ImageCollection(year_list.map(agg_annual)) + + elif t_interval.lower() == 'custom': + # Returning an ImageCollection to be consistent + return ee.ImageCollection(aggregate_image( + agg_start_date=start_date, agg_end_date=end_date, + date_format='YYYYMMdd')) diff --git a/openet/sims/model.py b/openet/sims/model.py index 9b5da58..b2edf4f 100644 --- a/openet/sims/model.py +++ b/openet/sims/model.py @@ -220,7 +220,7 @@ def _crop_type(self): if utils.is_number(self.crop_type_source): # Interpret numbers as constant images # CGM - Should we use the ee_types here instead? - # i.e. ee.ee_types.isNumber(self.etr_source) + # i.e. ee.ee_types.isNumber(self.et_reference_source) crop_type_img = ee.Image.constant(self.crop_type_source) # .rename('crop_type') # properties = properties.set('id', 'constant') @@ -252,7 +252,10 @@ def _crop_type(self): properties = properties.set('id', crop_type_img.get('system:id')) elif (type(self.crop_type_source) is str and - self.crop_type_source.lower() == 'projects/openet/crop_type'): + self.crop_type_source.lower() in [ + 'projects/openet/crop_type', + 'projects/openet/assets/crop_type', + 'projects/earthengine-legacy/assets/projects/openet/crop_type']): # Use the crop_type image closest to the image date # Hard coding the year range but it could be computed dynamically year_min = ee.Number(2016) diff --git a/openet/sims/tests/conftest.py b/openet/sims/tests/conftest.py index caa69f1..8001996 100644 --- a/openet/sims/tests/conftest.py +++ b/openet/sims/tests/conftest.py @@ -10,20 +10,20 @@ def pytest_configure(): # Called before tests are collected # https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_sessionstart logging.basicConfig(level=logging.DEBUG, format='%(message)s') + logging.getLogger('googleapiclient').setLevel(logging.ERROR) logging.debug('Test Setup') # On Travis-CI authenticate using private key environment variable if 'EE_PRIVATE_KEY_B64' in os.environ: print('Writing privatekey.json from environmental variable ...') content = base64.b64decode(os.environ['EE_PRIVATE_KEY_B64']).decode('ascii') - EE_PRIVATE_KEY_FILE = 'privatekey.json' - with open(EE_PRIVATE_KEY_FILE, 'w') as f: + GEE_KEY_FILE = 'privatekey.json' + with open(GEE_KEY_FILE, 'w') as f: f.write(content) - EE_CREDENTIALS = ee.ServiceAccountCredentials( - '', key_file=EE_PRIVATE_KEY_FILE) - ee.Initialize(EE_CREDENTIALS) + ee.Initialize(ee.ServiceAccountCredentials('', key_file=GEE_KEY_FILE), + use_cloud_api=True) else: - ee.Initialize() + ee.Initialize(use_cloud_api=True) @pytest.fixture(scope="session", autouse=True) diff --git a/openet/sims/tests/test_a_utils.py b/openet/sims/tests/test_a_utils.py index 8bccc46..542a255 100644 --- a/openet/sims/tests/test_a_utils.py +++ b/openet/sims/tests/test_a_utils.py @@ -52,7 +52,7 @@ def test_point_image_value(tol=0.001): assert abs(output['elevation'] - expected) <= tol -def point_coll_value(tol=0.001): +def test_point_coll_value(tol=0.001): expected = 2364.351 output = utils.point_coll_value( ee.ImageCollection([ee.Image('USGS/NED')]), [-106.03249, 37.17777]) diff --git a/openet/sims/tests/test_c_image.py b/openet/sims/tests/test_c_image.py index 7761c1d..02256d6 100644 --- a/openet/sims/tests/test_c_image.py +++ b/openet/sims/tests/test_c_image.py @@ -37,18 +37,19 @@ def default_image(ndvi=0.8): }) -# Setting etr_source and etr_band on the default image to simplify testing -# but these do not have defaults in the Image class init -def default_image_args(ndvi=0.8, etr_source='IDAHO_EPSCOR/GRIDMET', - etr_band='etr', etr_factor=0.85, etr_resample='nearest', +# Setting et_reference_source and et_reference_band on the default image to +# simplify testing but these do not have defaults in the Image class init +def default_image_args(ndvi=0.8, et_reference_source='IDAHO_EPSCOR/GRIDMET', + et_reference_band='etr', et_reference_factor=0.85, + et_reference_resample='nearest', crop_type_source='USDA/NASS/CDL', crop_type_remap='CDL', crop_type_kc_flag=False, mask_non_ag_flag=False): return { 'image': default_image(ndvi=ndvi), - 'etr_source': etr_source, - 'etr_band': etr_band, - 'etr_factor': etr_factor, - 'etr_resample': etr_resample, + 'et_reference_source': et_reference_source, + 'et_reference_band': et_reference_band, + 'et_reference_factor': et_reference_factor, + 'et_reference_resample': et_reference_resample, 'crop_type_source': crop_type_source, 'crop_type_remap': crop_type_remap, 'crop_type_kc_flag': crop_type_kc_flag, @@ -56,24 +57,30 @@ def default_image_args(ndvi=0.8, etr_source='IDAHO_EPSCOR/GRIDMET', } -def default_image_obj(ndvi=0.8, etr_source='IDAHO_EPSCOR/GRIDMET', - etr_band='etr', etr_factor=0.85, etr_resample='nearest', +def default_image_obj(ndvi=0.8, et_reference_source='IDAHO_EPSCOR/GRIDMET', + et_reference_band='etr', et_reference_factor=0.85, + et_reference_resample='nearest', crop_type_source='USDA/NASS/CDL', crop_type_remap='CDL', crop_type_kc_flag=False, mask_non_ag_flag=False): return sims.Image(**default_image_args( - ndvi=ndvi, etr_source=etr_source, etr_band=etr_band, - etr_factor=etr_factor, etr_resample=etr_resample, - crop_type_source=crop_type_source, crop_type_remap=crop_type_remap, - crop_type_kc_flag=crop_type_kc_flag, mask_non_ag_flag=mask_non_ag_flag, + ndvi=ndvi, + et_reference_source=et_reference_source, + et_reference_band=et_reference_band, + et_reference_factor=et_reference_factor, + et_reference_resample=et_reference_resample, + crop_type_source=crop_type_source, + crop_type_remap=crop_type_remap, + crop_type_kc_flag=crop_type_kc_flag, + mask_non_ag_flag=mask_non_ag_flag, )) def test_Image_init_default_parameters(): m = sims.Image(image=default_image()) - assert m.etr_source == None - assert m.etr_band == None - assert m.etr_factor == None - assert m.etr_resample == None + assert m.et_reference_source == None + assert m.et_reference_band == None + assert m.et_reference_factor == None + assert m.et_reference_resample == None # assert m.crop_type_source == 'USDA/NASS/CDL' # assert m.crop_type_remap == 'CDL' # assert m.crop_type_kc_flag == False @@ -210,48 +217,49 @@ def test_Image_kc_properties(): assert output['properties']['image_id'] == COLL_ID + SCENE_ID -def test_Image_etf_properties(): - """Test if properties are set on the ETf image""" - output = utils.getinfo(default_image_obj().etf) - assert output['bands'][0]['id'] == 'etf' +def test_Image_et_fraction_properties(): + """Test if properties are set on the ET fraction image""" + output = utils.getinfo(default_image_obj().et_fraction) + assert output['bands'][0]['id'] == 'et_fraction' assert output['properties']['system:index'] == SCENE_ID assert output['properties']['system:time_start'] == SCENE_TIME assert output['properties']['image_id'] == COLL_ID + SCENE_ID -def test_Image_etf_constant_value(): - # ETf method returns Kc +def test_Image_et_fraction_constant_value(): + # ET fraction method returns Kc output = utils.constant_image_value(default_image_obj( - ndvi=0.8, crop_type_source=1).etf) - assert abs(output['etf'] - 0.9859994736) <= 0.0001 + ndvi=0.8, crop_type_source=1).et_fraction) + assert abs(output['et_fraction'] - 0.9859994736) <= 0.0001 -def test_Image_etr_constant_value(etr=10.0, tol=0.0001): +def test_Image_et_reference_constant_value(et_reference=10.0, tol=0.0001): output = utils.constant_image_value(default_image_obj( - etr_source=etr, etr_factor=0.85).etr) - assert abs(output['etr'] - etr * 0.85) <= tol + et_reference_source=et_reference, + et_reference_factor=0.85).et_reference) + assert abs(output['et_reference'] - et_reference * 0.85) <= tol -def test_Image_etr_properties(): - """Test if properties are set on the ETr image""" - output = utils.getinfo(default_image_obj().etr) - assert output['bands'][0]['id'] == 'etr' +def test_Image_et_reference_properties(): + """Test if properties are set on the reference ET image""" + output = utils.getinfo(default_image_obj().et_reference) + assert output['bands'][0]['id'] == 'et_reference' assert output['properties']['system:index'] == SCENE_ID assert output['properties']['system:time_start'] == SCENE_TIME assert output['properties']['image_id'] == COLL_ID + SCENE_ID -def test_Image_etr_source_exception(): +def test_Image_et_reference_source_exception(): """Test that an Exception is raise for an invalid image ID""" with pytest.raises(Exception): - utils.getinfo(default_image_obj(etr_source=None).etr) + utils.getinfo(default_image_obj(et_reference_source=None).et_reference) # CGM - I'm not sure why this is commented out -# def test_Image_etr_band_exception(): -# """Test that an Exception is raise for an invalid etr band name""" +# def test_Image_et_reference_band_exception(): +# """Test that an Exception is raise for an invalid et_reference band name""" # with pytest.raises(Exception): -# utils.getinfo(default_image_obj(etr_band=None).etr) +# utils.getinfo(default_image_obj(et_reference_band=None).et_reference) def test_Image_et_properties(): @@ -265,7 +273,7 @@ def test_Image_et_properties(): def test_Image_et_constant_value(): output = utils.constant_image_value(default_image_obj( - etr_source=10, etr_factor=1.0, crop_type_source=1).et) + et_reference_source=10, et_reference_factor=1.0, crop_type_source=1).et) assert abs(output['et'] - 10 * 0.986) <= 0.0001 @@ -311,8 +319,8 @@ def test_Image_calculate_variables_custom(): def test_Image_calculate_variables_all(): - variables = ['et', 'etf', 'etr', 'fc', 'kc', 'mask', 'ndvi', 'time'] - # variables = ['et', 'etr', 'fc', 'kc', 'crop_type', 'mask', 'ndvi', 'time'] + variables = ['et', 'et_fraction', 'et_reference', 'fc', 'kc', 'mask', + 'ndvi', 'time'] output = utils.getinfo(default_image_obj().calculate(variables=variables)) assert set([x['id'] for x in output['bands']]) == set(variables) @@ -347,7 +355,7 @@ def test_Image_from_landsat_c1_sr_image(): def test_Image_from_landsat_c1_sr_kc(): - """Test if ETf can be built from a Landsat images""" + """Test if ET fraction can be built from a Landsat images""" image_id = 'LANDSAT/LC08/C01/T1_SR/LC08_044033_20170716' output = utils.getinfo(sims.Image.from_landsat_c1_sr(image_id).kc) assert output['properties']['system:index'] == image_id.split('/')[-1] @@ -357,7 +365,8 @@ def test_Image_from_landsat_c1_sr_et(): """Test if ET can be built from a Landsat images""" image_id = 'LANDSAT/LC08/C01/T1_SR/LC08_044033_20170716' output = utils.getinfo(sims.Image.from_landsat_c1_sr( - image_id, etr_source='IDAHO_EPSCOR/GRIDMET', etr_band='etr').et) + image_id, et_reference_source='IDAHO_EPSCOR/GRIDMET', + et_reference_band='etr').et) assert output['properties']['system:index'] == image_id.split('/')[-1] @@ -384,4 +393,4 @@ def test_Image_from_method_kwargs(): """Test that the init parameters can be passed through the helper methods""" assert sims.Image.from_landsat_c1_sr( 'LANDSAT/LC08/C01/T1_SR/LC08_042035_20150713', - etr_band='FOO').etr_band == 'FOO' + et_reference_band='FOO').et_reference_band == 'FOO' diff --git a/openet/sims/tests/test_d_collection.py b/openet/sims/tests/test_d_collection.py index 95e0139..bb8c3e4 100644 --- a/openet/sims/tests/test_d_collection.py +++ b/openet/sims/tests/test_d_collection.py @@ -16,15 +16,15 @@ END_DATE = '2017-08-01' SCENE_GEOM = (-121.91, 38.99, -121.89, 39.01) SCENE_POINT = (-121.9, 39) -VARIABLES = {'et', 'etf', 'etr'} +VARIABLES = {'et', 'et_fraction', 'et_reference'} TEST_POINT = (-121.5265, 38.7399) default_coll_args = { 'collections': COLLECTIONS, 'geometry': ee.Geometry.Point(SCENE_POINT), 'start_date': START_DATE, 'end_date': END_DATE, 'variables': list(VARIABLES), 'cloud_cover_max': 70, - 'etr_source': 'IDAHO_EPSCOR/GRIDMET', 'etr_band': 'etr', - 'etr_factor': 0.85, 'etr_resample': 'nearest', + 'et_reference_source': 'IDAHO_EPSCOR/GRIDMET', 'et_reference_band': 'etr', + 'et_reference_factor': 0.85, 'et_reference_resample': 'nearest', 'model_args': {}, 'filter_args': {}, } @@ -44,22 +44,22 @@ def test_Collection_init_default_parameters(): """Test if init sets default parameters""" args = default_coll_args.copy() # These values are being set above but have defaults that need to be checked - del args['etr_source'] - del args['etr_band'] - del args['etr_factor'] - del args['etr_resample'] + del args['et_reference_source'] + del args['et_reference_band'] + del args['et_reference_factor'] + del args['et_reference_resample'] del args['variables'] m = sims.Collection(**args) assert m.variables == None - assert m.etr_source == None - assert m.etr_band == None - assert m.etr_factor == None - assert m.etr_resample == None + assert m.et_reference_source == None + assert m.et_reference_band == None + assert m.et_reference_factor == None + assert m.et_reference_resample == None assert m.cloud_cover_max == 70 assert m.model_args == {} assert m.filter_args == {} - assert m._interp_vars == ['ndvi', 'etf'] + assert m._interp_vars == ['ndvi', 'et_fraction'] def test_Collection_init_collection_str(coll_id='LANDSAT/LC08/C01/T1_SR'): @@ -310,81 +310,84 @@ def test_Collection_interpolate_t_interval_custom(): # NOTE: For the following tests the collection class is not being # re-instantiated for each test so it is necessary to clear the model_args -def test_Collection_interpolate_etr_source_not_set(): - """Test if Exception is raised if etr_source is not set""" +def test_Collection_interpolate_et_reference_source_not_set(): + """Test if Exception is raised if et_reference_source is not set""" with pytest.raises(ValueError): utils.getinfo(default_coll_obj( - etr_source=None, model_args={}).interpolate()) + et_reference_source=None, model_args={}).interpolate()) -def test_Collection_interpolate_etr_band_not_set(): - """Test if Exception is raised if etr_band is not set""" +def test_Collection_interpolate_et_reference_band_not_set(): + """Test if Exception is raised if et_reference_band is not set""" with pytest.raises(ValueError): utils.getinfo(default_coll_obj( - etr_band=None, model_args={}).interpolate()) + et_reference_band=None, model_args={}).interpolate()) -def test_Collection_interpolate_etr_factor_not_set(): - """Test if Exception is raised if etr_factor is not set""" +def test_Collection_interpolate_et_reference_factor_not_set(): + """Test if Exception is raised if et_reference_factor is not set""" with pytest.raises(ValueError): utils.getinfo(default_coll_obj( - etr_factor=None, model_args={}).interpolate()) + et_reference_factor=None, model_args={}).interpolate()) -def test_Collection_interpolate_etr_factor_exception(): - """Test if Exception is raised if etr_factor is not a number or negative""" +def test_Collection_interpolate_et_reference_factor_exception(): + """Test if Exception is raised if et_reference_factor is not a number or negative""" with pytest.raises(ValueError): utils.getinfo(default_coll_obj( - etr_factor=-1, model_args={}).interpolate()) + et_reference_factor=-1, model_args={}).interpolate()) -def test_Collection_interpolate_etr_resample_not_set(): - """Test if Exception is raised if etr_resample is not set""" +def test_Collection_interpolate_et_reference_resample_not_set(): + """Test if Exception is raised if et_reference_resample is not set""" with pytest.raises(ValueError): utils.getinfo(default_coll_obj( - etr_resample=None, model_args={}).interpolate()) + et_reference_resample=None, model_args={}).interpolate()) -def test_Collection_interpolate_etr_resample_exception(): - """Test if Exception is raised if etr_resample is not set""" +def test_Collection_interpolate_et_reference_resample_exception(): + """Test if Exception is raised if et_reference_resample is not set""" with pytest.raises(ValueError): utils.getinfo(default_coll_obj( - etr_resample='deadbeef', model_args={}).interpolate()) + et_reference_resample='deadbeef', model_args={}).interpolate()) -def test_Collection_interpolate_etr_params_kwargs(): - """Test setting etr parameters in the Collection init args""" +def test_Collection_interpolate_et_reference_params_kwargs(): + """Test setting et_reference parameters in the Collection init args""" output = utils.getinfo(default_coll_obj( - etr_source='IDAHO_EPSCOR/GRIDMET', etr_band='etr', - etr_factor=0.5, etr_resample='bilinear', model_args={}).interpolate()) + et_reference_source='IDAHO_EPSCOR/GRIDMET', et_reference_band='etr', + et_reference_factor=0.5, et_reference_resample='bilinear', + model_args={}).interpolate()) assert {y['id'] for x in output['features'] for y in x['bands']} == VARIABLES - assert output['features'][0]['properties']['etr_factor'] == 0.5 - assert output['features'][0]['properties']['etr_resample'] == 'bilinear' + assert output['features'][0]['properties']['et_reference_factor'] == 0.5 + assert output['features'][0]['properties']['et_reference_resample'] == 'bilinear' -def test_Collection_interpolate_etr_params_model_args(): - """Test setting etr parameters in the model_args""" +def test_Collection_interpolate_et_reference_params_model_args(): + """Test setting et_reference parameters in the model_args""" output = utils.getinfo(default_coll_obj( - etr_source=None, etr_band=None, etr_factor=None, etr_resample=None, - model_args={'etr_source': 'IDAHO_EPSCOR/GRIDMET', - 'etr_band': 'etr', 'etr_factor': 0.5, - 'etr_resample': 'bilinear'}).interpolate()) + et_reference_source=None, et_reference_band=None, + et_reference_factor=None, et_reference_resample=None, + model_args={'et_reference_source': 'IDAHO_EPSCOR/GRIDMET', + 'et_reference_band': 'etr', 'et_reference_factor': 0.5, + 'et_reference_resample': 'bilinear'}).interpolate()) assert {y['id'] for x in output['features'] for y in x['bands']} == VARIABLES - assert output['features'][0]['properties']['etr_factor'] == 0.5 - assert output['features'][0]['properties']['etr_resample'] == 'bilinear' + assert output['features'][0]['properties']['et_reference_factor'] == 0.5 + assert output['features'][0]['properties']['et_reference_resample'] == 'bilinear' -def test_Collection_interpolate_etr_params_interpolate_args(): - """Test setting etr parameters in the interpolate call""" - etr_args = {'etr_source': 'IDAHO_EPSCOR/GRIDMET', - 'etr_band': 'etr', 'etr_factor': 0.5, - 'etr_resample': 'bilinear'} +def test_Collection_interpolate_et_reference_params_interpolate_args(): + """Test setting et_reference parameters in the interpolate call""" + et_reference_args = {'et_reference_source': 'IDAHO_EPSCOR/GRIDMET', + 'et_reference_band': 'etr', 'et_reference_factor': 0.5, + 'et_reference_resample': 'bilinear'} output = utils.getinfo(default_coll_obj( - etr_source=None, etr_band=None, etr_factor=None, etr_resample=None, - model_args={}).interpolate(**etr_args)) + et_reference_source=None, et_reference_band=None, + et_reference_factor=None, et_reference_resample=None, + model_args={}).interpolate(**et_reference_args)) assert {y['id'] for x in output['features'] for y in x['bands']} == VARIABLES - assert output['features'][0]['properties']['etr_factor'] == 0.5 - assert output['features'][0]['properties']['etr_resample'] == 'bilinear' + assert output['features'][0]['properties']['et_reference_factor'] == 0.5 + assert output['features'][0]['properties']['et_reference_resample'] == 'bilinear' def test_Collection_interpolate_t_interval_exception(): @@ -455,12 +458,13 @@ def test_Collection_interpolate_no_variables_exception(): # args['start_date'] = '2017-06-30' # output = utils.point_coll_value( # sims.Collection(**args).overpass( -# variables=['kc', 'etr']), xy=TEST_POINT, scale=30) +# variables=['kc', 'et_reference']), xy=TEST_POINT, scale=30) # pprint.pprint(output) # # output = utils.point_coll_value( # sims.Collection(**args).interpolate( -# variables=['kc', 'etr'], t_interval='daily', interp_days=32), +# variables=['kc', 'et_reference'], t_interval='daily', +# interp_days=32), # xy=TEST_POINT, scale=30) # pprint.pprint(output) # assert False @@ -468,51 +472,32 @@ def test_Collection_interpolate_no_variables_exception(): def test_Collection_interpolate_output_type_default(): """Test if output_type parameter is defaulting to float""" - output = utils.getinfo(default_coll_obj( - variables=['et', 'etr', 'etf', 'ndvi', 'count']).interpolate()) + test_vars = ['et', 'et_reference', 'et_fraction', 'ndvi', 'count'] + output = utils.getinfo(default_coll_obj(variables=test_vars).interpolate()) output = output['features'][0]['bands'] bands = {info['id']: i for i, info in enumerate(output)} assert(output[bands['et']]['data_type']['precision'] == 'float') - assert(output[bands['etr']]['data_type']['precision'] == 'float') - assert(output[bands['etf']]['data_type']['precision'] == 'float') + assert(output[bands['et_reference']]['data_type']['precision'] == 'float') + assert(output[bands['et_fraction']]['data_type']['precision'] == 'float') assert(output[bands['ndvi']]['data_type']['precision'] == 'float') assert(output[bands['count']]['data_type']['precision'] == 'int') -@pytest.mark.parametrize( - 'output_type, precision, max_value', - [ - ['int8', 'int', 127], - ['uint8', 'int', 255], - ['int16', 'int', 32767], - ['uint16', 'int', 65535], - ['float', 'float', None], - ['double', 'double', None], - ] -) -def test_Collection_interpolate_output_type_parameter(output_type, precision, max_value): - """Test if changing the output_type parameter works""" - output = utils.getinfo(default_coll_obj().interpolate(output_type=output_type)) - output = output['features'][0]['bands'] - bands = {info['id']: i for i, info in enumerate(output)} - - assert(output[bands['et']]['data_type']['precision'] == precision) - assert(output[bands['etr']]['data_type']['precision'] == precision) - - if max_value is not None: - assert(output[bands['et']]['data_type']['max'] == max_value) - assert(output[bands['etr']]['data_type']['max'] == max_value) - - -def test_Collection_interpolate_output_type_exception(): - """Test if Exception is raised for an invalid interp_method parameter""" - with pytest.raises(ValueError): - utils.getinfo(default_coll_obj().interpolate(output_type='DEADBEEF')) - - def test_Collection_interpolate_custom_model_args(): """Test passing in a model specific parameter through model_args""" model_args = {'crop_type_source': 'projects/openet/crop_type'} output = utils.getinfo(default_coll_obj(model_args=model_args).interpolate()) output = output['features'][0]['properties'] assert output['crop_type_source'] == 'projects/openet/crop_type' + + +def test_Collection_interpolate_only_interpolate_images(): + """Test if count band is returned if no images in the date range""" + variables = {'et', 'count'} + output = utils.getinfo(default_coll_obj( + collections=['LANDSAT/LC08/C01/T1_SR'], + geometry=ee.Geometry.Point(-123.623, 44.745), + start_date='2017-04-01', end_date='2017-04-30', + variables=list(variables), cloud_cover_max=70).interpolate()) + pprint.pprint(output) + assert {y['id'] for x in output['features'] for y in x['bands']} == variables \ No newline at end of file diff --git a/openet/sims/tests/test_d_interpolate.py b/openet/sims/tests/test_d_interpolate.py new file mode 100644 index 0000000..25977c3 --- /dev/null +++ b/openet/sims/tests/test_d_interpolate.py @@ -0,0 +1,54 @@ +import pprint + +import ee +import pytest + +import openet.sims.interpolate as interpolate +import openet.sims.utils as utils + + +def test_from_scene_et_fraction_values(): + img = ee.Image('LANDSAT/LC08/C01/T1_TOA/LC08_044033_20170716') \ + .select(['B2']).double().multiply(0) + mask = img.add(1).updateMask(1).uint8() + + time1 = ee.Number(ee.Date.fromYMD(2017, 7, 8).millis()) + time2 = ee.Number(ee.Date.fromYMD(2017, 7, 16).millis()) + time3 = ee.Number(ee.Date.fromYMD(2017, 7, 24).millis()) + + # Mask and time bands currently get added on to the scene collection + # and images are unscaled just before interpolating + scene_coll = ee.ImageCollection([ + ee.Image([img.add(0.4), img.add(0.5), img.add(time1), mask]) \ + .rename(['et_fraction', 'ndvi', 'time', 'mask']) \ + .set({'system:index': 'LE07_044033_20170708', + 'system:time_start': time1}), + ee.Image([img.add(0.4), img.add(0.5), img.add(time2), mask]) \ + .rename(['et_fraction', 'ndvi', 'time', 'mask']) \ + .set({'system:index': 'LC08_044033_20170716', + 'system:time_start': time2}), + ee.Image([img.add(0.4), img.add(0.5), img.add(time3), mask]) \ + .rename(['et_fraction', 'ndvi', 'time', 'mask']) \ + .set({'system:index': 'LE07_044033_20170724', + 'system:time_start': time3}), + ]) + + etf_coll = interpolate.from_scene_et_fraction( + scene_coll, + start_date='2017-07-01', end_date='2017-07-31', + variables=['et', 'et_reference', 'et_fraction', 'ndvi', 'count'], + model_args={'et_reference_source': 'IDAHO_EPSCOR/GRIDMET', + 'et_reference_band': 'etr'}, + t_interval='monthly', interp_method='linear', interp_days=32) + + TEST_POINT = (-121.5265, 38.7399) + output = utils.point_coll_value(etf_coll, TEST_POINT, scale=10) + # pprint.pprint(output) + + tol = 0.0001 + # print(output) + assert abs(output['ndvi']['2017-07-01'] - 0.5) <= tol + assert abs(output['et_fraction']['2017-07-01'] - 0.4) <= tol + assert abs(output['et_reference']['2017-07-01'] - 303.622559) <= tol + assert abs(output['et']['2017-07-01'] - (303.622559 * 0.4)) <= tol + assert output['count']['2017-07-01'] == 3 diff --git a/requirements.txt b/requirements.txt index 999363a..b865680 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -earthengine-api>=0.1.185 -openet-core>=0.0.10 +earthengine-api>=0.1.204 +openet-core>=0.0.14 python-dateutil