diff --git a/config/config.default.yaml b/config/config.default.yaml index a2fd46ce..c96ccb42 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -254,6 +254,8 @@ renewable: clip_min_inflow: 1.0 conventional: + unit_commitment: false + dynamic_fuel_price: false nuclear: p_max_pu: "data/nuclear_p_max_pu.csv" # float of file name @@ -345,8 +347,6 @@ solar_thermal: # only relevant for foresight = myopic or perfect existing_capacities: - unit_commitment: true # if unit commitment (UC) for conventional power plants is used - # UC is only applied to extendable plants if linearized UC is used grouping_years_power: [1980, 1985, 1990, 1995, 2000, 2005, 2010, 2015, 2020, 2025, 2030] grouping_years_heat: [1980, 1985, 1990, 1995, 2000, 2005, 2010, 2015, 2019] # these should not extend 2020 threshold_capacity: 10 diff --git a/config/config.validation.yaml b/config/config.validation.yaml index afb53d37..b8bac52e 100644 --- a/config/config.validation.yaml +++ b/config/config.validation.yaml @@ -243,6 +243,8 @@ renewable: clip_min_inflow: 1.0 conventional: + unit_commitment: true + dynamic_fuel_price: true nuclear: p_max_pu: "data/nuclear_p_max_pu.csv" # float of file name diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk index c5f9ecf3..386a4202 100644 --- a/rules/build_electricity.smk +++ b/rules/build_electricity.smk @@ -296,7 +296,7 @@ rule add_electricity: countries=config["countries"], renewable=config["renewable"], electricity=config["electricity"], - conventional=config.get("conventional", {}), + conventional=config["conventional"], costs=config["costs"], input: **{ @@ -306,6 +306,7 @@ rule add_electricity: **{ f"conventional_{carrier}_{attr}": fn for carrier, d in config.get("conventional", {None: {}}).items() + if carrier in config["electricity"]["conventional_carriers"] for attr, fn in d.items() if str(fn).startswith("data/") }, @@ -315,6 +316,7 @@ rule add_electricity: powerplants=RESOURCES + "powerplants.csv", hydro_capacities=ancient("data/bundle/hydro_capacities.csv"), geth_hydro_capacities="data/geth2015_hydro_capacities.csv", + unit_commitment="data/unit_commitment.csv", fuel_price="data/validation/monthly_fuel_price.csv", load=RESOURCES + "load.csv", nuts3_shapes=RESOURCES + "nuts3_shapes.geojson", diff --git a/rules/solve_electricity.smk b/rules/solve_electricity.smk index fbfd1386..f73a5b0c 100644 --- a/rules/solve_electricity.smk +++ b/rules/solve_electricity.smk @@ -12,7 +12,6 @@ rule solve_network: "co2_sequestration_potential", 200 ), input: - unit_commitment_params="data/unit_commitment.csv", network=RESOURCES + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", output: network=RESULTS + "networks/elec_s{simpl}_{clusters}_ec_l{ll}_{opts}.nc", diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py index e6e2c4e3..43e148af 100755 --- a/scripts/add_electricity.py +++ b/scripts/add_electricity.py @@ -366,12 +366,13 @@ def attach_wind_and_solar( def attach_conventional_generators( n, costs, - fuel_price, ppl, conventional_carriers, extendable_carriers, conventional_params, conventional_inputs, + unit_commitment=None, + fuel_price=None, ): carriers = set(conventional_carriers) | set(extendable_carriers["Generator"]) _add_missing_carriers_from_costs(n, costs, carriers) @@ -383,19 +384,30 @@ def attach_conventional_generators( ) ppl["efficiency"] = ppl.efficiency.fillna(ppl.efficiency_r) - fuel_price = fuel_price.assign(OCGT=fuel_price["gas"], CCGT=fuel_price["gas"]).drop( - "gas", axis=1 - ) - fuel_price = fuel_price.reindex(ppl.carrier, axis=1) - fuel_price.fillna(costs.fuel, inplace=True) - fuel_price.columns = ppl.index - marginal_cost = (fuel_price.div(ppl.efficiency)).add(ppl.carrier.map(costs.VOM)) + if unit_commitment is not None: + committable_attrs = ppl.carrier.isin(unit_commitment).to_frame('committable') + for attr in unit_commitment.index: + default = pypsa.components.component_attrs['Generator'].default[attr] + committable_attrs[attr] = ppl.carrier.map(unit_commitment.loc[attr]).fillna(default) + else: + committable_attrs = {} - logger.info( - "Adding {} generators with capacities [GW] \n{}".format( - len(ppl), ppl.groupby("carrier").p_nom.sum().div(1e3).round(2) - ) - ) + + if fuel_price is not None: + fuel_price = (fuel_price.assign(OCGT=fuel_price['gas'], + CCGT=fuel_price['gas']) + .drop("gas", axis=1)) + missing_carriers = list(carriers - set(fuel_price)) + fuel_price = fuel_price.assign(**costs.fuel[missing_carriers]) + fuel_price = fuel_price.reindex(ppl.carrier, axis=1) + fuel_price.columns = ppl.index + marginal_cost = fuel_price.div(ppl.efficiency).add(ppl.carrier.map(costs.VOM)) + else: + marginal_cost = ppl.carrier.map(costs.VOM) + ppl.carrier.map(costs.fuel) / ppl.efficiency + + # Define generators using modified ppl DataFrame + caps = ppl.groupby("carrier").p_nom.sum().div(1e3).round(2) + logger.info(f"Adding {len(ppl)} generators with capacities [GW] \n{caps}") n.madd( "Generator", @@ -410,9 +422,10 @@ def attach_conventional_generators( capital_cost=ppl.capital_cost, build_year=ppl.datein.fillna(0).astype(int), lifetime=(ppl.dateout - ppl.datein).fillna(np.inf), + **committable_attrs ) - for carrier in conventional_params: + for carrier in set(conventional_params) & carriers: # Generators with technology affected idx = n.generators.query("carrier == @carrier").index @@ -752,18 +765,28 @@ if __name__ == "__main__": k: v for k, v in snakemake.input.items() if k.startswith("conventional_") } - m_fuel_price = pd.read_csv(snakemake.input.fuel_price, index_col=[0], header=[0]) - m_fuel_price.index = pd.date_range(start="2019-01-01", end="2019-12-01", freq="MS") - fuel_price = m_fuel_price.reindex(n.snapshots).fillna(method="ffill") + if params.conventional["unit_commitment"]: + unit_commitment = pd.read_csv(snakemake.input.unit_commitment, index_col=0) + else: + unit_commitment = None + + if params.conventional["dynamic_fuel_price"]: + monthly_fuel_price = pd.read_csv(snakemake.input.fuel_price, index_col=0, header=0) + monthly_fuel_price.index = pd.date_range(start=n.snapshots[0], end=n.snapshots[-1], freq='MS') + fuel_price = monthly_fuel_price.reindex(n.snapshots).fillna(method="ffill") + else: + fuel_price = None + attach_conventional_generators( n, costs, - fuel_price, ppl, conventional_carriers, extendable_carriers, params.conventional, conventional_inputs, + unit_commitment=unit_commitment, + fuel_price=fuel_price, ) attach_wind_and_solar( diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 8f1b57b8..94cb6056 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -149,8 +149,6 @@ def prepare_network( planning_horizons=None, co2_sequestration_potential=None, ): - if snakemake.config["existing_capacities"]["unit_commitment"]: - add_unit_commitment(n, snakemake.input.unit_commitment_params) if "clip_p_max_pu" in solve_opts: for df in ( @@ -600,19 +598,6 @@ def extra_functionality(n, snapshots): add_pipe_retrofit_constraint(n) -def add_unit_commitment(n, fn): - """ - Add unit commitment. - """ - c = "Generator" - uc_data = pd.read_csv(fn, index_col=0) - n.df(c).loc[n.df(c).carrier.isin(uc_data.columns), "committable"] = True - for attr in uc_data.index: - n.df(c)[attr].update(n.df(c)["carrier"].map(uc_data.loc[attr]).dropna()) - gen_i = n.df(c).query("carrier in @uc_data.columns").index - n.df(c).loc[gen_i, "committable"] = True - - def solve_network(n, config, solving, opts="", **kwargs): set_of_options = solving["solver"]["options"] solver_options = solving["solver_options"][set_of_options] if set_of_options else {}