Source code for powersimdata.design.generation.curtailment

import pandas as pd

from powersimdata.scenario.scenario import Scenario


[docs]def temporal_curtailment( scenario, pmin_by_type=None, pmin_by_id=None, curtailable=None, ): """Calculate the minimum share of potential renewable energy that will be curtailed due to supply/demand mismatch, assuming no storage is present. :param powersimdata.scenario.scenario.Scenario scenario: scenario instance. :param dict/pandas.Series pmin_by_type: Mapping of types to Pmin assumptions. Values between 0 and 1 (inclusive) are treated as shares of Pmax, and None values either maintain given Pmin values (for dispatchable resources) or track profiles (for profile resources, e.g. hydro). :param dict/pandas.Series pmin_by_id: Mapping of IDs to Pmin assumptions, as an override to the default behavior for that plant type. Values between 0 and 1 (inclusive) are treated as shares of Pmax, and None values either maintain given Pmin values (for dispatchable resources) or track profiles (for profile resources, e.g. hydro). :param iterable curtailable: resource types which can be curtailed. :return: (*float*) -- share of curtailable resources that will be curtailed. :raises TypeError: if inputs do not mach specified type. :raises ValueError: if any entries in curtailable or keys in pmin_by_type are not types in the grid, any keys in pmin_by_id are not plant IDs in the Grid, or any values in pmin_by_type/pmin_by_id are not in the range [0, 1] or None. """ if not isinstance(scenario, Scenario): raise TypeError("scenario must be a Scenario") if pmin_by_type is None: pmin_by_type = {"hydro": None} if pmin_by_id is None: pmin_by_id = {} if curtailable is None: curtailable = {"solar", "wind"} check_dicts = {"pmin_by_id": pmin_by_id, "pmin_by_type": pmin_by_type} for name, d in check_dicts.items(): if not isinstance(d, (dict, pd.Series)): raise TypeError(f"{name} must be a dict or pandas Series") # Access values via appropriate method whether d is a dict or a pandas Series values = d.values() if isinstance(d, dict) else d.values if not all([v is None or 0 <= v <= 1 for v in values]): err_msg = f"all entries in {name} must be None or in the range [0, 1]" raise ValueError(err_msg) grid = scenario.get_grid() plant = grid.plant valid_types = plant["type"].unique() if not set(pmin_by_type.keys()) <= set(valid_types): raise ValueError("Got invalid plant type as a key to pmin_by_type") if not set(pmin_by_id.keys()) <= set(plant.index): raise ValueError("Got invalid plant id as a key to pmin_by_id") try: if not set(curtailable) <= set(valid_types): raise ValueError("Got invalid plant type within curtailable") except TypeError: raise TypeError("curtailable must be an iterable") # Get profiles, filter out plant-level overrides, then sum all_profiles = pd.concat( [ scenario.get_profile(k) for k in grid.model_immutables.plants["group_profile_resources"] ], axis=1, ) plant_id_mask = ~plant.index.isin(pmin_by_id.keys()) base_plant_ids_by_type = plant.loc[plant_id_mask].groupby("type").groups valid_profile_types = ( set(base_plant_ids_by_type) & grid.model_immutables.plants["profile_resources"] ) plant_ids_for_summed_profiles = set().union( *[set(base_plant_ids_by_type[g]) for g in valid_profile_types] ) summed_profiles = ( all_profiles[list(plant_ids_for_summed_profiles)] .groupby(plant["type"], axis=1) .sum() ) # Build up a series of firm generation summed_demand = scenario.get_demand().sum(axis=1) firm_generation = pd.Series(0, index=summed_demand.index) # Add plants without plant-level overrides ('base' plants) pmin_dict = { **grid.model_immutables.plants["pmin_as_share_of_pmax"], **pmin_by_type, } # Don't iterate over plant types not present in this grid pmin_dict = {k: pmin_dict[k] for k in base_plant_ids_by_type} for resource, pmin in pmin_dict.items(): if (resource in curtailable) or (pmin == 0): continue if pmin is None: if resource in grid.model_immutables.plants["profile_resources"]: firm_generation += summed_profiles[resource] else: summed_pmin = plant.Pmin.loc[base_plant_ids_by_type[resource]].sum() firm_generation += pd.Series(summed_pmin, index=summed_demand.index) else: summed_pmin = pmin * plant.Pmax.loc[base_plant_ids_by_type[resource]].sum() firm_generation += pd.Series(summed_pmin, index=summed_demand.index) # Add plants with plant-level overrides for plant_id, pmin in pmin_by_id.items(): if pmin == 0: continue if pmin is None: if ( plant.loc[plant_id, "type"] in grid.model_immutables.plants["profile_resources"] ): firm_generation += all_profiles[plant_id] else: plant_pmin = plant.loc[plant_id, "Pmin"] firm_generation += pd.Series(plant_pmin, index=summed_demand.index) else: plant_pmin = plant.loc[plant_id, "Pmax"] * pmin firm_generation += pd.Series(plant_pmin, index=summed_demand.index) # Finally, compare this summed firm generation against summed curtailable generation total_curtailable = summed_profiles[list(curtailable)].sum(axis=1) net_demand = summed_demand - firm_generation curtailable_max_gen = pd.concat([net_demand, total_curtailable], axis=1).min(axis=1) curtailment_fraction = 1 - curtailable_max_gen.sum() / total_curtailable.sum() return curtailment_fraction