Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
410e488
Add PLM optimized dispatch controller, example 34, and tests
vijay092 Apr 22, 2026
cf46abe
Make sure the example runs
vijay092 Apr 22, 2026
e32b730
Add docs
vijay092 Apr 23, 2026
561a6da
Update docs and add figure
vijay092 Apr 23, 2026
7afb43a
Edit docs page
vijay092 Apr 23, 2026
9b9ac9b
Edit docstrings to include raises
vijay092 Apr 23, 2026
3d4735f
Add integartion tests
vijay092 Apr 25, 2026
42b63c3
Add PLM optimized dispatch controller, example 34, and tests
vijay092 Apr 26, 2026
b0a030c
Make sure the example runs
vijay092 Apr 22, 2026
125b48f
Add docs
vijay092 Apr 26, 2026
7903e39
Add docs
vijay092 Apr 23, 2026
eb9b449
Update docs and add figure
vijay092 Apr 23, 2026
053e586
Edit docs page
vijay092 Apr 23, 2026
2f0a325
Edit docstrings to include raises
vijay092 Apr 23, 2026
86c6fc4
Correct formatting
vijay092 Apr 27, 2026
a7fc28c
FIX formatting in supported modesl
vijay092 Apr 27, 2026
057d8fa
Fix formatting in test_all_ex
vijay092 Apr 27, 2026
0810a9a
Fix formatting in supported_models
vijay092 Apr 27, 2026
ac0c0a0
More formatting fixes
vijay092 Apr 27, 2026
fa9a4a7
Change code to handle varibale timesteps
vijay092 Apr 27, 2026
db9923f
Pre-commit fixes
vijay092 Apr 28, 2026
b25e5dd
Remove notebook
vijay092 Apr 28, 2026
966f75e
Add min separation input and make sure methods run correctly
vijay092 Apr 28, 2026
c33d498
Fix bug in peak window start time definition in example
vijay092 Apr 28, 2026
6ddb79a
Change user defined param fromn_max)hrs to n_max_events
vijay092 Apr 28, 2026
4c94935
Address reviewer comments
vijay092 Apr 29, 2026
aa7d94c
Make description in tech config more specific
vijay092 Apr 29, 2026
e1bd578
Move standard methds to the top and delete demand csv
vijay092 Apr 29, 2026
3281afa
Add figure in docs
vijay092 Apr 29, 2026
34d9cea
Make sure tests run
vijay092 Apr 29, 2026
d997dec
Make sure test_all_examples runs
vijay092 Apr 30, 2026
33f244f
Run pre-commit on all files
vijay092 Apr 30, 2026
92b8bcf
Address Elenya's comments
vijay092 Apr 30, 2026
5514e0d
Merge remote-tracking branch 'upstream/develop' into peakload-optimized
vijay092 Apr 30, 2026
feaaa70
Change n_control_window to n_control_window_hours
vijay092 Apr 30, 2026
09b8e52
Use methods and existing utils for example
vijay092 Apr 30, 2026
040e9c9
Solve for optimal power rather than assuming P_max
vijay092 May 1, 2026
406fab9
Give user option to either pick inc/event or /kwh
vijay092 May 3, 2026
cb1740c
Add comments for inc/event conversion
vijay092 May 3, 2026
bba953d
Merge remote-tracking branch 'upstream/develop' into peakload-optimized
vijay092 May 3, 2026
91c1cbc
Edit docs to include new definitions
vijay092 May 4, 2026
c127b15
Edit config comments
vijay092 May 4, 2026
10aee14
Minor edits to configs
vijay092 May 4, 2026
dec2b05
Add tests for storageperformancemodel
vijay092 May 4, 2026
d2c8ac1
Raise Valueerror if min peak separation < event dur
vijay092 May 4, 2026
4d8656a
Remove Valueerror if min peak separation < event dur
vijay092 May 4, 2026
e024fc8
Merge remote-tracking branch 'upstream/develop' into peakload-optimized
vijay092 May 5, 2026
9e359a3
Update h2integrate/control/control_strategies/storage/plm_optimized_s…
vijay092 May 5, 2026
eda288a
Update h2integrate/control/control_strategies/storage/plm_optimized_s…
vijay092 May 5, 2026
ae7eb89
Update h2integrate/control/control_strategies/storage/plm_optimized_s…
vijay092 May 5, 2026
d4511d2
Apply pre-commit formatting
vijay092 May 5, 2026
a290876
Update docs/control/pyomo_controllers.md
vijay092 May 5, 2026
9bd1834
Fix SOC min max notation
vijay092 May 6, 2026
cad9201
Merge branch 'peakload-optimized' of https://github.com/vijay092/H2In…
vijay092 May 6, 2026
b8d8182
Change round to ceil in steps_per_event calc
vijay092 May 6, 2026
05ba4c6
Add subtests
vijay092 May 6, 2026
6f9417e
Merge branch 'develop' into peakload-optimized
johnjasa May 7, 2026
060a22a
Minor changes to comments and subtests
vijay092 May 7, 2026
a340622
Change round to ceil when computing n_days
vijay092 May 7, 2026
0831c29
Update h2integrate/control/control_strategies/storage/plm_optimized_s…
vijay092 May 7, 2026
7a63466
Add _time_step_bound and make sure init_sc is within bounds
vijay092 May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ repos:
- id: yamlfix
# Exclude YAML files that are used as inputs for testing or examples, or ones for desired schedule in HOPP as they
# expect misformatted YAML files.
exclude: ^(h2integrate/core/test/inputs/.*|examples/11_hybrid_energy_plant/tech_inputs/desired_schedules/.*|examples/33_peak_load_management/demand_profiles/.*)$
exclude: ^(h2integrate/core/test/inputs/.*|examples/11_hybrid_energy_plant/tech_inputs/desired_schedules/.*|examples/33_peak_load_management/demand_profiles/.*|examples/34_plm_optimized_dispatch/demand_profiles/.*)$
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.8.1
Expand Down
Binary file added docs/control/figures/plm_optimized_dispatch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
121 changes: 121 additions & 0 deletions docs/control/pyomo_controllers.md
Comment thread
jaredthomas68 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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$

Comment thread
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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

![](./figures/plm_optimized_dispatch.png)
1 change: 1 addition & 0 deletions docs/user_guide/model_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ Below summarizes the available performance, cost, and financial models for each
- `'DemandOpenLoopStorageController'`: open-loop control; manages resource flow based on demand and storage constraints
- `'HeuristicLoadFollowingStorageController'`: open-loop control that works on a time window basis to set dispatch commands; uses Pyomo
- `'PeakLoadManagementHeuristicOpenLoopStorageController'`: open-loop control that reduces peaks rather than trying to meet a load
- `'PeakLoadManagementOptimizedStorageController'`: optimized controller for demand response that works on a time window basis.
- Optimized Dispatch:
- `'OptimizedDispatchStorageController'`: optimization-based dispatch using Pyomo

Expand Down
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
Loading
Loading