validation: add structure for price and crossborder comparison
This commit is contained in:
parent
07add5a10d
commit
406ce3d77e
12
config/test/config.validation.yaml
Normal file
12
config/test/config.validation.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# SPDX-FileCopyrightText: : 2017-2023 The PyPSA-Eur Authors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
run:
|
||||||
|
name: "validation-test" # use this to keep track of runs with different settings
|
||||||
|
|
||||||
|
scenario:
|
||||||
|
clusters: # number of nodes in Europe, any integer between 37 (1 node per country-zone) and several hundred
|
||||||
|
- 37
|
||||||
|
opts: # only relevant for PyPSA-Eur
|
||||||
|
- 'Ept-12h'
|
@ -84,6 +84,7 @@ rule validate_elec_networks:
|
|||||||
),
|
),
|
||||||
expand(
|
expand(
|
||||||
RESULTS
|
RESULTS
|
||||||
+ "figures/.validation_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}",
|
+ "figures/.validation_{kind}_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}",
|
||||||
**config["scenario"]
|
**config["scenario"],
|
||||||
|
kind=["production", "prices", "cross_border"]
|
||||||
),
|
),
|
||||||
|
@ -16,7 +16,7 @@ def memory(w):
|
|||||||
factor *= int(m.group(1)) / 8760
|
factor *= int(m.group(1)) / 8760
|
||||||
break
|
break
|
||||||
if w.clusters.endswith("m") or w.clusters.endswith("c"):
|
if w.clusters.endswith("m") or w.clusters.endswith("c"):
|
||||||
return int(factor * (35000 + 180 * int(w.clusters[:-1])))
|
return int(factor * (35000 + 600 * int(w.clusters[:-1])))
|
||||||
elif w.clusters == "all":
|
elif w.clusters == "all":
|
||||||
return int(factor * (18000 + 180 * 4000))
|
return int(factor * (18000 + 180 * 4000))
|
||||||
else:
|
else:
|
||||||
|
@ -2,6 +2,14 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: MIT
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
PRODUCTION_PLOTS = [
|
||||||
|
"production_bar",
|
||||||
|
"production_deviation_bar",
|
||||||
|
"seasonal_operation_area",
|
||||||
|
]
|
||||||
|
CROSS_BORDER_PLOTS = []
|
||||||
|
PRICES_PLOTS = ["price_bar"]
|
||||||
|
|
||||||
|
|
||||||
rule build_electricity_production:
|
rule build_electricity_production:
|
||||||
"""
|
"""
|
||||||
@ -21,10 +29,45 @@ rule build_electricity_production:
|
|||||||
"../scripts/build_electricity_production.py"
|
"../scripts/build_electricity_production.py"
|
||||||
|
|
||||||
|
|
||||||
PLOTS = ["production_bar", "production_deviation_bar", "seasonal_operation_area"]
|
rule build_cross_border_flows:
|
||||||
|
"""
|
||||||
|
This rule builds the cross-border flows from ENTSO-E data.
|
||||||
|
The data is used for validation of the optimization results.
|
||||||
|
"""
|
||||||
|
params:
|
||||||
|
snapshots=config["snapshots"],
|
||||||
|
countries=config["countries"],
|
||||||
|
input:
|
||||||
|
network=RESOURCES + "networks/base.nc",
|
||||||
|
output:
|
||||||
|
RESOURCES + "historical_cross_border_flows.csv",
|
||||||
|
log:
|
||||||
|
LOGS + "build_cross_border_flows.log",
|
||||||
|
resources:
|
||||||
|
mem_mb=5000,
|
||||||
|
script:
|
||||||
|
"../scripts/build_cross_border_flows.py"
|
||||||
|
|
||||||
|
|
||||||
rule plot_electricity_production:
|
rule build_electricity_prices:
|
||||||
|
"""
|
||||||
|
This rule builds the electricity prices from ENTSO-E data.
|
||||||
|
The data is used for validation of the optimization results.
|
||||||
|
"""
|
||||||
|
params:
|
||||||
|
snapshots=config["snapshots"],
|
||||||
|
countries=config["countries"],
|
||||||
|
output:
|
||||||
|
RESOURCES + "historical_electricity_prices.csv",
|
||||||
|
log:
|
||||||
|
LOGS + "build_electricity_prices.log",
|
||||||
|
resources:
|
||||||
|
mem_mb=5000,
|
||||||
|
script:
|
||||||
|
"../scripts/build_electricity_prices.py"
|
||||||
|
|
||||||
|
|
||||||
|
rule plot_validation_electricity_production:
|
||||||
input:
|
input:
|
||||||
network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc",
|
network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc",
|
||||||
electricity_production=RESOURCES + "historical_electricity_production.csv",
|
electricity_production=RESOURCES + "historical_electricity_production.csv",
|
||||||
@ -32,9 +75,41 @@ rule plot_electricity_production:
|
|||||||
**{
|
**{
|
||||||
plot: RESULTS
|
plot: RESULTS
|
||||||
+ f"figures/validation_{plot}_elec_s{{simpl}}_{{clusters}}_ec_l{{ll}}_{{opts}}.pdf"
|
+ f"figures/validation_{plot}_elec_s{{simpl}}_{{clusters}}_ec_l{{ll}}_{{opts}}.pdf"
|
||||||
for plot in PLOTS
|
for plot in PRODUCTION_PLOTS
|
||||||
},
|
},
|
||||||
plots_touch=RESULTS
|
plots_touch=RESULTS
|
||||||
+ "figures/.validation_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}",
|
+ "figures/.validation_production_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}",
|
||||||
script:
|
script:
|
||||||
"../scripts/plot_electricity_production.py"
|
"../scripts/plot_validation_electricity_production.py"
|
||||||
|
|
||||||
|
|
||||||
|
rule plot_validation_cross_border_flows:
|
||||||
|
input:
|
||||||
|
network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc",
|
||||||
|
cross_border_flows=RESOURCES + "historical_cross_border_flows.csv",
|
||||||
|
output:
|
||||||
|
**{
|
||||||
|
plot: RESULTS
|
||||||
|
+ f"figures/validation_{plot}_elec_s{{simpl}}_{{clusters}}_ec_l{{ll}}_{{opts}}.pdf"
|
||||||
|
for plot in CROSS_BORDER_PLOTS
|
||||||
|
},
|
||||||
|
plots_touch=RESULTS
|
||||||
|
+ "figures/.validation_cross_border_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}",
|
||||||
|
script:
|
||||||
|
"../scripts/plot_validation_cross_border_flows.py"
|
||||||
|
|
||||||
|
|
||||||
|
rule plot_validation_electricity_prices:
|
||||||
|
input:
|
||||||
|
network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc",
|
||||||
|
electricity_prices=RESOURCES + "historical_electricity_prices.csv",
|
||||||
|
output:
|
||||||
|
**{
|
||||||
|
plot: RESULTS
|
||||||
|
+ f"figures/validation_{plot}_elec_s{{simpl}}_{{clusters}}_ec_l{{ll}}_{{opts}}.pdf"
|
||||||
|
for plot in PRICES_PLOTS
|
||||||
|
},
|
||||||
|
plots_touch=RESULTS
|
||||||
|
+ "figures/.validation_prices_plots_elec_s{simpl}_{clusters}_ec_l{ll}_{opts}",
|
||||||
|
script:
|
||||||
|
"../scripts/plot_validation_electricity_prices.py"
|
||||||
|
@ -337,7 +337,7 @@ def _load_lines_from_eg(buses, eg_lines):
|
|||||||
)
|
)
|
||||||
|
|
||||||
lines["length"] /= 1e3
|
lines["length"] /= 1e3
|
||||||
|
lines["carrier"] = "AC"
|
||||||
lines = _remove_dangling_branches(lines, buses)
|
lines = _remove_dangling_branches(lines, buses)
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
65
scripts/build_cross_border_flows.py
Normal file
65
scripts/build_cross_border_flows.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-FileCopyrightText: : 2017-2023 The PyPSA-Eur Authors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pypsa
|
||||||
|
from _helpers import configure_logging
|
||||||
|
from entsoe import EntsoePandasClient
|
||||||
|
from entsoe.exceptions import InvalidBusinessParameterError, NoMatchingDataError
|
||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if "snakemake" not in globals():
|
||||||
|
from _helpers import mock_snakemake
|
||||||
|
|
||||||
|
snakemake = mock_snakemake("build_cross_border_flows")
|
||||||
|
configure_logging(snakemake)
|
||||||
|
|
||||||
|
api_key = snakemake.config["private"]["keys"]["entsoe_api"]
|
||||||
|
client = EntsoePandasClient(api_key=api_key)
|
||||||
|
|
||||||
|
n = pypsa.Network(snakemake.input.network)
|
||||||
|
start = pd.Timestamp(snakemake.params.snapshots["start"], tz="Europe/Brussels")
|
||||||
|
end = pd.Timestamp(snakemake.params.snapshots["end"], tz="Europe/Brussels")
|
||||||
|
|
||||||
|
branches = n.branches().query("carrier in ['AC', 'DC']")
|
||||||
|
c = n.buses.country
|
||||||
|
branch_countries = pd.concat([branches.bus0.map(c), branches.bus1.map(c)], axis=1)
|
||||||
|
branch_countries = branch_countries.query("bus0 != bus1")
|
||||||
|
branch_countries = branch_countries.apply(sorted, axis=1, result_type="broadcast")
|
||||||
|
country_pairs = branch_countries.drop_duplicates().reset_index(drop=True)
|
||||||
|
|
||||||
|
flows = []
|
||||||
|
unavailable_borders = []
|
||||||
|
for from_country, to_country in country_pairs.values:
|
||||||
|
try:
|
||||||
|
flow_directed = client.query_crossborder_flows(
|
||||||
|
from_country, to_country, start=start, end=end
|
||||||
|
)
|
||||||
|
flow_reverse = client.query_crossborder_flows(
|
||||||
|
to_country, from_country, start=start, end=end
|
||||||
|
)
|
||||||
|
flow = (flow_directed - flow_reverse).rename(
|
||||||
|
f"{from_country} - {to_country}"
|
||||||
|
)
|
||||||
|
flow = flow.tz_localize(None).resample("1h").mean()
|
||||||
|
flow = flow.loc[start.tz_localize(None) : end.tz_localize(None)]
|
||||||
|
flows.append(flow)
|
||||||
|
except (HTTPError, NoMatchingDataError, InvalidBusinessParameterError):
|
||||||
|
unavailable_borders.append(f"{from_country}-{to_country}")
|
||||||
|
|
||||||
|
if unavailable_borders:
|
||||||
|
logger.warning(
|
||||||
|
"Historical electricity cross-border flows for countries"
|
||||||
|
f" {', '.join(unavailable_borders)} not available."
|
||||||
|
)
|
||||||
|
|
||||||
|
flows = pd.concat(flows, axis=1)
|
||||||
|
flows.to_csv(snakemake.output[0])
|
52
scripts/build_electricity_prices.py
Normal file
52
scripts/build_electricity_prices.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-FileCopyrightText: : 2017-2023 The PyPSA-Eur Authors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from _helpers import configure_logging
|
||||||
|
from entsoe import EntsoePandasClient
|
||||||
|
from entsoe.exceptions import NoMatchingDataError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if "snakemake" not in globals():
|
||||||
|
from _helpers import mock_snakemake
|
||||||
|
|
||||||
|
snakemake = mock_snakemake("build_cross_border_flows")
|
||||||
|
configure_logging(snakemake)
|
||||||
|
|
||||||
|
api_key = snakemake.config["private"]["keys"]["entsoe_api"]
|
||||||
|
client = EntsoePandasClient(api_key=api_key)
|
||||||
|
|
||||||
|
start = pd.Timestamp(snakemake.params.snapshots["start"], tz="Europe/Brussels")
|
||||||
|
end = pd.Timestamp(snakemake.params.snapshots["end"], tz="Europe/Brussels")
|
||||||
|
|
||||||
|
countries = snakemake.params.countries
|
||||||
|
|
||||||
|
prices = []
|
||||||
|
unavailable_countries = []
|
||||||
|
|
||||||
|
for country in countries:
|
||||||
|
country_code = country
|
||||||
|
|
||||||
|
try:
|
||||||
|
gen = client.query_day_ahead_prices(country, start=start, end=end)
|
||||||
|
gen = gen.tz_localize(None).resample("1h").mean()
|
||||||
|
gen = gen.loc[start.tz_localize(None) : end.tz_localize(None)]
|
||||||
|
prices.append(gen)
|
||||||
|
except NoMatchingDataError:
|
||||||
|
unavailable_countries.append(country)
|
||||||
|
|
||||||
|
if unavailable_countries:
|
||||||
|
logger.warning(
|
||||||
|
f"Historical electricity prices for countries {', '.join(unavailable_countries)} not available."
|
||||||
|
)
|
||||||
|
|
||||||
|
keys = [c for c in countries if c not in unavailable_countries]
|
||||||
|
prices = pd.concat(prices, keys=keys, axis=1)
|
||||||
|
prices.to_csv(snakemake.output[0])
|
57
scripts/plot_validation_electricity_prices.py
Normal file
57
scripts/plot_validation_electricity_prices.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-FileCopyrightText: : 2017-2023 The PyPSA-Eur Authors
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import pandas as pd
|
||||||
|
import pypsa
|
||||||
|
import seaborn as sns
|
||||||
|
from _helpers import configure_logging
|
||||||
|
from pypsa.statistics import get_bus_and_carrier
|
||||||
|
|
||||||
|
sns.set_theme("paper", style="whitegrid")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if "snakemake" not in globals():
|
||||||
|
from _helpers import mock_snakemake
|
||||||
|
|
||||||
|
snakemake = mock_snakemake(
|
||||||
|
"plot_electricity_prices",
|
||||||
|
simpl="",
|
||||||
|
opts="Ept-12h",
|
||||||
|
clusters="37",
|
||||||
|
ll="v1.0",
|
||||||
|
)
|
||||||
|
configure_logging(snakemake)
|
||||||
|
|
||||||
|
n = pypsa.Network(snakemake.input.network)
|
||||||
|
n.loads.carrier = "load"
|
||||||
|
|
||||||
|
historic = pd.read_csv(
|
||||||
|
snakemake.input.electricity_prices,
|
||||||
|
index_col=0,
|
||||||
|
header=[0, 1],
|
||||||
|
parse_dates=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(historic.index) > len(n.snapshots):
|
||||||
|
historic = historic.resample(n.snapshots.inferred_freq).mean().loc[n.snapshots]
|
||||||
|
|
||||||
|
optimized = n.buses_t.marginal_price.groupby(n.buses.country, axis=1).mean()
|
||||||
|
|
||||||
|
data = pd.concat([historic, optimized], keys=["Historic", "Optimized"], axis=1)
|
||||||
|
data.columns.names = ["Kind", "Country"]
|
||||||
|
|
||||||
|
# %% total production per carrier
|
||||||
|
fig, ax = plt.subplots(figsize=(6, 6))
|
||||||
|
|
||||||
|
df = data.mean().unstack().T
|
||||||
|
df.plot.barh(ax=ax, xlabel="Electricity Price [€/MWh]", ylabel="")
|
||||||
|
ax.grid(axis="y")
|
||||||
|
fig.savefig(snakemake.output.price_bar, bbox_inches="tight")
|
||||||
|
|
||||||
|
# touch file
|
||||||
|
with open(snakemake.output.plots_touch, "a"):
|
||||||
|
pass
|
@ -45,8 +45,6 @@ if __name__ == "__main__":
|
|||||||
header=[0, 1],
|
header=[0, 1],
|
||||||
parse_dates=True,
|
parse_dates=True,
|
||||||
)
|
)
|
||||||
historic = historic.drop("Other renewable", axis=1, level=1)
|
|
||||||
historic = historic.drop("Marine", axis=1, level=1)
|
|
||||||
|
|
||||||
colors = n.carriers.set_index("nice_name").color.where(
|
colors = n.carriers.set_index("nice_name").color.where(
|
||||||
lambda s: s != "", "lightgrey"
|
lambda s: s != "", "lightgrey"
|
Loading…
Reference in New Issue
Block a user