Note
Download this example as a Jupyter notebook here: pypsa/atlite
Calculate Landuse Availabilities#
This example shows how atlite
can deal with landuse restrictions. For the demonstration the effective availability per weather cell is calculated while excluding areas specified by the CORINE CLC raster.
[1]:
import atlite
import xarray as xr
import matplotlib.pyplot as plt
import geopandas as gpd
from rasterio.plot import show
from atlite.gis import shape_availability, ExclusionContainer
With geopandas
we retrieve the geographic shapes for 5 countries on the Balkan Peninsula, namely Serbia, Croatia, Macedonia, Bosnia & Herzegovina and Montenegro.
[2]:
world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
countries = ["Serbia", "Croatia", "Bosnia and Herz.", "Montenegro"]
shapes = world[world.name.isin(countries)].set_index("name")
shapes.plot(figsize=(10, 7));
We create an atlite.Cutout
which covers the whole regions and builds the backbone for our analysis. Later, it will enable to retrieve the needed weather data.
[3]:
bounds = shapes.cascaded_union.buffer(1).bounds
cutout = atlite.Cutout(
"balkans", module="era5", bounds=bounds, time=slice("2013-01-01", "2013-01-02")
)
/tmp/ipykernel_36247/2540265064.py:1: FutureWarning: The 'cascaded_union' attribute is deprecated, use 'unary_union' instead
bounds = shapes.cascaded_union.buffer(1).bounds
/home/fabian/vres/py/atlite/atlite/cutout.py:185: UserWarning: Arguments module, bounds, time are ignored, since cutout is already built.
warn(
Let’s see how the grid cells and the regional shapes overlap.
[4]:
plt.rc("figure", figsize=[10, 7])
fig, ax = plt.subplots()
shapes.plot(ax=ax)
cutout.grid.plot(ax=ax, edgecolor="grey", color="None")
[4]:
<AxesSubplot: >
The CORINE Land Cover (CLC) database provides a 100 m x 100 m raster which, for each raster cell, indicates the type of landuse (forest, urban, industrial). In total there are 44 classes. Download the raster (.tif file) from the download page and store the raster as corine.tif
.
For calculating the availability per cutout weather cells, an ExclusionContainer
must be defined beforehand. It serves as a container for all rasters and geometries we want to exclude (or possibly include).
In many cases, rasters and geometries have different projections and resolutions. Therefore, the ExclusionContainer
is initialized by a CRS and a resolution which suits as a basis for all added rasters and geometries. Per default the CRS is 3035 and the resoultion 100, which leads set a raster of 100 meter resolution. All rasters and geometries will be converted to this (crs, res)
config if they don’t match it.
When adding a raster to the ExclusionContainer
you can specify which codes (integers) to exclude. By setting invert=True
, you can also restrict the inclusion to a set of codes. Further you can buffer around codes (see the docs for detail). Here we are going to exclude the first twenty landuse codes.
[5]:
CORINE = "corine.tif"
excluder = ExclusionContainer()
excluder.add_raster(CORINE, codes=range(20))
For the demonstration we want to see how the landuse availability behaves within one specific shape, e.g. Croatia.
Note that, since the excluder is in crs=3035
, we convert to geometry of Croatia to excluder.crs
for plotting it…
[6]:
croatia = shapes.loc[["Croatia"]].geometry.to_crs(excluder.crs)
…and use the shape_availability
function of atlite
to calculate a mask for the ExclusionContainer
excluder.
[7]:
masked, transform = excluder.compute_shape_availability(croatia)
The masked
object is a numpy array. Eligile raster cells have a 1 and excluded cells a 0. Note that this data still lives in the projection of excluder
. For calculating the eligible share we can use the following routine.
[8]:
eligible_share = masked.sum() * excluder.res**2 / croatia.geometry.item().area
print(f"The eligibility share is: {eligible_share:.2%}")
The eligibility share is: 78.99%
For plotting the geometry together with the excluded areas, we can use the function plot_shape_availability
which uses rasterio
’s and geopandas
’ plot function in the background.
[9]:
fig, ax = plt.subplots()
excluder.plot_shape_availability(croatia)
[9]:
<AxesSubplot: title={'center': 'Eligible area (green) 78.99%'}>
How does is look when we add our cutout grid to the plot? How do the weather cells intersect with the available area?
[10]:
fig, ax = plt.subplots()
excluder.plot_shape_availability(croatia, ax=ax)
cutout.grid.to_crs(excluder.crs).plot(edgecolor="grey", color="None", ax=ax, ls=":")
ax.set_title(f"Eligible area (green) {eligible_share:.2%}")
ax.axis("off");
We see that the weather cells are much larger than the raster cells. GDAL
provides a fast reprojection function for averaging fine-grained to coarse-grained rasters. Atlite
automates this calculation for all geometries in shapes
when calling the cutout.availabilitymatrix
function. Let’s see how this function performs. (Note that the steps before are not necessary for this calculation.)
INFO: For large sets of shapes set nprocesses to a number > 1 for parallelization.
[11]:
A = cutout.availabilitymatrix(shapes, excluder)
A
Compute availability matrix: 100%|██████████| 4/4 [00:01<00:00, 3.42 gridcells/s]
[11]:
<xarray.DataArray (name: 4, y: 31, x: 45)> array([[[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]], [[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]], [[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]], [[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]]]) Coordinates: * name (name) object 'Croatia' 'Bosnia and Herz.' 'Serbia' 'Montenegro' * y (y) float64 40.0 40.25 40.5 40.75 41.0 ... 46.75 47.0 47.25 47.5 * x (x) float64 12.75 13.0 13.25 13.5 13.75 ... 23.0 23.25 23.5 23.75
A
is an DataArray with 3 dimensions (shape
, x
, y
) and very sparse data. It indicates the relative overlap of weather cell (x, y)
with geometry shape
while excluding the area specified by the excluder
.
How does the availability look for our example of Croatia?
[12]:
fig, ax = plt.subplots()
A.sel(name="Croatia").plot(cmap="Greens")
shapes.loc[["Croatia"]].plot(ax=ax, edgecolor="k", color="None")
cutout.grid.plot(ax=ax, color="None", edgecolor="grey", ls=":")
[12]:
<AxesSubplot: title={'center': 'name = Croatia'}, xlabel='x', ylabel='y'>
Note that now the projection is in cutout.crs
. In the north-west, where most of the areas were excluded, the availability is lower than 0.5. That means less than the half of these weather cells and their potentials can be exploited.
And for the other shapes…
[13]:
fg = A.plot(row="name", col_wrap=3, cmap="Greens")
fg.set_titles("{value}")
for i, c in enumerate(shapes.index):
shapes.loc[[c]].plot(ax=fg.axs.flatten()[i], edgecolor="k", color="None")
The availibility matrix A
can now be used as a layoutmatrix in the conversion functions of atlite
, i.e. cutout.pv
, cutout.wind
. The normal approach would be to weigh the availabilities with the area per grid cell and the capacity per area.
[14]:
cap_per_sqkm = 1.7
area = cutout.grid.set_index(["y", "x"]).to_crs(3035).area / 1e6
area = xr.DataArray(area, dims=("spatial"))
capacity_matrix = A.stack(spatial=["y", "x"]) * area * cap_per_sqkm
After the cutout preparation, we can calculate the static and dynamic capacity factors of each region.
[15]:
cutout.prepare()
pv = cutout.pv(
matrix=capacity_matrix,
panel=atlite.solarpanels.CdTe,
orientation="latitude_optimal",
index=shapes.index,
)
Finally let’s see how the total power potential per region look.
[16]:
pv.to_pandas().div(1e3).plot(ylabel="Solar Power [GW]", ls="--", figsize=(10, 4));