Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set option to force minimum fraction of load served by ASHP if purchased #455

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ Classify the change according to the following categories:
### Deprecated
### Removed


## Develop min-load-to-ashp
### Added
- Added new attribute `` to **ASHPSpaceHeater** and **ASHPWaterHeater** technologies. When this is populated, this imposes a constraint on the minimum fraction of load served by the ASHP system in every period, if the system is purchased.

## Develop
### Added
- Battery residual value if choosing replacement strategy for degradation
Expand Down
48 changes: 33 additions & 15 deletions src/constraints/thermal_tech_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -107,29 +107,47 @@ end


function add_ashp_force_in_constraints(m, p; _n="")
if "ASHPSpaceHeater" in p.techs.ashp && p.s.ashp.force_into_system
for t in setdiff(p.techs.can_serve_space_heating, ["ASHPSpaceHeater"])
for ts in p.time_steps
fix(m[Symbol("dvHeatingProduction"*_n)][t,"SpaceHeating",ts], 0.0, force=true)
fix(m[Symbol("dvProductionToWaste"*_n)][t,"SpaceHeating",ts], 0.0, force=true)
if "ASHPSpaceHeater" in p.techs.ashp
if p.s.ashp.force_into_system
for t in setdiff(p.techs.can_serve_space_heating, ["ASHPSpaceHeater"])
for ts in p.time_steps
fix(m[Symbol("dvHeatingProduction"*_n)][t,"SpaceHeating",ts], 0.0, force=true)
fix(m[Symbol("dvProductionToWaste"*_n)][t,"SpaceHeating",ts], 0.0, force=true)
end
end
elseif p.s.ashp.min_allowable_load_service_fraction > 0.0
@constraint(m, [ts in p.time_steps],
m[Symbol("dvHeatingProduction"*_n)]["ASHPSpaceHeater","SpaceHeating",ts] >= p.s.ashp.min_allowable_load_service_fraction * p.heating_loads_kw["SpaceHeating"][ts] * m[Symbol("binSegmentASHPSpaceHeater")][1]
)
end
end

if "ASHPSpaceHeater" in p.techs.cooling && p.s.ashp.force_into_system
for t in setdiff(p.techs.cooling, ["ASHPSpaceHeater"])
for ts in p.time_steps
fix(m[Symbol("dvCoolingProduction"*_n)][t,ts], 0.0, force=true)
if "ASHPSpaceHeater" in p.techs.cooling
if p.s.ashp.force_into_system
for t in setdiff(p.techs.cooling, ["ASHPSpaceHeater"])
for ts in p.time_steps
fix(m[Symbol("dvCoolingProduction"*_n)][t,ts], 0.0, force=true)
end
end
end
elseif p.s.ashp.min_allowable_load_service_fraction > 0.0
@constraint(m, [ts in p.time_steps],
m[Symbol("dvCoolingProduction"*_n)]["ASHPSpaceHeater",ts] >= p.s.ashp.min_allowable_load_service_fraction * p.s.cooling_load.loads_kw_thermal[ts] * m[Symbol("binSegmentASHPSpaceHeater")][1]
)
end
end

if "ASHPWaterHeater" in p.techs.ashp && p.s.ashp_wh.force_into_system
for t in setdiff(p.techs.can_serve_dhw, ["ASHPWaterHeater"])
for ts in p.time_steps
fix(m[Symbol("dvHeatingProduction"*_n)][t,"DomesticHotWater",ts], 0.0, force=true)
fix(m[Symbol("dvProductionToWaste"*_n)][t,"DomesticHotWater",ts], 0.0, force=true)
if "ASHPWaterHeater" in p.techs.ashp
if p.s.ashp_wh.force_into_system
for t in setdiff(p.techs.can_serve_dhw, ["ASHPWaterHeater"])
for ts in p.time_steps
fix(m[Symbol("dvHeatingProduction"*_n)][t,"DomesticHotWater",ts], 0.0, force=true)
fix(m[Symbol("dvProductionToWaste"*_n)][t,"DomesticHotWater",ts], 0.0, force=true)
end
end
elseif p.s.ashp_wh.min_allowable_load_service_fraction > 0.0
@constraint(m, [ts in p.time_steps],
m[Symbol("dvHeatingProduction"*_n)]["ASHPWaterHeater","DomesticHotWater",ts] >= p.s.ashp_wh.min_allowable_load_service_fraction * p.heating_loads_kw["DomesticHotWater"][ts] * m[Symbol("binSegmentASHPWaterHeater")][1]
)
end
end
end
Expand Down
32 changes: 25 additions & 7 deletions src/core/ashp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ASHPSpaceHeater has the following attributes:
min_kw::Real # Minimum thermal power size
max_kw::Real # Maximum thermal power size
min_allowable_kw::Real # Minimum nonzero thermal power size if included
min_allowable_load_service_fraction::Real # Minimum load service required by ASHP system
sizing_factor::Real # Size multiplier of system, relative that of the max load given by dispatch profile
installed_cost_per_kw::Real # Thermal power-based cost
om_cost_per_kw::Real # Thermal power-based fixed O&M cost
Expand All @@ -33,6 +34,7 @@ struct ASHP <: AbstractThermalTech
min_kw::Real
max_kw::Real
min_allowable_kw::Real
min_allowable_load_service_fraction::Real
sizing_factor::Real
installed_cost_per_kw::Real
om_cost_per_kw::Real
Expand Down Expand Up @@ -106,6 +108,7 @@ function ASHPSpaceHeater(;
max_ton::Real = BIG_NUMBER,
min_allowable_ton::Union{Real, Nothing} = nothing,
min_allowable_peak_capacity_fraction::Union{Real, Nothing} = nothing,
min_allowable_load_service_fraction::Union{Real, Nothing} = nothing,
sizing_factor::Union{Real, Nothing} = nothing,
installed_cost_per_ton::Union{Real, Nothing} = nothing,
om_cost_per_ton::Union{Real, Nothing} = nothing,
Expand Down Expand Up @@ -206,16 +209,22 @@ function ASHPSpaceHeater(;
cooling_cf = Float64[]
end

if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_capacity_fraction)
throw(@error("at most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input."))
if !isnothing(min_allowable_ton) + !isnothing(min_allowable_peak_capacity_fraction) + !isnothing(min_allowable_load_service_fraction) > 1
throw(@error("at most one of min_allowable_ton, min_allowable_peak_capacity_fraction and min_allowable_load_service_fraction may be input."))
elseif !isnothing(min_allowable_ton)
min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR
min_allowable_load_service_fraction = 0.0
@warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.")
else
if isnothing(min_allowable_peak_capacity_fraction)
if !isnothing(min_allowable_peak_capacity_fraction)
min_allowable_load_service_fraction = 0.0
elseif !isnothing(min_allowable_load_service_fraction)
min_allowable_peak_capacity_fraction = min_allowable_load_service_fraction
else
min_allowable_peak_capacity_fraction = 0.5
min_allowable_load_service_fraction = 0.0
end
min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf, min_allowable_peak_capacity_fraction)
min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, Real[], Real[], min_allowable_peak_capacity_fraction)
end

if min_allowable_kw > max_kw
Expand All @@ -229,6 +238,7 @@ function ASHPSpaceHeater(;
min_kw,
max_kw,
min_allowable_kw,
min_allowable_load_service_fraction,
sizing_factor,
installed_cost_per_kw,
om_cost_per_kw,
Expand Down Expand Up @@ -296,6 +306,7 @@ function ASHPWaterHeater(;
max_ton::Real = BIG_NUMBER,
min_allowable_ton::Union{Real, Nothing} = nothing,
min_allowable_peak_capacity_fraction::Union{Real, Nothing} = nothing,
min_allowable_load_service_fraction::Union{Real, Nothing} = nothing,
sizing_factor::Union{Real, Nothing} = nothing,
installed_cost_per_ton::Union{Real, Nothing} = nothing,
om_cost_per_ton::Union{Real, Nothing} = nothing,
Expand Down Expand Up @@ -363,14 +374,20 @@ function ASHPWaterHeater(;

heating_cf[heating_cop .== 1] .= 1

if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_capacity_fraction)
throw(@error("at most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input."))
if !isnothing(min_allowable_ton) + !isnothing(min_allowable_peak_capacity_fraction) + !isnothing(min_allowable_load_service_fraction) > 1
throw(@error("at most one of min_allowable_ton, min_allowable_peak_capacity_fraction and min_allowable_load_service_fraction may be input."))
elseif !isnothing(min_allowable_ton)
min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR
min_allowable_load_service_fraction = 0.0
@warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.")
else
if isnothing(min_allowable_peak_capacity_fraction)
if !isnothing(min_allowable_peak_capacity_fraction)
min_allowable_load_service_fraction = 0.0
elseif !isnothing(min_allowable_load_service_fraction)
min_allowable_peak_capacity_fraction = min_allowable_load_service_fraction
else
min_allowable_peak_capacity_fraction = 0.5
min_allowable_load_service_fraction = 0.0
end
min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, Real[], Real[], min_allowable_peak_capacity_fraction)
end
Expand All @@ -383,6 +400,7 @@ function ASHPWaterHeater(;
min_kw,
max_kw,
min_allowable_kw,
min_allowable_load_service_fraction,
sizing_factor,
installed_cost_per_kw,
om_cost_per_kw,
Expand Down
8 changes: 4 additions & 4 deletions src/core/reopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,6 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
add_heating_cooling_constraints(m, p)
end

if !isempty(p.techs.ashp)
add_ashp_force_in_constraints(m, p)
end

if !isempty(p.avoided_capex_by_ashp_present_value) && !isempty(p.techs.ashp)
avoided_capex_by_ashp(m, p)
end
Expand Down Expand Up @@ -404,6 +400,10 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
)
end
end

if !isempty(p.techs.ashp)
add_ashp_force_in_constraints(m, p)
end

@expression(m, TotalStorageCapCosts, p.third_party_factor * (
sum( p.s.storage.attr[b].net_present_cost_per_kw * m[:dvStoragePower][b] for b in p.s.storage.types.elec) +
Expand Down
36 changes: 36 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2708,6 +2708,42 @@ else # run HiGHS tests
@test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4
@test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4
end

@testset "ASHP min load served" begin
d = JSON.parsefile("./scenarios/ashp.json")
d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760
d["DomesticHotWaterLoad"] = Dict{String,Any}("annual_mmbtu" => 0.5 * 8760, "doe_reference_name" => "FlatLoad")
d["CoolingLoad"] = Dict{String,Any}("thermal_loads_ton" => ones(8760)*0.1)
d["ExistingChiller"] = Dict{String,Any}("retire_in_optimal" => false, "cop" => 100)
d["ExistingBoiler"]["retire_in_optimal"] = false
d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0.001
d["ASHPSpaceHeater"]["can_serve_cooling"] = true
d["ASHPSpaceHeater"]["force_into_system"] = false
d["ASHPSpaceHeater"]["min_allowable_load_service_fraction"] = 0.5
d["ASHPWaterHeater"] = Dict{String,Any}("force_into_system" => false, "min_allowable_load_service_fraction" => 0.5, "max_ton" => 100000)

s = Scenario(d)
p = REoptInputs(s)
m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false))
results = run_reopt(m, p)

#min_allowable_load_fraction should allow zero-kW system, yield no output
@test results["ASHPWaterHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=1e-4
@test results["ASHPSpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=1e-4
@test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4

#force system to be purchased, which enforces min allowable load fraction to force dispatch
d["ASHPSpaceHeater"]["min_ton"] = 10
d["ASHPWaterHeater"]["min_ton"] = 10
s = Scenario(d)
p = REoptInputs(s)
m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false))
results = run_reopt(m, p)

@test results["ASHPWaterHeater"]["annual_electric_consumption_kwh"] ≈ sum(0.2 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPWaterHeater"][ts] for ts in p.time_steps) rtol=1e-4
@test results["ASHPSpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.2 * 8760 rtol=1e-4
@test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 438.0 rtol=1e-4
end

end

Expand Down
Loading