diff --git a/CHANGELOG.md b/CHANGELOG.md index c2c4fc23a..bf675c24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,17 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## v0.58.2 +### Added +- **Generator** **om_cost_per_hr_per_kw_rated**: Generator non-fuel variable operations and maintenance costs in \$/hr/kw_rated (default of 0.0) + +### Changed +- Refactored some results expressions so that `value.` isn't called within them. + +### Fixed +- Fixed an error creating results for flows from hot TES to the steam turbine. +- Fixed an bug preventing `include_cooling_in_chp_size` from being included in CHP inputs. + ## v0.58.1 ### Fixed - Calculation of offgrid_microgrid_lcoe_dollars_per_kwh for sub-hourly runs. @@ -38,14 +49,12 @@ Classify the change according to the following categories: ### Changed - Updated heating dispatch results by separating heat flows to absorption chiller from heating load served (formerly, these were aggregated). +- **HotThermalStorage** and **HighTempThermalStorage** output **storage_to_turbine_series_mmbtu_per_hour** to **storage_to_steamturbine_series_mmbtu_per_hour** ### Fixed - Fixed a bug in which the CHP system requires a **DomesticHotWater** load. - Fixed a bug in which the storage to steam turbine flow was included in the thermal heating load served. -### Changed -- **HotThermalStorage** and **HighTempThermalStorage** output **storage_to_turbine_series_mmbtu_per_hour** to **storage_to_steamturbine_series_mmbtu_per_hour** - ## v0.57.0 ### Fixed - Include boiler emissions in emissions calculations diff --git a/Project.toml b/Project.toml index 64795f7d4..29d73c49e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "REopt" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" authors = ["Nick Laws", "Hallie Dunham ", "Bill Becker ", "Bhavesh Rathod ", "Alex Zolan ", "Amanda Farthing ", "Xiangkun Li ", "An Pham ", "Byron Pullutasig "] -version = "0.58.1" +version = "0.58.2" [deps] ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" diff --git a/src/constraints/generator_constraints.jl b/src/constraints/generator_constraints.jl index b0a42d95e..8d2d23da3 100644 --- a/src/constraints/generator_constraints.jl +++ b/src/constraints/generator_constraints.jl @@ -36,7 +36,6 @@ function add_binGenIsOnInTS_constraints(m,p) end end - function add_gen_can_run_constraints(m,p) if p.s.generator.only_runs_during_grid_outage for ts in p.time_steps_with_grid, t in p.techs.gen @@ -58,6 +57,38 @@ function add_gen_rated_prod_constraint(m, p) ) end +""" + add_generator_hourly_om_charges(m, p) + +- add decision variable "dvOMByHourBySizeGen" for the hourly Generator operations and maintenance costs +- add the cost to TotalPerUnitHourOMCosts +""" +function add_generator_hourly_om_charges(m, p) + dv = "dvOMByHourBySizeGen" + m[Symbol(dv)] = @variable(m, [p.techs.gen, p.time_steps], base_name=dv, lower_bound=0) + + #Constraint Generator-hourly-om-a: om per hour, per time step >= per_unit_size_cost * size for when on, >= zero when off + @constraint(m, GeneratorHourlyOMBySizeA[t in p.techs.gen, ts in p.time_steps], + p.s.generator.om_cost_per_hr_per_kw_rated * m[Symbol("dvSize")][t] - + (p.s.generator.existing_kw + p.s.generator.max_kw) * p.s.generator.om_cost_per_hr_per_kw_rated * (1-m[Symbol("binGenIsOnInTS")][t,ts]) + <= m[Symbol("dvOMByHourBySizeGen")][t, ts] + ) + #Constraint Generator-hourly-om-b: om per hour, per time step <= per_unit_size_cost * size for each hour + @constraint(m, GeneratorHourlyOMBySizeB[t in p.techs.gen, ts in p.time_steps], + p.s.generator.om_cost_per_hr_per_kw_rated * m[Symbol("dvSize")][t] + >= m[Symbol("dvOMByHourBySizeGen")][t, ts] + ) + #Constraint Generator-hourly-om-c: om per hour, per time step <= zero when off, <= per_unit_size_cost*max_size + @constraint(m, GeneratorHourlyOMBySizeC[t in p.techs.gen, ts in p.time_steps], + (p.s.generator.existing_kw + p.s.generator.max_kw) * p.s.generator.om_cost_per_hr_per_kw_rated * m[Symbol("binGenIsOnInTS")][t,ts] + >= m[Symbol("dvOMByHourBySizeGen")][t, ts] + ) + + m[:TotalHourlyGenOMCosts] = @expression(m, p.third_party_factor * p.pwf_om * + sum(m[Symbol(dv)][t, ts] * p.hours_per_time_step for t in p.techs.gen, ts in p.time_steps)) + nothing +end + """ add_gen_constraints(m, p) @@ -70,6 +101,11 @@ function add_gen_constraints(m, p) add_gen_can_run_constraints(m,p) add_gen_rated_prod_constraint(m,p) + m[:TotalHourlyGenOMCosts] = 0 + if p.s.generator.om_cost_per_hr_per_kw_rated > 1.0E-7 + add_generator_hourly_om_charges(m, p) + end + m[:TotalGenPerUnitProdOMCosts] = @expression(m, p.third_party_factor * p.pwf_om * sum(p.s.generator.om_cost_per_kwh * p.hours_per_time_step * m[:dvRatedProduction][t, ts] for t in p.techs.gen, ts in p.time_steps) diff --git a/src/core/chp.jl b/src/core/chp.jl index 9e07b6360..f23b1e707 100644 --- a/src/core/chp.jl +++ b/src/core/chp.jl @@ -43,7 +43,6 @@ conflict_res_min_allowable_fraction_of_max = 0.25 serve_absorption_chiller_only::Bool = false # If CHP produced heat either serves absorption chiller or sends it to waste; only applies to the months specified in months_serving_absorption_chiller_only if true months_serving_absorption_chiller_only::AbstractVector{Int64} = Int64[] # months in which CHP only sevres the absorption chiller, with 1=January and 12=December; only applied when serve_absorption_chiller_only = true follow_electrical_load::Bool = false # If CHP follows the electrical load by running at capacity or meeting the load only. - include_cooling_in_chp_size::Bool = false # If true, includes cooling load (via absorption chiller) in the heuristic CHP sizing calculation along with heating loads. Defaults to true when AbsorptionChiller is present with CHP. Requires CoolingLoad to be specified. macrs_option_years::Int = 5 # Notes: this value cannot be 0 if aiming to apply 100% bonus depreciation; default may change if Site.sector is not "commercial/industrial" macrs_bonus_fraction::Float64 = 1.0 #Note: default may change if Site.sector is not "commercial/industrial" diff --git a/src/core/generator.jl b/src/core/generator.jl index 9ebaff85f..c9bec8e9f 100644 --- a/src/core/generator.jl +++ b/src/core/generator.jl @@ -9,6 +9,7 @@ installed_cost_per_kw::Real = off_grid_flag ? 880 : only_runs_during_grid_outage ? 650.0 : 800.0, om_cost_per_kw::Real = off_grid_flag ? 10.0 : 20.0, om_cost_per_kwh::Real = 0.0, + om_cost_per_hr_per_kw_rated::Float64 = 0.0, # Generator non-fuel variable operations and maintenance costs in \$/hr/kw_rated fuel_cost_per_gallon::Real = 2.25, electric_efficiency_full_load::Real = 0.322, electric_efficiency_half_load::Real = electric_efficiency_full_load, @@ -57,6 +58,7 @@ struct Generator <: AbstractGenerator installed_cost_per_kw om_cost_per_kw om_cost_per_kwh + om_cost_per_hr_per_kw_rated fuel_cost_per_gallon electric_efficiency_full_load electric_efficiency_half_load @@ -104,6 +106,7 @@ struct Generator <: AbstractGenerator installed_cost_per_kw::Real = off_grid_flag ? 880 : only_runs_during_grid_outage ? 650.0 : 800.0, om_cost_per_kw::Real= off_grid_flag ? 10.0 : 20.0, om_cost_per_kwh::Real = 0.0, + om_cost_per_hr_per_kw_rated::Float64 = 0.0, # Generator non-fuel variable operations and maintenance costs in \$/hr/kw_rated fuel_cost_per_gallon::Real = 2.25, electric_efficiency_full_load::Real = 0.322, electric_efficiency_half_load::Real = electric_efficiency_full_load, @@ -152,6 +155,7 @@ struct Generator <: AbstractGenerator installed_cost_per_kw, om_cost_per_kw, om_cost_per_kwh, + om_cost_per_hr_per_kw_rated, fuel_cost_per_gallon, electric_efficiency_full_load, electric_efficiency_half_load, diff --git a/src/core/reopt.jl b/src/core/reopt.jl index df4e10a65..264e4fa63 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -274,6 +274,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_gen_constraints(m, p) m[:TotalPerUnitProdOMCosts] += m[:TotalGenPerUnitProdOMCosts] m[:TotalFuelCosts] += m[:TotalGenFuelCosts] + m[:TotalPerUnitHourOMCosts] += m[:TotalHourlyGenOMCosts] end if !isempty(p.techs.chp) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 9031798f8..eaf61bb79 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -473,7 +473,11 @@ function Scenario(d::Dict; flex_hvac_from_json=false) avg_cooling_load_kw = nothing absorption_chiller_cop = nothing # User can override by explicitly setting include_cooling_in_chp_size = false - include_cooling_in_size = get(d["CHP"], "include_cooling_in_chp_size", haskey(d, "AbsorptionChiller")) + if "include_cooling_in_chp_size" in keys(d["CHP"]) + include_cooling_in_size = pop!(d["CHP"], "include_cooling_in_chp_size") + else + include_cooling_in_size = haskey(d, "AbsorptionChiller") + end if max_cooling_demand_kw > 0 && include_cooling_in_size # Use already-processed cooling_load object @@ -498,7 +502,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) sector = site.sector, federal_procurement_type = site.federal_procurement_type) else # Only if modeling CHP without heating_load and existing_boiler (for prime generator, electric-only) - chp = CHP(d["CHP"], + chp = CHP(d["CHP"]; electric_load_series_kw = electric_load.loads_kw, avg_cooling_load_kw = avg_cooling_load_kw, absorption_chiller_cop = absorption_chiller_cop, diff --git a/src/mpc/model.jl b/src/mpc/model.jl index 4d8cebe1b..674a18689 100644 --- a/src/mpc/model.jl +++ b/src/mpc/model.jl @@ -153,6 +153,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs) m[:TotalFuelCosts] = 0.0 m[:TotalPerUnitProdOMCosts] = 0.0 + m[:TotalPerUnitHourOMCosts] = 0.0 if !isempty(p.techs.gen) add_gen_constraints(m, p) @@ -164,6 +165,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs) sum(m[:dvFuelUsage][t,ts] * p.s.generator.fuel_cost_per_gallon for t in p.techs.gen, ts in p.time_steps) ) m[:TotalFuelCosts] += m[:TotalGenFuelCosts] + m[:TotalPerUnitHourOMCosts] += m[:TotalHourlyGenOMCosts] end add_elec_utility_expressions(m, p) @@ -203,7 +205,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs) @expression(m, Costs, # Variable O&M - m[:TotalPerUnitProdOMCosts] + + m[:TotalPerUnitProdOMCosts] + m[:TotalPerUnitHourOMCosts] + # Total Generator Fuel Costs m[:TotalFuelCosts] + diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index a96cf7366..5b166c279 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -261,6 +261,7 @@ function MPCGenerator(; only_runs_during_grid_outage::Bool = true, sells_energy_back_to_grid::Bool = false, om_cost_per_kwh::Real=0.0, + om_cost_per_hr_per_kw_rated::Real=0.0, ) ``` """ @@ -276,6 +277,7 @@ struct MPCGenerator <: AbstractGenerator only_runs_during_grid_outage sells_energy_back_to_grid om_cost_per_kwh + om_cost_per_hr_per_kw_rated function MPCGenerator(; size_kw::Real, @@ -288,6 +290,7 @@ struct MPCGenerator <: AbstractGenerator only_runs_during_grid_outage::Bool = true, sells_energy_back_to_grid::Bool = false, om_cost_per_kwh::Real=0.0, + om_cost_per_hr_per_kw_rated::Real=0.0, ) max_kw = size_kw @@ -304,6 +307,7 @@ struct MPCGenerator <: AbstractGenerator only_runs_during_grid_outage, sells_energy_back_to_grid, om_cost_per_kwh, + om_cost_per_hr_per_kw_rated ) end end diff --git a/src/results/boiler.jl b/src/results/boiler.jl index f7ff2de49..89439661f 100644 --- a/src/results/boiler.jl +++ b/src/results/boiler.jl @@ -55,8 +55,8 @@ function add_boiler_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n=" r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(NewBoilerToSteamTurbine), digits=3) if "AbsorptionChiller" in p.techs.cooling - @expression(m, NewBoilertoAbsorptionChillerKW[ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller]["Boiler",q,ts] for q in p.heating_loads))) - @expression(m, NewBoilertoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller]["Boiler",q,ts]))) + @expression(m, NewBoilertoAbsorptionChillerKW[ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller]["Boiler",q,ts] for q in p.heating_loads)) + @expression(m, NewBoilertoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller]["Boiler",q,ts])) else @expression(m, NewBoilertoAbsorptionChillerKW[ts in p.time_steps], 0.0) @expression(m, NewBoilertoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) diff --git a/src/results/chp.jl b/src/results/chp.jl index ccb476c3f..196157651 100644 --- a/src/results/chp.jl +++ b/src/results/chp.jl @@ -100,8 +100,8 @@ function add_chp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") end r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(CHPToSteamTurbineKW) / KWH_PER_MMBTU, digits=5) if "AbsorptionChiller" in p.techs.cooling - @expression(m, CHPtoAbsorptionChillerKW[ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller][t,q,ts] for t in p.techs.chp, q in p.heating_loads))) - @expression(m, CHPtoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller][t,q,ts] for t in p.techs.chp))) + @expression(m, CHPtoAbsorptionChillerKW[ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller][t,q,ts] for t in p.techs.chp, q in p.heating_loads)) + @expression(m, CHPtoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller][t,q,ts] for t in p.techs.chp)) else @expression(m, CHPtoAbsorptionChillerKW[ts in p.time_steps], 0.0) @expression(m, CHPtoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) diff --git a/src/results/cst.jl b/src/results/cst.jl index febe1996d..3094a7d01 100644 --- a/src/results/cst.jl +++ b/src/results/cst.jl @@ -71,8 +71,8 @@ function add_concentrating_solar_results(m::JuMP.AbstractModel, p::REoptInputs, r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(CSTToSteamTurbine) / KWH_PER_MMBTU, digits=3) if "AbsorptionChiller" in p.techs.cooling - @expression(m, CSTtoAbsorptionChillerKW[ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller]["CST",q,ts] for q in p.heating_loads))) - @expression(m, CSTtoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller]["CST",q,ts]))) + @expression(m, CSTtoAbsorptionChillerKW[ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller]["CST",q,ts] for q in p.heating_loads)) + @expression(m, CSTtoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller]["CST",q,ts])) else @expression(m, CSTtoAbsorptionChillerKW[ts in p.time_steps], 0.0) @expression(m, CSTtoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) diff --git a/src/results/electric_heater.jl b/src/results/electric_heater.jl index b9771d4a8..513bf46b0 100644 --- a/src/results/electric_heater.jl +++ b/src/results/electric_heater.jl @@ -68,8 +68,8 @@ function add_electric_heater_results(m::JuMP.AbstractModel, p::REoptInputs, d::D r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(ElectricHeaterToSteamTurbine) / KWH_PER_MMBTU, digits=3) if "AbsorptionChiller" in p.techs.cooling - @expression(m, ElectricHeatertoAbsorptionChillerKW[ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller]["ElectricHeater",q,ts] for q in p.heating_loads))) - @expression(m, ElectricHeatertoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller]["ElectricHeater",q,ts]))) + @expression(m, ElectricHeatertoAbsorptionChillerKW[ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller]["ElectricHeater",q,ts] for q in p.heating_loads)) + @expression(m, ElectricHeatertoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller]["ElectricHeater",q,ts])) else @expression(m, ElectricHeatertoAbsorptionChillerKW[ts in p.time_steps], 0.0) @expression(m, ElectricHeatertoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) diff --git a/src/results/existing_boiler.jl b/src/results/existing_boiler.jl index af0029500..6af93c0f7 100644 --- a/src/results/existing_boiler.jl +++ b/src/results/existing_boiler.jl @@ -58,8 +58,8 @@ function add_existing_boiler_results(m::JuMP.AbstractModel, p::REoptInputs, d::D r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(BoilerToSteamTurbineKW) ./ KWH_PER_MMBTU, digits=5) if "AbsorptionChiller" in p.techs.cooling - @expression(m, BoilertoAbsorptionChillerKW[ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller]["ExistingBoiler",q,ts] for q in p.heating_loads))) - @expression(m, BoilertoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller]["ExistingBoiler",q,ts]))) + @expression(m, BoilertoAbsorptionChillerKW[ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller]["ExistingBoiler",q,ts] for q in p.heating_loads)) + @expression(m, BoilertoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller]["ExistingBoiler",q,ts])) else @expression(m, BoilertoAbsorptionChillerKW[ts in p.time_steps], 0.0) @expression(m, BoilertoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) diff --git a/src/results/generator.jl b/src/results/generator.jl index 1e90e0fb0..4a58d74bf 100644 --- a/src/results/generator.jl +++ b/src/results/generator.jl @@ -4,8 +4,8 @@ - `size_kw` Optimal generator capacity - `lifecycle_fixed_om_cost_after_tax` Lifecycle fixed operations and maintenance cost in present value, after tax - `year_one_fixed_om_cost_before_tax` fixed operations and maintenance cost over the first year, before considering tax benefits -- `lifecycle_variable_om_cost_after_tax` Lifecycle variable operations and maintenance cost in present value, after tax -- `year_one_variable_om_cost_before_tax` variable operations and maintenance cost over the first year, before considering tax benefits +- `lifecycle_variable_om_cost_after_tax` Lifecycle variable operations and maintenance cost in present value, after tax. Includes om_cost_per_kwh and om_cost_per_hr_per_kw_rated +- `year_one_variable_om_cost_before_tax` variable operations and maintenance cost over the first year, before considering tax benefits. Includes om_cost_per_kwh and om_cost_per_hr_per_kw_rated - `lifecycle_fuel_cost_after_tax` Lifecycle fuel cost in present value, after tax - `year_one_fuel_cost_before_tax` Fuel cost over the first year, before considering tax benefits. Does not include fuel use during outages if using multiple outage modeling. - `year_one_fuel_cost_after_tax` Fuel cost over the first year, after considering tax benefits. Does not include fuel use during outages if using multiple outage modeling. @@ -28,17 +28,13 @@ function add_generator_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _ GenPerUnitSizeOMCosts = @expression(m, p.third_party_factor * p.pwf_om * sum(m[:dvSize][t] * p.om_cost_per_kw[t] for t in p.techs.gen)) - GenPerUnitProdOMCosts = @expression(m, p.third_party_factor * p.pwf_om * p.hours_per_time_step * - sum(m[:dvRatedProduction][t, ts] * p.production_factor[t, ts] * p.s.generator.om_cost_per_kwh - for t in p.techs.gen, ts in p.time_steps) - ) r["size_kw"] = round(value(sum(m[:dvSize][t] for t in p.techs.gen)), digits=2) r["lifecycle_fixed_om_cost_after_tax"] = round(value(GenPerUnitSizeOMCosts) * (1 - p.s.financial.owner_tax_rate_fraction), digits=0) - r["lifecycle_variable_om_cost_after_tax"] = round(value(m[:TotalPerUnitProdOMCosts]) * (1 - p.s.financial.owner_tax_rate_fraction), digits=0) + r["lifecycle_variable_om_cost_after_tax"] = round((value(m[:TotalGenPerUnitProdOMCosts]) + value(m[:TotalHourlyGenOMCosts])) * (1 - p.s.financial.owner_tax_rate_fraction), digits=0) r["lifecycle_fuel_cost_after_tax"] = round(value(m[:TotalGenFuelCosts]) * (1 - p.s.financial.offtaker_tax_rate_fraction), digits=2) r["year_one_fuel_cost_before_tax"] = round(value(m[:TotalGenFuelCosts]) / p.pwf_fuel["Generator"], digits=2) r["year_one_fuel_cost_after_tax"] = r["year_one_fuel_cost_before_tax"] * (1 - p.s.financial.offtaker_tax_rate_fraction) - r["year_one_variable_om_cost_before_tax"] = round(value(GenPerUnitProdOMCosts) / (p.pwf_om * p.third_party_factor), digits=0) + r["year_one_variable_om_cost_before_tax"] = round(value(m[:TotalGenPerUnitProdOMCosts] + m[:TotalHourlyGenOMCosts]) / (p.pwf_om * p.third_party_factor), digits=0) r["year_one_fixed_om_cost_before_tax"] = round(value(GenPerUnitSizeOMCosts) / (p.pwf_om * p.third_party_factor), digits=0) if !isempty(p.s.storage.types.elec) diff --git a/src/results/steam_turbine.jl b/src/results/steam_turbine.jl index 0e3f4d067..feae674cb 100644 --- a/src/results/steam_turbine.jl +++ b/src/results/steam_turbine.jl @@ -91,8 +91,8 @@ function add_steam_turbine_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dic r["thermal_to_high_temp_thermal_storage_series_mmbtu_per_hour"] = round.(value.(m[:SteamTurbineToHotSensibleTESKW]) ./ KWH_PER_MMBTU, digits=5) if "AbsorptionChiller" in p.techs.cooling - @expression(m, SteamTurbinetoAbsorptionChillerKW[ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller][t,q,ts] for t in p.techs.steam_turbine, q in p.heating_loads))) - @expression(m, SteamTurbinetoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(value.(m[:dvHeatToAbsorptionChiller][t,q,ts] for t in p.techs.steam_turbine))) + @expression(m, SteamTurbinetoAbsorptionChillerKW[ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller][t,q,ts] for t in p.techs.steam_turbine, q in p.heating_loads)) + @expression(m, SteamTurbinetoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(m[:dvHeatToAbsorptionChiller][t,q,ts] for t in p.techs.steam_turbine)) else @expression(m, SteamTurbinetoAbsorptionChillerKW[ts in p.time_steps], 0.0) @expression(m, SteamTurbinetoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) diff --git a/src/results/thermal_storage.jl b/src/results/thermal_storage.jl index 74d60f99f..f2264c329 100644 --- a/src/results/thermal_storage.jl +++ b/src/results/thermal_storage.jl @@ -43,24 +43,20 @@ function add_hot_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict, r["storage_to_absorption_chiller_series_mmbtu_per_hour"] = round.(value.(HotTEStoAbsorptionChillerKW) / KWH_PER_MMBTU, digits=7) if p.s.storage.attr[b].can_supply_steam_turbine && ("SteamTurbine" in p.techs.all) - storage_to_turbine = (sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,q,ts] for q in p.heating_loads) for ts in p.time_steps) - storage_to_turbine_sh = (sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,"SpaceHeating",ts]) for ts in p.time_steps) - storage_to_turbine_dhw = (sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,"DomesticHotWater",ts]) for ts in p.time_steps) - storage_to_turbine_ph = (sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,"ProcessHeat",ts]) for ts in p.time_steps) - r["storage_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(storage_to_turbine) / KWH_PER_MMBTU, digits=7) - r["storage_to_load_series_mmbtu_per_hour"] = round.(value.(discharge .- storage_to_turbine .- HotTEStoAbsorptionChillerKW) / KWH_PER_MMBTU, digits=7) + @expression(m, HotTEStoTurbineKW[ts in p.time_steps], sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,q,ts] for q in p.heating_loads)) + @expression(m, HotTEStoTurbineByQualityKW[q in p.heating_loads, ts in p.time_steps], m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,q,ts]) + r["storage_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(HotTEStoTurbineKW) / KWH_PER_MMBTU, digits=7) + r["storage_to_load_series_mmbtu_per_hour"] = round.(value.(discharge .- HotTEStoTurbineKW .- HotTEStoAbsorptionChillerKW) / KWH_PER_MMBTU, digits=7) else - storage_to_turbine = zeros(length(p.time_steps)) - storage_to_turbine_sh = zeros(length(p.time_steps)) - storage_to_turbine_dhw = zeros(length(p.time_steps)) - storage_to_turbine_ph = zeros(length(p.time_steps)) + @expression(m, HotTEStoTurbineKW[ts in p.time_steps], 0.0) + @expression(m, HotTEStoTurbineByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) r["storage_to_load_series_mmbtu_per_hour"] = round.(value.(discharge .- HotTEStoAbsorptionChillerKW) / KWH_PER_MMBTU, digits=7) r["storage_to_steamturbine_series_mmbtu_per_hour"] = zeros(length(p.time_steps)) end if "SpaceHeating" in p.heating_loads && p.s.storage.attr[b].can_serve_space_heating @expression(m, HotTESToSpaceHeatingKW[ts in p.time_steps], - m[Symbol("dvHeatFromStorage"*_n)][b,"SpaceHeating",ts] - storage_to_turbine_sh[ts] - HotTEStoAbsorptionChillerByQualityKW["SpaceHeating",ts] + m[Symbol("dvHeatFromStorage"*_n)][b,"SpaceHeating",ts] - HotTEStoTurbineByQualityKW["SpaceHeating",ts] - HotTEStoAbsorptionChillerByQualityKW["SpaceHeating",ts] ) else @expression(m, HotTESToSpaceHeatingKW[ts in p.time_steps], 0.0) @@ -69,7 +65,7 @@ function add_hot_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict, if "DomesticHotWater" in p.heating_loads && p.s.storage.attr[b].can_serve_dhw @expression(m, HotTESToDHWKW[ts in p.time_steps], - m[Symbol("dvHeatFromStorage"*_n)][b,"DomesticHotWater",ts] - storage_to_turbine_dhw[ts] - HotTEStoAbsorptionChillerByQualityKW["DomesticHotWater",ts] + m[Symbol("dvHeatFromStorage"*_n)][b,"DomesticHotWater",ts] - HotTEStoTurbineByQualityKW["DomesticHotWater",ts] - HotTEStoAbsorptionChillerByQualityKW["DomesticHotWater",ts] ) else @expression(m, HotTESToDHWKW[ts in p.time_steps], 0.0) @@ -78,7 +74,7 @@ function add_hot_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict, if "ProcessHeat" in p.heating_loads && p.s.storage.attr[b].can_serve_process_heat @expression(m, HotTESToProcessHeatKW[ts in p.time_steps], - m[Symbol("dvHeatFromStorage"*_n)][b,"ProcessHeat",ts] - storage_to_turbine_ph[ts] - HotTEStoAbsorptionChillerByQualityKW["ProcessHeat",ts] + m[Symbol("dvHeatFromStorage"*_n)][b,"ProcessHeat",ts] - HotTEStoTurbineByQualityKW["ProcessHeat",ts] - HotTEStoAbsorptionChillerByQualityKW["ProcessHeat",ts] ) else @expression(m, HotTESToProcessHeatKW[ts in p.time_steps], 0.0) @@ -197,8 +193,8 @@ function add_high_temp_thermal_storage_results(m::JuMP.AbstractModel, p::REoptIn discharge = (sum(m[Symbol("dvHeatFromStorage"*_n)][b,q,ts] for q in p.heating_loads) for ts in p.time_steps) if "AbsorptionChiller" in p.techs.cooling - @expression(m, HighTempTEStoAbsorptionChillerKW[ts in p.time_steps], sum(value.(m[:dvHeatFromStorageToAbsorptionChiller][b,q,ts] for q in p.heating_loads))) - @expression(m, HighTempTEStoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], value(m[:dvHeatFromStorageToAbsorptionChiller][b,q,ts])) + @expression(m, HighTempTEStoAbsorptionChillerKW[ts in p.time_steps], sum(m[:dvHeatFromStorageToAbsorptionChiller][b,q,ts] for q in p.heating_loads)) + @expression(m, HighTempTEStoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], m[:dvHeatFromStorageToAbsorptionChiller][b,q,ts]) else @expression(m, HighTempTEStoAbsorptionChillerKW[ts in p.time_steps], 0.0) @expression(m, HighTempTEStoAbsorptionChillerByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) @@ -206,24 +202,20 @@ function add_high_temp_thermal_storage_results(m::JuMP.AbstractModel, p::REoptIn r["storage_to_absorption_chiller_series_mmbtu_per_hour"] = round.(value.(HighTempTEStoAbsorptionChillerKW) / KWH_PER_MMBTU, digits=7) if p.s.storage.attr[b].can_supply_steam_turbine && ("SteamTurbine" in p.techs.all) - storage_to_turbine = (sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,q,ts] for q in p.heating_loads) for ts in p.time_steps) - storage_to_turbine_sh = (sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,"SpaceHeating",ts]) for ts in p.time_steps) - storage_to_turbine_dhw = (sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,"DomesticHotWater",ts]) for ts in p.time_steps) - storage_to_turbine_ph = (sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,"ProcessHeat",ts]) for ts in p.time_steps) - r["storage_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(storage_to_turbine) / KWH_PER_MMBTU, digits=7) - r["storage_to_load_series_mmbtu_per_hour"] = round.(value.(discharge .- storage_to_turbine .- HighTempTEStoAbsorptionChillerKW) / KWH_PER_MMBTU, digits=7) + @expression(m, HighTempTEStoTurbineKW[ts in p.time_steps], sum(m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,q,ts] for q in p.heating_loads)) + @expression(m, HighTempTEStoTurbineByQualityKW[q in p.heating_loads, ts in p.time_steps], m[Symbol("dvHeatFromStorageToTurbine"*_n)][b,q,ts]) + r["storage_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(HighTempTEStoTurbineKW) / KWH_PER_MMBTU, digits=7) + r["storage_to_load_series_mmbtu_per_hour"] = round.(value.(discharge .- HighTempTEStoTurbineKW .- HighTempTEStoAbsorptionChillerKW) / KWH_PER_MMBTU, digits=7) else - storage_to_turbine = zeros(length(p.time_steps)) - storage_to_turbine_sh = zeros(length(p.time_steps)) - storage_to_turbine_dhw = zeros(length(p.time_steps)) - storage_to_turbine_ph = zeros(length(p.time_steps)) + @expression(m, HighTempTEStoTurbineKW[ts in p.time_steps], 0.0) + @expression(m, HighTempTEStoTurbineByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) r["storage_to_load_series_mmbtu_per_hour"] = round.(value.(discharge .- HighTempTEStoAbsorptionChillerKW) / KWH_PER_MMBTU, digits=7) r["storage_to_steamturbine_series_mmbtu_per_hour"] = zeros(length(p.time_steps)) end if "SpaceHeating" in p.heating_loads && p.s.storage.attr[b].can_serve_space_heating @expression(m, HighTempTESToSpaceHeatingKW[ts in p.time_steps], - m[Symbol("dvHeatFromStorage"*_n)][b,"SpaceHeating",ts] - storage_to_turbine_sh[ts] - HighTempTEStoAbsorptionChillerByQualityKW["SpaceHeating",ts] + m[Symbol("dvHeatFromStorage"*_n)][b,"SpaceHeating",ts] - HighTempTEStoTurbineByQualityKW["SpaceHeating",ts] - HighTempTEStoAbsorptionChillerByQualityKW["SpaceHeating",ts] ) else @expression(m, HighTempTESToSpaceHeatingKW[ts in p.time_steps], 0.0) @@ -232,7 +224,7 @@ function add_high_temp_thermal_storage_results(m::JuMP.AbstractModel, p::REoptIn if "DomesticHotWater" in p.heating_loads && p.s.storage.attr[b].can_serve_dhw @expression(m, HighTempTESToDHWKW[ts in p.time_steps], - m[Symbol("dvHeatFromStorage"*_n)][b,"DomesticHotWater",ts] - storage_to_turbine_dhw[ts] - HighTempTEStoAbsorptionChillerByQualityKW["DomesticHotWater",ts] + m[Symbol("dvHeatFromStorage"*_n)][b,"DomesticHotWater",ts] - HighTempTEStoTurbineByQualityKW["DomesticHotWater",ts] - HighTempTEStoAbsorptionChillerByQualityKW["DomesticHotWater",ts] ) else @expression(m, HighTempTESToDHWKW[ts in p.time_steps], 0.0) @@ -241,7 +233,7 @@ function add_high_temp_thermal_storage_results(m::JuMP.AbstractModel, p::REoptIn if "ProcessHeat" in p.heating_loads && p.s.storage.attr[b].can_serve_process_heat @expression(m, HighTempTESToProcessHeatKW[ts in p.time_steps], - m[Symbol("dvHeatFromStorage"*_n)][b,"ProcessHeat",ts] - storage_to_turbine_ph[ts] - HighTempTEStoAbsorptionChillerByQualityKW["ProcessHeat",ts] + m[Symbol("dvHeatFromStorage"*_n)][b,"ProcessHeat",ts] - HighTempTEStoTurbineByQualityKW["ProcessHeat",ts] - HighTempTEStoAbsorptionChillerByQualityKW["ProcessHeat",ts] ) else @expression(m, HighTempTESToProcessHeatKW[ts in p.time_steps], 0.0) diff --git a/test/runtests.jl b/test/runtests.jl index d2cc32a2a..375b768f6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2500,6 +2500,7 @@ else # run HiGHS tests post["PV"]["max_kw"] = 0.0 post["ElectricStorage"]["max_kw"] = 0.0 post["Generator"]["min_turn_down_fraction"] = 0.0 + post["Generator"]["om_cost_per_hr_per_kw_rated"] = 2.0 finalize(backend(m)) empty!(m) GC.gc() @@ -2518,7 +2519,9 @@ else # run HiGHS tests @test r["Financial"]["lifecycle_capital_costs"] ≈ 100*(700+324.235442*(1-0.26)) + other_offgrid_capex_after_tax atol=0.1 # replacement in yr 10 is considered tax deductible @test r["Financial"]["initial_capital_costs_after_incentives"] ≈ 700*100 + other_offgrid_capex_after_tax atol=0.1 @test r["Financial"]["replacements_future_cost_after_tax"] ≈ 700*100 - @test r["Financial"]["replacements_present_cost_after_tax"] ≈ 100*(324.235442*(1-0.26)) atol=0.1 + @test r["Financial"]["replacements_present_cost_after_tax"] ≈ 100*(324.235442*(1-0.26)) atol=0.1 + generator_hours_runtime = sum(x -> x > 0, r["Generator"]["electric_to_load_series_kw"]) + sum(x -> x > 0, r["Generator"]["electric_to_storage_series_kw"]) + @test r["Generator"]["year_one_variable_om_cost_before_tax"] ≈ generator_hours_runtime * r["Generator"]["size_kw"] * post["Generator"]["om_cost_per_hr_per_kw_rated"] atol=0.1 ## Scenario 3: Fixed Generator that can meet load, but cannot meet load operating reserve requirement ## This test ensures the load operating reserve requirement is being enforced diff --git a/test/scenarios/chp_waste.json b/test/scenarios/chp_waste.json index d35982c26..d7985b263 100644 --- a/test/scenarios/chp_waste.json +++ b/test/scenarios/chp_waste.json @@ -86,6 +86,7 @@ "macrs_option_years": 5, "macrs_bonus_fraction": 0.6, "can_wholesale": false, + "include_cooling_in_chp_size": false, "fuel_cost_per_mmbtu": [ 6.26, 8.36,