diff --git a/workflow/rules/preprocess.smk b/workflow/rules/preprocess.smk index c1512c7b..545e1071 100644 --- a/workflow/rules/preprocess.smk +++ b/workflow/rules/preprocess.smk @@ -104,7 +104,6 @@ max_capacity_files = [ 'TotalAnnualMaxCapacity', 'TotalTechnologyAnnualActivityUpperLimit', 'AccumulatedAnnualDemand', -# 'TotalTechnologyModelPeriodActivityUpperLimit' ] user_capacity_files = [ @@ -176,16 +175,31 @@ rule powerplant_var_costs: output: var_costs = 'results/data/powerplant/VariableCost.csv' log: - log = 'results/logs/powerplant.log' + log = 'results/logs/powerplant_var_cost.log' script: "../scripts/osemosys_global/powerplant/variable_costs.py" +rule fuel_limits: + message: + "Generating mining fuel limits..." + input: + region_csv = "results/data/REGION.csv", + technology_csv = "results/data/powerplant/TECHNOLOGY.csv", + fuel_limit_csv = "resources/data/fuel_limits.csv", + output: + model_period_limit_csv = 'results/data/powerplant/ModelPeriodActivityUpperLimit.csv' + log: + log = 'results/logs/powerplant_fuel_limits.log' + script: + "../scripts/osemosys_global/powerplant/fuel_limits.py" + rule transmission: message: "Generating transmission data..." input: rules.powerplant.output.csv_files, - rules.powerplant_var_costs.output, + 'results/data/powerplant/VariableCost.csv', + 'results/data/powerplant/ModelPeriodActivityUpperLimit.csv', default_op_life = 'resources/data/operational_life.csv', gtd_existing = 'resources/data/GTD_existing.csv', gtd_planned = 'resources/data/GTD_planned.csv', diff --git a/workflow/scripts/osemosys_global/max_capacity.py b/workflow/scripts/osemosys_global/max_capacity.py index 554f3181..6da3fa40 100644 --- a/workflow/scripts/osemosys_global/max_capacity.py +++ b/workflow/scripts/osemosys_global/max_capacity.py @@ -272,25 +272,6 @@ def apply_fuel_limits(region, years, output_data_dir, input_dir, max_fuel): index=None, ) - # Model Period Activity Upper Limit for 'MINCOA***01' - min_tech_df = pd.read_csv(os.path.join(output_data_dir, "TECHNOLOGY.csv")) - min_tech = [ - x - for x in min_tech_df["VALUE"].unique() - if x.startswith("MINCOA") - if x.endswith("01") - ] - min_tech_df_final = pd.DataFrame(columns=["REGION", "TECHNOLOGY", "VALUE"]) - min_tech_df_final["TECHNOLOGY"] = min_tech - min_tech_df_final["REGION"] = region - min_tech_df_final["VALUE"] = 0 - min_tech_df_final.to_csv( - os.path.join( - output_data_dir, "TotalTechnologyModelPeriodActivityUpperLimit.csv" - ), - index=None, - ) - def apply_calibration(region, years, output_data_dir, calibration): diff --git a/workflow/scripts/osemosys_global/powerplant/fuel_limits.py b/workflow/scripts/osemosys_global/powerplant/fuel_limits.py new file mode 100644 index 00000000..0e790732 --- /dev/null +++ b/workflow/scripts/osemosys_global/powerplant/fuel_limits.py @@ -0,0 +1,97 @@ +"""Applies fuel limits to mining technologies""" + +import pandas as pd +from typing import Optional + + +def import_fuel_limits(f: str) -> pd.DataFrame: + """Imports contry level fuel limits. + + fuel_limits.csv + """ + return pd.read_csv(f) + + +def import_set(f: str) -> pd.Series: + s = pd.read_csv(f).squeeze() + if isinstance(s, pd.Series): + return s + else: + # single element series + return pd.Series(s) + + +def get_template_fuel_limit(regions: pd.Series, techs: pd.Series) -> pd.Series: + + r = regions.unique().tolist() + t = ( + techs[(techs.str.startswith("MIN")) & ~(techs.str.endswith("INT"))] + .unique() + .tolist() + ) + + idx = pd.MultiIndex.from_product([r, t], names=["REGION", "TECHNOLOGY"]) + s = pd.Series(index=idx).fillna(0) + s.name = "VALUE" + + return s + + +def get_user_fuel_limits( + regions: pd.Series, fuel_limits: Optional[pd.DataFrame] = None +) -> pd.Series: + + r = regions.unique().tolist() + assert len(r) == 1 + r = r[0] + + if fuel_limits.empty: + s = pd.Series(index=["REGION", "TECHNOLOGY"]) + s.name = "VALUE" + return s + + limits = fuel_limits.copy() + limits = limits[limits.VALUE > 0.001].copy() # assume these are zero + + # not sure what the year column represents? + limits = limits.drop(columns="YEAR").drop_duplicates( + subset=["FUEL", "COUNTRY"], keep="last" + ) + + limits["TECHNOLOGY"] = "MIN" + limits.FUEL + limits.COUNTRY + + s = limits[["TECHNOLOGY", "VALUE"]].copy() + s["REGION"] = r + + return s.set_index(["REGION", "TECHNOLOGY"]).squeeze() + + +def merge_template_user_limits( + template: pd.Series, user_limits: pd.Series +) -> pd.Series: + return user_limits.combine_first(template) + + +if __name__ == "__main__": + + if "snakemake" in globals(): + technology_csv = snakemake.input.technology_csv + fuel_limit_csv = snakemake.input.fuel_limit_csv + region_csv = snakemake.input.region_csv + model_period_limit_csv = snakemake.output.model_period_limit_csv + else: + technology_csv = "results/India/data/TECHNOLOGY.csv" + fuel_limit_csv = "resources/data/fuel_limits.csv" + region_csv = "results/India/data/REGION.csv" + model_period_limit_csv = "" + + regions = import_set(region_csv) + techs = import_set(technology_csv) + fuel_limits = import_fuel_limits(fuel_limit_csv) + + template = get_template_fuel_limit(regions, techs) + user_limits = get_user_fuel_limits(regions, fuel_limits) + + model_period_limit = merge_template_user_limits(template, user_limits) + + model_period_limit.to_csv(model_period_limit_csv, index=True) diff --git a/workflow/scripts/osemosys_global/powerplant/read.py b/workflow/scripts/osemosys_global/powerplant/read.py index 4dce2c6a..20abd3e0 100644 --- a/workflow/scripts/osemosys_global/powerplant/read.py +++ b/workflow/scripts/osemosys_global/powerplant/read.py @@ -86,13 +86,6 @@ def import_fuel_prices(f: str) -> pd.DataFrame: """ return pd.read_csv(f) -def import_fuel_limits(f: str) -> pd.DataFrame: - """Imports contry level fuel limits. - - fuel_limits.csv - """ - return pd.read_csv(f) - def import_set(f: str) -> pd.Series: s = pd.read_csv(f).squeeze() if isinstance(s, pd.Series): diff --git a/workflow/scripts/osemosys_global/powerplant/variable_costs.py b/workflow/scripts/osemosys_global/powerplant/variable_costs.py index 87d6c028..d3bd5436 100644 --- a/workflow/scripts/osemosys_global/powerplant/variable_costs.py +++ b/workflow/scripts/osemosys_global/powerplant/variable_costs.py @@ -11,7 +11,6 @@ import_cmo_forecasts, import_fuel_prices, import_set, - import_fuel_limits, ) # can not use constant file defintions due to waste being tagged incorrectly @@ -126,7 +125,7 @@ def expand_cmo_data( df_international = df_international.mul(international_cost_multiplier) df_country = df_template.copy().droplevel("COUNTRY") - df_country["COUNTRY"] = countries * len(df_country) + df_country["COUNTRY"] = [countries] * len(df_country) df_country = df_country.explode("COUNTRY") df_country = df_country.reset_index().set_index( ["FUEL", "COUNTRY", "UNIT", "ENERGY_CONTENT"] @@ -273,33 +272,6 @@ def filter_var_cost_technologies( return df[df.index.get_level_values("TECHNOLOGY").isin(available_techs)].copy() -def assign_international_limits( - var_costs: pd.DataFrame, fuel_limits: pd.DataFrame -) -> pd.DataFrame: - """Checks if a mining tech can contribute to international markets - - If a country has a fuel limit, the country can contribute to international markets. If the - country does not have a fuel limit, the international variable cost is set to a very high value. - """ - - df = var_costs.copy() - - cost = 999999 - - # no international trading - if fuel_limits.empty: - df["VALUE"] = cost - return df - - limits = fuel_limits.copy() - limits["name"] = "MIN" + limits.FUEL + limits.COUNTRY - limited_techs = limits.name.unique().tolist() - - df.loc[df.TECHNOLOGY.isin(limited_techs), "VALUE"] = cost - - return df - - def get_mining_data( costs: pd.DataFrame, mining_fuels: list[str], region: str ) -> pd.DataFrame: @@ -380,7 +352,6 @@ def main( nuclear_costs: int | float, waste_costs: int | float, international_cost_factor: Optional[int | float] = None, - fuel_limits: Optional[pd.DataFrame] = None, ) -> pd.DataFrame: """Creates variable cost data""" @@ -422,9 +393,6 @@ def main( mining_fuels = MINING_FUELS renewable_fuels = RENEWABLE_FUELS - if not isinstance(fuel_limits, pd.DataFrame): - fuel_limits = pd.DataFrame() # no international trading - # process cost data df = apply_energy_content(df) df = expand_merged_data(df, years) @@ -446,7 +414,6 @@ def main( file_regions = snakemake.input.regions file_years = snakemake.input.years file_technologies = snakemake.input.technologies - file_fuel_limits = snakemake.input.fuel_limits file_var_costs = snakemake.output.var_costs else: file_cmo_forecasts = "resources/data/CMO-October-2024-Forecasts.xlsx" @@ -454,12 +421,10 @@ def main( file_regions = "results/India/data/REGION.csv" file_years = "results/India/data/YEAR.csv" file_technologies = "results/India/data/TECHNOLOGY.csv" - file_fuel_limits = "resources/data/fuel_limits.csv" file_var_costs = "results/India/data/VariableCosts.csv" cmo_forecasts = import_cmo_forecasts(file_cmo_forecasts) user_fuel_prices = import_fuel_prices(file_fuel_prices) - user_fuel_limits = import_fuel_limits(file_fuel_limits) technologies = import_set(file_technologies) years = import_set(file_years) regions = import_set(file_regions) @@ -475,7 +440,6 @@ def main( "nuclear_costs": NUCLEAR_VAR_COSTS, "waste_costs": WASTE_VAR_COSTS, "international_cost_factor": INT_COST_FACTOR, - "fuel_limits": user_fuel_limits, } df = main(**input_data) diff --git a/workflow/scripts/osemosys_global/transmission/main.py b/workflow/scripts/osemosys_global/transmission/main.py index 2297adcc..02a8db23 100644 --- a/workflow/scripts/osemosys_global/transmission/main.py +++ b/workflow/scripts/osemosys_global/transmission/main.py @@ -18,7 +18,8 @@ import_max_cap_invest_base, import_min_cap_invest_base, import_res_cap_base, - import_set_base + import_set_base, + import_period_limit ) from constants import( @@ -75,7 +76,8 @@ def main( min_cap_invest_base: pd.DataFrame, res_cap_base: pd.DataFrame, tech_set_base: pd.DataFrame, - fuel_set_base: pd.DataFrame + fuel_set_base: pd.DataFrame, + model_period_limit: pd.DataFrame ): # CALL FUNCTIONS @@ -110,6 +112,10 @@ def main( # Adjust activity limits if cross border trade is not allowed following user config. activity_limit_trn = activity_transmission_limit(cross_border_trade, oar_trn) + if activity_limit_trn.empty: + activity_limit = model_period_limit.copy() + else: + activity_limit = pd.concat([activity_limit_trn, model_period_limit]) # Set operational life for transmission. op_life_trn = set_op_life_transmission(oar_trn, default_op_life, op_life_base, region_name) @@ -180,7 +186,7 @@ def main( iar_trn.to_csv(os.path.join(transmission_data_dir, "InputActivityRatio.csv"), index=None) - activity_limit_trn.to_csv(os.path.join(output_data_dir, + activity_limit.to_csv(os.path.join(output_data_dir, "TotalTechnologyModelPeriodActivityUpperLimit.csv"), index = None) @@ -246,6 +252,7 @@ def main( file_res_cap_base = f'{powerplant_data_dir}/ResidualCapacity.csv' file_tech_set = f'{powerplant_data_dir}/TECHNOLOGY.csv' file_fuel_set = f'{powerplant_data_dir}/FUEL.csv' + file_model_period_activity = f'{powerplant_data_dir}/ModelPeriodActivityUpperLimit.csv' # The below else statement defines variables if the 'transmission/main' script is to be run locally # outside the snakemake workflow. This is relevant for testing purposes only! User inputs when running @@ -290,6 +297,7 @@ def main( file_res_cap_base = f'{powerplant_data_dir}/ResidualCapacity.csv' file_tech_set = f'{powerplant_data_dir}/TECHNOLOGY.csv' file_fuel_set = f'{powerplant_data_dir}/FUEL.csv' + file_model_period_activity = f'{powerplant_data_dir}/ModelPeriodActivityUpperLimit.csv' # SET INPUT DATA gtd_exist = format_gtd_existing(import_gtd_existing(file_gtd_existing)) @@ -319,7 +327,8 @@ def main( min_cap_invest_base = import_min_cap_invest_base(file_min_cap_invest_base) res_cap_base = import_res_cap_base(file_res_cap_base) tech_set_base = import_set_base(file_tech_set) - fuel_set_base = import_set_base(file_fuel_set) + fuel_set_base = import_set_base(file_fuel_set) + model_period_limit = import_period_limit(file_model_period_activity) input_data = { "default_op_life": op_life_dict, @@ -340,6 +349,7 @@ def main( "res_cap_base" : res_cap_base, "tech_set_base" : tech_set_base, "fuel_set_base" : fuel_set_base, + "model_period_limit" : model_period_limit } # CALL MAIN diff --git a/workflow/scripts/osemosys_global/transmission/read.py b/workflow/scripts/osemosys_global/transmission/read.py index 0d64e6be..1da879f3 100644 --- a/workflow/scripts/osemosys_global/transmission/read.py +++ b/workflow/scripts/osemosys_global/transmission/read.py @@ -103,6 +103,13 @@ def import_res_cap_base(f: str) -> pd.DataFrame: """ return pd.read_csv(f) +def import_period_limit(f: str) -> pd.DataFrame: + """Imports ModelPeriodActivityUpperLimit.csv as output from the Powerplant rule. + + ModelPeriodActivityUpperLimit.csv + """ + return pd.read_csv(f) + def import_set_base(f: str) -> pd.DataFrame: """Imports a set csv""" return pd.read_csv(f) \ No newline at end of file