From b70aa59dce9576ede6d69b5d716073520e4a218c Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 1 Mar 2024 11:32:12 +0100 Subject: [PATCH 01/60] safe transport demand in unit kinetic energy --- scripts/build_transport_demand.py | 52 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/scripts/build_transport_demand.py b/scripts/build_transport_demand.py index de561e3f8..7e54b5670 100644 --- a/scripts/build_transport_demand.py +++ b/scripts/build_transport_demand.py @@ -19,13 +19,16 @@ def build_nodal_transport_data(fn, pop_layout): + # get numbers of car and fuel efficieny per country transport_data = pd.read_csv(fn, index_col=0) - + + # 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", @@ -35,10 +38,14 @@ def build_nodal_transport_data(fn, pop_layout): def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data): - ## Get overall demand curve for all vehicles - - traffic = pd.read_csv(traffic_fn, skiprows=2, usecols=["count"]).squeeze("columns") - + """ + returns transport demand per bus in unit kinetic energy. + """ + # 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, @@ -46,15 +53,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() @@ -67,16 +65,8 @@ 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 = ( @@ -87,8 +77,7 @@ def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data): return ( (transport_shape.multiply(energy_totals_transport) * 1e6 * nyears) - .divide(efficiency_gain * ice_correction) - .multiply(1 + dd_EV) + .divide(nodal_transport_data["average fuel efficiency"] * ice_correction) ) @@ -125,11 +114,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() ) @@ -149,7 +141,9 @@ 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" ] @@ -160,7 +154,7 @@ def bev_dsm_profile(snapshots, nodes, options): weekly_profile=dsm_week, ) - +#%% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake @@ -168,7 +162,7 @@ def bev_dsm_profile(snapshots, nodes, options): snakemake = mock_snakemake( "build_transport_demand", simpl="", - clusters=48, + clusters=37, ) configure_logging(snakemake) set_scenario_config(snakemake) From 548a99f273c2e1860d28cfa2d32469959ffe915e Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 1 Mar 2024 15:19:00 +0100 Subject: [PATCH 02/60] restructure to links without temperature correction --- scripts/prepare_sector_network.py | 311 +++++++++++++++++------------- 1 file changed, 176 insertions(+), 135 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9a9db36fa..67b3fd7dc 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -24,6 +24,7 @@ ) from add_electricity import calculate_annuity, sanitize_carriers, sanitize_locations from build_energy_totals import build_co2_totals, build_eea_co2, 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 @@ -1502,90 +1503,71 @@ 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 - - if electric_share > 0: - n.add("Carrier", "Li ion") +def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, + number_cars, temperature): + + n.add("Carrier", "Li ion") - n.madd( - "Bus", - nodes, - suffix=" EV battery", - location=nodes, - carrier="Li ion", - unit="MWh_el", - ) - - p_set = ( - electric_share - * ( - transport[nodes] - + cycling_shift(transport[nodes], 1) - + cycling_shift(transport[nodes], 2) - ) - / 3 - ) - - n.madd( - "Load", - nodes, - suffix=" land transport EV", - bus=nodes + " EV battery", - carrier="land transport EV", - p_set=p_set, - ) + n.madd( + "Bus", + nodes, + suffix=" EV battery", + location=nodes, + carrier="Li ion", + unit="MWh_el", + ) + + # temperature correction for EVs + 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"], + ) + + profile = p_set/p_set.max() + efficiency = costs.at['Battery electric (passenger cars)', 'efficiency'] + + n.madd( + "Link", + nodes, + suffix=" land transport EV", + bus0=nodes + " EV battery", + bus1=nodes + " land transport", + carrier="land transport EV", + efficiency=efficiency, + p_min_pu=profile, + p_max_pu=profile, + p_nom=electric_share*p_set.max()/efficiency, + p_nom_extendable=False, + ) + - p_nom = number_cars * options.get("bev_charge_rate", 0.011) * electric_share + p_nom = number_cars * options.get("bev_charge_rate", 0.011) * electric_share - n.madd( - "Link", - nodes, - suffix=" BEV charger", - bus0=nodes, - bus1=nodes + " EV battery", - p_nom=p_nom, - carrier="BEV charger", - p_max_pu=avail_profile[nodes], - 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 - ) + n.madd( + "Link", + nodes, + suffix=" BEV charger", + bus0=nodes, + bus1=nodes + " EV battery", + p_nom=p_nom, + carrier="BEV charger", + p_max_pu=avail_profile[nodes], + efficiency=options.get("bev_charge_efficiency", 0.9), + ) - if electric_share > 0 and options["v2g"]: + if options["v2g"]: n.madd( "Link", nodes, @@ -1597,15 +1579,15 @@ def add_land_transport(n, costs): p_max_pu=avail_profile[nodes], 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) * options["bev_availability"] * electric_share ) - + n.madd( "Store", nodes, @@ -1617,61 +1599,120 @@ def add_land_transport(n, costs): e_max_pu=1, e_min_pu=dsm_profile[nodes], ) + +def add_fuel_cell_cars(n, nodes, p_set, fuel_cell_share, temperature): + + efficiency = options["transport_fuel_cell_efficiency"] + profile = p_set / p_set.max() + p_nom = fuel_cell_share * p_set / efficiency + + n.madd( + "Link", + nodes, + suffix=" land transport fuel cell", + bus0=spatial.h2.nodes, + bus1=nodes + " land transport", + carrier="land transport fuel cell", + efficiency=efficiency, + p_nom_extendable=False, + p_nom=p_nom, + p_min_pu=profile, + p_max_pu=profile, + ) + + +def add_ice_cars(n, nodes, p_set, ice_share, temperature): + + add_carrier_buses(n, "oil") + + ice_efficiency = options["transport_internal_combustion_efficiency"] + + if not options["regional_oil_demand"]: + p_nom = ice_share * p_set.sum(axis=1).max() / ice_efficiency + else: + p_nom = ice_share * p_set.max() / ice_efficiency + + + n.madd( + "Link", + nodes + " land transport ICE", + bus0=spatial.oil.nodes, + bus1=nodes + " land transport", + bus2=["co2 atmosphere"], + carrier="land transport oil", + efficiency=ice_efficiency, + efficiency2=costs.at["oil", "CO2 intensity"], + p_nom_extendable=True, + # p_nom=p_nom, + capital_cost=1e4, + ) + +def add_land_transport(n, costs): + # TODO options? - if fuel_cell_share > 0: - n.madd( - "Load", + logger.info("Add land transport") + + # read in transport demand in units kinetic energy + 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) + + nodes = spatial.nodes + + # Add load for transport demand + n.add("Carrier", "land transport demand") + + n.madd("Bus", nodes, - suffix=" land transport fuel cell", - bus=nodes + " H2", - carrier="land transport fuel cell", - p_set=fuel_cell_share - / options["transport_fuel_cell_efficiency"] - * transport[nodes], - ) - - if ice_share > 0: - add_carrier_buses(n, "oil") - - ice_efficiency = options["transport_internal_combustion_efficiency"] - - p_set_land_transport_oil = ( - ice_share - / ice_efficiency - * transport[nodes].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" - ) - - n.madd( - "Bus", - spatial.oil.land_transport, - location=spatial.oil.demand_locations, - carrier="land transport oil", - unit="land transport", - ) - - n.madd( - "Load", - spatial.oil.land_transport, - bus=spatial.oil.land_transport, - carrier="land transport oil", - p_set=p_set_land_transport_oil, - ) - - n.madd( - "Link", - spatial.oil.land_transport, - bus0=spatial.oil.nodes, - bus1=spatial.oil.land_transport, - bus2="co2 atmosphere", - carrier="land transport oil", - efficiency2=costs.at["oil", "CO2 intensity"], - p_nom_extendable=True, - ) + location=nodes, + suffix=" land transport", + carrier="land transport demand", + unit="MWh_kinetic") + + p_set = transport[nodes] + + # add demand + n.madd( + "Load", + nodes, + suffix=" land transport", + bus=nodes + " land transport", + carrier="land transport demand", + p_set=p_set, + ) + + # 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, nodes, avail_profile, dsm_profile, p_set, shares["electric"], + number_cars, temperature) + + if shares["fuel_cell"] > 0: + add_fuel_cell_cars(n, nodes, p_set, shares["fuel_cell"], temperature) + + if shares["ice"] > 0: + add_ice_cars(n, nodes, p_set, shares["ice"], temperature) + def build_heat_demand(n): @@ -3565,19 +3606,19 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): -compression_per_1000km * n.links.loc[carrier_i, "length_original"] / 1e3 ) - +#%% 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", + sector_opts="730H-T-H-B-I-A-dist1", planning_horizons="2030", ) From bed2ef43d5b222bad6bb5c5c65361815b03746c3 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 1 Mar 2024 15:47:52 +0100 Subject: [PATCH 03/60] fix p_nom ICE --- scripts/prepare_sector_network.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 67b3fd7dc..ca763d1ed 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1627,24 +1627,22 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): ice_efficiency = options["transport_internal_combustion_efficiency"] - if not options["regional_oil_demand"]: - p_nom = ice_share * p_set.sum(axis=1).max() / ice_efficiency - else: - p_nom = ice_share * p_set.max() / ice_efficiency + p_nom = ice_share * p_set.max() / ice_efficiency + suffix = " land transport ICE" + p_nom.rename(lambda x: x + suffix, inplace=True) n.madd( "Link", - nodes + " land transport ICE", + nodes + suffix, bus0=spatial.oil.nodes, bus1=nodes + " land transport", bus2=["co2 atmosphere"], carrier="land transport oil", efficiency=ice_efficiency, efficiency2=costs.at["oil", "CO2 intensity"], - p_nom_extendable=True, - # p_nom=p_nom, - capital_cost=1e4, + p_nom_extendable=False, + p_nom=p_nom, ) def add_land_transport(n, costs): From 8efe43f35f685b58bc20d497da4d68c508b84de8 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 08:34:08 +0100 Subject: [PATCH 04/60] add temperature correction --- scripts/prepare_sector_network.py | 41 ++++++++++++++++++++++++++----- scripts/solve_network.py | 6 ++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ca763d1ed..db93ede8a 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1535,8 +1535,13 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, options["EV_upper_degree_factor"], ) + temp_eff = 1 / (1+dd_EV) + profile = p_set/p_set.max() - efficiency = costs.at['Battery electric (passenger cars)', 'efficiency'] + + car_efficiency = costs.at['Battery electric (passenger cars)', 'efficiency'] + + efficiency = car_efficiency * temp_eff n.madd( "Link", @@ -1602,9 +1607,21 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, def add_fuel_cell_cars(n, nodes, p_set, fuel_cell_share, temperature): - efficiency = options["transport_fuel_cell_efficiency"] + # correction factors for vehicle heating + cooling + dd_ICE = transport_degree_factor( + temperature, + options["transport_heating_deadband_lower"], + options["transport_heating_deadband_upper"], + options["ICE_lower_degree_factor"], + options["ICE_upper_degree_factor"], + ) + temp_eff = 1 / (1+dd_ICE) + car_efficiency = options["transport_fuel_cell_efficiency"] + efficiency = car_efficiency * temp_eff + + profile = p_set / p_set.max() - p_nom = fuel_cell_share * p_set / efficiency + p_nom = fuel_cell_share * p_set / car_efficiency n.madd( "Link", @@ -1624,10 +1641,22 @@ def add_fuel_cell_cars(n, nodes, p_set, fuel_cell_share, temperature): def add_ice_cars(n, nodes, p_set, ice_share, temperature): add_carrier_buses(n, "oil") + + # correction factors for vehicle heating + cooling + dd_ICE = transport_degree_factor( + temperature, + options["transport_heating_deadband_lower"], + options["transport_heating_deadband_upper"], + options["ICE_lower_degree_factor"], + options["ICE_upper_degree_factor"], + ) + temp_eff = 1 / (1+dd_ICE) - ice_efficiency = options["transport_internal_combustion_efficiency"] + car_efficiency = options["transport_internal_combustion_efficiency"] + + efficiency = car_efficiency * temp_eff - p_nom = ice_share * p_set.max() / ice_efficiency + p_nom = ice_share * p_set.max() / car_efficiency suffix = " land transport ICE" p_nom.rename(lambda x: x + suffix, inplace=True) @@ -1639,7 +1668,7 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): bus1=nodes + " land transport", bus2=["co2 atmosphere"], carrier="land transport oil", - efficiency=ice_efficiency, + efficiency=efficiency, efficiency2=costs.at["oil", "CO2 intensity"], p_nom_extendable=False, p_nom=p_nom, diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 7e53e6063..9ba38d0bd 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -921,19 +921,19 @@ def solve_network(n, config, solving, **kwargs): return n - +#%% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake snakemake = mock_snakemake( "solve_sector_network", - configfiles="../config/test/config.perfect.yaml", + # configfiles="../config/test/config.perfect.yaml", simpl="", opts="", clusters="37", ll="v1.0", - sector_opts="CO2L0-1H-T-H-B-I-A-dist1", + sector_opts="730H-T-H-B-I-A-dist1", planning_horizons="2030", ) configure_logging(snakemake) From 4ffa702d304c372afcb1f1875c8eba58063487e4 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 08:49:44 +0100 Subject: [PATCH 05/60] temperature correction in extra function --- scripts/prepare_sector_network.py | 77 ++++++++++++++++--------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index db93ede8a..33c06afc3 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1512,6 +1512,25 @@ def check_land_transport_shares(shares): "corresponding to increased or decreased demand assumptions." ) +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, nodes, avail_profile, dsm_profile, p_set, electric_share, number_cars, temperature): @@ -1526,23 +1545,17 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, unit="MWh_el", ) - # temperature correction for EVs - 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"], - ) + car_efficiency = costs.at['Battery electric (passenger cars)', 'efficiency'] - temp_eff = 1 / (1+dd_EV) + # 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"]) profile = p_set/p_set.max() - car_efficiency = costs.at['Battery electric (passenger cars)', 'efficiency'] - - efficiency = car_efficiency * temp_eff - n.madd( "Link", nodes, @@ -1553,7 +1566,7 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, efficiency=efficiency, p_min_pu=profile, p_max_pu=profile, - p_nom=electric_share*p_set.max()/efficiency, + p_nom=electric_share*p_set.max()/car_efficiency, p_nom_extendable=False, ) @@ -1607,21 +1620,18 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, def add_fuel_cell_cars(n, nodes, p_set, fuel_cell_share, temperature): - # correction factors for vehicle heating + cooling - dd_ICE = transport_degree_factor( - temperature, - options["transport_heating_deadband_lower"], - options["transport_heating_deadband_upper"], - options["ICE_lower_degree_factor"], - options["ICE_upper_degree_factor"], - ) - temp_eff = 1 / (1+dd_ICE) car_efficiency = options["transport_fuel_cell_efficiency"] - efficiency = car_efficiency * temp_eff + + # 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 = p_set / p_set.max() - p_nom = fuel_cell_share * p_set / car_efficiency + p_nom = fuel_cell_share * p_set.max() / car_efficiency n.madd( "Link", @@ -1642,19 +1652,14 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): add_carrier_buses(n, "oil") - # correction factors for vehicle heating + cooling - dd_ICE = transport_degree_factor( - temperature, - options["transport_heating_deadband_lower"], - options["transport_heating_deadband_upper"], - options["ICE_lower_degree_factor"], - options["ICE_upper_degree_factor"], - ) - temp_eff = 1 / (1+dd_ICE) - car_efficiency = options["transport_internal_combustion_efficiency"] - efficiency = car_efficiency * temp_eff + # 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"]) p_nom = ice_share * p_set.max() / car_efficiency suffix = " land transport ICE" From 04d3164d7f88a050acc363bccae52246d82ef950 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 09:41:22 +0100 Subject: [PATCH 06/60] adjust p_nom + profile --- scripts/prepare_sector_network.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 33c06afc3..6aba1bd59 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1553,20 +1553,23 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, options["transport_heating_deadband_upper"], options["EV_lower_degree_factor"], options["EV_upper_degree_factor"]) + suffix = " land transport EV" - profile = p_set/p_set.max() + p_nom = electric_share * p_set.div(efficiency).max() + + profile = p_set.div(efficiency)/p_set.div(efficiency).max() n.madd( "Link", nodes, - suffix=" land transport EV", + suffix=suffix, bus0=nodes + " EV battery", bus1=nodes + " land transport", carrier="land transport EV", efficiency=efficiency, p_min_pu=profile, p_max_pu=profile, - p_nom=electric_share*p_set.max()/car_efficiency, + p_nom=p_nom, p_nom_extendable=False, ) @@ -1629,14 +1632,16 @@ def add_fuel_cell_cars(n, nodes, p_set, fuel_cell_share, temperature): options["ICE_lower_degree_factor"], options["ICE_upper_degree_factor"]) + suffix = " land transport fuel cell" + + p_nom = fuel_cell_share * p_set.div(efficiency).max() - profile = p_set / p_set.max() - p_nom = fuel_cell_share * p_set.max() / car_efficiency + profile = p_set.div(efficiency)/p_set.div(efficiency).max() n.madd( "Link", nodes, - suffix=" land transport fuel cell", + suffix=suffix, bus0=spatial.h2.nodes, bus1=nodes + " land transport", carrier="land transport fuel cell", @@ -1660,15 +1665,15 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): options["transport_heating_deadband_upper"], options["ICE_lower_degree_factor"], options["ICE_upper_degree_factor"]) - - p_nom = ice_share * p_set.max() / car_efficiency suffix = " land transport ICE" - p_nom.rename(lambda x: x + suffix, inplace=True) - + p_nom = ice_share * p_set.div(efficiency).max() + + n.madd( "Link", - nodes + suffix, + nodes, + suffix=suffix, bus0=spatial.oil.nodes, bus1=nodes + " land transport", bus2=["co2 atmosphere"], From 4242a0841ce56c34e3c25ceddb7f7c01984e966c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 08:44:51 +0000 Subject: [PATCH 07/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/build_transport_demand.py | 20 ++- scripts/prepare_sector_network.py | 202 +++++++++++++++++------------- scripts/solve_network.py | 3 +- 3 files changed, 129 insertions(+), 96 deletions(-) diff --git a/scripts/build_transport_demand.py b/scripts/build_transport_demand.py index 7e54b5670..a13c8df98 100644 --- a/scripts/build_transport_demand.py +++ b/scripts/build_transport_demand.py @@ -21,7 +21,7 @@ def build_nodal_transport_data(fn, pop_layout): # get numbers of car and fuel efficieny per country transport_data = pd.read_csv(fn, index_col=0) - + # 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 @@ -39,12 +39,11 @@ def build_nodal_transport_data(fn, pop_layout): def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data): """ - returns transport demand per bus in unit kinetic energy. + Returns transport demand per bus in unit kinetic energy. """ # averaged weekly counts from the year 2010-2015 - traffic = pd.read_csv(traffic_fn, skiprows=2, - usecols=["count"]).squeeze("columns") - + 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, @@ -65,7 +64,6 @@ def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data): options["ICE_upper_degree_factor"], ) - # divide out the heating/cooling demand from ICE totals ice_correction = (transport_shape * (1 + dd_ICE)).sum() / transport_shape.sum() @@ -75,9 +73,8 @@ 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(nodal_transport_data["average fuel efficiency"] * ice_correction) + return (transport_shape.multiply(energy_totals_transport) * 1e6 * nyears).divide( + nodal_transport_data["average fuel efficiency"] * ice_correction ) @@ -141,7 +138,7 @@ 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[ @@ -154,7 +151,8 @@ def bev_dsm_profile(snapshots, nodes, options): weekly_profile=dsm_week, ) -#%% + +# %% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 6aba1bd59..e7bd105b0 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1505,15 +1505,22 @@ def add_storage_and_grids(n, costs): def check_land_transport_shares(shares): # Sums up the shares, ignoring None values - total_share = sum(filter(None, shares)) + 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." ) -def get_temp_efficency(car_efficiency, temperature, deadband_lw, deadband_up, - degree_factor_lw, degree_factor_up): + +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. @@ -1526,14 +1533,23 @@ def get_temp_efficency(car_efficiency, temperature, deadband_lw, deadband_up, degree_factor_lw, degree_factor_up, ) - - temp_eff = 1 / (1+dd) - + + temp_eff = 1 / (1 + dd) + return car_efficiency * temp_eff -def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, - number_cars, temperature): - + +def add_EVs( + n, + nodes, + avail_profile, + dsm_profile, + p_set, + electric_share, + number_cars, + temperature, +): + n.add("Carrier", "Li ion") n.madd( @@ -1544,35 +1560,37 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, carrier="Li ion", unit="MWh_el", ) - - car_efficiency = costs.at['Battery electric (passenger cars)', '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"]) + + car_efficiency = costs.at["Battery electric (passenger cars)", "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"], + ) suffix = " land transport EV" - + p_nom = electric_share * p_set.div(efficiency).max() - - profile = p_set.div(efficiency)/p_set.div(efficiency).max() - + + profile = p_set.div(efficiency) / p_set.div(efficiency).max() + n.madd( "Link", nodes, suffix=suffix, - bus0=nodes + " EV battery", + bus0=nodes + " EV battery", bus1=nodes + " land transport", carrier="land transport EV", - efficiency=efficiency, + efficiency=efficiency, p_min_pu=profile, p_max_pu=profile, p_nom=p_nom, p_nom_extendable=False, ) - p_nom = number_cars * options.get("bev_charge_rate", 0.011) * electric_share @@ -1600,7 +1618,7 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, p_max_pu=avail_profile[nodes], efficiency=options.get("bev_charge_efficiency", 0.9), ) - + if options["bev_dsm"]: e_nom = ( number_cars @@ -1608,7 +1626,7 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, * options["bev_availability"] * electric_share ) - + n.madd( "Store", nodes, @@ -1620,56 +1638,62 @@ def add_EVs(n, nodes, avail_profile, dsm_profile, p_set, electric_share, e_max_pu=1, e_min_pu=dsm_profile[nodes], ) - + + def add_fuel_cell_cars(n, nodes, 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"]) - + + # 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"], + ) + suffix = " land transport fuel cell" - + p_nom = fuel_cell_share * p_set.div(efficiency).max() - - profile = p_set.div(efficiency)/p_set.div(efficiency).max() - + + profile = p_set.div(efficiency) / p_set.div(efficiency).max() + n.madd( "Link", nodes, suffix=suffix, bus0=spatial.h2.nodes, bus1=nodes + " land transport", - carrier="land transport fuel cell", - efficiency=efficiency, + carrier="land transport fuel cell", + efficiency=efficiency, p_nom_extendable=False, p_nom=p_nom, p_min_pu=profile, p_max_pu=profile, ) - - + + def add_ice_cars(n, nodes, p_set, ice_share, temperature): - + add_carrier_buses(n, "oil") - + car_efficiency = options["transport_internal_combustion_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"]) + + # 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"], + ) suffix = " land transport ICE" - - p_nom = ice_share * p_set.div(efficiency).max() - - + + p_nom = ice_share * p_set.div(efficiency).max() + n.madd( "Link", nodes, @@ -1683,12 +1707,13 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): p_nom_extendable=False, p_nom=p_nom, ) - + + def add_land_transport(n, costs): # TODO options? logger.info("Add land transport") - + # read in transport demand in units kinetic energy transport = pd.read_csv( snakemake.input.transport_demand, index_col=0, parse_dates=True @@ -1702,54 +1727,62 @@ def add_land_transport(n, costs): 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) + shares[engine] = get(options[f"land_transport_{engine}_share"], investment_year) logger.info(f"{engine} share: {shares[engine]*100}%") - - check_land_transport_shares(shares) - + + check_land_transport_shares(shares) + nodes = spatial.nodes - + # Add load for transport demand n.add("Carrier", "land transport demand") - n.madd("Bus", - nodes, - location=nodes, - suffix=" land transport", - carrier="land transport demand", - unit="MWh_kinetic") - + n.madd( + "Bus", + nodes, + location=nodes, + suffix=" land transport", + carrier="land transport demand", + unit="MWh_kinetic", + ) + p_set = transport[nodes] - - # add demand + + # add demand n.madd( "Load", nodes, suffix=" land transport", bus=nodes + " land transport", carrier="land transport demand", - p_set=p_set, + p_set=p_set, ) - + # 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, nodes, avail_profile, dsm_profile, p_set, shares["electric"], - number_cars, temperature) - + add_EVs( + n, + nodes, + avail_profile, + dsm_profile, + p_set, + shares["electric"], + number_cars, + temperature, + ) + if shares["fuel_cell"] > 0: add_fuel_cell_cars(n, nodes, p_set, shares["fuel_cell"], temperature) if shares["ice"] > 0: add_ice_cars(n, nodes, p_set, shares["ice"], temperature) - def build_heat_demand(n): @@ -3643,7 +3676,8 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): -compression_per_1000km * n.links.loc[carrier_i, "length_original"] / 1e3 ) -#%% + +# %% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 9ba38d0bd..d1ca16d79 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -921,7 +921,8 @@ def solve_network(n, config, solving, **kwargs): return n -#%% + +# %% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake From f2ba96e1483e4c9dc7f756164fbcbef0d1d414c8 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 11:24:43 +0100 Subject: [PATCH 08/60] add color for land transport demand --- config/config.default.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index ad4144d56..aa6628396 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -981,6 +981,7 @@ plotting: BEV charger: '#baf238' V2G: '#e5ffa8' land transport EV: '#baf238' + land transport demand: '#38baf2' Li ion: '#baf238' # hot water storage water tanks: '#e69487' From d4bdf489c9ef8e729012f6ed2a70135ec7f2df8a Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 12:08:45 +0100 Subject: [PATCH 09/60] add cyclic shift for EVs --- scripts/prepare_sector_network.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e7bd105b0..b7bb4ebea 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1573,11 +1573,18 @@ def add_EVs( options["EV_upper_degree_factor"], ) suffix = " land transport EV" - + + 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 + p_nom = electric_share * p_set.div(efficiency).max() - profile = p_set.div(efficiency) / p_set.div(efficiency).max() - + profile = p_shifted.div(efficiency) / p_shifted.div(efficiency).max() + + n.madd( "Link", nodes, From c14f782f51790efee01bb66e6e175cb360f4cb4f Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 12:11:19 +0100 Subject: [PATCH 10/60] add lifetime to links --- scripts/prepare_sector_network.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b7bb4ebea..38c1e737c 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1597,6 +1597,7 @@ def add_EVs( p_max_pu=profile, p_nom=p_nom, p_nom_extendable=False, + lifetime=1, ) p_nom = number_cars * options.get("bev_charge_rate", 0.011) * electric_share @@ -1679,6 +1680,7 @@ def add_fuel_cell_cars(n, nodes, p_set, fuel_cell_share, temperature): p_nom=p_nom, p_min_pu=profile, p_max_pu=profile, + lifetime=1, ) @@ -1713,6 +1715,7 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): efficiency2=costs.at["oil", "CO2 intensity"], p_nom_extendable=False, p_nom=p_nom, + lifetime=1, ) From fa935be5cb2561d0c63673619a6c89b0c187df9f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:11:43 +0000 Subject: [PATCH 11/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 38c1e737c..c32a5d148 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1573,18 +1573,17 @@ def add_EVs( options["EV_upper_degree_factor"], ) suffix = " land transport EV" - + 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 - + p_nom = electric_share * p_set.div(efficiency).max() profile = p_shifted.div(efficiency) / p_shifted.div(efficiency).max() - - + n.madd( "Link", nodes, From 66257e1e843039852fd586e99c7c3de3a08b645c Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 15:41:57 +0100 Subject: [PATCH 12/60] fix bug with cyclic shift --- scripts/prepare_sector_network.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c32a5d148..c4554b872 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1573,17 +1573,18 @@ def add_EVs( options["EV_upper_degree_factor"], ) suffix = " land transport EV" - + 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 - + p_nom = electric_share * p_set.div(efficiency).max() - profile = p_shifted.div(efficiency) / p_shifted.div(efficiency).max() - + profile = p_set.div(efficiency) / p_set.div(efficiency).max() + + n.madd( "Link", nodes, From fb08d16be95094c0c16a53edbaee495cee53e60e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 14:49:17 +0000 Subject: [PATCH 13/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index c4554b872..0affa679b 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1573,18 +1573,17 @@ def add_EVs( options["EV_upper_degree_factor"], ) suffix = " land transport EV" - + 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 - + p_nom = electric_share * p_set.div(efficiency).max() profile = p_set.div(efficiency) / p_set.div(efficiency).max() - - + n.madd( "Link", nodes, From 6712e48aa0a6cb88b79d8afdec7f6048c1ee0fe1 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 15:50:50 +0100 Subject: [PATCH 14/60] fix spelling mistake --- scripts/build_transport_demand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_transport_demand.py b/scripts/build_transport_demand.py index a13c8df98..26f90ca7f 100644 --- a/scripts/build_transport_demand.py +++ b/scripts/build_transport_demand.py @@ -19,7 +19,7 @@ def build_nodal_transport_data(fn, pop_layout): - # get numbers of car and fuel efficieny per country + # get numbers of car and fuel efficiency per country transport_data = pd.read_csv(fn, index_col=0) # break number of cars down to nodal level based on population density From fe51fd7022aa973c4a03785f5dce7f87024aa75e Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 17:00:57 +0100 Subject: [PATCH 15/60] adjust for temporal aggregation --- scripts/prepare_sector_network.py | 34 +++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0affa679b..a82778d4c 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1701,7 +1701,9 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): suffix = " land transport ICE" p_nom = ice_share * p_set.div(efficiency).max() - + + profile = p_set.div(efficiency) / p_set.div(efficiency).max() + n.madd( "Link", nodes, @@ -1714,6 +1716,8 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): efficiency2=costs.at["oil", "CO2 intensity"], p_nom_extendable=False, p_nom=p_nom, + p_min_pu=profile, + p_max_pu=profile, lifetime=1, ) @@ -3686,6 +3690,30 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): ) + +def adjust_transport_temporal_agg(n): + + engine_types = {"fuel_cell":'land transport fuel cell', + "electric": 'land transport EV', + "ice": 'land transport oil'} + + p_set = n.loads_t.p_set.loc[:, n.loads.carrier=="land transport demand"] + + for engine, carrier in engine_types.items(): + share = get(options[f"land_transport_{engine}_share"], investment_year) + + if share==0: continue + links_i = n.links[n.links.carrier==carrier].index + efficiency = n.links_t.efficiency.loc[:, links_i] + p_set.columns = efficiency.columns + p_nom = share * p_set.div(efficiency).max() + profile = p_set.div(efficiency) / p_set.div(efficiency).max() + + n.links.loc[links_i, "p_nom"] = p_nom + n.links_t.p_max_pu[links_i] = profile + n.links_t.p_min_pu[links_i] = profile + + # %% if __name__ == "__main__": if "snakemake" not in globals(): @@ -3699,7 +3727,7 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): clusters="37", ll="v1.0", sector_opts="730H-T-H-B-I-A-dist1", - planning_horizons="2030", + planning_horizons="2050", ) configure_logging(snakemake) @@ -3784,6 +3812,8 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): solver_name = snakemake.config["solving"]["solver"]["name"] resolution = snakemake.params.time_resolution n = set_temporal_aggregation(n, resolution, solver_name) + + adjust_transport_temporal_agg(n) co2_budget = snakemake.params.co2_budget if isinstance(co2_budget, str) and co2_budget.startswith("cb"): From ff0093c8fed0b3b778c8a175c7a9d61cb80c75c6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:01:29 +0000 Subject: [PATCH 16/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a82778d4c..a56352357 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1701,9 +1701,9 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): suffix = " land transport ICE" p_nom = ice_share * p_set.div(efficiency).max() - + profile = p_set.div(efficiency) / p_set.div(efficiency).max() - + n.madd( "Link", nodes, @@ -3690,30 +3690,32 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}): ) - def adjust_transport_temporal_agg(n): - - engine_types = {"fuel_cell":'land transport fuel cell', - "electric": 'land transport EV', - "ice": 'land transport oil'} - - p_set = n.loads_t.p_set.loc[:, n.loads.carrier=="land transport demand"] - + + engine_types = { + "fuel_cell": "land transport fuel cell", + "electric": "land transport EV", + "ice": "land transport oil", + } + + p_set = n.loads_t.p_set.loc[:, n.loads.carrier == "land transport demand"] + for engine, carrier in engine_types.items(): share = get(options[f"land_transport_{engine}_share"], investment_year) - - if share==0: continue - links_i = n.links[n.links.carrier==carrier].index + + if share == 0: + continue + links_i = n.links[n.links.carrier == carrier].index efficiency = n.links_t.efficiency.loc[:, links_i] p_set.columns = efficiency.columns p_nom = share * p_set.div(efficiency).max() profile = p_set.div(efficiency) / p_set.div(efficiency).max() - + n.links.loc[links_i, "p_nom"] = p_nom n.links_t.p_max_pu[links_i] = profile n.links_t.p_min_pu[links_i] = profile - - + + # %% if __name__ == "__main__": if "snakemake" not in globals(): @@ -3812,7 +3814,7 @@ def adjust_transport_temporal_agg(n): solver_name = snakemake.config["solving"]["solver"]["name"] resolution = snakemake.params.time_resolution n = set_temporal_aggregation(n, resolution, solver_name) - + adjust_transport_temporal_agg(n) co2_budget = snakemake.params.co2_budget From 1b6e5dc0c2b4c04c51f158896067ef14d8d76a8e Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 17:22:52 +0100 Subject: [PATCH 17/60] adjust for perfect foresight --- scripts/prepare_perfect_foresight.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_perfect_foresight.py b/scripts/prepare_perfect_foresight.py index f7e8495ec..fbc17a524 100644 --- a/scripts/prepare_perfect_foresight.py +++ b/scripts/prepare_perfect_foresight.py @@ -21,6 +21,7 @@ from pypsa.descriptors import expand_series from pypsa.io import import_components_from_dataframe from six import iterkeys +from prepare_sector_network import adjust_transport_temporal_agg logger = logging.getLogger(__name__) @@ -520,7 +521,8 @@ def apply_time_segmentation_perfect( segments = snakemake.params.time_resolution if isinstance(segments, (int, float)): n = apply_time_segmentation_perfect(n, segments, solver_name=solver_name) - + adjust_transport_temporal_agg(n) + # adjust global constraints lv limit if the same for all years n = adjust_lvlimit(n) # adjust global constraints CO2 limit From 3549e6843d1362374aa45997502735b8b6df00d6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:24:49 +0000 Subject: [PATCH 18/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_perfect_foresight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_perfect_foresight.py b/scripts/prepare_perfect_foresight.py index fbc17a524..8df05ceec 100644 --- a/scripts/prepare_perfect_foresight.py +++ b/scripts/prepare_perfect_foresight.py @@ -18,10 +18,10 @@ update_config_from_wildcards, ) from add_existing_baseyear import add_build_year_to_new_assets +from prepare_sector_network import adjust_transport_temporal_agg from pypsa.descriptors import expand_series from pypsa.io import import_components_from_dataframe from six import iterkeys -from prepare_sector_network import adjust_transport_temporal_agg logger = logging.getLogger(__name__) @@ -522,7 +522,7 @@ def apply_time_segmentation_perfect( if isinstance(segments, (int, float)): n = apply_time_segmentation_perfect(n, segments, solver_name=solver_name) adjust_transport_temporal_agg(n) - + # adjust global constraints lv limit if the same for all years n = adjust_lvlimit(n) # adjust global constraints CO2 limit From 9c93917c31138c7fbc8b601761d10b76727e4284 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 4 Mar 2024 17:29:25 +0100 Subject: [PATCH 19/60] add release notes --- doc/release_notes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 8167f6dd2..9d6e3c269 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -9,6 +9,11 @@ Release Notes Upcoming Release ================ +* fix bug in land transport for temperature correction of ICEs and fuel cell cars + +* restructure land transport, demand is now attached to one single node, the +different car types (ICE, EV, fuel cell) are modelled as links + * Upgrade default techno-economic assumptions to ``technology-data`` v0.8.1. * Linearly interpolate missing investment periods in year-dependent From a805f5cfb3f6873144b6f5cfc20250535d102b29 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:38:36 +0000 Subject: [PATCH 20/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/release_notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 9d6e3c269..c3da4147f 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -11,7 +11,7 @@ Upcoming Release ================ * fix bug in land transport for temperature correction of ICEs and fuel cell cars -* restructure land transport, demand is now attached to one single node, the +* restructure land transport, demand is now attached to one single node, the different car types (ICE, EV, fuel cell) are modelled as links * Upgrade default techno-economic assumptions to ``technology-data`` v0.8.1. From 74504a0ff3bba7d8d69e7e6f7c50d8f4a0b9c5d0 Mon Sep 17 00:00:00 2001 From: lisazeyen <35347358+lisazeyen@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:04:30 +0100 Subject: [PATCH 21/60] add lifetime to BEV --- scripts/prepare_sector_network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index a56352357..bb4b71a88 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1610,6 +1610,7 @@ def add_EVs( p_nom=p_nom, carrier="BEV charger", p_max_pu=avail_profile[nodes], + lifetime=1, efficiency=options.get("bev_charge_efficiency", 0.9), ) From 08ea54a237daad001951e0f5560a71fc54b3602a Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 6 Mar 2024 14:51:00 +0100 Subject: [PATCH 22/60] first step adding endogenous transport --- scripts/prepare_sector_network.py | 51 +++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 71772e81a..7cf57b2ce 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1618,6 +1618,7 @@ def add_EVs( p_nom=p_nom, carrier="V2G", p_max_pu=avail_profile[nodes], + lifetime=1, efficiency=options.get("bev_charge_efficiency", 0.9), ) @@ -1717,6 +1718,40 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): ) +def adjust_endogenous_transport(n): + carrier = ['land transport EV', 'land transport oil', + "land transport fuel cell", "BEV charger", "V2G"] + + links_i = n.links[n.links.carrier.isin(carrier)].index + n.links.loc[links_i, "p_nom_extendable"] = True + + # costs todo + # assume here for all of Europe + # average driving distance 15 000 km /year and car + # EV -------------------------- + # average consumption EV 18 kWh/100 km + # annual demand per car 18 kWh/100km * 150 100km/a = 2.7 MWh/a + cost_EV = costs.loc["Battery electric (passenger cars)", "fixed"]/2.7 + # FCE ---------------------------- + # average consumption 0.7-1 kg_H2/100km assume 0.85-> 0.85*33.33 kWh_H2/100 km + # annual demand per car 33.33 kWh/100km * 150 100km/a = 4.25 MWh/a + cost_FCE = costs.loc["Hydrogen fuel cell (passenger cars)", "fixed"]/4.25 + # ICE --------------------------------------------------------- + # average consumption 6.5liter/100km + # energy content gasoline 9.7 kWh/liter + # annual demand per car 6.5 * 9.7 kWh/100km * 150 100km/a = 9.46 + cost_ICE = costs.at["Liquid fuels ICE (passenger cars)", "fixed"]/9.46 + # cost in unit input depending on car type + costs_car_type = {'land transport EV': cost_EV, + "land transport fuel cell": cost_FCE, + 'land transport oil': cost_ICE, + } + + for car_type, cost in costs_car_type.items(): + car_i = n.links[n.links.carrier==car_type].index + n.links.loc[car_i, "capital_cost"] = cost + n.links.loc[car_i, "lifetime"] = 15 + def add_land_transport(n, costs): # TODO options? @@ -1744,7 +1779,8 @@ def add_land_transport(n, costs): logger.info(f"{engine} share: {shares[engine]*100}%") check_land_transport_shares(shares) - + + endogenous = options["endogenous_transport"] nodes = spatial.nodes # Add load for transport demand @@ -1774,7 +1810,7 @@ def add_land_transport(n, costs): # temperature for correction factor for heating/cooling temperature = xr.open_dataarray(snakemake.input.temp_air_total).to_pandas() - if shares["electric"] > 0: + if shares["electric"] > 0 or endogenous: add_EVs( n, nodes, @@ -1786,12 +1822,15 @@ def add_land_transport(n, costs): temperature, ) - if shares["fuel_cell"] > 0: + if shares["fuel_cell"] > 0 or endogenous: add_fuel_cell_cars(n, nodes, p_set, shares["fuel_cell"], temperature) - if shares["ice"] > 0: + if shares["ice"] > 0 or endogenous: add_ice_cars(n, nodes, p_set, shares["ice"], temperature) - + + + if endogenous: + adjust_endogenous_transport(n) def build_heat_demand(n): heat_demand_shape = ( @@ -3724,7 +3763,7 @@ def adjust_transport_temporal_agg(n): clusters="37", ll="v1.0", sector_opts="730H-T-H-B-I-A-dist1", - planning_horizons="2050", + planning_horizons="2030", ) configure_logging(snakemake) From 030e7d7c40b59344c9851b3a4ac1b5896833c2b7 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 6 Mar 2024 15:39:07 +0100 Subject: [PATCH 23/60] first step add existing --- scripts/add_existing_baseyear.py | 49 ++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 0bbe19f0e..4ce5a3651 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -21,7 +21,7 @@ update_config_from_wildcards, ) from add_electricity import sanitize_carriers -from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs +from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs, get logger = logging.getLogger(__name__) cc = coco.CountryConverter() @@ -55,6 +55,47 @@ def add_build_year_to_new_assets(n, baseyear): c.pnl[attr] = c.pnl[attr].rename(columns=rename) +def add_existing_land_transport(baseyear, options): + # today ICE capacity assuming all internal combustion + share = get(options["land_transport_ice_share"], baseyear) + ice_i = n.links[n.links.carrier=="land transport oil"].index + p_nom = n.links[ice_i, "p_nom"] / share + efficiency = n.links_t.efficiency[ice_i] + p_max_pu = n.links_t.p_max_pu[ice_i] + + # TODO car ages + car_ages = { + 2020: 0.15, # 0-4 years (2020-2025) + 2015: 0.20, # 5-9 years (2010-2015) + 2010: 0.20, # 10-14 years (2005-2010) + 2005: 0.15, # 15-19 years (2000-2005) + 2000: 0.30 # 20+ years + } + for build_year, share in car_ages.items(): + df = n.links.loc[ice_i] + df = df[df.lifetime + build_year > baseyear] + if df.empty: continue + df["build_year"] = build_year + df["p_nom"] = share * p_nom + df["p_nom_extendable"] = False + df.rename(index=lambda x: x + f"-{build_year}", inplace=True) + + n.madd( + "Link", + df.index, + bus0=df.bus0, + bus1=df.bus1, + bus2=df.bus2, + carrier=df.carrier, + efficiency=efficiency, + efficiency2=df.efficiency2, + p_nom_extendable=False, + p_nom=df.p_nom, + p_min_pu=p_max_pu, + p_max_pu=p_max_pu, + lifetime=df.lifetime, + ) + def add_existing_renewables(df_agg): """ Append existing renewables to the df_agg pd.DataFrame with the conventional @@ -558,7 +599,7 @@ def add_heating_capacities_installed_before_baseyear( clusters="37", ll="v1.0", opts="", - sector_opts="8760-T-H-B-I-A-dist1", + sector_opts="730H-T-H-B-I-A-dist1", planning_horizons=2020, ) @@ -618,7 +659,9 @@ def add_heating_capacities_installed_before_baseyear( if options.get("cluster_heat_buses", False): cluster_heat_buses(n) - + + if options["endogenous_transport"]: + add_existing_land_transport(baseyear, options) n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) sanitize_carriers(n, snakemake.config) From fe663a545118328d9d877b2650c4b74e6eba6a3b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:45:46 +0000 Subject: [PATCH 24/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 33 +++++++++++++++----------- scripts/prepare_sector_network.py | 39 ++++++++++++++++++------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 4ce5a3651..689d737a4 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -21,7 +21,12 @@ update_config_from_wildcards, ) from add_electricity import sanitize_carriers -from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs, get +from prepare_sector_network import ( + cluster_heat_buses, + define_spatial, + get, + prepare_costs, +) logger = logging.getLogger(__name__) cc = coco.CountryConverter() @@ -58,28 +63,29 @@ def add_build_year_to_new_assets(n, baseyear): def add_existing_land_transport(baseyear, options): # today ICE capacity assuming all internal combustion share = get(options["land_transport_ice_share"], baseyear) - ice_i = n.links[n.links.carrier=="land transport oil"].index + ice_i = n.links[n.links.carrier == "land transport oil"].index p_nom = n.links[ice_i, "p_nom"] / share efficiency = n.links_t.efficiency[ice_i] p_max_pu = n.links_t.p_max_pu[ice_i] - + # TODO car ages car_ages = { - 2020: 0.15, # 0-4 years (2020-2025) - 2015: 0.20, # 5-9 years (2010-2015) - 2010: 0.20, # 10-14 years (2005-2010) - 2005: 0.15, # 15-19 years (2000-2005) - 2000: 0.30 # 20+ years - } + 2020: 0.15, # 0-4 years (2020-2025) + 2015: 0.20, # 5-9 years (2010-2015) + 2010: 0.20, # 10-14 years (2005-2010) + 2005: 0.15, # 15-19 years (2000-2005) + 2000: 0.30, # 20+ years + } for build_year, share in car_ages.items(): df = n.links.loc[ice_i] df = df[df.lifetime + build_year > baseyear] - if df.empty: continue + if df.empty: + continue df["build_year"] = build_year df["p_nom"] = share * p_nom df["p_nom_extendable"] = False df.rename(index=lambda x: x + f"-{build_year}", inplace=True) - + n.madd( "Link", df.index, @@ -95,7 +101,8 @@ def add_existing_land_transport(baseyear, options): p_max_pu=p_max_pu, lifetime=df.lifetime, ) - + + def add_existing_renewables(df_agg): """ Append existing renewables to the df_agg pd.DataFrame with the conventional @@ -659,7 +666,7 @@ def add_heating_capacities_installed_before_baseyear( if options.get("cluster_heat_buses", False): cluster_heat_buses(n) - + if options["endogenous_transport"]: add_existing_land_transport(baseyear, options) n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 7cf57b2ce..819f849b1 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1719,39 +1719,46 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): def adjust_endogenous_transport(n): - carrier = ['land transport EV', 'land transport oil', - "land transport fuel cell", "BEV charger", "V2G"] - + carrier = [ + "land transport EV", + "land transport oil", + "land transport fuel cell", + "BEV charger", + "V2G", + ] + links_i = n.links[n.links.carrier.isin(carrier)].index n.links.loc[links_i, "p_nom_extendable"] = True - + # costs todo # assume here for all of Europe # average driving distance 15 000 km /year and car # EV -------------------------- # average consumption EV 18 kWh/100 km # annual demand per car 18 kWh/100km * 150 100km/a = 2.7 MWh/a - cost_EV = costs.loc["Battery electric (passenger cars)", "fixed"]/2.7 + cost_EV = costs.loc["Battery electric (passenger cars)", "fixed"] / 2.7 # FCE ---------------------------- # average consumption 0.7-1 kg_H2/100km assume 0.85-> 0.85*33.33 kWh_H2/100 km # annual demand per car 33.33 kWh/100km * 150 100km/a = 4.25 MWh/a - cost_FCE = costs.loc["Hydrogen fuel cell (passenger cars)", "fixed"]/4.25 + cost_FCE = costs.loc["Hydrogen fuel cell (passenger cars)", "fixed"] / 4.25 # ICE --------------------------------------------------------- # average consumption 6.5liter/100km # energy content gasoline 9.7 kWh/liter # annual demand per car 6.5 * 9.7 kWh/100km * 150 100km/a = 9.46 - cost_ICE = costs.at["Liquid fuels ICE (passenger cars)", "fixed"]/9.46 + cost_ICE = costs.at["Liquid fuels ICE (passenger cars)", "fixed"] / 9.46 # cost in unit input depending on car type - costs_car_type = {'land transport EV': cost_EV, - "land transport fuel cell": cost_FCE, - 'land transport oil': cost_ICE, - } - + costs_car_type = { + "land transport EV": cost_EV, + "land transport fuel cell": cost_FCE, + "land transport oil": cost_ICE, + } + for car_type, cost in costs_car_type.items(): - car_i = n.links[n.links.carrier==car_type].index + car_i = n.links[n.links.carrier == car_type].index n.links.loc[car_i, "capital_cost"] = cost n.links.loc[car_i, "lifetime"] = 15 + def add_land_transport(n, costs): # TODO options? @@ -1779,7 +1786,7 @@ def add_land_transport(n, costs): logger.info(f"{engine} share: {shares[engine]*100}%") check_land_transport_shares(shares) - + endogenous = options["endogenous_transport"] nodes = spatial.nodes @@ -1827,11 +1834,11 @@ def add_land_transport(n, costs): if shares["ice"] > 0 or endogenous: add_ice_cars(n, nodes, p_set, shares["ice"], temperature) - - + if endogenous: adjust_endogenous_transport(n) + def build_heat_demand(n): heat_demand_shape = ( xr.open_dataset(snakemake.input.hourly_heat_demand_total) From e9a8899caa3c8c13bb82a52744a011e6b6182975 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 6 Mar 2024 16:20:31 +0100 Subject: [PATCH 25/60] add switch to config --- config/config.default.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.default.yaml b/config/config.default.yaml index 23b13ddf8..1b773f5b1 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -382,6 +382,7 @@ sector: 2050: 1.0 district_heating_loss: 0.15 cluster_heat_buses: true + endogenous_transport: true bev_dsm_restriction_value: 0.75 bev_dsm_restriction_time: 7 transport_heating_deadband_upper: 20. From d2f10f7ca7fca6a081bee4bd46e7c664afac4b5b Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 6 Mar 2024 16:21:21 +0100 Subject: [PATCH 26/60] fix bug add loc --- scripts/add_existing_baseyear.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 689d737a4..f69235e58 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -64,7 +64,7 @@ def add_existing_land_transport(baseyear, options): # today ICE capacity assuming all internal combustion share = get(options["land_transport_ice_share"], baseyear) ice_i = n.links[n.links.carrier == "land transport oil"].index - p_nom = n.links[ice_i, "p_nom"] / share + p_nom = n.links.loc[ice_i, "p_nom"] / share efficiency = n.links_t.efficiency[ice_i] p_max_pu = n.links_t.p_max_pu[ice_i] @@ -601,13 +601,13 @@ def add_heating_capacities_installed_before_baseyear( snakemake = mock_snakemake( "add_existing_baseyear", - # configfiles="config/test/config.myopic.yaml", + configfiles="config/test/config.myopic.yaml", simpl="", - clusters="37", - ll="v1.0", + clusters="5", + ll="v1.5", opts="", - sector_opts="730H-T-H-B-I-A-dist1", - planning_horizons=2020, + sector_opts="24H-T-H-B-I-A-dist1", + planning_horizons=2030, ) configure_logging(snakemake) From 573cad190fad550791c6ead0406196334033dc37 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 7 Mar 2024 10:11:51 +0100 Subject: [PATCH 27/60] correct naming --- scripts/add_existing_baseyear.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index f69235e58..9a0fc22cd 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -84,7 +84,8 @@ def add_existing_land_transport(baseyear, options): df["build_year"] = build_year df["p_nom"] = share * p_nom df["p_nom_extendable"] = False - df.rename(index=lambda x: x + f"-{build_year}", inplace=True) + df.rename(index=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), + inplace=True) n.madd( "Link", @@ -101,7 +102,8 @@ def add_existing_land_transport(baseyear, options): p_max_pu=p_max_pu, lifetime=df.lifetime, ) - + + n.links.loc[ice_i, "p_nom"] = 0 def add_existing_renewables(df_agg): """ From 709fd9898f63a5d61724f3cd166db3b60cdfc216 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 7 Mar 2024 11:15:42 +0100 Subject: [PATCH 28/60] add dummy generator for glpk --- scripts/prepare_sector_network.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 819f849b1..8335437ee 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1752,6 +1752,19 @@ def adjust_endogenous_transport(n): "land transport fuel cell": cost_FCE, "land transport oil": cost_ICE, } + + # add dummy generator only needed for solving with glpk with higher solver tolerance + n.add("Carrier", "dummy transport", color="#dd2e23", nice_name="Dummy transport") + buses_i = n.buses[n.buses.carrier=="land transport demand"].index + n.madd( + "Generator", + buses_i, + " load", + bus=buses_i, + carrier="load", + marginal_cost=1e5, + p_nom=1e9, + ) for car_type, cost in costs_car_type.items(): car_i = n.links[n.links.carrier == car_type].index From 0e7a7533f190c53008d4ce52826a7363af97588e Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 7 Mar 2024 11:16:29 +0100 Subject: [PATCH 29/60] change temporal resolution of perfect test run --- config/test/config.perfect.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/test/config.perfect.yaml b/config/test/config.perfect.yaml index 5d77c9c51..118916aff 100644 --- a/config/test/config.perfect.yaml +++ b/config/test/config.perfect.yaml @@ -18,7 +18,7 @@ scenario: clusters: - 5 sector_opts: - - 8760h-T-H-B-I-A-dist1 + - 24h-T-H-B-I-A-dist1 planning_horizons: - 2030 - 2040 From 014666606f724a4e3adc38dab53556d755899afb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:16:54 +0000 Subject: [PATCH 30/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 8 +++++--- scripts/prepare_sector_network.py | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 9a0fc22cd..1f74ba363 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -84,8 +84,9 @@ def add_existing_land_transport(baseyear, options): df["build_year"] = build_year df["p_nom"] = share * p_nom df["p_nom_extendable"] = False - df.rename(index=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), - inplace=True) + df.rename( + index=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), inplace=True + ) n.madd( "Link", @@ -102,9 +103,10 @@ def add_existing_land_transport(baseyear, options): p_max_pu=p_max_pu, lifetime=df.lifetime, ) - + n.links.loc[ice_i, "p_nom"] = 0 + def add_existing_renewables(df_agg): """ Append existing renewables to the df_agg pd.DataFrame with the conventional diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 8335437ee..d7af90c05 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1752,18 +1752,18 @@ def adjust_endogenous_transport(n): "land transport fuel cell": cost_FCE, "land transport oil": cost_ICE, } - + # add dummy generator only needed for solving with glpk with higher solver tolerance n.add("Carrier", "dummy transport", color="#dd2e23", nice_name="Dummy transport") - buses_i = n.buses[n.buses.carrier=="land transport demand"].index + buses_i = n.buses[n.buses.carrier == "land transport demand"].index n.madd( "Generator", buses_i, " load", bus=buses_i, carrier="load", - marginal_cost=1e5, - p_nom=1e9, + marginal_cost=1e5, + p_nom=1e9, ) for car_type, cost in costs_car_type.items(): From 1ca0f32cd8ef2d8886b2b7c5c7e3ecdf74ac2f04 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 11 Mar 2024 15:08:00 +0100 Subject: [PATCH 31/60] add lifetime and make store extendable --- scripts/prepare_sector_network.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d7af90c05..eed780a14 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1640,6 +1640,7 @@ def add_EVs( e_nom=e_nom, e_max_pu=1, e_min_pu=dsm_profile[nodes], + lifetime=1, ) @@ -1729,7 +1730,10 @@ def adjust_endogenous_transport(n): links_i = n.links[n.links.carrier.isin(carrier)].index n.links.loc[links_i, "p_nom_extendable"] = True - + + store_i = n.stores[n.stores.carrier=="battery storage"].index + n.stores.loc[store_i, "e_nom_extendable"] = True + # costs todo # assume here for all of Europe # average driving distance 15 000 km /year and car From 3bb43e4469efa7ef048d9a2c31566ab04c7346fc Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 14 Mar 2024 11:37:46 +0100 Subject: [PATCH 32/60] rename efficiencies + p_max_pu --- scripts/add_existing_baseyear.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 1f74ba363..0c183c7c8 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -76,6 +76,7 @@ def add_existing_land_transport(baseyear, options): 2005: 0.15, # 15-19 years (2000-2005) 2000: 0.30, # 20+ years } + for build_year, share in car_ages.items(): df = n.links.loc[ice_i] df = df[df.lifetime + build_year > baseyear] @@ -87,7 +88,10 @@ def add_existing_land_transport(baseyear, options): df.rename( index=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), inplace=True ) - + p_max_pu.rename(columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), + inplace=True) + efficiency.rename(columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), + inplace=True) n.madd( "Link", df.index, @@ -103,9 +107,11 @@ def add_existing_land_transport(baseyear, options): p_max_pu=p_max_pu, lifetime=df.lifetime, ) - + + n.links.loc[ice_i, "p_nom"] = 0 + def add_existing_renewables(df_agg): """ @@ -610,7 +616,7 @@ def add_heating_capacities_installed_before_baseyear( clusters="5", ll="v1.5", opts="", - sector_opts="24H-T-H-B-I-A-dist1", + sector_opts="24h-T-H-B-I-A-dist1", planning_horizons=2030, ) @@ -673,6 +679,7 @@ def add_heating_capacities_installed_before_baseyear( if options["endogenous_transport"]: add_existing_land_transport(baseyear, options) + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) sanitize_carriers(n, snakemake.config) From 2fabf96aff5c1d62bb7f971fbffb379807815106 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 14 Mar 2024 11:38:31 +0100 Subject: [PATCH 33/60] only add build_year for later investment periods --- scripts/prepare_perfect_foresight.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_perfect_foresight.py b/scripts/prepare_perfect_foresight.py index 8df05ceec..28bf85d3f 100644 --- a/scripts/prepare_perfect_foresight.py +++ b/scripts/prepare_perfect_foresight.py @@ -166,7 +166,8 @@ def concat_networks(years): year = years[i] network = pypsa.Network(network_path) adjust_electricity_grid(network, year, years) - add_build_year_to_new_assets(network, year) + if not i==0: + add_build_year_to_new_assets(network, year) # static ---------------------------------- for component in network.iterate_components( From 34dd83cdf8775837d554207266f9c0d0934d46f0 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 14 Mar 2024 12:07:37 +0100 Subject: [PATCH 34/60] some fixes --- scripts/prepare_sector_network.py | 35 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index eed780a14..1af50c63f 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1720,6 +1720,8 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): def adjust_endogenous_transport(n): + + logger.info("Assume endogenous land transport") carrier = [ "land transport EV", "land transport oil", @@ -1730,8 +1732,10 @@ def adjust_endogenous_transport(n): links_i = n.links[n.links.carrier.isin(carrier)].index n.links.loc[links_i, "p_nom_extendable"] = True + n.links.loc[links_i, "lifetime"] = 15 - store_i = n.stores[n.stores.carrier=="battery storage"].index + store_carrier = ["battery storage", "Li ion"] + store_i = n.stores[n.stores.carrier.isin(store_carrier)].index n.stores.loc[store_i, "e_nom_extendable"] = True # costs todo @@ -1766,14 +1770,29 @@ def adjust_endogenous_transport(n): " load", bus=buses_i, carrier="load", - marginal_cost=1e5, - p_nom=1e9, + marginal_cost=1e9, + p_nom=1e5, ) + + n.madd( + "Generator", + buses_i, + " load", + bus=buses_i, + carrier="load", + marginal_cost=1e9, + p_nom=1e5, + p_max_pu=0, + p_min_pu=-1, + sign=-1, + ) + + for car_type, cost in costs_car_type.items(): car_i = n.links[n.links.carrier == car_type].index n.links.loc[car_i, "capital_cost"] = cost - n.links.loc[car_i, "lifetime"] = 15 + def add_land_transport(n, costs): @@ -3759,11 +3778,11 @@ def adjust_transport_temporal_agg(n): p_set = n.loads_t.p_set.loc[:, n.loads.carrier == "land transport demand"] for engine, carrier in engine_types.items(): + + links_i = n.links[n.links.carrier == carrier].index + if links_i.empty: continue + share = get(options[f"land_transport_{engine}_share"], investment_year) - - if share == 0: - continue - links_i = n.links[n.links.carrier == carrier].index efficiency = n.links_t.efficiency.loc[:, links_i] p_set.columns = efficiency.columns p_nom = share * p_set.div(efficiency).max() From 1f76035b029b0860e7cf9d9e93af0da06c99956c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 12:39:44 +0000 Subject: [PATCH 35/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/add_existing_baseyear.py | 18 +++++++++--------- scripts/prepare_perfect_foresight.py | 2 +- scripts/prepare_sector_network.py | 20 +++++++++----------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 0c183c7c8..be9c6aaab 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -76,7 +76,7 @@ def add_existing_land_transport(baseyear, options): 2005: 0.15, # 15-19 years (2000-2005) 2000: 0.30, # 20+ years } - + for build_year, share in car_ages.items(): df = n.links.loc[ice_i] df = df[df.lifetime + build_year > baseyear] @@ -88,10 +88,12 @@ def add_existing_land_transport(baseyear, options): df.rename( index=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), inplace=True ) - p_max_pu.rename(columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), - inplace=True) - efficiency.rename(columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), - inplace=True) + p_max_pu.rename( + columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), inplace=True + ) + efficiency.rename( + columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), inplace=True + ) n.madd( "Link", df.index, @@ -107,11 +109,9 @@ def add_existing_land_transport(baseyear, options): p_max_pu=p_max_pu, lifetime=df.lifetime, ) - - + n.links.loc[ice_i, "p_nom"] = 0 - def add_existing_renewables(df_agg): """ @@ -679,7 +679,7 @@ def add_heating_capacities_installed_before_baseyear( if options["endogenous_transport"]: add_existing_land_transport(baseyear, options) - + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) sanitize_carriers(n, snakemake.config) diff --git a/scripts/prepare_perfect_foresight.py b/scripts/prepare_perfect_foresight.py index 28bf85d3f..ce3e84819 100644 --- a/scripts/prepare_perfect_foresight.py +++ b/scripts/prepare_perfect_foresight.py @@ -166,7 +166,7 @@ def concat_networks(years): year = years[i] network = pypsa.Network(network_path) adjust_electricity_grid(network, year, years) - if not i==0: + if not i == 0: add_build_year_to_new_assets(network, year) # static ---------------------------------- diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 1af50c63f..090ee13c5 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1720,7 +1720,7 @@ def add_ice_cars(n, nodes, p_set, ice_share, temperature): def adjust_endogenous_transport(n): - + logger.info("Assume endogenous land transport") carrier = [ "land transport EV", @@ -1733,11 +1733,11 @@ def adjust_endogenous_transport(n): links_i = n.links[n.links.carrier.isin(carrier)].index n.links.loc[links_i, "p_nom_extendable"] = True n.links.loc[links_i, "lifetime"] = 15 - + store_carrier = ["battery storage", "Li ion"] store_i = n.stores[n.stores.carrier.isin(store_carrier)].index n.stores.loc[store_i, "e_nom_extendable"] = True - + # costs todo # assume here for all of Europe # average driving distance 15 000 km /year and car @@ -1773,7 +1773,7 @@ def adjust_endogenous_transport(n): marginal_cost=1e9, p_nom=1e5, ) - + n.madd( "Generator", buses_i, @@ -1786,13 +1786,10 @@ def adjust_endogenous_transport(n): p_min_pu=-1, sign=-1, ) - - for car_type, cost in costs_car_type.items(): car_i = n.links[n.links.carrier == car_type].index n.links.loc[car_i, "capital_cost"] = cost - def add_land_transport(n, costs): @@ -3778,10 +3775,11 @@ def adjust_transport_temporal_agg(n): p_set = n.loads_t.p_set.loc[:, n.loads.carrier == "land transport demand"] for engine, carrier in engine_types.items(): - - links_i = n.links[n.links.carrier == carrier].index - if links_i.empty: continue - + + links_i = n.links[n.links.carrier == carrier].index + if links_i.empty: + continue + share = get(options[f"land_transport_{engine}_share"], investment_year) efficiency = n.links_t.efficiency.loc[:, links_i] p_set.columns = efficiency.columns From f762d6fb9f716f0b8351783bc3791c50e4f1e122 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Thu, 14 Mar 2024 14:57:49 +0100 Subject: [PATCH 36/60] add constraints for BEV --- scripts/solve_network.py | 49 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index d1ca16d79..cf45f00eb 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -820,6 +820,45 @@ def add_co2_atmosphere_constraint(n, snapshots): n.model.add_constraints(lhs <= rhs, name=f"GlobalConstraint-{name}") +def add_endogenous_transport_constraints(n, snapshots): + """add constraints to relate number of EVs to EV charger, V2G and DSM.""" + + # get index TODO only extendable + link_ext = n.links[n.links.p_nom_extendable] + ev_i = link_ext[link_ext.carrier=="land transport EV"].index + bev_i = link_ext[link_ext.carrier=="BEV charger"].index + v2g_i = link_ext[link_ext.carrier=="V2G"].index + bev_dsm_i = n.stores[(n.stores.carrier=="battery storage") + & n.stores.e_nom_extendable].index + + if ev_i.empty: return + # factor + f = (n.links.loc[ev_i, "p_nom"].rename(n.links.bus0) + .div(n.links.loc[bev_i, "p_nom"].rename(n.links.bus1))) + + # variables + link_p_nom = n.model.variables.Link_p_nom + + # constraint for BEV charger + lhs = link_p_nom.loc[ev_i] - (link_p_nom.loc[bev_i] * f.values) + n.model.add_constraints(lhs==0, name="p_nom-EV-BEV") + + if not v2g_i.empty: + # constraint for V2G + lhs = link_p_nom.loc[ev_i] - (link_p_nom.loc[v2g_i] * f.values) + n.model.add_constraints(lhs==0, name="p_nom-EV-V2G") + + if not bev_dsm_i.empty: + # factor + f = (n.links.loc[ev_i, "p_nom"].rename(n.links.bus0) + .div(n.stores.loc[bev_dsm_i, "e_nom"].rename(n.links.bus1))) + + store_e_nom = n.model.variables.Store_e_nom + + # constraint for DSM + lhs = link_p_nom.loc[ev_i] - (store_e_nom.loc[bev_dsm_i] * f.values) + n.model.add_constraints(lhs==0, name="e_nom-EV-DSM") + def extra_functionality(n, snapshots): """ Collects supplementary constraints which will be passed to @@ -854,7 +893,9 @@ def extra_functionality(n, snapshots): add_retrofit_gas_boiler_constraint(n, snapshots) else: add_co2_atmosphere_constraint(n, snapshots) - + + if n.config["sector"]["endogenous_transport"]: + add_endogenous_transport_constraints(n, snapshots) if snakemake.params.custom_extra_functionality: source_path = snakemake.params.custom_extra_functionality assert os.path.exists(source_path), f"{source_path} does not exist" @@ -932,9 +973,9 @@ def solve_network(n, config, solving, **kwargs): # configfiles="../config/test/config.perfect.yaml", simpl="", opts="", - clusters="37", - ll="v1.0", - sector_opts="730H-T-H-B-I-A-dist1", + clusters="5", + ll="v1.5", + sector_opts="CO2L0-24h-T-H-B-I-A-dist1", planning_horizons="2030", ) configure_logging(snakemake) From de8904825dfeabcea2295f7cf2a1464cc76eda92 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:58:22 +0000 Subject: [PATCH 37/60] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/solve_network.py | 61 ++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index cf45f00eb..19f2cb73c 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -821,44 +821,55 @@ def add_co2_atmosphere_constraint(n, snapshots): def add_endogenous_transport_constraints(n, snapshots): - """add constraints to relate number of EVs to EV charger, V2G and DSM.""" - + """ + Add constraints to relate number of EVs to EV charger, V2G and DSM. + """ + # get index TODO only extendable link_ext = n.links[n.links.p_nom_extendable] - ev_i = link_ext[link_ext.carrier=="land transport EV"].index - bev_i = link_ext[link_ext.carrier=="BEV charger"].index - v2g_i = link_ext[link_ext.carrier=="V2G"].index - bev_dsm_i = n.stores[(n.stores.carrier=="battery storage") - & n.stores.e_nom_extendable].index - - if ev_i.empty: return + ev_i = link_ext[link_ext.carrier == "land transport EV"].index + bev_i = link_ext[link_ext.carrier == "BEV charger"].index + v2g_i = link_ext[link_ext.carrier == "V2G"].index + bev_dsm_i = n.stores[ + (n.stores.carrier == "battery storage") & n.stores.e_nom_extendable + ].index + + if ev_i.empty: + return # factor - f = (n.links.loc[ev_i, "p_nom"].rename(n.links.bus0) - .div(n.links.loc[bev_i, "p_nom"].rename(n.links.bus1))) - + f = ( + n.links.loc[ev_i, "p_nom"] + .rename(n.links.bus0) + .div(n.links.loc[bev_i, "p_nom"].rename(n.links.bus1)) + ) + # variables link_p_nom = n.model.variables.Link_p_nom - + # constraint for BEV charger lhs = link_p_nom.loc[ev_i] - (link_p_nom.loc[bev_i] * f.values) - n.model.add_constraints(lhs==0, name="p_nom-EV-BEV") - + n.model.add_constraints(lhs == 0, name="p_nom-EV-BEV") + if not v2g_i.empty: # constraint for V2G - lhs = link_p_nom.loc[ev_i] - (link_p_nom.loc[v2g_i] * f.values) - n.model.add_constraints(lhs==0, name="p_nom-EV-V2G") - + lhs = link_p_nom.loc[ev_i] - (link_p_nom.loc[v2g_i] * f.values) + n.model.add_constraints(lhs == 0, name="p_nom-EV-V2G") + if not bev_dsm_i.empty: # factor - f = (n.links.loc[ev_i, "p_nom"].rename(n.links.bus0) - .div(n.stores.loc[bev_dsm_i, "e_nom"].rename(n.links.bus1))) - + f = ( + n.links.loc[ev_i, "p_nom"] + .rename(n.links.bus0) + .div(n.stores.loc[bev_dsm_i, "e_nom"].rename(n.links.bus1)) + ) + store_e_nom = n.model.variables.Store_e_nom - + # constraint for DSM lhs = link_p_nom.loc[ev_i] - (store_e_nom.loc[bev_dsm_i] * f.values) - n.model.add_constraints(lhs==0, name="e_nom-EV-DSM") - + n.model.add_constraints(lhs == 0, name="e_nom-EV-DSM") + + def extra_functionality(n, snapshots): """ Collects supplementary constraints which will be passed to @@ -893,7 +904,7 @@ def extra_functionality(n, snapshots): add_retrofit_gas_boiler_constraint(n, snapshots) else: add_co2_atmosphere_constraint(n, snapshots) - + if n.config["sector"]["endogenous_transport"]: add_endogenous_transport_constraints(n, snapshots) if snakemake.params.custom_extra_functionality: From 69897f6e9ef0119a04b4ed5aa3ba0ab9d577b3b3 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 26 Mar 2024 14:45:16 +0100 Subject: [PATCH 38/60] capital cost divide by efficiency --- scripts/prepare_sector_network.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 090ee13c5..71c23a1d0 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1744,16 +1744,19 @@ def adjust_endogenous_transport(n): # EV -------------------------- # average consumption EV 18 kWh/100 km # annual demand per car 18 kWh/100km * 150 100km/a = 2.7 MWh/a - cost_EV = costs.loc["Battery electric (passenger cars)", "fixed"] / 2.7 + car_efficiency = costs.at["Battery electric (passenger cars)", "efficiency"] + cost_EV = costs.loc["Battery electric (passenger cars)", "fixed"] / 2.7 / car_efficiency # FCE ---------------------------- # average consumption 0.7-1 kg_H2/100km assume 0.85-> 0.85*33.33 kWh_H2/100 km # annual demand per car 33.33 kWh/100km * 150 100km/a = 4.25 MWh/a - cost_FCE = costs.loc["Hydrogen fuel cell (passenger cars)", "fixed"] / 4.25 + car_efficiency = options["transport_fuel_cell_efficiency"] + cost_FCE = costs.loc["Hydrogen fuel cell (passenger cars)", "fixed"] / 4.25 / car_efficiency # ICE --------------------------------------------------------- # average consumption 6.5liter/100km # energy content gasoline 9.7 kWh/liter # annual demand per car 6.5 * 9.7 kWh/100km * 150 100km/a = 9.46 - cost_ICE = costs.at["Liquid fuels ICE (passenger cars)", "fixed"] / 9.46 + car_efficiency = options["transport_internal_combustion_efficiency"] + cost_ICE = costs.at["Liquid fuels ICE (passenger cars)", "fixed"] / 9.46 / car_efficiency # cost in unit input depending on car type costs_car_type = { "land transport EV": cost_EV, From 7fba3b0f5ed2becefe4110c7fb727d5c45edd1ca Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Wed, 27 Mar 2024 17:19:33 +0100 Subject: [PATCH 39/60] add electrobiofuels --- config/config.default.yaml | 2 ++ scripts/prepare_sector_network.py | 51 +++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 8df5ebb87..abf52805c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -383,6 +383,7 @@ sector: district_heating_loss: 0.15 cluster_heat_buses: true endogenous_transport: true + electrobiofuels: true bev_dsm_restriction_value: 0.75 bev_dsm_restriction_time: 7 transport_heating_deadband_upper: 20. @@ -914,6 +915,7 @@ plotting: gas pipeline: '#ebbca0' gas pipeline new: '#a87c62' # oil + electrobiofuels: '#a1b5ae' oil: '#c9c9c9' imported oil: '#a3a3a3' oil boiler: '#adadad' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 71c23a1d0..159d65d8f 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1643,7 +1643,29 @@ def add_EVs( lifetime=1, ) - +def add_electrobiofuels(n, nodes): + + print('Adding electrobiofuels') + efuel_scale_factor = costs.at['BtL', 'C stored'] + n.madd("Link", + nodes + " electrobiofuels", + bus0=spatial.biomass.nodes, + bus1=spatial.oil.nodes, + bus2=spatial.h2.nodes, + bus3="co2 atmosphere", + carrier="electrobiofuels", + lifetime=costs.at['electrobiofuels', 'lifetime'], + efficiency=costs.at['electrobiofuels', 'efficiency-biomass'], + efficiency2=-costs.at['electrobiofuels', 'efficiency-hydrogen'], + efficiency3=-costs.at['solid biomass', 'CO2 intensity'] + costs.at['BtL', 'CO2 stored'] * (1 - costs.at['Fischer-Tropsch', 'capture rate']), + p_nom_extendable=True, + capital_cost=costs.at['BtL', 'fixed'] * costs.at['electrobiofuels', 'efficiency-biomass'] \ + + efuel_scale_factor * costs.at['Fischer-Tropsch', 'fixed'] * costs.at['electrobiofuels', 'efficiency-hydrogen'], + marginal_cost=costs.at['BtL', 'VOM'] * costs.at['electrobiofuels', 'efficiency-biomass'] \ + + efuel_scale_factor * costs.at['Fischer-Tropsch', 'VOM'] * costs.at['electrobiofuels', 'efficiency-hydrogen'] + ) + + def add_fuel_cell_cars(n, nodes, p_set, fuel_cell_share, temperature): car_efficiency = options["transport_fuel_cell_efficiency"] @@ -1777,18 +1799,18 @@ def adjust_endogenous_transport(n): p_nom=1e5, ) - n.madd( - "Generator", - buses_i, - " load", - bus=buses_i, - carrier="load", - marginal_cost=1e9, - p_nom=1e5, - p_max_pu=0, - p_min_pu=-1, - sign=-1, - ) + # n.madd( + # "Generator", + # buses_i, + # " load negative", + # bus=buses_i, + # carrier="load", + # marginal_cost=1e9, + # p_nom=1e5, + # p_max_pu=0, + # p_min_pu=-1, + # sign=-1, + # ) for car_type, cost in costs_car_type.items(): car_i = n.links[n.links.carrier == car_type].index @@ -1873,6 +1895,9 @@ def add_land_transport(n, costs): if endogenous: adjust_endogenous_transport(n) + + if options['electrobiofuels']: + add_electrobiofuels(n, nodes) def build_heat_demand(n): From a913f3b7801ccdba861ea56d5e190e28cc3428a4 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Sat, 30 Mar 2024 18:15:41 +0100 Subject: [PATCH 40/60] only make summary if n solved --- scripts/make_summary.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 18642afc5..400a07c39 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -294,6 +294,7 @@ def calculate_energy(n, label, energy): ) # remove values where bus is missing (bug in nomopyomo) no_bus = c.df.index[c.df["bus" + port] == ""] + if totals.empty: continue totals.loc[no_bus] = float( n.component_attrs[c.name].loc["p" + port, "default"] ) @@ -653,7 +654,8 @@ def make_summaries(networks_dict): logger.info(f"Make summary for scenario {label}, using {filename}") n = pypsa.Network(filename) - + + if not hasattr(n, "objective"): continue assign_carriers(n) assign_locations(n) @@ -667,7 +669,7 @@ def to_csv(df): for key in df: df[key].to_csv(snakemake.output[key]) - +#%% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake From 3e20c4a1956ed2c667029125b795ae9bede394d7 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Sat, 30 Mar 2024 18:16:14 +0100 Subject: [PATCH 41/60] increase resources --- rules/build_electricity.smk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index 24f328eb8..f3451eed0 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -310,7 +310,7 @@ rule build_renewable_profiles: benchmarks("build_renewable_profiles_{technology}") threads: config["atlite"].get("nprocesses", 4) resources: - mem_mb=config["atlite"].get("nprocesses", 4) * 5000, + mem_mb=config["atlite"].get("nprocesses", 4) * 10000, wildcard_constraints: technology="(?!hydro).*", # Any technology other than hydro conda: @@ -481,7 +481,7 @@ rule simplify_network: benchmarks("simplify_network/elec_s{simpl}") threads: 1 resources: - mem_mb=12000, + mem_mb=30000, conda: "../envs/environment.yaml" script: From c70baaa1c88949522c9da0f6e68f5bfa192ef04e Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Sat, 30 Mar 2024 18:16:56 +0100 Subject: [PATCH 42/60] increase price load shedding --- scripts/solve_network.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 19f2cb73c..6629571a4 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -353,7 +353,7 @@ def prepare_network( buses_i = n.buses.index if not np.isscalar(load_shedding): # TODO: do not scale via sign attribute (use Eur/MWh instead of Eur/kWh) - load_shedding = 1e2 # Eur/kWh + load_shedding = 1e4 # Eur/kWh n.madd( "Generator", @@ -984,10 +984,10 @@ def solve_network(n, config, solving, **kwargs): # configfiles="../config/test/config.perfect.yaml", simpl="", opts="", - clusters="5", - ll="v1.5", - sector_opts="CO2L0-24h-T-H-B-I-A-dist1", - planning_horizons="2030", + clusters="37", + ll="v1.0", + sector_opts="Co2L0-25sn-T-H-B-I-A-dist1", + planning_horizons="2050", ) configure_logging(snakemake) set_scenario_config(snakemake) From f7708756a2d0d1ff634f7f9cfe3672679b05d3c0 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Sat, 30 Mar 2024 18:17:46 +0100 Subject: [PATCH 43/60] quick fix for infinity values in energy totals --- scripts/prepare_sector_network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 090ee13c5..f8708ae69 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3119,7 +3119,7 @@ def add_industry(n, costs): p_set = ( demand_factor - * pop_weighted_energy_totals.loc[nodes, all_aviation].sum(axis=1) + * pop_weighted_energy_totals.loc[nodes, all_aviation].replace(np.inf, 0).sum(axis=1) * 1e6 / nhours ).rename(lambda x: x + " kerosene for aviation") @@ -3803,8 +3803,8 @@ def adjust_transport_temporal_agg(n): opts="", clusters="37", ll="v1.0", - sector_opts="730H-T-H-B-I-A-dist1", - planning_horizons="2030", + sector_opts="25sn-T-H-B-I-A-dist1", + planning_horizons="2050", ) configure_logging(snakemake) From d5bb33bf848ca72e9e5eaba6839332b9101a2169 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 2 Apr 2024 11:56:34 +0200 Subject: [PATCH 44/60] add build year for existing cars --- scripts/add_existing_baseyear.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index be9c6aaab..7702ab332 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -107,6 +107,7 @@ def add_existing_land_transport(baseyear, options): p_nom=df.p_nom, p_min_pu=p_max_pu, p_max_pu=p_max_pu, + build_year=df.buildyear, lifetime=df.lifetime, ) From adc805e2c59cdb2d6833ac44dc4e69276ec39bff Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 2 Apr 2024 12:01:40 +0200 Subject: [PATCH 45/60] add try except when read in --- scripts/make_summary.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/make_summary.py b/scripts/make_summary.py index 400a07c39..e083bc672 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -652,8 +652,11 @@ def make_summaries(networks_dict): df = {output: pd.DataFrame(columns=columns, dtype=float) for output in outputs} for label, filename in networks_dict.items(): logger.info(f"Make summary for scenario {label}, using {filename}") - - n = pypsa.Network(filename) + try: + n = pypsa.Network(filename) + except FileNotFoundError: + logger.info(f"{label} not yet solved.") + continue if not hasattr(n, "objective"): continue assign_carriers(n) From 65441815038fb7e1d51082b5b41865cdcb8dd68c Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 2 Apr 2024 12:02:03 +0200 Subject: [PATCH 46/60] test changes solver settings --- config/config.default.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index abf52805c..b95e3e3b0 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -775,21 +775,22 @@ solving: threads: 4 method: 2 # barrier crossover: 0 - BarConvTol: 1.e-6 + BarConvTol: 1.e-5 + FeasibilityTol: 1.e-4 + OptimalityTol: 1.e-4 Seed: 123 AggFill: 0 PreDual: 0 GURO_PAR_BARDENSETHRESH: 200 gurobi-numeric-focus: - name: gurobi - NumericFocus: 3 # Favour numeric stability over speed + # NumericFocus: 3 # Favour numeric stability over speed method: 2 # barrier crossover: 0 # do not use crossover BarHomogeneous: 1 # Use homogeneous barrier if standard does not converge BarConvTol: 1.e-5 FeasibilityTol: 1.e-4 OptimalityTol: 1.e-4 - ObjScale: -0.5 + # ObjScale: -0.5 threads: 8 Seed: 123 gurobi-fallback: # Use gurobi defaults From f90be0bfa0caf28dabacdd7be4dc97b3fffe532f Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 5 Apr 2024 08:45:09 +0200 Subject: [PATCH 47/60] fix typo --- scripts/add_existing_baseyear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 7702ab332..d07ef6ec9 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -107,7 +107,7 @@ def add_existing_land_transport(baseyear, options): p_nom=df.p_nom, p_min_pu=p_max_pu, p_max_pu=p_max_pu, - build_year=df.buildyear, + build_year=df.build_year, lifetime=df.lifetime, ) From 893468223226ca2a11ebcd7ef81457154a7c1ce8 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 5 Apr 2024 08:46:42 +0200 Subject: [PATCH 48/60] adjust co2 neutral constraint --- scripts/prepare_perfect_foresight.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_perfect_foresight.py b/scripts/prepare_perfect_foresight.py index ce3e84819..812f68d07 100644 --- a/scripts/prepare_perfect_foresight.py +++ b/scripts/prepare_perfect_foresight.py @@ -336,7 +336,7 @@ def set_carbon_constraints(n): n.add( "GlobalConstraint", "carbon_neutral", - type="co2_limit", + type="co2_atmosphere", carrier_attribute="co2_emissions", sense="<=", constant=0, @@ -499,8 +499,8 @@ def apply_time_segmentation_perfect( simpl="", opts="", clusters="37", - ll="v1.5", - sector_opts="1p7-4380H-T-H-B-I-A-dist1", + ll="v1.0", + sector_opts="1p5-730H-T-H-B-I-A-dist1", ) configure_logging(snakemake) set_scenario_config(snakemake) From c363988f1dec89d591e240fa9ee5d9012e4e3274 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 5 Apr 2024 08:47:10 +0200 Subject: [PATCH 49/60] add cluster settings --- rules/solve_perfect.smk | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index a565d9788..7d355d5cf 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -113,7 +113,8 @@ rule solve_sector_network_perfect: + "postnetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_brownfield_all_years.nc", threads: solver_threads resources: - mem_mb=config_provider("solving", "mem"), + mem_mb=config_provider("solving", "mem_mb"), + runtime=config_provider("solving", "runtime", default="24h"), shadow: "shallow" log: From e8e6b73c6b8d133f4593d7a467d3238e9bfcb434 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 5 Apr 2024 08:48:09 +0200 Subject: [PATCH 50/60] add split light heavy vehicles --- scripts/build_energy_totals.py | 71 ++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index 1ffc4ae2c..dd48779af 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -240,9 +240,9 @@ def idees_per_country(ct, year, base_dir): ct_totals["total agriculture"] = df[row] # transport - + df = pd.read_excel(fn_transport, "TrRoad_ene", index_col=0)[year] - + ct_totals["total road"] = df["by fuel (EUROSTAT DATA)"] ct_totals["electricity road"] = df["Electricity"] @@ -272,6 +272,9 @@ def idees_per_country(ct, year, base_dir): assert df.index[61] == "Passenger cars" ct_totals["passenger car efficiency"] = df.iloc[61] + + assert df.index[81] == "Heavy duty vehicles" + ct_totals["heavy duty efficiency"] = df.iloc[81] df = pd.read_excel(fn_transport, "TrRail_ene", index_col=0)[year] @@ -330,9 +333,25 @@ def idees_per_country(ct, year, base_dir): ct_totals["total domestic navigation"] = df["by fuel (EUROSTAT DATA)"] df = pd.read_excel(fn_transport, "TrRoad_act", index_col=0)[year] - + + # total number of light duty vehicles assert df.index[85] == "Passenger cars" - ct_totals["passenger cars"] = df.iloc[85] + + ct_totals["Number Passenger cars"] = df.iloc[85] + + assert df.index[84] == "Powered 2-wheelers" + ct_totals['Number Powered 2-wheelers'] = df.iloc[84] + + assert df.index[99] == "Light duty vehicles" + ct_totals['Number Light duty vehicles'] = df.iloc[99] + + # total number of heavy duty vehicles + + assert df.index[92] == "Motor coaches, buses and trolley buses" + ct_totals["Number Motor coaches, buses and trolley buses"] = df.iloc[92] + + assert df.index[105] == 'Heavy duty vehicles' + ct_totals['Number Heavy duty vehicles'] = df.iloc[105] return pd.Series(ct_totals, name=ct) @@ -356,11 +375,12 @@ def build_idees(countries, year): totals = pd.concat(totals_list, axis=1) # convert ktoe to TWh - exclude = totals.index.str.fullmatch("passenger cars") + exclude = totals.index.str.contains("Number") totals.loc[~exclude] *= 11.63 / 1e3 # convert TWh/100km to kWh/km totals.loc["passenger car efficiency"] *= 10 + totals.loc["heavy duty efficiency"] *= 10 return totals.T @@ -368,7 +388,13 @@ def build_idees(countries, year): def build_energy_totals(countries, eurostat, swiss, idees): eurostat_fuels = {"electricity": "Electricity", "total": "Total all products"} - to_drop = ["passenger cars", "passenger car efficiency"] + to_drop = ["Number Passenger cars", + "Number Powered 2-wheelers", + "Number Light duty vehicles", + "Number Motor coaches, buses and trolley buses", + "Number Heavy duty vehicles", + "passenger car efficiency", + "heavy duty efficiency"] df = idees.reindex(countries).drop(to_drop, axis=1) eurostat_countries = eurostat.index.levels[0] @@ -671,8 +697,13 @@ def build_transport_data(countries, population, idees): transport_data = pd.DataFrame(index=countries) # collect number of cars - - transport_data["number cars"] = idees["passenger cars"] + car_cols = ["Number Passenger cars", + "Number Powered 2-wheelers", + "Number Light duty vehicles", + "Number Motor coaches, buses and trolley buses", + "Number Heavy duty vehicles"] + + transport_data[car_cols] = idees[car_cols] # CH from http://ec.europa.eu/eurostat/statistics-explained/index.php/Passenger_cars_in_the_EU#Luxembourg_has_the_highest_number_of_passenger_cars_per_inhabitant if "CH" in countries: @@ -688,17 +719,17 @@ def build_transport_data(countries, population, idees): transport_data.loc[missing, "number cars"] = cars_pp.mean() * population # collect average fuel efficiency in kWh/km - - transport_data["average fuel efficiency"] = idees["passenger car efficiency"] - - missing = transport_data.index[transport_data["average fuel efficiency"].isna()] - if not missing.empty: - logger.info( - f"Missing data on fuel efficiency from:\n{list(missing)}\nFilling gapswith averaged data." - ) - - fill_values = transport_data["average fuel efficiency"].mean() - transport_data.loc[missing, "average fuel efficiency"] = fill_values + for vehicle_type in ["passenger car", "heavy duty"]: + transport_data[f"average fuel efficiency {vehicle_type}"] = idees[f"{vehicle_type} efficiency"] + + missing = transport_data.index[transport_data[f"average fuel efficiency {vehicle_type}"].isna()] + if not missing.empty: + logger.info( + f"Missing data on fuel efficiency from:\n{list(missing)}\nFilling gapswith averaged data." + ) + + fill_values = transport_data[f"average fuel efficiency {vehicle_type}"].mean() + transport_data.loc[missing, f"average fuel efficiency {vehicle_type}"] = fill_values return transport_data @@ -827,7 +858,7 @@ def rescale_idees_from_eurostat( return energy - +#%% if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake From 9429b97bf706a3fcf2f1405f2dafef802c1e9000 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 5 Apr 2024 08:50:03 +0200 Subject: [PATCH 51/60] intermediate fix co2 budget --- scripts/_helpers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/_helpers.py b/scripts/_helpers.py index a1504c3c7..b244beb12 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -570,9 +570,18 @@ def update_config_from_wildcards(config, w, inplace=True): if "CCL" in opts: config["solving"]["constraints"]["CCL"] = True - + + co2_budget = { + "1p5": 34.2 , # 25.7 # Budget in Gt CO2 for 1.5 for Europe, global 420 Gt, assuming per capita share + "1p6": 43.259666 , # 35 # Budget in Gt CO2 for 1.6 for Europe, global 580 Gt + "1p7": 51.4, # 45 # Budget in Gt CO2 for 1.7 for Europe, global 800 Gt + "2p0": 69.778 , # 73.9 # Budget in Gt CO2 for 2 for Europe, global 1170 Gt + } eq_value = get_opt(opts, r"^EQ+\d*\.?\d+(c|)") for o in opts: + m = re.match(r"^\d+p\d$", o, re.IGNORECASE) + if m is not None: + config["co2_budget"]= co2_budget[m.group(0)] if eq_value is not None: config["solving"]["constraints"]["EQ"] = eq_value elif "EQ" in o: From 027a9a5896d90620c6424e4fd300c655fe9efcb9 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 5 Apr 2024 10:04:45 +0200 Subject: [PATCH 52/60] split between light heavy demand --- rules/build_sector.smk | 1 + scripts/build_transport_demand.py | 88 ++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 8d41e893b..bb30af107 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -735,6 +735,7 @@ rule build_transport_demand: transport_data=resources("transport_data.csv"), traffic_data_KFZ="data/bundle-sector/emobility/KFZ__count", traffic_data_Pkw="data/bundle-sector/emobility/Pkw__count", + traffic_data_Lkw="data/bundle-sector/emobility/Lkw__count", temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"), output: transport_demand=resources("transport_demand_s{simpl}_{clusters}.csv"), diff --git a/scripts/build_transport_demand.py b/scripts/build_transport_demand.py index 26f90ca7f..c9ebcae58 100644 --- a/scripts/build_transport_demand.py +++ b/scripts/build_transport_demand.py @@ -22,26 +22,38 @@ def build_nodal_transport_data(fn, pop_layout): # get numbers of car and fuel efficiency per country transport_data = pd.read_csv(fn, index_col=0) + # TODO check what is going wrong in LU + transport_data.loc['LU', 'average fuel efficiency heavy duty'] = transport_data.loc['SI', 'average fuel efficiency heavy duty'] + # 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"] + car_cols = ["Number Passenger cars", + "Number Powered 2-wheelers", + "Number Light duty vehicles", + "Number Motor coaches, buses and trolley buses", + "Number Heavy duty vehicles"] + + nodal_transport_data[car_cols] = ( + nodal_transport_data[car_cols].mul(pop_layout["fraction"], axis=0) ) # fill missing fuel efficiency with average data - nodal_transport_data.loc[ - nodal_transport_data["average fuel efficiency"] == 0.0, - "average fuel efficiency", - ] = transport_data["average fuel efficiency"].mean() + eff_cols = ['average fuel efficiency passenger car', + 'average fuel efficiency heavy duty'] + for col in eff_cols: + nodal_transport_data.loc[ + nodal_transport_data[col] == 0.0, + col, + ] = transport_data[col].mean() + + # TODO missing data for countries AL, RS, BA, NO, MK, CH, ME return nodal_transport_data -def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data): - """ - Returns transport demand per bus in unit kinetic energy. - """ - # averaged weekly counts from the year 2010-2015 + +def get_shape(traffic_fn): traffic = pd.read_csv(traffic_fn, skiprows=2, usecols=["count"]).squeeze("columns") # create annual profile take account time zone + summer time @@ -51,6 +63,17 @@ def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data): weekly_profile=traffic.values, ) transport_shape = transport_shape / transport_shape.sum() + + return transport_shape + +def build_transport_demand(traffic_fn_Pkw, traffic_fn_Lkw, + airtemp_fn, nodes, nodal_transport_data): + """ + Returns transport demand per bus in unit kinetic energy. + """ + transport_shape_light = get_shape(traffic_fn_Pkw) + + transport_shape_heavy = get_shape(traffic_fn_Lkw) # get heating demand for correction to demand time series temperature = xr.open_dataarray(airtemp_fn).to_pandas() @@ -65,18 +88,46 @@ def build_transport_demand(traffic_fn, airtemp_fn, nodes, nodal_transport_data): ) # divide out the heating/cooling demand from ICE totals - ice_correction = (transport_shape * (1 + dd_ICE)).sum() / transport_shape.sum() + ice_correction_light = (transport_shape_light * (1 + dd_ICE)).sum() / transport_shape_light.sum() + ice_correction_heavy = (transport_shape_light * (1 + dd_ICE)).sum() / transport_shape_heavy.sum() + energy_totals_transport = ( pop_weighted_energy_totals["total road"] + pop_weighted_energy_totals["total rail"] - pop_weighted_energy_totals["electricity rail"] ) - - return (transport_shape.multiply(energy_totals_transport) * 1e6 * nyears).divide( - nodal_transport_data["average fuel efficiency"] * ice_correction - ) - + + light_duty_cols = ['total two-wheel', 'total passenger cars', + 'total light duty road freight'] + + heavy_duty_cols = ['total other road passenger', # motor coaches, buses, trolley buses + 'total heavy duty road freight'] + + light_duty = pop_weighted_energy_totals[light_duty_cols].sum(axis=1) + + heavy_duty = pop_weighted_energy_totals[heavy_duty_cols].sum(axis=1) + + rail = pop_weighted_energy_totals["total rail"] - pop_weighted_energy_totals["electricity rail"] + + def get_demand(transport_shape, energy_totals_transport, nyears, + fuel_efficiency, ice_correction, name): + + demand = (transport_shape.multiply(energy_totals_transport) * 1e6 * nyears).divide( + fuel_efficiency * ice_correction + ) + + return pd.concat([demand], keys=[name], axis=1) + + demand_light = get_demand(transport_shape_light, light_duty, nyears, + nodal_transport_data["average fuel efficiency passenger car"], + ice_correction_light, name="light") + demand_heavy = get_demand(transport_shape_heavy, (heavy_duty + rail), nyears, + nodal_transport_data["average fuel efficiency heavy duty"], + ice_correction_heavy, name="heavy") + + return pd.concat([demand_light, demand_heavy], axis=1) + def transport_degree_factor( temperature, @@ -184,7 +235,8 @@ def bev_dsm_profile(snapshots, nodes, options): ) transport_demand = build_transport_demand( - snakemake.input.traffic_data_KFZ, + snakemake.input.traffic_data_Pkw, + snakemake.input.traffic_data_Lkw, snakemake.input.temp_air_total, nodes, nodal_transport_data, From 5b3746de6ac017163706a16b5567d019c4c91e53 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 5 Apr 2024 13:30:48 +0200 Subject: [PATCH 53/60] fill missing vehicle numbers --- scripts/build_energy_totals.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index dd48779af..e365c0a4e 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -243,6 +243,7 @@ def idees_per_country(ct, year, base_dir): df = pd.read_excel(fn_transport, "TrRoad_ene", index_col=0)[year] + # energy consumptiob by fuel (ktoe) ct_totals["total road"] = df["by fuel (EUROSTAT DATA)"] ct_totals["electricity road"] = df["Electricity"] @@ -269,7 +270,8 @@ def idees_per_country(ct, year, base_dir): row = "Heavy duty vehicles (Diesel oil incl. biofuels)" ct_totals["total heavy duty road freight"] = df[row] - + + # vehicle efficiency (kgoe/100km) assert df.index[61] == "Passenger cars" ct_totals["passenger car efficiency"] = df.iloc[61] @@ -707,16 +709,16 @@ def build_transport_data(countries, population, idees): # CH from http://ec.europa.eu/eurostat/statistics-explained/index.php/Passenger_cars_in_the_EU#Luxembourg_has_the_highest_number_of_passenger_cars_per_inhabitant if "CH" in countries: - transport_data.at["CH", "number cars"] = 4.136e6 - - missing = transport_data.index[transport_data["number cars"].isna()] - if not missing.empty: - logger.info( - f"Missing data on cars from:\n{list(missing)}\nFilling gaps with averaged data." - ) - - cars_pp = transport_data["number cars"] / population - transport_data.loc[missing, "number cars"] = cars_pp.mean() * population + transport_data.at["CH", "Number Passenger cars"] = 4.136e6 + + for col in car_cols: + missing = transport_data.index[transport_data[col].isna()] + if not missing.empty: + logger.info( + f"Missing data on {col} from:\n{list(missing)}\nFilling gaps with averaged data." + ) + cars_pp = transport_data[col] / population + transport_data.loc[missing, col] = cars_pp.mean() * population # collect average fuel efficiency in kWh/km for vehicle_type in ["passenger car", "heavy duty"]: From 952200f288402201ecf3bdb801531785179f462d Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 5 Apr 2024 13:31:18 +0200 Subject: [PATCH 54/60] clean up --- scripts/build_transport_demand.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/scripts/build_transport_demand.py b/scripts/build_transport_demand.py index c9ebcae58..2047de7c3 100644 --- a/scripts/build_transport_demand.py +++ b/scripts/build_transport_demand.py @@ -47,8 +47,6 @@ def build_nodal_transport_data(fn, pop_layout): col, ] = transport_data[col].mean() - # TODO missing data for countries AL, RS, BA, NO, MK, CH, ME - return nodal_transport_data @@ -91,13 +89,6 @@ def build_transport_demand(traffic_fn_Pkw, traffic_fn_Lkw, ice_correction_light = (transport_shape_light * (1 + dd_ICE)).sum() / transport_shape_light.sum() ice_correction_heavy = (transport_shape_light * (1 + dd_ICE)).sum() / transport_shape_heavy.sum() - - energy_totals_transport = ( - pop_weighted_energy_totals["total road"] - + pop_weighted_energy_totals["total rail"] - - pop_weighted_energy_totals["electricity rail"] - ) - light_duty_cols = ['total two-wheel', 'total passenger cars', 'total light duty road freight'] From 71114d81211650beb8d09d09ac5e80a3e29dd54c Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Fri, 5 Apr 2024 15:31:39 +0200 Subject: [PATCH 55/60] correct units --- scripts/prepare_sector_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index dceb90d1b..3e646ea77 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1765,7 +1765,7 @@ def adjust_endogenous_transport(n): # average driving distance 15 000 km /year and car # EV -------------------------- # average consumption EV 18 kWh/100 km - # annual demand per car 18 kWh/100km * 150 100km/a = 2.7 MWh/a + # annual demand per car 18 kWh/100km * 150 000km/a = 2.7 MWh/a car_efficiency = costs.at["Battery electric (passenger cars)", "efficiency"] cost_EV = costs.loc["Battery electric (passenger cars)", "fixed"] / 2.7 / car_efficiency # FCE ---------------------------- @@ -1857,7 +1857,7 @@ def add_land_transport(n, costs): location=nodes, suffix=" land transport", carrier="land transport demand", - unit="MWh_kinetic", + unit="100 km", ) p_set = transport[nodes] From 1a9ce6a2b20132c734eff4f7482710be63644538 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 8 Apr 2024 06:38:42 +0200 Subject: [PATCH 56/60] correct units + efficiencies to 100 km --- scripts/prepare_sector_network.py | 57 ++++++++++++++++++------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 3e646ea77..bf5892d90 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1554,7 +1554,11 @@ def add_EVs( carrier="Li ion", unit="MWh_el", ) - + + # https://ev-database.org/de/cheatsheet/energy-consumption-electric-car + # average energy consumption 188 Wh/km = 0.188 kWh/km = 18.8 kWh/100 km = 0.0188 MWh/ 100 km + # 1/0.188 -> 1 MWh = 53.19 * 100 km + costs.at["Battery electric (passenger cars)", "efficiency"] = 53.19 car_efficiency = costs.at["Battery electric (passenger cars)", "efficiency"] # temperature corrected efficiency @@ -1668,7 +1672,11 @@ def add_electrobiofuels(n, nodes): def add_fuel_cell_cars(n, nodes, p_set, fuel_cell_share, temperature): - car_efficiency = options["transport_fuel_cell_efficiency"] + # https://h2-mobility.de/wp-content/uploads/2021/02/H2M_Flottenpapier_English_20180822.pdf + # assume in average 1kg_H2 per 100 km -> 1kg_H2 = 33 kWh_H2 (LHV) + # 1 MWh_H2 = 30.003 100km + + car_efficiency = 30.003 # options["transport_fuel_cell_efficiency"] # temperature corrected efficiency efficiency = get_temp_efficency( @@ -1705,8 +1713,13 @@ def add_fuel_cell_cars(n, nodes, p_set, fuel_cell_share, temperature): def add_ice_cars(n, nodes, p_set, ice_share, temperature): add_carrier_buses(n, "oil") - - car_efficiency = options["transport_internal_combustion_efficiency"] + + # average consumption 7 liter per 100 km + # 0.008889 MWh_petrol = 1 liter + # 0.062223 MWh_petrol / 100 km + # 1 MWh_petrol = 16.0712 + + car_efficiency = 16.0712 # options["transport_internal_combustion_efficiency"] # temperature corrected efficiency efficiency = get_temp_efficency( @@ -1762,23 +1775,13 @@ def adjust_endogenous_transport(n): # costs todo # assume here for all of Europe - # average driving distance 15 000 km /year and car + # average driving distance 15 000 km /year and car = 150 100km /year # EV -------------------------- - # average consumption EV 18 kWh/100 km - # annual demand per car 18 kWh/100km * 150 000km/a = 2.7 MWh/a - car_efficiency = costs.at["Battery electric (passenger cars)", "efficiency"] - cost_EV = costs.loc["Battery electric (passenger cars)", "fixed"] / 2.7 / car_efficiency + cost_EV = costs.loc["Battery electric (passenger cars)", "fixed"] / 150 # FCE ---------------------------- - # average consumption 0.7-1 kg_H2/100km assume 0.85-> 0.85*33.33 kWh_H2/100 km - # annual demand per car 33.33 kWh/100km * 150 100km/a = 4.25 MWh/a - car_efficiency = options["transport_fuel_cell_efficiency"] - cost_FCE = costs.loc["Hydrogen fuel cell (passenger cars)", "fixed"] / 4.25 / car_efficiency + cost_FCE = costs.loc["Hydrogen fuel cell (passenger cars)", "fixed"] / 150 # ICE --------------------------------------------------------- - # average consumption 6.5liter/100km - # energy content gasoline 9.7 kWh/liter - # annual demand per car 6.5 * 9.7 kWh/100km * 150 100km/a = 9.46 - car_efficiency = options["transport_internal_combustion_efficiency"] - cost_ICE = costs.at["Liquid fuels ICE (passenger cars)", "fixed"] / 9.46 / car_efficiency + cost_ICE = costs.at["Liquid fuels ICE (passenger cars)", "fixed"] / 150 # cost in unit input depending on car type costs_car_type = { "land transport EV": cost_EV, @@ -1822,13 +1825,19 @@ def add_land_transport(n, costs): logger.info("Add land transport") - # read in transport demand in units kinetic energy + # read in transport demand in units 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" - ] + snakemake.input.transport_demand, index_col=[0], header=[0,1], + parse_dates=True + )["light"] + car_cols = ['Number Passenger cars', 'Number Powered 2-wheelers', + 'Number Light duty vehicles', + 'Number Motor coaches, buses and trolley buses', + 'Number Heavy duty vehicles'] + number_cars = pd.read_csv(snakemake.input.transport_data, index_col=0, + )[ + car_cols + ].sum(axis=1) avail_profile = pd.read_csv( snakemake.input.avail_profile, index_col=0, parse_dates=True ) From cf92641e2f756af52ce122555b8dfbad9e2d50dd Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Mon, 8 Apr 2024 13:08:34 +0200 Subject: [PATCH 57/60] correct unit from kWh/km -> MWh/100 km --- scripts/build_transport_demand.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/build_transport_demand.py b/scripts/build_transport_demand.py index 2047de7c3..ace11cbf6 100644 --- a/scripts/build_transport_demand.py +++ b/scripts/build_transport_demand.py @@ -23,7 +23,7 @@ def build_nodal_transport_data(fn, pop_layout): transport_data = pd.read_csv(fn, index_col=0) # TODO check what is going wrong in LU - transport_data.loc['LU', 'average fuel efficiency heavy duty'] = transport_data.loc['SI', 'average fuel efficiency heavy duty'] + # transport_data.loc['LU', 'average fuel efficiency heavy duty'] = transport_data.loc['SI', 'average fuel efficiency heavy duty'] # break number of cars down to nodal level based on population density nodal_transport_data = transport_data.loc[pop_layout.ct].fillna(0.0) @@ -38,7 +38,7 @@ def build_nodal_transport_data(fn, pop_layout): nodal_transport_data[car_cols] = ( nodal_transport_data[car_cols].mul(pop_layout["fraction"], axis=0) ) - # fill missing fuel efficiency with average data + # fill missing fuel efficiency [kWh/km] with average data eff_cols = ['average fuel efficiency passenger car', 'average fuel efficiency heavy duty'] for col in eff_cols: @@ -111,10 +111,11 @@ def get_demand(transport_shape, energy_totals_transport, nyears, return pd.concat([demand], keys=[name], axis=1) demand_light = get_demand(transport_shape_light, light_duty, nyears, - nodal_transport_data["average fuel efficiency passenger car"], + # convert 1 kWh/km = 0.1 MWh/ 100 km + 0.1*nodal_transport_data["average fuel efficiency passenger car"], ice_correction_light, name="light") demand_heavy = get_demand(transport_shape_heavy, (heavy_duty + rail), nyears, - nodal_transport_data["average fuel efficiency heavy duty"], + 0.1*nodal_transport_data["average fuel efficiency heavy duty"], ice_correction_heavy, name="heavy") return pd.concat([demand_light, demand_heavy], axis=1) From 2158f62437364d83ccb3bda86e5385c7a87f6a5a Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 9 Apr 2024 13:46:24 +0200 Subject: [PATCH 58/60] fix bug assigning existing --- scripts/add_existing_baseyear.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index d07ef6ec9..c9f0ac558 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -88,12 +88,11 @@ def add_existing_land_transport(baseyear, options): df.rename( index=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), inplace=True ) - p_max_pu.rename( - columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), inplace=True - ) - efficiency.rename( - columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), inplace=True - ) + profile = p_max_pu.rename( + columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}")) + eff = efficiency.rename( + columns=lambda x: x.replace(f"-{baseyear}", f"-{build_year}")) + n.madd( "Link", df.index, @@ -101,12 +100,14 @@ def add_existing_land_transport(baseyear, options): bus1=df.bus1, bus2=df.bus2, carrier=df.carrier, - efficiency=efficiency, + efficiency=eff, + capital_cost=df.capital_cost, + marginal_cost=df.marginal_cost, efficiency2=df.efficiency2, p_nom_extendable=False, p_nom=df.p_nom, - p_min_pu=p_max_pu, - p_max_pu=p_max_pu, + p_min_pu=profile, + p_max_pu=profile, build_year=df.build_year, lifetime=df.lifetime, ) @@ -612,13 +613,13 @@ def add_heating_capacities_installed_before_baseyear( snakemake = mock_snakemake( "add_existing_baseyear", - configfiles="config/test/config.myopic.yaml", + # configfiles="config/config.myopic.yaml", simpl="", - clusters="5", - ll="v1.5", + clusters="37", + ll="v1.0", opts="", - sector_opts="24h-T-H-B-I-A-dist1", - planning_horizons=2030, + sector_opts="49sn-T-H-B-I-A-dist1", + planning_horizons=2025, ) configure_logging(snakemake) From 6fa0e0c4ecebe9e761afbe96f3d602d2df40dce3 Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 9 Apr 2024 14:01:30 +0200 Subject: [PATCH 59/60] add rule to extract car ages per ct --- rules/build_sector.smk | 25 +++++++ scripts/build_existing_car_ages.py | 101 +++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 scripts/build_existing_car_ages.py diff --git a/rules/build_sector.smk b/rules/build_sector.smk index bb30af107..ad8b3b15c 100644 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -323,6 +323,31 @@ rule build_biomass_potentials: "../scripts/build_biomass_potentials.py" +rule build_existing_car_ages: + input: + ACEA_report=storage( + "https://www.acea.auto/files/ACEA-Report-Vehicles-on-European-roads-.pdf", + keep_local=True, + ), + output: + car_ages=resources( + "car_ages.csv" + ), + truck_ages=resources( + "truck_ages.csv" + ), + threads: 1 + resources: + mem_mb=1000, + log: + logs("build_existing_car_ages.log"), + benchmark: + benchmarks("build_existing_car_ages") + conda: + "../envs/environment.yaml" + script: + "../scripts/build_existing_car_ages.py" + rule build_biomass_transport_costs: input: transport_cost_data=storage( diff --git a/scripts/build_existing_car_ages.py b/scripts/build_existing_car_ages.py new file mode 100644 index 000000000..fc360e5ac --- /dev/null +++ b/scripts/build_existing_car_ages.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors +# +# SPDX-License-Identifier: MIT +""" +Read in existing car and truck ages from ACEA report (2024) +""" + + +import tabula +import numpy as np +import country_converter as coco +import pandas as pd +import logging +logger = logging.getLogger(__name__) +from _helpers import configure_logging, set_scenario_config + +def distirbute_cars(row): + if row.name=="EU": return row + i = 0 + for j, number in enumerate(row): + if np.isnan(number): + i += 1 + else: + distributed = number / (i+1) + row.iloc[j-i:j+1] = distributed + i = 0 + return row + +def distribute_older_ages(ds, number_of_years=8): + "Distribute columne <10 years based on averaged data" + share_last_year = ds.fillna(0).loc['>10 years'] + ds.drop('>10 years', inplace=True) + mean = ds.fillna(0).mean() + # convert index to int + ds.index = ds.index.astype(int) + years_i = np.arange(2012, 2012-number_of_years, -1) + av = share_last_year/mean + distributed = pd.Series(data=0, index=years_i, dtype="float64") + for i, year in enumerate(years_i): + if i+1 < av: + distributed[year] = mean + elif (i == int(av)): + distributed[year] = (av - i) * mean + else: + distributed[year] = 0 + + if share_last_year - distributed.iloc[:-1].sum()> mean: + distributed.iloc[-1] = share_last_year - distributed.iloc[:-1].sum() + + return pd.concat([ds, distributed]) + +def read_ages_from_pdf(fn, page, name="CARS BY AGE"): + tables = tabula.read_pdf(fn, pages=page) + table = tables[0] + table.set_index(name, inplace=True) + + table.columns = table.iloc[1,:] + table = table.iloc[2:,:] + + table = table.applymap(lambda x: np.nan if isinstance(x, str) and '–' in x else x) + table = table.apply(lambda x: (x.str.replace(",","")).astype(float)) + + # convert into to iso-code + cc = coco.CountryConverter() + table.index = cc.convert(table.index, to="iso2") + table.drop('not found', inplace=True) + + table = table.apply(lambda x: distirbute_cars(x), axis=1) + + # get shares instead of total numbers + table.iloc[:, :-2] = table.iloc[:, :-2].div(table["Total"], axis=0) + table.rename(columns={'(in years)': "Average age"}, inplace=True) + df = table.iloc[:,:-2] + # swedish data is missing, filling with average data + df = df.fillna(df.mean()) + final_table = df.apply(distribute_older_ages, axis=1) + + return pd.concat([final_table, table.iloc[:, -2:]], axis=1) + + +#%% +if __name__ == "__main__": + if "snakemake" not in globals(): + from _helpers import mock_snakemake + snakemake = mock_snakemake( + "build_existing_car_ages", + ) + + configure_logging(snakemake) + set_scenario_config(snakemake) + fn = snakemake.input.ACEA_report + # car ages + car_ages = read_ages_from_pdf(fn, page=11, name="CARS BY AGE") + + # trucks ages + truck_ages = read_ages_from_pdf(fn, page=13, name="TRUCKS BY AGE") + + + car_ages.to_csv(snakemake.output.car_ages) + truck_ages.to_csv(snakemake.output.truck_ages) From da2acf1f5b360303e3640152cd391e8e4706569e Mon Sep 17 00:00:00 2001 From: lisazeyen Date: Tue, 9 Apr 2024 15:48:19 +0200 Subject: [PATCH 60/60] add car fleet ages --- rules/solve_myopic.smk | 2 ++ rules/solve_perfect.smk | 2 ++ scripts/add_existing_baseyear.py | 28 +++++++++++++++++----------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index 57b8a9d3f..599bf1f1f 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -10,6 +10,8 @@ rule add_existing_baseyear: existing_capacities=config_provider("existing_capacities"), costs=config_provider("costs"), input: + car_ages=resources("car_ages.csv"), + truck_ages=resources("truck_ages.csv"), network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", powerplants=resources("powerplants.csv"), diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index 7d355d5cf..f570cfdce 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -8,6 +8,8 @@ rule add_existing_baseyear: existing_capacities=config_provider("existing_capacities"), costs=config_provider("costs"), input: + car_ages=resources("car_ages.csv"), + truck_ages=resources("truck_ages.csv"), network=RESULTS + "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc", powerplants=resources("powerplants.csv"), diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index c9f0ac558..852f52079 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -68,22 +68,28 @@ def add_existing_land_transport(baseyear, options): efficiency = n.links_t.efficiency[ice_i] p_max_pu = n.links_t.p_max_pu[ice_i] - # TODO car ages - car_ages = { - 2020: 0.15, # 0-4 years (2020-2025) - 2015: 0.20, # 5-9 years (2010-2015) - 2010: 0.20, # 10-14 years (2005-2010) - 2005: 0.15, # 15-19 years (2000-2005) - 2000: 0.30, # 20+ years - } - - for build_year, share in car_ages.items(): + car_ages = pd.read_csv(snakemake.input.car_ages, index_col=[0]).iloc[:,:-2] + car_ages.columns = car_ages.columns.astype(int) + # group data in 5 years interval + interval = 5 + # mapping forward (mapping backward would be year//5*5) + group_mapping = {year: year//interval*interval+4 for year in car_ages.columns} + + grouped = car_ages.T.groupby(group_mapping).sum().T + + pop_layout = pd.read_csv(snakemake.input.clustered_pop_layout, index_col=0) + + grouped = (grouped.reindex(pop_layout.ct) + .fillna(grouped.mean()).set_index(pop_layout.index)) + + for build_year in grouped.columns: df = n.links.loc[ice_i] df = df[df.lifetime + build_year > baseyear] if df.empty: continue + share = grouped[build_year] df["build_year"] = build_year - df["p_nom"] = share * p_nom + df["p_nom"] = p_nom.mul(share.values) df["p_nom_extendable"] = False df.rename( index=lambda x: x.replace(f"-{baseyear}", f"-{build_year}"), inplace=True