Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/core/chp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ conflict_res_min_allowable_fraction_of_max = 0.25
cooling_thermal_factor::Float64 = NaN # only needed with cooling load
unavailability_periods::AbstractVector{Dict} = Dict[] # CHP unavailability periods for scheduled and unscheduled maintenance, list of dictionaries with keys of "['month', 'start_week_of_month', 'start_day_of_week', 'start_hour', 'duration_hours'] all values are one-indexed and start_day_of_week uses 1 for Monday, 7 for Sunday
unavailability_hourly::AbstractVector{Int64} = Int64[] # Hourly 8760 profile of unavailability (1) and availability (0)
production_factor_series::Union{Nothing, Vector{<:Real}} = nothing # Optional user-provided production factor time-series (length of time_steps_per_hour * 8760). If provided, this will override the production factor calculated from unavailability.

# Optional inputs:
size_class::Union{Int, Nothing} = nothing # CHP size class for using appropriate default inputs, with size_class=0 using an average of all other size class data
Expand Down Expand Up @@ -113,6 +114,7 @@ Base.@kwdef mutable struct CHP <: AbstractCHP
can_serve_space_heating::Bool = true
can_serve_process_heat::Bool = true
is_electric_only::Bool = false
production_factor_series::Union{Nothing, Vector{<:Real}} = nothing

macrs_option_years::Int = 5
macrs_bonus_fraction::Float64 = 1.0
Expand Down
6 changes: 6 additions & 0 deletions src/core/production_factor.jl
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,15 @@ end
production_factor for CHP accounts for unavailability (`unavailability_periods`) of CHP due to
scheduled (mostly off-peak) and "unscheduled" (on-peak) maintenance.
Note: this same prod_factor should be applied to electric and thermal production

If the user provides their own production_factor_series, that will be returned instead of generating from unavailability.
"""
function get_production_factor(chp::AbstractCHP, year::Int=2017, outage_start_time_step::Int=0, outage_end_time_step::Int=0, ts_per_hour::Int=1)

if !(isnothing(chp.production_factor_series))
return chp.production_factor_series
end

prod_factor = [1.0 - chp.unavailability_hourly[i] for i in 1:8760 for _ in 1:ts_per_hour]

# Ignore unavailability in time_step if it intersects with an outage interval(s)
Expand Down
59 changes: 59 additions & 0 deletions test/test_chp_prodfactor.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Revise
using REopt
using JSON
using DelimitedFiles
using PlotlyJS
using Dates
using Test
using JuMP
using HiGHS
using DotEnv
DotEnv.load!()


########### CHP production_factor_series test #############
@testset "CHP with production_factor_series" begin
# Create a simple scenario with electric-only CHP and a custom production factor
input_data = Dict(
"Site" => Dict(
"latitude" => 39.7,
"longitude" => -104.9
),
"ElectricLoad" => Dict(
"loads_kw" => repeat([100.0], 8760),
"year" => 2025
),
"ElectricTariff" => Dict(
"urdb_label" => "5ed6c1a15457a3367add15ae"
),
"CHP" => Dict(
"is_electric_only" => true,
"fuel_cost_per_mmbtu" => 4.0,
"max_kw" => 100.0,
"min_kw" => 100.0,
"production_factor_series" => vcat(repeat([0.5], 4380), repeat([1.0], 4380)) # 50% for first half year, 100% for second half
)
)

s = Scenario(input_data)
p = REoptInputs(s)

# Verify the production factor series was properly assigned
@test !isnothing(s.chp.production_factor_series)
@test length(s.chp.production_factor_series) == 8760
@test s.chp.production_factor_series[1] ≈ 0.5 atol=0.001
@test s.chp.production_factor_series[8760] ≈ 1.0 atol=0.001

# Run the optimization
m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01))
results = run_reopt(m, p)

# Check that production matches the pattern (lower in first half, higher in second half)
first_half_avg = sum(results["CHP"]["electric_production_series_kw"][1:4380]) / 4380
second_half_avg = sum(results["CHP"]["electric_production_series_kw"][4381:8760]) / 4380

println("CHP production_factor_series test passed!")
println("CHP size: ", results["CHP"]["size_kw"], " kW")
println("First half avg production: ", round(first_half_avg, digits=2), " kW")
println("Second half avg production: ", round(second_half_avg, digits=2), " kW")
end
Loading