|
1
|
|
|
""" |
|
2
|
|
|
Calculation of hydrogen demand based on a Voronoi partition of counted truck traffic |
|
3
|
|
|
used to allocate it to NUTS3 regions and finally aggregate it on NUTS3 level. |
|
4
|
|
|
""" |
|
5
|
|
|
from geovoronoi import points_to_coords, voronoi_regions_from_coords |
|
6
|
|
|
from loguru import logger |
|
7
|
|
|
from shapely import wkt |
|
8
|
|
|
from shapely.geometry.multipolygon import MultiPolygon |
|
9
|
|
|
from shapely.geometry.polygon import Polygon |
|
10
|
|
|
from shapely.ops import cascaded_union |
|
11
|
|
|
import geopandas as gpd |
|
12
|
|
|
|
|
13
|
|
|
from egon.data import config, db |
|
14
|
|
|
from egon.data.datasets.emobility.heavy_duty_transport.data_io import get_data |
|
15
|
|
|
from egon.data.datasets.emobility.heavy_duty_transport.db_classes import ( |
|
16
|
|
|
EgonHeavyDutyTransportVoronoi, |
|
17
|
|
|
) |
|
18
|
|
|
|
|
19
|
|
|
DATASET_CFG = config.datasets()["mobility_hgv"] |
|
20
|
|
|
|
|
21
|
|
|
|
|
22
|
|
|
def run_egon_truck(): |
|
23
|
|
|
boundary_gdf, bast_gdf, nuts3_gdf = get_data() |
|
24
|
|
|
|
|
25
|
|
|
bast_gdf_within = bast_gdf.dropna().loc[ |
|
26
|
|
|
bast_gdf.within(boundary_gdf.geometry.iat[0]) |
|
27
|
|
|
] |
|
28
|
|
|
|
|
29
|
|
|
voronoi_gdf = voronoi(bast_gdf_within, boundary_gdf) |
|
30
|
|
|
|
|
31
|
|
|
nuts3_gdf = geo_intersect(voronoi_gdf, nuts3_gdf) |
|
32
|
|
|
|
|
33
|
|
|
nuts3_gdf = nuts3_gdf.assign( |
|
34
|
|
|
normalized_truck_traffic=( |
|
35
|
|
|
nuts3_gdf.truck_traffic / nuts3_gdf.truck_traffic.sum() |
|
36
|
|
|
) |
|
37
|
|
|
) |
|
38
|
|
|
|
|
39
|
|
|
scenarios = DATASET_CFG["constants"]["scenarios"] |
|
40
|
|
|
|
|
41
|
|
|
for scenario in scenarios: |
|
42
|
|
|
total_hydrogen_consumption = calculate_total_hydrogen_consumption( |
|
43
|
|
|
scenario=scenario |
|
44
|
|
|
) |
|
45
|
|
|
|
|
46
|
|
|
nuts3_gdf = nuts3_gdf.assign( |
|
47
|
|
|
hydrogen_consumption=( |
|
48
|
|
|
nuts3_gdf.normalized_truck_traffic * total_hydrogen_consumption |
|
49
|
|
|
), |
|
50
|
|
|
scenario=scenario, |
|
51
|
|
|
) |
|
52
|
|
|
|
|
53
|
|
|
nuts3_gdf.reset_index().to_postgis( |
|
54
|
|
|
name=EgonHeavyDutyTransportVoronoi.__table__.name, |
|
55
|
|
|
con=db.engine(), |
|
56
|
|
|
schema=EgonHeavyDutyTransportVoronoi.__table__.schema, |
|
57
|
|
|
if_exists="append", |
|
58
|
|
|
index=False, |
|
59
|
|
|
) |
|
60
|
|
|
|
|
61
|
|
|
|
|
62
|
|
|
def calculate_total_hydrogen_consumption(scenario: str = "eGon2035"): |
|
63
|
|
|
"""Calculate the total hydrogen demand for trucking in Germany.""" |
|
64
|
|
|
constants = DATASET_CFG["constants"] |
|
65
|
|
|
hgv_mileage = DATASET_CFG["hgv_mileage"] |
|
66
|
|
|
|
|
67
|
|
|
leakage = constants["leakage"] |
|
68
|
|
|
leakage_rate = constants["leakage_rate"] |
|
69
|
|
|
hydrogen_consumption = constants["hydrogen_consumption"] # kg/100km |
|
70
|
|
|
fcev_share = constants["fcev_share"] |
|
71
|
|
|
|
|
72
|
|
|
hgv_mileage = hgv_mileage[scenario] # km |
|
73
|
|
|
|
|
74
|
|
|
hydrogen_consumption_per_km = hydrogen_consumption / 100 # kg/km |
|
75
|
|
|
|
|
76
|
|
|
# calculate total hydrogen consumption kg/a |
|
77
|
|
|
if leakage: |
|
78
|
|
|
return ( |
|
79
|
|
|
hgv_mileage |
|
80
|
|
|
* hydrogen_consumption_per_km |
|
81
|
|
|
* fcev_share |
|
82
|
|
|
/ (1 - leakage_rate) |
|
83
|
|
|
) |
|
84
|
|
|
else: |
|
85
|
|
|
return hgv_mileage * hydrogen_consumption_per_km * fcev_share |
|
86
|
|
|
|
|
87
|
|
|
|
|
88
|
|
|
def geo_intersect( |
|
89
|
|
|
voronoi_gdf: gpd.GeoDataFrame, |
|
90
|
|
|
nuts3_gdf: gpd.GeoDataFrame, |
|
91
|
|
|
mode: str = "intersection", |
|
92
|
|
|
): |
|
93
|
|
|
"""Calculate Intersections between two GeoDataFrames and distribute truck traffic""" |
|
94
|
|
|
logger.info( |
|
95
|
|
|
"Calculating Intersections between Voronoi Field and Grid Districts " |
|
96
|
|
|
"and distributing truck traffic accordingly to the area share." |
|
97
|
|
|
) |
|
98
|
|
|
voronoi_gdf = voronoi_gdf.assign(voronoi_id=voronoi_gdf.index.tolist()) |
|
99
|
|
|
|
|
100
|
|
|
# Find Intersections between both GeoDataFrames |
|
101
|
|
|
intersection_gdf = gpd.overlay( |
|
102
|
|
|
voronoi_gdf, nuts3_gdf.reset_index()[["nuts3", "geometry"]], how=mode |
|
103
|
|
|
) |
|
104
|
|
|
|
|
105
|
|
|
# Calc Area of Intersections |
|
106
|
|
|
intersection_gdf = intersection_gdf.assign( |
|
107
|
|
|
surface_area=intersection_gdf.geometry.area / 10**6 |
|
108
|
|
|
) # km² |
|
109
|
|
|
|
|
110
|
|
|
# Initialize results column |
|
111
|
|
|
nuts3_gdf = nuts3_gdf.assign(truck_traffic=0) |
|
112
|
|
|
|
|
113
|
|
|
for voronoi_id in intersection_gdf.voronoi_id.unique(): |
|
114
|
|
|
voronoi_id_intersection_gdf = intersection_gdf.loc[ |
|
115
|
|
|
intersection_gdf.voronoi_id == voronoi_id |
|
116
|
|
|
] |
|
117
|
|
|
|
|
118
|
|
|
total_area = voronoi_id_intersection_gdf.surface_area.sum() |
|
119
|
|
|
|
|
120
|
|
|
truck_traffic = voronoi_id_intersection_gdf.truck_traffic.iat[0] |
|
121
|
|
|
|
|
122
|
|
|
for idx, row in voronoi_id_intersection_gdf.iterrows(): |
|
123
|
|
|
traffic_share = truck_traffic * row["surface_area"] / total_area |
|
124
|
|
|
|
|
125
|
|
|
nuts3_gdf.at[row["nuts3"], "truck_traffic"] += traffic_share |
|
126
|
|
|
|
|
127
|
|
|
logger.info("Done.") |
|
128
|
|
|
|
|
129
|
|
|
return nuts3_gdf |
|
130
|
|
|
|
|
131
|
|
|
|
|
132
|
|
|
def voronoi( |
|
133
|
|
|
points: gpd.GeoDataFrame, |
|
134
|
|
|
boundary: gpd.GeoDataFrame, |
|
135
|
|
|
): |
|
136
|
|
|
"""Building a Voronoi Field from points and a boundary.""" |
|
137
|
|
|
logger.info("Building Voronoi Field.") |
|
138
|
|
|
|
|
139
|
|
|
sources = DATASET_CFG["original_data"]["sources"] |
|
140
|
|
|
relevant_columns = sources["BAST"]["relevant_columns"] |
|
141
|
|
|
truck_col = relevant_columns[0] |
|
142
|
|
|
srid = DATASET_CFG["tables"]["srid"] |
|
143
|
|
|
|
|
144
|
|
|
# convert the boundary geometry into a union of the polygon |
|
145
|
|
|
# convert the Geopandas GeoSeries of Point objects to NumPy array of coordinates. |
|
146
|
|
|
boundary_shape = cascaded_union(boundary.geometry) |
|
147
|
|
|
coords = points_to_coords(points.geometry) |
|
148
|
|
|
|
|
149
|
|
|
# calculate Voronoi regions |
|
150
|
|
|
poly_shapes, pts, unassigned_pts = voronoi_regions_from_coords( |
|
151
|
|
|
coords, boundary_shape, return_unassigned_points=True |
|
152
|
|
|
) |
|
153
|
|
|
|
|
154
|
|
|
multipoly_shapes = {} |
|
155
|
|
|
|
|
156
|
|
|
for key, shape in poly_shapes.items(): |
|
157
|
|
|
if isinstance(shape, Polygon): |
|
158
|
|
|
shape = wkt.loads(str(shape)) |
|
159
|
|
|
shape = MultiPolygon([shape]) |
|
160
|
|
|
|
|
161
|
|
|
multipoly_shapes[key] = [shape] |
|
162
|
|
|
|
|
163
|
|
|
poly_gdf = gpd.GeoDataFrame.from_dict( |
|
164
|
|
|
multipoly_shapes, orient="index", columns=["geometry"] |
|
165
|
|
|
) |
|
166
|
|
|
|
|
167
|
|
|
# match points to old index |
|
168
|
|
|
# FIXME: This seems overcomplicated |
|
169
|
|
|
poly_gdf.index = [v[0] for v in pts.values()] |
|
170
|
|
|
|
|
171
|
|
|
poly_gdf = poly_gdf.sort_index() |
|
172
|
|
|
|
|
173
|
|
|
unmatched = [points.index[idx] for idx in unassigned_pts] |
|
174
|
|
|
|
|
175
|
|
|
points_matched = points.drop(unmatched) |
|
176
|
|
|
|
|
177
|
|
|
poly_gdf.index = points_matched.index |
|
178
|
|
|
|
|
179
|
|
|
# match truck traffic to new polys |
|
180
|
|
|
poly_gdf = poly_gdf.assign( |
|
181
|
|
|
truck_traffic=points.loc[poly_gdf.index][truck_col] |
|
182
|
|
|
) |
|
183
|
|
|
|
|
184
|
|
|
poly_gdf = poly_gdf.set_crs(epsg=srid, inplace=True) |
|
185
|
|
|
|
|
186
|
|
|
logger.info("Done.") |
|
187
|
|
|
|
|
188
|
|
|
return poly_gdf |
|
189
|
|
|
|