Skip to content

Commit 2667b7d

Browse files
authored
Merge pull request #497 from NREL/develop
v0.52.0 CAPEX Constraint and Fix PBI
2 parents 12ed42c + eed9ec3 commit 2667b7d

19 files changed

Lines changed: 424 additions & 239 deletions

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ Classify the change according to the following categories:
2525
### Deprecated
2626
### Removed
2727

28+
## v0.52.0
29+
### Added
30+
- Add **Financial** inputs `min_initial_capital_costs_before_incentives` and `max_initial_capital_costs_before_incentives` which, when provided, provide upper and lower bounds on initial capital costs for all technologies.
31+
- Add **SteamTurbine** inputs `production_incentive_per_kwh`, `production_incentive_max_benefit`, `production_incentive_years`, and `production_incentive_max_kw`
32+
- Add **CHP** output `initial_capital_costs`
33+
### Fixed
34+
- Fix implementation of production-based incentives
2835

2936
## v0.51.1
3037
### Added
@@ -37,7 +44,7 @@ Classify the change according to the following categories:
3744
### Fixed
3845
- Update the **REoptInputs** parameter **tech_renewable_energy_fraction** so that only electricity-producing and fuel-burning heating technologies are included (instead of all technologies).
3946
- Included the following in the `Financial.lifecycle_capital_costs` and `Financial.initial_capital_costs`: `m[Symbol("OffgridOtherCapexAfterDepr"*_n)] - m[Symbol("AvoidedCapexByGHP"*_n)] - m[Symbol("ResidualGHXCapCost"*_n)] - m[Symbol("AvoidedCapexByASHP"*_n)]`
40-
47+
4148
## v0.51.0
4249
### Added
4350
- Add the following inputs to account for the clean or renewable energy fraction of grid-purchased electricity:

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.51.1"
4+
version = "0.52.0"
55

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

src/constraints/cost_curve_constraints.jl

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ There are two situations under which we add binary constraints to the model in o
66
for a technology:
77
1. When a technology has tax or investment incentives with upper capacity limits < tech.max_kw
88
- first segment(s) have lower slope than last segment
9-
2. When a technology has multiple cost/size pairs (not implemented yet, used for CHP in v1)
9+
2. When a technology has multiple cost/size pairs
1010
- we interpolate the slope between the cost/size points, typically with economies of scale pricing
1111
We used to use cost curve segments for when a technology has a non-zero existing_kw by setting the first segment to a
1212
zero cost (and slope) from zero kw to the existing_kw. Instead, we now have dvPurchaseSize >= dvSize - existing_kw.
@@ -65,4 +65,154 @@ function add_cost_curve_vars_and_constraints(m, p; _n="")
6565
@constraint(m, [t in p.techs.segmented],
6666
m[Symbol("dvPurchaseSize"*_n)][t] == m[Symbol("dvSize"*_n)][t] - p.existing_sizes[t]
6767
)
68+
end
69+
70+
function add_capex_constraints(m, p; _n="")
71+
@warn "Adding capital costs constraints. These may cause an infeasible problem in some cases, particularly for resilience runs."
72+
if (!isnothing(p.s.financial.min_initial_capital_costs_before_incentives) && !isnothing(p.s.financial.max_initial_capital_costs_before_incentives)
73+
&& p.s.financial.min_initial_capital_costs_before_incentives > p.s.financial.max_initial_capital_costs_before_incentives)
74+
throw(@error("Minimum required capital cost is larger than maximum required capital cost - this problem is infeasible."))
75+
end
76+
if !isnothing(p.s.financial.min_initial_capital_costs_before_incentives)
77+
@constraint(m,
78+
m[:InitialCapexNoIncentives] >= p.s.financial.min_initial_capital_costs_before_incentives
79+
)
80+
end
81+
if !isnothing(p.s.financial.max_initial_capital_costs_before_incentives)
82+
@constraint(m,
83+
m[:InitialCapexNoIncentives] <= p.s.financial.max_initial_capital_costs_before_incentives
84+
)
85+
end
86+
end
87+
88+
function initial_capex_no_incentives(m::JuMP.AbstractModel, p::REoptInputs; _n="")
89+
m[:InitialCapexNoIncentives] = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0.0) # Avoids MethodError
90+
91+
add_to_expression!(m[:InitialCapexNoIncentives],
92+
p.s.financial.offgrid_other_capital_costs - m[Symbol("AvoidedCapexByASHP"*_n)] - m[Symbol("AvoidedCapexByGHP"*_n)]
93+
)
94+
95+
if !isempty(p.techs.gen) && isempty(_n) # generators not included in multinode model
96+
add_to_expression!(m[:InitialCapexNoIncentives],
97+
p.s.generator.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["Generator"]
98+
)
99+
end
100+
101+
if !isempty(p.techs.pv)
102+
for pv in p.s.pvs
103+
add_to_expression!(m[:InitialCapexNoIncentives],
104+
pv.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)][pv.name]
105+
)
106+
end
107+
end
108+
109+
for b in p.s.storage.types.elec
110+
if p.s.storage.attr[b].max_kw > 0
111+
add_to_expression!(m[:InitialCapexNoIncentives],
112+
p.s.storage.attr[b].installed_cost_per_kw * m[Symbol("dvStoragePower"*_n)][b]
113+
+ p.s.storage.attr[b].installed_cost_per_kwh * m[Symbol("dvStorageEnergy"*_n)][b]
114+
)
115+
end
116+
end
117+
118+
for b in p.s.storage.types.thermal
119+
if p.s.storage.attr[b].max_kw > 0
120+
add_to_expression!(m[:InitialCapexNoIncentives],
121+
p.s.storage.attr[b].installed_cost_per_kwh * m[Symbol("dvStorageEnergy"*_n)][b]
122+
)
123+
end
124+
end
125+
126+
if "Wind" in p.techs.all
127+
add_to_expression!(m[:InitialCapexNoIncentives],
128+
p.s.wind.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["Wind"]
129+
)
130+
end
131+
132+
if "CHP" in p.techs.all
133+
m[:CHPCapexNoIncentives] = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}()
134+
cost_list = p.s.chp.installed_cost_per_kw
135+
size_list = p.s.chp.tech_sizes_for_cost_curve
136+
137+
t="CHP"
138+
if t in p.techs.segmented
139+
# Use "no incentives" version of p.cap_cost_slope and p.seg_yint
140+
cost_slope_no_inc = [cost_list[1]]
141+
seg_yint_no_inc = [0.0]
142+
for s in range(2, stop=length(size_list))
143+
tmp_slope = round((cost_list[s] * size_list[s] - cost_list[s-1] * size_list[s-1]) /
144+
(size_list[s] - size_list[s-1]), digits=0)
145+
tmp_y_int = round(cost_list[s-1] * size_list[s-1] - tmp_slope * size_list[s-1], digits=0)
146+
append!(cost_slope_no_inc, tmp_slope)
147+
append!(seg_yint_no_inc, tmp_y_int)
148+
end
149+
append!(cost_slope_no_inc, cost_list[end])
150+
append!(seg_yint_no_inc, 0.0)
151+
152+
add_to_expression!(m[:CHPCapexNoIncentives],
153+
sum(cost_slope_no_inc[s] * m[Symbol("dvSegmentSystemSize"*t)][s] +
154+
seg_yint_no_inc[s] * m[Symbol("binSegment"*t)][s] for s in 1:p.n_segs_by_tech[t])
155+
)
156+
else
157+
add_to_expression!(m[:CHPCapexNoIncentives], cost_list * m[Symbol("dvPurchaseSize"*_n)]["CHP"])
158+
end
159+
if p.s.chp.supplementary_firing_capital_cost_per_kw > 0
160+
add_to_expression!(m[:CHPCapexNoIncentives],
161+
p.s.chp.supplementary_firing_capital_cost_per_kw * m[Symbol("dvSupplementaryFiringSize"*_n)]["CHP"]
162+
)
163+
end
164+
add_to_expression!(m[:InitialCapexNoIncentives], m[:CHPCapexNoIncentives])
165+
end
166+
167+
if "SteamTurbine" in p.techs.all
168+
add_to_expression!(m[:InitialCapexNoIncentives],
169+
p.s.steam_turbine.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["SteamTurbine"]
170+
)
171+
end
172+
173+
if "Boiler" in p.techs.all
174+
add_to_expression!(m[:InitialCapexNoIncentives],
175+
p.s.boiler.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["Boiler"]
176+
)
177+
end
178+
179+
if "AbsorptionChiller" in p.techs.all
180+
add_to_expression!(m[:InitialCapexNoIncentives],
181+
p.s.absorption_chiller.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["AbsorptionChiller"]
182+
)
183+
end
184+
185+
if !isempty(p.s.ghp_option_list)
186+
for option in enumerate(p.s.ghp_option_list)
187+
if option[2].heat_pump_configuration == "WSHP"
188+
add_to_expression!(m[:InitialCapexNoIncentives],
189+
option[2].installed_cost_per_kw[2]*option[2].heatpump_capacity_ton*m[Symbol("binGHP"*_n)][option[1]]
190+
)
191+
elseif option[2].heat_pump_configuration == "WWHP"
192+
add_to_expression!(m[:InitialCapexNoIncentives],
193+
(option[2].wwhp_heating_pump_installed_cost_curve[2]*option[2].wwhp_heating_pump_capacity_ton + option[2].wwhp_cooling_pump_installed_cost_curve[2]*option[2].wwhp_cooling_pump_capacity_ton)*m[Symbol("binGHP"*_n)][option[1]]
194+
)
195+
else
196+
@warn "Unknown heat pump configuration provided, excluding GHP costs from initial capital costs."
197+
end
198+
end
199+
end
200+
201+
if "ASHPSpaceHeater" in p.techs.all
202+
add_to_expression!(m[:InitialCapexNoIncentives],
203+
p.s.ashp.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["ASHPSpaceHeater"]
204+
)
205+
end
206+
207+
if "ASHPWaterHeater" in p.techs.all
208+
add_to_expression!(m[:InitialCapexNoIncentives],
209+
p.s.ashp_wh.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["ASHPWaterHeater"]
210+
)
211+
end
212+
213+
if !isempty(p.s.electric_utility.outage_durations)
214+
add_to_expression!(m[:InitialCapexNoIncentives],
215+
m[:mgTotalTechUpgradeCost] + m[:dvMGStorageUpgradeCost]
216+
)
217+
end
68218
end

src/constraints/production_incentive_constraints.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""
33
add_prod_incent_vars_and_constraints(m, p)
44
5-
When pbi_techs is not empty this function is called to add the variables and constraints for modeling production based
5+
When techs.pbi is not empty this function is called to add the variables and constraints for modeling production based
66
incentives.
77
"""
88
function add_prod_incent_vars_and_constraints(m, p)

src/core/bau_inputs.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ function BAUInputs(p::REoptInputs)
6565
cap_cost_slope[pvname] = 0.0
6666
tech_renewable_energy_fraction[pvname] = 1.0
6767
if pvname in p.techs.pbi
68-
push!(pbi_techs, pvname)
68+
push!(techs.pbi, pvname)
6969
end
7070
pv = get_pv_by_name(pvname, p.s.pvs)
71-
fillin_techs_by_exportbin(techs_by_exportbin, pv, pv.name)
71+
fillin_techs_by_exportbin(techs_by_exportbin, pv, pvname)
7272
if !pv.can_curtail
73-
push!(techs.no_curtail, pv.name)
73+
push!(techs.no_curtail, pvname)
7474
end
7575
end
7676

@@ -84,7 +84,7 @@ function BAUInputs(p::REoptInputs)
8484
tech_renewable_energy_fraction["Generator"] = p.s.generator.fuel_renewable_energy_fraction
8585
fillin_techs_by_exportbin(techs_by_exportbin, p.s.generator, "Generator")
8686
if "Generator" in p.techs.pbi
87-
push!(pbi_techs, "Generator")
87+
push!(techs.pbi, "Generator")
8888
end
8989
if !p.s.generator.can_curtail
9090
push!(techs.no_curtail, "Generator")
@@ -201,7 +201,7 @@ function BAUInputs(p::REoptInputs)
201201
seg_min_size,
202202
seg_max_size,
203203
seg_yint,
204-
p.pbi_pwf,
204+
p.pbi_pwf, # Same pbi dict as optimal case
205205
p.pbi_max_benefit,
206206
p.pbi_max_kw,
207207
p.pbi_benefit_per_kwh,

src/core/chp.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ conflict_res_min_allowable_fraction_of_max = 0.25
5454
utility_ibi_max::Float64 = 1.0e10
5555
utility_rebate_per_kw::Float64 = 0.0
5656
utility_rebate_max::Float64 = 1.0e10
57-
production_incentive_per_kwh::Float64 = 0.0
58-
production_incentive_max_benefit::Float64 = 1.0e9
59-
production_incentive_years::Int = 0
60-
production_incentive_max_kw::Float64 = 1.0e9
57+
production_incentive_per_kwh::Float64 = 0.0 # revenue from production incentive per kWh electricity produced, including curtailment
58+
production_incentive_max_benefit::Float64 = 1.0e9 # maximum allowable annual revenue from production incentives
59+
production_incentive_years::Int = 0 # number of year in which production incentives are paid
60+
production_incentive_max_kw::Float64 = 1.0e9 # maximum allowable system size to receive production incentives
6161
can_net_meter::Bool = false
6262
can_wholesale::Bool = false
6363
can_export_beyond_nem_limit::Bool = false

src/core/financial.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
macrs_five_year::Array{Float64,1} = [0.2, 0.32, 0.192, 0.1152, 0.1152, 0.0576], # IRS pub 946
2020
macrs_seven_year::Array{Float64,1} = [0.1429, 0.2449, 0.1749, 0.1249, 0.0893, 0.0892, 0.0893, 0.0446],
2121
offgrid_other_capital_costs::Real = 0.0, # only applicable when `off_grid_flag` is true. Straight-line depreciation is applied to this capex cost, reducing taxable income.
22-
offgrid_other_annual_costs::Real = 0.0 # only applicable when `off_grid_flag` is true. Considered tax deductible for owner. Costs are per year.
22+
offgrid_other_annual_costs::Real = 0.0 # only applicable when `off_grid_flag` is true. Considered tax deductible for owner. Costs are per year.
23+
min_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing # minimum up-front capital cost for all technologies, excluding replacement costs and incentives.
24+
max_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing # maximum up-front capital cost for all technologies, excluding replacement costs and incentives.
2325
# Emissions cost inputs
2426
CO2_cost_per_tonne::Real = 51.0,
2527
CO2_cost_escalation_rate_fraction::Real = 0.042173,
@@ -62,6 +64,8 @@ struct Financial
6264
macrs_seven_year::Array{Float64,1}
6365
offgrid_other_capital_costs::Float64
6466
offgrid_other_annual_costs::Float64
67+
min_initial_capital_costs_before_incentives::Union{Nothing,Real}
68+
max_initial_capital_costs_before_incentives::Union{Nothing,Real}
6569
CO2_cost_per_tonne::Float64
6670
CO2_cost_escalation_rate_fraction::Float64
6771
NOx_grid_cost_per_tonne::Float64
@@ -94,6 +98,8 @@ struct Financial
9498
macrs_seven_year::Array{<:Real,1} = [0.1429, 0.2449, 0.1749, 0.1249, 0.0893, 0.0892, 0.0893, 0.0446],
9599
offgrid_other_capital_costs::Real = 0.0, # only applicable when `off_grid_flag` is true. Straight-line depreciation is applied to this capex cost, reducing taxable income.
96100
offgrid_other_annual_costs::Real = 0.0, # only applicable when `off_grid_flag` is true. Considered tax deductible for owner.
101+
min_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing,
102+
max_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing,
97103
# Emissions cost inputs
98104
CO2_cost_per_tonne::Real = 51.0,
99105
CO2_cost_escalation_rate_fraction::Real = 0.042173,
@@ -191,6 +197,8 @@ struct Financial
191197
macrs_seven_year,
192198
offgrid_other_capital_costs,
193199
offgrid_other_annual_costs,
200+
min_initial_capital_costs_before_incentives,
201+
max_initial_capital_costs_before_incentives,
194202
CO2_cost_per_tonne,
195203
CO2_cost_escalation_rate_fraction,
196204
NOx_grid_cost_per_tonne,

src/core/generator.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
utility_ibi_max::Real = 1.0e10,
3434
utility_rebate_per_kw::Real = 0.0,
3535
utility_rebate_max::Real = 1.0e10,
36-
production_incentive_per_kwh::Real = 0.0,
37-
production_incentive_max_benefit::Real = 1.0e9,
38-
production_incentive_years::Int = 0,
39-
production_incentive_max_kw::Real = 1.0e9,
36+
production_incentive_per_kwh::Float64 = 0.0 # revenue from production incentive per kWh electricity produced, including curtailment
37+
production_incentive_max_benefit::Float64 = 1.0e9 # maximum allowable annual revenue from production incentives
38+
production_incentive_years::Int = 0 # number of year in which production incentives are paid
39+
production_incentive_max_kw::Float64 = 1.0e9 # maximum allowable system size to receive production incentives
4040
fuel_renewable_energy_fraction::Real = 0.0,
4141
emissions_factor_lb_CO2_per_gal::Real = 22.58, # CO2e
4242
emissions_factor_lb_NOx_per_gal::Real = 0.0775544,

src/core/pv.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@
3535
utility_ibi_max::Real = 1.0e10,
3636
utility_rebate_per_kw::Real = 0.0,
3737
utility_rebate_max::Real = 1.0e10,
38-
production_incentive_per_kwh::Real = 0.0, # Applies to total estimated production, including curtailed energy.
39-
production_incentive_max_benefit::Real = 1.0e9,
40-
production_incentive_years::Int = 1,
41-
production_incentive_max_kw::Real = 1.0e9,
38+
production_incentive_per_kwh::Float64 = 0.0 # revenue from production incentive per kWh electricity produced, including curtailment
39+
production_incentive_max_benefit::Float64 = 1.0e9 # maximum allowable annual revenue from production incentives
40+
production_incentive_years::Int = 1 # number of year in which production incentives are paid
41+
production_incentive_max_kw::Float64 = 1.0e9 # maximum allowable system size to receive production incentives
4242
can_net_meter::Bool = off_grid_flag ? false : true,
4343
can_wholesale::Bool = off_grid_flag ? false : true,
4444
can_export_beyond_nem_limit::Bool = off_grid_flag ? false : true,

src/core/reopt.jl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
398398
@warn "Adding binary variable(s) to model cost curves"
399399
add_cost_curve_vars_and_constraints(m, p)
400400
for t in p.techs.segmented # cannot have this for statement in sum( ... for t in ...) ???
401-
m[:TotalTechCapCosts] += p.third_party_factor * (
401+
m[:TotalTechCapCosts] += p.third_party_factor * (
402402
sum(p.cap_cost_slope[t][s] * m[Symbol("dvSegmentSystemSize"*t)][s] +
403403
p.seg_yint[t][s] * m[Symbol("binSegment"*t)][s] for s in 1:p.n_segs_by_tech[t])
404404
)
@@ -477,6 +477,12 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
477477
end
478478
end
479479

480+
# Get CAPEX expressions and optionally constrain CAPEX
481+
initial_capex_no_incentives(m, p)
482+
if !isnothing(p.s.financial.min_initial_capital_costs_before_incentives) || !isnothing(p.s.financial.max_initial_capital_costs_before_incentives)
483+
add_capex_constraints(m, p)
484+
end
485+
480486
################################# Objective Function ########################################
481487
@expression(m, Costs,
482488
# Capital Costs

0 commit comments

Comments
 (0)