Skip to content

Demand Response Optimization for Battery Energy Storage (Stage 2) - #808

Draft
abhineet-gupta wants to merge 5 commits into
NatLabRockies:developfrom
abhineet-gupta:PLM_DR
Draft

Demand Response Optimization for Battery Energy Storage (Stage 2)#808
abhineet-gupta wants to merge 5 commits into
NatLabRockies:developfrom
abhineet-gupta:PLM_DR

Conversation

@abhineet-gupta

@abhineet-gupta abhineet-gupta commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Demand Response Optimization for Battery Energy Storage (Stage 2)

This PR is a continuation of PR 679.
It adds a Pyomo optimization based controller for the BESS system.
The controller optimizes the battery dispatch to minimize the Co-Op's expenditure while allowing demand response capabilities based on G&T requirements during peak window.

Section 1: Type of Contribution

  • Feature Enhancement
    • Framework
    • New Model
    • Updated Model
    • Tools/Utilities
    • Other (please describe):
  • Bug Fix
  • Documentation Update
  • CI Changes
  • Other (please describe):

Section 2: Draft PR Checklist

  • Open draft PR
  • Describe the feature that will be added
  • Fill out TODO list steps
  • Describe requested feedback from reviewers on draft PR
  • Complete Section 7: New Model Checklist (if applicable)

TODO:

Type of Reviewer Feedback Requested (on Draft PR)

Structural feedback:
Do you agree with the terminology used in describing this approach in documentation.

Implementation feedback:
Is this the correct way to add highs solver for pyomo to github workflow. It has already been added to environment.yml.
Other feedback:

Section 3: General PR Checklist

  • PR description thoroughly describes the new feature, bug fix, etc.
  • Added tests for new functionality or bug fixes
  • Tests pass (If not, and this is expected, please elaborate in the Section 6: Test Results)
  • Documentation
    • Docstrings are up-to-date
    • Related docs/ files are up-to-date, or added when necessary
    • Documentation has been rebuilt successfully
    • Examples have been updated (if applicable)
  • CHANGELOG.md
    • At least one complete sentence has been provided to describe the changes made in this PR
    • After the above, a hyperlink has been provided to the PR using the following format:
      "A complete thought. [PR XYZ]((https://github.com/NatLabRockies/H2Integrate/pull/XYZ)", where
      XYZ should be replaced with the actual number.

Section 4: Related Issues

Section 5: Impacted Areas of the Software

Section 5.1: New Files

Section 5.2: Modified Files

  • h2integrate/control/control_strategies/storage/plm_optimized_storage_controller.py
    • Modified implementation under PeakLoadManagementOptimizedStorageController

Section 6: Additional Supporting Information

Section 7: Test Results, if applicable

Section 8 (Optional): New Model Checklist

  • Model Structure:
    • Follows established naming conventions outlined in docs/developer_guide/coding_guidelines.md
    • Used attrs class to define the Config to load in attributes for the model
      • If applicable: inherit from BaseConfig or CostModelBaseConfig
    • Added: initialize() method, setup() method, compute() method
      • If applicable: inherit from CostModelBaseClass
  • Integration: Model has been properly integrated into H2Integrate
    • Added to supported_models.py
    • If a new commodity_type is added, update create_financial_model in h2integrate_model.py
  • Tests: Unit tests have been added for the new model
    • Pytest-style unit tests
    • Unit tests are in a "test" folder within the folder a new model was added to
    • If applicable add integration tests
  • Example: If applicable, a working example demonstrating the new model has been created
    • Input file comments
    • Run file comments
    • Example has been tested and runs successfully in test_all_examples.py
  • Documentation:
    • Write docstrings using the Google style
    • Model added to the main models list in docs/user_guide/model_overview.md
      • Model documentation page added to the appropriate docs/ section
      • <model_name>.md is added to the _toc.yml
    • Run generate_class_hierarchy.py to update the class hierarchy diagram in docs/developer_guide/class_structure.md

@abhineet-gupta
abhineet-gupta changed the base branch from main to develop July 21, 2026 13:56
@abhineet-gupta
abhineet-gupta requested a review from vijay092 July 21, 2026 13:58
@abhineet-gupta
abhineet-gupta force-pushed the PLM_DR branch 3 times, most recently from 96a279f to bb33d74 Compare July 21, 2026 14:20

@johnjasa johnjasa left a comment

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.

This is great, thanks for the follow-on PR, @abhineet-gupta! I love how you updated the code, examples, tests, and docs. Really slick stuff with some good solver generalizations as well.

I've left some questions and suggestions, nothing huge but I do think some of the clarifications will be useful for users approaching this code.

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.

Could you please add definitions or explanations for the shaded gold regions in the figure or legends?

Comment thread environment.yml
dependencies:
- python=3.11
- glpk
- highspy

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.

Thanks for adding the highspy req here! Could you please add it to the conda install command within the install.md doc page too please?

)

PeakLoadManagementOptimizedStorageController.glpk_solve_call(model)
PeakLoadManagementOptimizedStorageController.pyomosolver_solve_call(model)

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.

I like this generalization away from glpk to the pyomosolver! Thanks

Comment on lines +836 to +842
"""Compute the cost a G$T charges to a CoOp based on the current grid price.

Args:
lmp (float): Current grid price (locational marginal price), e.g., in $/MWh.

Returns:
float: Cost charged by the G$T in the same units as `lmp`.

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.

Should this be G&T instead of G$T? Maybe I'm just not familiar with the initialism

Returns:
float: Cost charged by the G$T in the same units as `lmp`.
"""
return 1.05 * lmp + 20

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.

Should these multiplicative and offset coefficients be input or settable in some way? Or is it okay that they're hardcoded here? Will they always be these values?

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.

I agree.


@staticmethod
def glpk_solve_call(
def pyomosolver_solve_call(

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.

My own curiosity: why move away from GLPK here to use highs? Is it more robust, faster, better for this problem, or something else? Should we avoid using GLPK in other contexts within H2I?

```

# Optimized Demand Response Controller
## Optimized Demand Response Controller

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.

Suggestion: the actual class file has PeakLoadManagement in the name; I'd suggest keeping the same naming convention here in the docs, and maybe even referencing or linking to the class itself so users can dig deeper.

object.__setattr__(self.config, "max_charge_rate", float(inputs["max_charge_rate"][0]))
if "storage_capacity" in inputs:
object.__setattr__(self.config, "max_capacity", float(inputs["storage_capacity"][0]))

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.

Is this just no longer necessary? Can we still optimize the max_charge_rate and storage_capacity values at the OM level and they'll get updated? It looks like yes as they're used on line 250, but I'm asking to double-check!

@vijay092 vijay092 left a comment

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.

Thank you for working on this! I had some minor comments.


**Given:**
- $\lambda_t$ := `supervisory_signal`: price, demand, or price $\times$ demand time series at timestep $t$
- $\lambda_t$ := `lmp_signal`: electricity price time series at timestep $t$

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.

I'd call this a supervisory signal, since it could be either LMP or demand- essentially any external signal that the battery needs to respond to.


$$
\max_{u_t, v_t,p_{d,t}, p_{c,t}} \quad \gamma \cdot \Delta t \sum_{t \in \mathcal{T}} p_{d,t}
\min_{u_{gt,t},u_{coop,t},v_t,p^d_{gt,t},p^d_{coop,t},pc_t,\text{SOC}_t,p_{gt2coop,t}} \quad

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.

Could you confirm if the units are uniform in the first and second term? Incentive is $ / kwh and LMP is $/MWh?

Returns:
float: Cost charged by the G$T in the same units as `lmp`.
"""
return 1.05 * lmp + 20

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.

I agree.

discharge_gt = pyomo.value(model.discharge_gt[t]) # type: ignore[index]
discharge_coop = pyomo.value(model.discharge_coop[t]) # type: ignore[index]
with subtests.test(f"No simultaneous charge, discharge_gt or discharge_coop at t={t}"):
assert not (charge > 0.5 and discharge_gt > 0.5 and discharge_coop > 0.5)

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.

This allows two conditions to be > 0.5 which should not happen, right?


# Incentive revenue is earned for every kWh discharged.
# Power transmitted to CoOp
m.p_tocoop = pyomo.Var(

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.

p_tocoop is unbounded above which might cause issues later on. Good to add an upper bound.

u_dicharge_coop = controller.discharge_coop_bin_history
v_charge = controller.charge_bin_history
p_dicharge_gt = controller.p_discharge_gt_history
p_dicharge_coop = controller.p_discharge_coop_history

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.

typo

length ``n_control_window_hours`` per solve.
lmp_signal (list[float]): Locational Marginal Pricing (LMP)
forecast time series.
demand_signal (list[float]): Consumer demand forcast time series

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.

typo

)

# Objective is maximizing incentive revenue is earned for every kWh discharged and
# minimzing the cost of energy for the CoOp.

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.

typo

@@ -304,13 +306,34 @@ def pyomo_dispatch_solver(
# Solve the optimzation problem

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.

typo

with subtests.test("Discharge never above max_charge_rate"):
assert np.all(discharge <= 1.0 + 1e-4)

with subtests.test("SOC at t=0 equal to init_soc_fraction"):

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.

Why is this removed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants