From 8c196a7a21d25c41b4b2fffc1e344017ddd4c421 Mon Sep 17 00:00:00 2001 From: Tom Brown Date: Fri, 14 Aug 2020 09:11:19 +0200 Subject: [PATCH] Take care of CHPs when doing p_nom < threshold & extra_functionality - add_brownfield.py: Have to make sure that for each CHP there is both a heat and electric link, but they have different p_nom for each CHP, so have to make sure we don't remove one without the other. - solve_network.py: Make sure extra_functionality constraints for CHP power-heat feasibility graph also work for non-extendable CHPs. --- Snakefile | 0 config.default.yaml | 0 scripts/add_brownfield.py | 7 +++- scripts/prepare_sector_network.py | 26 +++++++------- scripts/solve_network.py | 58 ++++++++++++++++++++++++------- 5 files changed, 64 insertions(+), 27 deletions(-) mode change 100755 => 100644 Snakefile mode change 100755 => 100644 config.default.yaml mode change 100755 => 100644 scripts/prepare_sector_network.py diff --git a/Snakefile b/Snakefile old mode 100755 new mode 100644 diff --git a/config.default.yaml b/config.default.yaml old mode 100755 new mode 100644 diff --git a/scripts/add_brownfield.py b/scripts/add_brownfield.py index 03cef685..6bf3d67e 100755 --- a/scripts/add_brownfield.py +++ b/scripts/add_brownfield.py @@ -58,8 +58,13 @@ def add_brownfield(n, n_p, year): c.df.index[c.df.build_year + c.df.lifetime < year]) #remove assets if their optimized nominal capacity is lower than a threshold + #since CHP heat Link is proportional to CHP electric Link, make sure threshold is compatible + chp_heat = c.df.index[c.df[attr + "_nom_extendable"] & c.df.index.str.contains("urban central") & c.df.index.str.contains("CHP") & c.df.index.str.contains("heat")] + if not chp_heat.empty: + n_p.mremove(c.name, + chp_heat[c.df.loc[chp_heat, attr + "_nom_opt"] < snakemake.config['existing_capacities']['threshold_capacity']*c.df.efficiency[chp_heat.str.replace("heat","electric")].values*c.df.p_nom_ratio[chp_heat.str.replace("heat","electric")].values/c.df.efficiency[chp_heat].values]) n_p.mremove(c.name, - c.df.index[c.df[attr + "_nom_opt"] < snakemake.config['existing_capacities']['threshold_capacity']]) + c.df.index[c.df[attr + "_nom_extendable"] & ~c.df.index.isin(chp_heat) & (c.df[attr + "_nom_opt"] < snakemake.config['existing_capacities']['threshold_capacity'])]) #copy over assets but fix their capacity c.df[attr + "_nom"] = c.df[attr + "_nom_opt"] diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py old mode 100755 new mode 100644 index 754ee9a8..b4d5e0f5 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -52,11 +52,11 @@ def add_lifetime_wind_solar(n): if carrier in index], 'lifetime']=costs.at[carrier_name,'lifetime'] def update_wind_solar_costs(n,costs): """ - Update costs for wind and solar generators added with pypsa-eur to those + Update costs for wind and solar generators added with pypsa-eur to those cost in the planning year """ - + #assign clustered bus #map initial network -> simplified network busmap_s = pd.read_hdf(snakemake.input.clustermaps, @@ -72,13 +72,13 @@ def update_wind_solar_costs(n,costs): profile = snakemake.input.profile_offwind_dc if tech=='offwind-dc' else snakemake.input.profile_offwind_ac with xr.open_dataset(profile) as ds: underwater_fraction = ds['underwater_fraction'].to_pandas().to_frame() - underwater_fraction["cluster_bus"] = underwater_fraction.index.map(clustermaps) + underwater_fraction["cluster_bus"] = underwater_fraction.index.map(clustermaps) u_f = underwater_fraction.groupby("cluster_bus").mean() - + average_distance = ds['average_distance'].to_pandas().to_frame() - average_distance["cluster_bus"] = average_distance.index.map(clustermaps) + average_distance["cluster_bus"] = average_distance.index.map(clustermaps) a_d = average_distance.groupby("cluster_bus").mean() - + connection_cost = (snakemake.config['costs']['lines']['length_factor'] * a_d* (u_f * @@ -90,16 +90,16 @@ def update_wind_solar_costs(n,costs): connection_cost) logger.info("Added connection cost of {:0.0f}-{:0.0f} Eur/MW/a to {}" .format(connection_cost[0].min(), connection_cost[0].max(), tech)) - capital_cost.rename(index=lambda node: node + ' ' + tech, inplace=True) - n.generators.loc[n.generators.index[n.generators.carrier==tech],'capital_cost']=capital_cost[0] - + capital_cost.rename(index=lambda node: node + ' ' + tech, inplace=True) + n.generators.loc[n.generators.index[n.generators.carrier==tech],'capital_cost']=capital_cost[0] + elif suptech == 'solar': - capital_cost = costs.at['solar-utility', 'fixed'] + capital_cost = costs.at['solar-utility', 'fixed'] n.generators.loc[n.generators.index[n.generators.carrier==tech],'capital_cost']=capital_cost else: - capital_cost = costs.at['onwind', 'fixed'] + capital_cost = costs.at['onwind', 'fixed'] n.generators.loc[n.generators.index[n.generators.carrier==tech],'capital_cost']=capital_cost - + def add_carrier_buses(n, carriers): """ Add buses to connect e.g. coal, nuclear and oil plants @@ -1683,7 +1683,7 @@ if __name__ == "__main__": clustered_pop_layout='pypsa-eur-sec/resources/pop_layout_{network}_s{simpl}_{clusters}.csv', costs='pypsa-eur-sec/data/costs/costs_{planning_horizons}.csv', profile_offwind_ac='pypsa-eur/resources/profile_offwind-ac.nc', - profile_offwind_dc='pypsa-eur/resources/profile_offwind-dc.nc', + profile_offwind_dc='pypsa-eur/resources/profile_offwind-dc.nc', clustermaps="pypsa-eur/resources/clustermaps_{network}_s{simpl}_{clusters}.h5", cop_air_total='pypsa-eur-sec/resources/cop_air_total_{network}_s{simpl}_{clusters}.nc', cop_soil_total='pypsa-eur-sec/resources/cop_soil_total_{network}_s{simpl}_{clusters}.nc', diff --git a/scripts/solve_network.py b/scripts/solve_network.py index af35a8a7..b1e3fb11 100755 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -123,21 +123,36 @@ def add_battery_constraints(n): def add_chp_constraints(n): - electric = n.links.index[n.links.index.str.contains("urban central") & n.links.index.str.contains("CHP") & n.links.index.str.contains("electric")] - heat = n.links.index[n.links.index.str.contains("urban central") & n.links.index.str.contains("CHP") & n.links.index.str.contains("heat")] + electric_bool = (n.links.index.str.contains("urban central") + & n.links.index.str.contains("CHP") + & n.links.index.str.contains("electric")) + heat_bool = (n.links.index.str.contains("urban central") + & n.links.index.str.contains("CHP") + & n.links.index.str.contains("heat")) - if not electric.empty: + electric = n.links.index[electric_bool] + heat = n.links.index[heat_bool] + electric_ext = n.links.index[electric_bool & n.links.p_nom_extendable] + heat_ext = n.links.index[heat_bool & n.links.p_nom_extendable] + electric_fix = n.links.index[electric_bool & ~n.links.p_nom_extendable] + heat_fix = n.links.index[heat_bool & ~n.links.p_nom_extendable] + + + if not electric_ext.empty: link_p_nom = get_var(n, "Link", "p_nom") #ratio of output heat to electricity set by p_nom_ratio - lhs = linexpr((n.links.loc[electric,"efficiency"] - *n.links.loc[electric,'p_nom_ratio'], - link_p_nom[electric]), - (-n.links.loc[heat,"efficiency"].values, - link_p_nom[heat].values)) + lhs = linexpr((n.links.loc[electric_ext,"efficiency"] + *n.links.loc[electric_ext,'p_nom_ratio'], + link_p_nom[electric_ext]), + (-n.links.loc[heat_ext,"efficiency"].values, + link_p_nom[heat_ext].values)) define_constraints(n, lhs, "=", 0, 'chplink', 'fix_p_nom_ratio') + + if not electric.empty: + link_p = get_var(n, "Link", "p") #backpressure @@ -149,12 +164,29 @@ def add_chp_constraints(n): define_constraints(n, lhs, "<=", 0, 'chplink', 'backpressure') - #top_iso_fuel_line - lhs = linexpr((1,link_p[heat]), - (1,link_p[electric].values), - (-1,link_p_nom[electric].values)) - define_constraints(n, lhs, "<=", 0, 'chplink', 'top_iso_fuel_line') + if not electric_ext.empty: + + link_p_nom = get_var(n, "Link", "p_nom") + link_p = get_var(n, "Link", "p") + + #top_iso_fuel_line for extendable + lhs = linexpr((1,link_p[heat_ext]), + (1,link_p[electric_ext].values), + (-1,link_p_nom[electric_ext].values)) + + define_constraints(n, lhs, "<=", 0, 'chplink', 'top_iso_fuel_line_ext') + + + if not electric_fix.empty: + + link_p = get_var(n, "Link", "p") + + #top_iso_fuel_line for fixed + lhs = linexpr((1,link_p[heat_fix]), + (1,link_p[electric_fix].values)) + + define_constraints(n, lhs, "<=", n.links.loc[electric_fix,"p_nom"].values, 'chplink', 'top_iso_fuel_line_fix') def add_land_use_constraint(n): for carrier in ['solar', 'onwind', 'offwind-ac', 'offwind-dc']: