Add MPC as an option for BESS dispatch#721
Conversation
| # Then set ElectricStorage.fixed_soc_series_fraction = MPC SOC before running the main REopt optimization. If the user does not | ||
| # specify a fixed PV/battery size, optimally size the technologies using REopt (skip MPC dispatch if optimal battery size is 0). | ||
| electric_storage = get(d, "ElectricStorage", Dict()) | ||
| if get(electric_storage, "dispatch_strategy", nothing) == "daily_foresight_optimized" |
There was a problem hiding this comment.
Add checks to get_mpc_results:
- Checks on what techs are allowed in mpc --> error if anything other than PV and BESS
- Warning for tiered rates
- Warning for outages
- Error for emissions goals or RE goals?
There was a problem hiding this comment.
Added some of these checks but leaving this open for the remaining TODO there
| horizon = 24 * time_steps_per_hour | ||
| per_iter_timeout_s = 30.0 | ||
|
|
||
| # TODO: Should MPC handle multiple PVs? |
There was a problem hiding this comment.
would this require a lot of additional dev on the the REopt.jl side?
| pv_prod_factor = Float64.(pv["production_factor_series"]) | ||
| else | ||
| # TODO: These production factors don't consider degradation, problem? | ||
| # Does MPCPV need a degradation input to calculate the levelization factor used in the optimization? |
There was a problem hiding this comment.
From a REoptInputs call you could get the levelization factor:
i = REoptInputs(post)
i.levelization_factor["PV"]
But note that the user input for prod_factor_series also wouldn't account for degradation factor (I think) so these should be treated the same whether user provided or obtained from the get_pv_prod function
There was a problem hiding this comment.
@lixiangk1 in REopt.jl mpc/, it looks like levelization_factor just always get set to 1 anyway. I see a TODO there from Nick about whether or not it's needed. I think maybe we just continue to assume levelization factor = 1 for MPC runs? OR we would need to adjust that in REopt.jl
| "electric_curtailed_series_kw" => Float64[], | ||
| ), | ||
| "ElectricStorage" => Dict( | ||
| "storage_to_load_series_kw" => Float64[], |
There was a problem hiding this comment.
once we merge the storage export PR, will need to add storage_to_grid here and test
There was a problem hiding this comment.
See below comment about changes needed in REopt.jl mpc for allowing bess export and nem or whl
| tou_previous_peak_demands, monthly_previous_peak_demands, soc_init_frac) | ||
| return Dict( | ||
| "PV" => Dict( | ||
| "size_kw" => pv_kw, |
There was a problem hiding this comment.
so can_net_meter, can_wholesale, can_export_beyond_nem_limit, can_curtail are not inputs?
Same with ElectricTariff export_rates and other nem and whl-related inputs?
There was a problem hiding this comment.
@lixiangk1 after looking at the REopt.jl mpc inputs, it looks like:
- MPCElectricTariff has inputs:
net_meteringandexport_rates(so, currently NOT specific to each tech) - If both are provided, the documentation says the model will choose between them.
- Compensation for export beyond NEM is not accounted for in MPC
- If ONLY PV can export, this seems relatively straightforward to implement in the API code: set up the MPC inputs based on PV can_net_meter and can_wholesale, and use
i.s.electric_tariff.export_ratesto populate export_rates (check if this should be negative or positive for MPC) - BESS export introduces a complication: if PV can export and BESS can't, I don't think the REopt.jl mpc is set up to handle this currently; I see some TODOs in the code there about not assuming all techs have the same NEM schema, and I think we'd have to implement this if introducing BESS export to mpc.
|
Generally curious if running hourly is in line with available versions of this capability in BESS dispatch software, or if less frequent should be considered too? |
| try | ||
| @info "Running MPC to obtain daily foresight optimized battery dispatch profile." | ||
| mpc_results = get_mpc_results(d; solver_name=solver_name) | ||
| # TODO: Cache sizing run results and avoid a second call to REopt? Are those the same results? |
There was a problem hiding this comment.
@lixiangk1 could you explain what you mean by "are those the same results"?
| end | ||
| end | ||
|
|
||
| #TODO: What timeout and optimality tolerance should MPC use? |
There was a problem hiding this comment.
Is this TODO referring to the final REopt run when using MPC? If so, I think this could just use the same as what's used now for a regular REopt run?
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds a Model Predictive Control (MPC) based dispatch option for BESS by introducing a Julia MPC implementation and exposing it via the API, plus wiring it into the existing /reopt flow when a new dispatch strategy is selected.
Changes:
- Added a new rolling-horizon MPC implementation in Julia (
mpc.jl) for PV + ElectricStorage dispatch. - Integrated MPC into the
/reoptendpoint viaElectricStorage.dispatch_strategy == "daily_foresight_optimized"and added a dedicated/mpcendpoint. - Updated Django migrations and Julia manifest to align dependency/migration state.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| reoptjl/migrations/0118_merge_20260629_2347.py | Adds a Django merge migration to reconcile migration graph. |
| reoptjl/migrations/0117_merge_20260530_1640.py | Adds a Django merge migration to reconcile migration graph. |
| julia_src/mpc.jl | New MPC rolling-horizon dispatch implementation and sizing pre-step. |
| julia_src/http.jl | Wires MPC into /reopt, adds /mpc endpoint, includes mpc.jl. |
| julia_src/Manifest.toml | Points REopt.jl dependency to a specific git source/rev. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # TODO: MPC horizons and timeout are currently hard coded | ||
| time_steps_per_hour = Int(get(settings, "time_steps_per_hour", 1)) | ||
| length_of_data = 8760 * time_steps_per_hour | ||
| horizon = 24 * time_steps_per_hour | ||
| per_iter_timeout_s = 30.0 |
Please check if the PR fulfills these requirements
What kind of change does this PR introduce?
(Bug fix, feature, docs update, ...)
What is the current behavior?
(You can also link to an open issue here)
What is the new behavior (if this is a feature change)?
Does this PR introduce a breaking change?
(What changes might users need to make in their application due to this PR?)
Other information:
Things to test here / in REopt.jl: