def create_sced_instance(data_provider:DataProvider, current_state:SimulationState, options, sced_horizon, forecast_error_method = ForecastErrorMethod.PRESCIENT ): ''' Create a deterministic economic dispatch instance, given current forecasts and commitments. ''' assert current_state is not None 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) ################################################################################ # initialize the demand and renewables data, based on the forecast error model # ################################################################################ if forecast_error_method is ForecastErrorMethod.PRESCIENT: # Warning: This method can see into the future! 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: # Go through each time series that can be forecasted 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_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. if forecast[0] == 0.0: forecast_error_ratio = 0.0 else: forecast_error_ratio = sced_data[0] / forecast[0] for t in range(1, sced_horizon): sced_data[t] = forecast[t] * forecast_error_ratio _ensure_reserve_factor_honored(options, sced_md, range(sced_horizon)) _ensure_contingencies_monitored(options, sced_md) # Set generator commitments & future state for g, g_dict in sced_md.elements(element_type='generator', generator_type='thermal'): # Start by preparing an empty array of the correct size for each generator fixed_commitment = [None]*sced_horizon g_dict['fixed_commitment'] = _time_series_dict(fixed_commitment) # Now fill it in with data for t in range(sced_horizon): fixed_commitment[t] = current_state.get_generator_commitment(g,t) # Look as far into the future as we can for future startups / shutdowns last_commitment = fixed_commitment[-1] for t in range(sced_horizon, current_state.timestep_count): this_commitment = current_state.get_generator_commitment(g,t) if (this_commitment - last_commitment) > 0.5: # future startup future_status_time_steps = ( t - sced_horizon + 1 ) break elif (last_commitment - this_commitment) > 0.5: # future shutdown future_status_time_steps = -( t - sced_horizon + 1 ) break else: # no break future_status_time_steps = 0 g_dict['future_status'] = (current_state.minutes_per_step/60.) * future_status_time_steps if not options.no_startup_shutdown_curves: minutes_per_step = current_state.minutes_per_step for g, g_dict in sced_md.elements(element_type='generator', generator_type='thermal'): 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) else: sced_startup_capacity = _calculate_sced_startup_shutdown_capacity_from_existing( g_dict['startup_capacity'], g_dict['p_min'], minutes_per_step) g_dict['startup_curve'] = [ sced_startup_capacity - i*ramp_up_rate_sced \ for i in range(1,int(math.ceil(sced_startup_capacity/ramp_up_rate_sced))) ] for g, g_dict in sced_md.elements(element_type='generator', generator_type='thermal'): if 'shutdown_curve' in g_dict: 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'] # if we end up using a historical curve, it's important # for the time-horizons to match, particularly since this # function is also used to create long-horizon look-ahead # SCEDs for the unit commitment process create_sced_instance.shutdown_curves[g, minutes_per_step] = \ [ power_t0 - i*ramp_down_rate_sced for i in range(1,int(math.ceil(power_t0/ramp_down_rate_sced))) ] if (g,minutes_per_step) in create_sced_instance.shutdown_curves: g_dict['shutdown_curve'] = create_sced_instance.shutdown_curves[g,minutes_per_step] else: if 'shutdown_capacity' not in g_dict: sced_shutdown_capacity = _calculate_sced_startup_shutdown_capacity_from_none( g_dict['p_min'], ramp_down_rate_sced) else: sced_shutdown_capacity = _calculate_sced_startup_shutdown_capacity_from_existing( g_dict['shutdown_capacity'], g_dict['p_min'], minutes_per_step) g_dict['shutdown_curve'] = [ sced_shutdown_capacity - i*ramp_down_rate_sced \ for i in range(1,int(math.ceil(sced_shutdown_capacity/ramp_down_rate_sced))) ] if not options.enforce_sced_shutdown_ramprate: for g, g_dict in sced_md.elements(element_type='generator', generator_type='thermal'): # make sure the generator can immediately turn off g_dict['shutdown_capacity'] = max(g_dict['shutdown_capacity'], (60./current_state.minutes_per_step)*g_dict['initial_p_output'] + 1.) return sced_md
def generate_stack_graph(options, daily_stats: DailyStats): md_dict = ModelData.empty_model_data_dict() system = md_dict['system'] # put just the HH:MM in the graph system['time_keys'] = [ str(opstats.timestamp.time())[0:5] for opstats in daily_stats.operations_stats() ] system['reserve_requirement'] = _time_series_dict([ opstats.reserve_requirement for opstats in daily_stats.operations_stats() ]) system['reserve_shortfall'] = _time_series_dict([ opstats.reserve_shortfall for opstats in daily_stats.operations_stats() ]) elements = md_dict['elements'] elements['load'] = { 'system_load': { 'p_load': _time_series_dict([ opstats.total_demand for opstats in daily_stats.operations_stats() ]) } } elements['bus'] = { 'system_load_shed': { 'p_balance_violation': _time_series_dict([ opstats.load_shedding for opstats in daily_stats.operations_stats() ]) }, 'system_over_generation': { 'p_balance_violation': _time_series_dict([ -opstats.over_generation for opstats in daily_stats.operations_stats() ]) } } ## load in generation, storage generator_fuels = {} thermal_quickstart = {} thermal_dispatch = {} thermal_headroom = {} thermal_states = {} renewables_dispatch = {} renewables_curtailment = {} virtual_dispatch = {} storage_input_dispatch = {} storage_output_dispatch = {} storage_types = {} for opstats in daily_stats.operations_stats(): _collect_time_assert_equal(opstats.generator_fuels, generator_fuels) _collect_time_assert_equal(opstats.quick_start_capable, thermal_quickstart) _collect_time_assert_equal(opstats.storage_types, storage_types) _collect_time(opstats.observed_thermal_dispatch_levels, thermal_dispatch) _collect_time(opstats.observed_thermal_headroom_levels, thermal_headroom) _collect_time(opstats.observed_thermal_states, thermal_states) _collect_time(opstats.observed_renewables_levels, renewables_dispatch) _collect_time(opstats.observed_renewables_curtailment, renewables_curtailment) _collect_time(opstats.observed_virtual_dispatch_levels, virtual_dispatch) _collect_time(opstats.storage_input_dispatch_levels, storage_input_dispatch) _collect_time(opstats.storage_output_dispatch_levels, storage_output_dispatch) # load generation gen_dict = {} for g, fuel in generator_fuels.items(): gen_dict[g] = { 'fuel': fuel, 'generator_type': 'renewable' } # will get re-set below for thermal units for g, quickstart in thermal_quickstart.items(): gen_dict[g]['fast_start'] = quickstart gen_dict[g]['generator_type'] = 'thermal' for g in virtual_dispatch: gen_dict[g]['generator_type'] = 'virtual' _add_timeseries_attribute_to_egret_dict(gen_dict, thermal_dispatch, 'pg') _add_timeseries_attribute_to_egret_dict(gen_dict, thermal_headroom, 'headroom') _add_timeseries_attribute_to_egret_dict(gen_dict, thermal_states, 'commitment') _add_timeseries_attribute_to_egret_dict(gen_dict, renewables_dispatch, 'pg') _add_timeseries_attribute_to_egret_dict(gen_dict, renewables_curtailment, 'curtailment') _add_timeseries_attribute_to_egret_dict(gen_dict, virtual_dispatch, 'pg') for g_dict in gen_dict.values(): if g_dict['generator_type'] == 'renewable': pg = g_dict['pg']['values'] curtailment = g_dict['curtailment']['values'] g_dict['p_max'] = _time_series_dict( [pg_val + c_val for pg_val, c_val in zip(pg, curtailment)]) elements['generator'] = gen_dict # load storage storage_dict = {} for s, stype in storage_types.items(): storage_dict[s] = {'fuel': stype} _add_timeseries_attribute_to_egret_dict(storage_dict, storage_input_dispatch, 'p_charge') _add_timeseries_attribute_to_egret_dict(storage_dict, storage_output_dispatch, 'p_discharge') elements['storage'] = storage_dict figure_path = os.path.join( options.output_directory, "plots", "stackgraph_" + str(daily_stats.date) + ".png") graphutils.generate_stack_graph(ModelData(md_dict), bar_width=1, x_tick_frequency=4 * (60 // options.sced_frequency_minutes), title=str(daily_stats.date), save_fig=figure_path)
def _add_timeseries_attribute_to_egret_dict(egret_dict, attribute_dict, egret_attribute_name): for g, vals in attribute_dict.items(): egret_dict[g][egret_attribute_name] = _time_series_dict(vals)
def create_sced_instance(data_provider:DataProvider, current_state:SimulationState, options, sced_horizon, forecast_error_method = ForecastErrorMethod.PRESCIENT ): ''' Create an hourly deterministic economic dispatch instance, given current forecasts and commitments. ''' assert current_state != None sced_md = data_provider.get_initial_model(options, sced_horizon, current_state.minutes_per_step) # Set initial state _copy_initial_state_into_model(options, current_state, sced_md) ################################################################################ # initialize the demand and renewables data, based on the forecast error model # ################################################################################ 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_actuals): 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): # the first value is, by definition, the actual. sced_data[0] = current_actual # 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: forecast_error_ratio = 0.0 else: forecast_error_ratio = current_actual / forecast[0] for t in range(1, sced_horizon): sced_data[t] = forecast[t] * forecast_error_ratio _ensure_reserve_factor_honored(options, sced_md, range(sced_horizon)) ## TODO: propogate relax_t0_ramping_initial_day into this function ## if relaxing initial ramping, we need to relax it in the first SCED as well assert options.relax_t0_ramping_initial_day is False # Set generator commitments for g, g_dict in sced_md.elements(element_type='generator', generator_type='thermal'): # Start by preparing an empty array of the correct size for each generator fixed_commitment = [None]*sced_horizon g_dict['fixed_commitment'] = _time_series_dict(fixed_commitment) # Now fill it in with data for t in range(sced_horizon): fixed_commitment[t] = current_state.get_generator_commitment(g,t) return sced_md