Skip to contents

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_objective between 0 and 1

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:

minOt∑t=1T(Ot+LSt−Gt)2+λ∑t=1T−1(Ot+1−Ot)2 \min_{O_t} \; \sum_{t=1}^{T} \left(O_t + LS_t - G_t\right)^2 + \lambda \sum_{t=1}^{T-1}\left(O_{t+1} - O_t\right)^{2}

Where:

  • TT : Number of time intervals within the optimization window
  • GtG_t : Local power generation time-series vector
  • LStLS_t : Non-flexible (static) load time-series vector
  • LFtLF_t : Flexible load time-series vector (if not optimized)
  • OtO_t : Optimal flexible load time-series vector (decision variable)
  • LFmaxtLFmax_t : Maximum power that the flexible load can consume (load_capacity)
  • ICtIC_t, ECtEC_t : Grid import and export capacity
  • hh : Time horizon, in number of time slots
  • λ\lambda : 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 λ\lambda results in a smoother flexible load profile.

The only decision variable is the optimal load OtO_t, so this objective is solved as a QP over TT variables. Grid import and export are not explicit variables here: they are fully determined by the energy balance Ot+LSt−GtO_t + LS_t - G_t.

This optimization problem has the following constraints:

  • The energy consumed by the flexible load must remain the same than the expected behavior:

∑t=1TOtΔt=∑t=1TLFtΔt \sum_{t=1}^T O_t \Delta t = \sum_{t=1}^T LF_t \Delta t

  • The optimal flexible load must respect both its own maximum power and the grid connection capacity. Since OtO_t is the only decision variable, the grid limits

−ECt≤Ot+LSt−Gt≤ICtt∈T -EC_t \;\le\; O_t + LS_t - G_t \;\le\; IC_t \quad t \in T

are combined with 0≤Ot≤LFmaxt0 \le O_t \le LFmax_t into a single box bound:

O_t≤Ot≤LF¯tt∈T \underline{O}_t \;\le\; O_t \;\le\; \overline{LF}_t \quad t \in T

LF¯t=min⁡(max(Gt−LSt+ICt,0),LFmaxt),O_t=min⁡(max(Gt−LSt−ECt,0),LF¯t) \overline{LF}_t = \min\!\left(\max\!\left(G_t - LS_t + IC_t,\; 0\right),\; LFmax_t\right), \qquad \underline{O}_t = \min\!\left(\max\!\left(G_t - LS_t - EC_t,\; 0\right),\; \overline{LF}_t\right)

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:

  1. Postpone the consumption to later time-slots (shift the energy forward)
  2. 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 (hh) 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 OO must be higher than the cumulative sum of the original flexible load LFLF except the last hh time slots, and lower than the total cumulative sum of the original flexible load LFLF (energy can only be shifted forwards):

∑t=1u−hLFt≤∑t=1uOt≤∑t=1uLFtu=1…T \sum_{t = 1}^{u-h} LF_t \le \sum_{t=1}^u O_t \le \sum_{t=1}^u LF_t \quad u = 1 \dots T

  • The maximum values for the optimal demand OtO_t will depend on the time horizon hh:

Ou≤∑t=u−huLFtu=1…T O_u \le \sum_{t = u-h}^u LF_t \quad u = 1 \dots T

If the energy can only be shifted backward:

  • The cumulative sum of the optimal demand OO must be higher than the total cumulative sum of the original demand LFLF (energy can only be shifted backwards), and lower than the cumulative sum of the original demand LFLF except the following hh time slots:

∑t=1uLFt≤∑t=1uOt≤∑t=1u+hLFtu=1…T \sum_{t=1}^u LF_t \le \sum_{t=1}^u O_t \le \sum_{t=1}^{u+h} LF_t \quad u = 1 \dots T

  • The maximum values for the optimal demand OtO_t will depend on the time horizon hh:

Ou≤∑t=uu+hLFtu=1…T O_u \le \sum_{t = u}^{u+h} LF_t \quad u = 1 \dots T

In both cases these horizon caps are intersected with the box bound above, so the effective upper bound on OtO_t is the minimum of the horizon cap and LF¯t\overline{LF}_t.

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. max⁡(ICt,LSt+LFt−Gt)\max(IC_t,\; LS_t + LF_t - G_t) and max⁡(ECt,Gt−LSt−LFt)\max(EC_t,\; G_t - LS_t - LF_t). 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 O=LFO = LF is feasible by construction. If the solver still fails, the original profile LFLF 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 MtM_t (the slice of flexible load removed from slot tt) and OtMO^M_t (where that slice is placed):

minMt,OtM∑t=1TMt \min_{M_t,\, O^M_t} \; \sum_{t=1}^{T} M_t

subject to 0≤Mt≤LFt0 \le M_t \le LF_t, the same forward/backward horizon and energy conservation constraints as above applied to the pair (M,OM)(M, O^M), and the requirement that the resulting profile respects the grid connection box:

O_t≤LFt−Mt+OtM≤LF¯tt∈T \underline{O}_t \;\le\; LF_t - M_t + O^M_t \;\le\; \overline{LF}_t \quad t \in T

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 LFt−MtLF_t - M_t is added to the static load, and the net power formulation of the previous section is applied to the slice MtM_t 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:

minBt∑t=1T(Lt+Bt−Gt)2+λ∑t=1T−1(Bt+1−Bt)2 \min_{B_t} \; \sum_{t=1}^{T} \left(L_t + B_t - G_t\right)^2 + \lambda \sum_{t=1}^{T-1}\left(B_{t+1} - B_t\right)^{2}

Where:

  • TT : Number of time intervals within the optimization window
  • GtG_t : Local power generation time-series vector
  • LtL_t : Power load time-series vector
  • BtB_t : Net battery power exchange (decision variable, positive = charging, negative = discharging)
  • λ\lambda : Ramping penalty weight

Additionally, this optimization problem also counts with the following parameters used in the constraints below:

  • BcapB_{cap} : Battery capacity
  • BcB_c : Maximum charging power
  • BdB_d : Maximum discharging power
  • SOCminSOC_{min} : Minimum state of charge of the battery (%)
  • SOCmaxSOC_{max} : Maximum state of charge of the battery (%)
  • SOCiniSOC_{ini} : State of charge at the beginning/end of the optimization window (%)
  • ICtIC_t, ECtEC_t : Grid import and export capacity

Note that, unlike the cost objective, the net power objective uses a single net battery variable BtB_t instead of separate charging and discharging variables. The problem is therefore a QP over TT 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:

−ECt≤Lt+Bt−Gt≤ICtt∈T -EC_t \;\le\; L_t + B_t - G_t \;\le\; IC_t \quad t \in T

Both restrictions are imposed as a single box bound:

max⁡(−Bd,Gt−Lt−ECt)≤Bt≤min⁡(Bc,Gt−Lt+ICt)t∈T \max\!\left(-B_d,\; G_t - L_t - EC_t\right) \;\le\; B_t \;\le\; \min\!\left(B_c,\; G_t - L_t + IC_t\right) \quad t \in T

  • State of charge limits:

SOCmin≤SOCini+100Bcap∑k=1tBkΔt≤SOCmaxt=1,…,T SOC_{min} \le SOC_{ini} + \frac{100}{B_{cap}} \sum_{k=1}^t B_k \, \Delta t \le SOC_{max} \quad t = 1, \dots, T

  • The balance of stored energy must be 0 at the end of the optimization window to keep the same initial state of charge:

∑t=1TBtΔt=0 \sum_{t=1}^T B_t \, \Delta t = 0

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 (ηc=ηd=1\eta_c = \eta_d = 1). 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 stI,stE≥0s^I_t,\, s^E_t \ge 0 measuring by how much the slot misses its capacity:

Bt−stI≤Gt−Lt+ICt,Bt+stE≥Gt−Lt−ECtt∈T B_t - s^I_t \;\le\; G_t - L_t + IC_t, \qquad B_t + s^E_t \;\ge\; G_t - L_t - EC_t \quad t \in T

The slack is bounded by how far the pre-battery profile already missed the capacity,

0≤stI≤max⁡(0,(Lt−Gt)−ICt),0≤stE≤max⁡(0,(Gt−Lt)−ECt) 0 \le s^I_t \le \max\!\left(0,\; (L_t - G_t) - IC_t\right), \qquad 0 \le s^E_t \le \max\!\left(0,\; (G_t - L_t) - EC_t\right)

and penalised in the objective. Three consequences follow: B=0B = 0 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 ICtIC_t is an obligation to export at least |ICt||IC_t|, and a negative ECtEC_t an obligation to import. The constraint −ECt≤Lt+Bt−Gt≤ICt-EC_t \le L_t + B_t - G_t \le IC_t 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:

minBt∑t=1TBt2+λ∑t=1T−1(Bt+1−Bt)2 \min_{B_t} \; \sum_{t=1}^{T} B_t^2 + \lambda \sum_{t=1}^{T-1}\left(B_{t+1} - B_t\right)^{2}

The capacity is already enforced by the constraints, so the objective no longer has to mention the net power at all. Dropping the (Lt−Gt)\left(L_t - G_t\right) 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 ICtIC_t (or −ECt-EC_t) 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 Bt=0B_t = 0 without calling the solver.