Skip to content

Commit 151b102

Browse files
authored
Merge pull request #431 from NREL/develop
September 2024 Updates
2 parents 8307e70 + c30ff01 commit 151b102

40 files changed

+2549
-814
lines changed

.github/workflows/CI.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ jobs:
1313
matrix:
1414
julia-version: ['1.8']
1515
julia-arch: [x64]
16-
# os: [ubuntu-latest, windows-latest, macOS-11]
17-
os: [windows-latest, macOS-11]
16+
os: [windows-latest]
1817

1918
steps:
2019
- uses: actions/checkout@v2
@@ -24,4 +23,4 @@ jobs:
2423
- uses: julia-actions/julia-buildpkg@latest
2524
# - uses: mxschmitt/action-tmate@v3 # for interactive debugging
2625
- run: julia --project=. -e 'using Pkg; Pkg.activate("test"); Pkg.rm("Xpress"); Pkg.activate("."); using TestEnv; TestEnv.activate(); cd("test"); include("runtests.jl")'
27-
shell: bash
26+
shell: bash

CHANGELOG.md

+21
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ Classify the change according to the following categories:
2323
### Deprecated
2424
### Removed
2525

26+
## v0.48.0
27+
### Added
28+
- Added new file `src/core/ASHP.jl` with new technology **ASHP**, which uses electricity as input and provides heating and/or cooling as output; load balancing and technology-specific constraints have been updated and added accordingly
29+
- In `src/core/existing_chiller.jl`, Added new atttribute **retire_in_optimal** to the **ExistingChiller** struct
30+
- Financial output **initial_capital_costs_after_incentives_without_macrs** which has "net year one" CapEx after incentives except for MACRS, which helps with users defining their own "simple payback period"
31+
### Changed
32+
- Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage.
33+
- Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail.
34+
- Suppress JuMP warning messages from 15-minute and multiple PVs test scenarios to avoid flooding the test logs with those warnings.
35+
- Updated/specified User-Agent header of "REopt.jl" for PVWatts and Wind Toolkit API requests; default before was "HTTP.jl"; this allows specific tracking of REopt.jl usage which call PVWatts and Wind Toolkit through api.data.gov.
36+
- Improves DRY coding by replacing multiple instances of the same chunks of code for MACRS deprecation and CHP capital cost into functions that are now in financial.jl.
37+
- Simplifies the CHP sizing test to avoid a ~30 minute solve time, by avoiding the fuel burn y-intercept binaries which come with differences between full-load and part-load efficiency.
38+
- For third party analysis proforma.jl metrics, O&M cost for existing Generator is now kept with offtaker, not the owner/developer
39+
### Fixed
40+
- Proforma calcs including "simple" payback and IRR for thermal techs/scenarios.
41+
- The operating costs of fuel and O&M were missing for all thermal techs such as ExistingBoiler, CHP, and others; this adds those sections of code to properly calculate the operating costs.
42+
- Added a test to validate the simple payback calculation with CHP (and ExistingBoiler) and checks the REopt result value against a spreadsheet proforma calculation (see Bill's spreadsheet).
43+
- Added a couple of missing techs for the initial capital cost calculation in financial.jl.
44+
- An issue with setup_boiler_inputs in reopt_inputs.jl.
45+
- Fuel costs in proforma.jl were not consistent with the optimization costs, so that was corrected so that they are only added to the offtaker cashflows and not the owner/developer cashflows for third party.
46+
2647
## v0.47.2
2748
### Fixed
2849
- Increased the big-M bound on maximum net metering benefit to prevent artificially low export benefits.

Project.toml

+1-1
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 <[email protected]>", "Bill Becker <[email protected]>", "Bhavesh Rathod <[email protected]>", "Alex Zolan <[email protected]>", "Amanda Farthing <[email protected]>"]
4-
version = "0.47.2"
4+
version = "0.48.0"
55

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

data/ashp/ashp_defaults.json

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"SpaceHeating":
3+
{
4+
"max_ton": 99999999,
5+
"installed_cost_per_ton": 2250,
6+
"om_cost_per_ton": 40,
7+
"macrs_option_years": 0,
8+
"macrs_bonus_fraction": 0.0,
9+
"can_supply_steam_turbine": false,
10+
"can_serve_process_heat": false,
11+
"can_serve_dhw": false,
12+
"can_serve_space_heating": true,
13+
"can_serve_cooling": true,
14+
"back_up_temp_threshold_degF": 10.0,
15+
"sizing_factor": 1.0,
16+
"heating_cop_reference": [1.5,2.3,3.3,4.5],
17+
"heating_cf_reference": [0.38,0.64,1.0,1.4],
18+
"heating_reference_temps_degF": [-5,17,47,80],
19+
"cooling_cop_reference": [4.0, 3.5, 2.9, 2.2],
20+
"cooling_cf_reference": [1.03, 0.98, 0.93, 0.87],
21+
"cooling_reference_temps_degF": [70, 82, 95, 110]
22+
},
23+
"DomesticHotWater":
24+
{
25+
"max_ton": 99999999,
26+
"installed_cost_per_ton": 2250,
27+
"om_cost_per_ton": 40,
28+
"macrs_option_years": 0,
29+
"macrs_bonus_fraction": 0.0,
30+
"can_supply_steam_turbine": false,
31+
"can_serve_process_heat": false,
32+
"can_serve_dhw": true,
33+
"can_serve_space_heating": false,
34+
"can_serve_cooling": false,
35+
"back_up_temp_threshold_degF": 10.0,
36+
"sizing_factor": 1.0,
37+
"heating_cop_reference": [1.5,2.3,3.3,4.5],
38+
"heating_cf_reference": [0.38,0.64,1.0,1.4],
39+
"heating_reference_temps_degF": [-5,17,47,80]
40+
}
41+
}

docs/src/reopt/inputs.md

+5
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,8 @@ REopt.SteamTurbine
176176
```@docs
177177
REopt.ElectricHeater
178178
```
179+
180+
## ASHP
181+
```@docs
182+
REopt.ASHP
183+
```

src/REopt.jl

+6-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ export
2424
avert_emissions_profiles,
2525
cambium_emissions_profile,
2626
easiur_data,
27-
get_existing_chiller_default_cop
27+
get_existing_chiller_default_cop,
28+
get_electric_heater_defaults,
29+
get_ashp_defaults
2830

2931
import HTTP
3032
import JSON
@@ -135,6 +137,7 @@ include("core/chp.jl")
135137
include("core/ghp.jl")
136138
include("core/steam_turbine.jl")
137139
include("core/electric_heater.jl")
140+
include("core/ashp.jl")
138141
include("core/scenario.jl")
139142
include("core/bau_scenario.jl")
140143
include("core/reopt_inputs.jl")
@@ -179,6 +182,7 @@ include("results/thermal_storage.jl")
179182
include("results/outages.jl")
180183
include("results/wind.jl")
181184
include("results/electric_load.jl")
185+
include("results/heating_cooling_load.jl")
182186
include("results/existing_boiler.jl")
183187
include("results/boiler.jl")
184188
include("results/existing_chiller.jl")
@@ -188,7 +192,7 @@ include("results/flexible_hvac.jl")
188192
include("results/ghp.jl")
189193
include("results/steam_turbine.jl")
190194
include("results/electric_heater.jl")
191-
include("results/heating_cooling_load.jl")
195+
include("results/ashp.jl")
192196

193197
include("core/reopt.jl")
194198
include("core/reopt_multinode.jl")

src/constraints/electric_utility_constraints.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function add_export_constraints(m, p; _n="")
7272
sum(p.max_sizes[t] for t in NEM_techs),
7373
p.hours_per_time_step * maximum([sum((
7474
p.s.electric_load.loads_kw[ts] +
75-
p.s.cooling_load.loads_kw_thermal[ts]/p.cop["ExistingChiller"] +
75+
p.s.cooling_load.loads_kw_thermal[ts]/p.cooling_cop["ExistingChiller"][ts] +
7676
(p.s.space_heating_load.loads_kw[ts] + p.s.dhw_load.loads_kw[ts] + p.s.process_heat_load.loads_kw[ts])
7777
) for ts in p.s.electric_tariff.time_steps_monthly[m]) for m in p.months
7878
])

src/constraints/load_balance.jl

+6-6
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ function add_elec_load_balance_constraints(m, p; _n="")
1212
sum(sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for b in p.s.storage.types.elec)
1313
+ m[Symbol("dvCurtail"*_n)][t, ts] for t in p.techs.elec)
1414
+ sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec)
15-
+ sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp))
16-
+ sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater)
15+
+ sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cooling_cop[t][ts] for t in setdiff(p.techs.cooling,p.techs.ghp))
16+
+ sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater)
1717
+ p.s.electric_load.loads_kw[ts]
18-
- p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"]
18+
- p.s.cooling_load.loads_kw_thermal[ts] / p.cooling_cop["ExistingChiller"][ts]
1919
+ sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options)
2020
)
2121
else
@@ -28,10 +28,10 @@ function add_elec_load_balance_constraints(m, p; _n="")
2828
+ sum(m[Symbol("dvProductionToGrid"*_n)][t, u, ts] for u in p.export_bins_by_tech[t])
2929
+ m[Symbol("dvCurtail"*_n)][t, ts] for t in p.techs.elec)
3030
+ sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec)
31-
+ sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp))
32-
+ sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater)
31+
+ sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cooling_cop[t][ts] for t in setdiff(p.techs.cooling,p.techs.ghp))
32+
+ sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater)
3333
+ p.s.electric_load.loads_kw[ts]
34-
- p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"]
34+
- p.s.cooling_load.loads_kw_thermal[ts] / p.cooling_cop["ExistingChiller"][ts]
3535
+ sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options)
3636
)
3737
end

src/constraints/outage_constraints.jl

+4-4
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,22 @@ function add_outage_cost_constraints(m,p)
4949
end
5050
end
5151

52-
if !isempty(p.techs.segmented)
52+
if !isempty(intersect(p.techs.segmented, p.techs.elec))
5353
@warn "Adding binary variable(s) to model cost curves in stochastic outages"
5454
if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name)
55-
@constraint(m, [t in p.techs.segmented], # cannot have this for statement in sum( ... for t in ...) ???
55+
@constraint(m, [t in intersect(p.techs.segmented, p.techs.elec)], # cannot have this for statement in sum( ... for t in ...) ???
5656
m[:binMGTechUsed][t] => {m[:dvMGTechUpgradeCost][t] >= p.s.financial.microgrid_upgrade_cost_fraction * p.third_party_factor *
5757
sum(p.cap_cost_slope[t][s] * m[Symbol("dvSegmentSystemSize"*t)][s] +
5858
p.seg_yint[t][s] * m[Symbol("binSegment"*t)][s] for s in 1:p.n_segs_by_tech[t])}
5959
)
6060
else
61-
@constraint(m, [t in p.techs.segmented],
61+
@constraint(m, [t in intersect(p.techs.segmented, p.techs.elec)],
6262
m[:dvMGTechUpgradeCost][t] >= p.s.financial.microgrid_upgrade_cost_fraction * p.third_party_factor *
6363
sum(p.cap_cost_slope[t][s] * m[Symbol("dvSegmentSystemSize"*t)][s] +
6464
p.seg_yint[t][s] * m[Symbol("binSegment"*t)][s] for s in 1:p.n_segs_by_tech[t]) -
6565
(maximum(p.cap_cost_slope[t][s] for s in 1:p.n_segs_by_tech[t]) * p.max_sizes[t] + maximum(p.seg_yint[t][s] for s in 1:p.n_segs_by_tech[t]))*(1-m[:binMGTechUsed][t])
6666
)
67-
@constraint(m, [t in p.techs.segmented], m[:dvMGTechUpgradeCost][t] >= 0.0)
67+
@constraint(m, [t in intersect(p.techs.segmented, p.techs.elec)], m[:dvMGTechUpgradeCost][t] >= 0.0)
6868
end
6969
end
7070

src/constraints/thermal_tech_constraints.jl

+59-3
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function add_heating_tech_constraints(m, p; _n="")
6464
# Constraint (7_heating_prod_size): Production limit based on size for non-electricity-producing heating techs
6565
if !isempty(setdiff(p.techs.heating, union(p.techs.elec, p.techs.ghp)))
6666
@constraint(m, [t in setdiff(p.techs.heating, union(p.techs.elec, p.techs.ghp)), ts in p.time_steps],
67-
sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) <= m[Symbol("dvSize"*_n)][t]
67+
sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) <= m[Symbol("dvSize"*_n)][t] * p.heating_cf[t][ts]
6868
)
6969
end
7070
# Constraint (7_heating_load_compatability): Set production variables for incompatible heat loads to zero
@@ -88,7 +88,56 @@ function add_heating_tech_constraints(m, p; _n="")
8888
end
8989
end
9090
end
91-
# Enfore
91+
92+
# Enforce no waste heat for any technology that isn't both electricity- and heat-producing
93+
for t in setdiff(p.techs.heating, union(p.techs.elec, p.techs.ghp))
94+
for q in p.heating_loads
95+
for ts in p.time_steps
96+
fix(m[Symbol("dvProductionToWaste"*_n)][t,q,ts], 0.0, force=true)
97+
end
98+
end
99+
end
100+
end
101+
102+
function add_heating_cooling_constraints(m, p; _n="")
103+
@constraint(m, [t in setdiff(intersect(p.techs.cooling, p.techs.heating), p.techs.ghp), ts in p.time_steps],
104+
sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) / p.heating_cf[t][ts] + m[Symbol("dvCoolingProduction"*_n)][t,ts] / p.cooling_cf[t][ts] <= m[Symbol("dvSize"*_n)][t]
105+
)
106+
end
107+
108+
109+
function add_ashp_force_in_constraints(m, p; _n="")
110+
if "ASHPSpaceHeater" in p.techs.ashp && p.s.ashp.force_into_system
111+
for t in setdiff(p.techs.can_serve_space_heating, ["ASHPSpaceHeater"])
112+
for ts in p.time_steps
113+
fix(m[Symbol("dvHeatingProduction"*_n)][t,"SpaceHeating",ts], 0.0, force=true)
114+
fix(m[Symbol("dvProductionToWaste"*_n)][t,"SpaceHeating",ts], 0.0, force=true)
115+
end
116+
end
117+
end
118+
119+
if "ASHPSpaceHeater" in p.techs.cooling && p.s.ashp.force_into_system
120+
for t in setdiff(p.techs.cooling, ["ASHPSpaceHeater"])
121+
for ts in p.time_steps
122+
fix(m[Symbol("dvCoolingProduction"*_n)][t,ts], 0.0, force=true)
123+
end
124+
end
125+
end
126+
127+
if "ASHPWaterHeater" in p.techs.ashp && p.s.ashp_wh.force_into_system
128+
for t in setdiff(p.techs.can_serve_dhw, ["ASHPWaterHeater"])
129+
for ts in p.time_steps
130+
fix(m[Symbol("dvHeatingProduction"*_n)][t,"DomesticHotWater",ts], 0.0, force=true)
131+
fix(m[Symbol("dvProductionToWaste"*_n)][t,"DomesticHotWater",ts], 0.0, force=true)
132+
end
133+
end
134+
end
135+
end
136+
137+
function avoided_capex_by_ashp(m, p; _n="")
138+
m[:AvoidedCapexByASHP] = @expression(m,
139+
sum(p.avoided_capex_by_ashp_present_value[t] for t in p.techs.ashp)
140+
)
92141
end
93142

94143
function no_existing_boiler_production(m, p; _n="")
@@ -103,7 +152,7 @@ end
103152
function add_cooling_tech_constraints(m, p; _n="")
104153
# Constraint (7_cooling_prod_size): Production limit based on size for boiler
105154
@constraint(m, [t in setdiff(p.techs.cooling, p.techs.ghp), ts in p.time_steps_with_grid],
106-
m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t]
155+
m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t] * p.cooling_cf[t][ts]
107156
)
108157
# The load balance for cooling is only applied to time_steps_with_grid, so make sure we don't arbitrarily show cooling production for time_steps_without_grid
109158
for t in setdiff(p.techs.cooling, p.techs.ghp)
@@ -112,3 +161,10 @@ function add_cooling_tech_constraints(m, p; _n="")
112161
end
113162
end
114163
end
164+
165+
function no_existing_chiller_production(m, p; _n="")
166+
for ts in p.time_steps
167+
fix(m[Symbol("dvCoolingProduction"*_n)]["ExistingChiller",ts], 0.0, force=true)
168+
end
169+
fix(m[Symbol("dvSize"*_n)]["ExistingChiller"], 0.0, force=true)
170+
end

0 commit comments

Comments
 (0)