Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
f0dafbe
Update REopt.jl to multiple-chps
Bill-Becker Jan 31, 2026
2153761
Update REopt.jl to chp-expand
Bill-Becker Jan 31, 2026
f26e8ac
Update s.chp to s.chps[1] for the multiple CHPs update
Bill-Becker Jan 31, 2026
bb22d6a
Update REopt.jl#chp-expand after merging offgrid-chp
Bill-Becker Feb 2, 2026
6a5e329
Update REopt.jl#chp-expand after prodfactor update
Bill-Becker Feb 3, 2026
ae06f30
Update REopt.jl#chp-expand after merging develop
Bill-Becker Feb 3, 2026
b2f0fa7
Add output CHP.electric_load_curtailed_series_kw to model db that was…
Bill-Becker Feb 3, 2026
e8c2f70
add fixed soc inputs
adfarth May 15, 2026
ac53adf
Create 0116_rename_state_of_health_electricstorageoutputs_state_of_he…
adfarth May 15, 2026
0cf8a05
Update all_inputs_test.json
adfarth May 15, 2026
b4827d1
Merge remote-tracking branch 'origin/fixed-soc' into heuristic-dispat…
lixiangk1 May 30, 2026
55b3d1c
Add initial MPC implementation
lixiangk1 Jun 1, 2026
8a397d4
Merge branch 'develop' into fixed-soc
adfarth Jun 1, 2026
8087cc9
updt lower bound on fixed_soc tolerance
adfarth Jun 1, 2026
57c201d
Create 0117_merge_20260601_2056.py
adfarth Jun 1, 2026
25bd6f0
Merge pull request #718 from NatLabRockies/fixed-soc
adfarth Jun 3, 2026
bfcef2a
Update MPC modeling
lixiangk1 Jun 11, 2026
3e695b3
Update MPC variable names
lixiangk1 Jun 12, 2026
40d32db
Remove print statement
lixiangk1 Jun 12, 2026
dd45a05
Skip MPC dispatch if battery is not optimally sized
lixiangk1 Jun 13, 2026
3d259af
Update Manifest.toml
adfarth Jun 24, 2026
855f189
validation and help text
adfarth Jun 29, 2026
e25fadd
rm solver_name from get_mpc_results inputs
adfarth Jun 29, 2026
7d4cfaa
condense input processing
adfarth Jun 29, 2026
9fb665d
rmv lat long validation
adfarth Jun 29, 2026
63968f0
Merge branch 'develop' into heuristic-dispatch-mpc
adfarth Jun 29, 2026
7518e36
get pv prod from REopt run too
adfarth Jun 29, 2026
9c255ef
set pv prod for final run
adfarth Jun 29, 2026
01c0140
update pv prod in final REopt run
adfarth Jun 29, 2026
569b894
Create 0118_merge_20260629_2347.py
adfarth Jun 29, 2026
13bbbed
add back in TODO
adfarth Jun 29, 2026
2fc5065
pv prod and solver
adfarth Jul 1, 2026
919d30c
utf-8
adfarth Jul 1, 2026
0a557b2
consistent response
adfarth Jul 1, 2026
0e2efc0
fix inputs processing
adfarth Jul 9, 2026
41a68ab
add inputs to models.py
adfarth Jul 9, 2026
e2eb18a
add bess export io
adfarth Jul 15, 2026
3f6d57e
Update custom_table_config.py
adfarth Jul 15, 2026
756df19
fix settings and pv processing
adfarth Jul 16, 2026
e470e9c
update todos
adfarth Jul 16, 2026
4151422
account for nem and whl
adfarth Jul 17, 2026
181a316
updt branch
adfarth Jul 17, 2026
ac1b149
update REopt
adfarth Jul 17, 2026
41468b1
update for demand rates
adfarth Jul 17, 2026
8e3cbd4
Merge branch 'master' into heuristic-dispatch-mpc
Bill-Becker Jul 21, 2026
cf0ec36
Propagate REopt.jl changes to internal_efficiency_fraction in Electri…
lixiangk1 Jul 28, 2026
11ce69c
point to latest branch
adfarth Jul 28, 2026
f93cc1b
update input checks
adfarth Jul 29, 2026
cd8d017
add test, update errors
adfarth Jul 31, 2026
db41b1c
Update run_jump_model.py
adfarth Jul 31, 2026
7e536d9
rmv outages check
adfarth Jul 31, 2026
deeddf2
Update REopt.jl to avoid Generator key error if max_kw=0
Bill-Becker Jul 31, 2026
2445ed5
Allow tech like Generator to work with MPC if max_kw=0.
Bill-Becker Jul 31, 2026
6a2e60f
Pre-solve for sizes for SAM dispatch strategies, restructure functions
lixiangk1 Aug 3, 2026
4b48ba2
Fix NREL -> NLR for exception (error) handling messages
Bill-Becker Aug 3, 2026
1913aec
Merge branch 'heuristic-dispatch-mpc' of https://github.com/NatLabRoc…
Bill-Becker Aug 3, 2026
b7b19a9
Merge branch 'master' into chp-expand
Bill-Becker Aug 3, 2026
2060164
Move MPC var def after function call
lixiangk1 Aug 3, 2026
3bdd8f6
Merge branch 'heuristic-dispatch-mpc' of https://github.com/NatLabRoc…
lixiangk1 Aug 3, 2026
cba46ff
Merge migrations after merging master
Bill-Becker Aug 3, 2026
0ca96a7
Update REopt.jl#chp-expand
Bill-Becker Aug 3, 2026
4242015
Update REopt#dispatch-options to silence result log and update export
Bill-Becker Aug 3, 2026
726b02b
Merge branch 'heuristic-dispatch-mpc' into chp-expand
Bill-Becker Aug 4, 2026
809eff3
Merge migrations after merging dispatch
Bill-Becker Aug 4, 2026
18c2e3f
Update REopt.jl to chp-expand with dispatch branch
Bill-Becker Aug 4, 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
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ Classify the change according to the following categories:
##### Removed
### Patches

## heuristic-dispatch-option
### Minor udpates
#### Added
- Add **ElectricStorage** inputs field **dispatch_options** with heuristic options

## fixed-soc
### Minor udpates
#### Added
- Add **ElectricStorage** inputs field **fixed_soc_series_fraction** and **fixed_soc_series_fraction_tolerance** to allow users to fix the SOC timeseries within a chosen absolute tolerance
### Changed
- **ElectricStorage** result key **state_of_health** to **state_of_health_series_fraction**

## v3.20.0
### Minor Updates
Expand All @@ -40,7 +51,7 @@ Classify the change according to the following categories:

## v3.19.0
### Minor updates
##### Added
#### Added
- New `CHP` fields **serve_absorption_chiller_only**, **months_serving_absorption_chiller_only**, **follow_electrical_load**, and **include_cooling_in_chp_size**
- New output `thermal_to_absorption_chiller_series_mmbtu_per_hour` added to heating technologies `CHPOutputs`, `ElectricHeaterOutputs`, `CSTOutputs`, `BoilerOutputs`, `SteamTurbineOutputs`, and `ExistingBoilerOutputs`, and new output `storage_to_absorption_chiller_series_mmbtu_per_hour` for `HotThermalStorageOutputs` and `HighTempThermalStorageOutputs`.
##### Changed
Expand Down
6 changes: 4 additions & 2 deletions julia_src/Manifest.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

197 changes: 197 additions & 0 deletions julia_src/heuristic_dispatch_sizing.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NatLabRockies/REopt_API/blob/master/LICENSE.
# ============================================================================
# Heuristic battery dispatch sizing pre-solve
# ----------------------------------------------------------------------------
# Validates acceptable technologies and calls REopt to size PV and ElectricStorage
# when the user does not provide fixed sizes for heuristic battery dispatch strategies:
# * daily_foresight_optimized (MPC)
# * peak_shaving_look_ahead, peak_shaving_look_behind, self_consumption (SAM dispatches)
# These strategies require fixed, non-zero technology sizes.
# ============================================================================

"""
build_dispatch_response(status; skip_heuristic_dispatch=false, messages=Dict(), result_dict=Dict())

Build a consistent heuristic-dispatch response envelope with status, version info, and messages.
Ensures all response paths (success, error, skip) have uniform structure.
"""
function build_dispatch_response(status::String; skip_heuristic_dispatch=false, messages=Dict(), result_dict=Dict())
response = Dict(
"status" => status,
"reopt_version" => string(pkgversion(reoptjl)),
"Messages" => messages,
"skip_heuristic_dispatch" => skip_heuristic_dispatch,
)
# Merge result data if provided (for success case)
return merge(response, result_dict)
end

"""
get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solver_settings::Dict)

Determine PV and ElectricStorage sizes. If min_kw != max_kw and/or min_kwh != max_kwh,
call REopt to size technologies. User input battery sizes must also be greater than zero.
d is mutated in-place to update PV and ElectricStorage sizes.
model_inputs has already been processed to remove inputs not used in REopt.jl
"""
function get_technology_sizes!(d::Dict, model_inputs::reoptjl.REoptInputs, solver_settings::Dict)
# pv and batt are updated in-place from the dictionary `d` and used in the final REopt run
pv = get!(d, "PV", Dict())
batt = get!(d, "ElectricStorage", Dict())

function is_fixed(dct, lo_key, hi_key)
lo = get(dct, lo_key, nothing)
hi = get(dct, hi_key, nothing)
return lo !== nothing && hi !== nothing && Float64(lo) == Float64(hi)
end

# Check if both PV and BESS sizes fixed
pv_fixed = is_fixed(pv, "min_kw", "max_kw")
batt_fixed = is_fixed(batt, "min_kw", "max_kw") &&
is_fixed(batt, "min_kwh", "max_kwh")

if pv_fixed && batt_fixed
pv_kw = Float64(pv["min_kw"])
batt_kw = Float64(batt["min_kw"])
batt_kwh = Float64(batt["min_kwh"])
return (pv_kw = pv_kw, batt_kw = batt_kw, batt_kwh = batt_kwh, skip_heuristic_dispatch = false, pv_production_factor_series = nothing)
end

@info "PV and/or ElectricStorage sizes are not specified — running REopt sizing first."
# TODO: Should we "remove tiers" here for sizing or allow for optimizing with tiers?

m = get_solver_model(get_solver_model_type(solver_settings["solver_name"]), solver_settings["solver_attributes"])

sizing_results = reoptjl.run_reopt(m, model_inputs)

if get(sizing_results, "status", "") != "optimal"
status = get(sizing_results, "status", "unknown")
msgs = get(sizing_results, "Messages", Dict())
errs = get(msgs, "errors", [])
warns = get(msgs, "warnings", [])
error("REopt sizing pre-step did not solve (status = $(status)). " *
"REopt errors: $(errs). REopt warnings: $(warns).")
end

pv_kw = Float64(get(get(sizing_results, "PV", Dict()), "size_kw", 0.0))
batt_kw = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kw", 0.0))
batt_kwh = Float64(get(get(sizing_results, "ElectricStorage", Dict()), "size_kwh", 0.0))
pv_production_factor_series = get(get(sizing_results, "PV", Dict()), "production_factor_series", nothing)

# Skip the MPC loop if no battery is sized
if batt_kw <= 0.0 || batt_kwh <= 0.0
return (pv_kw = pv_kw, batt_kw = 0.0, batt_kwh = 0.0, skip_heuristic_dispatch = true, pv_production_factor_series = pv_production_factor_series)
end

# Fix inputs for final REopt run in http.jl
pv["min_kw"] = pv_kw
pv["max_kw"] = pv_kw
batt["min_kw"] = batt_kw
batt["max_kw"] = batt_kw
batt["min_kwh"] = batt_kwh
batt["max_kwh"] = batt_kwh

@info "REopt sizing solved with PV = $(pv_kw) kW and battery = $(batt_kw) kW / $(batt_kwh) kWh."
return (; pv_kw, batt_kw, batt_kwh, skip_heuristic_dispatch = false, pv_production_factor_series)
end

"""
validate_and_size_pv_storage!(d::Dict; solver_name, strategy_label)

Validates accepted technologies for heuristic battery dispatch strategies (daily_foresight_optimized,
peak_shaving_look_ahead, peak_shaving_look_behind, self_consumption). Then calls get_technology_sizes
to size PV and ElectricStorage if fixed sizes are not defined by the user.
"""
function validate_and_size_pv_storage!(d::Dict; solver_name::String="HiGHS", strategy_label::String="a heuristic battery dispatch strategy")
fail(error_dict) = (; error = error_dict, technology_sizes = nothing, model_inputs = nothing,
solver_settings = Dict(), time_steps_per_hour = 1)

## Validation on allowable inputs for predetermined heuristic battery dispatch strategies
# Error if any techs other than PV and ElectricStorage are provided
allowed_keys = Set(["PV", "ElectricStorage", "ElectricLoad", "ElectricTariff", "ElectricUtility", "Site", "Settings", "Financial"])
unsupported_keys = setdiff(keys(d), allowed_keys)
# Ignore disallowed technologies that are explicitly disabled with max_kw = 0
for key in copy(unsupported_keys)
val = get(d, key, nothing)
if val isa AbstractDict && haskey(val, "max_kw") && val["max_kw"] == 0
setdiff!(unsupported_keys, [key])
end
end
if !isempty(unsupported_keys)
return fail(build_dispatch_response(
"error",
messages = Dict("errors" => ["When using $(strategy_label), only PV and ElectricStorage are supported technologies. " *
"Unsupported inputs found: $(join(unsupported_keys, ", "))."])
))
end

# Heuristic dispatch through the API only supports a single PV, so error on multiple PVs and normalize a one-element array down to a Dict so downstream code can treat d["PV"] as a Dict.
# TODO: Handle multiple PVs
if haskey(d, "PV") && isa(d["PV"], AbstractArray)
if length(d["PV"]) > 1
return fail(build_dispatch_response(
"error",
messages = Dict("errors" => ["$(strategy_label): Multiple PV systems are not supported at this time."])
))
elseif length(d["PV"]) == 1
d["PV"] = d["PV"][1]
else
delete!(d, "PV") # empty PV array -> treat as no PV
end
end

# Check max storage sizing is greater than zero
batt = get(d, "ElectricStorage", Dict())
if Float64(get(batt, "max_kw", 1.0)) <= 0.0 || Float64(get(batt, "max_kwh", 1.0)) <= 0.0
return fail(build_dispatch_response(
"error",
messages = Dict("errors" => ["When using $(strategy_label), ElectricStorage max_kw and max_kwh must both be greater than zero."])
))
end

settings = get!(d, "Settings", Dict())
time_steps_per_hour = Int(get(settings, "time_steps_per_hour", 1))

# Update sizing_post to remove solver settings and dispatch inputs, to be able to validate inputs using REoptInputs
sizing_post = deepcopy(d)
sizing_settings = get(sizing_post, "Settings", Dict())
solver_settings = Dict()
delete!(sizing_settings, "run_bau") # Remove run_bau from sizing run
solver_settings["timeout_seconds"] = pop!(sizing_settings, "timeout_seconds", 600) # Only gets used in sizing run
solver_settings["optimality_tolerance"] = pop!(sizing_settings, "optimality_tolerance", 0.001) # Update to a higher value if solve time becomes an issue
solver_settings["solver_attributes"] = SolverAttributes(solver_settings["timeout_seconds"], solver_settings["optimality_tolerance"])
solver_settings["solver_name"] = solver_name
# Delete inputs specific to the heuristic battery dispatch run
if haskey(sizing_post, "ElectricStorage")
delete!(sizing_post["ElectricStorage"], "dispatch_strategy")
delete!(sizing_post["ElectricStorage"], "fixed_soc_series_fraction")
end

# Process and validate inputs using REoptInputs
model_inputs = nothing
try
model_inputs = reoptjl.REoptInputs(sizing_post)

# REoptInputs returns an error Dict (rather than throwing) when input validation fails.
# Surface those messages instead of falling through to get_technology_sizes!, which expects a REoptInputs and would otherwise raise a confusing MethodError.
if isa(model_inputs, Dict)
@error "REopt input validation failed during sizing pre-solve." messages=get(model_inputs, "Messages", Dict())
return fail(build_dispatch_response(
"error",
messages = get(model_inputs, "Messages", Dict("errors" => ["REopt input validation failed."]))
))
end
@info "Successfully processed REopt inputs."
catch e
@error "Something went wrong during REopt inputs processing!" exception=(e, catch_backtrace())
return fail(build_dispatch_response(
"error",
messages = Dict("errors" => [sprint(showerror, e)])
))
end

# If fixed PV and battery sizes are not provided, call REopt first in a sizing run
technology_sizes = get_technology_sizes!(d, model_inputs, solver_settings)

return (; error = nothing, technology_sizes, model_inputs, solver_settings, time_steps_per_hour)
end
Loading