Source code for powersimdata.input.transform_profile

import copy

from powersimdata.input.profile_input import ProfileInput
from powersimdata.input.transform_demand import TransformDemand


[docs]class TransformProfile: """Transform profile according to operations listed in change table.""" _default_dates = {"start_date": "2016-01-01 00:00", "end_date": "2016-12-31 23:00"} def __init__(self, scenario_info, grid, ct, slice=True): """Constructor. :param dict scenario_info: scenario information. :param powersimdata.input.grid.Grid grid: a Grid object previously transformed. :param dict ct: change table. :param bool slice: whether to slice the profiles by the Scenario's time range. """ self.slice = slice self._profile_input = ProfileInput() self.scenario_info = {**self._default_dates, **scenario_info} self.ct = copy.deepcopy(ct) self.grid = copy.deepcopy(grid) 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 plant with profiles. """ n_plant = [0, 0] if "new_plant" in self.ct.keys(): for p in self.ct["new_plant"]: n_plant[0] += 1 if p["type"] in set().union(*self.scale_keys.values()): n_plant[1] += 1 return n_plant def _get_renewable_profile(self, resource): """Return the transformed profile. :param str resource: a generator type with profile. :return: (*pandas.DataFrame*) -- power output for generators of specified type with plant identification number as columns and UTC timestamp as indices. """ plant_id = ( self.grid.plant.iloc[: len(self.grid.plant) - self.n_new_plant] .isin(self.scale_keys[resource]) .query("type == True") .index ) profile = self._profile_input.get_data(self.scenario_info, resource)[plant_id] scaled_profile = self._scale_plant_profile(profile) if self.n_new_clean_plant > 0: new_profile = self._add_plant_profile(profile, resource) return scaled_profile.join(new_profile) else: return scaled_profile def _scale_plant_profile(self, profile): """Scale profile. :param pandas.DataFrame profile: profile with plant identification number as columns and UTC timestamp as indices. Values are for 1-W generators. :return: (*pandas.DataFrame*) -- scaled power output profile. """ try: plant_id = list(map(int, profile.columns)) except: # noqa: E722 plant_id = profile.columns return profile * self.grid.plant.loc[plant_id, "Pmax"] def _add_plant_profile(self, profile, resource): """Add profile for plants added via the change table. :param pandas.DataFrame profile: profile with plant identification number as columns and UTC timestamp as indices. :param resource: fuel type. :return: (*pandas.DataFrame*) -- profile with additional columns corresponding to new generators inserted to the grid via the change table. """ new_plant_ids, neighbor_ids, scaling = [], [], [] for i, entry in enumerate(self.ct["new_plant"]): if entry["type"] in self.scale_keys[resource]: new_plant_ids.append(self.grid.plant.index[-self.n_new_plant + i]) neighbor_ids.append(entry["plant_id_neighbor"]) scaling.append(entry["Pmax"]) neighbor_profile = profile[neighbor_ids] new_profile = neighbor_profile.multiply(scaling, axis=1) new_profile.columns = new_plant_ids return new_profile def _get_demand_profile(self): """Return scaled demand profile. :return: (*pandas.DataFrame*) -- data frame of demand. """ zone_id = sorted(self.grid.id2zone) demand = self._profile_input.get_data(self.scenario_info, "demand").loc[ :, zone_id ] if bool(self.ct) and "demand" in list(self.ct.keys()): for key, value in self.ct["demand"]["zone_id"].items(): print( "Multiply demand in %s (#%d) by %.2f" % (self.grid.id2zone[key], key, value) ) demand.loc[:, key] *= value return demand def _get_demand_flexibility_profile(self, name): """Return the appropriately pruned demand flexibility profiles. Provides support for profiles that might have a mixed input of zones and buses. :param string name: The type of demand flexibility profile being specified. Can be one of: *'demand_flexibility_up'*, *'demand_flexibility_dn'*, *'demand_flexibility_cost_up'*, or *'demand_flexibility_cost_dn'*. :return: (*pandas.DataFrame*) -- data frame of a demand flexibility profile. """ # Access the specified demand flexibility profile flex_dem_dict = self.ct["demand_flexibility"] flex_dem_dict["grid_model"] = self.scenario_info["grid_model"] df = self._profile_input.get_data(flex_dem_dict, name) # Determine if the demand flexibility profile is indexed by zone, bus, or both area_indicator = [1 if "zone." in x else 0 for x in df.columns] area_ids = [] if sum(area_indicator) > 0: # Demand flexibility profile contains zone IDs zone_id = sorted(self.grid.id2zone) zone_id = [f"zone.{x}" for x in zone_id] area_ids += zone_id if sum(area_indicator) < len(area_indicator): # Demand flexibility profile contains bus IDs bus_id = sorted(self.grid.bus.index.unique().values) bus_id = [str(x) for x in bus_id] area_ids += bus_id # Prune the data frame according to the mix of zone IDs and bus IDs df = df.loc[:, df.columns.isin(area_ids)] # Warn if data frame is now empty (i.e., no relevant zones/buses were provided) if len(df.columns) == 0: raise ValueError( f"The {name} profile does not contain zone or buse IDs located in the " + f"{self.scenario_info['interconnect']} interconnect of the " + f"{self.scenario_info['grid_model']} grid model." ) # Return the pruned data frame return df def _get_electrified_demand(self): """Return the aggregate demand profile, including base demand and electrified demand. :return: (*pandas.DataFrame*) -- the full demand profile """ result = self._get_demand_profile() for kind in ("building", "transportation"): if kind in self.ct: result += TransformDemand(self.grid, self.ct, kind).value() return result def _slice_df(self, df): """Return dataframe, sliced by the times specified in scenario_info if and only if ``self.slice`` = True. :param pandas.DataFrame df: data frame to be sliced. :return: (*pandas.DataFrame*) -- sliced data frame. """ if not self.slice: return df return df.loc[self.scenario_info["start_date"] : self.scenario_info["end_date"]]
[docs] def get_profile(self, name): """Return profile. :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'*, *'demand_flexibility_up'*, *'demand_flexibility_dn'*, *'demand_flexibility_cost_up'*, *'demand_flexibility_cost_dn'* or a generator type wit profile. """ possible = { "demand_flexibility_up", "demand_flexibility_dn", "demand_flexibility_cost_up", "demand_flexibility_cost_dn", }.union(self.scale_keys.keys()) if name not in possible: raise ValueError( f"Invalid profile kind: {name}. Choose from %s" % " | ".join(possible) ) elif name == "demand": return self._slice_df(self._get_electrified_demand()) elif "demand_flexibility" in name: return self._slice_df(self._get_demand_flexibility_profile(name)) else: return self._slice_df(self._get_renewable_profile(name))