invest/tests/test_seasonal_water_yield_r...

1397 lines
56 KiB
Python

"""InVEST Seasonal water yield model tests that use the InVEST sample data."""
import os
import shutil
import tempfile
import unittest
import numpy
import pygeoprocessing
from osgeo import gdal
from osgeo import ogr
from osgeo import osr
gdal.UseExceptions()
REGRESSION_DATA = os.path.join(
os.path.dirname(__file__), '..', 'data', 'invest-test-data',
'seasonal_water_yield')
def make_simple_shp(base_shp_path, origin):
"""Make a 100x100 ogr rectangular geometry shapefile.
Args:
base_shp_path (str): path to the shapefile.
Returns:
None.
"""
# Create a new shapefile
driver = ogr.GetDriverByName('ESRI Shapefile')
data_source = driver.CreateDataSource(base_shp_path)
srs = osr.SpatialReference()
srs.ImportFromEPSG(26910) # Spatial reference UTM Zone 10N
layer = data_source.CreateLayer('layer', srs, ogr.wkbPolygon)
# Add an FID field to the layer
field_name = 'FID'
field = ogr.FieldDefn(field_name)
layer.CreateField(field)
# Create a rectangular geometry
lon, lat = origin[0], origin[1]
width = 100
rect = ogr.Geometry(ogr.wkbLinearRing)
rect.AddPoint(lon, lat)
rect.AddPoint(lon + width, lat)
rect.AddPoint(lon + width, lat - width)
rect.AddPoint(lon, lat - width)
rect.AddPoint(lon, lat)
# Create the feature from the geometry
poly = ogr.Geometry(ogr.wkbPolygon)
poly.AddGeometry(rect)
feature = ogr.Feature(layer.GetLayerDefn())
feature.SetField(field_name, '1')
feature.SetGeometry(poly)
layer.CreateFeature(feature)
feature = None
data_source = None
def make_raster_from_array(base_array, base_raster_path):
"""Make a raster from an array on a designated path.
Args:
array (numpy.ndarray): the 2D array for making the raster.
raster_path (str): path to the raster to be created.
Returns:
None.
"""
srs = osr.SpatialReference()
srs.ImportFromEPSG(26910) # UTM Zone 10N
project_wkt = srs.ExportToWkt()
# Each pixel is 1x1 m
pygeoprocessing.numpy_array_to_raster(
base_array, -1, (1, -1), (1180000, 690000), project_wkt,
base_raster_path)
def make_lulc_raster(lulc_ras_path):
"""Make a 100x100 LULC raster with two LULC codes on the raster path.
Args:
lulc_raster_path (str): path to the LULC raster.
Returns:
None.
"""
size = 100
lulc_array = numpy.zeros((size, size), dtype=numpy.int16)
lulc_array[size // 2:, :] = 1
make_raster_from_array(lulc_array, lulc_ras_path)
def make_soil_raster(soil_ras_path):
"""Make a 100x100 soil group raster with four soil groups on th raster path.
Args:
soil_ras_path (str): path to the soil group raster.
Returns:
None.
"""
size = 100
soil_groups = 4
soil_array = numpy.zeros((size, size), dtype=numpy.int32)
for i, row in enumerate(soil_array):
row[:] = i % soil_groups + 1
make_raster_from_array(soil_array, soil_ras_path)
def make_gradient_raster(grad_ras_path):
"""Make a raster with different values on each row on the raster path.
The raster values on each column are in an ascending order from 0 to the
nth column, based on the size of the array. This function can be used for
making DEM or climate zone rasters.
Args:
grad_ras_path (str): path to the gradient raster.
Returns:
None.
"""
size = 100
grad_array = numpy.resize(
numpy.arange(size, dtype=numpy.int32), (size, size))
make_raster_from_array(grad_array, grad_ras_path)
def make_eto_rasters(eto_dir_path):
"""Make twelve 100x100 rasters of monthly evapotranspiration.
Args:
eto_dir_path (str): path to the directory for saving the rasters.
Returns:
None.
"""
size = 100
for month in range(1, 13):
eto_raster_path = os.path.join(
eto_dir_path, 'eto' + str(month) + '.tif')
eto_array = numpy.full((size, size), month, dtype=numpy.int32)
make_raster_from_array(eto_array, eto_raster_path)
def make_precip_rasters(precip_dir_path):
"""Make twelve 100x100 rasters of monthly precipitation.
Args:
precip_dir_path (str): path to the directory for saving the rasters.
Returns:
None.
"""
size = 100
for month in range(1, 13):
precip_raster_path = os.path.join(
precip_dir_path, 'precip_mm_' + str(month) + '.tif')
precip_array = numpy.full((size, size), month + 10, dtype=numpy.int32)
make_raster_from_array(precip_array, precip_raster_path)
def make_zeropadded_rasters(dir_path, prefix):
"""Make twelve 1x1 raster files with filenames ending in zero-padded
month number.
Args:
dir_path (str): path to the directory for saving the rasters.
file_prefix (str): prefix of new files to create.
Returns:
list: monthly raster filenames
"""
size = 1
monthly_raster_list = []
for month in range(1, 13):
raster_path = os.path.join(
dir_path, prefix + str(month).zfill(2) + '.tif')
temp_array = numpy.full((size, size), 1, dtype=numpy.int8)
make_raster_from_array(temp_array, raster_path)
monthly_raster_list.append(raster_path)
return monthly_raster_list
def make_recharge_raster(recharge_ras_path):
"""Make a 100x100 raster of user defined recharge.
Args:
recharge_ras_path (str): path to the directory for saving the rasters.
Returns:
None.
"""
size = 100
recharge_array = numpy.full((size, size), 200, dtype=numpy.int32)
make_raster_from_array(recharge_array, recharge_ras_path)
def make_rain_csv(rain_csv_path):
"""Make a synthesized rain events csv on the designated csv path.
Args:
rain_csv_path (str): path to the rain events csv.
Returns:
None.
"""
with open(rain_csv_path, 'w') as open_table:
open_table.write('month,events\n')
for month in range(1, 13):
open_table.write(str(month) + ',' + '1\n')
def make_biophysical_csv(biophysical_csv_path):
"""Make a synthesized biophysical csv on the designated path.
Args:
biophysical_csv (str): path to the biophysical csv.
Returns:
None.
"""
with open(biophysical_csv_path, 'w') as open_table:
open_table.write(
'lucode,Description,CN_A,CN_B,CN_C,CN_D,Kc_1,Kc_2,Kc_3,Kc_4,')
open_table.write('Kc_5,Kc_6,Kc_7,Kc_8,Kc_9,Kc_10,Kc_11,Kc_12\n')
open_table.write('0,"lulc 1",50,50,0,0,0.7,0.7,0.7,0.7,0.7,0.7,0.7,')
open_table.write('0.7,0.7,0.7,0.7,0.7\n')
open_table.write('1,"lulc 2",72,82,0,0,0.4,0.4,0.4,0.4,0.4,0.4,0.4,')
open_table.write('0.4,0.4,0.4,0.4,0.4\n')
def make_bad_biophysical_csv(biophysical_csv_path):
"""Make a bad biophysical csv with bad values to test error handling.
Args:
biophysical_csv (str): path to the corrupted biophysical csv.
Returns:
None.
"""
with open(biophysical_csv_path, 'w') as open_table:
open_table.write(
'lucode,Description,CN_A,CN_B,CN_C,CN_D,Kc_1,Kc_2,Kc_3,Kc_4,')
open_table.write('Kc_5,Kc_6,Kc_7,Kc_8,Kc_9,Kc_10,Kc_11,Kc_12\n')
# look at that 'fifty'
open_table.write(
'0,"lulc 1",fifty,50,0,0,0.7,0.7,0.7,0.7,0.7,0.7,0.7,')
open_table.write('0.7,0.7,0.7,0.7,0.7\n')
open_table.write('1,"lulc 2",72,82,0,0,0.4,0.4,0.4,0.4,0.4,0.4,0.4,')
open_table.write('0.4,0.4,0.4,0.4,0.4\n')
def make_alpha_csv(alpha_csv_path):
"""Make a monthly alpha csv on the designated path.
Args:
alpha_csv_path (str): path to the alpha csv.
Returns:
None.
"""
with open(alpha_csv_path, 'w') as open_table:
open_table.write('month,alpha\n')
for month in range(1, 13):
open_table.write(str(month) + ',0.083333333\n')
def make_climate_zone_csv(cz_csv_path):
"""Make a climate zone csv with number of rain events per months and CZs.
Args:
cz_csv_path (str): path to the climate zone csv.
Returns:
None.
"""
climate_zones = 100
# Random rain events for each month
rain_events = [14, 17, 14, 15, 20, 18, 4, 6, 5, 16, 16, 20]
with open(cz_csv_path, 'w') as open_table:
open_table.write(
'cz_id,jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec\n')
for cz in range(climate_zones):
rain_events = [x + 1 for x in rain_events]
rain_events_str = [str(val) for val in [cz] + rain_events]
rain_events_str = ','.join(rain_events_str) + '\n'
open_table.write(rain_events_str)
def make_agg_results_csv(result_csv_path,
climate_zones=False,
recharge=False,
vector_exists=False):
"""Make csv file that has the expected aggregated_results_swy.shp table.
The csv table is in the form of fid,vri_sum,qb_val per line.
Args:
csv_path (str): path to the aggregated results csv file.
climate_zones (bool): True if model is executed in climate zone mode.
recharge (bool): True if user inputs recharge zone shapefile.
vector_preexists (bool): True if aggregate results exists.
Returns:
None.
"""
with open(result_csv_path, 'w') as open_table:
if climate_zones:
open_table.write('0,1.0,54.4764\n')
elif recharge:
open_table.write('0,0.00000,200.00000')
elif vector_exists:
open_table.write('0,2000000.00000,200.00000')
else:
open_table.write('0,1.0,51.359875\n')
class SeasonalWaterYieldUnusualDataTests(unittest.TestCase):
"""Tests for InVEST Seasonal Water Yield model.
These are tests that cover cases where input data are in an unusual
corner case.
"""
def setUp(self):
"""Make tmp workspace."""
self.workspace_dir = tempfile.mkdtemp()
def tearDown(self):
"""Delete workspace after test is done."""
shutil.rmtree(self.workspace_dir, ignore_errors=True)
def test_zeropadded_monthly_filenames(self):
"""test filenames with zero-padded months in
_get_monthly_file_lists function
"""
from natcap.invest.seasonal_water_yield.seasonal_water_yield import _get_monthly_file_lists
n_months = 12
# Make directory and file names with zero-padded months
test_precip_dir_path = os.path.join(self.workspace_dir,
'test_0pad_precip_dir')
os.makedirs(test_precip_dir_path)
precip_file_list = make_zeropadded_rasters(test_precip_dir_path, 'Prcp')
test_eto_dir_path = os.path.join(self.workspace_dir,
'test_0pad_eto_dir')
os.makedirs(test_eto_dir_path)
eto_file_list = make_zeropadded_rasters(test_eto_dir_path, 'et0_')
# Create list of monthly files for data_type
eto_path_list = _get_monthly_file_lists(
n_months, test_eto_dir_path)
precip_path_list = _get_monthly_file_lists(
n_months, test_precip_dir_path)
# Verify that the returned lists match the input
self.assertEqual(precip_path_list, precip_file_list)
self.assertEqual(eto_path_list, eto_file_list)
def test_nonpadded_monthly_filenames(self):
"""test filenames without zero-padded months in
_get_monthly_file_lists function
"""
from natcap.invest.seasonal_water_yield.seasonal_water_yield import _get_monthly_file_lists
n_months = 12
# Make directory and file names with (non-zero-padded) months
precip_dir_path = os.path.join(self.workspace_dir, 'precip_dir')
os.makedirs(precip_dir_path)
make_precip_rasters(precip_dir_path)
precip_path_list = _get_monthly_file_lists(
n_months, precip_dir_path)
# Create lists of monthly filenames to which to compare function output
# Note this is hardcoded to match the filenames created in make_precip_rasters
match_precip = [os.path.join(precip_dir_path,
"precip_mm_" + str(m) + ".tif")
for m in range(1, n_months + 1)]
# Verify that the returned lists match the input
self.assertEqual(precip_path_list, match_precip)
def test_ambiguous_precip_data(self):
"""SWY test case where there are more than 12 precipitation files."""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
precip_dir_path = os.path.join(self.workspace_dir, 'precip_dir')
test_precip_dir_path = os.path.join(self.workspace_dir,
'test_precip_dir')
os.makedirs(precip_dir_path)
make_precip_rasters(precip_dir_path)
shutil.copytree(precip_dir_path, test_precip_dir_path)
shutil.copy(
os.path.join(test_precip_dir_path, 'precip_mm_3.tif'),
os.path.join(test_precip_dir_path, 'bonus_precip_mm_3.tif'))
# A placeholder args that has the property that the aoi_path will be
# the same name as the output aggregate vector
args = {
'workspace_dir': self.workspace_dir,
'alpha_m': '1/12',
'beta_i': '1.0',
'gamma': '1.0',
'precip_dir': test_precip_dir_path, # test constructed one
'threshold_flow_accumulation': '1000',
'user_defined_climate_zones': False,
'user_defined_local_recharge': False,
'monthly_alpha': False,
}
watershed_shp_path = os.path.join(args['workspace_dir'],
'watershed.shp')
make_simple_shp(watershed_shp_path, (1180000.0, 690000.0))
args['aoi_path'] = watershed_shp_path
biophysical_csv_path = os.path.join(args['workspace_dir'],
'biophysical_table.csv')
make_biophysical_csv(biophysical_csv_path)
args['biophysical_table_path'] = biophysical_csv_path
dem_ras_path = os.path.join(args['workspace_dir'], 'dem.tif')
make_gradient_raster(dem_ras_path)
args['dem_raster_path'] = dem_ras_path
eto_dir_path = os.path.join(args['workspace_dir'], 'eto_dir')
os.makedirs(eto_dir_path)
make_eto_rasters(eto_dir_path)
args['et0_dir'] = eto_dir_path
lulc_ras_path = os.path.join(args['workspace_dir'], 'lulc.tif')
make_lulc_raster(lulc_ras_path)
args['lulc_raster_path'] = lulc_ras_path
rain_csv_path = os.path.join(args['workspace_dir'],
'rain_events_table.csv')
make_rain_csv(rain_csv_path)
args['rain_events_table_path'] = rain_csv_path
soil_ras_path = os.path.join(args['workspace_dir'], 'soil_group.tif')
make_soil_raster(soil_ras_path)
args['soil_group_path'] = soil_ras_path
with self.assertRaises(ValueError):
seasonal_water_yield.execute(args)
def test_precip_data_missing(self):
"""SWY test case where there is a missing precipitation file."""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
precip_dir_path = os.path.join(self.workspace_dir, 'precip_dir')
test_precip_dir_path = os.path.join(self.workspace_dir,
'test_precip_dir')
os.makedirs(precip_dir_path)
make_precip_rasters(precip_dir_path)
shutil.copytree(precip_dir_path, test_precip_dir_path)
os.remove(os.path.join(test_precip_dir_path, 'precip_mm_3.tif'))
# A placeholder args that has the property that the aoi_path will be
# the same name as the output aggregate vector
args = {
'workspace_dir': self.workspace_dir,
'alpha_m': '1/12',
'beta_i': '1.0',
'gamma': '1.0',
'precip_dir': test_precip_dir_path, # test constructed one
'threshold_flow_accumulation': '1000',
'user_defined_climate_zones': False,
'user_defined_local_recharge': False,
'monthly_alpha': False,
}
watershed_shp_path = os.path.join(args['workspace_dir'],
'watershed.shp')
make_simple_shp(watershed_shp_path, (1180000.0, 690000.0))
args['aoi_path'] = watershed_shp_path
biophysical_csv_path = os.path.join(args['workspace_dir'],
'biophysical_table.csv')
make_biophysical_csv(biophysical_csv_path)
args['biophysical_table_path'] = biophysical_csv_path
dem_ras_path = os.path.join(args['workspace_dir'], 'dem.tif')
make_gradient_raster(dem_ras_path)
args['dem_raster_path'] = dem_ras_path
eto_dir_path = os.path.join(args['workspace_dir'], 'eto_dir')
os.makedirs(eto_dir_path)
make_eto_rasters(eto_dir_path)
args['et0_dir'] = eto_dir_path
lulc_ras_path = os.path.join(args['workspace_dir'], 'lulc.tif')
make_lulc_raster(lulc_ras_path)
args['lulc_raster_path'] = lulc_ras_path
rain_csv_path = os.path.join(args['workspace_dir'],
'rain_events_table.csv')
make_rain_csv(rain_csv_path)
args['rain_events_table_path'] = rain_csv_path
soil_ras_path = os.path.join(args['workspace_dir'], 'soil_group.tif')
make_soil_raster(soil_ras_path)
args['soil_group_path'] = soil_ras_path
with self.assertRaises(ValueError):
seasonal_water_yield.execute(args)
def test_aggregate_vector_preexists(self):
"""SWY test model deletes a preexisting aggregate output result."""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# Set up data so there is enough code to do an aggregate over the
# rasters but the output vector already exists
aoi_path = os.path.join(self.workspace_dir, 'watershed.shp')
make_simple_shp(aoi_path, (1180000.0, 690000.0))
l_path = os.path.join(self.workspace_dir, 'L.tif')
make_recharge_raster(l_path)
aggregate_vector_path = os.path.join(self.workspace_dir,
'aggregated_results_swy.shp')
make_simple_shp(aggregate_vector_path, (1180000.0, 690000.0))
seasonal_water_yield._aggregate_recharge(aoi_path, l_path, l_path,
aggregate_vector_path)
# test if aggregate is expected
agg_results_csv_path = os.path.join(self.workspace_dir,
'agg_results_base.csv')
make_agg_results_csv(agg_results_csv_path, vector_exists=True)
result_vector = ogr.Open(aggregate_vector_path)
result_layer = result_vector.GetLayer()
incorrect_value_list = []
with open(agg_results_csv_path, 'r') as agg_result_file:
for line in agg_result_file:
fid, vri_sum, qb_val = [float(x) for x in line.split(',')]
feature = result_layer.GetFeature(int(fid))
for field, value in [('vri_sum', vri_sum), ('qb', qb_val)]:
if not numpy.isclose(
feature.GetField(field), value, rtol=1e-6):
incorrect_value_list.append(
'Unexpected value on feature %d, '
'expected %f got %f' % (fid, value,
feature.GetField(field)))
ogr.Feature.__swig_destroy__(feature)
feature = None
result_layer = None
ogr.DataSource.__swig_destroy__(result_vector)
result_vector = None
if incorrect_value_list:
raise AssertionError('\n' + '\n'.join(incorrect_value_list))
def test_duplicate_aoi_assertion(self):
"""SWY ensure model halts when AOI path identical to output vector."""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# A placeholder args that has the property that the aoi_path will be
# the same name as the output aggregate vector
args = {
'workspace_dir': self.workspace_dir,
'aoi_path': os.path.join(
self.workspace_dir, 'aggregated_results_swy_foo.shp'),
'results_suffix': 'foo',
'alpha_m': '1/12',
'beta_i': '1.0',
'gamma': '1.0',
'threshold_flow_accumulation': '1000',
'user_defined_climate_zones': False,
'user_defined_local_recharge': False,
'monthly_alpha': False,
}
biophysical_csv_path = os.path.join(args['workspace_dir'],
'biophysical_table.csv')
make_biophysical_csv(biophysical_csv_path)
args['biophysical_table_path'] = biophysical_csv_path
dem_ras_path = os.path.join(args['workspace_dir'], 'dem.tif')
make_gradient_raster(dem_ras_path)
args['dem_raster_path'] = dem_ras_path
eto_dir_path = os.path.join(args['workspace_dir'], 'eto_dir')
os.makedirs(eto_dir_path)
make_eto_rasters(eto_dir_path)
args['et0_dir'] = eto_dir_path
lulc_ras_path = os.path.join(args['workspace_dir'], 'lulc.tif')
make_lulc_raster(lulc_ras_path)
args['lulc_raster_path'] = lulc_ras_path
precip_dir_path = os.path.join(args['workspace_dir'], 'precip_dir')
os.makedirs(precip_dir_path)
make_precip_rasters(precip_dir_path)
args['precip_dir'] = precip_dir_path
rain_csv_path = os.path.join(args['workspace_dir'],
'rain_events_table.csv')
make_rain_csv(rain_csv_path)
args['rain_events_table_path'] = rain_csv_path
soil_ras_path = os.path.join(args['workspace_dir'], 'soil_group.tif')
make_soil_raster(soil_ras_path)
args['soil_group_path'] = soil_ras_path
with self.assertRaises(ValueError):
seasonal_water_yield.execute(args)
class SeasonalWaterYieldRegressionTests(unittest.TestCase):
"""Regression tests for InVEST Seasonal Water Yield model."""
def setUp(self):
"""Create temporary workspace."""
self.workspace_dir = tempfile.mkdtemp()
def tearDown(self):
"""Remove temporary workspace."""
shutil.rmtree(self.workspace_dir)
@staticmethod
def generate_base_args(workspace_dir):
"""Generate args list consistent across all three regression tests."""
args = {
'alpha_m': '1/12',
'beta_i': '1.0',
'gamma': '1.0',
'results_suffix': '',
'threshold_flow_accumulation': '50',
'workspace_dir': workspace_dir,
}
watershed_shp_path = os.path.join(workspace_dir, 'watershed.shp')
make_simple_shp(watershed_shp_path, (1180000.0, 690000.0))
args['aoi_path'] = watershed_shp_path
biophysical_csv_path = os.path.join(workspace_dir,
'biophysical_table.csv')
make_biophysical_csv(biophysical_csv_path)
args['biophysical_table_path'] = biophysical_csv_path
dem_ras_path = os.path.join(workspace_dir, 'dem.tif')
make_gradient_raster(dem_ras_path)
args['dem_raster_path'] = dem_ras_path
eto_dir_path = os.path.join(workspace_dir, 'eto_dir')
os.makedirs(eto_dir_path)
make_eto_rasters(eto_dir_path)
args['et0_dir'] = eto_dir_path
lulc_ras_path = os.path.join(workspace_dir, 'lulc.tif')
make_lulc_raster(lulc_ras_path)
args['lulc_raster_path'] = lulc_ras_path
precip_dir_path = os.path.join(workspace_dir, 'precip_dir')
os.makedirs(precip_dir_path)
make_precip_rasters(precip_dir_path)
args['precip_dir'] = precip_dir_path
rain_csv_path = os.path.join(workspace_dir, 'rain_events_table.csv')
make_rain_csv(rain_csv_path)
args['rain_events_table_path'] = rain_csv_path
soil_ras_path = os.path.join(workspace_dir, 'soil_group.tif')
make_soil_raster(soil_ras_path)
args['soil_group_path'] = soil_ras_path
return args
def test_base_regression(self):
"""SWY base regression test on sample data.
Executes SWY in default mode and checks that the output files are
generated and that the aggregate shapefile fields are the same as the
regression case.
"""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# use predefined directory so test can clean up files during teardown
args = SeasonalWaterYieldRegressionTests.generate_base_args(
self.workspace_dir)
# Ensure the model can pass when a nodata value is not defined.
size = 100
lulc_array = numpy.zeros((size, size), dtype=numpy.int8)
lulc_array[size // 2:, :] = 1
driver = gdal.GetDriverByName('GTiff')
new_raster = driver.Create(
args['lulc_raster_path'], lulc_array.shape[0],
lulc_array.shape[1], 1, gdal.GDT_Byte)
band = new_raster.GetRasterBand(1)
band.WriteArray(lulc_array)
geotransform = [1180000, 1, 0, 690000, 0, -1]
new_raster.SetGeoTransform(geotransform)
band = None
new_raster = None
driver = None
# make args explicit that this is a base run of SWY
args['user_defined_climate_zones'] = False
args['user_defined_local_recharge'] = False
args['monthly_alpha'] = False
args['results_suffix'] = ''
seasonal_water_yield.execute(args)
# generate aggregated results csv table for assertion
agg_results_csv_path = os.path.join(
args['workspace_dir'], 'agg_results_base.csv')
make_agg_results_csv(agg_results_csv_path)
SeasonalWaterYieldRegressionTests._assert_regression_results_equal(
os.path.join(args['workspace_dir'], 'aggregated_results_swy.shp'),
agg_results_csv_path)
def test_base_regression_nodata_inf(self):
"""SWY base regression test on sample data with really small nodata.
Executes SWY in default mode and checks that the output files are
generated and that the aggregate shapefile fields are the same as the
regression case.
"""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# use predefined directory so test can clean up files during teardown
args = SeasonalWaterYieldRegressionTests.generate_base_args(
self.workspace_dir)
# Ensure the model can pass when a nodata value is not defined.
size = 100
lulc_array = numpy.zeros((size, size), dtype=numpy.int8)
lulc_array[size // 2:, :] = 1
driver = gdal.GetDriverByName('GTiff')
new_raster = driver.Create(
args['lulc_raster_path'], lulc_array.shape[0],
lulc_array.shape[1], 1, gdal.GDT_Byte)
band = new_raster.GetRasterBand(1)
band.WriteArray(lulc_array)
geotransform = [1180000, 1, 0, 690000, 0, -1]
new_raster.SetGeoTransform(geotransform)
band = None
new_raster = None
driver = None
# set precip nodata values to a large, negative 64bit value.
nodata = numpy.finfo(numpy.float64).min
precip_nodata_dir = os.path.join(
self.workspace_dir, 'precip_nodata_dir')
os.makedirs(precip_nodata_dir)
size = 100
for month in range(1, 13):
precip_raster_path = os.path.join(
precip_nodata_dir, 'precip_mm_' + str(month) + '.tif')
precip_array = numpy.full(
(size, size), month + 10, dtype=numpy.float64)
precip_array[size - 1, :] = nodata
srs = osr.SpatialReference()
srs.ImportFromEPSG(26910) # UTM Zone 10N
project_wkt = srs.ExportToWkt()
# Each pixel is 1x1 m
pygeoprocessing.numpy_array_to_raster(
precip_array, nodata, (1, -1), (1180000, 690000), project_wkt,
precip_raster_path)
args['precip_dir'] = precip_nodata_dir
# make args explicit that this is a base run of SWY
args['user_defined_climate_zones'] = False
args['user_defined_local_recharge'] = False
args['monthly_alpha'] = False
args['results_suffix'] = ''
seasonal_water_yield.execute(args)
# generate aggregated results csv table for assertion
agg_results_csv_path = os.path.join(
args['workspace_dir'], 'agg_results_base.csv')
with open(agg_results_csv_path, 'w') as open_table:
open_table.write('0,1.0,50.076062\n')
SeasonalWaterYieldRegressionTests._assert_regression_results_equal(
os.path.join(args['workspace_dir'], 'aggregated_results_swy.shp'),
agg_results_csv_path)
def test_bad_biophysical_table(self):
"""SWY bad biophysical table with non-numerical values."""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# use predefined directory so test can clean up files during teardown
args = SeasonalWaterYieldRegressionTests.generate_base_args(
self.workspace_dir)
# make args explicit that this is a base run of SWY
args['user_defined_climate_zones'] = False
args['user_defined_local_recharge'] = False
args['monthly_alpha'] = False
args['results_suffix'] = ''
make_bad_biophysical_csv(args['biophysical_table_path'])
with self.assertRaises(ValueError) as context:
seasonal_water_yield.execute(args)
self.assertIn(
'could not be interpreted as numbers', str(context.exception))
def test_monthly_alpha_regression(self):
"""SWY monthly alpha values regression test on sample data.
Executes SWY using the monthly alpha table and checks that the output
files are generated and that the aggregate shapefile fields are the
same as the regression case.
"""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# use predefined directory so test can clean up files during teardown
args = SeasonalWaterYieldRegressionTests.generate_base_args(
self.workspace_dir)
# make args explicit that this is a base run of SWY
args['user_defined_climate_zones'] = False
args['user_defined_local_recharge'] = False
args['monthly_alpha'] = True
args['results_suffix'] = ''
alpha_csv_path = os.path.join(args['workspace_dir'],
'monthly_alpha.csv')
make_alpha_csv(alpha_csv_path)
args['monthly_alpha_path'] = alpha_csv_path
seasonal_water_yield.execute(args)
# generate aggregated results csv table for assertion
agg_results_csv_path = os.path.join(args['workspace_dir'],
'agg_results_base.csv')
make_agg_results_csv(agg_results_csv_path)
SeasonalWaterYieldRegressionTests._assert_regression_results_equal(
os.path.join(args['workspace_dir'], 'aggregated_results_swy.shp'),
agg_results_csv_path)
def test_climate_zones_missing_cz_id(self):
"""SWY climate zone regression test fails on bad cz table data.
Executes SWY in climate zones mode and checks that the test fails
when a climate zone raster value is not present in the climate
zone table.
"""
import pandas
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# use predefined directory so test can clean up files during teardown
args = SeasonalWaterYieldRegressionTests.generate_base_args(
self.workspace_dir)
# modify args to account for climate zones defined
cz_csv_path = os.path.join(args['workspace_dir'],
'climate_zone_events.csv')
make_climate_zone_csv(cz_csv_path)
args['climate_zone_table_path'] = cz_csv_path
cz_ras_path = os.path.join(args['workspace_dir'], 'dem.tif')
make_gradient_raster(cz_ras_path)
args['climate_zone_raster_path'] = cz_ras_path
# remove row from the climate zone table so cz raster value is missing
bad_cz_table_path = os.path.join(
self.workspace_dir, 'bad_climate_zone_table.csv')
cz_df = pandas.read_csv(args['climate_zone_table_path'])
cz_df = cz_df[cz_df['cz_id'] != 1]
cz_df.to_csv(bad_cz_table_path)
cz_df = None
args['climate_zone_table_path'] = bad_cz_table_path
args['user_defined_climate_zones'] = True
args['user_defined_local_recharge'] = False
args['monthly_alpha'] = False
args['results_suffix'] = 'cz'
with self.assertRaises(ValueError) as context:
seasonal_water_yield.execute(args)
self.assertTrue(
("The missing values found in the Climate Zone raster but not the"
" table are: [1]") in str(context.exception))
def test_biophysical_table_missing_lucode(self):
"""SWY test bad biophysical table with missing LULC value."""
import pygeoprocessing
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# use predefined directory so test can clean up files during teardown
args = SeasonalWaterYieldRegressionTests.generate_base_args(
self.workspace_dir)
# make args explicit that this is a base run of SWY
args['user_defined_climate_zones'] = False
args['user_defined_local_recharge'] = False
args['monthly_alpha'] = False
args['results_suffix'] = ''
# add a LULC value not found in biophysical csv
lulc_new_path = os.path.join(self.workspace_dir, 'lulc_new.tif')
lulc_info = pygeoprocessing.get_raster_info(args['lulc_raster_path'])
lulc_array = gdal.OpenEx(args['lulc_raster_path']).ReadAsArray()
lulc_array[0][0] = 321
# set a nodata value to make sure nodatas are handled correctly when
# reclassifying
lulc_array[0][1] = lulc_info['nodata'][0]
pygeoprocessing.numpy_array_to_raster(
lulc_array, lulc_info['nodata'][0], lulc_info['pixel_size'],
(lulc_info['geotransform'][0], lulc_info['geotransform'][3]),
lulc_info['projection_wkt'], lulc_new_path)
lulc_array = None
args['lulc_raster_path'] = lulc_new_path
with self.assertRaises(ValueError) as context:
seasonal_water_yield.execute(args)
self.assertTrue(
("The missing values found in the LULC raster but not the"
" table are: [321]") in str(context.exception))
def test_invalid_soil_group(self):
"""SWY test exception when user provides invalid soil group."""
import pygeoprocessing
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# use predefined directory so test can clean up files during teardown
args = SeasonalWaterYieldRegressionTests.generate_base_args(
self.workspace_dir)
# make args explicit that this is a base run of SWY
args['user_defined_climate_zones'] = False
args['user_defined_local_recharge'] = False
args['monthly_alpha'] = False
args['results_suffix'] = ''
soil_array = pygeoprocessing.raster_to_numpy_array(
args['soil_group_path'])
raster = gdal.OpenEx(args['soil_group_path'], gdal.GA_Update)
band = raster.GetRasterBand(1)
soil_array = band.ReadAsArray()
soil_array[50, 50] = 6 # invalid value
soil_array[51, 51] = 7 # invalid value
soil_array[52, 52] = band.GetNoDataValue() # valid, excluded
band.WriteArray(soil_array)
band = None
raster = None
with self.assertRaises(ValueError) as cm:
seasonal_water_yield.execute(args)
self.assertIn("Invalid group(s) 6, 7 were found in soil group raster",
str(cm.exception))
def test_user_recharge(self):
"""SWY user recharge regression test on sample data.
Executes SWY in user defined local recharge mode and checks that the
output files are generated and that the aggregate shapefile fields
are the same as the regression case.
"""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# use predefined directory so test can clean up files during teardown
args = SeasonalWaterYieldRegressionTests.generate_base_args(
self.workspace_dir)
# modify args to account for user recharge
args['user_defined_climate_zones'] = False
args['monthly_alpha'] = False
args['results_suffix'] = ''
args['user_defined_local_recharge'] = True
recharge_ras_path = os.path.join(args['workspace_dir'], 'L.tif')
make_recharge_raster(recharge_ras_path)
args['l_path'] = recharge_ras_path
seasonal_water_yield.execute(args)
# generate aggregated results csv table for assertion
agg_results_csv_path = os.path.join(args['workspace_dir'],
'agg_results_l.csv')
make_agg_results_csv(agg_results_csv_path, recharge=True)
SeasonalWaterYieldRegressionTests._assert_regression_results_equal(
os.path.join(args['workspace_dir'], 'aggregated_results_swy.shp'),
agg_results_csv_path)
@staticmethod
def _assert_regression_results_equal(
result_vector_path, agg_results_path):
"""Assert workspace results.
Test the state of the workspace against the expected list of files
and aggregated results.
Args:
result_vector_path (string): path to the summary shapefile
produced by the SWY model.
agg_results_path (string): path to a csv file that has the
expected aggregated_results_swy.shp table in the form of
fid,vri_sum,qb_val per line
Returns:
None
Raises:
AssertionError if any files are missing or results are out of
range by `tolerance_places`
"""
# we expect a file called 'aggregated_results_swy.shp'
result_vector = gdal.OpenEx(result_vector_path, gdal.OF_VECTOR)
result_layer = result_vector.GetLayer()
# The tolerance of 3 digits after the decimal was determined by
# experimentation on the application with the given range of numbers.
# This is an apparently reasonable approach as described by ChrisF:
# http://stackoverflow.com/a/3281371/42897
# and even more reading about picking numerical tolerance (it's hard):
# https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
tolerance_places = 3
with open(agg_results_path, 'r') as agg_result_file:
for line in agg_result_file:
fid, vri_sum, qb_val = [float(x) for x in line.split(',')]
feature = result_layer.GetFeature(int(fid))
for field, value in [('vri_sum', vri_sum), ('qb', qb_val)]:
numpy.testing.assert_allclose(
feature.GetField(field),
value,
rtol=0, atol=10**-tolerance_places)
ogr.Feature.__swig_destroy__(feature)
feature = None
result_layer = None
result_vector = None
def test_monthly_quickflow_undefined_nodata(self):
"""Test `_calculate_monthly_quick_flow` with undefined nodata values"""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# set up tiny raster arrays to test
precip_array = numpy.array([
[10, 10],
[10, 10]], dtype=numpy.float32)
si_array = numpy.array([
[15, 15],
[2.5, 2.5]], dtype=numpy.float32)
n_events_array = numpy.array([
[10, 10],
[1, 1]], dtype=numpy.float32)
stream_mask = numpy.array([
[0, 0],
[0, 0]], dtype=numpy.float32)
# results calculated by wolfram alpha
expected_quickflow_array = numpy.array([
[0, 0],
[0.61928378, 0.61928378]])
precip_path = os.path.join(self.workspace_dir, 'precip.tif')
si_path = os.path.join(self.workspace_dir, 'si.tif')
n_events_path = os.path.join(self.workspace_dir, 'n_events.tif')
stream_path = os.path.join(self.workspace_dir, 'stream.tif')
srs = osr.SpatialReference()
srs.ImportFromEPSG(26910) # UTM Zone 10N
project_wkt = srs.ExportToWkt()
output_path = os.path.join(self.workspace_dir, 'quickflow.tif')
# write all the test arrays to raster files
for array, path in [(precip_array, precip_path),
(n_events_array, n_events_path)]:
# make the nodata value undefined for user inputs
pygeoprocessing.numpy_array_to_raster(
array, None, (1, -1), (1180000, 690000), project_wkt, path)
for array, path in [(si_array, si_path),
(stream_mask, stream_path)]:
# define a nodata value for intermediate outputs
pygeoprocessing.numpy_array_to_raster(
array, -1, (1, -1), (1180000, 690000), project_wkt, path)
# save the quickflow results raster to quickflow.tif
seasonal_water_yield._calculate_monthly_quick_flow(
precip_path, n_events_path, stream_path, si_path, output_path)
# read the raster output back in to a numpy array
quickflow_array = pygeoprocessing.raster_to_numpy_array(output_path)
# assert each element is close to the expected value
numpy.testing.assert_allclose(
quickflow_array, expected_quickflow_array, atol=1e-5)
def test_monthly_quickflow_si_zero(self):
"""Test `_calculate_monthly_quick_flow` when s_i is zero"""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# QF should be equal to P when s_i is 0
precip_array = numpy.array([[10.5]], dtype=numpy.float32)
si_array = numpy.array([[0]], dtype=numpy.float32)
n_events_array = numpy.array([[10]], dtype=numpy.float32)
stream_mask = numpy.array([[0]], dtype=numpy.float32)
expected_quickflow_array = numpy.array([[10.5]])
precip_path = os.path.join(self.workspace_dir, 'precip.tif')
si_path = os.path.join(self.workspace_dir, 'si.tif')
n_events_path = os.path.join(self.workspace_dir, 'n_events.tif')
stream_path = os.path.join(self.workspace_dir, 'stream.tif')
srs = osr.SpatialReference()
srs.ImportFromEPSG(26910) # UTM Zone 10N
project_wkt = srs.ExportToWkt()
output_path = os.path.join(self.workspace_dir, 'quickflow.tif')
# write all the test arrays to raster files
for array, path in [(precip_array, precip_path),
(n_events_array, n_events_path),
(si_array, si_path),
(stream_mask, stream_path)]:
# define a nodata value for intermediate outputs
pygeoprocessing.numpy_array_to_raster(
array, -1, (1, -1), (1180000, 690000), project_wkt, path)
seasonal_water_yield._calculate_monthly_quick_flow(
precip_path, n_events_path, stream_path, si_path, output_path)
numpy.testing.assert_allclose(
pygeoprocessing.raster_to_numpy_array(output_path),
expected_quickflow_array, atol=1e-5)
def test_monthly_quickflow_large_si_aim_ratio(self):
"""Test `_calculate_monthly_quick_flow` with large s_i/a_im ratio"""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# with these values, the QF equation would overflow float32 if
# we didn't catch it early
precip_array = numpy.array([[6]], dtype=numpy.float32)
si_array = numpy.array([[23.33]], dtype=numpy.float32)
n_events_array = numpy.array([[10]], dtype=numpy.float32)
stream_mask = numpy.array([[0]], dtype=numpy.float32)
expected_quickflow_array = numpy.array([[0]])
precip_path = os.path.join(self.workspace_dir, 'precip.tif')
si_path = os.path.join(self.workspace_dir, 'si.tif')
n_events_path = os.path.join(self.workspace_dir, 'n_events.tif')
stream_path = os.path.join(self.workspace_dir, 'stream.tif')
srs = osr.SpatialReference()
srs.ImportFromEPSG(26910) # UTM Zone 10N
project_wkt = srs.ExportToWkt()
output_path = os.path.join(self.workspace_dir, 'quickflow.tif')
# write all the test arrays to raster files
for array, path in [(precip_array, precip_path),
(n_events_array, n_events_path),
(si_array, si_path),
(stream_mask, stream_path)]:
# define a nodata value for intermediate outputs
pygeoprocessing.numpy_array_to_raster(
array, -1, (1, -1), (1180000, 690000), project_wkt, path)
seasonal_water_yield._calculate_monthly_quick_flow(
precip_path, n_events_path, stream_path, si_path, output_path)
numpy.testing.assert_allclose(
pygeoprocessing.raster_to_numpy_array(output_path),
expected_quickflow_array, atol=1e-5)
def test_monthly_quickflow_negative_values_set_to_zero(self):
"""Test `_calculate_monthly_quick_flow` with negative QF result"""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# with these values, the QF equation evaluates to a small negative
# number. assert that it is set to zero
precip_array = numpy.array([[30]], dtype=numpy.float32)
si_array = numpy.array([[10]], dtype=numpy.float32)
n_events_array = numpy.array([[10]], dtype=numpy.float32)
stream_mask = numpy.array([[0]], dtype=numpy.float32)
expected_quickflow_array = numpy.array([[0]])
precip_path = os.path.join(self.workspace_dir, 'precip.tif')
si_path = os.path.join(self.workspace_dir, 'si.tif')
n_events_path = os.path.join(self.workspace_dir, 'n_events.tif')
stream_path = os.path.join(self.workspace_dir, 'stream.tif')
srs = osr.SpatialReference()
srs.ImportFromEPSG(26910) # UTM Zone 10N
project_wkt = srs.ExportToWkt()
output_path = os.path.join(self.workspace_dir, 'quickflow.tif')
# write all the test arrays to raster files
for array, path in [(precip_array, precip_path),
(n_events_array, n_events_path),
(si_array, si_path),
(stream_mask, stream_path)]:
# define a nodata value for intermediate outputs
pygeoprocessing.numpy_array_to_raster(
array, -1, (1, -1), (1180000, 690000), project_wkt, path)
seasonal_water_yield._calculate_monthly_quick_flow(
precip_path, n_events_path, stream_path, si_path, output_path)
numpy.testing.assert_allclose(
pygeoprocessing.raster_to_numpy_array(output_path),
expected_quickflow_array, atol=1e-5)
def test_local_recharge_undefined_nodata(self):
"""Test `calculate_local_recharge` with undefined nodata values"""
from natcap.invest.seasonal_water_yield import \
seasonal_water_yield_core
# set up tiny raster arrays to test
precip_array = numpy.array([
[10, 10],
[10, 10]], dtype=numpy.float32)
et0_array = numpy.array([
[100, 100],
[200, 200]], dtype=numpy.float32)
quickflow_array = numpy.array([
[0, 0],
[0.61, 0.61]], dtype=numpy.float32)
flow_dir_array = numpy.array([
[15, 25],
[50, 50]], dtype=numpy.float32)
kc_array = numpy.array([
[1, 1],
[1, 1]], dtype=numpy.float32)
stream_mask = numpy.array([
[0, 0],
[0, 0]], dtype=numpy.float32)
precip_path = os.path.join(self.workspace_dir, 'precip.tif')
et0_path = os.path.join(self.workspace_dir, 'et0.tif')
quickflow_path = os.path.join(self.workspace_dir, 'quickflow.tif')
flow_dir_path = os.path.join(self.workspace_dir, 'flow_dir.tif')
kc_path = os.path.join(self.workspace_dir, 'kc.tif')
stream_path = os.path.join(self.workspace_dir, 'stream.tif')
srs = osr.SpatialReference()
srs.ImportFromEPSG(26910) # UTM Zone 10N
project_wkt = srs.ExportToWkt()
output_path = os.path.join(self.workspace_dir, 'quickflow.tif')
# write all the test arrays to raster files
for array, path in [(precip_array, precip_path),
(et0_array, et0_path)]:
# make the nodata value undefined for user inputs
pygeoprocessing.numpy_array_to_raster(
array, None, (1, -1), (1180000, 690000), project_wkt, path)
for array, path in [(quickflow_array, quickflow_path),
(flow_dir_array, flow_dir_path),
(kc_array, kc_path),
(stream_mask, stream_path)]:
# define a nodata value for intermediate outputs
pygeoprocessing.numpy_array_to_raster(
array, -1, (1, -1), (1180000, 690000), project_wkt, path)
# arbitrary values for alpha, beta, gamma, etc.
# not verifying the output, just making sure there are no errors
seasonal_water_yield_core.calculate_local_recharge(
[precip_path for i in range(12)], [et0_path for i in range(12)],
[quickflow_path for i in range(12)], flow_dir_path,
[kc_path for i in range(12)], {i: 0.5 for i in range(12)}, 0.5,
0.5, stream_path,
os.path.join(self.workspace_dir, 'target_li_path.tif'),
os.path.join(self.workspace_dir, 'target_li_avail_path.tif'),
os.path.join(self.workspace_dir, 'target_l_sum_avail_path.tif'),
os.path.join(self.workspace_dir, 'target_aet_path.tif'),
os.path.join(self.workspace_dir, 'target_precip_path.tif'))
class SWYValidationTests(unittest.TestCase):
"""Tests for the SWY Model MODEL_SPEC and validation."""
def setUp(self):
"""Create a temporary workspace."""
self.workspace_dir = tempfile.mkdtemp()
self.base_required_keys = [
'workspace_dir',
'gamma',
'alpha_m',
'soil_group_path',
'user_defined_climate_zones',
'rain_events_table_path',
'biophysical_table_path',
'monthly_alpha',
'lulc_raster_path',
'dem_raster_path',
'beta_i',
'et0_dir',
'aoi_path',
'precip_dir',
'threshold_flow_accumulation',
'user_defined_local_recharge',
]
def tearDown(self):
"""Remove the temporary workspace after a test."""
shutil.rmtree(self.workspace_dir)
def test_missing_keys(self):
"""SWY Validate: assert missing required keys."""
from natcap.invest import validation
from natcap.invest.seasonal_water_yield import seasonal_water_yield
# empty args dict.
validation_errors = seasonal_water_yield.validate({})
invalid_keys = validation.get_invalid_keys(validation_errors)
expected_missing_keys = set(self.base_required_keys)
self.assertEqual(invalid_keys, expected_missing_keys)
def test_missing_keys_climate_zones(self):
"""SWY Validate: assert missing required keys given climate zones."""
from natcap.invest import validation
from natcap.invest.seasonal_water_yield import seasonal_water_yield
validation_errors = seasonal_water_yield.validate(
{'user_defined_climate_zones': True})
invalid_keys = validation.get_invalid_keys(validation_errors)
expected_missing_keys = set(
self.base_required_keys +
['climate_zone_table_path', 'climate_zone_raster_path'])
expected_missing_keys.difference_update(
{'user_defined_climate_zones', 'rain_events_table_path'})
self.assertEqual(invalid_keys, expected_missing_keys)
def test_missing_keys_local_recharge(self):
"""SWY Validate: assert missing required keys given local recharge."""
from natcap.invest import validation
from natcap.invest.seasonal_water_yield import seasonal_water_yield
validation_errors = seasonal_water_yield.validate(
{'user_defined_local_recharge': True})
invalid_keys = validation.get_invalid_keys(validation_errors)
expected_missing_keys = set(
self.base_required_keys + ['l_path'])
expected_missing_keys.difference_update(
{'user_defined_local_recharge',
'et0_dir',
'precip_dir',
'rain_events_table_path',
'soil_group_path'})
self.assertEqual(invalid_keys, expected_missing_keys)
def test_missing_keys_monthly_alpha_table(self):
"""SWY Validate: assert missing required keys given monthly alpha."""
from natcap.invest import validation
from natcap.invest.seasonal_water_yield import seasonal_water_yield
validation_errors = seasonal_water_yield.validate(
{'monthly_alpha': True})
invalid_keys = validation.get_invalid_keys(validation_errors)
expected_missing_keys = set(
self.base_required_keys + ['monthly_alpha_path'])
expected_missing_keys.difference_update(
{'monthly_alpha', 'alpha_m'})
self.assertEqual(invalid_keys, expected_missing_keys)
def test_all_inputs_valid(self):
"""SWY Validate: assert valid inputs have no validation errors."""
from natcap.invest.seasonal_water_yield import seasonal_water_yield
args = SeasonalWaterYieldRegressionTests.generate_base_args(
self.workspace_dir)
args.update({
'user_defined_climate_zones': False,
'user_defined_local_recharge': False,
'monthly_alpha': False})
# first test with none of the optional params
validation_errors = seasonal_water_yield.validate(args)
self.assertEqual(validation_errors, [])
cz_csv_path = os.path.join(self.workspace_dir, 'cz.csv')
make_climate_zone_csv(cz_csv_path)
cz_ras_path = os.path.join(args['workspace_dir'], 'dem.tif')
make_gradient_raster(cz_ras_path)
args['climate_zone_raster_path'] = cz_ras_path
args['climate_zone_table_path'] = cz_csv_path
args['user_defined_climate_zones'] = True
recharge_ras_path = os.path.join(self.workspace_dir, 'L.tif')
make_recharge_raster(recharge_ras_path)
args['l_path'] = recharge_ras_path
args['user_defined_local_recharge'] = True
alpha_csv_path = os.path.join(self.workspace_dir, 'monthly_alpha.csv')
make_alpha_csv(alpha_csv_path)
args['monthly_alpha_path'] = alpha_csv_path
args['monthly_alpha'] = True
# test with all of the optional params
validation_errors = seasonal_water_yield.validate(args)
self.assertEqual(validation_errors, [])