Merge pull request #957 from PyPSA/land-transport-fix

Land transport fix
This commit is contained in:
Fabian Neumann 2024-05-15 17:13:56 +02:00 committed by GitHub
commit 44fb8cad1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 280 additions and 187 deletions

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
#
# SPDX-License-Identifier: CC0-1.0
master
.snakemake*
.ipynb_checkpoints
__pycache__

View File

@ -431,7 +431,6 @@ sector:
bev_availability: 0.5
bev_energy: 0.05
bev_charge_efficiency: 0.9
bev_plug_to_wheel_efficiency: 0.2
bev_charge_rate: 0.011
bev_avail_max: 0.95
bev_avail_mean: 0.8
@ -460,8 +459,9 @@ sector:
2040: 0.3
2045: 0.15
2050: 0
transport_fuel_cell_efficiency: 0.5
transport_internal_combustion_efficiency: 0.3
transport_electric_efficiency: 53.19 # 1 MWh_el = 53.19*100 km
transport_fuel_cell_efficiency: 30.003 # 1 MWh_H2 = 30.003*100 km
transport_ice_efficiency: 16.0712 # 1 MWh_oil = 16.0712 * 100 km
agriculture_machinery_electric_share: 0
agriculture_machinery_oil_share: 1
agriculture_machinery_fuel_efficiency: 0.7
@ -1041,6 +1041,7 @@ plotting:
BEV charger: '#baf238'
V2G: '#e5ffa8'
land transport EV: '#baf238'
land transport demand: '#38baf2'
Li ion: '#baf238'
# hot water storage
water tanks: '#e69487'

View File

@ -24,7 +24,6 @@ bev_dsm,--,"{true, false}",Add the option for battery electric vehicles (BEV) to
bev_availability,--,float,The share for battery electric vehicles (BEV) that are able to do demand side management (DSM)
bev_energy,--,float,The average size of battery electric vehicles (BEV) in MWh
bev_charge_efficiency,--,float,Battery electric vehicles (BEV) charge and discharge efficiency
bev_plug_to_wheel _efficiency,km/kWh,float,The distance battery electric vehicles (BEV) can travel in km per kWh of energy charge in battery. Base value comes from `Tesla Model S <https://www.fueleconomy.gov/feg/>`_
bev_charge_rate,MWh,float,The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW.
bev_avail_max,--,float,The maximum share plugged-in availability for passenger electric vehicles.
bev_avail_mean,--,float,The average share plugged-in availability for passenger electric vehicles.
@ -32,8 +31,9 @@ v2g,--,"{true, false}",Allows feed-in to grid from EV battery
land_transport_fuel_cell _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses fuel cells in a given year
land_transport_electric _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses electric vehicles (EV) in a given year
land_transport_ice _share,--,Dictionary with planning horizons as keys.,The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE.
transport_fuel_cell _efficiency,--,float,The H2 conversion efficiencies of fuel cells in transport
transport_internal _combustion_efficiency,--,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport
transport_electric_efficiency,MWh/100km,float,The conversion efficiencies of electric vehicles in transport
transport_fuel_cell_efficiency,MWh/100km,float,The H2 conversion efficiencies of fuel cells in transport
transport_ice_efficiency,MWh/100km,float,The oil conversion efficiencies of internal combustion engine (ICE) in transport
agriculture_machinery _electric_share,--,float,The share for agricultural machinery that uses electricity
agriculture_machinery _oil_share,--,float,The share for agricultural machinery that uses oil
agriculture_machinery _fuel_efficiency,--,float,The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs.

1 Unit Values Description
24 bev_availability -- float The share for battery electric vehicles (BEV) that are able to do demand side management (DSM)
25 bev_energy -- float The average size of battery electric vehicles (BEV) in MWh
26 bev_charge_efficiency -- float Battery electric vehicles (BEV) charge and discharge efficiency
bev_plug_to_wheel _efficiency km/kWh float The distance battery electric vehicles (BEV) can travel in km per kWh of energy charge in battery. Base value comes from `Tesla Model S <https://www.fueleconomy.gov/feg/>`_
27 bev_charge_rate MWh float The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW.
28 bev_avail_max -- float The maximum share plugged-in availability for passenger electric vehicles.
29 bev_avail_mean -- float The average share plugged-in availability for passenger electric vehicles.
31 land_transport_fuel_cell _share -- Dictionary with planning horizons as keys. The share of vehicles that uses fuel cells in a given year
32 land_transport_electric _share -- Dictionary with planning horizons as keys. The share of vehicles that uses electric vehicles (EV) in a given year
33 land_transport_ice _share -- Dictionary with planning horizons as keys. The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE.
34 transport_fuel_cell _efficiency transport_electric_efficiency -- MWh/100km float The H2 conversion efficiencies of fuel cells in transport The conversion efficiencies of electric vehicles in transport
35 transport_internal _combustion_efficiency transport_fuel_cell_efficiency -- MWh/100km float The oil conversion efficiencies of internal combustion engine (ICE) in transport The H2 conversion efficiencies of fuel cells in transport
36 transport_ice_efficiency MWh/100km float The oil conversion efficiencies of internal combustion engine (ICE) in transport
37 agriculture_machinery _electric_share -- float The share for agricultural machinery that uses electricity
38 agriculture_machinery _oil_share -- float The share for agricultural machinery that uses oil
39 agriculture_machinery _fuel_efficiency -- float The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs.

View File

@ -81,6 +81,9 @@ Upcoming Release
* bugfix: convert Strings to pathlib.Path objects as input to ConfigSettings
* bugfix: fix distinction of temperature-dependent correction factors for the
energy demand of electric vehicles, ICES fuel cell cars.
* Allow the use of more solvers in clustering (Xpress, COPT, Gurobi, CPLEX, SCIP, MOSEK).
* Enhanced support for choosing different weather years

View File

@ -396,13 +396,12 @@ def build_idees(countries):
names=["country", "year"],
)
# efficiency kgoe/100km -> ktoe/100km
totals.loc[:, "passenger car efficiency"] *= 1e3
# convert ktoe to TWh
exclude = totals.columns.str.fullmatch("passenger cars")
totals.loc[:, ~exclude] *= 11.63 / 1e3
# convert TWh/100km to kWh/km
totals.loc[:, "passenger car efficiency"] *= 10
return totals

View File

@ -24,14 +24,17 @@ logger = logging.getLogger(__name__)
def build_nodal_transport_data(fn, pop_layout, year):
# get numbers of car and fuel efficiency per country
transport_data = pd.read_csv(fn, index_col=[0, 1])
transport_data = transport_data.xs(min(2015, year), level="year")
# break number of cars down to nodal level based on population density
nodal_transport_data = transport_data.loc[pop_layout.ct].fillna(0.0)
nodal_transport_data.index = pop_layout.index
nodal_transport_data["number cars"] = (
pop_layout["fraction"] * nodal_transport_data["number cars"]
)
# fill missing fuel efficiency with average data
nodal_transport_data.loc[
nodal_transport_data["average fuel efficiency"] == 0.0,
"average fuel efficiency",
@ -41,10 +44,13 @@ def build_nodal_transport_data(fn, pop_layout, year):
def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data):
## Get overall demand curve for all vehicles
"""
Returns transport demand per bus in unit km driven [100 km].
"""
# averaged weekly counts from the year 2010-2015
traffic = pd.read_csv(traffic_fn, skiprows=2, usecols=["count"]).squeeze("columns")
# create annual profile take account time zone + summer time
transport_shape = generate_periodic_profiles(
dt_index=snapshots,
nodes=nodes,
@ -52,15 +58,6 @@ def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data):
)
transport_shape = transport_shape / transport_shape.sum()
# electric motors are more efficient, so alter transport demand
plug_to_wheels_eta = options["bev_plug_to_wheel_efficiency"]
battery_to_wheels_eta = plug_to_wheels_eta * options["bev_charge_efficiency"]
efficiency_gain = (
nodal_transport_data["average fuel efficiency"] / battery_to_wheels_eta
)
# get heating demand for correction to demand time series
temperature = xr.open_dataarray(airtemp_fn).to_pandas()
@ -73,16 +70,7 @@ def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data):
options["ICE_upper_degree_factor"],
)
dd_EV = transport_degree_factor(
temperature,
options["transport_heating_deadband_lower"],
options["transport_heating_deadband_upper"],
options["EV_lower_degree_factor"],
options["EV_upper_degree_factor"],
)
# divide out the heating/cooling demand from ICE totals
# and multiply back in the heating/cooling demand for EVs
ice_correction = (transport_shape * (1 + dd_ICE)).sum() / transport_shape.sum()
energy_totals_transport = (
@ -91,10 +79,11 @@ def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data):
- pop_weighted_energy_totals["electricity rail"]
)
return (
(transport_shape.multiply(energy_totals_transport) * 1e6 * nyears)
.divide(efficiency_gain * ice_correction)
.multiply(1 + dd_EV)
# convert average fuel efficiency from kW/100 km -> MW/100km
eff = nodal_transport_data["average fuel efficiency"] / 1e3
return (transport_shape.multiply(energy_totals_transport) * 1e6 * nyears).divide(
eff * ice_correction
)
@ -131,11 +120,14 @@ def bev_availability_profile(fn, snapshots, nodes, options):
"""
Derive plugged-in availability for passenger electric vehicles.
"""
# car count in typical week
traffic = pd.read_csv(fn, skiprows=2, usecols=["count"]).squeeze("columns")
# maximum share plugged-in availability for passenger electric vehicles
avail_max = options["bev_avail_max"]
# average share plugged-in availability for passenger electric vehicles
avail_mean = options["bev_avail_mean"]
# linear scaling, highest when traffic is lowest, decreases if traffic increases
avail = avail_max - (avail_max - avail_mean) * (traffic - traffic.min()) / (
traffic.mean() - traffic.min()
)
@ -156,6 +148,8 @@ def bev_availability_profile(fn, snapshots, nodes, options):
def bev_dsm_profile(snapshots, nodes, options):
dsm_week = np.zeros((24 * 7,))
# assuming that at a certain time ("bev_dsm_restriction_time") EVs have to
# be charged to a minimum value (defined in bev_dsm_restriction_value)
dsm_week[(np.arange(0, 7, 1) * 24 + options["bev_dsm_restriction_time"])] = options[
"bev_dsm_restriction_value"
]
@ -167,6 +161,7 @@ def bev_dsm_profile(snapshots, nodes, options):
)
# %%
if __name__ == "__main__":
if "snakemake" not in globals():
from _helpers import mock_snakemake
@ -174,7 +169,7 @@ if __name__ == "__main__":
snakemake = mock_snakemake(
"build_transport_demand",
simpl="",
clusters=60,
clusters=37,
)
configure_logging(snakemake)
set_scenario_config(snakemake)

View File

@ -29,6 +29,7 @@ from build_energy_totals import (
build_eurostat,
build_eurostat_co2,
)
from build_transport_demand import transport_degree_factor
from networkx.algorithms import complement
from networkx.algorithms.connectivity.edge_augmentation import k_edge_augmentation
from prepare_network import maybe_adjust_costs_and_potentials
@ -1501,103 +1502,122 @@ def add_storage_and_grids(n, costs):
)
def add_land_transport(n, costs):
# TODO options?
logger.info("Add land transport")
transport = pd.read_csv(
snakemake.input.transport_demand, index_col=0, parse_dates=True
)
number_cars = pd.read_csv(snakemake.input.transport_data, index_col=0)[
"number cars"
]
avail_profile = pd.read_csv(
snakemake.input.avail_profile, index_col=0, parse_dates=True
)
dsm_profile = pd.read_csv(
snakemake.input.dsm_profile, index_col=0, parse_dates=True
)
fuel_cell_share = get(options["land_transport_fuel_cell_share"], investment_year)
electric_share = get(options["land_transport_electric_share"], investment_year)
ice_share = get(options["land_transport_ice_share"], investment_year)
total_share = fuel_cell_share + electric_share + ice_share
def check_land_transport_shares(shares):
# Sums up the shares, ignoring None values
total_share = sum(filter(None, shares))
if total_share != 1:
logger.warning(
f"Total land transport shares sum up to {total_share:.2%}, corresponding to increased or decreased demand assumptions."
f"Total land transport shares sum up to {total_share:.2%},"
"corresponding to increased or decreased demand assumptions."
)
logger.info(f"FCEV share: {fuel_cell_share*100}%")
logger.info(f"EV share: {electric_share*100}%")
logger.info(f"ICEV share: {ice_share*100}%")
nodes = pop_layout.index
def get_temp_efficency(
car_efficiency,
temperature,
deadband_lw,
deadband_up,
degree_factor_lw,
degree_factor_up,
):
"""
Correct temperature depending on heating and cooling for respective car
type.
"""
# temperature correction for EVs
dd = transport_degree_factor(
temperature,
deadband_lw,
deadband_up,
degree_factor_lw,
degree_factor_up,
)
temp_eff = 1 / (1 + dd)
return car_efficiency * temp_eff
def add_EVs(
n,
avail_profile,
dsm_profile,
p_set,
electric_share,
number_cars,
temperature,
):
if electric_share > 0:
n.add("Carrier", "Li ion")
n.madd(
"Bus",
nodes,
spatial.nodes,
suffix=" EV battery",
location=nodes,
location=spatial.nodes,
carrier="Li ion",
unit="MWh_el",
)
p_set = (
electric_share
* (
transport[nodes]
+ cycling_shift(transport[nodes], 1)
+ cycling_shift(transport[nodes], 2)
)
/ 3
car_efficiency = options["transport_electric_efficiency"]
# temperature corrected efficiency
efficiency = get_temp_efficency(
car_efficiency,
temperature,
options["transport_heating_deadband_lower"],
options["transport_heating_deadband_upper"],
options["EV_lower_degree_factor"],
options["EV_upper_degree_factor"],
)
p_shifted = (p_set + cycling_shift(p_set, 1) + cycling_shift(p_set, 2)) / 3
cyclic_eff = p_set.div(p_shifted)
efficiency *= cyclic_eff
profile = electric_share * p_set.div(efficiency)
n.madd(
"Load",
nodes,
spatial.nodes,
suffix=" land transport EV",
bus=nodes + " EV battery",
bus=spatial.nodes + " EV battery",
carrier="land transport EV",
p_set=p_set,
p_set=profile,
)
p_nom = number_cars * options.get("bev_charge_rate", 0.011) * electric_share
n.madd(
"Link",
nodes,
spatial.nodes,
suffix=" BEV charger",
bus0=nodes,
bus1=nodes + " EV battery",
bus0=spatial.nodes,
bus1=spatial.nodes + " EV battery",
p_nom=p_nom,
carrier="BEV charger",
p_max_pu=avail_profile[nodes],
p_max_pu=avail_profile[spatial.nodes],
lifetime=1,
efficiency=options.get("bev_charge_efficiency", 0.9),
# These were set non-zero to find LU infeasibility when availability = 0.25
# p_nom_extendable=True,
# p_nom_min=p_nom,
# capital_cost=1e6, #i.e. so high it only gets built where necessary
)
if electric_share > 0 and options["v2g"]:
if options["v2g"]:
n.madd(
"Link",
nodes,
spatial.nodes,
suffix=" V2G",
bus1=nodes,
bus0=nodes + " EV battery",
bus1=spatial.nodes,
bus0=spatial.nodes + " EV battery",
p_nom=p_nom,
carrier="V2G",
p_max_pu=avail_profile[nodes],
p_max_pu=avail_profile[spatial.nodes],
lifetime=1,
efficiency=options.get("bev_charge_efficiency", 0.9),
)
if electric_share > 0 and options["bev_dsm"]:
if options["bev_dsm"]:
e_nom = (
number_cars
* options.get("bev_energy", 0.05)
@ -1607,43 +1627,65 @@ def add_land_transport(n, costs):
n.madd(
"Store",
nodes,
spatial.nodes,
suffix=" battery storage",
bus=nodes + " EV battery",
bus=spatial.nodes + " EV battery",
carrier="battery storage",
e_cyclic=True,
e_nom=e_nom,
e_max_pu=1,
e_min_pu=dsm_profile[nodes],
e_min_pu=dsm_profile[spatial.nodes],
)
if fuel_cell_share > 0:
def add_fuel_cell_cars(n, p_set, fuel_cell_share, temperature):
car_efficiency = options["transport_fuel_cell_efficiency"]
# temperature corrected efficiency
efficiency = get_temp_efficency(
car_efficiency,
temperature,
options["transport_heating_deadband_lower"],
options["transport_heating_deadband_upper"],
options["ICE_lower_degree_factor"],
options["ICE_upper_degree_factor"],
)
profile = fuel_cell_share * p_set.div(efficiency)
n.madd(
"Load",
nodes,
spatial.nodes,
suffix=" land transport fuel cell",
bus=nodes + " H2",
bus=spatial.h2.nodes,
carrier="land transport fuel cell",
p_set=fuel_cell_share
/ options["transport_fuel_cell_efficiency"]
* transport[nodes],
p_set=profile,
)
if ice_share > 0:
def add_ice_cars(n, p_set, ice_share, temperature):
add_carrier_buses(n, "oil")
ice_efficiency = options["transport_internal_combustion_efficiency"]
car_efficiency = options["transport_ice_efficiency"]
p_set_land_transport_oil = (
ice_share
/ ice_efficiency
* transport[nodes].rename(columns=lambda x: x + " land transport oil")
# temperature corrected efficiency
efficiency = get_temp_efficency(
car_efficiency,
temperature,
options["transport_heating_deadband_lower"],
options["transport_heating_deadband_upper"],
options["ICE_lower_degree_factor"],
options["ICE_upper_degree_factor"],
)
profile = ice_share * p_set.div(efficiency).rename(
columns=lambda x: x + " land transport oil"
)
if not options["regional_oil_demand"]:
p_set_land_transport_oil = p_set_land_transport_oil.sum(axis=1).to_frame(
name="EU land transport oil"
)
profile = profile.sum(axis=1).to_frame(name="EU land transport oil")
n.madd(
"Bus",
@ -1658,7 +1700,7 @@ def add_land_transport(n, costs):
spatial.oil.land_transport,
bus=spatial.oil.land_transport,
carrier="land transport oil",
p_set=p_set_land_transport_oil,
p_set=profile,
)
n.madd(
@ -1673,6 +1715,56 @@ def add_land_transport(n, costs):
)
def add_land_transport(n, costs):
logger.info("Add land transport")
# read in transport demand in units driven km [100 km]
transport = pd.read_csv(
snakemake.input.transport_demand, index_col=0, parse_dates=True
)
number_cars = pd.read_csv(snakemake.input.transport_data, index_col=0)[
"number cars"
]
avail_profile = pd.read_csv(
snakemake.input.avail_profile, index_col=0, parse_dates=True
)
dsm_profile = pd.read_csv(
snakemake.input.dsm_profile, index_col=0, parse_dates=True
)
# exogenous share of passenger car type
engine_types = ["fuel_cell", "electric", "ice"]
shares = pd.Series()
for engine in engine_types:
shares[engine] = get(options[f"land_transport_{engine}_share"], investment_year)
logger.info(f"{engine} share: {shares[engine]*100}%")
check_land_transport_shares(shares)
p_set = transport[spatial.nodes]
# temperature for correction factor for heating/cooling
temperature = xr.open_dataarray(snakemake.input.temp_air_total).to_pandas()
if shares["electric"] > 0:
add_EVs(
n,
avail_profile,
dsm_profile,
p_set,
shares["electric"],
number_cars,
temperature,
)
if shares["fuel_cell"] > 0:
add_fuel_cell_cars(n, p_set, shares["fuel_cell"], temperature)
if shares["ice"] > 0:
add_ice_cars(n, p_set, shares["ice"], temperature)
def build_heat_demand(n):
heat_demand_shape = (
xr.open_dataset(snakemake.input.hourly_heat_demand_total)
@ -3666,19 +3758,20 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}):
)
# %%
if __name__ == "__main__":
if "snakemake" not in globals():
from _helpers import mock_snakemake
snakemake = mock_snakemake(
"prepare_sector_network",
configfiles="test/config.overnight.yaml",
# configfiles="test/config.overnight.yaml",
simpl="",
opts="",
clusters="37",
ll="v1.0",
sector_opts="CO2L0-24h-T-H-B-I-A-dist1",
planning_horizons="2030",
sector_opts="730H-T-H-B-I-A-dist1",
planning_horizons="2050",
)
configure_logging(snakemake)