Implementation¶
Note
SHIPP currently implements the analysis of two storage system components and two power generation components. The code has so far only be used for an hourly time step (\(\Delta t = 1\)), and has not been verified for other time increments.
Formulation of the storage system model¶
The storage system model linking power to stored energy is a piece-wise linear function (see Storage system model) and cannot be implemented directly in a linear or mixed-integer linear optimization problem. In SHIPP, three alternative formulations of the storage system model constraints are implemented:
a mixed-integer linear (
milp) formulation where integer variables are used to indicate if the storage is charging or discharging. Since the equations are enforced exactly, any feasible point of the optimization validates the model.a linear formulation using a relaxed form of the MILP (
lp_alt). In this case, the storage system model is only valid at the optimum. Feasible points for the optimization problem do not necessarily validate the storage model.a linear formulation (
lp) where the storage power is divded into a charging and discharging terms. Here as well, the validity of the storage model is not guaranteed due to the possibility of simultaneous charge and discharge.
MILP¶
The storage system model is enforced using a binary variable at each time step \(i\), denoted by \(z_i\). The storage power is divided into two components: \(p^{s+}\) in dischage and \(p^{s-}\). The constraints are implemented using a big-M form,
We use the convention \(z^s_i=0\) for charge and \(z^s_i=1\) for discharge. In addition, the following bounds are imposed on the storage power,
LP alt¶
This is the default formulation in SHIPP. Here, the storage system model is represented by two inequality constraints,
For this implementation to be representative, one of the inequality constraint must always be active at the optimum. As a result, the objective function must maximize the power at all time steps (\(p^s_i\)). This means that negative prices should not be used.
The bounds on the storage power are set as \( -\bar{P}^s \leq p^{s-}_i \leq \bar{P}^s\).
LP¶
The LP formulation is very similar to the MILP one. It consists of the same bounds on the power time series and the same equality constraint,
Without integer variables, it is possible to have simultaneous charge and discharge at the optimum, \(p^{s-}_i p^{s+}_i \neq 0\). In this case, there is a risk that the storage model is not respected. A regularization term in the objective function should be used as a mitigation measure.
Perfect foresight optimization¶
The functions solve_lp_sparse and solve_lp_pyomo implement a dispatch optimization problem where information about power and price is known perfectly for the entire simulation horizon. It is an integrated design problem, where design and operation are found simulatenously. It is also possible to solve the problem for dispatch only by setting the parameter fixed_cap to True.
The design variables of the problem \(\boldsymbol{x}\) are the storage system components energy and power capacities, the time series of storage power and energy, and the time series of curtailed power. If fixed_cap = True, the storage capacities are either removed from the list of design variables or fixed to their initial values. Depending on the problem formulation, the storage system power is represented by one single variable (\(\boldsymbol{p}^s\)) or by its components in charge and discharge, and integer variables may be added. Thus, the vector of design variable changes depending on the problem formulation and the parameter fixed_cap, as reported in the table below.
Formulation |
Objective function |
|
Design variables \(\boldsymbol{x}\) |
|---|---|---|---|
|
\(f_\text{NPV}\) |
False |
\(\boldsymbol{p}^c, (\bar{P}^s, \bar{E}^s, \boldsymbol{p}^{s+}, \boldsymbol{p}^{s-}, \boldsymbol{e}^s, \boldsymbol{z}^s_i)_{s\in{1,2}}\) |
|
\(f_R\) |
True |
\(\boldsymbol{p}^c, (\boldsymbol{p}^{s+}, \boldsymbol{p}^{s-}, \boldsymbol{e}^s, \boldsymbol{z}^s_i)_{s\in{1,2}}\) |
|
\(f_{\text{NPV}, \text{alt}}\) |
False |
\(\boldsymbol{p}^c, (\bar{P}^s, \bar{E}^s, \boldsymbol{p}^s, \boldsymbol{e}^s)_{s\in{1,2}}\) |
|
\(f_{R, \text{alt}}\) |
True |
\(\boldsymbol{p}^c, (\boldsymbol{p}^s, \boldsymbol{e}^s)_{s\in{1,2}}\) |
|
\(f_\text{NPV}\) |
False |
\(\boldsymbol{p}^c, (\bar{P}^s, \bar{E}^s, \boldsymbol{p}^{s+}, \boldsymbol{p}^{s-}, \boldsymbol{e}^s)_{s\in{1,2}}\) |
|
\(f_R\) |
True |
\(\boldsymbol{p}^c, (\boldsymbol{p}^{s+}, \boldsymbol{p}^{s-}, \boldsymbol{e}^s)_{s\in{1,2}}\) |
In solve_lp_sparse¶
In solve_lp_sparse, the implemented optimization problem is
where the storage model constraints depend on the formulation, as described above. The storage capacity bounds depend on the parameter fixed_cap. If fixed_cap = False, one has
where \( \bar{P}^s_\text{max}\) and \(\bar{E}^s_\text{max}\) are either equal to the initial capacities of the storage system ( \(\bar{P}^s_\text{init}, \bar{E}^s_\text{init}\)) or to an arbitrary large value if a capacity is set to None.
If fixed_cap = True, one has instead
The problem can be represented in standard form, as
The terms \(\boldsymbol{c}_\text{obj}, \mathbf{A}_\text{eq}, \boldsymbol{b}_\text{eq}, \mathbf{A}_\text{ineq}, \boldsymbol{b}_{ineq}, \boldsymbol{x}_\text{lb}, \boldsymbol{x}_\text{ub}\) are constructed with the routines build_lp_obj_npv, build_lp_obj_revenues and build_lp_cst_sparse and used as input in the solver scipy.linprog.
Note
To reduce memory use and due to the large size of the problem, all matrices are stored in a sparse format.
The objective function aims to either maximize the added NPV or added revenues from the storage components. It includes three regularization parameters: \(\alpha\), \(\beta\) and \(\epsilon\). The parameters \(\alpha\) and \(\beta\) are used to regularize the curtailed power with respect to the storage power. The parameter \(\epsilon\) is only relevant for the lp formulation and helps avoiding simulatenous charging and discharging in the storage systems.
For the lp and milp formulation, the storage power is divided into charge \(\boldsymbol{p}^{s-}\) and discharge \(\boldsymbol{p}^{s+}\). The objective function of the problem is:
For the integrated design problem:
For the dispatch only problem:
For the lp_alt formulation, the objective function is:
For the integrated design problem:
For the dispatch only problem:
In solve_lp_pyomo¶
The dispatch optimization in solve_lp_pyomo is currently only implemented using the lp_alt formulation. The design variables of the problem are the storage system components energy and power capacities, the time series of storage power and energy, and the time series of curtailed power, i.e., \(\boldsymbol{x} = [\boldsymbol{p}^c, (\bar{P}^s, \bar{E}^s, \boldsymbol{p}^s, \boldsymbol{e}^s)_{s\in{1,2}}]\). If fixed_cap = True, the storage capacities are either removed from the list of design variables or fixed to their initial values, i.e., \(\boldsymbol{x} = [\boldsymbol{p}^c, (\boldsymbol{p}^s, \boldsymbol{e}^s)_{s\in{1,2}}]\).
The objective function of the problem aims to maximize the added NPV, i.e., the contribution of the storage system components to the total NPV,
There is only one regularization parameter, \(\alpha\), to balance curtailment and storage use. When fixed_cap = True, the objective function does not change, but it becomes equivalent to maximization of revenues since the storage capacities are fixed.
The problem is expressed mathematically as
The baseload constraint is described here with a vector \( \boldsymbol{P}_\text{bl}\) and not a scalar, allowing a reliability below 100% to be set. The ramp limit constraint are associated to the parameter dp_lim and only enforced if its value is not None. The implemented constraints are
The optimization problem is described implicitely using the (pyomo)[https://pyomo.readthedocs.io/] interface.
Rolling horizon dispatch¶
The function solve_dispatch is used for rolling horizon dispatch to simulate the operation of the power plant with imperfect forecast. The goal of this optimization problem is to maximize revenues from electricity sales, while at the same time reducing as much as possible deviations from the required baseload or ramp-limit constraints. In addition, the dispatch problem can be solved to find the best operation considering several scenarios for the power forecast \(p^{g,j}, j =1,..., m\).. For simplicity, only one generation component is considered here.
The design variables of the problem are the time series of storage power and energy calculated for each forecast scenario, the time series of curtailed power for each forecast scenario, binary variables indicating if the dispatch constraints are satisfied, and two slack variables \(r\) and \(r_p\). One has \(\boldsymbol{x} = [(\boldsymbol{p}^{c,j})_{j=1,...,m}, (\boldsymbol{p}^{s,j}, \boldsymbol{e}^{s,j})_{s\in{1,2}, j=1,...,m}, \boldsymbol{y}, r, r_p]\).
The objective function of the problem combines a revenue term, a reliability term using the slack variables and two regularization terms,
where \(\mu \gg 1\) and \(\beta \ll 1 \).
The optimization problem can be written mathematically as
where \(e^s_\text{init}\) is the initial state-of-charge of storage system \(s\) and should match the operation decided at the previous time step.
The reliability constraint enforces a target reliability \(r_\text{th}\) based on the number of times where the dispatch constraints (baseload and ramp limit) are satisfied. This calculation is done for the time window of the forecast in addition to a window of past operation of length \(n_h\). This enables the problem to keep a memory of previous operation. The constraint is expressed as
where \(k_h\) is the number of time steps satisfying the dispatch constraints in the window of past operation. The terms \(r_\text{th}, n_h\) and \(k_h\) are inputs to the function solve_dispatch_pyomo.
The ramp limit constraints are expressed as follows, with a separate expression for the constraint at the first time steps,
where \(p_{h}\) is the value of the power produced at the previous time step minus curtailment and \(p^s_{h}\) the value of the storage power at the previous time step. The lower bound for the ramp-limit at the first time step uses the slack variable \(r_p\) instead of a binary variable. This is to reduce the violation of the constraint when the ramp-limit cannot be satisfied at the first time step.
Finally, to facilitate convergence of the algorithm, the bounds on the binary variables are set so that
in the presence of a baseload constraint, \(y_i = 1\) if \(\underset{j}{\text{min}}(p^{g,j}_i) \geq P_\text{bl}\)
in the absence of baseload or ramp-limit constraints, \(y_i = 1, \forall i\).
The dispatch optimization in solve_dispatch is implemented using the lp_alt formulation. Two regularization terms are included in the objective function to ensure the constraints are active at the optimum. Furthermore, a penalty on the reliability binary variables is needed to ensure the dispatch constraints are respected as much as possible. As such, the objective function becomes
Warning
The rolling horizon dispatch has only been used and verified for one storage system.
Implementation status¶
The code implement different dispatch optimization problems through three routines. However, not all variations described above are available in each routine. An overview of their differences is reported below.
Routine name |
Formulation |
Objective function |
Constraints |
Optimization algorithm |
|---|---|---|---|---|
|
lp, lp-alp, milp |
NPV or Revenues |
Baseload |
|
|
lp_alt |
NPV or Revenues |
Baseload, Ramp-limit |
pyomo-compatible (mosek, cplex, gurobi, etc.) |
|
lp_alt |
Trade off between revenues and reliability |
Baseload, Ramp-limit |
pyomo-compatible (mosek, cplex, gurobi, etc.) |