SDDP Model¶
This model explains the differences between the SDDP model and the normal model and also emphasizes key differences to the Divide Timesteps model which is very similar to SDDP.
Sets, Variables, Parameters and Expressions¶
support_timesteps
: Determine at which time steps the original problem is split into sub problems.com_max_tuples
: A set of all stock and environmental variables which have a maximum allowed usage amount.e_co_stock_state
: This variable gives the usage of a stock commodity up to a time step.e_co_stock_state_res
: This variable is a constraint on the state of a stock commodity at the beginning of a sub problem given by the previous problem.
Rules¶
There are some additional or different rules in SDDP compared to Divide Timesteps. These rules are there, to ensure the sub problems combined do not cross global restrictions on stock or CO2 , or other constraints that cannot be enforced in the master problem like in Divide Timesteps where the master has access to all support steps, but rather must be passed from sub problem to sub problem.
def_costs_rule
:- Master: The master costs includes the investment and fixed costs for all capacity variables, which can only be expanded in the master problem. If the first support step is not equal to the first time step, the master problem also has to carry the variable costs, fuel costs and environmental costs which occur in the time steps before the first support step. The cost of the first sub instance is added in the future costs (this means the master also includes all sub problem costs, because the first subproblems costs includes the costs of the second, the costs of the second the costs of the third and so on).
- Subs: The costs of the sub problem consists of the three time dependent cost types. These are variable costs, which are costs varying with the usage of commodities, fuel costs, which depend on the use of stock commodities, and environmental costs, which depend on the use of taxed environmental commodities. Also compare with Cost Variables (though not all cost variables of the master branch are supported yet). Additionally it contains the cost of the next sub problem in its future costs.
res_storage_state_by_capacity_rule
: Like in the original problem, except that in the sub problems the constraint need not be enforced for the first time step, because the first timestep is set by the previous problem.res_initial_storage_state_rule
: Unlike the ruleres_initial_and_final_storage_state_rule
in Divide Timesteps this rule is only included in the master instance and makes sure that the initial storage state is correct.final_storage_state_rule
: This rule makes sure that the final storage state is correct.sub_storage_content_rule
: This rules assures that the storage content in the first timestep of a sub problem obeys the storage content restriction given by the previous problem up to a deviation ofomega
timesLambda
.sub_com_generation_rule
: This rule asserts that the stock state (e_co_stock_state
, the amount of stock used so far) is at least the stock state restriction minusomega
timesLambda
.com_total_rule
: Asserts that the Env/Stock generation per site limitation is obeyed.com_state_rule
: This rule asserts that the stock state in time step t is equal to the stock state in time step t-1 plus the stock used in timestep t.global_co2_limit_rule
: Asserts that the global CO2 limit is not exceeded.sub_costs_rule
: Assures that the costs of the sub problem cannot be higher than the restriction on costs given by the master problem plusomega
timesLambda
.
Functions¶
Cut Generation¶
There are two methods in SDDP for cut generation:
add_cut()
calculates the weighted cut between the cuts of the possible realizations.def add_cut(self, realizations, cut_generating_problems, current_realized, probabilities): """ Adds a cut to this problem (in Sddp cuts can be added to both master and sub problems) Args: realizations: possible realizations (e.g. "low", "mid", "high") of the following supportsteps problem (= cut generating problems) cut_generating_problems: the realizations of the sub problem in the next timestep which generate the cut current_realized: realized instance of current problem probabilities: probabilities of realizations """ cur_probs = {} for cur_real in realizations: if cut_generating_problems[cur_real].Lambda() > 0.0000001: cur_probs[cur_real] = cut_generating_problems[cur_real] else: print('Cut skipped for subproblem ' + '(' + str(cut_generating_problems[cur_real].ts[1]) + ', ' + cur_real + '), Lambda = ' + str(cut_generating_problems[cur_real].Lambda()))
First, we check if
Lambda
is very close to zero for any cut generating problem. IfLambda
is zero, this means that the realization of the sub problem does not violate any constraints passed to it by the previous problem. This in turn means that the realization yields a feasible solution and does not contribute to the weighted cut for the previous problem.if len(cur_probs) > 0: self.Cut_Defn.add( sum(probabilities[cur_real] * self.get_cut_expression(cur_probs[cur_real]) for cur_real in cur_probs) >= sum(probabilities[cur_real] * (cur_probs[cur_real].Lambda() + current_realized.get_cut_expression(cur_probs[cur_real])()) for cur_real in cur_probs))
If there is at least one cut which has not been skipped, we generate the weighted cut for the current problem. To obtain one cut we take the cut expression generated by
get_cut_expression()
for each possible realization of the next timestep. We know that using the current values of the current problems variables the problem in the next time step cannot be solved without violating at least one constraint byLambda
(because the sub problem minimizesLambda
). This implies that in future iterations the cut expression has to be at least the evaluated cut expression plusLambda
for the sub problem to become feasible (Lambda
is (almost) zero). Because we can only evaluate the cut expression for the realized instance (we only know the values for the variables of the instance we solved in the forward recursion), we use its cut expression as an approximate substitute for all the realizations. To obtain the weighted cut we multiply each generated cut with the realization’s probability on both sides and take their sum.get_cut_expression()
creates the cut expression for the current realization generated by one possible realization in the next time step.def get_cut_expression(self, cut_generating_problem): """ Calculates the cut expression for one realization Args: cut_generating problem: the realization which generates the cut Returns: the generated cut expression """ multi_index = pd.MultiIndex.from_tuples([(t,) + sto for t in cut_generating_problem.t for sto in cut_generating_problem.sto_tuples], names=['t', 'sit', 'sto', 'com']) dual_sto = pd.Series(0, index=multi_index) dual_sto_help = get_entity(cut_generating_problem, 'sub_storage_content') dual_sto = dual_sto.add(-abs(dual_sto_help.loc[[cut_generating_problem.ts[1]]]), fill_value=0)
We start with initializing the dual variables. For every constraint the corresponding dual variables states how much the objective would change if the constraint is changed by one. Note that this means the duals are not really variables (in the mathematical sense), but rather fixed rational numbers. The storage constraint dual is made negative for the first time step of the cut generating problem, because increasing the storage available in the beginning would decrease the objective function. Unlike Divide Timesteps there is no constraint on the last time step of a sub problem, because the master problem has no access to that time step.
dual_pro = get_entity(cut_generating_problem, 'def_process_capacity') dual_tra = get_entity(cut_generating_problem, 'def_transmission_capacity') dual_sto_cap = get_entity(cut_generating_problem, 'def_storage_capacity') dual_sto_capl = get_entity(cut_generating_problem, 'def_storage_capacity_l') dual_sto_pow = get_entity(cut_generating_problem, 'def_storage_power') dual_com = get_entity(cut_generating_problem, 'sub_com_generation') dual_zero = cut_generating_problem.dual[cut_generating_problem.sub_costs]
Next, we initialize all other dual variables. For every constraint there is exactly one dual. Note that one rule can describe more than one constraint and in turn the corresponding dual variable is actually a vector of dual variables. As an example consider
def_process_capacity
. This rule defines a constraint for each process which meansdual_pro
contains one dual variable for every one of these constraints. In SDDP there are the capacity constraints, the generation constraint (sub_com_generation
), which unifies the commodity and environmental constraints, and the cost constraint. To generate the cut we also need the value ofLambda
for the cut generating problem.cut_expression = - 1 * (sum(dual_pro[pro] * self.cap_pro[pro] for pro in self.pro_tuples) + sum(dual_tra[tra] * self.cap_tra[tra] for tra in self.tra_tuples) + sum((dual_sto_cap[sto] - dual_sto_capl[sto]) * self.cap_sto_c[sto] for sto in self.sto_tuples) + sum(dual_sto_pow[sto] * self.cap_sto_p[sto] for sto in self.sto_tuples) + dual_zero * self.eta) cut_expression += -1 * (sum([dual_sto[(self.t[-1],) + sto] * self.e_sto_con[(self.t[-1],) + sto] for sto in self.sto_tuples]) - sum([dual_com[(self.t[-1],) + com] * self.e_co_stock_state[ (self.t[-1],) + com] for com in self.com_tuples if com in self.com_max_tuples]) )
With the dual variables we can generate the cut expression: The cut expression is the sum of all dual variables times the corresponding variables in the current instance. This reflects that by increasing one variable in the current instance (e.g. a process:
cap_pro[pro]
) the objective function of the sub problem would change by the corresponding dual (e.g.dual_pro[pro]
). As increasing the capacity would decrease the objective function and decreasing it would increase the objective function we have to multiply by minus one. The same holds for the cost constraint, while the generation constraint is not multiplied by minus one (or to be more precise in the implementation it is subtracted and then multiplied by minus one, which is equivalent). This makes sense, because the generation constraint says how much of the commodity has already been generated in the case of CO2 or used in the case of stock commodities. If the amount of CO2 generated or stock commodities used increases the objective function increases.return cut_expression
Return the generated cut expression.
Create Uncertainty¶
To introduce uncertainty in the data we use the function create_uncertainty_data()
which itself uses the function create_uncertainty_supim()
.
def create_uncertainty_data(self, data, factor):
"""
Change dataframe to include modified uncertain time series
Args:
data: pandas DataFrame with original data
factor: float, between -1 and 1, which corresponds to the realization of the uncertainty
Returns:
pandas DataFrame with modified data
"""
# get supim sheet
supim = data['supim']
new_data = data.copy()
new_supim = supim.copy(deep=True)
wind_supim = new_supim.xs('Wind', axis=1, level=1)
help_df = self.create_uncertainty_supim(wind_supim, factor)
help_df.columns = pd.MultiIndex.from_product([help_df.columns, ['Wind']])
new_supim.loc[:, (slice(None), 'Wind')] = help_df
new_data['supim'] = new_supim
return new_data
The uncertainty data is created by copying the old data, then introducing uncertainty using the function create_uncertainty_supim()
for all desired supim time series (in this case only done for wind). The in this way newly created supim data is inserted back into the data.
How much uncertainty is introduced is controlled by the passed factor and is passed on to create_uncertainty_supim()
.
def create_uncertainty_supim(self, supim, factor):
"""
create convex combination of supim time series for different scenarios
Args:
supim: pandas Series or DataFrame of supim time series of a specific commodity
factor: float, between -1 and 1, which corresponds to the realization of the uncertainty
Returns:
pandas Series or DataFrame with convex combination
"""
if factor < 0:
supim_convex = (1 - abs(factor)) * supim
elif factor > 0:
supim_convex = abs(factor) + (1 - abs(factor)) * supim
else:
supim_convex = supim
return supim_convex
This function manipulates a supim time series by taking a convex combination of the minimum or maximum possible value depending on whether factor is negative or positive respectively. The minimum value for any supim series is 0 and the maximum value is 1. The value of the factor is fixed for the entire time series.