-
Notifications
You must be signed in to change notification settings - Fork 35
Demand Response Optimization for Battery Energy Storage #679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
410e488
cf46abe
e32b730
561a6da
7afb43a
9b9ac9b
3d4735f
42b63c3
b0a030c
125b48f
7903e39
eb9b449
053e586
2f0a325
86c6fc4
a7fc28c
057d8fa
0810a9a
ac0c0a0
fa9a4a7
db9923f
b25e5dd
966f75e
c33d498
6ddb79a
4c94935
aa7d94c
e1bd578
3281afa
34d9cea
d997dec
33f244f
92b8bcf
5514e0d
feaaa70
09b8e52
040e9c9
406fab9
cb1740c
bba953d
91c1cbc
c127b15
10aee14
dec2b05
d2c8ac1
4d8656a
e024fc8
9e359a3
eda288a
ae7eb89
d4511d2
a290876
9bd1834
cad9201
b8d8182
05ba4c6
6f9417e
060a22a
a340622
0831c29
7a63466
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -110,3 +110,124 @@ tech_to_dispatch_connections: [ | |
| ["battery", "battery"], | ||
| ] | ||
| ``` | ||
|
|
||
| # Optimized Demand Response Controller | ||
|
|
||
| This controller optimizes the dispatch of a Battery Energy Storage System (BESS) based on a pre-defined supervisory signal. This signal could be the Locational Marginal Price (LMP), a demand profile, or an $LMP\times demand$ product depending on the application. The objective is to maximize incentive payments to the battery, subject to constraints on the maximum number of dispatch events per month and on the battery state of charge. | ||
|
|
||
| The controller works at any simulation timestep resolution (`dt`). All time-based parameters ( `event_duration`, `min_peak_separation`) are specified in physical time units (hours, minutes, etc.) and are internally converted to timesteps using `dt`. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we should include a short description of event_duration in the docs.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
|
||
| ## Definitions | ||
|
|
||
| **Given:** | ||
| - $\lambda_t$ := `supervisory_signal`: price, demand, or price $\times$ demand time series at timestep $t$ | ||
| - $\Delta t$ := simulation timestep duration (hours), derived from `dt` in the plant config | ||
| - $\mathcal{W}$ := `peak_window`: set of timesteps eligible for dispatch (e.g., 12:00-20:00 each day) | ||
| - $\lambda_*$ := signal threshold = `signal_threshold_percentile`-th percentile of $\lambda_t$ over $\mathcal{W}$ | ||
| - `min_peak_separation` := minimum required time between two eligible peaks, expressed as a ``{units, val}`` dict. When set, only the first eligible peak is chosen. | ||
| - $\mathcal{E}$ := eligible peak timesteps: $\{t \in \mathcal{W} : \lambda_t \geq \lambda_*\}$, respecting `min_peak_separation` | ||
| - `event_duration` := total duration of one discharge event, expressed as a ``{units, val}`` dict (e.g. ``{units: h, val: 4}`` for a 4-hour event) | ||
| - $\mathcal{D}$ := dispatch window: $\pm$`event_duration`/2 neighbourhoods around each peak in $\mathcal{E}$ (equals $\mathcal{E}$ when `event_duration` is `null`) | ||
| - $\gamma$ := incentive revenue per kWh discharged (\$/kWh). Specified directly via `performance_incentive`, or derived from `performance_incentive_per_event` (\$/event) as $\gamma = \gamma_{\text{event}} / (\tau \cdot \Delta t \cdot P_{\max})$ | ||
| - $P_{\max}$ := `max_charge_rate` (kW): maximum charge and discharge rate | ||
| - $E_{\max} :=$ `max_capacity` $\times$ (`max_soc_fraction` $-$ `min_soc_fraction`): usable energy capacity (kWh) | ||
| - $\eta_c$ := `charge_efficiency`, $\quad \eta_d$ := `discharge_efficiency` | ||
| - $\text{SoC}_{\max}$ := `max_soc_fraction`, $\quad \text{SoC}_{\min}$ := `min_soc_fraction` | ||
| - `n_control_window_hours` := rolling horizon length in hours; converted to $T =$ `n_control_window_hours` / $\Delta t$ timesteps | ||
| - $\mathcal{T} := \{0, 1, \ldots, T-1\}$: timesteps in the current rolling window | ||
| - $\mathcal{M}_m$ := set of timesteps in month $m$, for $m = 1, \ldots, 12$ | ||
| - $N_{\max}$ := `n_max_events`: maximum number of discharge events per calendar month | ||
| - $\tau$ := `steps_per_event` : number of timesteps per event (1 when `event_duration` is `null`) | ||
| - $B_m$ := remaining event budget for month $m$ = $N_{\max}$ minus events already dispatched in prior windows | ||
|
|
||
| ## Dispatch Window Construction | ||
|
|
||
| Before the MILP is solved, the dispatch window $\mathcal{D}$ is built in two steps: | ||
|
|
||
| **Step 1 : Peak selection:** Within $\mathcal{W}$, timesteps at or above the `signal_threshold_percentile` of $\lambda_t$ are marked eligible: $\mathcal{E} = \{t \in \mathcal{W} : \lambda_t \geq \lambda_*\}$. If `min_peak_separation` is set, only the first peak is chosen. | ||
|
|
||
| **Step 2 : Event window expansion:** If `event_duration` is specified, each peak in $\mathcal{E}$ is expanded by $\pm$ `event_duration`/2 timesteps to form $\mathcal{D}$. If `event_duration` is `null`, $\mathcal{D} = \mathcal{E}$. | ||
|
|
||
| ## Decision Variables | ||
|
|
||
|
|
||
| - $u_t \in \{0, 1\}$ := discharge binary: 1 if a discharge event is active at timestep $t$; used for event counting and window feasibility constraints only | ||
| - $v_t \in \{0, 1\}$ := charge binary: 1 if a charge event is active at timestep $t$ | ||
| - $p_{d,t} \in [0,\, P_{\max}]$ := discharge power (kW) actually dispatched at timestep $t$ | ||
| - $p_{c,t} \in [0,\, P_{\max}]$ := charge power (kW) actually consumed at timestep $t$ | ||
| - $\text{SoC}_t \in [\text{SoC}_{\min},\, \text{SoC}_{\max}]$ := state of charge (fraction) at timestep $t$ | ||
|
|
||
|
jaredthomas68 marked this conversation as resolved.
|
||
| ## Optimization Problem | ||
|
|
||
| This optimization is executed for each rolling window. At each window boundary the terminal SoC is carried forward as the initial condition for the next window. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be good to highlight the variable used for rolling window. I believe you are incorporating MPC here? |
||
|
|
||
| ### Objective | ||
|
|
||
| Maximize total incentive revenue over the window: | ||
|
|
||
| $$ | ||
| \max_{u_t, v_t,p_{d,t}, p_{c,t}} \quad \gamma \cdot \Delta t \sum_{t \in \mathcal{T}} p_{d,t} | ||
| $$ | ||
|
|
||
| The factor $\Delta t$ converts power (kW) to energy (kWh), so the objective is correctly scaled at any timestep resolution. | ||
|
|
||
| ### Constraints | ||
|
|
||
| - Dispatch only within the event window $\mathcal{D}$: | ||
|
|
||
| $$ | ||
| u_t = 0 \qquad \forall\, t \notin \mathcal{D} | ||
| $$ | ||
|
|
||
| - Maximum $N_{\max}$ discharge events per month. Because `event_duration` fixes each event to exactly $\tau$ timesteps, the event cap translates directly into a timestep cap: | ||
|
|
||
| $$ | ||
| \sum_{t \in \mathcal{M}_m \cap \mathcal{T}} u_t \leq B_m \cdot \tau \qquad \forall\, m | ||
| $$ | ||
|
|
||
| After each window is solved, events are counted via rising-edge detection (a new event begins whenever $u_t = 1$ and $u_{t-1} = 0$) and $B_m$ is decremented accordingly for subsequent windows. | ||
|
|
||
| - Power is zero when the binary is 0, and at most $P_{\max}$ when it is 1: | ||
|
|
||
| $$ | ||
| p_{d,t} \leq P_{\max} \cdot u_t \qquad \forall\, t \in \mathcal{T} | ||
| $$ | ||
|
|
||
| $$ | ||
| p_{c,t} \leq P_{\max} \cdot v_t \qquad \forall\, t \in \mathcal{T} | ||
| $$ | ||
|
|
||
| - SoC evolution with continuous charge and discharge power: | ||
|
|
||
| $$ | ||
| \text{SoC}_{t} = \text{SoC}_{t-1} + \frac{\eta_c \cdot p_{c,t} \cdot \Delta t}{E_{\max}} - \frac{p_{d,t} \cdot \Delta t}{\eta_d \cdot E_{\max}} \qquad \forall\, t \in \mathcal{T},\, t > 0 | ||
| $$ | ||
|
|
||
| - SoC bounds: | ||
|
|
||
| $$ | ||
| \text{SoC}_{\min} \leq \text{SoC}_t \leq \text{SoC}_{\max} \qquad \forall\, t \in \mathcal{T} | ||
| $$ | ||
|
|
||
| - No simultaneous charge and discharge: | ||
|
|
||
| $$ | ||
| u_t + v_t \leq 1 \qquad \forall\, t \in \mathcal{T} | ||
| $$ | ||
|
|
||
| - No charging during the dispatch window (battery reserved for discharge): | ||
|
|
||
| $$ | ||
| v_t = 0 \qquad \forall\, t \in \mathcal{D} | ||
| $$ | ||
|
|
||
| - Variable domains: | ||
|
|
||
| $$ | ||
| u_t \in \{0, 1\}, \quad v_t \in \{0, 1\}, \quad p_{d,t},\, p_{c,t} \in [0,\, P_{\max}], \quad \text{SoC}_t \in [\text{SoC}_{\min},\, \text{SoC}_{\max}] \qquad \forall\, t | ||
| $$ | ||
|
|
||
|
|
||
| Example 34 performs the optimization with a synthetic LMP signal. The look-ahead horizon (`n_control_window_hours`) controls how many hours are optimized at once. Larger values improve solution quality but increase solve time. See the figure below for results. | ||
|
|
||
|  | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| name: H2Integrate_config | ||
| system_summary: PLM MILP-optimized battery dispatch | ||
| driver_config: driver_config.yaml | ||
| technology_config: tech_config.yaml | ||
| plant_config: plant_config.yaml |
Uh oh!
There was an error while loading. Please reload this page.