Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
142 changes: 141 additions & 1 deletion src/constraints/cost_curve_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ There are two situations under which we add binary constraints to the model in o
for a technology:
1. When a technology has tax or investment incentives with upper capacity limits < tech.max_kw
- first segment(s) have lower slope than last segment
2. When a technology has multiple cost/size pairs (not implemented yet, used for CHP in v1)
2. When a technology has multiple cost/size pairs
- we interpolate the slope between the cost/size points, typically with economies of scale pricing
We used to use cost curve segments for when a technology has a non-zero existing_kw by setting the first segment to a
zero cost (and slope) from zero kw to the existing_kw. Instead, we now have dvPurchaseSize >= dvSize - existing_kw.
Expand Down Expand Up @@ -65,4 +65,144 @@ function add_cost_curve_vars_and_constraints(m, p; _n="")
@constraint(m, [t in p.techs.segmented],
m[Symbol("dvPurchaseSize"*_n)][t] == m[Symbol("dvSize"*_n)][t] - p.existing_sizes[t]
)
end

function add_capex_constraints(m, p; _n="")
@warn "Adding capital costs constraints. These may cause an infeasible problem in some cases, particularly for resilience runs."
if !isnothing(p.s.financial.min_initial_capital_costs_before_incentives)
@constraint(m,
m[:InitialCapexNoIncentives] >= p.s.financial.min_initial_capital_costs_before_incentives
)
end
if !isnothing(p.s.financial.max_initial_capital_costs_before_incentives)
@constraint(m,
m[:InitialCapexNoIncentives] <= p.s.financial.max_initial_capital_costs_before_incentives
)
end
end

function initial_capex_no_incentives(m::JuMP.AbstractModel, p::REoptInputs; _n="")
m[:InitialCapexNoIncentives] = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}(0.0) # Avoids MethodError

add_to_expression!(m[:InitialCapexNoIncentives],
p.s.financial.offgrid_other_capital_costs - m[Symbol("AvoidedCapexByASHP"*_n)] - m[Symbol("AvoidedCapexByGHP"*_n)]
)

if !isempty(p.techs.gen) && isempty(_n) # generators not included in multinode model
add_to_expression!(m[:InitialCapexNoIncentives],
p.s.generator.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["Generator"]
)
end

if !isempty(p.techs.pv)
for pv in p.s.pvs
add_to_expression!(m[:InitialCapexNoIncentives],
pv.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)][pv.name]
)
end
end

for b in p.s.storage.types.elec
if p.s.storage.attr[b].max_kw > 0
add_to_expression!(m[:InitialCapexNoIncentives],
p.s.storage.attr[b].installed_cost_per_kw * m[Symbol("dvStoragePower"*_n)][b]
+ p.s.storage.attr[b].installed_cost_per_kwh * m[Symbol("dvStorageEnergy"*_n)][b]
)
end
end

for b in p.s.storage.types.thermal
if p.s.storage.attr[b].max_kw > 0
add_to_expression!(m[:InitialCapexNoIncentives],
p.s.storage.attr[b].installed_cost_per_kwh * m[Symbol("dvStorageEnergy"*_n)][b]
)
end
end

if "Wind" in p.techs.all
add_to_expression!(m[:InitialCapexNoIncentives],
p.s.wind.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["Wind"]
)
end

if "CHP" in p.techs.all
m[:CHPCapexNoIncentives] = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}()
cost_list = p.s.chp.installed_cost_per_kw
size_list = p.s.chp.tech_sizes_for_cost_curve

t="CHP"
if t in p.techs.segmented
# Use "no incentives" version of p.cap_cost_slope and p.seg_yint
cost_slope_no_inc = [cost_list[1]]
seg_yint_no_inc = [0.0]
for s in range(2, stop=length(size_list))
tmp_slope = round((cost_list[s] * size_list[s] - cost_list[s-1] * size_list[s-1]) /
(size_list[s] - size_list[s-1]), digits=0)
tmp_y_int = round(cost_list[s-1] * size_list[s-1] - tmp_slope * size_list[s-1], digits=0)
append!(cost_slope_no_inc, tmp_slope)
append!(seg_yint_no_inc, tmp_y_int)
end
append!(cost_slope_no_inc, cost_list[end])
append!(seg_yint_no_inc, 0.0)

add_to_expression!(m[:CHPCapexNoIncentives],
sum(cost_slope_no_inc[s] * m[Symbol("dvSegmentSystemSize"*t)][s] +
seg_yint_no_inc[s] * m[Symbol("binSegment"*t)][s] for s in 1:p.n_segs_by_tech[t])
)
else
add_to_expression!(m[:CHPCapexNoIncentives], cost_list * m[Symbol("dvPurchaseSize"*_n)]["CHP"])
end
# TODO: include supplementary firing costs?
#Add supplementary firing capital cost
# chp_supp_firing_size = self.nested_outputs["Scenario"]["Site"][tech].get("size_supplementary_firing_kw")
# chp_supp_firing_cost = self.inputs[tech].get("supplementary_firing_capital_cost_per_kw") or 0
# add_to_expression!(m[:CHPCapexNoIncentives], chp_supp_firing_size * chp_supp_firing_cost)
Comment thread
adfarth marked this conversation as resolved.
Outdated
add_to_expression!(m[:InitialCapexNoIncentives], m[:CHPCapexNoIncentives])
end

if "SteamTurbine" in p.techs.all
add_to_expression!(m[:InitialCapexNoIncentives],
p.s.steam_turbine.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["SteamTurbine"]
)
end

if "Boiler" in p.techs.all
add_to_expression!(m[:InitialCapexNoIncentives],
p.s.boiler.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["Boiler"]
)
end

if "AbsorptionChiller" in p.techs.all
add_to_expression!(m[:InitialCapexNoIncentives],
p.s.absorption_chiller.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["AbsorptionChiller"]
)
end

if !isempty(p.s.ghp_option_list)
for option in enumerate(p.s.ghp_option_list)
if option[2].heat_pump_configuration == "WSHP"
add_to_expression!(m[:InitialCapexNoIncentives],
option[2].installed_cost_per_kw[2]*option[2].heatpump_capacity_ton*m[Symbol("binGHP"*_n)][option[1]]
)
elseif option[2].heat_pump_configuration == "WWHP"
add_to_expression!(m[:InitialCapexNoIncentives],
(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]]
)
else
@warn "Unknown heat pump configuration provided, excluding GHP costs from initial capital costs."
end
end
end

if "ASHPSpaceHeater" in p.techs.all
add_to_expression!(m[:InitialCapexNoIncentives],
p.s.ashp.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["ASHPSpaceHeater"]
)
end

if "ASHPWaterHeater" in p.techs.all
add_to_expression!(m[:InitialCapexNoIncentives],
p.s.ashp_wh.installed_cost_per_kw * m[Symbol("dvPurchaseSize"*_n)]["ASHPWaterHeater"]
)
end
Comment thread
adfarth marked this conversation as resolved.
end
10 changes: 9 additions & 1 deletion src/core/financial.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
macrs_five_year::Array{Float64,1} = [0.2, 0.32, 0.192, 0.1152, 0.1152, 0.0576], # IRS pub 946
macrs_seven_year::Array{Float64,1} = [0.1429, 0.2449, 0.1749, 0.1249, 0.0893, 0.0892, 0.0893, 0.0446],
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.
offgrid_other_annual_costs::Real = 0.0 # only applicable when `off_grid_flag` is true. Considered tax deductible for owner. Costs are per year.
offgrid_other_annual_costs::Real = 0.0 # only applicable when `off_grid_flag` is true. Considered tax deductible for owner. Costs are per year.
min_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing # minimum up-front capital cost for all technologies, excluding replacement costs and incentives.
max_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing # maximum up-front capital cost for all technologies, excluding replacement costs and incentives.
# Emissions cost inputs
CO2_cost_per_tonne::Real = 51.0,
CO2_cost_escalation_rate_fraction::Real = 0.042173,
Expand Down Expand Up @@ -62,6 +64,8 @@ struct Financial
macrs_seven_year::Array{Float64,1}
offgrid_other_capital_costs::Float64
offgrid_other_annual_costs::Float64
min_initial_capital_costs_before_incentives::Union{Nothing,Real}
max_initial_capital_costs_before_incentives::Union{Nothing,Real}
CO2_cost_per_tonne::Float64
CO2_cost_escalation_rate_fraction::Float64
NOx_grid_cost_per_tonne::Float64
Expand Down Expand Up @@ -94,6 +98,8 @@ struct Financial
macrs_seven_year::Array{<:Real,1} = [0.1429, 0.2449, 0.1749, 0.1249, 0.0893, 0.0892, 0.0893, 0.0446],
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.
offgrid_other_annual_costs::Real = 0.0, # only applicable when `off_grid_flag` is true. Considered tax deductible for owner.
min_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing,
max_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing,
# Emissions cost inputs
CO2_cost_per_tonne::Real = 51.0,
CO2_cost_escalation_rate_fraction::Real = 0.042173,
Expand Down Expand Up @@ -191,6 +197,8 @@ struct Financial
macrs_seven_year,
offgrid_other_capital_costs,
offgrid_other_annual_costs,
min_initial_capital_costs_before_incentives,
max_initial_capital_costs_before_incentives,
CO2_cost_per_tonne,
CO2_cost_escalation_rate_fraction,
NOx_grid_cost_per_tonne,
Expand Down
8 changes: 7 additions & 1 deletion src/core/reopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
@warn "Adding binary variable(s) to model cost curves"
add_cost_curve_vars_and_constraints(m, p)
for t in p.techs.segmented # cannot have this for statement in sum( ... for t in ...) ???
m[:TotalTechCapCosts] += p.third_party_factor * (
m[:TotalTechCapCosts] += p.third_party_factor * (
sum(p.cap_cost_slope[t][s] * m[Symbol("dvSegmentSystemSize"*t)][s] +
p.seg_yint[t][s] * m[Symbol("binSegment"*t)][s] for s in 1:p.n_segs_by_tech[t])
)
Expand Down Expand Up @@ -477,6 +477,12 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
end
end

# Get CAPEX expressions and optionally constrain CAPEX
initial_capex_no_incentives(m, p)
if !isnothing(p.s.financial.min_initial_capital_costs_before_incentives) || !isnothing(p.s.financial.max_initial_capital_costs_before_incentives)
add_capex_constraints(m, p)
end

################################# Objective Function ########################################
@expression(m, Costs,
# Capital Costs
Expand Down
3 changes: 2 additions & 1 deletion src/results/chp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- `year_one_standby_cost_after_tax` CHP standby charges in year one, after tax
- `lifecycle_standby_cost_after_tax` Present value of all CHP standby charges, after tax.
- `thermal_production_series_mmbtu_per_hour`
- `initial_capital_costs` Initial capital costs of the CHP system, before incentives [\$]

!!! note "'Series' and 'Annual' energy outputs are average annual"
REopt performs load balances using average annual production values for technologies that include degradation.
Expand Down Expand Up @@ -137,7 +138,7 @@ function add_chp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")
r["year_one_standby_cost_before_tax"] = round(value(m[Symbol("TotalCHPStandbyCharges")]) / p.pwf_e, digits=0)
r["year_one_standby_cost_after_tax"] = r["year_one_standby_cost_before_tax"] * (1 - p.s.financial.offtaker_tax_rate_fraction)
r["lifecycle_standby_cost_after_tax"] = round(value(m[Symbol("TotalCHPStandbyCharges")]) * (1 - p.s.financial.offtaker_tax_rate_fraction), digits=0)

r["initial_capital_costs"] = round(value(m[Symbol("CHPCapexNoIncentives")]), digits=2)

d["CHP"] = r
nothing
Expand Down
Loading