invest/tests/test_wind_energy.py

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)