diff --git a/powersimdata/design/generation/clean_capacity_scaling.py b/powersimdata/design/generation/clean_capacity_scaling.py index 0a3e629f1..feb0ddb4b 100644 --- a/powersimdata/design/generation/clean_capacity_scaling.py +++ b/powersimdata/design/generation/clean_capacity_scaling.py @@ -156,10 +156,10 @@ def add_resource_data_to_targets(input_targets, scenario, calculate_curtailment= :param pandas.DataFrame input_targets: table includeing target names, used to summarize resource data. :param powersimdata.scenario.scenario.Scenario scenario: A Scenario instance. - :return: (*pandas.DataFrame*) -- DataFrame of targets including resource data. + :return: (*pandas.DataFrame*) -- data frame of targets including resource data. """ targets = input_targets.copy() - grid = scenario.state.get_grid() + grid = scenario.get_grid() plant = grid.plant curtailment_types = ["hydro", "solar", "wind"] scenario_length = _get_scenario_length(scenario) @@ -174,23 +174,33 @@ def add_resource_data_to_targets(input_targets, scenario, calculate_curtailment= capacity_groupby = plant.Pmax.groupby(groupby_cols) capacity_by_target_type = capacity_groupby.sum().unstack(fill_value=0) # Generated energy - pg_groupby = scenario.state.get_pg().sum().groupby(groupby_cols) + pg_groupby = scenario.get_pg().sum().groupby(groupby_cols) summed_generation = pg_groupby.sum().unstack(fill_value=0) # Calculate capacity factors possible_energy = scenario_length * capacity_by_target_type[curtailment_types] capacity_factor = summed_generation[curtailment_types] / possible_energy + + # To be generalized if calculate_curtailment: # Calculate: curtailment, no_curtailment_cap_factor # Hydro and solar are straightforward - hydro_plant_sum = scenario.state.get_hydro().sum() - hydro_plant_targets = plant[plant.type == "hydro"].target_area + hydro_plant_sum = scenario.get_profile("hydro").sum() + hydro_plant_targets = plant[ + plant["type"].isin( + grid.model_immutables.plants["group_profile_resources"]["hydro"] + ) + ]["target_area"] hydro_potential_by_target = hydro_plant_sum.groupby(hydro_plant_targets).sum() - solar_plant_sum = scenario.state.get_solar().sum() - solar_plant_targets = plant[plant.type == "solar"].target_area + solar_plant_sum = scenario.get_profile("solar").sum() + solar_plant_targets = plant[ + plant["type"].isin( + grid.model_immutables.plants["group_profile_resources"]["solar"] + ) + ]["target_area"] solar_potential_by_target = solar_plant_sum.groupby(solar_plant_targets).sum() # Wind is a little tricker because get_wind() returns 'wind' and 'wind_offshore' onshore_wind_plants = plant[plant.type == "wind"].index - onshore_wind_plant_sum = scenario.state.get_wind().sum()[onshore_wind_plants] + onshore_wind_plant_sum = scenario.get_wind().sum()[onshore_wind_plants] wind_plant_targets = plant[plant.type == "wind"].target_area wind_potential_by_target = onshore_wind_plant_sum.groupby( wind_plant_targets @@ -234,7 +244,7 @@ def add_demand_to_targets(input_targets, scenario): zonename2target = _make_zonename2target(grid, targets) zoneid2target = {grid.zone2id[z]: target for z, target in zonename2target.items()} - summed_demand = scenario.state.get_demand().sum().to_frame() + summed_demand = scenario.get_demand().sum().to_frame() summed_demand["target"] = [zoneid2target[id] for id in summed_demand.index] targets["demand"] = summed_demand.groupby("target").sum() return targets @@ -512,7 +522,7 @@ def calculate_clean_capacity_scaling( # Input validation if not isinstance(ref_scenario, Scenario): raise TypeError("ref_scenario must be a Scenario object") - if ref_scenario.state.name != "analyze": + if ref_scenario.name != "analyze": raise ValueError("ref_scenario must be in Analyze state") if method not in allowed_methods: raise ValueError(f"method must be one of: {allowed_methods}") diff --git a/powersimdata/design/generation/curtailment.py b/powersimdata/design/generation/curtailment.py index a620bcf5b..418329a98 100644 --- a/powersimdata/design/generation/curtailment.py +++ b/powersimdata/design/generation/curtailment.py @@ -2,6 +2,7 @@ from powersimdata.scenario.scenario import Scenario +# To be generalized default_pmin_dict = { "coal": None, "dfo": 0, @@ -14,19 +15,13 @@ "wind": 0, "wind_offshore": 0, } -profile_methods = { - "hydro": "get_hydro", - "solar": "get_solar", - "wind": "get_wind", - "wind_offshore": "get_wind", -} def temporal_curtailment( scenario, pmin_by_type=None, pmin_by_id=None, curtailable={"solar", "wind"} ): """Calculate the minimum share of potential renewable energy that will be curtailed - due to supply/demand mismatch, assuming no storage is present. + 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 @@ -60,7 +55,8 @@ def temporal_curtailment( 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) - plant = scenario.state.get_grid().plant + 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") @@ -74,11 +70,17 @@ def temporal_curtailment( # Get profiles, filter out plant-level overrides, then sum all_profiles = pd.concat( - [getattr(scenario.state, m)() for m in set(profile_methods.values())], axis=1 + [ + 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) & set(profile_methods) + 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] ) @@ -89,7 +91,7 @@ def temporal_curtailment( ) # Build up a series of firm generation - summed_demand = scenario.state.get_demand().sum(axis=1) + 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 = {**default_pmin_dict, **pmin_by_type} @@ -99,7 +101,7 @@ def temporal_curtailment( if (resource in curtailable) or (pmin == 0): continue if pmin is None: - if resource in profile_methods: + 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() @@ -112,7 +114,10 @@ def temporal_curtailment( if pmin == 0: continue if pmin is None: - if plant.loc[plant_id, "type"] in profile_methods: + 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"] diff --git a/powersimdata/design/transmission/upgrade.py b/powersimdata/design/transmission/upgrade.py index edd882392..255e10d79 100644 --- a/powersimdata/design/transmission/upgrade.py +++ b/powersimdata/design/transmission/upgrade.py @@ -106,6 +106,7 @@ def scale_renewable_stubs(change_table, fuzz=1, inplace=True, verbose=False): ct["branch"]["branch_id"] = {} branch_id_ct = ct["branch"]["branch_id"] + # To be replaced ren_types = ("hydro", "solar", "wind", "wind_offshore") for r in ren_types: ren_plants = ref_plant[ref_plant["type"] == r] diff --git a/powersimdata/input/change_table.py b/powersimdata/input/change_table.py index 5fab9b42a..75e6bef61 100644 --- a/powersimdata/input/change_table.py +++ b/powersimdata/input/change_table.py @@ -18,31 +18,16 @@ ) from powersimdata.input.transform_grid import TransformGrid -_resources = ( - "coal", - "dfo", - "geothermal", - "ng", - "nuclear", - "hydro", - "solar", - "wind", - "wind_offshore", - "biomass", - "other", -) - class ChangeTable: """Create change table for changes that need to be applied to the original grid as well as to the original demand, hydro, solar and wind profiles. A pickle file enclosing the change table in form of a dictionary can be created and transferred on the server. Keys are *'demand'*, *'branch'*, *'dcline'*, - '*new_branch*', *'new_dcline'*, *'new_plant'*, *'storage'*, - *'[resource]'*, *'[resource]_cost'*, and *'[resource]_pmin'*,; where 'resource' - is one of: {*'biomass'*, *'coal'*, *'dfo'*, *'geothermal'*, *'ng'*, *'nuclear'*, - *'hydro'*, *'solar'*, *'wind'*, *'wind_offshore'*, *'other'*}. - If a key is missing in the dictionary, then no changes will be applied. + '*new_branch*', *'new_dcline'*, *'new_plant'*, *'storage'*, *'[resource]'*, + *'[resource]_cost'*, and *'[resource]_pmin'*,; where 'resource' are defined in + :class:`powersimdata.network.constants.plants.Resource` and depends on the grid + model. If a key is missing in the dictionary, then no changes will be applied. The data structure is given below: * *'demand'*: @@ -149,14 +134,13 @@ def __init__(self, grid): } } - @staticmethod - def _check_resource(resource): + def _check_resource(self, resource): """Checks resource. :param str resource: type of generator. :raises ValueError: if resource cannot be changed. """ - possible = _resources + possible = self.grid.model_immutables.plants["all_resources"] if resource not in possible: print("-----------------------") print("Possible Generator type") @@ -248,7 +232,7 @@ def clear(self, which=None): for key in {"new_plant", "remove_plant"}: if key in self.ct: del self.ct[key] - for r in _resources: + for r in self.grid.model_immutables.plants["all_resources"]: for suffix in {"", "_cost", "_pmin"}: key = r + suffix if key in self.ct: diff --git a/powersimdata/input/changes/plant.py b/powersimdata/input/changes/plant.py index cb0b59d3c..712352ba7 100644 --- a/powersimdata/input/changes/plant.py +++ b/powersimdata/input/changes/plant.py @@ -3,8 +3,6 @@ from powersimdata.input.transform_grid import TransformGrid from powersimdata.utility.distance import find_closest_neighbor -_profile_resource = {"hydro", "solar", "wind", "wind_offshore"} - def add_plant(obj, info): """Sets parameters of new generator(s) in change table. @@ -42,7 +40,7 @@ def add_plant(obj, info): if plant["Pmin"] < 0 or plant["Pmin"] > plant["Pmax"]: err_msg = f"0 <= Pmin <= Pmax must be satisfied for plant #{i + 1}" raise ValueError(err_msg) - if plant["type"] in _profile_resource: + if plant["type"] in obj.grid.model_immutables.plants["profile_resources"]: lon = anticipated_bus.loc[plant["bus_id"]].lon lat = anticipated_bus.loc[plant["bus_id"]].lat plant_same_type = obj.grid.plant.groupby("type").get_group(plant["type"]) diff --git a/powersimdata/input/transform_grid.py b/powersimdata/input/transform_grid.py index 028ed2ab5..ba57bd25d 100644 --- a/powersimdata/input/transform_grid.py +++ b/powersimdata/input/transform_grid.py @@ -17,20 +17,6 @@ def __init__(self, grid, ct): """ self.grid = copy.deepcopy(grid) self.ct = copy.deepcopy(ct) - self.gen_types = [ - "biomass", - "coal", - "dfo", - "geothermal", - "ng", - "nuclear", - "hydro", - "solar", - "wind", - "wind_offshore", - "other", - ] - self.thermal_gen_types = ["coal", "dfo", "geothermal", "ng", "nuclear"] def get_grid(self): """Returns the transformed grid. @@ -44,7 +30,7 @@ def get_grid(self): def _apply_change_table(self): """Apply changes listed in change table to the grid.""" # First scale by zones, so that zone factors are not applied to additions. - for g in self.gen_types: + for g in self.grid.model_immutables.plants["all_resources"]: if g in self.ct.keys(): self._scale_gen_by_zone(g) if f"{g}_cost" in self.ct.keys(): @@ -72,7 +58,7 @@ def _apply_change_table(self): self._add_storage() # Scale by IDs, so that additions can be scaled. - for g in self.gen_types: + for g in self.grid.model_immutables.plants["all_resources"]: if g in self.ct.keys(): self._scale_gen_by_id(g) if f"{g}_cost" in self.ct.keys(): @@ -106,7 +92,7 @@ def _scale_gen_by_zone(self, gen_type): .index.tolist() ) self._scale_gen_capacity(plant_id, factor) - if gen_type in self.thermal_gen_types: + if gen_type in self.grid.model_immutables.plants["thermal_resources"]: self._scale_gencost_by_capacity(plant_id, factor) def _scale_gen_by_id(self, gen_type): @@ -118,7 +104,7 @@ def _scale_gen_by_id(self, gen_type): if "plant_id" in self.ct[gen_type].keys(): for plant_id, factor in self.ct[gen_type]["plant_id"].items(): self._scale_gen_capacity(plant_id, factor) - if gen_type in self.thermal_gen_types: + if gen_type in self.grid.model_immutables.plants["thermal_resources"]: self._scale_gencost_by_capacity(plant_id, factor) def _scale_gencost_by_zone(self, gen_type): @@ -389,7 +375,7 @@ def _add_gencost(self): new_gencost["type"] = 2 new_gencost["n"] = 3 new_gencost["interconnect"] = self.grid.bus.loc[bus_id].interconnect - if entry["type"] in self.thermal_gen_types: + if entry["type"] in self.grid.model_immutables.plants["thermal_resources"]: new_gencost["c0"] = entry["c0"] new_gencost["c1"] = entry["c1"] new_gencost["c2"] = entry["c2"] diff --git a/powersimdata/input/transform_profile.py b/powersimdata/input/transform_profile.py index 74943a503..dc3dbb93b 100644 --- a/powersimdata/input/transform_profile.py +++ b/powersimdata/input/transform_profile.py @@ -25,20 +25,17 @@ def __init__(self, scenario_info, grid, ct, slice=True): self.ct = copy.deepcopy(ct) self.grid = copy.deepcopy(grid) - self.scale_keys = { - "wind": {"wind", "wind_offshore"}, - "solar": {"solar"}, - "hydro": {"hydro"}, - "demand": {"demand"}, - } + self.scale_keys = {"demand": {"demand"}} | self.grid.model_immutables.plants[ + "group_profile_resources" + ] + self.n_new_plant, self.n_new_clean_plant = self._get_number_of_new_plant() def _get_number_of_new_plant(self): """Return the total number of new plant and new plant with profiles. :return: (*tuple*) -- first element is the total number of new plant and second - element is the total number of new clean plant (*hydro*, *solar*, - *onshore wind* and *offshore wind*). + element is the total number of new plant with profile. """ n_plant = [0, 0] if "new_plant" in self.ct.keys(): @@ -51,7 +48,7 @@ def _get_number_of_new_plant(self): def _get_renewable_profile(self, resource): """Return the transformed profile. - :param str resource: *'hydro'*, *'solar'* or *'wind'*. + :param str resource: a genertaor type with profile. :return: (*pandas.DataFrame*) -- power output for generators of specified type with plant identification number as columns and UTC timestamp as indices. """ @@ -188,24 +185,21 @@ def _slice_df(self, df): def get_profile(self, name): """Return profile. - :param str name: either *demand*, *'hydro'*, *'solar'*, *'wind'*, - *'demand_flexibility_up'*, *'demand_flexibility_dn'*, - *'demand_flexibility_cost_up'*, or *'demand_flexibility_cost_dn'*. + :param str name: either *demand*, *'demand_flexibility_up'*, + *'demand_flexibility_dn'*, *'demand_flexibility_cost_up'*, + *'demand_flexibility_cost_dn'* or a generator type with profile. :return: (*pandas.DataFrame*) -- profile. - :raises ValueError: if argument not one of *'demand'*, *'hydro'*, *'solar'*, - *'wind'*, *'demand_flexibility_up'*, *'demand_flexibility_dn'*, - *'demand_flexibility_cost_up'*, or *'demand_flexibility_cost_dn'*. + :raises ValueError: if argument not one of *'demand'*, + *'demand_flexibility_up'*, *'demand_flexibility_dn'*, + *'demand_flexibility_cost_up'*, *'demand_flexibility_cost_dn'* or a generator type wit profile. """ - possible = [ - "demand", - "hydro", - "solar", - "wind", + possible = { "demand_flexibility_up", "demand_flexibility_dn", "demand_flexibility_cost_up", "demand_flexibility_cost_dn", - ] + }.union(*self.scale_keys.values()) + if name not in possible: raise ValueError("Choose from %s" % " | ".join(possible)) elif name == "demand": diff --git a/powersimdata/scenario/ready.py b/powersimdata/scenario/ready.py index 3762da69f..eb16f4666 100644 --- a/powersimdata/scenario/ready.py +++ b/powersimdata/scenario/ready.py @@ -50,6 +50,7 @@ def get_base_grid(self): source=self._scenario_info["grid_model"], ) + # To be refactored to return profiles for different grid models def get_profile(self, kind): """Returns demand, hydro, solar or wind profile. @@ -59,6 +60,7 @@ def get_profile(self, kind): profile = TransformProfile(self._scenario_info, self.get_grid(), self.get_ct()) return profile.get_profile(kind) + # To be generalized def get_hydro(self): """Returns hydro profile @@ -66,6 +68,7 @@ def get_hydro(self): """ return self.get_profile("hydro") + # To be generalized def get_solar(self): """Returns solar profile @@ -73,6 +76,7 @@ def get_solar(self): """ return self.get_profile("solar") + # To be generalized def get_wind(self): """Returns wind profile @@ -80,6 +84,7 @@ def get_wind(self): """ return self.get_profile("wind") + # To be generalized def get_wind_onshore(self): """Returns wind onshore profile @@ -91,6 +96,7 @@ def get_wind_onshore(self): onshore_id = grid.plant.groupby(["type"]).get_group("wind").index return wind[onshore_id] + # To be generalized def get_wind_offshore(self): """Returns wind offshore profile