926 lines
39 KiB
Python
926 lines
39 KiB
Python
"""Module for Regression Testing the InVEST Wind Energy module."""
|
|
import unittest
|
|
import csv
|
|
import shutil
|
|
import tempfile
|
|
import os
|
|
import pickle
|
|
|
|
import numpy
|
|
import numpy.testing
|
|
from shapely.geometry import box
|
|
from shapely.geometry import Polygon
|
|
from shapely.geometry import Point
|
|
from osgeo import gdal
|
|
from osgeo import ogr
|
|
from osgeo import osr
|
|
|
|
import pygeoprocessing
|
|
|
|
gdal.UseExceptions()
|
|
SAMPLE_DATA = os.path.join(
|
|
os.path.dirname(__file__), '..', 'data', 'invest-test-data', 'wind_energy',
|
|
'input')
|
|
REGRESSION_DATA = os.path.join(
|
|
os.path.dirname(__file__), '..', 'data', 'invest-test-data', 'wind_energy')
|
|
|
|
|
|
def _create_vertical_csv(data, file_path):
|
|
"""Create a new CSV table where the fields are in the left column.
|
|
|
|
This CSV table is created with fields / keys running vertically
|
|
down the first column. The second column has the corresponding
|
|
values. This is how Wind Energy csv inputs are expected.
|
|
|
|
Args:
|
|
data (dict): a Dictionary where each key is the name
|
|
of a field and set in the first column. The second
|
|
column is set with the value of that key.
|
|
file_path (string): a file path for the new table to be written to
|
|
disk.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
csv_file = open(file_path, 'w')
|
|
|
|
writer = csv.writer(csv_file)
|
|
for key, val in data.items():
|
|
writer.writerow([key, val])
|
|
|
|
csv_file.close()
|
|
|
|
|
|
class WindEnergyUnitTests(unittest.TestCase):
|
|
"""Unit tests for the Wind Energy module."""
|
|
|
|
def setUp(self):
|
|
"""Overriding setUp func. to create temporary workspace directory."""
|
|
# this lets us delete the workspace after its done no matter the
|
|
# the rest result
|
|
self.workspace_dir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
"""Overriding tearDown function to remove temporary directory."""
|
|
shutil.rmtree(self.workspace_dir)
|
|
|
|
def test_calculate_distances_land_grid(self):
|
|
"""WindEnergy: testing 'calculate_distances_land_grid' function."""
|
|
from natcap.invest import wind_energy
|
|
|
|
srs = osr.SpatialReference()
|
|
srs.ImportFromEPSG(3157)
|
|
projection_wkt = srs.ExportToWkt()
|
|
origin = (443723.127327877911739, 4956546.905980412848294)
|
|
pos_x = origin[0]
|
|
pos_y = origin[1]
|
|
|
|
# Setup parameters for creating point shapefile
|
|
fields = {'id': ogr.OFTReal, 'L2G': ogr.OFTReal}
|
|
attrs = [{'id': 1, 'L2G': 10}, {'id': 2, 'L2G': 20}]
|
|
|
|
geometries = [
|
|
Point(pos_x + 50, pos_y - 50), Point(pos_x + 50, pos_y - 150)]
|
|
land_shape_path = os.path.join(self.workspace_dir, 'temp_shape.shp')
|
|
# Create point shapefile to use for testing input
|
|
pygeoprocessing.shapely_geometry_to_vector(
|
|
geometries, land_shape_path, projection_wkt, 'ESRI Shapefile',
|
|
fields=fields, attribute_list=attrs, ogr_geom_type=ogr.wkbPoint)
|
|
|
|
# Setup parameters for create raster
|
|
matrix = numpy.array([[1, 1, 1, 1], [1, 1, 1, 1]], dtype=numpy.int32)
|
|
harvested_masked_path = os.path.join(
|
|
self.workspace_dir, 'temp_raster.tif')
|
|
# Create raster to use for testing input
|
|
pygeoprocessing.numpy_array_to_raster(
|
|
matrix, -1, (100, -100), origin, projection_wkt,
|
|
harvested_masked_path)
|
|
|
|
tmp_dist_final_path = os.path.join(
|
|
self.workspace_dir, 'dist_final.tif')
|
|
# Call function to test given testing inputs
|
|
wind_energy._calculate_distances_land_grid(
|
|
land_shape_path, harvested_masked_path, tmp_dist_final_path, '')
|
|
|
|
# Compare the results
|
|
res_array = pygeoprocessing.raster_to_numpy_array(tmp_dist_final_path)
|
|
exp_array = numpy.array(
|
|
[[10, 110, 210, 310], [20, 120, 220, 320]], dtype=numpy.int32)
|
|
numpy.testing.assert_allclose(res_array, exp_array)
|
|
|
|
def test_calculate_land_to_grid_distance(self):
|
|
"""WindEnergy: testing 'point_to_polygon_distance' function."""
|
|
from natcap.invest import wind_energy
|
|
|
|
# Setup parameters for creating polygon and point shapefiles
|
|
fields = {'vec_id': ogr.OFTInteger}
|
|
attr_pt = [{'vec_id': 1}, {'vec_id': 2}, {'vec_id': 3}, {'vec_id': 4}]
|
|
attr_poly = [{'vec_id': 1}, {'vec_id': 2}]
|
|
|
|
srs = osr.SpatialReference()
|
|
srs.ImportFromEPSG(3157)
|
|
projection_wkt = srs.ExportToWkt()
|
|
origin = (443723.127327877911739, 4956546.905980412848294)
|
|
pos_x = origin[0]
|
|
pos_y = origin[1]
|
|
|
|
poly_geoms = {
|
|
'poly_1': [(pos_x + 200, pos_y), (pos_x + 250, pos_y),
|
|
(pos_x + 250, pos_y - 100), (pos_x + 200, pos_y - 100),
|
|
(pos_x + 200, pos_y)],
|
|
'poly_2': [(pos_x, pos_y - 150), (pos_x + 100, pos_y - 150),
|
|
(pos_x + 100, pos_y - 200), (pos_x, pos_y - 200),
|
|
(pos_x, pos_y - 150)]}
|
|
|
|
poly_geometries = [
|
|
Polygon(poly_geoms['poly_1']), Polygon(poly_geoms['poly_2'])]
|
|
poly_vector_path = os.path.join(self.workspace_dir, 'poly_shape.shp')
|
|
# Create polygon shapefile to use as testing input
|
|
pygeoprocessing.shapely_geometry_to_vector(
|
|
poly_geometries, poly_vector_path, projection_wkt,
|
|
'ESRI Shapefile', fields=fields, attribute_list=attr_poly,
|
|
ogr_geom_type=ogr.wkbPolygon)
|
|
|
|
point_geometries = [
|
|
Point(pos_x, pos_y), Point(pos_x + 100, pos_y),
|
|
Point(pos_x, pos_y - 100), Point(pos_x + 100, pos_y - 100)]
|
|
point_vector_path = os.path.join(self.workspace_dir, 'point_shape.shp')
|
|
# Create point shapefile to use as testing input
|
|
pygeoprocessing.shapely_geometry_to_vector(
|
|
point_geometries, point_vector_path, projection_wkt,
|
|
'ESRI Shapefile', fields=fields, attribute_list=attr_pt,
|
|
ogr_geom_type=ogr.wkbPoint)
|
|
|
|
target_point_vector_path = os.path.join(
|
|
self.workspace_dir, 'target_point.shp')
|
|
# Call function to test
|
|
field_name = 'L2G'
|
|
wind_energy._calculate_land_to_grid_distance(
|
|
point_vector_path, poly_vector_path, field_name,
|
|
target_point_vector_path)
|
|
|
|
exp_results = [.15, .1, .05, .05]
|
|
|
|
point_vector = gdal.OpenEx(target_point_vector_path)
|
|
point_layer = point_vector.GetLayer()
|
|
field_index = point_layer.GetFeature(0).GetFieldIndex(field_name)
|
|
for i, point_feat in enumerate(point_layer):
|
|
result_val = point_feat.GetField(field_index)
|
|
numpy.testing.assert_allclose(result_val, exp_results[i])
|
|
|
|
def test_wind_data_to_point_vector(self):
|
|
"""WindEnergy: testing 'wind_data_to_point_vector' function."""
|
|
from natcap.invest import wind_energy
|
|
|
|
wind_data = {
|
|
(31.79, 123.76): {
|
|
'LONG': 123.76, 'LATI': 31.79, 'Ram-080m': 7.98,
|
|
'K-010m': 1.90}
|
|
}
|
|
wind_data_pickle_path = os.path.join(
|
|
self.workspace_dir, 'wind_data.pickle')
|
|
with open(wind_data_pickle_path, 'wb') as file:
|
|
pickle.dump(wind_data, file)
|
|
|
|
layer_name = "datatopoint"
|
|
out_path = os.path.join(self.workspace_dir, 'datatopoint.shp')
|
|
|
|
wind_energy._wind_data_to_point_vector(
|
|
wind_data_pickle_path, layer_name, out_path)
|
|
|
|
field_names = ['LONG', 'LATI', 'Ram-080m', 'K-010m']
|
|
ogr_point = ogr.Geometry(ogr.wkbPoint)
|
|
ogr_point.AddPoint_2D(123.76, 31.79)
|
|
|
|
shape = ogr.Open(out_path)
|
|
layer = shape.GetLayer()
|
|
|
|
feat = layer.GetNextFeature()
|
|
while feat is not None:
|
|
|
|
geom = feat.GetGeometryRef()
|
|
if bool(geom.Equals(ogr_point)) is False:
|
|
raise AssertionError(
|
|
'Geometries are not equal. Expected is: %s, '
|
|
'but current is %s' % (ogr_point, geom))
|
|
|
|
for field in field_names:
|
|
try:
|
|
field_val = feat.GetField(field)
|
|
self.assertEqual(
|
|
wind_data[(31.79, 123.76)][field], field_val)
|
|
except ValueError:
|
|
raise AssertionError(
|
|
'Could not find field %s' % field)
|
|
|
|
feat = layer.GetNextFeature()
|
|
|
|
def test_wind_data_to_point_vector_360(self):
|
|
"""WindEnergy: testing 'wind_data_to_point_vector' function.
|
|
|
|
This test is to test that when Longitude values range from -360 to 0,
|
|
instead of the normal -180 to 180, they are handled properly.
|
|
"""
|
|
from natcap.invest import wind_energy
|
|
|
|
# Set up a coordinate with a longitude in the range of -360 to 0.
|
|
wind_data = {
|
|
(31.79, -200): {
|
|
'LONG': -200, 'LATI': 31.79, 'Ram-080m': 7.98,
|
|
'K-010m': 1.90}
|
|
}
|
|
wind_data_pickle_path = os.path.join(
|
|
self.workspace_dir, 'wind_data.pickle')
|
|
with open(wind_data_pickle_path, 'wb') as file:
|
|
pickle.dump(wind_data, file)
|
|
|
|
layer_name = "datatopoint"
|
|
out_path = os.path.join(self.workspace_dir, 'datatopoint.shp')
|
|
|
|
wind_energy._wind_data_to_point_vector(
|
|
wind_data_pickle_path, layer_name, out_path)
|
|
|
|
field_names = ['LONG', 'LATI', 'Ram-080m', 'K-010m']
|
|
ogr_point = ogr.Geometry(ogr.wkbPoint)
|
|
# Point geometry should have been converted to the WSG84 norm of
|
|
# -180 to 180
|
|
ogr_point.AddPoint_2D(160, 31.79)
|
|
|
|
shape = ogr.Open(out_path)
|
|
layer = shape.GetLayer()
|
|
|
|
feat = layer.GetNextFeature()
|
|
while feat is not None:
|
|
|
|
geom = feat.GetGeometryRef()
|
|
if bool(geom.Equals(ogr_point)) is False:
|
|
raise AssertionError(
|
|
'Geometries are not equal. Expected is: %s, '
|
|
'but current is %s' % (ogr_point, geom))
|
|
|
|
for field in field_names:
|
|
try:
|
|
field_val = feat.GetField(field)
|
|
self.assertEqual(
|
|
wind_data[(31.79, -200)][field], field_val)
|
|
except ValueError:
|
|
raise AssertionError(
|
|
'Could not find field %s' % field)
|
|
|
|
feat = layer.GetNextFeature()
|
|
|
|
def test_create_distance_raster(self):
|
|
"""WindEnergy: testing '_create_distance_raster' function."""
|
|
from natcap.invest import wind_energy
|
|
|
|
srs = osr.SpatialReference()
|
|
srs.ImportFromEPSG(3157) # UTM Zone 10N
|
|
projection_wkt = srs.ExportToWkt()
|
|
origin = (443723.127327877911739, 4956546.905980412848294)
|
|
pos_x = origin[0]
|
|
pos_y = origin[1]
|
|
|
|
# Setup and create vector to pass to function
|
|
fields = {'id': ogr.OFTReal}
|
|
attrs = [{'id': 1}]
|
|
|
|
# Square polygon that will overlap the 4 pixels of the raster in the
|
|
# upper left corner
|
|
poly_geometry = [box(pos_x, pos_y - 17, pos_x + 17, pos_y)]
|
|
poly_vector_path = os.path.join(
|
|
self.workspace_dir, 'distance_from_vector.gpkg')
|
|
# Create polygon shapefile to use as testing input
|
|
pygeoprocessing.shapely_geometry_to_vector(
|
|
poly_geometry, poly_vector_path, projection_wkt,
|
|
'GPKG', fields=fields, attribute_list=attrs,
|
|
ogr_geom_type=ogr.wkbPolygon)
|
|
|
|
# Create 2x5 raster
|
|
matrix = numpy.array(
|
|
[[1, 1, 1, 1, 1], [1, 1, 1, 1, 1]], dtype=numpy.float32)
|
|
base_raster_path = os.path.join(self.workspace_dir, 'temp_raster.tif')
|
|
# Create raster to use for testing input
|
|
pygeoprocessing.numpy_array_to_raster(
|
|
matrix, -1, (10, -10), origin, projection_wkt, base_raster_path)
|
|
|
|
dist_raster_path = os.path.join(self.workspace_dir, 'dist.tif')
|
|
# Call function to test given testing inputs
|
|
wind_energy._create_distance_raster(
|
|
base_raster_path, poly_vector_path, dist_raster_path,
|
|
self.workspace_dir)
|
|
|
|
# Compare the results
|
|
res_array = pygeoprocessing.raster_to_numpy_array(dist_raster_path)
|
|
exp_array = numpy.array(
|
|
[[0, 0, 10, 20, 30], [0, 0, 10, 20, 30]], dtype=numpy.float32)
|
|
numpy.testing.assert_allclose(res_array, exp_array)
|
|
|
|
def test_calculate_npv_levelized_rasters(self):
|
|
"""WindEnergy: testing '_calculate_npv_levelized_rasters' function."""
|
|
from natcap.invest import wind_energy
|
|
|
|
val_parameters_dict = {
|
|
'air_density': 1.225,
|
|
'exponent_power_curve': 2,
|
|
'decommission_cost': 0.03,
|
|
'operation_maintenance_cost': 0.03,
|
|
'miscellaneous_capex_cost': 0.05,
|
|
'installation_cost': 0.2,
|
|
'infield_cable_length': 0.9,
|
|
'infield_cable_cost': 260000,
|
|
'mw_coef_ac': 810000,
|
|
'mw_coef_dc': 1090000,
|
|
'cable_coef_ac': 1360000,
|
|
'cable_coef_dc': 890000,
|
|
'ac_dc_distance_break': 60,
|
|
'time_period': 5,
|
|
'rotor_diameter_factor': 7,
|
|
'carbon_coefficient': 6.90E-04,
|
|
'air_density_coefficient': 1.19E-04,
|
|
'loss_parameter': 0.05,
|
|
'turbine_cost': 10000,
|
|
'turbine_rated_pwr': 5,
|
|
'foundation_cost': 1000000,
|
|
'discount_rate': 0.01,
|
|
'number_of_turbines': 10
|
|
}
|
|
|
|
price_list = [0.10, 0.10, 0.10, 0.10, 0.10]
|
|
|
|
srs = osr.SpatialReference()
|
|
srs.ImportFromEPSG(3157) # UTM Zone 10N
|
|
projection_wkt = srs.ExportToWkt()
|
|
origin = (443723.127327877911739, 4956546.905980412848294)
|
|
|
|
# Create harvested raster
|
|
harvest_val = 1000000
|
|
harvest_matrix = numpy.array(
|
|
[[harvest_val, harvest_val + 1e5, harvest_val + 2e5,
|
|
harvest_val + 3e5, harvest_val + 4e5],
|
|
[harvest_val, harvest_val + 1e5, harvest_val + 2e5,
|
|
harvest_val + 3e5, harvest_val + 4e5]],
|
|
dtype=numpy.float32)
|
|
base_harvest_path = os.path.join(self.workspace_dir, 'harvest_raster.tif')
|
|
# Create raster to use for testing input
|
|
pygeoprocessing.numpy_array_to_raster(
|
|
harvest_matrix, -1, (10, -10), origin, projection_wkt, base_harvest_path)
|
|
# Create distance raster
|
|
dist_matrix = numpy.array(
|
|
[[0, 10, 20, 30, 40], [0, 10, 20, 30, 40]], dtype=numpy.float32)
|
|
base_distance_path = os.path.join(self.workspace_dir, 'dist_raster.tif')
|
|
# Create raster to use for testing input
|
|
pygeoprocessing.numpy_array_to_raster(
|
|
dist_matrix, -1, (10, -10), origin, projection_wkt, base_distance_path)
|
|
|
|
target_npv_raster_path = os.path.join(self.workspace_dir, 'npv.tif')
|
|
target_levelized_raster_path = os.path.join(
|
|
self.workspace_dir, 'levelized.tif')
|
|
# Call function to test given testing inputs
|
|
wind_energy._calculate_npv_levelized_rasters(
|
|
base_harvest_path, base_distance_path,
|
|
target_npv_raster_path, target_levelized_raster_path,
|
|
val_parameters_dict, price_list)
|
|
|
|
# Compare the results that were "eye" tested.
|
|
desired_npv_array = numpy.array(
|
|
[[309332320.0, 348331200.0, 387330020.0, 426328930.0,
|
|
465327800.0],
|
|
[309332320.0, 348331200.0, 387330020.0, 426328930.0,
|
|
465327800.0]], dtype=numpy.float32)
|
|
actual_npv_array = pygeoprocessing.raster_to_numpy_array(
|
|
target_npv_raster_path)
|
|
numpy.testing.assert_allclose(actual_npv_array, desired_npv_array)
|
|
|
|
desired_levelized_array = numpy.array(
|
|
[[0.016496297, 0.015000489, 0.0137539795, 0.01269924, 0.011795178],
|
|
[0.016496297, 0.015000489, 0.0137539795, 0.01269924, 0.011795178]],
|
|
dtype=numpy.float32)
|
|
actual_levelized_array = pygeoprocessing.raster_to_numpy_array(
|
|
target_levelized_raster_path)
|
|
numpy.testing.assert_allclose(
|
|
actual_levelized_array, desired_levelized_array)
|
|
|
|
|
|
class WindEnergyRegressionTests(unittest.TestCase):
|
|
"""Regression tests for the Wind Energy module."""
|
|
|
|
def setUp(self):
|
|
"""Override setUp function to create temporary workspace directory."""
|
|
# this lets us delete the workspace after its done no matter the
|
|
# the rest result
|
|
self.workspace_dir = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
"""Override tearDown function to remove temporary directory."""
|
|
shutil.rmtree(self.workspace_dir)
|
|
|
|
@staticmethod
|
|
def generate_base_args(workspace_dir):
|
|
"""Generate an args list that is consistent across regression tests."""
|
|
args = {
|
|
'workspace_dir': workspace_dir,
|
|
'wind_data_path': os.path.join(
|
|
SAMPLE_DATA, 'resampled_wind_points.csv'),
|
|
'bathymetry_path': os.path.join(
|
|
SAMPLE_DATA, 'resampled_global_dem_unprojected.tif'),
|
|
'global_wind_parameters_path': os.path.join(
|
|
SAMPLE_DATA, 'global_wind_energy_parameters.csv'),
|
|
'turbine_parameters_path': os.path.join(
|
|
SAMPLE_DATA, '3_6_turbine.csv'),
|
|
'number_of_turbines': '80', # pass str to test casting
|
|
'min_depth': '3', # pass str to test casting
|
|
'max_depth': 180,
|
|
'n_workers': -1
|
|
}
|
|
|
|
return args
|
|
|
|
def test_no_aoi(self):
|
|
"""WindEnergy: testing base case w/o AOI, distances, or valuation."""
|
|
from natcap.invest import wind_energy
|
|
from natcap.invest.utils import _assert_vectors_equal
|
|
|
|
args = WindEnergyRegressionTests.generate_base_args(self.workspace_dir)
|
|
# Also test on input bathymetry that has equal x, y pixel sizes
|
|
args['bathymetry_path'] = os.path.join(
|
|
SAMPLE_DATA, 'resampled_global_dem_equal_pixel.tif')
|
|
|
|
wind_energy.execute(args)
|
|
|
|
raster_results = [
|
|
'density_W_per_m2.tif', 'harvested_energy_MWhr_per_yr.tif']
|
|
|
|
for raster_path in raster_results:
|
|
model_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(args['workspace_dir'], 'output', raster_path))
|
|
reg_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(REGRESSION_DATA, 'noaoi', raster_path))
|
|
numpy.testing.assert_allclose(model_array, reg_array)
|
|
|
|
vector_path = 'wind_energy_points.shp'
|
|
|
|
_assert_vectors_equal(
|
|
os.path.join(args['workspace_dir'], 'output', vector_path),
|
|
os.path.join(REGRESSION_DATA, 'noaoi', vector_path))
|
|
|
|
def test_no_land_polygon(self):
|
|
"""WindEnergy: testing case w/ AOI but w/o land poly or distances."""
|
|
from natcap.invest import wind_energy
|
|
from natcap.invest.utils import _assert_vectors_equal
|
|
|
|
args = WindEnergyRegressionTests.generate_base_args(self.workspace_dir)
|
|
args['aoi_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'New_England_US_Aoi.shp')
|
|
|
|
wind_energy.execute(args)
|
|
|
|
raster_results = [
|
|
'density_W_per_m2.tif', 'harvested_energy_MWhr_per_yr.tif']
|
|
|
|
for raster_path in raster_results:
|
|
model_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(args['workspace_dir'], 'output', raster_path))
|
|
reg_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(REGRESSION_DATA, 'nolandpoly', raster_path))
|
|
numpy.testing.assert_allclose(model_array, reg_array)
|
|
|
|
vector_path = 'wind_energy_points.shp'
|
|
|
|
_assert_vectors_equal(
|
|
os.path.join(args['workspace_dir'], 'output', vector_path),
|
|
os.path.join(REGRESSION_DATA, 'nolandpoly', vector_path))
|
|
|
|
def test_no_distances(self):
|
|
"""WindEnergy: testing case w/ AOI and land poly, but w/o distances."""
|
|
from natcap.invest import wind_energy
|
|
from natcap.invest.utils import _assert_vectors_equal
|
|
|
|
args = WindEnergyRegressionTests.generate_base_args(self.workspace_dir)
|
|
args['aoi_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'New_England_US_Aoi.shp')
|
|
args['land_polygon_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'simple_north_america_polygon.shp')
|
|
|
|
wind_energy.execute(args)
|
|
|
|
raster_results = [
|
|
'density_W_per_m2.tif', 'harvested_energy_MWhr_per_yr.tif']
|
|
|
|
for raster_path in raster_results:
|
|
model_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(args['workspace_dir'], 'output', raster_path))
|
|
reg_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(REGRESSION_DATA, 'nodistances', raster_path))
|
|
numpy.testing.assert_allclose(model_array, reg_array)
|
|
|
|
vector_path = 'wind_energy_points.shp'
|
|
|
|
_assert_vectors_equal(
|
|
os.path.join(args['workspace_dir'], 'output', vector_path),
|
|
os.path.join(REGRESSION_DATA, 'nodistances', vector_path))
|
|
|
|
def test_val_gridpts_windprice(self):
|
|
"""WindEnergy: testing Valuation w/ grid pts and wind price."""
|
|
from natcap.invest import wind_energy
|
|
from natcap.invest.utils import _assert_vectors_equal
|
|
|
|
args = WindEnergyRegressionTests.generate_base_args(self.workspace_dir)
|
|
args['aoi_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'New_England_US_Aoi.shp')
|
|
args['land_polygon_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'simple_north_america_polygon.shp')
|
|
args['min_distance'] = 0
|
|
args['max_distance'] = 200000
|
|
args['valuation_container'] = True
|
|
args['foundation_cost'] = 2000000
|
|
args['discount_rate'] = '0.07' # pass str to test casting
|
|
# Test that only grid points are provided in grid_points_path
|
|
args['grid_points_path'] = os.path.join(
|
|
SAMPLE_DATA, 'resampled_grid_pts.csv')
|
|
args['price_table'] = False
|
|
args['wind_price'] = 0.187
|
|
args['rate_change'] = '0.2' # pass str to test casting
|
|
|
|
wind_energy.execute(args)
|
|
|
|
# Make sure the output files were created.
|
|
vector_path = 'wind_energy_points.shp'
|
|
self.assertTrue(os.path.exists(
|
|
os.path.join(args['workspace_dir'], 'output', vector_path)))
|
|
|
|
# Run through the model again, which should mean deleting shapefiles
|
|
# that have already been made, but which need to be created again.
|
|
wind_energy.execute(args)
|
|
|
|
raster_results = [
|
|
'carbon_emissions_tons.tif',
|
|
'levelized_cost_price_per_kWh.tif', 'npv.tif']
|
|
|
|
for raster_path in raster_results:
|
|
model_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(args['workspace_dir'], 'output', raster_path))
|
|
reg_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(REGRESSION_DATA, 'pricevalgrid', raster_path))
|
|
numpy.testing.assert_allclose(model_array, reg_array)
|
|
|
|
vector_path = 'wind_energy_points.shp'
|
|
|
|
_assert_vectors_equal(
|
|
os.path.join(args['workspace_dir'], 'output', vector_path),
|
|
os.path.join(REGRESSION_DATA, 'pricevalgrid', vector_path))
|
|
|
|
def test_val_land_grid_points(self):
|
|
"""WindEnergy: testing Valuation w/ grid/land pts and wind price."""
|
|
from natcap.invest import wind_energy
|
|
from natcap.invest.utils import _assert_vectors_equal
|
|
args = WindEnergyRegressionTests.generate_base_args(self.workspace_dir)
|
|
|
|
args['aoi_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'New_England_US_Aoi.shp')
|
|
args['land_polygon_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'simple_north_america_polygon.shp')
|
|
args['min_distance'] = 0
|
|
args['max_distance'] = 200000
|
|
args['valuation_container'] = True
|
|
args['foundation_cost'] = 2000000
|
|
args['discount_rate'] = 0.07
|
|
# there was no sample data that provided landing points, thus for
|
|
# testing, grid points in 'resampled_grid_pts.csv' were duplicated and
|
|
# marked as land points. So the distances will be zero, keeping the
|
|
# result the same but testing that section of code
|
|
args['grid_points_path'] = os.path.join(
|
|
SAMPLE_DATA, 'resampled_grid_land_pts.csv')
|
|
args['price_table'] = False
|
|
args['wind_price'] = 0.187
|
|
args['rate_change'] = 0.2
|
|
|
|
wind_energy.execute(args)
|
|
|
|
raster_results = [
|
|
'carbon_emissions_tons.tif', 'levelized_cost_price_per_kWh.tif',
|
|
'npv.tif']
|
|
|
|
for raster_path in raster_results:
|
|
model_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(args['workspace_dir'], 'output', raster_path))
|
|
reg_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(REGRESSION_DATA, 'pricevalgridland', raster_path))
|
|
# loosened tolerance to pass against GDAL 2.2.4 and 2.4.1
|
|
numpy.testing.assert_allclose(
|
|
model_array, reg_array, rtol=1e-04)
|
|
|
|
vector_path = 'wind_energy_points.shp'
|
|
_assert_vectors_equal(
|
|
os.path.join(args['workspace_dir'], 'output', vector_path),
|
|
os.path.join(REGRESSION_DATA, 'pricevalgridland', vector_path))
|
|
|
|
def test_val_no_grid_land_pts(self):
|
|
"""WindEnergy: testing Valuation without grid or land points."""
|
|
from natcap.invest import wind_energy
|
|
from natcap.invest.utils import _assert_vectors_equal
|
|
args = WindEnergyRegressionTests.generate_base_args(self.workspace_dir)
|
|
# Also use an already projected bathymetry
|
|
args['bathymetry_path'] = os.path.join(
|
|
SAMPLE_DATA, 'resampled_global_dem_projected.tif')
|
|
args['aoi_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'New_England_US_Aoi.shp')
|
|
args['land_polygon_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'simple_north_america_polygon.shp')
|
|
args['min_distance'] = 0
|
|
args['max_distance'] = 200000
|
|
args['valuation_container'] = True
|
|
args['foundation_cost'] = 2000000
|
|
args['discount_rate'] = 0.07
|
|
args['price_table'] = True
|
|
args['wind_schedule'] = os.path.join(
|
|
SAMPLE_DATA, 'price_table_example.csv')
|
|
args['wind_price'] = 0.187
|
|
args['rate_change'] = 0.2
|
|
args['avg_grid_distance'] = 4
|
|
|
|
wind_energy.execute(args)
|
|
|
|
raster_results = [
|
|
'carbon_emissions_tons.tif', 'levelized_cost_price_per_kWh.tif',
|
|
'npv.tif']
|
|
|
|
for raster_path in raster_results:
|
|
model_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(args['workspace_dir'], 'output', raster_path))
|
|
reg_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(REGRESSION_DATA, 'priceval', raster_path))
|
|
numpy.testing.assert_allclose(model_array, reg_array, rtol=1e-6)
|
|
|
|
vector_path = 'wind_energy_points.shp'
|
|
_assert_vectors_equal(
|
|
os.path.join(args['workspace_dir'], 'output', vector_path),
|
|
os.path.join(REGRESSION_DATA, 'priceval', vector_path))
|
|
|
|
def test_valuation_taskgraph(self):
|
|
"""WindEnergy: testing Valuation with async TaskGraph."""
|
|
from natcap.invest import wind_energy
|
|
from natcap.invest.utils import _assert_vectors_equal
|
|
args = WindEnergyRegressionTests.generate_base_args(self.workspace_dir)
|
|
# Also use an already projected bathymetry
|
|
args['bathymetry_path'] = os.path.join(
|
|
SAMPLE_DATA, 'resampled_global_dem_projected.tif')
|
|
args['aoi_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'New_England_US_Aoi.shp')
|
|
args['land_polygon_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'simple_north_america_polygon.shp')
|
|
args['min_distance'] = 0
|
|
args['max_distance'] = 200000
|
|
args['valuation_container'] = True
|
|
args['foundation_cost'] = 2000000
|
|
args['discount_rate'] = 0.07
|
|
args['price_table'] = True
|
|
args['wind_schedule'] = os.path.join(
|
|
SAMPLE_DATA, 'price_table_example.csv')
|
|
args['wind_price'] = 0.187
|
|
args['rate_change'] = 0.2
|
|
args['avg_grid_distance'] = 4
|
|
args['n_workers'] = 1
|
|
|
|
wind_energy.execute(args)
|
|
|
|
raster_results = [
|
|
'carbon_emissions_tons.tif', 'levelized_cost_price_per_kWh.tif',
|
|
'npv.tif']
|
|
|
|
for raster_path in raster_results:
|
|
model_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(args['workspace_dir'], 'output', raster_path))
|
|
reg_array = pygeoprocessing.raster_to_numpy_array(
|
|
os.path.join(REGRESSION_DATA, 'priceval', raster_path))
|
|
numpy.testing.assert_allclose(model_array, reg_array, rtol=1e-6)
|
|
|
|
vector_path = 'wind_energy_points.shp'
|
|
_assert_vectors_equal(
|
|
os.path.join(args['workspace_dir'], 'output', vector_path),
|
|
os.path.join(REGRESSION_DATA, 'priceval', vector_path))
|
|
|
|
def test_field_error_missing_bio_param(self):
|
|
"""WindEnergy: test that validation catches missing bio param."""
|
|
from natcap.invest import wind_energy
|
|
|
|
# for testing raised exceptions, running on a set of data that was
|
|
# created by hand and has no numerical validity. Helps test the
|
|
# raised exception quicker
|
|
args = {
|
|
'workspace_dir': self.workspace_dir,
|
|
'wind_data_path': os.path.join(
|
|
REGRESSION_DATA, 'smoke', 'wind_data_smoke.csv'),
|
|
'bathymetry_path': os.path.join(
|
|
REGRESSION_DATA, 'smoke', 'dem_smoke.tif'),
|
|
'global_wind_parameters_path': os.path.join(
|
|
SAMPLE_DATA, 'global_wind_energy_parameters.csv'),
|
|
'number_of_turbines': 80,
|
|
'min_depth': 3,
|
|
'max_depth': 200,
|
|
'aoi_vector_path': os.path.join(
|
|
REGRESSION_DATA, 'smoke', 'aoi_smoke.shp'),
|
|
'land_polygon_vector_path': os.path.join(
|
|
REGRESSION_DATA, 'smoke', 'landpoly_smoke.shp'),
|
|
'min_distance': 0,
|
|
'max_distance': 200000
|
|
}
|
|
|
|
# creating a stand in turbine parameter csv file that is missing
|
|
# the 'cut_out_wspd' entry.
|
|
tmp, file_path = tempfile.mkstemp(
|
|
suffix='.csv', dir=args['workspace_dir'])
|
|
os.close(tmp)
|
|
data = {
|
|
'hub_height': 80, 'cut_in_wspd': 4, 'rated_wspd': 12.5,
|
|
'turbine_rated_pwr': 3.6, 'turbine_cost': 8
|
|
}
|
|
_create_vertical_csv(data, file_path)
|
|
args['turbine_parameters_path'] = file_path
|
|
|
|
validation_messages = wind_energy.validate(args)
|
|
self.assertEqual(len(validation_messages), 1)
|
|
|
|
def test_time_period_exception(self):
|
|
"""WindEnergy: validation message if 'time' and 'wind_sched' differ."""
|
|
from natcap.invest import wind_energy
|
|
|
|
# for testing raised exceptions, running on a set of data that was
|
|
# created by hand and has no numerical validity. Helps test the
|
|
# raised exception quicker
|
|
args = {
|
|
'workspace_dir': self.workspace_dir,
|
|
'wind_data_path': os.path.join(
|
|
REGRESSION_DATA, 'smoke', 'wind_data_smoke.csv'),
|
|
'bathymetry_path': os.path.join(
|
|
REGRESSION_DATA, 'smoke', 'dem_smoke.tif'),
|
|
'turbine_parameters_path': os.path.join(
|
|
SAMPLE_DATA, '3_6_turbine.csv'),
|
|
'number_of_turbines': 80,
|
|
'min_depth': 3,
|
|
'max_depth': 200,
|
|
'aoi_vector_path': os.path.join(
|
|
REGRESSION_DATA, 'smoke', 'aoi_smoke.shp'),
|
|
'land_polygon_vector_path': os.path.join(
|
|
REGRESSION_DATA, 'smoke', 'landpoly_smoke.shp'),
|
|
'min_distance': 0,
|
|
'max_distance': 200000,
|
|
'valuation_container': True,
|
|
'foundation_cost': 2000000,
|
|
'discount_rate': 0.07,
|
|
'avg_grid_distance': 4,
|
|
'price_table': True,
|
|
'wind_schedule': os.path.join(
|
|
SAMPLE_DATA, 'price_table_example.csv'),
|
|
'results_suffix': '_test'
|
|
}
|
|
|
|
# creating a stand in global wind params table that has a different
|
|
# 'time' value than what is given in the wind schedule table.
|
|
# This should raise the exception
|
|
tmp, file_path = tempfile.mkstemp(
|
|
suffix='.csv', dir=args['workspace_dir'])
|
|
os.close(tmp)
|
|
data = {
|
|
'air_density': 1.225, 'exponent_power_curve': 2,
|
|
'decommission_cost': .037, 'operation_maintenance_cost': .035,
|
|
'miscellaneous_capex_cost': .05, 'installation_cost': .20,
|
|
'infield_cable_length': 0.91, 'infield_cable_cost': 0.26,
|
|
'mw_coef_ac': .81, 'mw_coef_dc': 1.09, 'cable_coef_ac': 1.36,
|
|
'cable_coef_dc': .89, 'ac_dc_distance_break': 60,
|
|
'time_period': 10, 'rotor_diameter_factor': 7,
|
|
'carbon_coefficient': 6.8956e-4,
|
|
'air_density_coefficient': 1.194e-4, 'loss_parameter': .05
|
|
}
|
|
_create_vertical_csv(data, file_path)
|
|
args['global_wind_parameters_path'] = file_path
|
|
validation_messages = wind_energy.validate(args)
|
|
self.assertEqual(len(validation_messages), 1)
|
|
|
|
def test_clip_vector_value_error(self):
|
|
"""WindEnergy: Test AOI doesn't intersect Wind Data points."""
|
|
from natcap.invest import wind_energy
|
|
|
|
args = WindEnergyRegressionTests.generate_base_args(self.workspace_dir)
|
|
args['aoi_vector_path'] = os.path.join(
|
|
SAMPLE_DATA, 'New_England_US_Aoi.shp')
|
|
|
|
# Make up some Wind Data points that live outside AOI
|
|
wind_data_csv = os.path.join(
|
|
args['workspace_dir'], 'temp-wind-data.csv')
|
|
with open(wind_data_csv, 'w') as open_table:
|
|
open_table.write('LONG,LATI,LAM,K,REF\n')
|
|
open_table.write('-60.5,25.0,7.59,2.6,10\n')
|
|
open_table.write('-59.5,24.0,7.59,2.6,10\n')
|
|
open_table.write('-58.5,24.5,7.59,2.6,10\n')
|
|
open_table.write('-58.95,24.95,7.59,2.6,10\n')
|
|
open_table.write('-57.95,24.95,7.59,2.6,10\n')
|
|
open_table.write('-57.95,25.95,7.59,2.6,10\n')
|
|
|
|
args['wind_data_path'] = wind_data_csv
|
|
|
|
# AOI and wind data should not overlap, leading to a ValueError in
|
|
# clip_vector_by_vector
|
|
with self.assertRaises(ValueError) as cm:
|
|
wind_energy.execute(args)
|
|
|
|
self.assertTrue(
|
|
"returned 0 features. This means the AOI " in str(cm.exception))
|
|
|
|
|
|
class WindEnergyValidationTests(unittest.TestCase):
|
|
"""Tests for the Wind Energy Model MODEL_SPEC and validation."""
|
|
|
|
def setUp(self):
|
|
"""Setup a list of required keys."""
|
|
self.base_required_keys = [
|
|
'workspace_dir',
|
|
'number_of_turbines',
|
|
'min_depth',
|
|
'max_depth',
|
|
'turbine_parameters_path',
|
|
'bathymetry_path',
|
|
'global_wind_parameters_path',
|
|
'wind_data_path'
|
|
]
|
|
|
|
def test_missing_keys(self):
|
|
"""Wind Energy Validate: assert missing required keys."""
|
|
from natcap.invest import wind_energy
|
|
from natcap.invest import validation
|
|
|
|
validation_errors = wind_energy.validate({}) # empty args dict.
|
|
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_with_valuation(self):
|
|
"""Wind Energy Validate: assert missing required for valuation."""
|
|
from natcap.invest import wind_energy
|
|
from natcap.invest import validation
|
|
|
|
base_required_valuation = ['land_polygon_vector_path',
|
|
'min_distance',
|
|
'max_distance',
|
|
'foundation_cost',
|
|
'discount_rate']
|
|
required_no_price_table = ['wind_price', 'rate_change']
|
|
required_no_grid_points = ['avg_grid_distance']
|
|
required_no_grid_distance = ['grid_points_path']
|
|
|
|
# Test that many args become required for valuation.
|
|
args = {'valuation_container': True}
|
|
validation_errors = wind_energy.validate(args)
|
|
invalid_keys = validation.get_invalid_keys(validation_errors)
|
|
expected_missing_keys = set(
|
|
self.base_required_keys +
|
|
base_required_valuation +
|
|
['price_table'] +
|
|
required_no_price_table +
|
|
required_no_grid_distance +
|
|
required_no_grid_points)
|
|
self.assertEqual(invalid_keys, expected_missing_keys)
|
|
|
|
# Test wind_price, rate_change are not required if price_table
|
|
args = {
|
|
'valuation_container': True,
|
|
'price_table': True
|
|
}
|
|
validation_errors = wind_energy.validate(args)
|
|
invalid_keys = validation.get_invalid_keys(validation_errors)
|
|
expected_missing_keys = set(
|
|
self.base_required_keys +
|
|
base_required_valuation +
|
|
['wind_schedule'] + # required when price_table
|
|
required_no_grid_distance +
|
|
required_no_grid_points)
|
|
self.assertEqual(invalid_keys, expected_missing_keys)
|
|
|
|
# Test grid_points_path is not required if avg_grid_distance:
|
|
args = {
|
|
'valuation_container': True,
|
|
'avg_grid_distance': 9
|
|
}
|
|
validation_errors = wind_energy.validate(args)
|
|
invalid_keys = validation.get_invalid_keys(validation_errors)
|
|
expected_missing_keys = set(
|
|
self.base_required_keys +
|
|
base_required_valuation +
|
|
['price_table'] +
|
|
required_no_price_table)
|
|
self.assertEqual(invalid_keys, expected_missing_keys)
|
|
|
|
# TestAOI becomes required when these two args present:
|
|
args = {
|
|
'valuation_container': True,
|
|
'grid_points_path': 'foo.shp'
|
|
}
|
|
validation_errors = wind_energy.validate(args)
|
|
invalid_keys = validation.get_invalid_keys(validation_errors)
|
|
expected_missing_keys = set(
|
|
self.base_required_keys +
|
|
base_required_valuation +
|
|
['price_table'] +
|
|
required_no_price_table +
|
|
['grid_points_path', 'aoi_vector_path'])
|
|
self.assertEqual(invalid_keys, expected_missing_keys)
|