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), with
opt_objective = "grid" - Minimize only the power that exceeds the grid connection capacity,
with
opt_objective = "capacity" - Minimize the energy cost, with
opt_objective = "cost" - Minimize a weighted combination of net power and cost, with a
numeric
opt_objectivebetween0and1
In this article, we’ll cover the optimization problem for the first two objectives, net power minimization and grid capacity minimization, for both the flexible demand (e.g. heatpumps, electric vehicles, etc.) and the battery.
Demand optimization
Net power objective
To minimize the energy exchanged with the distribution grid while maximizing the use of local generation, using the flexibility from a power demand profile, 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
- : Non-flexible (static) load time-series vector
- : Flexible load time-series vector (if not optimized)
- : Optimal flexible load time-series vector (decision variable)
-
: Maximum power that the flexible load can consume
(
load_capacity) - , : Grid import and export capacity
- : Time horizon, in number of time slots
- : Ramping penalty weight
The squared term penalizes high values of net power, so the optimization always results in the most flat as possible net power profile (peak shaving). The second term penalizes slot-to-slot changes in the optimal load, so a positive results in a smoother flexible load profile.
The only decision variable is the optimal load , so this objective is solved as a QP over variables. Grid import and export are not explicit variables here: they are fully determined by the energy balance .
This optimization problem has the following constraints:
- The energy consumed by the flexible load must remain the same than the expected behavior:
- The optimal flexible load must respect both its own maximum power and the grid connection capacity. Since is the only decision variable, the grid limits
are combined with into a single box bound:
Note the lower bound: when the local generation exceeds the static load by more than the export capacity, the flexible load is forced to consume the surplus that cannot be exported.
At the same time, we can optimize the flexible energy demand with two opposite approaches:
- Postpone the consumption to later time-slots (shift the energy forward)
- Consume now (store) the energy that will be consumed later (shift the energy backward)
We consider both approaches for different objectives or applications. The energy shift is always done within a maximum time horizon () to consider realistic scenarios. For example, the time horizon for the energy demand of a water boiler could have a time horizon of 6 hours, because it wouldn’t make sense to heat the water more than 6 hours before the final consumption. Each one of these approaches brings extra constraints to the optimization problem:
If the energy can only be shifted forward:
- The cumulative sum of the optimal load must be higher than the cumulative sum of the original flexible load except the last time slots, and lower than the total cumulative sum of the original flexible load (energy can only be shifted forwards):
- The maximum values for the optimal demand will depend on the time horizon :
If the energy can only be shifted backward:
- The cumulative sum of the optimal demand must be higher than the total cumulative sum of the original demand (energy can only be shifted backwards), and lower than the cumulative sum of the original demand except the following time slots:
- The maximum values for the optimal demand will depend on the time horizon :
In both cases these horizon caps are intersected with the box bound above, so the effective upper bound on is the minimum of the horizon cap and .
Infeasible windows. When the grid capacities make a window infeasible, the grid limits are not simply dropped. Instead each capacity is raised only as far as the original, unshifted profile already requires, i.e. and . Slots that were within their capacity keep the true limit, so the optimizer can never create a violation worse than the input profile already had. With these limits the do-nothing point is feasible by construction. If the solver still fails, the original profile is returned unchanged.
Grid capacity objective
The "capacity" objective does not flatten the whole net
power profile. Instead, it minimizes the amount of flexible
energy that has to be moved in order to respect the grid
connection limits, and only that moved energy is then reshaped. This is
useful when the flexible load should be disturbed as little as possible
and the only real requirement is to stay within the grid connection
capacity.
It is solved in two stages:
Stage 1 — select the minimal slice (LP, HiGHS). With decision variables (the slice of flexible load removed from slot ) and (where that slice is placed):
subject to , the same forward/backward horizon and energy conservation constraints as above applied to the pair , and the requirement that the resulting profile respects the grid connection box:
If the optimal slice is zero (the original profile already respects the grid limits), the flexible load is returned unchanged.
Stage 2 — reshape only that slice (QP, OSQP). The untouched part is added to the static load, and the net power formulation of the previous section is applied to the slice alone, with its maximum power reduced accordingly. The final result is the sum of the fixed part and the optimized slice.
If Stage 1 is infeasible under the true capacities, the same minimal relaxation described above is applied and the plain net power formulation is used on the full flexible load.
Battery optimization
Net power objective
To minimize the energy exchanged with the distribution grid while maximizing the use of local generation, 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
- : Net battery power exchange (decision variable, positive = charging, negative = discharging)
- : 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 (%)
- , : Grid import and export capacity
Note that, unlike the cost objective, the net power objective uses a single net battery variable instead of separate charging and discharging variables. The problem is therefore a QP over continuous variables, with no binary variables: since charging and discharging are the two signs of one variable, they cannot happen simultaneously by construction.
Optimization constraints:
- Battery power limits, combined with the grid connection capacity. The battery power is bounded by its own charging and discharging limits and, at the same time, the net site power must respect the grid connection:
Both restrictions are imposed as a single box bound:
- State of charge limits:
- The balance of stored energy must be 0 at the end of the optimization window to keep the same initial state of charge:
Efficiencies. The charge_eff and
discharge_eff parameters of
add_battery_optimization() are not applied
by the net power and grid capacity objectives, which assume a lossless
battery
().
Round-trip losses are only embedded in the state-of-charge constraints
of the cost
objective.
Unreachable capacities. A capacity the battery cannot meet is approached, not dropped. Instead of a hard box, each direction carries a slack variable measuring by how much the slot misses its capacity:
The slack is bounded by how far the pre-battery profile already missed the capacity,
and penalised in the objective. Three consequences follow: with the slack at its ceiling is always feasible, so a window can never be infeasible; the result is never worse than the profile the battery started from; and slots that can meet their capacity have a ceiling of zero, so they stay hard capped. Where the capacity is out of reach, the penalty makes the battery spend its full physical capability approaching it instead of reverting to plain flattening.
When no slot misses its capacity — including the default of no capacity limits at all — every ceiling is zero, the slack variables are dropped, and the problem reduces exactly to the box-constrained one above.
Forced flows. A negative is an obligation to export at least , and a negative an obligation to import. The constraint covers both without special casing. Because the slack penalty dominates the objective, such an obligation outranks it: the battery meets the capacity first and flattens second.
If OSQP does not converge, a greedy forward-pass heuristic clipped to the feasible box is used, and as a last resort the battery is disabled for that window.
Grid capacity objective
Like for the demand, the "capacity" objective for the
battery only targets the power that exceeds the grid connection
capacity, rather than flattening the whole net power profile. It keeps
every constraint of the net power formulation above —
the same box, state of charge, energy balance and slack rows — and
changes only the objective, to the least battery that
satisfies them:
The capacity is already enforced by the constraints, so the objective no longer has to mention the net power at all. Dropping the term is the whole difference between the two objectives: with it, the optimizer flattens the profile and spends whatever battery it is given; without it, it spends as little as the capacity allows.
Two consequences are worth spelling out, because they are what makes
this objective answer a different question from "grid":
The battery discharges onto the capacity, not below it. Since the quadratic penalises battery power rather than net power, every kW of discharge past the binding capacity costs the objective and buys nothing. The net profile therefore rests exactly on (or ) through the violation and is left untouched elsewhere, and the energy that has to be given back is recharged at the lowest power the window allows.
The nameplate no longer decides how hard the battery works — the capacity does. Two batteries that can both clear a window return the same profile, so the objective can be used to find the minimum battery a congestion limit requires. Earlier versions instead reserved a slice of the battery sized to the overshoot volume and ran the net power objective on that slice; the reserve was a proxy for “use as little as possible” that both under- and over-shot, since a reserve too small could not clear the window while a reserve large enough was still spent flattening.
If the pre-battery profile violates neither capacity in a window, there is nothing to solve — the least battery that keeps it within its limits is no battery — and the window returns without calling the solver.
