diff --git a/prescient/data/data_provider.py b/prescient/data/data_provider.py index 4b68c153..250aa40f 100644 --- a/prescient/data/data_provider.py +++ b/prescient/data/data_provider.py @@ -45,8 +45,22 @@ def negotiate_data_frequency(self, desired_frequency_minutes:int): pass @abstractmethod - def get_initial_model(self, options:Options, num_time_steps:int) -> EgretModel: - ''' Get a model ready to be populated with data + def get_initial_forecast_model(self, options:Options, num_time_steps:int) -> EgretModel: + ''' Get a forecast model ready to be populated with data + + Returns + ------- + A model object populated with static system information, such as + buses and generators, and with time series arrays that are large + enough to hold num_time_steps entries. + + Initial values in time time series do not have meaning. + ''' + pass + + @abstractmethod + def get_initial_actuals_model(self, options:Options, num_time_steps:int) -> EgretModel: + ''' Get an actuals model ready to be populated with data Returns ------- diff --git a/prescient/data/providers/dat_data_provider.py b/prescient/data/providers/dat_data_provider.py index 05cbb6fa..b5971845 100644 --- a/prescient/data/providers/dat_data_provider.py +++ b/prescient/data/providers/dat_data_provider.py @@ -59,7 +59,13 @@ def negotiate_data_frequency(self, desired_frequency_minutes:int): # This provider can only return one value every 60 minutes. return 60 - def get_initial_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: + def get_initial_forecast_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: + return self._get_initial_model(options, num_time_steps, minutes_per_timestep) + + def get_initial_actuals_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: + return self._get_initial_model(options, num_time_steps, minutes_per_timestep) + + def _get_initial_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: ''' Get a model ready to be populated with data Returns @@ -236,7 +242,7 @@ def _populate_with_forecastable_data(self, options:Options, dat = identify_dat(day) - for src, target in forecast_helper.get_forecastables(dat, model): + for src, target in _get_forecastables(dat, model): target[step_index] = src[src_step_index] @@ -307,3 +313,33 @@ def _recurse_copy_with_time_series_length(root:Dict[str, Any], time_count:int) - else: new_node[key] = copy.deepcopy(att) return new_node + +def _get_forecastables(*models: EgretModel) -> Iterable[ Tuple[MutableSequence[float]] ]: + ''' Get all data that are predicted by forecasting, for any number of models. + Specialization of this for the dat data provider with fixed checks + + The iterable returned by this function yields tuples containing one list from each model + passed to the function. Each tuple of lists corresponds to one type of data that is included + in forecast predictions, such as loads on a particular bus, or limits on a renewable generator. + The lengths of the lists matches the number of time steps present in the underlying models. + Modifying list values modifies the underlying model. + ''' + # Renewables limits + model1 = models[0] + for gen, gdata1 in model1.elements('generator', generator_type=('renewable','virtual')): + if isinstance(gdata1['p_min'], dict): + yield tuple(m.data['elements']['generator'][gen]['p_min']['values'] for m in models) + if isinstance(gdata1['p_max'], dict): + yield tuple(m.data['elements']['generator'][gen]['p_max']['values'] for m in models) + if 'p_cost' in gdata1 and isinstance(gdata1['p_cost'], dict): + yield tuple(m.data['elements']['generator'][gen]['p_cost']['values'] for m in models) + + # Loads + for bus, bdata1 in model1.elements('load'): + yield tuple(m.data['elements']['load'][bus]['p_load']['values'] for m in models) + if 'p_price' in bdata1 and isinstance(bdata1['p_price'], dict): + yield tuple(m.data['elements']['load'][bus]['p_price']['values'] for m in models) + + # Reserve requirement + if 'reserve_requirement' in model1.data['system']: + yield tuple(m.data['system']['reserve_requirement']['values'] for m in models) diff --git a/prescient/data/providers/gmlc_data_provider.py b/prescient/data/providers/gmlc_data_provider.py index 683057c4..0dab9f85 100644 --- a/prescient/data/providers/gmlc_data_provider.py +++ b/prescient/data/providers/gmlc_data_provider.py @@ -18,6 +18,7 @@ import copy from egret.parsers import rts_gmlc_parser as parser +from egret.parsers.rts_gmlc._reserves import reserve_name_map from egret.data.model_data import ModelData as EgretModel from ..data_provider import DataProvider @@ -60,7 +61,13 @@ def negotiate_data_frequency(self, desired_frequency_minutes:int): else: return native_frequency - def get_initial_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: + def get_initial_actuals_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: + return self._get_initial_model("REAL_TIME", options, num_time_steps, minutes_per_timestep) + + def get_initial_forecast_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: + return self._get_initial_model("DAY_AHEAD", options, num_time_steps, minutes_per_timestep) + + def _get_initial_model(self, sim_type:str, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: ''' Get a model ready to be populated with data Returns @@ -75,7 +82,7 @@ def get_initial_model(self, options:Options, num_time_steps:int, minutes_per_tim data['system']['time_period_length_minutes'] = minutes_per_timestep data['system']['time_keys'] = [str(i) for i in range(1,num_time_steps+1)] md = EgretModel(data) - forecast_helper.ensure_forecastable_storage(num_time_steps, md) + self._ensure_forecastable_storage(sim_type, num_time_steps, md) return md def populate_initial_state_data(self, options:Options, @@ -176,7 +183,6 @@ def populate_with_actuals(self, options:Options, ''' self._populate_with_forecastable_data('REAL_TIME', start_time, num_time_periods, time_period_length_minutes, model) - def _populate_with_forecastable_data(self, sim_type:str, start_time:datetime, @@ -201,6 +207,65 @@ def _populate_with_forecastable_data(self, dt = start_time + i*delta time_labels[i] = dt.strftime('%Y-%m-%d %H:%M') + def _get_forecastable_locations(self, simulation_type:str, md:EgretModel): + df = self._cache.timeseries_df + + system = md.data['system'] + loads = md.data['elements']['load'] + generators = md.data['elements']['generator'] + areas = md.data['elements']['area'] + + # Go through each timeseries value for this simulation type + for i in range(self._cache._first_indices[simulation_type], len(df)): + if df.iat[i, df.columns.get_loc('Simulation')] != simulation_type: + break + + category = df.iat[i, df.columns.get_loc('Category')] + + if category == 'Generator': + gen_name = df.iat[i, df.columns.get_loc('Object')] + param = df.iat[i, df.columns.get_loc('Parameter')] + + if param == 'PMin MW': + yield (generators[gen_name], 'p_min') + elif param == 'PMax MW': + yield (generators[gen_name], 'p_max') + else: + raise ValueError(f"Unexpected generator timeseries data: {param}") + + elif category == 'Area': + area_name = df.iat[i, df.columns.get_loc('Object')] + param = df.iat[i, df.columns.get_loc('Parameter')] + assert(param == "MW Load") + for l_d in loads.values(): + # Skip loads from other areas + if l_d['area'] != area_name: + continue + yield (l_d, 'p_load') + yield (l_d, 'q_load') + + elif category == 'Reserve': + res_name = df.iat[i, df.columns.get_loc('Object')] + if res_name in reserve_name_map: + yield (system, reserve_name_map[res_name]) + else: + # reserve name must be _R, + # split into type and area + res_name, area_name = res_name.split("_R", 1) + yield (areas[area_name], reserve_name_map[res_name]) + + def _ensure_forecastable_storage(self, sim_type:str, num_entries:int, model:EgretModel) -> None: + """ Ensure that the model has an array allocated for every type of forecastable data + """ + for data, key in self._get_forecastable_locations(sim_type, model): + if (key not in data or \ + type(data[key]) is not dict or \ + data[key]['data_type'] != 'time_series' or \ + len(data[key]['values'] != num_entries) + ): + data[key] = { 'data_type': 'time_series', + 'values': [None]*num_entries} + def _recurse_copy_at_ratio(src:dict[str, Any], target:dict[str, Any], ratio:int) -> None: ''' Copy every Nth value from a src dict's time_series values into corresponding arrays in a target dict. ''' diff --git a/prescient/data/providers/shortcut_data_provider.py b/prescient/data/providers/shortcut_data_provider.py index 3b21cc06..18e3b163 100644 --- a/prescient/data/providers/shortcut_data_provider.py +++ b/prescient/data/providers/shortcut_data_provider.py @@ -83,7 +83,13 @@ def negotiate_data_frequency(self, desired_frequency_minutes:int): else: return native_frequency - def get_initial_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: + def get_initial_forecast_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: + return self._get_initial_model(options, num_time_steps, minutes_per_timestep) + + def get_initial_actuals_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: + return self._get_initial_model(options, num_time_steps, minutes_per_timestep) + + def _get_initial_model(self, options:Options, num_time_steps:int, minutes_per_timestep:int) -> EgretModel: ''' Get a model ready to be populated with data Returns ------- diff --git a/prescient/data/simulation_state/mutable_simulation_state.py b/prescient/data/simulation_state/mutable_simulation_state.py index 6e6b90ef..90531c78 100644 --- a/prescient/data/simulation_state/mutable_simulation_state.py +++ b/prescient/data/simulation_state/mutable_simulation_state.py @@ -26,8 +26,8 @@ class MutableSimulationState(SimulationState): ''' def __init__(self): - self._forecasts = [] - self._actuals = [] + self._forecasts = {} + self._actuals = {} self._commits = {} self._init_gen_state = {} @@ -49,7 +49,9 @@ def __init__(self): @property def timestep_count(self) -> int: ''' The number of timesteps we have data for ''' - return len(self._forecasts[0]) if len(self._forecasts) > 0 else 0 + for _ in self._forecasts.values(): + return len(_) + return 0 @property def minutes_per_step(self) -> int: @@ -73,18 +75,17 @@ def get_initial_state_of_charge(self, s:S) -> float: ''' Get state of charge in the previous time period ''' return self._init_soc[s] - def get_current_actuals(self) -> Iterable[float]: - ''' Get the current actual value for each forecastable. + def get_current_actuals(self, forecastable:str) -> float: + ''' Get the current actual value for forecastable This is the actual value for the current time period (time index 0). Values are returned in the same order as forecast_helper.get_forecastables, but instead of returning arrays it returns a single value. ''' - for forecastable in self._actuals: - yield forecastable[0] + return self._actuals[forecastable][0] - def get_forecasts(self) -> Iterable[Sequence[float]]: - ''' Get the forecast values for each forecastable + def get_forecasts(self, forecastable:str) -> Sequence[float]: + ''' Get the forecast values for forecastable This is very similar to forecast_helper.get_forecastables(); the function yields an array per forecastable, in the same order as @@ -93,18 +94,16 @@ def get_forecasts(self) -> Iterable[Sequence[float]]: Note that the value at index 0 is the forecast for the current time, not the actual value for the current time. ''' - for forecastable in self._forecasts: - yield forecastable + return self._forecasts[forecastable] - def get_future_actuals(self) -> Iterable[Sequence[float]]: - ''' Warning: Returns actual values for the current time AND FUTURE TIMES. + def get_future_actuals(self, forecastable:str) -> Sequence[float]: + ''' Warning: Returns actual values of forecastable for the current time AND FUTURE TIMES. Be aware that this function returns information that is not yet known! The function lets you peek into the future. Future actuals may be used by some (probably unrealistic) algorithm options, such as ''' - for forecastable in self._actuals: - yield forecastable + return self._actuals[forecastable] def apply_ruc(self, options, ruc:RucModel) -> None: ''' Incorporate a RUC instance into the current state. @@ -192,14 +191,14 @@ def apply_sced(self, options, sced) -> None: self._simulation_minute += self._sced_frequency while self._next_forecast_pop_minute <= self._simulation_minute: - for value_deque in self._forecasts: + for value_deque in self._forecasts.values(): value_deque.popleft() for value_deque in self._commits.values(): value_deque.popleft() self._next_forecast_pop_minute += self._minutes_per_forecast_step while self._simulation_minute >= self._next_actuals_pop_minute: - for value_deque in self._actuals: + for value_deque in self._actuals.values(): value_deque.popleft() self._next_actuals_pop_minute += self._minutes_per_actuals_step @@ -225,17 +224,17 @@ def _save_forecastables(options, ruc, where_to_store, steps_per_hour): max_length = steps_per_hour*(ruc_delay + options.ruc_horizon) # Save all forecastables, in forecastable order - for idx, (new_ruc_vals,) in enumerate(get_forecastables(ruc)): + for key, new_ruc_vals in get_forecastables(ruc): if first_ruc: # append to storage array forecast = deque(maxlen=max_length) - where_to_store.append(forecast) + where_to_store[key] = forecast else: - forecast = where_to_store[idx] + forecast = where_to_store[key] # Pop until the first "ruc_delay" items are the only items in the list for _ in range(len(forecast) - steps_per_hour*ruc_delay): forecast.pop() # Put the new values into the value queue - forecast.extend(new_ruc_vals) + forecast.extend(new_ruc_vals) diff --git a/prescient/data/simulation_state/simulation_state.py b/prescient/data/simulation_state/simulation_state.py index a8babeea..38eaef53 100644 --- a/prescient/data/simulation_state/simulation_state.py +++ b/prescient/data/simulation_state/simulation_state.py @@ -47,8 +47,8 @@ def get_initial_state_of_charge(self, s:S): pass @abstractmethod - def get_current_actuals(self) -> Iterable[float]: - ''' Get the current actual value for each forecastable. + def get_current_actuals(self, forecastable:str) -> float: + ''' Get the current actual value for forecastable This is the actual value for the current time period (time index 0). Values are returned in the same order as forecast_helper.get_forecastables, @@ -57,8 +57,8 @@ def get_current_actuals(self) -> Iterable[float]: pass @abstractmethod - def get_forecasts(self) -> Iterable[Sequence[float]]: - ''' Get the forecast values for each forecastable + def get_forecasts(self, forecastable:str) -> Sequence[float]: + ''' Get the forecast values for forecastable This is very similar to forecast_helper.get_forecastables(); the function yields an array per forecastable, in the same order as @@ -70,8 +70,8 @@ def get_forecasts(self) -> Iterable[Sequence[float]]: pass @abstractmethod - def get_future_actuals(self) -> Iterable[Sequence[float]]: - ''' Warning: Returns actual values of forecastables for the current time AND FUTURE TIMES. + def get_future_actuals(self, forecastable:str) -> Sequence[float]: + ''' Warning: Returns actual values of forecastable for the current time AND FUTURE TIMES. Be aware that this function returns information that is not yet known! The function lets you peek into the future. Future actuals may be used diff --git a/prescient/data/simulation_state/state_with_offset.py b/prescient/data/simulation_state/state_with_offset.py index 17355bbb..34476d5e 100644 --- a/prescient/data/simulation_state/state_with_offset.py +++ b/prescient/data/simulation_state/state_with_offset.py @@ -82,18 +82,17 @@ def get_initial_state_of_charge(self, s:S): ''' Get state of charge in the previous time period ''' return self._init_soc[s] - def get_current_actuals(self) -> Iterable[float]: - ''' Get the current actual value for each forecastable. + def get_current_actuals(self, forecastable:str) -> float: + ''' Get the current actual value for forecastable This is the actual value for the current time period (time index 0). Values are returned in the same order as forecast_helper.get_forecastables, but instead of returning arrays it returns a single value. ''' - for actual in self._parent.get_future_actuals(): - yield actual[self._offset] + return self._parent.get_future_actuals(forecastable)[self._offset] - def get_forecasts(self) -> Iterable[Sequence[float]]: - ''' Get the forecast values for each forecastable + def get_forecasts(self, forecastable:str) -> Sequence[float]: + ''' Get the forecast values for forecastable This is very similar to forecast_helper.get_forecastables(); the function yields an array per forecastable, in the same order as @@ -102,19 +101,15 @@ def get_forecasts(self) -> Iterable[Sequence[float]]: Note that the value at index 0 is the forecast for the current time, not the actual value for the current time. ''' - for forecast in self._parent.get_forecasts(): - # Copy the relevent portion to a new array - portion = list(itertools.islice(forecast, self._offset, None)) - yield portion + # Copy the relevent portion to a new array + return list(itertools.islice(self._parent.get_forecasts(forecastable), self._offset, None)) - def get_future_actuals(self) -> Iterable[Sequence[float]]: - ''' Warning: Returns actual values for the current time AND FUTURE TIMES. + def get_future_actuals(self, forecastable:str) -> Sequence[float]: + ''' Warning: Returns actual values of forecastable for the current time AND FUTURE TIMES. Be aware that this function returns information that is not yet known! The function lets you peek into the future. Future actuals may be used by some (probably unrealistic) algorithm options, such as ''' - for future in self._parent.get_future_actuals(): - # Copy the relevent portion to a new array - portion = list(itertools.islice(future, self._offset, None)) - yield portion + # Copy the relevent portion to a new array + return list(itertools.islice(self._parent.get_future_actuals(forecastable), self._offset, None)) diff --git a/prescient/data/simulation_state/time_interpolated_state.py b/prescient/data/simulation_state/time_interpolated_state.py index 788c2adc..4b0bf8e6 100644 --- a/prescient/data/simulation_state/time_interpolated_state.py +++ b/prescient/data/simulation_state/time_interpolated_state.py @@ -130,27 +130,27 @@ def get_initial_state_of_charge(self, s:S): ''' Get state of charge in the previous time period ''' return self._inner_state.get_initial_state_of_charge(s) - def get_current_actuals(self) -> Iterable[float]: - ''' Get the current actual value for each forecastable. + def get_current_actuals(self, forecastable:str) -> float: + ''' Get the current actual value for forecastable - This is the actual value for the current time period (outer step index 0). + This is the actual value for the current time period (time index 0). Values are returned in the same order as forecast_helper.get_forecastables, but instead of returning arrays it returns a single value. ''' if self._minutes_past_first_actuals == 0: - yield from self._inner_state.get_current_actuals() + return self._inner_state.get_current_actuals(forecastable) else: fractional_index = get_interpolated_index_position(0, self._minutes_past_first_actuals, self._minutes_per_inner_actuals, self._minutes_per_outer_step) - for forecastable in self._inner_state.get_future_actuals(): - yield interpolate_between(forecastable[fractional_index.index_before], - forecastable[fractional_index.index_after], - fractional_index.fraction_between) + forecastable = self._inner_state.get_future_actuals(forecastable) + return interpolate_between(forecastable[fractional_index.index_before], + forecastable[fractional_index.index_after], + fractional_index.fraction_between) - def get_forecasts(self) -> Iterable[Sequence[float]]: - ''' Get the forecast values for each forecastable + def get_forecasts(self, forecastable:str) -> Sequence[float]: + ''' Get the forecast values for forecastable This is very similar to forecast_helper.get_forecastables(); the function yields an array per forecastable, in the same order as @@ -159,29 +159,28 @@ def get_forecasts(self) -> Iterable[Sequence[float]]: Note that the value at index 0 is the forecast for the current time, not the actual value for the current time. ''' - return self._get_forecastables(self._inner_state.get_forecasts(), + return self._get_forecastables(self._inner_state.get_forecasts(forecastable), self._minutes_per_inner_forecast, self._minutes_past_first_forecast) - def get_future_actuals(self) -> Iterable[Sequence[float]]: - ''' Warning: Returns actual values of forecastables for the current time AND FUTURE TIMES. + def get_future_actuals(self, forecastable:str) -> Sequence[float]: + ''' Warning: Returns actual values of forecastable for the current time AND FUTURE TIMES. Be aware that this function returns information that is not yet known! The function lets you peek into the future. Future actuals may be used - by some (probably unrealistic) algorithm options. + by some (probably unrealistic) algorithm options, such as ''' - return self._get_forecastables(self._inner_state.get_future_actuals(), + return self._get_forecastables(self._inner_state.get_future_actuals(forecastable), self._minutes_per_inner_actuals, self._minutes_past_first_actuals) - def _get_forecastables(self, forecastables:Iterable[Sequence[float]], + def _get_forecastables(self, forecastable:Sequence[float], minutes_per_inner_step:int, minutes_past_first:int ) -> Iterable[Sequence[float]]: # if we don't have to interpolate... if minutes_per_inner_step == self._minutes_per_outer_step and minutes_past_first == 0: - yield from forecastables + return forecastable # Build an InterpolatingSequence for each forecastable - for f in forecastables: - yield InterpolatingSequence(f, minutes_per_inner_step, self._minutes_per_outer_step, minutes_past_first) + return InterpolatingSequence(forecastable, minutes_per_inner_step, self._minutes_per_outer_step, minutes_past_first) diff --git a/prescient/engine/egret/egret_plugin.py b/prescient/engine/egret/egret_plugin.py index f29c9b40..eaf15e69 100644 --- a/prescient/engine/egret/egret_plugin.py +++ b/prescient/engine/egret/egret_plugin.py @@ -38,12 +38,6 @@ from egret.data.model_data import ModelData as EgretModel -######################################################################################## -# a utility to find the "nearest" - quantified via Euclidean distance - scenario among # -# a candidate set relative to the input scenario, up through and including the # -# specified simulation hour. # -######################################################################################## - def call_solver(solver,instance,options,solver_options,relaxed=False, set_instance=True): tee = options.output_solver_logs if not tee: @@ -117,7 +111,7 @@ def create_sced_instance(data_provider:DataProvider, ''' assert current_state is not None - sced_md = data_provider.get_initial_model(options, sced_horizon, current_state.minutes_per_step) + sced_md = data_provider.get_initial_actuals_model(options, sced_horizon, current_state.minutes_per_step) # Set initial state _copy_initial_state_into_model(options, current_state, sced_md) @@ -128,28 +122,24 @@ def create_sced_instance(data_provider:DataProvider, if forecast_error_method is ForecastErrorMethod.PRESCIENT: # Warning: This method can see into the future! - future_actuals = current_state.get_future_actuals() - sced_forecastables = get_forecastables(sced_md) - for future, (sced_data,) in zip(future_actuals, sced_forecastables): + for forecastable, sced_data in get_forecastables(sced_md): + future = current_state.get_future_actuals(forecastable) for t in range(sced_horizon): sced_data[t] = future[t] else: # persistent forecast error: - current_actuals = current_state.get_current_actuals() - forecasts = current_state.get_forecasts() - sced_forecastables = get_forecastables(sced_md) # Go through each time series that can be forecasted - for current_actual, forecast, (sced_data,) in zip(current_actuals, forecasts, sced_forecastables): + for forecastable, sced_data in get_forecastables(sced_md): + forecast = current_state.get_forecasts(forecastable) # the first value is, by definition, the actual. - sced_data[0] = current_actual + sced_data[0] = current_state.get_current_actuals(forecastable) # Find how much the first forecast was off from the actual, as a fraction of # the forecast. For all subsequent times, adjust the forecast by the same fraction. - current_forecast = forecast[0] - if current_forecast == 0.0: + if forecast[0] == 0.0: forecast_error_ratio = 0.0 else: - forecast_error_ratio = current_actual / forecast[0] + forecast_error_ratio = sced_data[0] / forecast[0] for t in range(1, sced_horizon): sced_data[t] = forecast[t] * forecast_error_ratio @@ -188,6 +178,9 @@ def create_sced_instance(data_provider:DataProvider, if 'startup_curve' in g_dict: continue ramp_up_rate_sced = g_dict['ramp_up_60min'] * minutes_per_step/60. + # this rarely happens, e.g., synchronous condenser + if ramp_up_rate_sced == 0: + continue if 'startup_capacity' not in g_dict: sced_startup_capacity = _calculate_sced_startup_shutdown_capacity_from_none( g_dict['p_min'], ramp_up_rate_sced) @@ -203,6 +196,9 @@ def create_sced_instance(data_provider:DataProvider, continue ramp_down_rate_sced = g_dict['ramp_down_60min'] * minutes_per_step/60. + # this rarely happens, e.g., synchronous condenser + if ramp_down_rate_sced == 0: + continue # compute a new shutdown curve if we go from "on" to "off" if g_dict['initial_status'] > 0 and g_dict['fixed_commitment']['values'][0] == 0: power_t0 = g_dict['initial_p_output'] @@ -353,7 +349,7 @@ def create_deterministic_ruc(options, start_time = datetime.datetime.combine(start_day, datetime.time(hour=this_hour)) # Create a new model - md = data_provider.get_initial_model(options, ruc_horizon, 60) + md = data_provider.get_initial_forecast_model(options, ruc_horizon, 60) # Populate the T0 data if current_state is None or current_state.timestep_count == 0: @@ -371,7 +367,7 @@ def create_deterministic_ruc(options, ruc_delay = -(options.ruc_execution_hour%(-options.ruc_every_hours)) if options.ruc_prescience_hour > ruc_delay + 1: improved_hour_count = options.ruc_prescience_hour - ruc_delay - 1 - for (forecast,), actuals in zip(get_forecastables(md), + for forecast, actuals in zip(get_forecastables(md), current_state.get_future_actuals()): for t in range(0, improved_hour_count): forecast_portion = (ruc_delay+t)/options.ruc_prescience_hour @@ -620,7 +616,7 @@ def create_simulation_actuals( # Get a new model total_step_count = options.ruc_horizon * 60 // step_size_minutes - md = data_provider.get_initial_model(options, total_step_count, step_size_minutes) + md = data_provider.get_initial_actuals_model(options, total_step_count, step_size_minutes) # Fill it in with data if this_hour == 0: diff --git a/prescient/engine/egret/reporting.py b/prescient/engine/egret/reporting.py index 5eed68af..04f544ac 100644 --- a/prescient/engine/egret/reporting.py +++ b/prescient/engine/egret/reporting.py @@ -121,7 +121,8 @@ def report_curtailment_for_deterministic_ruc(ruc): for i,t in enumerate(time_periods): quantity_curtailed_this_period = sum(gdict['p_max']['values'][i] - gdict['pg']['values'][i] \ for gdict in rn_gens.values()) - if quantity_curtailed_this_period > 0.0: + # don't print 0.00 below + if quantity_curtailed_this_period >= 5e-3: if curtailment_in_some_period == False: print("Renewables curtailment summary (time-period, aggregate_quantity):") curtailment_in_some_period = True diff --git a/prescient/engine/forecast_helper.py b/prescient/engine/forecast_helper.py index bcb41eda..992f0b97 100644 --- a/prescient/engine/forecast_helper.py +++ b/prescient/engine/forecast_helper.py @@ -28,8 +28,30 @@ class InferrableForecastable(NamedTuple): inferral_type: InferralType forecastable: MutableSequence[float] - -def get_forecastables(*models: EgretModel) -> Iterable[ Tuple[MutableSequence[float]] ]: +def _recurse_into_time_series_values(name:str, data_dict: dict) -> Iterable[MutableSequence[float]]: + for att_name, att in data_dict.items(): + if isinstance(att, dict): + if 'data_type' in att and att['data_type'] == 'time_series': + yield name+'__'+att_name, att['values'] + else: + _recurse_into_time_series_values(name+'__'+att_name, att) + +_egret_element_types = [ + 'generator', + 'load', + 'branch', + 'dc_branch', + 'bus', + 'shunt', + 'storage', + 'area', + 'zone', + 'interface', + 'fuel_supply', + 'interchange', + ] + +def get_forecastables(model: EgretModel) -> Iterable[ Tuple[str, MutableSequence[float]] ]: ''' Get all data that are predicted by forecasting, for any number of models. The iterable returned by this function yields tuples containing one list from each model @@ -38,84 +60,27 @@ def get_forecastables(*models: EgretModel) -> Iterable[ Tuple[MutableSequence[fl The lengths of the lists matches the number of time steps present in the underlying models. Modifying list values modifies the underlying model. ''' - # Renewables limits - model1 = models[0] - for gen, gdata1 in model1.elements('generator', generator_type=('renewable','virtual')): - if isinstance(gdata1['p_min'], dict): - yield tuple(m.data['elements']['generator'][gen]['p_min']['values'] for m in models) - if isinstance(gdata1['p_max'], dict): - yield tuple(m.data['elements']['generator'][gen]['p_max']['values'] for m in models) - if 'p_cost' in gdata1 and isinstance(gdata1['p_cost'], dict): - yield tuple(m.data['elements']['generator'][gen]['p_cost']['values'] for m in models) - - # Loads - for bus, bdata1 in model1.elements('load'): - yield tuple(m.data['elements']['load'][bus]['p_load']['values'] for m in models) - if 'p_price' in bdata1 and isinstance(bdata1['p_price'], dict): - yield tuple(m.data['elements']['load'][bus]['p_price']['values'] for m in models) - - # Reserve requirement - if 'reserve_requirement' in model1.data['system']: - yield tuple(m.data['system']['reserve_requirement']['values'] for m in models) - - return + for element_type in _egret_element_types: + for name, data in model.elements(element_type): + yield from _recurse_into_time_series_values(element_type+'__'+name, data) + yield from _recurse_into_time_series_values('system', model.data['system']) def get_forecastables_with_inferral_method(model:EgretModel) -> Iterable[InferrableForecastable]: """ Get all data predicted by forecasting in a model, with the method used to infer values after the first day """ - # Renewables limits - for gen, gdata in model.elements('generator', generator_type=('renewable','virtual')): - how_to_infer = InferralType.REPEAT_LAST if ('fuel' in gdata and gdata['fuel'] == 'W') \ - else InferralType.COPY_FIRST_DAY - if isinstance(gdata['p_min'], dict): - yield InferrableForecastable(how_to_infer, gdata['p_min']['values']) - if isinstance(gdata['p_max'], dict): - yield InferrableForecastable(how_to_infer, gdata['p_max']['values']) - if 'p_cost' in gdata and isinstance(gdata['p_cost'], dict): - yield InferrableForecastable(how_to_infer, gdata['p_cost']['values']) - - # Loads - for bus, bdata in model.elements('load'): - yield InferrableForecastable(InferralType.COPY_FIRST_DAY, bdata['p_load']['values']) - if 'p_price' in bdata and isinstance(bdata['p_price'], dict): - yield InferrableForecastable(InferralType.COPY_FIRST_DAY, bdata['p_price']['values']) - - # Reserve requirement - if 'reserve_requirement' in model.data['system']: - yield InferrableForecastable(InferralType.COPY_FIRST_DAY, model.data['system']['reserve_requirement']['values']) - - return - - -def ensure_forecastable_storage(num_entries:int, model:EgretModel) -> None: - """ Ensure that the model has an array allocated for every type of forecastable data - """ - def _get_forecastable_locations(model): - """ get all locations where data[key]['values'] is expected to return a forecastable's value array - - Returns - ------- - data:dict - Parent dict with an entry that points to a forecastable time series - key:Any - Key into data where forecastable time series is expected - """ - # Generators - for gen, gdata in model.elements('generator', generator_type='renewable'): - yield (gdata, 'p_min') - yield (gdata, 'p_max') - # Loads - for bus, bdata in model.elements('load'): - yield (bdata, 'p_load') - # Reserve requirement (if present, this is optional) - if 'reserve_requirement' in model.data['system']: - yield (model.data['system'], 'reserve_requirement') - - for data, key in _get_forecastable_locations(model): - if (not key in data or \ - type(data[key]) is not dict or \ - data[key]['data_type'] != 'time_series' or \ - len(data[key]['values'] != num_entries) - ): - data[key] = { 'data_type': 'time_series', - 'values': [None]*num_entries} + # Generator is the first element type + for _, gdata in model.elements('generator'): + if ('fuel' in gdata and gdata['fuel'].lower() in ('w', 'wind')): + for _,vals in _recurse_into_time_series_values('',gdata): + yield InferralType.REPEAT_LAST, vals + else: + for _,vals in _recurse_into_time_series_values('',gdata): + yield InferralType.COPY_FIRST_DAY, vals + + for element_type in _egret_element_types[1:]: + for _, data in model.elements(element_type): + for _,vals in _recurse_into_time_series_values('',data): + yield InferralType.COPY_FIRST_DAY, vals + + for _,vals in _recurse_into_time_series_values('',model.data['system']): + yield InferralType.COPY_FIRST_DAY, vals