Skip to content

Commit b34682c

Browse files
authored
Merge pull request #543 from NREL/develop
v0.56.1 Patch fixes for Peak Scaling, URDB meta, HT-TES Bypass
2 parents 7b38053 + 394e948 commit b34682c

File tree

9 files changed

+95
-38
lines changed

9 files changed

+95
-38
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ Classify the change according to the following categories:
2525
### Deprecated
2626
### Removed
2727

28+
## V0.56.1
29+
### Fixed
30+
- `CST` bypassing constraints for not serving (`can_serve_.. = false`) heating load types by going through the `HighTempThermalStorage`
31+
- Type error with `ElectricLoad.monthly_peaks_kw` so that it now converts Vector{Any} to Vector{<:Real}
32+
- `ElectricTariff.urdb_metadata.rate_effective_date` with the right URDB parameter
2833

2934
## v0.56.0
3035
### Added

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "REopt"
22
uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6"
33
authors = ["Nick Laws", "Hallie Dunham <hallie.dunham@nrel.gov>", "Bill Becker <william.becker@nrel.gov>", "Bhavesh Rathod <bhavesh.rathod@nrel.gov>", "Alex Zolan <alexander.zolan@nrel.gov>", "Amanda Farthing <amanda.farthing@nrel.gov>", "Xiangkun Li <xiangkun.li@nrel.gov>", "An Pham <an.pham@nrel.gov>", "Byron Pullutasig <byron.pullatasig@nrel.gov>"]
4-
version = "0.56.0"
4+
version = "0.56.1"
55

66
[deps]
77
ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"

src/constraints/storage_constraints.jl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,63 @@ function add_storage_sum_grid_constraints(m, p; _n="")
306306
sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) >=
307307
sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec)
308308
)
309+
end
310+
311+
"""
312+
add_hot_tes_flow_restrictions!(m, p, b)
313+
314+
Add flow restrictions from individual heating technologies to charge via dvHeatToStorage and
315+
discharge via dvHeatFromStorage depending on the compatible loads served.
316+
"""
317+
function add_hot_tes_flow_restrictions!(m, p, b)
318+
# If there are incompatible heating techs for the storage (i.e., TES fills load the tech cannot), all charge to storage from that tech is zero
319+
for t in union(p.techs.heating, p.techs.chp)
320+
incompatible_loads_served = []
321+
if ("DomesticHotWater" in p.heating_loads_served_by_tes[b] && !(t in p.techs.can_serve_dhw))
322+
push!(incompatible_loads_served, "DomesticHotWater")
323+
end
324+
if ("SpaceHeating" in p.heating_loads_served_by_tes[b] && !(t in p.techs.can_serve_space_heating))
325+
push!(incompatible_loads_served, "SpaceHeating")
326+
end
327+
if ("ProcessHeat" in p.heating_loads_served_by_tes[b] && !(t in p.techs.can_serve_process_heat))
328+
push!(incompatible_loads_served, "ProcessHeat")
329+
end
330+
if !isempty(incompatible_loads_served)
331+
@warn "Technology "*t*" is ineligible to serve storage system "*b*" due to the following incompatible loads served "*string(incompatible_loads_served)
332+
for q in p.heating_loads_served_by_tes[b]
333+
for ts in p.time_steps
334+
fix(m[:dvHeatToStorage][b,t,q,ts], 0.0, force=true)
335+
end
336+
end
337+
end
338+
end
339+
340+
#If load isn't served by storage, all charge or discharge flows of that quality heat are zero
341+
if !isempty(setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]))
342+
@constraint(m, [t in union(p.techs.heating, p.techs.chp),
343+
q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]),
344+
ts in p.time_steps],
345+
m[:dvHeatToStorage][b,t,q,ts] == 0
346+
)
347+
@constraint(m, [q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]),
348+
ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0
349+
)
350+
end
351+
352+
# If a heating load is served by a storage vehicle, only allow charge from compatible techs. otherwise, allow no charge for that heat quality.
353+
if "DomesticHotWater" in p.heating_loads_served_by_tes[b] && !isempty(setdiff(union(p.techs.heating, p.techs.chp), p.techs.can_serve_dhw))
354+
@constraint(m, [t in setdiff(union(p.techs.heating, p.techs.chp), p.techs.can_serve_dhw), ts in p.time_steps],
355+
m[:dvHeatToStorage][b,t,"DomesticHotWater",ts] == 0
356+
)
357+
end
358+
if "SpaceHeating" in p.heating_loads_served_by_tes[b] && !isempty(setdiff(union(p.techs.heating, p.techs.chp), p.techs.can_serve_space_heating))
359+
@constraint(m, [t in setdiff(union(p.techs.heating, p.techs.chp), p.techs.can_serve_space_heating), ts in p.time_steps],
360+
m[:dvHeatToStorage][b,t,"SpaceHeating",ts] == 0
361+
)
362+
end
363+
if "ProcessHeat" in p.heating_loads_served_by_tes[b] && !isempty(setdiff(union(p.techs.heating, p.techs.chp), p.techs.can_serve_process_heat))
364+
@constraint(m, [t in setdiff(union(p.techs.heating, p.techs.chp), p.techs.can_serve_process_heat), ts in p.time_steps],
365+
m[:dvHeatToStorage][b,t,"ProcessHeat",ts] == 0
366+
)
367+
end
309368
end

src/core/electric_load.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -271,26 +271,26 @@ end
271271

272272
"""
273273
scale_load_to_monthly_peaks(
274-
initial_loads_kw::Vector{Float64},
275-
target_monthly_peaks_kw::Vector{Float64},
274+
initial_loads_kw::Vector{<:Real},
275+
target_monthly_peaks_kw::Vector{<:Real},
276276
time_steps_per_hour::Int,
277277
year::Int
278278
) -> Vector{Float64}
279279
280280
Scales an electric load profile to match specified monthly peak demands while preserving the overall shape of the load profile.
281281
282282
# Arguments
283-
- `initial_loads_kw::Vector{Float64}`: The original load profile (kW) for the entire year, with a length of `8760 * time_steps_per_hour`.
284-
- `target_monthly_peaks_kw::Vector{Float64}`: A vector of 12 values representing the desired peak demand (kW) for each month.
283+
- `initial_loads_kw::Vector{<:Real}`: The original load profile (kW) for the entire year, with a length of `8760 * time_steps_per_hour`.
284+
- `target_monthly_peaks_kw::Vector{<:Real}`: A vector of 12 values representing the desired peak demand (kW) for each month.
285285
- `time_steps_per_hour::Int`: The number of time steps per hour (e.g., 1 for hourly data, 4 for 15-minute intervals).
286286
- `year::Int`: The year of the load profile, used to determine the number of days in each month (e.g., leap years).
287287
288288
# Returns
289289
- `Vector{Float64}`: A scaled load profile (kW) with the same length as `initial_loads_kw`, adjusted to meet the specified monthly peak demands.
290290
"""
291291
function scale_load_to_monthly_peaks(
292-
initial_loads_kw::Vector{Float64},
293-
target_monthly_peaks_kw::Vector{Float64},
292+
initial_loads_kw::Vector{<:Real},
293+
target_monthly_peaks_kw::Vector{<:Real},
294294
time_steps_per_hour::Int,
295295
year::Int
296296
)
@@ -340,7 +340,7 @@ Args:
340340
Returns:
341341
Profile for given month, scaled to peak
342342
"""
343-
function apply_linear_flattening(initial_load_series_kw::Vector{Float64}, target_peak_kw::Float64)
343+
function apply_linear_flattening(initial_load_series_kw::Vector{<:Real}, target_peak_kw::Real)
344344

345345
# The flat load is the average power (kW) that sums to the same total energy over the time period
346346
flat_load_kw = sum(initial_load_series_kw) / length(initial_load_series_kw)
@@ -367,7 +367,7 @@ Args:
367367
Returns:
368368
Profile for given month, scaled to peak
369369
"""
370-
function apply_exponential_stretching(initial_load_series_kw::Vector{Float64}, target_peak_kw::Float64, time_steps_per_hour::Int)
370+
function apply_exponential_stretching(initial_load_series_kw::Vector{<:Real}, target_peak_kw::Real, time_steps_per_hour::Int)
371371
initial_peak_kw = maximum(initial_load_series_kw)
372372
transformed_load_series_kw = initial_load_series_kw .* (target_peak_kw / initial_peak_kw)
373373
target_total_energy_kwh = sum(initial_load_series_kw) / time_steps_per_hour

src/core/reopt.jl

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
204204
end
205205
end
206206
end
207-
208207
for b in p.s.storage.types.all
209208
if p.s.storage.attr[b].max_kw == 0 || p.s.storage.attr[b].max_kwh == 0
210209
@constraint(m, [ts in p.time_steps], m[:dvStoredEnergy][b, ts] == 0)
@@ -216,22 +215,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
216215
@constraint(m, [t in p.techs.elec, ts in p.time_steps_with_grid],
217216
m[:dvProductionToStorage][b, t, ts] == 0)
218217
elseif b in p.s.storage.types.hot
219-
@constraint(m, [q in q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]), ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0)
220-
if "DomesticHotWater" in p.heating_loads_served_by_tes[b]
221-
@constraint(m, [t in setdiff(p.heating_techs, p.techs_can_serve_dhw), ts in p.time_steps], m[:dvHeatToStorage][b,t,"DomesticHotWater",ts] == 0)
222-
else
223-
@constraint(m, [t in p.heating_techs, ts in p.time_steps], m[:dvHeatToStorage][b,t,"DomesticHotWater",ts] == 0)
224-
end
225-
if "SpaceHeating" in p.heating_loads_served_by_tes[b]
226-
@constraint(m, [t in setdiff(p.heating_techs, p.techs_can_serve_space_heating), ts in p.time_steps], m[:dvHeatToStorage][b,t,"SpaceHeating",ts] == 0)
227-
else
228-
@constraint(m, [t in p.heating_techs, ts in p.time_steps], m[:dvHeatToStorage][b,t,"SpaceHeating",ts] == 0)
229-
end
230-
if "ProcessHeat" in p.heating_loads_served_by_tes[b]
231-
@constraint(m, [t in setdiff(p.heating_techs, p.techs_can_serve_process_heat), ts in p.time_steps], m[:dvHeatToStorage][b,t,"ProcessHeat",ts] == 0)
232-
else
233-
@constraint(m, [t in p.heating_techs, ts in p.time_steps], m[:dvHeatToStorage][b,t,"ProcessHeat",ts] == 0)
234-
end
218+
@constraint(m, [q in p.heating_loads, ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0)
219+
@constraint(m, [t in union(p.techs.heating, p.techs.chp), q in p.heating_loads, ts in p.time_steps], m[:dvHeatToStorage][b,t,q,ts] == 0)
235220
end
236221
else
237222
add_storage_size_constraints(m, p, b)
@@ -244,6 +229,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
244229
end
245230
elseif b in p.s.storage.types.hot
246231
add_hot_thermal_storage_dispatch_constraints(m, p, b)
232+
add_hot_tes_flow_restrictions!(m, p, b)
247233
elseif b in p.s.storage.types.cold
248234
add_cold_thermal_storage_dispatch_constraints(m, p, b)
249235
else
@@ -714,6 +700,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs)
714700
end
715701
end
716702
if !isempty(p.s.storage.types.hot)
703+
# TODO introduce these as sparse variables, add a set of techs charging storage?
717704
@variable(m, dvHeatToStorage[p.s.storage.types.hot, union(p.techs.heating, p.techs.chp), p.heating_loads, p.time_steps] >= 0) # Power charged to hot storage b at quality q [kW]
718705
@variable(m, dvHeatFromStorage[p.s.storage.types.hot, p.heating_loads, p.time_steps] >= 0) # Power discharged from hot storage system b for load q [kW]
719706
if !isempty(p.techs.steam_turbine)

src/core/reopt_inputs.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,13 @@ function REoptInputs(s::AbstractScenario)
246246
if !isempty(s.storage.types.hot)
247247
for b in s.storage.types.hot
248248
heating_loads_served_by_tes[b] = String[]
249-
if s.storage.attr[b].can_serve_dhw && !isnothing(s.dhw_load)
249+
if s.storage.attr[b].can_serve_dhw && ("DomesticHotWater" in heating_loads) && sum(heating_loads_kw["DomesticHotWater"]) > 0.0
250250
push!(heating_loads_served_by_tes[b],"DomesticHotWater")
251251
end
252-
if s.storage.attr[b].can_serve_space_heating && !isnothing(s.space_heating_load)
252+
if s.storage.attr[b].can_serve_space_heating && ("SpaceHeating" in heating_loads) && sum(heating_loads_kw["SpaceHeating"]) > 0.0
253253
push!(heating_loads_served_by_tes[b],"SpaceHeating")
254254
end
255-
if s.storage.attr[b].can_serve_process_heat && !isnothing(s.process_heat_load)
255+
if s.storage.attr[b].can_serve_process_heat && ("ProcessHeat" in heating_loads) && sum(heating_loads_kw["ProcessHeat"]) > 0.0
256256
push!(heating_loads_served_by_tes[b],"ProcessHeat")
257257
end
258258
end

src/core/urdb.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function URDBrate(urdb_response::Dict, year::Int; time_steps_per_hour=1)
8686
label = get(urdb_response, "label", "")
8787
rate_name = get(urdb_response, "name", "")
8888
utility = get(urdb_response, "utility", "")
89-
latest_update_unix = get(urdb_response, "latest_update", 0.0)
89+
latest_update_unix = get(urdb_response, "startdate", 0.0)
9090
voltage_level = get(urdb_response, "voltagecategory", "")
9191
rate_description = get(urdb_response, "description", "")
9292
peak_kw_capacity_min = get(urdb_response, "peakkwcapacitymin", 0.0)
@@ -96,10 +96,10 @@ function URDBrate(urdb_response::Dict, year::Int; time_steps_per_hour=1)
9696
demand_comments = get(urdb_response, "demandcomments", "")
9797
url_link = get(urdb_response, "uri", "")
9898

99-
# Convert Unix timestamp to datetime string
99+
# Convert Unix timestamp to date string
100100
rate_effective_date = ""
101101
if latest_update_unix > 0
102-
rate_effective_date = string(Dates.unix2datetime(latest_update_unix))
102+
rate_effective_date = string(Date(Dates.unix2datetime(latest_update_unix)))
103103
end
104104

105105
# Convert matrix to array if needed

test/runtests.jl

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,7 @@ else # run HiGHS tests
813813
d["ElectricHeater"]["installed_cost_per_mmbtu_per_hour"] = 1.0
814814
d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0]
815815
d["HotThermalStorage"]["max_gal"] = 0.0
816+
d["HotThermalStorage"]["min_gal"] = 0.0
816817
s = Scenario(d)
817818
inputs = REoptInputs(s)
818819
m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false))
@@ -3085,17 +3086,17 @@ else # run HiGHS tests
30853086
p = REoptInputs(s)
30863087
m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false))
30873088
results = run_reopt(m, p)
3088-
3089-
annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour
3089+
annual_thermal_prod = 0.64 * 8760 #80% efficient boiler --> 0.64 MMBTU of heat load per hour for electric heater
30903090
annual_electric_heater_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP
30913091
annual_energy_supplied = 87600 + annual_electric_heater_consumption
30923092

3093-
#Second run: ElectricHeater produces the required heat with free electricity
3094-
@test results["ElectricHeater"]["size_mmbtu_per_hour"] 0.8 atol=0.1
3093+
#Second run: ElectricHeater produces the required heat with free electricity for two of three heat sources
3094+
@test results["ElectricHeater"]["size_mmbtu_per_hour"] 0.64 atol=0.1
30953095
@test results["ElectricHeater"]["annual_thermal_production_mmbtu"] annual_thermal_prod rtol=1e-4
30963096
@test results["ElectricHeater"]["annual_electric_consumption_kwh"] annual_electric_heater_consumption rtol=1e-4
30973097
@test results["ElectricUtility"]["annual_energy_supplied_kwh"] annual_energy_supplied rtol=1e-4
3098-
3098+
#no bypass of Electric Heater through Hot TES to process heat
3099+
@test sum(results["HotThermalStorage"]["storage_to_process_heat_load_series_mmbtu_per_hour"]) 0.0 atol=0.1
30993100
finalize(backend(m))
31003101
empty!(m)
31013102
GC.gc()

test/scenarios/electric_heater.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
"monthly_energy_rates": [0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1]
5555
},
5656
"HotThermalStorage":{
57-
"max_gal":2500
57+
"min_gal":2500,
58+
"max_gal":2500,
59+
"can_serve_dhw":true,
60+
"can_serve_space_heating":true,
61+
"can_serve_process_heat":false,
62+
"thermal_decay_rate_fraction": 0.0
5863
}
5964
}

0 commit comments

Comments
 (0)