Functions optimize_demand(),
add_battery_optimization() and
smart_charging() solve one independent optimization problem
per optimization window (typically one day). Depending
on the objective and the variables involved, the window problem is
solved either as a quadratic program (QP) with the OSQP backend, or as a linear or
mixed-integer linear program (LP/MILP) with the HiGHS backend. Currently, the
flextools package allows to optimize a time-series power
load considering the following objectives:
- Minimize the power exchanged with the grid (net power)
- Minimize the energy cost
- A weighted combination of both
In this article, we’ll cover the optimization problem for the energy cost minimization objective, for both the flexible demand (e.g. heatpumps, electric vehicles, etc.) and the battery.
Our energy cost optimization takes into account multiple profiles of energy prices with the objective to minimize the total cost, considering:
- Imported energy cost
- Exported energy income
- Income from balancing markets (turn up/down demand), for the flexible demand only
Demand optimization
To minimize the energy cost using the flexibility from a power demand profile, the objective function of the optimization problem has been raised in the following way:
The objective function and constraints of this optimization problem are represented below, where:
- : Number of time intervals within the optimization window
- : Local power generation time-series vector
- : Non-flexible (static) load time-series vector
- : Flexible load time-series vector (if not optimized)
- : Optimal flexible load time-series vector (decision variable)
- : Imported power (decision variable, non-negative)
- : Exported power (decision variable, non-negative)
- : Binary grid mode (1 = importing, 0 = exporting)
-
: Maximum power that the flexible load can consume
(
load_capacity) - , : Grid import and export capacity
- : Imported energy price
- : Exported energy price
- : Balancing price for turn-up power
- : Balancing price for turn-down power
- : Time horizon, in number of time slots
- : Ramping penalty weight
Since is a known input, the two balancing terms collapse into a single linear coefficient on the decision variable, and the implementation minimizes the equivalent expression (both differ only by a constant):
Note that penalizes slot-to-slot changes in the optimal load (ramping), not the deviation from the original flexible load profile.
The decision variables are , i.e. continuous variables, and the solver depends on :
lambda |
Solver | Extra variables | Import/export exclusivity |
|---|---|---|---|
0 (default) |
HiGHS MILP | ( binary) | Enforced exactly by the binary grid mode |
> 0 |
OSQP QP | — | No binary variable; see the note below |
Moreover, this optimization problem has the following constraints:
- The energy consumed by the flexible load must remain the same than the expected behavior:
- Energy balance behind-the-meter:
- The optimal flexible load must respect both its own maximum power and the grid connection capacity, combined into a single box bound exactly as described in the net power article:
- The imported and exported power must remain between 0 and the grid import and export capacity:
- Imported and exported energy can not be positive at the same time. When this is enforced exactly with one binary grid mode variable per time slot:
Where and are the tightest per-slot bounds implied by the optimal load bounds and the site balance:
- The forward and backward time horizon constraints are identical to those described in the net power article, and are not repeated here.
When lambda > 0. The quadratic
ramping term makes the problem a QP, which is solved with OSQP over the
same
continuous variables but without the binary grid mode.
Two adjustments keep that QP well posed:
- Export prices are clipped to . Without this, a slot where exporting pays more than importing costs would make the objective unbounded below. A warning is emitted once when clipping occurs.
- The upper bounds on and are tightened to and above, which is what keeps the problem bounded when import prices are negative.
Because there is no binary variable on this path, the solver may in
principle return a small simultaneous import and export in the same
slot. Since only the net flow
is physically meaningful and it is preserved, the reported profiles are
collapsed to a single direction per slot afterwards. If a strict
per-slot exclusivity guarantee is required, use the default
lambda = 0.
Infeasible windows. The same minimal grid capacity relaxation described in the net power article applies here, falling back to the original profile if the solver still fails.
Battery optimization
To minimize the energy cost using the flexibility from a battery, the objective function of the optimization problem has been raised in the following way:
Where:
- : Number of time intervals within the optimization window
- : Local power generation time-series vector
- : Power load time-series vector
- : Battery charging power (decision variable, non-negative)
- : Battery discharging power (decision variable, non-negative)
- : Net battery exchange (positive = charging)
- : Imported power (decision variable, non-negative)
- : Exported power (decision variable, non-negative)
- : Binary grid mode (1 = importing, 0 = exporting)
- : Imported energy price
- : Exported energy price
- : Ramping penalty weight
Additionally, this optimization problem also counts with the following parameters used in the constraints below:
- : Battery capacity
- : Maximum charging power
- : Maximum discharging power
- : Minimum state of charge of the battery (%)
- : Maximum state of charge of the battery (%)
- : State of charge at the beginning/end of the optimization window (%)
-
: Battery charging efficiency (
charge_eff) -
: Battery discharging efficiency (
discharge_eff) -
: Cycle degradation cost per kWh cycled (
cycle_cost)
Unlike the net power objective, the cost objective uses separate charging and discharging variables and , which is what allows the charging and discharging efficiencies to be applied correctly in the state-of-charge constraints.
Note also that the balancing market prices (, ) only apply to the flexible demand optimization; they are not part of the battery objective.
Optimization constraints:
- Charging and discharging power limits:
There is no binary battery mode variable forcing
.
Simultaneous charging and discharging is avoided implicitly: it either
has no benefit (at equal prices there is nothing to gain from cycling
energy through the battery) or is directly penalized, since a positive
cycle_cost makes it economically self-defeating.
- State of charge limits, considering charging and discharging efficiencies:
- The balance of stored energy must be 0 at the end of the optimization window to have the same initial state of charge at the beginning of every optimization window:
- Energy balance behind-the-meter:
- The imported and exported power must remain between 0 and the grid import and export capacity:
Solver paths
The combination of lambda and cycle_cost
selects one of three formulations:
lambda |
cycle_cost |
Solver | Variables | Import/export exclusivity |
|---|---|---|---|---|
0 (default) |
0 (default) |
HiGHS MILP | + binary | Enforced exactly by the binary grid mode |
0 |
> 0 |
HiGHS LP | Not enforced; see below | |
> 0 |
any | OSQP QP | + SOC state | Not enforced; see below |
MILP path (default). Imported and exported energy can not be positive at the same time, enforced with one binary grid mode variable per time slot:
This is the most accurate but also the most expensive path, since a branch-and-bound problem is solved for every optimization window.
LP path (cycle_cost > 0). A positive
cycle cost adds the linear term
to the objective, which converts the per-kWh degradation cost into a
penalty on discharged power, so the optimizer trades energy cost savings
against battery wear. This penalty alone makes simultaneous charging and
discharging uneconomical, so the binary grid mode is dropped and the
problem becomes a pure LP — substantially faster than the MILP. On this
path the import and export bounds are tightened to their physically
achievable values, which is what keeps the LP bounded when import prices
are negative:
QP path (lambda > 0). The quadratic
ramping term is applied to the net battery power
and the problem is solved with OSQP. Both cycle_cost and
lambda are active simultaneously when both are positive. As
for the demand QP, export prices are clipped to
to keep the objective bounded below, with a warning emitted once. If
OSQP fails to converge, the solver falls back to the LP path, which
retains the cycle cost but loses the ramping term.
Neither the LP nor the QP path includes a binary variable enforcing
.
In practice the state-of-charge constraint couples all slots of the
window strongly enough that within-slot simultaneous flows are not
profitable. When a strict exclusivity guarantee is required, use the
default MILP path (lambda = 0 and
cycle_cost = 0).
