Passed
Pull Request — dev (#894)
by
unknown
01:25
created

data.datasets.chp   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 540
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 31
eloc 295
dl 0
loc 540
rs 9.92
c 0
b 0
f 0

22 Functions

Rating   Name   Duplication   Size   Complexity  
A extension_HH() 0 2 1
A extension_BY() 0 2 1
A extension_HE() 0 2 1
A extension_NS() 0 2 1
A extension_BE() 0 2 1
A extension_ST() 0 2 1
A extension_SH() 0 2 1
A extension_SN() 0 2 1
A extension_RP() 0 2 1
A extension_BB() 0 2 1
A insert_chp_egon2035() 0 40 1
A extension_HB() 0 2 1
A extension_SL() 0 2 1
A extension_NW() 0 2 1
A extension_BW() 0 2 1
A extension_MV() 0 2 1
A extension_TH() 0 2 1
B insert_biomass_chp() 0 75 5
A create_tables() 0 14 1
B assign_heat_bus() 0 97 3
B insert_chp_egon100re() 0 83 2
A nearest() 0 46 2

1 Method

Rating   Name   Duplication   Size   Complexity  
A Chp.__init__() 0 10 1
1
"""
2
The central module containing all code dealing with combined heat and power
3
(CHP) plants.
4
"""
5
6
from geoalchemy2 import Geometry
7
from shapely.ops import nearest_points
8
from sqlalchemy import Boolean, Column, Float, Integer, Sequence, String
9
from sqlalchemy.dialects.postgresql import JSONB
10
from sqlalchemy.ext.declarative import declarative_base
11
from sqlalchemy.orm import sessionmaker
12
import geopandas as gpd
13
import pandas as pd
14
15
from egon.data import config, db
16
from egon.data.datasets import Dataset
17
from egon.data.datasets.chp.match_nep import insert_large_chp
18
from egon.data.datasets.chp.small_chp import (
19
    assign_use_case,
20
    existing_chp_smaller_10mw,
21
    extension_per_federal_state,
22
    select_target,
23
)
24
from egon.data.datasets.power_plants import (
25
    assign_bus_id,
26
    assign_voltage_level,
27
    filter_mastr_geometry,
28
    scale_prox2now,
29
)
30
import pypsa
31
from egon.data.datasets.chp.small_chp import extension_to_areas
32
from pathlib import Path
33
34
Base = declarative_base()
35
36
37
class EgonChp(Base):
38
    __tablename__ = "egon_chp_plants"
39
    __table_args__ = {"schema": "supply"}
40
    id = Column(Integer, Sequence("chp_seq"), primary_key=True)
41
    sources = Column(JSONB)
42
    source_id = Column(JSONB)
43
    carrier = Column(String)
44
    district_heating = Column(Boolean)
45
    el_capacity = Column(Float)
46
    th_capacity = Column(Float)
47
    electrical_bus_id = Column(Integer)
48
    district_heating_area_id = Column(Integer)
49
    ch4_bus_id = Column(Integer)
50
    voltage_level = Column(Integer)
51
    scenario = Column(String)
52
    geom = Column(Geometry("POINT", 4326))
53
54
55
class EgonMaStRConventinalWithoutChp(Base):
56
    __tablename__ = "egon_mastr_conventional_without_chp"
57
    __table_args__ = {"schema": "supply"}
58
    id = Column(Integer, Sequence("mastr_conventional_seq"), primary_key=True)
59
    EinheitMastrNummer = Column(String)
60
    carrier = Column(String)
61
    el_capacity = Column(Float)
62
    plz = Column(Integer)
63
    city = Column(String)
64
    federal_state = Column(String)
65
    geometry = Column(Geometry("POINT", 4326))
66
67
68
def create_tables():
69
    """Create tables for chp data
70
    Returns
71
    -------
72
    None.
73
    """
74
75
    db.execute_sql("CREATE SCHEMA IF NOT EXISTS supply;")
76
    engine = db.engine()
77
    EgonChp.__table__.drop(bind=engine, checkfirst=True)
78
    EgonChp.__table__.create(bind=engine, checkfirst=True)
79
    EgonMaStRConventinalWithoutChp.__table__.drop(bind=engine, checkfirst=True)
80
    EgonMaStRConventinalWithoutChp.__table__.create(
81
        bind=engine, checkfirst=True
82
    )
83
84
85
def nearest(
86
    row,
87
    df,
88
    centroid=False,
89
    row_geom_col="geometry",
90
    df_geom_col="geometry",
91
    src_column=None,
92
):
93
    """
94
    Finds the nearest point and returns the specified column values
95
96
    Parameters
97
    ----------
98
    row : pandas.Series
99
        Data to which the nearest data of df is assigned.
100
    df : pandas.DataFrame
101
        Data which includes all options for the nearest neighbor alogrithm.
102
    centroid : boolean
103
        Use centroid geoemtry. The default is False.
104
    row_geom_col : str, optional
105
        Name of row's geometry column. The default is 'geometry'.
106
    df_geom_col : str, optional
107
        Name of df's geometry column. The default is 'geometry'.
108
    src_column : str, optional
109
        Name of returned df column. The default is None.
110
111
    Returns
112
    -------
113
    value : pandas.Series
114
        Values of specified column of df
115
116
    """
117
118
    if centroid:
119
        unary_union = df.centroid.unary_union
120
    else:
121
        unary_union = df[df_geom_col].unary_union
122
    # Find the geometry that is closest
123
    nearest = (
124
        df[df_geom_col] == nearest_points(row[row_geom_col], unary_union)[1]
125
    )
126
127
    # Get the corresponding value from df (matching is based on the geometry)
128
    value = df[nearest][src_column].values[0]
129
130
    return value
131
132
133
def assign_heat_bus(scenario="eGon2035"):
134
    """Selects heat_bus for chps used in district heating.
135
136
    Parameters
137
    ----------
138
    scenario : str, optional
139
        Name of the corresponding scenario. The default is 'eGon2035'.
140
141
    Returns
142
    -------
143
    None.
144
145
    """
146
    sources = config.datasets()["chp_location"]["sources"]
147
    target = config.datasets()["chp_location"]["targets"]["chp_table"]
148
149
    # Select CHP with use_case = 'district_heating'
150
    chp = db.select_geodataframe(
151
        f"""
152
        SELECT * FROM
153
        {target['schema']}.{target['table']}
154
        WHERE scenario = '{scenario}'
155
        AND district_heating = True
156
        """,
157
        index_col="id",
158
        epsg=4326,
159
    )
160
161
    # Select district heating areas and their centroid
162
    district_heating = db.select_geodataframe(
163
        f"""
164
        SELECT area_id, ST_Centroid(geom_polygon) as geom
165
        FROM
166
        {sources['district_heating_areas']['schema']}.
167
        {sources['district_heating_areas']['table']}
168
        WHERE scenario = '{scenario}'
169
        """,
170
        epsg=4326,
171
    )
172
173
    # Assign district heating area_id to district_heating_chp
174
    # According to nearest centroid of district heating area
175
    chp["district_heating_area_id"] = chp.apply(
176
        nearest,
177
        df=district_heating,
178
        row_geom_col="geom",
179
        df_geom_col="geom",
180
        centroid=True,
181
        src_column="area_id",
182
        axis=1,
183
    )
184
185
    # Drop district heating CHP without heat_bus_id
186
    db.execute_sql(
187
        f"""
188
        DELETE FROM {target['schema']}.{target['table']}
189
        WHERE scenario = '{scenario}'
190
        AND district_heating = True
191
        """
192
    )
193
194
    # Insert district heating CHP with heat_bus_id
195
    session = sessionmaker(bind=db.engine())()
196
    for i, row in chp.iterrows():
197
        if row.carrier != "biomass":
198
            entry = EgonChp(
199
                id=i,
200
                sources=row.sources,
201
                source_id=row.source_id,
202
                carrier=row.carrier,
203
                el_capacity=row.el_capacity,
204
                th_capacity=row.th_capacity,
205
                electrical_bus_id=row.electrical_bus_id,
206
                ch4_bus_id=row.ch4_bus_id,
207
                district_heating_area_id=row.district_heating_area_id,
208
                district_heating=row.district_heating,
209
                voltage_level=row.voltage_level,
210
                scenario=scenario,
211
                geom=f"SRID=4326;POINT({row.geom.x} {row.geom.y})",
212
            )
213
        else:
214
            entry = EgonChp(
215
                id=i,
216
                sources=row.sources,
217
                source_id=row.source_id,
218
                carrier=row.carrier,
219
                el_capacity=row.el_capacity,
220
                th_capacity=row.th_capacity,
221
                electrical_bus_id=row.electrical_bus_id,
222
                district_heating_area_id=row.district_heating_area_id,
223
                district_heating=row.district_heating,
224
                voltage_level=row.voltage_level,
225
                scenario=scenario,
226
                geom=f"SRID=4326;POINT({row.geom.x} {row.geom.y})",
227
            )
228
        session.add(entry)
229
    session.commit()
230
231
232
def insert_biomass_chp(scenario):
233
    """Insert biomass chp plants of future scenario
234
235
    Parameters
236
    ----------
237
    scenario : str
238
        Name of scenario.
239
240
    Returns
241
    -------
242
    None.
243
244
    """
245
    cfg = config.datasets()["chp_location"]
246
247
    # import target values from NEP 2021, scneario C 2035
248
    target = select_target("biomass", scenario)
249
250
    # import data for MaStR
251
    mastr = pd.read_csv(cfg["sources"]["mastr_biomass"]).query(
252
        "EinheitBetriebsstatus=='InBetrieb'"
253
    )
254
255
    # Drop entries without federal state or 'AusschließlichWirtschaftszone'
256
    mastr = mastr[
257
        mastr.Bundesland.isin(
258
            pd.read_sql(
259
                f"""SELECT DISTINCT ON (gen)
260
        REPLACE(REPLACE(gen, '-', ''), 'ü', 'ue') as states
261
        FROM {cfg['sources']['vg250_lan']['schema']}.
262
        {cfg['sources']['vg250_lan']['table']}""",
263
                con=db.engine(),
264
            ).states.values
265
        )
266
    ]
267
268
    # Scaling will be done per federal state in case of eGon2035 scenario.
269
    if scenario == "eGon2035":
270
        level = "federal_state"
271
    else:
272
        level = "country"
273
    # Choose only entries with valid geometries inside DE/test mode
274
    mastr_loc = filter_mastr_geometry(mastr).set_geometry("geometry")
275
276
    # Scale capacities to meet target values
277
    mastr_loc = scale_prox2now(mastr_loc, target, level=level)
278
279
    # Assign bus_id
280
    if len(mastr_loc) > 0:
281
        mastr_loc["voltage_level"] = assign_voltage_level(mastr_loc, cfg)
282
        mastr_loc = assign_bus_id(mastr_loc, cfg)
283
    mastr_loc = assign_use_case(mastr_loc, cfg["sources"])
284
285
    # Insert entries with location
286
    session = sessionmaker(bind=db.engine())()
287
    for i, row in mastr_loc.iterrows():
288
        if row.ThermischeNutzleistung > 0:
289
            entry = EgonChp(
290
                sources={
291
                    "chp": "MaStR",
292
                    "el_capacity": "MaStR scaled with NEP 2021",
293
                    "th_capacity": "MaStR",
294
                },
295
                source_id={"MastrNummer": row.EinheitMastrNummer},
296
                carrier="biomass",
297
                el_capacity=row.Nettonennleistung,
298
                th_capacity=row.ThermischeNutzleistung / 1000,
299
                scenario=scenario,
300
                district_heating=row.district_heating,
301
                electrical_bus_id=row.bus_id,
302
                voltage_level=row.voltage_level,
303
                geom=f"SRID=4326;POINT({row.Laengengrad} {row.Breitengrad})",
304
            )
305
            session.add(entry)
306
    session.commit()
307
308
309
def insert_chp_egon2035():
310
    """Insert CHP plants for eGon2035 considering NEP and MaStR data
311
312
    Returns
313
    -------
314
    None.
315
316
    """
317
318
    create_tables()
319
320
    sources = config.datasets()["chp_location"]["sources"]
321
322
    targets = config.datasets()["chp_location"]["targets"]
323
324
    insert_biomass_chp("eGon2035")
325
326
    # Insert large CHPs based on NEP's list of conventional power plants
327
    MaStR_konv = insert_large_chp(sources, targets["chp_table"], EgonChp)
328
329
    # Insert smaller CHPs (< 10MW) based on existing locations from MaStR
330
    existing_chp_smaller_10mw(sources, MaStR_konv, EgonChp)
331
332
    gpd.GeoDataFrame(
333
        MaStR_konv[
334
            [
335
                "EinheitMastrNummer",
336
                "el_capacity",
337
                "geometry",
338
                "carrier",
339
                "plz",
340
                "city",
341
                "federal_state",
342
            ]
343
        ]
344
    ).to_postgis(
345
        targets["mastr_conventional_without_chp"]["table"],
346
        schema=targets["mastr_conventional_without_chp"]["schema"],
347
        con=db.engine(),
348
        if_exists="replace",
349
    )
350
351
352
def extension_BW():
353
    extension_per_federal_state("BadenWuerttemberg", EgonChp)
354
355
356
def extension_BY():
357
    extension_per_federal_state("Bayern", EgonChp)
358
359
360
def extension_HB():
361
    extension_per_federal_state("Bremen", EgonChp)
362
363
364
def extension_BB():
365
    extension_per_federal_state("Brandenburg", EgonChp)
366
367
368
def extension_HH():
369
    extension_per_federal_state("Hamburg", EgonChp)
370
371
372
def extension_HE():
373
    extension_per_federal_state("Hessen", EgonChp)
374
375
376
def extension_MV():
377
    extension_per_federal_state("MecklenburgVorpommern", EgonChp)
378
379
380
def extension_NS():
381
    extension_per_federal_state("Niedersachsen", EgonChp)
382
383
384
def extension_NW():
385
    extension_per_federal_state("NordrheinWestfalen", EgonChp)
386
387
388
def extension_SN():
389
    extension_per_federal_state("Sachsen", EgonChp)
390
391
392
def extension_TH():
393
    extension_per_federal_state("Thueringen", EgonChp)
394
395
396
def extension_SL():
397
    extension_per_federal_state("Saarland", EgonChp)
398
399
400
def extension_ST():
401
    extension_per_federal_state("SachsenAnhalt", EgonChp)
402
403
404
def extension_RP():
405
    extension_per_federal_state("RheinlandPfalz", EgonChp)
406
407
408
def extension_BE():
409
    extension_per_federal_state("Berlin", EgonChp)
410
411
412
def extension_SH():
413
    extension_per_federal_state("SchleswigHolstein", EgonChp)
414
415
416
def insert_chp_egon100re():
417
    """Insert CHP plants for eGon100RE considering results from pypsa-eur-sec
418
419
    Returns
420
    -------
421
    None.
422
423
    """
424
425
    sources = config.datasets()["chp_location"]["sources"]
426
427
    db.execute_sql(
428
        f"""
429
        DELETE FROM {EgonChp.__table__.schema}.{EgonChp.__table__.name}
430
        WHERE scenario = 'eGon100RE'
431
        """
432
    )
433
434
    # select target values from pypsa-eur-sec
435
    additional_capacity = db.select_dataframe(
436
        """
437
        SELECT capacity
438
        FROM supply.egon_scenario_capacities
439
        WHERE scenario_name = 'eGon100RE'
440
        AND carrier = 'urban_central_gas_CHP'
441
        """
442
    ).capacity[0]
443
444
    if config.settings()["egon-data"]["--dataset-boundary"] != "Everything":
445
        additional_capacity /= 16
446
    target_file = (
447
        Path(".")
448
        / "data_bundle_egon_data"
449
        / "pypsa_eur_sec"
450
        / "2022-05-04-egondata-integration"
451
        / "postnetworks"
452
        / "elec_s_37_lv2.0__Co2L0-3H-T-H-B-I-dist1_2050.nc"
453
    )
454
455
    network = pypsa.Network(str(target_file))
456
    chp_index = "DE0 0 urban central gas CHP"
457
458
    standard_chp_th = 10
459
    standard_chp_el = (
460
        standard_chp_th
461
        * network.links.loc[chp_index, "efficiency"]
462
        / network.links.loc[chp_index, "efficiency2"]
463
    )
464
465
    areas = db.select_geodataframe(
466
        f"""
467
            SELECT
468
            residential_and_service_demand as demand, area_id,
469
            ST_Transform(ST_PointOnSurface(geom_polygon), 4326)  as geom
470
            FROM
471
            {sources['district_heating_areas']['schema']}.
472
            {sources['district_heating_areas']['table']}
473
            WHERE scenario = 'eGon100RE'
474
            """
475
    )
476
477
    existing_chp = pd.DataFrame(
478
        data={
479
            "el_capacity": standard_chp_el,
480
            "th_capacity": standard_chp_th,
481
            "voltage_level": 5,
482
        },
483
        index=range(1),
484
    )
485
486
    flh = (
487
        network.links_t.p0[chp_index].sum()
488
        / network.links.p_nom_opt[chp_index]
489
    )
490
491
    extension_to_areas(
492
        areas,
493
        additional_capacity,
494
        existing_chp,
495
        flh,
496
        EgonChp,
497
        district_heating=True,
498
        scenario="eGon100RE",
499
    )
500
501
502
# Add one task per federal state for small CHP extension
503
if (
504
    config.settings()["egon-data"]["--dataset-boundary"]
505
    == "Schleswig-Holstein"
506
):
507
    extension = extension_SH
508
else:
509
    extension = {
510
        extension_BW,
511
        extension_BY,
512
        extension_HB,
513
        extension_BB,
514
        extension_HE,
515
        extension_MV,
516
        extension_NS,
517
        extension_NW,
518
        extension_SH,
519
        extension_HH,
520
        extension_RP,
521
        extension_SL,
522
        extension_SN,
523
        extension_ST,
524
        extension_TH,
525
        extension_BE,
526
    }
527
528
529
class Chp(Dataset):
530
    def __init__(self, dependencies):
531
        super().__init__(
532
            name="Chp",
533
            version="0.0.5",
534
            dependencies=dependencies,
535
            tasks=(
536
                create_tables,
537
                {insert_chp_egon2035, insert_chp_egon100re},
538
                assign_heat_bus,
539
                extension,
540
            ),
541
        )
542