Using SQL, I calculate the area of zones in which wind power is not permitted based on wind siting policy parameters. Using the remaining available area, I calculate the energy production potential for wind-siting zones in Iowa.
This analysis comes from an assignment I completed in my Masters of Environmental Data Science course, EDS223 Spatial Analysis (ESM 267 Advanced GIS). I completed it with my course partner and MESM counterpart, Meghan Fletcher.
1.a. Load Libraries
Identify Land Suitable for Turbine Placement
2.a.Buildings
2.b. Airports
2.c. Military
2.d. Nature Reserves, Parks, and Wetlands
2.f. Waterbodies
2.g. Power Lines
2.h. Power Plants
2.i. Wind Turbines
2.j. Merge the Subqueries
Number of Turbines Per Polygon
4.a. Scenario 1
4.b Scenario 2
In this assignment, we will be taking spatial data from the state of Iowa and determining how much land could be utilized for wind turbine placement given specified siting constraints. From this analysis, we will then determine the overall amount of energy that could be produced under two different scenarios involving two different placement scenarios determined by distance from residential buldings (3H and 10 H, defined in the Parameters section). The setup for this assignment is broken down into five sections, linked in the table of contents above.
First, we need to read in the various libraries necessary for performing the spatial analysis required throughout the assignment:
import sqlalchemy
import geopandas
import psycopg2
import pandas
import geopandas
import math
Next, we need to connect to the postGIS database in order to pull in data from the the osmiowa dataset which we will be analyzing in order to produce our final results.
= 'postgresql+psycopg2://{user}:{pwd}@{host}/{db_name}'
pg_uri_template
= pg_uri_template.format(
db_uri = '128.111.89.111',
host = 'eds223_students',
user = 'eds223',
pwd = 'osmiowa'
db_name )
= sqlalchemy.create_engine(db_uri) db
Finally, to simplify the process of identifying suitable land for wind turbine placement using subqueries, we must set the parameters based on the siting constraints assigned to the various features being used within our analysis.
= 7500 #meters
airport_buff = 150 #meters
h = h * 3
h3 = h * 10
h10 = h * 2
h2 = 136 #meters
d = math.pi * (d * 5)**2 #square meters turbine_footprint
## 2. Identify Land Suitable for Turbine Placement
To identify suitable land, we run SQL queries which select all land unsuitable for wind turbine placement and create suitable buffers around those sites set by wind turbine siting constraints. We then assign those queries to a variable to call in a later part of the notebook where we merge the subqueries and create a geodataframe of all the sites where wind turbines cannot go.
For the buildings queries, we run two queries under two different scenarios: one where the siting constraint specified turbines must be at least 3 times the turbine height in distance from residential buildings, and one where they must be 10 times the turbine height in distance. We also have a query for nonresidential buildings.
Scenario 1: Looking at residential buildings that would be at leasy 3H from turbines:
= """ SELECT osm_id, ST_BUFFER(way, {h}) as way
sql_buildings_residential FROM planet_osm_polygon
WHERE building in ('yes', 'residential', 'apartments', 'house', 'static_caravan', 'detached')
OR landuse = 'residential'
OR place = 'town'"""
# In order to properly create the buffer we need to format each parameter as follows
= sql_buildings_residential.format(h = h3) sql_buildings_residential_1
Scenario 2: Looking at residential buildings that would be at leasy 10H from turbines:
#same query as above, but with updated buffer
= sql_buildings_residential.format(h=h10) sql_buildings_residential_2
= f"""SELECT osm_id, ST_BUFFER(way, {h3}) as way
sql_buildings_nonres FROM planet_osm_polygon
WHERE building not in ('yes', 'residential', 'apartments', 'house', 'static_caravan', 'detached')
AND building IS NOT NULL"""
= f"""SELECT osm_id, ST_BUFFER(way, {airport_buff}) as way
sql_airports FROM planet_osm_polygon
WHERE aeroway is not NULL"""
= f"""SELECT osm_id, way
sql_military FROM planet_osm_polygon
WHERE military is not NULL
OR landuse = 'military'"""
= f"""SELECT osm_id, way
sql_nature FROM planet_osm_polygon
WHERE leisure in ('nature_reserve', 'park')
OR planet_osm_polygon.natural = 'wetland'"""
= f"""SELECT osm_id, ST_BUFFER(way, {h2}) as way
sql_railroads FROM planet_osm_line
WHERE railway not in ('disused', 'abandoned')
OR highway in ('trunk', 'primary', 'secondary', 'motorway')"""
= f"""SELECT osm_id, ST_BUFFER(way, {h}) as way
sql_rivers FROM planet_osm_line
WHERE waterway = 'river'"""
= f"""SELECT osm_id, way
sql_lakes FROM planet_osm_polygon
WHERE water = 'lake'"""
= f"""SELECT osm_id, ST_BUFFER(way, {h2}) as way
sql_power FROM planet_osm_line
WHERE planet_osm_line.power IS NOT NULL"""
= f"""SELECT osm_id, ST_BUFFER(way, {h}) as way
sql_powerplants FROM planet_osm_polygon
WHERE planet_osm_polygon.power IS NOT NULL"""
= f"""SELECT osm_id, ST_BUFFER(way, 5 * {d}) as way
sql_turbines FROM planet_osm_point
WHERE "generator:source" = 'wind'"""
Next, we use SELECT
and UNION SELECT
to
select the geometries from all the subqueries above. We do this for both
the 3H and 10H residential building siting scenarios, and then use the
unioned queries to create geodataframes of all the space in Iowa where
turbines cannot be:
# Create a union of the subqueries in order to calculate the total area unavailable for wind turbine placement building scenario 1
= f"""{sql_buildings_residential_1}
sql_scenario_1 UNION
{sql_airports}
UNION
{sql_military}
UNION
{sql_nature}
UNION
{sql_railroads}
UNION
{sql_rivers}
UNION
{sql_lakes}
UNION
{sql_power}
UNION
{sql_powerplants}
UNION
{sql_turbines}
"""
#create a geodataframe from the query union using the column 'way' as the geometry
= geopandas.read_postgis(sql_scenario_1, con = db, geom_col = 'way') scenario_1_df
# Create a union of the subqueries in order to calculate the total area unavailable for wind turbine placement under building scenario 2
= f"""{sql_buildings_residential_2}
sql_scenario_2 UNION
{sql_airports}
UNION
{sql_military}
UNION
{sql_nature}
UNION
{sql_railroads}
UNION
{sql_rivers}
UNION
{sql_lakes}
UNION
{sql_power}
UNION
{sql_powerplants}
UNION
{sql_turbines}
"""
#create a geodataframe from the query union using the column 'way' as the geometry
= geopandas.read_postgis(sql_scenario_2, con = db, geom_col = 'way') scenario_2_df
We know that the area of Iowa is 144,669.2 km^2. However, finding the area of each of the buffers we’ve created will give us areas much larger than Iowa itself. To correct this, we need to dissolve the overlapping areas. We can dissolve and solve for the are in one step for each scenario.
# Total area under scenario 1
/1000/1000 scenario_1_df.dissolve().area
0 64197.937194
dtype: float64
# Total area under scenario 2
/1000/1000 scenario_2_df.dissolve().area
0 72131.583716
dtype: float64
Now we need to calculate the total number of wind turbines that could be placed in each suitable cell. This suitability is based on the parameters established using the subqueries from above as well as an additional dataset that looks at 10 km^2 polygons with associated average annual wind speeds, which is called using SQL in the next code chunk. Once we determine the suitable cells and then the total number of placeable turbines, we will be able to calculate the total energy production that all of the wind turbines could create.
# Select for wind cells to determine the wind cells suitable for wind turbine placement
= """SELECT * FROM wind_cells_10000"""
sql_wind_grid = geopandas.read_postgis(sql_wind_grid, con = db, geom_col = 'geom') wind_grid_df
Next, we subtract the union of the siting constraints (for both
scenarios from the wind cells, leaving only the cells that could
accomodate new wind turbines using overlay()
:
= wind_grid_df.overlay(scenario_1_df, how ='difference') wind_constraint_cells_1
/Users/scoutleonard/opt/anaconda3/envs/eds223/lib/python3.8/site-packages/geopandas/geodataframe.py:2196: UserWarning: `keep_geom_type=True` in overlay resulted in 88 dropped geometries of different geometry types than df1 has. Set `keep_geom_type=False` to retain all geometries
return geopandas.overlay(
Now, we are left with a dataframe of the areas that are suitable for wind turbine placement.
Next we calculate total number of turbines per polygon and use that estimate and the wind per cell to predict the potential for wind energy in the two different scenarios:
#create area column based on geom column which contains area of the polygon in each cell based on the geom column
"area"] = wind_constraint_cells_1["geom"].area wind_constraint_cells_1[
#calculate the total number of turbines that could fit in the total area by dividing the total area by turbine footprint
"turbine_no"] = wind_constraint_cells_1["area"]/turbine_footprint wind_constraint_cells_1[
#calculate the energy per single turbine per cell based on the wind speed in that cell
"energy_per_turbine"] = (wind_constraint_cells_1["wind_speed"]*2.6)-5 wind_constraint_cells_1[
#calculate the total energy
"energy_per_cell"] = wind_constraint_cells_1["turbine_no"]*wind_constraint_cells_1["energy_per_turbine"] wind_constraint_cells_1[
= wind_constraint_cells_1['energy_per_cell'].sum() total_energy_scenario_1
We repeat the process of calculating total energy per cell in the available polygons, but for the second scenario now. Note the overlay using the second query:
= wind_grid_df.overlay(scenario_2_df, how ='difference') wind_constraint_cells_2
"area"] = wind_constraint_cells_2["geom"].area wind_constraint_cells_2[
"turbine_no"] = wind_constraint_cells_2["area"]/turbine_footprint wind_constraint_cells_2[
"energy_per_turbine"] = (wind_constraint_cells_2["wind_speed"]*2.6)-5 wind_constraint_cells_2[
"energy_per_cell"] = wind_constraint_cells_2["turbine_no"]*wind_constraint_cells_2["energy_per_turbine"] wind_constraint_cells_2[
= wind_constraint_cells_2['energy_per_cell'].sum() total_energy_scenario_2
print('The total energy of all the turbines that could be placed in scenario 1 is', total_energy_scenario_1, 'GWh.')
The total energy of all the turbines that could be placed in scenario 1 is 1065473.0533352676 GWh.
print('The total energy of all the turbines that could be placed in scenario 2 is', total_energy_scenario_2, 'GWh.')
The total energy of all the turbines that could be placed in scenario 2 is 967482.4765797912 GWh.
For attribution, please cite this work as
Leonard (2022, Sept. 7). Scout Leonard (she/her): Wind Power Potential: Spatial Analysis Using Python and SQL. Retrieved from https://scoutcleonard.github.io/posts/2022-09-07-wind-power-potential-spatial-analysis-using-r-python-and-sql/
BibTeX citation
@misc{leonard2022wind, author = {Leonard, Scout}, title = {Scout Leonard (she/her): Wind Power Potential: Spatial Analysis Using Python and SQL}, url = {https://scoutcleonard.github.io/posts/2022-09-07-wind-power-potential-spatial-analysis-using-r-python-and-sql/}, year = {2022} }