Passed
Pull Request — dev (#1008)
by
unknown
02:05
created

data.datasets.storages.home_batteries   A

Complexity

Total Complexity 12

Size/Duplication

Total Lines 204
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 12
eloc 87
dl 0
loc 204
rs 10
c 0
b 0
f 0

3 Functions

Rating   Name   Duplication   Size   Complexity  
A get_cbat_pbat_ratio() 0 21 1
C allocate_home_batteries_to_buildings() 0 105 10
A create_table() 0 13 1
1
"""
2
Home Battery allocation to buildings
3
4
Main module for allocation of home batteries onto buildings and sizing them
5
depending on pv rooftop system size.
6
7
**Contents of this module**
8
* Creation of DB tables
9
* Allocate given home battery capacity per mv grid to buildings with pv rooftop
10
  systems. The sizing of the home battery system depends on the size of the
11
  pv rooftop system and can be set within the *datasets.yml*. Default sizing is
12
  1:1 between the pv rooftop capacity (kWp) and the battery capacity (kWh).
13
* Write results to DB
14
15
**Configuration**
16
17
The config of this dataset can be found in *datasets.yml* in section
18
*home_batteries*.
19
20
**Scenarios and variations**
21
22
Assumptions can be changed within the *datasets.yml*.
23
24
Only buildings with a pv rooftop systems are considered within the allocation
25
process. The default sizing of home batteries is 1:1 between the pv rooftop
26
capacity (kWp) and the battery capacity (kWh). Reaching the exact value of the
27
allocation of battery capacities per grid area leads to slight deviations from
28
this specification.
29
30
## Methodology
31
32
The selection of buildings is done randomly until a result is reached which is
33
close to achieving the sizing specification.
34
"""
35
from loguru import logger
36
from numpy.random import RandomState
37
from sqlalchemy import Column, Float, Integer, String
38
from sqlalchemy.ext.declarative import declarative_base
39
import numpy as np
40
import pandas as pd
41
42
from egon.data import config, db
43
44
Base = declarative_base()
45
46
47
def get_cbat_pbat_ratio():
48
    """
49
    Mean ratio between the storage capacity and the power of the pv rooftop
50
    system
51
52
    Returns
53
    -------
54
    int
55
        Mean ratio between the storage capacity and the power of the pv
56
        rooftop system
57
    """
58
    sources = config.datasets()["home_batteries"]["sources"]
59
60
    sql = f"""
61
    SELECT max_hours
62
    FROM {sources["etrago_storage"]["schema"]}
63
    .{sources["etrago_storage"]["table"]}
64
    WHERE carrier = 'home_battery'
65
    """
66
67
    return int(db.select_dataframe(sql).iat[0, 0])
68
69
70
def allocate_home_batteries_to_buildings():
71
    """
72
    Allocate home battery storage systems to buildings with pv rooftop systems
73
    """
74
    # get constants
75
    constants = config.datasets()["home_batteries"]["constants"]
76
    scenarios = constants["scenarios"]
77
    cbat_ppv_ratio = constants["cbat_ppv_ratio"]
78
    rtol = constants["rtol"]
79
    max_it = constants["max_it"]
80
    cbat_pbat_ratio = get_cbat_pbat_ratio()
81
82
    sources = config.datasets()["home_batteries"]["sources"]
83
84
    df_list = []
85
86
    for scenario in scenarios:
87
        # get home battery capacity per mv grid id
88
        sql = f"""
89
        SELECT el_capacity as p_nom_min, bus_id as bus FROM
90
        {sources["storage"]["schema"]}
91
        .{sources["storage"]["table"]}
92
        WHERE carrier = 'home_battery'
93
        AND scenario = '{scenario}';
94
        """
95
96
        home_batteries_df = db.select_dataframe(sql)
97
98
        home_batteries_df = home_batteries_df.assign(
99
            bat_cap=home_batteries_df.p_nom_min * cbat_pbat_ratio
100
        )
101
102
        sql = """
103
        SELECT building_id, capacity
104
        FROM supply.egon_power_plants_pv_roof_building
105
        WHERE scenario = '{}'
106
        AND bus_id = {}
107
        """
108
109
        for bus_id, bat_cap in home_batteries_df[
110
            ["bus", "bat_cap"]
111
        ].itertuples(index=False):
112
            pv_df = db.select_dataframe(sql.format(scenario, bus_id))
113
114
            grid_ratio = bat_cap / pv_df.capacity.sum()
115
116
            if grid_ratio > cbat_ppv_ratio:
117
                logger.warning(
118
                    f"In Grid {bus_id} and scenario {scenario}, the ratio of "
119
                    f"home storage capacity to pv rooftop capacity is above 1"
120
                    f" ({grid_ratio: g}). The storage capacity of pv rooftop "
121
                    f"systems will be high."
122
                )
123
124
            if grid_ratio < cbat_ppv_ratio:
125
                random_state = RandomState(seed=bus_id)
126
127
                n = max(int(len(pv_df) * grid_ratio), 1)
128
129
                best_df = pv_df.sample(n=n, random_state=random_state)
130
131
                i = 0
132
133
                while (
134
                    not np.isclose(best_df.capacity.sum(), bat_cap, rtol=rtol)
135
                    and i < max_it
136
                ):
137
                    sample_df = pv_df.sample(n=n, random_state=random_state)
138
139
                    if abs(best_df.capacity.sum() - bat_cap) > abs(
140
                        sample_df.capacity.sum() - bat_cap
141
                    ):
142
                        best_df = sample_df.copy()
143
144
                    i += 1
145
146
                    if sample_df.capacity.sum() < bat_cap:
147
                        n = min(n + 1, len(pv_df))
148
                    else:
149
                        n = max(n - 1, 1)
150
151
                if not np.isclose(best_df.capacity.sum(), bat_cap, rtol=rtol):
152
                    logger.warning(
153
                        f"No suitable generators could be found in Grid "
154
                        f"{bus_id} and scenario {scenario} to achieve the "
155
                        f"desired ratio between battery capacity and pv "
156
                        f"rooftop capacity. The ratio will be "
157
                        f"{bat_cap / best_df.capacity.sum()}."
158
                    )
159
160
                pv_df = best_df.copy()
161
162
            bat_df = pv_df.drop(columns=["capacity"]).assign(
163
                capacity=pv_df.capacity / pv_df.capacity.sum() * bat_cap,
164
                p_nom=pv_df.capacity
165
                / pv_df.capacity.sum()
166
                * bat_cap
167
                / cbat_pbat_ratio,
168
                scenario=scenario,
169
                bus_id=bus_id,
170
            )
171
172
            df_list.append(bat_df)
173
174
    create_table(pd.concat(df_list, ignore_index=True))
175
176
177
class EgonHomeBatteries(Base):
178
    targets = config.datasets()["home_batteries"]["targets"]
179
180
    __tablename__ = targets["home_batteries"]["table"]
181
    __table_args__ = {"schema": targets["home_batteries"]["schema"]}
182
183
    index = Column(Integer, primary_key=True, index=True)
184
    scenario = Column(String)
185
    bus_id = Column(Integer)
186
    building_id = Column(Integer)
187
    p_nom = Column(Float)
188
    capacity = Column(Float)
189
190
191
def create_table(df):
192
    """Create mapping table home battery <-> building id"""
193
    engine = db.engine()
194
195
    EgonHomeBatteries.__table__.drop(bind=engine, checkfirst=True)
196
    EgonHomeBatteries.__table__.create(bind=engine, checkfirst=True)
197
198
    df.reset_index().to_sql(
199
        name=EgonHomeBatteries.__table__.name,
200
        schema=EgonHomeBatteries.__table__.schema,
201
        con=engine,
202
        if_exists="append",
203
        index=False,
204
    )
205