Demand Response Optimization for Battery Energy Storage (Stage 2) - #808
Demand Response Optimization for Battery Energy Storage (Stage 2)#808abhineet-gupta wants to merge 11 commits into
Conversation
96a279f to
bb33d74
Compare
johnjasa
left a comment
There was a problem hiding this comment.
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.
vijay092
left a comment
There was a problem hiding this comment.
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$ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
This controller needs both LMP and demand signals as inputs at the same time, hence the distinction.
There was a problem hiding this comment.
Is there just one demand signal? If the LMP could also be an upstream demand signal (as in the heuristic control work), then it may make sense to adjust the name as @vijay092 suggested. I see a supervisory signal file, but I only see one demand and lmp in the docs formulation.
|
|
||
| $$ | ||
| \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 |
There was a problem hiding this comment.
Could you confirm if the units are uniform in the first and second term? Incentive is $ / kwh and LMP is $/MWh?
There was a problem hiding this comment.
The optimization setup expects $/kWh for both. I will double check the input data in example to ensure that they follow the convention.
|
|
||
| # Incentive revenue is earned for every kWh discharged. | ||
| # Power transmitted to CoOp | ||
| m.p_tocoop = pyomo.Var( |
There was a problem hiding this comment.
p_tocoop is unbounded above which might cause issues later on. Good to add an upper bound.
There was a problem hiding this comment.
That's a good point. There would be a practical upper bound (based on interconnections etc). However, there is no theoretical upper bound (we can potentially model as large a G&T and CoOp under this framework as we like). Not sure how to pick a reasonable upper bound here.
There was a problem hiding this comment.
I think the upper bound could be exposed as a user input to the control in the tech_config. I think it would appropriate to have a default value of None but allow the user to set it as well. It may also work to pull it from the performance model rating information, perhaps with a buffer.
| 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"): |
There was a problem hiding this comment.
SOC at t = 0 is calculated after the first iteration is complete. So this test is only valid if battery doesn't discharge at the first step. Previously, that wasn't possible because the battery could only discharge during peak window but now it can also discharge at the first step.
…imizedStorageController
|
@johnjasa, |
jaredthomas68
left a comment
There was a problem hiding this comment.
Nice work, this is looking really good! Most of my comments are related to generality and needs of other projects hoping to use this work. If you have any questions or concerns I'm happy to clarify and discuss potential solutions.
|
|
||
| **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$ |
There was a problem hiding this comment.
Is there just one demand signal? If the LMP could also be an upstream demand signal (as in the heuristic control work), then it may make sense to adjust the name as @vijay092 suggested. I see a supervisory signal file, but I only see one demand and lmp in the docs formulation.
|
|
||
| $$ | ||
| \sum_{t \in \mathcal{M}_m \cap \mathcal{T}} u_t \leq B_m \cdot \tau \qquad \forall\, m | ||
| \sum_{t \in \mathcal{M}_m \cap \mathcal{T}} u_{gt,t} \leq B_m \cdot \tau \qquad \forall\, m |
There was a problem hiding this comment.
It appears that only the number of hours (or timesteps?) are constrained per month, rather than the number of events, which I think is intended to be a full battery cycle per event. From the plot, it looks like multiple discharge events happen per day, while we need to be able limit the number of events per day as well as the number of events per month. I may be misunderstanding, but I think it would be worth revisiting this formulation against the requirements. My understanding of what is needed:
- constrain the number of events in a month
- constrain the number of events in a day
- specify whether an event should be a full battery cycle or use some other definition of what an "event" is.
To be clear, I like what you have here and I think we should keep it. I also think it is not sufficient for the current project needs.
There was a problem hiding this comment.
It may be worth moving these demand and lmp profiles to the library/demand_profiles as I did in #773, but probably less important here since they are only used in one example so far.
There was a problem hiding this comment.
Can you add descriptions at the top of each lmp/demand/other signal with information about the data, where it came from, units, etc?
| storage_out = np.zeros(self.n_timesteps) | ||
| soc_out = np.zeros(self.n_timesteps) | ||
|
|
||
| # Per-timestep history of the MILP decision variables, accumulated |
There was a problem hiding this comment.
I think this is a fine way of storing if you just need them for use from the object immediately after a run. If you want them saved in the openmdao recorder database for later analysis from a saved file then we should consider making them outputs of the component.
| domain=pyomo.NonNegativeReals, | ||
| bounds=(0, P_max), | ||
| doc="Discharge power (kW) at timestep t", | ||
| doc="Discharge power to Co-Op (kW) at timestep t", |
There was a problem hiding this comment.
as determined by the local/coop
| m.soc_evolution = pyomo.Constraint(m.T, rule=soc_evolution_rule) | ||
|
|
||
| # Can't charge and discharge at the same time. | ||
| # Can't charge to both G&T and CoOp or discharge at the same time. |
There was a problem hiding this comment.
The discharge only ever impacts the coop directly, the impact on the G&T is secondary.
| # Can't charge to both G&T and CoOp or discharge at the same time. | |
| # Can't simultaneously discharge according to both G&T and COOP, or charge and discharge at the same time. |
| ), | ||
| ) | ||
|
|
||
| # Can't discharge to CoOp in the dispatch window. |
There was a problem hiding this comment.
| # Can't discharge to CoOp in the dispatch window. | |
| # Can't follow discharge commands from the COOP in the dispatch window. |
|
|
||
| @staticmethod | ||
| def glpk_solve_call( | ||
| def pyomosolver_solve_call( |
There was a problem hiding this comment.
I like the move away from having a method call in the control to call a specific solver by name, but I think we should use that general method (pyomosolver_solve_call) to run the solver of choice (GLPK, HIGHs, etc). could we either add some logic or methods for each solver that can be called by pyomosolver_solve_call so the user can choose the solver to use?
| float: Cost charged by the G$T in the same units as `lmp`. | ||
| """ | ||
| return ( | ||
| self.config.GnT_pricingfunction_coeffs[0] * lmp |
There was a problem hiding this comment.
I think the COOP price needs to be independent of LMP in some cases. Maybe that just means setting the first coeff to 0? I'm also wondering how we may be able to build in other electricity pricing approaches than what is represented here. We should probably discuss offline about other project needs and make sure we make the pricing and objective functions here work for what is needed.
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
Section 2: Draft PR Checklist
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
docs/files are up-to-date, or added when necessaryCHANGELOG.md"A complete thought. [PR XYZ]((https://github.com/NatLabRockies/H2Integrate/pull/XYZ)", where
XYZshould 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.pyPeakLoadManagementOptimizedStorageControllerSection 6: Additional Supporting Information
Section 7: Test Results, if applicable
Section 8 (Optional): New Model Checklist
docs/developer_guide/coding_guidelines.mdattrsclass to define theConfigto load in attributes for the modelBaseConfigorCostModelBaseConfiginitialize()method,setup()method,compute()methodCostModelBaseClasssupported_models.pycreate_financial_modelinh2integrate_model.pytest_all_examples.pydocs/user_guide/model_overview.mddocs/section<model_name>.mdis added to the_toc.ymlgenerate_class_hierarchy.pyto update the class hierarchy diagram indocs/developer_guide/class_structure.md